All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.model.TimetableSnapshot Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.model;

import static org.opentripplanner.utils.collection.CollectionUtils.getByNullableKey;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.RealTimeRaptorTransitDataUpdater;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.framework.Result;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate;
import org.opentripplanner.transit.model.timetable.TripOnServiceDate;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.updater.spi.UpdateError;
import org.opentripplanner.updater.spi.UpdateSuccess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A TimetableSnapshot holds a set of realtime-updated Timetables frozen at a moment in time. It
 * can return a Timetable for any TripPattern in the public transit network considering all
 * accumulated realtime updates, falling back on the scheduled Timetable if no updates have been
 * applied for a given TripPattern.
 * 

* This is a central part of managing concurrency when many routing searches may be happening, but * realtime updates are also streaming in which change the vehicle arrival and departure times. * Any given request will only see one unchanging TimetableSnapshot over the course of its search. *

* An instance of TimetableSnapshot first serves as a buffer to accumulate a batch of incoming * updates on top of any already known updates to the base schedules. From time to time such a batch * of updates is committed (like a database transaction). At this point the TimetableSnapshot is * treated as immutable and becomes available for use by new incoming routing requests. *

* All updates to a snapshot must be completed before it is handed off to any searches. A single * snapshot should be used for an entire search, and should remain unchanged for that duration to * provide a consistent view not only of trips that have been boarded, but of relative arrival and * departure times of other trips that have not necessarily been boarded. *

* A TimetableSnapshot instance may only be modified by a single thread. This makes it easier to * reason about how the snapshot is built up and used. Write operations are applied one by one, in * order, with no concurrent access. Read operations are then allowed concurrently by many threads * after writing is forbidden. *

* The fact that TripPattern instances carry a reference only to their scheduled Timetable and not * to their realtime timetable is largely due to historical path-dependence in OTP development. * Streaming realtime support was added around 2013 as a sort of sandbox feature that was switched * off by default. Looking up realtime timetables during routing was a fringe feature that needed * to impose near-zero cost and avoid introducing complexity into the primary codebase. Now over * ten years later, the principles of how this system operates are rather stable, but the * implementation would benefit from some deduplication and cleanup. Once that is complete, looking * up timetables on this class could conceivably be replaced with snapshotting entire views of the * transit network. It would also be possible to make the realtime version of Timetables or * TripTimes the primary view, and include references back to their scheduled versions. *

* Implementation note: when a snapshot is committed, the mutable state of this class is stored * in final fields and completely initialized in the constructor. This provides an additional * guarantee of safe-publication without synchronization. * (see final Field Semantics) */ public class TimetableSnapshot { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshot.class); /** * During the construction phase of the TimetableSnapshot, before it is considered immutable and * used in routing, this Map holds all timetables that have been modified and are waiting to be * indexed. * A real-time timetable overrides the scheduled timetable of a TripPattern for only a single * service date. There can be only one overriding timetable per TripPattern and per service date. * This is enforced by indexing the map with a pair (TripPattern, service date). * This map is cleared when the TimetableSnapshot becomes read-only. */ private final Map dirtyTimetables = new HashMap<>(); /** * For each TripPattern (sequence of stops on a particular Route) for which we have received a * realtime update, an ordered set of timetables on different days. The key TripPatterns may * include ones from the scheduled GTFS, as well as ones added by realtime messages and * tracked by the TripPatternCache.

* Note that the keys do not include all scheduled TripPatterns, only those for which we have at * least one update.

* The members of the SortedSet (the Timetable for a particular day) are treated as copy-on-write * when we're updating them. If an update will modify the timetable for a particular day, that * timetable is replicated before any modifications are applied to avoid affecting any previous * TimetableSnapshots still in circulation which reference that same Timetable instance.

* Alternative implementations: A. This could be an array indexed using the integer pattern * indexes. B. It could be made into a flat hashtable with compound keys (TripPattern, LocalDate). * The compound key approach better reflects the fact that there should be only one Timetable per * TripPattern and date. */ private final Map> timetables; /** * For cases where the trip pattern (sequence of stops visited) has been changed by a realtime * update, a Map associating the updated trip pattern with a compound key of the feed-scoped * trip ID and the service date. * This index includes only modified trip patterns for existing trips. * It does not include trip patterns for new trips created by real-time updates (extra journeys). * . * TODO RT_AB: clarify if this is an index or the original source of truth. */ private final Map realTimeNewTripPatternsForModifiedTrips; /** * This is an index of TripPatterns, not the primary collection. It tracks which TripPatterns * that were updated or newly created by realtime messages contain which stops. This allows them * to be readily found and included in API responses containing stop times at a specific stop. * This is a SetMultimap, so that each pattern is only retained once per stop even if it's added * more than once. * TODO RT_AB: More general handling of all realtime indexes outside primary data structures. */ private final SetMultimap patternsForStop; /** * The realTimeAdded* maps are indexes on the trips created at runtime (extra-journey), and the * Route, TripPattern, TripOnServiceDate they refer to. * They are meant to override the corresponding indexes in TimetableRepositoryIndex. */ private final Map realtimeAddedRoutes; private final Map realTimeAddedTrips; private final Map realTimeAddedPatternForTrip; private final Multimap realTimeAddedPatternsForRoute; private final Map realTimeAddedTripOnServiceDateById; private final Map< TripIdAndServiceDate, TripOnServiceDate > realTimeAddedTripOnServiceDateForTripAndDay; /** * Boolean value indicating that timetable snapshot is read only if true. Once it is true, it * shouldn't be possible to change it to false anymore. */ private final boolean readOnly; /** * Boolean value indicating that this timetable snapshot contains changes compared to the state of * the last commit if true. */ private boolean dirty = false; public TimetableSnapshot() { this( new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), HashMultimap.create(), new HashMap<>(), new HashMap<>(), HashMultimap.create(), false ); } private TimetableSnapshot( Map> timetables, Map realTimeNewTripPatternsForModifiedTrips, Map realtimeAddedRoutes, Map realtimeAddedTrips, Map realTimeAddedPatternForTrip, Multimap realTimeAddedPatternsForRoute, Map realTimeAddedTripOnServiceDateById, Map realTimeAddedTripOnServiceDateForTripAndDay, SetMultimap patternsForStop, boolean readOnly ) { this.timetables = timetables; this.realTimeNewTripPatternsForModifiedTrips = realTimeNewTripPatternsForModifiedTrips; this.realtimeAddedRoutes = realtimeAddedRoutes; this.realTimeAddedTrips = realtimeAddedTrips; this.realTimeAddedPatternForTrip = realTimeAddedPatternForTrip; this.realTimeAddedPatternsForRoute = realTimeAddedPatternsForRoute; this.realTimeAddedTripOnServiceDateById = realTimeAddedTripOnServiceDateById; this.realTimeAddedTripOnServiceDateForTripAndDay = realTimeAddedTripOnServiceDateForTripAndDay; this.patternsForStop = patternsForStop; this.readOnly = readOnly; } /** * Returns an updated timetable for the specified pattern if one is available in this snapshot, or * the originally scheduled timetable if there are no updates in this snapshot. */ public Timetable resolve(TripPattern pattern, LocalDate serviceDate) { SortedSet sortedTimetables = timetables.get(pattern); if (sortedTimetables != null && serviceDate != null) { for (Timetable timetable : sortedTimetables) { if (timetable != null && timetable.isValidFor(serviceDate)) { return timetable; } } } return pattern.getScheduledTimetable(); } /** * Get the current trip pattern given a trip id and a service date, if it has been changed from * the scheduled pattern with an update, for which the stopPattern is different. * * @return trip pattern created by the updater; null if trip is on the original trip pattern */ @Nullable public TripPattern getNewTripPatternForModifiedTrip(FeedScopedId tripId, LocalDate serviceDate) { TripIdAndServiceDate tripIdAndServiceDate = new TripIdAndServiceDate(tripId, serviceDate); return realTimeNewTripPatternsForModifiedTrips.get(tripIdAndServiceDate); } /** * List trips which have been canceled. */ public List listCanceledTrips() { return findTripsOnServiceDates(TripTimes::isCanceled); } /** * @return if any trip patterns were modified */ public boolean hasNewTripPatternsForModifiedTrips() { return !realTimeNewTripPatternsForModifiedTrips.isEmpty(); } /** * Return the route created by the updater for the given id. */ @Nullable public Route getRealtimeAddedRoute(FeedScopedId id) { return getByNullableKey(id, realtimeAddedRoutes); } public Collection listRealTimeAddedRoutes() { return Collections.unmodifiableCollection(realtimeAddedRoutes.values()); } /** * Return the trip created by the updater for the given id. */ @Nullable public Trip getRealTimeAddedTrip(FeedScopedId id) { return getByNullableKey(id, realTimeAddedTrips); } public Collection listRealTimeAddedTrips() { return Collections.unmodifiableCollection(realTimeAddedTrips.values()); } /** * Return the trip pattern created by the updater for the given trip. */ @Nullable public TripPattern getRealTimeAddedPatternForTrip(Trip trip) { return getByNullableKey(trip, realTimeAddedPatternForTrip); } /** * Return the trip patterns created by the updater for the given route. */ public Collection getRealTimeAddedPatternForRoute(Route route) { return realTimeAddedPatternsForRoute.get(route); } /** * Return the trip on service date created by the updater for the given id. */ @Nullable public TripOnServiceDate getRealTimeAddedTripOnServiceDateById(FeedScopedId id) { return getByNullableKey(id, realTimeAddedTripOnServiceDateById); } /** * Return the trip on service date created by the updater for the given trip and service date. */ @Nullable public TripOnServiceDate getRealTimeAddedTripOnServiceDateForTripAndDay( TripIdAndServiceDate tripIdAndServiceDate ) { return getByNullableKey(tripIdAndServiceDate, realTimeAddedTripOnServiceDateForTripAndDay); } public Collection listRealTimeAddedTripOnServiceDate() { return Collections.unmodifiableCollection(realTimeAddedTripOnServiceDateForTripAndDay.values()); } /** * Update the TripTimes of one Trip in a Timetable of a TripPattern. If the Trip of the TripTimes * does not exist yet in the Timetable, add it. This method will make a protective copy of the * Timetable if such a copy has not already been made while building up this snapshot, handling * both cases where patterns were pre-existing in static data or created by realtime data. * * @return whether the update was actually applied */ public Result update(RealTimeTripUpdate realTimeTripUpdate) { validateNotReadOnly(); TripPattern pattern = realTimeTripUpdate.pattern(); LocalDate serviceDate = realTimeTripUpdate.serviceDate(); TripTimes updatedTripTimes = realTimeTripUpdate.updatedTripTimes(); Timetable tt = resolve(pattern, serviceDate); TimetableBuilder ttb = tt.copyOf().withServiceDate(serviceDate); // Assume all trips in a pattern are from the same feed, which should be the case. ttb.addOrUpdateTripTimes(updatedTripTimes); Timetable updated = ttb.build(); swapTimetable(pattern, tt, updated); Trip trip = updatedTripTimes.getTrip(); if (pattern.isCreatedByRealtimeUpdater()) { // Remember this pattern for the added trip id and service date FeedScopedId tripId = trip.getId(); TripIdAndServiceDate tripIdAndServiceDate = new TripIdAndServiceDate(tripId, serviceDate); realTimeNewTripPatternsForModifiedTrips.put(tripIdAndServiceDate, pattern); } // To make these trip patterns visible for departureRow searches. addPatternToIndex(pattern); Route route = trip.getRoute(); if (realTimeTripUpdate.routeCreation()) { realtimeAddedRoutes.put(route.getId(), route); } if (realTimeTripUpdate.tripCreation()) { FeedScopedId tripId = trip.getId(); realTimeAddedTrips.put(tripId, trip); realTimeAddedPatternForTrip.put(trip, pattern); realTimeAddedPatternsForRoute.put(route, pattern); TripOnServiceDate tripOnServiceDate = realTimeTripUpdate.addedTripOnServiceDate(); if (tripOnServiceDate != null) { realTimeAddedTripOnServiceDateById.put(tripOnServiceDate.getId(), tripOnServiceDate); realTimeAddedTripOnServiceDateForTripAndDay.put( new TripIdAndServiceDate(tripId, serviceDate), tripOnServiceDate ); } } // The time tables are finished during the commit return Result.success(UpdateSuccess.noWarnings(realTimeTripUpdate.producer())); } /** * This produces a small delay of typically around 50ms, which is almost entirely due to the * indexing step. Cloning the map is much faster (2ms). It is perhaps better to index timetables * as they are changed to avoid experiencing all this lag at once, but we want to avoid * re-indexing when receiving multiple updates for the same timetable in rapid succession. This * compromise is expressed by the maxSnapshotFrequency property of StoptimeUpdater. The indexing * could be made much more efficient as well. * * @return an immutable copy of this TimetableSnapshot with all updates applied */ public TimetableSnapshot commit() { return commit(null, false); } public TimetableSnapshot commit( RealTimeRaptorTransitDataUpdater realtimeRaptorTransitDataUpdater, boolean force ) { validateNotReadOnly(); if (!force && !this.isDirty()) { return null; } TimetableSnapshot ret = new TimetableSnapshot( Map.copyOf(timetables), Map.copyOf(realTimeNewTripPatternsForModifiedTrips), Map.copyOf(realtimeAddedRoutes), Map.copyOf(realTimeAddedTrips), Map.copyOf(realTimeAddedPatternForTrip), ImmutableSetMultimap.copyOf(realTimeAddedPatternsForRoute), Map.copyOf(realTimeAddedTripOnServiceDateById), Map.copyOf(realTimeAddedTripOnServiceDateForTripAndDay), ImmutableSetMultimap.copyOf(patternsForStop), true ); if (realtimeRaptorTransitDataUpdater != null) { realtimeRaptorTransitDataUpdater.update(dirtyTimetables.values(), timetables); } this.dirtyTimetables.clear(); this.dirty = false; return ret; } /** * Clear all data of snapshot for the provided feed id * * @param feedId feed id to clear the snapshot for */ public void clear(String feedId) { validateNotReadOnly(); // Clear all data from snapshot. boolean timetablesWereCleared = clearTimetables(feedId); boolean newTripPatternsForModifiedTripsWereCleared = clearNewTripPatternsForModifiedTrips( feedId ); boolean addedTripPatternsWereCleared = clearEntriesForRealtimeAddedTrips(feedId); // If this snapshot was modified, it will be dirty after the clear actions. if ( timetablesWereCleared || newTripPatternsForModifiedTripsWereCleared || addedTripPatternsWereCleared ) { dirty = true; } } /** * If a previous realtime update has changed which trip pattern is associated with the given trip * on the given service date, this method will dissociate the trip from that pattern and remove * the trip's timetables from that pattern on that particular service date. * * For this service date, the trip will revert to its original trip pattern from the scheduled * data, remaining on that pattern unless it's changed again by a future realtime update. * * @return true if the trip was found to be shifted to a different trip pattern by a realtime * message and an attempt was made to re-associate it with its originally scheduled trip pattern. */ public boolean revertTripToScheduledTripPattern(FeedScopedId tripId, LocalDate serviceDate) { validateNotReadOnly(); boolean success = false; final TripPattern pattern = getNewTripPatternForModifiedTrip(tripId, serviceDate); if (pattern != null) { // Dissociate the given trip from any realtime-added pattern. // The trip will then fall back to its original scheduled pattern. realTimeNewTripPatternsForModifiedTrips.remove(new TripIdAndServiceDate(tripId, serviceDate)); // Remove times for the trip from any timetables // under that now-obsolete realtime-added pattern. SortedSet sortedTimetables = this.timetables.get(pattern); if (sortedTimetables != null) { TripTimes tripTimesToRemove = null; for (Timetable timetable : sortedTimetables) { if (timetable.isValidFor(serviceDate)) { final TripTimes tripTimes = timetable.getTripTimes(tripId); if (tripTimes == null) { LOG.debug("No triptimes to remove for trip {}", tripId); } else if (tripTimesToRemove != null) { LOG.debug("Found two triptimes to remove for trip {}", tripId); } else { tripTimesToRemove = tripTimes; } } } if (tripTimesToRemove != null) { for (Timetable originalTimetable : sortedTimetables) { if (originalTimetable.getTripTimes().contains(tripTimesToRemove)) { Timetable updatedTimetable = originalTimetable .copyOf() .removeTripTimes(tripTimesToRemove) .build(); swapTimetable(pattern, originalTimetable, updatedTimetable); } } } } success = true; } return success; } /** * Removes all Timetables which are valid for a ServiceDate on-or-before the one supplied. * * @return true if any data has been modified and false if no purging has happened. */ public boolean purgeExpiredData(LocalDate serviceDate) { validateNotReadOnly(); boolean modified = false; for (Iterator it = timetables.keySet().iterator(); it.hasNext();) { TripPattern pattern = it.next(); SortedSet sortedTimetables = timetables.get(pattern); SortedSet toKeepTimetables = new TreeSet<>(new SortedTimetableComparator()); for (Timetable timetable : sortedTimetables) { if (serviceDate.isBefore(timetable.getServiceDate())) { toKeepTimetables.add(timetable); } else { modified = true; } } if (toKeepTimetables.isEmpty()) { it.remove(); } else { timetables.put(pattern, ImmutableSortedSet.copyOfSorted(toKeepTimetables)); } } // Also remove last added trip pattern for days that are purged for ( Iterator> iterator = realTimeNewTripPatternsForModifiedTrips.entrySet().iterator(); iterator.hasNext(); ) { TripIdAndServiceDate tripIdAndServiceDate = iterator.next().getKey(); if (!serviceDate.isBefore(tripIdAndServiceDate.serviceDate())) { iterator.remove(); modified = true; } } return modified; } public boolean isDirty() { if (readOnly) { return false; } return dirty; } public String toString() { String d = readOnly ? "committed" : String.format("%d dirty", dirtyTimetables.size()); return String.format("Timetable snapshot: %d timetables (%s)", timetables.size(), d); } public Collection getPatternsForStop(StopLocation stop) { return patternsForStop.get(stop); } /** * Does this snapshot contain any realtime data or is it completely empty? */ public boolean isEmpty() { return ( dirtyTimetables.isEmpty() && timetables.isEmpty() && realTimeNewTripPatternsForModifiedTrips.isEmpty() ); } /** * Clear timetable for all patterns matching the provided feed id. * * @param feedId feed id to clear out * @return true if the timetable changed as a result of the call */ private boolean clearTimetables(String feedId) { return timetables.keySet().removeIf(tripPattern -> feedId.equals(tripPattern.getFeedId())); } /** * Clear new trip patterns for modified trips matching the provided feed id. * * @param feedId feed id to clear out * @return true if the newTripPatternForModifiedTrip changed as a result of the call */ private boolean clearNewTripPatternsForModifiedTrips(String feedId) { return realTimeNewTripPatternsForModifiedTrips .keySet() .removeIf(tripIdAndServiceDate -> feedId.equals(tripIdAndServiceDate.tripId().getFeedId())); } /** * Clear all realtime added routes, trip patterns and trips matching the provided feed id. * * */ private boolean clearEntriesForRealtimeAddedTrips(String feedId) { // it is sufficient to test for the removal of added trips, since other indexed entities are // added only if a new trip is added. boolean removedEntry = realTimeAddedTrips .keySet() .removeIf(id -> feedId.equals(id.getFeedId())); realTimeAddedPatternForTrip.keySet().removeIf(trip -> feedId.equals(trip.getId().getFeedId())); realTimeAddedTripOnServiceDateForTripAndDay .keySet() .removeIf(tripOnServiceDate -> feedId.equals(tripOnServiceDate.tripId().getFeedId())); realTimeAddedTripOnServiceDateById.keySet().removeIf(id -> feedId.equals(id.getFeedId())); realTimeAddedPatternsForRoute .keySet() .removeIf(route -> feedId.equals(route.getId().getFeedId())); realtimeAddedRoutes.keySet().removeIf(id -> feedId.equals(id.getFeedId())); return removedEntry; } /** * Add the patterns to the stop index, only if they come from a modified pattern */ private void addPatternToIndex(TripPattern tripPattern) { if (tripPattern.isCreatedByRealtimeUpdater()) { //TODO - SIRI: Add pattern to index? for (var stop : tripPattern.getStops()) { patternsForStop.put(stop, tripPattern); } } } /** * Replace the original Timetable by the updated one in the timetable index. * The SortedSet that holds the collection of Timetables for that pattern * (sorted by service date) is shared between multiple snapshots and must be copied as well.
* Note on performance: if multiple Timetables are modified in a SortedSet, the SortedSet will be * copied multiple times. The impact on memory/garbage collection is assumed to be minimal * since the collection is small. * The SortedSet is made immutable to prevent change after snapshot publication. */ private void swapTimetable(TripPattern pattern, Timetable original, Timetable updated) { SortedSet sortedTimetables = timetables.get(pattern); if (sortedTimetables == null) { sortedTimetables = new TreeSet<>(new SortedTimetableComparator()); } else { SortedSet temp = new TreeSet<>(new SortedTimetableComparator()); temp.addAll(sortedTimetables); sortedTimetables = temp; } // This is a minor optimization: // Since sortedTimetables contains only timetables created in real-time, no need to try to // remove the original if it was not created by real-time. if (original.isCreatedByRealTimeUpdater()) { sortedTimetables.remove(original); } sortedTimetables.add(updated); timetables.put(pattern, ImmutableSortedSet.copyOfSorted(sortedTimetables)); // if the timetable was already modified by a previous real-time update in the same snapshot // and for the same service date, // then the previously updated timetable is superseded by the new one dirtyTimetables.put(new TripPatternAndServiceDate(pattern, updated.getServiceDate()), updated); dirty = true; } private void validateNotReadOnly() { if (readOnly) { throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); } } private TripOnServiceDate mapToTripOnServiceDate(TripTimes tripTimes, Timetable timetable) { return TripOnServiceDate.of(tripTimes.getTrip().getId()) .withServiceDate(timetable.getServiceDate()) .withTrip(tripTimes.getTrip()) .build(); } /** * Find trips from timetables based on filter criteria. * * @param filter used to filter {@link TripTimes}. */ private List findTripsOnServiceDates(Predicate filter) { return timetables .values() .stream() .flatMap(timetables -> timetables .stream() .flatMap(timetable -> timetable .getTripTimes() .stream() .filter(filter) .map(tripTimes -> mapToTripOnServiceDate(tripTimes, timetable)) ) ) .collect(Collectors.toCollection(ArrayList::new)); } protected static class SortedTimetableComparator implements Comparator { @Override public int compare(Timetable t1, Timetable t2) { return t1.getServiceDate().compareTo(t2.getServiceDate()); } } /** * A pair made of a TripPattern and one of the service dates it is running on. */ private record TripPatternAndServiceDate(TripPattern tripPattern, LocalDate serviceDate) {} }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy