
org.opentripplanner.ext.siri.SiriFuzzyTripMatcher Maven / Gradle / Ivy
Show all versions of otp Show documentation
package org.opentripplanner.ext.siri;
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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opentripplanner.framework.time.ServiceDateUtils;
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.network.Route;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;
import org.opentripplanner.transit.service.TransitService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.org.siri.siri20.EstimatedVehicleJourney;
import uk.org.siri.siri20.MonitoredVehicleJourneyStructure;
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 VehicleActivity to a set of possible Trips based on tripId
*/
public Trip match(
MonitoredVehicleJourneyStructure monitoredVehicleJourney,
EntityResolver entityResolver
) {
if (monitoredVehicleJourney.getDestinationRef() != null) {
String destinationRef = monitoredVehicleJourney.getDestinationRef().getValue();
ZonedDateTime arrivalTime = monitoredVehicleJourney.getDestinationAimedArrivalTime();
if (arrivalTime != null) {
Set trips = getMatchingTripsOnStopOrSiblings(
destinationRef,
arrivalTime,
entityResolver
);
if (trips.isEmpty()) {
return null;
}
return getTripForJourney(trips, monitoredVehicleJourney);
}
}
return null;
}
/**
* Matches EstimatedVehicleJourney to a set of possible Trips based on tripId
*/
@Nullable
public TripAndPattern match(
EstimatedVehicleJourney journey,
EntityResolver entityResolver,
BiFunction getCurrentTimetable,
BiFunction getRealtimeAddedTripPattern
) {
List calls = CallWrapper.of(journey);
if (calls.isEmpty()) {
return null;
}
if (calls.getFirst().getAimedDepartureTime() == null) {
return null;
}
Set trips = null;
if (
journey.getVehicleRef() != null &&
journey.getVehicleModes().contains(VehicleModesEnumeration.RAIL)
) {
trips = getCachedTripsByInternalPlanningCode(journey.getVehicleRef().getValue());
}
if (trips == null || trips.isEmpty()) {
CallWrapper lastStop = calls.getLast();
String lastStopPoint = lastStop.getStopPointRef();
ZonedDateTime arrivalTime = lastStop.getAimedArrivalTime() != null
? lastStop.getAimedArrivalTime()
: lastStop.getAimedDepartureTime();
if (arrivalTime != null) {
trips = getMatchingTripsOnStopOrSiblings(lastStopPoint, arrivalTime, entityResolver);
}
}
if (trips == null || trips.isEmpty()) {
return null;
}
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,
getRealtimeAddedTripPattern
);
}
/**
* 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.getAllTrips()) {
TripPattern tripPattern = index.getPatternForTrip(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(String lastStopId, int lastStopArrivalTime) {
return lastStopId + ":" + lastStopArrivalTime;
}
@Nonnull
private Set getMatchingTripsOnStopOrSiblings(
String lastStopPoint,
ZonedDateTime arrivalTime,
EntityResolver entityResolver
) {
int secondsSinceMidnight = ServiceDateUtils.secondsSinceStartOfService(
arrivalTime,
arrivalTime,
transitService.getTimeZone()
);
int secondsSinceMidnightYesterday = ServiceDateUtils.secondsSinceStartOfService(
arrivalTime.minusDays(1),
arrivalTime,
transitService.getTimeZone()
);
Set trips = startStopTripCache.get(
createStartStopKey(lastStopPoint, secondsSinceMidnight)
);
if (trips == null) {
//Attempt to fetch trips that started yesterday - i.e. add 24 hours to arrival-time
trips =
startStopTripCache.get(createStartStopKey(lastStopPoint, secondsSinceMidnightYesterday));
}
if (trips != null) {
return trips;
}
//SIRI-data may report other platform, but still on the same Parent-stop
var stop = entityResolver.resolveQuay(lastStopPoint);
if (stop == null || !stop.isPartOfStation()) {
return Set.of();
}
trips = new HashSet<>();
var allQuays = stop.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
*/
@Nullable
TripAndPattern getTripAndPatternForJourney(
Set trips,
List calls,
EntityResolver entityResolver,
BiFunction getCurrentTimetable,
BiFunction getRealtimeAddedTripPattern
) {
var journeyFirstStop = entityResolver.resolveQuay(calls.getFirst().getStopPointRef());
var journeyLastStop = entityResolver.resolveQuay(calls.getLast().getStopPointRef());
if (journeyFirstStop == null || journeyLastStop == null) {
return null;
}
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 realTimeAddedTripPattern = getRealtimeAddedTripPattern.apply(trip.getId(), serviceDate);
TripPattern tripPattern = realTimeAddedTripPattern != null
? realTimeAddedTripPattern
: transitService.getPatternForTrip(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 null;
} else if (possibleTrips.size() > 1) {
LOG.warn("Multiple trip and pattern combinations found, skipping all, {}", possibleTrips);
return null;
} else {
return possibleTrips.iterator().next();
}
}
/**
* Finds the correct trip based on OTP-ServiceDate and SIRI-DepartureTime
*/
private Trip getTripForJourney(
Set trips,
MonitoredVehicleJourneyStructure monitoredVehicleJourney
) {
ZonedDateTime date = monitoredVehicleJourney.getOriginAimedDepartureTime();
if (date == null) {
//If no date is set - assume Realtime-data is reported for 'today'.
date = ZonedDateTime.now();
}
LocalDate serviceDate = date.toLocalDate();
List results = new ArrayList<>();
for (Trip trip : trips) {
Set serviceDatesForServiceId = transitService
.getCalendarService()
.getServiceDatesForServiceId(trip.getServiceId());
for (LocalDate next : serviceDatesForServiceId) {
if (next.equals(serviceDate)) {
results.add(trip);
}
}
}
if (results.size() == 1) {
return results.getFirst();
} else if (results.size() > 1) {
// Multiple possible matches - check if lineRef/routeId matches
if (
monitoredVehicleJourney.getLineRef() != null &&
monitoredVehicleJourney.getLineRef().getValue() != null
) {
String lineRef = monitoredVehicleJourney.getLineRef().getValue();
for (Trip trip : results) {
if (lineRef.equals(trip.getRoute().getId().getId())) {
// Return first trip where the lineRef matches routeId
return trip;
}
}
}
// Line does not match any routeId - return first result.
return results.getFirst();
}
return null;
}
}