package prefuse.render; import java.awt.BasicStroke; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.CubicCurve2D; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import prefuse.Constants; import prefuse.util.ColorLib; import prefuse.util.GraphicsLib; import prefuse.util.StrokeLib; import prefuse.visual.EdgeItem; import prefuse.visual.VisualItem; /** * <p>Renderer that draws edges as lines connecting nodes. Both * straight and curved lines are supported. Curved lines are drawn using * cubic Bezier curves. Subclasses can override the * {@link #getCurveControlPoints(EdgeItem, Point2D[], double, double, double, double)} * method to provide custom control point assignment for such curves.</p> * * <p>This class also supports arrows for directed edges. See the * {@link #setArrowType(int)} method for more.</p> * * @version 1.0 * @author <a href="http://jheer.org">jeffrey heer</a> */ public class EdgeRenderer extends AbstractShapeRenderer { public static final String EDGE_TYPE = "edgeType"; protected static final double HALF_PI = Math.PI / 2; protected Line2D m_line = new Line2D.Float(); protected CubicCurve2D m_cubic = new CubicCurve2D.Float(); protected int m_edgeType = Constants.EDGE_TYPE_LINE; protected int m_xAlign1 = Constants.CENTER; protected int m_yAlign1 = Constants.CENTER; protected int m_xAlign2 = Constants.CENTER; protected int m_yAlign2 = Constants.CENTER; protected double m_width = 1; protected float m_curWidth = 1; protected Point2D m_tmpPoints[] = new Point2D[2]; protected Point2D m_ctrlPoints[] = new Point2D[2]; protected Point2D m_isctPoints[] = new Point2D[2]; // arrow head handling protected int m_edgeArrow = Constants.EDGE_ARROW_FORWARD; protected int m_arrowWidth = 8; protected int m_arrowHeight = 12; protected Polygon m_arrowHead = updateArrowHead( m_arrowWidth, m_arrowHeight); protected AffineTransform m_arrowTrans = new AffineTransform(); protected Shape m_curArrow; /** * Create a new EdgeRenderer. */ public EdgeRenderer() { m_tmpPoints[0] = new Point2D.Float(); m_tmpPoints[1] = new Point2D.Float(); m_ctrlPoints[0] = new Point2D.Float(); m_ctrlPoints[1] = new Point2D.Float(); m_isctPoints[0] = new Point2D.Float(); m_isctPoints[1] = new Point2D.Float(); } /** * Create a new EdgeRenderer with the given edge type. * @param edgeType the edge type, one of * {@link prefuse.Constants#EDGE_TYPE_LINE} or * {@link prefuse.Constants#EDGE_TYPE_CURVE}. */ public EdgeRenderer(int edgeType) { this(edgeType, Constants.EDGE_ARROW_FORWARD); } /** * Create a new EdgeRenderer with the given edge and arrow types. * @param edgeType the edge type, one of * {@link prefuse.Constants#EDGE_TYPE_LINE} or * {@link prefuse.Constants#EDGE_TYPE_CURVE}. * @param arrowType the arrow type, one of * {@link prefuse.Constants#EDGE_ARROW_FORWARD}, * {@link prefuse.Constants#EDGE_ARROW_REVERSE}, or * {@link prefuse.Constants#EDGE_ARROW_NONE}. * @see #setArrowType(int) */ public EdgeRenderer(int edgeType, int arrowType) { this(); setEdgeType(edgeType); setArrowType(arrowType); } /** * @see prefuse.render.AbstractShapeRenderer#getRenderType(prefuse.visual.VisualItem) */ public int getRenderType(VisualItem item) { return RENDER_TYPE_DRAW; } /** * @see prefuse.render.AbstractShapeRenderer#getRawShape(prefuse.visual.VisualItem) */ protected Shape getRawShape(VisualItem item) { EdgeItem edge = (EdgeItem)item; VisualItem item1 = edge.getSourceItem(); VisualItem item2 = edge.getTargetItem(); int type = m_edgeType; getAlignedPoint(m_tmpPoints[0], item1.getBounds(), m_xAlign1, m_yAlign1); getAlignedPoint(m_tmpPoints[1], item2.getBounds(), m_xAlign2, m_yAlign2); m_curWidth = (float)(m_width * getLineWidth(item)); // create the arrow head, if needed EdgeItem e = (EdgeItem)item; if ( e.isDirected() && m_edgeArrow != Constants.EDGE_ARROW_NONE ) { // get starting and ending edge endpoints boolean forward = (m_edgeArrow == Constants.EDGE_ARROW_FORWARD); Point2D start = null, end = null; start = m_tmpPoints[forward?0:1]; end = m_tmpPoints[forward?1:0]; // compute the intersection with the target bounding box VisualItem dest = forward ? e.getTargetItem() : e.getSourceItem(); int i = GraphicsLib.intersectLineRectangle(start, end, dest.getBounds(), m_isctPoints); if ( i > 0 ) end = m_isctPoints[0]; // create the arrow head shape AffineTransform at = getArrowTrans(start, end, m_curWidth); m_curArrow = at.createTransformedShape(m_arrowHead); // update the endpoints for the edge shape // need to bias this by arrow head size Point2D lineEnd = m_tmpPoints[forward?1:0]; lineEnd.setLocation(0, -m_arrowHeight); at.transform(lineEnd, lineEnd); } else { m_curArrow = null; } // create the edge shape Shape shape = null; double n1x = m_tmpPoints[0].getX(); double n1y = m_tmpPoints[0].getY(); double n2x = m_tmpPoints[1].getX(); double n2y = m_tmpPoints[1].getY(); switch ( type ) { case Constants.EDGE_TYPE_LINE: m_line.setLine(n1x, n1y, n2x, n2y); shape = m_line; break; case Constants.EDGE_TYPE_CURVE: getCurveControlPoints(edge, m_ctrlPoints,n1x,n1y,n2x,n2y); m_cubic.setCurve(n1x, n1y, m_ctrlPoints[0].getX(), m_ctrlPoints[0].getY(), m_ctrlPoints[1].getX(), m_ctrlPoints[1].getY(), n2x, n2y); shape = m_cubic; break; default: throw new IllegalStateException("Unknown edge type"); } // return the edge shape return shape; } /** * @see prefuse.render.Renderer#render(java.awt.Graphics2D, prefuse.visual.VisualItem) */ public void render(Graphics2D g, VisualItem item) { // render the edge line super.render(g, item); // render the edge arrow head, if appropriate if ( m_curArrow != null ) { g.setPaint(ColorLib.getColor(item.getFillColor())); g.fill(m_curArrow); } } /** * Returns an affine transformation that maps the arrowhead shape * to the position and orientation specified by the provided * line segment end points. */ protected AffineTransform getArrowTrans(Point2D p1, Point2D p2, double width) { m_arrowTrans.setToTranslation(p2.getX(), p2.getY()); m_arrowTrans.rotate(-HALF_PI + Math.atan2(p2.getY()-p1.getY(), p2.getX()-p1.getX())); if ( width > 1 ) { double scalar = width/4; m_arrowTrans.scale(scalar, scalar); } return m_arrowTrans; } /** * Update the dimensions of the arrow head, creating a new * arrow head if necessary. The return value is also set * as the member variable <code>m_arrowHead</code> * @param w the width of the untransformed arrow head base, in pixels * @param h the height of the untransformed arrow head, in pixels * @return the untransformed arrow head shape */ protected Polygon updateArrowHead(int w, int h) { if ( m_arrowHead == null ) { m_arrowHead = new Polygon(); } else { m_arrowHead.reset(); } m_arrowHead.addPoint(0, 0); m_arrowHead.addPoint(-w/2, -h); m_arrowHead.addPoint( w/2, -h); m_arrowHead.addPoint(0, 0); return m_arrowHead; } /** * @see prefuse.render.AbstractShapeRenderer#getTransform(prefuse.visual.VisualItem) */ protected AffineTransform getTransform(VisualItem item) { return null; } /** * @see prefuse.render.Renderer#locatePoint(java.awt.geom.Point2D, prefuse.visual.VisualItem) */ public boolean locatePoint(Point2D p, VisualItem item) { Shape s = getShape(item); if ( s == null ) { return false; } else { double width = Math.max(2, getLineWidth(item)); double halfWidth = width/2.0; return s.intersects(p.getX()-halfWidth, p.getY()-halfWidth, width,width); } } /** * @see prefuse.render.Renderer#setBounds(prefuse.visual.VisualItem) */ public void setBounds(VisualItem item) { if ( !m_manageBounds ) return; Shape shape = getShape(item); if ( shape == null ) { item.setBounds(item.getX(), item.getY(), 0, 0); return; } GraphicsLib.setBounds(item, shape, getStroke(item)); if ( m_curArrow != null ) { Rectangle2D bbox = (Rectangle2D)item.get(VisualItem.BOUNDS); Rectangle2D.union(bbox, m_curArrow.getBounds2D(), bbox); } } /** * Returns the line width to be used for this VisualItem. By default, * returns the base width value set using the {@link #setDefaultLineWidth(double)} * method, scaled by the item size returned by * {@link VisualItem#getSize()}. Subclasses can override this method to * perform custom line width determination, however, the preferred * method is to change the item size value itself. * @param item the VisualItem for which to determine the line width * @return the desired line width, in pixels */ protected double getLineWidth(VisualItem item) { return item.getSize(); } /** * Returns the stroke value returned by {@link VisualItem#getStroke()}, * scaled by the current line width * determined by the {@link #getLineWidth(VisualItem)} method. Subclasses * may override this method to perform custom stroke assignment, but should * respect the line width paremeter stored in the {@link #m_curWidth} * member variable, which caches the result of <code>getLineWidth</code>. * @see prefuse.render.AbstractShapeRenderer#getStroke(prefuse.visual.VisualItem) */ protected BasicStroke getStroke(VisualItem item) { return StrokeLib.getDerivedStroke(item.getStroke(), m_curWidth); } /** * Determines the control points to use for cubic (Bezier) curve edges. * Override this method to provide custom curve specifications. * To reduce object initialization, the entries of the Point2D array are * already initialized, so use the <tt>Point2D.setLocation()</tt> method rather than * <tt>new Point2D.Double()</tt> to more efficiently set custom control points. * @param eitem the EdgeItem we are determining the control points for * @param cp array of Point2D's (length >= 2) in which to return the control points * @param x1 the x co-ordinate of the first node this edge connects to * @param y1 the y co-ordinate of the first node this edge connects to * @param x2 the x co-ordinate of the second node this edge connects to * @param y2 the y co-ordinate of the second node this edge connects to */ protected void getCurveControlPoints(EdgeItem eitem, Point2D[] cp, double x1, double y1, double x2, double y2) { double dx = x2-x1, dy = y2-y1; cp[0].setLocation(x1+2*dx/3,y1); cp[1].setLocation(x2-dx/8,y2-dy/8); } /** * Helper method, which calculates the top-left co-ordinate of a rectangle * given the rectangle's alignment. */ protected static void getAlignedPoint(Point2D p, Rectangle2D r, int xAlign, int yAlign) { double x = r.getX(), y = r.getY(), w = r.getWidth(), h = r.getHeight(); if ( xAlign == Constants.CENTER ) { x = x+(w/2); } else if ( xAlign == Constants.RIGHT ) { x = x+w; } if ( yAlign == Constants.CENTER ) { y = y+(h/2); } else if ( yAlign == Constants.BOTTOM ) { y = y+h; } p.setLocation(x,y); } /** * Returns the type of the drawn edge. This is one of * {@link prefuse.Constants#EDGE_TYPE_LINE} or * {@link prefuse.Constants#EDGE_TYPE_CURVE}. * @return the edge type */ public int getEdgeType() { return m_edgeType; } /** * Sets the type of the drawn edge. This must be one of * {@link prefuse.Constants#EDGE_TYPE_LINE} or * {@link prefuse.Constants#EDGE_TYPE_CURVE}. * @param type the new edge type */ public void setEdgeType(int type) { if ( type < 0 || type >= Constants.EDGE_TYPE_COUNT ) throw new IllegalArgumentException( "Unrecognized edge curve type: "+type); m_edgeType = type; } /** * Returns the type of the drawn edge. This is one of * {@link prefuse.Constants#EDGE_ARROW_FORWARD}, * {@link prefuse.Constants#EDGE_ARROW_REVERSE}, or * {@link prefuse.Constants#EDGE_ARROW_NONE}. * @return the edge type */ public int getArrowType() { return m_edgeArrow; } /** * Sets the type of the drawn edge. This is either * {@link prefuse.Constants#EDGE_ARROW_NONE} for no edge arrows, * {@link prefuse.Constants#EDGE_ARROW_FORWARD} for arrows from source to * target on directed edges, or * {@link prefuse.Constants#EDGE_ARROW_REVERSE} for arrows from target to * source on directed edges. * @param type the new arrow type */ public void setArrowType(int type) { if ( type < 0 || type >= Constants.EDGE_ARROW_COUNT ) throw new IllegalArgumentException( "Unrecognized edge arrow type: "+type); m_edgeArrow = type; } /** * Sets the dimensions of an arrow head for a directed edge. This specifies * the pixel dimensions when both the zoom level and the size factor * (a combination of item size value and default stroke width) are 1.0. * @param width the untransformed arrow head width, in pixels. This * specifies the span of the base of the arrow head. * @param height the untransformed arrow head height, in pixels. This * specifies the distance from the point of the arrow to its base. */ public void setArrowHeadSize(int width, int height) { m_arrowWidth = width; m_arrowHeight = height; m_arrowHead = updateArrowHead(width, height); } /** * Get the height of the untransformed arrow head. This is the distance, * in pixels, from the tip of the arrow to its base. * @return the default arrow head height */ public int getArrowHeadHeight() { return m_arrowHeight; } /** * Get the width of the untransformed arrow head. This is the length, * in pixels, of the base of the arrow head. * @return the default arrow head width */ public int getArrowHeadWidth() { return m_arrowWidth; } /** * Get the horizontal aligment of the edge mount point with the first node. * @return the horizontal alignment, one of {@link prefuse.Constants#LEFT}, * {@link prefuse.Constants#RIGHT}, or {@link prefuse.Constants#CENTER}. */ public int getHorizontalAlignment1() { return m_xAlign1; } /** * Get the vertical aligment of the edge mount point with the first node. * @return the vertical alignment, one of {@link prefuse.Constants#TOP}, * {@link prefuse.Constants#BOTTOM}, or {@link prefuse.Constants#CENTER}. */ public int getVerticalAlignment1() { return m_yAlign1; } /** * Get the horizontal aligment of the edge mount point with the second * node. * @return the horizontal alignment, one of {@link prefuse.Constants#LEFT}, * {@link prefuse.Constants#RIGHT}, or {@link prefuse.Constants#CENTER}. */ public int getHorizontalAlignment2() { return m_xAlign2; } /** * Get the vertical aligment of the edge mount point with the second node. * @return the vertical alignment, one of {@link prefuse.Constants#TOP}, * {@link prefuse.Constants#BOTTOM}, or {@link prefuse.Constants#CENTER}. */ public int getVerticalAlignment2() { return m_yAlign2; } /** * Set the horizontal aligment of the edge mount point with the first node. * @param align the horizontal alignment, one of * {@link prefuse.Constants#LEFT}, {@link prefuse.Constants#RIGHT}, or * {@link prefuse.Constants#CENTER}. */ public void setHorizontalAlignment1(int align) { m_xAlign1 = align; } /** * Set the vertical aligment of the edge mount point with the first node. * @param align the vertical alignment, one of * {@link prefuse.Constants#TOP}, {@link prefuse.Constants#BOTTOM}, or * {@link prefuse.Constants#CENTER}. */ public void setVerticalAlignment1(int align) { m_yAlign1 = align; } /** * Set the horizontal aligment of the edge mount point with the second * node. * @param align the horizontal alignment, one of * {@link prefuse.Constants#LEFT}, {@link prefuse.Constants#RIGHT}, or * {@link prefuse.Constants#CENTER}. */ public void setHorizontalAlignment2(int align) { m_xAlign2 = align; } /** * Set the vertical aligment of the edge mount point with the second node. * @param align the vertical alignment, one of * {@link prefuse.Constants#TOP}, {@link prefuse.Constants#BOTTOM}, or * {@link prefuse.Constants#CENTER}. */ public void setVerticalAlignment2(int align) { m_yAlign2 = align; } /** * Sets the default width of lines. This width value will * be scaled by the value of an item's size data field. The default * base width is 1. * @param w the desired default line width, in pixels */ public void setDefaultLineWidth(double w) { m_width = w; } /** * Gets the default width of lines. This width value that will * be scaled by the value of an item's size data field. The default * base width is 1. * @return the default line width, in pixels */ public double getDefaultLineWidth() { return m_width; } } // end of class EdgeRenderer