/*
 * @(#)EdgeView.java	1.0 03-JUL-04
 * 
 * Copyright (c) 2001-2004 Gaudenz Alder
 *  
 */
package org.jgraph.graph;

import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.swing.SwingUtilities;

import org.jgraph.JGraph;
import org.jgraph.plaf.GraphUI;
import org.jgraph.plaf.basic.BasicGraphUI;

/**
 * The default implementation of an edge view. The getEdgeRenderer method
 * assumes a renderer of type EdgeRenderer. If you provide a custom renderer to
 * a subclass, you must also override the methods that call this method, namely:
 * getShape, getLabelBounds, getExtraLabelBounds, intersects and getBounds.
 * 
 * @version 1.0 1/1/02
 * @author Gaudenz Alder
 */

public class EdgeView extends AbstractCellView {

	/** Renderer for the class. */
	public static transient EdgeRenderer renderer = new EdgeRenderer();

	/** List of points of the edge. May contain ports. */
	protected List points;

	/** Cached source and target portview of the edge. */
	protected CellView source, target;

	protected CellView sourceParentView, targetParentView;

	/** Cached label position of the edge. */
	protected Point2D labelPosition;

	protected Point2D[] extraLabelPositions;

	protected transient Point2D labelVector = null;

	/** Drawing attributes that are created on the fly */
	public transient Shape beginShape, endShape, lineShape;

	/** Shared-path tune-up. */
	public transient GeneralPath sharedPath = null;

	protected transient Rectangle2D cachedBounds = null;

	/**
	 * Constructs an empty edge view.
	 */
	public EdgeView() {
		super();
	}

	/**
	 * Constructs an edge view for the specified model object.
	 * 
	 * @param cell
	 *            reference to the model object
	 */
	public EdgeView(Object cell) {
		super(cell);
	}

	//
	// Data Source
	//

	/**
	 * Overrides the parent method to udpate the cached points, source and
	 * target port. If the source or target is removed, a point is inserted into
	 * the array of points.
	 */
	public void refresh(GraphLayoutCache cache, CellMapper mapper,
			boolean createDependentViews) {
		// Makes sure the manual control points are passed to
		// the router instead of the cached control points after
		// changes to the edge (normally manual point changes).
		points = null;
		super.refresh(cache, mapper, createDependentViews);
		// Re-sync source- and targetportviews
		GraphModel model = cache.getModel();
		Object modelSource = model.getSource(cell);
		Object modelTarget = model.getTarget(cell);
		setSource(mapper.getMapping(modelSource, createDependentViews));
		setTarget(mapper.getMapping(modelTarget, createDependentViews));
		if (modelSource != null && getSource() == null)
			sourceParentView = getVisibleParent(model, mapper, modelSource);
		else
			sourceParentView = null;
		if (modelTarget != null && getTarget() == null)
			targetParentView = getVisibleParent(model, mapper, modelTarget);
		else
			targetParentView = null;
	}

	protected CellView getVisibleParent(GraphModel model, CellMapper mapper,
			Object port) {
		CellView view = null;
		do {
			view = mapper.getMapping(port, false);
			port = model.getParent(port);
		} while (view == null && port != null);
		return view;
	}

	/**
	 * Update attributes and recurse children.
	 */
	public void update(GraphLayoutCache cache) {
		super.update(cache);
		// Save the reference to the points so they can be changed
		// in-place by use of setPoint, setSource, setTarget methods.
		List controlPoints = GraphConstants.getPoints(allAttributes);
		if (controlPoints == null) {
			controlPoints = new ArrayList(4);
			controlPoints.add(allAttributes.createPoint(10, 10));
			controlPoints.add(allAttributes.createPoint(20, 20));
			GraphConstants.setPoints(allAttributes, controlPoints);
		}

		// Uses the manual control points while the edge is being routed.
		// Otherwise uses the cached points (eg. for preview).
		if (points == null)
			points = controlPoints;

		Edge.Routing routing = GraphConstants.getRouting(allAttributes);
		List routedPoints = null;
		// Passes the current cached points to the router
		if (routing != null)
			routedPoints = routing.route(cache, this);

		// Shadows the manual control points with the
		// routed control points
		points = (routedPoints != null && !routedPoints.isEmpty()) ? routedPoints
				: controlPoints;

		// Overrides manual point locations with the real port views
		if (points == controlPoints) {
			if (source != null)
				setSource(source);
			if (target != null)
				setTarget(target);
		}

		// Checks and caches label positions
		checkDefaultLabelPosition();
		Point2D[] positions = GraphConstants
				.getExtraLabelPositions(allAttributes);
		if (positions != null) {
			extraLabelPositions = new Point2D[positions.length];
			for (int i = 0; i < positions.length; i++)
				extraLabelPositions[i] = positions[i];
		} else
			extraLabelPositions = null;

		// Clear cached shapes
		beginShape = null;
		endShape = null;
		lineShape = null;
		invalidate();
	}

	/**
	 * Hook for subclassers to avoid default label positions.
	 */
	protected void checkDefaultLabelPosition() {
		labelPosition = GraphConstants.getLabelPosition(allAttributes);
		String label = String.valueOf(getCell());
		if (labelPosition == null && label != null && label.length() > 0) {
			int center = GraphConstants.PERMILLE / 2;
			labelPosition = new Point(center, 0);
			GraphConstants.setLabelPosition(allAttributes, labelPosition);
		}
	}

	/**
	 * Resets the cached values of the edge view
	 */
	protected void invalidate() {
		labelVector = null;
		sharedPath = null;
		cachedBounds = null;
	}

	/**
	 * Returns the shape of the view according to the last rendering state
	 */
	public Shape getShape() {
		if (sharedPath != null)
			return sharedPath;
		else {
			return sharedPath = (GeneralPath) getEdgeRenderer().createShape();
		}
	}

	//
	// View Methods
	//

	/**
	 * Returns true if this view intersects the given rectangle.
	 */
	public boolean intersects(JGraph graph, Rectangle2D rect) {
		boolean intersects = super.intersects(graph, rect);
		if (!isLeaf()) {
			return intersects;
		} else if (intersects) {
			Rectangle r = new Rectangle((int) rect.getX(), (int) rect.getY(),
					(int) rect.getWidth(), (int) rect.getHeight());
			return getEdgeRenderer().intersects(graph, this, r);
		}
		return false;
	}

