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

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

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

import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_FUZZY_TRIP_MATCH;
import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_VALID_STOPS;

import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.opentripplanner.model.Timetable;
import org.opentripplanner.model.calendar.CalendarService;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.framework.Result;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.transit.service.TransitService;
import org.opentripplanner.updater.spi.UpdateError;
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.VehicleModesEnumeration;

/**
 * This class is used for matching TripDescriptors without trip_ids to scheduled GTFS data and to
 * feed back that information into a new TripDescriptor with proper trip_id.
 * 

* It is mainly written for Entur (Norway) data, which doesn't yet have complete ID-based matching * of SIRI messages to transit model objects. But it may be easily adaptable to other locations, as * it's mainly looking at the last stop and arrival times of the scheduled trip. The matching * process will always be applied even in places where you have good quality IDs in SIRI data and * don't need it - we'd have to add a way to disable it. *

* The same instance of this SiriFuzzyTripMatcher may appear in different SIRI updaters. Be sure * to fetch the instance at during the setup of the updaters, the initialization is not thread-safe. */ public class SiriFuzzyTripMatcher { private static final Logger LOG = LoggerFactory.getLogger(SiriFuzzyTripMatcher.class); private final Map> internalPlanningCodeCache = new HashMap<>(); private final Map> startStopTripCache = new HashMap<>(); private final TransitService transitService; public SiriFuzzyTripMatcher(TransitService transitService) { this.transitService = transitService; initCache(this.transitService); } /** * Matches EstimatedVehicleJourney to a set of possible Trips based on tripId */ public Result match( EstimatedVehicleJourney journey, EntityResolver entityResolver, BiFunction getCurrentTimetable, BiFunction getNewTripPatternForModifiedTrip ) { List calls = CallWrapper.of(journey); if (calls.isEmpty()) { return Result.failure(NO_VALID_STOPS); } if (calls.getFirst().getAimedDepartureTime() == null) { return Result.failure(NO_FUZZY_TRIP_MATCH); } Set trips = null; if ( journey.getVehicleRef() != null && journey.getVehicleModes().contains(VehicleModesEnumeration.RAIL) ) { trips = getCachedTripsByInternalPlanningCode(journey.getVehicleRef().getValue()); } if (trips == null || trips.isEmpty()) { CallWrapper lastCall = calls.getLast(); // resolves a scheduled stop point id to a quay (regular stop) if necessary // quay ids also work RegularStop stop = entityResolver.resolveQuay(lastCall.getStopPointRef()); if (stop == null) { return Result.failure(NO_FUZZY_TRIP_MATCH); } ZonedDateTime arrivalTime = lastCall.getAimedArrivalTime() != null ? lastCall.getAimedArrivalTime() : lastCall.getAimedDepartureTime(); if (arrivalTime != null) { trips = getMatchingTripsOnStopOrSiblings(stop, arrivalTime); } } if (trips == null || trips.isEmpty()) { return Result.failure(NO_FUZZY_TRIP_MATCH); } if (journey.getLineRef() != null) { var lineRef = journey.getLineRef().getValue(); Route route = entityResolver.resolveRoute(lineRef); if (route != null) { trips = trips .stream() .filter(trip -> trip.getRoute().equals(route)) .collect(Collectors.toSet()); } } return getTripAndPatternForJourney( trips, calls, entityResolver, getCurrentTimetable, getNewTripPatternForModifiedTrip ); } /** * Returns a match of tripIds that match the provided values. */ public List getTripIdForInternalPlanningCodeServiceDate( String internalPlanningCode, LocalDate serviceDate ) { List matches = new ArrayList<>(); for (Trip trip : getCachedTripsByInternalPlanningCode(internalPlanningCode)) { Set serviceDates = transitService .getCalendarService() .getServiceDatesForServiceId(trip.getServiceId()); if (serviceDates.contains(serviceDate)) { matches.add(trip.getId()); } } return matches; } private void initCache(TransitService index) { for (Trip trip : index.listTrips()) { TripPattern tripPattern = index.findPattern(trip); if (tripPattern == null) { continue; } if (tripPattern.getRoute().getMode().equals(TransitMode.RAIL)) { String internalPlanningCode = trip.getNetexInternalPlanningCode(); if (internalPlanningCode != null) { internalPlanningCodeCache .computeIfAbsent(internalPlanningCode, key -> new HashSet<>()) .add(trip); } } String lastStopId = tripPattern.lastStop().getId().getId(); TripTimes tripTimes = tripPattern.getScheduledTimetable().getTripTimes(trip); if (tripTimes != null) { int arrivalTime = tripTimes.getArrivalTime(tripTimes.getNumStops() - 1); String key = createStartStopKey(lastStopId, arrivalTime); startStopTripCache.computeIfAbsent(key, k -> new HashSet<>()).add(trip); } } LOG.info("Built internalPlanningCode-cache [{}].", internalPlanningCodeCache.size()); LOG.info("Built start-stop-cache [{}].", startStopTripCache.size()); } private static String createStartStopKey(RegularStop stop, int lastStopArrivalTime) { return createStartStopKey(stop.getId().getId(), lastStopArrivalTime); } private static String createStartStopKey(String lastStopId, int lastStopArrivalTime) { return lastStopId + ":" + lastStopArrivalTime; } private Set getMatchingTripsOnStopOrSiblings( RegularStop lastStop, ZonedDateTime arrivalTime ) { int secondsSinceMidnight = ServiceDateUtils.secondsSinceStartOfService( arrivalTime, arrivalTime, transitService.getTimeZone() ); int secondsSinceMidnightYesterday = ServiceDateUtils.secondsSinceStartOfService( arrivalTime.minusDays(1), arrivalTime, transitService.getTimeZone() ); Set trips = startStopTripCache.get(createStartStopKey(lastStop, secondsSinceMidnight)); if (trips == null) { //Attempt to fetch trips that started yesterday - i.e. add 24 hours to arrival-time trips = startStopTripCache.get(createStartStopKey(lastStop, secondsSinceMidnightYesterday)); } if (trips != null) { return trips; } //SIRI-data may report other platform, but still on the same Parent-stop if (!lastStop.isPartOfStation()) { return Set.of(); } trips = new HashSet<>(); var allQuays = lastStop.getParentStation().getChildStops(); for (var quay : allQuays) { Set tripSet = startStopTripCache.get( createStartStopKey(quay.getId().getId(), secondsSinceMidnight) ); if (tripSet != null) { trips.addAll(tripSet); } } return trips; } private Set getCachedTripsByInternalPlanningCode(String internalPlanningCode) { if (internalPlanningCode == null) { return null; } return internalPlanningCodeCache.getOrDefault(internalPlanningCode, new HashSet<>()); } /** * Finds the correct trip based on OTP-ServiceDate and SIRI-DepartureTime */ private Result getTripAndPatternForJourney( Set trips, List calls, EntityResolver entityResolver, BiFunction getCurrentTimetable, BiFunction getNewTripPatternForModifiedTrip ) { var journeyFirstStop = entityResolver.resolveQuay(calls.getFirst().getStopPointRef()); var journeyLastStop = entityResolver.resolveQuay(calls.getLast().getStopPointRef()); if (journeyFirstStop == null || journeyLastStop == null) { return Result.failure(NO_VALID_STOPS); } ZonedDateTime date = calls.getFirst().getAimedDepartureTime(); LocalDate serviceDate = date.toLocalDate(); int departureInSecondsSinceMidnight = ServiceDateUtils.secondsSinceStartOfService( date, date, transitService.getTimeZone() ); CalendarService calendarService = transitService.getCalendarService(); Set possibleTrips = new HashSet<>(); for (Trip trip : trips) { if (!calendarService.getServiceDatesForServiceId(trip.getServiceId()).contains(serviceDate)) { continue; } var newTripPatternForModifiedTrip = getNewTripPatternForModifiedTrip.apply( trip.getId(), serviceDate ); TripPattern tripPattern = newTripPatternForModifiedTrip != null ? newTripPatternForModifiedTrip : transitService.findPattern(trip); var firstStop = tripPattern.firstStop(); var lastStop = tripPattern.lastStop(); boolean firstStopIsMatch = firstStop.equals(journeyFirstStop) || firstStop.isPartOfSameStationAs(journeyFirstStop); boolean lastStopIsMatch = lastStop.equals(journeyLastStop) || lastStop.isPartOfSameStationAs(journeyLastStop); if (!firstStopIsMatch || !lastStopIsMatch) { continue; } TripTimes times = getCurrentTimetable.apply(tripPattern, serviceDate).getTripTimes(trip); if (times != null && times.getScheduledDepartureTime(0) == departureInSecondsSinceMidnight) { // Found matches possibleTrips.add(new TripAndPattern(times.getTrip(), tripPattern)); } } if (possibleTrips.isEmpty()) { return Result.failure(UpdateError.UpdateErrorType.NO_FUZZY_TRIP_MATCH); } else if (possibleTrips.size() > 1) { LOG.warn("Multiple trip and pattern combinations found, skipping all, {}", possibleTrips); return Result.failure(UpdateError.UpdateErrorType.MULTIPLE_FUZZY_TRIP_MATCHES); } else { return Result.success(possibleTrips.iterator().next()); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy