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

org.opentripplanner.updater.trip.siri.ModifiedTripBuilder Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.updater.trip.siri;

import static java.lang.Boolean.TRUE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.STOP_MISMATCH;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TOO_FEW_STOPS;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TOO_MANY_STOPS;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.UNKNOWN_STOP;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.opentripplanner.transit.model.framework.DataValidationException;
import org.opentripplanner.transit.model.framework.Result;
import org.opentripplanner.transit.model.network.StopPattern;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.RealTimeState;
import org.opentripplanner.transit.model.timetable.RealTimeTripTimes;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.updater.spi.DataValidationExceptionMapper;
import org.opentripplanner.updater.spi.UpdateError;
import org.opentripplanner.updater.trip.siri.mapping.PickDropMapper;
import org.opentripplanner.utils.time.ServiceDateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.org.siri.siri20.EstimatedVehicleJourney;
import uk.org.siri.siri20.OccupancyEnumeration;

/**
 * A helper class for creating new StopPattern and TripTimes based on a SIRI-ET
 * EstimatedVehicleJourney.
 */
class ModifiedTripBuilder {

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

  private final TripTimes existingTripTimes;
  private final TripPattern pattern;
  private final LocalDate serviceDate;
  private final ZoneId zoneId;
  private final EntityResolver entityResolver;
  private final List calls;
  private final boolean cancellation;
  private final OccupancyEnumeration occupancy;
  private final boolean predictionInaccurate;
  private final String dataSource;

  public ModifiedTripBuilder(
    TripTimes existingTripTimes,
    TripPattern pattern,
    EstimatedVehicleJourney journey,
    LocalDate serviceDate,
    ZoneId zoneId,
    EntityResolver entityResolver
  ) {
    this.existingTripTimes = existingTripTimes;
    this.pattern = pattern;
    this.serviceDate = serviceDate;
    this.zoneId = zoneId;
    this.entityResolver = entityResolver;

    calls = CallWrapper.of(journey);
    cancellation = TRUE.equals(journey.isCancellation());
    predictionInaccurate = TRUE.equals(journey.isPredictionInaccurate());
    occupancy = journey.getOccupancy();
    dataSource = journey.getDataSource();
  }

  /**
   * Constructor for tests
   */
  public ModifiedTripBuilder(
    TripTimes existingTripTimes,
    TripPattern pattern,
    LocalDate serviceDate,
    ZoneId zoneId,
    EntityResolver entityResolver,
    List calls,
    boolean cancellation,
    OccupancyEnumeration occupancy,
    boolean predictionInaccurate,
    String dataSource
  ) {
    this.existingTripTimes = existingTripTimes;
    this.pattern = pattern;
    this.serviceDate = serviceDate;
    this.zoneId = zoneId;
    this.entityResolver = entityResolver;
    this.calls = calls;
    this.cancellation = cancellation;
    this.occupancy = occupancy;
    this.predictionInaccurate = predictionInaccurate;
    this.dataSource = dataSource;
  }

  /**
   * Create a new StopPattern and TripTimes for the trip based on the calls, and other fields read
   * in form the SIRI-ET update.
   */
  public Result build() {
    RealTimeTripTimes newTimes = existingTripTimes.copyScheduledTimes();

    if (cancellation) {
      return cancelTrip(newTimes);
    }

    if (calls.size() < existingTripTimes.getNumStops()) {
      return UpdateError.result(existingTripTimes.getTrip().getId(), TOO_FEW_STOPS, dataSource);
    }

    if (calls.size() > existingTripTimes.getNumStops()) {
      return UpdateError.result(existingTripTimes.getTrip().getId(), TOO_MANY_STOPS, dataSource);
    }

    var result = createStopPattern(pattern, calls, entityResolver);
    if (result.isFailure()) {
      int invalidStopIndex = result.failureValue().stopIndex();
      LOG.info(
        "Invalid SIRI-ET data for trip {} - {} at stop index {}",
        existingTripTimes.getTrip().getId(),
        result.failureValue().errorType(),
        invalidStopIndex
      );
      return Result.failure(
        new UpdateError(
          existingTripTimes.getTrip().getId(),
          result.failureValue().errorType(),
          invalidStopIndex,
          dataSource
        )
      );
    }

    StopPattern stopPattern = result.successValue();
    if (stopPattern.isAllStopsNonRoutable()) {
      return cancelTrip(newTimes);
    }

    applyUpdates(newTimes);

    if (pattern.getStopPattern().equals(stopPattern)) {
      // This is the first update, and StopPattern has not been changed
      newTimes.setRealTimeState(RealTimeState.UPDATED);
    } else {
      // This update modified stopPattern
      newTimes.setRealTimeState(RealTimeState.MODIFIED);
    }

    // TODO - Handle DataValidationException at the outermost level (pr trip)
    try {
      newTimes.validateNonIncreasingTimes();
    } catch (DataValidationException e) {
      LOG.info(
        "Invalid SIRI-ET data for trip {} - TripTimes failed to validate after applying SIRI delay propagation. {}",
        newTimes.getTrip().getId(),
        e.getMessage()
      );
      return DataValidationExceptionMapper.toResult(e, dataSource);
    }

    int numStopsInUpdate = newTimes.getNumStops();
    int numStopsInPattern = pattern.numberOfStops();
    if (numStopsInUpdate != numStopsInPattern) {
      LOG.info(
        "Invalid SIRI-ET data for trip {} - Inconsistent number of updated stops ({}) and stops in pattern ({})",
        newTimes.getTrip().getId(),
        numStopsInUpdate,
        numStopsInPattern
      );
      return UpdateError.result(existingTripTimes.getTrip().getId(), TOO_FEW_STOPS, dataSource);
    }

    LOG.debug("A valid TripUpdate object was applied using the Timetable class update method.");
    return Result.success(new TripUpdate(stopPattern, newTimes, serviceDate, dataSource));
  }

  /**
   * Full cancellation of a trip.
   */
  private Result cancelTrip(RealTimeTripTimes newTimes) {
    newTimes.cancelTrip();
    return Result.success(
      new TripUpdate(pattern.getStopPattern(), newTimes, serviceDate, dataSource)
    );
  }

  /**
   * Applies real-time updates from the calls into newTimes.
   * Precondition: the number of calls is equal to the number of stops in the pattern (this is
   * verified before calling this method).
   */
  private void applyUpdates(RealTimeTripTimes newTimes) {
    ZonedDateTime startOfService = ServiceDateUtils.asStartOfService(serviceDate, zoneId);
    Set alreadyVisited = new HashSet<>();

    List stopsInPattern = pattern.getStops();
    for (int stopIndex = 0; stopIndex < stopsInPattern.size(); stopIndex++) {
      StopLocation stopInPattern = stopsInPattern.get(stopIndex);
      CallWrapper matchingCall = null;

      for (CallWrapper call : calls) {
        if (alreadyVisited.contains(call)) {
          continue;
        }
        //Current stop is being updated
        RegularStop stopPoint = entityResolver.resolveQuay(call.getStopPointRef());
        if (stopInPattern.equals(stopPoint) || stopInPattern.isPartOfSameStationAs(stopPoint)) {
          matchingCall = call;
          break;
        }
      }

      if (matchingCall == null) {
        throw new IllegalStateException(
          "The stop at index %d on the trip %s cannot be matched with any call. This implies a bug.".formatted(
              stopIndex,
              newTimes.getTrip().getId()
            )
        );
      }

      TimetableHelper.applyUpdates(
        startOfService,
        newTimes,
        stopIndex,
        stopIndex == (stopsInPattern.size() - 1),
        predictionInaccurate,
        matchingCall,
        occupancy
      );

      alreadyVisited.add(matchingCall);
    }
  }

  /**
   * Creates a new StopPattern, based on an existing pattern, and list of calls. The stops can be
   * replaced with stops belonging to the same Station/StopPlace. The PickDrop values are updated
   * as well.
   * Precondition: the number of calls is equal to the number of stops in the pattern (this is
   * verified before calling this method).
   */
  static Result createStopPattern(
    TripPattern pattern,
    List calls,
    EntityResolver entityResolver
  ) {
    int numberOfStops = pattern.numberOfStops();
    var builder = pattern.copyPlannedStopPattern();

    Set alreadyVisited = new HashSet<>();
    // modify updated stop-times
    for (int i = 0; i < numberOfStops; i++) {
      StopLocation stop = builder.stops.original(i);

      boolean matchFound = false;
      for (CallWrapper call : calls) {
        if (alreadyVisited.contains(call)) {
          continue;
        }

        //Current stop is being updated
        var callStop = entityResolver.resolveQuay(call.getStopPointRef());
        if (callStop == null) {
          return Result.failure(new UpdateError(null, UNKNOWN_STOP, i));
        }

        if (!stop.equals(callStop) && !stop.isPartOfSameStationAs(callStop)) {
          continue;
        }
        matchFound = true;

        // Used in lambda
        final int stopIndex = i;
        builder.stops.with(stopIndex, callStop);

        PickDropMapper.mapPickUpType(call, builder.pickups.original(stopIndex)).ifPresent(value ->
          builder.pickups.with(stopIndex, value)
        );

        PickDropMapper.mapDropOffType(call, builder.dropoffs.original(stopIndex)).ifPresent(value ->
          builder.dropoffs.with(stopIndex, value)
        );

        alreadyVisited.add(call);
        break;
      }
      if (!matchFound) {
        return Result.failure(new UpdateError(null, STOP_MISMATCH, i));
      }
    }
    var newStopPattern = builder.build();
    return (pattern.isModified() && pattern.getStopPattern().equals(newStopPattern))
      ? Result.success(pattern.getStopPattern())
      : Result.success(newStopPattern);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy