diff --git a/application/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java b/application/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java index 85aa1a25004..60d03e484df 100644 --- a/application/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java +++ b/application/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java @@ -9,7 +9,7 @@ import org.locationtech.jts.geom.LineString; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.Leg; -import org.opentripplanner.model.plan.LegTime; +import org.opentripplanner.model.plan.LegCallTime; import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.TransitLeg; @@ -55,12 +55,12 @@ public Trip getTrip() { } @Override - public LegTime start() { + public LegCallTime start() { return first.start(); } @Override - public LegTime end() { + public LegCallTime end() { return second.end(); } diff --git a/application/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java b/application/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java index 25c4abee12e..769407aa05a 100644 --- a/application/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java +++ b/application/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java @@ -13,7 +13,7 @@ import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.Leg; -import org.opentripplanner.model.plan.LegTime; +import org.opentripplanner.model.plan.LegCallTime; import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.TransitLeg; @@ -87,13 +87,13 @@ public Accessibility getTripWheelchairAccessibility() { } @Override - public LegTime start() { - return LegTime.ofStatic(startTime); + public LegCallTime start() { + return LegCallTime.ofStatic(startTime); } @Override - public LegTime end() { - return LegTime.ofStatic(endTime); + public LegCallTime end() { + return LegCallTime.ofStatic(endTime); } @Override diff --git a/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java b/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java index ce9ceeb7f46..18d2eebadd1 100644 --- a/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java +++ b/application/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java @@ -87,7 +87,7 @@ public ApiLeg mapLeg( api.departureDelay = domain.getDepartureDelay(); api.arrivalDelay = domain.getArrivalDelay(); - api.realTime = domain.getRealTime(); + api.realTime = domain.isRealTimeUpdated(); api.isNonExactFrequency = domain.getNonExactFrequency(); api.headway = domain.getHeadway(); api.distance = round3Decimals(domain.getDistanceMeters()); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLUtils.java b/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLUtils.java index 3fb339daa32..fe63add7d49 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLUtils.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLUtils.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.Locale; +import javax.annotation.Nullable; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLFilterPlaceType; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLFormFactor; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLInputField; @@ -9,6 +10,7 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLWheelchairBoarding; import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.model.StopTime; import org.opentripplanner.routing.api.response.InputField; import org.opentripplanner.routing.api.response.RoutingErrorCode; import org.opentripplanner.routing.graphfinder.PlaceType; @@ -109,4 +111,17 @@ public static boolean startsWith(String str, String name, Locale locale) { public static boolean startsWith(I18NString str, String name, Locale locale) { return str != null && str.toString(locale).toLowerCase(locale).startsWith(name); } + + /** + * Generally the missing values are removed during the graph build. However, for flex trips they + * are not and have to be converted to null here. + */ + @Nullable + public static Integer stopTimeToInt(int value) { + if (value == StopTime.MISSING_VALUE) { + return null; + } else { + return value; + } + } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index 34e9b2f8346..302458ac656 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -31,18 +31,22 @@ import org.opentripplanner.apis.gtfs.datafetchers.BikeRentalStationImpl; import org.opentripplanner.apis.gtfs.datafetchers.BookingInfoImpl; import org.opentripplanner.apis.gtfs.datafetchers.BookingTimeImpl; +import org.opentripplanner.apis.gtfs.datafetchers.CallScheduledTimeTypeResolver; +import org.opentripplanner.apis.gtfs.datafetchers.CallStopLocationTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.CarParkImpl; import org.opentripplanner.apis.gtfs.datafetchers.ContactInfoImpl; import org.opentripplanner.apis.gtfs.datafetchers.CoordinatesImpl; import org.opentripplanner.apis.gtfs.datafetchers.CurrencyImpl; import org.opentripplanner.apis.gtfs.datafetchers.DefaultFareProductImpl; import org.opentripplanner.apis.gtfs.datafetchers.DepartureRowImpl; +import org.opentripplanner.apis.gtfs.datafetchers.EstimatedTimeImpl; import org.opentripplanner.apis.gtfs.datafetchers.FareProductTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.FareProductUseImpl; import org.opentripplanner.apis.gtfs.datafetchers.FeedImpl; import org.opentripplanner.apis.gtfs.datafetchers.GeometryImpl; import org.opentripplanner.apis.gtfs.datafetchers.ItineraryImpl; import org.opentripplanner.apis.gtfs.datafetchers.LegImpl; +import org.opentripplanner.apis.gtfs.datafetchers.LegTimeImpl; import org.opentripplanner.apis.gtfs.datafetchers.MoneyImpl; import org.opentripplanner.apis.gtfs.datafetchers.NodeTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.OpeningHoursImpl; @@ -52,6 +56,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.PlanConnectionImpl; import org.opentripplanner.apis.gtfs.datafetchers.PlanImpl; import org.opentripplanner.apis.gtfs.datafetchers.QueryTypeImpl; +import org.opentripplanner.apis.gtfs.datafetchers.RealTimeEstimateImpl; import org.opentripplanner.apis.gtfs.datafetchers.RentalPlaceTypeResolver; import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleImpl; import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleTypeImpl; @@ -59,6 +64,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.RouteImpl; import org.opentripplanner.apis.gtfs.datafetchers.RouteTypeImpl; import org.opentripplanner.apis.gtfs.datafetchers.RoutingErrorImpl; +import org.opentripplanner.apis.gtfs.datafetchers.StopCallImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopGeometriesImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopImpl; import org.opentripplanner.apis.gtfs.datafetchers.StopOnRouteImpl; @@ -71,6 +77,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.TranslatedStringImpl; import org.opentripplanner.apis.gtfs.datafetchers.TripImpl; import org.opentripplanner.apis.gtfs.datafetchers.TripOccupancyImpl; +import org.opentripplanner.apis.gtfs.datafetchers.TripOnServiceDateImpl; import org.opentripplanner.apis.gtfs.datafetchers.UnknownImpl; import org.opentripplanner.apis.gtfs.datafetchers.VehicleParkingImpl; import org.opentripplanner.apis.gtfs.datafetchers.VehiclePositionImpl; @@ -128,6 +135,8 @@ protected static GraphQLSchema buildSchema() { .type("StopPosition", type -> type.typeResolver(new StopPosition() {})) .type("FareProduct", type -> type.typeResolver(new FareProductTypeResolver())) .type("AlertEntity", type -> type.typeResolver(new AlertEntityTypeResolver())) + .type("CallStopLocation", type -> type.typeResolver(new CallStopLocationTypeResolver())) + .type("CallScheduledTime", type -> type.typeResolver(new CallScheduledTimeTypeResolver())) .type(typeWiring.build(AgencyImpl.class)) .type(typeWiring.build(AlertImpl.class)) .type(typeWiring.build(BikeParkImpl.class)) @@ -180,7 +189,12 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(CurrencyImpl.class)) .type(typeWiring.build(FareProductUseImpl.class)) .type(typeWiring.build(DefaultFareProductImpl.class)) + .type(typeWiring.build(TripOnServiceDateImpl.class)) + .type(typeWiring.build(StopCallImpl.class)) .type(typeWiring.build(TripOccupancyImpl.class)) + .type(typeWiring.build(LegTimeImpl.class)) + .type(typeWiring.build(RealTimeEstimateImpl.class)) + .type(typeWiring.build(EstimatedTimeImpl.class)) .build(); SchemaGenerator schemaGenerator = new SchemaGenerator(); return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/CallScheduledTimeTypeResolver.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/CallScheduledTimeTypeResolver.java new file mode 100644 index 00000000000..ac45b3766b6 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/CallScheduledTimeTypeResolver.java @@ -0,0 +1,21 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.TypeResolutionEnvironment; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.TypeResolver; +import org.opentripplanner.apis.gtfs.model.ArrivalDepartureTime; + +public class CallScheduledTimeTypeResolver implements TypeResolver { + + @Override + public GraphQLObjectType getType(TypeResolutionEnvironment environment) { + Object o = environment.getObject(); + GraphQLSchema schema = environment.getSchema(); + + if (o instanceof ArrivalDepartureTime) { + return schema.getObjectType("ArrivalDepartureTime"); + } + return null; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/CallStopLocationTypeResolver.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/CallStopLocationTypeResolver.java new file mode 100644 index 00000000000..5a7b02320a3 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/CallStopLocationTypeResolver.java @@ -0,0 +1,21 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.TypeResolutionEnvironment; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.TypeResolver; +import org.opentripplanner.transit.model.site.StopLocation; + +public class CallStopLocationTypeResolver implements TypeResolver { + + @Override + public GraphQLObjectType getType(TypeResolutionEnvironment environment) { + Object o = environment.getObject(); + GraphQLSchema schema = environment.getSchema(); + + if (o instanceof StopLocation) { + return schema.getObjectType("Stop"); + } + return null; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EstimatedTimeImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EstimatedTimeImpl.java new file mode 100644 index 00000000000..eebb64ee625 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/EstimatedTimeImpl.java @@ -0,0 +1,25 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.time.Duration; +import java.time.OffsetDateTime; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.transit.model.timetable.EstimatedTime; + +public class EstimatedTimeImpl implements GraphQLDataFetchers.GraphQLEstimatedTime { + + @Override + public DataFetcher delay() { + return environment -> getSource(environment).delay(); + } + + @Override + public DataFetcher time() { + return environment -> getSource(environment).time().toOffsetDateTime(); + } + + private EstimatedTime getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java index 5e892c06368..23dd1a437c4 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java @@ -10,15 +10,15 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.mapping.NumberMapper; +import org.opentripplanner.apis.gtfs.mapping.PickDropMapper; import org.opentripplanner.apis.gtfs.mapping.RealtimeStateMapper; import org.opentripplanner.ext.restapi.mapping.LocalDateMapper; import org.opentripplanner.ext.ridehailing.model.RideEstimate; import org.opentripplanner.ext.ridehailing.model.RideHailingLeg; import org.opentripplanner.framework.graphql.GraphQLUtils; -import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.Leg; -import org.opentripplanner.model.plan.LegTime; +import org.opentripplanner.model.plan.LegCallTime; import org.opentripplanner.model.plan.ScheduledTransitLeg; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.StreetLeg; @@ -67,12 +67,12 @@ public DataFetcher dropOffBookingInfo() { } @Override - public DataFetcher dropoffType() { + public DataFetcher dropoffType() { return environment -> { if (getSource(environment).getAlightRule() == null) { - return PickDrop.SCHEDULED.name(); + return GraphQLTypes.GraphQLPickupDropoffType.SCHEDULED; } - return getSource(environment).getAlightRule().name(); + return PickDropMapper.map(getSource(environment).getAlightRule()); }; } @@ -82,7 +82,7 @@ public DataFetcher duration() { } @Override - public DataFetcher end() { + public DataFetcher end() { return environment -> getSource(environment).end(); } @@ -178,18 +178,18 @@ public DataFetcher pickupBookingInfo() { } @Override - public DataFetcher pickupType() { + public DataFetcher pickupType() { return environment -> { if (getSource(environment).getBoardRule() == null) { - return PickDrop.SCHEDULED.name(); + return GraphQLTypes.GraphQLPickupDropoffType.SCHEDULED; } - return getSource(environment).getBoardRule().name(); + return PickDropMapper.map(getSource(environment).getBoardRule()); }; } @Override public DataFetcher realTime() { - return environment -> getSource(environment).getRealTime(); + return environment -> getSource(environment).isRealTimeUpdated(); } @Override @@ -228,7 +228,7 @@ public DataFetcher serviceDate() { } @Override - public DataFetcher start() { + public DataFetcher start() { return environment -> getSource(environment).start(); } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegTimeImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegTimeImpl.java new file mode 100644 index 00000000000..571c0f2f38c --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegTimeImpl.java @@ -0,0 +1,25 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.time.OffsetDateTime; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.model.plan.LegCallTime; +import org.opentripplanner.model.plan.LegRealTimeEstimate; + +public class LegTimeImpl implements GraphQLDataFetchers.GraphQLLegTime { + + @Override + public DataFetcher estimated() { + return environment -> getSource(environment).estimated(); + } + + @Override + public DataFetcher scheduledTime() { + return environment -> getSource(environment).scheduledTime().toOffsetDateTime(); + } + + private LegCallTime getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java index abb3a607ab9..05662e4c9b6 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java @@ -7,7 +7,7 @@ import org.opentripplanner.apis.gtfs.model.StopPosition; import org.opentripplanner.apis.gtfs.model.StopPosition.PositionAtStop; import org.opentripplanner.framework.graphql.GraphQLUtils; -import org.opentripplanner.model.plan.LegTime; +import org.opentripplanner.model.plan.LegCallTime; import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.VertexType; @@ -19,7 +19,7 @@ public class PlaceImpl implements GraphQLDataFetchers.GraphQLPlace { @Override - public DataFetcher arrival() { + public DataFetcher arrival() { return environment -> getSource(environment).arrival; } @@ -58,7 +58,7 @@ public DataFetcher carPark() { @Deprecated @Override - public DataFetcher departure() { + public DataFetcher departure() { return environment -> getSource(environment).departure; } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index d02d7b59ffa..5d76cfd146f 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -71,6 +71,7 @@ import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.service.TransitService; import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.utils.time.ServiceDateUtils; @@ -195,7 +196,10 @@ public DataFetcher> bikeRentalStations() { }; } - // TODO + /** + * @deprecated Replaced by {@link #canceledTrips()}. + */ + @Deprecated @Override public DataFetcher> cancelledTripTimes() { return environment -> null; @@ -831,6 +835,14 @@ public DataFetcher> trips() { }; } + @Override + public DataFetcher> canceledTrips() { + return environment -> { + var trips = getTransitService(environment).listCanceledTrips(); + return new SimpleListConnection<>(trips).get(environment); + }; + } + @Override public DataFetcher vehicleParking() { return environment -> { diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RealTimeEstimateImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RealTimeEstimateImpl.java new file mode 100644 index 00000000000..fbbf5226691 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RealTimeEstimateImpl.java @@ -0,0 +1,25 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.time.Duration; +import java.time.OffsetDateTime; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.model.plan.LegRealTimeEstimate; + +public class RealTimeEstimateImpl implements GraphQLDataFetchers.GraphQLRealTimeEstimate { + + @Override + public DataFetcher delay() { + return environment -> getSource(environment).delay(); + } + + @Override + public DataFetcher time() { + return environment -> getSource(environment).time().toOffsetDateTime(); + } + + private LegRealTimeEstimate getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopCallImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopCallImpl.java new file mode 100644 index 00000000000..4d3ede74c76 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopCallImpl.java @@ -0,0 +1,76 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import static org.opentripplanner.apis.gtfs.GraphQLUtils.stopTimeToInt; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.time.ZonedDateTime; +import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.apis.gtfs.model.ArrivalDepartureTime; +import org.opentripplanner.apis.gtfs.model.CallRealTime; +import org.opentripplanner.apis.gtfs.model.CallSchedule; +import org.opentripplanner.model.TripTimeOnDate; +import org.opentripplanner.transit.model.timetable.EstimatedTime; +import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.utils.time.ServiceDateUtils; + +public class StopCallImpl implements GraphQLDataFetchers.GraphQLStopCall { + + @Override + public DataFetcher realTime() { + return environment -> { + var tripTime = getSource(environment); + if (!tripTime.isRealtime()) { + return null; + } + var scheduledArrival = getZonedDateTime(environment, tripTime.getScheduledArrival()); + var estimatedArrival = scheduledArrival == null + ? null + : EstimatedTime.of(scheduledArrival, tripTime.getArrivalDelay()); + var scheduledDeparture = getZonedDateTime(environment, tripTime.getScheduledDeparture()); + var estimatedDeparture = scheduledDeparture == null + ? null + : EstimatedTime.of(scheduledDeparture, tripTime.getDepartureDelay()); + return new CallRealTime(estimatedArrival, estimatedDeparture); + }; + } + + @Override + public DataFetcher schedule() { + return environment -> { + var tripTime = getSource(environment); + var scheduledArrival = getZonedDateTime(environment, tripTime.getScheduledArrival()); + var scheduledDeparture = getZonedDateTime(environment, tripTime.getScheduledDeparture()); + return new CallSchedule( + new ArrivalDepartureTime( + scheduledArrival == null ? null : scheduledArrival.toOffsetDateTime(), + scheduledDeparture == null ? null : scheduledDeparture.toOffsetDateTime() + ) + ); + }; + } + + @Override + public DataFetcher stopLocation() { + return environment -> getSource(environment).getStop(); + } + + private TransitService getTransitService(DataFetchingEnvironment environment) { + return environment.getContext().transitService(); + } + + private ZonedDateTime getZonedDateTime(DataFetchingEnvironment environment, int time) { + var fixedTime = stopTimeToInt(time); + if (fixedTime == null) { + return null; + } + var serviceDate = getSource(environment).getServiceDay(); + TransitService transitService = getTransitService(environment); + return ServiceDateUtils.toZonedDateTime(serviceDate, transitService.getTimeZone(), fixedTime); + } + + private TripTimeOnDate getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StoptimeImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StoptimeImpl.java index d7c8e97e255..ff39fe5994e 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StoptimeImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StoptimeImpl.java @@ -1,12 +1,14 @@ package org.opentripplanner.apis.gtfs.datafetchers; +import static org.opentripplanner.apis.gtfs.GraphQLUtils.stopTimeToInt; + import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.mapping.PickDropMapper; import org.opentripplanner.apis.gtfs.mapping.RealtimeStateMapper; import org.opentripplanner.framework.graphql.GraphQLUtils; -import org.opentripplanner.model.StopTime; import org.opentripplanner.model.TripTimeOnDate; import org.opentripplanner.transit.model.timetable.Trip; @@ -14,7 +16,7 @@ public class StoptimeImpl implements GraphQLDataFetchers.GraphQLStoptime { @Override public DataFetcher arrivalDelay() { - return environment -> missingValueToNull(getSource(environment).getArrivalDelay()); + return environment -> stopTimeToInt(getSource(environment).getArrivalDelay()); } @Override @@ -23,15 +25,8 @@ public DataFetcher departureDelay() { } @Override - public DataFetcher dropoffType() { - return environment -> - switch (getSource(environment).getDropoffType()) { - case SCHEDULED -> "SCHEDULED"; - case NONE -> "NONE"; - case CALL_AGENCY -> "CALL_AGENCY"; - case COORDINATE_WITH_DRIVER -> "COORDINATE_WITH_DRIVER"; - case CANCELLED -> null; - }; + public DataFetcher dropoffType() { + return environment -> PickDropMapper.map(getSource(environment).getDropoffType()); } @Override @@ -41,15 +36,8 @@ public DataFetcher headsign() { } @Override - public DataFetcher pickupType() { - return environment -> - switch (getSource(environment).getPickupType()) { - case SCHEDULED -> "SCHEDULED"; - case NONE -> "NONE"; - case CALL_AGENCY -> "CALL_AGENCY"; - case COORDINATE_WITH_DRIVER -> "COORDINATE_WITH_DRIVER"; - case CANCELLED -> null; - }; + public DataFetcher pickupType() { + return environment -> PickDropMapper.map(getSource(environment).getPickupType()); } @Override @@ -59,12 +47,12 @@ public DataFetcher realtime() { @Override public DataFetcher realtimeArrival() { - return environment -> missingValueToNull(getSource(environment).getRealtimeArrival()); + return environment -> stopTimeToInt(getSource(environment).getRealtimeArrival()); } @Override public DataFetcher realtimeDeparture() { - return environment -> missingValueToNull(getSource(environment).getRealtimeDeparture()); + return environment -> stopTimeToInt(getSource(environment).getRealtimeDeparture()); } @Override @@ -77,12 +65,12 @@ public DataFetcher realtimeState() { @Override public DataFetcher scheduledArrival() { - return environment -> missingValueToNull(getSource(environment).getScheduledArrival()); + return environment -> stopTimeToInt(getSource(environment).getScheduledArrival()); } @Override public DataFetcher scheduledDeparture() { - return environment -> missingValueToNull(getSource(environment).getScheduledDeparture()); + return environment -> stopTimeToInt(getSource(environment).getScheduledDeparture()); } @Override @@ -118,16 +106,4 @@ public DataFetcher trip() { private TripTimeOnDate getSource(DataFetchingEnvironment environment) { return environment.getSource(); } - - /** - * Generally the missing values are removed during the graph build. However, for flex trips they - * are not and have to be converted to null here. - */ - private Integer missingValueToNull(int value) { - if (value == StopTime.MISSING_VALUE) { - return null; - } else { - return value; - } - } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripOnServiceDateImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripOnServiceDateImpl.java new file mode 100644 index 00000000000..9960687c6a9 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripOnServiceDateImpl.java @@ -0,0 +1,125 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +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.TripOnServiceDate; +import org.opentripplanner.transit.service.TransitService; +import org.opentripplanner.utils.time.ServiceDateUtils; + +public class TripOnServiceDateImpl implements GraphQLDataFetchers.GraphQLTripOnServiceDate { + + @Override + public DataFetcher serviceDate() { + return env -> getSource(env).getServiceDate(); + } + + @Override + public DataFetcher end() { + return environment -> { + var arguments = getFromTripTimesArguments(environment); + if (arguments.timetable() == null) { + return null; + } + return TripTimeOnDate.lastFromTripTimes( + arguments.timetable(), + arguments.trip(), + arguments.serviceDate(), + arguments.midnight() + ); + }; + } + + @Override + public DataFetcher start() { + return environment -> { + var arguments = getFromTripTimesArguments(environment); + if (arguments.timetable() == null) { + return null; + } + return TripTimeOnDate.firstFromTripTimes( + arguments.timetable(), + arguments.trip(), + arguments.serviceDate(), + arguments.midnight() + ); + }; + } + + @Override + public DataFetcher> stopCalls() { + return environment -> { + var arguments = getFromTripTimesArguments(environment); + if (arguments.timetable() == null) { + return List.of(); + } + return TripTimeOnDate.fromTripTimes( + arguments.timetable(), + arguments.trip(), + arguments.serviceDate(), + arguments.midnight() + ); + }; + } + + @Override + public DataFetcher trip() { + return this::getTrip; + } + + @Nullable + private Timetable getTimetable( + DataFetchingEnvironment environment, + Trip trip, + LocalDate serviceDate + ) { + TransitService transitService = getTransitService(environment); + TripPattern tripPattern = transitService.findPattern(trip, serviceDate); + // no matching pattern found + if (tripPattern == null) { + return null; + } + + return transitService.findTimetable(tripPattern, serviceDate); + } + + private TransitService getTransitService(DataFetchingEnvironment environment) { + return environment.getContext().transitService(); + } + + private Trip getTrip(DataFetchingEnvironment environment) { + return getSource(environment).getTrip(); + } + + private TripOnServiceDate getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } + + private FromTripTimesArguments getFromTripTimesArguments(DataFetchingEnvironment environment) { + TransitService transitService = getTransitService(environment); + Trip trip = getTrip(environment); + var serviceDate = getSource(environment).getServiceDate(); + + Instant midnight = ServiceDateUtils + .asStartOfService(serviceDate, transitService.getTimeZone()) + .toInstant(); + Timetable timetable = getTimetable(environment, trip, serviceDate); + return new FromTripTimesArguments(trip, serviceDate, midnight, timetable); + } + + private record FromTripTimesArguments( + Trip trip, + LocalDate serviceDate, + Instant midnight, + @Nullable Timetable timetable + ) {} +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index dd74347b928..f5798893419 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -18,10 +18,13 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLInputField; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLOccupancyStatus; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLPickupDropoffType; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRealtimeState; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRelativeDirection; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRoutingErrorCode; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; +import org.opentripplanner.apis.gtfs.model.CallRealTime; +import org.opentripplanner.apis.gtfs.model.CallSchedule; import org.opentripplanner.apis.gtfs.model.FeedPublisher; import org.opentripplanner.apis.gtfs.model.PlanPageInfo; import org.opentripplanner.apis.gtfs.model.RideHailingProvider; @@ -40,7 +43,8 @@ import org.opentripplanner.model.plan.Emissions; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.model.plan.Leg; -import org.opentripplanner.model.plan.LegTime; +import org.opentripplanner.model.plan.LegCallTime; +import org.opentripplanner.model.plan.LegRealTimeEstimate; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.WalkStep; import org.opentripplanner.routing.alertpatch.TransitAlert; @@ -65,7 +69,9 @@ import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; +import org.opentripplanner.transit.model.timetable.EstimatedTime; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.booking.BookingInfo; import org.opentripplanner.transit.model.timetable.booking.BookingTime; @@ -140,6 +146,13 @@ public interface GraphQLAlert { /** Entity related to an alert */ public interface GraphQLAlertEntity extends TypeResolver {} + /** Arrival and departure time (not relative to midnight). */ + public interface GraphQLArrivalDepartureTime { + public DataFetcher arrival(); + + public DataFetcher departure(); + } + /** Bike park represents a location where bicycles can be parked. */ public interface GraphQLBikePark { public DataFetcher bikeParkId(); @@ -237,6 +250,24 @@ public interface GraphQLBookingTime { public DataFetcher time(); } + /** Real-time estimates for arrival and departure times for a stop location. */ + public interface GraphQLCallRealTime { + public DataFetcher arrival(); + + public DataFetcher departure(); + } + + /** What is scheduled for a trip on a service date for a stop location. */ + public interface GraphQLCallSchedule { + public DataFetcher time(); + } + + /** Scheduled times for a trip on a service date for a stop location. */ + public interface GraphQLCallScheduledTime extends TypeResolver {} + + /** Location where a transit vehicle stops at. */ + public interface GraphQLCallStopLocation extends TypeResolver {} + /** Car park represents a location where cars can be parked. */ public interface GraphQLCarPark { public DataFetcher carParkId(); @@ -357,6 +388,13 @@ public interface GraphQLEmissions { public DataFetcher co2(); } + /** Real-time estimates for an arrival or departure at a certain place. */ + public interface GraphQLEstimatedTime { + public DataFetcher delay(); + + public DataFetcher time(); + } + /** A 'medium' that a fare product applies to, for example cash, 'Oyster Card' or 'DB Navigator App'. */ public interface GraphQLFareMedium { public DataFetcher id(); @@ -467,11 +505,11 @@ public interface GraphQLLeg { public DataFetcher dropOffBookingInfo(); - public DataFetcher dropoffType(); + public DataFetcher dropoffType(); public DataFetcher duration(); - public DataFetcher end(); + public DataFetcher end(); public DataFetcher endTime(); @@ -501,7 +539,7 @@ public interface GraphQLLeg { public DataFetcher pickupBookingInfo(); - public DataFetcher pickupType(); + public DataFetcher pickupType(); public DataFetcher> previousLegs(); @@ -517,7 +555,7 @@ public interface GraphQLLeg { public DataFetcher serviceDate(); - public DataFetcher start(); + public DataFetcher start(); public DataFetcher startTime(); @@ -537,7 +575,7 @@ public interface GraphQLLeg { * available. */ public interface GraphQLLegTime { - public DataFetcher estimated(); + public DataFetcher estimated(); public DataFetcher scheduledTime(); } @@ -625,7 +663,7 @@ public interface GraphQLPattern { } public interface GraphQLPlace { - public DataFetcher arrival(); + public DataFetcher arrival(); public DataFetcher arrivalTime(); @@ -635,7 +673,7 @@ public interface GraphQLPlace { public DataFetcher carPark(); - public DataFetcher departure(); + public DataFetcher departure(); public DataFetcher departureTime(); @@ -768,6 +806,8 @@ public interface GraphQLQueryType { public DataFetcher> bikeRentalStations(); + public DataFetcher> canceledTrips(); + public DataFetcher> cancelledTripTimes(); public DataFetcher carPark(); @@ -1043,6 +1083,15 @@ public interface GraphQLStop { public DataFetcher zoneId(); } + /** Stop call represents the time when a specific trip on a specific date arrives to and/or departs from a specific stop location. */ + public interface GraphQLStopCall { + public DataFetcher realTime(); + + public DataFetcher schedule(); + + public DataFetcher stopLocation(); + } + public interface GraphQLStopGeometries { public DataFetcher geoJson(); @@ -1078,11 +1127,11 @@ public interface GraphQLStoptime { public DataFetcher departureDelay(); - public DataFetcher dropoffType(); + public DataFetcher dropoffType(); public DataFetcher headsign(); - public DataFetcher pickupType(); + public DataFetcher pickupType(); public DataFetcher realtime(); @@ -1209,6 +1258,39 @@ public interface GraphQLTripOccupancy { public DataFetcher occupancyStatus(); } + /** A trip on a specific service date. */ + public interface GraphQLTripOnServiceDate { + public DataFetcher end(); + + public DataFetcher serviceDate(); + + public DataFetcher start(); + + public DataFetcher> stopCalls(); + + public DataFetcher trip(); + } + + /** + * A connection to a list of trips on service dates that follows + * [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + */ + public interface GraphQLTripOnServiceDateConnection { + public DataFetcher>> edges(); + + public DataFetcher pageInfo(); + } + + /** + * An edge for TripOnServiceDate connection. Part of the + * [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + */ + public interface GraphQLTripOnServiceDateEdge { + public DataFetcher cursor(); + + public DataFetcher node(); + } + /** This is used for alert entities that we don't explicitly handle or they are missing. */ public interface GraphQLUnknown { public DataFetcher description(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index b94541d2470..a969b5223b1 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -2469,6 +2469,55 @@ public void setGraphQLIds(List ids) { } } + public static class GraphQLQueryTypeCanceledTripsArgs { + + private String after; + private String before; + private Integer first; + private Integer last; + + public GraphQLQueryTypeCanceledTripsArgs(Map args) { + if (args != null) { + this.after = (String) args.get("after"); + this.before = (String) args.get("before"); + this.first = (Integer) args.get("first"); + this.last = (Integer) args.get("last"); + } + } + + public String getGraphQLAfter() { + return this.after; + } + + public String getGraphQLBefore() { + return this.before; + } + + public Integer getGraphQLFirst() { + return this.first; + } + + public Integer getGraphQLLast() { + return this.last; + } + + public void setGraphQLAfter(String after) { + this.after = after; + } + + public void setGraphQLBefore(String before) { + this.before = before; + } + + public void setGraphQLFirst(Integer first) { + this.first = first; + } + + public void setGraphQLLast(Integer last) { + this.last = last; + } + } + public static class GraphQLQueryTypeCancelledTripTimesArgs { private List feeds; diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index 59a2329a461..a9bb87a6ea5 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -59,6 +59,10 @@ config: ContactInfo: org.opentripplanner.transit.model.organization.ContactInfo Cluster: Object Coordinates: org.locationtech.jts.geom.Coordinate#Coordinate + StopCall: org.opentripplanner.model.TripTimeOnDate#TripTimeOnDate + TripOnServiceDate: org.opentripplanner.transit.model.timetable.TripOnServiceDate#TripOnServiceDate + TripOnServiceDateConnection: graphql.relay.Connection#Connection + TripOnServiceDateEdge: graphql.relay.Edge#Edge debugOutput: org.opentripplanner.api.resource.DebugOutput#DebugOutput DepartureRow: org.opentripplanner.routing.graphfinder.PatternAtStop#PatternAtStop elevationProfileComponent: org.opentripplanner.model.plan.ElevationProfile.Step @@ -69,13 +73,13 @@ config: InputField: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLInputField#GraphQLInputField Itinerary: org.opentripplanner.model.plan.Itinerary#Itinerary Leg: org.opentripplanner.model.plan.Leg#Leg - LegTime: org.opentripplanner.model.plan.LegTime#LegTime + LegTime: org.opentripplanner.model.plan.LegCallTime#LegCallTime Mode: String OccupancyStatus: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLOccupancyStatus#GraphQLOccupancyStatus TransitMode: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode#GraphQLTransitMode PageInfo: Object Pattern: org.opentripplanner.transit.model.network.TripPattern#TripPattern - PickupDropoffType: String + PickupDropoffType: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLPickupDropoffType#GraphQLPickupDropoffType Place: org.opentripplanner.model.plan.StopArrival#StopArrival placeAtDistance: org.opentripplanner.routing.graphfinder.PlaceAtDistance#PlaceAtDistance placeAtDistanceConnection: graphql.relay.Connection#Connection @@ -125,5 +129,9 @@ config: FareMedium: org.opentripplanner.model.fare.FareMedium#FareMedium RiderCategory: org.opentripplanner.model.fare.RiderCategory#RiderCategory StopPosition: org.opentripplanner.apis.gtfs.model.StopPosition#StopPosition + RealTimeEstimate: org.opentripplanner.model.plan.LegRealTimeEstimate#LegRealTimeEstimate + EstimatedTime: org.opentripplanner.transit.model.timetable.EstimatedTime#EstimatedTime + CallRealTime: org.opentripplanner.apis.gtfs.model.CallRealTime#CallRealTime RentalPlace: org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace#VehicleRentalPlace + CallSchedule: org.opentripplanner.apis.gtfs.model.CallSchedule#CallSchedule diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/PickDropMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/PickDropMapper.java new file mode 100644 index 00000000000..acb7e61cc4b --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/PickDropMapper.java @@ -0,0 +1,19 @@ +package org.opentripplanner.apis.gtfs.mapping; + +import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.model.PickDrop; + +public final class PickDropMapper { + + @Nullable + public static GraphQLTypes.GraphQLPickupDropoffType map(PickDrop pickDrop) { + return switch (pickDrop) { + case SCHEDULED -> GraphQLTypes.GraphQLPickupDropoffType.SCHEDULED; + case NONE -> GraphQLTypes.GraphQLPickupDropoffType.NONE; + case CALL_AGENCY -> GraphQLTypes.GraphQLPickupDropoffType.CALL_AGENCY; + case COORDINATE_WITH_DRIVER -> GraphQLTypes.GraphQLPickupDropoffType.COORDINATE_WITH_DRIVER; + case CANCELLED -> null; + }; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/model/ArrivalDepartureTime.java b/application/src/main/java/org/opentripplanner/apis/gtfs/model/ArrivalDepartureTime.java new file mode 100644 index 00000000000..f72bdd0b3e9 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/model/ArrivalDepartureTime.java @@ -0,0 +1,9 @@ +package org.opentripplanner.apis.gtfs.model; + +import java.time.OffsetDateTime; +import javax.annotation.Nullable; + +public record ArrivalDepartureTime( + @Nullable OffsetDateTime arrival, + @Nullable OffsetDateTime departure +) {} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/model/CallRealTime.java b/application/src/main/java/org/opentripplanner/apis/gtfs/model/CallRealTime.java new file mode 100644 index 00000000000..94caf731627 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/model/CallRealTime.java @@ -0,0 +1,6 @@ +package org.opentripplanner.apis.gtfs.model; + +import javax.annotation.Nullable; +import org.opentripplanner.transit.model.timetable.EstimatedTime; + +public record CallRealTime(@Nullable EstimatedTime arrival, @Nullable EstimatedTime departure) {} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/model/CallSchedule.java b/application/src/main/java/org/opentripplanner/apis/gtfs/model/CallSchedule.java new file mode 100644 index 00000000000..6b35d2ed425 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/model/CallSchedule.java @@ -0,0 +1,3 @@ +package org.opentripplanner.apis.gtfs.model; + +public record CallSchedule(ArrivalDepartureTime time) {} diff --git a/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java b/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java index 4b405f50f66..60d98c1bd24 100644 --- a/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java +++ b/application/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java @@ -204,7 +204,7 @@ public static GraphQLObjectType create( .name("realtime") .description("Whether there is real-time data about this leg") .type(new GraphQLNonNull(Scalars.GraphQLBoolean)) - .dataFetcher(env -> leg(env).getRealTime()) + .dataFetcher(env -> leg(env).isRealTimeUpdated()) .build() ) .field( diff --git a/application/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/application/src/main/java/org/opentripplanner/model/TimetableSnapshot.java index 94b490c48a0..44ce1e5cb18 100644 --- a/application/src/main/java/org/opentripplanner/model/TimetableSnapshot.java +++ b/application/src/main/java/org/opentripplanner/model/TimetableSnapshot.java @@ -8,16 +8,20 @@ import com.google.common.collect.Multimap; import com.google.common.collect.SetMultimap; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedSet; import java.util.TreeSet; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.transit.model.framework.FeedScopedId; @@ -222,6 +226,13 @@ public TripPattern getNewTripPatternForModifiedTrip(FeedScopedId tripId, LocalDa return realTimeNewTripPatternsForModifiedTrips.get(tripIdAndServiceDate); } + /** + * List trips which have been canceled. + */ + public List listCanceledTrips() { + return findTripsOnServiceDates(TripTimes::isCanceled); + } + /** * @return if any trip patterns were modified */ @@ -643,6 +654,37 @@ private void validateNotReadOnly() { } } + private TripOnServiceDate mapToTripOnServiceDate(TripTimes tripTimes, Timetable timetable) { + return TripOnServiceDate + .of(tripTimes.getTrip().getId()) + .withServiceDate(timetable.getServiceDate()) + .withTrip(tripTimes.getTrip()) + .build(); + } + + /** + * Find trips from timetables based on filter criteria. + * + * @param filter used to filter {@link TripTimes}. + */ + private List findTripsOnServiceDates(Predicate filter) { + return timetables + .values() + .stream() + .flatMap(timetables -> + timetables + .stream() + .flatMap(timetable -> + timetable + .getTripTimes() + .stream() + .filter(filter) + .map(tripTimes -> mapToTripOnServiceDate(tripTimes, timetable)) + ) + ) + .collect(Collectors.toCollection(ArrayList::new)); + } + protected static class SortedTimetableComparator implements Comparator { @Override diff --git a/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java b/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java index 71aaa734f0e..857bb42ee51 100644 --- a/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java +++ b/application/src/main/java/org/opentripplanner/model/TripTimeOnDate.java @@ -89,6 +89,38 @@ public static List fromTripTimes( return out; } + /** + * Get first stop TripTimeOnDate from Timetable. + */ + public static TripTimeOnDate firstFromTripTimes( + Timetable table, + Trip trip, + LocalDate serviceDate, + Instant midnight + ) { + TripTimes times = table.getTripTimes(trip); + return new TripTimeOnDate(times, 0, table.getPattern(), serviceDate, midnight); + } + + /** + * Get last stop TripTimeOnDate from Timetable. + */ + public static TripTimeOnDate lastFromTripTimes( + Timetable table, + Trip trip, + LocalDate serviceDate, + Instant midnight + ) { + TripTimes times = table.getTripTimes(trip); + return new TripTimeOnDate( + times, + times.getNumStops() - 1, + table.getPattern(), + serviceDate, + midnight + ); + } + public static Comparator compareByDeparture() { return Comparator.comparing(t -> t.getServiceDayMidnight() + t.getRealtimeDeparture()); } @@ -140,14 +172,14 @@ public int getRealtimeDeparture() { * Returns the actual arrival time if available. Otherwise -1 is returned. */ public int getActualArrival() { - return tripTimes.isRecordedStop(stopIndex) ? tripTimes.getArrivalTime(stopIndex) : UNDEFINED; + return isRecordedStop() ? tripTimes.getArrivalTime(stopIndex) : UNDEFINED; } /** * Returns the actual departure time if available. Otherwise -1 is returned. */ public int getActualDeparture() { - return tripTimes.isRecordedStop(stopIndex) ? tripTimes.getDepartureTime(stopIndex) : UNDEFINED; + return isRecordedStop() ? tripTimes.getDepartureTime(stopIndex) : UNDEFINED; } public int getArrivalDelay() { @@ -190,6 +222,14 @@ public boolean isNoDataStop() { return tripTimes.isNoDataStop(stopIndex); } + /** + * Is the real-time time a recorded time (i.e. has the vehicle already passed the stop). + * This information is currently only available from SIRI feeds. + */ + public boolean isRecordedStop() { + return tripTimes.isRecordedStop(stopIndex); + } + public RealTimeState getRealTimeState() { return tripTimes.isNoDataStop(stopIndex) ? RealTimeState.SCHEDULED diff --git a/application/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java b/application/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java index dc00dd49f22..6226b0d2b01 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java +++ b/application/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java @@ -55,8 +55,8 @@ public List getIntermediateStops() { StopArrival visit = new StopArrival( Place.forStop(stop), - LegTime.ofStatic(ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, arrivalTime)), - LegTime.ofStatic(ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, departureTime)), + LegCallTime.ofStatic(ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, arrivalTime)), + LegCallTime.ofStatic(ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, departureTime)), i, tripTimes.gtfsSequenceOfStopIndex(i) ); diff --git a/application/src/main/java/org/opentripplanner/model/plan/Leg.java b/application/src/main/java/org/opentripplanner/model/plan/Leg.java index b3c9b526f03..27e67400daf 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/Leg.java +++ b/application/src/main/java/org/opentripplanner/model/plan/Leg.java @@ -209,12 +209,12 @@ default Accessibility getTripWheelchairAccessibility() { /** * The time (including realtime information) when the leg starts. */ - LegTime start(); + LegCallTime start(); /** * The time (including realtime information) when the leg ends. */ - LegTime end(); + LegCallTime end(); /** * The date and time this leg begins. @@ -245,7 +245,7 @@ default int getArrivalDelay() { /** * Whether there is real-time data about this Leg */ - default boolean getRealTime() { + default boolean isRealTimeUpdated() { return false; } diff --git a/application/src/main/java/org/opentripplanner/model/plan/LegCallTime.java b/application/src/main/java/org/opentripplanner/model/plan/LegCallTime.java new file mode 100644 index 00000000000..676b45c8f92 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/model/plan/LegCallTime.java @@ -0,0 +1,52 @@ +package org.opentripplanner.model.plan; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Objects; +import javax.annotation.Nullable; + +/** + * A scheduled time of a transit vehicle at a certain location with an optional realtime + * information. This is meant to be used in transit legs. + */ +public class LegCallTime { + + private final ZonedDateTime scheduledTime; + + @Nullable + private final LegRealTimeEstimate estimated; + + private LegCallTime(ZonedDateTime scheduledTime, @Nullable LegRealTimeEstimate estimated) { + this.scheduledTime = Objects.requireNonNull(scheduledTime); + this.estimated = estimated; + } + + public static LegCallTime of(ZonedDateTime realtime, int delaySecs) { + var delay = Duration.ofSeconds(delaySecs); + return new LegCallTime(realtime.minus(delay), new LegRealTimeEstimate(realtime, delay)); + } + + public static LegCallTime ofStatic(ZonedDateTime staticTime) { + return new LegCallTime(staticTime, null); + } + + public ZonedDateTime scheduledTime() { + return scheduledTime; + } + + public LegRealTimeEstimate estimated() { + return estimated; + } + + /** + * The most up-to-date time available: if realtime data is available it is returned, if not then + * the scheduled one is. + */ + public ZonedDateTime time() { + if (estimated == null) { + return scheduledTime; + } else { + return estimated.time(); + } + } +} diff --git a/application/src/main/java/org/opentripplanner/model/plan/LegRealTimeEstimate.java b/application/src/main/java/org/opentripplanner/model/plan/LegRealTimeEstimate.java new file mode 100644 index 00000000000..a924b0c22ae --- /dev/null +++ b/application/src/main/java/org/opentripplanner/model/plan/LegRealTimeEstimate.java @@ -0,0 +1,30 @@ +package org.opentripplanner.model.plan; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Objects; + +/** + * Realtime information about a vehicle at a certain place. Meant to be used in transit legs. + */ +public class LegRealTimeEstimate { + + private final ZonedDateTime time; + private final Duration delay; + + /** + * @param delay Delay or "earliness" of a vehicle. Earliness is expressed as a negative number. + */ + public LegRealTimeEstimate(ZonedDateTime time, Duration delay) { + this.time = Objects.requireNonNull(time); + this.delay = Objects.requireNonNull(delay); + } + + public ZonedDateTime time() { + return time; + } + + public Duration delay() { + return delay; + } +} diff --git a/application/src/main/java/org/opentripplanner/model/plan/LegTime.java b/application/src/main/java/org/opentripplanner/model/plan/LegTime.java deleted file mode 100644 index 71ced95942a..00000000000 --- a/application/src/main/java/org/opentripplanner/model/plan/LegTime.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.opentripplanner.model.plan; - -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.Objects; -import javax.annotation.Nullable; - -/** - * A scheduled time of a transit vehicle at a certain location with a optional realtime information. - */ -public record LegTime(ZonedDateTime scheduledTime, @Nullable RealTimeEstimate estimated) { - public LegTime { - Objects.requireNonNull(scheduledTime); - } - - public static LegTime of(ZonedDateTime realtime, int delaySecs) { - var delay = Duration.ofSeconds(delaySecs); - return new LegTime(realtime.minus(delay), new RealTimeEstimate(realtime, delay)); - } - - public static LegTime ofStatic(ZonedDateTime staticTime) { - return new LegTime(staticTime, null); - } - - /** - * The most up-to-date time available: if realtime data is available it is returned, if not then - * the scheduled one is. - */ - public ZonedDateTime time() { - if (estimated == null) { - return scheduledTime; - } else { - return estimated.time; - } - } - - /** - * Realtime information about a vehicle at a certain place. - * @param delay Delay or "earliness" of a vehicle. Earliness is expressed as a negative number. - */ - record RealTimeEstimate(ZonedDateTime time, Duration delay) {} -} diff --git a/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index f3fba29515e..55e4bccfdec 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/application/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -161,20 +161,20 @@ public Accessibility getTripWheelchairAccessibility() { } @Override - public LegTime start() { - if (getRealTime()) { - return LegTime.of(startTime, getDepartureDelay()); + public LegCallTime start() { + if (isRealTimeUpdated()) { + return LegCallTime.of(startTime, getDepartureDelay()); } else { - return LegTime.ofStatic(startTime); + return LegCallTime.ofStatic(startTime); } } @Override - public LegTime end() { - if (getRealTime()) { - return LegTime.of(endTime, getArrivalDelay()); + public LegCallTime end() { + if (isRealTimeUpdated()) { + return LegCallTime.of(endTime, getArrivalDelay()); } else { - return LegTime.ofStatic(endTime); + return LegCallTime.ofStatic(endTime); } } @@ -214,13 +214,10 @@ public int getArrivalDelay() { } @Override - public boolean getRealTime() { + public boolean isRealTimeUpdated() { return ( - !tripTimes.isScheduled() && - ( - !tripTimes.isNoDataStop(boardStopPosInPattern) || - !tripTimes.isNoDataStop(alightStopPosInPattern) - ) + tripTimes.isRealTimeUpdated(boardStopPosInPattern) || + tripTimes.isRealTimeUpdated(alightStopPosInPattern) ); } @@ -281,7 +278,7 @@ public List getIntermediateStops() { for (int i = boardStopPosInPattern + 1; i < alightStopPosInPattern; i++) { StopLocation stop = tripPattern.getStop(i); - final StopArrival visit = mapper.map(i, stop, getRealTime()); + final StopArrival visit = mapper.map(i, stop, isRealTimeUpdated()); visits.add(visit); } return visits; @@ -417,7 +414,7 @@ public String toString() { .addObj("to", getTo()) .addTime("startTime", startTime) .addTime("endTime", endTime) - .addBool("realTime", getRealTime()) + .addBool("realTime", isRealTimeUpdated()) .addNum("distance", distanceMeters, "m") .addNum("cost", generalizedCost) .addNum("routeType", getRouteType()) diff --git a/application/src/main/java/org/opentripplanner/model/plan/StopArrival.java b/application/src/main/java/org/opentripplanner/model/plan/StopArrival.java index d43df69ff21..478c8eea379 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/StopArrival.java +++ b/application/src/main/java/org/opentripplanner/model/plan/StopArrival.java @@ -9,8 +9,8 @@ public final class StopArrival { public final Place place; - public final LegTime arrival; - public final LegTime departure; + public final LegCallTime arrival; + public final LegCallTime departure; public final Integer stopPosInPattern; public final Integer gtfsStopSequence; @@ -24,8 +24,8 @@ public final class StopArrival { */ public StopArrival( Place place, - LegTime arrival, - LegTime departure, + LegCallTime arrival, + LegCallTime departure, Integer stopPosInPattern, Integer gtfsStopSequence ) { diff --git a/application/src/main/java/org/opentripplanner/model/plan/StopArrivalMapper.java b/application/src/main/java/org/opentripplanner/model/plan/StopArrivalMapper.java index c6a719addd5..a4c83e10901 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/StopArrivalMapper.java +++ b/application/src/main/java/org/opentripplanner/model/plan/StopArrivalMapper.java @@ -34,12 +34,12 @@ StopArrival map(int i, StopLocation stop, boolean realTime) { tripTimes.getDepartureTime(i) ); - var arrival = LegTime.ofStatic(arrivalTime); - var departure = LegTime.ofStatic(departureTime); + var arrival = LegCallTime.ofStatic(arrivalTime); + var departure = LegCallTime.ofStatic(departureTime); if (realTime) { - arrival = LegTime.of(arrivalTime, tripTimes.getArrivalDelay(i)); - departure = LegTime.of(departureTime, tripTimes.getDepartureDelay(i)); + arrival = LegCallTime.of(arrivalTime, tripTimes.getArrivalDelay(i)); + departure = LegCallTime.of(departureTime, tripTimes.getDepartureDelay(i)); } return new StopArrival( diff --git a/application/src/main/java/org/opentripplanner/model/plan/StreetLeg.java b/application/src/main/java/org/opentripplanner/model/plan/StreetLeg.java index a61c68dccc5..cb577b9ec49 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/StreetLeg.java +++ b/application/src/main/java/org/opentripplanner/model/plan/StreetLeg.java @@ -157,13 +157,13 @@ public boolean hasSameMode(Leg other) { } @Override - public LegTime start() { - return LegTime.ofStatic(startTime); + public LegCallTime start() { + return LegCallTime.ofStatic(startTime); } @Override - public LegTime end() { - return LegTime.ofStatic(endTime); + public LegCallTime end() { + return LegCallTime.ofStatic(endTime); } @Override diff --git a/application/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java b/application/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java index 45a9c6561d3..443621b750a 100644 --- a/application/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java +++ b/application/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java @@ -70,13 +70,13 @@ public boolean hasSameMode(Leg other) { } @Override - public LegTime start() { - return LegTime.ofStatic(startTime); + public LegCallTime start() { + return LegCallTime.ofStatic(startTime); } @Override - public LegTime end() { - return LegTime.ofStatic(endTime); + public LegCallTime end() { + return LegCallTime.ofStatic(endTime); } @Override diff --git a/application/src/main/java/org/opentripplanner/street/model/vertex/StationCentroidVertex.java b/application/src/main/java/org/opentripplanner/street/model/vertex/StationCentroidVertex.java index f6f3b00191d..30de8e0f597 100644 --- a/application/src/main/java/org/opentripplanner/street/model/vertex/StationCentroidVertex.java +++ b/application/src/main/java/org/opentripplanner/street/model/vertex/StationCentroidVertex.java @@ -20,7 +20,6 @@ public Station getStation() { return this.station; } - @Nonnull @Override public I18NString getName() { return station.getName(); diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/EstimatedTime.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/EstimatedTime.java new file mode 100644 index 00000000000..359a8c7de6a --- /dev/null +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/EstimatedTime.java @@ -0,0 +1,36 @@ +package org.opentripplanner.transit.model.timetable; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Objects; + +/** + * Realtime information about a vehicle at a certain place. This is meant to be used in timetables + * (not in transit legs). + */ +public class EstimatedTime { + + private final ZonedDateTime time; + private final Duration delay; + + /** + * @param delay Delay or "earliness" of a vehicle. Earliness is expressed as a negative number. + */ + private EstimatedTime(ZonedDateTime time, Duration delay) { + this.time = Objects.requireNonNull(time); + this.delay = Objects.requireNonNull(delay); + } + + public static EstimatedTime of(ZonedDateTime scheduledTime, int delaySecs) { + var delay = Duration.ofSeconds(delaySecs); + return new EstimatedTime(scheduledTime.minus(delay), delay); + } + + public ZonedDateTime time() { + return time; + } + + public Duration delay() { + return delay; + } +} diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java index 0ce120ab1eb..0568e48940c 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java @@ -191,6 +191,13 @@ public boolean isPredictionInaccurate(int stop) { return isStopRealTimeStates(stop, StopRealTimeState.INACCURATE_PREDICTIONS); } + public boolean isRealTimeUpdated(int stop) { + return ( + realTimeState != RealTimeState.SCHEDULED && + !isStopRealTimeStates(stop, StopRealTimeState.NO_DATA) + ); + } + public void setOccupancyStatus(int stop, OccupancyStatus occupancyStatus) { prepareForRealTimeUpdates(); this.occupancyStatus[stop] = occupancyStatus; diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java index 9a7298cd15e..6bfa0f01e69 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java @@ -240,6 +240,11 @@ public boolean isPredictionInaccurate(int stop) { return false; } + @Override + public boolean isRealTimeUpdated(int stop) { + return false; + } + @Override public I18NString getTripHeadsign() { return trip.getHeadsign(); diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/TripOnServiceDate.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/TripOnServiceDate.java index 1a22ec70000..c1f42e3b31f 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/TripOnServiceDate.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/TripOnServiceDate.java @@ -7,7 +7,8 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; /** - * Class for holding data about a certain trip on a certain day. Essentially a DatedServiceJourney. + * Class for holding data about a certain trip on a certain day. Essentially a DatedServiceJourney + * or an instance of a generic trip on a certain service date. */ public class TripOnServiceDate extends AbstractTransitEntity { diff --git a/application/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java b/application/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java index 062a7c17344..fe6385920b5 100644 --- a/application/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java +++ b/application/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java @@ -124,6 +124,11 @@ default int compareTo(TripTimes other) { boolean isPredictionInaccurate(int stop); + /** + * Return if trip has been updated and stop has not been given a NO_DATA update. + */ + boolean isRealTimeUpdated(int stop); + /** * @return the whole trip's headsign. Individual stops can have different headsigns. */ 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 d35977cae74..cf5bb11c3b3 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/DefaultTransitService.java @@ -10,6 +10,7 @@ import java.time.ZonedDateTime; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -273,6 +274,21 @@ public Trip getScheduledTrip(FeedScopedId id) { return this.timetableRepositoryIndex.getTripForId(id); } + /** + * TODO This only supports realtime cancelled trips for now. + */ + @Override + public List listCanceledTrips() { + OTPRequestTimeoutException.checkForTimeout(); + var timetableSnapshot = lazyGetTimeTableSnapShot(); + if (timetableSnapshot == null) { + return List.of(); + } + List canceledTrips = timetableSnapshot.listCanceledTrips(); + canceledTrips.sort(new TripOnServiceDateComparator()); + return canceledTrips; + } + @Override public Collection listTrips() { OTPRequestTimeoutException.checkForTimeout(); @@ -763,4 +779,32 @@ private static Stream sortByOccurrenceAndReduce(Stream input) { .sorted(Map.Entry.comparingByValue().reversed()) .map(Map.Entry::getKey); } + + private int getDepartureTime(TripOnServiceDate trip) { + var pattern = findPattern(trip.getTrip()); + var timetable = timetableSnapshot.resolve(pattern, trip.getServiceDate()); + return timetable.getTripTimes(trip.getTrip()).getDepartureTime(0); + } + + private class TripOnServiceDateComparator implements Comparator { + + @Override + public int compare(TripOnServiceDate t1, TripOnServiceDate t2) { + if (t1.getServiceDate().isBefore(t2.getServiceDate())) { + return -1; + } else if (t2.getServiceDate().isBefore(t1.getServiceDate())) { + return 1; + } + var departure1 = getDepartureTime(t1); + var departure2 = getDepartureTime(t2); + if (departure1 < departure2) { + return -1; + } else if (departure1 > departure2) { + return 1; + } else { + // identical departure day and time, so sort by unique feedscope id + return t1.getTrip().getId().compareTo(t2.getTrip().getId()); + } + } + } } 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 c1bba355cdf..11a628508d6 100644 --- a/application/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/application/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -156,6 +156,11 @@ public interface TransitService { */ Collection listTrips(); + /** + * List all canceled trips. + */ + List listCanceledTrips(); + /** * Return all routes, including those created by real-time updates. */ diff --git a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index a131b95fc8e..04fd2220283 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -73,6 +73,12 @@ interface PlaceInterface { "Entity related to an alert" union AlertEntity = Agency | Pattern | Route | RouteType | Stop | StopOnRoute | StopOnTrip | Trip | Unknown +"Scheduled times for a trip on a service date for a stop location." +union CallScheduledTime = ArrivalDepartureTime + +"Location where a transit vehicle stops at." +union CallStopLocation = Stop + "Rental place union that represents either a VehicleRentalStation or a RentalVehicle" union RentalPlace = RentalVehicle | VehicleRentalStation @@ -166,6 +172,14 @@ type Alert implements Node { trip: Trip @deprecated(reason : "Alert can have multiple affected entities now instead of there being duplicate alerts\nfor different entities. This will return only one of the affected trips.\nUse entities instead.") } +"Arrival and departure time (not relative to midnight)." +type ArrivalDepartureTime { + "Arrival time as an ISO-8601-formatted datetime." + arrival: OffsetDateTime + "Departure time as an ISO-8601-formatted datetime." + departure: OffsetDateTime +} + "Bike park represents a location where bicycles can be parked." type BikePark implements Node & PlaceInterface { "ID of the bike park" @@ -297,6 +311,20 @@ type BookingTime { time: String } +"Real-time estimates for arrival and departure times for a stop location." +type CallRealTime { + "Real-time estimates for the arrival." + arrival: EstimatedTime + "Real-time estimates for the departure." + departure: EstimatedTime +} + +"What is scheduled for a trip on a service date for a stop location." +type CallSchedule { + "Scheduled time for a trip on a service date for a stop location." + time: CallScheduledTime +} + "Car park represents a location where cars can be parked." type CarPark implements Node & PlaceInterface { "ID of the car park" @@ -456,6 +484,18 @@ type Emissions { co2: Grams } +"Real-time estimates for an arrival or departure at a certain place." +type EstimatedTime { + """ + The delay or "earliness" of the vehicle at a certain place. This estimate can change quite often. + + If the vehicle is early then this is a negative duration. + """ + delay: Duration! + "The estimate for a call event (such as arrival or departure) at a certain place. This estimate can change quite often." + time: OffsetDateTime! +} + "A 'medium' that a fare product applies to, for example cash, 'Oyster Card' or 'DB Navigator App'." type FareMedium { "ID of the medium" @@ -1130,31 +1170,60 @@ type QueryType { """ ids: [String] ): [BikeRentalStation] @deprecated(reason : "Use rentalVehicles or vehicleRentalStations instead") - "Get cancelled TripTimes." + """ + Get pages of canceled trips. Planned cancellations are not currently supported. Limiting the number of + returned trips with either `first` or `last` is highly recommended since the number of returned trips + can be really high when there is a strike affecting the transit services, for example. Follows the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + canceledTrips( + """ + This parameter is part of the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) + and should be used together with the `first` parameter. + """ + after: String, + """ + This parameter is part of the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) + and should be used together with the `last` parameter. + """ + before: String, + """ + Limits how many trips are returned. This parameter is part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) and can be used together with + the `after` parameter. + """ + first: Int, + """ + This parameter is part of the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) + and should be used together with the `before` parameter. + """ + last: Int + ): TripOnServiceDateConnection + "Get canceled TripTimes." cancelledTripTimes( "Feed feedIds (e.g. [\"HSL\"])." feeds: [String], """ - Only cancelled trip times that have last stop arrival time at maxArrivalTime + Only canceled trip times that have last stop arrival time at maxArrivalTime or before are returned. Format: seconds since midnight of maxDate. """ maxArrivalTime: Int, - "Only cancelled trip times scheduled to run on maxDate or before are returned. Format: \"2019-12-23\" or \"20191223\"." + "Only canceled trip times scheduled to run on maxDate or before are returned. Format: \"2019-12-23\" or \"20191223\"." maxDate: String, """ - Only cancelled trip times that have first stop departure time at + Only canceled trip times that have first stop departure time at maxDepartureTime or before are returned. Format: seconds since midnight of maxDate. """ maxDepartureTime: Int, """ - Only cancelled trip times that have last stop arrival time at minArrivalTime + Only canceled trip times that have last stop arrival time at minArrivalTime or after are returned. Format: seconds since midnight of minDate. """ minArrivalTime: Int, - "Only cancelled trip times scheduled to run on minDate or after are returned. Format: \"2019-12-23\" or \"20191223\"." + "Only canceled trip times scheduled to run on minDate or after are returned. Format: \"2019-12-23\" or \"20191223\"." minDate: String, """ - Only cancelled trip times that have first stop departure time at + Only canceled trip times that have first stop departure time at minDepartureTime or after are returned. Format: seconds since midnight of minDate. """ minDepartureTime: Int, @@ -1164,7 +1233,7 @@ type QueryType { routes: [String], "Trip gtfsIds (e.g. [\"HSL:1098_20190405_Ma_2_1455\"])." trips: [String] - ): [Stoptime] + ): [Stoptime] @deprecated(reason : "`cancelledTripTimes` is not implemented. Use `canceledTrips` instead.") "Get a single car park based on its ID, i.e. value of field `carParkId`" carPark(id: String!): CarPark @deprecated(reason : "carPark is deprecated. Use vehicleParking instead.") "Get all car parks" @@ -2159,6 +2228,16 @@ type Stop implements Node & PlaceInterface { zoneId: String } +"Stop call represents the time when a specific trip on a specific date arrives to and/or departs from a specific stop location." +type StopCall { + "Real-time estimates for arrival and departure times for this stop location." + realTime: CallRealTime + "Scheduled arrival and departure times for this stop location." + schedule: CallSchedule + "The stop where this arrival/departure happens." + stopLocation: CallStopLocation! +} + type StopGeometries { "Representation of the stop geometries as GeoJSON (https://geojson.org/)" geoJson: GeoJson @@ -2414,6 +2493,60 @@ type TripOccupancy { occupancyStatus: OccupancyStatus } +"A trip on a specific service date." +type TripOnServiceDate { + "Information related to trip's scheduled arrival to the final stop location. Can contain real-time information." + end: StopCall! + """ + The service date when the trip occurs. + + **Note**: A service date is a technical term useful for transit planning purposes and might not + correspond to a how a passenger thinks of a calendar date. For example, a night bus running + on Sunday morning at 1am to 3am, might have the previous Saturday's service date. + """ + serviceDate: LocalDate! + "Information related to trip's scheduled departure from the first stop location. Can contain real-time information." + start: StopCall! + "List of times when this trip arrives to or departs from a stop location and information related to the visit to the stop location." + stopCalls: [StopCall!]! + "This trip on service date is an instance of this trip." + trip: Trip +} + +""" +A connection to a list of trips on service dates that follows +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). +""" +type TripOnServiceDateConnection { + """ + Edges which contain the trips. Part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + edges: [TripOnServiceDateEdge] + """ + Contains cursors to fetch more pages of trips. + Part of the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + pageInfo: PageInfo! +} + +""" +An edge for TripOnServiceDate connection. Part of the +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). +""" +type TripOnServiceDateEdge { + """ + The cursor of the edge. Part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + cursor: String! + """ + Trip on a service date as a node. Part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + node: TripOnServiceDate +} + "This is used for alert entities that we don't explicitly handle or they are missing." type Unknown { "Entity's description" @@ -2831,7 +2964,7 @@ enum AlertSeverityLevelType { INFO """ Severe alerts are used when a significant part of public transport services is - affected, for example: All train services are cancelled due to technical problems. + affected, for example: All train services are canceled due to technical problems. """ SEVERE "Severity of alert is unknown" @@ -4335,21 +4468,21 @@ input TimetablePreferencesInput { When false, real-time updates are considered during the routing. In practice, when this option is set as true, some of the suggestions might not be realistic as the transfers could be invalid due to delays, - trips can be cancelled or stops can be skipped. + trips can be canceled or stops can be skipped. """ excludeRealTimeUpdates: Boolean """ - When true, departures that have been cancelled ahead of time will be + When true, departures that have been canceled ahead of time will be included during the routing. This means that an itinerary can include - a cancelled departure while some other alternative that contains no cancellations + a canceled departure while some other alternative that contains no cancellations could be filtered out as the alternative containing a cancellation would normally be better. """ includePlannedCancellations: Boolean """ - When true, departures that have been cancelled through a real-time feed will be + When true, departures that have been canceled through a real-time feed will be included during the routing. This means that an itinerary can include - a cancelled departure while some other alternative that contains no cancellations + a canceled departure while some other alternative that contains no cancellations could be filtered out as the alternative containing a cancellation would normally be better. This option can't be set to true while `includeRealTimeUpdates` is false. """ 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 12e68c2d453..7e1bf24287a 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; +import static org.opentripplanner._support.time.ZoneIds.BERLIN; import static org.opentripplanner.model.plan.PlanTestConstants.D10m; import static org.opentripplanner.model.plan.PlanTestConstants.T11_00; import static org.opentripplanner.model.plan.PlanTestConstants.T11_01; @@ -22,6 +23,7 @@ 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.temporal.ChronoUnit; import java.util.Arrays; @@ -35,14 +37,17 @@ import org.junit.jupiter.params.ParameterizedTest; import org.locationtech.jts.geom.Coordinate; import org.opentripplanner._support.text.I18NStrings; -import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.ext.fares.FaresToItineraryMapper; import org.opentripplanner.ext.fares.impl.DefaultFareService; import org.opentripplanner.framework.geometry.WgsCoordinate; 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.RealTimeTripUpdate; +import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.model.calendar.CalendarServiceData; import org.opentripplanner.model.fare.FareMedium; import org.opentripplanner.model.fare.FareProduct; import org.opentripplanner.model.fare.ItineraryFares; @@ -164,15 +169,26 @@ static void setup() { var model = siteRepository.build(); var timetableRepository = new TimetableRepository(model, DEDUPLICATOR); + var cal_id = TimetableRepositoryForTest.id("CAL_1"); var trip = TimetableRepositoryForTest .trip("123") .withHeadsign(I18NString.of("Trip Headsign")) + .withServiceId(cal_id) .build(); var stopTimes = TEST_MODEL.stopTimesEvery5Minutes(3, trip, "11:00"); var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, DEDUPLICATOR); + var trip2 = TimetableRepositoryForTest + .trip("321Canceled") + .withHeadsign(I18NString.of("Trip Headsign")) + .withServiceId(cal_id) + .build(); + var stopTimes2 = TEST_MODEL.stopTimesEvery5Minutes(3, trip2, "11:30"); + var tripTimes2 = TripTimesFactory.tripTimes(trip2, stopTimes2, DEDUPLICATOR); final TripPattern pattern = TEST_MODEL .pattern(BUS) - .withScheduledTimeTableBuilder(builder -> builder.addTripTimes(tripTimes)) + .withScheduledTimeTableBuilder(builder -> + builder.addTripTimes(tripTimes).addTripTimes(tripTimes2) + ) .build(); timetableRepository.addTripPattern(id("pattern-1"), pattern); @@ -189,7 +205,7 @@ static void setup() { .build(); timetableRepository.addAgency(agency); - timetableRepository.initTimeZone(ZoneIds.BERLIN); + timetableRepository.initTimeZone(BERLIN); timetableRepository.index(); var routes = Arrays .stream(TransitMode.values()) @@ -228,6 +244,24 @@ public Set findRoutes(StopLocation stop) { }; routes.forEach(transitService::addRoutes); + // Crate a calendar (needed for testing cancelled trips) + CalendarServiceData calendarServiceData = new CalendarServiceData(); + var firstDate = LocalDate.of(2024, 8, 8); + var secondDate = LocalDate.of(2024, 8, 9); + calendarServiceData.putServiceDatesForServiceId(cal_id, List.of(firstDate, secondDate)); + timetableRepository.getServiceCodes().put(cal_id, 0); + timetableRepository.updateCalendarServiceData( + true, + calendarServiceData, + DataImportIssueStore.NOOP + ); + TimetableSnapshot timetableSnapshot = new TimetableSnapshot(); + tripTimes2.cancelTrip(); + timetableSnapshot.update(new RealTimeTripUpdate(pattern, tripTimes2, secondDate)); + + var snapshot = timetableSnapshot.commit(); + timetableRepository.initTimetableSnapshotProvider(() -> snapshot); + var step1 = walkStep("street") .withRelativeDirection(RelativeDirection.DEPART) .withAbsoluteDirection(20) diff --git a/application/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java b/application/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java index 7fc2b577fd2..f85c8bea943 100644 --- a/application/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java +++ b/application/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java @@ -1,6 +1,8 @@ package org.opentripplanner.model; 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 java.time.LocalTime; import org.junit.jupiter.api.Test; @@ -29,4 +31,23 @@ void gtfsSequence() { var departure = LocalTime.ofSecondOfDay(subject.getScheduledDeparture()); assertEquals(LocalTime.of(11, 10), departure); } + + @Test + void isRecordedStop() { + var testModel = TimetableRepositoryForTest.of(); + var pattern = testModel.pattern(TransitMode.BUS).build(); + var trip = TimetableRepositoryForTest.trip("123").build(); + var stopTimes = testModel.stopTimesEvery5Minutes(3, trip, "11:00"); + + var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); + tripTimes.setRecorded(1); + + var subject = new TripTimeOnDate(tripTimes, 0, pattern); + + assertFalse(subject.isRecordedStop()); + + subject = new TripTimeOnDate(tripTimes, 1, pattern); + + assertTrue(subject.isRecordedStop()); + } } diff --git a/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java b/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java index 89eb13c56af..6980a488d7a 100644 --- a/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java +++ b/application/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java @@ -1,8 +1,10 @@ package org.opentripplanner.model.plan; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; import java.time.OffsetDateTime; @@ -49,6 +51,7 @@ void legTimesWithoutRealTime() { assertNull(leg.start().estimated()); assertNull(leg.end().estimated()); + assertFalse(leg.isRealTimeUpdated()); } @Test @@ -67,6 +70,7 @@ void legTimesWithRealTime() { assertNotNull(leg.start().estimated()); assertNotNull(leg.end().estimated()); + assertTrue(leg.isRealTimeUpdated()); } private static ScheduledTransitLegBuilder builder() { diff --git a/application/src/test/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimesTest.java b/application/src/test/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimesTest.java index 29da6582d8f..93e4c107b00 100644 --- a/application/src/test/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimesTest.java +++ b/application/src/test/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimesTest.java @@ -402,6 +402,17 @@ public void testNoData() { assertFalse(updatedTripTimesA.isNoDataStop(2)); } + @Test + public void testRealTimeUpdated() { + RealTimeTripTimes updatedTripTimesA = createInitialTripTimes().copyScheduledTimes(); + assertFalse(updatedTripTimesA.isRealTimeUpdated(1)); + updatedTripTimesA.setRealTimeState(RealTimeState.UPDATED); + assertTrue(updatedTripTimesA.isRealTimeUpdated(1)); + updatedTripTimesA.setNoData(1); + assertTrue(updatedTripTimesA.isRealTimeUpdated(0)); + assertFalse(updatedTripTimesA.isRealTimeUpdated(1)); + } + @Nested class GtfsStopSequence { 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 c3086bcc61b..ab644227cf2 100644 --- a/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java +++ b/application/src/test/java/org/opentripplanner/transit/service/DefaultTransitServiceTest.java @@ -13,8 +13,11 @@ import java.util.Set; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; import org.opentripplanner.model.RealTimeTripUpdate; import org.opentripplanner.model.TimetableSnapshot; +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,6 +28,8 @@ 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.transit.model.timetable.TripTimesFactory; class DefaultTransitServiceTest { @@ -52,6 +57,13 @@ class DefaultTransitServiceTest { .withCreatedByRealtimeUpdater(true) .build(); + static FeedScopedId CALENDAR_ID = TimetableRepositoryForTest.id("CAL_1"); + static Trip TRIP = TimetableRepositoryForTest + .trip("123") + .withHeadsign(I18NString.of("Trip Headsign")) + .withServiceId(CALENDAR_ID) + .build(); + @BeforeAll static void setup() { var siteRepository = TEST_MODEL @@ -61,25 +73,40 @@ static void setup() { .withStation(STATION) .build(); + var deduplicator = new Deduplicator(); + var transitModel = new TimetableRepository(siteRepository, deduplicator); + var canceledStopTimes = TEST_MODEL.stopTimesEvery5Minutes(3, TRIP, "11:30"); + var canceledTripTimes = TripTimesFactory.tripTimes(TRIP, canceledStopTimes, deduplicator); + canceledTripTimes.cancelTrip(); + transitModel.addTripPattern(RAIL_PATTERN.getId(), RAIL_PATTERN); + + // Crate a calendar (needed for testing cancelled trips) + CalendarServiceData calendarServiceData = new CalendarServiceData(); + var firstDate = LocalDate.of(2024, 8, 8); + var secondDate = LocalDate.of(2024, 8, 9); + calendarServiceData.putServiceDatesForServiceId(CALENDAR_ID, List.of(firstDate, secondDate)); + transitModel.getServiceCodes().put(CALENDAR_ID, 0); + transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP); + + transitModel.index(); var timetableRepository = new TimetableRepository(siteRepository, new Deduplicator()); 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, tripTimes, LocalDate.now()) - ); - - return timetableSnapshot.commit(); - }); + 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, tripTimes, firstDate)); + timetableSnapshot.update(new RealTimeTripUpdate(RAIL_PATTERN, canceledTripTimes, firstDate)); + timetableSnapshot.update(new RealTimeTripUpdate(RAIL_PATTERN, canceledTripTimes, secondDate)); + + var snapshot = timetableSnapshot.commit(); + timetableRepository.initTimetableSnapshotProvider(() -> snapshot); service = new DefaultTransitService(timetableRepository) { @@ -124,6 +151,12 @@ void getPatternForStopsWithRealTime() { assertEquals(Set.of(FERRY_PATTERN, RAIL_PATTERN, REAL_TIME_PATTERN), patternsForStop); } + @Test + void listCanceledTrips() { + var canceledTrips = service.listCanceledTrips(); + assertEquals("[TripOnServiceDate{F:123}, TripOnServiceDate{F:123}]", canceledTrips.toString()); + } + @Test void containsTrip() { assertFalse(service.containsTrip(new FeedScopedId("x", "x"))); diff --git a/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CanceledTripTest.java b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CanceledTripTest.java new file mode 100644 index 00000000000..75c8d5caa46 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/updater/trip/moduletests/cancellation/CanceledTripTest.java @@ -0,0 +1,41 @@ +package org.opentripplanner.updater.trip.moduletests.cancellation; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship.CANCELED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id; +import static org.opentripplanner.updater.spi.UpdateResultAssertions.assertSuccess; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.updater.trip.RealtimeTestConstants; +import org.opentripplanner.updater.trip.RealtimeTestEnvironment; +import org.opentripplanner.updater.trip.TripInput; +import org.opentripplanner.updater.trip.TripUpdateBuilder; + +public class CanceledTripTest implements RealtimeTestConstants { + + @Test + void listCanceledTrips() { + var env = RealtimeTestEnvironment + .gtfs() + .addTrip( + TripInput + .of(TRIP_1_ID) + .addStop(STOP_A1, "0:00:10", "0:00:11") + .addStop(STOP_B1, "0:00:20", "0:00:21") + .build() + ) + .build(); + + assertThat(env.getTransitService().listCanceledTrips()).isEmpty(); + + var update = new TripUpdateBuilder(TRIP_1_ID, SERVICE_DATE, CANCELED, TIME_ZONE).build(); + assertSuccess(env.applyTripUpdate(update)); + + var canceled = env.getTransitService().listCanceledTrips(); + assertThat(canceled).hasSize(1); + var trip = canceled.getFirst(); + assertEquals(id(TRIP_1_ID), trip.getTrip().getId()); + assertEquals(SERVICE_DATE, trip.getServiceDate()); + } +} diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/canceled-trips.json b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/canceled-trips.json new file mode 100644 index 00000000000..6b9425875f3 --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/expectations/canceled-trips.json @@ -0,0 +1,119 @@ +{ + "data": { + "canceledTrips": { + "pageInfo": { + "hasNextPage": false, + "hasPreviousPage": false, + "startCursor": "c2ltcGxlLWN1cnNvcjA=", + "endCursor": "c2ltcGxlLWN1cnNvcjA=" + }, + "edges": [ + { + "node": { + "serviceDate": "2024-08-09", + "end": { + "schedule": { + "time": { + "arrival": "2024-08-09T11:40:00+02:00" + } + }, + "realTime": { + "arrival": { + "delay": "PT0S", + "time": "2024-08-09T11:40:00+02:00" + } + }, + "stopLocation": { + "gtfsId": "F:Stop_2" + } + }, + "start": { + "schedule": { + "time": { + "departure": "2024-08-09T11:30:00+02:00" + } + }, + "realTime": { + "departure": { + "delay": "PT0S", + "time": "2024-08-09T11:30:00+02:00" + } + }, + "stopLocation": { + "gtfsId": "F:Stop_0" + } + }, + "stopCalls": [ + { + "schedule": { + "time": { + "arrival": "2024-08-09T11:30:00+02:00", + "departure": "2024-08-09T11:30:00+02:00" + } + }, + "realTime": { + "arrival": { + "delay": "PT0S", + "time": "2024-08-09T11:30:00+02:00" + }, + "departure": { + "delay": "PT0S", + "time": "2024-08-09T11:30:00+02:00" + } + }, + "stopLocation": { + "gtfsId": "F:Stop_0" + } + }, + { + "schedule": { + "time": { + "arrival": "2024-08-09T11:35:00+02:00", + "departure": "2024-08-09T11:35:00+02:00" + } + }, + "realTime": { + "arrival": { + "delay": "PT0S", + "time": "2024-08-09T11:35:00+02:00" + }, + "departure": { + "delay": "PT0S", + "time": "2024-08-09T11:35:00+02:00" + } + }, + "stopLocation": { + "gtfsId": "F:Stop_1" + } + }, + { + "schedule": { + "time": { + "arrival": "2024-08-09T11:40:00+02:00", + "departure": "2024-08-09T11:40:00+02:00" + } + }, + "realTime": { + "arrival": { + "delay": "PT0S", + "time": "2024-08-09T11:40:00+02:00" + }, + "departure": { + "delay": "PT0S", + "time": "2024-08-09T11:40:00+02:00" + } + }, + "stopLocation": { + "gtfsId": "F:Stop_2" + } + } + ], + "trip": { + "gtfsId": "F:321Canceled" + } + } + } + ] + } + } +} 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 73ee8b0495e..4aac9295cc9 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:321Canceled", + "stoptimes": [ + { + "stop": { + "gtfsId": "F:Stop_0", + "name": "Stop_0" + }, + "headsign": "Stop headsign at stop 10", + "scheduledArrival": 41400, + "scheduledDeparture": 41400, + "stopPosition": 10, + "stopPositionInPattern": 0, + "realtimeState": "CANCELED", + "pickupType": null, + "dropoffType": null + }, + { + "stop": { + "gtfsId": "F:Stop_1", + "name": "Stop_1" + }, + "headsign": "Stop headsign at stop 20", + "scheduledArrival": 41700, + "scheduledDeparture": 41700, + "stopPosition": 20, + "stopPositionInPattern": 1, + "realtimeState": "CANCELED", + "pickupType": null, + "dropoffType": null + }, + { + "stop": { + "gtfsId": "F:Stop_2", + "name": "Stop_2" + }, + "headsign": "Stop headsign at stop 30", + "scheduledArrival": 42000, + "scheduledDeparture": 42000, + "stopPosition": 30, + "stopPositionInPattern": 2, + "realtimeState": "CANCELED", + "pickupType": null, + "dropoffType": null + } + ], + "occupancy": { + "occupancyStatus": "NO_DATA_AVAILABLE" + } } ], "vehiclePositions": [ diff --git a/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/canceled-trips.graphql b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/canceled-trips.graphql new file mode 100644 index 00000000000..988a1e62cd9 --- /dev/null +++ b/application/src/test/resources/org/opentripplanner/apis/gtfs/queries/canceled-trips.graphql @@ -0,0 +1,83 @@ +{ + canceledTrips(first: 2) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + node { + serviceDate + end { + schedule { + time { + ... on ArrivalDepartureTime { + arrival + } + } + } + realTime { + arrival { + delay + time + } + } + stopLocation { + ... on Stop { + gtfsId + } + } + } + start { + schedule { + time { + ... on ArrivalDepartureTime { + departure + } + } + } + realTime { + departure { + delay + time + } + } + stopLocation { + ... on Stop { + gtfsId + } + } + } + stopCalls { + schedule { + time { + ... on ArrivalDepartureTime { + arrival + departure + } + } + } + realTime { + arrival { + delay + time + } + departure { + delay + time + } + } + stopLocation { + ... on Stop { + gtfsId + } + } + } + trip { + gtfsId + } + } + } + } +}