/* 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.impl;
import com.google.common.collect.Lists;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.opentripplanner.api.resource.DebugOutput;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.routing.algorithm.AStar;
import org.opentripplanner.routing.algorithm.strategies.EuclideanRemainingWeightHeuristic;
import org.opentripplanner.routing.algorithm.strategies.InterleavedBidirectionalHeuristic;
import org.opentripplanner.routing.algorithm.strategies.RemainingWeightHeuristic;
import org.opentripplanner.routing.algorithm.strategies.TrivialRemainingWeightHeuristic;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.edgetype.LegSwitchingEdge;
import org.opentripplanner.routing.error.PathNotFoundException;
import org.opentripplanner.routing.error.VertexNotFoundException;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.spt.DominanceFunction;
import org.opentripplanner.routing.spt.GraphPath;
import org.opentripplanner.standalone.Router;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
/**
* This class contains the logic for repeatedly building shortest path trees and accumulating paths through
* the graph until the requested number of them have been found.
* It is used in point-to-point (i.e. not one-to-many / analyst) routing.
*
* Its exact behavior will depend on whether the routing request allows transit.
*
* When using transit it will incorporate techniques from what we called "long distance" mode, which is designed to
* provide reasonable response times when routing over large graphs (e.g. the entire Netherlands or New York State).
* In this case it only uses the street network at the first and last legs of the trip, and all other transfers
* between transit vehicles will occur via SimpleTransfer edges which are pre-computed by the graph builder.
*
* More information is available on the OTP wiki at:
* https://github.com/openplans/OpenTripPlanner/wiki/LargeGraphs
*
* One instance of this class should be constructed per search (i.e. per RoutingRequest: it is request-scoped).
* Its behavior is undefined if it is reused for more than one search.
*
* It is very close to being an abstract library class with only static functions. However it turns out to be convenient
* and harmless to have the OTPServer object etc. in fields, to avoid passing context around in function parameters.
*/
public class GraphPathFinder {
private static final Logger LOG = LoggerFactory.getLogger(GraphPathFinder.class);
private static final double DEFAULT_MAX_WALK = 2000;
private static final double CLAMP_MAX_WALK = 15000;
Router router;
public GraphPathFinder(Router router) {
this.router = router;
}
/**
* Repeatedly build shortest path trees, retaining the best path to the destination after each try.
* For search N, all trips used in itineraries retained from trips 0..(N-1) are "banned" to create variety.
* The goal direction heuristic is reused between tries, which means the later tries have more information to
* work with (in the case of the more sophisticated bidirectional heuristic, which improves over time).
*/
public List<GraphPath> getPaths(RoutingRequest options) {
if (options == null) {
LOG.error("PathService was passed a null routing request.");
return null;
}
// Reuse one instance of AStar for all N requests, which are carried out sequentially
AStar aStar = new AStar();
if (options.rctx == null) {
options.setRoutingContext(router.graph);
// The special long-distance heuristic should be sufficient to constrain the search to the right area.
}
// If this Router has a GraphVisualizer attached to it, set it as a callback for the AStar search
if (router.graphVisualizer != null) {
aStar.setTraverseVisitor(router.graphVisualizer.traverseVisitor);
// options.disableRemainingWeightHeuristic = true; // DEBUG
}
// Without transit, we'd just just return multiple copies of the same on-street itinerary.
if (!options.modes.isTransit()) {
options.numItineraries = 1;
}
options.dominanceFunction = new DominanceFunction.MinimumWeight(); // FORCING the dominance function to weight only
LOG.debug("rreq={}", options);
// Choose an appropriate heuristic for goal direction.
RemainingWeightHeuristic heuristic;
if (options.disableRemainingWeightHeuristic) {
heuristic = new TrivialRemainingWeightHeuristic();
} else if (options.modes.isTransit()) {
// Only use the BiDi heuristic for transit. It is not very useful for on-street modes.
// heuristic = new InterleavedBidirectionalHeuristic(options.rctx.graph);
// Use a simplistic heuristic until BiDi heuristic is improved, see #2153
heuristic = new InterleavedBidirectionalHeuristic();
} else {
heuristic = new EuclideanRemainingWeightHeuristic();
}
options.rctx.remainingWeightHeuristic = heuristic;
/* In RoutingRequest, maxTransfers defaults to 2. Over long distances, we may see
* itineraries with far more transfers. We do not expect transfer limiting to improve
* search times on the LongDistancePathService, so we set it to the maximum we ever expect
* to see. Because people may use either the traditional path services or the
* LongDistancePathService, we do not change the global default but override it here. */
options.maxTransfers = 4;
// Now we always use what used to be called longDistance mode. Non-longDistance mode is no longer supported.
options.longDistance = true;
/* In long distance mode, maxWalk has a different meaning than it used to.
* It's the radius around the origin or destination within which you can walk on the streets.
* If no value is provided, max walk defaults to the largest double-precision float.
* This would cause long distance mode to do unbounded street searches and consider the whole graph walkable. */
if (options.maxWalkDistance == Double.MAX_VALUE) options.maxWalkDistance = DEFAULT_MAX_WALK;
if (options.maxWalkDistance > CLAMP_MAX_WALK) options.maxWalkDistance = CLAMP_MAX_WALK;
long searchBeginTime = System.currentTimeMillis();
LOG.debug("BEGIN SEARCH");
List<GraphPath> paths = Lists.newArrayList();
while (paths.size() < options.numItineraries) {
// TODO pull all this timeout logic into a function near org.opentripplanner.util.DateUtils.absoluteTimeout()
int timeoutIndex = paths.size();
if (timeoutIndex >= router.timeouts.length) {
timeoutIndex = router.timeouts.length - 1;
}
double timeout = searchBeginTime + (router.timeouts[timeoutIndex] * 1000);
timeout -= System.currentTimeMillis(); // Convert from absolute to relative time
timeout /= 1000; // Convert milliseconds to seconds
if (timeout <= 0) {
// Catch the case where advancing to the next (lower) timeout value means the search is timed out
// before it even begins. Passing a negative relative timeout in the SPT call would mean "no timeout".
options.rctx.aborted = true;
break;
}
aStar.getShortestPathTree(options, timeout);
if (options.rctx.aborted) {
break; // Search timed out or was gracefully aborted for some other reason.
}
// Don't dig through the SPT object, just ask the A star algorithm for the states that reached the target.
List<GraphPath> newPaths = aStar.getPathsToTarget();
if (newPaths.isEmpty()) {
break;
}
// Find all trips used in this path and ban them for the remaining searches
for (GraphPath path : newPaths) {
// path.dump();
List<AgencyAndId> tripIds = path.getTrips();
for (AgencyAndId tripId : tripIds) {
options.banTrip(tripId);
}
if (tripIds.isEmpty()) {
// This path does not use transit (is entirely on-street). Do not repeatedly find the same one.
options.onlyTransitTrips = true;
}
}
paths.addAll(newPaths.stream()
.filter(path -> path.getDuration() < options.maxHours * 60 * 60)
.collect(Collectors.toList()));
LOG.debug("we have {} paths", paths.size());
}
LOG.debug("END SEARCH ({} msec)", System.currentTimeMillis() - searchBeginTime);
Collections.sort(paths, new PathComparator(options.arriveBy));
return paths;
}
/* Try to find N paths through the Graph */
public List<GraphPath> graphPathFinderEntryPoint (RoutingRequest request) {
// We used to perform a protective clone of the RoutingRequest here.
// There is no reason to do this if we don't modify the request.
// Any code that changes them should be performing the copy!
List<GraphPath> paths = null;
try {
paths = getGraphPathsConsideringIntermediates(request);
if (paths == null && request.wheelchairAccessible) {
// There are no paths that meet the user's slope restrictions.
// Try again without slope restrictions, and warn the user in the response.
RoutingRequest relaxedRequest = request.clone();
relaxedRequest.maxSlope = Double.MAX_VALUE;
request.rctx.slopeRestrictionRemoved = true;
paths = getGraphPathsConsideringIntermediates(relaxedRequest);
}
request.rctx.debugOutput.finishedCalculating();
} catch (VertexNotFoundException e) {
LOG.info("Vertex not found: " + request.from + " : " + request.to);
throw e;
}
// Detect and report that most obnoxious of bugs: path reversal asymmetry.
// Removing paths might result in an empty list, so do this check before the empty list check.
if (paths != null) {
Iterator<GraphPath> gpi = paths.iterator();
while (gpi.hasNext()) {
GraphPath graphPath = gpi.next();
// TODO check, is it possible that arriveBy and time are modifed in-place by the search?
if (request.arriveBy) {
if (graphPath.states.getLast().getTimeSeconds() > request.dateTime) {
LOG.error("A graph path arrives after the requested time. This implies a bug.");
gpi.remove();
}
} else {
if (graphPath.states.getFirst().getTimeSeconds() < request.dateTime) {
LOG.error("A graph path leaves before the requested time. This implies a bug.");
gpi.remove();
}
}
}
}
if (paths == null || paths.size() == 0) {
LOG.debug("Path not found: " + request.from + " : " + request.to);
request.rctx.debugOutput.finishedRendering(); // make sure we still report full search time
throw new PathNotFoundException();
}
return paths;
}
/**
* Break up a RoutingRequest with intermediate places into separate requests, in the given order.
*
* If there are no intermediate places, issue a single request. Otherwise process the places
* list [from, i1, i2, ..., to] either from left to right (if {@code request.arriveBy==false})
* or from right to left (if {@code request.arriveBy==true}). In the latter case the order of
* the requested subpaths is (i2, to), (i1, i2), and (from, i1) which has to be reversed at
* the end.
*/
private List<GraphPath> getGraphPathsConsideringIntermediates (RoutingRequest request) {
if (request.hasIntermediatePlaces()) {
List<GenericLocation> places = Lists.newArrayList(request.from);
places.addAll(request.intermediatePlaces);
places.add(request.to);
long time = request.dateTime;
List<GraphPath> paths = new ArrayList<>();
DebugOutput debugOutput = null;
int placeIndex = (request.arriveBy ? places.size() - 1 : 1);
while (0 < placeIndex && placeIndex < places.size()) {
RoutingRequest intermediateRequest = request.clone();
intermediateRequest.setNumItineraries(1);
intermediateRequest.dateTime = time;
intermediateRequest.from = places.get(placeIndex - 1);
intermediateRequest.to = places.get(placeIndex);
intermediateRequest.rctx = null;
intermediateRequest.setRoutingContext(router.graph);
if (debugOutput != null) {// Restore the previous debug info accumulator
intermediateRequest.rctx.debugOutput = debugOutput;
} else {// Store the debug info accumulator
debugOutput = intermediateRequest.rctx.debugOutput;
}
List<GraphPath> partialPaths = getPaths(intermediateRequest);
if (partialPaths.size() == 0) {
return partialPaths;
}
GraphPath path = partialPaths.get(0);
paths.add(path);
time = (request.arriveBy ? path.getStartTime() : path.getEndTime());
placeIndex += (request.arriveBy ? -1 : +1);
}
request.setRoutingContext(router.graph);
request.rctx.debugOutput = debugOutput;
if (request.arriveBy) {
Collections.reverse(paths);
}
return Collections.singletonList(joinPaths(paths));
} else {
return getPaths(request);
}
}
private static GraphPath joinPaths(List<GraphPath> paths) {
State lastState = paths.get(0).states.getLast();
GraphPath newPath = new GraphPath(lastState, false);
Vertex lastVertex = lastState.getVertex();
//With more paths we should allow more transfers
lastState.getOptions().maxTransfers *= paths.size();
for (GraphPath path : paths.subList(1, paths.size())) {
lastState = newPath.states.getLast();
// add a leg-switching state
LegSwitchingEdge legSwitchingEdge = new LegSwitchingEdge(lastVertex, lastVertex);
lastState = legSwitchingEdge.traverse(lastState);
newPath.edges.add(legSwitchingEdge);
newPath.states.add(lastState);
// add the next subpath
for (Edge e : path.edges) {
lastState = e.traverse(lastState);
newPath.edges.add(e);
newPath.states.add(lastState);
}
lastVertex = path.getEndVertex();
}
return newPath;
}
/*
TODO reimplement
This should probably be done with a special value in the departure/arrival time.
public static TripPlan generateFirstTrip(RoutingRequest request) {
request.setArriveBy(false);
TimeZone tz = graph.getTimeZone();
GregorianCalendar calendar = new GregorianCalendar(tz);
calendar.setTimeInMillis(request.dateTime * 1000);
calendar.set(Calendar.HOUR, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.AM_PM, 0);
calendar.set(Calendar.SECOND, graph.index.overnightBreak);
request.dateTime = calendar.getTimeInMillis() / 1000;
return generate(request);
}
public static TripPlan generateLastTrip(RoutingRequest request) {
request.setArriveBy(true);
TimeZone tz = graph.getTimeZone();
GregorianCalendar calendar = new GregorianCalendar(tz);
calendar.setTimeInMillis(request.dateTime * 1000);
calendar.set(Calendar.HOUR, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.AM_PM, 0);
calendar.set(Calendar.SECOND, graph.index.overnightBreak);
calendar.add(Calendar.DAY_OF_YEAR, 1);
request.dateTime = calendar.getTimeInMillis() / 1000;
return generate(request);
}
*/
}