/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package edu.mit.csail.sdg.alloy4graph; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.awt.Color; import java.awt.geom.Line2D; import java.awt.geom.RoundRectangle2D; import edu.mit.csail.sdg.alloy4.Pair; import edu.mit.csail.sdg.alloy4.Util; import static edu.mit.csail.sdg.alloy4graph.Artist.getBounds; /** Mutable; represents a graph. * * <p><b>Thread Safety:</b> Can be called only by the AWT event thread. */ public final strictfp class Graph { //================================ adjustable options ========================================================================// /** Minimum horizontal distance between adjacent nodes. */ static final int xJump = 30; /** Minimum vertical distance between adjacent layers. */ static final int yJump = 60; /** The horizontal distance between the first self-loop and the node itself. */ static final int selfLoopA = 40; /** The horizontal padding to put on the left side of a self-loop's edge label. */ static final int selfLoopGL = 2; /** The horizontal padding to put on the right side of a self-loop's edge label. */ static final int selfLoopGR = 20; /** The maximum ascent and descent. We deliberately do NOT make this field "static" because only AWT thread can call Artist. */ private final int ad = Artist.getMaxAscentAndDescent(); //=============================== fields ======================================================================================// /** The default magnification. */ final double defaultScale; /** The left edge. */ private int left = 0; /** The top edge. */ private int top = 0; /** The bottom edge. */ private int bottom = 0; /** The total width of the graph; this value is computed by layout(). */ private int totalWidth = 0; /** The total height of the graph; this value is computed by layout(). */ private int totalHeight = 0; /** The height of each layer. */ int[] layerPH = null; /** The list of layers; must stay in sync with GraphNode.graph and GraphNode.layer * (empty iff there are no nodes; every node is always in exactly one layer, and appears exactly once in that layer) */ final List<List<GraphNode>> layerlist = new ArrayList<List<GraphNode>>(); /** The list of nodes; must stay in sync with GraphNode.graph and GraphNode.pos * (every node is in exactly one graph's nodelist, and appears exactly once in that graph's nodelist) */ final List<GraphNode> nodelist = new ArrayList<GraphNode>(); /** The list of edges; must stay in sync with GraphEdge.a.graph and GraphEdge.b.graph * (every edge is in exactly one graph's edgelist, and appears exactly once in that graph's edgelist) */ final List<GraphEdge> edgelist = new ArrayList<GraphEdge>(); /** An unmodifiable view of the list of nodes. */ public final List<GraphNode> nodes = Collections.unmodifiableList(nodelist); /** An unmodifiable view of the list of edges. */ public final List<GraphEdge> edges = Collections.unmodifiableList(edgelist); /** An unmodifiable empty list. */ private final List<GraphNode> emptyListOfNodes = Collections.unmodifiableList(new ArrayList<GraphNode>(0)); //============================================================================================================================// /** Constructs an empty Graph object. */ public Graph(double defaultScale) { this.defaultScale = defaultScale; } /** Assuming layout() has been called, this returns the left edge. */ public int getLeft() { return left; } /** Assuming layout() has been called, this returns the top edge. */ public int getTop() { return top; } /** Assuming layout() has been called, this returns the total width. */ public int getTotalWidth() { return totalWidth; } /** Assuming layout() has been called, this returns the total height. */ public int getTotalHeight() { return totalHeight; } /** Returns an unmodifiable view of the list of nodes in the given layer (0..#layer-1); return an empty list if no such layer. */ List<GraphNode> layer(int i) { if (i>=0 && i<layerlist.size()) return Collections.unmodifiableList(layerlist.get(i)); return emptyListOfNodes; } /** Return the number of layers; can be 0. */ int layers() { return layerlist.size(); } /** Swap the given two nodes in the giver layer. */ void swapNodes(int layer, int node1, int node2) { List<GraphNode> list = layerlist.get(layer); GraphNode n1 = list.get(node1), n2 = list.get(node2); list.set(node1, n2); list.set(node2, n1); } /** Sort the list of nodes according to the order in the given list. */ void sortNodes(Iterable<GraphNode> newOrder) { // The nodes that are common to this.nodelist and newOrder are moved to the front of the list, in the given order. // The nodes that are in this.nodelist but not in newOrder are moved to the back in an unspecified order. // The nodes that are in newOrder but not in this.nodelist are ignored. int i=0, n=nodelist.size(); again: for(GraphNode x:newOrder) for(int j=i; j<n; j++) if (nodelist.get(j)==x) { if (i!=j) { GraphNode tmp=nodelist.get(i); nodelist.set(i,x); nodelist.set(j,tmp); } i++; continue again; } i=0; for(GraphNode x: nodelist) { x.pos = i; i++; } } /** Sort the list of nodes in a given layer (0..#layer-1) using the given comparator. */ void sortLayer(int layer, Comparator<GraphNode> comparator) { Collections.sort(layerlist.get(layer), comparator); } /** A list of legends; each legend is an Object with the associated text label and color. */ private final SortedMap<Comparable<?>,Pair<String,Color>> legends = new TreeMap<Comparable<?>,Pair<String,Color>>(); /** Add a legend with the given object and the associated text label; if color==null, that means we will * still add this legend into the list of legends, but this legend will be hidden. */ public void addLegend(Comparable<?> object, String label, Color color) { legends.put(object, new Pair<String,Color>(label,color)); } //============================================================================================================================// /** Layout step #1: assign a total order on the nodes. */ private void layout_assignOrder() { // This is an implementation of the GR algorithm described by Peter Eades, Xuemin Lin, and William F. Smyth // in "A Fast & Effective Heuristic for the Feedback Arc Set Problem" // in Information Processing Letters, Volume 47, Number 6, Pages 319-323, 1993 final int num = nodes.size(); if ((Integer.MAX_VALUE-1)/2 < num) throw new OutOfMemoryError(); // Now, allocate 2n+1 bins labeled -n .. n // Note: inside this method, whenever we see #in and #out, we ignore repeated edges. // Note: since Java ArrayList always start at 0, we'll index it by adding "n" to it. final List<List<GraphNode>> bins = new ArrayList<List<GraphNode>>(2*num+1); for(int i=0; i<2*num+1; i++) bins.add(new LinkedList<GraphNode>()); // For each N, figure out its in-neighbors and out-neighbors, then put it in the correct bin ArrayList<LinkedList<GraphNode>> grIN=new ArrayList<LinkedList<GraphNode>>(num); ArrayList<LinkedList<GraphNode>> grOUT=new ArrayList<LinkedList<GraphNode>>(num); int[] grBIN=new int[num]; for(GraphNode n: nodes) { int ni = n.pos(); LinkedList<GraphNode> in=new LinkedList<GraphNode>(), out=new LinkedList<GraphNode>(); for(GraphEdge e: n.ins) { GraphNode a = e.a(); if (!in.contains(a)) in.add(a); } for(GraphEdge e: n.outs) { GraphNode b = e.b(); if (!out.contains(b)) out.add(b); } grIN.add(in); grOUT.add(out); grBIN[ni] = (out.size()==0) ? 0 : (in.size()==0 ? (2*num) : (out.size()-in.size()+num)); bins.get(grBIN[ni]).add(n); // bin[0] = { v | #out=0 } // bin[n + d] = { v | d=#out-#in and #out!=0 and #in!=0 } for -n < d < n // bin[n + n] = { v | #in=0 and #out>0 } } // Main loop final LinkedList<GraphNode> s1=new LinkedList<GraphNode>(), s2=new LinkedList<GraphNode>(); while(true) { GraphNode x=null; if (!bins.get(0).isEmpty()) { // If a sink exists, take a sink X and prepend X to S2 x = bins.get(0).remove(bins.get(0).size()-1); s1.add(x); } else for(int j=2*num; j>0; j--) { // Otherwise, let x be a source if one exists, or a node with the highest #out-#in. Then append X to S1. List<GraphNode> bin=bins.get(j); int sz=bin.size(); if (sz>0) { x=bin.remove(sz-1); s2.addFirst(x); break; } } if (x==null) break; // This means we're done; else, delete X from its bin, and move each of X's neighbor into their new bin bins.get(grBIN[x.pos()]).remove(x); for(GraphNode n:grIN.get(x.pos())) grOUT.get(n.pos()).remove(x); for(GraphNode n:grOUT.get(x.pos())) grIN.get(n.pos()).remove(x); for(GraphNode n:Util.fastJoin(grIN.get(x.pos()), grOUT.get(x.pos()))) { int ni=n.pos(), out=grOUT.get(ni).size(), in=grIN.get(ni).size(); int b=(out==0)?0:(in==0?(2*num):(out-in+num)); if (grBIN[ni]!=b) { bins.get(grBIN[ni]).remove(n); grBIN[ni]=b; bins.get(b).add(n); } } } sortNodes(Util.fastJoin(s1,s2)); } //============================================================================================================================// /** Layout step #2: reverses all backward edges. */ private void layout_backEdges() { for(GraphEdge e: edges) if (e.a().pos() < e.b().pos()) e.set(e.bhead(), e.ahead()).reverse(); } //============================================================================================================================// /** Layout step #3: assign the nodes into one or more layers, then return the number of layers. */ private int layout_decideLayer() { // Here, for each node X, I compute its maximum length to a sink; if X is a sink, its length to sink is 0. final int n = nodes.size(); int[] len = new int[n]; for(GraphNode x: nodes) { // Since we ensured that arrows only ever go from a node with bigger pos() to a node with smaller pos(), // we can compute the "len" array in O(n) time by visiting each node IN THE SORTED ORDER int max=0; for(GraphEdge e: x.outs) { GraphNode y = e.b(); int yLen = len[y.pos()]+1; if (max < yLen) max = yLen; } len[x.pos()] = max; } // Now, we simply do the simplest thing: assign each node to the layer corresponding to its max-length-to-sink. for(GraphNode x: nodes) x.setLayer(len[x.pos()]); // Now, apply a simple trick: whenever every one of X's incoming edge is more than one layer above, then move X up while(true) { boolean changed = false; for(GraphNode x: nodes) if (x.ins.size() > 0) { int closestLayer=layers()+1; for(GraphEdge e: x.ins) { int y = e.a().layer(); if (closestLayer>y) closestLayer=y; } if (closestLayer-1>x.layer()) { x.setLayer(closestLayer-1); changed=true; } } if (!changed) break; } // All done! return layers(); } //============================================================================================================================// /** Layout step #4: add dummy nodes so that each edge only goes between adjacent layers. */ private void layout_dummyNodesIfNeeded() { for(final GraphEdge edge: new ArrayList<GraphEdge>(edges)) { GraphEdge e = edge; GraphNode a = e.a(), b=e.b(); while(a.layer() - b.layer() > 1) { GraphNode tmp = a; a = new GraphNode(a.graph, e.uuid).set((DotShape)null); a.setLayer(tmp.layer()-1); // now we have three nodes in the vertical order of "tmp", "a", then "b" e.change(a); // let old edge go from "tmp" to "a" e = new GraphEdge(a, b, e.uuid, "", e.ahead(), e.bhead(), e.style(), e.color(), e.group); // let new edge go from "a" to "b" } } } //============================================================================================================================// /** Layout step #5: decide the order of the nodes within each layer. */ private void layout_reorderPerLayer() { // This uses the original Barycenter heuristic final IdentityHashMap<GraphNode,Object> map = new IdentityHashMap<GraphNode,Object>(); final double[] bc = new double[nodes.size()+1]; int i=1; for(GraphNode n:layer(0)) { bc[n.pos()] = i; i++; } for(int layer=0; layer<layers()-1; layer++) { for(GraphNode n:layer(layer+1)) { map.clear(); int count = 0; double sum = 0; for(GraphEdge e: n.outs) { GraphNode nn=e.b(); if (map.put(nn,nn)==null) { count++; sum += bc[nn.pos()]; } } bc[n.pos()] = count==0 ? 0 : (sum/count); } sortLayer(layer+1, new Comparator<GraphNode>() { public int compare(GraphNode o1, GraphNode o2) { // If the two nodes have the same barycenter, we use their ordering that was established during layout_assignOrder() if (o1==o2) return 0; int n = Double.compare(bc[o1.pos()], bc[o2.pos()]); if (n!=0) return n; else if (o1.pos()<o2.pos()) return -1; else return 1; } }); int j=1; for(GraphNode n:layer(layer+1)) { bc[n.pos()]=j; j++; } } } //============================================================================================================================// /** Layout step #6: decide the exact X position of each component. */ private void layout_xAssignment(List<GraphNode> nodes) { // This implementation uses the iterative approach described in the paper "Layout of Bayesian Networks" // by Kim Marriott, Peter Moulder, Lucas Hope, and Charles Twardy final int n = nodes.size(); if (n==0) return; final Block[] block = new Block[n+1]; block[0] = new Block(); // The sentinel block for(int i=1; i<=n; i++) { Block b = new Block(nodes.get(i-1), i); block[i] = b; while(block[b.first-1].posn + block[b.first-1].width > b.posn) { b = new Block(block[b.first-1], b); block[b.last] = b; block[b.first] = b; } } int i=1; while(true) { Block b = block[i]; double tmp = b.posn + (nodes.get(b.first-1).getWidth() + nodes.get(b.first-1).getReserved() + xJump)/2D; nodes.get(i-1).setX((int)tmp); for(i=i+1; i<=b.last; i++) { GraphNode v1 = nodes.get(i-1); GraphNode v2 = nodes.get(i-2); int xsep = (v1.getWidth() + v1.getReserved() + v2.getWidth() + v2.getReserved())/2 + xJump; v1.setX(v2.x() + xsep); } i=b.last+1; if (i>n) break; } } /** This computes the des() value as described in the paper. * <p> The desired location of V = ("sum e:in(V) | wt(e) * phi(start of e)" + "sum e:out(V) | wt(e) * phi(end of e)") / wt(v) */ private static double des(GraphNode n) { int wt = wt(n); if (wt==0) return 0; // This means it has no "in" edges and no "out" edges double ans=0; for(GraphEdge e: n.ins) ans += ((double)e.weight()) * e.a().x(); for(GraphEdge e: n.outs) ans += ((double)e.weight()) * e.b().x(); return ans/wt; } /** This computes the wt() value as described in the paper. * <p> The weight of a node is the sum of the weights of its in-edges and out-edges. */ private static int wt(GraphNode n) { int ans=0; for(GraphEdge e: n.ins) ans += e.weight(); for(GraphEdge e: n.outs) ans += e.weight(); return ans; } /** This corresponds to the Block structure described in the paper. */ private static final class Block { /** These fields are described in the paper. */ private final int first, last, weight; /** These fields are described in the paper. */ private final double width, posn, wposn; /** This constructs a regular block. */ public Block(GraphNode v, int i) { first=i; last=i; width=v.getWidth()+v.getReserved()+xJump; posn=des(v)-(width/2); weight=wt(v); wposn=weight*posn; } /** This merges the two existing blocks into a new block. */ public Block(Block a, Block b) { first=a.first; last=b.last; width=a.width+b.width; wposn=a.wposn+b.wposn-a.width*b.weight; weight=a.weight+b.weight; posn=wposn/weight; } /** This constructs a sentinel block. */ public Block() { posn=Double.NEGATIVE_INFINITY; first=0; last=0; weight=0; width=0; wposn=0; } } //============================================================================================================================// /** For each edge coming out of this layer of nodes, add bends to it if it currently overlaps some nodes inappropriately. */ private void checkUpperCollision(List<GraphNode> top) { final int room=2; // This is how much we need to stay clear of a node's boundary for(int i=0; i<top.size(); i++) { GraphNode a = top.get(i); double left = a.x() - a.getWidth()/2, right = a.x() - a.getWidth()/2; for(GraphEdge e: a.outs) { GraphNode b = e.b(); if (b.x()>=right) for(int j=i+1; j<top.size(); j++) { // This edge goes from top-left to bottom-right GraphNode c=top.get(j); if (c.shape()==null) continue; // You can intersect thru a dummy node double ctop=c.y()-c.getHeight()/2, cleft=c.x()-c.getWidth()/2, cbottom=c.y()+c.getHeight()/2; e.path().bendDown(cleft, ctop-room, cbottom+room, 3); } else if (b.x()<=left) for(int j=i-1; j>=0; j--) { // This edge goes from top-right to bottom-left GraphNode c=top.get(j); if (c.shape()==null) continue; // You can intersect thru a dummy node double ctop=c.y()-c.getHeight()/2, cright=c.x()+c.getWidth()/2, cbottom=c.y()+c.getHeight()/2; e.path().bendDown(cright, ctop-room, cbottom+room, 3); } } } } //============================================================================================================================// /** For each edge going into this layer of nodes, add bends to it if it currently overlaps some nodes inappropriately. */ private void checkLowerCollision(List<GraphNode> bottom) { final int room=2; // This is how much we need to stay clear of a node's boundary for(int i=0; i<bottom.size(); i++) { GraphNode b=bottom.get(i); double left=b.x()-b.getWidth()/2, right=b.x()-b.getWidth()/2; for(GraphEdge e: b.ins) { GraphNode a=e.a(); if (a.x()<=left) for(int j=i-1; j>=0; j--) { // This edge goes from top-left to bottom-right GraphNode c=bottom.get(j); if (c.shape()==null) continue; // You can intersect thru a dummy node double ctop=c.y()-c.getHeight()/2, cright=c.x()+c.getWidth()/2, cbottom=c.y()+c.getHeight()/2; e.path().bendUp(cright, ctop-room, cbottom+room, 3); } else if (a.x()>=right) for(int j=i+1; j<bottom.size(); j++) { // This edge goes from top-right to bottom-left GraphNode c=bottom.get(j); if (c.shape()==null) continue; // You can intersect thru a dummy node double ctop=c.y()-c.getHeight()/2, cleft=c.x()-c.getWidth()/2, cbottom=c.y()+c.getHeight()/2; e.path().bendUp(cleft, ctop-room, cbottom+room, 3); } } } } //============================================================================================================================// /** Returns true if a direct line between a and b will not intersect any other node. */ private boolean free(GraphNode a, GraphNode b) { if (a.layer() > b.layer()) { GraphNode tmp=a; a=b; b=tmp; } Line2D.Double line = new Line2D.Double(a.x(), a.y(), b.x(), b.y()); for(GraphNode n:nodes) if (n!=a && n!=b && a.layer()<n.layer() && n.layer()<b.layer() && n.shape()!=null) { if (line.intersects(n.getBoundingBox(10,10))) return false; } return true; } //============================================================================================================================// /** (Re-)perform the layout. */ public void layout() { // The rest of the code below assumes at least one node, so we return right away if nodes.size()==0 if (nodes.size()==0) return; // Calculate each node's width and height for(GraphNode n:nodes) n.calcBounds(); // Layout the nodes layout_assignOrder(); layout_backEdges(); final int layers = layout_decideLayer(); layout_dummyNodesIfNeeded(); layout_reorderPerLayer(); // For each layer, this array stores the height of its tallest node layerPH = new int[layers]; // figure out the Y position of each layer, and also give each component an initial X position for(int layer=layers-1; layer>=0; layer--) { int x=5; // So that we're not touching the left-edge of the window int h=0; for(GraphNode n: layer(layer)) { int nHeight = n.getHeight(), nWidth = n.getWidth(); n.setX(x + nWidth/2); if (h < nHeight) h = nHeight; x = x + nWidth + n.getReserved() + 20; } layerPH[layer] = h; } // If there are more than one layer, then iteratively refine the X position of each component 3 times; 4 is a good number if (layers>1) { // It's important to NOT DO THIS when layers<=1, because without edges the nodes will overlap each other into the center for(int i=0; i<3; i++) for(int layer=0; layer<layers; layer++) layout_xAssignment(layer(layer)); } // Calculate each node's y; we start at y==5 so that we're not touching the top-edge of the window int py=5; for(int layer=layers-1; layer>=0; layer--) { final int ph = layerPH[layer]; for(GraphNode n:layer(layer)) n.setY(py + ph/2); py = py + ph + yJump; } relayout_edges(true); // Since we're doing layout for the first time, we need to explicitly set top and bottom, since // otherwise "recalcBound" will merely "extend top and bottom" as needed. recalcBound(true); } //============================================================================================================================// /** Re-establish top/left/width/height. */ void recalcBound(boolean fresh) { if (nodes.size()==0) { top=0; bottom=10; totalHeight=10; left=0; totalWidth=10; return; } if (fresh) { top=nodes.get(0).y()-nodes.get(0).getHeight()/2-5; bottom=nodes.get(0).y()+nodes.get(0).getHeight()/2+5; } // Find the leftmost and rightmost pixel int minX = nodes.get(0).x() - nodes.get(0).getWidth()/2 - 5; int maxX = nodes.get(0).x() + nodes.get(0).getWidth()/2 + nodes.get(0).getReserved() + 5; for(GraphNode n: nodes) { int min = n.x() - n.getWidth()/2 - 5; if (minX>min) minX=min; int max = n.x() + n.getWidth()/2 + n.getReserved() + 5; if (maxX<max) maxX=max; } for(GraphEdge e: edges) if (e.getLabelW()>0 && e.getLabelH()>0) { int x1=e.getLabelX(), x2=x1+e.getLabelW()-1; if (minX>x1) minX=x1; if (maxX<x2) maxX=x2; } left = minX-20; totalWidth = maxX-minX+20; // Find the topmost and bottommost pixel for(int layer=layers()-1; layer>=0; layer--) { for(GraphNode n: layer(layer)) { int ytop = n.y()-n.getHeight()/2-5; if (top > ytop) top=ytop; int ybottom = n.y()+n.getHeight()/2+5; if (bottom < ybottom) bottom=ybottom; } } totalHeight = bottom-top; int widestLegend = 0, legendHeight = 30; for(Pair<String,Color> e: legends.values()) { if (e.b==null) continue; // that means this legend is not visible int widthOfLegend = (int) getBounds(true, e.a).getWidth(); if (widestLegend < widthOfLegend) widestLegend = widthOfLegend; legendHeight += ad; } if (widestLegend>0) { left -= (widestLegend+10); totalWidth += (widestLegend*2+10); if (totalHeight<legendHeight) { bottom=bottom+(legendHeight-totalHeight); totalHeight=legendHeight; } } } //============================================================================================================================// /** Assuming everything was laid out already, but at least one node just moved, this re-layouts ALL edges. */ void relayout_edges(boolean straighten) { // Move pairs of virtual nodes to straighten the lines if possible if (straighten) for(int i=0; i<5; i++) for(GraphNode n:nodes) if (n.shape()==null) { GraphEdge e1 = n.ins.get(0), e2 = n.outs.get(0); if (!free(e1.a(), e2.b())) continue; double slope = (e2.b().x()-e1.a().x()) / ((double)(e2.b().y()-e1.a().y())); double xx = (n.y()-e1.a().y())*slope + e1.a().x(); n.setX((int)xx); } // Move the virtual nodes between endpoints to straighten the lines if possible if (straighten) for(GraphEdge e:edges) if (e.a().shape()!=null && e.b().shape()==null) { GraphNode a=e.a(), b; for(GraphEdge ee=e;;) { b=ee.b(); if (b.shape()!=null) break; ee = b.outs.get(0); } if (!free(a,b)) continue; double slope = (b.x()-a.x()) / ((double)(b.y()-a.y())); for(GraphEdge ee=e;;) { b=ee.b(); if (b.shape()!=null) break; double xx = (b.y()-a.y())*slope + a.x(); b.setX((int)xx); ee = b.outs.get(0); } } // Now restore the invariant that nodes in each layer is ordered by x if (straighten) for(int i=0; i<layers(); i++) { sortLayer(i, new Comparator<GraphNode>() { public int compare(GraphNode o1, GraphNode o2) { if (o1.x()<o2.x()) return -1; else if (o1.x()>o2.x()) return 1; return 0; } }); // Ensure that nodes are not bunched up together horizontally. List<GraphNode> layer=new ArrayList<GraphNode>(layer(i)); for(int j=layer.size()/2; j>=0 && j<layer.size()-1; j++) { GraphNode a=layer.get(j), b=layer.get(j+1); int ax = a.shape()==null ? a.x() : (a.x()+a.getWidth()/2+a.getReserved()); int bx = b.shape()==null ? b.x() : (b.x()-b.getWidth()/2); if (bx<=ax || bx-ax<5) b.setX(ax+5+b.getWidth()/2); } for(int j=layer.size()/2; j>0 && j<layer.size(); j--) { GraphNode a=layer.get(j-1), b=layer.get(j); int ax = a.shape()==null ? a.x() : (a.x()+a.getWidth()/2+a.getReserved()); int bx = b.shape()==null ? b.x() : (b.x()-b.getWidth()/2); if (bx<=ax || bx-ax<5) a.setX(bx-5-a.getWidth()/2-a.getReserved()); } } // Now layout the edges, initially as straight lines for(GraphEdge e:edges) e.resetPath(); // Now, scan layer-by-layer to find edges that intersect nodes improperly, and bend them accordingly for(int layer=layers()-1; layer>0; layer--) { List<GraphNode> top=layer(layer), bottom=layer(layer-1); checkUpperCollision(top); checkLowerCollision(bottom); checkUpperCollision(top); } // Now, for each edge, adjust its arrowhead and label. AvailableSpace sp = new AvailableSpace(); for(GraphNode n: nodes) if (n.shape()!=null) sp.add(n.x()-n.getWidth()/2, n.y()-n.getHeight()/2, n.getWidth()+n.getReserved(), n.getHeight()); for(GraphEdge e: edges) { e.layout_arrowHead(); e.repositionLabel(sp); } } //============================================================================================================================// /** Assuming everything was laid out already, but nodes in layer[i] just moved horizontally, this re-layouts edges to+from layer i. */ void relayout_edges(int i) { if (nodes.size()==0) return; // The rest of the code assumes there is at least one node for(GraphNode n: layer(i)) for(GraphEdge e: n.selfs) { e.resetPath(); e.layout_arrowHead(); } if (i>0) { List<GraphNode> top=layer(i), bottom=layer(i-1); for(GraphNode n: top) for(GraphEdge e: n.outs) e.resetPath(); checkUpperCollision(top); checkLowerCollision(bottom); checkUpperCollision(top); } if (i<layers()-1) { List<GraphNode> top=layer(i+1), bottom=layer(i); for(GraphNode n: top) for(GraphEdge e: n.outs) e.resetPath(); checkUpperCollision(top); checkLowerCollision(bottom); checkUpperCollision(top); } // Now, for each edge, adjust its arrowhead and label. AvailableSpace sp = new AvailableSpace(); for(GraphNode n:nodes) if (n.shape()!=null) sp.add(n.x()-n.getWidth()/2, n.y()-n.getHeight()/2, n.getWidth()+n.getReserved(), n.getHeight()); for(GraphEdge e:edges) { e.layout_arrowHead(); e.repositionLabel(sp); } } //============================================================================================================================// /** Locates the node or edge at the given (X,Y) location. */ public Object find(double scale, int mouseX, int mouseY) { int h = getTop() + 10 - ad; double x = mouseX/scale + getLeft(), y = mouseY/scale + getTop(); for(Map.Entry<Comparable<?>,Pair<String,Color>> e: legends.entrySet()) { if (e.getValue().b == null) continue; h = h + ad; if (y<h || y>=h+ad) continue; int w = (int) getBounds(true, e.getValue().a).getWidth(); if (x>=getLeft()+10 && x<=getLeft()+10+w) return e.getKey(); } for(GraphNode n: nodes) { if (n.shape()==null && Math.abs(n.x()-x)<10 && Math.abs(n.y()-y)<10) return n; if (n.contains(x,y)) return n; } for(GraphEdge e: edges) { if (e.a() != e.b()) { double dx; dx = e.path().getXatY(y, 0, 1, Double.NaN); if (!Double.isNaN(dx) && StrictMath.abs(x-dx)<12/scale) return e; } else { double dx; dx = e.path().getXatY(y, 0.25, 0.75, Double.NaN); if (!Double.isNaN(dx) && StrictMath.abs(x-dx)<12/scale) return e; dx = e.path().getXatY(y, 0, 0.25, Double.NaN); if (!Double.isNaN(dx) && StrictMath.abs(x-dx)<12/scale) return e; dx = e.path().getXatY(y, 0.75, 1, Double.NaN); if (!Double.isNaN(dx) && StrictMath.abs(x-dx)<12/scale) return e; } } return null; } //============================================================================================================================// /** Assuming layout has been performed, this draws the graph with the given magnification scale. */ void draw(Artist gr, double scale, Object highlight, boolean showLegends) { if (nodes.size()==0) return; // The rest of this procedure assumes there is at least one node Object group = null; GraphNode highFirstNode = null, highLastNode = null; GraphEdge highFirstEdge = null, highLastEdge = null; if (highlight instanceof GraphEdge) { highFirstEdge = (GraphEdge)highlight; highLastEdge = highFirstEdge; group = highFirstEdge.group; while(highFirstEdge.a().shape() == null) highFirstEdge = highFirstEdge.a().ins.get(0); while(highLastEdge.b().shape() == null) highLastEdge = highLastEdge.b().outs.get(0); highFirstNode=highFirstEdge.a(); highLastNode=highLastEdge.b(); } else if (!(highlight instanceof GraphNode) && highlight!=null) { group = highlight; } // Since drawing an edge will automatically draw all segments if they're connected via dummy nodes, // we must make sure we only draw out edges from non-dummy-nodes int maxAscent = Artist.getMaxAscent(); for(GraphNode n:nodes) if (n.shape()!=null) { for(GraphEdge e:n.outs) if (e.group!=group) e.draw(gr, scale, highFirstEdge, group); for(GraphEdge e:n.selfs) if (e.group!=group) e.draw(gr, scale, highFirstEdge, group); } if (group!=null) { for(GraphNode n:nodes) if (n.shape()!=null) { for(GraphEdge e:n.outs) if (e.group==group && e!=highFirstEdge) e.draw(gr, scale, highFirstEdge, group); for(GraphEdge e:n.selfs) if (e.group==group && e!=highFirstEdge) e.draw(gr, scale, highFirstEdge, group); } if (highFirstEdge!=null) highFirstEdge.draw(gr, scale, highFirstEdge, group); } for(GraphNode n:nodes) if (highFirstNode!=n && highLastNode!=n) n.draw(gr, scale, n==highlight); if (highFirstNode!=null) highFirstNode.draw(gr, scale, true); if (highLastNode!=null && highLastNode!=highFirstNode) highLastNode.draw(gr, scale, true); if (highFirstEdge!=null) highFirstEdge.drawLabel(gr, highFirstEdge.color(), new Color(255,255,255,160)); // show legends? if (!showLegends || legends.size()==0) return; boolean groupFound=false; int y=0, maxWidth=0; for(Map.Entry<Comparable<?>,Pair<String,Color>> e:legends.entrySet()) { if (e.getValue().b==null) continue; if (group!=null && e.getKey()==group) groupFound=true; int w = (int) getBounds(true, e.getValue().a).getWidth(); if (maxWidth<w) maxWidth=w; y = y + ad; } if (y==0) return; // This means no legends need to be drawn gr.setColor(Color.GRAY); gr.draw(new RoundRectangle2D.Double(5, 5, maxWidth+10, y+10, 5, 5), false); y=10; for(Map.Entry<Comparable<?>,Pair<String,Color>> e:legends.entrySet()) { Color color = e.getValue().b; if (color==null) continue; gr.setFont((groupFound && e.getKey()==group) || !groupFound); gr.setColor((!groupFound || e.getKey()==group) ? color : Color.GRAY); gr.drawString(e.getValue().a, 8, y+maxAscent); y = y + ad; } } //============================================================================================================================// /** Helper method that encodes a String for printing into a DOT file. */ static String esc(String name) { if (name.indexOf('\"') < 0) return name; StringBuilder out = new StringBuilder(); for(int i=0; i<name.length(); i++) { char c=name.charAt(i); if (c=='\"') out.append('\\'); out.append(c); } return out.toString(); } //============================================================================================================================// /** Returns a DOT representation of this graph. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("digraph \"graph\" {\n" + "graph [fontsize=12]\n" + "node [fontsize=12]\n" + "edge [fontsize=12]\n" + "rankdir=TB;\n"); for (GraphEdge e: edges) sb.append(e); for (GraphNode n: nodes) sb.append(n); sb.append("}\n"); return sb.toString(); } }