/*******************************************************************************
 * Copyright (c) 2004, 2010 IBM Corporation and others.
 * 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.draw2d.graph;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;

/**
 * A Path representation for the ShortestPathRouting. A Path has a start and end
 * point and may have bendpoints. The output of a path is accessed via the
 * method <code>getPoints()</code>.
 * 
 * This class is for internal use only.
 * 
 * @author Whitney Sorenson
 * @since 3.0
 */
public class Path {

	/**
	 * A Stack of segments.
	 */
	private static class SegmentStack extends ArrayList {

		Segment pop() {
			return (Segment) remove(size() - 1);
		}

		Obstacle popObstacle() {
			return (Obstacle) remove(size() - 1);
		}

		void push(Object obj) {
			add(obj);
		}

	}

	private static final Point CURRENT = new Point();
	private static final double EPSILON = 1.04;
	private static final Point NEXT = new Point();
	private static final double OVAL_CONSTANT = 1.13;

	/**
	 * The bendpoint constraints. The path must go through these bendpoints.
	 */
	PointList bendpoints;
	/**
	 * An arbitrary data field which can be used to map a Path back to some
	 * client object.
	 */
	public Object data;
	List excludedObstacles;
	List grownSegments;
	/**
	 * this field is for internal use only. It is true whenever a property has
	 * been changed which requires the solver to resolve this path.
	 */
	public boolean isDirty = true;

	boolean isInverted = false;
	boolean isMarked = false;
	PointList points;

	/**
	 * The previous cost ratio of the path. The cost ratio is the actual path
	 * length divided by the length from the start to the end.
	 */
	private double prevCostRatio;
	List segments;

	private SegmentStack stack;
	Vertex start, end;
	private Path subPath;
	double threshold;
	Set visibleObstacles;
	Set visibleVertices;

	/**
	 * Constructs a new path.
	 * 
	 * @since 3.0
	 */
	public Path() {
		segments = new ArrayList();
		grownSegments = new ArrayList();
		points = new PointList();
		visibleVertices = new HashSet();
		stack = new SegmentStack();
		visibleObstacles = new HashSet();
		excludedObstacles = new ArrayList();
	}

	/**
	 * Constructs a new path with the given data.
	 * 
	 * @since 3.0
	 * @param data
	 *            an arbitrary data field
	 */
	public Path(Object data) {
		this();
		this.data = data;
	}

	/**
	 * Constructs a new path with the given data, start and end point.
	 * 
	 * @param start
	 *            the start point for this path
	 * @param end
	 *            the end point for this path
	 */
	public Path(Point start, Point end) {
		this(new Vertex(start, null), new Vertex(end, null));
	}

	/**
	 * Creates a path between the given vertices.
	 * 
	 * @param start
	 *            start vertex
	 * @param end
	 *            end vertex
	 */
	Path(Vertex start, Vertex end) {
		this();
		this.start = start;
		this.end = end;
	}

	/**
	 * Attempts to add all segments between the given obstacles to the
	 * visibility graph.
	 * 
	 * @param source
	 *            the source obstacle
	 * @param target
	 *            the target obstacle
	 */
	private void addAllSegmentsBetween(Obstacle source, Obstacle target) {
		addConnectingSegment(new Segment(source.bottomLeft, target.bottomLeft),
				source, target, false, false);
		addConnectingSegment(
				new Segment(source.bottomRight, target.bottomRight), source,
				target, true, true);
		addConnectingSegment(new Segment(source.topLeft, target.topLeft),
				source, target, true, true);
		addConnectingSegment(new Segment(source.topRight, target.topRight),
				source, target, false, false);

		if (source.bottom() == target.bottom()) {
			addConnectingSegment(new Segment(source.bottomLeft,
					target.bottomRight), source, target, false, true);
			addConnectingSegment(new Segment(source.bottomRight,
					target.bottomLeft), source, target, true, false);
		}
		if (source.y == target.y) {
			addConnectingSegment(new Segment(source.topLeft, target.topRight),
					source, target, true, false);
			addConnectingSegment(new Segment(source.topRight, target.topLeft),
					source, target, false, true);
		}
		if (source.x == target.x) {
			addConnectingSegment(
					new Segment(source.bottomLeft, target.topLeft), source,
					target, false, true);
			addConnectingSegment(
					new Segment(source.topLeft, target.bottomLeft), source,
					target, true, false);
		}
		if (source.right() == target.right()) {
			addConnectingSegment(new Segment(source.bottomRight,
					target.topRight), source, target, true, false);
			addConnectingSegment(new Segment(source.topRight,
					target.bottomRight), source, target, false, true);
		}
	}

