Ptidej-v5.2-master/POM Tests/data/ArgoUML_0.28/src/org/argouml/uml/diagram/ui/PathItemPlacement.java
// $Id: PathItemPlacement.java 16856 2009-03-04 20:19:03Z dthompson $
// 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 */
    
}
