package org.opentripplanner.profile;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import gnu.trove.iterator.TObjectIntIterator;
import gnu.trove.map.TObjectIntMap;
import org.onebusaway.gtfs.model.Stop;
import org.opentripplanner.analyst.TimeSurface;
import org.opentripplanner.api.parameter.QualifiedMode;
import org.opentripplanner.api.parameter.QualifiedModeSet;
import org.opentripplanner.api.resource.SimpleIsochrone;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.common.model.T2;
import org.opentripplanner.common.pqueue.BinHeap;
import org.opentripplanner.routing.algorithm.AStar;
import org.opentripplanner.routing.algorithm.TraverseVisitor;
import org.opentripplanner.routing.core.OptimizeType;
import org.opentripplanner.routing.core.RoutingContext;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.spt.DominanceFunction;
import org.opentripplanner.routing.spt.ShortestPathTree;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Rather than finding a single optimal route, ProfileRouter aims to find all reasonable ways to go from an origin
* to a destination over a given time window, and expresses those results in terms of route combinations and time ranges.
* For example:
* riding Train A followed by Bus B between 9AM and 11AM takes between 1.2 and 1.6 hours;
* riding Train A followed by Bus C takes between 1.4 and 1.8 hours.
*
* Create one instance of ProfileRouter per profile search. It is not intended to be threadsafe or reusable.
* You MUST call the cleanup method on all ProfileRouter instances that have been used for routing before
* they are released for garbage collection.
*/
public class ProfileRouter {
private static final Logger LOG = LoggerFactory.getLogger(ProfileRouter.class);
/* Search configuration constants */
public static final int SLACK = 60; // in seconds, time required to catch a transit vehicle
private static final int TIMEOUT = 20; // in seconds, maximum computation time
public static final int MAX_DURATION = 90 * 60; // in seconds, the longest we want to travel
private static final int MAX_RIDES = 3; // maximum number of boardings in a trip
public final Graph graph;
public final ProfileRequest request;
public ProfileRouter(Graph graph, ProfileRequest request) {
this.graph = graph;
this.request = request;
}
/* Search state */
Multimap<StopCluster, StopAtDistance> fromStopPaths, toStopPaths; // ways to reach each origin or dest stop cluster
List<RoutingContext> routingContexts = Lists.newArrayList();
/* Analyst: time bounds for each vertex. This field contains the output after the search is run. */
public TimeSurface.RangeSet timeSurfaceRangeSet = null;
// while finding direct paths:
// if dist < M meters OR we don't yet have N stations: record station
Collection<StopAtDistance> directPaths = Lists.newArrayList(); // ways to reach the destination without transit
Multimap<StopCluster, Ride> retainedRides = ArrayListMultimap.create(); // the rides arriving at each stop that are worth continuing to explore.
BinHeap<Ride> queue = new BinHeap<Ride>(); // rides to be explored, prioritized by minumum travel time
// TODO rename fromStopsByPattern
Multimap<TripPattern, StopAtDistance> fromStops, toStops;
TimeWindow window; // filters trips used by time of day and service schedule
/** @return true if the given stop cluster has at least one transfer coming from the given pattern. */
private boolean hasTransfers(StopCluster stopCluster, TripPattern pattern) {
for (ProfileTransfer tr : graph.index.transfersFromStopCluster.get(stopCluster)) {
if (tr.tp1 == pattern) return true;
}
return false;
}
public void printStopsForPatterns(String prefix, Multimap<TripPattern, StopAtDistance> stopClustersByPattern) {
for (TripPattern tp : stopClustersByPattern.keySet()) {
LOG.info("{} {}", prefix, tp.code);
for (StopAtDistance sd : stopClustersByPattern.get(tp)) {
LOG.info(" {}", sd);
}
}
}
private static void logRide(Ride ride) {
LOG.info("{}", ride.toString());
LOG.info(" {}", ride.accessStats);
for (PatternRide pr : ride.patternRides) {
LOG.info(" {} {}", pr.pattern.route, pr);
}
}
/* Maybe don't even try to accumulate stats and weights on the fly, just enumerate options. */
/* TODO Or actually use a priority queue based on the min time. */
public ProfileResponse route () {
// Lazy-initialize stop clusters (threadsafe method)
graph.index.clusterStopsAsNeeded();
// Lazy-initialize profile transfers (before setting timeouts, since this is slow)
if (graph.index.transfersFromStopCluster == null) {
synchronized (graph.index) {
// why another if statement? so that if another thread initialized this in the meantime
// we don't initialize it again.
if (graph.index.transfersFromStopCluster == null) {
graph.index.initializeProfileTransfers();
}
}
}
LOG.info("access modes: {}", request.accessModes);
LOG.info("egress modes: {}", request.egressModes);
LOG.info("direct modes: {}", request.directModes);
// Establish search timeouts
long searchBeginTime = System.currentTimeMillis();
long abortTime = searchBeginTime + TIMEOUT * 1000;
// TimeWindow could constructed in the caller, which does have access to the graph index.
this.window = new TimeWindow(request.fromTime, request.toTime, graph.index.servicesRunning(request.date));
LOG.info("Finding access/egress paths.");
// Look for stops that are within a given time threshold of the origin and destination
// Find the closest stop on each pattern near the origin and destination
// TODO consider that some stops may be closer by one mode than another
// and that some stops may be accessible by one mode but not another
fromStopPaths = findClosestStops(false);
fromStops = findClosestPatterns(fromStopPaths);
if ( ! request.analyst) {
toStopPaths = findClosestStops(true);
toStops = findClosestPatterns(toStopPaths);
// Also look for options connecting origin to destination with no transit.
for (QualifiedMode qmode : request.directModes.qModes) {
LOG.info("Finding non-transit path for mode {}", qmode);
findDirectOption(qmode);
}
}
LOG.info("Done finding access/egress paths.");
//printStopsForPatterns("from", fromStops);
//printStopsForPatterns("to", toStops);
/* Enqueue an unfinished PatternRide for each pattern near the origin, grouped by Stop into unfinished Rides. */
Map<StopCluster, Ride> initialRides = Maps.newHashMap(); // One ride per stop cluster
/* This Multimap will contain multiple entries for the same pattern if it can be reached by multiple modes. */
for (TripPattern pattern : fromStops.keySet()) {
if ( ! request.transitModes.contains(pattern.mode)) {
continue; // FIXME do not even store these patterns when performing access/egress searches
}
Collection<StopAtDistance> sds = fromStops.get(pattern);
for (StopAtDistance sd : sds) {
/* Fetch or construct a new Ride beginning at this stop cluster. */
Ride ride = initialRides.get(sd.stopCluster);
if (ride == null) {
ride = new Ride(sd.stopCluster, null); // null previous ride because this is the first ride
ride.accessStats = new Stats(); // empty stats, times for each access mode will be merged in
ride.accessStats.min = Integer.MAX_VALUE; // higher than any value that will be merged in
ride.accessDist = (int) sd.state.getWalkDistance(); // TODO verify correctness and uses
initialRides.put(sd.stopCluster, ride);
}
/* Record the access time for this mode in the stats. */
ride.accessStats.merge(sd.etime);
/* Loop over stop clusters in case stop cluster appears more than once in the pattern. */
STOP_INDEX: for (int i = 0; i < pattern.getStops().size(); ++i) {
if (sd.stopCluster == graph.index.stopClusterForStop.get(pattern.getStops().get(i))) {
PatternRide newPatternRide = new PatternRide(pattern, i);
/* Only add the PatternRide once per unique (pattern,stopIndex). */
// TODO this would be a !contains() call if PatternRides had semantic equality
for (PatternRide existingPatternRide : ride.patternRides) {
if (existingPatternRide.pattern == newPatternRide.pattern &&
existingPatternRide.fromIndex == newPatternRide.fromIndex) {
continue STOP_INDEX;
}
}
ride.patternRides.add(newPatternRide);
}
}
}
}
for (Ride ride : initialRides.values()) {
// logRide(ride);
queue.insert(ride, 0);
}
/* Explore incomplete rides as long as there are any in the queue. */
while ( ! queue.empty()) {
/* Get the minimum-time unfinished ride off the queue. */
Ride ride = queue.extract_min();
/* Skip this ride if it has been dominated since it was enqueued. */
// TODO should we check whether ride.previous != null (it is an initial ride)?
if (dominated(ride, ride.from)) continue;
// Maybe when ride is complete, then find transfers here, but that makes for more queue operations.
if (ride.to != null) throw new AssertionError("Ride should be unfinished.");
/* Track finished Rides by their destination StopCluster, so we can add PatternRides to them. */
Map<StopCluster, Ride> rides = Maps.newHashMap();
/* Complete partial PatternRides (with only a pattern and a beginning stop) which were enqueued in this
* partial ride. This is done by scanning through the Pattern, creating rides to all downstream stops. */
PR: for (PatternRide pr : ride.patternRides) {
// LOG.info(" {}", pr);
List<Stop> stops = pr.pattern.getStops();
for (int s = pr.fromIndex + 1; s < stops.size(); ++s) {
StopCluster cluster = graph.index.stopClusterForStop.get(stops.get(s));
/* Originally we only extended rides to destination stops considered useful in the search, i.e.
* those that had transfers leading out of them or were known to be near the destination.
* However, analyst needs to know the times we can reach every stop, and pruning is more effective
* if we know when rides pass through all stops.*/
PatternRide pr2 = pr.extendToIndex(s, window);
// PatternRide may be empty because there are no trips in time window.
if (pr2 == null) continue PR;
// LOG.info(" {}", pr2);
// Get or create the completed Ride to this destination stop.
Ride ride2 = rides.get(cluster);
if (ride2 == null) {
ride2 = ride.extendTo(cluster);
rides.put(cluster, ride2);
}
// Add the completed PatternRide to the completed Ride.
ride2.patternRides.add(pr2);
}
}
/* Build new downstream Rides by transferring from patterns in current Rides. */
// Create a map of incomplete rides (start but no end point) after transfers, one for each stop.
Map<StopCluster, Ride> xferRides = Maps.newHashMap();
for (Ride r1 : rides.values()) {
r1.calcStats(window, request.walkSpeed);
if (r1.waitStats == null) {
// WaitStats can be null if it was not possible to board these trips in the time window.
// This is a sign of a questionable algorithm, since we eliminate the ride rather late.
continue;
} else {
r1.recomputeBounds();
}
/* Retain this ride if it is not dominated by some existing ride at the same location. */
if (dominated(r1, r1.to)) continue;
retainedRides.put(r1.to, r1);
/* We have a new, non-dominated, completed ride. Find transfers out of this new ride, respecting the transfer limit. */
int nRides = r1.pathLength;
if (nRides >= MAX_RIDES) continue;
boolean penultimateRide = (nRides == MAX_RIDES - 1);
// Invariant: r1.to should be the same as r1's key in rides
// TODO benchmark, this is so not efficient
for (ProfileTransfer tr : graph.index.transfersFromStopCluster.get(r1.to)) {
if ( ! request.transitModes.contains(tr.tp2.mode)) continue;
if (r1.containsPattern(tr.tp1)) {
// Prune loopy or repetitive paths.
if (r1.pathContainsRoute(tr.tp2.route)) continue;
if (tr.sc1 != tr.sc2 && r1.pathContainsStop(tr.sc2)) continue;
// Optimization: on the last ride of point-to-point searches,
// only transfer to patterns that pass near the destination.
if ( ! request.analyst && penultimateRide && ! toStops.containsKey(tr.tp2)) continue;
// Scan through stops looking for transfer target: stop might appear more than once in a pattern.
TARGET_STOP : for (int i = 0; i < tr.tp2.getStops().size(); ++i) {
StopCluster cluster = graph.index.stopClusterForStop.get(tr.tp2.getStops().get(i));
if (cluster == tr.sc2) {
// Save transfer result in an unfinished ride for later exploration.
Ride r2 = xferRides.get(tr.sc2);
if (r2 == null) {
r2 = new Ride(tr.sc2, r1);
r2.accessDist = tr.distance;
r2.accessStats = new Stats((int)(tr.distance / request.walkSpeed));
r2.recomputeBounds();
xferRides.put(tr.sc2, r2);
}
for (PatternRide pr : r2.patternRides) {
// Multiple patterns can have transfers to the same target pattern,
// but Rides should not have duplicate PatternRides.
// TODO refactor with equals function and contains().
if (pr.pattern == tr.tp2 && pr.fromIndex == i) continue TARGET_STOP;
}
r2.patternRides.add(new PatternRide(tr.tp2, i));
}
}
}
}
}
/* Enqueue new incomplete Rides resulting from transfers if they are not dominated at their from-cluster. */
for (Ride r : xferRides.values()) {
if ( ! dominated(r, r.from)) {
// This ride is unfinished, use the previous ride's travel time lower bound as the p-queue key.
// Note that we are not adding these transfer results to the retained rides, just enqueuing them.
queue.insert(r, r.dlb);
}
}
if (System.currentTimeMillis() > abortTime)
throw new RuntimeException("TIMEOUT");
}
LOG.info("Profile routing request finished in {} sec.", (System.currentTimeMillis() - searchBeginTime) / 1000.0);
if (request.analyst) {
makeSurfaces();
return null;
}
/* In non-analyst (point-to-point) mode, determine which rides are good ways to reach the destination. */
/* A fake stop cluster to allow applying generic domination logic at the final destination. */
final StopCluster DESTINATION = new StopCluster("The Destination", "The Destination");
for (StopCluster cluster : toStopPaths.keySet()) {
// TODO shared logic for making access/egress stats
Stats egressStats = new Stats();
egressStats.min = Integer.MAX_VALUE;
for (StopAtDistance sd : toStopPaths.get(cluster)) {
egressStats.merge(sd.etime);
}
for (Ride ride : retainedRides.get(cluster)) {
// Construct a new unfinished ride, representing "transferring" from the final stop to the destination
Ride rideAtDestination = new Ride(DESTINATION, ride);
rideAtDestination.accessStats = egressStats;
rideAtDestination.recomputeBounds();
if ( ! dominated(rideAtDestination, DESTINATION)) {
retainedRides.put(DESTINATION, rideAtDestination);
}
}
}
LOG.info("{} nondominated rides reach the destination.", retainedRides.get(DESTINATION).size());
/* Non-analyst: Build the list of Options by following the back-pointers in Rides. */
List<Option> options = Lists.newArrayList();
for (Ride ride : retainedRides.get(DESTINATION)) {
// slice off the final unfinished ride that only contains the egress stats
// TODO actually use this ride in preparing the response
ride = ride.previous;
// All PatternRides in a Ride end at the same stop.
Collection<StopAtDistance> accessPaths = fromStopPaths.get(ride.getAccessStopCluster());
Collection<StopAtDistance> egressPaths = toStopPaths.get(ride.getEgressStopCluster());
Option option = new Option(ride, accessPaths, egressPaths);
if ( ! option.hasEmptyRides()) options.add(option);
}
/* Include the direct (no-transit) biking, driving, and walking options. */
options.add(new Option(null, directPaths, null));
return new ProfileResponse(options, request.orderBy, request.limit);
}
/** @return the set of qualified modes used to access the chain of rides ending with the given ride. */
private Set<QualifiedMode> accessModesForRide(Ride ride) {
Collection<StopAtDistance> sds = fromStopPaths.get(ride.getAccessStopCluster());
Set<QualifiedMode> qmodes = Sets.newHashSetWithExpectedSize(sds.size());
for (StopAtDistance sd : sds) {
qmodes.add(sd.qmode);
}
return qmodes;
}
/**
* Check whether a new ride has too long a duration relative to existing rides at the given location,
* or relative to the global travel time limit.
* TODO auto-detect unfinished rides via to==null
*/
public boolean dominated (Ride newRide, StopCluster atCluster) {
if (newRide.dlb > MAX_DURATION) return true;
// Check whether any existing rides at the same location (stop cluster) dominate the new one.
Set<QualifiedMode> newRideAccessModes = accessModesForRide(newRide);
for (Ride oldRide : retainedRides.get(atCluster)) {
// All retained rides should be finished (to != null), except those at DESTINATION.
// Certain pairs of options should not be presented as alternatives to one another.
// They are in direct competition with one another and strict dominance applies.
if (oldRide.pathLength < newRide.pathLength &&
oldRide.dlb < newRide.dlb &&
oldRide.dub < newRide.dub &&
accessModesForRide(oldRide).containsAll(newRideAccessModes)) {
return true;
}
// Strict dominance does not apply.
// Check whether time ranges overlap, considering the tolerance for suboptimality.
if (newRide.dlb > oldRide.dub + request.suboptimalMinutes * 60) {
return true;
}
}
return false; // No existing ride is considered sufficiently better than the new ride to eject it.
}
/**
* @param stopClusters a multimap from stop clusters to one or more StopAtDistance objects at the corresponding cluster.
* @return for each TripPattern that passes through any of the supplied stop clusters, the stop cluster that is
* closest to the origin or destination point according to the distances in the StopAtDistance objects.
*
* In short, take a bunch of stop clusters near the origin or destination and return the quickest way to reach each
* pattern that passes through them.
* We want stop cluster references rather than indexes within the patterns because when a stop cluster appears more
* than once in a pattern, we want to consider boarding or alighting from that pattern at every index where the
* cluster occurs.
*/
public Multimap<TripPattern, StopAtDistance> findClosestPatterns(Multimap<StopCluster, StopAtDistance> stopClusters) {
SimpleIsochrone.MinMap<T2<TripPattern, QualifiedMode>, StopAtDistance> closest = new SimpleIsochrone.MinMap<>();
// Iterate over all StopAtDistance for all Stops. The fastest mode will win at each stop.
for (StopAtDistance stopDist : stopClusters.values()) {
for (Stop stop : stopDist.stopCluster.children) {
for (TripPattern pattern : graph.index.patternsForStop.get(stop)) {
closest.putMin(new T2(pattern, stopDist.qmode), stopDist);
}
}
}
/* Remove the QualifiedModes from the keys. Maybe it would be better to just return the result including them. */
Multimap<TripPattern, StopAtDistance> result = ArrayListMultimap.create();
for (Entry<T2<TripPattern, QualifiedMode>, StopAtDistance> entry : closest.entrySet()) {
result.put(entry.getKey().first, entry.getValue());
}
// We used to truncate long lists to include a mix of nearby bus and train patterns
// but this gives crazy results. Now we just warn on this condition.
final int MAX_PATTERNS = 1000;
if (result.size() > MAX_PATTERNS) {
LOG.warn("Excessively long list of patterns. {} patterns, max allowed is {}.", closest.size(), MAX_PATTERNS);
}
return result;
}
/**
* Perform an on-street search around a point with each of several modes to find nearby stops.
* @return one or more paths to each reachable stop using the various modes.
*/
private Multimap<StopCluster, StopAtDistance> findClosestStops(boolean dest) {
Multimap<StopCluster, StopAtDistance> pathsByStop = ArrayListMultimap.create();
QualifiedModeSet qModes = dest ? request.egressModes : request.accessModes;
for (QualifiedMode qmode : qModes.qModes) {
LOG.info("{} mode {}", dest ? "egress" : "access", qmode);
for (StopAtDistance sd : findClosestStops(qmode, dest)) {
pathsByStop.put(sd.stopCluster, sd);
}
}
return pathsByStop;
}
/**
* Perform an on-street search around a point with a specific mode to find nearby stops.
* @param dest : whether to search at the destination instead of the origin.
*/
private Collection<StopAtDistance> findClosestStops(final QualifiedMode qmode, boolean dest) {
// Make a normal OTP routing request so we can traverse edges and use GenericAStar
// TODO make a function that builds normal routing requests from profile requests
RoutingRequest rr = new RoutingRequest(new TraverseModeSet());
qmode.applyToRoutingRequest(rr, request.transitModes.isTransit());
rr.from = (new GenericLocation(request.fromLat, request.fromLon));
// FIXME requires destination to be set, not necessary for analyst
rr.to = new GenericLocation(request.toLat, request.toLon);
rr.setArriveBy(dest);
rr.setRoutingContext(graph);
// Set batch after context, so both origin and dest vertices will be found.
rr.batch = (true);
rr.walkSpeed = request.walkSpeed;
rr.dominanceFunction = new DominanceFunction.EarliestArrival();
// RR dateTime defaults to currentTime.
// If elapsed time is not capped, searches are very slow.
int minAccessTime = 0;
int maxAccessTime = request.maxWalkTime;
if (qmode.mode == TraverseMode.BICYCLE) {
rr.bikeSpeed = request.bikeSpeed;
minAccessTime = request.minBikeTime;
maxAccessTime = request.maxBikeTime;
rr.optimize = OptimizeType.TRIANGLE;
rr.setTriangleNormalized(request.bikeSafe, request.bikeSlope, request.bikeTime);
} else if (qmode.mode == TraverseMode.CAR) {
rr.carSpeed = request.carSpeed;
minAccessTime = request.minCarTime;
maxAccessTime = request.maxCarTime;
}
long worstElapsedTimeSeconds = maxAccessTime * 60; // convert from minutes to seconds
if (dest) worstElapsedTimeSeconds *= -1;
rr.worstTime = (rr.dateTime + worstElapsedTimeSeconds);
AStar astar = new AStar();
rr.setNumItineraries(1);
StopFinderTraverseVisitor visitor = new StopFinderTraverseVisitor(qmode, minAccessTime * 60);
astar.setTraverseVisitor(visitor);
astar.getShortestPathTree(rr, 5); // timeout in seconds
// Save the routing context for later cleanup. We need its temporary edges to render street segments at the end.
routingContexts.add(rr.rctx);
return visitor.stopClustersFound.values();
}
static class StopFinderTraverseVisitor implements TraverseVisitor {
QualifiedMode qmode;
int minTravelTimeSeconds = 0;
Map<StopCluster, StopAtDistance> stopClustersFound = Maps.newHashMap();
public StopFinderTraverseVisitor(QualifiedMode qmode, int minTravelTimeSeconds) {
this.qmode = qmode;
this.minTravelTimeSeconds = minTravelTimeSeconds;
}
@Override public void visitEdge(Edge edge, State state) { }
@Override public void visitEnqueue(State state) { }
// Accumulate stops into ret as the search runs.
@Override public void visitVertex(State state) {
Vertex vertex = state.getVertex();
if (vertex instanceof TransitStop) {
StopAtDistance sd = new StopAtDistance(state, qmode);
if (qmode.mode == TraverseMode.CAR && sd.etime < minTravelTimeSeconds) return;
if (stopClustersFound.containsKey(sd.stopCluster)) return; // record only the closest stop in each cluster
LOG.debug("found stop cluster: {}", sd);
stopClustersFound.put(sd.stopCluster, sd);
}
}
}
/** Look for an option connecting origin to destination without using transit. */
private void findDirectOption(QualifiedMode qmode) {
// Make a normal OTP routing request so we can traverse edges and use GenericAStar
RoutingRequest rr = new RoutingRequest(new TraverseModeSet());
qmode.applyToRoutingRequest(rr, false); // false because we never use transit in direct options
if (qmode.mode == TraverseMode.BICYCLE) {
// TRIANGLE should only affect bicycle searches, but we wrap this in a conditional just to be clear.
rr.optimize = OptimizeType.TRIANGLE;
rr.setTriangleNormalized(request.bikeSafe, request.bikeSlope, request.bikeTime);
}
rr.from = (new GenericLocation(request.fromLat, request.fromLon));
rr.to = new GenericLocation(request.toLat, request.toLon);
rr.setArriveBy(false);
rr.setRoutingContext(graph);
rr.dominanceFunction = new DominanceFunction.MinimumWeight();
// This is not a batch search, it is a point-to-point search with goal direction.
// Impose a max time to protect against very slow searches.
int worstElapsedTime = request.streetTime * 60;
rr.worstTime = (rr.dateTime + worstElapsedTime);
rr.walkSpeed = request.walkSpeed;
rr.bikeSpeed = request.bikeSpeed;
AStar astar = new AStar();
rr.setNumItineraries(1);
ShortestPathTree spt = astar.getShortestPathTree(rr, 5);
State state = spt.getState(rr.rctx.target);
if (state != null) {
LOG.info("Found non-transit option for {}", qmode);
directPaths.add(new StopAtDistance(state, qmode));
}
routingContexts.add(rr.rctx); // save context for later cleanup so temp edges remain available
}
/**
* Destroy all routing contexts created during this search. This method must be called manually on any
* ProfileRouter instance before it is released for garbage collection, because RoutingContexts remain linked into
* the graph by temporary edges if they are not cleaned up.
*/
public int cleanup() {
int n = 0;
for (RoutingContext rctx : routingContexts) {
rctx.destroy();
n += 1;
}
routingContexts.clear();
LOG.debug("destroyed {} routing contexts.", n);
return n;
}
/**
* This finalizer is intended as a failsafe to prevent memory leakage in case someone does
* not clean up routing contexts. It should be considered an error if this method does any work.
*/
@Override
public void finalize() {
if (routingContexts.size() > 0) {
LOG.error("RoutingContexts were observed in the ProfileRouter finalizer: this is a memory leak.");
cleanup();
}
}
private void makeSurfaces() {
LOG.info("Propagating from transit stops to the street network...");
// A map to store the travel time to each vertex
TimeSurface minSurface = new TimeSurface(this);
TimeSurface avgSurface = new TimeSurface(this);
TimeSurface maxSurface = new TimeSurface(this);
// Grab a cached map of distances to street intersections from each transit stop
StopTreeCache stopTreeCache = graph.index.getStopTreeCache();
// Iterate over all nondominated rides at all clusters
for (Entry<StopCluster, Ride> entry : retainedRides.entries()) {
StopCluster cluster = entry.getKey();
Ride ride = entry.getValue();
for (Stop stop : cluster.children) {
TransitStop tstop = graph.index.stopVertexForStop.get(stop);
// Iterate over street intersections in the vicinity of this particular transit stop.
// Shift the time range at this transit stop, merging it into that for all reachable street intersections.
TObjectIntMap<Vertex> distanceToVertex = null; //FIXME stopTreeCache.getDistancesForStop(tstop);
for (TObjectIntIterator<Vertex> iter = distanceToVertex.iterator(); iter.hasNext(); ) {
iter.advance();
Vertex vertex = iter.key();
// distance in meters over walkspeed in meters per second --> seconds
int egressWalkTimeSeconds = (int) (iter.value() / request.walkSpeed);
if (egressWalkTimeSeconds > request.maxWalkTime * 60) {
continue;
}
int propagated_min = ride.dlb + egressWalkTimeSeconds;
int propagated_max = ride.dub + egressWalkTimeSeconds;
int propagated_avg = (int)(((long) propagated_min + propagated_max) / 2); // FIXME HACK
int existing_min = minSurface.times.get(vertex);
int existing_max = maxSurface.times.get(vertex);
int existing_avg = avgSurface.times.get(vertex);
// FIXME this is taking the least lower bound and the least upper bound
// which is not necessarily wrong but it's a crude way to perform the combination
if (existing_min == TimeSurface.UNREACHABLE || existing_min > propagated_min) {
minSurface.times.put(vertex, propagated_min);
}
if (existing_max == TimeSurface.UNREACHABLE || existing_max > propagated_max) {
maxSurface.times.put(vertex, propagated_max);
}
if (existing_avg == TimeSurface.UNREACHABLE || existing_avg > propagated_avg) {
avgSurface.times.put(vertex, propagated_avg);
}
}
}
}
LOG.info("Done with propagation.");
/* Store the results in a field in the router object. */
timeSurfaceRangeSet = new TimeSurface.RangeSet();
timeSurfaceRangeSet.min = minSurface;
timeSurfaceRangeSet.max = maxSurface;
timeSurfaceRangeSet.avg = avgSurface;
}
}