/* 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.algorithm;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.opentripplanner.common.pqueue.BinHeap;
import org.opentripplanner.routing.algorithm.strategies.RemainingWeightHeuristic;
import org.opentripplanner.routing.algorithm.strategies.SearchTerminationStrategy;
import org.opentripplanner.routing.algorithm.strategies.TrivialRemainingWeightHeuristic;
import org.opentripplanner.routing.core.RoutingContext;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.spt.*;
import org.opentripplanner.util.DateUtils;
import org.opentripplanner.util.monitoring.MonitoringStore;
import org.opentripplanner.util.monitoring.MonitoringStoreFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.beust.jcommander.internal.Lists;
/**
* Find the shortest path between graph vertices using A*.
* A basic Dijkstra search is a special case of AStar where the heuristic is always zero.
*
* NOTE this is now per-request scoped, which has caused some threading problems in the past.
* Always make one new instance of this class per request, it contains a lot of state fields.
*/
public class AStar {
private static final Logger LOG = LoggerFactory.getLogger(AStar.class);
// FIXME this is not really a factory, it's a way to fake a global variable. This should be stored at the OTPServer level.
private static final MonitoringStore store = MonitoringStoreFactory.getStore();
private static final double OVERSEARCH_MULTIPLIER = 4.0;
private boolean verbose = false;
private TraverseVisitor traverseVisitor;
enum RunStatus {
RUNNING, STOPPED
}
/* TODO instead of having a separate class for search state, we should just make one GenericAStar per request. */
class RunState {
public State u;
public ShortestPathTree spt;
BinHeap<State> pq;
RemainingWeightHeuristic heuristic;
public RoutingContext rctx;
public int nVisited;
public List<State> targetAcceptedStates;
public RunStatus status;
private RoutingRequest options;
private SearchTerminationStrategy terminationStrategy;
public Vertex u_vertex;
Double foundPathWeight = null;
public RunState(RoutingRequest options, SearchTerminationStrategy terminationStrategy) {
this.options = options;
this.terminationStrategy = terminationStrategy;
}
}
private RunState runState;
/**
* Compute SPT using default timeout and termination strategy.
*/
public ShortestPathTree getShortestPathTree(RoutingRequest req) {
return getShortestPathTree(req, -1, null); // negative timeout means no timeout
}
/**
* Compute SPT using default termination strategy.
*/
public ShortestPathTree getShortestPathTree(RoutingRequest req, double relTimeoutSeconds) {
return this.getShortestPathTree(req, relTimeoutSeconds, null);
}
/** set up a single-origin search */
public void startSearch(RoutingRequest options,
SearchTerminationStrategy terminationStrategy, long abortTime) {
startSearch(options, terminationStrategy, abortTime, true);
}
/** set up the search, optionally not adding the initial state to the queue (for multi-state Dijkstra) */
private void startSearch(RoutingRequest options,
SearchTerminationStrategy terminationStrategy, long abortTime, boolean addToQueue) {
runState = new RunState( options, terminationStrategy );
runState.rctx = options.getRoutingContext();
runState.spt = options.getNewShortestPathTree();
// We want to reuse the heuristic instance in a series of requests for the same target to avoid repeated work.
// "Batch" means one-to-many mode, where there is no goal to reach so we use a trivial heuristic.
runState.heuristic = options.batch ?
new TrivialRemainingWeightHeuristic() :
runState.rctx.remainingWeightHeuristic;
// Since initial states can be multiple, heuristic cannot depend on the initial state.
// Initializing the bidirectional heuristic is a pretty complicated operation that involves searching through
// the streets around the origin and destination.
runState.heuristic.initialize(runState.options, abortTime);
if (abortTime < Long.MAX_VALUE && System.currentTimeMillis() > abortTime) {
LOG.warn("Timeout during initialization of goal direction heuristic.");
options.rctx.debugOutput.timedOut = true;
runState = null; // Search timed out
return;
}
// Priority Queue.
// The queue is self-resizing, so we initialize it to have size = O(sqrt(|V|)) << |V|.
// For reference, a random, undirected search on a uniform 2d grid will examine roughly sqrt(|V|) vertices
// before reaching its target.
int initialSize = runState.rctx.graph.getVertices().size();
initialSize = (int) Math.ceil(2 * (Math.sqrt((double) initialSize + 1)));
runState.pq = new BinHeap<>(initialSize);
runState.nVisited = 0;
runState.targetAcceptedStates = Lists.newArrayList();
if (addToQueue) {
State initialState = new State(options);
runState.spt.add(initialState);
runState.pq.insert(initialState, 0);
}
}
boolean iterate(){
// print debug info
if (verbose) {
double w = runState.pq.peek_min_key();
System.out.println("pq min key = " + w);
}
// interleave some heuristic-improving work (single threaded)
runState.heuristic.doSomeWork();
// get the lowest-weight state in the queue
runState.u = runState.pq.extract_min();
// check that this state has not been dominated
// and mark vertex as visited
if (!runState.spt.visit(runState.u)) {
// state has been dominated since it was added to the priority queue, so it is
// not in any optimal path. drop it on the floor and try the next one.
return false;
}
if (traverseVisitor != null) {
traverseVisitor.visitVertex(runState.u);
}
runState.u_vertex = runState.u.getVertex();
if (verbose)
System.out.println(" vertex " + runState.u_vertex);
runState.nVisited += 1;
Collection<Edge> edges = runState.options.arriveBy ? runState.u_vertex.getIncoming() : runState.u_vertex.getOutgoing();
for (Edge edge : edges) {
// Iterate over traversal results. When an edge leads nowhere (as indicated by
// returning NULL), the iteration is over. TODO Use this to board multiple trips.
for (State v = edge.traverse(runState.u); v != null; v = v.getNextResult()) {
// Could be: for (State v : traverseEdge...)
if (traverseVisitor != null) {
traverseVisitor.visitEdge(edge, v);
}
double remaining_w = runState.heuristic.estimateRemainingWeight(v);
// LOG.info("{} {}", v, remaining_w);
if (remaining_w < 0 || Double.isInfinite(remaining_w) ) {
continue;
}
double estimate = v.getWeight() + remaining_w;
if (verbose) {
System.out.println(" edge " + edge);
System.out.println(" " + runState.u.getWeight() + " -> " + v.getWeight()
+ "(w) + " + remaining_w + "(heur) = " + estimate + " vert = "
+ v.getVertex());
}
// avoid enqueuing useless branches
if (estimate > runState.options.maxWeight) {
// too expensive to get here
if (verbose)
System.out.println(" too expensive to reach, not enqueued. estimated weight = " + estimate);
continue;
}
if (isWorstTimeExceeded(v, runState.options)) {
// too much time to get here
if (verbose)
System.out.println(" too much time to reach, not enqueued. time = " + v.getTimeSeconds());
continue;
}
// spt.add returns true if the state is hopeful; enqueue state if it's hopeful
if (runState.spt.add(v)) {
// report to the visitor if there is one
if (traverseVisitor != null)
traverseVisitor.visitEnqueue(v);
//LOG.info("u.w={} v.w={} h={}", runState.u.weight, v.weight, remaining_w);
runState.pq.insert(v, estimate);
}
}
}
return true;
}
void runSearch(long abortTime){
/* the core of the A* algorithm */
while (!runState.pq.empty()) { // Until the priority queue is empty:
/*
* Terminate based on timeout?
*/
if (abortTime < Long.MAX_VALUE && System.currentTimeMillis() > abortTime) {
LOG.warn("Search timeout. origin={} target={}", runState.rctx.origin, runState.rctx.target);
// Rather than returning null to indicate that the search was aborted/timed out,
// we instead set a flag in the routing context and return the SPT anyway. This
// allows returning a partial list results even when a timeout occurs.
runState.options.rctx.aborted = true; // signal search cancellation up to higher stack frames
runState.options.rctx.debugOutput.timedOut = true; // signal timeout in debug output object
break;
}
/*
* Get next best state and, if it hasn't already been dominated, add adjacent states to queue.
* If it has been dominated, the iteration is over; don't bother checking for termination condition.
*
* Note that termination is checked after adjacent states are added. This presents the negligible inefficiency
* that adjacent states are generated for a state which could be the last one you need to check. The advantage
* of this is that the algorithm is always left in a restartable state, which is useful for debugging or
* potential future variations.
*/
if(!iterate()){
continue;
}
/*
* Should we terminate the search?
*/
// Don't search too far past the most recently found accepted path/state
if (runState.foundPathWeight != null &&
runState.u.getWeight() > runState.foundPathWeight * OVERSEARCH_MULTIPLIER ) {
break;
}
if (runState.terminationStrategy != null) {
if (runState.terminationStrategy.shouldSearchTerminate (
runState.rctx.origin, runState.rctx.target, runState.u, runState.spt, runState.options)) {
break;
}
} else if (!runState.options.batch && runState.u_vertex == runState.rctx.target && runState.u.isFinal()) {
if (runState.options.onlyTransitTrips && !runState.u.isEverBoarded()) {
continue;
}
runState.targetAcceptedStates.add(runState.u);
runState.foundPathWeight = runState.u.getWeight();
runState.options.rctx.debugOutput.foundPath();
// new GraphPath(runState.u, false).dump();
/* Only find one path at a time in long distance mode. */
if (runState.options.longDistance) {
break;
}
/* Break out of the search if we've found the requested number of paths. */
if (runState.targetAcceptedStates.size() >= runState.options.getNumItineraries()) {
LOG.debug("total vertices visited {}", runState.nVisited);
break;
}
}
}
}
/** @return the shortest path, or null if none is found */
public ShortestPathTree getShortestPathTree(RoutingRequest options, double relTimeoutSeconds,
SearchTerminationStrategy terminationStrategy) {
ShortestPathTree spt = null;
long abortTime = DateUtils.absoluteTimeout(relTimeoutSeconds);
startSearch (options, terminationStrategy, abortTime);
if (runState != null) {
runSearch(abortTime);
spt = runState.spt;
}
storeMemory();
return spt;
}
/** Get an SPT, starting from a collection of states */
public ShortestPathTree getShortestPathTree(RoutingRequest options, double relTimeoutSeconds,
SearchTerminationStrategy terminationStrategy, Collection<State> initialStates) {
ShortestPathTree spt = null;
long abortTime = DateUtils.absoluteTimeout(relTimeoutSeconds);
startSearch (options, terminationStrategy, abortTime, false);
if (runState != null) {
for (State state : initialStates) {
runState.spt.add(state);
// TODO: hardwired for earliest arrival
// TODO: weights are seconds, no?
runState.pq.insert(state, state.getElapsedTimeSeconds());
}
runSearch(abortTime);
spt = runState.spt;
}
return spt;
}
private void storeMemory() {
if (store.isMonitoring("memoryUsed")) {
System.gc();
long memoryUsed = Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory();
store.setLongMax("memoryUsed", memoryUsed);
}
}
private boolean isWorstTimeExceeded(State v, RoutingRequest opt) {
if (opt.arriveBy)
return v.getTimeSeconds() < opt.worstTime;
else
return v.getTimeSeconds() > opt.worstTime;
}
public void setTraverseVisitor(TraverseVisitor traverseVisitor) {
this.traverseVisitor = traverseVisitor;
}
public List<GraphPath> getPathsToTarget() {
List<GraphPath> ret = new LinkedList<>();
for (State s : runState.targetAcceptedStates) {
if (s.isFinal()) {
ret.add(new GraphPath(s, true));
}
}
return ret;
}
}