package org.opentripplanner.profile;
import com.google.common.collect.*;
import gnu.trove.iterator.TObjectIntIterator;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import org.onebusaway.gtfs.model.Stop;
import org.opentripplanner.analyst.TimeSurface;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.routing.algorithm.AStar;
import org.opentripplanner.routing.algorithm.TraverseVisitor;
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.edgetype.SimpleTransfer;
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.trippattern.FrequencyEntry;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* More optimized ProfileRouter targeting one-to-many searches on entirely frequency-based transit networks.
*
* This requires simpleTransfers to exist in the graph, so it needs to be built in longDistance mode.
*/
public class AnalystProfileRouterPrototype {
private static final Logger LOG = LoggerFactory.getLogger(AnalystProfileRouterPrototype.class);
/* Search configuration constants */
public static final int SLACK = 60; // in seconds, time required to catch a transit vehicle
private static final int TIMEOUT = 10; // 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 = 4; // maximum number of boardings in a trip
private static final List<TraverseMode> ACCESS_MODES =
Lists.newArrayList(TraverseMode.WALK, TraverseMode.BICYCLE, TraverseMode.CAR);
private static final List<TraverseMode> EGRESS_MODES =
Lists.newArrayList(TraverseMode.WALK);
public final Graph graph;
public final ProfileRequest request;
public final Map<Vertex, TimeRange> propagatedTimes = Maps.newHashMap(); // the travel times propagated onto the street network
// TODO propagate times from the origin point without transit.
public AnalystProfileRouterPrototype(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();
TObjectIntMap<Stop> fromStops;
TimeWindow window; // filters trips used by time of day and service schedule
/** Return a set of all patterns that pass through the stops that are present in the given Tracker. */
public Set<TripPattern> uniquePatternsVisiting(Set<Stop> stops) {
Set<TripPattern> patterns = Sets.newHashSet();
for (Stop stop : stops) {
for (TripPattern pattern : graph.index.patternsForStop.get(stop)) {
patterns.add(pattern);
}
}
return patterns;
}
long searchBeginTime;
long abortTime;
private void checkTimeout() {
if (System.currentTimeMillis() > abortTime) {
throw new RuntimeException("TIMEOUT");
}
}
public TimeSurface.RangeSet route () {
// NOT USED here, however FIXME this is not threadsafe, needs lock graph.index.clusterStopsAsNeeded();
LOG.info("access modes: {}", request.accessModes);
LOG.info("egress modes: {}", request.egressModes);
LOG.info("direct modes: {}", request.directModes);
// Establish search timeouts
searchBeginTime = System.currentTimeMillis();
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));
fromStops = findClosestStops(TraverseMode.WALK);
LOG.info("From patterns/stops: {}", fromStops);
/* Initialize time range tracker to begin the search. */
TimeRange.Tracker times = new TimeRange.Tracker();
for (Stop stop : fromStops.keySet()) {
times.set(stop, fromStops.get(stop));
}
Set<Stop> stopsUpdated = fromStops.keySet();
for (int round = 0; round < MAX_RIDES; round++) {
// TODO maybe even loop until no updates happen? That should happen automatically if MAX_RIDES is high enough.
/* Get all patterns passing through stops updated in the last round, then reinitialize the updated stops set. */
Set<TripPattern> patternsUpdated = uniquePatternsVisiting(stopsUpdated);
LOG.info("ROUND {} : {} stops and {} patterns to explore.", round, stopsUpdated.size(), patternsUpdated.size());
stopsUpdated = Sets.newHashSet();
/* RAPTOR style: iterate over each pattern once. */
for (TripPattern pattern : patternsUpdated) {
//checkTimeout();
TimeRange rangeBeingPropagated = null;
List<Stop> stops = pattern.getStops();
FrequencyEntry freq = pattern.getSingleFrequencyEntry();
if (freq == null) continue;
TripTimes tt = freq.tripTimes;
int headway = freq.headway;
for (int sidx = 0; sidx < stops.size(); sidx++) {
Stop stop = stops.get(sidx);
TimeRange existingRange = times.get(stop);
TimeRange reBoardRange = (existingRange != null) ? existingRange.wait(headway) : null;
if (rangeBeingPropagated == null) {
// We do not yet have a range worth propagating
if (reBoardRange != null) {
rangeBeingPropagated = reBoardRange; // this is a fresh protective copy
}
} else {
// We already have a range that is being propagated along the pattern.
// We are certain sidx >= 1 here because we have already boarded in a previous iteration.
TimeRange arrivalRange = rangeBeingPropagated.shift(tt.getRunningTime(sidx - 1));
if (times.add(stop, arrivalRange)) {
// The propagated time improved the best known time in some way.
stopsUpdated.add(stop);
}
// TODO handle case where arrival and departure are different
rangeBeingPropagated = arrivalRange.shift(tt.getDwellTime(sidx));
if (reBoardRange != null) {
rangeBeingPropagated.mergeIn(reBoardRange);
}
}
}
}
/* Transfer from updated stops to adjacent stops before beginning the next round.
Iterate over a protective copy because we add more stops to the updated list during iteration. */
if ( ! graph.hasDirectTransfers) {
throw new RuntimeException("Requires the SimpleTransfers generated in long distance mode.");
}
for (Stop stop : Lists.newArrayList(stopsUpdated)) {
Collection<Edge> outgoingEdges = graph.index.stopVertexForStop.get(stop).getOutgoing();
for (SimpleTransfer transfer : Iterables.filter(outgoingEdges, SimpleTransfer.class)) {
Stop targetStop = ((TransitStop) transfer.getToVertex()).getStop();
double walkTime = transfer.getDistance() / request.walkSpeed;
TimeRange rangeAfterTransfer = times.get(stop).shift((int)walkTime);
if (times.add(targetStop, rangeAfterTransfer)) {
stopsUpdated.add(targetStop);
}
}
}
}
LOG.info("Done with transit.");
LOG.info("Propagating from transit stops to the street network...");
// Grab a cached map of distances to street intersections from each transit stop
StopTreeCache stopTreeCache = graph.index.getStopTreeCache();
// Iterate over all stops that were reached in the transit part of the search
for (Stop stop : times) {
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.
TimeRange rangeAtTransitStop = times.get(stop);
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;
}
TimeRange propagatedRange = rangeAtTransitStop.shift(egressWalkTimeSeconds);
TimeRange existingTimeRange = propagatedTimes.get(vertex);
if (existingTimeRange == null) {
propagatedTimes.put(vertex, propagatedRange);
} else {
existingTimeRange.mergeIn(propagatedRange);
}
}
}
LOG.info("Done with propagation.");
TimeSurface.RangeSet result = TimeSurface.makeSurfaces(this);
LOG.info("Done making time surfaces.");
return result;
}
/**
* Perform an on-street search around a point with a specific mode to find nearby stops.
* TODO merge with NearbyStopFinder
*/
private TObjectIntMap<Stop> findClosestStops(final TraverseMode mode) {
RoutingRequest rr = new RoutingRequest(mode);
GenericLocation gl = new GenericLocation(request.fromLat, request.fromLon);
rr.from = gl;
// FIXME destination must be set, even though this is meaningless for one-to-many
rr.to = gl;
rr.setRoutingContext(graph);
// Set batch after context, so both origin and dest vertices will be found.
rr.batch = (true);
rr.walkSpeed = request.walkSpeed;
// RR dateTime defaults to currentTime.
// If elapsed time is not capped, searches are very slow.
int minAccessTime = 0;
int maxAccessTime = request.maxWalkTime;
rr.worstTime = (rr.dateTime + maxAccessTime * 60);
AStar astar = new AStar();
rr.dominanceFunction = new DominanceFunction.EarliestArrival();
rr.setNumItineraries(1);
StopFinderTraverseVisitor visitor = new StopFinderTraverseVisitor(mode, minAccessTime * 60);
astar.setTraverseVisitor(visitor);
astar.getShortestPathTree(rr, 5); // timeout in seconds
rr.cleanup();
return visitor.stopsFound;
}
// TODO merge with NearbyStopFinder
static class StopFinderTraverseVisitor implements TraverseVisitor {
TraverseMode mode;
int minTravelTimeSeconds = 0;
TObjectIntMap<Stop> stopsFound = new TObjectIntHashMap();
public StopFinderTraverseVisitor(TraverseMode mode, int minTravelTimeSeconds) {
this.mode = mode;
this.minTravelTimeSeconds = minTravelTimeSeconds;
}
@Override public void visitEdge(Edge edge, State state) { }
@Override public void visitEnqueue(State state) { }
// Accumulate stops as the search runs.
@Override public void visitVertex(State state) {
Vertex vertex = state.getVertex();
if (vertex instanceof TransitStop) {
Stop stop = ((TransitStop)vertex).getStop();
if (stopsFound.containsKey(stop)) return; // record only the closest stop in each cluster
stopsFound.put(stop, (int) state.getElapsedTimeSeconds());
}
}
}
/** Given a minimum and maximum at a starting vertex, build an on-street SPT and propagate those values outward. */
class ExtremaPropagationTraverseVisitor implements TraverseVisitor {
final TimeRange range0;
ExtremaPropagationTraverseVisitor(TimeRange range0) {
this.range0 = range0;
}
@Override public void visitEdge(Edge edge, State state) { }
@Override public void visitEnqueue(State state) { }
@Override public void visitVertex(State state) {
TimeRange propagatedRange = range0.shift((int) state.getElapsedTimeSeconds());
Vertex vertex = state.getVertex();
TimeRange existing = propagatedTimes.get(vertex);
if (existing == null) {
propagatedTimes.put(vertex, propagatedRange);
} else {
existing.mergeIn(propagatedRange);
}
}
}
public void cleanup() { /* DO NOTHING */ }
}