/* 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.edgetype; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.StopTime; import org.onebusaway.gtfs.model.Trip; import org.opentripplanner.routing.core.ServiceDay; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.trippattern.CanceledTripTimes; import org.opentripplanner.routing.trippattern.DecayingDelayTripTimes; import org.opentripplanner.routing.trippattern.ScheduledTripTimes; import org.opentripplanner.routing.trippattern.TripTimes; import org.opentripplanner.routing.trippattern.UpdateBlock; import org.opentripplanner.routing.trippattern.UpdatedTripTimes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Timetables provide most of the TripPattern functionality. Each TripPattern may possess more * than one Timetable when stop time updates are being applied: one for the scheduled stop times, * one for each snapshot of updated stop times, another for a working buffer of updated stop * times, etc. Timetable is a non-static nested (inner) class, so each Timetable belongs to a * specific TripPattern, whose fields it can access. */ public class Timetable implements Serializable { private static final long serialVersionUID = 1L; private static final Logger LOG = LoggerFactory.getLogger(Timetable.class); /** * The Timetable size (number of TripTimes) at which indexes will be built for all stops. * Below this size, departure and arrival times will be found by linear search. Above this * size, it will be possible to use binary search. * Break even list size for linear and binary searches was determined to be around 16. */ private static final int INDEX_THRESHOLD = 16; private final TableTripPattern pattern; /** * Contains one TripTimes object for each scheduled trip (even cancelled ones) and possibly * additional TripTimes objects for unscheduled trips. */ private final ArrayList<TripTimes> tripTimes; /** * If the departures index is null, this timetable has not been indexed: use a linear search. * Unfortunately you really do need 2 indexes, because dwell times for different trips at * the same stop may overlap. The indexes always contain the same elements as the main * tripTimes List, but are re-sorted at each stop to allow binary searches. */ private transient TripTimes[][] arrivalsIndex = null; private transient TripTimes[][] departuresIndex = null; /** For each hop, the best running time. This serves to provide lower bounds on traversal time. */ private transient int bestRunningTimes[]; /** For each stop, the best dwell time. This serves to provide lower bounds on traversal time. */ private transient int bestDwellTimes[]; /** Construct an empty Timetable. */ public Timetable(TableTripPattern pattern) { tripTimes = new ArrayList<TripTimes>(); this.pattern = pattern; } /** * Copy constructor: create an un-indexed Timetable with the same TripTimes as the * specified timetable. */ private Timetable (Timetable tt) { tripTimes = new ArrayList<TripTimes>(tt.tripTimes); this.pattern = tt.pattern; } /** * This copy instance method can see the enclosing TripPattern instance, while the copy * constructor does not. The only publicly visible way to make a timetable, and it should * probably be protected. */ public Timetable copy() { return new Timetable(this); } /** * Produces 2D index arrays that are stop-major and sorted, allowing binary search at any * given stop. It is of course inefficient to call this after updating only one or two * trips in a pattern since we can usually get by with swapping only the new trip into the * existing already-sorted lists. But let's see realistically how resource-intensive this * is before optimizing it. */ private void index() { int nHops = pattern.getHopCount(); arrivalsIndex = new TripTimes[nHops][]; departuresIndex = new TripTimes[nHops][]; boolean departuresFifo = true; boolean arrivalsMatchDepartures = true; for (int hop = 0; hop < nHops; hop++) { // copy canonical TripTimes List into new arrays arrivalsIndex[hop] = tripTimes.toArray(new TripTimes[tripTimes.size()]); departuresIndex[hop] = tripTimes.toArray(new TripTimes[tripTimes.size()]); // TODO: STOP VS HOP Arrays.sort(arrivalsIndex[hop], new TripTimes.ArrivalsComparator(hop)); Arrays.sort(departuresIndex[hop], new TripTimes.DeparturesComparator(hop)); if (hop > 0) { if (Arrays.equals(departuresIndex[hop], departuresIndex[hop - 1])) departuresIndex[hop] = departuresIndex[hop - 1]; else departuresFifo = false; } if (Arrays.equals(departuresIndex[hop], arrivalsIndex[hop])) arrivalsIndex[hop] = departuresIndex[hop]; else arrivalsMatchDepartures = false; } if (departuresFifo) { //LOG.debug("Compressing FIFO Timetable index."); departuresIndex = Arrays.copyOf(departuresIndex, 1); } if (arrivalsMatchDepartures) { //LOG.debug("Reusing departures index where arrivals index is identical."); arrivalsIndex = departuresIndex; } } /** * Get the next (previous) trip that departs (arrives) from the specified stop * at or after (before) the specified time. The haveBicycle parameter must be passed in * because we cannot determine whether the user is in possession of a rented bicycle from * the options alone. If a pre-allocated array is passed in via * the optional adjacentTimes parameter, that array will be filled with the main result plus * a suitable number of TripTimes roughly temporally adjacent to the main result. If the main * result is null, the contents of the adjacentTimes array are undefined. Note that no * guarantees of exhaustiveness, contiguity, etc. are made about the additional TripTimes objects. * * @return the TripTimes object representing the (possibly updated) best trip, or null if no * trip matches both the time and other criteria. */ protected TripTimes getNextTrip(int stopIndex, int time, State state0, ServiceDay sd, boolean haveBicycle, boolean boarding) { TripTimes bestTrip = null; int index; TripTimes[][] tableIndex = boarding ? departuresIndex : arrivalsIndex; int stopOffset = boarding ? 0 : 1; Stop currentStop = pattern.getStop(stopIndex + stopOffset); if (tableIndex != null) { TripTimes[] sorted; // this timetable has been indexed, use binary search if (tableIndex.length == 1) // for optimized FIFO patterns sorted = tableIndex[0]; else sorted = tableIndex[stopIndex]; // an alternative to conditional increment/decrement would be to sort the arrivals // index in decreasing order, but that would require changing the search algorithm if (boarding) { index = TripTimes.binarySearchDepartures(sorted, stopIndex, time); while (index < sorted.length) { TripTimes tt = sorted[index++]; if (tt.tripAcceptable(state0, currentStop, sd, haveBicycle, stopIndex, boarding)) { bestTrip = tt; break; } } } else { index = TripTimes.binarySearchArrivals(sorted, stopIndex, time); while (index >= 0) { TripTimes tt = sorted[index--]; if (tt.tripAcceptable(state0, currentStop, sd, haveBicycle, stopIndex, boarding)) { bestTrip = tt; break; } } } } else { // no index present on this timetable. use a linear search: // because trips may change with stoptime updates, we cannot count on them being sorted int bestTime = boarding ? Integer.MAX_VALUE : Integer.MIN_VALUE; for (TripTimes tt : tripTimes) { // hoping JVM JIT will distribute the loop over the if clauses as needed if (boarding) { int depTime = tt.getDepartureTime(stopIndex); if (depTime >= time && depTime < bestTime && tt.tripAcceptable(state0, currentStop, sd, haveBicycle, stopIndex, boarding)) { bestTrip = tt; bestTime = depTime; } } else { int arvTime = tt.getArrivalTime(stopIndex); if (arvTime <= time && arvTime > bestTime && tt.tripAcceptable(state0, currentStop, sd, haveBicycle, stopIndex, boarding)) { bestTrip = tt; bestTime = arvTime; } } } } return bestTrip; } /** Gets the departure time for a given hop on a given trip */ public int getDepartureTime(int hop, int trip) { return tripTimes.get(trip).getDepartureTime(hop); } /** Gets the arrival time for a given hop on a given trip */ public int getArrivalTime(int hop, int trip) { return tripTimes.get(trip).getArrivalTime(hop); } /** Gets the running time after a given stop (i.e. for the given hop) on a given trip */ public int getRunningTime(int stopIndex, int trip) { return tripTimes.get(trip).getRunningTime(stopIndex); } /** Gets the dwell time at a given stop (i.e. before then given hop) on a given trip */ public int getDwellTime(int hop, int trip) { // the dwell time of a hop is the dwell time *before* that hop. return tripTimes.get(trip).getDwellTime(hop); } /** * Finish off a TripPattern once all TripTimes have been added to it. This involves caching * lower bounds on the running times and dwell times at each stop, and may perform other * actions to compact the data structure such as trimming and deduplicating arrays. */ public void finish() { int nHops = pattern.getHopCount(); int nTrips = tripTimes.size(); bestRunningTimes = new int[nHops]; boolean nullArrivals = false; // TODO: should scan through triptimes? if ( ! nullArrivals) { bestDwellTimes = new int[nHops]; for (int h = 1; h < nHops; ++h) { // dwell time is undefined on first hop bestDwellTimes[h] = Integer.MAX_VALUE; for (int t = 0; t < nTrips; ++t) { int dt = this.getDwellTime(h, t); if (bestDwellTimes[h] > dt) { bestDwellTimes[h] = dt; } } } } // Q: Why is incoming running times 1 shorter than departures? // A: Because when there are no arrivals array, the last departure is actually used for an arrival. for (int h = 0; h < nHops; ++h) { bestRunningTimes[h] = Integer.MAX_VALUE; for (int t = 0; t < nTrips; ++t) { int rt = this.getRunningTime(h, t); if (bestRunningTimes[h] > rt) { bestRunningTimes[h] = rt; } } } if (nTrips > INDEX_THRESHOLD) { //LOG.debug("indexing pattern with {} trips", nTrips); index(); } else { arrivalsIndex = null; departuresIndex = null; } } public class DeparturesIterator implements Iterator<Integer> { int nextPosition = 0; private int stopIndex; public DeparturesIterator(int stopIndex) { this.stopIndex = stopIndex; } @Override public boolean hasNext() { return nextPosition < tripTimes.size(); } @Override public Integer next() { return tripTimes.get(nextPosition++).getDepartureTime(stopIndex); } @Override public void remove() { throw new UnsupportedOperationException(); } } /** Gets all the departure times at a given stop (not used in routing) */ public Iterator<Integer> getDepartureTimes(int stopIndex) { return new DeparturesIterator(stopIndex); } /** @return the index of TripTimes for this Trip(Id) in this particular Timetable */ public int getTripIndex(AgencyAndId tripId) { int ret = 0; for (TripTimes tt : tripTimes) { // could replace linear search with indexing in stoptime updater, but not necessary // at this point since the updater thread is far from pegged. if (tt.getTrip().getId().equals(tripId)) return ret; ret += 1; } return -1; } /** * Not private because it's used when traversing interline dwells, which refer to order * in the scheduled trip pattern. */ public TripTimes getTripTimes(int tripIndex) { return tripTimes.get(tripIndex); } /** * Apply the UpdateBlock to the appropriate ScheduledTripTimes from this Timetable. * The existing TripTimes must not be modified directly because they may be shared with * the underlying scheduledTimetable, or other updated Timetables. * The StoptimeUpdater performs the protective copying of this Timetable. It is not done in * this update method to avoid repeatedly cloning the same Timetable when several updates * are applied to it at once. * @return whether or not the timetable actually changed as a result of this operation * (maybe it should do the cloning and return the new timetable to enforce copy-on-write?) */ public boolean update(UpdateBlock block) { try { // Though all timetables have the same trip ordering, some may have extra trips due to // the dynamic addition of unscheduled trips. // However, we want to apply trip update blocks on top of *scheduled* times int tripIndex = getTripIndex(block.tripId); if (tripIndex == -1) { LOG.info("tripId {} not found in pattern.", block.tripId); return false; } else { LOG.trace("tripId {} found at index {} (in scheduled timetable)", block.tripId, tripIndex); } // 'stop' Index as in transit stop (not 'end', not 'hop') int stopIndex = block.findUpdateStopIndex(pattern); if (stopIndex == UpdateBlock.MATCH_FAILED) { LOG.warn("Unable to match update block to stopIds."); return false; } TripTimes existingTimes = getTripTimes(tripIndex); ScheduledTripTimes scheduledTimes = existingTimes.getScheduledTripTimes(); TripTimes newTimes; if (block.isCancellation()) { newTimes = new CanceledTripTimes(scheduledTimes); } else { newTimes = new UpdatedTripTimes(scheduledTimes, block, stopIndex); if ( ! newTimes.timesIncreasing()) { LOG.warn("Resulting UpdatedTripTimes has non-increasing times. " + "Falling back on DecayingDelayTripTimes."); LOG.warn(block.toString()); LOG.warn(newTimes.toString()); int delay = newTimes.getDepartureDelay(stopIndex); // maybe decay should be applied on top of the update (wrap Updated in Decaying), // starting at the end of the update block newTimes = new DecayingDelayTripTimes(scheduledTimes, stopIndex, delay); LOG.warn(newTimes.toString()); if ( ! newTimes.timesIncreasing()) { LOG.error("Even these trip times are non-increasing. Underlying schedule problem?"); return false; } } } // Update succeeded, save the new TripTimes back into this Timetable. this.tripTimes.set(tripIndex, newTimes); return true; } catch (Exception e) { // prevent server from dying while debugging e.printStackTrace(); return false; } } /** * Add a trip to this Timetable. The Timetable must be analyzed, compacted, and indexed * any time trips are added, but this is not done automatically because it is time consuming * and should only be done once after an entire batch of trips are added. * Any new trip that is added is a ScheduledTripTimes. The scheduledTimetable will then * contain only ScheduledTripTimes, and any updated Timetables will contain TripTimes * that wrap these ScheduledTripTimes, plus any additional trips as ScheduledTripTimes. * Maybe subclass ScheduledTripTimes with an equivalent ExtraTripTimes class to make this * distinction clear. */ public void addTrip(Trip trip, List<StopTime> stopTimes) { // TODO: double-check that the stops and pickup/dropoffs are right for this trip tripTimes.add(new ScheduledTripTimes(trip, stopTimes)); // TODO eliminate delegation / encapsulation fail pattern.trips.add(trip); } /** * Check that all dwell times at the given stop are zero, which allows removing the dwell edge. */ boolean allDwellsZero(int hopIndex) { for (int t = 0; t < tripTimes.size(); ++t) { if (getDwellTime(hopIndex, t) != 0) { return false; } } return true; } /** Returns the shortest possible running time for this stop */ public int getBestRunningTime(int stopIndex) { return bestRunningTimes[stopIndex]; } /** Returns the shortest possible dwell time at this stop */ public int getBestDwellTime(int stopIndex) { if (bestDwellTimes == null) { return 0; } return bestDwellTimes[stopIndex]; } }