blob: ac1156ad4a673702fe1421f91696423e63b977ce [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2005 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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;
/**
* Bends a collection of {@link Path Paths} around rectangular obstacles. This class
* maintains a list of paths and obstacles. Updates can be made to the paths and/or
* obstacles, and then an incremental solve can be invoked.
* <P>
* The algorithm will attempt to find the shortest non-intersecting path between each
* path's start and end points. Once all paths have been found, they will be offset based
* on how many paths bend around the same corner of each obstacle.
* <P>
* The worst-case performance of this algorithm is p * s * n^2, where p is the number of
* paths, n is the number of obstacles, and s is the average number of segments in each
* path's final solution.
* <P>
* This class is not intended to be subclassed.
* @author Whitney Sorenson
* @author Randy Hudson
* @since 3.0
*/
public class ShortestPathRouter {
/**
* A stack of Paths.
*/
static class PathStack extends ArrayList {
Path pop() {
return (Path)remove(size() - 1);
}
void push(Path path) {
add(path);
}
}
/**
* The number of times to grow obstacles and test for intersections. This is a tradeoff
* between performance and quality of output.
*/
private static final int NUM_GROW_PASSES = 2;
private int spacing = 4;
private boolean growPassChangedObstacles;
private List orderedPaths;
private Map pathsToChildPaths;
private PathStack stack;
private List subPaths;
private List userObstacles;
private List userPaths;
private List workingPaths;
/**
* Creates a new shortest path routing.
*/
public ShortestPathRouter() {
userPaths = new ArrayList();
workingPaths = new ArrayList();
pathsToChildPaths = new HashMap();
userObstacles = new ArrayList();
}
/**
* Adds an obstacle with the given bounds to the obstacles.
*
* @param rect the bounds of this obstacle
* @return <code>true</code> if the added obstacle has dirtied one or more paths
*/
public boolean addObstacle(Rectangle rect) {
return internalAddObstacle(new Obstacle(rect, this));
}
/**
* Adds a path to the routing.
* @param path the path to add.
*/
public void addPath(Path path) {
userPaths.add(path);
workingPaths.add(path);
}
/**
* Fills the point lists of the Paths to the correct bent points.
*/
private void bendPaths() {
for (int i = 0; i < orderedPaths.size(); i++) {
Path path = (Path) orderedPaths.get(i);
Segment segment = null;
path.points.addPoint(new Point(path.start.x, path.start.y));
for (int v = 0; v < path.grownSegments.size(); v++) {
segment = (Segment) path.grownSegments.get(v);
Vertex vertex = segment.end;
if (vertex != null && v < path.grownSegments.size() - 1) {
if (vertex.type == Vertex.INNIE) {
vertex.count++;
path.points.addPoint(vertex.bend(vertex.count));
} else {
path.points.addPoint(vertex.bend(vertex.totalCount));
vertex.totalCount--;
}
}
}
path.points.addPoint(new Point(path.end.x, path.end.y));
}
}
/**
* Checks a vertex to see if its offset should shrink
* @param vertex the vertex to check
*/
private void checkVertexForIntersections(Vertex vertex) {
if (vertex.nearestObstacle != 0 || vertex.nearestObstacleChecked)
return;
int sideLength, x, y;
sideLength = 2 * (vertex.totalCount * getSpacing()) + 1;
if ((vertex.positionOnObstacle & PositionConstants.NORTH) > 0)
y = vertex.y - sideLength;
else
y = vertex.y;
if ((vertex.positionOnObstacle & PositionConstants.EAST) > 0)
x = vertex.x;
else
x = vertex.x - sideLength;
Rectangle r = new Rectangle(x, y, sideLength, sideLength);
int xDist, yDist;
for (int o = 0; o < userObstacles.size(); o++) {
Obstacle obs = (Obstacle)userObstacles.get(o);
if (obs != vertex.obs && r.intersects(obs)) {
int pos = obs.getPosition(vertex);
if (pos == 0)
continue;
if ((pos & PositionConstants.NORTH) > 0)
// use top
yDist = obs.y - vertex.y;
else
// use bottom
yDist = vertex.y - obs.bottom() + 1;
if ((pos & PositionConstants.EAST) > 0)
// use right
xDist = vertex.x - obs.right() + 1;
else
// use left
xDist = obs.x - vertex.x;
if (Math.max(xDist, yDist) < vertex.nearestObstacle
|| vertex.nearestObstacle == 0) {
vertex.nearestObstacle = Math.max(xDist, yDist);
vertex.updateOffset();
}
}
}
vertex.nearestObstacleChecked = true;
}
/**
* Checks all vertices along paths for intersections
*/
private void checkVertexIntersections() {
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path)workingPaths.get(i);
for (int s = 0; s < path.segments.size() - 1; s++) {
Vertex vertex = ((Segment)path.segments.get(s)).end;
checkVertexForIntersections(vertex);
}
}
}
/**
* Frees up fields which aren't needed between invocations.
*/
private void cleanup() {
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path)workingPaths.get(i);
path.cleanup();
}
}
/**
* Counts how many paths are on given vertices in order to increment their total count.
*/
private void countVertices() {
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path) workingPaths.get(i);
for (int v = 0; v < path.segments.size() - 1; v++)
((Segment)path.segments.get(v)).end.totalCount++;
}
}
/**
* Dirties the paths that are on the given vertex
* @param vertex the vertex that has the paths
*/
private boolean dirtyPathsOn(Vertex vertex) {
List paths = vertex.paths;
if (paths != null && paths.size() != 0) {
for (int i = 0; i < paths.size(); i++)
((Path)paths.get(i)).isDirty = true;
return true;
}
return false;
}
/**
* Resyncs the parent paths with any new child paths that are necessary because bendpoints
* have been added to the parent path.
*
private void generateChildPaths() {
for (int i = 0; i < userPaths.size(); i++) {
Path path = (Path)userPaths.get(i);
PointList bendPoints = path.bendpoints;
if (bendPoints != null && bendPoints.size() != 0) {
List childPaths = new ArrayList(bendPoints.size() + 1);
Path child = null;
Vertex prevVertex = path.start;
Vertex currVertex = null;
for (int b = 0; b < bendPoints.size(); b++) {
Point bp = (Point)bendPoints.getPoint(b);
currVertex = new Vertex(bp, null);
child = new Path(prevVertex, currVertex);
childPaths.add(child);
workingPaths.add(child);
prevVertex = currVertex;
}
child = new Path(prevVertex, path.end);
childPaths.add(child);
workingPaths.add(child);
pathsToChildPaths.put(path, childPaths);
} else
workingPaths.add(path);
} //End FOR
}*/
/**
* Returns the closest vertex to the given segment.
* @param v1 the first vertex
* @param v2 the second vertex
* @param segment the segment
* @return v1, or v2 whichever is closest to the segment
*/
private Vertex getNearestVertex(Vertex v1, Vertex v2, Segment segment) {
if (segment.start.getDistance(v1) + segment.end.getDistance(v1)
> segment.start.getDistance(v2) + segment.end.getDistance(v2))
return v2;
else
return v1;
}
/**
* Returns the spacing maintained between paths.
* @return the default path spacing
* @see #setSpacing(int)
* @since 3.2
*/
public int getSpacing() {
return spacing;
}
/**
* Returns the subpath for a split on the given path at the given segment.
* @param path the path
* @param segment the segment
* @return the new subpath
*/
private Path getSubpathForSplit(Path path, Segment segment) {
Path newPath = path.getSubPath(segment);
workingPaths.add(newPath);
subPaths.add(newPath);
return newPath;
}
/**
* Grows all obstacles in in routing and tests for new intersections
*/
private void growObstacles() {
growPassChangedObstacles = false;
for (int i = 0; i < NUM_GROW_PASSES; i++) {
if (i == 0 || growPassChangedObstacles)
growObstaclesPass();
}
}
/**
* Performs a single pass of the grow obstacles step, this can be repeated as desired.
* Grows obstacles, then tests paths against the grown obstacles.
*/
private void growObstaclesPass() {
// grow obstacles
for (int i = 0; i < userObstacles.size(); i++)
((Obstacle)userObstacles.get(i)).growVertices();
// go through paths and test segments
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path) workingPaths.get(i);
for (int e = 0; e < path.excludedObstacles.size(); e++)
((Obstacle)path.excludedObstacles.get(e)).exclude = true;
if (path.grownSegments.size() == 0) {
for (int s = 0; s < path.segments.size(); s++)
testOffsetSegmentForIntersections((Segment)path.segments.get(s), -1, path);
} else {
int counter = 0;
List currentSegments = new ArrayList(path.grownSegments);
for (int s = 0; s < currentSegments.size(); s++)
counter += testOffsetSegmentForIntersections((Segment)currentSegments.get(s), s + counter, path);
}
for (int e = 0; e < path.excludedObstacles.size(); e++)
((Obstacle)path.excludedObstacles.get(e)).exclude = false;
}
// revert obstacles
for (int i = 0; i < userObstacles.size(); i++)
((Obstacle)userObstacles.get(i)).shrinkVertices();
}
/**
* Adds an obstacle to the routing
* @param obs the obstacle
*/
private boolean internalAddObstacle(Obstacle obs) {
userObstacles.add(obs);
return testAndDirtyPaths(obs);
}
/**
* Removes an obstacle from the routing.
* @param rect the bounds of the obstacle
* @return the obstacle removed
*/
private boolean internalRemoveObstacle(Rectangle rect) {
Obstacle obs = null;
int index = -1;
for (int i = 0; i < userObstacles.size(); i++) {
obs = (Obstacle)userObstacles.get(i);
if (obs.equals(rect)) {
index = i;
break;
}
}
userObstacles.remove(index);
boolean result = false;
result |= dirtyPathsOn(obs.bottomLeft);
result |= dirtyPathsOn(obs.topLeft);
result |= dirtyPathsOn(obs.bottomRight);
result |= dirtyPathsOn(obs.topRight);
for (int p = 0; p < workingPaths.size(); p++) {
Path path = (Path)workingPaths.get(p);
if (path.isDirty)
continue;
if (path.isObstacleVisible(obs))
path.isDirty = result = true;
}
return result;
}
/**
* Labels the given path's vertices as innies, or outies, as well as determining if this
* path is inverted.
* @param path the path
*/
private void labelPath(Path path) {
Segment segment = null;
Segment nextSegment = null;
Vertex vertex = null;
boolean agree = false;
for (int v = 0; v < path.grownSegments.size() - 1; v++) {
segment = (Segment) path.grownSegments.get(v);
nextSegment = (Segment) path.grownSegments.get(v + 1);
vertex = segment.end;
long crossProduct = segment.crossProduct(new Segment(vertex, vertex.obs.center));
if (vertex.type == Vertex.NOT_SET) {
labelVertex(segment, crossProduct, path);
} else if (!path.isInverted
&& ((crossProduct > 0 && vertex.type == Vertex.OUTIE)
|| (crossProduct < 0 && vertex.type == Vertex.INNIE))) {
if (agree) {
// split detected.
stack.push(getSubpathForSplit(path, segment));
return;
} else {
path.isInverted = true;
path.invertPriorVertices(segment);
}
} else if (path.isInverted
&& ((crossProduct < 0 && vertex.type == Vertex.OUTIE)
|| (crossProduct > 0 && vertex.type == Vertex.INNIE))) {
// split detected.
stack.push(getSubpathForSplit(path, segment));
return;
} else
agree = true;
if (vertex.paths != null) {
for (int i = 0;i < vertex.paths.size();i++) {
Path nextPath = (Path)vertex.paths.get(i);
if (!nextPath.isMarked) {
nextPath.isMarked = true;
stack.push(nextPath);
}
}
}
vertex.addPath(path, segment, nextSegment);
}
}
/**
* Labels all path's vertices in the routing.
*/
private void labelPaths() {
Path path = null;
for (int i = 0; i < workingPaths.size(); i++) {
path = (Path) workingPaths.get(i);
stack.push(path);
}
while (!stack.isEmpty()) {
path = stack.pop();
if (!path.isMarked) {
path.isMarked = true;
labelPath(path);
}
}
// revert is marked so we can use it again in ordering.
for (int i = 0;i < workingPaths.size(); i++) {
path = (Path)workingPaths.get(i);
path.isMarked = false;
}
}
/**
* Labels the vertex at the end of the semgent based on the cross product.
* @param segment the segment to this vertex
* @param crossProduct the cross product of this segment and a segment to the obstacles center
* @param path the path
*/
private void labelVertex(Segment segment, long crossProduct, Path path) {
// assumes vertex in question is segment.end
if (crossProduct > 0) {
if (path.isInverted)
segment.end.type = Vertex.OUTIE;
else
segment.end.type = Vertex.INNIE;
} else if (crossProduct < 0) {
if (path.isInverted)
segment.end.type = Vertex.INNIE;
else
segment.end.type = Vertex.OUTIE;
} else if (segment.start.type != Vertex.NOT_SET)
segment.end.type = segment.start.type;
else
segment.end.type = Vertex.INNIE;
}
/**
* Orders the path by comparing its angle at shared vertices with other paths.
* @param path the path
*/
private void orderPath(Path path) {
if (path.isMarked)
return;
path.isMarked = true;
Segment segment = null;
Vertex vertex = null;
for (int v = 0; v < path.grownSegments.size() - 1; v++) {
segment = (Segment) path.grownSegments.get(v);
vertex = segment.end;
double thisAngle = ((Double)vertex.cachedCosines.get(path)).doubleValue();
if (path.isInverted)
thisAngle = -thisAngle;
for (int i = 0; i < vertex.paths.size(); i++) {
Path vPath = (Path)vertex.paths.get(i);
if (!vPath.isMarked) {
double otherAngle = ((Double)vertex.cachedCosines.get(vPath)).doubleValue();
if (vPath.isInverted)
otherAngle = -otherAngle;
if (otherAngle < thisAngle)
orderPath(vPath);
}
}
}
orderedPaths.add(path);
}
/**
* Orders all paths in the graph.
*/
private void orderPaths() {
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path) workingPaths.get(i);
orderPath(path);
}
}
/**
* Populates the parent paths with all the child paths that were created to represent
* bendpoints.
*/
private void recombineChildrenPaths() {
// only populate those paths with children paths.
Iterator keyItr = pathsToChildPaths.keySet().iterator();
while (keyItr.hasNext()) {
Path path = (Path)keyItr.next();
path.fullReset();
List childPaths = (List)pathsToChildPaths.get(path);
Path childPath = null;
for (int i = 0; i < childPaths.size(); i++) {
childPath = (Path)childPaths.get(i);
path.points.addAll(childPath.getPoints());
// path will overlap
path.points.removePoint(path.points.size() - 1);
//path.grownSegments.addAll(childPath.grownSegments);
path.segments.addAll(childPath.segments);
path.visibleObstacles.addAll(childPath.visibleObstacles);
}
// add last point.
path.points.addPoint(childPath.points.getLastPoint());
}
}
/**
* Reconnects all subpaths.
*/
private void recombineSubpaths() {
for (int p = 0; p < orderedPaths.size(); p++) {
Path path = (Path)orderedPaths.get(p);
path.reconnectSubPaths();
}
orderedPaths.removeAll(subPaths);
workingPaths.removeAll(subPaths);
subPaths = null;
}
/**
* Removes the obstacle with the rectangle's bounds from the routing.
*
* @param rect the bounds of the obstacle to remove
* @return <code>true</code> if the removal has dirtied one or more paths
*/
public boolean removeObstacle(Rectangle rect) {
return internalRemoveObstacle(rect);
}
/**
* Removes the given path from the routing.
*
* @param path the path to remove.
* @return <code>true</code> if the removal may have affected one of the remaining paths
*/
public boolean removePath(Path path) {
userPaths.remove(path);
List children = (List)pathsToChildPaths.get(path);
if (children == null)
workingPaths.remove(path);
else
workingPaths.removeAll(children);
return true;
}
/**
* Resets exclude field on all obstacles
*/
private void resetObstacleExclusions() {
for (int i = 0; i < userObstacles.size(); i++)
((Obstacle)userObstacles.get(i)).exclude = false;
}
/**
* Resets all vertices found on paths and obstacles.
*/
private void resetVertices() {
for (int i = 0; i < userObstacles.size(); i++) {
Obstacle obs = (Obstacle)userObstacles.get(i);
obs.reset();
}
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path)workingPaths.get(i);
path.start.fullReset();
path.end.fullReset();
}
}
/**
* Sets the default spacing between paths. The spacing is the minimum distance that path
* should be offset from other paths or obstacles. The default value is 4. When this value
* can not be satisfied, paths will be squeezed together uniformly.
* @param spacing the path spacing
* @since 3.2
*/
public void setSpacing(int spacing) {
this.spacing = spacing;
}
/**
* Updates the points in the paths in order to represent the current solution
* with the given paths and obstacles.
*
* @return returns the list of paths which were updated.
*/
public List solve() {
solveDirtyPaths();
countVertices();
checkVertexIntersections();
growObstacles();
subPaths = new ArrayList();
stack = new PathStack();
labelPaths();
stack = null;
orderedPaths = new ArrayList();
orderPaths();
bendPaths();
recombineSubpaths();
orderedPaths = null;
subPaths = null;
recombineChildrenPaths();
cleanup();
return Collections.unmodifiableList(userPaths);
}
/**
* Solves paths that are dirty.
* @return number of dirty paths
*/
private int solveDirtyPaths() {
int numSolved = 0;
for (int i = 0; i < userPaths.size(); i++) {
Path path = (Path)userPaths.get(i);
if (!path.isDirty)
continue;
List children = (List)pathsToChildPaths.get(path);
int prevCount = 1, newCount = 1;
if (children == null)
children = Collections.EMPTY_LIST;
else
prevCount = children.size();
if (path.getBendPoints() != null)
newCount = path.getBendPoints().size() + 1;
if (prevCount != newCount)
children = regenerateChildPaths(path, children, prevCount, newCount);
refreshChildrenEndpoints(path, children);
}
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path)workingPaths.get(i);
path.refreshExcludedObstacles(userObstacles);
if (!path.isDirty) {
path.resetPartial();
continue;
}
numSolved++;
path.fullReset();
boolean pathFoundCheck = path.generateShortestPath(userObstacles);
if (!pathFoundCheck || path.end.cost > path.threshold) {
// path not found, or path found was too long
resetVertices();
path.fullReset();
path.threshold = 0;
pathFoundCheck = path.generateShortestPath(userObstacles);
}
resetVertices();
}
resetObstacleExclusions();
if (numSolved == 0)
resetVertices();
return numSolved;
}
/**
* @since 3.0
* @param path
* @param children
*/
private void refreshChildrenEndpoints(Path path, List children) {
Point previous = path.getStartPoint();
Point next;
PointList bendpoints = path.getBendPoints();
Path child;
for (int i = 0; i < children.size(); i++) {
if (i < bendpoints.size())
next = bendpoints.getPoint(i);
else
next = path.getEndPoint();
child = (Path)children.get(i);
child.setStartPoint(previous);
child.setEndPoint(next);
previous = next;
}
}
/**
* @since 3.0
* @param path
* @param children
*/
private List regenerateChildPaths(Path path, List children, int currentSize, int newSize) {
//Path used to be simple but now is compound, children is EMPTY.
if (currentSize == 1) {
workingPaths.remove(path);
currentSize = 0;
children = new ArrayList(newSize);
pathsToChildPaths.put(path, children);
} else
//Path is becoming simple but was compound. children becomes empty.
if (newSize == 1) {
workingPaths.removeAll(children);
workingPaths.add(path);
pathsToChildPaths.remove(path);
return Collections.EMPTY_LIST;
}
//Add new working paths until the sizes are the same
while (currentSize < newSize) {
Path child = new Path();
workingPaths.add(child);
children.add(child);
currentSize++;
}
while (currentSize > newSize) {
Path child = (Path)children.remove(children.size() - 1);
workingPaths.remove(child);
currentSize--;
}
return children;
}
/**
* Tests a segment that has been offset for new intersections
* @param segment the segment
* @param index the index of the segment along the path
* @param path the path
* @return 1 if new segments have been inserted
*/
private int testOffsetSegmentForIntersections(Segment segment, int index, Path path) {
for (int i = 0; i < userObstacles.size(); i++) {
Obstacle obs = (Obstacle) userObstacles.get(i);
if (segment.end.obs == obs || segment.start.obs == obs || obs.exclude)
continue;
Vertex vertex = null;
int offset = getSpacing();
if (segment.getSlope() < 0) {
if (segment.intersects(obs.topLeft.x - offset,
obs.topLeft.y - offset,
obs.bottomRight.x + offset,
obs.bottomRight.y + offset))
vertex = getNearestVertex(obs.topLeft, obs.bottomRight, segment);
else if (segment.intersects(obs.bottomLeft.x - offset,
obs.bottomLeft.y + offset,
obs.topRight.x + offset,
obs.topRight.y - offset))
vertex = getNearestVertex(obs.bottomLeft, obs.topRight, segment);
} else {
if (segment.intersects(obs.bottomLeft.x - offset,
obs.bottomLeft.y + offset,
obs.topRight.x + offset,
obs.topRight.y - offset))
vertex = getNearestVertex(obs.bottomLeft, obs.topRight, segment);
else if (segment.intersects(obs.topLeft.x - offset,
obs.topLeft.y - offset,
obs.bottomRight.x + offset,
obs.bottomRight.y + offset))
vertex = getNearestVertex(obs.topLeft, obs.bottomRight, segment);
}
if (vertex != null) {
Rectangle vRect = vertex.getDeformedRectangle(offset);
if (segment.end.obs != null) {
Rectangle endRect = segment.end.getDeformedRectangle(offset);
if (vRect.intersects(endRect))
continue;
}
if (segment.start.obs != null) {
Rectangle startRect = segment.start.getDeformedRectangle(offset);
if (vRect.intersects(startRect))
continue;
}
Segment newSegmentStart = new Segment(segment.start, vertex);
Segment newSegmentEnd = new Segment(vertex, segment.end);
vertex.totalCount++;
vertex.nearestObstacleChecked = false;
vertex.shrink();
checkVertexForIntersections(vertex);
vertex.grow();
if (vertex.nearestObstacle != 0)
vertex.updateOffset();
growPassChangedObstacles = true;
if (index != -1) {
path.grownSegments.remove(segment);
path.grownSegments.add(index, newSegmentStart);
path.grownSegments.add(index + 1, newSegmentEnd);
} else {
path.grownSegments.add(newSegmentStart);
path.grownSegments.add(newSegmentEnd);
}
return 1;
}
}
if (index == -1)
path.grownSegments.add(segment);
return 0;
}
/**
* Tests all paths against the given obstacle
* @param obs the obstacle
*/
private boolean testAndDirtyPaths(Obstacle obs) {
boolean result = false;
for (int i = 0; i < workingPaths.size(); i++) {
Path path = (Path)workingPaths.get(i);
result |= path.testAndSet(obs);
}
return result;
}
/**
* Updates the position of an existing obstacle.
* @param oldBounds the old bounds(used to find the obstacle)
* @param newBounds the new bounds
* @return <code>true</code> if the change the current results to become stale
*/
public boolean updateObstacle(Rectangle oldBounds, Rectangle newBounds) {
boolean result = internalRemoveObstacle(oldBounds);
result |= addObstacle(newBounds);
return result;
}
}