diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java index 59995062eb7..c52a86c6ade 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/AddedTripBuilderTest.java @@ -32,7 +32,7 @@ import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitEditorService; @@ -135,15 +135,16 @@ void testAddedTrip() { assertTrue(addedTrip.isSuccess(), "Trip creation should succeed"); + TripUpdate tripUpdate = addedTrip.successValue(); // Assert trip - Trip trip = addedTrip.successValue().tripTimes().getTrip(); + Trip trip = tripUpdate.tripTimes().getTrip(); assertEquals(TRIP_ID, trip.getId(), "Trip should be mapped"); assertEquals(OPERATOR, trip.getOperator(), "operator should be mapped"); assertEquals(TRANSIT_MODE, trip.getMode(), "transitMode should be mapped"); assertEquals(SubMode.of(SUB_MODE), trip.getNetexSubMode(), "submode should be mapped"); assertNotNull(trip.getHeadsign(), "Headsign should be mapped"); assertEquals(HEADSIGN, trip.getHeadsign().toString(), "Headsign should be mapped"); - assertEquals(SERVICE_DATE, addedTrip.successValue().serviceDate()); + assertEquals(SERVICE_DATE, tripUpdate.serviceDate()); // Assert route Route route = trip.getRoute(); @@ -154,17 +155,11 @@ void testAddedTrip() { assertEquals(SubMode.of(SUB_MODE), route.getNetexSubmode(), "submode should be mapped"); assertNotEquals(REPLACED_ROUTE, route, "Should not re-use replaced route"); - assertEquals( - route, - transitService.getRouteForId(TransitModelForTest.id(LINE_REF)), - "Route should be added to transit index" - ); - assertEquals( - trip, - transitService.getTripForId(TRIP_ID), - "Route should be added to transit index" - ); - var pattern = transitService.getPatternForTrip(trip); + assertTrue(tripUpdate.isAddedRoute(), "The route is marked as created by real time updater"); + + assertTrue(tripUpdate.isAddedTrip(), "The trip is marked as created by real time updater"); + + TripPattern pattern = tripUpdate.addedTripPattern(); assertNotNull(pattern); assertEquals(route, pattern.getRoute()); assertTrue( @@ -173,19 +168,11 @@ void testAddedTrip() { .contains(TRANSIT_MODEL.getServiceCodes().get(trip.getServiceId())), "serviceId should be running on service date" ); - assertNotNull( - transitService.getTripOnServiceDateById(TRIP_ID), - "TripOnServiceDate should be added to transit index by id" - ); - assertNotNull( - transitService.getTripOnServiceDateForTripAndDay( - new TripIdAndServiceDate(TRIP_ID, SERVICE_DATE) - ), - "TripOnServiceDate should be added to transit index for trip and day" - ); + TripOnServiceDate tripOnServiceDate = tripUpdate.addedTripOnServiceDate(); + assertNotNull(tripOnServiceDate, "The TripUpdate should contain a new TripOnServiceDate"); // Assert stop pattern - var stopPattern = addedTrip.successValue().stopPattern(); + var stopPattern = tripUpdate.stopPattern(); assertEquals(stopPattern, pattern.getStopPattern()); assertEquals(3, stopPattern.getSize()); assertEquals(STOP_A, stopPattern.getStop(0)); @@ -215,7 +202,7 @@ void testAddedTrip() { assertEquals(0, scheduledTimes.getArrivalDelay(2)); // Assert updated trip times - var times = addedTrip.successValue().tripTimes(); + var times = tripUpdate.tripTimes(); assertEquals(trip, times.getTrip()); assertEquals(RealTimeState.ADDED, times.getRealTimeState()); assertFalse(times.isScheduled()); @@ -259,6 +246,8 @@ void testAddedTripOnAddedRoute() { .build(); assertTrue(firstAddedTrip.isSuccess(), "Trip creation should succeed"); + assertTrue(firstAddedTrip.successValue().isAddedRoute()); + var firstTrip = firstAddedTrip.successValue().tripTimes().getTrip(); var tripId2 = TransitModelForTest.id("TRIP_ID_2"); @@ -291,12 +280,6 @@ void testAddedTripOnAddedRoute() { assertEquals(tripId2, secondTrip.getId(), "Trip should be mapped"); assertNotEquals(firstTrip, secondTrip); - // Assert route - Route route = secondTrip.getRoute(); - assertSame(firstTrip.getRoute(), route, "route be reused from the first trip"); - - assertEquals(2, transitService.getPatternsForRoute(route).size()); - // Assert trip times var times = secondAddedTrip.successValue().tripTimes(); assertEquals(secondTrip, times.getTrip()); @@ -338,6 +321,9 @@ void testAddedTripOnExistingRoute() { Trip trip = addedTrip.successValue().tripTimes().getTrip(); assertEquals(TRIP_ID, trip.getId(), "Trip should be mapped"); assertSame(REPLACED_ROUTE, trip.getRoute()); + + // Assert route + assertFalse(addedTrip.successValue().isAddedRoute(), "The existing route should be reused"); } @Test @@ -370,6 +356,7 @@ void testAddedTripWithoutReplacedRoute() { assertEquals(TRIP_ID, trip.getId(), "Trip should be mapped"); // Assert route + assertTrue(addedTrip.successValue().isAddedRoute(), "A new route should be created"); Route route = trip.getRoute(); assertEquals(LINE_REF, route.getId().getId(), "route should be mapped"); assertEquals(AGENCY, route.getAgency(), "Agency should be taken from replaced route"); diff --git a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java index be5b30b38b9..cdbaa8f3d4c 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java @@ -33,7 +33,6 @@ import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.TransitEditorService; @@ -173,6 +172,7 @@ Result build() { return UpdateError.result(tripId, NO_START_DATE); } + boolean isAddedRoute = false; Route route = entityResolver.resolveRoute(lineRef); if (route == null) { Agency agency = resolveAgency(); @@ -180,8 +180,8 @@ Result build() { return UpdateError.result(tripId, CANNOT_RESOLVE_AGENCY); } route = createRoute(agency); + isAddedRoute = true; LOG.info("Adding route {} to transitModel.", route); - transitService.addRoutes(route); } Trip trip = createTrip(route, calServiceId); @@ -265,18 +265,16 @@ Result build() { .withReplacementFor(replacedTrips) .build(); - // Adding trip to index necessary to include values in graphql-queries - // TODO - SIRI: should more data be added to index? - transitService.addTripForId(tripId, trip); - transitService.addPatternForTrip(trip, pattern); - transitService.addPatternsForRoute(route, pattern); - transitService.addTripOnServiceDateById(tripOnServiceDate.getId(), tripOnServiceDate); - transitService.addTripOnServiceDateForTripAndDay( - new TripIdAndServiceDate(tripId, serviceDate), - tripOnServiceDate + return Result.success( + new TripUpdate( + stopPattern, + updatedTripTimes, + serviceDate, + tripOnServiceDate, + pattern, + isAddedRoute + ) ); - - return Result.success(new TripUpdate(stopPattern, updatedTripTimes, serviceDate)); } /** diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 9e2dfaff76b..6a68557167f 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; +import org.opentripplanner.model.RealtimeUpdate; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.model.TimetableSnapshotProvider; @@ -292,14 +293,25 @@ private Result addTripToGraphAndBuffer(TripUpdate tr Trip trip = tripUpdate.tripTimes().getTrip(); LocalDate serviceDate = tripUpdate.serviceDate(); - // Get cached trip pattern or create one if it doesn't exist yet - final TripPattern pattern = tripPatternCache.getOrCreateTripPattern( - tripUpdate.stopPattern(), - trip, - serviceDate - ); + final TripPattern pattern; + if (tripUpdate.isAddedTripPattern()) { + pattern = tripUpdate.addedTripPattern(); + } else { + // Get cached trip pattern or create one if it doesn't exist yet + pattern = + tripPatternCache.getOrCreateTripPattern(tripUpdate.stopPattern(), trip, serviceDate); + } + // Add new trip times to buffer, making protective copies as needed. Bubble success/error up. - var result = snapshotManager.updateBuffer(pattern, tripUpdate.tripTimes(), serviceDate); + RealtimeUpdate realtimeUpdate = new RealtimeUpdate( + pattern, + tripUpdate.tripTimes(), + serviceDate, + tripUpdate.addedTripOnServiceDate(), + tripUpdate.isAddedTrip(), + tripUpdate.isAddedRoute() + ); + var result = snapshotManager.updateBuffer(realtimeUpdate); LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); return result; } @@ -324,7 +336,7 @@ private boolean markScheduledTripAsDeleted(Trip trip, final LocalDate serviceDat } else { final RealTimeTripTimes newTripTimes = tripTimes.copyScheduledTimes(); newTripTimes.deleteTrip(); - snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); + snapshotManager.updateBuffer(new RealtimeUpdate(pattern, newTripTimes, serviceDate)); success = true; } } diff --git a/src/ext/java/org/opentripplanner/ext/siri/TripUpdate.java b/src/ext/java/org/opentripplanner/ext/siri/TripUpdate.java index 3c351f4596c..803c54c8ba4 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/TripUpdate.java +++ b/src/ext/java/org/opentripplanner/ext/siri/TripUpdate.java @@ -2,11 +2,52 @@ import java.time.LocalDate; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; +/** + * Represents the SIRI real-time update of a single trip. + * @param stopPattern the stop pattern to which belongs the updated trip. + * @param tripTimes the new trip times for the updated trip. + * @param serviceDate the service date for which this update applies (updates are valid only for one service date) + * @param addedTripOnServiceDate optionally if this trip update adds a new trip, the TripOnServiceDate corresponding to this new trip. + * @param addedTripPattern optionally if this trip update adds a new trip pattern , the new trip pattern to this new trip. + * @param isAddedRoute true if an added trip cannot be registered under an existing route and a new route must be created. + */ record TripUpdate( @Nonnull StopPattern stopPattern, @Nonnull TripTimes tripTimes, - @Nonnull LocalDate serviceDate -) {} + @Nonnull LocalDate serviceDate, + @Nullable TripOnServiceDate addedTripOnServiceDate, + @Nullable TripPattern addedTripPattern, + boolean isAddedRoute +) { + /** + * Create a trip update for an existing trip. + */ + public TripUpdate( + StopPattern stopPattern, + RealTimeTripTimes updatedTripTimes, + LocalDate serviceDate + ) { + this(stopPattern, updatedTripTimes, serviceDate, null, null, false); + } + + /** + * Return true if this trip update creates a new trip pattern. + */ + public boolean isAddedTripPattern() { + return addedTripPattern != null; + } + + /** + * Return true if this trip update creates a new trip. + */ + public boolean isAddedTrip() { + return addedTripOnServiceDate != null; + } +} diff --git a/src/main/java/org/opentripplanner/model/RealtimeUpdate.java b/src/main/java/org/opentripplanner/model/RealtimeUpdate.java new file mode 100644 index 00000000000..b163e2496dc --- /dev/null +++ b/src/main/java/org/opentripplanner/model/RealtimeUpdate.java @@ -0,0 +1,33 @@ +package org.opentripplanner.model; + +import java.time.LocalDate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.model.timetable.TripTimes; + +/** + * Represents the real-time update of a single trip. + * @param pattern the pattern to which belongs the updated trip. This can be a new pattern created in real-time. + * @param updatedTripTimes the new trip times for the updated trip. + * @param serviceDate the service date for which this update applies (updates are valid only for one service date) + * @param addedTripOnServiceDate optionally if this trip update adds a new trip, the TripOnServiceDate corresponding to this new trip. + * @param isAddedTrip true if this update creates a new trip, not present in scheduled data. + * @param isAddedRoute true if an added trip cannot be registered under an existing route and a new route must be created. + */ +public record RealtimeUpdate( + @Nonnull TripPattern pattern, + @Nonnull TripTimes updatedTripTimes, + @Nonnull LocalDate serviceDate, + @Nullable TripOnServiceDate addedTripOnServiceDate, + boolean isAddedTrip, + boolean isAddedRoute +) { + /** + * Create a real-time update for an existing trip. + */ + public RealtimeUpdate(TripPattern pattern, TripTimes updatedTripTimes, LocalDate serviceDate) { + this(pattern, updatedTripTimes, serviceDate, null, false, false); + } +} diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index c0a7737abce..ea1e5f657e5 100644 --- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -3,6 +3,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Multimap; import com.google.common.collect.SetMultimap; import java.time.LocalDate; import java.util.Collection; @@ -21,9 +22,12 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; 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.StopLocation; +import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateSuccess; @@ -118,6 +122,13 @@ public class TimetableSnapshot { */ private final SetMultimap patternsForStop; + private final Map realtimeAddedRoutes; + private final Map realTimeAddedTrips; + private final Map realTimeAddedPatternForTrip; + private final Multimap realTimeAddedPatternForRoute; + private final Map realTimeAddedTripOnServiceDateById; + private final Map realTimeAddedTripOnServiceDateForTripAndDay; + /** * Boolean value indicating that timetable snapshot is read only if true. Once it is true, it * shouldn't be possible to change it to false anymore. @@ -131,17 +142,40 @@ public class TimetableSnapshot { private boolean dirty = false; public TimetableSnapshot() { - this(new HashMap<>(), new HashMap<>(), HashMultimap.create(), false); + this( + new HashMap<>(), + new HashMap<>(), + new HashMap<>(), + new HashMap<>(), + new HashMap<>(), + HashMultimap.create(), + new HashMap<>(), + new HashMap<>(), + HashMultimap.create(), + false + ); } private TimetableSnapshot( Map> timetables, Map realtimeAddedTripPattern, + Map realtimeAddedRoutes, + Map realtimeAddedTrips, + Map realTimeAddedPatternForTrip, + Multimap realTimeAddedPatternForRoute, + Map realTimeAddedTripOnServiceDateById, + Map realTimeAddedTripOnServiceDateForTripAndDay, SetMultimap patternsForStop, boolean readOnly ) { this.timetables = timetables; this.realtimeAddedTripPattern = realtimeAddedTripPattern; + this.realtimeAddedRoutes = realtimeAddedRoutes; + this.realTimeAddedTrips = realtimeAddedTrips; + this.realTimeAddedPatternForTrip = realTimeAddedPatternForTrip; + this.realTimeAddedPatternForRoute = realTimeAddedPatternForRoute; + this.realTimeAddedTripOnServiceDateById = realTimeAddedTripOnServiceDateById; + this.realTimeAddedTripOnServiceDateForTripAndDay = realTimeAddedTripOnServiceDateForTripAndDay; this.patternsForStop = patternsForStop; this.readOnly = readOnly; } @@ -183,23 +217,71 @@ public boolean hasRealtimeAddedTripPatterns() { return !realtimeAddedTripPattern.isEmpty(); } + /** + * Return the route created by the updater for the given id. + */ + @Nullable + public Route getRealtimeAddedRoute(FeedScopedId id) { + return realtimeAddedRoutes.get(id); + } + + /** + * Return the trip created by the updater for the given id. + */ + @Nullable + public Trip getRealTimeAddedTrip(FeedScopedId id) { + return realTimeAddedTrips.get(id); + } + + /** + * Return the trip pattern created by the updater for the given trip. + */ + @Nullable + public TripPattern getRealTimeAddedPatternForTrip(Trip trip) { + return realTimeAddedPatternForTrip.get(trip); + } + + /** + * Return the trip patterns created by the updater for the given route. + */ + @Nullable + public Collection getRealTimeAddedPatternForRoute(Route route) { + return realTimeAddedPatternForRoute.get(route); + } + + /** + * Return the trip on service date created by the updater for the given id. + */ + @Nullable + public TripOnServiceDate getRealTimeAddedTripOnServiceDateById(FeedScopedId id) { + return realTimeAddedTripOnServiceDateById.get(id); + } + + /** + * Return the trip on service date created by the updater for the given trip and service date. + */ + @Nullable + public TripOnServiceDate getRealTimeAddedTripOnServiceDateForTripAndDay( + TripIdAndServiceDate tripIdAndServiceDate + ) { + return realTimeAddedTripOnServiceDateForTripAndDay.get(tripIdAndServiceDate); + } + /** * Update the TripTimes of one Trip in a Timetable of a TripPattern. If the Trip of the TripTimes - * does not exist yet in the Timetable, add it. This method will make a protective copy - * of the Timetable if such a copy has not already been made while building up this snapshot, - * handling both cases where patterns were pre-existing in static data or created by realtime data. + * does not exist yet in the Timetable, add it. This method will make a protective copy of the + * Timetable if such a copy has not already been made while building up this snapshot, handling + * both cases where patterns were pre-existing in static data or created by realtime data. * - * @param serviceDate service day for which this update is valid * @return whether the update was actually applied */ - public Result update( - TripPattern pattern, - TripTimes updatedTripTimes, - LocalDate serviceDate - ) { + public Result update(RealtimeUpdate realtimeUpdate) { // Preconditions + TripPattern pattern = realtimeUpdate.pattern(); Objects.requireNonNull(pattern); + LocalDate serviceDate = realtimeUpdate.serviceDate(); Objects.requireNonNull(serviceDate); + TripTimes updatedTripTimes = realtimeUpdate.updatedTripTimes(); if (readOnly) { throw new ConcurrentModificationException("This TimetableSnapshot is read-only."); @@ -212,7 +294,8 @@ public Result update( // Assume all trips in a pattern are from the same feed, which should be the case. // Find trip index - int tripIndex = tt.getTripIndex(updatedTripTimes.getTrip().getId()); + Trip trip = updatedTripTimes.getTrip(); + int tripIndex = tt.getTripIndex(trip.getId()); if (tripIndex == -1) { // Trip not found, add it tt.addTripTimes(updatedTripTimes); @@ -223,7 +306,7 @@ public Result update( if (pattern.isCreatedByRealtimeUpdater()) { // Remember this pattern for the added trip id and service date - FeedScopedId tripId = updatedTripTimes.getTrip().getId(); + FeedScopedId tripId = trip.getId(); TripIdAndServiceDate tripIdAndServiceDate = new TripIdAndServiceDate(tripId, serviceDate); realtimeAddedTripPattern.put(tripIdAndServiceDate, pattern); } @@ -231,6 +314,25 @@ public Result update( // To make these trip patterns visible for departureRow searches. addPatternToIndex(pattern); + Route route = trip.getRoute(); + if (realtimeUpdate.isAddedRoute()) { + realtimeAddedRoutes.put(route.getId(), route); + } + if (realtimeUpdate.isAddedTrip()) { + FeedScopedId tripId = trip.getId(); + realTimeAddedTrips.put(tripId, trip); + realTimeAddedPatternForTrip.put(trip, pattern); + realTimeAddedPatternForRoute.put(route, pattern); + TripOnServiceDate tripOnServiceDate = realtimeUpdate.addedTripOnServiceDate(); + if (tripOnServiceDate != null) { + realTimeAddedTripOnServiceDateById.put(tripOnServiceDate.getId(), tripOnServiceDate); + realTimeAddedTripOnServiceDateForTripAndDay.put( + new TripIdAndServiceDate(tripId, serviceDate), + tripOnServiceDate + ); + } + } + // The time tables are finished during the commit return Result.success(UpdateSuccess.noWarnings()); @@ -262,6 +364,12 @@ public TimetableSnapshot commit(TransitLayerUpdater transitLayerUpdater, boolean TimetableSnapshot ret = new TimetableSnapshot( Map.copyOf(timetables), Map.copyOf(realtimeAddedTripPattern), + Map.copyOf(realtimeAddedRoutes), + Map.copyOf(realTimeAddedTrips), + Map.copyOf(realTimeAddedPatternForTrip), + ImmutableSetMultimap.copyOf(realTimeAddedPatternForRoute), + Map.copyOf(realTimeAddedTripOnServiceDateById), + Map.copyOf(realTimeAddedTripOnServiceDateForTripAndDay), ImmutableSetMultimap.copyOf(patternsForStop), true ); diff --git a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index b3d68b6ffa5..b938ba538b7 100644 --- a/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -190,7 +190,14 @@ public RegularStop getRegularStop(FeedScopedId id) { @Override public Route getRouteForId(FeedScopedId id) { - return this.transitModelIndex.getRouteForId(id); + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot != null) { + Route realtimeAddedRoute = currentSnapshot.getRealtimeAddedRoute(id); + if (realtimeAddedRoute != null) { + return realtimeAddedRoute; + } + } + return transitModelIndex.getRouteForId(id); } /** @@ -272,16 +279,20 @@ public StopLocationsGroup getStopLocationsGroup(FeedScopedId id) { @Override public Trip getTripForId(FeedScopedId id) { - return this.transitModelIndex.getTripForId().get(id); + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot != null) { + Trip trip = currentSnapshot.getRealTimeAddedTrip(id); + if (trip != null) { + return trip; + } + } + return getScheduledTripForId(id); } - /** - * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * this when doing the issue #3030. - */ + @Nullable @Override - public void addTripForId(FeedScopedId tripId, Trip trip) { - transitModelIndex.getTripForId().put(tripId, trip); + public Trip getScheduledTripForId(FeedScopedId id) { + return this.transitModelIndex.getTripForId().get(id); } @Override @@ -298,18 +309,16 @@ public Collection getAllRoutes() { @Override public TripPattern getPatternForTrip(Trip trip) { + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot != null) { + TripPattern realtimeAddedTripPattern = currentSnapshot.getRealTimeAddedPatternForTrip(trip); + if (realtimeAddedTripPattern != null) { + return realtimeAddedTripPattern; + } + } return this.transitModelIndex.getPatternForTrip().get(trip); } - /** - * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * this when doing the issue #3030. - */ - @Override - public void addPatternForTrip(Trip trip, TripPattern pattern) { - transitModelIndex.getPatternForTrip().put(trip, pattern); - } - @Override public TripPattern getPatternForTrip(Trip trip, LocalDate serviceDate) { TripPattern realtimePattern = getRealtimeAddedTripPattern(trip.getId(), serviceDate); @@ -322,18 +331,16 @@ public TripPattern getPatternForTrip(Trip trip, LocalDate serviceDate) { @Override public Collection getPatternsForRoute(Route route) { OTPRequestTimeoutException.checkForTimeout(); + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot != null) { + Collection tripPatterns = currentSnapshot.getRealTimeAddedPatternForRoute(route); + if (!tripPatterns.isEmpty()) { + return tripPatterns; + } + } return this.transitModelIndex.getPatternsForRoute().get(route); } - /** - * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * this when doing the issue #3030. - */ - @Override - public void addPatternsForRoute(Route route, TripPattern pattern) { - transitModelIndex.getPatternsForRoute().put(route, pattern); - } - @Override public MultiModalStation getMultiModalStationForStation(Station station) { return this.transitModel.getStopModel().getMultiModalStationForStation(station); @@ -524,18 +531,18 @@ private TimetableSnapshot lazyGetTimeTableSnapShot() { @Override public TripOnServiceDate getTripOnServiceDateById(FeedScopedId datedServiceJourneyId) { + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot != null) { + TripOnServiceDate tripOnServiceDate = currentSnapshot.getRealTimeAddedTripOnServiceDateById( + datedServiceJourneyId + ); + if (tripOnServiceDate != null) { + return tripOnServiceDate; + } + } return transitModelIndex.getTripOnServiceDateById().get(datedServiceJourneyId); } - /** - * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * this when doing the issue #3030. - */ - @Override - public void addTripOnServiceDateById(FeedScopedId id, TripOnServiceDate tripOnServiceDate) { - transitModelIndex.getTripOnServiceDateById().put(id, tripOnServiceDate); - } - @Override public Collection getAllTripOnServiceDates() { return transitModelIndex.getTripOnServiceDateForTripAndDay().values(); @@ -545,23 +552,18 @@ public Collection getAllTripOnServiceDates() { public TripOnServiceDate getTripOnServiceDateForTripAndDay( TripIdAndServiceDate tripIdAndServiceDate ) { + TimetableSnapshot currentSnapshot = lazyGetTimeTableSnapShot(); + if (currentSnapshot != null) { + TripOnServiceDate tripOnServiceDate = currentSnapshot.getRealTimeAddedTripOnServiceDateForTripAndDay( + tripIdAndServiceDate + ); + if (tripOnServiceDate != null) { + return tripOnServiceDate; + } + } return transitModelIndex.getTripOnServiceDateForTripAndDay().get(tripIdAndServiceDate); } - /** - * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix - * this when doing the issue #3030. - */ - @Override - public void addTripOnServiceDateForTripAndDay( - TripIdAndServiceDate tripIdAndServiceDate, - TripOnServiceDate tripOnServiceDate - ) { - transitModelIndex - .getTripOnServiceDateForTripAndDay() - .put(tripIdAndServiceDate, tripOnServiceDate); - } - /** * TODO OTP2 - This is NOT THREAD-SAFE and is used in the real-time updaters, we need to fix * this when doing the issue #3030. diff --git a/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java b/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java index 150d1749272..567cb245af5 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitEditorService.java @@ -20,23 +20,10 @@ public interface TransitEditorService extends TransitService { void addFeedInfo(FeedInfo info); - void addPatternForTrip(Trip trip, TripPattern pattern); - - void addPatternsForRoute(Route route, TripPattern pattern); - void addRoutes(Route route); void addTransitMode(TransitMode mode); - void addTripForId(FeedScopedId tripId, Trip trip); - - void addTripOnServiceDateById(FeedScopedId id, TripOnServiceDate tripOnServiceDate); - - void addTripOnServiceDateForTripAndDay( - TripIdAndServiceDate tripIdAndServiceDate, - TripOnServiceDate tripOnServiceDate - ); - FeedScopedId getOrCreateServiceIdForDate(LocalDate serviceDate); /** diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 94870643f71..faf75d36b9e 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -132,8 +132,18 @@ public interface TransitService { AreaStop getAreaStop(FeedScopedId id); + /** + * Return the trip for the given id, including trips created in real time. + */ + @Nullable Trip getTripForId(FeedScopedId id); + /** + * Return the trip for the given id, not including trips created in real time. + */ + @Nullable + Trip getScheduledTripForId(FeedScopedId id); + Collection getAllTrips(); Collection getAllRoutes(); diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java index b5683a9a62e..cb7cae7270b 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.function.Supplier; import javax.annotation.Nullable; +import org.opentripplanner.model.RealtimeUpdate; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; @@ -11,7 +12,6 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.Result; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.updater.TimetableSnapshotSourceParameters; import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateSuccess; @@ -183,19 +183,14 @@ public void clearBuffer(String feedId) { /** * Update the TripTimes of one Trip in a Timetable of a TripPattern. If the Trip of the TripTimes - * does not exist yet in the Timetable, add it. This method will make a protective copy - * of the Timetable if such a copy has not already been made while building up this snapshot, - * handling both cases where patterns were pre-existing in static data or created by realtime data. + * does not exist yet in the Timetable, add it. This method will make a protective copy of the + * Timetable if such a copy has not already been made while building up this snapshot, handling + * both cases where patterns were pre-existing in static data or created by realtime data. * - * @param serviceDate service day for which this update is valid * @return whether the update was actually applied */ - public Result updateBuffer( - TripPattern pattern, - TripTimes tripTimes, - LocalDate serviceDate - ) { - return buffer.update(pattern, tripTimes, serviceDate); + public Result updateBuffer(RealtimeUpdate realtimeUpdate) { + return buffer.update(realtimeUpdate); } /** diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index 56e3c380b67..0c95124292b 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -41,6 +41,7 @@ import org.opentripplanner.framework.lang.StringUtils; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.gtfs.mapping.TransitModeMapper; +import org.opentripplanner.model.RealtimeUpdate; import org.opentripplanner.model.StopTime; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; @@ -424,10 +425,14 @@ private Result handleScheduledTrip( ); cancelScheduledTrip(tripId, serviceDate, CancelationType.DELETE); - return snapshotManager.updateBuffer(newPattern, updatedTripTimes, serviceDate); + return snapshotManager.updateBuffer( + new RealtimeUpdate(newPattern, updatedTripTimes, serviceDate) + ); } else { // Set the updated trip times in the buffer - return snapshotManager.updateBuffer(pattern, updatedTripTimes, serviceDate); + return snapshotManager.updateBuffer( + new RealtimeUpdate(pattern, updatedTripTimes, serviceDate) + ); } } @@ -453,7 +458,7 @@ private Result validateAndHandleAddedTrip( // // Check whether trip id already exists in graph - final Trip trip = transitEditorService.getTripForId(tripId); + final Trip trip = transitEditorService.getScheduledTripForId(tripId); if (trip != null) { // TODO: should we support this and add a new instantiation of this trip (making it @@ -630,7 +635,16 @@ private Result handleAddedTrip( "number of stop should match the number of stop time updates" ); - Route route = getOrCreateRoute(tripDescriptor, tripId); + Route route; + boolean routeExists = routeExists(tripId.getFeedId(), tripDescriptor); + if (routeExists) { + route = + transitEditorService.getRouteForId( + new FeedScopedId(tripId.getFeedId(), tripDescriptor.getRouteId()) + ); + } else { + route = createRoute(tripDescriptor, tripId); + } // Create new Trip @@ -660,19 +674,14 @@ private Result handleAddedTrip( stopTimeUpdates, stops, serviceDate, - RealTimeState.ADDED + RealTimeState.ADDED, + !routeExists ); } - private Route getOrCreateRoute(TripDescriptor tripDescriptor, FeedScopedId tripId) { - if (routeExists(tripId.getFeedId(), tripDescriptor)) { - // Try to find route - return transitEditorService.getRouteForId( - new FeedScopedId(tripId.getFeedId(), tripDescriptor.getRouteId()) - ); - } + private Route createRoute(TripDescriptor tripDescriptor, FeedScopedId tripId) { // the route in this update doesn't already exist, but the update contains the information so it will be created - else if ( + if ( tripDescriptor.hasExtension(MfdzRealtimeExtensions.tripDescriptor) && !routeExists(tripId.getFeedId(), tripDescriptor) ) { @@ -696,10 +705,7 @@ else if ( var name = Objects.requireNonNullElse(addedRouteExtension.routeLongName(), tripId.toString()); builder.withLongName(new NonLocalizedString(name)); builder.withUrl(addedRouteExtension.routeUrl()); - - var route = builder.build(); - transitEditorService.addRoutes(route); - return route; + return builder.build(); } // no information about the rout is given, so we create a dummy one else { @@ -713,9 +719,7 @@ else if ( // Create route name I18NString longName = NonLocalizedString.ofNullable(tripDescriptor.getTripId()); builder.withLongName(longName); - var route = builder.build(); - transitEditorService.addRoutes(route); - return route; + return builder.build(); } } @@ -755,7 +759,8 @@ private Result addTripToGraphAndBuffer( final List stopTimeUpdates, final List stops, final LocalDate serviceDate, - final RealTimeState realTimeState + final RealTimeState realTimeState, + final boolean isAddedRoute ) { // Preconditions Objects.requireNonNull(stops); @@ -869,7 +874,17 @@ private Result addTripToGraphAndBuffer( pattern.lastStop().getName() ); // Add new trip times to the buffer - return snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); + // TODO add support for TripOnServiceDate for GTFS-RT + return snapshotManager.updateBuffer( + new RealtimeUpdate( + pattern, + newTripTimes, + serviceDate, + null, + realTimeState == RealTimeState.ADDED, + isAddedRoute + ) + ); } /** @@ -900,7 +915,7 @@ private boolean cancelScheduledTrip( case CANCEL -> newTripTimes.cancelTrip(); case DELETE -> newTripTimes.deleteTrip(); } - snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); + snapshotManager.updateBuffer(new RealtimeUpdate(pattern, newTripTimes, serviceDate)); success = true; } } @@ -939,7 +954,7 @@ private boolean cancelPreviouslyAddedTrip( case CANCEL -> newTripTimes.cancelTrip(); case DELETE -> newTripTimes.deleteTrip(); } - snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); + snapshotManager.updateBuffer(new RealtimeUpdate(pattern, newTripTimes, serviceDate)); cancelledAddedTrip = true; } } @@ -1045,7 +1060,8 @@ private Result handleModifiedTrip( tripUpdate.getStopTimeUpdateList(), stops, serviceDate, - RealTimeState.MODIFIED + RealTimeState.MODIFIED, + false ); } diff --git a/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java b/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java index 89c30da79e4..1a8af9166fe 100644 --- a/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java +++ b/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java @@ -269,7 +269,7 @@ void testCannotUpdateReadOnlyTimetableSnapshot() { TripPattern pattern = patternIndex.get(new FeedScopedId(feedId, "1.1")); assertThrows( ConcurrentModificationException.class, - () -> committedSnapshot.update(pattern, null, today) + () -> committedSnapshot.update(new RealtimeUpdate(pattern, null, today)) ); } @@ -323,7 +323,9 @@ private Result updateResolver( BackwardsDelayPropagationType.REQUIRED_NO_DATA ); if (result.isSuccess()) { - return resolver.update(pattern, result.successValue().getTripTimes(), serviceDate); + return resolver.update( + new RealtimeUpdate(pattern, result.successValue().getTripTimes(), serviceDate) + ); } throw new RuntimeException("createUpdatedTripTimes returned an error: " + result); } diff --git a/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java b/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java index 54733f084d6..c2317032d75 100644 --- a/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java +++ b/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java @@ -12,6 +12,7 @@ import java.util.Set; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.opentripplanner.model.RealtimeUpdate; import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.Deduplicator; @@ -68,7 +69,7 @@ static void setup() { .withDepartureTimes(new int[] { 0, 1 }) .build() ); - timetableSnapshot.update(REAL_TIME_PATTERN, tripTimes, LocalDate.now()); + timetableSnapshot.update(new RealtimeUpdate(REAL_TIME_PATTERN, tripTimes, LocalDate.now())); return timetableSnapshot.commit(); }); diff --git a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java index 384e8a41fd1..6f21d9627dd 100644 --- a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java @@ -6,7 +6,6 @@ import static org.opentripplanner.updater.trip.TimetableSnapshotManagerTest.SameAssert.NotSame; import static org.opentripplanner.updater.trip.TimetableSnapshotManagerTest.SameAssert.Same; -import java.time.Duration; import java.time.LocalDate; import java.time.Month; import java.util.concurrent.atomic.AtomicReference; @@ -14,6 +13,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.model.RealtimeUpdate; import org.opentripplanner.model.TimetableSnapshot; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.network.TripPattern; @@ -86,7 +86,7 @@ public void testPurgeExpiredData( clock::get ); - var res1 = snapshotManager.updateBuffer(PATTERN, TRIP_TIMES, YESTERDAY); + var res1 = snapshotManager.updateBuffer(new RealtimeUpdate(PATTERN, TRIP_TIMES, YESTERDAY)); assertTrue(res1.isSuccess()); snapshotManager.commitTimetableSnapshot(true); @@ -95,7 +95,7 @@ public void testPurgeExpiredData( // Turn the clock to tomorrow clock.set(TOMORROW); - var res2 = snapshotManager.updateBuffer(PATTERN, TRIP_TIMES, TODAY); + var res2 = snapshotManager.updateBuffer(new RealtimeUpdate(PATTERN, TRIP_TIMES, TODAY)); assertTrue(res2.isSuccess()); snapshotManager.purgeAndCommit();