	/**
	 * Attempts to add a segment between the given obstacles to the visibility
	 * graph. This method is specifically written for the case where the two
	 * obstacles intersect and contains a boolean as to whether to check the
	 * diagonal that includes the top right point of the other obstacle.
	 * 
	 * @param segment
	 *            the segment to check
	 * @param o1
	 *            the first obstacle
	 * @param o2
	 *            the second obstacle
	 * @param checkTopRight1
	 *            whether or not to check the diagonal containing top right
	 *            point
	 */
	private void addConnectingSegment(Segment segment, Obstacle o1,
			Obstacle o2, boolean checkTopRight1, boolean checkTopRight2) {
		if (threshold != 0
				&& (segment.end.getDistance(end)
						+ segment.end.getDistance(start) > threshold || segment.start
						.getDistance(end) + segment.start.getDistance(start) > threshold))
			return;

		if (o2.containsProper(segment.start) || o1.containsProper(segment.end))
			return;

		if (checkTopRight1
				&& segment.intersects(o1.x, o1.bottom() - 1, o1.right() - 1,
						o1.y))
			return;
		if (checkTopRight2
				&& segment.intersects(o2.x, o2.bottom() - 1, o2.right() - 1,
						o2.y))
			return;
		if (!checkTopRight1
				&& segment.intersects(o1.x, o1.y, o1.right() - 1,
						o1.bottom() - 1))
			return;
		if (!checkTopRight2
				&& segment.intersects(o2.x, o2.y, o2.right() - 1,
						o2.bottom() - 1))
			return;

		stack.push(o1);
		stack.push(o2);
		stack.push(segment);
	}

	/**
	 * Adds an obstacle to the visibility graph and generates new segments
	 * 
	 * @param newObs
	 *            the new obstacle, should not be in the graph already
	 */
	private void addObstacle(Obstacle newObs) {
		visibleObstacles.add(newObs);
		Iterator oItr = new HashSet(visibleObstacles).iterator();
		while (oItr.hasNext()) {
			Obstacle currObs = (Obstacle) oItr.next();
			if (newObs != currObs)
				addSegmentsFor(newObs, currObs);
		}
		addPerimiterSegments(newObs);
		addSegmentsFor(start, newObs);
		addSegmentsFor(end, newObs);
	}

	/**
	 * Adds the segments along the perimiter of an obstacle to the visiblity
	 * graph queue.
	 * 
	 * @param obs
	 *            the obstacle
	 */
	private void addPerimiterSegments(Obstacle obs) {
		Segment seg = new Segment(obs.topLeft, obs.topRight);
		stack.push(obs);
		stack.push(null);
		stack.push(seg);
		seg = new Segment(obs.topRight, obs.bottomRight);
		stack.push(obs);
		stack.push(null);
		stack.push(seg);
		seg = new Segment(obs.bottomRight, obs.bottomLeft);
		stack.push(obs);
		stack.push(null);
		stack.push(seg);
		seg = new Segment(obs.bottomLeft, obs.topLeft);
		stack.push(obs);
		stack.push(null);
		stack.push(seg);
	}

	/**
	 * Attempts to add a segment to the visibility graph. First checks to see if
	 * the segment is outside the threshold oval. Then it compares the segment
	 * against all obstacles. If it is clean, the segment is finally added to
	 * the graph.
	 * 
	 * @param segment
	 *            the segment
	 * @param exclude1
	 *            an obstacle to exclude from the search
	 * @param exclude2
	 *            another obstacle to exclude from the search
	 * @param allObstacles
	 *            the list of all obstacles
	 */
	private void addSegment(Segment segment, Obstacle exclude1,
			Obstacle exclude2, List allObstacles) {
		if (threshold != 0
				&& (segment.end.getDistance(end)
						+ segment.end.getDistance(start) > threshold || segment.start
						.getDistance(end) + segment.start.getDistance(start) > threshold))
			return;

		for (int i = 0; i < allObstacles.size(); i++) {
			Obstacle obs = (Obstacle) allObstacles.get(i);

			if (obs == exclude1 || obs == exclude2 || obs.exclude)
				continue;

			if (segment.intersects(obs.x, obs.y, obs.right() - 1,
					obs.bottom() - 1)
					|| segment.intersects(obs.x, obs.bottom() - 1,
							obs.right() - 1, obs.y)
					|| obs.containsProper(segment.start)
					|| obs.containsProper(segment.end)) {
				if (!visibleObstacles.contains(obs))
					addObstacle(obs);
				return;
			}
		}

		linkVertices(segment);
	}

	/**
	 * Adds the segments between the given obstacles.
	 * 
	 * @param source
	 *            source obstacle
	 * @param target
	 *            target obstacle
	 */
	private void addSegmentsFor(Obstacle source, Obstacle target) {
		if (source.intersects(target))
			addAllSegmentsBetween(source, target);
		else if (target.bottom() - 1 < source.y)
			addSegmentsTargetAboveSource(source, target);
		else if (source.bottom() - 1 < target.y)
			addSegmentsTargetAboveSource(target, source);
		else if (target.right() - 1 < source.x)
			addSegmentsTargetBesideSource(source, target);
		else
			addSegmentsTargetBesideSource(target, source);
	}

	/**
	 * Adds the segments between the given obstacles.
	 * 
	 * @param source
	 *            source obstacle
	 * @param target
	 *            target obstacle
	 */
	private void addSegmentsFor(Vertex vertex, Obstacle obs) {
		Segment seg = null;
		Segment seg2 = null;

		switch (obs.getPosition(vertex)) {
		case PositionConstants.SOUTH_WEST:
		case PositionConstants.NORTH_EAST:
			seg = new Segment(vertex, obs.topLeft);
			seg2 = new Segment(vertex, obs.bottomRight);
			break;
		case PositionConstants.SOUTH_EAST:
		case PositionConstants.NORTH_WEST:
			seg = new Segment(vertex, obs.topRight);
			seg2 = new Segment(vertex, obs.bottomLeft);
			break;
		case PositionConstants.NORTH:
			seg = new Segment(vertex, obs.topLeft);
			seg2 = new Segment(vertex, obs.topRight);
			break;
		case PositionConstants.EAST:
			seg = new Segment(vertex, obs.bottomRight);
			seg2 = new Segment(vertex, obs.topRight);
			break;
		case PositionConstants.SOUTH:
			seg = new Segment(vertex, obs.bottomRight);
			seg2 = new Segment(vertex, obs.bottomLeft);
			break;
		case PositionConstants.WEST:
			seg = new Segment(vertex, obs.topLeft);
			seg2 = new Segment(vertex, obs.bottomLeft);
			break;
		default:
			if (vertex.x == obs.x) {
				seg = new Segment(vertex, obs.topLeft);
				seg2 = new Segment(vertex, obs.bottomLeft);
			} else if (vertex.y == obs.y) {
				seg = new Segment(vertex, obs.topLeft);
				seg2 = new Segment(vertex, obs.topRight);
			} else if (vertex.y == obs.bottom() - 1) {
				seg = new Segment(vertex, obs.bottomLeft);
				seg2 = new Segment(vertex, obs.bottomRight);
			} else if (vertex.x == obs.right() - 1) {
				seg = new Segment(vertex, obs.topRight);
				seg2 = new Segment(vertex, obs.bottomRight);
			} else {
				throw new RuntimeException("Unexpected vertex conditions"); //$NON-NLS-1$
			}
		}

		stack.push(obs);
		stack.push(null);
		stack.push(seg);
		stack.push(obs);
		stack.push(null);
		stack.push(seg2);
	}

	private void addSegmentsTargetAboveSource(Obstacle source, Obstacle target) {
		// target located above source
		Segment seg = null;
		Segment seg2 = null;
		if (target.x > source.x) {
			seg = new Segment(source.topLeft, target.topLeft);
			if (target.x < source.right() - 1)
				seg2 = new Segment(source.topRight, target.bottomLeft);
			else
				seg2 = new Segment(source.bottomRight, target.topLeft);
		} else if (source.x == target.x) {
			seg = new Segment(source.topLeft, target.bottomLeft);
			seg2 = new Segment(source.topRight, target.bottomLeft);
		} else {
			seg = new Segment(source.bottomLeft, target.bottomLeft);
			seg2 = new Segment(source.topRight, target.bottomLeft);
		}

		stack.push(source);
		stack.push(target);
		stack.push(seg);
		stack.push(source);
		stack.push(target);
		stack.push(seg2);
		seg = null;
		seg2 = null;

		if (target.right() < source.right()) {
			seg = new Segment(source.topRight, target.topRight);
			if (target.right() - 1 > source.x)
				seg2 = new Segment(source.topLeft, target.bottomRight);
			else
				seg2 = new Segment(source.bottomLeft, target.topRight);
		} else if (source.right() == target.right()) {
			seg = new Segment(source.topRight, target.bottomRight);
			seg2 = new Segment(source.topLeft, target.bottomRight);
		} else {
			seg = new Segment(source.bottomRight, target.bottomRight);
			seg2 = new Segment(source.topLeft, target.bottomRight);
		}

		stack.push(source);
		stack.push(target);
		stack.push(seg);
		stack.push(source);
		stack.push(target);
		stack.push(seg2);
	}

