/* 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.awt.Color; import java.awt.Polygon; import java.awt.Shape; import java.awt.geom.GeneralPath; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import static java.lang.StrictMath.sqrt; import static java.lang.StrictMath.round; import static edu.mit.csail.sdg.alloy4graph.Artist.getBounds; import static edu.mit.csail.sdg.alloy4graph.Graph.selfLoopA; import static edu.mit.csail.sdg.alloy4graph.Graph.selfLoopGL; import static edu.mit.csail.sdg.alloy4graph.Graph.selfLoopGR; import static edu.mit.csail.sdg.alloy4graph.Graph.esc; /** Mutable; represents a graphical node. * * <p><b>Thread Safety:</b> Can be called only by the AWT event thread. */ public final strictfp class GraphNode { // =============================== adjustable options ================================================== /** This determines the minimum width of a dummy node. */ private static final int dummyWidth = 30; /** This determines the minimum height of a dummy node. */ private static final int dummyHeight = 10; /** This determines the minimum amount of padding added above, left, right, and below the text label. */ private static final int labelPadding = 5; /** Color to use to show a highlighted node. */ private static final Color COLOR_CHOSENNODE = Color.LIGHT_GRAY; // =============================== cached for performance =================================== /** 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(); /** Caches the value of sqrt(3.0). The extra digits in the definition will be truncated by the Java compiler. */ private static final double sqrt3 = 1.7320508075688772935274463415058723669428052538103806280558D; /** Caches the value of sin(36 degree). The extra digits in the definition will be truncated by the Java compiler. */ private static final double sin36 = 0.5877852522924731291687059546390727685976524376431459910723D; /** Caches the value of cos(36 degree). The extra digits in the definition will be truncated by the Java compiler. */ private static final double cos36 = 0.8090169943749474241022934171828190588601545899028814310677D; /** Caches the value of cos(18 degree). The extra digits in the definition will be truncated by the Java compiler. */ private static final double cos18 = 0.9510565162951535721164393333793821434056986341257502224473D; /** Caches the value of tan(18 degree). The extra digits in the definition will be truncated by the Java compiler. */ private static final double tan18 = 0.3249196962329063261558714122151344649549034715214751003078D; // =============================== these fields do not affect the computed bounds =============================================== /** a user-provided annotation that will be associated with this node (can be null) (need not be unique) */ public final Object uuid; /** The X coordinate of the center of the node; modified by tweak(), layout_computeX(), layout(), and relayout_edges() */ private int centerX = 0; /** The Y coordinate of the center of the node; modified by tweak(), layout_computeX(), layout(), and relayout_edges() */ private int centerY = 0; /** The graph that this node belongs to; must stay in sync with Graph.nodelist and Graph.layerlist */ final Graph graph; /** The layer that this node is in; must stay in sync with Graph.layerlist */ private int layer = 0; /** The current position of this node in the graph's node list; must stay in sync with Graph.nodelist */ int pos; /** The "in" edges not including "self" edges; must stay in sync with GraphEdge.a and GraphEdge.b */ final LinkedList<GraphEdge> ins = new LinkedList<GraphEdge>(); /** The "out" edges not including "self" edges; must stay in sync with GraphEdge.a and GraphEdge.b */ final LinkedList<GraphEdge> outs = new LinkedList<GraphEdge>(); // =============================== these fields affect the computed bounds =================================================== /** The "self" edges; must stay in sync with GraphEdge.a and GraphEdge.b * <p> When this value changes, we should invalidate the previously computed bounds information. */ final LinkedList<GraphEdge> selfs = new LinkedList<GraphEdge>(); /** The font boldness. * <p> When this value changes, we should invalidate the previously computed bounds information. */ private boolean fontBold = false; /** The node labels; if null or empty, then the node has no labels. * <p> When this value changes, we should invalidate the previously computed bounds information. */ private List<String> labels = null; /** The node color; never null. * <p> When this value changes, we should invalidate the previously computed bounds information. */ private Color color = Color.WHITE; /** The line style; never null. * <p> When this value changes, we should invalidate the previously computed bounds information. */ private DotStyle style = DotStyle.SOLID; /** The node shape; if null, then the node is a dummy node. * <p> When this value changes, we should invalidate the previously computed bounds information. */ private DotShape shape = DotShape.BOX; // ============================ these fields are computed by calcBounds() ========================================= /** If (updown>=0), this is the distance from the center to the top edge. */ private int updown = (-1); /** If (updown>=0), this is the distance from the center to the left edge. */ private int side = 0; /** If (updown>=0), this is the vertical distance between the center of the text label and the center of the node. */ private int yShift = 0; /** If (updown>=0), this is the width of the text label. */ private int width = 0; /** If (updown>=0), this is the height of the text label. */ private int height = 0; /** If (updown>=0), this is the amount of space on the right set-aside for self-loops (which is 0 if node has no self loops) */ private int reserved = 0; /** If (updown>=0 and shape!=null), this is the bounding polygon. * Note: if not null, it must be either a GeneralPath or a Polygon. */ private Shape poly = null; /** If (updown>=0 and shape!=null and poly2!=null), then poly2 will also be drawn during the draw() method. * Note: if not null, it must be either a GeneralPath or a Polygon. */ private Shape poly2 = null; /** If (updown>=0 and shape!=null and poly3!=null), then poly3 will also be drawn during the draw() method. * Note: if not null, it must be either a GeneralPath or a Polygon. */ private Shape poly3 = null; //=================================================================================================== /** Create a new node with the given list of labels, then add it to the given graph. */ public GraphNode(Graph graph, Object uuid, String... labels) { this.uuid = uuid; this.graph = graph; this.pos = graph.nodelist.size(); graph.nodelist.add(this); if (graph.layerlist.size()==0) graph.layerlist.add(new ArrayList<GraphNode>()); graph.layerlist.get(0).add(this); if (labels!=null && labels.length>0) { this.labels = new ArrayList<String>(labels.length); for(int i=0; i<labels.length; i++) this.labels.add(labels[i]); } } /** Changes the layer that this node is in; the new layer must be 0 or greater. * <p> If a node is removed from a layer, the order of the other nodes in that layer remain unchanged. * <p> If a node is added to a new layer, then it is added to the right of the original rightmost node in that layer. */ void setLayer(int newLayer) { if (newLayer < 0) throw new IllegalArgumentException("The layer cannot be negative!"); if (layer == newLayer) return; graph.layerlist.get(layer).remove(this); layer = newLayer; while(layer >= graph.layerlist.size()) graph.layerlist.add(new ArrayList<GraphNode>()); graph.layerlist.get(layer).add(this); } /** Returns an unmodifiable view of the list of "in" edges. */ public List<GraphEdge> inEdges() { return Collections.unmodifiableList(ins); } /** Returns an unmodifiable view of the list of "out" edges. */ public List<GraphEdge> outEdges() { return Collections.unmodifiableList(outs); } /** Returns an unmodifiable view of the list of "self" edges. */ public List<GraphEdge> selfEdges() { return Collections.unmodifiableList(selfs); } /** Returns the node's current position in the node list, which is always between 0 and node.size()-1 */ int pos() { return pos; } /** Returns the layer that this node is in. */ int layer() { return layer; } /** Returns the X coordinate of the center of the node. */ public int x() { return centerX; } /** Returns the Y coordinate of the center of the node. */ public int y() { return centerY; } /** Changes the X coordinate of the center of the node, without invalidating the computed bounds. */ void setX(int x) { centerX = x;} /** Changes the Y coordinate of the center of the node, without invalidating the computed bounds. */ void setY(int y) { centerY = y; } /** Returns the node shape (or null if the node is a dummy node). */ DotShape shape() { return shape; } /** Changes the node shape (where null means change the node into a dummy node), then invalidate the computed bounds. */ public GraphNode set(DotShape shape) { if (this.shape!=shape) { this.shape = shape; updown = (-1); } return this; } /** Changes the node color, then invalidate the computed bounds. */ public GraphNode set(Color color) { if (this.color!=color && color!=null) { this.color = color; updown = (-1); } return this; } /** Changes the line style, then invalidate the computed bounds. */ public GraphNode set(DotStyle style) { if (this.style!=style && style!=null) { this.style = style; updown = (-1); } return this; } /** Changes the font boldness, then invalidate the computed bounds. */ public GraphNode setFontBoldness(boolean bold) { if (this.fontBold != bold) { this.fontBold = bold; updown = (-1); } return this; } /** Add the given label after the existing labels, then invalidate the computed bounds. */ public GraphNode addLabel(String label) { if (label==null || label.length()==0) return this; if (labels==null) labels=new ArrayList<String>(); labels.add(label); updown = (-1); return this; } /** Returns the node height. */ int getHeight() { if (updown<0) calcBounds(); return updown+updown; } /** Returns the node width. */ int getWidth() { if (updown<0) calcBounds(); return side+side; } /** Returns the bounding rectangle (with 2*xfluff added to the width, and 2*yfluff added to the height) */ Rectangle2D getBoundingBox(int xfluff, int yfluff) { if (updown<0) calcBounds(); return new Rectangle2D.Double(x()-side-xfluff, y()-updown-yfluff, side+side+xfluff+xfluff, updown+updown+yfluff+yfluff); } /** Returns the amount of space we need to reserve on the right hand side for the self edges (0 if this has no self edges now) */ int getReserved() { if (selfs.isEmpty()) return 0; else if (updown<0) calcBounds(); return reserved; } /** Returns true if the node contains the given point or not. */ boolean contains(double x, double y) { if (shape==null) return false; else if (updown<0) calcBounds(); return poly.contains(x-centerX, y-centerY); } /** Draws this node at its current (x, y) location; this method will call calcBounds() if necessary. */ void draw(Artist gr, double scale, boolean highlight) { if (shape==null) return; else if (updown<0) calcBounds(); final int top = graph.getTop(), left = graph.getLeft(); gr.set(style, scale); gr.translate(centerX-left, centerY-top); gr.setFont(fontBold); if (highlight) gr.setColor(COLOR_CHOSENNODE); else gr.setColor(color); if (shape==DotShape.CIRCLE || shape==DotShape.M_CIRCLE || shape==DotShape.DOUBLE_CIRCLE) { int hw=width/2, hh=height/2; int radius = ((int) (sqrt( hw*((double)hw) + ((double)hh)*hh ))) + 2; if (shape==DotShape.DOUBLE_CIRCLE) radius=radius+5; gr.fillCircle(radius); gr.setColor(Color.BLACK); gr.drawCircle(radius); if (style==DotStyle.DOTTED || style==DotStyle.DASHED) gr.set(DotStyle.SOLID, scale); if (shape==DotShape.M_CIRCLE && 10*radius>=25 && radius>5) { int d = (int) sqrt(10*radius - 25.0D); if (d>0) { gr.drawLine(-d,-radius+5,d,-radius+5); gr.drawLine(-d,radius-5,d,radius-5); } } if (shape==DotShape.DOUBLE_CIRCLE) gr.drawCircle(radius-5); } else { gr.draw(poly,true); gr.setColor(Color.BLACK); gr.draw(poly,false); if (poly2!=null) gr.draw(poly2,false); if (poly3!=null) gr.draw(poly3,false); if (style==DotStyle.DOTTED || style==DotStyle.DASHED) gr.set(DotStyle.SOLID, scale); if (shape==DotShape.M_DIAMOND) { gr.drawLine(-side+8, -8, -side+8, 8); gr.drawLine(-8, -side+8, 8, -side+8); gr.drawLine(side-8, -8, side-8, 8); gr.drawLine(-8, side-8, 8, side-8); } if (shape==DotShape.M_SQUARE) { gr.drawLine(-side, -side+8, -side+8, -side); gr.drawLine(side, -side+8, side-8, -side); gr.drawLine(-side, side-8, -side+8, side); gr.drawLine(side, side-8, side-8, side); } } gr.set(DotStyle.SOLID, scale); int clr = color.getRGB() & 0xFFFFFF; gr.setColor((clr==0x000000 || clr==0xff0000 || clr==0x0000ff) ? Color.WHITE : Color.BLACK); if (labels!=null && labels.size()>0) { int x=(-width/2), y=yShift+(-labels.size()*ad/2); for(int i=0; i<labels.size(); i++) { String t = labels.get(i); int w = ((int) (getBounds(fontBold, t).getWidth())) + 1; // Round it up if (width>w) w=(width-w)/2; else w=0; gr.drawString(t, x+w, y+Artist.getMaxAscent()); y=y+ad; } } gr.translate(left-centerX, top-centerY); } /** Helper method that sets the Y coordinate of every node in a given layer. */ private void setY(int layer, int y) { for(GraphNode n: graph.layer(layer)) n.centerY = y; } /** Helper method that shifts a node up. */ private void shiftUp(int y) { final int[] ph = graph.layerPH; final int yJump = Graph.yJump/6; int i=layer(); setY(i,y); y=y-ph[i]/2; // y is now the top-most edge of this layer for(i++; i<graph.layers(); i++) { List<GraphNode> list=graph.layer(i); GraphNode first=list.get(0); if (first.centerY+ph[i]/2+yJump > y) setY(i, y-ph[i]/2-yJump); y=first.centerY-ph[i]/2; } graph.relayout_edges(false); } /** Helper method that shifts a node down. */ private void shiftDown(int y) { final int[] ph = graph.layerPH; final int yJump = Graph.yJump/6; int i=layer(); setY(i,y); y=y+ph[i]/2; // y is now the bottom-most edge of this layer for(i--; i>=0; i--) { List<GraphNode> list=graph.layer(i); GraphNode first=list.get(0); if (first.centerY-ph[i]/2-yJump < y) setY(i, y+ph[i]/2+yJump); y=first.centerY+ph[i]/2; } graph.relayout_edges(false); } /** Helper method that shifts a node left. */ private void shiftLeft(List<GraphNode> peers, int i, int x) { final int xJump = Graph.xJump/3; centerX = x; x=x-(shape==null?0:side); // x is now the left-most edge of this node for(i--;i>=0;i--) { GraphNode node=peers.get(i); int side=(node.shape==null?0:node.side); if (node.centerX+side+node.getReserved()+xJump>x) node.centerX=x-side-node.getReserved()-xJump; x=node.centerX-side; } } /** Helper method that shifts a node right. */ private void shiftRight(List<GraphNode> peers, int i, int x) { final int xJump = Graph.xJump/3; centerX = x; x=x+(shape==null?0:side)+getReserved(); // x is now the right most edge of this node for(i++;i<peers.size();i++) { GraphNode node=peers.get(i); int side=(node.shape==null?0:node.side); if (node.centerX-side-xJump<x) node.centerX=x+side+xJump; x=node.centerX+side+node.getReserved(); } } /** Helper method that swaps a node towards the left. */ private void swapLeft(List<GraphNode> peers, int i, int x) { int side=(shape==null ? 2 : this.side); int left=x-side; while(true) { if (i==0) { centerX=x; return; } // no clash possible GraphNode other=peers.get(i-1); int otherSide=(other.shape==null ? 0 : other.side); int otherRight=other.centerX+otherSide+other.getReserved(); if (otherRight<left) { centerX=x; return; } // no clash graph.swapNodes(layer(), i, i-1); i--; if (other.shape!=null) other.shiftRight(peers, i+1, x + side + getReserved() + otherSide); } } /** Helper method that swaps a node towards the right. */ private void swapRight(List<GraphNode> peers, int i, int x) { int side = (shape==null ? 2 : this.side); int right=x+side+getReserved(); while(true) { if (i==peers.size()-1) { centerX=x; return; } // no clash possible GraphNode other=peers.get(i+1); int otherSide=(other.shape==null ? 0 : other.side); int otherLeft=other.centerX-otherSide; if (otherLeft>right) { centerX=x; return; } // no clash graph.swapNodes(layer(), i, i+1); i++; if (other.shape!=null) other.shiftLeft(peers, i-1, x - side - other.getReserved() - otherSide); } } /** Assuming the graph is already laid out, this shifts this node (and re-layouts nearby nodes/edges as necessary) */ void tweak(int x, int y) { if (centerX==x && centerY==y) return; // If no change, then return right away List<GraphNode> layer = graph.layer(layer()); final int n = layer.size(); int i; for(i=0; i<n; i++) if (layer.get(i)==this) break; // Figure out this node's position in its layer if (centerX>x) swapLeft(layer,i,x); else if (centerX<x) swapRight(layer,i,x); if (centerY>y) shiftUp(y); else if (centerY<y) shiftDown(y); else graph.relayout_edges(layer()); graph.recalcBound(false); } //=================================================================================================== /** (Re-)calculate this node's bounds. */ void calcBounds() { reserved=(yShift=0); width=2*labelPadding; if (width<dummyWidth) side=dummyWidth/2; height=width; if (height<dummyHeight) updown=dummyHeight/2; poly=(poly2=(poly3=null)); if (shape==null) return; Polygon poly=new Polygon(); if (labels!=null) for(int i=0; i<labels.size(); i++) { String t = labels.get(i); Rectangle2D rect = getBounds(fontBold, t); int ww = ((int)(rect.getWidth())) + 1; // Round it up if (width<ww) width=ww; height=height+ad; } int hw=((width+1)/2)+labelPadding; if (hw<ad/2) hw=ad/2; width=hw*2; side=hw; int hh=((height+1)/2)+labelPadding; if (hh<ad/2) hh=ad/2; height=hh*2; updown=hh; switch(shape) { case HOUSE: { yShift = ad/2; updown = updown + yShift; poly.addPoint(-hw,yShift-hh); poly.addPoint(0,-updown); poly.addPoint(hw,yShift-hh); poly.addPoint(hw,yShift+hh); poly.addPoint(-hw,yShift+hh); break; } case INV_HOUSE: { yShift = -ad/2; updown = updown - yShift; poly.addPoint(-hw,yShift-hh); poly.addPoint(hw,yShift-hh); poly.addPoint(hw,yShift+hh); poly.addPoint(0,updown); poly.addPoint(-hw,yShift+hh); break; } case TRIANGLE: case INV_TRIANGLE: { int dx = (int) (height/sqrt3); dx=dx+1; if (dx<6) dx=6; int dy = (int) (hw*sqrt3); dy=dy+1; if (dy<6) dy=6; dy=(dy/2)*2; side += dx; updown += dy/2; if (shape==DotShape.TRIANGLE) { yShift = dy/2; poly.addPoint(0, -updown); poly.addPoint(hw+dx, updown); poly.addPoint(-hw-dx, updown); } else { yShift = -dy/2; poly.addPoint(0, updown); poly.addPoint(hw+dx, -updown); poly.addPoint(-hw-dx, -updown); } break; } case HEXAGON: { side += ad; poly.addPoint(-hw-ad, 0); poly.addPoint(-hw, -hh); poly.addPoint(hw, -hh); poly.addPoint(hw+ad, 0); poly.addPoint(hw, hh); poly.addPoint(-hw, hh); break; } case TRAPEZOID: { side += ad; poly.addPoint(-hw,-hh); poly.addPoint(hw,-hh); poly.addPoint(hw+ad,hh); poly.addPoint(-hw-ad,hh); break; } case INV_TRAPEZOID: { side += ad; poly.addPoint(-hw-ad, -hh); poly.addPoint(hw+ad, -hh); poly.addPoint(hw, hh); poly.addPoint(-hw, hh); break; } case PARALLELOGRAM: { side += ad; poly.addPoint(-hw, -hh); poly.addPoint(hw+ad, -hh); poly.addPoint(hw, hh); poly.addPoint(-hw-ad, hh); break; } case M_DIAMOND: case DIAMOND: { if (shape==DotShape.M_DIAMOND) { if (hw<10) { hw=10; side=10; width=20; } if (hh<10) { hh=10; updown=10; height=20; } } updown += hw; side += hh; poly.addPoint(-hw-hh, 0); poly.addPoint(0, -hh-hw); poly.addPoint(hw+hh, 0); poly.addPoint(0, hh+hw); break; } case M_SQUARE: { if (hh<hw) hh=hw; else hw=hh; if (hh<6) { hh=6; hw=6; } this.width=hw*2; this.side=hw; this.height=hh*2; this.updown=hh; side += 4; updown +=4; poly.addPoint(-hw-4,-hh-4); poly.addPoint(hw+4,-hh-4); poly.addPoint(hw+4,hh+4); poly.addPoint(-hw-4,hh+4); break; } case OCTAGON: case DOUBLE_OCTAGON: case TRIPLE_OCTAGON: { int dx=(width)/3, dy=ad; updown += dy; poly.addPoint(-hw, -hh); poly.addPoint(-hw+dx, -hh-dy); poly.addPoint(hw-dx, -hh-dy); poly.addPoint(hw, -hh); poly.addPoint(hw, hh); poly.addPoint(hw-dx, hh+dy); poly.addPoint(-hw+dx, hh+dy); poly.addPoint(-hw, hh); if (shape==DotShape.OCTAGON) break; double c=sqrt(dx*dx+dy*dy), a=(dx*dy)/c, k=((a+5)*dy)/dx, r=sqrt((a+5)*(a+5)+k*k)-dy; double dx1=((r-5)*dx)/dy, dy1=-(((dx+5D)*dy)/dx-dy-r); int x1=(int)(round(dx1)), y1=(int)(round(dy1)); updown+=5; side+=5; poly2=poly; poly=new Polygon(); poly.addPoint(-hw-5, -hh-y1); poly.addPoint(-hw+dx-x1, -hh-dy-5); poly.addPoint(hw-dx+x1, -hh-dy-5); poly.addPoint(hw+5, -hh-y1); poly.addPoint(hw+5, hh+y1); poly.addPoint(hw-dx+x1, hh+dy+5); poly.addPoint(-hw+dx-x1, hh+dy+5); poly.addPoint(-hw-5, hh+y1); if (shape==DotShape.DOUBLE_OCTAGON) break; updown+=5; side+=5; poly3=poly; poly=new Polygon(); x1=(int)(round(dx1*2)); y1=(int)(round(dy1*2)); poly.addPoint(-hw-10, -hh-y1); poly.addPoint(-hw+dx-x1, -hh-dy-10); poly.addPoint(hw-dx+x1, -hh-dy-10); poly.addPoint(hw+10, -hh-y1); poly.addPoint(hw+10, hh+y1); poly.addPoint(hw-dx+x1, hh+dy+10); poly.addPoint(-hw+dx-x1, hh+dy+10); poly.addPoint(-hw-10, hh+y1); break; } case M_CIRCLE: case CIRCLE: case DOUBLE_CIRCLE: { int radius = ((int) (sqrt( hw*((double)hw) + ((double)hh)*hh ))) + 2; if (shape==DotShape.DOUBLE_CIRCLE) radius=radius+5; int L = ((int) (radius / cos18))+2, a = (int) (L * sin36), b = (int) (L * cos36), c = (int) (radius * tan18); poly.addPoint(-L,0); poly.addPoint(-b,a); poly.addPoint(-c,L); poly.addPoint(c,L); poly.addPoint(b,a); poly.addPoint(L,0); poly.addPoint(b,-a); poly.addPoint(c,-L); poly.addPoint(-c,-L); poly.addPoint(-b,-a); updown=L; side=L; break; } case EGG: case ELLIPSE: { int pad = ad/2; side+=pad; updown+=pad; int d = (shape==DotShape.ELLIPSE) ? 0 : (ad/2); GeneralPath path=new GeneralPath(); path.moveTo(-side,d); path.quadTo(-side,-updown,0,-updown); path.quadTo(side,-updown,side,d); path.quadTo(side,updown,0,updown); path.quadTo(-side,updown,-side,d); path.closePath(); this.poly=path; } default: { // BOX if (shape!=DotShape.BOX) { int d=ad/2; hw=hw+d; side=hw; hh=hh+d; updown=hh; } poly.addPoint(-hw,-hh); poly.addPoint(hw,-hh); poly.addPoint(hw,hh); poly.addPoint(-hw,hh); } } if (shape!=DotShape.EGG && shape!=DotShape.ELLIPSE) this.poly = poly; for(int i=0; i<selfs.size(); i++) { if (i==0) { reserved=side+selfLoopA; continue; } String label = selfs.get(i-1).label(); reserved=reserved+(int)(getBounds(false,label).getWidth())+selfLoopGL+selfLoopGR; } if (reserved>0) { String label = selfs.get(selfs.size()-1).label(); reserved=reserved+(int)(getBounds(false,label).getWidth())+selfLoopGL+selfLoopGR; } } //=================================================================================================== /** Returns a DOT representation of this node (or "" if this is a dummy node) */ @Override public String toString() { if (shape == null) return ""; // This means it's a virtual node int rgb = color.getRGB() & 0xFFFFFF; String text = (rgb==0xFF0000 || rgb==0x0000FF || rgb==0) ? "FFFFFF" : "000000"; String main = Integer.toHexString(rgb); while(main.length() < 6) { main = "0" + main; } StringBuilder out = new StringBuilder(); out.append("\"N" + pos + "\""); out.append(" ["); out.append("uuid=\""); if (uuid!=null) out.append(esc(uuid.toString())); out.append("\", label=\""); boolean first = true; if (labels != null) for(String label: labels) if (label.length() > 0) { out.append((first ? "" : "\\n") + esc(label)); first = false; } out.append("\", color=\"#" + main + "\""); out.append(", fontcolor = \"#" + text + "\""); out.append(", shape = \"" + shape.getDotText() + "\""); out.append(", style = \"filled, " + style.getDotText() + "\""); out.append("]\n"); return out.toString(); } }