/******************************************************************************* * 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; } }