	/**
	 * Returns the location for this edgeview.
	 */
	public Rectangle2D getBounds() {
		Rectangle2D rect = super.getBounds();
		if (rect == null) {
			if (cachedBounds == null) {
				cachedBounds = getEdgeRenderer().getBounds(this);
			}
			rect = cachedBounds;
		}
		return rect;
	}

	/**
	 * Returns the local renderer. Do not access the renderer field directly.
	 * Use this method instead. Note: This method is package private.
	 */
	EdgeRenderer getEdgeRenderer() {
		return (EdgeRenderer) getRenderer();
	}

	/**
	 * Returns a renderer for the class.
	 */
	public CellViewRenderer getRenderer() {
		return renderer;
	}

	/**
	 * Returns a cell handle for the view.
	 */
	public CellHandle getHandle(GraphContext context) {
		return new EdgeHandle(this, context);
	}

	//
	// Cached Values
	//

	/**
	 * Returns the CellView that represents the source of the edge.
	 */
	public CellView getSource() {
		return source;
	}

	public CellView getSourceParentView() {
		return sourceParentView;
	}

	/**
	 * Sets the <code>sourceView</code> of the edge.
	 */
	public void setSource(CellView sourceView) {
		sourceParentView = null;
		source = sourceView;
		if (source != null)
			points.set(0, source);
		else
			points.set(0, getPoint(0));
		invalidate();
	}

	/**
	 * Returns the CellView that represents the target of the edge.
	 */
	public CellView getTarget() {
		return target;
	}

	public CellView getTargetParentView() {
		return targetParentView;
	}

	/**
	 * Sets the <code>targetView</code> of the edge.
	 */
	public void setTarget(CellView targetView) {
		target = targetView;
		targetParentView = null;
		int n = points.size() - 1;
		if (target != null)
			points.set(n, target);
		else
			points.set(n, getPoint(n));
		invalidate();
	}

	/**
	 * Returns a point that describes the position of the label.
	 */
	public Point2D getExtraLabelPosition(int index) {
		return extraLabelPositions[index];
	}

	/**
	 * Returns a point that describes the position of the label.
	 */
	public Point2D getLabelPosition() {
		return labelPosition;
	}

	/**
	 * Sets the description of the label position.
	 */
	public void setLabelPosition(Point2D pos) {
		labelPosition.setLocation(pos);
		invalidate();
	}

	/**
	 * Sets the description of the label position.
	 */
	public void setExtraLabelPosition(int index, Point2D pos) {
		extraLabelPositions[index].setLocation(pos);
		invalidate();
	}

	//
	// Points
	//

	/**
	 * Returns true if the edge is a loop.
	 */
	public boolean isLoop() {
		return (getSource() != null && getSource() == getTarget())
				|| (sourceParentView != null && sourceParentView == targetParentView)
				|| (sourceParentView != null && getTarget() != null && getTarget()
						.getParentView() == sourceParentView)
				|| (targetParentView != null && getSource() != null && getSource()
						.getParentView() == targetParentView);
	}

	/**
	 * Returns the points.
	 * 
	 * @return List
	 */
	public List getPoints() {
		return points;
	}

	/**
	 * Returns the number of point for this edge.
	 */
	public int getPointCount() {
		if (points != null) {
			return points.size();
		} else {
			return 0;
		}
	}

	/**
	 * Returns the cached points for this edge.
	 */
	public Point2D getPoint(int index) {
		Object obj = points.get(index);
		if (index == 0 && sourceParentView != null) {
			return sourceParentView.getPerimeterPoint(this,
					getCenterPoint(sourceParentView),
					getNearestPoint(index == 0));
		} else if (index == getPointCount() - 1 && targetParentView != null) {
			return targetParentView.getPerimeterPoint(this,
					getCenterPoint(targetParentView),
					getNearestPoint(index == 0));
		} else if (obj instanceof PortView)
			// Port Location Seen From This Edge
			return ((PortView) obj).getLocation(this,
					getNearestPoint(index == 0));
		else if (obj instanceof CellView) {
			// Should not happen
			Rectangle2D r = ((CellView) obj).getBounds();
			return new Point2D.Double(r.getX(), r.getY());
		} else if (obj instanceof Point2D)
			// Regular Point
			return (Point2D) obj;
		return null;
	}

	/**
	 * Returns the nearest point wrt to the source or target. This method
	 * returns the next or previous point or port in the points list, eg. if
	 * source is true it returns the location of the point or port at index 1
	 * without calling the getLocation method on any ports.<br>
	 * Likewise, the method returns the location at index getPointCount()-2 if
	 * source is false.
	 */
	protected Point2D getNearestPoint(boolean source) {
		if (getPointCount() == 2) {
			if (source
					&& target instanceof PortView
					&& GraphConstants.getOffset(target.getAllAttributes()) != null) {
				return ((PortView) target).getLocation(this);
			}
			if (!source
					&& this.source instanceof PortView
					&& GraphConstants.getOffset(this.source.getAllAttributes()) != null) {
				return ((PortView) this.source).getLocation(this);
			}
			if (source && targetParentView != null && targetParentView.isLeaf())
				return getCenterPoint(targetParentView);
			else if (!source && sourceParentView != null
					&& sourceParentView.isLeaf())
				return getCenterPoint(sourceParentView);
		}
		return getPointLocation((source) ? 1 : getPointCount() - 2);
	}

	/**
	 * Returns the point of <code>edge</code> at <code>index</code>. Avoids
	 * calling <code>getLocation</code> on any ports of <code>edge</code>.
	 * <br>
	 * This is used from within getPoint to pass the nearest point to the
	 * portview to find it's location. This uses the center point of the parent
	 * view to determine the port view's location to avoid infinite recursion.
	 */
	protected Point2D getPointLocation(int index) {
		Object obj = points.get(index);
		if (obj instanceof Point2D)
			return (Point2D) obj;
		else if (obj instanceof PortView) {
			CellView vertex = ((CellView) obj).getParentView();
			if (vertex != null)
				return getCenterPoint(vertex);
		}
		return null;
	}

