
org.opentripplanner.model.Timetable Maven / Gradle / Ivy
Show all versions of otp Show documentation
package org.opentripplanner.model;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.INVALID_ARRIVAL_TIME;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.INVALID_DEPARTURE_TIME;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.INVALID_INPUT_STRUCTURE;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.INVALID_STOP_SEQUENCE;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.TOO_FEW_STOPS;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.TRIP_NOT_FOUND;
import static org.opentripplanner.model.UpdateError.UpdateErrorType.TRIP_NOT_FOUND_IN_PATTERN;
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.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.framework.Result;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.timetable.Direction;
import org.opentripplanner.transit.model.timetable.FrequencyEntry;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.updater.GtfsRealtimeMapper;
import org.opentripplanner.updater.trip.BackwardsDelayPropagationType;
import org.opentripplanner.util.time.ServiceDateUtils;
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 final TripPattern pattern;
private final List tripTimes = new ArrayList<>();
private final List frequencyEntries = new ArrayList<>();
private final LocalDate serviceDate;
/** 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, LocalDate serviceDate) {
tripTimes.addAll(tt.tripTimes);
this.serviceDate = serviceDate;
this.pattern = tt.pattern;
}
/** @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;
}
public TripTimes getTripTimes(FeedScopedId tripId) {
for (TripTimes tt : tripTimes) {
if (tt.getTrip().getId() == tripId) {
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
* @param backwardsDelayPropagationType Defines when delays are propagated to previous stops and
* if these stops are given the NO_DATA flag
* @return {@link Result} contains either a 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 and a list of stop indices that have been skipped with the
* realtime update; or an error 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 Result createUpdatedTripTimes(
TripUpdate tripUpdate,
ZoneId timeZone,
LocalDate updateServiceDate,
BackwardsDelayPropagationType backwardsDelayPropagationType
) {
Result invalidInput = Result.failure(
UpdateError.noTripId(INVALID_INPUT_STRUCTURE)
);
if (tripUpdate == null) {
LOG.debug("A null TripUpdate pointer was passed to the Timetable class update method.");
return invalidInput;
}
// 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.debug("TripUpdate object has no TripDescriptor field.");
return invalidInput;
}
TripDescriptor tripDescriptor = tripUpdate.getTrip();
if (!tripDescriptor.hasTripId()) {
LOG.debug("TripDescriptor object has no TripId field");
Result.failure(UpdateError.noTripId(TRIP_NOT_FOUND));
}
String tripId = tripDescriptor.getTripId();
var feedScopedTripId = new FeedScopedId(this.getPattern().getFeedId(), tripId);
int tripIndex = getTripIndex(tripId);
if (tripIndex == -1) {
LOG.debug("tripId {} not found in pattern.", tripId);
return Result.failure(new UpdateError(feedScopedTripId, TRIP_NOT_FOUND_IN_PATTERN));
} else {
LOG.trace("tripId {} found at index {} in timetable.", tripId, tripIndex);
}
TripTimes newTimes = new TripTimes(getTripTimes(tripIndex));
List skippedStopIndices = new ArrayList<>();
// 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 Result.failure(new UpdateError(feedScopedTripId, TOO_FEW_STOPS));
}
StopTimeUpdate update = updates.next();
int numStops = newTimes.getNumStops();
Integer delay = null;
Integer firstUpdatedIndex = null;
final long today = ServiceDateUtils
.asStartOfService(updateServiceDate, timeZone)
.toEpochSecond();
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) {
skippedStopIndices.add(i);
newTimes.setCancelled(i);
int delayOrZero = delay != null ? delay : 0;
newTimes.updateArrivalDelay(i, delayOrZero);
newTimes.updateDepartureDelay(i, delayOrZero);
} else if (scheduleRelationship == StopTimeUpdate.ScheduleRelationship.NO_DATA) {
newTimes.updateArrivalDelay(i, 0);
newTimes.updateDepartureDelay(i, 0);
delay = 0;
newTimes.setNoData(i);
} else {
if (update.hasArrival()) {
if (firstUpdatedIndex == null) {
firstUpdatedIndex = i;
}
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 Result.failure(new UpdateError(feedScopedTripId, INVALID_ARRIVAL_TIME));
}
} else if (delay != null) {
newTimes.updateArrivalDelay(i, delay);
}
if (update.hasDeparture()) {
if (firstUpdatedIndex == null) {
firstUpdatedIndex = i;
}
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 Result.failure(new UpdateError(feedScopedTripId, INVALID_DEPARTURE_TIME));
}
} else if (delay != null) {
newTimes.updateDepartureDelay(i, delay);
}
}
if (updates.hasNext()) {
update = updates.next();
} else {
update = null;
}
} else if (delay != null) {
newTimes.updateArrivalDelay(i, delay);
newTimes.updateDepartureDelay(i, delay);
}
}
if (update != null) {
LOG.debug(
"Part of a TripUpdate object could not be applied successfully to trip {}.",
tripId
);
return Result.failure(new UpdateError(feedScopedTripId, INVALID_STOP_SEQUENCE));
}
if (firstUpdatedIndex != null && firstUpdatedIndex > 0) {
if (
(
backwardsDelayPropagationType == BackwardsDelayPropagationType.REQUIRED_NO_DATA &&
newTimes.adjustTimesBeforeWhenRequired(firstUpdatedIndex, true)
) ||
(
backwardsDelayPropagationType == BackwardsDelayPropagationType.REQUIRED &&
newTimes.adjustTimesBeforeWhenRequired(firstUpdatedIndex, false)
) ||
(
backwardsDelayPropagationType == BackwardsDelayPropagationType.ALWAYS &&
newTimes.adjustTimesBeforeAlways(firstUpdatedIndex)
)
) {
LOG.debug(
"Propagated delay from stop index {} backwards on trip {}.",
firstUpdatedIndex,
tripId
);
}
}
var result = newTimes.validateNonIncreasingTimes();
if (result.isFailure()) {
LOG.debug(
"TripTimes are non-increasing after applying GTFS-RT delay propagation to trip {} after stop index {}.",
tripId,
result.failureValue().stopIndex()
);
return Result.failure(result.failureValue());
}
if (tripUpdate.hasVehicle()) {
var vehicleDescriptor = tripUpdate.getVehicle();
if (vehicleDescriptor.hasWheelchairAccessible()) {
GtfsRealtimeMapper
.mapWheelchairAccessible(vehicleDescriptor.getWheelchairAccessible())
.ifPresent(newTimes::updateWheelchairAccessibility);
}
}
LOG.trace(
"A valid TripUpdate object was applied to trip {} using the Timetable class update method.",
tripId
);
return Result.success(new TripTimesPatch(newTimes, skippedStopIndices));
}
/**
* 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(LocalDate 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 LocalDate getServiceDate() {
return serviceDate;
}
/**
* The direction for all the trips in this pattern.
*/
public Direction getDirection() {
return Optional
.ofNullable(getRepresentativeTripTimes())
.map(TripTimes::getTrip)
.map(Trip::getDirection)
.orElse(Direction.UNKNOWN);
}
public TripTimes getRepresentativeTripTimes() {
if (!getTripTimes().isEmpty()) {
return getTripTimes(0);
} else if (!getFrequencyEntries().isEmpty()) {
return getFrequencyEntries().get(0).tripTimes;
} else {
// Pattern is created only for real-time updates
return null;
}
}
}