diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java index 956b35309e2..243ce6d1897 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java @@ -4,7 +4,6 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.text.ParseException; -import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; @@ -13,6 +12,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; @@ -23,7 +23,6 @@ import org.opentripplanner.apis.gtfs.mapping.BikesAllowedMapper; import org.opentripplanner.apis.gtfs.model.TripOccupancy; import org.opentripplanner.apis.support.SemanticHash; -import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TripTimeOnDate; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; @@ -36,7 +35,6 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Direction; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.utils.time.ServiceDateUtils; @@ -132,38 +130,13 @@ public DataFetcher> alerts() { @Override public DataFetcher arrivalStoptime() { return environment -> { - try { - TransitService transitService = getTransitService(environment); - TripPattern tripPattern = getTripPattern(environment); - if (tripPattern == null) { - return null; - } - Timetable timetable = tripPattern.getScheduledTimetable(); - - TripTimes triptimes = timetable.getTripTimes(getSource(environment)); - LocalDate serviceDate = null; - Instant midnight = null; - - var args = new GraphQLTypes.GraphQLTripArrivalStoptimeArgs(environment.getArguments()); - if (args.getGraphQLServiceDate() != null) { - serviceDate = ServiceDateUtils.parseString(args.getGraphQLServiceDate()); - midnight = - ServiceDateUtils - .asStartOfService(serviceDate, transitService.getTimeZone()) - .toInstant(); - } - - return new TripTimeOnDate( - triptimes, - triptimes.getNumStops() - 1, - tripPattern, - serviceDate, - midnight - ); - } catch (ParseException e) { - //Invalid date format - return null; - } + var serviceDate = getOptionalServiceDateArgument(environment); + var trip = getSource(environment); + var transitService = getTransitService(environment); + var stopTimes = serviceDate + .map(date -> transitService.getTripTimeOnDates(trip, date)) + .orElseGet(() -> transitService.getScheduledTripTimes(trip)); + return stopTimes.map(List::getLast).orElse(null); }; } @@ -180,32 +153,13 @@ public DataFetcher blockId() { @Override public DataFetcher departureStoptime() { return environment -> { - try { - TransitService transitService = getTransitService(environment); - TripPattern tripPattern = getTripPattern(environment); - if (tripPattern == null) { - return null; - } - Timetable timetable = tripPattern.getScheduledTimetable(); - - TripTimes triptimes = timetable.getTripTimes(getSource(environment)); - LocalDate serviceDate = null; - Instant midnight = null; - - var args = new GraphQLTypes.GraphQLTripDepartureStoptimeArgs(environment.getArguments()); - if (args.getGraphQLServiceDate() != null) { - serviceDate = ServiceDateUtils.parseString(args.getGraphQLServiceDate()); - midnight = - ServiceDateUtils - .asStartOfService(serviceDate, transitService.getTimeZone()) - .toInstant(); - } - - return new TripTimeOnDate(triptimes, 0, tripPattern, serviceDate, midnight); - } catch (ParseException e) { - //Invalid date format - return null; - } + var serviceDate = getOptionalServiceDateArgument(environment); + var trip = getSource(environment); + var transitService = getTransitService(environment); + var stopTimes = serviceDate + .map(date -> transitService.getTripTimeOnDates(trip, date)) + .orElseGet(() -> transitService.getScheduledTripTimes(trip)); + return stopTimes.map(List::getFirst).orElse(null); }; } @@ -300,43 +254,23 @@ public DataFetcher> stops() { @Override public DataFetcher> stoptimes() { - return environment -> { - TripPattern tripPattern = getTripPattern(environment); - if (tripPattern == null) { - return List.of(); - } - return TripTimeOnDate.fromTripTimes( - tripPattern.getScheduledTimetable(), - getSource(environment) - ); - }; + return environment -> + getTransitService(environment).getScheduledTripTimes(getSource(environment)).orElse(null); } @Override public DataFetcher> stoptimesForDate() { return environment -> { - try { - TransitService transitService = getTransitService(environment); - Trip trip = getSource(environment); - var args = new GraphQLTypes.GraphQLTripStoptimesForDateArgs(environment.getArguments()); - - ZoneId timeZone = transitService.getTimeZone(); - LocalDate serviceDate = args.getGraphQLServiceDate() != null - ? ServiceDateUtils.parseString(args.getGraphQLServiceDate()) - : LocalDate.now(timeZone); - - TripPattern tripPattern = transitService.getPatternForTrip(trip, serviceDate); - // no matching pattern found - if (tripPattern == null) { - return List.of(); - } - - Instant midnight = ServiceDateUtils.asStartOfService(serviceDate, timeZone).toInstant(); - Timetable timetable = transitService.getTimetableForTripPattern(tripPattern, serviceDate); - return TripTimeOnDate.fromTripTimes(timetable, trip, serviceDate, midnight); - } catch (ParseException e) { - return null; // Invalid date format - } + TransitService transitService = getTransitService(environment); + Trip trip = getSource(environment); + var args = new GraphQLTypes.GraphQLTripStoptimesForDateArgs(environment.getArguments()); + + ZoneId timeZone = transitService.getTimeZone(); + LocalDate serviceDate = args.getGraphQLServiceDate() != null + ? ServiceDateUtils.parseString(args.getGraphQLServiceDate()) + : LocalDate.now(timeZone); + + return transitService.getTripTimeOnDates(trip, serviceDate).orElse(null); }; } @@ -400,6 +334,15 @@ private TripPattern getTripPattern(DataFetchingEnvironment environment) { return getTransitService(environment).getPatternForTrip(environment.getSource()); } + private TripPattern getTripPattern( + DataFetchingEnvironment environment, + @Nullable LocalDate date + ) { + return date == null + ? getTripPattern(environment) + : getTransitService(environment).getPatternForTrip(environment.getSource(), date); + } + private TransitService getTransitService(DataFetchingEnvironment environment) { return environment.getContext().transitService(); } @@ -408,6 +351,16 @@ private RealtimeVehicleService getRealtimeVehiclesService(DataFetchingEnvironmen return environment.getContext().realTimeVehicleService(); } + private static Optional getOptionalServiceDateArgument( + DataFetchingEnvironment environment + ) throws ParseException { + var args = new GraphQLTypes.GraphQLTripArrivalStoptimeArgs(environment.getArguments()); + if (args.getGraphQLServiceDate() != null) { + return Optional.of(ServiceDateUtils.parseString(args.getGraphQLServiceDate())); + } + return Optional.empty(); + } + private Trip getSource(DataFetchingEnvironment environment) { return environment.getSource(); } diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java index 1ad05118e11..be5895736fa 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java @@ -17,7 +17,6 @@ import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; -import org.opentripplanner.routing.TripTimeOnDateHelper; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; @@ -152,11 +151,10 @@ public static GraphQLObjectType create( ) .dataFetcher(environment -> { TripOnServiceDate tripOnServiceDate = tripOnServiceDate(environment); - return TripTimeOnDateHelper.getTripTimeOnDates( - GqlUtil.getTransitService(environment), - tripOnServiceDate.getTrip(), - tripOnServiceDate.getServiceDate() - ); + return GqlUtil + .getTransitService(environment) + .getTripTimeOnDates(tripOnServiceDate.getTrip(), tripOnServiceDate.getServiceDate()) + .orElse(null); }) .build() ) diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java index 20a290863df..26db29a9022 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java @@ -21,8 +21,6 @@ import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.framework.geometry.EncodedPolyline; -import org.opentripplanner.model.TripTimeOnDate; -import org.opentripplanner.routing.TripTimeOnDateHelper; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; @@ -235,14 +233,7 @@ public static GraphQLObjectType create( .description( "Returns scheduled passing times only - without real-time-updates, for realtime-data use 'estimatedCalls'" ) - .dataFetcher(env -> { - Trip trip = trip(env); - TripPattern tripPattern = GqlUtil.getTransitService(env).getPatternForTrip(trip); - if (tripPattern == null) { - return List.of(); - } - return TripTimeOnDate.fromTripTimes(tripPattern.getScheduledTimetable(), trip); - }) + .dataFetcher(env -> GqlUtil.getTransitService(env).getScheduledTripTimes(trip(env))) .build() ) .field( @@ -269,11 +260,10 @@ public static GraphQLObjectType create( .ofNullable(environment.getArgument("date")) .map(LocalDate.class::cast) .orElse(LocalDate.now(GqlUtil.getTransitService(environment).getTimeZone())); - return TripTimeOnDateHelper.getTripTimeOnDates( - GqlUtil.getTransitService(environment), - trip(environment), - serviceDate - ); + return GqlUtil + .getTransitService(environment) + .getTripTimeOnDates(trip(environment), serviceDate) + .orElse(null); }) .build() ) diff --git a/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java b/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java index 71aaa734f0e..d5e59d7322b 100644 --- a/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java +++ b/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java @@ -5,6 +5,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; @@ -31,7 +33,10 @@ public class TripTimeOnDate { private final int stopIndex; // This is only needed because TripTimes has no reference to TripPattern private final TripPattern tripPattern; + + @Nullable private final LocalDate serviceDate; + private final long midnight; public TripTimeOnDate(TripTimes tripTimes, int stopIndex, TripPattern tripPattern) { @@ -46,8 +51,8 @@ public TripTimeOnDate( TripTimes tripTimes, int stopIndex, TripPattern tripPattern, - LocalDate serviceDate, - Instant midnight + @Nullable LocalDate serviceDate, + @Nullable Instant midnight ) { this.tripTimes = tripTimes; this.stopIndex = stopIndex; @@ -59,9 +64,15 @@ public TripTimeOnDate( /** * Must pass in both Timetable and Trip, because TripTimes do not have a reference to * StopPatterns. + * + * @return null if the trip does not exist in the timetable */ + @Nullable public static List fromTripTimes(Timetable table, Trip trip) { TripTimes times = table.getTripTimes(trip); + if (times == null) { + return null; + } List out = new ArrayList<>(); for (int i = 0; i < times.getNumStops(); ++i) { out.add(new TripTimeOnDate(times, i, table.getPattern())); @@ -74,7 +85,10 @@ public static List fromTripTimes(Timetable table, Trip trip) { * StopPatterns. * * @param serviceDate service day to set, if null none is set + * @return null if the trip does not exist in the timetable */ + + @Nullable public static List fromTripTimes( Timetable table, Trip trip, @@ -82,6 +96,9 @@ public static List fromTripTimes( Instant midnight ) { TripTimes times = table.getTripTimes(trip); + if (times == null) { + return null; + } List out = new ArrayList<>(); for (int i = 0; i < times.getNumStops(); ++i) { out.add(new TripTimeOnDate(times, i, table.getPattern(), serviceDate, midnight)); @@ -270,4 +287,22 @@ public BookingInfo getPickupBookingInfo() { public BookingInfo getDropOffBookingInfo() { return tripTimes.getDropOffBookingInfo(stopIndex); } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TripTimeOnDate that = (TripTimeOnDate) o; + return ( + stopIndex == that.stopIndex && + midnight == that.midnight && + Objects.equals(tripTimes, that.tripTimes) && + Objects.equals(tripPattern, that.tripPattern) && + Objects.equals(serviceDate, that.serviceDate) + ); + } + + @Override + public int hashCode() { + return Objects.hash(tripTimes, stopIndex, tripPattern, serviceDate, midnight); + } } diff --git a/application/src/main/java/org/opentripplanner/routing/TripTimeOnDateHelper.java b/application/src/main/java/org/opentripplanner/routing/TripTimeOnDateHelper.java deleted file mode 100644 index f2806d0a1f2..00000000000 --- a/application/src/main/java/org/opentripplanner/routing/TripTimeOnDateHelper.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.opentripplanner.routing; - -import java.time.Instant; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import org.opentripplanner.model.Timetable; -import org.opentripplanner.model.TripTimeOnDate; -import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; -import org.opentripplanner.transit.service.TransitService; -import org.opentripplanner.utils.time.ServiceDateUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TripTimeOnDateHelper { - - private static final Logger LOG = LoggerFactory.getLogger(TripTimeOnDateHelper.class); - - public static List getTripTimeOnDates( - TransitService transitService, - Trip trip, - LocalDate serviceDate - ) { - TripPattern pattern = transitService.getPatternForTrip(trip, serviceDate); - - Timetable timetable = transitService.getTimetableForTripPattern(pattern, serviceDate); - - // If realtime moved pattern back to original trip, fetch it instead - if (timetable.getTripIndex(trip.getId()) == -1) { - LOG.warn( - "Trip {} not found in realtime pattern. This should not happen, and indicates a bug.", - trip - ); - pattern = transitService.getPatternForTrip(trip); - timetable = transitService.getTimetableForTripPattern(pattern, serviceDate); - } - - // This check is made here to avoid changing TripTimeOnDate.fromTripTimes - TripTimes times = timetable.getTripTimes(trip); - if ( - !transitService.getServiceCodesRunningForDate(serviceDate).contains(times.getServiceCode()) - ) { - return new ArrayList<>(); - } else { - Instant midnight = ServiceDateUtils - .asStartOfService(serviceDate, transitService.getTimeZone()) - .toInstant(); - return TripTimeOnDate.fromTripTimes(timetable, trip, serviceDate, midnight); - } - } -} diff --git a/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java b/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java index f8d1e437a34..1b843a277ae 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -57,8 +57,12 @@ 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.GraphUpdaterStatus; import org.opentripplanner.utils.collection.CollectionsView; +import org.opentripplanner.utils.time.ServiceDateUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Default implementation of the Transit Service and Transit Editor Service. @@ -68,6 +72,7 @@ */ public class DefaultTransitService implements TransitEditorService { + private static final Logger LOG = LoggerFactory.getLogger(DefaultTransitService.class); private final TimetableRepository timetableRepository; private final TimetableRepositoryIndex timetableRepositoryIndex; @@ -91,6 +96,45 @@ public DefaultTransitService( this.timetableSnapshot = timetableSnapshotBuffer; } + public Optional> getScheduledTripTimes(Trip trip) { + TripPattern tripPattern = getPatternForTrip(trip); + if (tripPattern == null) { + return Optional.empty(); + } + return Optional.ofNullable( + TripTimeOnDate.fromTripTimes(tripPattern.getScheduledTimetable(), trip) + ); + } + + public Optional> getTripTimeOnDates(Trip trip, LocalDate serviceDate) { + TripPattern pattern = getPatternForTrip(trip, serviceDate); + + Timetable timetable = getTimetableForTripPattern(pattern, serviceDate); + + // If realtime moved pattern back to original trip, fetch it instead + if (timetable.getTripIndex(trip.getId()) == -1) { + LOG.warn( + "Trip {} not found in realtime pattern. This should not happen, and indicates a bug.", + trip + ); + pattern = getPatternForTrip(trip); + timetable = getTimetableForTripPattern(pattern, serviceDate); + } + + // This check is made here to avoid changing TripTimeOnDate.fromTripTimes + TripTimes times = timetable.getTripTimes(trip); + if (!this.getServiceCodesRunningForDate(serviceDate).contains(times.getServiceCode())) { + return Optional.empty(); + } else { + Instant midnight = ServiceDateUtils + .asStartOfService(serviceDate, this.getTimeZone()) + .toInstant(); + return Optional.ofNullable( + TripTimeOnDate.fromTripTimes(timetable, trip, serviceDate, midnight) + ); + } + } + @Override public Collection getFeedIds() { return this.timetableRepository.getFeedIds(); diff --git a/application/src/main/java/org/opentripplanner/transit/service/TransitService.java b/application/src/main/java/org/opentripplanner/transit/service/TransitService.java index b77eb77d892..ee603b054c2 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -64,6 +64,16 @@ * copy-on-write and shares a lot of objects with any other TransitLayer instances. */ public interface TransitService { + /** + * @return empty if the trip doesn't exist in the timetable (e.g. real-time added) + */ + Optional> getScheduledTripTimes(Trip trip); + + /** + * @return empty if the trip doesn't run on the date specified + */ + Optional> getTripTimeOnDates(Trip trip, LocalDate serviceDate); + Collection getFeedIds(); Collection getAgencies(); 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 4bc911e114c..c2cf085cc16 100644 --- a/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/application/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -116,10 +116,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 88fbe943bba..1d35cabb4d5 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 final 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, "11: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/transit/service/DefaultTransitServiceTest.java b/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java index ac0530d1ef9..0cd6ee2c984 100644 --- a/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java +++ b/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java @@ -7,14 +7,19 @@ import static org.opentripplanner.transit.model.basic.TransitMode.RAIL; import static org.opentripplanner.transit.model.basic.TransitMode.TRAM; +import java.time.Instant; import java.time.LocalDate; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.model.RealTimeTripUpdate; import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.model.TripTimeOnDate; +import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.transit.model._data.TimetableRepositoryForTest; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -25,32 +30,74 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.utils.time.ServiceDateUtils; class DefaultTransitServiceTest { private static final TimetableRepositoryForTest TEST_MODEL = TimetableRepositoryForTest.of(); - static TransitService service; - static Station STATION = TEST_MODEL.station("C").build(); - static RegularStop STOP_A = TEST_MODEL + private static TransitService service; + private static final Station STATION = TEST_MODEL.station("C").build(); + private static final RegularStop STOP_A = TEST_MODEL .stop("A") .withVehicleType(TRAM) .withParentStation(STATION) .build(); - static RegularStop STOP_B = TEST_MODEL.stop("B").withParentStation(STATION).build(); - static TripPattern RAIL_PATTERN = TEST_MODEL.pattern(RAIL).build(); - static TripPattern FERRY_PATTERN = TEST_MODEL.pattern(FERRY).build(); - static TripPattern BUS_PATTERN = TEST_MODEL.pattern(BUS).build(); + private static final RegularStop STOP_B = TEST_MODEL.stop("B").withParentStation(STATION).build(); + + private static final FeedScopedId SERVICE_ID = new FeedScopedId("FEED", "SERVICE"); + private static final int SERVICE_CODE = 0; + private static final Trip TRIP = TimetableRepositoryForTest.trip("REAL_TIME_TRIP").build(); + private static final Trip ADDED_TRIP = TimetableRepositoryForTest + .trip("REAL_TIME_ADDED_TRIP") + .withServiceId(SERVICE_ID) + .build(); + private static final ScheduledTripTimes SCHEDULED_TRIP_TIMES = ScheduledTripTimes + .of() + .withTrip(TRIP) + .withArrivalTimes(new int[] { 0, 1 }) + .withDepartureTimes(new int[] { 0, 1 }) + .withServiceCode(SERVICE_CODE) + .build(); + + private static final TripPattern RAIL_PATTERN = TEST_MODEL + .pattern(RAIL) + .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(SCHEDULED_TRIP_TIMES)) + .build(); + private static final TripPattern FERRY_PATTERN = TEST_MODEL.pattern(FERRY).build(); + private static final TripPattern BUS_PATTERN = TEST_MODEL.pattern(BUS).build(); - static StopPattern REAL_TIME_STOP_PATTERN = TimetableRepositoryForTest.stopPattern( + private static final StopPattern REAL_TIME_STOP_PATTERN = TimetableRepositoryForTest.stopPattern( STOP_A, STOP_B ); - static TripPattern REAL_TIME_PATTERN = TEST_MODEL + private static final TripPattern REAL_TIME_PATTERN = TEST_MODEL .pattern(BUS) .withStopPattern(REAL_TIME_STOP_PATTERN) .withCreatedByRealtimeUpdater(true) .build(); + private static final int DELAY = 120; + private static final RealTimeTripTimes REALTIME_TRIP_TIMES = SCHEDULED_TRIP_TIMES.copyScheduledTimes(); + private static final RealTimeTripTimes ADDED_TRIP_TIMES = RealTimeTripTimes.of( + ScheduledTripTimes + .of() + .withTrip(ADDED_TRIP) + .withArrivalTimes(new int[] { 10, 11 }) + .withDepartureTimes(new int[] { 10, 11 }) + .withServiceCode(SERVICE_CODE) + .build() + ); + + static { + for (var i = 0; i < REALTIME_TRIP_TIMES.getNumStops(); ++i) { + REALTIME_TRIP_TIMES.updateArrivalDelay(i, DefaultTransitServiceTest.DELAY); + REALTIME_TRIP_TIMES.updateDepartureDelay(i, DefaultTransitServiceTest.DELAY); + } + } + + private static final LocalDate SERVICE_DATE = LocalDate.of(2024, 1, 1); + private static final LocalDate NO_SERVICE_DATE = LocalDate.of(2000, 1, 1); @BeforeAll static void setup() { @@ -62,20 +109,21 @@ static void setup() { .build(); var timetableRepository = new TimetableRepository(siteRepository, new Deduplicator()); + var calendar = new CalendarServiceData(); + calendar.putServiceDatesForServiceId(SERVICE_ID, List.of(SERVICE_DATE)); + var serviceCodes = timetableRepository.getServiceCodes(); + serviceCodes.put(SERVICE_ID, SERVICE_CODE); + timetableRepository.updateCalendarServiceData(true, calendar, DataImportIssueStore.NOOP); timetableRepository.addTripPattern(RAIL_PATTERN.getId(), RAIL_PATTERN); timetableRepository.index(); timetableRepository.initTimetableSnapshotProvider(() -> { TimetableSnapshot timetableSnapshot = new TimetableSnapshot(); - RealTimeTripTimes tripTimes = RealTimeTripTimes.of( - ScheduledTripTimes - .of() - .withTrip(TimetableRepositoryForTest.trip("REAL_TIME_TRIP").build()) - .withDepartureTimes(new int[] { 0, 1 }) - .build() + timetableSnapshot.update( + new RealTimeTripUpdate(REAL_TIME_PATTERN, REALTIME_TRIP_TIMES, SERVICE_DATE) ); timetableSnapshot.update( - new RealTimeTripUpdate(REAL_TIME_PATTERN, tripTimes, LocalDate.now()) + new RealTimeTripUpdate(REAL_TIME_PATTERN, ADDED_TRIP_TIMES, SERVICE_DATE) ); return timetableSnapshot.commit(); @@ -128,4 +176,61 @@ void getPatternForStopsWithRealTime() { void containsTrip() { assertFalse(service.containsTrip(new FeedScopedId("x", "x"))); } + + @Test + void getScheduledTripTimes() { + assertEquals( + Optional.of( + List.of( + new TripTimeOnDate(SCHEDULED_TRIP_TIMES, 0, RAIL_PATTERN), + new TripTimeOnDate(SCHEDULED_TRIP_TIMES, 1, RAIL_PATTERN) + ) + ), + service.getScheduledTripTimes(TRIP) + ); + } + + @Test + void getRealtimeTripTimes() { + Instant midnight = ServiceDateUtils + .asStartOfService(SERVICE_DATE, service.getTimeZone()) + .toInstant(); + + assertEquals( + Optional.of( + List.of( + new TripTimeOnDate(REALTIME_TRIP_TIMES, 0, REAL_TIME_PATTERN, SERVICE_DATE, midnight), + new TripTimeOnDate(REALTIME_TRIP_TIMES, 1, REAL_TIME_PATTERN, SERVICE_DATE, midnight) + ) + ), + service.getTripTimeOnDates(TRIP, SERVICE_DATE) + ); + } + + @Test + void getTripTimesOnNoServiceDay() { + assertEquals(Optional.empty(), service.getTripTimeOnDates(TRIP, NO_SERVICE_DATE)); + } + + @Test + void getScheduledTripTimesForAddedTrip() { + assertEquals(Optional.empty(), service.getScheduledTripTimes(ADDED_TRIP)); + } + + @Test + void getRealtimeTripTimesForAddedTrip() { + Instant midnight = ServiceDateUtils + .asStartOfService(SERVICE_DATE, service.getTimeZone()) + .toInstant(); + + assertEquals( + Optional.of( + List.of( + new TripTimeOnDate(ADDED_TRIP_TIMES, 0, REAL_TIME_PATTERN, SERVICE_DATE, midnight), + new TripTimeOnDate(ADDED_TRIP_TIMES, 1, REAL_TIME_PATTERN, SERVICE_DATE, midnight) + ) + ), + service.getTripTimeOnDates(ADDED_TRIP, SERVICE_DATE) + ); + } } 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 08bcc490e0f..78ecb2aefc8 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 @@ -54,6 +54,56 @@ "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, + "stopPositionInPattern" : 0, + "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, + "stopPositionInPattern" : 1, + "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, + "stopPositionInPattern" : 2, + "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 + } +}