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

org.opentripplanner.routing.trippattern.TripTimes Maven / Gradle / Ivy

package org.opentripplanner.routing.trippattern;

import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.List;
import org.opentripplanner.model.BookingInfo;
import org.opentripplanner.model.StopPattern;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.Trip;
import org.opentripplanner.model.TripPattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A TripTimes represents the arrival and departure times for a single trip in an Timetable. It is carried
 * along by States when routing to ensure that they have a consistent, fast view of the trip when
 * realtime updates have been applied. All times are expressed as seconds since midnight (as in GTFS).
 */
public class TripTimes implements Serializable, Comparable {

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

    /**
     * This allows re-using the same scheduled arrival and departure time arrays for many
     * different TripTimes. It is also used in materializing frequency-based TripTimes.
     */
    private int timeShift;

    /** The trips whose arrivals and departures are represented by this TripTimes */
    private final Trip trip;

    // not final because these are set later, after TripTimes construction.
    private int serviceCode = -1;

    /**
     * Both trip_headsign and stop_headsign (per stop on a particular trip) are optional GTFS
     * fields. If the headsigns array is null, we will report the trip_headsign (which may also
     * be null) at every stop on the trip. If all the stop_headsigns are the same as the
     * trip_headsign we may also set the headsigns array to null to save space.
     * Field is private to force use of the getter method which does the necessary fallbacks.
     */
    private final String[] headsigns;

    /**
     * Contains a list of via names for each stop.
     * This field provides info about intermediate stops between current stop and final trip destination.
     * This is 2D array since there can be more than one via name/stop per each record in stop sequence).
     * This is mapped from NeTEx DestinationDisplay.vias. No GTFS mapping at the moment.
     * Outer array may be null if there are no vias in stop sequence. Inner array may be null if
     * there are no vias for particular stop. This is done in order to save space.
     * Field is private to force use of the getter method which does the necessary fallbacks.
     */
    private final String[][] headsignVias;

    /**
     * The time in seconds after midnight at which the vehicle should arrive at each stop according
     * to the original schedule.
     */
    private final int[] scheduledArrivalTimes;

    /**
     * The time in seconds after midnight at which the vehicle should leave each stop according
     * to the original schedule.
     */
    private final int[] scheduledDepartureTimes;

    /**
     * The time in seconds after midnight at which the vehicle arrives at each stop, accounting for
     * any real-time updates. Non-final to allow updates.
     */
    private int[] arrivalTimes;

    /**
     * The time in seconds after midnight at which the vehicle leaves each stop, accounting for
     * any real-time updates. Non-final to allow updates.
     */
    private int[] departureTimes;

    /**
     * Flag to indicate that the stop has been passed without removing arrival/departure-times - i.e. "estimates" are
     * actual times, no longer estimates.
     *
     * This is only for API-purposes. Non-final to allow updates.
     */
    private boolean[] recordedStops;

    /**
     * Whether each stop has been cancelled by realtime updates (in the most recent derived realtime StopPattern).
     * The cancellation information here need not match the information in the StopPattern for this TripTimes.
     * This is somewhat redundant: it duplicates cancellation information found in the {@link StopPattern} of the new
     * {@link TripPattern} created by the realtime update. The use case is displaying cancellation information when
     * this original (non-real-time) pattern is queried, even though this original pattern allows pickup and dropoff
     * at the canceled stop. This is only for API purposes and does not affect routing.
     *
     * // TODO This seems to only be changed after a new TripTimes has been created by the realtime
     * // TODO updater and the scheduled TripTimes is never changed. We should clean this up
     * // TODO when refactoring the realtime model. Preferably so that a reference is kept between
     * // TODO the realtime data and the scheduled data, so that data is not duplicated
     *
     * Non-final to allow updates.
     */
    private boolean[] cancelledStops;

    /**
     * Flag to indicate inaccurate predictions on each stop. Non-final to allow updates.
     */
    private boolean[] predictionInaccurateOnStops;

    private final List dropOffBookingInfos;

    private final List pickupBookingInfos;

    /**
     * These are the GTFS stop sequence numbers, which show the order in which the vehicle visits
     * the stops. Despite the face that the StopPattern or TripPattern enclosing this TripTimes
     * provides an ordered list of Stops, the original stop sequence numbers may still be needed for
     * matching with GTFS-RT update messages. Unfortunately, each individual trip can have totally
     * different sequence numbers for the same stops, so we need to store them at the individual
     * trip level. An effort is made to re-use the sequence number arrays when they are the same
     * across different trips in the same pattern.
     */
    private final int[] originalGtfsStopSequence;

    /**
     * The real-time state of this TripTimes.
     */
    private RealTimeState realTimeState = RealTimeState.SCHEDULED;

    /** A Set of stop indexes that are marked as timepoints in the GTFS input. */
    private final BitSet timepoints;

    private static final String[] EMPTY_STRING_ARRAY = new String[0];

    /**
     * The provided stopTimes are assumed to be pre-filtered, valid, and monotonically increasing.
     * The non-interpolated stoptimes should already be marked at timepoints by a previous filtering step.
     */
    public TripTimes(final Trip trip, final Collection stopTimes, final Deduplicator deduplicator) {
        this.trip = trip;
        final int nStops = stopTimes.size();
        final int[] departures = new int[nStops];
        final int[] arrivals   = new int[nStops];
        final int[] sequences  = new int[nStops];
        final BitSet timepoints = new BitSet(nStops);
        // Times are always shifted to zero. This is essential for frequencies and deduplication.
        this.timeShift = stopTimes.iterator().next().getArrivalTime();
        final List dropOffBookingInfos = new ArrayList<>();
        final List pickupBookingInfos = new ArrayList<>();
        int s = 0;
        for (final StopTime st : stopTimes) {
            departures[s] = st.getDepartureTime() - timeShift;
            arrivals[s] = st.getArrivalTime() - timeShift;
            sequences[s] = st.getStopSequence();
            timepoints.set(s, st.getTimepoint() == 1);

            dropOffBookingInfos.add(st.getDropOffBookingInfo());
            pickupBookingInfos.add(st.getPickupBookingInfo());
            s++;
        }
        this.scheduledDepartureTimes = deduplicator.deduplicateIntArray(departures);
        this.scheduledArrivalTimes = deduplicator.deduplicateIntArray(arrivals);
        this.originalGtfsStopSequence = deduplicator.deduplicateIntArray(sequences);
        this.headsigns = deduplicator.deduplicateStringArray(makeHeadsignsArray(stopTimes));
        this.headsignVias = deduplicator.deduplicateString2DArray(makeHeadsignViasArray(stopTimes));

        this.dropOffBookingInfos = deduplicator.deduplicateImmutableList(BookingInfo.class, dropOffBookingInfos);
        this.pickupBookingInfos = deduplicator.deduplicateImmutableList(BookingInfo.class, pickupBookingInfos);
        // We set these to null to indicate that this is a non-updated/scheduled TripTimes.
        // We cannot point to the scheduled times because they are shifted, and updated times are not.
        this.arrivalTimes = null;
        this.departureTimes = null;
        this.recordedStops = null;
        this.cancelledStops = null;
        this.timepoints = deduplicator.deduplicateBitSet(timepoints);
        LOG.trace("trip {} has timepoint at indexes {}", trip, timepoints);
    }

    /** This copy constructor does not copy the actual times, only the scheduled times. */
    public TripTimes(final TripTimes object) {
        this.timeShift = object.timeShift;
        this.trip = object.trip;
        this.serviceCode = object.serviceCode;
        this.headsigns = object.headsigns;
        this.headsignVias = object.headsignVias;
        this.scheduledArrivalTimes = object.scheduledArrivalTimes;
        this.scheduledDepartureTimes = object.scheduledDepartureTimes;
        this.arrivalTimes = null;
        this.departureTimes = null;
        this.recordedStops = object.recordedStops;
        this.predictionInaccurateOnStops = object.predictionInaccurateOnStops;
        this.pickupBookingInfos = object.pickupBookingInfos;
        this.dropOffBookingInfos = object.dropOffBookingInfos;
        this.originalGtfsStopSequence = object.originalGtfsStopSequence;
        this.realTimeState = object.realTimeState;
        this.timepoints = object.timepoints;
    }

    public void setServiceCode(int serviceCode) {
        this.serviceCode = serviceCode;
    }

    /**
     * @return either an array of headsigns (one for each stop on this trip) or null if the
     * headsign is the same at all stops (including null) and can be found in the Trip object.
     */
    private String[] makeHeadsignsArray(final Collection stopTimes) {
        final String tripHeadsign = trip.getTripHeadsign();
        boolean useStopHeadsigns = false;
        if (tripHeadsign == null) {
            useStopHeadsigns = true;
        } else {
            for (final StopTime st : stopTimes) {
                if ( ! (tripHeadsign.equals(st.getStopHeadsign()))) {
                    useStopHeadsigns = true;
                    break;
                }
            }
        }
        if (!useStopHeadsigns) {
            return null; //defer to trip_headsign
        }
        boolean allNull = true;
        int i = 0;
        final String[] hs = new String[stopTimes.size()];
        for (final StopTime st : stopTimes) {
            final String headsign = st.getStopHeadsign();
            hs[i++] = headsign;
            if (headsign != null) allNull = false;
        }
        if (allNull) {
            return null;
        } else {
            return hs;
        }
    }


