/*******************************************************************************
* Copyright (c) 2012, 2016 itemis AG 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:
* Rene Kuhlemann - provided first version of code based on the initial paper
* of Sugiyama et al. (http://dx.doi.org/10.1109/TSMC.1981.4308636),
* associated to bugzilla entry #384730
* Adam Kovacs - implements the new LayerProvider and
* CrossingReducer interfaces
* Matthias Wienand (itemis AG) - refactorings
*
*******************************************************************************/
package org.eclipse.gef.layout.algorithms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.geometry.planar.Rectangle;
import org.eclipse.gef.graph.Edge;
import org.eclipse.gef.graph.Node;
import org.eclipse.gef.layout.ILayoutAlgorithm;
import org.eclipse.gef.layout.LayoutContext;
import org.eclipse.gef.layout.LayoutProperties;
/**
* The SugiyamaLayoutAlgorithm class implements an algorithm to arrange a
* directed graph in a layered tree-like layout. The final presentation follows
* five design principles for enhanced readability:
*
* - Hierarchical layout of vertices - Least crossings of lines (edges) -
* Straightness of lines when ever possible - Close layout of vertices connected
* to each other, i.e. short paths - Balanced layout of lines coming into or
* going from a vertex
*
* For further information see http://dx.doi.org/10.1109/TSMC.1981.4308636
*
* This layout algorithm works only with - directed graphs (otherwise an
* appropriate RuntimeException is thrown)
*
* @author Rene Kuhlemann
* @author Adam Kovacs
* @author mwienand
*/
public class SugiyamaLayoutAlgorithm implements ILayoutAlgorithm {
/**
* Specifies the direction for the {@link SugiyamaLayoutAlgorithm}.
*/
public enum Direction {
/**
* Horizontal direction, i.e. left to right.
*/
HORIZONTAL,
/**
* Vertical direction, i.e. top to bottom.
*/
VERTICAL
}
/**
*
* An interface for heuristics that reduces edge crossings.
*
* @author Adam Kovacs
*/
public static interface CrossingReducer {
/**
* From the given nodes it creates a map of NodeLayouts and NodeWrappers
* which contains the layers and indexes of the nodes
*
* @param nodes
* List of nodes needed to be organized
*/
void crossReduction(List<List<NodeWrapper>> nodes);
}
/**
* @author Rene Kuhlemann
*/
public static class BarycentricCrossingReducer implements CrossingReducer {
private List<List<NodeWrapper>> layers = new ArrayList<>();
private Map<Node, NodeWrapper> map = new IdentityHashMap<>();
private static final int MAX_SWEEPS = 35;
private int last; // index of the last element in a layer after padding
// process
/**
* Fills in virtual nodes, so the layer system finally becomes an
* equidistant grid
*/
private void padLayers() {
last = 0;
for (List<NodeWrapper> iter : layers)
if (iter.size() > last)
last = iter.size();
last--; // index of the last element of any layer
for (List<NodeWrapper> iter : layers) { // padding is always
// added at
// the END of each layer!
for (int i = iter.size(); i <= last; i++)
iter.add(new NodeWrapper());
updateIndex(iter);
}
}
/**
* Removes the padding nodes from the layers
*/
private void unpadLayers() {
for (List<NodeWrapper> layer : layers) {
Iterator<NodeWrapper> it = layer.iterator();
while (it.hasNext()) {
NodeWrapper wrapper = it.next();
if (wrapper.isPadding()) {
it.remove();
}
}
}
}
/**
* Reduces connection crossings between two adjacent layers by a
* combined top-down and bottom-up approach. It uses a heuristic
* approach based on the predecessor's barycenter.
*/
private void reduceCrossings() {
for (int round = 0; round < MAX_SWEEPS; round++) {
if ((round & 1) == 0) { // if round is even then do a bottom-up
// scan
for (int index = 1; index < layers.size(); index++)
reduceCrossingsDown(layers.get(index));
} else { // else top-down
for (int index = layers.size() - 2; index >= 0; index--)
reduceCrossingsUp(layers.get(index));
}
}
}
private void reduceCrossingsDown(List<NodeWrapper> layer) {
// DOWN: scan PREDECESSORS
for (NodeWrapper node : layer)
node.index = node.getBaryCenter(node.pred);
Collections.sort(layer, new Comparator<NodeWrapper>() {
public int compare(NodeWrapper node1, NodeWrapper node2) {
return (node1.index - node2.index);
}
});
updateIndex(layer);
}
private void reduceCrossingsUp(List<NodeWrapper> layer) {
// UP: scan SUCCESSORS
for (NodeWrapper node : layer)
node.index = node.getBaryCenter(node.succ);
Collections.sort(layer, new Comparator<NodeWrapper>() {
public int compare(NodeWrapper node1, NodeWrapper node2) {
return (node1.index - node2.index);
}
});
updateIndex(layer);
}
private void refineLayers() {
// from Sugiyama
// paper: down, up, and down yields best results, wonder why..
for (int index = 1; index < layers.size(); index++)
refineLayersDown(layers.get(index));
for (int index = layers.size() - 2; index >= 0; index--)
refineLayersUp(layers.get(index));
for (int index = 1; index < layers.size(); index++)
refineLayersDown(layers.get(index));
}
private void refineLayersDown(List<NodeWrapper> layer) {
// first, get a priority list
List<NodeWrapper> list = new ArrayList<>(layer);
Collections.sort(list, new Comparator<NodeWrapper>() {
public int compare(NodeWrapper node1, NodeWrapper node2) {
return (node2.getPriorityDown() - node1.getPriorityDown()); // descending
// ordering!!!
}
});
// second, remove padding from the layer's end and place them in
// front of the current node to improve its position
for (NodeWrapper iter : list) {
// break, if there are no more "real" nodes
if (iter.isPadding())
break;
// compute distance to new position
int delta = iter.getBaryCenter(iter.pred) - iter.index;
for (int i = 0; i < delta; i++)
layer.add(iter.index, layer.remove(last));
}
updateIndex(layer);
}
private void refineLayersUp(List<NodeWrapper> layer) {
// first, get a priority list
List<NodeWrapper> list = new ArrayList<>(layer);
Collections.sort(list, new Comparator<NodeWrapper>() {
public int compare(NodeWrapper node1, NodeWrapper node2) {
// descending order
return (node2.getPriorityUp() - node1.getPriorityUp());
}
});
// second, remove padding from the layer's end and place them in
// front of the current node to improve its position
for (NodeWrapper iter : list) {
// break, if there are no more "real" nodes
if (iter.isPadding())
break;
// compute distance to new position
int delta = iter.getBaryCenter(iter.succ) - iter.index;
for (int i = 0; i < delta; i++)
layer.add(iter.index, layer.remove(last));
}
updateIndex(layer);
}
private void updateIndex(List<NodeWrapper> list) {
for (int index = 0; index < list.size(); index++) {
list.get(index).index = index;
map.put(list.get(index).node, list.get(index));
}
}
public void crossReduction(List<List<NodeWrapper>> nodes) {
this.layers = nodes;
padLayers();
// reduce and refine iteratively, depending on the depth of the
// graph
for (int i = 0; i < layers.size(); i++) {
reduceCrossings();
refineLayers();
}
reduceCrossings();
unpadLayers();
}
}
/**
* Implements the CrossingReducer interface. This algorithm divides each
* layer by a pivot node based on the relative position of connected nodes
* and decides which side of the pivot point it should be for the fewer edge
* crossing.
*
* @author Adam Kovacs
*
*/
public static class SplitCrossingReducer implements CrossingReducer {
private final Map<Node, NodeWrapper> map = new IdentityHashMap<>();
/**
* Filters the multiple connections from the two arrays
*
* @param a
* @param b
* @return
*/
private ArrayList<Node> unionOfNodes(Collection<Node> a,
Collection<Node> b) {
ArrayList<Node> res = new ArrayList<>(a);
for (Node n : b) {
if (!res.contains(n)) {
res.add(n);
}
}
return res;
}
/**
* Returns the number of crosses between the two nodes and those
* connected to them.
*
* @param nodeA
* @param nodeB
* @return
*/
private int numberOfCrosses(NodeWrapper nodeA, NodeWrapper nodeB) {
int numOfCrosses = 0;
if (nodeA.equals(nodeB))
return 0;
// Filter nodes connected with bidirectional edges
ArrayList<Node> adjacentNodesOfA = unionOfNodes(
nodeA.node.getPredecessorNodes(),
nodeA.node.getSuccessorNodes());
ArrayList<Node> adjacentNodesOfB = unionOfNodes(
nodeB.node.getPredecessorNodes(),
nodeB.node.getSuccessorNodes());
for (Node aNode : adjacentNodesOfA) {
ArrayList<Integer> alreadyCrossed = new ArrayList<>();
NodeWrapper aNodeWrapper = map.get(aNode);
for (int i = 0; i < adjacentNodesOfB.size(); i++) {
NodeWrapper nw = map.get(adjacentNodesOfB.get(i));
if (!alreadyCrossed.contains(i) && nw != null) {
// only if on the same side
if ((nw.layer > nodeA.layer
&& aNodeWrapper.layer > nodeA.layer)
|| (nw.layer < nodeA.layer
&& aNodeWrapper.layer < nodeA.layer)) {
if (nodeA.index < nodeB.index) {
if (aNodeWrapper.index > nw.index) {
numOfCrosses++;
alreadyCrossed.add(i);
} else if (nw.index == aNodeWrapper.index) {
if (nodeA.index >= nw.index) {
// implies nodeB.index > nw.index
if ((aNodeWrapper.layer > nw.layer
&& nodeA.layer < nw.layer)
|| (aNodeWrapper.layer < nw.layer
&& nw.layer < nodeA.layer)) {
// top-left or bottom-left quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
} else if (nodeB.index <= nw.index) {
// implies nodeA.index < nw.index
if ((aNodeWrapper.layer > nw.layer
&& aNodeWrapper.layer < nodeB.layer)
|| (aNodeWrapper.layer < nw.layer
&& aNodeWrapper.layer > nodeB.layer)) {
// top-right or bottom-right quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
}
}
} else if (nodeA.index > nodeB.index) {
if (aNodeWrapper.index < nw.index) {
numOfCrosses++;
alreadyCrossed.add(i);
} else if (nw.index == aNodeWrapper.index) {
if (nodeB.index >= nw.index) {
// implies nodeB.index > nw.index
if ((aNodeWrapper.layer > nw.layer
&& nodeB.layer > aNodeWrapper.layer)
|| (aNodeWrapper.layer < nw.layer
&& aNodeWrapper.layer > nodeB.layer)) {
// top-left or bottom-left quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
} else if (nodeA.index <= nw.index) {
// implies nodeA.index < nw.index
if ((aNodeWrapper.layer > nw.layer
&& nw.layer > nodeA.layer)
|| (aNodeWrapper.layer < nw.layer
&& nw.layer < nodeA.layer)) {
// top-right or bottom-right quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
}
}
}
}
}
}
}
return numOfCrosses;
}
/**
* Selects the pivot node by random and decides the order.
*
* @param layer
* @return
*/
private List<NodeWrapper> splitHeuristic(List<NodeWrapper> layer) {
ArrayList<NodeWrapper> left = new ArrayList<>();
ArrayList<NodeWrapper> right = new ArrayList<>();
if (layer.size() < 1)
return layer;
Random random = new Random();
NodeWrapper pivot = layer.get(random.nextInt(layer.size()));
// NodeWrapper pivot = layer.get(0);
// NodeWrapper pivot = layer.get((int)(layer.size() / 2));
for (NodeWrapper node : layer) {
if (!node.equals(pivot) && node.node != null
&& pivot.node != null) {
int num1 = numberOfCrosses(node, pivot);
int num2 = numberOfCrosses(pivot, node);
if (num1 < num2)
left.add(node);
else if (num1 > num2)
right.add(node);
else {
if (num1 == num2 && num1 > 0) {
int tmpindex = map.get(pivot.node).index;
map.get(pivot.node).index = map
.get(node.node).index;
map.get(node.node).index = tmpindex;
}
if (node.index < pivot.index)
left.add(node);
else
right.add(node);
}
}
}
ArrayList<NodeWrapper> res = new ArrayList<>();
res.addAll(splitHeuristic(left));
res.add(pivot);
res.addAll(splitHeuristic(right));
return res;
}
public void crossReduction(List<List<NodeWrapper>> nodes) {
// Building the map
for (List<NodeWrapper> layer : nodes)
for (NodeWrapper nw : layer)
map.put(nw.node, nw);
for (int i = 0; i < nodes.size(); i++) {
if (!nodes.get(i).isEmpty()) {
splitHeuristic(nodes.get(i));
}
}
}
}
/**
* Implemented the CrossingReducer interface. This algorithm select
* neighbouring nodes and decides there order based on the number of edge
* crossings between them and those connected to them.
*
* @author Adam Kovacs
*
*/
public static class GreedyCrossingReducer implements CrossingReducer {
private final Map<Node, NodeWrapper> map = new IdentityHashMap<>();
private List<List<NodeWrapper>> layers = new ArrayList<>();
private Map<Integer, Integer> crossesForLayers = new IdentityHashMap<>();
/**
* Filters the multiple connections from the two arrays.
*
* @param a
* @param b
* @return
*/
private ArrayList<Node> unionOfNodes(Collection<Node> a,
Collection<Node> b) {
ArrayList<Node> res = new ArrayList<>(a);
for (Node n : b) {
if (!res.contains(n)) {
res.add(n);
}
}
return res;
}
/**
* Returns the number of crosses between the two nodes and those
* connected to them.
*
* @param nodeA
* @param nodeB
* @return
*/
private int numberOfCrosses(NodeWrapper nodeA, NodeWrapper nodeB) {
int numOfCrosses = 0;
if (nodeA.equals(nodeB))
return 0;
// Filter nodes connected with bidirectional edges
ArrayList<Node> adjacentNodesOfA = unionOfNodes(
nodeA.node.getPredecessorNodes(),
nodeA.node.getSuccessorNodes());
ArrayList<Node> adjacentNodesOfB = unionOfNodes(
nodeB.node.getPredecessorNodes(),
nodeB.node.getSuccessorNodes());
for (Node aNode : adjacentNodesOfA) {
ArrayList<Integer> alreadyCrossed = new ArrayList<>();
NodeWrapper aNodeWrapper = map.get(aNode);
for (int i = 0; i < adjacentNodesOfB.size(); i++) {
NodeWrapper nw = map.get(adjacentNodesOfB.get(i));
if (!alreadyCrossed.contains(i) && nw != null) {
// only if on the same side
if ((nw.layer > nodeA.layer
&& aNodeWrapper.layer > nodeA.layer)
|| (nw.layer < nodeA.layer
&& aNodeWrapper.layer < nodeA.layer)) {
if (nodeA.index < nodeB.index) {
if (aNodeWrapper.index > nw.index) {
numOfCrosses++;
alreadyCrossed.add(i);
} else if (nw.index == aNodeWrapper.index) {
if (nodeA.index >= nw.index) {
// implies nodeB.index > nw.index
if ((aNodeWrapper.layer > nw.layer
&& nodeA.layer < nw.layer)
|| (aNodeWrapper.layer < nw.layer
&& nw.layer < nodeA.layer)) {
// top-left or bottom-left quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
} else if (nodeB.index <= nw.index) {
// implies nodeA.index < nw.index
if ((aNodeWrapper.layer > nw.layer
&& aNodeWrapper.layer < nodeB.layer)
|| (aNodeWrapper.layer < nw.layer
&& aNodeWrapper.layer > nodeB.layer)) {
// top-right or bottom-right quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
}
}
} else if (nodeA.index > nodeB.index) {
if (aNodeWrapper.index < nw.index) {
numOfCrosses++;
alreadyCrossed.add(i);
} else if (nw.index == aNodeWrapper.index) {
if (nodeB.index >= nw.index) {
// implies nodeB.index > nw.index
if ((aNodeWrapper.layer > nw.layer
&& nodeB.layer > aNodeWrapper.layer)
|| (aNodeWrapper.layer < nw.layer
&& aNodeWrapper.layer > nodeB.layer)) {
// top-left or bottom-left quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
} else if (nodeA.index <= nw.index) {
// implies nodeA.index < nw.index
if ((aNodeWrapper.layer > nw.layer
&& nw.layer > nodeA.layer)
|| (aNodeWrapper.layer < nw.layer
&& nw.layer < nodeA.layer)) {
// top-right or bottom-right quarter
numOfCrosses++;
alreadyCrossed.add(i);
}
}
}
}
}
}
}
}
return numOfCrosses;
}
/**
* Iterates the list and switches that results in less crossings.
*
* @param layer
* @return
*/
private boolean greedyHeuristic(List<NodeWrapper> layer) {
boolean res = false;
if (layer.size() > 1) {
for (int i = 0; i < layer.size() - 1; i++) {
if (layer.get(i).node != null
&& layer.get(i + 1).node != null) {
int num1 = numberOfCrosses(layer.get(i),
layer.get(i + 1));
int num2 = numberOfCrosses(layer.get(i + 1),
layer.get(i));
if (num1 > num2 || (num1 == num2 && num1 > 0)) {
if (!crossesForLayers
.containsKey((layer.get(i).layer))
|| crossesForLayers
.get(layer.get(i).layer) > num2) {
crossesForLayers.put(layer.get(i).layer, num2);
res = true;
int level = layer.get(0).layer;
NodeWrapper tmp = layers.get(level).get(i);
int tmpindex = layers.get(level).get(i).index;
layers.get(level).get(i).index = layers
.get(level).get(i + 1).index;
layers.get(level).set(i,
layers.get(level).get(i + 1));
layers.get(level).get(i + 1).index = tmpindex;
layers.get(level).set(i + 1, tmp);
}
}
}
}
}
return res;
}
public void crossReduction(List<List<NodeWrapper>> nodes) {
crossesForLayers.clear();
layers = nodes;
// Builds the map
for (List<NodeWrapper> layer : nodes)
for (NodeWrapper node : layer)
map.put(node.node, node);
// After three iteration with no change it stops
int iteration = 0;
boolean change = false;
while (iteration < 3) {
change = false;
for (int i = 0; i < nodes.size(); i++) {
if (greedyHeuristic(layers.get(i))) {
change = true;
}
}
if (!change)
iteration++;
}
}
}
/**
* Structure to store nodes and their positions in the layers. Furthermore
* predecessors and successors can be assigned to the nodes.
*
* @author Adam Kovacs
*/
public static class NodeWrapper {
/**
* The index of this {@link NodeWrapper} (used to find crossings).
*/
int index;
/**
* The layer this {@link NodeWrapper} is in (used to find crossings).
*/
final int layer;
/**
* The wrapped {@link Node}.
*/
final Node node;
/**
* A {@link List} containing the predecessors of this
* {@link NodeWrapper}.
*/
final List<NodeWrapper> pred = new LinkedList<>();
/**
* A {@link List} containing the successors of this {@link NodeWrapper}.
*/
final List<NodeWrapper> succ = new LinkedList<>();
private static final int PADDING = -1;
/**
* Constructs a new {@link NodeWrapper} to wrap the given {@link Node}.
*
* @param n
* The {@link Node} that is wrapped.
* @param l
* The layer this {@link NodeWrapper} is on.
*/
NodeWrapper(Node n, int l) {
node = n;
layer = l;
} // NodeLayout wrapper
/**
* Constructs a new dummy {@link NodeWrapper} on the specified layer.
* Dummy nodes are used to make the hierarchy proper, i.e. it does only
* have edges between two consecutive layers.
*
* @param l
* The layer this {@link NodeWrapper} is on.
*/
NodeWrapper(int l) {
this(null, l);
} // Dummy to connect two NodeLayout objects
/**
* Constructs a new padding {@link NodeWrapper} (layer -1).
*/
NodeWrapper() {
this(null, PADDING);
} // Padding for final refinement phase
/**
* Adds the given {@link NodeWrapper} to the list of predecessors that
* is managed by this {@link NodeWrapper}.
*
* @param node
* The {@link NodeWrapper} that is added to the list of
* predecessors.
*/
void addPredecessor(NodeWrapper node) {
pred.add(node);
}
/**
* Adds the given {@link NodeWrapper} to the list of successors that is
* managed by this {@link NodeWrapper}.
*
* @param node
* The {@link NodeWrapper} that is added to the list of
* successors.
*/
void addSuccessor(NodeWrapper node) {
succ.add(node);
}
/**
* Returns <code>true</code> if this {@link NodeWrapper} is a dummy.
* Otherwise returns <code>false</code>.
*
* @return <code>true</code> if this {@link NodeWrapper} is a dummy,
* otherwise <code>false</code>.
*/
boolean isDummy() {
return ((node == null) && (layer != PADDING));
}
/**
* Returns <code>true</code> if this {@link NodeWrapper} is a padding
* node. Otherwise returns <code>false</code>.
*
* @return <code>true</code> if this {@link NodeWrapper} is a padding
* node, otherwise <code>false</code>.
*/
boolean isPadding() {
return ((node == null) && (layer == PADDING));
}
/**
* Computes the barycenter of the given list of {@link NodeWrapper}s.
*
* @param list
* The list of {@link NodeWrapper}s for which to compute the
* barycenter.
* @return The barycenter of the given {@link NodeWrapper}s.
*/
int getBaryCenter(List<NodeWrapper> list) {
if (list.isEmpty())
return (this.index);
if (list.size() == 1)
return (list.get(0).index);
double barycenter = 0;
for (NodeWrapper node : list)
barycenter += node.index;
// always round down to avoid wrap around in position refining
return ((int) (barycenter / list.size()));
}
/**
* Returns the down priority for this {@link NodeWrapper}:
* <ol>
* <li>Padding nodes: <code>0</code>
* <li>Dummy nodes: <code>Integer.MAX_VALUE >> 1</code>
* <li>Dummy nodes with dummy successor: <code>Integer.MAX_VALUE</code>
* <li>Otherwise: Number of predecessors.
* </ol>
*
* @return The down priority for this {@link NodeWrapper}.
*/
int getPriorityDown() {
if (isPadding())
return (0);
if (isDummy()) {
if (succ != null && succ.size() > 0) {
if (succ.get(0).isDummy())
// part of a straight line
return (Integer.MAX_VALUE);
else
// start of a straight line
return (Integer.MAX_VALUE >> 1);
}
}
return (pred.size());
}
/**
* Returns the up priority for this {@link NodeWrapper}:
* <ol>
* <li>Padding nodes: <code>0</code>
* <li>Dummy nodes: <code>Integer.MAX_VALUE >> 1</code>
* <li>Dummy nodes with dummy predecessor:
* <code>Integer.MAX_VALUE</code>
* <li>Otherwise: Number of successors.
* </ol>
*
* @return The up priority for this {@link NodeWrapper}.
*/
int getPriorityUp() {
if (isPadding())
return (0);
if (isDummy()) {
if (pred != null && pred.size() > 0) {
if (pred.get(0).isDummy())
// part of a straight line
return (Integer.MAX_VALUE);
else
// start of a straight line
return (Integer.MAX_VALUE >> 1);
}
}
return (succ.size());
}
}
/**
*
* An interface for creating layers. Interface for parameterizable layering
* heuristics.
*
* @author Adam Kovacs
*/
public static interface LayerProvider {
/**
* Creating layers of the nodes and makes it possible to assign layers
* to those nodes.
*
* @param nodes
* List of all the nodes that needs to be organized
* @return a list of layers for the given nodes, represented each as a
* list of {@link NodeWrapper}s
*/
List<List<NodeWrapper>> calculateLayers(List<Node> nodes);
}
/**
* Processing the nodes based on depth first search and creating a list of
* layers
*
* @author Adam Kovacs
*
*/
public static class DFSLayerProvider implements LayerProvider {
private Map<Node, Integer> assignedNodes = new IdentityHashMap<>();
/**
* Returns the mutual connections of the two array given as parameters.
*
* @param a
* @param b
* @return
*/
private List<Edge> intersectOfConnections(Collection<Edge> a,
Collection<Edge> b) {
ArrayList<Edge> res = new ArrayList<>();
for (Edge e : a) {
if (b.contains(e)) {
res.add(e);
}
}
return res;
}
private void addToInitClosedList(Node node, int layout,
List<Node> initClosedList, Map<Node, NodeWrapper> map) {
NodeWrapper nw = new NodeWrapper(node, layout);
map.put(node, nw);
initClosedList.add(node);
}
/**
* Finds the root elements in the list of nodes based on their
* connections.
*
* @param nodes
* The list of {@link Node}s for which to find the root
* elements.
* @return the list of root elements
*/
public ArrayList<Node> getRoots(List<Node> nodes) {
ArrayList<Node> res = new ArrayList<>();
for (Node node : nodes) {
// directed edges
if (node.getIncomingEdges().size() == 0)
res.add(node);
else {
int sizeOfIntersect = intersectOfConnections(
node.getIncomingEdges(), node.getOutgoingEdges())
.size();
// there are more outgoing edges, besides the bidirectionals
if (node.getOutgoingEdges().size() > sizeOfIntersect)
res.add(node);
// only bidirectional edges, no incoming directed edges
if (node.getIncomingEdges().size() == sizeOfIntersect
&& node.getOutgoingEdges()
.size() == sizeOfIntersect)
res.add(node);
}
}
// if no sources then we only have bidirectional edges and/or cycles
if (res.size() == 0)
res.add(nodes.get(0));
return res;
}
/**
* Returns a {@link Map} that stores the assignment of layers to
* {@link Node}s.
*
* @return A {@link Map} that stores the assignment of layers to
* {@link Node}s.
*/
public Map<Node, Integer> getAssignedNodes() {
return assignedNodes;
}
/**
* Assigns the given layer to the given {@link Node}.
*
* @param node
* The {@link Node} to which a layer is assigned.
* @param layer
* The layer that is assigned to that {@link Node}.
*/
public void addAssignedNode(Node node, int layer) {
assignedNodes.put(node, layer);
}
/**
* Clears the {@link Map} that stores the layer assignments.
*/
public void clearAssignedNodes() {
assignedNodes.clear();
}
private static void updateIndex(List<NodeWrapper> list) {
for (int index = 0; index < list.size(); index++)
list.get(index).index = index;
}
/**
* Creates a new layer and puts the elements of this layer to the map.
*
* @param list
*/
private void addLayer(List<Node> list, List<List<NodeWrapper>> layers,
Map<Node, NodeWrapper> map) {
ArrayList<NodeWrapper> layer = new ArrayList<>(list.size());
for (Node node : list) {
// wrap each NodeLayout with the internal data object and
// provide a corresponding mapping
NodeWrapper nw = new NodeWrapper(node, layers.size());
map.put(node, nw);
layer.add(nw);
}
layers.add(layer);
updateIndex(layer);
}
/**
* Finds the connected nodes to be processed.
*
* @param toUnfold
* @return
*/
private ArrayList<Node> Unfold(Node toUnfold, Set<Node> openedList,
Set<Node> closedList) {
ArrayList<Node> res = new ArrayList<>();
for (Edge e : toUnfold.getOutgoingEdges()) {
Node endPoint = e.getTarget();
if (endPoint.equals(toUnfold))
endPoint = e.getSource();
if (!closedList.contains(endPoint)
&& !openedList.contains(endPoint)
&& !res.contains(endPoint))
res.add(endPoint);
}
for (Edge e : toUnfold.getIncomingEdges()) {
Node endPoint = e.getTarget();
if (endPoint.equals(toUnfold))
endPoint = e.getSource();
if (!closedList.contains(endPoint)
&& !openedList.contains(endPoint)
&& !res.contains(endPoint))
res.add(endPoint);
}
return res;
}
public List<List<NodeWrapper>> calculateLayers(List<Node> nodeLayouts) {
List<Node> nodes = new ArrayList<>(nodeLayouts);
Set<Node> openedList = new HashSet<>();
List<Node> initClosedList = new ArrayList<>();
Set<Node> closedList = new HashSet<>();
List<List<NodeWrapper>> layers = new ArrayList<>();
Map<Node, NodeWrapper> map = new IdentityHashMap<>();
// Assigns the given nodes to there layers
if (assignedNodes.size() > 0) {
for (Node node : nodes) {
if (assignedNodes.containsKey(node))
addToInitClosedList(node, assignedNodes.get(node),
initClosedList, map);
}
}
// Only at first iteration, clearing initClosedList, starting to
// build layers
if (initClosedList.size() > 0) {
closedList.addAll(initClosedList);
nodes.removeAll(initClosedList);
initClosedList.clear();
for (Node node : closedList) {
if (map.get(node).layer < layers.size()) {
layers.get(map.get(node).layer).add(map.get(node));
updateIndex(layers.get(map.get(node).layer));
} else {
while (map.get(node).layer != layers.size()) {
ArrayList<Node> layer = new ArrayList<>();
addLayer(layer, layers, map);
}
ArrayList<Node> layer = new ArrayList<>();
layer.add(node);
addLayer(layer, layers, map);
}
}
}
ArrayList<Node> startPoints = new ArrayList<>();
// Starts by finding a root or selecting the first from the assigned
// ones
if (layers.size() > 0 && layers.get(0).size() > 0)
startPoints.add(layers.get(0).get(0).node);
else if (layers.size() == 0) {
startPoints.add(getRoots(nodes).get(0));
addLayer(startPoints, layers, map);
} else {
startPoints.add(getRoots(nodes).get(0));
for (Node startPoint : startPoints) {
if (!map.containsKey(startPoint)) {
NodeWrapper nw = new NodeWrapper(startPoint, 0);
map.put(startPoint, nw);
layers.get(0).add(nw);
}
}
updateIndex(layers.get(0));
}
openedList.addAll(startPoints);
Node toUnfold = startPoints.get(0);
while (nodes.size() > 0) {
// while openedList isn't empty it searches for further nodes
// and adding them to the next layer
while (openedList.size() != 0) {
ArrayList<Node> unfolded = Unfold(toUnfold, openedList,
closedList);
if (unfolded.size() > 0) {
int level = map.get(toUnfold).layer + 1;
if (level < layers.size()) {
for (Node n : unfolded) {
if (!map.containsKey(n)) {
NodeWrapper nw = new NodeWrapper(n, level);
map.put(n, nw);
layers.get(level).add(nw);
}
}
updateIndex(layers.get(level));
} else {
ArrayList<Node> layer = new ArrayList<>();
layer.addAll(unfolded);
addLayer(layer, layers, map);
}
openedList.addAll(unfolded);
}
closedList.add(toUnfold);
openedList.remove(toUnfold);
nodes.remove(toUnfold);
if (openedList.size() != 0)
toUnfold = openedList.iterator().next();
}
if (nodes.size() > 0) {
final Node node = nodes.get(0);
openedList.add(node);
NodeWrapper nw = new NodeWrapper(node, 0);
map.put(node, nw);
layers.get(0).add(nw);
}
}
return layers;
}
}
/**
*
* @author Rene Kuhlemann
*
*/
public static class SimpleLayerProvider implements LayerProvider {
private static final int MAX_LAYERS = 10;
private final List<List<NodeWrapper>> layers = new ArrayList<>(
MAX_LAYERS);
private final Map<Node, NodeWrapper> map = new IdentityHashMap<>();
private static List<Node> findRoots(List<Node> list) {
List<Node> roots = new ArrayList<>();
for (Node iter : list) {
// no predecessors means: this is a root, add it to list
if (iter.getPredecessorNodes().size() == 0)
roots.add(iter);
}
return (roots);
}
/**
* Wraps all {@link Node} objects into an internal presentation
* {@link NodeWrapper} and inserts dummy wrappers into the layers
* between an object and their predecessing nodes if necessary. Finally,
* all nodes are chained over immediate adjacent layers down to their
* predecessors. This is necessary to apply the final step of the
* Sugiyama algorithm to refine the node position within a layer.
*
* @param list
* : List of all {@link Node} objects within the current
* layer
*/
private void addLayer(List<Node> list) {
ArrayList<NodeWrapper> layer = new ArrayList<>(list.size());
for (Node node : list) {
// wrap each NodeLayout with the internal data object and
// provide a corresponding mapping
NodeWrapper nw = new NodeWrapper(node, layers.size());
map.put(node, nw);
layer.add(nw);
// insert dummy nodes if the adjacent layer does not contain the
// predecessor
for (Node node_predecessor : node.getPredecessorNodes()) {
NodeWrapper nw_predecessor = map.get(node_predecessor);
if (nw_predecessor != null) {
for (int level = nw_predecessor.layer
+ 1; level < nw.layer; level++) {
// add "virtual" wrappers (dummies) to the layers in
// between
// virtual wrappers are in fact parts of a double
// linked list
NodeWrapper nw_dummy = new NodeWrapper(level);
nw_dummy.addPredecessor(nw_predecessor);
nw_predecessor.addSuccessor(nw_dummy);
nw_predecessor = nw_dummy;
layers.get(level).add(nw_dummy);
}
nw.addPredecessor(nw_predecessor);
nw_predecessor.addSuccessor(nw);
}
}
}
layers.add(layer);
updateIndex(layer);
}
private static void updateIndex(List<NodeWrapper> list) {
for (int index = 0; index < list.size(); index++)
list.get(index).index = index;
}
public List<List<NodeWrapper>> calculateLayers(List<Node> nodes) {
map.clear();
List<Node> predecessors = findRoots(nodes);
nodes.removeAll(predecessors);
// nodes now contains only nodes that are no roots
addLayer(predecessors);
for (int level = 1; nodes.isEmpty() == false; level++) {
if (level > MAX_LAYERS)
throw new RuntimeException(
"Graphical tree exceeds maximum depth of "
+ MAX_LAYERS
+ "! (Graph not directed? Cycles?)");
List<Node> layer = new ArrayList<>();
for (Node item : nodes) {
if (predecessors.containsAll(item.getPredecessorNodes()))
layer.add(item);
}
if (layer.size() == 0)
layer.add(nodes.get(0));
nodes.removeAll(layer);
predecessors.addAll(layer);
addLayer(layer);
}
return layers;
}
}
private List<List<NodeWrapper>> layers = new ArrayList<>();
private Map<Node, NodeWrapper> map = new IdentityHashMap<>();
private final Direction direction;
private final Dimension dimension;
// index of the last element in a layer after padding process
private int last;
private LayerProvider layerProvider;
private CrossingReducer crossingReducer;
/**
* Constructs a tree-like, layered layout of a directed graph.
*
* @param dir
* {@link Direction#HORIZONTAL}: left to right -
* {@link Direction#VERTICAL} : top to bottom
*
* @param dim
* - desired size of the layout area. Uses the BOUNDS_PROPERTY of
* the LayoutContext if not set
*
* @param layering
* - implementation of LayerProvider interface
*
* @param crossing
* - implementation of CrossingReducer interface
*/
public SugiyamaLayoutAlgorithm(Direction dir, Dimension dim,
LayerProvider layering, CrossingReducer crossing) {
direction = dir;
dimension = dim;
layerProvider = (layering == null) ? new SimpleLayerProvider()
: layering;
crossingReducer = (crossing == null) ? new BarycentricCrossingReducer()
: crossing;
}
/**
* Constructs a new {@link SugiyamaLayoutAlgorithm} with the given
* {@link Direction}, {@link LayerProvider}, and {@link CrossingReducer}.
*
* @param dir
* The {@link Direction} for this {@link SugiyamaLayoutAlgorithm}
* .
* @param layerProvider
* The LayerProvider for this {@link SugiyamaLayoutAlgorithm}.
* @param crossing
* The CrossingReducer for this {@link SugiyamaLayoutAlgorithm}.
*/
public SugiyamaLayoutAlgorithm(Direction dir, LayerProvider layerProvider,
CrossingReducer crossing) {
this(dir, null, layerProvider, crossing);
}
/**
* Constructs a new {@link SugiyamaLayoutAlgorithm} with the given
* {@link Direction}, {@link LayerProvider}, and a
* {@link BarycentricCrossingReducer}.
*
* @param dir
* The {@link Direction} for this {@link SugiyamaLayoutAlgorithm}
* .
* @param layerProvider
* The LayerProvider for this {@link SugiyamaLayoutAlgorithm}.
*/
public SugiyamaLayoutAlgorithm(Direction dir, LayerProvider layerProvider) {
this(dir, null, layerProvider, new BarycentricCrossingReducer());
}
/**
* Constructs a new {@link SugiyamaLayoutAlgorithm} with the given
* {@link Direction}, and {@link CrossingReducer}.
*
* @param dir
* The {@link Direction} for this {@link SugiyamaLayoutAlgorithm}
* .
* @param crossing
* The CrossingReducer for this {@link SugiyamaLayoutAlgorithm}.
*/
public SugiyamaLayoutAlgorithm(Direction dir, CrossingReducer crossing) {
this(dir, null, null, crossing);
}
/**
* Constructs a new {@link SugiyamaLayoutAlgorithm} with the given
* {@link Direction}, and the given dimension.
*
* @param dir
* The {@link Direction} for this {@link SugiyamaLayoutAlgorithm}
* .
* @param dim
* The desired size of the layout area. Uses the BOUNDS_PROPERTY
* of the LayoutContext if not set.
*/
public SugiyamaLayoutAlgorithm(Direction dir, Dimension dim) {
this(dir, dim, null, null);
}
/**
* Constructs a new {@link SugiyamaLayoutAlgorithm} with the given
* {@link Direction}.
*
* @param dir
* The {@link Direction} for this {@link SugiyamaLayoutAlgorithm}
* .
*/
public SugiyamaLayoutAlgorithm(Direction dir) {
this(dir, null, null, null);
}
/**
* Constructs a new {@link SugiyamaLayoutAlgorithm} with
* {@link Direction#VERTICAL} direction.
*/
public SugiyamaLayoutAlgorithm() {
this(Direction.VERTICAL, null, null, null);
}
public void applyLayout(LayoutContext layoutContext, boolean clean) {
if (!clean)
return;
layers.clear();
map.clear();
ArrayList<Node> nodes = new ArrayList<>();
ArrayList<Node> nodes2 = new ArrayList<>();
for (Node node : layoutContext.getNodes()) {
nodes.add(node);
nodes2.add(node);
}
layers = layerProvider.calculateLayers(nodes);
crossingReducer.crossReduction(layers);
for (List<NodeWrapper> layer : layers) {
if (layer.size() > last)
last = layer.size();
for (NodeWrapper nw : layer) {
map.put(nw.node, nw);
}
}
calculatePositions(layoutContext);
}
private void calculatePositions(LayoutContext context) {
Rectangle boundary = LayoutProperties.getBounds(context.getGraph());
if (dimension != null)
boundary = new Rectangle(0, 0, dimension.getWidth(),
dimension.getHeight());
double dx = boundary.getWidth() / layers.size();
double dy = boundary.getHeight() / (last + 1);
if (direction == Direction.HORIZONTAL)
for (Node node : context.getNodes()) {
NodeWrapper nw = map.get(node);
LayoutProperties.setLocation(node, new Point(
(nw.layer + 0.5d) * dx, (nw.index + 0.5d) * dy));
}
else
for (Node node : context.getNodes()) {
NodeWrapper nw = map.get(node);
LayoutProperties.setLocation(node, new Point(
(nw.index + 0.5d) * dx, (nw.layer + 0.5d) * dy));
}
}
}