de.cau.cs.kieler.klay.layered.p5edges.PolylineEdgeRouter.java Source code

Java tutorial

Introduction

Here is the source code for de.cau.cs.kieler.klay.layered.p5edges.PolylineEdgeRouter.java

Source

/*
 * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient
 *
 * http://www.informatik.uni-kiel.de/rtsys/kieler/
 * 
 * Copyright 2010 by
 * + Christian-Albrechts-University of Kiel
 *   + Department of Computer Science
 *     + Real-Time and Embedded Systems Group
 * 
 * This code is provided under the terms of the Eclipse Public License (EPL).
 * See the file epl-v10.html for the license text.
 */
package de.cau.cs.kieler.klay.layered.p5edges;

import java.util.ListIterator;
import java.util.Set;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

import de.cau.cs.kieler.core.alg.IKielerProgressMonitor;
import de.cau.cs.kieler.core.math.KVector;
import de.cau.cs.kieler.core.math.KielerMath;
import de.cau.cs.kieler.kiml.options.PortSide;
import de.cau.cs.kieler.klay.layered.ILayoutPhase;
import de.cau.cs.kieler.klay.layered.IntermediateProcessingConfiguration;
import de.cau.cs.kieler.klay.layered.graph.LEdge;
import de.cau.cs.kieler.klay.layered.graph.LGraph;
import de.cau.cs.kieler.klay.layered.graph.LGraphUtil;
import de.cau.cs.kieler.klay.layered.graph.LNode;
import de.cau.cs.kieler.klay.layered.graph.LNode.NodeType;
import de.cau.cs.kieler.klay.layered.graph.LPort;
import de.cau.cs.kieler.klay.layered.graph.Layer;
import de.cau.cs.kieler.klay.layered.intermediate.IntermediateProcessorStrategy;
import de.cau.cs.kieler.klay.layered.properties.GraphProperties;
import de.cau.cs.kieler.klay.layered.properties.InternalProperties;
import de.cau.cs.kieler.klay.layered.properties.PortType;
import de.cau.cs.kieler.klay.layered.properties.Properties;

/**
 * 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
 * @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) {
            return node.getNodeType() == NodeType.EXTERNAL_PORT
                    && (node.getProperty(InternalProperties.EXT_PORT_SIDE) == PortSide.WEST
                            || node.getProperty(InternalProperties.EXT_PORT_SIDE) == 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);

    /** 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;

    /**
     * {@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;
    }

    /**
     * {@inheritDoc}
     */
    public void process(final LGraph layeredGraph, final IKielerProgressMonitor monitor) {
        monitor.begin("Polyline edge routing", 1);

        float nodeSpacing = layeredGraph.getProperty(InternalProperties.SPACING);
        float edgeSpaceFac = layeredGraph.getProperty(Properties.EDGE_SPACING_FACTOR);

        double xpos = 0.0;
        double layerSpacing = 0.0;

        // Determine the spacing required for west-side in-layer edges of the first layer
        if (!layeredGraph.getLayers().isEmpty()) {
            double firstDiff = determineNextInLayerDiff(layeredGraph.getLayers().get(0));
            xpos = LAYER_SPACE_FAC * edgeSpaceFac * firstDiff;
        }

        // 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 (externalLayer && xpos > 0) {
                xpos -= nodeSpacing;
            }

            // Set horizontal coordinates for all nodes of the layer
            LGraphUtil.placeNodes(layer, xpos);

            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 are already
                // routed at this point by inserting bend points appropriately
                double maxOutputYDiff = 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()) {
                        // We have an in-layer edge -- route it!
                        routeInLayerEdge(outgoingEdge, xpos, layer.getSize().x,
                                LAYER_SPACE_FAC * edgeSpaceFac * Math.abs(sourcePos - targetPos));
                        if (outgoingEdge.getSource().getSide() == PortSide.WEST) {
                            // West-side in-layer edges have been considered in the previous iteration
                            sourcePos = 0;
                            targetPos = 0;
                        }
                    }
                    maxOutputYDiff = KielerMath.maxd(maxOutputYDiff, targetPos - sourcePos, sourcePos - targetPos);
                }

                // Different node types have to be handled differently
                NodeType nodeType = node.getNodeType();
                // FIXME use switch-case here?
                if (nodeType == NodeType.NORMAL) {
                    processNormalNode(node, xpos, layer.getSize().x);
                } else if (nodeType == NodeType.LONG_EDGE) {
                    processLongEdgeDummyNode(node, nodeSpacing, edgeSpaceFac, xpos, maxOutputYDiff);
                } else if (nodeType == NodeType.LABEL) {
                    processLabelDummyNode(node, xpos, layer.getSize().x);
                }

                maxVertDiff = Math.max(maxVertDiff, maxOutputYDiff);
            }

            // Consider west-side in-layer edges of the next layer
            if (layerIter.hasNext()) {
                double nextDiff = determineNextInLayerDiff(layerIter.next());
                maxVertDiff = Math.max(maxVertDiff, nextDiff);
                layerIter.previous();
            }

            // Determine placement of next layer based on the maximal vertical difference (as the
            // maximum vertical difference edges span grows, the layer grows wider to allow enough
            // space for such sloped edges to avoid too harsh angles)
            layerSpacing = LAYER_SPACE_FAC * edgeSpaceFac * maxVertDiff;
            if (!externalLayer && layerIter.hasNext()) {
                layerSpacing += nodeSpacing;
            }
            xpos += layer.getSize().x + layerSpacing;
        }

        // Set the graph's horizontal size
        layeredGraph.getSize().x = xpos;

        monitor.done();
    }

    private double determineNextInLayerDiff(final Layer layer) {
        double nextInLayerDiff = 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;
                    nextInLayerDiff = KielerMath.maxd(nextInLayerDiff, targetPos - sourcePos,
                            sourcePos - targetPos);
                }
            }
        }
        return nextInLayerDiff;
    }

    /**
     * Inserts bend points to edges of this node as appropriate according to the node's margins. This
     * is to ensure that edges don't cross port labels or anything. The edges are first routed
     * horizontally through the node's margin before sloping off to wherever they're going.
     * 
     * However, only add bendpoints that are unequal to the absolute anchor points of the 
     * ports an edge is attached to.
     * 
     * @param node the node whose edges to insert bend points for.
     * @param layerXPos the x position of the node's layer.
     * @param layerWidth the width of the node's layer.
     */
    private void processNormalNode(final LNode node, final double layerXPos, final double layerWidth) {
        // The right side of the layer
        final double layerRightXPos = layerXPos + layerWidth;

        for (LPort port : node.getPorts()) {
            if (port.getSide() == PortSide.EAST) {
                // Port is on the eastern side
                KVector bendPoint = new KVector(layerRightXPos, port.getAbsoluteAnchor().y);

                for (LEdge edge : port.getOutgoingEdges()) {
                    if (edge.getTarget().getNode().getLayer() != node.getLayer()
                            && !bendPoint.equals(edge.getSource().getAbsoluteAnchor())
                            && Math.abs(edge.getTarget().getAbsoluteAnchor().y - bendPoint.y) > MIN_VERT_DIFF) {

                        edge.getBendPoints().add(0, new KVector(bendPoint));
                    }
                }

                for (LEdge edge : port.getIncomingEdges()) {
                    if (edge.getSource().getNode().getLayer() != node.getLayer()
                            && !bendPoint.equals(edge.getTarget().getAbsoluteAnchor())
                            && Math.abs(edge.getSource().getAbsoluteAnchor().y - bendPoint.y) > MIN_VERT_DIFF) {

                        edge.getBendPoints().add(new KVector(bendPoint));
                    }
                }
            } else if (port.getSide() == PortSide.WEST) {
                // Port is on the western side
                KVector bendPoint = new KVector(layerXPos, port.getAbsoluteAnchor().y);

                for (LEdge edge : port.getOutgoingEdges()) {
                    if (edge.getTarget().getNode().getLayer() != node.getLayer()
                            && !bendPoint.equals(edge.getSource().getAbsoluteAnchor())
                            && Math.abs(edge.getTarget().getAbsoluteAnchor().y - bendPoint.y) > MIN_VERT_DIFF) {

                        edge.getBendPoints().add(0, new KVector(bendPoint));
                    }
                }

                for (LEdge edge : port.getIncomingEdges()) {
                    if (edge.getSource().getNode().getLayer() != node.getLayer()
                            && !bendPoint.equals(edge.getTarget().getAbsoluteAnchor())
                            && Math.abs(edge.getSource().getAbsoluteAnchor().y - bendPoint.y) > MIN_VERT_DIFF) {

                        edge.getBendPoints().add(new KVector(bendPoint));
                    }
                }
            }
        }
    }

    /**
     * Routes the edges connected to a {@code LONG_EDGE} dummy node.
     * 
     * @param node the dummy node whose incident edges to route.
     * @param spacing spacing between objects.
     * @param edgeSpaceFac edge spacing factor.
     * @param xpos the layer's x position.
     * @param maxOutputYDiff the maximal vertical span of output edges connected to the node.
     */
    private void processLongEdgeDummyNode(final LNode node, final float spacing, final float edgeSpaceFac,
            final double xpos, final double maxOutputYDiff) {

        // TODO: This code looks much too complicated for my taste. I bet it can be simplified.

        // Calculate the maximal vertical span of input edges
        double maxInputYDiff = 0.0;
        for (LPort targetPort : node.getPorts(PortType.INPUT)) {
            double targetPos = targetPort.getAbsoluteAnchor().y;

            // Iterate over the connected source ports
            for (LPort sourcePort : targetPort.getPredecessorPorts()) {
                double sourcePos = sourcePort.getAbsoluteAnchor().y;
                maxInputYDiff = KielerMath.maxd(maxInputYDiff, targetPos - sourcePos, sourcePos - targetPos);
            }
        }

        Layer currentLayer = node.getLayer();
        if (maxInputYDiff >= MIN_VERT_DIFF && maxOutputYDiff >= MIN_VERT_DIFF) {
            // Both the incoming and the outgoing edges have significant differences. Check
            // how large the vertical span is in relation to the layer's width and thus
            // determine if we need to insert bend points at all
            double layerSize = node.getLayer().getSize().x;
            double diff = Math.max(maxInputYDiff, maxOutputYDiff);
            double deviation = diff / (layerSize / 2.0 + spacing + LAYER_SPACE_FAC * edgeSpaceFac * diff)
                    * layerSize / 2.0;

            if (deviation >= edgeSpaceFac * spacing) {
                // Insert for incoming and outgoing edges
                for (LEdge incoming : node.getIncomingEdges()) {
                    if (currentLayer != incoming.getSource().getNode().getLayer()) {
                        incoming.getBendPoints().add(xpos, incoming.getTarget().getAbsoluteAnchor().y);
                    }
                }

                for (LEdge outgoing : node.getOutgoingEdges()) {
                    if (currentLayer != outgoing.getTarget().getNode().getLayer()) {
                        outgoing.getBendPoints().add(xpos + layerSize, outgoing.getSource().getAbsoluteAnchor().y);
                    }
                }
            } else {
                // Insert only for incoming edges in the layer's horizontal center
                for (LEdge incoming : node.getIncomingEdges()) {
                    if (currentLayer != incoming.getSource().getNode().getLayer()) {
                        incoming.getBendPoints().add(xpos + layerSize / 2.0,
                                incoming.getTarget().getAbsoluteAnchor().y);
                    }
                }
            }
        } else if (maxInputYDiff >= MIN_VERT_DIFF) {
            // Only the incoming edges have significant differences
            for (LEdge incoming : node.getIncomingEdges()) {
                if (currentLayer != incoming.getSource().getNode().getLayer()) {
                    incoming.getBendPoints().add(xpos, incoming.getTarget().getAbsoluteAnchor().y);
                }
            }
        } else if (maxOutputYDiff >= MIN_VERT_DIFF) {
            // Only the outgoing edges have significant differences
            for (LEdge outgoing : node.getOutgoingEdges()) {
                if (currentLayer != outgoing.getTarget().getNode().getLayer()) {
                    outgoing.getBendPoints().add(xpos + node.getLayer().getSize().x,
                            outgoing.getSource().getAbsoluteAnchor().y);
                }
            }
        }
    }

    /**
     * Routes edges connected to {@code LABEL} dummy nodes. Bend points are inserted left and right
     * of the node to ensure that the edge doesn't cross the label.
     * 
     * @param node the dummy node whose incident edges to route.
     * @param layerXPos the x position of the node's layer.
     * @param layerWidth the width of the node's layer.
     */
    private void processLabelDummyNode(final LNode node, final double layerXPos, final double layerWidth) {

        // Insert bend points left and right of the node so that the label does not overlap the edge.
        Layer currentLayer = node.getLayer();

        for (LEdge incoming : node.getIncomingEdges()) {
            if (currentLayer != incoming.getSource().getNode().getLayer()) {
                incoming.getBendPoints().add(layerXPos, incoming.getTarget().getAbsoluteAnchor().y);
            }
        }

        for (LEdge outgoing : node.getOutgoingEdges()) {
            if (currentLayer != outgoing.getTarget().getNode().getLayer()) {
                outgoing.getBendPoints().add(layerXPos + layerWidth, outgoing.getSource().getAbsoluteAnchor().y);
            }
        }
    }

    /**
     * Computes the bend points for in-layer edges. In-layer edges are assumed to always connect either
     * two western or two eastern ports, but not two ports on different sides. This method makes no
     * restrictions as to the kinds of nodes connected by the in-layer edge. That is, it does not for
     * example assume at least one of the connected nodes to be a regular node.
     * 
     * @param edge the in-layer edge to route.
     * @param layerXPos the layer's x position.
     * @param layerWidth the layer's width.
     * @param edgeSpacing the spacing to respect for the in-layer edge bend points.
     */
    private void routeInLayerEdge(final LEdge edge, final double layerXPos, final double layerWidth,
            final double edgeSpacing) {

        /* We will add two bend points to the edge:
         *  1. One will be vertically centered between the connected ports, with the x position
         *     slightly to the left (western ports) or to the right (eastern ports) of the layer.
         *  2. The other will be just where the port of the dummy node is anchored. (all in-layer
         *     edges are assumed to connect to at least one dummy node.)
         */

        LPort sourcePort = edge.getSource();
        LPort targetPort = edge.getTarget();

        // Calculate the two x coordinates used (one at the layer start / end, and one a bit off)
        double nearX = 0.0;
        double farX = 0.0;

        // Since in-layer edges connect two eastern or two western ports, we only need to look at the
        // port side of the source port
        if (sourcePort.getSide() == PortSide.EAST) {
            nearX = layerXPos + layerWidth;
            farX = nearX + edgeSpacing;
        } else if (sourcePort.getSide() == PortSide.WEST) {
            nearX = layerXPos;
            farX = nearX - edgeSpacing;
        }

        // FIRST BEND POINT (if the source node is a dummy node)
        if (sourcePort.getNode().getNodeType() != NodeType.NORMAL) {
            edge.getBendPoints().add(new KVector(nearX, sourcePort.getAbsoluteAnchor().y));
        }

        // SECOND BEND POINT (halfway between the ports)
        edge.getBendPoints().add(
                new KVector(farX, (sourcePort.getAbsoluteAnchor().y + targetPort.getAbsoluteAnchor().y) / 2.0));

        // THIRD BEND POINT (if the target node is a dummy node)
        if (targetPort.getNode().getNodeType() != NodeType.NORMAL) {
            edge.getBendPoints().add(new KVector(nearX, targetPort.getAbsoluteAnchor().y));
        }
    }
}