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

org.opentripplanner.updater.stoptime.TimetableSnapshotSource Maven / Gradle / Ivy

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.Stop;
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.raptor.transit.TransitLayer;
import org.opentripplanner.routing.algorithm.raptor.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;

/**
 * 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.scheduledTimetable.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 List 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 Stop 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
            route = new Route();
            // Use route id of trip descriptor if available
            if (tripUpdate.getTrip().hasRouteId()) {
                route.setId(new FeedScopedId(feedId, tripUpdate.getTrip().getRouteId()));
            } else {
                route.setId(new FeedScopedId(feedId, tripId));
            }
            // 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.setType(3);
            route.setMode(TransitMode.BUS);
            // Create route name
            route.setLongName(tripId);
        }

        // Create new Trip

        final Trip trip = new Trip();
        // TODO: which Agency ID to use? Currently use feed id.
        trip.setId(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(feedId, 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 String feedId, 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 Stop 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(1); // No pickup available
                } else {
                    stopTime.setPickupType(0); // Regularly scheduled pickup
                }
                // Set drop off type
                // Set different drop off type for first stop
                if (index == 0) {
                    stopTime.setDropOffType(1); // No drop off available
                } else {
                    stopTime.setDropOffType(0); // 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.serviceCode = 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.scheduledTimetable;
            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.cancel();
                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.cancel();
                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
        List 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(feedId, 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();
        final ServiceDate previously = today.previous().previous(); // Just to be safe...

        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 Stop getStopForStopId(String feedId, String stopId) {
        return routingService.getStopForId(new FeedScopedId(feedId, stopId));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy