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

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

package org.opentripplanner.updater.vehicle_position;

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.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
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 org.opentripplanner.common.model.T2;
import org.opentripplanner.model.vehicle_position.RealtimeVehiclePosition;
import org.opentripplanner.model.vehicle_position.RealtimeVehiclePosition.StopStatus;
import org.opentripplanner.routing.services.RealtimeVehiclePositionService;
import org.opentripplanner.transit.model.basic.WgsCoordinate;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.util.time.ServiceDateUtils;
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 VehiclePositionPatternMatcher {

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

  private final String feedId;
  private final RealtimeVehiclePositionService service;
  private final ZoneId timeZoneId;

  private final Function getTripForId;
  private final Function getStaticPattern;
  private final BiFunction getRealtimePattern;

  private Set patternsInPreviousUpdate = Set.of();

  public VehiclePositionPatternMatcher(
    String feedId,
    Function getTripForId,
    Function getStaticPattern,
    BiFunction getRealtimePattern,
    RealtimeVehiclePositionService service,
    ZoneId timeZoneId
  ) {
    this.feedId = feedId;
    this.getTripForId = getTripForId;
    this.getStaticPattern = getStaticPattern;
    this.getRealtimePattern = getRealtimePattern;
    this.service = service;
    this.timeZoneId = timeZoneId;
  }

  /**
   * Attempts to match each vehicle position to a pattern, then adds each to a pattern
   *
   * @param vehiclePositions List of vehicle positions to match to patterns
   */
  public void applyVehiclePositionUpdates(List vehiclePositions) {
    // we take the list of positions and out of them create a Map>
    // that map makes it very easy to update the positions in the service
    // it also enables the bookkeeping about which pattern previously had positions but no longer do
    // these need to be removed from the service as we assume that the vehicle has stopped
    var positions = vehiclePositions
      .stream()
      .map(vehiclePosition -> toRealtimeVehiclePosition(feedId, vehiclePosition))
      .filter(Objects::nonNull)
      .collect(Collectors.groupingBy(t -> t.first))
      .entrySet()
      .stream()
      .collect(
        Collectors.toMap(
          Entry::getKey,
          e -> e.getValue().stream().map(t -> t.second).collect(Collectors.toList())
        )
      );

    positions.forEach(service::setVehiclePositions);
    Set patternsInCurrentUpdate = positions.keySet();

    // if there was a position in the previous update but not in the current one, we assume
    // that the pattern has no more vehicle positions.
    var toDelete = Sets.difference(patternsInPreviousUpdate, patternsInCurrentUpdate);
    toDelete.forEach(service::clearVehiclePositions);
    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
      );
    }
  }

  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 RealtimeVehiclePosition which can be used by * the API. */ private static RealtimeVehiclePosition mapVehiclePosition( VehiclePosition vehiclePosition, List stopsOnVehicleTrip, Trip trip ) { var newPosition = RealtimeVehiclePosition.builder(); if (vehiclePosition.hasPosition()) { var position = vehiclePosition.getPosition(); newPosition.setCoordinates( new WgsCoordinate(position.getLatitude(), position.getLongitude()) ); if (position.hasSpeed()) { newPosition.setSpeed(position.getSpeed()); } if (position.hasBearing()) { newPosition.setHeading(position.getBearing()); } } if (vehiclePosition.hasVehicle()) { var vehicle = vehiclePosition.getVehicle(); var id = new FeedScopedId(trip.getId().getFeedId(), vehicle.getId()); newPosition .setVehicleId(id) .setLabel(Optional.ofNullable(vehicle.getLabel()).orElse(vehicle.getLicensePlate())); } if (vehiclePosition.hasTimestamp()) { newPosition.setTime(Instant.ofEpochSecond(vehiclePosition.getTimestamp())); } if (vehiclePosition.hasCurrentStatus()) { newPosition.setStopStatus(toModel(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) { newPosition.setStop(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()) { var stop = stopsOnVehicleTrip.get(vehiclePosition.getCurrentStopSequence()); newPosition.setStop(stop); } newPosition.setTrip(trip); return newPosition.build(); } private record TemporalDistance(LocalDate date, long distance) {} private static StopStatus toModel(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 String toString(VehiclePosition vehiclePosition) { try { return JsonFormat.printer().omittingInsignificantWhitespace().print(vehiclePosition); } catch (InvalidProtocolBufferException ignored) { return vehiclePosition.toString(); } } private T2 toRealtimeVehiclePosition( String feedId, VehiclePosition vehiclePosition ) { if (!vehiclePosition.hasTrip()) { LOG.warn( "Realtime vehicle positions {} has no trip ID. Ignoring.", toString(vehiclePosition) ); return null; } var tripId = vehiclePosition.getTrip().getTripId(); var trip = getTripForId.apply(new FeedScopedId(feedId, tripId)); if (trip == null) { LOG.warn( "Unable to find trip ID in feed '{}' for vehicle position with trip ID {}", feedId, tripId ); return null; } var serviceDate = Optional .of(vehiclePosition.getTrip().getStartDate()) .map(Strings::emptyToNull) .flatMap(ServiceDateUtils::parseStringToOptional) .orElseGet(() -> inferServiceDate(trip)); var pattern = getRealtimePattern.apply(trip, serviceDate); if (pattern == null) { LOG.warn("Unable to match OTP pattern ID for vehicle position with trip ID {}", tripId); return null; } // Add position to pattern var newPosition = mapVehiclePosition(vehiclePosition, pattern.getStops(), trip); return new T2<>(pattern, newPosition); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy