package att.grappa; import java.awt.*; import java.util.*; /** * This class provides the basis for the drawing of edges. * Extensions of this class and its subclasses allow the drawing of edges * to be customized. * * @version 1.1, 18 May 1998; Copyright 1996, 1997, 1998 by AT&T Corp. * @author John Mocenigo, Research @ AT&T Labs */ public class DrawEdge extends DrawObject { int lineStyle = Grappa.LINE_SOLID; int lineWidth = Grappa.LINE_WIDTH; /** * Arrow head length */ public final static int arrowLength = 10; /** * Arrow head width */ public final static int arrowWidth = 5; int arrowType = -1; /** * Default edge representation class name. */ public final static String defaultEdgeClassName = "Line"; private Point ePoint = null; private Point sPoint = null; private LineVector linePoints = null; /** * The spline corresponding to the supplied control points (for splines). */ BezierSpline spline = null; /** * Indicator for spline (bezier) */ protected final static int SPLINE = 1; /** * Indicator for line */ protected final static int LINE = 2; /** * Defines the drawing style of the node; its * value is set when the class object is created */ protected int shape = DrawEdge.SPLINE; // as a default /** * Total number of specified points. */ protected int totalPoints = 0; /** * The actual shape used for drawing. * This value may differ from shape since * setBounds() may alter the value of * drawShape from that * of shape to use based on the number * of supplied points. */ protected int drawShape = shape; private Polygon edgePolygon = null; private Polygon startArrow = null; private Polygon endArrow = null; /** * This constructor creates an uninitialized DrawEdge with a default * set of attributes that it will observe (in its role as an Observer) * * @see java.util.Observer */ public DrawEdge() { //super(); attrOfInterest("lp"); attrOfInterest("pos"); attrOfInterest("style"); } /** * This method to be called when the bounding box needs to be calculated. * * @return a rectangle representing the bounding box of the object in its * original co-ordinates */ public Rectangle setBounds() { Rectangle oldBox = BoundingBox; if(!getBoundsFlag()) return oldBox; BoundingBox = null; if(linePoints == null) { linePoints = new LineVector(); } drawShape = shape; if(linePoints.size() < 4 || (linePoints.size()-1)%3 != 0 || linePoints.isStraight()) { drawShape = DrawEdge.LINE; } totalPoints = 0; if(sPoint != null) totalPoints++; if(ePoint != null) totalPoints++; totalPoints += linePoints.size(); if(totalPoints < 2) { sPoint = null; ePoint = null; linePoints.removeAllElements(); if(getElement().getGraph().isDirected()) { sPoint = pointForTuple(((Edge)getElement()).getHead().getAttributeValue("pos")); linePoints.addPoint(pointForTuple(((Edge)getElement()).getTail().getAttributeValue("pos"))); } else { linePoints.addPoint(pointForTuple(((Edge)getElement()).getHead().getAttributeValue("pos"))); linePoints.addPoint(pointForTuple(((Edge)getElement()).getTail().getAttributeValue("pos"))); } } switch(drawShape) { case DrawEdge.SPLINE: if(linePoints != null && linePoints.size() > 0) { if(spline == null) { spline = new BezierSpline(); } spline.setSpline((Vector)linePoints); edgePolygon = computePolygon(sPoint,ePoint,(LineVector)spline); BoundingBox = new Rectangle(edgePolygon.getBounds()); if(startArrow != null) BoundingBox.add(startArrow.getBounds()); if(endArrow != null) BoundingBox.add(endArrow.getBounds()); } break; case DrawEdge.LINE: edgePolygon = computePolygon(sPoint,ePoint,linePoints); BoundingBox = new Rectangle(edgePolygon.getBounds()); if(startArrow != null) BoundingBox.add(startArrow.getBounds()); if(endArrow != null) BoundingBox.add(endArrow.getBounds()); break; } if(getTextLabel() != null) { if(BoundingBox != null) { BoundingBox.add(getTextLabel().getBounds()); } else { BoundingBox = new Rectangle(getTextLabel().getBounds()); } } /* * Rectangle bbox = BoundingBox.getBounds(); * int fuzz = (int)Math.round(DrawObject.fudgeFactor); * BoundingBox.setBounds(bbox.x - fuzz, bbox.y - fuzz, bbox.width + 2*fuzz, bbox.height + 2*fuzz); */ setBoundsFlag(false); needSetup(); return oldBox; } /** * Returns the polygon enclosing the edge. * * @return the edge polygon. */ public Polygon getEdgePolygon() { setBounds(); return edgePolygon; } /** * Gets the polygon representing the arrow at the head-end of the edge. * * @return the outline of the head-end arrow or null. */ public Polygon getStartArrow() { setBounds(); return startArrow; } /** * Gets the polygon representing the arrow at the tail-end of the edge. * * @return the outline of the tail-end arrow or null. */ public Polygon getEndArrow() { setBounds(); return endArrow; } /** * Checks if the current edge co-ordinates are different from those supplied. * * @param oldSPoint the head-end point to compare * @param oldEPoint the tail-end point to compare * @param oldPoints the control points to compare * * @return true if the points are different, false otherwise */ public boolean pointsChanged(Point oldSPoint, Point oldEPoint, LineVector oldPoints) { if(sPoint != oldSPoint && (sPoint == null || !sPoint.equals(oldSPoint))) { return true; } if(ePoint != oldEPoint && (ePoint == null || !ePoint.equals(oldEPoint))) { return true; } if(linePoints == oldPoints) return false; if(linePoints == null) return true; return !linePoints.equals(oldPoints); } /** * Sets the points describing the edge. * * @param newSPoint the new head-end point * @param newEPoint the new tail-end point * @param newPoints the new control points */ public void setPoints(Point newSPoint, Point newEPoint, LineVector newPoints) { Point oldSPoint = sPoint; Point oldEPoint = ePoint; LineVector oldPoints = linePoints; if (newEPoint != null) { ePoint = new Point(newEPoint.x,newEPoint.y); } else { ePoint = null; } if (newSPoint != null) { sPoint = new Point(newSPoint.x,newSPoint.y); } else { sPoint = null; } linePoints = new LineVector(newPoints.size(),9); Point tmpPoint; for(int i = 0; i < newPoints.size(); i++) { tmpPoint = newPoints.getPointAt(i); linePoints.addPoint(tmpPoint.x,tmpPoint.y); } if(pointsChanged(oldSPoint,oldEPoint,oldPoints)) { setRedrawFlag(true); setBoundsFlag(true); setBounds(); getElement().getGraph().addToBBox(getBounds()); } } /** * Constructs a position string representation of the edge points. * * @return a position string representation of the edge points suitable * for the value of the "pos" attribute */ public String positionString() { StringBuffer posString = new StringBuffer(); if (sPoint != null) { posString.append("s,"); posString.append(sPoint.x); posString.append(","); posString.append(sPoint.y); } if (ePoint != null) { if(posString.length() > 0) posString.append(" "); posString.append("e,"); posString.append(ePoint.x); posString.append(","); posString.append(ePoint.y); } Point aPoint; for (int i = 0; i < linePoints.size(); i++) { aPoint = linePoints.getPointAt(i); if(posString.length() > 0) posString.append(" "); posString.append(aPoint.x); posString.append(","); posString.append(aPoint.y); } return posString.toString(); } /** * Compute the polygon that encloses the edge. * The arrow heads, if any, are not included. * * @return a polygon representing the edge */ Polygon computePolygon(Point sp, Point ep, LineVector pts) { Point pt1 = null, pt2 = null; Point spt1 = null, spt2 = null; int startOffset = 0; int endOffset = 0; startArrow = null; endArrow = null; if(sp != null) { startOffset = 1; } if(ep != null) { endOffset = 1; } int npoints = 2 * (pts.size() + startOffset + endOffset); int[] xpoints = new int[npoints]; int[] ypoints = new int[npoints]; Point np = null; if(sp != null) { try { np = pts.firstPoint(); } catch(java.util.NoSuchElementException nse) { np = ep; } startArrow = computeArrow(sp,np,lineWidth,true); if(lineWidth == 1) { xpoints[0] = startArrow.xpoints[4]; ypoints[0] = startArrow.ypoints[4]; } else { xpoints[0] = startArrow.xpoints[0]; ypoints[0] = startArrow.ypoints[0]; } xpoints[npoints-1] = startArrow.xpoints[4]; ypoints[npoints-1] = startArrow.ypoints[4]; } if(ep != null) { try { np = pts.lastPoint(); } catch(java.util.NoSuchElementException nse) { np = sp; } endArrow = computeArrow(ep,np,lineWidth,false); if(lineWidth == 1) { xpoints[startOffset+pts.size()] = endArrow.xpoints[4]; ypoints[startOffset+pts.size()] = endArrow.ypoints[4]; } else { xpoints[startOffset+pts.size()] = endArrow.xpoints[0]; ypoints[startOffset+pts.size()] = endArrow.ypoints[0]; } xpoints[startOffset+pts.size()+1] = endArrow.xpoints[4]; ypoints[startOffset+pts.size()+1] = endArrow.ypoints[4]; } Point leftPt = new Point(); Point rightPt = new Point(); if(pts.size() > 1) { double lastAngle = -361; for(int i = 1; i < pts.size(); i++) { lastAngle = computePoints(pts.getPointAt(i-1),pts.getPointAt(i),lastAngle,lineWidth,leftPt,rightPt); xpoints[(i+startOffset)-1] = leftPt.x; ypoints[(i+startOffset)-1] = leftPt.y; xpoints[(npoints-startOffset)-i] = rightPt.x; ypoints[(npoints-startOffset)-i] = rightPt.y; } computePoints(pts.getPointAt(pts.size()-1),pts.getPointAt(pts.size()-2),-361.0,lineWidth,leftPt,rightPt); if(lineWidth == 1) { xpoints[startOffset+pts.size()-1] = leftPt.x; ypoints[startOffset+pts.size()-1] = leftPt.y; } else { xpoints[startOffset+pts.size()-1] = rightPt.x; ypoints[startOffset+pts.size()-1] = rightPt.y; } xpoints[startOffset+pts.size()+2*endOffset] = leftPt.x; ypoints[startOffset+pts.size()+2*endOffset] = leftPt.y; } else { Point ptA = null; Point ptB = null; if(pts.size() == 1) { if(sPoint != null) { ptA = sPoint; ptB = pts.lastPoint(); } else { ptA = ePoint; ptB = pts.firstPoint(); } } else { ptA = sPoint; ptB = ePoint; } computePoints(ptA,ptB,-361.0,lineWidth,leftPt,rightPt); xpoints[startOffset] = leftPt.x; ypoints[startOffset] = leftPt.y; xpoints[(npoints-startOffset)-1] = rightPt.x; ypoints[(npoints-startOffset)-1] = rightPt.y; if(lineWidth == 1) { xpoints[startOffset+pts.size()-1] = leftPt.x + (ptB.x-ptA.x); ypoints[startOffset+pts.size()-1] = leftPt.y + (ptB.y-ptA.y); } else { xpoints[startOffset+pts.size()-1] = rightPt.x + (ptB.x-ptA.x); ypoints[startOffset+pts.size()-1] = rightPt.y + (ptB.y-ptA.y); } xpoints[startOffset+pts.size()+2*endOffset] = leftPt.x + (ptB.x-ptA.x); ypoints[startOffset+pts.size()+2*endOffset] = leftPt.y + (ptB.y-ptA.y); } return new Polygon(xpoints,ypoints,npoints); } double computePoints(Point ptA, Point ptB, double lastAngle, int lineWidth, Point leftPt, Point rightPt) { double baseAngle = Math.atan2((double)(ptB.y - ptA.y), (double)(ptB.x-ptA.x)); double theta = 0; if(lastAngle >= -360.0 && lastAngle <= 360.0) { theta = (baseAngle + lastAngle) / 2.0; } else { theta = baseAngle; } double widone = (double)Math.round((double)lineWidth * 0.5 - 0.001); double widtwo; if(lineWidth%2 != 0) { widtwo = widone + 1.0; if(lineWidth == 1) widtwo+=2.0; } else { widtwo = widone; } leftPt.x = ptA.x + (int)Math.round(widone * Math.sin(theta)); leftPt.y = ptA.y - (int)Math.round(widone * Math.cos(theta)); rightPt.x = ptA.x - (int)Math.round(widtwo * Math.sin(theta)); rightPt.y = ptA.y + (int)Math.round(widtwo * Math.cos(theta)); return baseAngle; } Polygon computeArrow(Point tipPt, Point midBase, int lineWidth, boolean atStart) { int[] xpoints = new int[5]; int[] ypoints = new int[5]; double theta = Math.atan2((double)(midBase.y - tipPt.y), (double)(midBase.x-tipPt.x)); double len = arrowLength; double wid = (double)Math.round((double)(arrowWidth + 2 * lineWidth + 1) * 0.5); xpoints[3] = tipPt.x + (int)Math.round(len * Math.cos(theta) + wid * Math.sin(theta)); ypoints[3] = tipPt.y + (int)Math.round(len * Math.sin(theta) - wid * Math.cos(theta)); xpoints[2] = tipPt.x; ypoints[2] = tipPt.y; xpoints[1] = tipPt.x + (int)Math.round(len * Math.cos(theta) - wid * Math.sin(theta)); ypoints[1] = tipPt.y + (int)Math.round(len * Math.sin(theta) + wid * Math.cos(theta)); double widone = (double)Math.round((double)lineWidth * 0.5 - 0.001); double widtwo; if(lineWidth%2 != 0) { widtwo = widone + 1.0; if(lineWidth == 1) widtwo+=2.0; } else { widtwo = widone; } xpoints[4] = tipPt.x + (int)Math.round(len * Math.cos(theta) + widone * Math.sin(theta)); ypoints[4] = tipPt.y + (int)Math.round(len * Math.sin(theta) - widone * Math.cos(theta)); xpoints[0] = tipPt.x + (int)Math.round(len * Math.cos(theta) - widtwo * Math.sin(theta)); ypoints[0] = tipPt.y + (int)Math.round(len * Math.sin(theta) + widtwo * Math.cos(theta)); return new Polygon(xpoints,ypoints,5); } /** * This method is called whenever an observed Attribute is changed. * It is required by the Observer interface. * * @param obs the observable object that has been updated * @param arg when not null, it indicates that obs need no longer be * observed and in its place arg should be observed. */ public void update(Observable obs, Object arg) { // begin boilerplate if(!(obs instanceof Attribute)) { throw new IllegalArgumentException("expected to be observing attributes only (obs)"); } Attribute attr = (Attribute)obs; if(arg != null) { if(!(arg instanceof Attribute)) { throw new IllegalArgumentException("expected to be observing attributes only (arg)"); } attr.deleteObserver(this); attr = (Attribute)arg; attr.addObserver(this); // in case we call: super.update(obs,arg) obs = attr; arg = null; } // end boilerplate if(attr.getNameHash() == DrawObject.POS_HASH) { if(emptyMeansRemove(attr)) return; Point oldSPoint = sPoint; Point oldEPoint = ePoint; LineVector oldPoints = linePoints; String points = attr.getValue(); Point newPoint = null; arrowType = Grappa.ARROW_NONE; sPoint = null; ePoint = null; linePoints = new LineVector(4,9); int space = 0; if(points.startsWith("s")) { arrowType |= Grappa.ARROW_FIRST; space = points.indexOf(' ', 2); if(space < 0) { sPoint = pointForTuple(points.substring(2)); points = ""; } else { sPoint = pointForTuple(points.substring(2,space)); points = points.substring(space+1); } } if(points.startsWith("e")) { arrowType |= Grappa.ARROW_LAST; space = points.indexOf(' ', 2); if(space < 0) { ePoint = pointForTuple(points.substring(2)); points = ""; } else { ePoint = pointForTuple(points.substring(2,space)); points = points.substring(space+1); } } if(points.startsWith("s")) { arrowType |= Grappa.ARROW_FIRST; space = points.indexOf(' ', 2); if(space < 0) { sPoint = pointForTuple(points.substring(2)); points = ""; } else { sPoint = pointForTuple(points.substring(2,space)); points = points.substring(space+1); } } while (points.length() > 0) { space = points.indexOf(' '); if(space < 0) { newPoint = pointForTuple(points); points = ""; } else { newPoint = pointForTuple(points.substring(0,space)); points = points.substring(space+1); } linePoints.addPoint(newPoint); } if(pointsChanged(oldSPoint,oldEPoint,oldPoints)) { setRedrawFlag(true); setBoundsFlag(true); getElement().getGraph().addToBBox(getBounds()); } return; } else if(attr.getNameHash() == DrawObject.STYLE_HASH) { if(emptyMeansRemove(attr)) return; int oldStyle = lineStyle; int oldWidth = lineWidth; boolean inParens = false; lineStyle = Grappa.LINE_SOLID; lineWidth = Grappa.LINE_WIDTH; Vector styles = parseStyle(attr.getValue()); String token = null; for(int i = 0; i < styles.size(); i++) { token = (String)styles.elementAt(i); if(token.equals(Grappa.LINE_SOLID_STRING)) { lineStyle = Grappa.LINE_SOLID; } else if(token.equals(Grappa.LINE_DASHED_STRING)) { lineStyle = Grappa.LINE_DASHED; } else if(token.equals(Grappa.LINE_DOTTED_STRING)) { lineStyle = Grappa.LINE_DOTTED; } else if(token.equals(Grappa.LINE_WIDTH_STRING)) { token = (String)styles.elementAt(++i); if(token.equals("(")) { token = (String)styles.elementAt(++i); try { lineWidth = Integer.valueOf(token).intValue(); token = (String)styles.elementAt(++i); } catch(Exception nfe) { // Number format or array bounds getElement().getGraph().printError("style line width format error"); lineWidth = oldWidth; } } } } if(lineStyle != oldStyle || lineWidth != oldWidth) { setRedrawFlag(true); setBoundsFlag(true); } return; } // if we got to here, then let the super class have a crack at it super.update(obs,arg); } /** * Creates the drawing peer specific for this object and the specified pane. * * @param pane the DrawPane upon which the object will be drawn. */ public void createPeer(DrawPane pane) { if(pane == null) { throw new IllegalArgumentException("supplied DrawPane cannot be null"); } // references to and from the Peer are set during its init, so // no need to capture the new peer new DrawEdgePeer(this, pane); return; } /** * Get the line width value of the edge. * * @return the line width */ public int getLineWidth() { return lineWidth; } /** * Get the line style value of the edge. * * @return the style width */ public int getLineStyle() { return lineStyle; } }