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

org.opentripplanner.transit.model.timetable.TripTimes Maven / Gradle / Ivy

package org.opentripplanner.transit.model.timetable;

import static org.opentripplanner.model.UpdateError.UpdateErrorType.NEGATIVE_DWELL_TIME;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.NEGATIVE_HOP_TIME;

import java.io.Serializable;
import java.time.Duration;
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.StopTime;
import org.opentripplanner.model.UpdateError;
import org.opentripplanner.transit.model.basic.Accessibility;
import org.opentripplanner.transit.model.basic.I18NString;
import org.opentripplanner.transit.model.framework.Deduplicator;
import org.opentripplanner.transit.model.framework.Result;
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);
  private static final String[] EMPTY_STRING_ARRAY = new String[0];
  /** The trips whose arrivals and departures are represented by this TripTimes */
  private final Trip trip;
  /**
   * 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 I18NString[] 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;
  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;
  /** A Set of stop indexes that are marked as timepoints in the GTFS input. */
  private final BitSet timepoints;
  /**
   * 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;
  // not final because these are set later, after TripTimes construction.
  private int serviceCode = -1;
  /**
   * 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;

  /**
   * States of the stops in the trip. If the state is DEFAULT for a stop, {@link #realTimeState}
   * should determine the realtime state of the stop.
   * 

* This is only for API-purposes (does not affect routing). Non-final to allow updates. */ private StopRealTimeState[] stopRealTimeStates; /** * This is only for API-purposes (does not affect routing). Non-final to allow updates. */ private OccupancyStatus[] occupancyStatus; /** * The real-time state of this TripTimes. */ private RealTimeState realTimeState = RealTimeState.SCHEDULED; public Accessibility wheelchairAccessibility; /** * 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.deduplicateObjectArray(I18NString.class, 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.stopRealTimeStates = null; this.timepoints = deduplicator.deduplicateBitSet(timepoints); this.wheelchairAccessibility = trip.getWheelchairBoarding(); 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.stopRealTimeStates = object.stopRealTimeStates; this.pickupBookingInfos = object.pickupBookingInfos; this.dropOffBookingInfos = object.dropOffBookingInfos; this.originalGtfsStopSequence = object.originalGtfsStopSequence; this.realTimeState = object.realTimeState; this.timepoints = object.timepoints; this.wheelchairAccessibility = object.wheelchairAccessibility; } /** * 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 I18NString getHeadsign(final int stop) { if (headsigns == null) { return getTrip().getHeadsign(); } 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 getHeadsignVias(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) { prepareForRealTimeUpdates(); stopRealTimeStates[stop] = StopRealTimeState.RECORDED; } public void setCancelled(int stop) { prepareForRealTimeUpdates(); stopRealTimeStates[stop] = StopRealTimeState.CANCELLED; } public void setNoData(int stop) { prepareForRealTimeUpdates(); stopRealTimeStates[stop] = StopRealTimeState.NO_DATA; } public void setPredictionInaccurate(int stop) { prepareForRealTimeUpdates(); stopRealTimeStates[stop] = StopRealTimeState.INACCURATE_PREDICTIONS; } public boolean isCancelledStop(int stop) { if (stopRealTimeStates == null) { return false; } return stopRealTimeStates[stop] == StopRealTimeState.CANCELLED; } // TODO OTP2 - Unused, but will be used by Transmodel API public boolean isRecordedStop(int stop) { if (stopRealTimeStates == null) { return false; } return stopRealTimeStates[stop] == StopRealTimeState.RECORDED; } public boolean isNoDataStop(int stop) { if (stopRealTimeStates == null) { return false; } return stopRealTimeStates[stop] == StopRealTimeState.NO_DATA; } // TODO OTP2 - Unused, but will be used by Transmodel API public boolean isPredictionInaccurate(int stop) { if (stopRealTimeStates == null) { return false; } return stopRealTimeStates[stop] == StopRealTimeState.INACCURATE_PREDICTIONS; } public void setOccupancyStatus(int stop, OccupancyStatus occupancyStatus) { prepareForRealTimeUpdates(); this.occupancyStatus[stop] = occupancyStatus; } public OccupancyStatus getOccupancyStatus(int stop) { if (this.occupancyStatus == null) { return OccupancyStatus.NO_DATA; } return this.occupancyStatus[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 * checks that all times are increasing. * * @return empty if times were found to be increasing, stop index of the first error otherwise */ public Result validateNonIncreasingTimes() { final int nStops = scheduledArrivalTimes.length; int prevDep = -9_999_999; for (int s = 0; s < nStops; s++) { final int arr = getArrivalTime(s); final int dep = getDepartureTime(s); if (dep < arr) { return Result.failure(new UpdateError(getTrip().getId(), NEGATIVE_DWELL_TIME, s)); } if (prevDep > arr) { return Result.failure(new UpdateError(getTrip().getId(), NEGATIVE_HOP_TIME, s)); } prevDep = dep; } return Result.success(); } /** 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; } public Accessibility getWheelchairAccessibility() { return wheelchairAccessibility; } public void updateWheelchairAccessibility(Accessibility wheelchairAccessibility) { this.wheelchairAccessibility = wheelchairAccessibility; } 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; } // Time-shift all times on this trip. This is used when updating the time zone for the trip. public void timeShift(Duration duration) { timeShift += duration.toSeconds(); } /** 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); } /** The code for the service on which this trip runs. For departure search optimizations. */ public int getServiceCode() { return serviceCode; } public void setServiceCode(int serviceCode) { this.serviceCode = serviceCode; } /** The trips whose arrivals and departures are represented by this TripTimes */ public Trip getTrip() { return trip; } /** * Adjusts arrival time for the stop at the firstUpdatedIndex if no update was given for it and * arrival/departure times for the stops before that stop. Returns {@code true} if times have been * adjusted. */ public boolean adjustTimesBeforeAlways(int firstUpdatedIndex) { boolean hasAdjustedTimes = false; int delay = getDepartureDelay(firstUpdatedIndex); if (getArrivalDelay(firstUpdatedIndex) == 0) { updateArrivalDelay(firstUpdatedIndex, delay); hasAdjustedTimes = true; } delay = getArrivalDelay(firstUpdatedIndex); if (delay == 0) { return false; } for (int i = firstUpdatedIndex - 1; i >= 0; i--) { hasAdjustedTimes = true; updateDepartureDelay(i, delay); updateArrivalDelay(i, delay); } return hasAdjustedTimes; } /** * Adjusts arrival and departure times for the stops before the stop at firstUpdatedIndex when * required to ensure that the times are increasing. Can set NO_DATA flag on the updated previous * stops. Returns {@code true} if times have been adjusted. */ public boolean adjustTimesBeforeWhenRequired(int firstUpdatedIndex, boolean setNoData) { if (getArrivalTime(firstUpdatedIndex) > getDepartureTime(firstUpdatedIndex)) { // The given trip update has arrival time after departure time for the first updated stop. // This method doesn't try to fix issues in the given data, only for the missing part return false; } int nextStopArrivalTime = getArrivalTime(firstUpdatedIndex); int delay = getArrivalDelay(firstUpdatedIndex); boolean hasAdjustedTimes = false; boolean adjustTimes = true; for (int i = firstUpdatedIndex - 1; i >= 0; i--) { if (setNoData && !isCancelledStop(i)) { setNoData(i); } if (adjustTimes) { if (getDepartureTime(i) < nextStopArrivalTime) { adjustTimes = false; continue; } else { hasAdjustedTimes = true; updateDepartureDelay(i, delay); } if (getArrivalTime(i) < getDepartureTime(i)) { adjustTimes = false; } else { updateArrivalDelay(i, delay); nextStopArrivalTime = getArrivalTime(i); } } } return hasAdjustedTimes; } /** * @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 I18NString[] makeHeadsignsArray(final Collection stopTimes) { final I18NString tripHeadsign = trip.getHeadsign(); 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 I18NString[] hs = new I18NString[stopTimes.size()]; for (final StopTime st : stopTimes) { final I18NString 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; i++; continue; } vias[i] = st.getHeadsignVias().toArray(EMPTY_STRING_ARRAY); i++; } return vias; } /** * 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.stopRealTimeStates = new StopRealTimeState[arrivalTimes.length]; this.occupancyStatus = new OccupancyStatus[arrivalTimes.length]; for (int i = 0; i < arrivalTimes.length; i++) { arrivalTimes[i] += timeShift; departureTimes[i] += timeShift; stopRealTimeStates[i] = StopRealTimeState.DEFAULT; occupancyStatus[i] = OccupancyStatus.NO_DATA; } // Update the real-time state realTimeState = RealTimeState.UPDATED; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy