/* $Id: PathItemPlacement.java 17865 2010-01-12 20:45:26Z linus $ ***************************************************************************** * Copyright (c) 2009 Contributors - see below * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * dthompson ***************************************************************************** * * Some portions of this file was previously release using the BSD License: */ // Copyright (c) 2008 Tom Morris and other contributors. All // Rights Reserved. Permission to use, copy, modify, and distribute this // software and its documentation without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph appear in all copies. This software program and // documentation are copyrighted by The Contributors. // The software program and documentation are supplied "AS // IS", without any accompanying services from The Contributors. They // do not warrant that the operation of the program will be // uninterrupted or error-free. The end-user understands that the program // was developed for research purposes and is advised not to rely // exclusively on the program for any reason. IN NO EVENT SHALL THE // CONTRIBUTORS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE CONTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. THE CONTRIBUTORS SPECIFICALLY DISCLAIM ANY // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE CONTRIBUTORS // HAVE NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. package org.argouml.uml.diagram.ui; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Line2D; import org.apache.log4j.Logger; import org.tigris.gef.base.Globals; import org.tigris.gef.base.PathConv; import org.tigris.gef.presentation.Fig; import org.tigris.gef.presentation.FigEdge; /** * This class implements the coordinate generation required for GEF's * FigEdge.addPathItem. It can be used to place labels at an offset relative to * an anchor position along the path described by a FigEdge. For example, a * label can be placed in the middle of a FigEdge by using 50% or near an end by * using 0% width an offset of +5 or 100% with an offset of -5. * <p> * The calculated anchor position along the path is then used as a base to which * additional offsets are added. This can be either in the form of a * displacement vector and distance specified using an angle relative to the * angle of the edge at that point or a fixed x,y offset. * <p> * This class tries to avoid placing the itemFig so that it intersects the * pathFig. Note that:<ul> * <li>itemFig must return correct size information for this to work properly, * which is not currently true of all GEF figs (eg. text figs). * <li>Only the path is considered, so you can still get overlaps with the * connected nodes on the ends of the edges or other labels on the same edge or * other figs in the diagram. Using a displacement angle of 135 or -135 degrees * is a good way to help avoid the connected nodes. * </ul> * * @author Tom Morris <tfmorris@gmail.com> * @since 0.27.3 */ public class PathItemPlacement extends PathConv { private static final Logger LOG = Logger.getLogger(PathItemPlacement.class); private boolean useCollisionCheck = true; private boolean useAngle = true; private double angle = 90; // default angle is 90 deg. /** * the fig to be placed. */ private Fig itemFig; /** * Percentage of the way along the path to place anchor. */ private int percent; /** * Fixed delta offset from the computed percentage location. */ private int pathOffset; /** * Distance along the displacement vector (ie distance from the edge) */ private int vectorOffset; /** * Fixed offset to use for manual positioning. Coordinates are interpreted * as an XY offset. */ private Point offset; /** * Set true to keep items on same side (top or bottom) of path as * it rotates through vertical. */ private final boolean swap = true; /** * Construct a new path to coordinate conversion object which positions at a * percentage along a path with a given distance perpendicular to the path * at the anchor point. * * @param pathFig fig representing the edge which will be used for * positioning. * @param theItemFig the fig to be placed. * @param pathPercent distance in integer percentages along path for anchor * point from which the offset is computed.. Beginning of path is * 0 and end of path is 100. * @param displacement distance from the edge to place the fig. This is * computed along the normal. */ public PathItemPlacement(FigEdge pathFig, Fig theItemFig, int pathPercent, int displacement) { this(pathFig, theItemFig, pathPercent, 0, 90, displacement); } /** * Construct a new path to coordinate conversion object which positions * an anchor point on the path at a percentage along a path with an offset, * and from the anchor point at a distance measured at a given angle. * * @param pathFig fig representing the edge which will be used for * positioning. * @param theItemFig the fig to be placed. * @param pathPercent distance in integer percentages along path for anchor * point from which the offset is computed. Beginning of path is * 0 and end of path is 100. * @param pathDelta delta distance in coordinate space units to add to the * computed percentage position * @param displacementAngle angle to add to computed line slope when * computing the displacement vector * @param displacementDistance distance from the edge to place the fig. This * is computed along the normal from the anchor position using * pathPercent & pathDelta. */ public PathItemPlacement(FigEdge pathFig, Fig theItemFig, int pathPercent, int pathDelta, int displacementAngle, int displacementDistance) { super(pathFig); itemFig = theItemFig; setAnchor(pathPercent, pathDelta); setDisplacementVector(displacementAngle + 180, displacementDistance); } /** * Construct a new path to coordinate conversion object which positions * an anchor point on the path at a percentage along a path with an offset, * and from the anchor point at a distance measured in X, Y coordinates. * * @param pathFig fig representing the edge which will be used for * positioning. * @param theItemFig the fig to be placed. * @param pathPercent distance in integer percentages along path for anchor * point from which the offset is computed. Beginning of path is * 0 and end of path is 100. * @param pathDelta delta distance in coordinate space units to add to the * computed percentage position * @param absoluteOffset point representing XY offset from anchor to use for * positioning. */ public PathItemPlacement(FigEdge pathFig, Fig theItemFig, int pathPercent, int pathDelta, Point absoluteOffset) { super(pathFig); itemFig = theItemFig; setAnchor(pathPercent, pathDelta); setAbsoluteOffset(absoluteOffset); } /** * Returns the Fig that this PathItemPlacement places. * To get the Fig of the Edge which owns this fig, use use getPathFig() * @see org.tigris.gef.base.PathConv#getPathFig() * @note Used by PGML.tee. * @return The fig that this path item places. */ public Fig getItemFig() { return itemFig; } /** * Compute a position. This strangely named method computes a * position using the current set of parameters and returns the result * by updating the provided Point. * * @param result Point in which to return result. Not read as input. * * @see org.tigris.gef.base.PathConv#stuffPoint(java.awt.Point) */ public void stuffPoint(Point result) { result = getPosition(result); } /** * Get the computed target position based on the current set of parameters. * * @return the computed position */ public Point getPosition() { return getPosition(new Point()); } @Override public Point getPoint() { return getPosition(); } /** * Get the anchor position. The represents the point along the path that * is used as the starting point for all other calculations. * * @return the anchor position represented by the current percentage and * path offset parameters */ public Point getAnchorPosition() { int pathDistance = getPathDistance(); Point anchor = new Point(); _pathFigure.stuffPointAlongPerimeter(pathDistance, anchor); return anchor; } /** * Compute distance along the path based on percentage and offset, clamped * to the length of the path. * * @return the distance */ private int getPathDistance() { int length = _pathFigure.getPerimeterLength(); int distance = Math.max(0, (length * percent) / 100 + pathOffset); // Boundary condition in GEF, make sure this is LESS THAN, not equal if (distance >= length) { distance = length - 1; } return distance; } /** * Get the computed position based on the current set of parameters. * * @param result Point in which to return result. Not read as input, but it * <em>is</em> modified. * @return the updated point */ private Point getPosition(Point result) { Point anchor = getAnchorPosition(); result.setLocation(anchor); // If we're using a fixed offset, just add it and return // No collision detection is done in this case if (!useAngle) { result.translate(offset.x, offset.y); return result; } double slope = getSlope(); result.setLocation(applyOffset(slope, vectorOffset, anchor)); // Check for a collision between our computed position and the edge if (useCollisionCheck) { int increment = 2; // increase offset by 2px at a time // TODO: The size of text figs, which is what we care about most, // isn't computed correctly by GEF. If we got ambitious, we could // recompute a proper size ourselves. Dimension size = new Dimension(itemFig.getWidth(), itemFig .getHeight()); // Get the points representing the poly line for our edge FigEdge fp = (FigEdge) _pathFigure; Point[] points = fp.getPoints(); if (intersects(points, result, size)) { // increase offset by increments until we're clear int scaledOffset = vectorOffset + increment; int limit = 20; int count = 0; // limit our retries in case its too hard to get free while (intersects(points, result, size) && count++ < limit) { result.setLocation( applyOffset(slope, scaledOffset, anchor)); scaledOffset += increment; } // If we timed out, give it one more try on the other side if (false /* count >= limit */) { LOG.debug("Retry limit exceeded. Trying other side"); result.setLocation(anchor); // TODO: This works for 90 degree angles, but is suboptimal // for other angles. It should reflect the angle, rather // than just using a negative offset along the same vector result.setLocation( applyOffset(slope, -vectorOffset, anchor)); count = 0; scaledOffset = -scaledOffset; while (intersects(points, result, size) && count++ < limit) { result.setLocation( applyOffset(slope, scaledOffset, anchor)); scaledOffset += increment; } } // LOG.debug("Final point #" + count + " " + result // + " offset of " + scaledOffset); } } return result; } /** * Check for intersection between the segments of a poly line and a * rectangle. Unlike FigEdge.intersects(), this only checks the main * path, not any associated path items (like ourselves). * * @param points set of points representing line segments * @param center position of center * @param size size of bounding box * @return true if they intersect */ private boolean intersects(Point[] points, Point center, Dimension size) { // Convert to bounding box // Very screwy! GEF sometimes uses center and sometimes upper left // TODO: GEF also positions text at the nominal baseline which is // well inside the bounding box and gives the overall size incorrectly Rectangle r = new Rectangle(center.x - (size.width / 2), center.y - (size.height / 2), size.width, size.height); Line2D line = new Line2D.Double(); for (int i = 0; i < points.length - 1; i++) { line.setLine(points[i], points[i + 1]); if (r.intersectsLine(line)) { return true; } } return false; } /** * Convenience method to set anchor percentage distance and offset. * * @param newPercent distance as a percent of total path 0<=percent<=100 * @param newOffset offset in drawing coordinate system */ public void setAnchor(int newPercent, int newOffset) { setAnchorPercent(newPercent); setAnchorOffset(newOffset); } /** * Set distance along path of anchor in integer percentages. * @param newPercent distance as a percent of total path 0<=percent<=100 */ public void setAnchorPercent(int newPercent) { percent = newPercent; } /** * Set offset along path to be applied to anchor after percentage based * location is calculated. Specified in units of the drawing coordinate * system. * * @param newOffset offset in drawing coordinate system */ public void setAnchorOffset(int newOffset) { pathOffset = newOffset; } /** * Set a fixed offset from the anchor point. * @param newOffset a Point who's x & y coordinates will be used as a * displacement from anchor point */ public void setAbsoluteOffset(Point newOffset) { offset = newOffset; useAngle = false; } /** * Attempts to set a new location for the fig being controlled * by this path item. Takes the given Point which represents an x,y * position, and calculates the most appropriate angle and displacement * to achieve this new position. Used when the user drags a label * on the diagram. * @override * @param newPoint The new target location for the PathItem fig. * @see org.tigris.gef.base.PathConv#setPoint(java.awt.Point) */ public void setPoint(Point newPoint) { int vect[] = computeVector(newPoint); setDisplacementAngle(vect[0]); setDisplacementDistance(vect[1]); } /** * Compute an angle and distance which is equivalent to the given point. * This is a convenience method to help callers get coordinates in a form * that can be passed back in using {@link #setDisplacementVector(int, int)} * * @param point the desired target point * @return an array of two integers containing the angle and distance */ public int[] computeVector(Point point) { Point anchor = getAnchorPosition(); int distance = (int) anchor.distance(point); int angl = 0; double pathSlope = getSlope(); double offsetSlope = getSlope(anchor, point); if (swap && pathSlope > Math.PI / 2 && pathSlope < Math.PI * 3 / 2) { angl = -(int) ((offsetSlope - pathSlope) / Math.PI * 180); } else { angl = (int) ((offsetSlope - pathSlope) / Math.PI * 180); } int[] result = new int[] {angl, distance}; return result; } /** * Set the displacement vector to the given angle and distance. * * @param vectorAngle angle in degrees relative to the edge at the anchor * point. * @param vectorDistance distance along vector in drawing coordinate units */ public void setDisplacementVector(int vectorAngle, int vectorDistance) { setDisplacementAngle(vectorAngle); setDisplacementDistance(vectorDistance); } /** * Set the displacement vector to the given angle and distance. * * @param vectorAngle angle in degrees relative to the edge at the anchor * point. * @param vectorDistance distance along vector in drawing coordinate units */ public void setDisplacementVector(double vectorAngle, int vectorDistance) { setDisplacementAngle(vectorAngle); setDisplacementDistance(vectorDistance); } /** * @param offsetAngle the new angle for the displacement vector, * specified in degrees relative to the edge at the anchor. */ public void setDisplacementAngle(int offsetAngle) { angle = offsetAngle * Math.PI / 180.0; useAngle = true; } /** * @param offsetAngle the new angle for the displacement vector, * specified in degrees relative to the edge at the anchor. */ public void setDisplacementAngle(double offsetAngle) { angle = offsetAngle * Math.PI / 180.0; useAngle = true; } /** * Set distance along displacement vector to place the figure. * @param newDistance distance in units of the drawing coordinate system */ public void setDisplacementDistance(int newDistance) { vectorOffset = newDistance; useAngle = true; } /** * Don't know what this is supposed to do since GEF has no API spec for it, * but we don't implement it and it'll throw an * UnsupportedOperationException if you try to use it. * * @param newPoint ignored * @see org.tigris.gef.base.PathConv#setClosestPoint(java.awt.Point) */ public void setClosestPoint(Point newPoint) { throw new UnsupportedOperationException(); } /** * Compute slope of path at the anchor point. Slope is computed using a * short segment instead of using the instantaneous slope, so it will give * unusual results near discontinuities in the path (ie bends). * @return the slope radians in the range 0 < slope < 2PI */ private double getSlope() { final int slopeSegLen = 40; // segment size for computing slope int pathLength = _pathFigure.getPerimeterLength(); int pathDistance = getPathDistance(); // Two points for line segment used to compute slope of path here // NOTE that this is the average slope, not instantaneous, so it will // give screwy results near bends in the path int d1 = Math.max(0, pathDistance - slopeSegLen / 2); // If our position was clamped, try to make it up on the other end int d2 = Math.min(pathLength - 1, d1 + slopeSegLen); // Can't get the slope of a point. Just return an arbitrary point. if (d1 == d2) { return 0; } Point p1 = _pathFigure.pointAlongPerimeter(d1); Point p2 = _pathFigure.pointAlongPerimeter(d2); double theta = getSlope(p1, p2); return theta; } /** * Compute the slope in radians of the line between two points. * @param p1 first point * @param p2 second point * @return slope in radians in the range 0<=slope<=2PI */ private static double getSlope(Point p1, Point p2) { // Our angle theta is arctan(opposite/adjacent) // Because y increases going down the screen, positive angles are // clockwise rather than counterclockwise int opposite = p2.y - p1.y; int adjacent = p2.x - p1.x; double theta; if (adjacent == 0) { // This shouldn't happen, because of our line segment size check if (opposite == 0) { return 0; } // "We're going vertical!" - Goose in "Top Gun" if (opposite < 0) { theta = Math.PI * 3 / 2; } else { theta = Math.PI / 2; } } else { // Arctan only returns -PI/2 to PI/2 // Handle the other two quadrants and normalize to 0 - 2PI theta = Math.atan((double) opposite / (double) adjacent); // Quadrant II & III if (adjacent < 0) { theta += Math.PI; } // Quadrant IV if (theta < 0) { theta += Math.PI * 2; } } return theta; } /** * Apply an offset for a given distance along the normal vector computed * to the line specified by the two points. * * @param p1 point one of line to use in computing normal vector * @param p2 point two of line to use in computing normal vector * @param theOffset distance to displace fig along normal vector * @param anchor The start point to apply the offset from. Not modified. * @return A new computed point describing the location after the offset * has been applied to the anchor. */ private Point applyOffset(double theta, int theOffset, Point anchor) { Point result = new Point(anchor); // Set the following for some backward compatibility with old algorithm final boolean aboveAndRight = false; // LOG.debug("Slope = " + theta / Math.PI + "PI " // + theta / Math.PI * 180.0); // Add displacement angle to slope if (swap && theta > Math.PI / 2 && theta < Math.PI * 3 / 2) { theta = theta - angle; } else { theta = theta + angle; } // Transform to 0 - 2PI range if we've gone all the way around circle if (theta > Math.PI * 2) { theta -= Math.PI * 2; } if (theta < 0) { theta += Math.PI * 2; } // Compute our deltas int dx = (int) (theOffset * Math.cos(theta)); int dy = (int) (theOffset * Math.sin(theta)); // For backward compatibility everything is above and right // TODO: Do in polar domain? if (aboveAndRight) { dx = Math.abs(dx); dy = -Math.abs(dy); } result.x += dx; result.y += dy; // LOG.debug(result.x + ", " + result.y // + " theta = " + theta * 180 / Math.PI // + " dx = " + dx + " dy = " + dy); return result; } /** * Paint the virtual connection from the edge to where the path item * is placed according to this path item placement algorithm. * * @param g the Graphics object * @see org.tigris.gef.base.PathConv#paint(java.awt.Graphics) */ public void paint(Graphics g) { final Point p1 = getAnchorPosition(); Point p2 = getPoint(); Rectangle r = itemFig.getBounds(); // Load the standard colour, just add an alpha channel. Color c = Globals.getPrefs().handleColorFor(itemFig); c = new Color(c.getRed(), c.getGreen(), c.getBlue(), 100); g.setColor(c); r.grow(2, 2); g.fillRoundRect(r.x, r.y, r.width, r.height, 8, 8); if (r.contains(p2)) { p2 = getRectLineIntersection(r, p1, p2); } g.drawLine(p1.x, p1.y, p2.x, p2.y); } /** * Finds the point where a rectangle and line intersect. * Finds the intersection point between the border of a Rectangle r and * a line drawn between two Points pOut (outside the rectangle) and pIn * (inside the rectangle). * If the pIn is not inside the rectangle, or if any other problem occurs, * pIn is returned. * @param r Rectangle to find the intersection of. * @param pOut Point outside the rectangle. * @param pIn Point inside the rectangle. * @return The intersection between Line(pOut, pIn) and Rectangle r. */ private Point getRectLineIntersection(Rectangle r, Point pOut, Point pIn) { Line2D.Double m, n; m = new Line2D.Double(pOut, pIn); n = new Line2D.Double(r.x, r.y, r.x + r.width, r.y); if (m.intersectsLine(n)) { return intersection(m, n); } n = new Line2D.Double(r.x + r.width, r.y, r.x + r.width, r.y + r.height); if (m.intersectsLine(n)) { return intersection(m, n); } n = new Line2D.Double(r.x, r.y + r.height, r.x + r.width, r.y + r.height); if (m.intersectsLine(n)) { return intersection(m, n); } n = new Line2D.Double(r.x, r.y, r.x, r.y + r.width); if (m.intersectsLine(n)) { return intersection(m, n); } // Should never get here. If we do, return the inner point. LOG.warn("Could not find rectangle intersection, using inner point."); return pIn; } /** * Finds the intersection point of two lines. * It is surprising that this method isn't already available in the base * Line2D class of Java. If a stock method exists or is implemented in * future, feel free replace this code with it. * @param m First line. * @param n Second line. * @return Intersection point of first and second line. */ private Point intersection(Line2D m, Line2D n) { double d = (n.getY2() - n.getY1()) * (m.getX2() - m.getX1()) - (n.getX2() - n.getX1()) * (m.getY2() - m.getY1()); double a = (n.getX2() - n.getX1()) * (m.getY1() - n.getY1()) - (n.getY2() - n.getY1()) * (m.getX1() - n.getX1()); double as = a / d; double x = m.getX1() + as * (m.getX2() - m.getX1()); double y = m.getY1() + as * (m.getY2() - m.getY1()); return new Point((int) x, (int) y); } /** * Returns the value of the percent field - the position of the anchor * point as a percentage of the edge. * @important Used by PGML.tee. * @return The value of the percent field. */ public int getPercent() { return percent; } /** * Returns the value of the angle field converted to degrees. * The angle of the path item relative to the edge. * @important Used by PGML.tee. * @return The value of the angle field in degrees. */ public double getAngle() { return angle * 180 / Math.PI; } /** * Returns the value of the vectorOffset field. * The vectorOffset field is the distance away from the edge, along the * path vector that the item Fig is placed. * @important Used by PGML.tee. * @return The value of the vectorOffset field. */ public int getVectorOffset() { return vectorOffset; } /** End of methods used by PGML.tee */ }