Package org.opentripplanner.routing.edgetype

Source Code of org.opentripplanner.routing.edgetype.Timetable

/* 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.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import com.beust.jcommander.internal.Lists;

import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.opentripplanner.common.MavenVersion;
import org.opentripplanner.routing.core.ServiceDay;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.StopTransfer;
import org.opentripplanner.routing.core.TransferTable;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.trippattern.FrequencyEntry;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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;


/**
* 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.
*/
public class Timetable implements Serializable {

    private static final Logger LOG = LoggerFactory.getLogger(Timetable.class);
    private static final long serialVersionUID = MavenVersion.VERSION.getUID();

    /**
     * A circular reference between TripPatterns and their scheduled (non-updated) timetables.
     */
    public final TripPattern 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 final List<TripTimes> tripTimes = Lists.newArrayList();

    /**
     * Contains one FrequencyEntry object for each block of frequency-based trips.
     */
    public final List<FrequencyEntry> frequencyEntries = Lists.newArrayList();

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

    /**
     * For each hop, the best running time. This serves to provide lower bounds on traversal time.
     */
    private transient int minRunningTimes[];

    /**
     * For each stop, the best dwell time. This serves to provide lower bounds on traversal time.
     */
    private transient int minDwellTimes[];

    /**
     * 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.services)) 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;
    }

    /**
     * Get the next (previous) trip that departs (arrives) from the specified stop at or after
     * (before) the specified time.
     * @return the TripTimes object representing the (possibly updated) best trip, or null if no
     * trip matches both the time and other criteria.
     */
    public TripTimes getNextTrip(State s0, ServiceDay serviceDay, int stopIndex, boolean boarding) {
        /* Search at the state's time, but relative to midnight on the given service day. */
        int time = serviceDay.secondsSinceMidnight(s0.getTimeSeconds());
        // NOTE the time is sometimes negative here. That is fine, we search for the first trip of the day.
        TripTimes bestTrip = null;
        Stop currentStop = pattern.getStop(stopIndex);
        // Linear search through the timetable looking for the best departure.
        // We no longer use a binary search on Timetables because:
        // 1. we allow combining trips from different service IDs on the same tripPattern.
        // 2. We mix frequency-based and one-off TripTimes together on tripPatterns.
        // 3. Stoptimes may change with realtime updates, and we cannot count on them being sorted.
        //    The complexity of keeping sorted indexes up to date does not appear to be worth the
        //    apparently minor speed improvement.
        int bestTime = boarding ? Integer.MAX_VALUE : Integer.MIN_VALUE;
        // Hoping JVM JIT will distribute the loop over the if clauses as needed.
        // We could invert this and skip some service days based on schedule overlap as in RRRR.
        for (TripTimes tt : tripTimes) {
            if ( ! serviceDay.serviceRunning(tt.serviceCode)) continue; // TODO merge into call on next line
            if ( ! tt.tripAcceptable(s0, stopIndex)) continue;
            int adjustedTime = adjustTimeForTransfer(s0, currentStop, tt.trip, boarding, serviceDay, time);
            if (adjustedTime == -1) continue;
            if (boarding) {
                int depTime = tt.getDepartureTime(stopIndex);
                if (depTime < 0) continue;
                if (depTime >= adjustedTime && depTime < bestTime) {
                    bestTrip = tt;
                    bestTime = depTime;
                }
            } else {
                int arvTime = tt.getArrivalTime(stopIndex);
                if (arvTime < 0) continue;
                if (arvTime <= adjustedTime && arvTime > bestTime) {
                    bestTrip = tt;
                    bestTime = arvTime;
                }
            }
        }
        // ACK all logic is identical to above.
        // A sign that FrequencyEntries and TripTimes need a common interface.
        FrequencyEntry bestFreq = null;
        for (FrequencyEntry freq : frequencyEntries) {
            TripTimes tt = freq.tripTimes;
            if ( ! serviceDay.serviceRunning(tt.serviceCode)) continue; // TODO merge into call on next line
            if ( ! tt.tripAcceptable(s0, stopIndex)) continue;
            int adjustedTime = adjustTimeForTransfer(s0, currentStop, tt.trip, boarding, serviceDay, time);
            if (adjustedTime == -1) continue;
            LOG.debug("  running freq {}", freq);
            if (boarding) {
                int depTime = freq.nextDepartureTime(stopIndex, adjustedTime); // min transfer time included in search
                if (depTime < 0) continue;
                if (depTime >= adjustedTime && depTime < bestTime) {
                    bestFreq = freq;
                    bestTime = depTime;
                }
            } else {
                int arvTime = freq.prevArrivalTime(stopIndex, adjustedTime); // min transfer time included in search
                if (arvTime < 0) continue;
                if (arvTime <= adjustedTime && arvTime > bestTime) {
                    bestFreq = freq;
                    bestTime = arvTime;
                }
            }
        }
        if (bestFreq != null) {
            // A FrequencyEntry beat all the TripTimes.
            // Materialize that FrequencyEntry entry at the given time.
            bestTrip = bestFreq.tripTimes.timeShift(stopIndex, bestTime, boarding);
        }
        return bestTrip;
    }

    /**
     * Check transfer table rules. Given the last alight time from the State,
     * return the boarding time t0 adjusted for this particular trip's minimum transfer time,
     * or -1 if boarding this trip is not allowed.
     * FIXME adjustedTime can legitimately be -1! But negative times might as well be zero.
     */
    private int adjustTimeForTransfer(State state, Stop currentStop, Trip trip, boolean boarding, ServiceDay serviceDay, int t0) {
        if ( ! state.isEverBoarded()) {
            // This is the first boarding not a transfer.
            return t0;
        }
        TransferTable transferTable = state.getOptions().getRoutingContext().transferTable;
        int transferTime = transferTable.getTransferTime(state.getPreviousStop(), currentStop, state.getPreviousTrip(), trip, boarding);
        // Check whether back edge is TimedTransferEdge
        if (state.getBackEdge() instanceof TimedTransferEdge) {
            // Transfer must be of type TIMED_TRANSFER
            if (transferTime != StopTransfer.TIMED_TRANSFER) {
                return -1;
            }
        }
        if (transferTime == StopTransfer.UNKNOWN_TRANSFER) {
            return t0; // no special rules, just board
        }
        if (transferTime == StopTransfer.FORBIDDEN_TRANSFER) {
            // This transfer is forbidden
            return -1;
        }
        // There is a minimum transfer time to make this transfer. Ensure that it is respected.
        int minTime = serviceDay.secondsSinceMidnight(state.getLastAlightedTimeSeconds());
        if (boarding) {
            minTime += transferTime;
            if (minTime > t0) return minTime;
        } else {
            minTime -= transferTime;
            if (minTime < t0) return minTime;
        }
        return t0;
    }

    /**
     * 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.stopPattern.size;
        int nHops = nStops - 1;
        /* Find lower bounds on dwell and running times at each stop. */
        minDwellTimes = new int[nHops];
        minRunningTimes = new int[nHops];
        Arrays.fill(minDwellTimes, Integer.MAX_VALUE);
        Arrays.fill(minRunningTimes, Integer.MAX_VALUE);
        // Concatenate raw TripTimes and those referenced from FrequencyEntries
        List<TripTimes> allTripTimes = Lists.newArrayList(tripTimes);
        for (FrequencyEntry freq : frequencyEntries) allTripTimes.add(freq.tripTimes);
        for (TripTimes tt : allTripTimes) {
            for (int h = 0; h < nHops; ++h) {
                int dt = tt.getDwellTime(h);
                if (minDwellTimes[h] > dt) {
                    minDwellTimes[h] = dt;
                }
                int rt = tt.getRunningTime(h);
                if (minRunningTimes[h] > rt) {
                    minRunningTimes[h] = rt;
                }
            }
        }
        /* 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(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.trip.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.trip.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.trip == trip) return tt;
        }
        return null;
    }

    /**
     * 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 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.
     * We assume here that all trips in a timetable are from the same feed, which should always be the case.
     *
     * @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(TripUpdate tripUpdate, TimeZone timeZone, ServiceDate updateServiceDate) {
        if (tripUpdate == null) {
            LOG.error("A null TripUpdate pointer was passed to the Timetable class update method.");
            return false;
        } else 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 updates on top of *scheduled* times
            if (!tripUpdate.hasTrip()) {
                LOG.error("TripUpdate object has no TripDescriptor field.");
                return false;
            }

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

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

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

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

                for (int i = 0; i < numStops; i++) {
                    boolean match = false;
                    if (update != null) {
                        if (update.hasStopSequence()) {
                            match = update.getStopSequence() == newTimes.getStopSequence(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) {
                            // TODO: Handle partial trip cancellations
                            LOG.warn("Partially canceled trips are currently unsupported." +
                                    " Skipping TripUpdate.");
                            return false;
                        } else if (scheduleRelationship ==
                                StopTimeUpdate.ScheduleRelationship.NO_DATA) {
                            newTimes.updateArrivalDelay(i, 0);
                            newTimes.updateDepartureDelay(i, 0);
                            delay = 0;
                        } else {
                            long today = updateServiceDate.getAsDate(timeZone).getTime() / 1000;

                            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 false;
                                }
                            } else {
                                if (delay == null) {
                                    newTimes.updateArrivalTime(i, TripTimes.UNAVAILABLE);
                                } 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 false;
                                }
                            } else {
                                if (delay == null) {
                                    newTimes.updateDepartureTime(i, TripTimes.UNAVAILABLE);
                                } else {
                                    newTimes.updateDepartureDelay(i, delay);
                                }
                            }
                        }

                        if (updates.hasNext()) {
                            update = updates.next();
                        } else {
                            update = null;
                        }
                    } else {
                        if (delay == null) {
                            newTimes.updateArrivalTime(i, TripTimes.UNAVAILABLE);
                            newTimes.updateDepartureTime(i, TripTimes.UNAVAILABLE);
                        } else {
                            newTimes.updateArrivalDelay(i, delay);
                            newTimes.updateDepartureDelay(i, delay);
                        }
                    }
                }
                if (update != null) {
                    LOG.error("Part of a TripUpdate object could not be applied successfully.");
                    return false;
                }
            }
            if (!newTimes.timesIncreasing()) {
                LOG.error("TripTimes are non-increasing after applying GTFS-RT delay propagation.");
                return false;
            }

            // Update succeeded, save the new TripTimes back into this Timetable.
            tripTimes.set(tripIndex, newTimes);
        } catch (Exception e) { // prevent server from dying while debugging
            e.printStackTrace();
            return false;
        }

        LOG.debug("A valid TripUpdate object was applied using the Timetable class update method.");
        return true;
    }

    /**
     * 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);
    }

    /**
     * Check that all dwell times at the given stop are zero, which allows removing the dwell edge.
     * TODO we should probably just eliminate dwell-deletion. It won't be important if we get rid of transit edges.
     */
    boolean allDwellsZero(int hopIndex) {
        for (TripTimes tt : tripTimes) {
            if (tt.getDwellTime(hopIndex) != 0) {
                return false;
            }
        }
        return true;
    }

    /** Returns the shortest possible running time for this stop */
    public int getBestRunningTime(int stopIndex) {
        return minRunningTimes[stopIndex];
    }

    /** Returns the shortest possible dwell time at this stop */
    public int getBestDwellTime(int stopIndex) {
        if (minDwellTimes == null) {
            return 0;
        }
        return minDwellTimes[stopIndex];
    }

    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<AgencyAndId, Integer> serviceCodes) {
        for (TripTimes tt : this.tripTimes) {
            tt.serviceCode = serviceCodes.get(tt.trip.getServiceId());
        }
        // Repeated code... bad sign...
        for (FrequencyEntry freq : this.frequencyEntries) {
            TripTimes tt = freq.tripTimes;
            tt.serviceCode = serviceCodes.get(tt.trip.getServiceId());
        }
    }

TOP

Related Classes of org.opentripplanner.routing.edgetype.Timetable

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.