	/**
	 * Sets the point at <code>index</code> to <code>p</code>.
	 */
	public void setPoint(int index, Point2D p) {
		points.set(index, p);
		invalidate();
	}

	/**
	 * Adds <code>p</code> at position <code>index</code>.
	 */
	public void addPoint(int index, Point2D p) {
		points.add(index, p);
		invalidate();
	}

	/**
	 * Removes the point at position <code>index</code>.
	 */
	public void removePoint(int index) {
		points.remove(index);
		invalidate();
	}

	/**
	 * Adds an extra label.
	 */
	public void addExtraLabel(Point2D location, Object label) {
		Object[] extraLabels = GraphConstants
				.getExtraLabels(getAllAttributes());
		Point2D[] positions = GraphConstants
				.getExtraLabelPositions(getAllAttributes());

		// Inserts a new extra label
		if (extraLabels == null) {
			extraLabels = new Object[1];
			positions = new Point2D[1];
		} else {
			Object[] tmp = new Object[extraLabels.length + 1];
			System.arraycopy(extraLabels, 0, tmp, 0, extraLabels.length);
			extraLabels = tmp;
			Point2D[] pts = new Point2D[positions.length + 1];
			System.arraycopy(positions, 0, pts, 0, positions.length);
			positions = pts;
		}
		int newIndex = extraLabels.length - 1;
		extraLabels[newIndex] = label;
		positions[newIndex] = location;
		GraphConstants.setExtraLabels(getAllAttributes(), extraLabels);
		GraphConstants.setExtraLabelPositions(getAllAttributes(), positions);
	}

	/**
	 * Removes the point at position <code>index</code>.
	 */
	public void removeExtraLabel(int index) {
		Object[] labels = GraphConstants.getExtraLabels(getAllAttributes());
		Point2D[] pts = GraphConstants
				.getExtraLabelPositions(getAllAttributes());
		if (labels == null || labels.length > 1) {
			Object[] newLabels = new Object[labels.length - 1];
			Point2D[] newPts = new Point2D[pts.length - 1];
			System.arraycopy(labels, 0, newLabels, 0, index);
			if (index < newLabels.length)
				System.arraycopy(labels, index + 1, newLabels, index,
						newLabels.length - index);
			System.arraycopy(pts, 0, newPts, 0, index);
			if (index < newPts.length)
				System.arraycopy(pts, index + 1, newPts, index, newPts.length
						- index);
			GraphConstants.setExtraLabels(getAllAttributes(), newLabels);
			GraphConstants.setExtraLabelPositions(getAllAttributes(), newPts);
		} else {
			// TODO: Remove via REMOVEATTRIBUTES
			GraphConstants.setExtraLabels(getAllAttributes(), new Object[0]);
			GraphConstants.setExtraLabelPositions(getAllAttributes(),
					new Point2D[0]);
		}
	}

	/**
	 * Hook to return the vector that is taken as the base vector to compute
	 * relative label positions. Normally, the vector goes from the first to the
	 * last point on the edge, unless these points are equal, in which case the
	 * average distance of all points to the source point is used.
	 */
	public Point2D getLabelVector() {
		if (labelVector == null) {
			Point2D p0 = getPoint(0);
			double dx = 0;
			double dy = 0;
			// Finds an average distance
			dx = 0;
			dy = 0;
			int n = getPointCount();
			if (isLoop()) {
				for (int i = 1; i < n; i++) {
					Point2D point = getPoint(i);
					dx += point.getX() - p0.getX();
					dy += point.getY() - p0.getY();
				}
				n /= 2;
				dx /= n;
				dy /= n;
				labelVector = new Point2D.Double(dx, dy);
			} else {
				Point2D point = getPoint(n - 1);
				dx += point.getX() - p0.getX();
				dy += point.getY() - p0.getY();
				labelVector = new Point2D.Double(dx, dy);
			}
		}
		return labelVector;
	}

	/**
	 * Returns the absolute position of the main label
	 * @return the absolute position of the main label
	 */
	protected Point2D getAbsoluteLabelPosition() {
		Point2D result = getAbsoluteLabelPositionFromRelative(GraphConstants.getLabelPosition(getAllAttributes()));
		return result;
	}
	
	/**
	 * Returns the absolute position of the specified extra label
	 * @param index the index of the extra label
	 * @return the absolute position of the specified extra label
	 */
	protected Point2D getAbsoluteExtraLabelPosition(int index) {
		Point2D[] positions = GraphConstants
				.getExtraLabelPositions(getAllAttributes());
		if (positions != null && positions.length > index) {
			Point2D result = getAbsoluteLabelPositionFromRelative(positions[index]);
			return result;
		}
		return null;
	}
	
	/**
	 * Converts relative label position to absolute and allows for
	 * any label offset.
	 * @param geometry the relative label position
	 * @return the absolute label position including any offset
	 */
	protected Point2D getAbsoluteLabelPositionFromRelative(Point2D geometry) {
		Point2D result = convertRelativeLabelPositionToAbsolute(geometry);
		
		if (result != null)
		{
			double offsetX = 0;
			double offsetY = 0;

			Point2D offset = GraphConstants.getOffset(getAllAttributes());

			if (offset != null) {
				offsetX = offset.getX();
				offsetY = offset.getY();
			}

			double x = result.getX() + offsetX;
			double y = result.getY() + offsetY;
			return new Point2D.Double(x, y);
		}
		
		return null;
	}
	
	/**
	 * Converts an relative label position (x is distance along edge and y is
	 * distance above/below edge vector) into an absolute co-ordination point
	 * @param geometry the relative label position
	 * @return the absolute label position
	 */
	protected Point2D convertRelativeLabelPositionToAbsolute(Point2D geometry) {
		int pointCount = getPointCount();
		
		double length = 0;
		double[] segments = new double[pointCount];
		Point2D pt = getPoint(0);
		
		if (pt != null)
		{
			// Find the total length of the segments and also store the length
			// of each segment
			for (int i = 1; i < pointCount; i++)
			{
				Point2D tmp = getPoint(i);
				
				if (tmp != null)
				{
					double dx = pt.getX() - tmp.getX();
					double dy = pt.getY() - tmp.getY();

					double segment = Math.sqrt(dx * dx + dy * dy);
					
					segments[i - 1] = segment;
					length += segment;
					pt = tmp;
				}
			}

			// Change x to be a value between 0 and 1 indicating how far
			// along the edge the label is
			double x = geometry.getX()/GraphConstants.PERMILLE;
			double y = geometry.getY();
			
			// dist is the distance along the edge the label is
			double dist = x * length;
			length = 0;
			
			int index = 1;
			double segment = segments[0];

			// Find the length up to the start of the segment the label is
			// on (length) and retrieve the length of that segment (segment)
			while (dist > length + segment && index < pointCount - 1)
			{
				length += segment;
				segment = segments[index++];
			}

			// factor is the proportion along this segment the label lies at
			double factor = (dist - length) / segment;
			
			Point2D p0 = getPoint(index - 1);
			Point2D pe = getPoint(index);

			if (p0 != null && pe != null)
			{
				// The x and y offsets of the label from the start point
				// of the segment
				double dx = pe.getX() - p0.getX();
				double dy = pe.getY() - p0.getY();
				
				// The normal vectors of
				double nx = dy / segment;
				double ny = dx / segment;
				
				// The x position is the start x of the segment + the factor of
				// the x offset between the start and end of the segment + the
				// x component of the y (height) offset contributed along the
				// normal vector.
				x = p0.getX() + dx * factor - nx * y;

				// The x position is the start y of the segment + the factor of
				// the y offset between the start and end of the segment + the
				// y component of the y (height) offset contributed along the
				// normal vector.
				y = p0.getY() + dy * factor + ny * y;
				return new Point2D.Double(x, y);
			}
		}
		
		return null;
	}

	//
	// Routing
	//

	public static double getLength(CellView view) {
		double cost = 1;
		if (view instanceof EdgeView) {
			EdgeView edge = (EdgeView) view;
			Point2D last = null, current = null;
			for (int i = 0; i < edge.getPointCount(); i++) {
				current = edge.getPoint(i);
				if (last != null)
					cost += last.distance(current);
				last = current;
			}
		}
		return cost;
	}

	//
	// Handle
	//

	// This implementation uses the point instance to make the change. No index
	// is used for the current point because routing could change the index
	// during
	// the move operation.
	public static class EdgeHandle implements CellHandle, Serializable {

		protected JGraph graph;

		/* Pointer to the edge and its clone. */
		protected EdgeView edge, orig;

		/*
		 * Boolean indicating whether the source, target or label is being
		 * edited.
		 */
		protected boolean label = false, source = false, target = false;

		/**
		 * Holds the index of the current (editing) label or point.
		 */
		protected int currentLabel = -1, currentIndex = -1;

		/* Pointer to the currently selected point. */
		protected Point2D currentPoint;

		/* Array of control points represented as rectangles. */
		protected transient Rectangle2D[] r;

		/* A control point for the label position. */
		protected transient Rectangle2D loc;

		protected transient Rectangle2D[] extraLabelLocations;

		protected boolean firstOverlayCall = true;

		protected boolean isEdgeConnectable = true;

		protected EdgeView relevantEdge = null;

		/**
		 * True if the cell is being edited.
		 */
		protected boolean editing = false;

		/**
		 * Holds the initial location of the label.
		 */
		protected Point2D initialLabelLocation = null;

		/**
		 * Indicates whether the edge has been modified during the last mouse
		 * pressed and dragged operations.
		 */
		protected boolean edgeModified = false;

		public EdgeHandle(EdgeView edge, GraphContext ctx) {
			this.graph = ctx.getGraph();
			this.edge = edge;
			editing = graph.getEditingCell() == edge.getCell();
			loc = new Rectangle();
			Object[] labels = GraphConstants.getExtraLabels(edge
					.getAllAttributes());
			if (labels != null) {
				extraLabelLocations = new Rectangle[labels.length];
				for (int i = 0; i < extraLabelLocations.length; i++)
					extraLabelLocations[i] = new Rectangle();
			}
			orig = (EdgeView) graph.getGraphLayoutCache().getMapping(
					edge.getCell(), false);
			reloadPoints(orig);
			isEdgeConnectable = GraphConstants.isConnectable(edge
					.getAllAttributes());
		}

		protected void reloadPoints(EdgeView edge) {
			relevantEdge = edge;
			r = new Rectangle[edge.getPointCount()];
			for (int i = 0; i < r.length; i++)
				r[i] = new Rectangle();
			invalidate();
		}

		// Update and paint control points
		public void paint(Graphics g) {
			invalidate();
			if (!edge.isLeaf())
				return;
			for (int i = 0; i < r.length; i++) {
				if (isEdgeConnectable && !editing)
					g.setColor(graph.getHandleColor());
				else
					g.setColor(graph.getLockedHandleColor());
				g.fill3DRect((int) r[i].getX(), (int) r[i].getY(), (int) r[i]
						.getWidth(), (int) r[i].getHeight(), true);
				CellView port = null;
				if (i == 0 && edge.getSource() != null)
					port = edge.getSource();
				else if (i == r.length - 1 && edge.getTarget() != null)
					port = edge.getTarget();
				if (port != null
						|| (i == 0 && edge.getSourceParentView() != null)
						|| (i == r.length - 1 && edge.getTargetParentView() != null)) {
					g.setColor(graph.getLockedHandleColor());
					Point2D tmp = (port != null) ? GraphConstants
							.getOffset(port.getAllAttributes()) : null;
					if (tmp != null) {
						g.drawLine((int) r[i].getX() + 1,
								(int) r[i].getY() + 1,
								(int) (r[i].getX() + r[i].getWidth()) - 3,
								(int) (r[i].getY() + r[i].getHeight()) - 3);
						g.drawLine((int) r[i].getX() + 1,
								(int) (r[i].getY() + r[i].getHeight()) - 3,
								(int) (r[i].getX() + r[i].getWidth()) - 3,
								(int) r[i].getY() + 1);
					} else
						g.drawRect((int) r[i].getX() + 2,
								(int) r[i].getY() + 2,
								(int) r[i].getWidth() - 5, (int) r[i]
										.getHeight() - 5);
				}
			}
			if (!graph.isXorEnabled()) {
				firstOverlayCall = false;
				overlay(g);
			}
		}

		public void overlay(Graphics g) {
			if (edge != null && !firstOverlayCall && edge.isLeaf()) {
				// g.setColor(graph.getBackground()); // JDK 1.3
				g.setColor(graph.getForeground());
				if (graph.isXorEnabled()) {
					g.setXORMode(graph.getBackground().darker());
				}
				Graphics2D g2 = (Graphics2D) g;
				AffineTransform oldTransform = g2.getTransform();
				g2.scale(graph.getScale(), graph.getScale());
				graph.getUI().paintCell(g, edge, edge.getBounds(), true);
				g2.setTransform(oldTransform);
				if (isSourceEditing() && edge.getSource() != null)
					paintPort(g, edge.getSource());
				else if (isTargetEditing() && edge.getTarget() != null)
					paintPort(g, edge.getTarget());
			}
			firstOverlayCall = false;
		}

		protected void paintPort(Graphics g, CellView p) {
			boolean offset = (GraphConstants.getOffset(p.getAllAttributes()) != null);
			Rectangle2D r = (offset) ? p.getBounds() : p.getParentView()
					.getBounds();
			r = graph.toScreen((Rectangle2D) r.clone());
			int s = 3;
			r.setFrame(r.getX() - s, r.getY() - s, r.getWidth() + 2 * s, r
					.getHeight()
					+ 2 * s);
			graph.getUI().paintCell(g, p, r, true);
		}

		protected boolean snap(boolean source, Point2D point) {
			boolean connect = graph.isConnectable() && isEdgeConnectable;
			Object port = graph.getPortForLocation(point.getX(), point.getY());
			if (port != null
					&& graph.getModel().getParent(port) == edge.getCell())
				port = null;
			if (port != null && connect) {
				CellView portView = graph.getGraphLayoutCache().getMapping(
						port, false);
				Rectangle2D dirty = edge.getBounds();
				dirty.add(portView.getParentView().getBounds());
				if (GraphConstants.isConnectable(portView.getParentView()
						.getAllAttributes())) {
					Object cell = edge.getCell();
					if (source && graph.getModel().acceptsSource(cell, port)) {
						if (edge.getSource() != portView) {
							edgeModified = true;
							if (graph.isXorEnabled()) {
								overlay(graph.getGraphics());
							}
							edge.setSource(portView);
							edge.update(graph.getGraphLayoutCache());
							if (graph.isXorEnabled()) {
								overlay(graph.getGraphics());
							} else {
								dirty.add(edge.getBounds());
								graph.repaint((int) dirty.getX(), (int) dirty
										.getY(), (int) dirty.getWidth(),
										(int) dirty.getHeight());
							}
						}
						return true;
					} else if (!source
							&& graph.getModel().acceptsTarget(cell, port)) {
						if (edge.getTarget() != portView) {
							edgeModified = true;
							if (graph.isXorEnabled()) {
								overlay(graph.getGraphics());
							}
							edge.setTarget(portView);
							edge.update(graph.getGraphLayoutCache());
							if (graph.isXorEnabled()) {
								overlay(graph.getGraphics());
							} else {
								dirty.add(edge.getBounds());
								graph.repaint((int) dirty.getX(), (int) dirty
										.getY(), (int) dirty.getWidth(),
										(int) dirty.getHeight());
							}
						}
						return true;
					}
				}
			}
			return false;
		}

		public boolean isConstrainedMoveEvent(MouseEvent e) {
			GraphUI ui = graph.getUI();
			if (ui instanceof BasicGraphUI)
				return ((BasicGraphUI) ui).isConstrainedMoveEvent(e);
			return false;
		}

		/**
		 * Returning true signifies a mouse event adds a new point to an edge.
		 */
		public boolean isAddPointEvent(MouseEvent event) {
			return event.isPopupTrigger()
					|| SwingUtilities.isRightMouseButton(event);
		}

		/**
		 * Returning true signifies a mouse event removes a given point.
		 */
		public boolean isRemovePointEvent(MouseEvent event) {
			return event.isPopupTrigger()
					|| SwingUtilities.isRightMouseButton(event);
		}

		protected boolean isSourceEditing() {
			return source;
		}

		protected boolean isTargetEditing() {
			return target;
		}

		/*
		 * Returns true if either the source, target, label or a point is being
		 * edited.
		 */
		protected boolean isEditing() {
			return source || target || label || currentLabel >= 0
					|| currentPoint != null;
		}

		/**
		 * Invoked when the mouse pointer has been moved on a component (with no
		 * buttons down).
		 */
		public void mouseMoved(MouseEvent event) {
			for (int i = 0; i < r.length; i++)
				if (r[i].contains(event.getPoint())) {
					graph.setCursor(new Cursor(Cursor.CROSSHAIR_CURSOR));
					event.consume();
					return;
				}
			if (loc.contains(event.getPoint()) && graph.isMoveable()
					&& GraphConstants.isMoveable(edge.getAllAttributes())) {
				graph.setCursor(new Cursor(Cursor.HAND_CURSOR));
				event.consume();
			}
			if (extraLabelLocations != null && graph.isMoveable()
					&& GraphConstants.isMoveable(edge.getAllAttributes())) {
				for (int i = 0; i < extraLabelLocations.length; i++) {
					if (extraLabelLocations[i].contains(event.getPoint())) {
						graph.setCursor(new Cursor(Cursor.HAND_CURSOR));
						event.consume();
					}
				}
			}
		}

		// Handle mouse pressed event.
		public void mousePressed(MouseEvent event) {
			/* INV: currentPoint = null; source = target = label = false; */
			if (!edge.isLeaf())
				return;
			boolean bendable = graph.isBendable()
					&& GraphConstants.isBendable(edge.getAllAttributes());
			int x = event.getX();
			int y = event.getY();
			// Detect hit on control point
			int index = 0;
			for (index = 0; index < r.length; index++) {
				if (r[index].contains(x, y)) {
					currentPoint = edge.getPoint(index);
					currentIndex = index;
					source = index == 0;
					target = index == r.length - 1;
					break;
				}
			}
			// Detect hit on label
			if (!isEditing() && graph.isMoveable()
					&& GraphConstants.isMoveable(edge.getAllAttributes())
					&& loc != null && loc.contains(x, y)
					&& !isAddPointEvent(event) && !isRemovePointEvent(event)
					&& graph.getEdgeLabelsMovable()) {
				initialLabelLocation = (Point2D) edge.getLabelPosition()
						.clone();
				label = true;
			}
			// Detect hit on extra labels
			else if (extraLabelLocations != null && !isEditing()
					&& graph.isMoveable() && graph.getEdgeLabelsMovable()
					&& GraphConstants.isMoveable(edge.getAllAttributes())) {
				for (int i = 0; i < extraLabelLocations.length; i++) {
					if (extraLabelLocations[i] != null
							&& extraLabelLocations[i].contains(x, y)) {
						currentLabel = i;
						initialLabelLocation = (Point2D) edge
								.getExtraLabelPosition(currentLabel).clone();
						if (isRemovePointEvent(event)) {
							edge.removeExtraLabel(i);
							edgeModified = true;
							mouseReleased(event);
						}
						break;
					}
				}
			}
			// Remove Point
			if (isRemovePointEvent(event)
					&& currentPoint != null
					&& !source
					&& !target
					&& bendable
					&& (edge.getSource() == null || currentIndex > 0)
					&& (edge.getTarget() == null || currentIndex < edge
							.getPointCount() - 1)) {
				edge.removePoint(index);
				edgeModified = true;
				mouseReleased(event);
				// Add Point
			} else if (isAddPointEvent(event) && !isEditing() && bendable) {
				int s = graph.getHandleSize();
				Rectangle2D rect = graph.fromScreen(new Rectangle(x - s, y - s,
						2 * s, 2 * s));
				if (edge.intersects(graph, rect)) {
					Point2D point = graph.fromScreen(graph.snap(new Point(event
							.getPoint())));
					double min = Double.MAX_VALUE, dist = 0;
					for (int i = 0; i < edge.getPointCount() - 1; i++) {
						Point2D p = edge.getPoint(i);
						Point2D p1 = edge.getPoint(i + 1);
						dist = new Line2D.Double(p, p1).ptSegDistSq(point);
						if (dist < min) {
							min = dist;
							index = i + 1;
						}
					}
					edge.addPoint(index, point);
					edgeModified = true;
					currentPoint = point;
					reloadPoints(edge);
					paint(graph.getGraphics());
				}
			}
			if (isEditing())
				event.consume();
		}

		public void mouseDragged(MouseEvent event) {
			Point2D p = graph.fromScreen(new Point(event.getPoint()));
			// Move Label
			if (label || currentLabel >= 0) {
				Rectangle2D r = edge.getBounds();
				if (r != null) {
					edgeModified = true;
					if (graph.isXorEnabled()) {
						overlay(graph.getGraphics());
					}
					if (!GraphConstants.isLabelAlongEdge(edge.getAllAttributes())) {
						p = getRelativeLabelPosition(edge, p);
					} else {
						double x = p.getX();
						double y = p.getY();

						Point2D p0 = edge.getPoint(0);

						double p0x = p0.getX();
						double p0y = p0.getY();

						Point2D vector = edge.getLabelVector();
						double dx = vector.getX();
						double dy = vector.getY();

						double pex = p0.getX() + dx;
						double pey = p0.getY() + dy;

						double len = Math.sqrt(dx * dx + dy * dy);
						if (len > 0) {
							double u = GraphConstants.PERMILLE;
							double posy = len
									* (-y * dx + p0y * dx + x * dy - p0x * dy)
									/ (-pey * dy + p0y * dy - dx * pex + dx * p0x);
							double posx = u
									* (-y * pey + y * p0y + p0y * pey - p0y * p0y
											- pex * x + pex * p0x + p0x * x - p0x
											* p0x)
									/ (-pey * dy + p0y * dy - dx * pex + dx * p0x);
							p = new Point2D.Double(posx, posy);
						} else {
							p = new Point2D.Double(x - p0.getX(), y - p0.getY());
						}
					}
					Rectangle2D dirty = edge.getBounds();
					if (label) {
						edge.setLabelPosition(p);
					} else {
						edge.setExtraLabelPosition(currentLabel, p);
					}
					edge.update(graph.getGraphLayoutCache());
					if (graph.isXorEnabled()) {
						overlay(graph.getGraphics());
					} else {
						graph.repaint((int) dirty.getX(), (int) dirty
								.getY(), (int) dirty.getWidth(),
								(int) dirty.getHeight());
					}
				}
			} else if (isEditing() && currentPoint != null) {
				boolean disconnectable = (!source && !target)
						|| (graph.isDisconnectable() && GraphConstants
								.isDisconnectable(orig.getAllAttributes()));
				if (source)
					disconnectable = disconnectable
							&& ((orig.getSource() == null && orig
									.getSourceParentView() == null)
									|| (orig.getSource() != null && GraphConstants
											.isDisconnectable(orig.getSource()
													.getParentView()
													.getAllAttributes())) || (orig
									.getSourceParentView() != null && GraphConstants
									.isDisconnectable(orig
											.getSourceParentView()
											.getAllAttributes())));
				if (target)
					disconnectable = disconnectable
							&& ((orig.getTarget() == null && orig
									.getTargetParentView() == null)
									|| (orig.getTarget() != null && GraphConstants
											.isDisconnectable(orig.getTarget()
													.getParentView()
													.getAllAttributes())) || (orig
									.getTargetParentView() != null && GraphConstants
									.isDisconnectable(orig
											.getTargetParentView()
											.getAllAttributes())));
				// Find Source/Target Port
				if (!((source && snap(true, event.getPoint())) || (target && snap(
						false, event.getPoint())))
						&& disconnectable) {
					// Else Use Point
					boolean acceptSource = source
							&& (graph.getModel().acceptsSource(edge.getCell(),
									null) || graph.isPreviewInvalidNullPorts());
					boolean acceptTarget = target
							&& (graph.getModel().acceptsTarget(edge.getCell(),
									null) || graph.isPreviewInvalidNullPorts());
					if (acceptSource || acceptTarget || !(source || target)) {
						edgeModified = true;
						Rectangle2D dirty = edge.getBounds();
						if (edge.getSource() != null) {
							dirty.add(edge.getSource().getParentView()
									.getBounds());
						}
						if (edge.getTarget() != null) {
							dirty.add(edge.getTarget().getParentView()
									.getBounds());
						}
						if (graph.isXorEnabled()) {
							overlay(graph.getGraphics());
						}
						p = graph.fromScreen(graph.snap(new Point(event
								.getPoint())));
						// Constrained movement
						if (isConstrainedMoveEvent(event) && currentIndex >= 0) {
							// Reset Initial Positions
							EdgeView orig = (EdgeView) graph
									.getGraphLayoutCache().getMapping(
											edge.getCell(), false);
							Point2D origPoint = orig.getPoint(currentIndex);
							double totDx = p.getX() - origPoint.getX();
							double totDy = p.getY() - origPoint.getY();
							if (Math.abs(totDx) < Math.abs(totDy))
								p.setLocation(origPoint.getX(), p.getY());
							else
								p.setLocation(p.getX(), origPoint.getY());
						}
						// Do not move into negative space
						p.setLocation(Math.max(0, p.getX()), Math.max(0, p
								.getY()));
						currentPoint.setLocation(p);
						if (source) {
							edge.setPoint(0, p);
							edge.setSource(null);
						} else if (target) {
							edge.setPoint(edge.getPointCount() - 1, p);
							edge.setTarget(null);
						}
						edge.update(graph.getGraphLayoutCache());
						dirty.add(edge.getBounds());
						if (graph.isXorEnabled()) {
							overlay(graph.getGraphics());
						} else {
							if (edge.getSource() != null) {
								dirty.add(edge.getSource().getParentView()
										.getBounds());
							}
							if (edge.getTarget() != null) {
								dirty.add(edge.getTarget().getParentView()
										.getBounds());
							}
							graph.repaint((int) dirty.getX(), (int) dirty
									.getY(), (int) dirty.getWidth(),
									(int) dirty.getHeight());
						}
					}
				}
			}
		}
		