	private void addSegmentsTargetBesideSource(Obstacle source, Obstacle target) {
		// target located above source
		Segment seg = null;
		Segment seg2 = null;
		if (target.y > source.y) {
			seg = new Segment(source.topLeft, target.topLeft);
			if (target.y < source.bottom() - 1)
				seg2 = new Segment(source.bottomLeft, target.topRight);
			else
				seg2 = new Segment(source.bottomRight, target.topLeft);
		} else if (source.y == target.y) {
			// degenerate case
			seg = new Segment(source.topLeft, target.topRight);
			seg2 = new Segment(source.bottomLeft, target.topRight);
		} else {
			seg = new Segment(source.topRight, target.topRight);
			seg2 = new Segment(source.bottomLeft, target.topRight);
		}
		stack.push(source);
		stack.push(target);
		stack.push(seg);
		stack.push(source);
		stack.push(target);
		stack.push(seg2);
		seg = null;
		seg2 = null;

		if (target.bottom() < source.bottom()) {
			seg = new Segment(source.bottomLeft, target.bottomLeft);
			if (target.bottom() - 1 > source.y)
				seg2 = new Segment(source.topLeft, target.bottomRight);
			else
				seg2 = new Segment(source.topRight, target.bottomLeft);
		} else if (source.bottom() == target.bottom()) {
			seg = new Segment(source.bottomLeft, target.bottomRight);
			seg2 = new Segment(source.topLeft, target.bottomRight);
		} else {
			seg = new Segment(source.bottomRight, target.bottomRight);
			seg2 = new Segment(source.topLeft, target.bottomRight);
		}
		stack.push(source);
		stack.push(target);
		stack.push(seg);
		stack.push(source);
		stack.push(target);
		stack.push(seg2);
	}

	/**
 * 
 */
	void cleanup() {
		// segments.clear();
		visibleVertices.clear();
	}

	/**
	 * Begins the creation of the visibility graph with the first segment
	 * 
	 * @param allObstacles
	 *            list of all obstacles
	 */
	private void createVisibilityGraph(List allObstacles) {
		stack.push(null);
		stack.push(null);
		stack.push(new Segment(start, end));

		while (!stack.isEmpty())
			addSegment(stack.pop(), stack.popObstacle(), stack.popObstacle(),
					allObstacles);
	}

	/**
	 * Once the visibility graph is constructed, this is called to label the
	 * graph and determine the shortest path. Returns false if no path can be
	 * found.
	 * 
	 * @return true if a path can be found.
	 */
	private boolean determineShortestPath() {
		if (!labelGraph())
			return false;
		Vertex vertex = end;
		prevCostRatio = end.cost / start.getDistance(end);

		Vertex nextVertex;
		while (!vertex.equals(start)) {
			nextVertex = vertex.label;
			if (nextVertex == null)
				return false;
			Segment s = new Segment(nextVertex, vertex);
			segments.add(s);
			vertex = nextVertex;
		}

		Collections.reverse(segments);
		return true;
	}

	/**
	 * Resets all necessary fields for a solve.
	 */
	void fullReset() {
		visibleVertices.clear();
		segments.clear();
		if (prevCostRatio == 0) {
			double distance = start.getDistance(end);
			threshold = distance * OVAL_CONSTANT;
		} else
			threshold = prevCostRatio * EPSILON * start.getDistance(end);
		visibleObstacles.clear();
		resetPartial();
	}

	/**
	 * Creates the visibility graph and returns whether or not a shortest path
	 * could be determined.
	 * 
	 * @param allObstacles
	 *            the list of all obstacles
	 * @return true if a shortest path was found
	 */
	boolean generateShortestPath(List allObstacles) {
		createVisibilityGraph(allObstacles);

		if (visibleVertices.size() == 0)
			return false;

		return determineShortestPath();
	}

	/**
	 * Returns the list of constrained points through which this path must pass
	 * or <code>null</code>.
	 * 
	 * @see #setBendPoints(PointList)
	 * @return list of bend points
	 */
	public PointList getBendPoints() {
		return bendpoints;
	}

	/**
	 * Returns the end point for this path
	 * 
	 * @return end point for this path
	 */
	public Point getEndPoint() {
		return end;
	}

	/**
	 * Returns the solution to this path.
	 * 
	 * @return the points for this path.
	 */
	public PointList getPoints() {
		return points;
	}

	/**
	 * Returns the start point for this path
	 * 
	 * @return start point for this path
	 */
	public Point getStartPoint() {
		return start;
	}

	/**
	 * Returns a subpath for this path at the given segment
	 * 
	 * @param currentSegment
	 *            the segment at which the subpath should be created
	 * @return the new path
	 */
	Path getSubPath(Segment currentSegment) {
		// ready new path
		Path newPath = new Path(currentSegment.start, end);
		newPath.grownSegments = new ArrayList(grownSegments.subList(
				grownSegments.indexOf(currentSegment), grownSegments.size()));

		// fix old path
		grownSegments = new ArrayList(grownSegments.subList(0,
				grownSegments.indexOf(currentSegment) + 1));
		end = currentSegment.end;

		subPath = newPath;
		return newPath;
	}

	/**
	 * Resets the vertices that this path has traveled prior to this segment.
	 * This is called when the path has become inverted and needs to rectify any
	 * labeling mistakes it made before it knew it was inverted.
	 * 
	 * @param currentSegment
	 *            the segment at which the path found it was inverted
	 */
	void invertPriorVertices(Segment currentSegment) {
		int stop = grownSegments.indexOf(currentSegment);
		for (int i = 0; i < stop; i++) {
			Vertex vertex = ((Segment) grownSegments.get(i)).end;
			if (vertex.type == Vertex.INNIE)
				vertex.type = Vertex.OUTIE;
			else
				vertex.type = Vertex.INNIE;
		}
	}

	/**
	 * Returns true if this obstacle is in the visibility graph
	 * 
	 * @param obs
	 *            the obstacle
	 * @return true if obstacle is in the visibility graph
	 */
	boolean isObstacleVisible(Obstacle obs) {
		return visibleObstacles.contains(obs);
	}

	/**
	 * Labels the visibility graph to assist in finding the shortest path
	 * 
	 * @return false if there was a gap in the visibility graph
	 */
	private boolean labelGraph() {
		int numPermanentNodes = 1;
		Vertex vertex = start;
		Vertex neighborVertex = null;
		vertex.isPermanent = true;
		double newCost;
		while (numPermanentNodes != visibleVertices.size()) {
			List neighbors = vertex.neighbors;
			if (neighbors == null)
				return false;
			// label neighbors if they have a new shortest path
			for (int i = 0; i < neighbors.size(); i++) {
				neighborVertex = (Vertex) neighbors.get(i);
				if (!neighborVertex.isPermanent) {
					newCost = vertex.cost + vertex.getDistance(neighborVertex);
					if (neighborVertex.label == null) {
						neighborVertex.label = vertex;
						neighborVertex.cost = newCost;
					} else if (neighborVertex.cost > newCost) {
						neighborVertex.label = vertex;
						neighborVertex.cost = newCost;
					}
				}
			}
			// find the next none-permanent, labeled vertex with smallest cost
			double smallestCost = 0;
			Vertex tempVertex = null;
			Iterator v = visibleVertices.iterator();
			while (v.hasNext()) {
				tempVertex = (Vertex) v.next();
				if (!tempVertex.isPermanent
						&& tempVertex.label != null
						&& (tempVertex.cost < smallestCost || smallestCost == 0)) {
					smallestCost = tempVertex.cost;
					vertex = tempVertex;
				}
			}
			// set the new vertex to permanent.
			vertex.isPermanent = true;
			numPermanentNodes++;
		}
		return true;
	}

	/**
	 * Links two vertices together in the visibility graph
	 * 
	 * @param segment
	 *            the segment to add
	 */
	private void linkVertices(Segment segment) {
		if (segment.start.neighbors == null)
			segment.start.neighbors = new ArrayList();
		if (segment.end.neighbors == null)
			segment.end.neighbors = new ArrayList();

		if (!segment.start.neighbors.contains(segment.end)) {
			segment.start.neighbors.add(segment.end);
			segment.end.neighbors.add(segment.start);
		}

		visibleVertices.add(segment.start);
		visibleVertices.add(segment.end);
	}

	/**
	 * Called to reconnect a subpath back onto this path. Does a depth-first
	 * search to reconnect all paths. Should be called after sorting.
	 */
	void reconnectSubPaths() {
		if (subPath != null) {
			subPath.reconnectSubPaths();

			Segment changedSegment = (Segment) subPath.grownSegments.remove(0);
			Segment oldSegment = (Segment) grownSegments.get(grownSegments
					.size() - 1);

			oldSegment.end = changedSegment.end;
			grownSegments.addAll(subPath.grownSegments);

			subPath.points.removePoint(0);
			points.removePoint(points.size() - 1);
			points.addAll(subPath.points);

			visibleObstacles.addAll(subPath.visibleObstacles);

			end = subPath.end;
			subPath = null;
		}
	}

	/**
	 * Refreshes the exclude field on the obstacles in the list. Excludes all
	 * obstacles that contain the start or end point for this path.
	 * 
	 * @param allObstacles
	 *            list of all obstacles
	 */
	void refreshExcludedObstacles(List allObstacles) {
		excludedObstacles.clear();

		for (int i = 0; i < allObstacles.size(); i++) {
			Obstacle o = (Obstacle) allObstacles.get(i);
			o.exclude = false;

			if (o.contains(start)) {
				if (o.containsProper(start))
					o.exclude = true;
				else {
					/*
					 * $TODO Check for corners. If the path begins exactly at
					 * the corner of an obstacle, the exclude should also be
					 * true.
					 * 
					 * Or, change segment intersection so that two segments that
					 * share an endpoint do not intersect.
					 */
				}
			}

			if (o.contains(end)) {
				if (o.containsProper(end))
					o.exclude = true;
				else {
					// check for corners. See above statement.
				}
			}

			if (o.exclude && !excludedObstacles.contains(o))
				excludedObstacles.add(o);
		}
	}

	/**
	 * Resets the fields for everything in the solve after the visibility graph
	 * steps.
	 */
	void resetPartial() {
		isMarked = false;
		isInverted = false;
		subPath = null;
		isDirty = false;
		grownSegments.clear();
		points.removeAllPoints();
	}

	/**
	 * Sets the list of bend points to the given list and dirties the path.
	 * 
	 * @param bendPoints
	 *            the list of bend points
	 */
	public void setBendPoints(PointList bendPoints) {
		this.bendpoints = bendPoints;
		isDirty = true;
	}

	/**
	 * Sets the end point for this path to the given point.
	 * 
	 * @param end
	 *            the new end point for this path
	 */
	public void setEndPoint(Point end) {
		if (end.equals(this.end))
			return;
		this.end = new Vertex(end, null);
		isDirty = true;
	}

	/**
	 * Sets the start point for this path to the given point.
	 * 
	 * @param start
	 *            the new start point for this path
	 */
	public void setStartPoint(Point start) {
		if (start.equals(this.start))
			return;
		this.start = new Vertex(start, null);
		isDirty = true;
	}

	/**
	 * Returns <code>true</code> if the path is clean and intersects the given
	 * obstacle. Also dirties the path in the process.
	 * 
	 * @since 3.0
	 * @param obs
	 *            the obstacle
	 * @return <code>true</code> if a clean path touches the obstacle
	 */
	boolean testAndSet(Obstacle obs) {
		if (isDirty)
			return false;
		// This will never actually happen because obstacles are not stored by
		// identity
		if (excludedObstacles.contains(obs))
			return false;

		Segment seg1 = new Segment(obs.topLeft, obs.bottomRight);
		Segment seg2 = new Segment(obs.topRight, obs.bottomLeft);

		for (int s = 0; s < points.size() - 1; s++) {
			points.getPoint(CURRENT, s);
			points.getPoint(NEXT, s + 1);

			if (seg1.intersects(CURRENT, NEXT)
					|| seg2.intersects(CURRENT, NEXT) || obs.contains(CURRENT)
					|| obs.contains(NEXT)) {
				isDirty = true;
				return true;
			}
		}
		return false;
	}

}
