Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package org.opentripplanner.updater.stoptime;
import com.google.common.base.Preconditions;
import com.google.transit.realtime.GtfsRealtime.TripDescriptor;
import com.google.transit.realtime.GtfsRealtime.TripUpdate;
import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate;
import org.opentripplanner.model.Agency;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.Route;
import org.opentripplanner.model.StopLocation;
import org.opentripplanner.model.StopPattern;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.Timetable;
import org.opentripplanner.model.TimetableSnapshot;
import org.opentripplanner.model.TimetableSnapshotProvider;
import org.opentripplanner.model.TransitMode;
import org.opentripplanner.model.Trip;
import org.opentripplanner.model.TripPattern;
import org.opentripplanner.model.calendar.ServiceDate;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.RoutingService;
import org.opentripplanner.routing.trippattern.RealTimeState;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.locks.ReentrantLock;
import static org.opentripplanner.model.PickDrop.NONE;
import static org.opentripplanner.model.PickDrop.SCHEDULED;
/**
* This class should be used to create snapshots of lookup tables of realtime data. This is
* necessary to provide planning threads a consistent constant view of a graph with realtime data at
* a specific point in time.
*/
public class TimetableSnapshotSource implements TimetableSnapshotProvider {
private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotSource.class);
/**
* Number of milliseconds per second
*/
private static final int MILLIS_PER_SECOND = 1000;
/**
* Maximum time in seconds since midnight for arrivals and departures
*/
private static final long MAX_ARRIVAL_DEPARTURE_TIME = 48 * 60 * 60;
public int logFrequency = 2000;
private int appliedBlockCount = 0;
/**
* If a timetable snapshot is requested less than this number of milliseconds after the previous
* snapshot, just return the same one. Throttles the potentially resource-consuming task of
* duplicating a TripPattern → Timetable map and indexing the new Timetables.
*/
public int maxSnapshotFrequency = 1000; // msec
/**
* The last committed snapshot that was handed off to a routing thread. This snapshot may be
* given to more than one routing thread if the maximum snapshot frequency is exceeded.
*/
private volatile TimetableSnapshot snapshot = null;
/**
* The working copy of the timetable snapshot. Should not be visible to routing threads. Should
* only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that
* might modify this buffer will correctly acquire the lock.
*/
private final TimetableSnapshot buffer = new TimetableSnapshot();
/**
* Lock to indicate that buffer is in use
*/
private final ReentrantLock bufferLock = new ReentrantLock(true);
/**
* A synchronized cache of trip patterns that are added to the graph due to GTFS-realtime messages.
*/
private final TripPatternCache tripPatternCache = new TripPatternCache();
/** Should expired realtime data be purged from the graph. */
public boolean purgeExpiredData = true;
protected ServiceDate lastPurgeDate = null;
/** Epoch time in milliseconds at which the last snapshot was generated. */
protected long lastSnapshotTime = -1;
private final TimeZone timeZone;
private final RoutingService routingService;
public GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher;
private TransitLayer realtimeTransitLayer;
private TransitLayerUpdater transitLayerUpdater;
public TimetableSnapshotSource(final Graph graph) {
timeZone = graph.getTimeZone();
routingService = new RoutingService(graph);
realtimeTransitLayer = graph.getRealtimeTransitLayer();
transitLayerUpdater = graph.transitLayerUpdater;
}
/**
* @return an up-to-date snapshot mapping TripPatterns to Timetables. This snapshot and the
* timetable objects it references are guaranteed to never change, so the requesting
* thread is provided a consistent view of all TripTimes. The routing thread need only
* release its reference to the snapshot to release resources.
*/
public TimetableSnapshot getTimetableSnapshot() {
TimetableSnapshot snapshotToReturn;
// Try to get a lock on the buffer
if (bufferLock.tryLock()) {
// Make a new snapshot if necessary
try {
snapshotToReturn = getTimetableSnapshot(false);
} finally {
bufferLock.unlock();
}
} else {
// No lock could be obtained because there is either a snapshot commit busy or updates
// are applied at this moment, just return the current snapshot
snapshotToReturn = snapshot;
}
return snapshotToReturn;
}
private TimetableSnapshot getTimetableSnapshot(final boolean force) {
final long now = System.currentTimeMillis();
if (force || now - lastSnapshotTime > maxSnapshotFrequency) {
if (force || buffer.isDirty()) {
LOG.debug("Committing {}", buffer.toString());
snapshot = buffer.commit(transitLayerUpdater, force);
} else {
LOG.debug("Buffer was unchanged, keeping old snapshot.");
}
lastSnapshotTime = System.currentTimeMillis();
} else {
LOG.debug("Snapshot frequency exceeded. Reusing snapshot {}", snapshot);
}
return snapshot;
}
/**
* Method to apply a trip update list to the most recent version of the timetable snapshot. A
* GTFS-RT feed is always applied against a single static feed (indicated by feedId).
*
* However, multi-feed support is not completed and we currently assume there is only one static
* feed when matching IDs.
*
* @param graph graph to update (needed for adding/changing stop patterns)
* @param fullDataset true iff the list with updates represent all updates that are active right
* now, i.e. all previous updates should be disregarded
* @param updates GTFS-RT TripUpdate's that should be applied atomically
* @param feedId
*/
public void applyTripUpdates(final Graph graph, final boolean fullDataset, final List updates, final String feedId) {
if (updates == null) {
LOG.warn("updates is null");
return;
}
// Acquire lock on buffer
bufferLock.lock();
try {
if (fullDataset) {
// Remove all updates from the buffer
buffer.clear(feedId);
}
LOG.debug("message contains {} trip updates", updates.size());
int uIndex = 0;
for (TripUpdate tripUpdate : updates) {
if (fuzzyTripMatcher != null && tripUpdate.hasTrip()) {
final TripDescriptor trip = fuzzyTripMatcher.match(feedId, tripUpdate.getTrip());
tripUpdate = tripUpdate.toBuilder().setTrip(trip).build();
}
if (!tripUpdate.hasTrip()) {
LOG.warn("Missing TripDescriptor in gtfs-rt trip update: \n{}", tripUpdate);
continue;
}
ServiceDate serviceDate = new ServiceDate();
final TripDescriptor tripDescriptor = tripUpdate.getTrip();
if (tripDescriptor.hasStartDate()) {
try {
serviceDate = ServiceDate.parseString(tripDescriptor.getStartDate());
} catch (final ParseException e) {
LOG.warn("Failed to parse start date in gtfs-rt trip update: \n{}", tripUpdate);
continue;
}
} else {
// TODO: figure out the correct service date. For the special case that a trip
// starts for example at 40:00, yesterday would probably be a better guess.
}
uIndex += 1;
LOG.debug("trip update #{} ({} updates) :",
uIndex, tripUpdate.getStopTimeUpdateCount());
LOG.trace("{}", tripUpdate);
// Determine what kind of trip update this is
boolean applied = false;
final TripDescriptor.ScheduleRelationship tripScheduleRelationship = determineTripScheduleRelationship(
tripUpdate);
switch (tripScheduleRelationship) {
case SCHEDULED:
applied = handleScheduledTrip(tripUpdate, feedId, serviceDate);
break;
case ADDED:
applied = validateAndHandleAddedTrip(graph, tripUpdate, feedId, serviceDate);
break;
case UNSCHEDULED:
applied = handleUnscheduledTrip(tripUpdate, feedId, serviceDate);
break;
case CANCELED:
applied = handleCanceledTrip(tripUpdate, feedId, serviceDate);
break;
case MODIFIED:
applied = validateAndHandleModifiedTrip(graph, tripUpdate, feedId, serviceDate);
break;
}
if (applied) {
appliedBlockCount++;
} else {
LOG.warn("Failed to apply TripUpdate.");
LOG.trace(" Contents: {}", tripUpdate);
}
if (appliedBlockCount % logFrequency == 0) {
LOG.info("Applied {} trip updates.", appliedBlockCount);
}
}
LOG.debug("end of update message");
// Make a snapshot after each message in anticipation of incoming requests
// Purge data if necessary (and force new snapshot if anything was purged)
// Make sure that the public (locking) getTimetableSnapshot function is not called.
if (purgeExpiredData) {
final boolean modified = purgeExpiredData();
getTimetableSnapshot(modified);
} else {
getTimetableSnapshot(false);
}
} finally {
// Always release lock
bufferLock.unlock();
}
}
/**
* Determine how the trip update should be handled.
*
* @param tripUpdate trip update
* @return TripDescriptor.ScheduleRelationship indicating how the trip update should be handled
*/
private TripDescriptor.ScheduleRelationship determineTripScheduleRelationship(final TripUpdate tripUpdate) {
// Assume default value
TripDescriptor.ScheduleRelationship tripScheduleRelationship = TripDescriptor.ScheduleRelationship.SCHEDULED;
// If trip update contains schedule relationship, use it
if (tripUpdate.hasTrip() && tripUpdate.getTrip().hasScheduleRelationship()) {
tripScheduleRelationship = tripUpdate.getTrip().getScheduleRelationship();
}
if (tripScheduleRelationship.equals(TripDescriptor.ScheduleRelationship.SCHEDULED)) {
// Loop over stops to check whether there are ADDED or SKIPPED stops
boolean hasModifiedStops = false;
for (final StopTimeUpdate stopTimeUpdate : tripUpdate.getStopTimeUpdateList()) {
// Check schedule relationship
if (stopTimeUpdate.hasScheduleRelationship()) {
final StopTimeUpdate.ScheduleRelationship stopScheduleRelationship = stopTimeUpdate
.getScheduleRelationship();
if (stopScheduleRelationship.equals(StopTimeUpdate.ScheduleRelationship.SKIPPED)
// TODO: uncomment next line when StopTimeUpdate.ScheduleRelationship.ADDED exists
// || stopScheduleRelationship.equals(StopTimeUpdate.ScheduleRelationship.ADDED)
) {
hasModifiedStops = true;
}
}
}
// If stops are modified, handle trip update like a modified trip
if (hasModifiedStops) {
tripScheduleRelationship = TripDescriptor.ScheduleRelationship.MODIFIED;
}
}
return tripScheduleRelationship;
}
private boolean handleScheduledTrip(final TripUpdate tripUpdate, final String feedId, final ServiceDate serviceDate) {
final TripDescriptor tripDescriptor = tripUpdate.getTrip();
// This does not include Agency ID or feed ID, trips are feed-unique and we currently assume a single static feed.
final String tripId = tripDescriptor.getTripId();
final TripPattern pattern = getPatternForTripId(feedId, tripId);
if (pattern == null) {
LOG.warn("No pattern found for tripId {}, skipping TripUpdate.", tripId);
return false;
}
if (tripUpdate.getStopTimeUpdateCount() < 1) {
LOG.warn("TripUpdate contains no updates, skipping.");
return false;
}
// If this trip_id has been used for previously ADDED/MODIFIED trip message (e.g. when the sequence of stops has
// changed, and is now changing back to the originally scheduled one) cancel that previously created trip.
cancelPreviouslyAddedTrip(new FeedScopedId(feedId, tripId), serviceDate);
// Apply update on the *scheduled* time table and set the updated trip times in the buffer
final TripTimes updatedTripTimes = pattern.getScheduledTimetable().createUpdatedTripTimes(tripUpdate,
timeZone, serviceDate);
if (updatedTripTimes == null) {
return false;
}
// Make sure that updated trip times have the correct real time state
updatedTripTimes.setRealTimeState(RealTimeState.UPDATED);
final boolean success = buffer.update(pattern, updatedTripTimes, serviceDate);
return success;
}
/**
* Validate and handle GTFS-RT TripUpdate message containing an ADDED trip.
*
* @param graph graph to update
* @param tripUpdate GTFS-RT TripUpdate message
* @param feedId
* @param serviceDate
* @return true iff successful
*/
private boolean validateAndHandleAddedTrip(final Graph graph, final TripUpdate tripUpdate,
final String feedId, final ServiceDate serviceDate) {
// Preconditions
Preconditions.checkNotNull(graph);
Preconditions.checkNotNull(tripUpdate);
Preconditions.checkNotNull(serviceDate);
//
// Validate added trip
//
// Check whether trip id of ADDED trip is available
final TripDescriptor tripDescriptor = tripUpdate.getTrip();
if (!tripDescriptor.hasTripId()) {
LOG.warn("No trip id found for ADDED trip, skipping.");
return false;
}
// Check whether trip id already exists in graph
final String tripId = tripDescriptor.getTripId();
final Trip trip = getTripForTripId(feedId, tripId);
if (trip != null) {
// TODO: should we support this and add a new instantiation of this trip (making it
// frequency based)?
LOG.warn("Graph already contains trip id of ADDED trip, skipping.");
return false;
}
// Check whether a start date exists
if (!tripDescriptor.hasStartDate()) {
// TODO: should we support this and apply update to all days?
LOG.warn("ADDED trip doesn't have a start date in TripDescriptor, skipping.");
return false;
}
// Check whether at least two stop updates exist
if (tripUpdate.getStopTimeUpdateCount() < 2) {
LOG.warn("ADDED trip has less then two stops, skipping.");
return false;
}
// Check whether all stop times are available and all stops exist
final var stops = checkNewStopTimeUpdatesAndFindStops(feedId, tripUpdate);
if (stops == null) {
return false;
}
//
// Handle added trip
//
final boolean success = handleAddedTrip(graph, tripUpdate, stops, feedId, serviceDate);
return success;
}
/**
* Check stop time updates of trip update that results in a new trip (ADDED or MODIFIED) and
* find all stops of that trip.
*
* @param feedId feed id this trip update is intented for
* @param tripUpdate trip update
* @return stops when stop time updates are correct; null if there are errors
*/
private List checkNewStopTimeUpdatesAndFindStops(final String feedId, final TripUpdate tripUpdate) {
Integer previousStopSequence = null;
Long previousTime = null;
final List stopTimeUpdates = tripUpdate.getStopTimeUpdateList();
final List stops = new ArrayList<>(stopTimeUpdates.size());
for (int index = 0; index < stopTimeUpdates.size(); ++index) {
final StopTimeUpdate stopTimeUpdate = stopTimeUpdates.get(index);
// Determine whether stop is skipped
final boolean skippedStop = isStopSkipped(stopTimeUpdate);
// Check stop sequence
if (stopTimeUpdate.hasStopSequence()) {
final Integer stopSequence = stopTimeUpdate.getStopSequence();
// Check non-negative
if (stopSequence < 0) {
LOG.warn("Trip update contains negative stop sequence, skipping.");
return null;
}
// Check whether sequence is increasing
if (previousStopSequence != null && previousStopSequence > stopSequence) {
LOG.warn("Trip update contains decreasing stop sequence, skipping.");
return null;
}
previousStopSequence = stopSequence;
} else {
// Allow missing stop sequences for ADDED and MODIFIED trips
}
// Find stops
if (stopTimeUpdate.hasStopId()) {
// Find stop
final var stop = getStopForStopId(feedId, stopTimeUpdate.getStopId());
if (stop != null) {
// Remember stop
stops.add(stop);
} else if (skippedStop) {
// Set a null value for a skipped stop
stops.add(null);
} else {
LOG.warn("Graph doesn't contain stop id \"{}\" of trip update, skipping.",
stopTimeUpdate.getStopId());
return null;
}
} else {
LOG.warn("Trip update misses some stop ids, skipping.");
return null;
}
// Only check arrival and departure times for non-skipped stops
if (!skippedStop) {
// Check arrival time
if (stopTimeUpdate.hasArrival() && stopTimeUpdate.getArrival().hasTime()) {
// Check for increasing time
final Long time = stopTimeUpdate.getArrival().getTime();
if (previousTime != null && previousTime > time) {
LOG.warn("Trip update contains decreasing times, skipping.");
return null;
}
previousTime = time;
} else {
// Only first non-skipped stop is allowed to miss arrival time
// TODO: should we support only requiring an arrival time on the last stop and interpolate?
for (int earlierIndex = 0; earlierIndex < index; earlierIndex++) {
final StopTimeUpdate earlierStopTimeUpdate = stopTimeUpdates.get(earlierIndex);
// Determine whether earlier stop is skipped
final boolean earlierSkippedStop = isStopSkipped(earlierStopTimeUpdate);
if (!earlierSkippedStop) {
LOG.warn("Trip update misses arrival time, skipping.");
return null;
}
}
}
// Check departure time
if (stopTimeUpdate.hasDeparture() && stopTimeUpdate.getDeparture().hasTime()) {
// Check for increasing time
final Long time = stopTimeUpdate.getDeparture().getTime();
if (previousTime != null && previousTime > time) {
LOG.warn("Trip update contains decreasing times, skipping.");
return null;
}
previousTime = time;
} else {
// Only last non-skipped stop is allowed to miss departure time
// TODO: should we support only requiring a departure time on the first stop and interpolate?
for (int laterIndex = stopTimeUpdates.size() - 1; laterIndex > index; laterIndex--) {
final StopTimeUpdate laterStopTimeUpdate = stopTimeUpdates.get(laterIndex);
// Determine whether later stop is skipped
final boolean laterSkippedStop = isStopSkipped(laterStopTimeUpdate);
if (!laterSkippedStop) {
LOG.warn("Trip update misses departure time, skipping.");
return null;
}
}
}
}
}
return stops;
}
/**
* Determine whether stop time update represents a SKIPPED stop.
*
* @param stopTimeUpdate stop time update
* @return true iff stop is SKIPPED; false otherwise
*/
private boolean isStopSkipped(final StopTimeUpdate stopTimeUpdate) {
final boolean isSkipped = stopTimeUpdate.hasScheduleRelationship() &&
stopTimeUpdate.getScheduleRelationship().equals(StopTimeUpdate.ScheduleRelationship.SKIPPED);
return isSkipped;
}
/**
* Handle GTFS-RT TripUpdate message containing an ADDED trip.
*
* @param graph graph to update
* @param tripUpdate GTFS-RT TripUpdate message
* @param stops the stops of each StopTimeUpdate in the TripUpdate message
* @param feedId
* @param serviceDate service date for added trip
* @return true iff successful
*/
private boolean handleAddedTrip(final Graph graph, final TripUpdate tripUpdate, final List stops,
final String feedId, final ServiceDate serviceDate) {
// Preconditions
Preconditions.checkNotNull(stops);
Preconditions.checkArgument(tripUpdate.getStopTimeUpdateCount() == stops.size(),
"number of stop should match the number of stop time updates");
// Check whether trip id has been used for previously ADDED trip message and cancel
// previously created trip
final String tripId = tripUpdate.getTrip().getTripId();
cancelPreviouslyAddedTrip(new FeedScopedId(feedId, tripId), serviceDate);
//
// Create added trip
//
Route route = null;
if (tripUpdate.getTrip().hasRouteId()) {
// Try to find route
route = getRouteForRouteId(feedId, tripUpdate.getTrip().getRouteId());
}
if (route == null) {
// Create new Route
// Use route id of trip descriptor if available
FeedScopedId id = tripUpdate.getTrip().hasRouteId()
? new FeedScopedId(feedId, tripUpdate.getTrip().getRouteId())
: new FeedScopedId(feedId, tripId);
route = new Route(id);
// Create dummy agency for added trips
Agency dummyAgency = new Agency(
new FeedScopedId(feedId, "Dummy"),
"Dummy",
"Europe/Paris"
);
route.setAgency(dummyAgency);
// Guess the route type as it doesn't exist yet in the specifications
// Bus. Used for short- and long-distance bus routes.
route.setGtfsType(3);
route.setMode(TransitMode.BUS);
// Create route name
route.setLongName(tripId);
}
// Create new Trip
// TODO: which Agency ID to use? Currently use feed id.
final Trip trip = new Trip(new FeedScopedId(feedId, tripUpdate.getTrip().getTripId()));
trip.setRoute(route);
// Find service ID running on this service date
final Set serviceIds = graph.getCalendarService().getServiceIdsOnDate(serviceDate);
if (serviceIds.isEmpty()) {
// No service id exists: return error for now
LOG.warn("ADDED trip has service date for which no service id is available, skipping.");
return false;
} else {
// Just use first service id of set
trip.setServiceId(serviceIds.iterator().next());
}
final boolean success = addTripToGraphAndBuffer(graph, trip, tripUpdate, stops, serviceDate, RealTimeState.ADDED);
return success;
}
/**
* Add a (new) trip to the graph and the buffer
*
* @param graph graph
* @param trip trip
* @param tripUpdate trip update containing stop time updates
* @param stops list of stops corresponding to stop time updates
* @param serviceDate service date of trip
* @param realTimeState real-time state of new trip
* @return true iff successful
*/
private boolean addTripToGraphAndBuffer(final Graph graph, final Trip trip,
final TripUpdate tripUpdate, final List stops, final ServiceDate serviceDate,
final RealTimeState realTimeState) {
// Preconditions
Preconditions.checkNotNull(stops);
Preconditions.checkArgument(tripUpdate.getStopTimeUpdateCount() == stops.size(),
"number of stop should match the number of stop time updates");
// Calculate seconds since epoch on GTFS midnight (noon minus 12h) of service date
final Calendar serviceCalendar = serviceDate.getAsCalendar(timeZone);
final long midnightSecondsSinceEpoch = serviceCalendar.getTimeInMillis() / MILLIS_PER_SECOND;
// Create StopTimes
final List stopTimes = new ArrayList<>(tripUpdate.getStopTimeUpdateCount());
for (int index = 0; index < tripUpdate.getStopTimeUpdateCount(); ++index) {
final StopTimeUpdate stopTimeUpdate = tripUpdate.getStopTimeUpdate(index);
final var stop = stops.get(index);
// Determine whether stop is skipped
final boolean skippedStop = isStopSkipped(stopTimeUpdate);
// Only create stop time for non-skipped stops
if (!skippedStop) {
// Create stop time
final StopTime stopTime = new StopTime();
stopTime.setTrip(trip);
stopTime.setStop(stop);
// Set arrival time
if (stopTimeUpdate.hasArrival() && stopTimeUpdate.getArrival().hasTime()) {
final long arrivalTime = stopTimeUpdate.getArrival().getTime() - midnightSecondsSinceEpoch;
if (arrivalTime < 0 || arrivalTime > MAX_ARRIVAL_DEPARTURE_TIME) {
LOG.warn("ADDED trip has invalid arrival time (compared to start date in "
+ "TripDescriptor), skipping.");
return false;
}
stopTime.setArrivalTime((int) arrivalTime);
}
// Set departure time
if (stopTimeUpdate.hasDeparture() && stopTimeUpdate.getDeparture().hasTime()) {
final long departureTime = stopTimeUpdate.getDeparture().getTime() - midnightSecondsSinceEpoch;
if (departureTime < 0 || departureTime > MAX_ARRIVAL_DEPARTURE_TIME) {
LOG.warn("ADDED trip has invalid departure time (compared to start date in "
+ "TripDescriptor), skipping.");
return false;
}
stopTime.setDepartureTime((int) departureTime);
}
stopTime.setTimepoint(1); // Exact time
if (stopTimeUpdate.hasStopSequence()) {
stopTime.setStopSequence(stopTimeUpdate.getStopSequence());
}
// Set pickup type
// Set different pickup type for last stop
if (index == tripUpdate.getStopTimeUpdateCount() - 1) {
stopTime.setPickupType(NONE); // No pickup available
} else {
stopTime.setPickupType(SCHEDULED); // Regularly scheduled pickup
}
// Set drop off type
// Set different drop off type for first stop
if (index == 0) {
stopTime.setDropOffType(NONE); // No drop off available
} else {
stopTime.setDropOffType(SCHEDULED); // Regularly scheduled drop off
}
// Add stop time to list
stopTimes.add(stopTime);
}
}
// TODO: filter/interpolate stop times like in PatternHopFactory?
// Create StopPattern
final StopPattern stopPattern = new StopPattern(stopTimes);
// Get cached trip pattern or create one if it doesn't exist yet
final TripPattern pattern = tripPatternCache.getOrCreateTripPattern(stopPattern, trip, graph);
// Add service code to bitset of pattern if needed (using copy on write)
final int serviceCode = graph.getServiceCodes().get(trip.getServiceId());
if (!pattern.getServices().get(serviceCode)) {
final BitSet services = (BitSet) pattern.getServices().clone();
services.set(serviceCode);
pattern.setServices(services);
}
// Create new trip times
final TripTimes newTripTimes = new TripTimes(trip, stopTimes, graph.deduplicator);
// Update all times to mark trip times as realtime
// TODO: should we incorporate the delay field if present?
for (int stopIndex = 0; stopIndex < newTripTimes.getNumStops(); stopIndex++) {
newTripTimes.updateArrivalTime(stopIndex, newTripTimes.getScheduledArrivalTime(stopIndex));
newTripTimes.updateDepartureTime(stopIndex, newTripTimes.getScheduledDepartureTime(stopIndex));
}
// Set service code of new trip times
newTripTimes.setServiceCode(serviceCode);
// Make sure that updated trip times have the correct real time state
newTripTimes.setRealTimeState(realTimeState);
// Add new trip times to the buffer
final boolean success = buffer.update(pattern, newTripTimes, serviceDate);
return success;
}
/**
* Cancel scheduled trip in buffer given trip id (without agency id) on service date
*
* @param tripId trip id without agency id
* @param serviceDate service date
* @return true if scheduled trip was cancelled
*/
private boolean cancelScheduledTrip(String feedId, String tripId, final ServiceDate serviceDate) {
boolean success = false;
final TripPattern pattern = getPatternForTripId(feedId, tripId);
if (pattern != null) {
// Cancel scheduled trip times for this trip in this pattern
final Timetable timetable = pattern.getScheduledTimetable();
final int tripIndex = timetable.getTripIndex(tripId);
if (tripIndex == -1) {
LOG.warn("Could not cancel scheduled trip {}", tripId);
} else {
final TripTimes newTripTimes = new TripTimes(timetable.getTripTimes(tripIndex));
newTripTimes.cancelTrip();
buffer.update(pattern, newTripTimes, serviceDate);
success = true;
}
}
return success;
}
/**
* Cancel previously added trip from buffer if there is a previously added trip with given trip
* id (without agency id) on service date. This does not remove the modified/added trip from the buffer, it just
* marks it as canceled. This also does not remove the corresponding vertices and edges from the Graph. Any
* TripPattern that was created for the added/modified trip continues to exist, and will be reused if a similar
* added/modified trip message is received with the same route and stop sequence.
*
* @param tripId trip id without agency id
* @param serviceDate service date
* @return true if a previously added trip was cancelled
*/
private boolean cancelPreviouslyAddedTrip(FeedScopedId tripId, final ServiceDate serviceDate) {
boolean success = false;
final TripPattern pattern = buffer.getLastAddedTripPattern(tripId, serviceDate);
if (pattern != null) {
// Cancel trip times for this trip in this pattern
final Timetable timetable = buffer.resolve(pattern, serviceDate);
final int tripIndex = timetable.getTripIndex(tripId);
if (tripIndex == -1) {
LOG.warn("Could not cancel previously added trip {}", tripId);
} else {
final TripTimes newTripTimes = new TripTimes(timetable.getTripTimes(tripIndex));
newTripTimes.cancelTrip();
buffer.update(pattern, newTripTimes, serviceDate);
success = true;
}
}
return success;
}
private boolean handleUnscheduledTrip(final TripUpdate tripUpdate, final String feedId, final ServiceDate serviceDate) {
// TODO: Handle unscheduled trip
LOG.warn("Unscheduled trips are currently unsupported. Skipping TripUpdate.");
return false;
}
/**
* Validate and handle GTFS-RT TripUpdate message containing a MODIFIED trip.
*
* @param graph graph to update
* @param tripUpdate GTFS-RT TripUpdate message
* @param feedId
* @param serviceDate
* @return true iff successful
*/
private boolean validateAndHandleModifiedTrip(final Graph graph, final TripUpdate tripUpdate, final String feedId, final ServiceDate serviceDate) {
// Preconditions
Preconditions.checkNotNull(graph);
Preconditions.checkNotNull(tripUpdate);
Preconditions.checkNotNull(serviceDate);
//
// Validate modified trip
//
// Check whether trip id of MODIFIED trip is available
final TripDescriptor tripDescriptor = tripUpdate.getTrip();
if (!tripDescriptor.hasTripId()) {
LOG.warn("No trip id found for MODIFIED trip, skipping.");
return false;
}
// Check whether trip id already exists in graph
String tripId = tripDescriptor.getTripId();
Trip trip = getTripForTripId(feedId, tripId);
if (trip == null) {
// TODO: should we support this and consider it an ADDED trip?
LOG.warn("Graph does not contain trip id of MODIFIED trip, skipping.");
return false;
}
// Check whether a start date exists
if (!tripDescriptor.hasStartDate()) {
// TODO: should we support this and apply update to all days?
LOG.warn("MODIFIED trip doesn't have a start date in TripDescriptor, skipping.");
return false;
} else {
// Check whether service date is served by trip
final Set serviceIds = graph.getCalendarService().getServiceIdsOnDate(serviceDate);
if (!serviceIds.contains(trip.getServiceId())) {
// TODO: should we support this and change service id of trip?
LOG.warn("MODIFIED trip has a service date that is not served by trip, skipping.");
return false;
}
}
// Check whether at least two stop updates exist
if (tripUpdate.getStopTimeUpdateCount() < 2) {
LOG.warn("MODIFIED trip has less then two stops, skipping.");
return false;
}
// Check whether all stop times are available and all stops exist
var stops = checkNewStopTimeUpdatesAndFindStops(feedId, tripUpdate);
if (stops == null) {
return false;
}
//
// Handle modified trip
//
final boolean success = handleModifiedTrip(graph, trip, tripUpdate, stops, feedId, serviceDate);
return success;
}
/**
* Handle GTFS-RT TripUpdate message containing a MODIFIED trip.
*
* @param graph graph to update
* @param trip trip that is modified
* @param tripUpdate GTFS-RT TripUpdate message
* @param stops the stops of each StopTimeUpdate in the TripUpdate message
* @param feedId
* @param serviceDate service date for modified trip
* @return true iff successful
*/
private boolean handleModifiedTrip(final Graph graph, final Trip trip, final TripUpdate tripUpdate, final List stops,
final String feedId, final ServiceDate serviceDate) {
// Preconditions
Preconditions.checkNotNull(stops);
Preconditions.checkArgument(tripUpdate.getStopTimeUpdateCount() == stops.size(),
"number of stop should match the number of stop time updates");
// Cancel scheduled trip
final String tripId = tripUpdate.getTrip().getTripId();
cancelScheduledTrip(feedId, tripId, serviceDate);
// Check whether trip id has been used for previously ADDED/MODIFIED trip message and cancel
// previously created trip
cancelPreviouslyAddedTrip(new FeedScopedId(feedId, tripId), serviceDate);
// Add new trip
final boolean success =
addTripToGraphAndBuffer(graph, trip, tripUpdate, stops, serviceDate, RealTimeState.MODIFIED);
return success;
}
private boolean handleCanceledTrip(final TripUpdate tripUpdate, final String feedId, final ServiceDate serviceDate) {
boolean success = false;
if (tripUpdate.getTrip().hasTripId()) {
// Try to cancel scheduled trip
final String tripId = tripUpdate.getTrip().getTripId();
final boolean cancelScheduledSuccess = cancelScheduledTrip(feedId, tripId, serviceDate);
// Try to cancel previously added trip
final boolean cancelPreviouslyAddedSuccess = cancelPreviouslyAddedTrip(new FeedScopedId(feedId, tripId), serviceDate);
if (cancelScheduledSuccess || cancelPreviouslyAddedSuccess) {
success = true;
} else {
LOG.warn("No pattern found for tripId {}, skipping TripUpdate.", tripId);
}
} else {
LOG.warn("No trip id in CANCELED trip update, skipping TripUpdate.");
}
return success;
}
private boolean purgeExpiredData() {
final ServiceDate today = new ServiceDate();
// TODO: Base this on numberOfDaysOfLongestTrip for tripPatterns
final ServiceDate previously = today.previous().previous(); // Just to be safe...
// Purge data only if we have changed date
if(lastPurgeDate != null && lastPurgeDate.compareTo(previously) >= 0) {
return false;
}
LOG.debug("purging expired realtime data");
lastPurgeDate = previously;
return buffer.purgeExpiredData(previously);
}
/**
* Retrieve a trip pattern given a feed id and trip id.
*
* @param feedId feed id for the trip id
* @param tripId trip id without agency
* @return trip pattern or null if no trip pattern was found
*/
private TripPattern getPatternForTripId(String feedId, String tripId) {
Trip trip = routingService.getTripForId().get(new FeedScopedId(feedId, tripId));
TripPattern pattern = routingService.getPatternForTrip().get(trip);
return pattern;
}
/**
* Retrieve route given a route id without an agency
*
* @param feedId feed id for the route id
* @param routeId route id without the agency
* @return route or null if route can't be found in graph index
*/
private Route getRouteForRouteId(String feedId, String routeId) {
return routingService.getRouteForId(new FeedScopedId(feedId, routeId));
}
/**
* Retrieve trip given a trip id without an agency
*
* @param feedId feed id for the trip id
* @param tripId trip id without the agency
* @return trip or null if trip can't be found in graph index
*/
private Trip getTripForTripId(String feedId, String tripId) {
return routingService.getTripForId().get(new FeedScopedId(feedId, tripId));
}
/**
* Retrieve stop given a feed id and stop id.
*
* @param feedId feed id for the stop id
* @param stopId trip id without the agency
* @return stop or null if stop doesn't exist
*/
private StopLocation getStopForStopId(String feedId, String stopId) {
return routingService.getStopForId(new FeedScopedId(feedId, stopId));
}
}