/* * Copyright 2012 Odysseus Software GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.odysseus.ithaka.digraph.layout.sugiyama; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import de.odysseus.ithaka.digraph.Digraph; import de.odysseus.ithaka.digraph.Digraphs; import de.odysseus.ithaka.digraph.DoubledDigraph; import de.odysseus.ithaka.digraph.EdgeWeights; import de.odysseus.ithaka.digraph.layout.DigraphLayout; import de.odysseus.ithaka.digraph.layout.DigraphLayoutArc; import de.odysseus.ithaka.digraph.layout.DigrpahLayoutBuilder; import de.odysseus.ithaka.digraph.layout.DigraphLayoutDimension; import de.odysseus.ithaka.digraph.layout.DigraphLayoutDimensionProvider; import de.odysseus.ithaka.digraph.layout.DigraphLayoutNode; import de.odysseus.ithaka.digraph.layout.DigraphLayoutPoint; import de.odysseus.ithaka.digraph.util.fas.FeedbackArcSet; import de.odysseus.ithaka.digraph.util.fas.FeedbackArcSetPolicy; import de.odysseus.ithaka.digraph.util.fas.SimpleFeedbackArcSetProvider; /** * Sugiyama's algorithm. Work in progress... * * @author Christoph Beck * * @param <V> Vertex type * @param <E> Edge type */ public class SugiyamaBuilder<V, E> implements DigrpahLayoutBuilder<V, E> { protected final int horizontalSpacing; protected final int verticalSpacing; protected final boolean transpose; public SugiyamaBuilder(int horizontalSpacing, int verticalSpacing) { this(horizontalSpacing, verticalSpacing, false); } public SugiyamaBuilder(int horizontalSpacing, int verticalSpacing, boolean transpose) { this.horizontalSpacing = horizontalSpacing; this.verticalSpacing = verticalSpacing; this.transpose = transpose; } protected List<List<SugiyamaNode<V>>> createLayers(Digraph<SugiyamaNode<V>,?> graph) { List<List<SugiyamaNode<V>>> layers = new ArrayList<List<SugiyamaNode<V>>>(); for (SugiyamaNode<V> node : graph.vertices()) { for (int i = layers.size(); i <= node.getLayer(); i++) { layers.add(new ArrayList<SugiyamaNode<V>>()); } layers.get(node.getLayer()).add(node); } return layers; } protected DigraphLayoutDimension computeLayoutDimension(List<List<SugiyamaNode<V>>> layers) { int maxX = 0; for (List<SugiyamaNode<V>> layer : layers) { SugiyamaNode<V> last = layer.get(layer.size()-1); maxX = Math.max(maxX, last.getPoint().x + last.getDimension().w); } int maxY = 0; for (SugiyamaNode<V> node : layers.get(layers.size()-1)) { maxY = Math.max(maxY, node.getPoint().y + node.getDimension().h); } return new DigraphLayoutDimension(maxX, maxY); } private void computeNodePoints(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph, List<List<SugiyamaNode<V>>> layers) { int minPosition = 0; // let the leftmost vertex(s) have position 0 for (List<SugiyamaNode<V>> layer : layers) { minPosition = Math.min(minPosition, (int)layer.get(0).getPosition()); } int levelY = -verticalSpacing; for (List<SugiyamaNode<V>> layer : layers) { int levelHeight = 0; levelY += verticalSpacing; for (SugiyamaNode<V> node : layer) { node.setPoint(new DigraphLayoutPoint((int)node.getPosition() - minPosition - node.getDimension().w / 2, levelY)); levelHeight = Math.max(levelHeight, node.getDimension().h); } levelY += levelHeight; levelY += computeExtraVerticalSpacing(graph, layer); } } private int computeExtraVerticalSpacing(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph, List<SugiyamaNode<V>> layer) { double maxHorizontalDistance = 0; for (SugiyamaNode<V> source : layer) { for (SugiyamaNode<V> target : graph.targets(source)) { maxHorizontalDistance = Math.max(maxHorizontalDistance, Math.abs(target.getPosition() - source.getPosition())); } } int maxExtraSpace = verticalSpacing / 2; int maxAcceptableAngle = 82; // magic double maxAcceptableTangens = Math.tan((2 * Math.PI * maxAcceptableAngle) / 360); if (maxHorizontalDistance / verticalSpacing <= maxAcceptableTangens) { return 0; } int extraSpace = Math.min(maxExtraSpace, (int)(maxHorizontalDistance / maxAcceptableTangens) - verticalSpacing ); // System.out.println("maxAngle = " + Math.atan(maxHorizontalDistance / verticalSpacing) * 360 / (2 * Math.PI) + ", extraSpace = " + extraSpace + ", newMaxAngle = " + Math.atan(maxHorizontalDistance / (verticalSpacing + extraSpace)) * 360 / (2 * Math.PI)); return Math.min(maxExtraSpace, extraSpace); } /** * Split long arc into segment with intermediate dummy nodes * @param graph graph * @param arc long arc * @param zero dimension used for dummy nodes */ private void insertSegment(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph, SugiyamaArc<V,E> arc, DigraphLayoutDimension zero) { assert arc.getTarget().getLayer() > arc.getSource().getLayer() + 1; SugiyamaNode<V> source = arc.getSource(); SugiyamaNode<V> target = arc.getTarget(); SugiyamaNode<V> lower = new SugiyamaNode<V>(zero); lower.setLayer(source.getLayer() + 1); lower.setUpper(source); graph.put(source, lower, new SugiyamaArc<V,E>(source, lower, arc.isFeedback(), arc.getEdge())); for (int layer = source.getLayer() + 2; layer < target.getLayer(); layer++) { SugiyamaNode<V> upper = lower; lower = new SugiyamaNode<V>(zero); lower.setLayer(layer); lower.setUpper(upper); upper.setLower(lower); graph.put(upper, lower, new SugiyamaArc<V,E>(upper, lower, arc.isFeedback(), arc.getEdge())); } lower.setLower(target); graph.put(lower, target, new SugiyamaArc<V,E>(lower, target, arc.isFeedback(), arc.getEdge())); } /** * Split long arcs by inserting dummy nodes */ protected void insertDummyNodes(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph, DigraphLayoutDimensionProvider<V> dimensions) { List<SugiyamaArc<V,E>> longArcs = new LinkedList<SugiyamaArc<V,E>>(); for (SugiyamaNode<V> source : graph.vertices()) { for (SugiyamaNode<V> target : graph.targets(source)) { if (target.getLayer() - source.getLayer() > 1) { longArcs.add(graph.get(source, target)); } } } DigraphLayoutDimension zero = dimensions.getDimension(null); for (SugiyamaArc<V,E> arc : longArcs) { insertSegment(graph, arc, zero); if (arc.getBackArc() != null) { insertSegment(graph, arc.getBackArc(), zero); } graph.remove(arc.getSource(), arc.getTarget()); } } protected void removeDummyNodes(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph) { Set<SugiyamaNode<V>> dummies = new HashSet<SugiyamaNode<V>>(); List<SugiyamaArc<V,E>> newArcs = new LinkedList<SugiyamaArc<V,E>>(); List<SugiyamaArc<V,E>> oldArcs = new LinkedList<SugiyamaArc<V,E>>(); for (SugiyamaNode<V> source : graph.vertices()) { if (source.isDummy()) { dummies.add(source); } else { for (SugiyamaNode<V> target : graph.targets(source)) { if (target.isDummy()) { List<DigraphLayoutPoint> points = graph.get(source, target).getBendPoints(); SugiyamaNode<V> current = target; SugiyamaNode<V> previous = null; do { if (current.getUpper().getPosition() != current.getPosition() || current.getLower().getPosition() != current.getPosition()) { if (points.isEmpty()) { points = new LinkedList<DigraphLayoutPoint>(); } points.add(current.getPoint()); List<DigraphLayoutPoint> additionalBends = graph.get(current, current.getLower()).getBendPoints(); if (additionalBends != null) { points.addAll(additionalBends); } } previous = current; current = current.getLower(); } while (current.isDummy()); SugiyamaArc<V,E> firstArc = graph.get(source, target); SugiyamaArc<V,E> lastArc = graph.get(previous, current); SugiyamaArc<V,E> newArc = null; if (firstArc.isFeedback()) { newArc = new SugiyamaArc<V,E>(current, source, true, firstArc.getEdge()); newArc.setSourceSlot(lastArc.getTargetSlot()); newArc.setTargetSlot(firstArc.getSourceSlot()); if (!points.isEmpty()) { Collections.reverse(points); } newArc.setBendPoints(points); } else { newArc = new SugiyamaArc<V,E>(source, current, false, firstArc.getEdge()); newArc.setSourceSlot(firstArc.getSourceSlot()); newArc.setTargetSlot(lastArc.getTargetSlot()); newArc.setBendPoints(points); } newArcs.add(newArc); } else { SugiyamaArc<V,E> arc = graph.get(source, target); if (arc.isFeedback()) { SugiyamaArc<V,E> newArc = new SugiyamaArc<V,E>(target, source, true, arc.getEdge()); newArc.setSourceSlot(arc.getTargetSlot()); newArc.setTargetSlot(arc.getSourceSlot()); newArc.setBendPoints(arc.getBendPoints()); newArcs.add(newArc); oldArcs.add(arc); } if (arc.getBackArc() != null) { newArcs.add(arc.getBackArc()); } } } } } graph.removeAll(dummies); for (SugiyamaArc<V,E> arc : oldArcs) { graph.remove(arc.getSource(), arc.getTarget()); } for (SugiyamaArc<V,E> arc : newArcs) { graph.put(arc.getSource(), arc.getTarget(), arc); } } private void fixEndPoints(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph) { for (SugiyamaNode<V> source : graph.vertices()) { for (SugiyamaNode<V> target : graph.targets(source)) { graph.get(source, target).fixEndPoints(); } } } /** * add bend point to feedback arc (v,w) if (w,v) is also present. */ protected void finalizeLayout(Digraph<SugiyamaNode<V>, SugiyamaArc<V, E>> graph) { int minHorizontalDistance = Math.min(transpose ? 12 : 16, horizontalSpacing); int minVerticalDistance = 0; // vertical displacement currently not used for (SugiyamaNode<V> source : graph.vertices()) { for (SugiyamaNode<V> target : graph.targets(source)) { SugiyamaArc<V, E> arc1 = graph.get(source, target); if (arc1.isFeedback() && graph.contains(target, source) && arc1.getBendPoints().isEmpty()) { SugiyamaArc<V, E> arc2 = graph.get(target, source); if (arc2.getBendPoints().isEmpty()) { boolean bend = false; // horizontal displacement int x1 = (arc1.getStartPoint().x + arc1.getEndPoint().x) / 2; int x2 = (arc2.getStartPoint().x + arc2.getEndPoint().x) / 2; int horizontalDistance = Math.abs(x2 - x1); if (horizontalDistance < minHorizontalDistance) { int delta = minHorizontalDistance - horizontalDistance + 1; x1 = arc1.getStartPoint().x <= arc2.getEndPoint().x ? x1 - delta/2 : x1 + delta/2; x2 = arc1.getStartPoint().x <= arc2.getEndPoint().x ? x2 + delta/2 : x2 - delta/2; bend = true; } // vertical displacement int y1 = (arc1.getStartPoint().y + arc1.getEndPoint().y) / 2; int y2 = (arc2.getStartPoint().y + arc2.getEndPoint().y) / 2; int verticalDistance = Math.abs(y2 - y1); if (verticalDistance < minVerticalDistance) { int delta = minVerticalDistance - verticalDistance + 1; y1 = arc1.getStartPoint().x <= arc1.getEndPoint().x ? y1 - delta/2 : y1 + delta/2; y2 = arc2.getEndPoint().x <= arc2.getStartPoint().x ? y2 + delta/2 : y2 - delta/2; bend = true; } if (bend) { // add bend points List<DigraphLayoutPoint> points = null; points = new LinkedList<DigraphLayoutPoint>(); points.add(new DigraphLayoutPoint(x1, y1)); arc1.setBendPoints(points); points = new LinkedList<DigraphLayoutPoint>(); points.add(new DigraphLayoutPoint(x2, y2)); arc2.setBendPoints(points); } } } } } } protected DoubledDigraph<SugiyamaNode<V>,SugiyamaArc<V,E>> createLayoutGraph(Digraph<V,E> graph, DigraphLayoutDimensionProvider<V> dimensions, Digraph<V,?> feedback) { return new SugiyamaStep1<V,E>().createLayoutGraph(graph, dimensions, feedback, horizontalSpacing); } protected void minimizeCrossings(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph, List<List<SugiyamaNode<V>>> layers) { new SugiyamaStep2<V,E>().minimizeCrossings(graph, layers); } protected void adjustNodePositions(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph, List<List<SugiyamaNode<V>>> layers) { new SugiyamaStep3<V>(horizontalSpacing).adjustNodePositions(graph, layers); } protected void routeArcs(Digraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph, List<List<SugiyamaNode<V>>> layers) { new SugiyamaStep4<V,E>().routeArcs(graph, layers); } @Override public DigraphLayout<V,E> build(Digraph<V,E> digraph, DigraphLayoutDimensionProvider<V> dimensions) { FeedbackArcSet<V,E> feedback = new SimpleFeedbackArcSetProvider().getFeedbackArcSet(digraph, EdgeWeights.UNIT_WEIGHTS, FeedbackArcSetPolicy.MIN_WEIGHT); return layout(digraph, dimensions, feedback); } public DigraphLayout<V,E> layout(Digraph<V,E> digraph, DigraphLayoutDimensionProvider<V> dimensions, Digraph<V,?> feedback) { if (digraph.getVertexCount() == 0) { return new DigraphLayout<V,E>(Digraphs.<DigraphLayoutNode<V>,DigraphLayoutArc<V,E>>emptyDigraph(), new DigraphLayoutDimension(0, 0)); } DoubledDigraph<SugiyamaNode<V>,SugiyamaArc<V,E>> graph = createLayoutGraph(digraph, dimensions, feedback); insertDummyNodes(graph, dimensions); if (transpose) { for (SugiyamaNode<V> source : graph.vertices()) { source.getDimension().transpose(); } } List<List<SugiyamaNode<V>>> layers = createLayers(graph); minimizeCrossings(graph, layers); adjustNodePositions(graph, layers); computeNodePoints(graph, layers); DigraphLayoutDimension dimension = computeLayoutDimension(layers); routeArcs(graph, layers); removeDummyNodes(graph); fixEndPoints(graph); finalizeLayout(graph); if (transpose) { for (SugiyamaNode<V> source : graph.vertices()) { source.getPoint().transpose(); source.getDimension().transpose(); for (SugiyamaNode<V> target : graph.targets(source)) { SugiyamaArc<V, E> arc = graph.get(source, target); arc.getStartPoint().transpose(); arc.getEndPoint().transpose(); for (DigraphLayoutPoint point : arc.getBendPoints()) { point.transpose(); } } } dimension.transpose(); } return new DigraphLayout<V,E>(graph, dimension); } }