diff --git a/application/src/main/java/org/opentripplanner/updater/siri/SiriFuzzyTripMatcher.java b/application/src/main/java/org/opentripplanner/updater/siri/SiriFuzzyTripMatcher.java index 2cffc81b440..7a9c64703c1 100644 --- a/application/src/main/java/org/opentripplanner/updater/siri/SiriFuzzyTripMatcher.java +++ b/application/src/main/java/org/opentripplanner/updater/siri/SiriFuzzyTripMatcher.java @@ -1,5 +1,8 @@ package org.opentripplanner.updater.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; @@ -10,21 +13,22 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; -import javax.annotation.Nullable; 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.MonitoredVehicleJourneyStructure; import uk.org.siri.siri20.VehicleModesEnumeration; /** @@ -53,37 +57,10 @@ public SiriFuzzyTripMatcher(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( + public Result match( EstimatedVehicleJourney journey, EntityResolver entityResolver, BiFunction getCurrentTimetable, @@ -92,11 +69,11 @@ public TripAndPattern match( List calls = CallWrapper.of(journey); if (calls.isEmpty()) { - return null; + return Result.failure(NO_VALID_STOPS); } if (calls.getFirst().getAimedDepartureTime() == null) { - return null; + return Result.failure(NO_FUZZY_TRIP_MATCH); } Set trips = null; @@ -108,18 +85,23 @@ public TripAndPattern match( } if (trips == null || trips.isEmpty()) { - CallWrapper lastStop = calls.getLast(); - String lastStopPoint = lastStop.getStopPointRef(); - ZonedDateTime arrivalTime = lastStop.getAimedArrivalTime() != null - ? lastStop.getAimedArrivalTime() - : lastStop.getAimedDepartureTime(); + 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(lastStopPoint, arrivalTime, entityResolver); + trips = getMatchingTripsOnStopOrSiblings(stop, arrivalTime); } } if (trips == null || trips.isEmpty()) { - return null; + return Result.failure(NO_FUZZY_TRIP_MATCH); } if (journey.getLineRef() != null) { @@ -191,14 +173,17 @@ private void initCache(TransitService index) { 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( - String lastStopPoint, - ZonedDateTime arrivalTime, - EntityResolver entityResolver + RegularStop lastStop, + ZonedDateTime arrivalTime ) { int secondsSinceMidnight = ServiceDateUtils.secondsSinceStartOfService( arrivalTime, @@ -211,13 +196,10 @@ private Set getMatchingTripsOnStopOrSiblings( transitService.getTimeZone() ); - Set trips = startStopTripCache.get( - createStartStopKey(lastStopPoint, secondsSinceMidnight) - ); + 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(lastStopPoint, secondsSinceMidnightYesterday)); + trips = startStopTripCache.get(createStartStopKey(lastStop, secondsSinceMidnightYesterday)); } if (trips != null) { @@ -225,13 +207,12 @@ private Set getMatchingTripsOnStopOrSiblings( } //SIRI-data may report other platform, but still on the same Parent-stop - var stop = entityResolver.resolveQuay(lastStopPoint); - if (stop == null || !stop.isPartOfStation()) { + if (!lastStop.isPartOfStation()) { return Set.of(); } trips = new HashSet<>(); - var allQuays = stop.getParentStation().getChildStops(); + var allQuays = lastStop.getParentStation().getChildStops(); for (var quay : allQuays) { Set tripSet = startStopTripCache.get( createStartStopKey(quay.getId().getId(), secondsSinceMidnight) @@ -253,8 +234,7 @@ private Set getCachedTripsByInternalPlanningCode(String internalPlanningCo /** * Finds the correct trip based on OTP-ServiceDate and SIRI-DepartureTime */ - @Nullable - TripAndPattern getTripAndPatternForJourney( + private Result getTripAndPatternForJourney( Set trips, List calls, EntityResolver entityResolver, @@ -264,7 +244,7 @@ TripAndPattern getTripAndPatternForJourney( var journeyFirstStop = entityResolver.resolveQuay(calls.getFirst().getStopPointRef()); var journeyLastStop = entityResolver.resolveQuay(calls.getLast().getStopPointRef()); if (journeyFirstStop == null || journeyLastStop == null) { - return null; + return Result.failure(NO_VALID_STOPS); } ZonedDateTime date = calls.getFirst().getAimedDepartureTime(); @@ -310,63 +290,12 @@ TripAndPattern getTripAndPatternForJourney( } if (possibleTrips.isEmpty()) { - return null; + 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 null; + return Result.failure(UpdateError.UpdateErrorType.MULTIPLE_FUZZY_TRIP_MATCHES); } 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); - } - } + return Result.success(possibleTrips.iterator().next()); } - - 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; } } diff --git a/application/src/main/java/org/opentripplanner/updater/siri/SiriRealTimeTripUpdateAdapter.java b/application/src/main/java/org/opentripplanner/updater/siri/SiriRealTimeTripUpdateAdapter.java index 35fd5508169..bdb54e6e459 100644 --- a/application/src/main/java/org/opentripplanner/updater/siri/SiriRealTimeTripUpdateAdapter.java +++ b/application/src/main/java/org/opentripplanner/updater/siri/SiriRealTimeTripUpdateAdapter.java @@ -219,21 +219,22 @@ private Result handleModifiedTrip( pattern = transitEditorService.findPattern(trip); } else if (fuzzyTripMatcher != null) { // No exact match found - search for trips based on arrival-times/stop-patterns - TripAndPattern tripAndPattern = fuzzyTripMatcher.match( + var result = fuzzyTripMatcher.match( estimatedVehicleJourney, entityResolver, this::getCurrentTimetable, snapshotManager::getNewTripPatternForModifiedTrip ); - if (tripAndPattern == null) { + if (result.isFailure()) { LOG.debug( "No trips found for EstimatedVehicleJourney. {}", DebugString.of(estimatedVehicleJourney) ); - return UpdateError.result(null, NO_FUZZY_TRIP_MATCH, dataSource); + return UpdateError.result(null, result.failureValue(), dataSource); } + var tripAndPattern = result.successValue(); trip = tripAndPattern.trip(); pattern = tripAndPattern.tripPattern(); } else { diff --git a/application/src/main/java/org/opentripplanner/updater/spi/UpdateError.java b/application/src/main/java/org/opentripplanner/updater/spi/UpdateError.java index 945139e2457..23f9d4e2955 100644 --- a/application/src/main/java/org/opentripplanner/updater/spi/UpdateError.java +++ b/application/src/main/java/org/opentripplanner/updater/spi/UpdateError.java @@ -41,6 +41,7 @@ public enum UpdateErrorType { TRIP_NOT_FOUND, TRIP_NOT_FOUND_IN_PATTERN, NO_FUZZY_TRIP_MATCH, + MULTIPLE_FUZZY_TRIP_MATCHES, EMPTY_STOP_POINT_REF, NO_TRIP_FOR_CANCELLATION_FOUND, TRIP_ALREADY_EXISTS, diff --git a/application/src/test/java/org/opentripplanner/updater/siri/SiriEtBuilder.java b/application/src/test/java/org/opentripplanner/updater/siri/SiriEtBuilder.java index 737fb04917e..793bec905e8 100644 --- a/application/src/test/java/org/opentripplanner/updater/siri/SiriEtBuilder.java +++ b/application/src/test/java/org/opentripplanner/updater/siri/SiriEtBuilder.java @@ -51,6 +51,10 @@ public List buildEstimatedTimetableDeliveri return List.of(etd); } + public EstimatedVehicleJourney buildEstimatedVehicleJourney() { + return evj; + } + public SiriEtBuilder withCancellation(boolean canceled) { evj.setCancellation(canceled); return this; diff --git a/application/src/test/java/org/opentripplanner/updater/siri/SiriFuzzyTripMatcherTest.java b/application/src/test/java/org/opentripplanner/updater/siri/SiriFuzzyTripMatcherTest.java new file mode 100644 index 00000000000..8de77a8b951 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/updater/siri/SiriFuzzyTripMatcherTest.java @@ -0,0 +1,125 @@ +package org.opentripplanner.updater.siri; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; +import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.MULTIPLE_FUZZY_TRIP_MATCHES; +import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_FUZZY_TRIP_MATCH; +import static org.opentripplanner.updater.trip.RealtimeTestConstants.STOP_A1; +import static org.opentripplanner.updater.trip.RealtimeTestConstants.STOP_B1; +import static org.opentripplanner.updater.trip.RealtimeTestConstants.TRIP_1_ID; +import static org.opentripplanner.updater.trip.RealtimeTestConstants.TRIP_2_ID; + +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model.framework.Result; +import org.opentripplanner.updater.spi.UpdateError; +import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripInput; +import uk.org.siri.siri20.EstimatedVehicleJourney; + +class SiriFuzzyTripMatcherTest { + + @Test + void match() { + TripInput trip1Input = tripInput(TRIP_1_ID); + + var env = RealtimeTestEnvironment.of().addTrip(trip1Input).build(); + var evj = estimatedVehicleJourney(env); + + var result = match(evj, env); + assertTrue(result.isSuccess()); + } + + @Test + void multipleMatches() { + var trip1input = tripInput(TRIP_1_ID); + var trip2input = tripInput(TRIP_2_ID); + + var env = RealtimeTestEnvironment.of().addTrip(trip1input).addTrip(trip2input).build(); + + var evj = estimatedVehicleJourney(env); + + var result = match(evj, env); + assertTrue(result.isFailure()); + assertEquals(MULTIPLE_FUZZY_TRIP_MATCHES, result.failureValue()); + } + + @Test + void scheduledStopPoint() { + var scheduledStopPointId = "ssp-1"; + var trip1input = tripInput(TRIP_1_ID); + + var env = RealtimeTestEnvironment.of().addTrip(trip1input).build(); + env.timetableRepository.addScheduledStopPointMapping(Map.of(id(scheduledStopPointId), STOP_B1)); + + var journey = new SiriEtBuilder(env.getDateTimeHelper()) + .withEstimatedCalls(builder -> + builder + .call(STOP_A1) + .departAimedExpected("00:10:00", "00:10:00") + .call(scheduledStopPointId) + .arriveAimedExpected("00:20:00", "00:20:00") + ) + .buildEstimatedVehicleJourney(); + + var result = match(journey, env); + assertTrue(result.isSuccess()); + } + + @Test + void unknownStopPointRef() { + var trip1input = tripInput(TRIP_1_ID); + + var env = RealtimeTestEnvironment.of().addTrip(trip1input).build(); + + var journey = new SiriEtBuilder(env.getDateTimeHelper()) + .withEstimatedCalls(builder -> + builder + .call(STOP_A1) + .departAimedExpected("00:10:00", "00:10:00") + .call("SOME_MADE_UP_ID") + .arriveAimedExpected("00:20:00", "00:20:00") + ) + .buildEstimatedVehicleJourney(); + + var result = match(journey, env); + assertTrue(result.isFailure()); + assertEquals(NO_FUZZY_TRIP_MATCH, result.failureValue()); + } + + private static Result match( + EstimatedVehicleJourney evj, + RealtimeTestEnvironment env + ) { + var transitService = env.getTransitService(); + var fuzzyMatcher = new SiriFuzzyTripMatcher(transitService); + return fuzzyMatcher.match( + evj, + new EntityResolver(transitService, env.getFeedId()), + transitService::findTimetable, + transitService::findNewTripPatternForModifiedTrip + ); + } + + private static EstimatedVehicleJourney estimatedVehicleJourney(RealtimeTestEnvironment env) { + return new SiriEtBuilder(env.getDateTimeHelper()) + .withEstimatedCalls(builder -> + builder + .call(STOP_A1) + .departAimedExpected("00:10:00", "00:10:00") + .call(STOP_B1) + .arriveAimedExpected("00:20:00", "00:20:00") + ) + .buildEstimatedVehicleJourney(); + } + + private static TripInput tripInput(String trip1Id) { + return TripInput + .of(trip1Id) + .addStop(STOP_A1, "0:10:00", "0:10:00") + .addStop(STOP_B1, "0:20:00", "0:20:00") + .build(); + } +}