		protected Point2D getRelativeLabelPosition(EdgeView edge, Point2D p) {
			int pointCount = edge.getPointCount();
			
			double totalLength = 0;
			double[] segments = new double[pointCount];
			
			Point2D p0 = edge.getPoint(0);
			Point2D pt = p0;
			
			// Calculate the total length of the edge
			for (int i = 1; i < pointCount; i++)
			{
				Point2D tmp = edge.getPoint(i);
				
				if (tmp != null)
				{
					double dx = pt.getX() - tmp.getX();
					double dy = pt.getY() - tmp.getY();

					double segment = Math.sqrt(dx * dx + dy * dy);
					
					segments[i - 1] = segment;
					totalLength += segment;
					pt = tmp;
				}
			}
			
			// Work which line segment the point of the label is closest to
			Point2D last = edge.getPoint(1);
			Line2D line = new Line2D.Double(p0, last);
			double minDist = line.ptSegDistSq(p);
			
			int index = 0;
			double tmp = 0;
			double length = 0;
			
			for (int i = 2; i < pointCount; i++)
			{
				tmp += segments[i-2];
				
				line = new Line2D.Double(edge.getPoint(i), last);
				double dist = line.ptSegDistSq(p);
				
				if (dist < minDist) {
					minDist = dist;
					index = i-1;
					length = tmp;
				}
				
				last = edge.getPoint(i);
			}
			
			double seg = segments[index];
			
			pt = edge.getPoint(index);
			
			double x2 = pt.getX();
			double y2 = pt.getY();
			
			Point2D pt2 = edge.getPoint(index+1);
			
			double x1 = pt2.getX();
			double y1 = pt2.getY();
			
			double px = p.getX();
			double py = p.getY();
			
			double xSegment = x2 - x1;
			double ySegment = y2 - y1;
	
			px -= x1;
			py -= y1;
	
			double projlenSq = 0;
	
		    px = xSegment - px;
		    py = ySegment - py;
		    double dotprod = px * xSegment + py * ySegment;

		    if (dotprod <= 0.0)
		    {
				projlenSq = 0;
		    }
		    else
		    {
				projlenSq = dotprod * dotprod / (xSegment * xSegment + ySegment * ySegment);
		    }

		    double projlen = Math.sqrt(projlenSq);
			if (projlen > seg)
			{
				projlen = seg;						
			}

			double yDistance = Line2D.ptLineDist(pt2.getX(), pt2.getY(), pt.getX(), pt.getY(), p.getX(), p.getY());
			int direction = Line2D.relativeCCW(pt2.getX(), pt2.getY(), pt.getX(), pt.getY(), p.getX(), p.getY());
			
			if (direction == -1) {
				yDistance = -yDistance;
			}
			
			// Constructs the relative point for the label
			Point2D result = new Point2D.Double(((((totalLength/2 - length - projlen)/
					totalLength)*-2)+1)*GraphConstants.PERMILLE / 2, yDistance);
			
			// Use the utility method to find 
			Point2D storedRelativePosition = edge.convertRelativeLabelPositionToAbsolute(result);
			if (p.equals(storedRelativePosition)) {
				GraphConstants.setRemoveAttributes(edge.getAllAttributes(), new Object[] {GraphConstants.OFFSET});
			} else {
				Point2D off = new Point2D.Double(p.getX() - storedRelativePosition.getX(), p.getY() - storedRelativePosition.getY());
				GraphConstants.setOffset(edge.getAllAttributes(), off);
			}
			return result;
		}

		// Handle mouse released event
		public void mouseReleased(MouseEvent e) {
			boolean clone = e.isControlDown() && graph.isCloneable();
			GraphModel model = graph.getModel();
			Object source = (edge.getSource() != null) ? edge.getSource()
					.getCell() : null;
			Object target = (edge.getTarget() != null) ? edge.getTarget()
					.getCell() : null;
			if (edgeModified && model.acceptsSource(edge.getCell(), source)
					&& model.acceptsTarget(edge.getCell(), target)) {

				// Creates an extra label if the label was cloned
				if (clone && initialLabelLocation != null) {

					// Resets the dragging label position and adds a new label
					// instead. Note: label locations are modified in-place
					// which is why we need to clone at beginning.
					Object value = null;
					Point2D location = null;
					Object[] extraLabels = GraphConstants.getExtraLabels(edge
							.getAllAttributes());
					if (label) {
						location = (Point2D) edge.getLabelPosition().clone();
						value = graph.convertValueToString(orig);
						edge.setLabelPosition(initialLabelLocation);
					} else {
						location = (Point2D) edge.getExtraLabelPosition(
								currentLabel).clone();
						value = extraLabels[currentLabel];
						edge.setExtraLabelPosition(currentLabel,
								initialLabelLocation);
					}
					edge.addExtraLabel(location, value);
					edge.update(graph.getGraphLayoutCache());
					clone = false;
				}

				// Creates the data required for the edit/insert call
				ConnectionSet cs = createConnectionSet(edge, clone);
				Map nested = GraphConstants.createAttributes(
						new CellView[] { edge }, null);

				// The cached points may be different from what's
				// in the attribute map if the edge is routed.
				Map tmp = (Map) nested.get(edge.getCell());
				List controlPoints = GraphConstants.getPoints(tmp);
				List currentPoints = edge.getPoints();

				// Checks if we're dealing with a routing algorithm
				// and if we are, replaces only the source and target
				// in the control point list.
				if (controlPoints != currentPoints) {
					controlPoints.set(0, edge.getPoint(0));
					controlPoints.set(controlPoints.size() - 1, edge
							.getPoint(edge.getPointCount() - 1));
				}

				if (clone) {
					Map cellMap = graph.cloneCells(graph
							.getDescendants(new Object[] { edge.getCell() }));
					processNestedMap(nested, true);
					nested = GraphConstants.replaceKeys(cellMap, nested);
					cs = cs.clone(cellMap);
					Object[] cells = cellMap.values().toArray();
					graph.getGraphLayoutCache().insert(cells, nested, cs, null,
							null);
				} else {
					processNestedMap(nested, false);
					graph.getGraphLayoutCache().edit(nested, cs, null, null);
				}
			} else {
				if (graph.isXorEnabled()) {
					overlay(graph.getGraphics());
				} else {
					Rectangle2D dirty = edge.getBounds();
					graph.repaint((int) dirty.getX(), (int) dirty.getY(),
							(int) dirty.getWidth(), (int) dirty.getHeight());
				}
				edge.refresh(graph.getGraphLayoutCache(), graph.getGraphLayoutCache(),
						false);
			}
			initialLabelLocation = null;
			currentPoint = null;
			this.edgeModified = false;
			this.label = false;
			this.source = false;
			this.target = false;
			currentLabel = -1;
			currentIndex = -1;
			firstOverlayCall = true;
			e.consume();
		}

		protected void processNestedMap(Map nested, boolean clone) {
			// subclassers can override this to modify the attributes
		}

		protected ConnectionSet createConnectionSet(EdgeView view,
				boolean verbose) {
			Object edge = view.getCell();
			GraphModel model = graph.getModel();
			ConnectionSet cs = new ConnectionSet();
			Object sourcePort = null, targetPort = null;
			if (view.getSource() != null)
				sourcePort = view.getSource().getCell();
			else if (view.getSourceParentView() != null)
				sourcePort = model.getSource(edge);
			if (view.getTarget() != null)
				targetPort = view.getTarget().getCell();
			else if (view.getTargetParentView() != null)
				targetPort = model.getTarget(edge);
			if (view.getTarget() != null)
				targetPort = view.getTarget().getCell();
			if (verbose || (sourcePort != model.getSource(edge) && source))
				cs.connect(edge, sourcePort, true);
			if (verbose || (targetPort != model.getTarget(edge) && target))
				cs.connect(edge, targetPort, false);
			return cs;
		}

		// Update control points
		protected void invalidate() {
			EdgeView e = relevantEdge;
			int handlesize = graph.getHandleSize();
			EdgeRenderer er = (EdgeRenderer) edge.getRenderer();
			Point2D labelPosition = er.getLabelPosition(e);
			Point2D p = null;
			if (labelPosition != null) {
				p = (Point2D)labelPosition.clone();
				graph.toScreen(p);
			}
			Dimension d = er.getLabelSize(e, graph.convertValueToString(e));
			if (p != null && d != null) {
				Point2D s = graph.toScreen(new Point2D.Double(d.width,
						d.height));
				loc.setFrame(p.getX() - s.getX() / 2, p.getY() - s.getY()
						/ 2, s.getX(), s.getY());
			}
			for (int i = 0; i < r.length; i++) {
				p = e.getPoint(i);
				p = graph.toScreen(new Point2D.Double(p.getX(), p.getY()));
				r[i].setFrame(p.getX() - handlesize, p.getY() - handlesize,
						2 * handlesize, 2 * handlesize);

			}
			if (extraLabelLocations != null) {
				for (int i = 0; i < extraLabelLocations.length; i++) {
					p = er.getExtraLabelPosition(e, i);
					if (p != null) {
						p = graph.toScreen((Point2D) p.clone());
						d = er.getExtraLabelSize(graph, e, i);
						if (d != null) {
							Point2D s = graph.toScreen(new Point2D.Double(
									d.width, d.height));
							extraLabelLocations[i].setFrame(p.getX() - s.getX()
									/ 2, p.getY() - s.getY() / 2, s.getX(), s
									.getY());
						}
					}
				}
			}
		}

	}

	public Point2D getPerimeterPoint(EdgeView edge, Point2D source, Point2D p) {
		if (getPointCount() > 2)
			return getPoint(getPointCount() / 2);
		Point2D p0 = getPoint(0);
		Point2D pe = getPoint(getPointCount() - 1);
		return new Point2D.Double((pe.getX() + p0.getX()) / 2, (pe.getY() + p0
				.getY()) / 2);
	}
}