/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.routing.util; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.opentripplanner.common.pqueue.BinHeap; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; /** * Path finder for use in debugging and testing the vehicle location inference * engine. Specs from Brandon Willard: Given an origin and destination edge, * find all paths of a given length that could connect the two. The path length * is to be interpreted as being exact, but some degree of tolerance is provided * by the fact that edges have length. The complexity is exponential in path * length. However, these methods will only be used over timesteps of around 30 * seconds, so search space should still be limited in practice. The paths are * not necessarily needed - only the edges that make up those paths are * essential. This fact can be exploited to reduce complexity by not storing * back pointers and just keeping a set of edges touched. Rather than finding * all paths with some exact length, we can also just place an upper bound on * shortest path length, and include all edges that are in any such shortest * path. We may actually want to index turn *vertices* and use them to begin at * all turn edges in a bundle. * * Apparently skipping paths containing cycles is in fact not a win. Tracking which * edges have been visited with a multiset doubles the run time, and runtime remains * almost unchanged when using a plain old hashset. There are just not that many * solution paths with repeated loops to eliminate! * * This is likely because the length of loops must be "tuned" just right to hit the * target edge given the distance constraint. In a naive recursive enumeration, these * useless loops would still be followed in circles until the target length is * exceeded. Our pre-computed lower bound on distance to the destination helps * prune paths containing repeated occurrences of "out of tune" loops before * they branch out of control. * * @author abyrd */ public class LengthConstrainedPathFinder { //these represent being somewhere on the linestring private final Edge startEdge, targetEdge; private final double targetLength, epsilon; private Set<State> solutions; // cached derived values private final boolean reverse; public final Map<Vertex, Double> bounds; public LengthConstrainedPathFinder (Edge startVertex, Edge targetVertex, double targetLength, double epsilon, boolean calculateBounds) { this.startEdge = startVertex; this.targetEdge = targetVertex; reverse = targetLength < 0; targetLength = Math.abs(targetLength); this.targetLength = targetLength; if (epsilon < 0) throw new InvalidParameterException(); else this.epsilon = epsilon; this.bounds = calculateBounds ? findBounds() : null; } // use reverse search from the target edge to find lower bounds on distance for pruning private Map<Vertex, Double> findBounds() { Map<Vertex, Double> ret = new HashMap<Vertex, Double>(); ret.put(targetEdge.getToVertex(), 0.0); BinHeap<Vertex> pq = new BinHeap<Vertex>(); pq.insert(targetEdge.getToVertex(), 0.0); while ( ! pq.empty()) { double length0 = pq.peek_min_key(); Vertex v0 = pq.extract_min(); for (Edge edge : getOutgoing(v0, !reverse)) { double length1 = length0 + edge.getDistance(); // sense intentionally swapped because this is a search backward from target Vertex v1 = reverse ? edge.getToVertex() : edge.getFromVertex(); if (length1 < targetLength) { Double existingLength = ret.get(v1); if (existingLength == null || length1 < existingLength) { ret.put(v1, length1); pq.insert(v1, length1); } } } } return ret; } /** returns cached solutions if any are present, otherwise finds them with DFS */ public Set<State> getSolutions() { if (solutions == null) solveDepthFirst(); return solutions; } /** accounts for negative target distances */ private Iterable<Edge> getOutgoing(Vertex vertex, boolean reverse) { if (reverse) return vertex.getIncoming(); else return vertex.getOutgoing(); } // /** breadth-first search */ // public Set<State> solveBreadthFirst() { // solutions = new HashSet<State>(); // Queue<State> q = new LinkedList<State>(); // q.add(new State(startEdge)); // while ( ! q.isEmpty()) { // State s0 = q.poll(); // //System.out.println(s0.toString()); // if (s0.isSolution()) // solutions.add(s0); // for (Edge edge : getAdjacentEdges(s0.edge, reverse)) { // State s1 = s0.traverse(edge); // if (s1.isCandidate()) // q.add(s1); // } // } // return getSolutions(); // } /** recursive depth-first search using JVM stack */ public Set<State> solveDepthFirst() { solutions = new HashSet<State>(); depthFirst(new State(startEdge.getFromVertex())); return getSolutions(); } private void depthFirst(State s0) { if (s0.isSolution()) solutions.add(s0); for (Edge edge : getOutgoing(s0.vertex, reverse)) { //skip direct back-and-forthing if (s0.back != null && (reverse ? edge.getFromVertex() : edge.getToVertex()) == s0.back.vertex) { continue; } State s1 = s0.traverse(edge); if (s1.isCandidate()) depthFirst(s1); } } public class State { final Vertex vertex; final double minLength; // before traversing the edge, without initial/final edge fragments final State back; public State(Vertex vertex) { // make initial state. min distance should be 0 paths of length 2. // zero is harmless because it is only used as a lower bound this(vertex, 0, null); } private State(Vertex vertex, double minLength, State back) { this.vertex = vertex; this.minLength = minLength; this.back = back; } public List<Vertex> toVertexList() { List<Vertex> ret = new LinkedList<Vertex>(); for (State s = this; s != null; s = s.back) ret.add(0, s.vertex); return ret; } private boolean isCandidate() { Double lBound = 0.0; if (bounds != null) lBound = bounds.get(this.vertex); if (lBound == null) lBound = Double.POSITIVE_INFINITY; return this.minLength - epsilon + lBound < targetLength; } private double getMaxLength() { return this.minLength; } private boolean isSolution() { return vertex == targetEdge.getToVertex() && this.minLength - epsilon <= targetLength && this.getMaxLength() + epsilon >= targetLength; } private State traverse(Edge edge) { return new State(reverse ? edge.getFromVertex() : edge.getToVertex(), this.minLength + edge.getDistance(), this); } public String toString() { return String.format("%5.0f %s", minLength, vertex); } public String toStringVerbose() { List<Vertex> vertices = this.toVertexList(); return String.format("%5.0f %d %s", this.minLength, vertices.size(), vertices); } } public Map<Vertex, Double> pathProportions() { Map<Vertex, Double> vertexCounts = new HashMap<Vertex, Double>(); for (State path : getSolutions()) { for (Vertex vertex : path.toVertexList()) { Double count = vertexCounts.get(vertex); if (count == null) count = 0.0; vertexCounts.put(vertex, count + 1); } } int nPaths = getSolutions().size(); List<Vertex> vs = new ArrayList<Vertex>(vertexCounts.keySet()); for (Vertex v : vs) { Double count = vertexCounts.get(v); vertexCounts.put(v, count / nPaths); } return vertexCounts; } }