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

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

The newest version!
package org.opentripplanner.ext.siri;

import static java.lang.Boolean.TRUE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NOT_MONITORED;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_FUZZY_TRIP_MATCH;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_START_DATE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TRIP_NOT_FOUND;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TRIP_NOT_FOUND_IN_PATTERN;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.UNKNOWN;
import static org.opentripplanner.updater.trip.UpdateIncrementality.FULL_DATASET;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import org.opentripplanner.model.RealTimeTripUpdate;
import org.opentripplanner.model.Timetable;
import org.opentripplanner.model.TimetableSnapshot;
import org.opentripplanner.model.TimetableSnapshotProvider;
import org.opentripplanner.transit.model.framework.DataValidationException;
import org.opentripplanner.transit.model.framework.Result;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.timetable.RealTimeTripTimes;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.transit.service.DefaultTransitService;
import org.opentripplanner.transit.service.TransitEditorService;
import org.opentripplanner.transit.service.TransitModel;
import org.opentripplanner.updater.TimetableSnapshotSourceParameters;
import org.opentripplanner.updater.spi.DataValidationExceptionMapper;
import org.opentripplanner.updater.spi.UpdateError;
import org.opentripplanner.updater.spi.UpdateResult;
import org.opentripplanner.updater.spi.UpdateSuccess;
import org.opentripplanner.updater.trip.TimetableSnapshotManager;
import org.opentripplanner.updater.trip.UpdateIncrementality;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.org.siri.siri20.EstimatedTimetableDeliveryStructure;
import uk.org.siri.siri20.EstimatedVehicleJourney;

/**
 * This class should be used to create snapshots of lookup tables of real-time data. This is
 * necessary to provide planning threads a consistent constant view of a graph with real-time data at
 * a specific point in time.
 */
public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider {

  private static final Logger LOG = LoggerFactory.getLogger(SiriTimetableSnapshotSource.class);

  /**
   * Use a id generator to generate TripPattern ids for new TripPatterns created by RealTime
   * updates.
   */
  private final SiriTripPatternIdGenerator tripPatternIdGenerator = new SiriTripPatternIdGenerator();
  /**
   * A synchronized cache of trip patterns that are added to the graph due to GTFS-real-time
   * messages.
   */
  private final SiriTripPatternCache tripPatternCache;

  /**
   * Long-lived transit editor service that has access to the timetable snapshot buffer.
   * This differs from the usual use case where the transit service refers to the latest published
   * timetable snapshot.
   */
  private final TransitEditorService transitEditorService;

  private final TimetableSnapshotManager snapshotManager;

  public SiriTimetableSnapshotSource(
    TimetableSnapshotSourceParameters parameters,
    TransitModel transitModel
  ) {
    this.snapshotManager =
      new TimetableSnapshotManager(
        transitModel.getTransitLayerUpdater(),
        parameters,
        () -> LocalDate.now(transitModel.getTimeZone())
      );
    this.transitEditorService =
      new DefaultTransitService(transitModel, getTimetableSnapshotBuffer());
    this.tripPatternCache =
      new SiriTripPatternCache(tripPatternIdGenerator, transitEditorService::getPatternForTrip);

    transitModel.initTimetableSnapshotProvider(this);
  }

  /**
   * Method to apply a trip update list to the most recent version of the timetable snapshot.
   * FIXME RT_AB: TripUpdate is the GTFS term, and these SIRI ETs are never converted into that
   *              same internal model.
   *
   * @param incrementality  the incrementality of the update, for example if updates represent all
   *                        updates that are active right now, i.e. all previous updates should be
   *                        disregarded
   * @param updates    SIRI EstimatedTimetable deliveries that should be applied atomically.
   */
  public UpdateResult applyEstimatedTimetable(
    @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher,
    EntityResolver entityResolver,
    String feedId,
    UpdateIncrementality incrementality,
    List updates
  ) {
    if (updates == null) {
      LOG.warn("updates is null");
      return UpdateResult.empty();
    }

    List> results = new ArrayList<>();

    if (incrementality == FULL_DATASET) {
      // Remove all updates from the buffer
      snapshotManager.clearBuffer(feedId);
    }

    for (var etDelivery : updates) {
      for (var estimatedJourneyVersion : etDelivery.getEstimatedJourneyVersionFrames()) {
        var journeys = estimatedJourneyVersion.getEstimatedVehicleJourneies();
        LOG.debug("Handling {} EstimatedVehicleJourneys.", journeys.size());
        for (EstimatedVehicleJourney journey : journeys) {
          results.add(apply(journey, transitEditorService, fuzzyTripMatcher, entityResolver));
        }
      }
    }

    LOG.debug("message contains {} trip updates", updates.size());

    return UpdateResult.ofResults(results);
  }

  @Override
  public TimetableSnapshot getTimetableSnapshot() {
    return snapshotManager.getTimetableSnapshot();
  }

  /**
   * @return the current timetable snapshot buffer that contains pending changes (not yet published
   * in a snapshot).
   * This should be used in the context of an updater to build a TransitEditorService that sees all
   * the changes applied so far by real-time updates.
   */
  public TimetableSnapshot getTimetableSnapshotBuffer() {
    return snapshotManager.getTimetableSnapshotBuffer();
  }

  private Result apply(
    EstimatedVehicleJourney journey,
    TransitEditorService transitService,
    @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher,
    EntityResolver entityResolver
  ) {
    boolean shouldAddNewTrip = false;
    try {
      shouldAddNewTrip = shouldAddNewTrip(journey, entityResolver);
      Result result;
      if (shouldAddNewTrip) {
        result =
          new AddedTripBuilder(
            journey,
            transitService,
            entityResolver,
            tripPatternIdGenerator::generateUniqueTripPatternId
          )
            .build();
      } else {
        result = handleModifiedTrip(fuzzyTripMatcher, entityResolver, journey);
      }

      if (result.isFailure()) {
        return result.toFailureResult();
      }

      /* commit */
      return addTripToGraphAndBuffer(result.successValue());
    } catch (DataValidationException e) {
      return DataValidationExceptionMapper.toResult(e);
    } catch (Exception e) {
      LOG.warn(
        "{} EstimatedJourney {} failed.",
        shouldAddNewTrip ? "Adding" : "Updating",
        DebugString.of(journey),
        e
      );
      return Result.failure(UpdateError.noTripId(UNKNOWN));
    }
  }

  /**
   * Check if VehicleJourney is a replacement departure according to SIRI-ET requirements.
   */
  private boolean shouldAddNewTrip(
    EstimatedVehicleJourney vehicleJourney,
    EntityResolver entityResolver
  ) {
    // Replacement departure only if ExtraJourney is true
    if (!(TRUE.equals(vehicleJourney.isExtraJourney()))) {
      return false;
    }

    // And if the trip has not been added before
    return entityResolver.resolveTrip(vehicleJourney) == null;
  }

  /**
   * 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, LocalDate serviceDate) { return getTimetableSnapshotBuffer().resolve(tripPattern, serviceDate); } private Result handleModifiedTrip( @Nullable SiriFuzzyTripMatcher fuzzyTripMatcher, EntityResolver entityResolver, EstimatedVehicleJourney estimatedVehicleJourney ) { Trip trip = entityResolver.resolveTrip(estimatedVehicleJourney); // Check if EstimatedVehicleJourney is reported as NOT monitored, ignore the notMonitored-flag // if the journey is NOT monitored because it has been cancelled if ( !TRUE.equals(estimatedVehicleJourney.isMonitored()) && !TRUE.equals(estimatedVehicleJourney.isCancellation()) ) { return UpdateError.result(trip != null ? trip.getId() : null, NOT_MONITORED); } LocalDate serviceDate = entityResolver.resolveServiceDate(estimatedVehicleJourney); if (serviceDate == null) { return UpdateError.result(trip != null ? trip.getId() : null, NO_START_DATE); } TripPattern pattern; if (trip != null) { // Found exact match pattern = transitEditorService.getPatternForTrip(trip); } else if (fuzzyTripMatcher != null) { // No exact match found - search for trips based on arrival-times/stop-patterns TripAndPattern tripAndPattern = fuzzyTripMatcher.match( estimatedVehicleJourney, entityResolver, this::getCurrentTimetable, snapshotManager::getRealtimeAddedTripPattern ); if (tripAndPattern == null) { LOG.debug( "No trips found for EstimatedVehicleJourney. {}", DebugString.of(estimatedVehicleJourney) ); return UpdateError.result(null, NO_FUZZY_TRIP_MATCH); } trip = tripAndPattern.trip(); pattern = tripAndPattern.tripPattern(); } else { return UpdateError.result(null, TRIP_NOT_FOUND); } Timetable currentTimetable = getCurrentTimetable(pattern, serviceDate); TripTimes existingTripTimes = currentTimetable.getTripTimes(trip); if (existingTripTimes == null) { LOG.debug("tripId {} not found in pattern.", trip.getId()); return UpdateError.result(trip.getId(), TRIP_NOT_FOUND_IN_PATTERN); } var updateResult = new ModifiedTripBuilder( existingTripTimes, pattern, estimatedVehicleJourney, serviceDate, transitEditorService.getTimeZone(), entityResolver ) .build(); if (updateResult.isFailure()) { return updateResult.toFailureResult(); } if (!updateResult.successValue().stopPattern().equals(pattern.getStopPattern())) { // Replace scheduled trip pattern, if pattern has changed markScheduledTripAsDeleted(trip, serviceDate); } // Also check whether trip id has been used for previously ADDED/MODIFIED trip message and // remove the previously created trip this.snapshotManager.revertTripToScheduledTripPattern(trip.getId(), serviceDate); return updateResult; } /** * Add a (new) trip to the transitModel and the buffer */ private Result addTripToGraphAndBuffer(TripUpdate tripUpdate) { Trip trip = tripUpdate.tripTimes().getTrip(); LocalDate serviceDate = tripUpdate.serviceDate(); final TripPattern pattern; if (tripUpdate.tripPatternCreation()) { pattern = tripUpdate.addedTripPattern(); } else { // Get cached trip pattern or create one if it doesn't exist yet pattern = tripPatternCache.getOrCreateTripPattern(tripUpdate.stopPattern(), trip, serviceDate); } // Add new trip times to buffer, making protective copies as needed. Bubble success/error up. RealTimeTripUpdate realTimeTripUpdate = new RealTimeTripUpdate( pattern, tripUpdate.tripTimes(), serviceDate, tripUpdate.addedTripOnServiceDate(), tripUpdate.tripCreation(), tripUpdate.routeCreation() ); var result = snapshotManager.updateBuffer(realTimeTripUpdate); LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); return result; } /** * Mark the scheduled trip in the buffer as deleted, given trip on service date * * @return true if scheduled trip was marked as deleted */ private boolean markScheduledTripAsDeleted(Trip trip, final LocalDate serviceDate) { boolean success = false; final TripPattern pattern = transitEditorService.getPatternForTrip(trip); if (pattern != null) { // Mark scheduled trip times for this trip in this pattern as deleted final Timetable timetable = pattern.getScheduledTimetable(); final TripTimes tripTimes = timetable.getTripTimes(trip); if (tripTimes == null) { LOG.warn("Could not mark scheduled trip as deleted {}", trip.getId()); } else { final RealTimeTripTimes newTripTimes = tripTimes.copyScheduledTimes(); newTripTimes.deleteTrip(); snapshotManager.updateBuffer(new RealTimeTripUpdate(pattern, newTripTimes, serviceDate)); success = true; } } return success; } /** * Flush pending changes in the timetable snapshot buffer and publish a new snapshot. */ public void flushBuffer() { snapshotManager.purgeAndCommit(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy