Java tutorial
/******************************************************************************* * Copyright (c) 2010, 2015 Kiel University and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Kiel University - initial API and implementation *******************************************************************************/ package org.eclipse.elk.layered.p5edges; import java.util.ListIterator; import java.util.Set; import java.util.stream.StreamSupport; import org.eclipse.elk.core.math.KVector; import org.eclipse.elk.core.math.KVectorChain; import org.eclipse.elk.core.options.LayoutOptions; import org.eclipse.elk.core.options.PortSide; import org.eclipse.elk.core.util.IElkProgressMonitor; import org.eclipse.elk.layered.ILayoutPhase; import org.eclipse.elk.layered.IntermediateProcessingConfiguration; import org.eclipse.elk.layered.graph.LEdge; import org.eclipse.elk.layered.graph.LGraph; import org.eclipse.elk.layered.graph.LGraphUtil; import org.eclipse.elk.layered.graph.LNode; import org.eclipse.elk.layered.graph.LPort; import org.eclipse.elk.layered.graph.Layer; import org.eclipse.elk.layered.graph.LNode.NodeType; import org.eclipse.elk.layered.intermediate.IntermediateProcessorStrategy; import org.eclipse.elk.layered.properties.GraphProperties; import org.eclipse.elk.layered.properties.InternalProperties; import org.eclipse.elk.layered.properties.Properties; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; /** * Edge router module that draws edges with non-orthogonal line segments. * * <dl> * <dt>Precondition:</dt> * <dd>the graph has a proper layering with assigned node and port positions</dd> * <dd>the size of each layer is correctly set</dd> * <dd>at least one of the nodes connected by an in-layer edge is a dummy node</dd> * <dt>Postcondition:</dt> * <dd>each node is assigned a horizontal coordinate</dd> * <dd>the bend points of each edge are set</dd> * <dd>the width of the whole graph is set</dd> * </dl> * * @author msp * @author cds * @kieler.design 2012-08-10 chsch grh * @kieler.rating proposed yellow by msp */ public final class PolylineEdgeRouter implements ILayoutPhase { /** * Predicate that checks whether nodes represent external ports. */ public static final Predicate<LNode> PRED_EXTERNAL_WEST_OR_EAST_PORT = new Predicate<LNode>() { public boolean apply(final LNode node) { PortSide extPortSide = node.getProperty(InternalProperties.EXT_PORT_SIDE); return node.getType() == NodeType.EXTERNAL_PORT && (extPortSide == PortSide.WEST || extPortSide == PortSide.EAST); } }; /* The basic processing strategy for this phase is empty. Depending on the graph features, * dependencies on intermediate processors are added dynamically as follows: * * Before phase 1: * - None. * * Before phase 2: * - For center edge labels: * - LABEL_DUMMY_INSERTER * * Before phase 3: * - For non-free ports: * - NORTH_SOUTH_PORT_PREPROCESSOR * - INVERTED_PORT_PROCESSOR * * - For edge labels: * - LABEL_SIDE_SELECTOR * * - For center edge labels: * - LABEL_DUMMY_SWITCHER * * Before phase 4: * - For center edge labels: * - LABEL_SIDE_SELECTOR * * Before phase 5: * - None. * * After phase 5: * - For non-free ports: * - NORTH_SOUTH_PORT_POSTPROCESSOR * * - For center edge labels: * - LABEL_DUMMY_REMOVER * * - For end edge labels: * - END_LABEL_PROCESSOR */ /** additional processor dependencies for graphs with possible inverted ports. */ private static final IntermediateProcessingConfiguration INVERTED_PORT_PROCESSING_ADDITIONS = IntermediateProcessingConfiguration .createEmpty().addBeforePhase3(IntermediateProcessorStrategy.INVERTED_PORT_PROCESSOR); /** additional processor dependencies for graphs with northern / southern non-free ports. */ private static final IntermediateProcessingConfiguration NORTH_SOUTH_PORT_PROCESSING_ADDITIONS = IntermediateProcessingConfiguration .createEmpty().addBeforePhase3(IntermediateProcessorStrategy.NORTH_SOUTH_PORT_PREPROCESSOR) .addAfterPhase5(IntermediateProcessorStrategy.NORTH_SOUTH_PORT_POSTPROCESSOR); /** additional processor dependencies for graphs with center edge labels. */ private static final IntermediateProcessingConfiguration CENTER_EDGE_LABEL_PROCESSING_ADDITIONS = IntermediateProcessingConfiguration .createEmpty().addBeforePhase2(IntermediateProcessorStrategy.LABEL_DUMMY_INSERTER) .addBeforePhase3(IntermediateProcessorStrategy.LABEL_DUMMY_SWITCHER) .addBeforePhase4(IntermediateProcessorStrategy.LABEL_SIDE_SELECTOR) .addAfterPhase5(IntermediateProcessorStrategy.LABEL_DUMMY_REMOVER); /** additional processor dependencies for graphs with head or tail edge labels. */ private static final IntermediateProcessingConfiguration END_EDGE_LABEL_PROCESSING_ADDITIONS = IntermediateProcessingConfiguration .createEmpty().addBeforePhase4(IntermediateProcessorStrategy.LABEL_SIDE_SELECTOR) .addAfterPhase5(IntermediateProcessorStrategy.END_LABEL_PROCESSOR); /** * {@inheritDoc} */ public IntermediateProcessingConfiguration getIntermediateProcessingConfiguration(final LGraph graph) { Set<GraphProperties> graphProperties = graph.getProperty(InternalProperties.GRAPH_PROPERTIES); // Basic configuration IntermediateProcessingConfiguration configuration = IntermediateProcessingConfiguration.createEmpty(); // Additional dependencies if (graphProperties.contains(GraphProperties.NON_FREE_PORTS) || graph.getProperty(Properties.FEEDBACK_EDGES)) { configuration.addAll(INVERTED_PORT_PROCESSING_ADDITIONS); if (graphProperties.contains(GraphProperties.NORTH_SOUTH_PORTS)) { configuration.addAll(NORTH_SOUTH_PORT_PROCESSING_ADDITIONS); } } if (graphProperties.contains(GraphProperties.CENTER_LABELS)) { configuration.addAll(CENTER_EDGE_LABEL_PROCESSING_ADDITIONS); } if (graphProperties.contains(GraphProperties.END_LABELS)) { configuration.addAll(END_EDGE_LABEL_PROCESSING_ADDITIONS); } return configuration; } /** the minimal vertical difference for creating bend points. */ private static final double MIN_VERT_DIFF = 1.0; /** factor for layer spacing. */ private static final double LAYER_SPACE_FAC = 0.4; /** Set of already created junction points, to avoid multiple points at the same position. */ private final Set<KVector> createdJunctionPoints = Sets.newHashSet(); /* Implementation Note: * * The process method works by going through each layer and possibly adding bend points to incoming * edges of nodes and to outgoing edges of nodes to avoid edge-node-overlaps. While other edge * routing algorithms insert all bend points required to route edges between a pair of layers, this * router actually routes any edge in two steps: first while iterating through the source layer, and * second while iterating through the target layer. Understanding causes the code below to make a * lot more sense. */ /** * {@inheritDoc} */ public void process(final LGraph layeredGraph, final IElkProgressMonitor monitor) { monitor.begin("Polyline edge routing", 1); final float nodeSpacing = layeredGraph.getProperty(InternalProperties.SPACING); final float edgeSpaceFac = layeredGraph.getProperty(Properties.EDGE_SPACING_FACTOR); double xpos = 0.0; double layerSpacing = 0.0; // Determine the horizontal spacing required to route west-side in-layer edges of the first layer if (!layeredGraph.getLayers().isEmpty()) { double yDiff = calculateWestInLayerEdgeYDiff(layeredGraph.getLayers().get(0)); xpos = LAYER_SPACE_FAC * edgeSpaceFac * yDiff; } // Iterate over the layers ListIterator<Layer> layerIter = layeredGraph.getLayers().listIterator(); while (layerIter.hasNext()) { Layer layer = layerIter.next(); boolean externalLayer = Iterables.all(layer, PRED_EXTERNAL_WEST_OR_EAST_PORT); // The rightmost layer is not given any node spacing if it's an external port layer if (externalLayer && xpos > 0) { xpos -= nodeSpacing; } // Set horizontal coordinates for all nodes of the layer LGraphUtil.placeNodesHorizontally(layer, xpos); // While routing edges, we remember the maximum vertical span of any edge between this and // the next layer to insert enough space between the layers to keep the edge slopes from // becoming too steep double maxVertDiff = 0.0; // Iterate over the layer's nodes for (LNode node : layer) { // Calculate the maximal vertical span of output edges. In-layer edges will also be // routed at this point double maxCurrOutputYDiff = 0.0; for (LEdge outgoingEdge : node.getOutgoingEdges()) { double sourcePos = outgoingEdge.getSource().getAbsoluteAnchor().y; double targetPos = outgoingEdge.getTarget().getAbsoluteAnchor().y; if (layer == outgoingEdge.getTarget().getNode().getLayer()) { // In-layer edges require an extra bend point to make them look nice processInLayerEdge(outgoingEdge, xpos, LAYER_SPACE_FAC * edgeSpaceFac * Math.abs(sourcePos - targetPos)); if (outgoingEdge.getSource().getSide() == PortSide.WEST) { // The spacing required for routing in-layer edges on the west side doesn't // contribute anything to the spacing required between this and the next // layer and was already taken into account previously sourcePos = 0; targetPos = 0; } } maxCurrOutputYDiff = Math.max(maxCurrOutputYDiff, Math.abs(targetPos - sourcePos)); } // We currently only handle certain node types. This might change in the future switch (node.getType()) { case NORMAL: case LABEL: case LONG_EDGE: case NORTH_SOUTH_PORT: processNode(node, xpos); break; } maxVertDiff = Math.max(maxVertDiff, maxCurrOutputYDiff); } // Consider the span of west-side in-layer edges of the next layer to be sure to reserve // enough space for routing them during the next iteration if (layerIter.hasNext()) { double yDiff = calculateWestInLayerEdgeYDiff(layerIter.next()); maxVertDiff = Math.max(maxVertDiff, yDiff); layerIter.previous(); } // Determine where next layer should start based on the maximal vertical span of edges // between the two layers layerSpacing = LAYER_SPACE_FAC * edgeSpaceFac * maxVertDiff; if (!externalLayer && layerIter.hasNext()) { layerSpacing += nodeSpacing; } xpos += layer.getSize().x + layerSpacing; } createdJunctionPoints.clear(); // Set the graph's horizontal size layeredGraph.getSize().x = xpos; monitor.done(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Actual Edge Routing Code /** * Inserts bend points for edges incident to this node. The bend points are inserted such that * the segments that cross the layer's area are straight or pretty much straight. A bend point * is only added if it's necessary: if the bend point to be inserted differs from the edge's end * point. * * @param node * the node whose incident edges to insert bend points for. * @param layerLeftXPos * the x position of the node's layer. */ private void processNode(final LNode node, final double layerLeftXPos) { // The right side of the layer final double layerRightXPos = layerLeftXPos + node.getLayer().getSize().x; for (LPort port : node.getPorts()) { KVector absolutePortAnchor = port.getAbsoluteAnchor(); KVector bendPoint = new KVector(0, absolutePortAnchor.y); if (port.getSide() == PortSide.EAST) { bendPoint.x = layerRightXPos; } else if (port.getSide() == PortSide.WEST) { bendPoint.x = layerLeftXPos; } else { // We only know what to do with eastern and western ports continue; } // If the port's absolute anchor equals the bend point, we don't want to insert anything if (absolutePortAnchor.x == bendPoint.x) { continue; } // Whether to add a junction point or not boolean addJunctionPoint = port.getOutgoingEdges().size() + port.getIncomingEdges().size() > 1; // Iterate over the edges and add bend (and possibly junction) points StreamSupport.stream(port.getConnectedEdges().spliterator(), false) // Only route an edge if it isn't (nearly) straight anyway .filter(e -> { LPort otherPort = e.getSource() == port ? e.getTarget() : e.getSource(); return Math.abs(otherPort.getAbsoluteAnchor().y - bendPoint.y) > MIN_VERT_DIFF; }) // Insert bend point .forEach(e -> addBendPoint(e, bendPoint, addJunctionPoint, port)); } } /** * In-layer edges get an extra bend point in addition to the usual source and target bend * points. The additional bend point is inserted halfway between the edge's upper and lower end, * a little way from the layer's boundary. * * @param edge * the in-layer edge to route. * @param layerXPos * the layer's x position. * @param edgeSpacing * the spacing to respect for the in-layer edge bend points. */ private void processInLayerEdge(final LEdge edge, final double layerXPos, final double edgeSpacing) { LPort sourcePort = edge.getSource(); LPort targetPort = edge.getTarget(); double midY = (sourcePort.getAbsoluteAnchor().y + targetPort.getAbsoluteAnchor().y) / 2.0; /* This method is called if an outgoing in-layer edge is found before any other edges of the * offending node are routed. Thus, if the edge's list of bend points is not empty, any bend * point must be at the target port. It follows that our extra bend point can safely be * inserted at the start of the bend point list. */ KVector bendPoint = null; if (sourcePort.getSide() == PortSide.EAST) { bendPoint = new KVector(layerXPos + sourcePort.getNode().getLayer().getSize().x + edgeSpacing, midY); } else { bendPoint = new KVector(layerXPos - edgeSpacing, midY); } edge.getBendPoints().add(0, bendPoint); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Utility Methods /** * Calculates the maximum vertical span of any in-layer edge connecting west-side ports of nodes * in the given layer. * * @param layer * the layer to iterate over. * @return maximum vertical span of west-side in-layer edges. */ private double calculateWestInLayerEdgeYDiff(final Layer layer) { double maxYDiff = 0.0; for (LNode node : layer) { for (LEdge outgoingEdge : node.getOutgoingEdges()) { if (layer == outgoingEdge.getTarget().getNode().getLayer() && outgoingEdge.getSource().getSide() == PortSide.WEST) { double sourcePos = outgoingEdge.getSource().getAbsoluteAnchor().y; double targetPos = outgoingEdge.getTarget().getAbsoluteAnchor().y; maxYDiff = Math.max(maxYDiff, Math.abs(targetPos - sourcePos)); } } } return maxYDiff; } /** * Adds a copy of the given bend point to the given edge at the appropriate place in the list of * bend points. The appropriate place is determined by the given port: if it's the source port, * the bend point is prepended to the list; otherwise is is appended. The bend point is not * added to the list at all if it wouldn't have any visual effect; that is, if the port's anchor * equals the bend point. * * @param edge * the edge to add the bend point to. * @param bendPoint * the bend point to add. * @param addJunctionPoint * if {@code true}, a copy of the bend point will be added to the edge's list of * junction points, if necessary. * @param currPort * the port the bend point is near. */ private void addBendPoint(final LEdge edge, final KVector bendPoint, final boolean addJunctionPoint, final LPort currPort) { // Only insert the bend point if necessary if (!currPort.getAbsoluteAnchor().equals(bendPoint)) { if (edge.getSource() == currPort) { edge.getBendPoints().add(0, new KVector(bendPoint)); } else { edge.getBendPoints().add(new KVector(bendPoint)); } if (addJunctionPoint && !createdJunctionPoints.contains(bendPoint)) { // create a new junction point for the edge at the bend point's position KVectorChain junctionPoints = edge.getProperty(LayoutOptions.JUNCTION_POINTS); if (junctionPoints == null) { junctionPoints = new KVectorChain(); edge.setProperty(LayoutOptions.JUNCTION_POINTS, junctionPoints); } KVector jpoint = new KVector(bendPoint); junctionPoints.add(jpoint); createdJunctionPoints.add(jpoint); } } } }