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

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

package org.opentripplanner.model;

import com.beust.jcommander.internal.Lists;
import com.google.transit.realtime.GtfsRealtime.TripDescriptor;
import com.google.transit.realtime.GtfsRealtime.TripUpdate;
import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent;
import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.opentripplanner.model.calendar.ServiceDate;
import org.opentripplanner.routing.core.ServiceDay;
import org.opentripplanner.routing.trippattern.FrequencyEntry;
import org.opentripplanner.routing.trippattern.TripTimes;
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.
 *
 *  TODO OTP2 - Move this to package: org.opentripplanner.model
 *            - after as Entur NeTEx PRs are merged.
 *            - Also consider moving its dependencies in: org.opentripplanner.routing
 *            - The NEW Timetable should not have any dependencies to
 */
public class Timetable implements Serializable {

    private static final Logger LOG = LoggerFactory.getLogger(Timetable.class);
    private static final long serialVersionUID = 1L;

    private final TripPattern pattern;

    private final List tripTimes = Lists.newArrayList();

    private final List frequencyEntries = Lists.newArrayList();

    private final ServiceDate serviceDate;

    /** 
     * Helps determine whether a particular pattern is worth searching for departures at a given time. 
     */
    private transient int minTime, maxTime;
    
    /** Construct an empty Timetable. */
    public Timetable(TripPattern pattern) {
        this.pattern = pattern;
        this.serviceDate = null;
    }

    /**
     * Copy constructor: create an un-indexed Timetable with the same TripTimes as the specified timetable.
     */
    Timetable (Timetable tt, ServiceDate serviceDate) {
        tripTimes.addAll(tt.tripTimes);
        this.serviceDate = serviceDate;
        this.pattern = tt.pattern;
    }

    /**
     * Before performing the relatively expensive iteration over all the trips in this pattern, check whether it's even
     * possible to board any of them given the time at which we are searching, and whether it's possible that any of
     * them could improve on the best known time. This is only an optimization, but a significant one. When we search
     * for departures, we look at three separate days: yesterday, today, and tomorrow. Many patterns do not have
     * service at all hours of the day or past midnight. This optimization can cut the search time for each pattern
     * by 66 to 100 percent.
     *
     * @param bestWait -1 means there is not yet any best known time.
     */
    public boolean temporallyViable(ServiceDay sd, long searchTime, int bestWait, boolean boarding) {
        // Check whether any services are running at all on this pattern.
        if ( ! sd.anyServiceRunning(this.pattern.getServices())) { return false; }
        // Make the search time relative to the given service day.
        searchTime = sd.secondsSinceMidnight(searchTime);
        // Check whether any trip can be boarded at all, given the search time
        if (boarding ? (searchTime > this.maxTime) : (searchTime < this.minTime)) { return false; }
        // Check whether any trip can improve on the best time yet found
        if (bestWait >= 0) {
            long bestTime = boarding ? (searchTime + bestWait) : (searchTime - bestWait);
            if (boarding ? (bestTime < this.minTime) : (bestTime > this.maxTime)) { return false; }
        }
        return true;
    }

    /**
     * Finish off a Timetable 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 nStops = pattern.numberOfStops();

        // Concatenate raw TripTimes and those referenced from FrequencyEntries
        List allTripTimes = Lists.newArrayList(tripTimes);
        for (FrequencyEntry freq : frequencyEntries) allTripTimes.add(freq.tripTimes);

        /* Find the time range over which this timetable is active. Allows departure search optimizations. */
        minTime = Integer.MAX_VALUE;
        maxTime = Integer.MIN_VALUE;
        for (TripTimes tt : tripTimes) {
            minTime = Math.min(minTime, tt.getDepartureTime(0));
            maxTime = Math.max(maxTime, tt.getArrivalTime(nStops - 1));
        }
        // Slightly repetitive code.
        // Again it seems reasonable to have a shared interface between FrequencyEntries and normal TripTimes.
        for (FrequencyEntry freq : frequencyEntries) {
            minTime = Math.min(minTime, freq.getMinDeparture());
            maxTime = Math.max(maxTime, freq.getMaxArrival());
        }
    }

    /** @return the index of TripTimes for this trip ID in this particular Timetable */
    public int getTripIndex(FeedScopedId 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;
    }

    /** @return the index of TripTimes for this trip ID in this particular Timetable, ignoring AgencyIds. */
    public int getTripIndex(String tripId) {
        int ret = 0;
        for (TripTimes tt : tripTimes) {
            if (tt.getTrip().getId().getId().equals(tripId)) { return ret; }
            ret += 1;
        }
        return -1;
    }

    public TripTimes getTripTimes(int tripIndex) {
        return tripTimes.get(tripIndex);
    }

    public TripTimes getTripTimes(Trip trip) {
        for (TripTimes tt : tripTimes) {
            if (tt.getTrip() == trip) { return tt; }
        }
        return null;
    }

    /**
     * Set new trip times for trip given a trip index
     * 
     * @param tripIndex trip index of trip
     * @param tt new trip times for trip
     * @return old trip times of trip
     */
    public TripTimes setTripTimes(int tripIndex, TripTimes tt) {
        return tripTimes.set(tripIndex, tt);
    }

    /**
     * Apply the TripUpdate to the appropriate TripTimes 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 {@link TimetableSnapshot} 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. We
     * assume here that all trips in a timetable are from the same feed, which should always be the
     * case.
     *
     * @param tripUpdate GTFS-RT trip update
     * @param timeZone time zone of trip update
     * @param updateServiceDate service date of trip update
     * @return new copy of updated TripTimes after TripUpdate has been applied on TripTimes of trip
     *         with the id specified in the trip descriptor of the TripUpdate; null if something
     *         went wrong
     *
     * TODO OTP2 - This method depend on GTFS RealTime classes. Refactor this so GTFS RT can do
     *           - its job without sending in GTFS specific classes. A generic update would support
     *           - other RealTime updats, not just from GTFS.
     */
    public TripTimes createUpdatedTripTimes(TripUpdate tripUpdate, TimeZone timeZone, ServiceDate updateServiceDate) {
        if (tripUpdate == null) {
            LOG.error("A null TripUpdate pointer was passed to the Timetable class update method.");
            return null;
        }
        
        // 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 updates on top of *scheduled* times
        if (!tripUpdate.hasTrip()) {
            LOG.error("TripUpdate object has no TripDescriptor field.");
            return null;
        }

        TripDescriptor tripDescriptor = tripUpdate.getTrip();
        if (!tripDescriptor.hasTripId()) {
            LOG.error("TripDescriptor object has no TripId field");
            return null;
        }
        String tripId = tripDescriptor.getTripId();
        int tripIndex = getTripIndex(tripId);
        if (tripIndex == -1) {
            LOG.info("tripId {} not found in pattern.", tripId);
            return null;
        } else {
            LOG.trace("tripId {} found at index {} in timetable.", tripId, tripIndex);
        }

        TripTimes newTimes = new TripTimes(getTripTimes(tripIndex));

        if (tripDescriptor.hasScheduleRelationship() && tripDescriptor.getScheduleRelationship()
                == TripDescriptor.ScheduleRelationship.CANCELED) {
            newTimes.cancelTrip();
        } else {
            // The GTFS-RT reference specifies that StopTimeUpdates are sorted by stop_sequence.
            Iterator updates = tripUpdate.getStopTimeUpdateList().iterator();
            if (!updates.hasNext()) {
                LOG.warn("Won't apply zero-length trip update to trip {}.", tripId);
                return null;
            }
            StopTimeUpdate update = updates.next();

            int numStops = newTimes.getNumStops();
            Integer delay = null;

            final long today = updateServiceDate.getAsDate(timeZone).getTime() / 1000;

            for (int i = 0; i < numStops; i++) {
                boolean match = false;
                if (update != null) {
                    if (update.hasStopSequence()) {
                        match = update.getStopSequence() == newTimes.getOriginalGtfsStopSequence(i);
                    } else if (update.hasStopId()) {
                        match = pattern.getStop(i).getId().getId().equals(update.getStopId());
                    }
                }

                if (match) {
                    StopTimeUpdate.ScheduleRelationship scheduleRelationship =
                            update.hasScheduleRelationship() ? update.getScheduleRelationship()
                            : StopTimeUpdate.ScheduleRelationship.SCHEDULED;
                    if (scheduleRelationship == StopTimeUpdate.ScheduleRelationship.SKIPPED) {
                        LOG.warn("Partially canceled trips are unsupported by this method." +
                                " Skipping TripUpdate.");
                        return null;
                    } else if (scheduleRelationship ==
                            StopTimeUpdate.ScheduleRelationship.NO_DATA) {
                        newTimes.updateArrivalDelay(i, 0);
                        newTimes.updateDepartureDelay(i, 0);
                        delay = 0;
                    } else {
                        if (update.hasArrival()) {
                            StopTimeEvent arrival = update.getArrival();
                            if (arrival.hasDelay()) {
                                delay = arrival.getDelay();
                                if (arrival.hasTime()) {
                                    newTimes.updateArrivalTime(i,
                                            (int) (arrival.getTime() - today));
                                } else {
                                    newTimes.updateArrivalDelay(i, delay);
                                }
                            } else if (arrival.hasTime()) {
                                newTimes.updateArrivalTime(i,
                                        (int) (arrival.getTime() - today));
                                delay = newTimes.getArrivalDelay(i);
                            } else {
                                LOG.error("Arrival time at index {} is erroneous.", i);
                                return null;
                            }
                        } else {
                            if (delay == null) {
                                // newTimes.cancelDropOffForStop(i); TODO This needs to cancel on a StopTime object so that a new StopPattern/TripPattern can be constructed
                            } else {
                                newTimes.updateArrivalDelay(i, delay);
                            }
                        }

                        if (update.hasDeparture()) {
                            StopTimeEvent departure = update.getDeparture();
                            if (departure.hasDelay()) {
                                delay = departure.getDelay();
                                if (departure.hasTime()) {
                                    newTimes.updateDepartureTime(i,
                                            (int) (departure.getTime() - today));
                                } else {
                                    newTimes.updateDepartureDelay(i, delay);
                                }
                            } else if (departure.hasTime()) {
                                newTimes.updateDepartureTime(i,
                                        (int) (departure.getTime() - today));
                                delay = newTimes.getDepartureDelay(i);
                            } else {
                                LOG.error("Departure time at index {} is erroneous.", i);
                                return null;
                            }
                        } else {
                            if (delay == null) {
                                //newTimes.cancelPickupForStop(i); TODO This needs to cancel on a StopTime object so that a new StopPattern/TripPattern can be constructed
                            } else {
                                newTimes.updateDepartureDelay(i, delay);
                            }
                        }
                    }

                    if (updates.hasNext()) {
                        update = updates.next();
                    } else {
                        update = null;
                    }
                } else {
                    if (delay == null) {
                        // newTimes.cancelDropOffForStop(i); TODO This needs to cancel on a StopTime object so that a new StopPattern/TripPattern can be constructed
                        // newTimes.cancelPickupForStop(i); TODO This needs to cancel on a StopTime object so that a new StopPattern/TripPattern can be constructed
                    } else {
                        newTimes.updateArrivalDelay(i, delay);
                        newTimes.updateDepartureDelay(i, delay);
                    }
                }
            }
            if (update != null) {
                LOG.error("Part of a TripUpdate object could not be applied successfully to trip {}.", tripId);
                return null;
            }
        }
        if (!newTimes.timesIncreasing()) {
            LOG.error("TripTimes are non-increasing after applying GTFS-RT delay propagation to trip {}.", tripId);
            return null;
        }

        LOG.debug("A valid TripUpdate object was applied to trip {} using the Timetable class update method.", tripId);
        return newTimes;
    }

    /**
     * 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.
     * Note that the trip is not added to the enclosing pattern here, but in the pattern's wrapper function.
     * Here we don't know if it's a scheduled trip or a realtime-added trip.
     */
    public void addTripTimes(TripTimes tt) {
        tripTimes.add(tt);
    }

    /**
     * Add a frequency entry to this Timetable. See addTripTimes method. Maybe Frequency Entries should
     * just be TripTimes for simplicity.
     */
    public void addFrequencyEntry(FrequencyEntry freq) {
        frequencyEntries.add(freq);
    }

    public boolean isValidFor(ServiceDate serviceDate) {
        return this.serviceDate == null || this.serviceDate.equals(serviceDate);
    }
    
    /** Find and cache service codes. Duplicates information in trip.getServiceId for optimization. */
    // TODO maybe put this is a more appropriate place
    public void setServiceCodes (Map serviceCodes) {
        for (TripTimes tt : this.tripTimes) {
            tt.setServiceCode(serviceCodes.get(tt.getTrip().getServiceId()));
        }
        // Repeated code... bad sign...
        for (FrequencyEntry freq : this.frequencyEntries) {
            TripTimes tt = freq.tripTimes;
            tt.setServiceCode(serviceCodes.get(tt.getTrip().getServiceId()));
        }
    }

    /**
     * A circular reference between TripPatterns and their scheduled (non-updated) timetables.
     */
    public TripPattern getPattern() {
        return pattern;
    }

    /**
     * Contains one TripTimes object for each scheduled trip (even cancelled ones) and possibly
     * additional TripTimes objects for unscheduled trips. Frequency entries are stored separately.
     */
    public List getTripTimes() {
        return tripTimes;
    }

    /**
     * Contains one FrequencyEntry object for each block of frequency-based trips.
     */
    public List getFrequencyEntries() {
        return frequencyEntries;
    }

    /**
     * The ServiceDate for which this (updated) timetable is valid. If null, then it is valid for all dates.
     */
    public ServiceDate getServiceDate() {
        return serviceDate;
    }


    /**
     * The direction for all the trips in this pattern.
     */
    public Direction getDirection() {
        if (!tripTimes.isEmpty()) {
            return tripTimes.get(0).getTrip().getDirection();
        } else if (!frequencyEntries.isEmpty()) {
            return frequencyEntries.get(0).tripTimes.getTrip().getDirection();
        } else {
            return Direction.UNKNOWN;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy