diff --git a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index 54c3901f3e8..1ea0542eda0 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -114,10 +114,9 @@ public TimetableSnapshotSource( } /** - * Constructor is package local to allow unit-tests to provide their own clock, not using system - * time. + * Constructor to allow tests to provide their own clock, not using system time. */ - TimetableSnapshotSource( + public TimetableSnapshotSource( TimetableSnapshotSourceParameters parameters, TimetableRepository timetableRepository, Supplier localDateNow diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 3aad9075f73..0e64db8979f 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -16,13 +16,16 @@ import static org.opentripplanner.transit.model.basic.TransitMode.FERRY; import static org.opentripplanner.transit.model.timetable.OccupancyStatus.FEW_SEATS_AVAILABLE; +import com.google.transit.realtime.GtfsRealtime; import jakarta.ws.rs.core.Response; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Instant; +import java.time.LocalDate; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Comparator; @@ -42,7 +45,9 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.framework.model.Grams; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.model.FeedInfo; +import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.model.fare.FareMedium; import org.opentripplanner.model.fare.FareProduct; import org.opentripplanner.model.fare.ItineraryFares; @@ -60,6 +65,10 @@ import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TimePeriod; import org.opentripplanner.routing.alertpatch.TransitAlert; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitTuningParameters; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerMapper; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.GraphFinder; @@ -91,11 +100,18 @@ import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; +import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TimetableRepository; import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; +import org.opentripplanner.updater.TimetableSnapshotSourceParameters; +import org.opentripplanner.updater.trip.BackwardsDelayPropagationType; +import org.opentripplanner.updater.trip.TimetableSnapshotSource; +import org.opentripplanner.updater.trip.TripUpdateBuilder; +import org.opentripplanner.updater.trip.UpdateIncrementality; import org.opentripplanner.utils.collection.ListUtils; class GraphQLIntegrationTest { @@ -116,6 +132,10 @@ class GraphQLIntegrationTest { .map(p -> (RegularStop) p.stop) .toList(); private static final Route ROUTE = TimetableRepositoryForTest.route("a-route").build(); + private static final String ADDED_TRIP_ID = "ADDED_TRIP"; + private static final String REPLACEMENT_TRIP_ID = "REPLACEMENT_TRIP"; + public static final ZoneId TIME_ZONE = ZoneIds.BERLIN; + public static final String FEED_ID = TimetableRepositoryForTest.FEED_ID; private static VehicleRentalStation VEHICLE_RENTAL_STATION = new TestVehicleRentalStationBuilder() .withVehicles(10) @@ -137,6 +157,8 @@ class GraphQLIntegrationTest { static final Instant ALERT_END_TIME = ALERT_START_TIME.plus(1, ChronoUnit.DAYS); private static final int TEN_MINUTES = 10 * 60; + private static final LocalDate SERVICE_DATE = LocalDate.of(2024, 1, 1); + private static GraphQLRequestContext context; private static final Deduplicator DEDUPLICATOR = new Deduplicator(); @@ -157,10 +179,10 @@ static void setup() { List.of() ); - var siteRepository = TEST_MODEL.siteRepositoryBuilder(); - STOP_LOCATIONS.forEach(siteRepository::withRegularStop); - var model = siteRepository.build(); - var timetableRepository = new TimetableRepository(model, DEDUPLICATOR); + var siteRepositoryBuilder = TEST_MODEL.siteRepositoryBuilder(); + STOP_LOCATIONS.forEach(siteRepositoryBuilder::withRegularStop); + var siteRepository = siteRepositoryBuilder.build(); + var timetableRepository = new TimetableRepository(siteRepository, DEDUPLICATOR); var trip = TimetableRepositoryForTest .trip("123") @@ -168,27 +190,48 @@ static void setup() { .build(); var stopTimes = TEST_MODEL.stopTimesEvery5Minutes(3, trip, T11_00); var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, DEDUPLICATOR); + + var calendar = new CalendarServiceData(); + FeedScopedId serviceId = calendar.getOrCreateServiceIdForDate(SERVICE_DATE); + + var tripToBeReplaced = TimetableRepositoryForTest + .trip(REPLACEMENT_TRIP_ID) + .withServiceId(serviceId) + .build(); final TripPattern pattern = TEST_MODEL .pattern(BUS) - .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) + .withScheduledTimeTableBuilder(builder -> + builder + .addTripTimes(tripTimes) + .addTripTimes( + TripTimesFactory.tripTimes( + tripToBeReplaced, + TEST_MODEL.stopTimesEvery5Minutes(3, tripToBeReplaced, T11_30), + DEDUPLICATOR + ) + ) + ) .build(); timetableRepository.addTripPattern(id("pattern-1"), pattern); - var feedId = "testfeed"; - var feedInfo = FeedInfo.dummyForTest(feedId); + var feedInfo = FeedInfo.dummyForTest(FEED_ID); timetableRepository.addFeedInfo(feedInfo); var agency = Agency - .of(new FeedScopedId(feedId, "agency-xx")) + .of(new FeedScopedId(FEED_ID, "agency-xx")) .withName("speedtransit") .withUrl("www.otp-foo.bar") .withTimezone("Europe/Berlin") .build(); timetableRepository.addAgency(agency); - timetableRepository.initTimeZone(ZoneIds.BERLIN); + timetableRepository.initTimeZone(TIME_ZONE); + var serviceCodes = timetableRepository.getServiceCodes(); + serviceCodes.put(serviceId, serviceCodes.size()); + timetableRepository.updateCalendarServiceData(true, calendar, DataImportIssueStore.NOOP); timetableRepository.index(); + var routes = Arrays .stream(TransitMode.values()) .sorted(Comparator.comparing(Enum::name)) @@ -226,6 +269,29 @@ public Set getRoutesForStop(StopLocation stop) { }; routes.forEach(transitService::addRoutes); + timetableRepository.setTransitLayer( + TransitLayerMapper.map(TransitTuningParameters.FOR_TEST, timetableRepository) + ); + timetableRepository.setRealtimeTransitLayer( + new TransitLayer(timetableRepository.getTransitLayer()) + ); + var transitLayerUpdater = new TransitLayerUpdater(transitService); + timetableRepository.setTransitLayerUpdater(transitLayerUpdater); + + var timetableSnapshotProvider = new TimetableSnapshotSource( + TimetableSnapshotSourceParameters.DEFAULT, + timetableRepository, + () -> SERVICE_DATE + ); + timetableSnapshotProvider.applyTripUpdates( + new GtfsRealtimeFuzzyTripMatcher(transitService), + BackwardsDelayPropagationType.REQUIRED_NO_DATA, + UpdateIncrementality.FULL_DATASET, + List.of(getAddedTrip(busRoute), getReplacementTrip(tripToBeReplaced)), + FEED_ID + ); + timetableSnapshotProvider.flushBuffer(); + var step1 = walkStep("street") .withRelativeDirection(RelativeDirection.DEPART) .withAbsoluteDirection(20) @@ -323,6 +389,35 @@ public Set getRoutesForStop(StopLocation stop) { ); } + private static GtfsRealtime.TripUpdate getAddedTrip(Route route) { + var tripUpdateBuilder = new TripUpdateBuilder( + route.getId().getId(), + ADDED_TRIP_ID, + SERVICE_DATE, + GtfsRealtime.TripDescriptor.ScheduleRelationship.ADDED, + TIME_ZONE + ); + tripUpdateBuilder.addStopTime(A.stop.getId().getId(), 0); + tripUpdateBuilder.addStopTime(B.stop.getId().getId(), 300); + tripUpdateBuilder.addStopTime(C.stop.getId().getId(), 600); + tripUpdateBuilder.addStopTime(D.stop.getId().getId(), 900); + return tripUpdateBuilder.build(); + } + + private static GtfsRealtime.TripUpdate getReplacementTrip(Trip trip) { + var tripUpdateBuilder = new TripUpdateBuilder( + trip.getId().getId(), + SERVICE_DATE, + GtfsRealtime.TripDescriptor.ScheduleRelationship.REPLACEMENT, + TIME_ZONE + ); + tripUpdateBuilder.addStopTime(A.stop.getId().getId(), 0); + tripUpdateBuilder.addStopTime(B.stop.getId().getId(), 300); + tripUpdateBuilder.addStopTime(C.stop.getId().getId(), 600); + tripUpdateBuilder.addStopTime(D.stop.getId().getId(), 900); + return tripUpdateBuilder.build(); + } + private static BikeAccess bikesAllowed(TransitMode m) { return switch (m.ordinal() % 3) { case 0 -> BikeAccess.ALLOWED; 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 e8218edfc1f..16d1efc5ca4 100644 --- a/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateBuilder.java +++ b/application/src/test/java/org/opentripplanner/updater/trip/TripUpdateBuilder.java @@ -37,10 +37,21 @@ public TripUpdateBuilder( this.midnight = ServiceDateUtils.asStartOfService(serviceDate, zoneId); } - public TripUpdateBuilder addStopTime(String stopId, int minutes) { + public TripUpdateBuilder( + String routeId, + String tripId, + LocalDate serviceDate, + GtfsRealtime.TripDescriptor.ScheduleRelationship scheduleRelationship, + ZoneId zoneId + ) { + this(tripId, serviceDate, scheduleRelationship, zoneId); + tripDescriptorBuilder.setRouteId(routeId); + } + + public TripUpdateBuilder addStopTime(String stopId, int secondsFromMidnight) { return addStopTime( stopId, - minutes, + secondsFromMidnight, NO_VALUE, NO_DELAY, NO_DELAY, @@ -49,10 +60,14 @@ public TripUpdateBuilder addStopTime(String stopId, int minutes) { ); } - public TripUpdateBuilder addStopTime(String stopId, int minutes, DropOffPickupType pickDrop) { + public TripUpdateBuilder addStopTime( + String stopId, + int secondsFromMidnight, + DropOffPickupType pickDrop + ) { return addStopTime( stopId, - minutes, + secondsFromMidnight, NO_VALUE, NO_DELAY, NO_DELAY, @@ -122,7 +137,7 @@ public TripUpdateBuilder addRawStopTime(StopTimeUpdate stopTime) { private TripUpdateBuilder addStopTime( String stopId, - int minutes, + int secondsFromMidnight, int stopSequence, int arrivalDelay, int departureDelay, @@ -153,8 +168,8 @@ private TripUpdateBuilder addStopTime( final GtfsRealtime.TripUpdate.StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); final GtfsRealtime.TripUpdate.StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); - if (minutes > NO_VALUE) { - var epochSeconds = midnight.plusHours(8).plusMinutes(minutes).toEpochSecond(); + if (secondsFromMidnight > NO_VALUE) { + var epochSeconds = midnight.plusSeconds(secondsFromMidnight).toEpochSecond(); arrivalBuilder.setTime(epochSeconds); departureBuilder.setTime(epochSeconds); } diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json index b23ced4f954..a141220d0e9 100644 --- a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json @@ -51,6 +51,53 @@ "occupancy" : { "occupancyStatus" : "FEW_SEATS_AVAILABLE" } + }, + { + "gtfsId" : "F:REPLACEMENT_TRIP", + "stoptimes" : [ + { + "stop" : { + "gtfsId" : "F:Stop_0", + "name" : "Stop_0" + }, + "headsign" : "Stop headsign at stop 10", + "scheduledArrival" : 41400, + "scheduledDeparture" : 41400, + "stopPosition" : 10, + "realtimeState" : "SCHEDULED", + "pickupType" : "SCHEDULED", + "dropoffType" : "SCHEDULED" + }, + { + "stop" : { + "gtfsId" : "F:Stop_1", + "name" : "Stop_1" + }, + "headsign" : "Stop headsign at stop 20", + "scheduledArrival" : 41700, + "scheduledDeparture" : 41700, + "stopPosition" : 20, + "realtimeState" : "SCHEDULED", + "pickupType" : "SCHEDULED", + "dropoffType" : "SCHEDULED" + }, + { + "stop" : { + "gtfsId" : "F:Stop_2", + "name" : "Stop_2" + }, + "headsign" : "Stop headsign at stop 30", + "scheduledArrival" : 42000, + "scheduledDeparture" : 42000, + "stopPosition" : 30, + "realtimeState" : "SCHEDULED", + "pickupType" : "SCHEDULED", + "dropoffType" : "SCHEDULED" + } + ], + "occupancy" : { + "occupancyStatus" : "NO_DATA_AVAILABLE" + } } ], "vehiclePositions" : [ diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/realtime-trip.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/realtime-trip.json new file mode 100644 index 00000000000..258f7ba1ff3 --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/realtime-trip.json @@ -0,0 +1,114 @@ +{ + "data": { + "addedTrip": { + "gtfsId": "F:ADDED_TRIP", + "timetabledOrigin": null, + "datedOrigin": { + "realtimeDeparture": 0 + }, + "timetabledDestination": null, + "datedDestination": { + "realtimeArrival": 900 + }, + "stoptimes": null, + "stoptimesForDate": [ + { + "stop": { + "gtfsId": "F:A" + }, + "scheduledDeparture": 0, + "realtimeDeparture": 0 + }, + { + "stop": { + "gtfsId": "F:B" + }, + "scheduledDeparture": 300, + "realtimeDeparture": 300 + }, + { + "stop": { + "gtfsId": "F:C" + }, + "scheduledDeparture": 600, + "realtimeDeparture": 600 + }, + { + "stop": { + "gtfsId": "F:D" + }, + "scheduledDeparture": 900, + "realtimeDeparture": 900 + } + ] + }, + "replacementTrip": { + "gtfsId": "F:REPLACEMENT_TRIP", + "timetabledOrigin": { + "scheduledDeparture": 41400 + }, + "datedOrigin": { + "realtimeDeparture": 0 + }, + "timetabledDestination": { + "scheduledArrival": 42000 + }, + "datedDestination": { + "realtimeArrival": 900 + }, + "stoptimes": [ + { + "stop": { + "gtfsId": "F:Stop_0" + }, + "scheduledDeparture": 41400, + "realtimeDeparture": 41400 + }, + { + "stop": { + "gtfsId": "F:Stop_1" + }, + "scheduledDeparture": 41700, + "realtimeDeparture": 41700 + }, + { + "stop": { + "gtfsId": "F:Stop_2" + }, + "scheduledDeparture": 42000, + "realtimeDeparture": 42000 + } + ], + "stoptimesForDate": [ + { + "stop": { + "gtfsId": "F:A" + }, + "scheduledDeparture": 0, + "realtimeDeparture": 0 + }, + { + "stop": { + "gtfsId": "F:B" + }, + "scheduledDeparture": 300, + "realtimeDeparture": 300 + }, + { + "stop": { + "gtfsId": "F:C" + }, + "scheduledDeparture": 600, + "realtimeDeparture": 600 + }, + { + "stop": { + "gtfsId": "F:D" + }, + "scheduledDeparture": 900, + "realtimeDeparture": 900 + } + ] + } + } +} \ No newline at end of file diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/realtime-trip.graphql b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/realtime-trip.graphql new file mode 100644 index 00000000000..5072649e571 --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/realtime-trip.graphql @@ -0,0 +1,38 @@ +fragment StoptimeSummary on Stoptime { + stop { + gtfsId + } + scheduledDeparture + realtimeDeparture +} + +fragment TripSummary on Trip { + gtfsId + timetabledOrigin: departureStoptime { + scheduledDeparture + } + datedOrigin: departureStoptime(serviceDate: "20240101") { + realtimeDeparture + } + timetabledDestination: arrivalStoptime { + scheduledArrival + } + datedDestination: arrivalStoptime(serviceDate: "20240101") { + realtimeArrival + } + stoptimes { + ...StoptimeSummary + } + stoptimesForDate(serviceDate: "20240101") { + ...StoptimeSummary + } +} + +query GtfsExampleQuery { + addedTrip: trip(id: "F:ADDED_TRIP") { + ...TripSummary + } + replacementTrip: trip(id: "F:REPLACEMENT_TRIP") { + ...TripSummary + } +}