    /**
     * Create 2D String array for via names for each stop in sequence.
     * @return May be null if no vias are present in stop sequence.
     */
    private String[][] makeHeadsignViasArray(final Collection stopTimes) {

        if (stopTimes.stream().allMatch(st -> st.getHeadsignVias() == null || st.getHeadsignVias().isEmpty())) {
            return null;
        }

        String[][] vias = new String[stopTimes.size()][];

        int i = 0;
        for (final StopTime st : stopTimes) {
            if (st.getHeadsignVias() == null) {
                vias[i] = EMPTY_STRING_ARRAY;
                continue;
            }

            vias[i] = st.getHeadsignVias().toArray(EMPTY_STRING_ARRAY);
            i++;
        }

        return vias;
    }

    /**
     * Trips may also have null headsigns, in which case we should fall back on a Timetable or
     * Pattern-level headsign. Such a string will be available when we give TripPatterns or
     * StopPatterns unique human readable route variant names, but a TripTimes currently does not
     * have a pointer to its enclosing timetable or pattern.
     */
    public String getHeadsign(final int stop) {
        if (headsigns == null) {
            return getTrip().getTripHeadsign();
        } else {
            return headsigns[stop];
        }
    }

    /**
     * Return list of via names per particular stop.
     * This field provides info about intermediate stops between current stop and final trip destination.
     * Mapped from NeTEx DestinationDisplay.vias. No GTFS mapping at the moment.
     *
     * @return Empty list if there are no vias registered for a stop.
     */
    public List getVia(final int stop) {
        if (headsignVias == null || headsignVias[stop] == null) {
            return List.of();
        }
        return List.of(headsignVias[stop]);
    }

    /** @return the time in seconds after midnight that the vehicle arrives at the stop. */
    public int getScheduledArrivalTime(final int stop) {
        return scheduledArrivalTimes[stop] + timeShift;
    }

    /** @return the amount of time in seconds that the vehicle waits at the stop. */
    public int getScheduledDepartureTime(final int stop) {
        return scheduledDepartureTimes[stop] + timeShift;
    }

    /**
     * Return an integer which can be used to sort TripTimes in order of departure/arrivals.
     * 

* This sorted trip times is used to search for trips. OTP assume one trip do NOT pass another * trip down the line. */ public int sortIndex() { return getArrivalTime(0); } /** @return the time in seconds after midnight that the vehicle arrives at the stop. */ public int getArrivalTime(final int stop) { if (arrivalTimes == null) { return getScheduledArrivalTime(stop); } else return arrivalTimes[stop]; // updated times are not time shifted. } /** @return the amount of time in seconds that the vehicle waits at the stop. */ public int getDepartureTime(final int stop) { if (departureTimes == null) { return getScheduledDepartureTime(stop); } else return departureTimes[stop]; // updated times are not time shifted. } /** @return the difference between the scheduled and actual arrival times at this stop. */ public int getArrivalDelay(final int stop) { return getArrivalTime(stop) - (scheduledArrivalTimes[stop] + timeShift); } /** @return the difference between the scheduled and actual departure times at this stop. */ public int getDepartureDelay(final int stop) { return getDepartureTime(stop) - (scheduledDepartureTimes[stop] + timeShift); } public void setRecorded(int stop, boolean recorded) { prepareForRealTimeUpdates(); recordedStops[stop] = recorded; } public void setCancelled(int stop) { prepareForRealTimeUpdates(); cancelledStops[stop] = true; } public boolean isCancelledStop(int stop) { if (cancelledStops == null) { return false; } return cancelledStops[stop]; } // TODO OTP2 - Unused, but will be used by Transmodel API public boolean isRecordedStop(int stop) { if (recordedStops == null) { return false; } return recordedStops[stop]; } //Is prediction for single stop inaccurate public void setPredictionInaccurate(int stop, boolean predictionInaccurate) { prepareForRealTimeUpdates(); predictionInaccurateOnStops[stop] = predictionInaccurate; } // TODO OTP2 - Unused, but will be used by Transmodel API public boolean isPredictionInaccurate(int stop) { if (predictionInaccurateOnStops == null) { return false; } return predictionInaccurateOnStops[stop]; } public BookingInfo getDropOffBookingInfo(int stop) { return dropOffBookingInfos.get(stop); } public BookingInfo getPickupBookingInfo(int stop) { return pickupBookingInfos.get(stop); } /** * @return true if this TripTimes represents an unmodified, scheduled trip from a published * timetable or false if it is a updated, cancelled, or otherwise modified one. This * method differs from {@link #getRealTimeState()} in that it checks whether real-time * information is actually available in this TripTimes. */ public boolean isScheduled() { return realTimeState == RealTimeState.SCHEDULED; } /** * @return true if this TripTimes is canceled */ public boolean isCanceled() { return realTimeState == RealTimeState.CANCELED; } /** * @return the real-time state of this TripTimes */ public RealTimeState getRealTimeState() { return realTimeState; } public void setRealTimeState(final RealTimeState realTimeState) { this.realTimeState = realTimeState; } /** * When creating a scheduled TripTimes or wrapping it in updates, we could potentially imply * negative running or dwell times. We really don't want those being used in routing. * This method check that all times are increasing, and logs warnings if this is not the case. * @return whether the times were found to be increasing. */ public boolean timesIncreasing() { final int nStops = scheduledArrivalTimes.length; int prevDep = -1; for (int s = 0; s < nStops; s++) { final int arr = getArrivalTime(s); final int dep = getDepartureTime(s); if (dep < arr) { LOG.warn("Negative dwell time in TripTimes at stop index {}.", s); return false; } if (prevDep > arr) { LOG.warn("Negative running time in TripTimes after stop index {}.", s); return false; } prevDep = dep; } return true; } /** Cancel this entire trip */ public void cancelTrip() { realTimeState = RealTimeState.CANCELED; } public void updateDepartureTime(final int stop, final int time) { prepareForRealTimeUpdates(); departureTimes[stop] = time; } public void updateDepartureDelay(final int stop, final int delay) { prepareForRealTimeUpdates(); departureTimes[stop] = scheduledDepartureTimes[stop] + timeShift + delay; } public void updateArrivalTime(final int stop, final int time) { prepareForRealTimeUpdates(); arrivalTimes[stop] = time; } public void updateArrivalDelay(final int stop, final int delay) { prepareForRealTimeUpdates(); arrivalTimes[stop] = scheduledArrivalTimes[stop] + timeShift + delay; } /** * If they don't already exist, create arrays for updated arrival and departure times * that are just time-shifted copies of the zero-based scheduled departure times. * * Also sets the realtime state to UPDATED. */ private void prepareForRealTimeUpdates() { if (arrivalTimes == null) { this.arrivalTimes = Arrays.copyOf(scheduledArrivalTimes, scheduledArrivalTimes.length); this.departureTimes = Arrays.copyOf(scheduledDepartureTimes, scheduledDepartureTimes.length); this.recordedStops = new boolean[arrivalTimes.length]; this.cancelledStops = new boolean[arrivalTimes.length]; this.predictionInaccurateOnStops = new boolean[arrivalTimes.length]; for (int i = 0; i < arrivalTimes.length; i++) { arrivalTimes[i] += timeShift; departureTimes[i] += timeShift; recordedStops[i] = false; cancelledStops[i] = false; predictionInaccurateOnStops[i] = false; } // Update the real-time state realTimeState = RealTimeState.UPDATED; } } public int getNumStops () { return scheduledArrivalTimes.length; } /** Sort TripTimes based on first departure time. */ @Override public int compareTo(final TripTimes other) { return this.getDepartureTime(0) - other.getDepartureTime(0); } /** * Returns a time-shifted copy of this TripTimes in which the vehicle passes the given stop * index (not stop sequence number) at the given time. We only have a mechanism to shift the * scheduled stoptimes, not the real-time stoptimes. Therefore, this only works on trips * without updates for now (frequency trips don't have updates). */ public TripTimes timeShift (final int stop, final int time, final boolean depart) { if (arrivalTimes != null || departureTimes != null) { return null; } final TripTimes shifted = new TripTimes(this); // Adjust 0-based times to match desired stoptime. final int shift = time - (depart ? getDepartureTime(stop) : getArrivalTime(stop)); // existing shift should usually (always?) be 0 on freqs shifted.timeShift = shifted.timeShift + shift; return shifted; } /** Just to create uniform getter-syntax across the whole public interface of TripTimes. */ public int getOriginalGtfsStopSequence(final int stop) { return originalGtfsStopSequence[stop]; } /** @return whether or not stopIndex is considered a timepoint in this TripTimes. */ public boolean isTimepoint(final int stopIndex) { return timepoints.get(stopIndex); } /** * Hash the scheduled arrival/departure times. Used in creating stable IDs for trips across GTFS feed versions. * Use hops rather than stops because: * a) arrival at stop zero and departure from last stop are irrelevant * b) this hash function needs to stay stable when users switch from 0.10.x to 1.0 */ public HashCode semanticHash(final HashFunction hashFunction) { final Hasher hasher = hashFunction.newHasher(); for (int hop = 0; hop < getNumStops() - 1; hop++) { hasher.putInt(getScheduledDepartureTime(hop)); hasher.putInt(getScheduledArrivalTime(hop + 1)); } return hasher.hash(); } /** The code for the service on which this trip runs. For departure search optimizations. */ public int getServiceCode() { return serviceCode; } /** The trips whose arrivals and departures are represented by this TripTimes */ public Trip getTrip() { return trip; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy