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

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

package org.opentripplanner.model;

import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.opentripplanner.model.calendar.ServiceDate;
import org.opentripplanner.routing.algorithm.raptor.transit.mappers.TransitLayerUpdater;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

// this is only currently in edgetype because that's where Trippattern is.
// move these classes elsewhere.

/**
 * Part of concurrency control for stoptime updates.
 *
 * All updates should be performed on a snapshot 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.
 *
 * At this point, only one writing thread at a time is supported.
 *
 *  TODO OTP2 - Move this to package: org.opentripplanner.model
 *            - after ass Entur NeTEx PRs are merged.
 */
public class TimetableSnapshot {

    protected static class SortedTimetableComparator implements Comparator {
        @Override
        public int compare(Timetable t1, Timetable t2) {
            return t1.serviceDate.compareTo(t2.serviceDate);
        }
    }
    
    /**
     * Class to use as key in HashMap containing feed id, trip id and service date
     * TODO shouldn't this be a static class?
     */
    protected class TripIdAndServiceDate {
        private final FeedScopedId tripId;
        private final ServiceDate serviceDate;
        
        public TripIdAndServiceDate(final FeedScopedId tripId, final ServiceDate serviceDate) {
            this.tripId = tripId;
            this.serviceDate = serviceDate;
        }

        public FeedScopedId getTripId() {
            return tripId;
        }

        public ServiceDate getServiceDate() {
            return serviceDate;
        }


        @Override
        public int hashCode() {
            int result = Objects.hash(tripId, serviceDate);
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            TripIdAndServiceDate other = (TripIdAndServiceDate) obj;
            boolean result = Objects.equals(this.tripId, other.tripId) &&
                    Objects.equals(this.serviceDate, other.serviceDate);
            return result;
        }
    }

