package org.opentripplanner.profile; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.onebusaway.gtfs.model.Route; import org.opentripplanner.routing.edgetype.TripPattern; import org.opentripplanner.routing.trippattern.FrequencyEntry; import org.opentripplanner.routing.trippattern.TripTimes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import gnu.trove.iterator.TIntIterator; import gnu.trove.list.TIntList; import gnu.trove.list.array.TIntArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; /** * A Ride is defined by its from-stop and to-stop, as well as the previous ride (from which it transfers). * It serves as a container for multiple PatternRides, all of which connect that same pair of Stops. * A Ride may be unfinished, which means it does not yet have a destination and contains only unfinished PatternRides. * * TODO this could just be a RouteRide key mapped to lists of patternRides with the same characteristics. * its constructor would pull out the right fields. * * TODO if patternRides can be added later, and stats and transfers are computed after the fact, * then maybe we no longer need a round-based approach. * We need to put _groups_ of patternRides into the queue, grouped by back ride and beginning stop. */ public class Ride { private static final Logger LOG = LoggerFactory.getLogger(Ride.class); /* Here we store stop objects rather than indexes. The start and end indexes in the Ride's * constituent PatternRides should correspond to these same stops. */ final StopCluster from; final StopCluster to; final Ride previous; final List<PatternRide> patternRides = Lists.newArrayList(); Stats rideStats; // filled in only once the ride is complete (has all PatternRides). Stats waitStats; // filled in only once the ride is complete (has all PatternRides). Stats accessStats; // min and max time to reach this ride from the previous one, or from the origin point on the first ride int accessDist; // meters from the previous ride, or from the origin point on the first ride int dlb = 0; int dub = Integer.MAX_VALUE; int pathLength = 0; /** Construct a partial ride (resulting from a transfer, waiting to be completed). */ // TODO add additional parameter for access stats, then recalc bounds in the constructor. public Ride (StopCluster from, Ride previous) { this.from = from; this.to = null; // this is a "partial ride" waiting to be completed. this.previous = previous; } /** Construct a partial copy with no PatternRides or Stats and the given arrival StopCluster. */ public Ride (Ride other, StopCluster to) { this.from = other.from; this.to = to; this.previous = other.previous; this.accessStats = other.accessStats; this.accessDist = other.accessDist; // Bounds will be recomputed when stats are computed. } /** Extend this incomplete ride to the given stop, creating a container for PatternRides. */ public Ride extendTo(StopCluster toStopCluster) { return new Ride(this, toStopCluster); } public String toString() { return String.format("Ride from %s to %s (%d patterns on routes %s)", from, to, patternRides.size(), getRoutes()); } /** Output this entire chain of rides. */ public void dumpRideChain() { List<Ride> rides = Lists.newLinkedList(); Ride ride = this; while (ride != null) { rides.add(0, ride); ride = ride.previous; } LOG.info("Path from {} to {}", rides.get(0).from, rides.get(rides.size() - 1).to); for (Ride r : rides) LOG.info(" {}", r.toString()); } public Multimap<Route, PatternRide> getPatternRidesByRoute() { Multimap<Route, PatternRide> ret = HashMultimap.create(); for (PatternRide pr : patternRides) ret.put(pr.pattern.route, pr); return ret; } public Set<Route> getRoutes() { Set<Route> routes = Sets.newHashSet(); for (PatternRide ride : patternRides) routes.add(ride.pattern.route); return routes; } public boolean containsPattern(TripPattern pattern) { for (PatternRide patternRide : patternRides) { if (patternRide.pattern == pattern) return true; } return false; } public boolean pathContainsRoute(Route route) { // Linear search, could use sets if this proves to be time consuming Ride ride = this; while (ride != null) { for (PatternRide pr : patternRides) { if (pr.pattern.route == route) return true; } ride = ride.previous; } return false; } // TODO rename _cluster_ public boolean pathContainsStop(StopCluster stopCluster) { Ride ride = this; while (ride != null) { if (ride.from == stopCluster || ride.to == stopCluster) return true; ride = ride.previous; } return false; } /** * Calculate length and upper and lower bounds on duration for the chain of rides ending with this one. * This should be called whenever the ride/wait/access stats for the ride are updated. * We can't call it in the constructor or update methods because access stats are set after construction, * and wait stats are sometimes null. */ public void recomputeBounds() { dlb = 0; dub = 0; pathLength = 0; Ride ride = this; if (ride.to == null) { // This is an unfinished ride that just came off the queue. // It is the result of a transfer. It has access time, but not wait or ride time yet. dlb += ride.accessStats.min; dub += ride.accessStats.max; ride = ride.previous; } while (ride != null) { pathLength += 1; dlb += ride.rideStats.min; dlb += ride.waitStats.min; dlb += ride.accessStats.min; dub += ride.rideStats.max; dub += ride.waitStats.max; dub += ride.accessStats.max; ride = ride.previous; } } /** * Create a compound Stats for all the constituent PatternRides of this Ride. * This should not be called until all PatternRides have been added to this Ride. * There are two separate Stats objects: The rideStats includes the time spent on the patterns themselves. * The waitStats capture the time spent waiting to board those patterns (transfer or initial boarding). */ public void calcStats(TimeWindow window, double walkSpeed) { /* Stats for the ride on transit. */ List<Stats> stats = Lists.newArrayList(); for (PatternRide patternRide : patternRides) { stats.add(patternRide.stats); } rideStats = new Stats(stats); /* Stats for the wait between the last ride and this one, NOT including walk time. */ waitStats = calcStatsForFreqs(window); // Only try schedule-based boarding if there were no non-exact frequency entries. // FIXME there is an assumption here that there are only frequency or non-frequency entries in a PatternRide if (waitStats == null) { if (previous == null) { // If there is no previous ride, assume uniformly distributed arrival times. waitStats = calcStatsForBoarding(window); } else { // There is a previous ride, so account for arrival and departure times before and after the transfer. waitStats = calcStatsForTransfer(window, walkSpeed); } } } /* Maybe store transfer distances by stop pair, and look them up. */ /** * @param arrivals find arrival times rather than departure times for this Ride. * @return a list of sorted departure or arrival times within the window. * FIXME this is a hot spot in execution, about 50 percent of runtime. */ public TIntList getSortedStoptimes (TimeWindow window, boolean arrivals) { // Using Lists because we don't know the length in advance TIntList times = new TIntArrayList(); // TODO include exact-times frequency trips along with non-frequency trips // non-exact (headway-based) frequency trips will be handled elsewhere since they don't have specific boarding times. for (PatternRide patternRide : patternRides) { for (TripTimes tt : patternRide.pattern.scheduledTimetable.tripTimes) { if (window.servicesRunning.get(tt.serviceCode)) { int t = arrivals ? tt.getArrivalTime(patternRide.toIndex) : tt.getDepartureTime(patternRide.fromIndex); if (window.includes(t)) times.add(t); } } } times.sort(); return times; } /** Calculate the wait time stats for boarding all (non-exact) frequency entries in this Ride. */ private Stats calcStatsForFreqs(TimeWindow window) { Stats stats = new Stats(); // all stats fields are initialized to zero stats.num = 0; // the total number of seconds that headway boarding is possible for (PatternRide patternRide : patternRides) { for (FrequencyEntry freq : patternRide.pattern.scheduledTimetable.frequencyEntries) { if (freq.exactTimes) { LOG.error("Exact times not yet supported in profile routing."); return null; } int overlap = window.overlap(freq.startTime, freq.endTime, freq.tripTimes.serviceCode); if (overlap > 0) { if (freq.headway > stats.max) stats.max = freq.headway; // weight the average of each headway by the number of seconds it is valid stats.avg += (freq.headway / 2) * overlap; stats.num += overlap; } } } if (stats.num == 0) return null; /* Some frequency entries were added to the stats. */ stats.avg /= stats.num; return stats; } /** * Produce stats about boarding an initial Ride, which has no previous ride. * This assumes arrival times are uniformly distributed during the window. * The Ride must contain some trips, and the window must have a positive duration. */ public Stats calcStatsForBoarding(TimeWindow window) { Stats stats = new Stats (); stats.min = 0; // You can always arrive just before a train departs. TIntList departures = getSortedStoptimes(window, false); int last = window.from; double avgAccumulated = 0.0; /* All departures in the list are known to be running and within the window. */ for (TIntIterator it = departures.iterator(); it.hasNext();) { int dep = it.next(); int maxWait = dep - last; if (maxWait > stats.max) stats.max = maxWait; /* Weight the average of each interval by the number of seconds it contains. */ avgAccumulated += (maxWait / 2.0) * maxWait; stats.num += maxWait; last = dep; } if (stats.num > 0) { stats.avg = (int) (avgAccumulated / stats.num); } return stats; } /** * Calculates Stats for the transfer to the given ride from the previous ride. * This should only be called after all PatternRides have been added to the ride. * Distances can be stored in rides, including the first and last distance. But waits must be * calculated from full sets of patterns, which are not known until a round is over. */ public Stats calcStatsForTransfer (TimeWindow window, double walkSpeed) { TIntList arrivals = previous.getSortedStoptimes(window, true); TIntList departures = this.getSortedStoptimes(window, false); List<Integer> waits = Lists.newArrayList(); TIntIterator departureIterator = departures.iterator(); int departure = departureIterator.next(); ARRIVAL : for (TIntIterator arrivalsIterator = arrivals.iterator(); arrivalsIterator.hasNext();) { int arrival = arrivalsIterator.next(); // On transfers the access stats should have max=min=avg // We use the min, which would be best if min != max since it should only relax the bounds somewhat. int boardTime = arrival + accessStats.min + ProfileRouter.SLACK; while (departure <= boardTime) { if (!departureIterator.hasNext()) break ARRIVAL; departure = departureIterator.next(); } waits.add(departure - boardTime); } /* Waits list may be empty if no transfers are possible. */ if (waits.isEmpty()) return null; // Impossible to make this transfer. return new Stats (waits); } /** @return the stop at which the rider would board the chain of Rides this Ride belongs to. */ public StopCluster getAccessStopCluster() { Ride ride = this; while (ride.previous != null) { ride = ride.previous; } return ride.from; } /** @return the stop from which the rider will walk to the final destination, assuming this is the final Ride in a chain. */ public StopCluster getEgressStopCluster() { return this.to; } }