Mercurial > projects > dwt-addons
diff dwtx/draw2d/graph/Path.d @ 98:95307ad235d9
Added Draw2d code, still work in progress
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Sun, 03 Aug 2008 00:52:14 +0200 |
parents | |
children | 2d6540440fe6 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/draw2d/graph/Path.d Sun Aug 03 00:52:14 2008 +0200 @@ -0,0 +1,838 @@ +/******************************************************************************* + * 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 + * Port to the D programming language: + * Frank Benoit <benoit@tionex.de> + *******************************************************************************/ +module dwtx.draw2d.graph.Path; + +import dwt.dwthelper.utils; +import dwtx.dwtxhelper.Collection; + +import dwtx.draw2d.PositionConstants; +import dwtx.draw2d.geometry.Point; +import dwtx.draw2d.geometry.PointList; +import dwtx.draw2d.graph.Segment; +import dwtx.draw2d.graph.Vertex; +import dwtx.draw2d.graph.Obstacle; + +/** + * 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 : ArrayList { + +Segment pop() { + return cast(Segment)remove(size() - 1); +} + +Obstacle popObstacle() { + return cast(Obstacle)remove(size() - 1); +} + +void push(Object obj) { + add(obj); +} + +} + +private static const Point CURRENT; +private static const double EPSILON = 1.04; +private static const Point NEXT; +private static const double OVAL_CONSTANT = 1.13; + +static this(){ + CURRENT = new Point(); + NEXT = new Point(); +} + +/** + * 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 bool isDirty = true; + +bool isInverted = false; +bool 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 this() { + 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 this(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 this(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 + */ +this(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() is 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 is 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 is 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() is 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 bool 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, + bool checkTopRight1, bool checkTopRight2) { + if (threshold !is 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 = cast(Obstacle)oItr.next(); + if (newObs !is 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 !is 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 = cast(Obstacle)allObstacles.get(i); + + if (obs is exclude1 || obs is 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 is obs.x) { + seg = new Segment(vertex, obs.topLeft); + seg2 = new Segment(vertex, obs.bottomLeft); + } else if (vertex.y is obs.y) { + seg = new Segment(vertex, obs.topLeft); + seg2 = new Segment(vertex, obs.topRight); + } else if (vertex.y is obs.bottom() - 1) { + seg = new Segment(vertex, obs.bottomLeft); + seg2 = new Segment(vertex, obs.bottomRight); + } else if (vertex.x is 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 is 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() is 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 is 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() is 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 bool determineShortestPath() { + if (!labelGraph()) + return false; + Vertex vertex = end; + prevCostRatio = end.cost / start.getDistance(end); + + Vertex nextVertex; + while (!vertex.opEquals(start)) { + nextVertex = vertex.label; + if (nextVertex is 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 is 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 + */ +bool generateShortestPath(List allObstacles) { + createVisibilityGraph(allObstacles); + + if (visibleVertices.size() is 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 = (cast(Segment)grownSegments.get(i)).end; + if (vertex.type is 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 + */ +bool 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 bool labelGraph() { + int numPermanentNodes = 1; + Vertex vertex = start; + Vertex neighborVertex = null; + vertex.isPermanent = true; + double newCost; + while (numPermanentNodes !is visibleVertices.size()) { + List neighbors = vertex.neighbors; + if (neighbors is null) + return false; + // label neighbors if they have a new shortest path + for (int i = 0; i < neighbors.size(); i++) { + neighborVertex = cast(Vertex)neighbors.get(i); + if (!neighborVertex.isPermanent) { + newCost = vertex.cost + vertex.getDistance(neighborVertex); + if (neighborVertex.label is 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 = cast(Vertex)v.next(); + if (!tempVertex.isPermanent && tempVertex.label !is null + && (tempVertex.cost < smallestCost || smallestCost is 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 is null) + segment.start.neighbors = new ArrayList(); + if (segment.end.neighbors is 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 !is null) { + subPath.reconnectSubPaths(); + + Segment changedSegment = cast(Segment)subPath.grownSegments.remove(0); + Segment oldSegment = cast(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 = cast(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.opEquals(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.opEquals(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 + */ + bool 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; +} + +}