Skip to content

Commit

Permalink
Merge pull request #6448 from leonardehrenfried/siri-multiple-matches
Browse files Browse the repository at this point in the history
Return error codes from SiriFuzzyTripMatcher, add support for scheduled stop points
  • Loading branch information
leonardehrenfried authored Feb 13, 2025
2 parents e26cbb5 + 59888c3 commit bdd72dc
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 110 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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<Trip> 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<TripAndPattern, UpdateError.UpdateErrorType> match(
EstimatedVehicleJourney journey,
EntityResolver entityResolver,
BiFunction<TripPattern, LocalDate, Timetable> getCurrentTimetable,
Expand All @@ -92,11 +69,11 @@ public TripAndPattern match(
List<CallWrapper> 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<Trip> trips = null;
Expand All @@ -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) {
Expand Down Expand Up @@ -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<Trip> getMatchingTripsOnStopOrSiblings(
String lastStopPoint,
ZonedDateTime arrivalTime,
EntityResolver entityResolver
RegularStop lastStop,
ZonedDateTime arrivalTime
) {
int secondsSinceMidnight = ServiceDateUtils.secondsSinceStartOfService(
arrivalTime,
Expand All @@ -211,27 +196,23 @@ private Set<Trip> getMatchingTripsOnStopOrSiblings(
transitService.getTimeZone()
);

Set<Trip> trips = startStopTripCache.get(
createStartStopKey(lastStopPoint, secondsSinceMidnight)
);
Set<Trip> 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) {
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()) {
if (!lastStop.isPartOfStation()) {
return Set.of();
}

trips = new HashSet<>();
var allQuays = stop.getParentStation().getChildStops();
var allQuays = lastStop.getParentStation().getChildStops();
for (var quay : allQuays) {
Set<Trip> tripSet = startStopTripCache.get(
createStartStopKey(quay.getId().getId(), secondsSinceMidnight)
Expand All @@ -253,8 +234,7 @@ private Set<Trip> getCachedTripsByInternalPlanningCode(String internalPlanningCo
/**
* Finds the correct trip based on OTP-ServiceDate and SIRI-DepartureTime
*/
@Nullable
TripAndPattern getTripAndPatternForJourney(
private Result<TripAndPattern, UpdateError.UpdateErrorType> getTripAndPatternForJourney(
Set<Trip> trips,
List<CallWrapper> calls,
EntityResolver entityResolver,
Expand All @@ -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();
Expand Down Expand Up @@ -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<Trip> 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<Trip> results = new ArrayList<>();
for (Trip trip : trips) {
Set<LocalDate> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,21 +219,22 @@ private Result<TripUpdate, UpdateError> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public List<EstimatedTimetableDeliveryStructure> buildEstimatedTimetableDeliveri
return List.of(etd);
}

public EstimatedVehicleJourney buildEstimatedVehicleJourney() {
return evj;
}

public SiriEtBuilder withCancellation(boolean canceled) {
evj.setCancellation(canceled);
return this;
Expand Down
Loading

0 comments on commit bdd72dc

Please sign in to comment.