    private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshot.class);
    
    /**
     * The timetables for different days, for each TripPattern (each sequence of stops on a particular Route) for which
     * we have an updated Timetable. The keys include both TripPatterns from the scheduled GTFS, and TripPatterns added
     * by realtime messages and tracked by the TripPatternCache. Note that the keys will not include all scheduled
     * TripPatterns, only those for which we've got an update.
     * We use a HashMap rather than a Map so we can clone it. If this turns out to be slow/spacious we can use an array
     * with integer pattern indexes. The SortedSet members are copy-on-write.
     * FIXME: this could be made into a flat hashtable with compound keys.
     */
    private HashMap> timetables = new HashMap();

    /**
     * 

* Map containing the last added trip pattern given a trip id (without agency) and a * service date as a result of a call to {@link #update(TripPattern, TripTimes, ServiceDate)} * with trip times of a trip that didn't exist yet in the trip pattern. *

*

* This is a HashMap and not a Map so the clone function is available. *

* TODO clarify what it means to say "last" added trip pattern. There can be more than one? What happens to the older ones? */ private HashMap lastAddedTripPattern = new HashMap<>(); /** * This maps contains all of the new or updated TripPatterns added by realtime data indexed on * stop. This has to be kept in order for them to be included in the stop times api call on a * specific stop. * * TODO Find a generic way to keep all realtime indexes. */ private Multimap patternsForStop = ArrayListMultimap.create(); /** * 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 boolean readOnly = false; /** * Boolean value indicating that this timetable snapshot contains changes compared to the state * of the last commit if true. */ private boolean dirty = false; /** * A set of all timetables which have been modified and are waiting to be indexed. When * dirty is null, it indicates that the snapshot is read-only. */ private Set dirtyTimetables = new HashSet<>(); /** * 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, ServiceDate 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.scheduledTimetable; } /** * Get the last added trip pattern given a trip id (without agency) and a service date as * a result of a call to {@link #update(TripPattern, TripTimes, ServiceDate)} with trip times of * a trip that didn't exist yet in the trip pattern. * TODO clarify what it means to say "last" added trip pattern. There can be more than one? What happens to the older ones? * * @param tripId trip id * @param serviceDate service date * @return last added trip pattern; null if trip never was added to a trip pattern */ public TripPattern getLastAddedTripPattern(FeedScopedId tripId, ServiceDate serviceDate) { TripIdAndServiceDate tripIdAndServiceDate = new TripIdAndServiceDate(tripId, serviceDate); TripPattern pattern = lastAddedTripPattern.get(tripIdAndServiceDate); return pattern; } /** * Update the trip times of one trip in a timetable of a trip pattern. If the trip of the trip * times does not exist yet in the timetable, add it. * * @param pattern trip pattern * @param updatedTripTimes updated trip times * @param serviceDate service day for which this update is valid * @return whether or not the update was actually applied */ public boolean update(TripPattern pattern, TripTimes updatedTripTimes, ServiceDate serviceDate) { // Preconditions Preconditions.checkNotNull(pattern); Preconditions.checkNotNull(serviceDate); if (readOnly) { throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); } Timetable tt = resolve(pattern, serviceDate); // we need to perform the copy of Timetable here rather than in Timetable.update() // to avoid repeatedly copying in case several updates are applied to the same timetable if ( ! dirtyTimetables.contains(tt)) { Timetable old = tt; tt = new Timetable(tt, serviceDate); 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; } if(old.serviceDate != null) sortedTimetables.remove(old); sortedTimetables.add(tt); timetables.put(pattern, sortedTimetables); dirtyTimetables.add(tt); dirty = true; } // Assume all trips in a pattern are from the same feed, which should be the case. // Find trip index int tripIndex = tt.getTripIndex(updatedTripTimes.trip.getId()); if (tripIndex == -1) { // Trip not found, add it tt.addTripTimes(updatedTripTimes); // Remember this pattern for the added trip id and service date FeedScopedId tripId = updatedTripTimes.trip.getId(); TripIdAndServiceDate tripIdAndServiceDate = new TripIdAndServiceDate(tripId, serviceDate); lastAddedTripPattern.put(tripIdAndServiceDate, pattern); } else { // Set updated trip times of trip tt.setTripTimes(tripIndex, updatedTripTimes); } // To make these trip patterns visible for departureRow searches. addPatternToIndex(pattern); // The time tables are finished during the commit return true; } /** * 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); } @SuppressWarnings("unchecked") public TimetableSnapshot commit(TransitLayerUpdater transitLayerUpdater, boolean force) { if (readOnly) { throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); } TimetableSnapshot ret = new TimetableSnapshot(); if (!force && !this.isDirty()) return null; for (Timetable tt : dirtyTimetables) { tt.finish(); // summarize, index, etc. the new timetables } ret.timetables = (HashMap>) this.timetables.clone(); ret.lastAddedTripPattern = (HashMap) this.lastAddedTripPattern.clone(); if (transitLayerUpdater != null) { transitLayerUpdater.update(dirtyTimetables); } this.dirtyTimetables.clear(); this.dirty = false; ret.setPatternsForStop(HashMultimap.create(this.patternsForStop)); ret.readOnly = true; // mark the snapshot as henceforth immutable return ret; } /** * Clear all data of snapshot for the provided feed id * * @param feedId feed id to clear the snapshop for */ public void clear(String feedId) { if (readOnly) { throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); } // Clear all data from snapshot. boolean timetableWasModified = clearTimetable(feedId); boolean lastAddedWasModified = clearLastAddedTripPattern(feedId); // If this snapshot was modified, it will be dirty after the clear actions. if (timetableWasModified || lastAddedWasModified) { dirty = true; } } /** * 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 */ protected boolean clearTimetable(String feedId) { return timetables.keySet().removeIf(tripPattern -> feedId.equals(tripPattern.getFeedId())); } /** * Clear all last added trip patterns matching the provided feed id. * * @param feedId feed id to clear out * @return true if the lastAddedTripPattern changed as a result of the call */ protected boolean clearLastAddedTripPattern(String feedId) { return lastAddedTripPattern.keySet().removeIf( lastAddedTripPattern -> feedId.equals(lastAddedTripPattern.getTripId().getFeedId()) ); } /** * Removes all Timetables which are valid for a ServiceDate on-or-before the one supplied. */ public boolean purgeExpiredData(ServiceDate serviceDate) { if (readOnly) { throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); } 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.compareTo(timetable.serviceDate) < 0) { toKeepTimetables.add(timetable); } else { modified = true; } } if(toKeepTimetables.isEmpty()) { it.remove(); } else { timetables.put(pattern, toKeepTimetables); } } // Also remove last added trip pattern for days that are purged for (Iterator> iterator = lastAddedTripPattern .entrySet().iterator(); iterator.hasNext();) { TripIdAndServiceDate tripIdAndServiceDate = iterator.next().getKey(); if (serviceDate.compareTo(tripIdAndServiceDate.getServiceDate()) >= 0) { 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); } /** * @return all TripPatterns for which we have any updated timetables created by realtime messages, including both * patterns that were in the scheduled (static) transit data and those that were added to this snapshot by * rerouted or added trips. */ public Collection getAllRealtimeTripPatterns () { return timetables.keySet(); } private void addPatternToIndex(TripPattern tripPattern) { for (Stop stop: tripPattern.getStops()) { patternsForStop.put(stop, tripPattern); } } public Collection getPatternsForStop(Stop stop) { return patternsForStop.get(stop); } public void setPatternsForStop(Multimap patternsForStop) { this.patternsForStop = patternsForStop; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy