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

org.opentripplanner.updater.vehicle_position.RealtimeVehiclePatternMatcher Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.updater.vehicle_position;

import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.OCCUPANCY;
import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.POSITION;
import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.STOP_POSITION;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_INPUT_STRUCTURE;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_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 com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat;
import com.google.transit.realtime.GtfsRealtime.VehiclePosition;
import com.google.transit.realtime.GtfsRealtime.VehiclePosition.VehicleStopStatus;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.opentripplanner.framework.geometry.WgsCoordinate;
import org.opentripplanner.framework.lang.StringUtils;
import org.opentripplanner.framework.time.ServiceDateUtils;
import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository;
import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle;
import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopStatus;
import org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.framework.Result;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.OccupancyStatus;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher;
import org.opentripplanner.updater.spi.ResultLogger;
import org.opentripplanner.updater.spi.UpdateError;
import org.opentripplanner.updater.spi.UpdateResult;
import org.opentripplanner.updater.spi.UpdateSuccess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Responsible for converting vehicle positions in memory to exportable ones, and associating each
 * position with a pattern.
 */
public class RealtimeVehiclePatternMatcher {

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

  private final String feedId;
  private final RealtimeVehicleRepository repository;
  private final ZoneId timeZoneId;

  private final Function getTripForId;
  private final Function getStaticPattern;
  private final BiFunction getRealtimePattern;
  private final GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher;
  private final Set vehiclePositionFeatures;

  private Set patternsInPreviousUpdate = Set.of();

  public RealtimeVehiclePatternMatcher(
    String feedId,
    Function getTripForId,
    Function getStaticPattern,
    BiFunction getRealtimePattern,
    RealtimeVehicleRepository repository,
    ZoneId timeZoneId,
    GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher,
    Set vehiclePositionFeatures
  ) {
    this.feedId = feedId;
    this.getTripForId = getTripForId;
    this.getStaticPattern = getStaticPattern;
    this.getRealtimePattern = getRealtimePattern;
    this.repository = repository;
    this.timeZoneId = timeZoneId;
    this.fuzzyTripMatcher = fuzzyTripMatcher;
    this.vehiclePositionFeatures = vehiclePositionFeatures;
  }

  /**
   * Attempts to match each vehicle to a pattern, then adds each to a pattern
   *
   * @param vehiclePositions List of vehicle positions to match to patterns
   */
  public UpdateResult applyRealtimeVehicleUpdates(List vehiclePositions) {
    var matchResults = vehiclePositions
      .stream()
      .map(vehiclePosition -> toRealtimeVehicle(feedId, vehiclePosition))
      .toList();

    // we take the list of vehicles and out of them create a Map>
    // that map makes it very easy to update the vehicles in the service
    // it also enables the bookkeeping about which pattern previously had vehicles but no longer do
    // these need to be removed from the service as we assume that the vehicle has stopped
    var vehicles = matchResults
      .stream()
      .filter(Result::isSuccess)
      .map(Result::successValue)
      .collect(Collectors.groupingBy(PatternAndRealtimeVehicle::pattern))
      .entrySet()
      .stream()
      .collect(
        Collectors.toMap(
          Entry::getKey,
          e ->
            e
              .getValue()
              .stream()
              .map(PatternAndRealtimeVehicle::vehicle)
              .collect(Collectors.toList())
        )
      );

    vehicles.forEach(repository::setRealtimeVehicles);
    Set patternsInCurrentUpdate = vehicles.keySet();

    // if there was a vehicle in the previous update but not in the current one, we assume
    // that the pattern has no more vehicles.
    var toDelete = Sets.difference(patternsInPreviousUpdate, patternsInCurrentUpdate);
    toDelete.forEach(repository::clearRealtimeVehicles);
    patternsInPreviousUpdate = patternsInCurrentUpdate;

    if (!vehiclePositions.isEmpty() && patternsInCurrentUpdate.isEmpty()) {
      LOG.error(
        "Could not match any vehicle positions for feedId '{}'. Are you sure that the updater is using the correct feedId?",
        feedId
      );
    }

    // need to convert the sucess to the correct type.
    var results = matchResults
      .stream()
      .map(e -> e.mapSuccess(ignored -> UpdateSuccess.noWarnings()))
      .toList();
    // needs to be put into a new list so the types are correct
    var updateResult = UpdateResult.ofResults(new ArrayList<>(results));
    ResultLogger.logUpdateResult(feedId, "gtfs-rt-vehicle-positions", updateResult);

    return updateResult;
  }

  private LocalDate inferServiceDate(Trip trip) {
    var staticTripTimes = getStaticPattern.apply(trip).getScheduledTimetable().getTripTimes(trip);
    return inferServiceDate(staticTripTimes, timeZoneId, Instant.now());
  }

  /**
   * When a vehicle position doesn't state the service date of its trip then we need to infer it.
   * 

* {@see https://github.com/opentripplanner/OpenTripPlanner/issues/4058} */ protected static LocalDate inferServiceDate( TripTimes staticTripTimes, ZoneId zoneId, Instant now ) { var start = staticTripTimes.getScheduledDepartureTime(0); var end = staticTripTimes.getScheduledDepartureTime(staticTripTimes.getNumStops() - 1); var today = now.atZone(zoneId).toLocalDate(); var yesterday = today.minusDays(1); var tomorrow = today.plusDays(1); // we compute the temporal "distance" to either the start or the end of the trip on either // yesterday, today or tomorrow. whichever one has the lowest "distance" to now is guessed to be // the service day of the undated vehicle position // if this is concerning to you, you should put a start_date in your feed. return Stream .of(yesterday, today, tomorrow) .flatMap(day -> { var startTime = ServiceDateUtils.toZonedDateTime(day, zoneId, start).toInstant(); var endTime = ServiceDateUtils.toZonedDateTime(day, zoneId, end).toInstant(); return Stream .of(Duration.between(startTime, now), Duration.between(endTime, now)) .map(Duration::abs) // temporal "distances" can be positive and negative .map(duration -> new TemporalDistance(day, duration.toSeconds())); }) .min(Comparator.comparingLong(TemporalDistance::distance)) .map(TemporalDistance::date) .orElse(today); } /** * Converts GtfsRealtime vehicle position to the OTP RealtimeVehicle which can be used by * the API. * * @param stopIndexOfGtfsSequence A function that takes a GTFS stop_sequence and returns the index * of the stop in the trip. */ private RealtimeVehicle mapRealtimeVehicle( VehiclePosition vehiclePosition, List stopsOnVehicleTrip, @Nonnull Trip trip, @Nonnull Function stopIndexOfGtfsSequence ) { var newVehicle = RealtimeVehicle.builder(); if (vehiclePositionFeatures.contains(POSITION) && vehiclePosition.hasPosition()) { var position = vehiclePosition.getPosition(); newVehicle.withCoordinates( new WgsCoordinate(position.getLatitude(), position.getLongitude()) ); if (position.hasSpeed()) { newVehicle.withSpeed(position.getSpeed()); } if (position.hasBearing()) { newVehicle.withHeading(position.getBearing()); } } if (vehiclePosition.hasVehicle()) { var vehicle = vehiclePosition.getVehicle(); var id = new FeedScopedId(feedId, vehicle.getId()); newVehicle .withVehicleId(id) .withLabel(Optional.ofNullable(vehicle.getLabel()).orElse(vehicle.getLicensePlate())); } if (vehiclePosition.hasTimestamp()) { newVehicle.withTime(Instant.ofEpochSecond(vehiclePosition.getTimestamp())); } if (vehiclePositionFeatures.contains(STOP_POSITION)) { if (vehiclePosition.hasCurrentStatus()) { newVehicle.withStopStatus(stopStatusToModel(vehiclePosition.getCurrentStatus())); } // we prefer the to get the current stop from the stop_id if (vehiclePosition.hasStopId()) { var matchedStops = stopsOnVehicleTrip .stream() .filter(stop -> stop.getId().getId().equals(vehiclePosition.getStopId())) .toList(); if (matchedStops.size() == 1) { newVehicle.withStop(matchedStops.get(0)); } else { LOG.warn( "Stop ID {} is not in trip {}. Not setting stopRelationship.", vehiclePosition.getStopId(), trip.getId() ); } } // but if stop_id isn't there we try current_stop_sequence else if (vehiclePosition.hasCurrentStopSequence()) { stopIndexOfGtfsSequence .apply(vehiclePosition.getCurrentStopSequence()) .ifPresent(stopIndex -> { if (validStopIndex(stopIndex, stopsOnVehicleTrip)) { var stop = stopsOnVehicleTrip.get(stopIndex); newVehicle.withStop(stop); } }); } } newVehicle.withTrip(trip); if (vehiclePositionFeatures.contains(OCCUPANCY) && vehiclePosition.hasOccupancyStatus()) { newVehicle.withOccupancyStatus(occupancyStatusToModel(vehiclePosition.getOccupancyStatus())); } return newVehicle.build(); } /** * Checks that the stop index can actually be found in the pattern. */ private static boolean validStopIndex(int stopIndex, List stopsOnVehicleTrip) { return stopIndex < stopsOnVehicleTrip.size() - 1; } private record TemporalDistance(LocalDate date, long distance) {} private static StopStatus stopStatusToModel(VehicleStopStatus currentStatus) { return switch (currentStatus) { case IN_TRANSIT_TO -> StopStatus.IN_TRANSIT_TO; case INCOMING_AT -> StopStatus.INCOMING_AT; case STOPPED_AT -> StopStatus.STOPPED_AT; }; } private static OccupancyStatus occupancyStatusToModel( VehiclePosition.OccupancyStatus occupancyStatus ) { return switch (occupancyStatus) { case NO_DATA_AVAILABLE -> OccupancyStatus.NO_DATA_AVAILABLE; case EMPTY -> OccupancyStatus.EMPTY; case MANY_SEATS_AVAILABLE -> OccupancyStatus.MANY_SEATS_AVAILABLE; case FEW_SEATS_AVAILABLE -> OccupancyStatus.FEW_SEATS_AVAILABLE; case STANDING_ROOM_ONLY -> OccupancyStatus.STANDING_ROOM_ONLY; case CRUSHED_STANDING_ROOM_ONLY -> OccupancyStatus.CRUSHED_STANDING_ROOM_ONLY; case FULL -> OccupancyStatus.FULL; case NOT_ACCEPTING_PASSENGERS -> OccupancyStatus.NOT_ACCEPTING_PASSENGERS; case NOT_BOARDABLE -> OccupancyStatus.NOT_ACCEPTING_PASSENGERS; }; } private static String toString(VehiclePosition vehiclePosition) { try { return JsonFormat.printer().omittingInsignificantWhitespace().print(vehiclePosition); } catch (InvalidProtocolBufferException ignored) { return vehiclePosition.toString(); } } private VehiclePosition fuzzilySetTrip(VehiclePosition vehiclePosition) { var trip = fuzzyTripMatcher.match(feedId, vehiclePosition.getTrip()); return vehiclePosition.toBuilder().setTrip(trip).build(); } private Result toRealtimeVehicle( String feedId, VehiclePosition vehiclePosition ) { if (!vehiclePosition.hasTrip()) { LOG.debug( "Realtime vehicle positions {} has no trip ID. Ignoring.", toString(vehiclePosition) ); return Result.failure(UpdateError.noTripId(INVALID_INPUT_STRUCTURE)); } var vehiclePositionWithTripId = fuzzyTripMatcher == null ? vehiclePosition : fuzzilySetTrip(vehiclePosition); var tripId = vehiclePositionWithTripId.getTrip().getTripId(); if (StringUtils.hasNoValue(tripId)) { return Result.failure(UpdateError.noTripId(UpdateError.UpdateErrorType.NO_TRIP_ID)); } var scopedTripId = new FeedScopedId(feedId, tripId); var trip = getTripForId.apply(scopedTripId); if (trip == null) { LOG.debug( "Unable to find trip ID in feed '{}' for vehicle position with trip ID {}", feedId, tripId ); return UpdateError.result(scopedTripId, TRIP_NOT_FOUND); } var serviceDate = Optional .of(vehiclePositionWithTripId.getTrip().getStartDate()) .map(Strings::emptyToNull) .flatMap(ServiceDateUtils::parseStringToOptional) .orElseGet(() -> inferServiceDate(trip)); var pattern = getRealtimePattern.apply(trip, serviceDate); if (pattern == null) { LOG.debug("Unable to match OTP pattern ID for vehicle position with trip ID {}", tripId); return UpdateError.result(scopedTripId, NO_SERVICE_ON_DATE); } // the trip times are only used for mapping the GTFS-RT stop_sequence back to a stop. // because new trips without trip times are created for realtime-updated ones, we explicitly // look at the static trips for the stop_sequence->stop mapping var staticTripTimes = getStaticPattern.apply(trip).getScheduledTimetable().getTripTimes(trip); if (staticTripTimes == null) { return UpdateError.result(scopedTripId, TRIP_NOT_FOUND_IN_PATTERN); } // Add position to pattern var newVehicle = mapRealtimeVehicle( vehiclePositionWithTripId, pattern.getStops(), trip, staticTripTimes::stopIndexOfGtfsSequence ); return Result.success(new PatternAndRealtimeVehicle(pattern, newVehicle)); } record PatternAndRealtimeVehicle(TripPattern pattern, RealtimeVehicle vehicle) {} }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy