diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RealTimeRaptorTransitDataUpdater.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RealTimeRaptorTransitDataUpdater.java index 7bb4b82bc1a..2b83cc5678b 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RealTimeRaptorTransitDataUpdater.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RealTimeRaptorTransitDataUpdater.java @@ -66,6 +66,31 @@ public RealTimeRaptorTransitDataUpdater(TimetableRepository timetableRepository) this.timetableRepository = timetableRepository; } + /// Updates the real-time [RaptorTransitData] to use the modified timetables. + /// + /// This method bridges the different update approaches: + /// 1. `updatedTimetables` and `timetables` only contains [Timetable]s with real-time + /// updates. This means that removed items are not present. + /// 2. [RaptorTransitData] requires applying the changes to a previous snapshot: adding, + /// updating and removing timetables. + /// + /// To support this the method has three tasks: + /// 1. Collect [TripPatternForDate]s which have invalidated data (`oldTripPatternsForDate`). + /// Trips may change in multiple ways and because of that may move between [TripPattern]s. To + /// track a [TripIdAndServiceDate] it's previous state needs to be stored so that all relevant + /// places may be updated. + /// * a trip may have a new (real-time) Timetable, which results in two updated [Timetable]s + /// * a trip may move between scheduled [StopPattern]s and/or real-time [StopPattern]s + /// 2. Collect [TripPatternForDate]s which have valid data (`newTripPatternsForDate`). + /// There are two options: + /// 1. an update was received + /// 2. no update was received, and so the previous updated should be removed. If the update + /// was for a scheduled trip, then the schedule should be restored. + /// 3. Remove the `oldTripPatternsForDate` and add the `newTripPatternsForDate` to the + /// [RaptorTransitData]. + /// + /// @param updatedTimetables that changed with the current snapshot + /// @param timetables which are affected by real-time updates public void update( Collection updatedTimetables, Map> timetables @@ -204,7 +229,8 @@ public void update( patternsForDate.remove(tripPatternForDate); } } else { - LOG.warn("Could not fetch timetable for {}", pattern); + LOG.warn("Could not fetch timetable for {}, removing.", pattern); + patternsForDate.remove(tripPatternForDate); } } } diff --git a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdater.java b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdater.java index 39af689a2b8..87033f08711 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdater.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdater.java @@ -15,6 +15,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import javax.annotation.Nullable; @@ -132,10 +133,26 @@ public static Result createUpdatedTripTimesFromGtfs } if (match) { - update.stopHeadsign().ifPresent(x -> builder.withStopHeadsign(index, x)); - update.pickup().ifPresent(x -> updatedPickups.put(index, x)); - update.dropoff().ifPresent(x -> updatedDropoffs.put(index, x)); - update.assignedStopId().ifPresent(x -> replacedStopIndices.put(index, x)); + var scheduledStopId = timetable.getPattern().getStop(i).getId().getId(); + var scheduledStopHeadsign = tripTimes.getHeadsign(i); + var scheduledPickup = timetable.getPattern().getBoardType(i); + var scheduledDropoff = timetable.getPattern().getAlightType(i); + update + .stopHeadsign() + .filter(x -> !Objects.equals(x, scheduledStopHeadsign)) + .ifPresent(x -> builder.withStopHeadsign(index, x)); + update + .pickup() + .filter(x -> x != scheduledPickup) + .ifPresent(x -> updatedPickups.put(index, x)); + update + .dropoff() + .filter(x -> x != scheduledDropoff) + .ifPresent(x -> updatedDropoffs.put(index, x)); + update + .assignedStopId() + .filter(x -> !Objects.equals(x, scheduledStopId)) + .ifPresent(x -> replacedStopIndices.put(index, x)); var scheduleRelationship = update.scheduleRelationship(); // Handle each schedule relationship case diff --git a/application/src/test/java/org/opentripplanner/transit/model/_data/TestTransitTuningParameters.java b/application/src/test/java/org/opentripplanner/transit/model/_data/TestTransitTuningParameters.java new file mode 100644 index 00000000000..fd269fdf04a --- /dev/null +++ b/application/src/test/java/org/opentripplanner/transit/model/_data/TestTransitTuningParameters.java @@ -0,0 +1,40 @@ +package org.opentripplanner.transit.model._data; + +import java.time.Duration; +import java.util.List; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitTuningParameters; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.transit.model.site.StopTransferPriority; + +class TestTransitTuningParameters implements TransitTuningParameters { + + @Override + public boolean enableStopTransferPriority() { + return false; + } + + @Override + public Integer stopBoardAlightDuringTransferCost(StopTransferPriority key) { + return 0; + } + + @Override + public int transferCacheMaxSize() { + return 0; + } + + @Override + public Duration maxSearchWindow() { + return null; + } + + @Override + public List pagingSearchWindowAdjustments() { + return List.of(); + } + + @Override + public List transferCacheRequests() { + return List.of(); + } +} diff --git a/application/src/test/java/org/opentripplanner/transit/model/_data/TransitTestEnvironment.java b/application/src/test/java/org/opentripplanner/transit/model/_data/TransitTestEnvironment.java index be2b3af3c6e..a57e2b5264a 100644 --- a/application/src/test/java/org/opentripplanner/transit/model/_data/TransitTestEnvironment.java +++ b/application/src/test/java/org/opentripplanner/transit/model/_data/TransitTestEnvironment.java @@ -6,6 +6,9 @@ import java.time.ZoneId; import org.opentripplanner.LocalTimeParser; import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RaptorTransitData; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.RaptorTransitDataMapper; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.RealTimeRaptorTransitDataUpdater; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitService; @@ -40,8 +43,14 @@ public static TransitTestEnvironmentBuilder of(LocalDate serviceDate, ZoneId tim this.timetableRepository = timetableRepository; this.timetableRepository.index(); + this.timetableRepository.setRaptorTransitData( + RaptorTransitDataMapper.map(new TestTransitTuningParameters(), timetableRepository) + ); + this.timetableRepository.setRealtimeRaptorTransitData( + new RaptorTransitData(timetableRepository.getRaptorTransitData()) + ); this.snapshotManager = new TimetableSnapshotManager( - null, + new RealTimeRaptorTransitDataUpdater(timetableRepository), TimetableSnapshotParameters.PUBLISH_IMMEDIATELY, () -> defaultServiceDate ); diff --git a/application/src/test/java/org/opentripplanner/updater/trip/GtfsRtTestHelper.java b/application/src/test/java/org/opentripplanner/updater/trip/GtfsRtTestHelper.java index d1c4e5899a9..98fe1416e42 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/GtfsRtTestHelper.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/GtfsRtTestHelper.java @@ -55,6 +55,10 @@ public UpdateResult applyTripUpdate( return applyTripUpdates(List.of(update), incrementality); } + public UpdateResult applyTripUpdates(List updates) { + return applyTripUpdates(updates, FULL_DATASET); + } + public UpdateResult applyTripUpdates( List updates, UpdateIncrementality incrementality diff --git a/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestConstants.java b/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestConstants.java index 7e0a1868d10..65ca23c9dc0 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestConstants.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/RealtimeTestConstants.java @@ -10,4 +10,5 @@ public interface RealtimeTestConstants { String STOP_B_ID = "B"; String STOP_C_ID = "C"; String STOP_D_ID = "D"; + String STOP_E_ID = "E"; } diff --git a/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateBuilder.java b/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateBuilder.java index 7d996f2ef31..d8d341e741c 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateBuilder.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateBuilder.java @@ -39,10 +39,47 @@ public TripUpdateBuilder( this.midnight = ServiceDateUtils.asStartOfService(serviceDate, zoneId); } + public TripUpdateBuilder( + String tripId, + LocalDate serviceDate, + GtfsRealtime.TripDescriptor.ScheduleRelationship scheduleRelationship, + ZoneId zoneId, + String tripHeadsign, + String tripShortName + ) { + this(tripId, serviceDate, scheduleRelationship, zoneId); + if (tripHeadsign != null) { + tripUpdateBuilder.getTripPropertiesBuilder().setTripHeadsign(tripHeadsign); + } + + if (tripShortName != null) { + tripUpdateBuilder.getTripPropertiesBuilder().setTripShortName(tripShortName); + } + } + + public TripUpdateBuilder addStopTime(int stopSequence, String time) { + return addStopTime( + null, + time, + time, + stopSequence, + NO_DELAY, + NO_DELAY, + DEFAULT_SCHEDULE_RELATIONSHIP, + null, + null, + null, + null, + null, + null + ); + } + public TripUpdateBuilder addStopTime(String stopId, String time) { return addStopTime( stopId, time, + time, NO_VALUE, NO_DELAY, NO_DELAY, @@ -50,6 +87,8 @@ public TripUpdateBuilder addStopTime(String stopId, String time) { null, null, null, + null, + null, null ); } @@ -58,6 +97,7 @@ public TripUpdateBuilder addStopTime(String stopId, String time, String headsign return addStopTime( stopId, time, + time, NO_VALUE, NO_DELAY, NO_DELAY, @@ -65,6 +105,8 @@ public TripUpdateBuilder addStopTime(String stopId, String time, String headsign null, null, headsign, + null, + null, null ); } @@ -73,6 +115,7 @@ public TripUpdateBuilder addStopTimeWithDelay(String stopId, String time, int de return addStopTime( stopId, time, + time, NO_VALUE, delay, delay, @@ -80,6 +123,8 @@ public TripUpdateBuilder addStopTimeWithDelay(String stopId, String time, int de null, null, null, + null, + null, null ); } @@ -92,6 +137,7 @@ public TripUpdateBuilder addStopTimeWithScheduled( return addStopTime( stopId, time, + time, NO_VALUE, NO_DELAY, NO_DELAY, @@ -99,7 +145,9 @@ public TripUpdateBuilder addStopTimeWithScheduled( null, null, null, - scheduledTime + scheduledTime, + scheduledTime, + null ); } @@ -107,6 +155,7 @@ public TripUpdateBuilder addStopTime(String stopId, String time, DropOffPickupTy return addStopTime( stopId, time, + time, NO_VALUE, NO_DELAY, NO_DELAY, @@ -114,6 +163,8 @@ public TripUpdateBuilder addStopTime(String stopId, String time, DropOffPickupTy pickDrop, null, null, + null, + null, null ); } @@ -126,6 +177,7 @@ public TripUpdateBuilder addStopTime( return addStopTime( stopId, time, + time, NO_VALUE, NO_DELAY, NO_DELAY, @@ -133,12 +185,55 @@ public TripUpdateBuilder addStopTime( null, pickDrop, null, + null, + null, + null + ); + } + + public TripUpdateBuilder addStopTimeWithArrivalAndDeparture( + int stopSequence, + String arrivalTime, + String departureTime + ) { + return addStopTime( + null, + arrivalTime, + departureTime, + stopSequence, + NO_DELAY, + NO_DELAY, + DEFAULT_SCHEDULE_RELATIONSHIP, + null, + null, + null, + null, + null, null ); } public TripUpdateBuilder addDelayedStopTime(int stopSequence, int delay) { return addStopTime( + null, + null, + null, + stopSequence, + delay, + delay, + DEFAULT_SCHEDULE_RELATIONSHIP, + null, + null, + null, + null, + null, + null + ); + } + + public TripUpdateBuilder addDelayedStopTime(int stopSequence, int delay, String headsign) { + return addStopTime( + null, null, null, stopSequence, @@ -147,6 +242,8 @@ public TripUpdateBuilder addDelayedStopTime(int stopSequence, int delay) { DEFAULT_SCHEDULE_RELATIONSHIP, null, null, + headsign, + null, null, null ); @@ -158,6 +255,7 @@ public TripUpdateBuilder addDelayedStopTime( int departureDelay ) { return addStopTime( + null, null, null, stopSequence, @@ -167,6 +265,8 @@ public TripUpdateBuilder addDelayedStopTime( null, null, null, + null, + null, null ); } @@ -176,6 +276,7 @@ public TripUpdateBuilder addDelayedStopTime( */ public TripUpdateBuilder addNoDataStop(int stopSequence) { return addStopTime( + null, null, null, stopSequence, @@ -185,6 +286,8 @@ public TripUpdateBuilder addNoDataStop(int stopSequence) { null, null, null, + null, + null, null ); } @@ -194,6 +297,7 @@ public TripUpdateBuilder addNoDataStop(int stopSequence) { */ public TripUpdateBuilder addSkippedStop(int stopSequence) { return addStopTime( + null, null, null, stopSequence, @@ -203,6 +307,8 @@ public TripUpdateBuilder addSkippedStop(int stopSequence) { null, null, null, + null, + null, null ); } @@ -211,6 +317,7 @@ public TripUpdateBuilder addSkippedStop(String stopId, String time) { return addStopTime( stopId, time, + time, NO_VALUE, NO_DELAY, NO_DELAY, @@ -218,6 +325,8 @@ public TripUpdateBuilder addSkippedStop(String stopId, String time) { null, null, null, + null, + null, null ); } @@ -226,6 +335,7 @@ public TripUpdateBuilder addSkippedStop(String stopId, String time, DropOffPicku return addStopTime( stopId, time, + time, NO_VALUE, NO_DELAY, NO_DELAY, @@ -233,10 +343,34 @@ public TripUpdateBuilder addSkippedStop(String stopId, String time, DropOffPicku pickDrop, null, null, + null, + null, null ); } + public TripUpdateBuilder addAssignedStopTime( + int stopSequence, + String time, + String assignedStopId + ) { + return addStopTime( + null, + time, + time, + stopSequence, + NO_DELAY, + NO_DELAY, + StopTimeUpdate.ScheduleRelationship.SCHEDULED, + null, + null, + null, + null, + null, + assignedStopId + ); + } + /** * As opposed to the other convenience methods, this one takes a raw {@link StopTimeUpdate} and * adds it to the trip. This is useful if you want to test invalid ones. @@ -248,7 +382,8 @@ public TripUpdateBuilder addRawStopTime(StopTimeUpdate stopTime) { private TripUpdateBuilder addStopTime( @Nullable String stopId, - @Nullable String time, + @Nullable String arrivalTime, + @Nullable String departureTime, int stopSequence, int arrivalDelay, int departureDelay, @@ -256,7 +391,9 @@ private TripUpdateBuilder addStopTime( @Nullable DropOffPickupType pickDrop, @Nullable StopTimeUpdate.StopTimeProperties.DropOffPickupType gtfsPickDrop, @Nullable String headsign, - @Nullable String scheduledTime + @Nullable String scheduledArrivalTime, + @Nullable String scheduledDepartureTime, + @Nullable String assignedStopId ) { final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); @@ -270,7 +407,7 @@ private TripUpdateBuilder addStopTime( stopTimeUpdateBuilder.setStopSequence(stopSequence); } - if (pickDrop != null || gtfsPickDrop != null || headsign != null) { + if (pickDrop != null || gtfsPickDrop != null || headsign != null || assignedStopId != null) { var stopTimePropsBuilder = stopTimeUpdateBuilder.getStopTimePropertiesBuilder(); if (headsign != null) { @@ -288,6 +425,10 @@ private TripUpdateBuilder addStopTime( var ext = b.build(); stopTimePropsBuilder.setExtension(MfdzRealtimeExtensions.stopTimeProperties, ext); } + + if (assignedStopId != null) { + stopTimePropsBuilder.setAssignedStopId(assignedStopId); + } } final GtfsRealtime.TripUpdate.StopTimeEvent.Builder arrivalBuilder = @@ -295,15 +436,25 @@ private TripUpdateBuilder addStopTime( final GtfsRealtime.TripUpdate.StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - if (time != null) { - var epochSeconds = midnight.plusSeconds(TimeUtils.time(time)).toEpochSecond(); + if (arrivalTime != null) { + var epochSeconds = midnight.plusSeconds(TimeUtils.time(arrivalTime)).toEpochSecond(); arrivalBuilder.setTime(epochSeconds); + } + + if (departureTime != null) { + var epochSeconds = midnight.plusSeconds(TimeUtils.time(departureTime)).toEpochSecond(); departureBuilder.setTime(epochSeconds); } - if (scheduledTime != null) { - var epochSeconds = midnight.plusSeconds(TimeUtils.time(scheduledTime)).toEpochSecond(); + if (scheduledArrivalTime != null) { + var epochSeconds = midnight.plusSeconds(TimeUtils.time(scheduledArrivalTime)).toEpochSecond(); arrivalBuilder.setScheduledTime(epochSeconds); + } + + if (scheduledDepartureTime != null) { + var epochSeconds = midnight + .plusSeconds(TimeUtils.time(scheduledDepartureTime)) + .toEpochSecond(); departureBuilder.setScheduledTime(epochSeconds); } diff --git a/application/src/test/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdaterTest.java b/application/src/test/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdaterTest.java index ebe9b48b53f..acb6c611a1a 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdaterTest.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/gtfs/TripTimesUpdaterTest.java @@ -17,8 +17,6 @@ import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent; import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.Month; import java.time.ZoneId; import java.util.HashMap; import java.util.Map; @@ -33,14 +31,14 @@ import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.Timetable; 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.RealTimeState; +import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.trip.TripUpdateBuilder; -import org.opentripplanner.updater.trip.gtfs.model.TripTimesPatch; import org.opentripplanner.updater.trip.gtfs.model.TripUpdate; +import org.opentripplanner.utils.time.TimeUtils; public class TripTimesUpdaterTest { @@ -73,19 +71,12 @@ public static void setUp() throws Exception { @Test public void tripNotFoundInPattern() { // non-existing trip - var tripDescriptorBuilder = tripDescriptorBuilder("b"); + var nonExistingTripId = "b"; + var tripUpdate = new TripUpdateBuilder(nonExistingTripId, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addNoDataStop(0) + .build(); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder; - StopTimeUpdate.Builder stopTimeUpdateBuilder; - - tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(0); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.NO_DATA); - var tripUpdate = tripUpdateBuilder.build(); - - Result result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), TIME_ZONE, @@ -96,25 +87,18 @@ public void tripNotFoundInPattern() { assertTrue(result.isFailure()); result.ifFailure(r -> { - assertEquals(new FeedScopedId(feedId, "b"), r.tripId()); + assertEquals(new FeedScopedId(feedId, nonExistingTripId), r.tripId()); assertEquals(TRIP_NOT_FOUND_IN_PATTERN, r.errorType()); }); } @Test public void badData() { - GtfsRealtime.TripUpdate tripUpdate; - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder; - StopTimeUpdate.Builder stopTimeUpdateBuilder; - // update trip with bad data - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(0); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SKIPPED); - tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addSkippedStop(0) + .build(); + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -131,21 +115,10 @@ public void badData() { @Test public void nonIncreasingTimes() { // update trip with non-increasing data - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - var tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - var stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - var stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setTime( - LocalDateTime.of(2009, Month.AUGUST, 7, 0, 10, 1, 0).atZone(ZoneIds.NEW_YORK).toEpochSecond() - ); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setTime( - LocalDateTime.of(2009, Month.AUGUST, 7, 0, 10, 0, 0).atZone(TIME_ZONE).toEpochSecond() - ); - var tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addStopTimeWithArrivalAndDeparture(2, "00:10:01", "00:10:00") + .build(); + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -161,27 +134,22 @@ public void nonIncreasingTimes() { @Test public void update() { - // update trip - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - - var tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - var stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - var stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setTime( - LocalDateTime.of(2009, Month.AUGUST, 7, 0, 2, 0, 0).atZone(ZoneIds.NEW_YORK).toEpochSecond() - ); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setTime( - LocalDateTime.of(2009, Month.AUGUST.getValue() - 1 + 1, 7, 0, 2, 0, 0) - .atZone(ZoneId.of("America/New_York")) - .toEpochSecond() - ); - var tripUpdate = tripUpdateBuilder.build(); var timetable = TripTimesUpdaterTest.timetable; - assertEquals(20 * 60, timetable.getTripTimes(tripId).getArrivalTime(2)); + assertTimetable( + timetable.getTripTimes(tripId), + "00:00:00", + "00:00:00", + "00:10:00", + "00:10:00", + "00:20:00", + "00:20:00" + ); + + // update trip + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addStopTime(1, "00:02:00") + .build(); + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -197,18 +165,26 @@ public void update() { var updatedTripTimes = p.tripTimes(); assertNotNull(updatedTripTimes); timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); - assertEquals(20 * 60 + 120, timetable.getTripTimes(tripId).getArrivalTime(2)); + assertTimetable( + timetable.getTripTimes(tripId), + "00:02:00", + "00:02:00", + "00:12:00", + "00:12:00", + "00:22:00", + "00:22:00" + ); // update trip arrival time incorrectly - tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(0); - tripUpdate = tripUpdateBuilder.build(); + tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(1) + .setArrival(StopTimeEvent.newBuilder().setDelay(0).build()) + .build() + ) + .build(); + result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -224,19 +200,25 @@ public void update() { updatedTripTimes = p.tripTimes(); assertNotNull(updatedTripTimes); timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); + assertTimetable( + timetable.getTripTimes(tripId), + "00:00:00", + "00:00:00", + "00:10:00", + "00:10:00", + "00:20:00", + "00:20:00" + ); // update trip arrival time only - tripDescriptorBuilder = TripDescriptor.newBuilder(); - tripDescriptorBuilder.setTripId(TRIP_ID); - tripDescriptorBuilder.setScheduleRelationship(SCHEDULED); - tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(1); - tripUpdate = tripUpdateBuilder.build(); + tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(2) + .setArrival(StopTimeEvent.newBuilder().setDelay(1).build()) + .build() + ) + .build(); result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -253,18 +235,26 @@ public void update() { updatedTripTimes = p.tripTimes(); assertNotNull(updatedTripTimes); timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); + assertTimetable( + timetable.getTripTimes(tripId), + "00:00:00", + "00:00:00", + "00:10:01", + "00:10:01", + "00:20:01", + "00:20:01" + ); // update trip departure time only - tripDescriptorBuilder = tripDescriptorBuilder(); - tripDescriptorBuilder.setTripId(TRIP_ID); - tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(120); - tripUpdate = tripUpdateBuilder.build(); + tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(2) + .setDeparture(StopTimeEvent.newBuilder().setDelay(120).build()) + .build() + ) + .build(); + result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -280,17 +270,26 @@ public void update() { updatedTripTimes = p.tripTimes(); assertNotNull(updatedTripTimes); timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); + assertTimetable( + timetable.getTripTimes(tripId), + "00:00:00", + "00:00:00", + "00:10:00", + "00:12:00", + "00:22:00", + "00:22:00" + ); // update trip using stop id - tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopId("B"); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(120); - tripUpdate = tripUpdateBuilder.build(); + tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopId("B") + .setDeparture(StopTimeEvent.newBuilder().setDelay(180).build()) + .build() + ) + .build(); + result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -305,26 +304,35 @@ public void update() { p = result.successValue(); updatedTripTimes = p.tripTimes(); assertNotNull(updatedTripTimes); - timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); + timetable = timetable.copyOf().addOrUpdateTripTimes(updatedTripTimes).build(); + assertTimetable( + timetable.getTripTimes(tripId), + "00:00:00", + "00:00:00", + "00:10:00", + "00:13:00", + "00:23:00", + "00:23:00" + ); } @Test public void fixIncoherentTimes() { // update trip arrival time at first stop and make departure time incoherent at second stop - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - var tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - var stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - var stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(900); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(-1); - var tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(1) + .setArrival(StopTimeEvent.newBuilder().setDelay(900).build()) + .build() + ) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(2) + .setDeparture(StopTimeEvent.newBuilder().setDelay(-1).build()) + .build() + ) + .build(); var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -339,15 +347,15 @@ public void fixIncoherentTimes() { @Test public void testUpdateWithNoForwardPropagationWhenItIsRequired() { - TripDescriptor.Builder tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(15); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + // update trip arrival time at first stop and make departure time incoherent at second stop + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(1) + .setArrival(StopTimeEvent.newBuilder().setDelay(15).build()) + .build() + ) + .build(); var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -365,31 +373,11 @@ public void testUpdateWithNoForwardPropagationWhenItIsRequired() { @Test public void testUpdateWithNoForwardPropagationWithCompleteData() { - TripDescriptor.Builder tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(15); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(20); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(25); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(30); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(2); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(35); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(40); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(1, 15, 20) + .addDelayedStopTime(2, 25, 30) + .addDelayedStopTime(3, 35, 40) + .build(); var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -415,20 +403,12 @@ public void testUpdateWithNoForwardPropagationWithCompleteData() { @Test public void testUpdateWithNoData() { - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.NO_DATA); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SKIPPED); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(2); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.NO_DATA); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addNoDataStop(1) + .addSkippedStop(2) + .addNoDataStop(3) + .build(); + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -456,30 +436,98 @@ public void testUpdateWithNoData() { }); } + @Test + public void testUpdateWithUnchangedTripAndStopProperties() { + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE, "foo", null) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(1) + .setArrival(StopTimeEvent.newBuilder().setDelay(0).build()) + .setDeparture(StopTimeEvent.newBuilder().setDelay(0).build()) + .setStopTimeProperties( + StopTimeUpdate.StopTimeProperties.newBuilder() + .setStopHeadsign("foo") + .setAssignedStopId("A") + .build() + ) + .build() + ) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(2) + .setStopTimeProperties( + StopTimeUpdate.StopTimeProperties.newBuilder() + .setDropOffType(StopTimeUpdate.StopTimeProperties.DropOffPickupType.REGULAR) + .setPickupType(StopTimeUpdate.StopTimeProperties.DropOffPickupType.REGULAR) + .build() + ) + .build() + ) + .addDelayedStopTime(3, 0) + .build(); + + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( + timetable, + new TripUpdate(tripUpdate), + TIME_ZONE, + SERVICE_DATE, + ForwardsDelayPropagationType.DEFAULT, + BackwardsDelayPropagationType.REQUIRED_NO_DATA + ); + + assertTrue(result.isSuccess()); + + result.ifSuccess(p -> { + assertTrue(p.updatedDropoff().isEmpty(), "dropoffs are not modified"); + assertTrue(p.updatedPickup().isEmpty(), "pickups are not modified"); + assertTrue(p.replacedStopIndices().isEmpty(), "stop indices are not modified"); + assertEquals( + "foo", + p.tripTimes().getHeadsign(0).toString(), + "headsigns [1] are not modified" + ); + assertEquals( + "foo", + p.tripTimes().getHeadsign(1).toString(), + "headsigns [2] are not modified" + ); + assertEquals( + "foo", + p.tripTimes().getHeadsign(2).toString(), + "headsigns [3] are not modified" + ); + }); + } + @Test public void testUpdateWithTripAndStopProperties() { - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - tripUpdateBuilder.getTripPropertiesBuilder().setTripHeadsign("new trip headsign"); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.getArrivalBuilder().setDelay(0); - stopTimeUpdateBuilder.getDepartureBuilder().setDelay(0); - stopTimeUpdateBuilder.getStopTimePropertiesBuilder().setStopHeadsign("new stop headsign"); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SKIPPED); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(2); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.getArrivalBuilder().setDelay(0); - stopTimeUpdateBuilder.getDepartureBuilder().setDelay(0); - stopTimeUpdateBuilder - .getStopTimePropertiesBuilder() - .setPickupType(StopTimeUpdate.StopTimeProperties.DropOffPickupType.NONE) - .setDropOffType(StopTimeUpdate.StopTimeProperties.DropOffPickupType.COORDINATE_WITH_DRIVER); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder( + TRIP_ID, + SERVICE_DATE, + SCHEDULED, + TIME_ZONE, + "new trip headsign", + null + ) + .addDelayedStopTime(1, 0, "new stop headsign") + .addSkippedStop(2) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(0).build()) + .setDeparture(StopTimeEvent.newBuilder().setDelay(0).build()) + .setStopTimeProperties( + StopTimeUpdate.StopTimeProperties.newBuilder() + .setPickupType(StopTimeUpdate.StopTimeProperties.DropOffPickupType.NONE) + .setDropOffType( + StopTimeUpdate.StopTimeProperties.DropOffPickupType.COORDINATE_WITH_DRIVER + ) + .build() + ) + .build() + ) + .build(); + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -513,23 +561,16 @@ public void testUpdateWithTripAndStopProperties() { @Test public void testUpdateWithAlwaysDelayPropagationFromSecondStop() { - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(10); - stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(10); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(15); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(2, 10, 10) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(15).build()) + .build() + ) + .build(); + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -560,15 +601,14 @@ public void testUpdateWithAlwaysDelayPropagationFromSecondStop() { @Test public void testUpdateWithAlwaysDelayPropagationFromThirdStop() { - TripDescriptor.Builder tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(15); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(15).build()) + .build() + ) + .build(); var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -595,15 +635,14 @@ public void testUpdateWithAlwaysDelayPropagationFromThirdStop() { @Test public void testUpdateWithNoBackwardPropagationWhenItIsNotRequired() { - TripDescriptor.Builder tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(1); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(15); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(1) + .setArrival(StopTimeEvent.newBuilder().setDelay(15).build()) + .build() + ) + .build(); var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -626,15 +665,14 @@ public void testUpdateWithNoBackwardPropagationWhenItIsNotRequired() { @Test public void testUpdateWithNoBackwardPropagationWhenItIsRequired() { - TripDescriptor.Builder tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(15); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(15).build()) + .build() + ) + .build(); var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -654,15 +692,15 @@ public void testUpdateWithNoBackwardPropagationWhenItIsRequired() { @Test public void testUpdateWithRequiredNoDataDelayPropagationWhenItsNotRequired() { - TripDescriptor.Builder tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(-100); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(-100).build()) + .build() + ) + .build(); + var patch = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -695,15 +733,15 @@ public void testUpdateWithRequiredNoDataDelayPropagationWhenItsNotRequired() { @Test public void testUpdateWithRequiredNoDataDelayPropagationWhenItsRequired() { - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(-700); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(-700).build()) + .build() + ) + .build(); + var patch = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -736,20 +774,21 @@ public void testUpdateWithRequiredNoDataDelayPropagationWhenItsRequired() { @Test public void testUpdateWithRequiredNoDataDelayPropagationOnArrivalTime() { - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - GtfsRealtime.TripUpdate.Builder tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(2); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - stopTimeEventBuilder.setDelay(-700); - stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(15); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(2) + .setDeparture(StopTimeEvent.newBuilder().setDelay(-700).build()) + .build() + ) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(15).build()) + .build() + ) + .build(); + var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, new TripUpdate(tripUpdate), @@ -774,15 +813,14 @@ public void testUpdateWithRequiredNoDataDelayPropagationOnArrivalTime() { @Test public void testUpdateWithRequiredDelayPropagationWhenItsRequired() { - var tripDescriptorBuilder = tripDescriptorBuilder(TRIP_ID); - var tripUpdateBuilder = GtfsRealtime.TripUpdate.newBuilder(); - tripUpdateBuilder.setTrip(tripDescriptorBuilder); - StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0); - stopTimeUpdateBuilder.setStopSequence(3); - stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); - StopTimeEvent.Builder stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); - stopTimeEventBuilder.setDelay(-700); - GtfsRealtime.TripUpdate tripUpdate = tripUpdateBuilder.build(); + var tripUpdate = new TripUpdateBuilder(TRIP_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addRawStopTime( + StopTimeUpdate.newBuilder() + .setStopSequence(3) + .setArrival(StopTimeEvent.newBuilder().setDelay(-700).build()) + .build() + ) + .build(); var result = TripTimesUpdater.createUpdatedTripTimesFromGtfsRt( timetable, @@ -1232,4 +1270,14 @@ private static TripDescriptor.Builder tripDescriptorBuilder(String tripId) { tripDescriptorBuilder.setTripId(tripId); return tripDescriptorBuilder; } + + private void assertTimetable(TripTimes tripTimes, String... expectedTimes) { + var actualTimes = new String[tripTimes.getNumStops() * 2]; + for (int i = 0; i < tripTimes.getNumStops(); i++) { + actualTimes[i * 2] = TimeUtils.timeToStrLong(tripTimes.getArrivalTime(i)); + actualTimes[i * 2 + 1] = TimeUtils.timeToStrLong(tripTimes.getDepartureTime(i)); + } + + assertEquals(String.join(" ", expectedTimes), String.join(" ", actualTimes)); + } } diff --git a/application/src/test/java/org/opentripplanner/updater/trip/gtfs/moduletests/delay/AssignedStopIdsTest.java b/application/src/test/java/org/opentripplanner/updater/trip/gtfs/moduletests/delay/AssignedStopIdsTest.java new file mode 100644 index 00000000000..5d5bbf4770a --- /dev/null +++ b/application/src/test/java/org/opentripplanner/updater/trip/gtfs/moduletests/delay/AssignedStopIdsTest.java @@ -0,0 +1,464 @@ +package org.opentripplanner.updater.trip.gtfs.moduletests.delay; + +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.SCHEDULED; +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.updater.spi.UpdateResultAssertions.assertSuccess; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Collection; +import java.util.List; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripPatternForDate; +import org.opentripplanner.transit.model._data.TransitTestEnvironment; +import org.opentripplanner.transit.model._data.TransitTestEnvironmentBuilder; +import org.opentripplanner.transit.model._data.TripInput; +import org.opentripplanner.transit.model.framework.AbstractTransitEntity; +import org.opentripplanner.transit.model.network.RoutingTripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.updater.trip.GtfsRtTestHelper; +import org.opentripplanner.updater.trip.RealtimeTestConstants; +import org.opentripplanner.updater.trip.TripUpdateBuilder; + +/** + * Tests updating and reverting the stops/platforms for existing trips. + */ +class AssignedStopIdsTest implements RealtimeTestConstants { + + private static final LocalDate SERVICE_DATE = LocalDate.of(2024, 1, 1); + private static final LocalDate SERVICE_DATE_PLUS = SERVICE_DATE.plusDays(1); + private static final ZoneId TIME_ZONE = ZoneId.of("Europe/Paris"); + private final TransitTestEnvironmentBuilder ENV_BUILDER = TransitTestEnvironment.of( + SERVICE_DATE, + TIME_ZONE + ); + private final RegularStop STOP_A = ENV_BUILDER.stop(STOP_A_ID); + private final RegularStop STOP_B = ENV_BUILDER.stop(STOP_B_ID); + private final RegularStop STOP_C = ENV_BUILDER.stop(STOP_C_ID); + private final RegularStop STOP_D = ENV_BUILDER.stop(STOP_D_ID); + private final RegularStop STOP_E = ENV_BUILDER.stop(STOP_E_ID); + + private final TripInput TRIP_1_INPUT = TripInput.of(TRIP_1_ID) + .withServiceDates(SERVICE_DATE, SERVICE_DATE_PLUS) + .addStop(STOP_A, "10:00:00", "10:00:00") + .addStop(STOP_B, "10:01:00", "10:01:00") + .addStop(STOP_C, "10:02:00", "10:02:00"); + + private final TripInput TRIP_2_INPUT = TripInput.of(TRIP_2_ID) + .withServiceDates(SERVICE_DATE, SERVICE_DATE_PLUS) + .addStop(STOP_A, "11:00:00", "11:00:00") + .addStop(STOP_B, "11:01:00", "11:01:00") + .addStop(STOP_C, "11:02:00", "11:02:00"); + + @Test + void assignedThenRevertedStopIds() { + var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).build(); + + assertFalse(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + + var rt = GtfsRtTestHelper.of(env); + var tripUpdate1 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "09:50:00", STOP_D_ID) + .addStopTime(1, "10:01:00") + .addStopTime(2, "10:02:00") + .build(); + + assertSuccess(rt.applyTripUpdate(tripUpdate1)); + assertEquals( + "UPDATED | D 9:50 9:50 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertTrue(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Route1::rt#1"), routingTripPatternIdsForDate(env)); + + var tripUpdate2 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "09:55:00", STOP_E_ID) + .addStopTime(1, "10:01:00") + .addStopTime(2, "10:02:00") + .build(); + + assertSuccess(rt.applyTripUpdate(tripUpdate2)); + assertEquals( + "UPDATED | E 9:55 9:55 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertTrue(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Route1::rt#2"), routingTripPatternIdsForDate(env)); + + var tripUpdate3 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "10:01:00", STOP_A_ID) + .addStopTime(1, "10:02:00") + .addStopTime(2, "10:03:00") + .build(); + + assertSuccess(rt.applyTripUpdate(tripUpdate3)); + assertEquals( + "UPDATED | A 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID).showTimetable() + ); + + assertFalse(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + } + + @Test + @Disabled + void reuseRealtimeTripPatterns() { + var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).addTrip(TRIP_2_INPUT).build(); + + assertFalse(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse(env.tripData(TRIP_2_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + + var rt = GtfsRtTestHelper.of(env); + var tripUpdate1 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "10:01", STOP_E_ID) + .build(); + + var tripUpdate2 = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "11:01", STOP_E_ID) + .build(); + + assertSuccess(rt.applyTripUpdate(tripUpdate1)); + assertEquals( + "UPDATED | E 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertEquals( + "SCHEDULED | A 11:00 11:00 | B 11:01 11:01 | C 11:02 11:02", + env.tripData(TRIP_2_ID).showTimetable() + ); + assertTrue(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse(env.tripData(TRIP_2_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Pattern1", "F:Route1::rt#1"), routingTripPatternIdsForDate(env)); + + assertSuccess(rt.applyTripUpdates(List.of(tripUpdate1, tripUpdate2))); + assertEquals( + "UPDATED | E 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertEquals( + "UPDATED | E 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID).showTimetable() + ); + assertTrue(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertTrue(env.tripData(TRIP_2_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Route1::rt#1"), routingTripPatternIdsForDate(env)); + + assertSuccess(rt.applyTripUpdate(tripUpdate2)); + assertEquals( + "SCHEDULED | A 10:00 10:00 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertEquals( + "UPDATED | E 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID).showTimetable() + ); + assertFalse(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertTrue(env.tripData(TRIP_2_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Pattern1", "F:Route1::rt#1"), routingTripPatternIdsForDate(env)); + + assertSuccess( + rt.applyTripUpdates( + List.of( + new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 0) + .build(), + new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 0) + .build() + ) + ) + ); + assertFalse(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse(env.tripData(TRIP_2_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + } + + @Test + @Disabled + void reuseRealtimeTripPatternsOnDifferentServiceDates() { + var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).addTrip(TRIP_2_INPUT).build(); + + assertFalse(env.tripData(TRIP_1_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse( + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertFalse(env.tripData(TRIP_2_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse( + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + + var rt = GtfsRtTestHelper.of(env); + var tripUpdate11 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "10:01", STOP_E_ID) + .build(); + var tripUpdate12 = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "11:01", STOP_E_ID) + .build(); + + var tripUpdate21 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE_PLUS, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "10:01", STOP_E_ID) + .build(); + var tripUpdate22 = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE_PLUS, SCHEDULED, TIME_ZONE) + .addAssignedStopTime(0, "11:01", STOP_E_ID) + .build(); + + assertSuccess(rt.applyTripUpdates(List.of(tripUpdate11, tripUpdate12))); + assertEquals( + "UPDATED | E 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "UPDATED | E 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "SCHEDULED | A 10:00 10:00 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertEquals( + "SCHEDULED | A 11:00 11:00 | B 11:01 11:01 | C 11:02 11:02", + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertTrue(env.tripData(TRIP_1_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertTrue(env.tripData(TRIP_2_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse( + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertFalse( + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertEquals(List.of("F:Route1::rt#1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + + assertSuccess( + rt.applyTripUpdates(List.of(tripUpdate11, tripUpdate12, tripUpdate21, tripUpdate22)) + ); + assertEquals( + "UPDATED | E 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "UPDATED | E 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "UPDATED | E 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertEquals( + "UPDATED | E 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertTrue(env.tripData(TRIP_1_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertTrue(env.tripData(TRIP_2_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertTrue( + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertTrue( + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertEquals(List.of("F:Route1::rt#1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Route1::rt#1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + + assertSuccess(rt.applyTripUpdates(List.of(tripUpdate21, tripUpdate22))); + assertEquals( + "SCHEDULED | A 10:00 10:00 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "SCHEDULED | A 11:00 11:00 | B 11:01 11:01 | C 11:02 11:02", + env.tripData(TRIP_2_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "UPDATED | E 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertEquals( + "UPDATED | E 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertFalse(env.tripData(TRIP_1_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse(env.tripData(TRIP_2_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertTrue( + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertTrue( + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Route1::rt#1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + + assertSuccess( + rt.applyTripUpdates( + List.of( + new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 0) + .build(), + new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 0) + .build(), + new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE_PLUS, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 0) + .build(), + new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE_PLUS, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 0) + .build() + ) + ) + ); + assertFalse(env.tripData(TRIP_1_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse(env.tripData(TRIP_2_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse( + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertFalse( + env.tripData(TRIP_2_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + } + + @Test + void reuseScheduledTripPatterns() { + var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).addTrip(TRIP_2_INPUT).build(); + + assertFalse(env.tripData(TRIP_1_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse(env.tripData(TRIP_2_ID).tripPattern().isCreatedByRealtimeUpdater()); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + + var rt = GtfsRtTestHelper.of(env); + var tripUpdate1 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 60) + .build(); + + var tripUpdate2 = new TripUpdateBuilder(TRIP_2_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 60) + .build(); + + assertSuccess(rt.applyTripUpdate(tripUpdate1)); + assertEquals( + "UPDATED | A 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertEquals( + "SCHEDULED | A 11:00 11:00 | B 11:01 11:01 | C 11:02 11:02", + env.tripData(TRIP_2_ID).showTimetable() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + + assertSuccess(rt.applyTripUpdates(List.of(tripUpdate1, tripUpdate2))); + assertEquals( + "UPDATED | A 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertEquals( + "UPDATED | A 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID).showTimetable() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + + assertSuccess(rt.applyTripUpdate(tripUpdate2)); + assertEquals( + "SCHEDULED | A 10:00 10:00 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID).showTimetable() + ); + assertEquals( + "UPDATED | A 11:01 11:01 | B 11:02 11:02 | C 11:03 11:03", + env.tripData(TRIP_2_ID).showTimetable() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env)); + } + + @Test + void reuseScheduledTripPatternsOnDifferentServiceDates() { + var env = ENV_BUILDER.addTrip(TRIP_1_INPUT).build(); + + assertFalse(env.tripData(TRIP_1_ID, SERVICE_DATE).tripPattern().isCreatedByRealtimeUpdater()); + assertFalse( + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).tripPattern().isCreatedByRealtimeUpdater() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + + var rt = GtfsRtTestHelper.of(env); + var tripUpdate1 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 60) + .build(); + + var tripUpdate2 = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE_PLUS, SCHEDULED, TIME_ZONE) + .addDelayedStopTime(0, 60) + .build(); + + assertSuccess(rt.applyTripUpdate(tripUpdate1)); + assertEquals( + "UPDATED | A 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "SCHEDULED | A 10:00 10:00 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + + assertSuccess(rt.applyTripUpdates(List.of(tripUpdate1, tripUpdate2))); + assertEquals( + "UPDATED | A 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "UPDATED | A 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + + assertSuccess(rt.applyTripUpdate(tripUpdate2)); + assertEquals( + "SCHEDULED | A 10:00 10:00 | B 10:01 10:01 | C 10:02 10:02", + env.tripData(TRIP_1_ID, SERVICE_DATE).showTimetable() + ); + assertEquals( + "UPDATED | A 10:01 10:01 | B 10:02 10:02 | C 10:03 10:03", + env.tripData(TRIP_1_ID, SERVICE_DATE_PLUS).showTimetable() + ); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE)); + assertEquals(List.of("F:Pattern1"), routingTripPatternIdsForDate(env, SERVICE_DATE_PLUS)); + } + + private List routingTripPatternIdsForDate(TransitTestEnvironment env) { + return routingTripPatternIdsForDate(env, env.defaultServiceDate()); + } + + private List routingTripPatternIdsForDate( + TransitTestEnvironment env, + LocalDate serviceDate + ) { + return tripPatternsForDate(env, serviceDate) + .stream() + .map(TripPatternForDate::getTripPattern) + .map(RoutingTripPattern::getPattern) + .map(AbstractTransitEntity::getId) + .map(Object::toString) + .sorted() + .toList(); + } + + private Collection tripPatternsForDate( + TransitTestEnvironment env, + LocalDate serviceDate + ) { + return env + .transitService() + .getRealtimeRaptorTransitData() + .getTripPatternsForRunningDate(serviceDate); + } +}