blob: fc6faa3b4ae99d1b6a7d8e8f79e15935c5c74bf4 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}