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

org.opentripplanner.ext.siri.SiriTimetableSnapshotSource Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
package org.opentripplanner.ext.siri;

import com.google.common.base.Preconditions;
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.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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.org.siri.siri20.ArrivalBoardingActivityEnumeration;
import uk.org.siri.siri20.DepartureBoardingActivityEnumeration;
import uk.org.siri.siri20.EstimatedCall;
import uk.org.siri.siri20.EstimatedTimetableDeliveryStructure;
import uk.org.siri.siri20.EstimatedVehicleJourney;
import uk.org.siri.siri20.EstimatedVersionFrameStructure;
import uk.org.siri.siri20.NaturalLanguageStringStructure;
import uk.org.siri.siri20.RecordedCall;
import uk.org.siri.siri20.VehicleActivityCancellationStructure;
import uk.org.siri.siri20.VehicleActivityStructure;
import uk.org.siri.siri20.VehicleModesEnumeration;
import uk.org.siri.siri20.VehicleMonitoringDeliveryStructure;

import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.locks.ReentrantLock;

import static org.opentripplanner.ext.siri.TimetableHelper.createModifiedStopTimes;
import static org.opentripplanner.ext.siri.TimetableHelper.createModifiedStops;
import static org.opentripplanner.ext.siri.TimetableHelper.createUpdatedTripTimes;
import static org.opentripplanner.model.StopPattern.PICKDROP_NONE;
import static org.opentripplanner.model.StopPattern.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 SiriTimetableSnapshotSource implements TimetableSnapshotProvider {
    private static final Logger LOG = LoggerFactory.getLogger(SiriTimetableSnapshotSource.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 SiriTripPatternCache tripPatternCache = new SiriTripPatternCache();

    /** Should expired realtime data be purged from the graph. */
    public boolean purgeExpiredData = true;

    protected ServiceDate lastPurgeDate = null;

    protected long lastSnapshotTime = -1;

    private final TimeZone timeZone;

    private final RoutingService routingService;

    public SiriFuzzyTripMatcher siriFuzzyTripMatcher;

    private TransitLayer realtimeTransitLayer;

    private TransitLayerUpdater transitLayerUpdater;

    public SiriTimetableSnapshotSource(final Graph graph) {
        timeZone = graph.getTimeZone();
        routingService = new RoutingService(graph);
        realtimeTransitLayer = graph.getRealtimeTransitLayer();
        transitLayerUpdater = graph.transitLayerUpdater;

        siriFuzzyTripMatcher = new SiriFuzzyTripMatcher(routingService);
    }

    /**
     * @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.
     *
     *
     * @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 SIRI VehicleMonitoringDeliveries that should be applied atomically
     */
    public void applyVehicleMonitoring(final Graph graph, final String feedId, final boolean fullDataset, final List updates) {
        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);
            }

            for (VehicleMonitoringDeliveryStructure vmDelivery : updates) {

                ServiceDate serviceDate = new ServiceDate();

                List activities = vmDelivery.getVehicleActivities();
                if (activities != null) {
                    //Handle activities
                    LOG.info("Handling {} VM-activities.", activities.size());
                    int handledCounter = 0;
                    int skippedCounter = 0;
                    for (VehicleActivityStructure activity : activities) {
                        boolean handled = handleModifiedTrip(graph, feedId, activity, serviceDate);
                        if (handled) {
                            handledCounter++;
                        } else {
                            skippedCounter++;
                        }
                    }
                    LOG.info("Applied {} VM-activities, skipped {}.", handledCounter, skippedCounter);
                }
                List cancellations = vmDelivery.getVehicleActivityCancellations();
                if (cancellations != null && !cancellations.isEmpty()) {
                    //Handle cancellations
                    LOG.info("TODO: Handle {} cancellations.", cancellations.size());
                }

                List notes = vmDelivery.getVehicleActivityNotes();
                if (notes != null && !notes.isEmpty()) {
                    //Handle notes
                    LOG.info("TODO: Handle {} notes.", notes.size());
                }

            }

            // 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();
            if (keepLogging) {
                LOG.info("Reducing SIRI-VM logging until restart");
                keepLogging = false;
            }
        }
    }

    /**
     * Method to apply a trip update list to the most recent version of the timetable snapshot.
     *
     *  @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 SIRI VehicleMonitoringDeliveries that should be applied atomically
     */
    public void applyEstimatedTimetable(final Graph graph, final String feedId, final boolean fullDataset, final List updates) {
        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);
            }

            for (EstimatedTimetableDeliveryStructure etDelivery : updates) {

                List estimatedJourneyVersions = etDelivery.getEstimatedJourneyVersionFrames();
                if (estimatedJourneyVersions != null) {
                    //Handle deliveries
                    for (EstimatedVersionFrameStructure estimatedJourneyVersion : estimatedJourneyVersions) {
                        List journeys = estimatedJourneyVersion.getEstimatedVehicleJourneies();
                        LOG.info("Handling {} EstimatedVehicleJourneys.", journeys.size());
                        int handledCounter = 0;
                        int skippedCounter = 0;
                        int addedCounter = 0;
                        int notMonitoredCounter = 0;
                        for (EstimatedVehicleJourney journey : journeys) {
                            if (journey.isExtraJourney() != null && journey.isExtraJourney()) {
                                // Added trip
                                try {
                                    if (handleAddedTrip(graph, feedId, journey)) {
                                        addedCounter++;
                                    } else {
                                        skippedCounter++;
                                    }
                                } catch (Throwable t) {
                                    // Since this is work in progress - catch everything to continue processing updates
                                    LOG.warn("Adding ExtraJourney with id='{}' failed, caused by '{}'.", journey.getEstimatedVehicleJourneyCode(), t.getMessage());
                                    skippedCounter++;
                                }
                            } else {
                                // Updated trip
                                if (handleModifiedTrip(graph, feedId, journey)) {
                                    handledCounter++;
                                } else {
                                    if (journey.isMonitored() != null && !journey.isMonitored()) {
                                        notMonitoredCounter++;
                                    } else {
                                        skippedCounter++;
                                    }
                                }
                            }
                        }
                        LOG.info("Processed EstimatedVehicleJourneys: updated {}, added {}, skipped {}, not monitored {}.", handledCounter, addedCounter, skippedCounter, notMonitoredCounter);
                    }
                }
            }

            LOG.debug("message contains {} trip updates", updates.size());
            int uIndex = 0;
            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();
        }
    }

    /**
     * Returns any new TripPatterns added by real time information for a given stop.
     *
     * @param stop the stop
     * @return list of TripPatterns created by real time sources for the stop.
     */
    public List getAddedTripPatternsForStop(Stop stop) {
        return tripPatternCache.getAddedTripPatternsForStop(stop);
    }

    private static boolean keepLogging = true;

    private boolean handleModifiedTrip(Graph graph, String feedId, VehicleActivityStructure activity, ServiceDate serviceDate) {
        if (activity.getValidUntilTime().isBefore(ZonedDateTime.now())) {
            //Activity has expired
            return false;
        }


        if (activity.getMonitoredVehicleJourney() == null ||
                activity.getMonitoredVehicleJourney().getVehicleRef() == null ||
                activity.getMonitoredVehicleJourney().getLineRef() == null) {
            //No vehicle reference or line reference
            return false;
        }

        Boolean isMonitored = activity.getMonitoredVehicleJourney().isMonitored();
        if (isMonitored != null && !isMonitored) {
            //Vehicle is reported as NOT monitored
            return false;
        }

        Set trips = siriFuzzyTripMatcher.match(activity);

        if (trips == null || trips.isEmpty()) {
            if (keepLogging) {
                String lineRef = (activity.getMonitoredVehicleJourney().getLineRef() != null ? activity.getMonitoredVehicleJourney().getLineRef().getValue():null);
                String vehicleRef = (activity.getMonitoredVehicleJourney().getVehicleRef() != null ? activity.getMonitoredVehicleJourney().getVehicleRef().getValue():null);
                String tripId =  (activity.getMonitoredVehicleJourney().getCourseOfJourneyRef() != null ? activity.getMonitoredVehicleJourney().getCourseOfJourneyRef().getValue():null);
                LOG.debug("No trip found for [isMonitored={}, lineRef={}, vehicleRef={}, tripId={}], skipping VehicleActivity.", isMonitored, lineRef, vehicleRef, tripId);
            }

            return false;
        }

        //Find the trip that best corresponds to MonitoredVehicleJourney
        Trip trip = getTripForJourney(trips, activity.getMonitoredVehicleJourney());

        if (trip == null) {
            return false;
        }

        final Set patterns = getPatternsForTrip(trips, activity.getMonitoredVehicleJourney());

        if (patterns == null) {
            return false;
        }
        boolean success = false;
        for (TripPattern pattern : patterns) {
            if (handleTripPatternUpdate(graph, pattern, activity, trip, serviceDate)) {
                success = true;
            }
        }

        if (!success) {
            LOG.info("Pattern not updated for trip " + trip.getId());
        }
        return success;
    }

    private boolean handleTripPatternUpdate(Graph graph, TripPattern pattern, VehicleActivityStructure activity, Trip trip, ServiceDate serviceDate) {

        // Apply update on the *scheduled* time table and set the updated trip times in the buffer
        Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate);
        final TripTimes updatedTripTimes = createUpdatedTripTimes(currentTimetable, graph, activity, timeZone, trip.getId());
        if (updatedTripTimes == null) {
            return false;
        }

        final boolean success = buffer.update(pattern, updatedTripTimes, serviceDate);

        return success;
    }

    /**
     * Get the latest timetable for TripPattern for a given service date.
     *
     * Snapshot timetable is used as source if initialised, trip patterns scheduled timetable if not.
     *
     */
    private Timetable getCurrentTimetable(TripPattern tripPattern, ServiceDate serviceDate) {
        TimetableSnapshot timetableSnapshot=getTimetableSnapshot();
        if (timetableSnapshot!=null) {
            return getTimetableSnapshot().resolve(tripPattern, serviceDate);
        }
        return tripPattern.scheduledTimetable;
    }

    private boolean handleAddedTrip(Graph graph, String feedId,  EstimatedVehicleJourney estimatedVehicleJourney) {

        // Verifying values required in SIRI Profile

        // Added ServiceJourneyId
        String newServiceJourneyRef = estimatedVehicleJourney.getEstimatedVehicleJourneyCode();
        Preconditions.checkNotNull(newServiceJourneyRef, "EstimatedVehicleJourneyCode is required");

        // Replaced/duplicated ServiceJourneyId
//        VehicleJourneyRef existingServiceJourneyRef = estimatedVehicleJourney.getVehicleJourneyRef();
//        Preconditions.checkNotNull(existingServiceJourneyRef, "VehicleJourneyRef is required");

        // LineRef of added trip
        Preconditions.checkNotNull(estimatedVehicleJourney.getLineRef(), "LineRef is required");
        String lineRef = estimatedVehicleJourney.getLineRef().getValue();

        //OperatorRef of added trip
        Preconditions.checkNotNull(estimatedVehicleJourney.getOperatorRef(), "OperatorRef is required");
        String operatorRef = estimatedVehicleJourney.getOperatorRef().getValue();

        //Required in SIRI, but currently not in use by OTP
//        Preconditions.checkNotNull(estimatedVehicleJourney.getRouteRef(), "RouteRef is required");
//        String routeRef = estimatedVehicleJourney.getRouteRef().getValue();

//        Preconditions.checkNotNull(estimatedVehicleJourney.getGroupOfLinesRef(), "GroupOfLinesRef is required");
//        String groupOfLines = estimatedVehicleJourney.getGroupOfLinesRef().getValue();

//        Preconditions.checkNotNull(estimatedVehicleJourney.getExternalLineRef(), "ExternalLineRef is required");
        String externalLineRef = estimatedVehicleJourney.getExternalLineRef().getValue();

        // TODO - SIRI: Where is the Operator?
//        Operator operator = graphIndex.operatorForId.get(new FeedScopedId(feedId, operatorRef));
//        Preconditions.checkNotNull(operator, "Operator " + operatorRef + " is unknown");

        FeedScopedId tripId = new FeedScopedId(feedId, newServiceJourneyRef);
        FeedScopedId serviceId = new FeedScopedId(feedId, newServiceJourneyRef);

        Route replacedRoute = null;
        if (externalLineRef != null) {
            replacedRoute = graph.index.getRouteForId(new FeedScopedId(feedId, externalLineRef));
        }

        FeedScopedId routeId = new FeedScopedId(feedId, lineRef);
        Route route = graph.index.getRouteForId(routeId);

        if (route == null) { // Route is unknown - create new
            route = new Route();
            route.setId(routeId);
            route.setType(getRouteType(estimatedVehicleJourney.getVehicleModes()));
//            route.setOperator(operator);

            // TODO - SIRI: Is there a better way to find authority/Agency?
            // Finding first Route with same Operator, and using same Authority
            Agency agency = graph.index.getAllRoutes().stream()
//                    .filter(route1 -> route1 != null &&
//                            route1.getOperator() != null &&
//                            route1.getOperator().equals(operator))
                    .findFirst().get().getAgency();
            route.setAgency(agency);

            if (estimatedVehicleJourney.getPublishedLineNames() != null && !estimatedVehicleJourney.getPublishedLineNames().isEmpty()) {
                route.setShortName("" + estimatedVehicleJourney.getPublishedLineNames().get(0).getValue());
            }
            LOG.info("Adding route {} to graph.", routeId);
            graph.index.addRoutes(route);
        }

        Trip trip = new Trip();
        trip.setId(tripId);
        trip.setRoute(route);

        // TODO - SIRI: Set transport-submode based on replaced- and replacement-route
        if (replacedRoute != null) {
            if (replacedRoute.getType() >= 100 && replacedRoute.getType() < 200) { // Replaced-route is RAIL
                if (route.getType() == 100) {
                    // Replacement-route is also RAIL
//                    trip.setTransportSubmode(TransmodelTransportSubmode.REPLACEMENT_RAIL_SERVICE);
                } else if (route.getType() == 700) {
                    // Replacement-route is BUS
//                    trip.setTransportSubmode(TransmodelTransportSubmode.RAIL_REPLACEMENT_BUS);
                }
            }
        }

        trip.setServiceId(serviceId);

        // TODO - SIRI: PublishedLineName not defined in SIRI-profile
        if (estimatedVehicleJourney.getPublishedLineNames() != null && !estimatedVehicleJourney.getPublishedLineNames().isEmpty()) {
            trip.setRouteShortName("" + estimatedVehicleJourney.getPublishedLineNames().get(0).getValue());
        }

//        trip.setTripOperator(operator);

        // TODO - SIRI: Populate these?
        trip.setShapeId(null);          // Replacement-trip has different shape
//        trip.setTripPrivateCode(null);
//        trip.setTripPublicCode(null);
        trip.setBlockId(null);
        trip.setTripShortName(null);
        trip.setTripHeadsign(null);
//        trip.setKeyValues(null);

        List addedStops = new ArrayList<>();
        List aimedStopTimes = new ArrayList<>();

        List estimatedCalls = estimatedVehicleJourney.getEstimatedCalls().getEstimatedCalls();
        for (int i = 0; i < estimatedCalls.size(); i++) {
            EstimatedCall estimatedCall = estimatedCalls.get(i);

            Stop stop = getStopForStopId(feedId,estimatedCall.getStopPointRef().getValue());

            StopTime stopTime = new StopTime();
            stopTime.setStop(stop);
            stopTime.setStopSequence(i);
            stopTime.setTrip(trip);

            ZonedDateTime aimedArrivalTime = estimatedCall.getAimedArrivalTime();
            ZonedDateTime aimedDepartureTime = estimatedCall.getAimedDepartureTime();

            if (aimedArrivalTime != null) {
                stopTime.setArrivalTime(calculateSecondsSinceMidnight(aimedArrivalTime));
            }
            if (aimedDepartureTime != null) {
                stopTime.setDepartureTime(calculateSecondsSinceMidnight(aimedDepartureTime));
            }

            if (estimatedCall.getArrivalBoardingActivity() == ArrivalBoardingActivityEnumeration.ALIGHTING) {
                stopTime.setDropOffType(PICKDROP_SCHEDULED);
            } else {
                stopTime.setDropOffType(PICKDROP_NONE);
            }

            if (estimatedCall.getDepartureBoardingActivity() == DepartureBoardingActivityEnumeration.BOARDING) {
                stopTime.setPickupType(PICKDROP_SCHEDULED);
            } else {
                stopTime.setPickupType(PICKDROP_NONE);
            }

            if (estimatedCall.getDestinationDisplaies() != null && !estimatedCall.getDestinationDisplaies().isEmpty()) {
                NaturalLanguageStringStructure destinationDisplay = estimatedCall.getDestinationDisplaies().get(0);
                stopTime.setStopHeadsign(destinationDisplay.getValue());
            }

            if (i == 0) {
                // Fake arrival on first stop
                stopTime.setArrivalTime(stopTime.getDepartureTime());
            } else if (i == (estimatedCalls.size() - 1)) {
                // Fake departure from last stop
                stopTime.setDepartureTime(stopTime.getArrivalTime());
            }

            addedStops.add(stop);
            aimedStopTimes.add(stopTime);
        }

        StopPattern stopPattern = new StopPattern(aimedStopTimes);
        TripPattern pattern = new TripPattern(trip.getRoute(), stopPattern);

        TripTimes tripTimes = new TripTimes(trip, aimedStopTimes, graph.deduplicator);

        boolean isJourneyPredictionInaccurate = (estimatedVehicleJourney.isPredictionInaccurate() != null && estimatedVehicleJourney.isPredictionInaccurate());

        // If added trip is updated with realtime - loop through and add delays
        for (int i = 0; i < estimatedCalls.size(); i++) {
            EstimatedCall estimatedCall = estimatedCalls.get(i);
            ZonedDateTime expectedArrival = estimatedCall.getExpectedArrivalTime();
            ZonedDateTime expectedDeparture = estimatedCall.getExpectedDepartureTime();

            int aimedArrivalTime = aimedStopTimes.get(i).getArrivalTime();
            int aimedDepartureTime = aimedStopTimes.get(i).getDepartureTime();

            if (expectedArrival != null) {
                int expectedArrivalTime = calculateSecondsSinceMidnight(expectedArrival);
                tripTimes.updateArrivalDelay(i, expectedArrivalTime - aimedArrivalTime);
            }
            if (expectedDeparture != null) {
                int expectedDepartureTime = calculateSecondsSinceMidnight(expectedDeparture);
                tripTimes.updateDepartureDelay(i, expectedDepartureTime - aimedDepartureTime);
            }

            if (estimatedCall.isCancellation() != null) {
                tripTimes.setCancelledStop(i,  estimatedCall.isCancellation());
            }

            boolean isCallPredictionInaccurate = estimatedCall.isPredictionInaccurate() != null && estimatedCall.isPredictionInaccurate();
            tripTimes.setPredictionInaccurate(i, (isJourneyPredictionInaccurate | isCallPredictionInaccurate));

            if (i == 0) {
                // Fake arrival on first stop
                tripTimes.updateArrivalTime(i,tripTimes.getDepartureTime(i));
            } else if (i == (estimatedCalls.size() - 1)) {
                // Fake departure from last stop
                tripTimes.updateDepartureTime(i,tripTimes.getArrivalTime(i));
            }
        }

        // Adding trip to index necessary to include values in graphql-queries
        // TODO - SIRI: should more data be added to index?
        graph.index.getTripForId().put(tripId, trip);
        graph.index.getPatternForTrip().put(trip, pattern);

        if (estimatedVehicleJourney.isCancellation() != null && estimatedVehicleJourney.isCancellation()) {
            tripTimes.cancel();
        } else {
            tripTimes.setRealTimeState(RealTimeState.ADDED);
        }


        if (!graph.getServiceCodes().containsKey(serviceId)) {
            graph.getServiceCodes().put(serviceId, graph.getServiceCodes().size());
        }
        tripTimes.serviceCode = graph.getServiceCodes().get(serviceId);

        pattern.add(tripTimes);

        Preconditions.checkState(tripTimes.timesIncreasing(), "Non-increasing triptimes for added trip");

        ServiceDate serviceDate = getServiceDateForEstimatedVehicleJourney(estimatedVehicleJourney);

        if (graph.getCalendarService().getServiceDatesForServiceId(serviceId) == null ||
                graph.getCalendarService().getServiceDatesForServiceId(serviceId).isEmpty()) {
            LOG.info("Adding serviceId {} to CalendarService", serviceId);
            // TODO - SIRI: Need to add the ExtraJourney as a Trip - alerts may be attached to it
//           graph.getCalendarService().addServiceIdAndServiceDates(serviceId, Arrays.asList(serviceDate));
        }


        return addTripToGraphAndBuffer(feedId, graph, trip, aimedStopTimes, addedStops, tripTimes, serviceDate);
    }

    /*
     * Resolves TransportMode from SIRI VehicleMode
     */
    private int getRouteType(List vehicleModes) {
        if (vehicleModes != null && !vehicleModes.isEmpty()) {
            VehicleModesEnumeration vehicleModesEnumeration = vehicleModes.get(0);
            switch (vehicleModesEnumeration) {
                case RAIL:
                    return 100;
                case COACH:
                    return 200;
                case BUS:
                    return 700;
                case METRO:
                    return 701;
                case TRAM:
                    return 900;
                case FERRY:
                    return 1000;
                case AIR:
                    return 1100;
            }
        }
        return 700;
    }

    private boolean handleModifiedTrip(Graph graph, String feedId, EstimatedVehicleJourney estimatedVehicleJourney) {

        //Check if EstimatedVehicleJourney is reported as NOT monitored
        if (estimatedVehicleJourney.isMonitored() != null && !estimatedVehicleJourney.isMonitored()) {
            //Ignore the notMonitored-flag if the journey is NOT monitored because it has been cancelled
            if (estimatedVehicleJourney.isCancellation() != null && !estimatedVehicleJourney.isCancellation()) {
                return false;
            }
        }

        //Values used in logging
        String operatorRef = (estimatedVehicleJourney.getOperatorRef() != null ? estimatedVehicleJourney.getOperatorRef().getValue() : null);
        String vehicleModes = "" + estimatedVehicleJourney.getVehicleModes();
        String lineRef = estimatedVehicleJourney.getLineRef().getValue();
        String vehicleRef = (estimatedVehicleJourney.getVehicleRef() != null ? estimatedVehicleJourney.getVehicleRef().getValue() : null);

        ServiceDate serviceDate = getServiceDateForEstimatedVehicleJourney(estimatedVehicleJourney);

        if (serviceDate == null) {
            return false;
        }

        Set times = new HashSet<>();
        Set patterns = new HashSet<>();

        Trip tripMatchedByServiceJourneyId = siriFuzzyTripMatcher.findTripByDatedVehicleJourneyRef(estimatedVehicleJourney);

        if (tripMatchedByServiceJourneyId != null) {
            /*
              Found exact match
             */
            TripPattern exactPattern = routingService.getPatternForTrip().get(tripMatchedByServiceJourneyId);

            if (exactPattern != null) {
                Timetable currentTimetable = getCurrentTimetable(exactPattern, serviceDate);
                TripTimes exactUpdatedTripTimes = createUpdatedTripTimes(graph, currentTimetable, estimatedVehicleJourney, timeZone, tripMatchedByServiceJourneyId.getId());
                if (exactUpdatedTripTimes != null) {
                    times.add(exactUpdatedTripTimes);
                    patterns.add(exactPattern);
                } else {
                    LOG.info("Failed to update TripTimes for trip found by exact match {}", tripMatchedByServiceJourneyId.getId());
                    return false;
                }
            }
        } else {
            /*
                No exact match found - search for trips based on arrival-times/stop-patterns
             */
            Set trips = siriFuzzyTripMatcher.match(estimatedVehicleJourney);

            if (trips == null || trips.isEmpty()) {
                LOG.debug("No trips found for EstimatedVehicleJourney. [operator={}, vehicleModes={}, lineRef={}, vehicleRef={}]", operatorRef, vehicleModes, lineRef, vehicleRef);
                return false;
            }

            //Find the trips that best corresponds to EstimatedVehicleJourney
            Set matchingTrips = getTripForJourney(trips, estimatedVehicleJourney);

            if (matchingTrips == null || matchingTrips.isEmpty()) {
                LOG.debug("Found no matching trip for SIRI ET (serviceDate, departureTime). [operator={}, vehicleModes={}, lineRef={}, vehicleJourneyRef={}]", operatorRef, vehicleModes, lineRef, vehicleRef);
                return false;
            }

            for (Trip matchingTrip : matchingTrips) {
                TripPattern pattern = getPatternForTrip(matchingTrip, estimatedVehicleJourney);
                if (pattern != null) {
                    Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate);
                    TripTimes updatedTripTimes = createUpdatedTripTimes(graph, currentTimetable, estimatedVehicleJourney, timeZone, matchingTrip.getId());
                    if (updatedTripTimes != null) {
                        patterns.add(pattern);
                        times.add(updatedTripTimes);
                    }
                }
            }
        }

        if (patterns.isEmpty()) {
            LOG.debug("Found no matching pattern for SIRI ET (firstStopId, lastStopId, numberOfStops). [operator={}, vehicleModes={}, lineRef={}, vehicleRef={}]", operatorRef, vehicleModes, lineRef, vehicleRef);
            return false;
        }

        if (times.isEmpty()) {
            return false;
        }

        boolean result = false;
        for (TripTimes tripTimes : times) {
            Trip trip = tripTimes.trip;
            for (TripPattern pattern : patterns) {
                if (tripTimes.getNumStops() == pattern.stopPattern.stops.length) {
                    if (!tripTimes.isCanceled()) {
                        /*
                          UPDATED and MODIFIED tripTimes should be handled the same way to always allow latest realtime-update
                          to replace previous update regardless of realtimestate
                         */

                        cancelScheduledTrip(feedId, trip.getId().getId(), serviceDate);

                        // Check whether trip id has been used for previously ADDED/MODIFIED trip message and cancel
                        // previously created trip
                        cancelPreviouslyAddedTrip(feedId, trip.getId().getId(), serviceDate);

                        // Calculate modified stop-pattern
                        Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate);
                        List modifiedStops = createModifiedStops(currentTimetable, estimatedVehicleJourney,
                            routingService
                        );
                        List modifiedStopTimes = createModifiedStopTimes(currentTimetable, tripTimes, estimatedVehicleJourney, trip,
                            routingService
                        );

                        if (modifiedStops != null && modifiedStops.isEmpty()) {
                            tripTimes.cancel();
                        } else {
                            // Add new trip
                            result = result | addTripToGraphAndBuffer(feedId, graph, trip, modifiedStopTimes, modifiedStops, tripTimes, serviceDate);
                        }
                    } else {
                        result = result | buffer.update(pattern, tripTimes, serviceDate);
                    }

                    LOG.debug("Applied realtime data for trip {}", trip.getId().getId());
                } else {
                    LOG.debug("Ignoring update since number of stops do not match");
                }
            }
        }

        return result;
    }

    private ServiceDate getServiceDateForEstimatedVehicleJourney(EstimatedVehicleJourney estimatedVehicleJourney) {
        ZonedDateTime date;
        if (estimatedVehicleJourney.getRecordedCalls() != null && !estimatedVehicleJourney.getRecordedCalls().getRecordedCalls().isEmpty()){
            date = estimatedVehicleJourney.getRecordedCalls().getRecordedCalls().get(0).getAimedDepartureTime();
        } else {
            EstimatedCall firstCall = estimatedVehicleJourney.getEstimatedCalls().getEstimatedCalls().get(0);
            date = firstCall.getAimedDepartureTime();
        }

        if (date == null) {
            return null;
        }


        return new ServiceDate(date.getYear(), date.getMonthValue(), date.getDayOfMonth());
    }

    private int calculateSecondsSinceMidnight(ZonedDateTime dateTime) {
        return dateTime.toLocalTime().toSecondOfDay();
    }


    /**
     * Add a (new) trip to the graph and the buffer
     *
     * @return true if successful
     */
    private boolean addTripToGraphAndBuffer(final String feedId, final Graph graph, final Trip trip,
                                            final List stopTimes, final List stops, TripTimes updatedTripTimes,
                                            final ServiceDate serviceDate) {

        // Preconditions
        Preconditions.checkNotNull(stops);
        Preconditions.checkArgument(stopTimes.size() == stops.size(),
                "number of stop should match the number of stop time updates");

        // 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, serviceDate);

        // 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);
        }


        /*
         * Update pattern with triptimes so get correct dwell times and lower bound on running times.
         * New patterns only affects a single trip, previously added tripTimes is no longer valid, and is therefore removed
         */
        pattern.scheduledTimetable.tripTimes.clear();
        pattern.scheduledTimetable.addTripTimes(updatedTripTimes);
        pattern.scheduledTimetable.finish();

        // Remove trip times to avoid real time trip times being visible for ignoreRealtimeInformation queries
        pattern.scheduledTimetable.tripTimes.clear();

        // Add to buffer as-is to include it in the 'lastAddedTripPattern'
        buffer.update(pattern, updatedTripTimes, serviceDate);

        //TODO - SIRI: Add pattern to index?

        // Add new trip times to the buffer
        final boolean success = buffer.update(pattern, updatedTripTimes, 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
     *
     * @param feedId feed id the trip id belongs to
     * @param tripId trip id without agency id
     * @param serviceDate service date
     * @return true if a previously added trip was cancelled
     */
    private boolean cancelPreviouslyAddedTrip(final String feedId, final String tripId, final ServiceDate serviceDate) {
        boolean success = false;

        final TripPattern pattern = buffer.getLastAddedTripPattern(new FeedScopedId(feedId, 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);
//                buffer.removeLastAddedTripPattern(feedId, tripId, serviceDate);
                success = true;
            }
        }

        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 trid 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));
        return routingService.getPatternForTrip().get(trip);
    }

    private Set getPatternsForTrip(Set matches, VehicleActivityStructure.MonitoredVehicleJourney monitoredVehicleJourney) {

        if (monitoredVehicleJourney.getOriginRef() == null) {
            return null;
        }

        ZonedDateTime date = monitoredVehicleJourney.getOriginAimedDepartureTime();
        if (date == null) {
            //If no date is set - assume Realtime-data is reported for 'today'.
            date = ZonedDateTime.now();
        }
        ServiceDate realTimeReportedServiceDate = new ServiceDate(date.getYear(), date.getMonthValue(), date.getDayOfMonth());

        Set patterns = new HashSet<>();
        for (Iterator iterator = matches.iterator(); iterator.hasNext(); ) {
            Trip currentTrip = iterator.next();
            TripPattern tripPattern = routingService.getPatternForTrip().get(currentTrip);
            Set serviceDates = routingService.getCalendarService().getServiceDatesForServiceId(currentTrip.getServiceId());

            if (!serviceDates.contains(realTimeReportedServiceDate)) {
                // Current trip has no service on the date of the 'MonitoredVehicleJourney'
                continue;
            }

            Stop firstStop = tripPattern.getStop(0);
            Stop lastStop = tripPattern.getStop(tripPattern.getStops().size() - 1);

            String siriOriginRef = monitoredVehicleJourney.getOriginRef().getValue();

            if (monitoredVehicleJourney.getDestinationRef() != null) {
                String siriDestinationRef = monitoredVehicleJourney.getDestinationRef().getValue();

                boolean firstStopIsMatch = firstStop.getId().getId().equals(siriOriginRef);
                boolean lastStopIsMatch = lastStop.getId().getId().equals(siriDestinationRef);

                if (!firstStopIsMatch && firstStop.isPartOfStation()) {
                    Stop otherFirstStop = routingService.getStopForId(
                            new FeedScopedId(firstStop.getId().getFeedId(), siriOriginRef)
                    );
                    firstStopIsMatch = firstStop.isPartOfSameStationAs(otherFirstStop);
                }

                if (!lastStopIsMatch && lastStop.isPartOfStation()) {
                    Stop otherLastStop = routingService.getStopForId(
                            new FeedScopedId(lastStop.getId().getFeedId(), siriDestinationRef)
                    );
                    lastStopIsMatch = lastStop.isPartOfSameStationAs(otherLastStop);
                }

                if (firstStopIsMatch & lastStopIsMatch) {
                    // Origin and destination matches
                    TripPattern lastAddedTripPattern = buffer.getLastAddedTripPattern(currentTrip.getId(), realTimeReportedServiceDate);
                    if (lastAddedTripPattern != null) {
                        patterns.add(lastAddedTripPattern);
                    } else {
                        patterns.add(tripPattern);
                    }
                }
            } else {
                //Match origin only - since destination is not defined
                if (firstStop.getId().getId().equals(siriOriginRef)) {
                    tripPattern.scheduledTimetable.tripTimes.get(0).getDepartureTime(0); // TODO does this line do anything?
                    patterns.add(tripPattern);
                }
            }


        }
        return patterns;
    }

    private Set getPatternForTrip(Set trips, EstimatedVehicleJourney journey) {
        Set patterns = new HashSet<>();
        for (Trip trip : trips) {
            TripPattern pattern = getPatternForTrip(trip, journey);
            if (pattern != null) {
                patterns.add(pattern);
            }
        }
        return patterns;
    }
    private TripPattern getPatternForTrip(Trip trip, EstimatedVehicleJourney journey) {

        Set serviceDates = routingService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId());

        List recordedCalls = (journey.getRecordedCalls() != null ? journey.getRecordedCalls().getRecordedCalls():new ArrayList<>());
        List estimatedCalls = journey.getEstimatedCalls().getEstimatedCalls();

            String journeyFirstStopId;
            ServiceDate journeyDate;
            if (recordedCalls != null && !recordedCalls.isEmpty()) {
                RecordedCall recordedCall = recordedCalls.get(0);
                journeyFirstStopId = recordedCall.getStopPointRef().getValue();
                journeyDate = new ServiceDate(Date.from(recordedCall.getAimedDepartureTime().toInstant()));
            } else if (estimatedCalls != null && !estimatedCalls.isEmpty()) {
                EstimatedCall estimatedCall = estimatedCalls.get(0);
                journeyFirstStopId = estimatedCall.getStopPointRef().getValue();
                journeyDate = new ServiceDate(Date.from(estimatedCall.getAimedDepartureTime().toInstant()));
            } else {
                return null;
            }

            String journeyLastStopId = estimatedCalls.get(estimatedCalls.size() - 1).getStopPointRef().getValue();


            TripPattern lastAddedTripPattern = null;
            if (getTimetableSnapshot() != null) {
                lastAddedTripPattern  = getTimetableSnapshot().getLastAddedTripPattern(trip.getId(), journeyDate);
            }

            TripPattern tripPattern;
            if (lastAddedTripPattern != null) {
                tripPattern = lastAddedTripPattern;
            } else {
                tripPattern = routingService.getPatternForTrip().get(trip);
            }


            Stop firstStop = tripPattern.getStop(0);
            Stop lastStop = tripPattern.getStop(tripPattern.getStops().size() - 1);

            if (serviceDates.contains(journeyDate)) {
                boolean firstStopIsMatch = firstStop.getId().getId().equals(journeyFirstStopId);
                boolean lastStopIsMatch = lastStop.getId().getId().equals(journeyLastStopId);

                if (!firstStopIsMatch && firstStop.isPartOfStation()) {
                    Stop otherFirstStop = routingService
                        .getStopForId(
                                new FeedScopedId(firstStop.getId().getFeedId(), journeyFirstStopId)
                        );
                    firstStopIsMatch = firstStop.isPartOfSameStationAs(otherFirstStop);
                }

                if (!lastStopIsMatch && lastStop.isPartOfStation()) {
                    Stop otherLastStop = routingService
                        .getStopForId(
                                new FeedScopedId(lastStop.getId().getFeedId(), journeyLastStopId)
                        );
                    lastStopIsMatch = lastStop.isPartOfSameStationAs(otherLastStop);
                }

                if (firstStopIsMatch & lastStopIsMatch) {
                    // Found matches
                    return tripPattern;
                }

                return null;
            }

        return null;
    }

    /**
     * Finds the correct trip based on OTP-ServiceDate and SIRI-DepartureTime
     * @param trips
     * @param monitoredVehicleJourney
     * @return
     */
    private Trip getTripForJourney(Set trips, VehicleActivityStructure.MonitoredVehicleJourney monitoredVehicleJourney) {
        ZonedDateTime date = monitoredVehicleJourney.getOriginAimedDepartureTime();
        if (date == null) {
            //If no date is set - assume Realtime-data is reported for 'today'.
            date = ZonedDateTime.now();
        }
        ServiceDate serviceDate = new ServiceDate(date.getYear(), date.getMonthValue(), date.getDayOfMonth());

        List results = new ArrayList<>();
        for (Iterator iterator = trips.iterator(); iterator.hasNext(); ) {

            Trip trip = iterator.next();
            Set serviceDatesForServiceId = routingService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId());

            for (Iterator serviceDateIterator = serviceDatesForServiceId.iterator(); serviceDateIterator.hasNext(); ) {
                ServiceDate next = serviceDateIterator.next();
                if (next.equals(serviceDate)) {
                    results.add(trip);
                }
            }
        }

        if (results.size() == 1) {
            return results.get(0);
        } else if (results.size() > 1) {
            // Multiple possible matches - check if lineRef/routeId matches
            if (monitoredVehicleJourney.getLineRef() != null && monitoredVehicleJourney.getLineRef().getValue() != null) {
                String lineRef = monitoredVehicleJourney.getLineRef().getValue();
                for (Trip trip : results) {
                    if (lineRef.equals(trip.getRoute().getId().getId())) {
                        // Return first trip where the lineRef matches routeId
                        return trip;
                    }
                }
            }

            // Line does not match any routeId - return first result.
            return results.get(0);
        }

        return null;
    }

    /**
     * Finds the correct trip based on OTP-ServiceDate and SIRI-DepartureTime
     * @param trips
     * @param journey
     * @return
     */
    private Set getTripForJourney(Set trips, EstimatedVehicleJourney journey) {


        List recordedCalls = (journey.getRecordedCalls() != null ? journey.getRecordedCalls().getRecordedCalls():new ArrayList<>());
        List estimatedCalls = journey.getEstimatedCalls().getEstimatedCalls();

        ZonedDateTime date;
        int stopNumber = 1;
        String firstStopId;
        if (recordedCalls != null && !recordedCalls.isEmpty()) {
            RecordedCall recordedCall = recordedCalls.get(0);
            date = recordedCall.getAimedDepartureTime();
            firstStopId = recordedCall.getStopPointRef().getValue();
        } else if (estimatedCalls != null && !estimatedCalls.isEmpty()) {
            EstimatedCall estimatedCall = estimatedCalls.get(0);
            if (estimatedCall.getOrder() != null) {
                stopNumber = estimatedCall.getOrder().intValue();
            } else if (estimatedCall.getVisitNumber() != null) {
                stopNumber = estimatedCall.getVisitNumber().intValue();
            }
            firstStopId = estimatedCall.getStopPointRef().getValue();
            date = estimatedCall.getAimedDepartureTime();
        } else {
            return null;
        }

        if (date == null) {
            //If no date is set - assume Realtime-data is reported for 'today'.
            date = ZonedDateTime.now();
        }
        ServiceDate serviceDate = new ServiceDate(date.getYear(), date.getMonthValue(), date.getDayOfMonth());

        int departureInSecondsSinceMidnight = calculateSecondsSinceMidnight(date);
        Set result = new HashSet<>();
        for (Iterator iterator = trips.iterator(); iterator.hasNext(); ) {

            Trip trip = iterator.next();
            Set serviceDatesForServiceId = routingService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId());
            if (serviceDatesForServiceId.contains(serviceDate)) {

                TripPattern pattern = routingService.getPatternForTrip().get(trip);

                if (stopNumber < pattern.stopPattern.stops.length) {
                    boolean firstReportedStopIsFound = false;
                    Stop stop = pattern.stopPattern.stops[stopNumber-1];
                    if (firstStopId.equals(stop.getId().getId())) {
                       firstReportedStopIsFound = true;
                    } else {
                        String agencyId = stop.getId().getFeedId();
                        if (stop.isPartOfStation()) {
                            Stop alternativeStop = routingService
                                .getStopForId(new FeedScopedId(agencyId, firstStopId));
                            if (stop.isPartOfSameStationAs(alternativeStop)) {
                                firstReportedStopIsFound = true;
                            }
                        }
                    }
                    if (firstReportedStopIsFound) {
                        for (TripTimes times : getCurrentTimetable(pattern, serviceDate).tripTimes) {
                            if (times.getScheduledDepartureTime(stopNumber - 1) == departureInSecondsSinceMidnight) {
                                if (routingService.getCalendarService().getServiceDatesForServiceId(times.trip.getServiceId()).contains(serviceDate)) {
                                    result.add(times.trip);
                                }
                            }
                        }
                    }
                }
            }
        }

        if (result.size() >= 1) {
            return result;
        } else {
            return null;
        }
    }

    /**
     * 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) {
        Trip trip = routingService.getTripForId().get(new FeedScopedId(feedId, tripId));
        return trip;
    }

    /**
     * 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