diff --git a/docs/apis/GraphQL-Tutorial.md b/docs/apis/GraphQL-Tutorial.md index 8df6d6f38ee..92645f9b245 100644 --- a/docs/apis/GraphQL-Tutorial.md +++ b/docs/apis/GraphQL-Tutorial.md @@ -88,25 +88,33 @@ Most people want to get routing results out of OTP, so lets see the query for th }, ]) { itineraries { - startTime - endTime + start + end legs { mode - startTime - endTime from { name lat lon - departureTime - arrivalTime + departure { + scheduledTime + estimated { + time + delay + } + } } to { name lat lon - departureTime - arrivalTime + arrival { + scheduledTime + estimated { + time + delay + } + } } route { gtfsId diff --git a/magidoc.mjs b/magidoc.mjs index a02976f4bcc..a57b17a4308 100644 --- a/magidoc.mjs +++ b/magidoc.mjs @@ -35,7 +35,8 @@ To learn how to deactivate it, read the appTitle: 'OTP GTFS GraphQL API', queryGenerationFactories: { 'Polyline': '<>', - 'GeoJson': '<>' + 'GeoJson': '<>', + 'OffsetDateTime': '2024-02-05T18:04:23+01:00' }, } }, diff --git a/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java b/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java index 0e9c10de7eb..9d10b87bbd6 100644 --- a/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java +++ b/src/ext/java/org/opentripplanner/ext/fares/impl/CombinedInterlinedTransitLeg.java @@ -10,6 +10,7 @@ import org.opentripplanner.framework.collection.ListUtils; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.Leg; +import org.opentripplanner.model.plan.LegTime; import org.opentripplanner.model.plan.Place; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.TransitLeg; @@ -56,6 +57,16 @@ public Trip getTrip() { return first.getTrip(); } + @Override + public LegTime start() { + return first.start(); + } + + @Override + public LegTime end() { + return second.end(); + } + @Override public ZonedDateTime getStartTime() { return first.getStartTime(); diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java b/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java index 0e83372175a..fac1118556f 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java @@ -16,6 +16,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.Place; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.TransitLeg; @@ -84,6 +85,16 @@ public Accessibility getTripWheelchairAccessibility() { return edge.getFlexTrip().getTrip().getWheelchairBoarding(); } + @Override + public LegTime start() { + return LegTime.ofStatic(startTime); + } + + @Override + public LegTime end() { + return LegTime.ofStatic(endTime); + } + @Override @Nonnull public TransitMode getMode() { diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/PlaceMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/PlaceMapper.java index b1eb3410af8..28ceb0b84e3 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/PlaceMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/PlaceMapper.java @@ -39,8 +39,8 @@ public List mapStopArrivals(Collection domain) { public ApiPlace mapStopArrival(StopArrival domain) { return mapPlace( domain.place, - domain.arrival, - domain.departure, + domain.arrival.time(), + domain.departure.time(), domain.stopPosInPattern, domain.gtfsStopSequence ); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java index 6280ac28ac2..42ffe992539 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java @@ -10,18 +10,24 @@ import graphql.schema.CoercingParseValueException; import graphql.schema.CoercingSerializeException; import graphql.schema.GraphQLScalarType; +import java.text.ParseException; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import javax.annotation.Nonnull; import org.locationtech.jts.geom.Geometry; import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory; import org.opentripplanner.framework.model.Grams; +import org.opentripplanner.framework.time.OffsetDateTimeParser; public class GraphQLScalars { private static final ObjectMapper geoJsonMapper = new ObjectMapper() .registerModule(new JtsModule(GeometryUtils.getGeometryFactory())); - public static GraphQLScalarType durationScalar = DurationScalarFactory.createDurationScalar(); + public static GraphQLScalarType DURATION_SCALAR = DurationScalarFactory.createDurationScalar(); - public static GraphQLScalarType polylineScalar = GraphQLScalarType + public static final GraphQLScalarType POLYLINE_SCALAR = GraphQLScalarType .newScalar() .name("Polyline") .description( @@ -50,7 +56,63 @@ public String parseLiteral(Object input) { ) .build(); - public static GraphQLScalarType geoJsonScalar = GraphQLScalarType + public static final GraphQLScalarType OFFSET_DATETIME_SCALAR = GraphQLScalarType + .newScalar() + .name("OffsetDateTime") + .coercing( + new Coercing() { + @Override + public String serialize(@Nonnull Object dataFetcherResult) + throws CoercingSerializeException { + if (dataFetcherResult instanceof ZonedDateTime zdt) { + return zdt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } else if (dataFetcherResult instanceof OffsetDateTime odt) { + return odt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } else { + throw new CoercingSerializeException( + "Cannot serialize object of class %s".formatted( + dataFetcherResult.getClass().getSimpleName() + ) + ); + } + } + + @Override + public OffsetDateTime parseValue(Object input) throws CoercingParseValueException { + if (input instanceof CharSequence cs) { + try { + return OffsetDateTimeParser.parseLeniently(cs); + } catch (ParseException e) { + int errorOffset = e.getErrorOffset(); + throw new CoercingParseValueException( + "Cannot parse %s into an OffsetDateTime. Error at character index %s".formatted( + input, + errorOffset + ) + ); + } + } + throw new CoercingParseValueException( + "Cannot parse %s into an OffsetDateTime. Must be a string." + ); + } + + @Override + public OffsetDateTime parseLiteral(Object input) throws CoercingParseLiteralException { + if (input instanceof StringValue sv) { + try { + return OffsetDateTimeParser.parseLeniently(sv.getValue()); + } catch (ParseException e) { + throw new CoercingSerializeException(); + } + } + throw new CoercingParseLiteralException(); + } + } + ) + .build(); + + public static final GraphQLScalarType GEOJSON_SCALAR = GraphQLScalarType .newScalar() .name("GeoJson") .description("Geographic data structures in JSON format. See: https://geojson.org/") @@ -78,7 +140,7 @@ public Geometry parseLiteral(Object input) throws CoercingParseLiteralException ) .build(); - public static GraphQLScalarType graphQLIDScalar = GraphQLScalarType + public static final GraphQLScalarType GRAPHQL_ID_SCALAR = GraphQLScalarType .newScalar() .name("ID") .coercing( @@ -118,7 +180,7 @@ public Relay.ResolvedGlobalId parseLiteral(Object input) ) .build(); - public static GraphQLScalarType gramsScalar = GraphQLScalarType + public static final GraphQLScalarType GRAMS_SCALAR = GraphQLScalarType .newScalar() .name("Grams") .coercing( diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index ff3e8681ce8..1fd78765a07 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -106,11 +106,12 @@ protected static GraphQLSchema buildSchema() { IntrospectionTypeWiring typeWiring = new IntrospectionTypeWiring(typeRegistry); RuntimeWiring runtimeWiring = RuntimeWiring .newRuntimeWiring() - .scalar(GraphQLScalars.durationScalar) - .scalar(GraphQLScalars.polylineScalar) - .scalar(GraphQLScalars.geoJsonScalar) - .scalar(GraphQLScalars.graphQLIDScalar) - .scalar(GraphQLScalars.gramsScalar) + .scalar(GraphQLScalars.DURATION_SCALAR) + .scalar(GraphQLScalars.POLYLINE_SCALAR) + .scalar(GraphQLScalars.GEOJSON_SCALAR) + .scalar(GraphQLScalars.GRAPHQL_ID_SCALAR) + .scalar(GraphQLScalars.GRAMS_SCALAR) + .scalar(GraphQLScalars.OFFSET_DATETIME_SCALAR) .scalar(ExtendedScalars.GraphQLLong) .type("Node", type -> type.typeResolver(new NodeTypeResolver())) .type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver())) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/ItineraryImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/ItineraryImpl.java index c7ae82a2355..20d5a9c348d 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/ItineraryImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/ItineraryImpl.java @@ -2,6 +2,7 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import java.time.OffsetDateTime; import java.util.List; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.mapping.NumberMapper; @@ -32,6 +33,12 @@ public DataFetcher elevationLost() { return environment -> getSource(environment).getElevationLost(); } + @Override + public DataFetcher end() { + return environment -> getSource(environment).endTime().toOffsetDateTime(); + } + + @Deprecated @Override public DataFetcher endTime() { return environment -> getSource(environment).endTime().toInstant().toEpochMilli(); @@ -57,6 +64,12 @@ public DataFetcher numberOfTransfers() { return environment -> getSource(environment).getNumberOfTransfers(); } + @Override + public DataFetcher start() { + return environment -> getSource(environment).startTime().toOffsetDateTime(); + } + + @Deprecated @Override public DataFetcher startTime() { return environment -> getSource(environment).startTime().toInstant().toEpochMilli(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java index 639cb95bf28..975d8d56831 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java @@ -18,6 +18,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.ScheduledTransitLeg; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.StreetLeg; @@ -78,6 +79,12 @@ public DataFetcher duration() { } @Override + public DataFetcher end() { + return environment -> getSource(environment).end(); + } + + @Override + @Deprecated public DataFetcher endTime() { return environment -> getSource(environment).getEndTime().toInstant().toEpochMilli(); } @@ -93,8 +100,8 @@ public DataFetcher from() { Leg source = getSource(environment); return new StopArrival( source.getFrom(), - source.getStartTime(), - source.getStartTime(), + source.start(), + source.start(), source.getBoardStopPosInPattern(), source.getBoardingGtfsStopSequence() ); @@ -216,6 +223,12 @@ public DataFetcher serviceDate() { } @Override + public DataFetcher start() { + return environment -> getSource(environment).start(); + } + + @Override + @Deprecated public DataFetcher startTime() { return environment -> getSource(environment).getStartTime().toInstant().toEpochMilli(); } @@ -231,8 +244,8 @@ public DataFetcher to() { Leg source = getSource(environment); return new StopArrival( source.getTo(), - source.getEndTime(), - source.getEndTime(), + source.end(), + source.end(), source.getAlightStopPosInPattern(), source.getAlightGtfsStopSequence() ); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java index ce3ca0986b1..145321f809c 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlaceImpl.java @@ -7,6 +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.Place; import org.opentripplanner.model.plan.StopArrival; import org.opentripplanner.model.plan.VertexType; @@ -17,9 +18,18 @@ public class PlaceImpl implements GraphQLDataFetchers.GraphQLPlace { + @Override + public DataFetcher arrival() { + return environment -> getSource(environment).arrival; + } + + @Deprecated @Override public DataFetcher arrivalTime() { - return environment -> getSource(environment).arrival.toInstant().toEpochMilli(); + return environment -> { + StopArrival stopArrival = getSource(environment); + return stopArrival.arrival.time().toInstant().toEpochMilli(); + }; } @Override @@ -30,7 +40,8 @@ public DataFetcher bikePark() { @Override public DataFetcher bikeRentalStation() { return environment -> { - Place place = getSource(environment).place; + StopArrival stopArrival = getSource(environment); + Place place = stopArrival.place; if (!place.vertexType.equals(VertexType.VEHICLERENTAL)) { return null; @@ -45,31 +56,49 @@ public DataFetcher carPark() { return this::getCarPark; } + @Deprecated + @Override + public DataFetcher departure() { + return environment -> getSource(environment).departure; + } + @Override public DataFetcher departureTime() { - return environment -> getSource(environment).departure.toInstant().toEpochMilli(); + return environment -> { + StopArrival stopArrival = getSource(environment); + return stopArrival.departure.time().toInstant().toEpochMilli(); + }; } @Override public DataFetcher lat() { - return environment -> getSource(environment).place.coordinate.latitude(); + return environment -> { + StopArrival stopArrival = getSource(environment); + return stopArrival.place.coordinate.latitude(); + }; } @Override public DataFetcher lon() { - return environment -> getSource(environment).place.coordinate.longitude(); + return environment -> { + StopArrival stopArrival = getSource(environment); + return stopArrival.place.coordinate.longitude(); + }; } @Override public DataFetcher name() { - return environment -> - GraphQLUtils.getTranslation(getSource(environment).place.name, environment); + return environment -> { + StopArrival stopArrival = getSource(environment); + return GraphQLUtils.getTranslation(stopArrival.place.name, environment); + }; } @Override public DataFetcher rentalVehicle() { return environment -> { - Place place = getSource(environment).place; + StopArrival stopArrival = getSource(environment); + Place place = stopArrival.place; if ( !place.vertexType.equals(VertexType.VEHICLERENTAL) || @@ -84,13 +113,17 @@ public DataFetcher rentalVehicle() { @Override public DataFetcher stop() { - return environment -> getSource(environment).place.stop; + return environment -> { + StopArrival stopArrival = getSource(environment); + return stopArrival.place.stop; + }; } @Override public DataFetcher stopPosition() { return environment -> { - var seq = getSource(environment).gtfsStopSequence; + StopArrival stopArrival = getSource(environment); + var seq = stopArrival.gtfsStopSequence; if (seq != null) { return new PositionAtStop(seq); } else { @@ -107,7 +140,8 @@ public DataFetcher vehicleParking() { @Override public DataFetcher vehicleRentalStation() { return environment -> { - Place place = getSource(environment).place; + StopArrival stopArrival = getSource(environment); + Place place = stopArrival.place; if ( !place.vertexType.equals(VertexType.VEHICLERENTAL) || @@ -123,7 +157,8 @@ public DataFetcher vehicleRentalStation() { @Override public DataFetcher vertexType() { return environment -> { - var place = getSource(environment).place; + StopArrival stopArrival = getSource(environment); + var place = stopArrival.place; return switch (place.vertexType) { case NORMAL -> GraphQLVertexType.NORMAL.name(); case TRANSIT -> GraphQLVertexType.TRANSIT.name(); @@ -134,7 +169,8 @@ public DataFetcher vertexType() { } private VehicleParking getBikePark(DataFetchingEnvironment environment) { - var vehicleParkingWithEntrance = getSource(environment).place.vehicleParkingWithEntrance; + StopArrival stopArrival = getSource(environment); + var vehicleParkingWithEntrance = stopArrival.place.vehicleParkingWithEntrance; if ( vehicleParkingWithEntrance == null || !vehicleParkingWithEntrance.getVehicleParking().hasBicyclePlaces() @@ -146,7 +182,8 @@ private VehicleParking getBikePark(DataFetchingEnvironment environment) { } private VehicleParking getCarPark(DataFetchingEnvironment environment) { - var vehicleParkingWithEntrance = getSource(environment).place.vehicleParkingWithEntrance; + StopArrival stopArrival = getSource(environment); + var vehicleParkingWithEntrance = stopArrival.place.vehicleParkingWithEntrance; if ( vehicleParkingWithEntrance == null || !vehicleParkingWithEntrance.getVehicleParking().hasAnyCarPlaces() @@ -158,7 +195,8 @@ private VehicleParking getCarPark(DataFetchingEnvironment environment) { } private VehicleParking getVehicleParking(DataFetchingEnvironment environment) { - var vehicleParkingWithEntrance = getSource(environment).place.vehicleParkingWithEntrance; + StopArrival stopArrival = getSource(environment); + var vehicleParkingWithEntrance = stopArrival.place.vehicleParkingWithEntrance; if (vehicleParkingWithEntrance == null) { return null; } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 2f39f7f4030..7fab5720b9a 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -36,6 +36,7 @@ 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.StopArrival; import org.opentripplanner.model.plan.WalkStep; import org.opentripplanner.routing.alertpatch.TransitAlert; @@ -400,6 +401,8 @@ public interface GraphQLItinerary { public DataFetcher emissionsPerPerson(); + public DataFetcher end(); + public DataFetcher endTime(); public DataFetcher> fares(); @@ -410,6 +413,8 @@ public interface GraphQLItinerary { public DataFetcher numberOfTransfers(); + public DataFetcher start(); + public DataFetcher startTime(); public DataFetcher> systemNotices(); @@ -440,6 +445,8 @@ public interface GraphQLLeg { public DataFetcher duration(); + public DataFetcher end(); + public DataFetcher endTime(); public DataFetcher> fareProducts(); @@ -480,6 +487,8 @@ public interface GraphQLLeg { public DataFetcher serviceDate(); + public DataFetcher start(); + public DataFetcher startTime(); public DataFetcher> steps(); @@ -493,6 +502,16 @@ public interface GraphQLLeg { public DataFetcher walkingBike(); } + /** + * Time information about a passenger at a certain place. May contain real-time information if + * available. + */ + public interface GraphQLLegTime { + public DataFetcher estimated(); + + public DataFetcher scheduledTime(); + } + /** A span of time. */ public interface GraphQLLocalTimeSpan { public DataFetcher from(); @@ -576,6 +595,8 @@ public interface GraphQLPattern { } public interface GraphQLPlace { + public DataFetcher arrival(); + public DataFetcher arrivalTime(); public DataFetcher bikePark(); @@ -584,6 +605,8 @@ public interface GraphQLPlace { public DataFetcher carPark(); + public DataFetcher departure(); + public DataFetcher departureTime(); public DataFetcher lat(); @@ -740,6 +763,12 @@ public interface GraphQLQueryType { public DataFetcher viewer(); } + public interface GraphQLRealtimeEstimate { + public DataFetcher delay(); + + public DataFetcher time(); + } + /** Rental vehicle represents a vehicle that belongs to a rental network. */ public interface GraphQLRentalVehicle { public DataFetcher allowPickupNow(); @@ -1235,6 +1264,10 @@ public interface GraphQLElevationProfileComponent { public DataFetcher elevation(); } + /** + * This type is only here for backwards-compatibility and this API will never return it anymore. + * Please use the leg's `fareProducts` instead. + */ public interface GraphQLFare { public DataFetcher cents(); @@ -1245,7 +1278,10 @@ public interface GraphQLFare { public DataFetcher type(); } - /** Component of the fare (i.e. ticket) for a part of the itinerary */ + /** + * This type is only here for backwards-compatibility and this API will never return it anymore. + * Please use the leg's `fareProducts` instead. + */ public interface GraphQLFareComponent { public DataFetcher cents(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index c141266d2e6..55f76863cc6 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -26,6 +26,7 @@ config: Polyline: String GeoJson: org.locationtech.jts.geom.Geometry Grams: org.opentripplanner.framework.model.Grams + OffsetDateTime: java.time.OffsetDateTime Duration: java.time.Duration mappers: AbsoluteDirection: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLAbsoluteDirection#GraphQLAbsoluteDirection @@ -59,6 +60,7 @@ 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 Mode: String OccupancyStatus: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLOccupancyStatus#GraphQLOccupancyStatus TransitMode: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode#GraphQLTransitMode diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/scalars/DateTimeScalarFactory.java b/src/main/java/org/opentripplanner/apis/transmodel/model/scalars/DateTimeScalarFactory.java index 706adc81704..9738a277a41 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/scalars/DateTimeScalarFactory.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/scalars/DateTimeScalarFactory.java @@ -10,10 +10,10 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; import javax.annotation.Nonnull; +import org.opentripplanner.framework.time.OffsetDateTimeParser; public final class DateTimeScalarFactory { @@ -25,22 +25,7 @@ public final class DateTimeScalarFactory { Example: `2017-04-23T18:25:43+02:00` or `2017-04-23T16:25:43Z`"""; - // We need to have two offsets, in order to parse both "+0200" and "+02:00". The first is not - // really ISO-8601 compatible with the extended date and time. We need to make parsing strict, in - // order to keep the minute mandatory, otherwise we would be left with an unparsed minute - private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder() - .parseCaseInsensitive() - .parseLenient() - .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - .optionalStart() - .parseStrict() - .appendOffset("+HH:MM:ss", "Z") - .parseLenient() - .optionalEnd() - .optionalStart() - .appendOffset("+HHmmss", "Z") - .optionalEnd() - .toFormatter(); + private static final DateTimeFormatter PARSER = OffsetDateTimeParser.LENIENT_PARSER; private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; diff --git a/src/main/java/org/opentripplanner/framework/graphql/scalar/DurationScalarFactory.java b/src/main/java/org/opentripplanner/framework/graphql/scalar/DurationScalarFactory.java index 30dd3754198..50af0fbc759 100644 --- a/src/main/java/org/opentripplanner/framework/graphql/scalar/DurationScalarFactory.java +++ b/src/main/java/org/opentripplanner/framework/graphql/scalar/DurationScalarFactory.java @@ -33,7 +33,7 @@ private static class DurationCoercing implements Coercing { @Nonnull public String serialize(@Nonnull Object input) throws CoercingSerializeException { if (input instanceof Duration duration) { - return duration.toString(); + return DurationUtils.formatDurationWithLeadingMinus(duration); } throw new CoercingSerializeException(input + " cannot be cast to 'Duration'"); diff --git a/src/main/java/org/opentripplanner/framework/time/DurationUtils.java b/src/main/java/org/opentripplanner/framework/time/DurationUtils.java index bf59964fbb2..9a01d308a1c 100644 --- a/src/main/java/org/opentripplanner/framework/time/DurationUtils.java +++ b/src/main/java/org/opentripplanner/framework/time/DurationUtils.java @@ -196,4 +196,22 @@ public static int toIntMilliseconds(Duration timeout, int defaultValue) { private static String msToSecondsStr(String formatSeconds, double timeMs) { return String.format(ROOT, formatSeconds, timeMs / 1000.0) + " seconds"; } + + /** + * Formats a duration and if it's a negative amount, it places the minus before the "P" rather + * than in the middle of the value. + *

+ * Background: There are multiple ways to express -1.5 hours: "PT-1H-30M" and "-PT1H30M". + *

+ * The first version is what you get when calling toString() but it's quite confusing. Therefore, + * this method makes sure that you get the second form "-PT1H30M". + */ + public static String formatDurationWithLeadingMinus(Duration duration) { + if (duration.isNegative()) { + var positive = duration.abs().toString(); + return "-" + positive; + } else { + return duration.toString(); + } + } } diff --git a/src/main/java/org/opentripplanner/framework/time/OffsetDateTimeParser.java b/src/main/java/org/opentripplanner/framework/time/OffsetDateTimeParser.java new file mode 100644 index 00000000000..8b40697977a --- /dev/null +++ b/src/main/java/org/opentripplanner/framework/time/OffsetDateTimeParser.java @@ -0,0 +1,42 @@ +package org.opentripplanner.framework.time; + +import java.text.ParseException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; + +public class OffsetDateTimeParser { + + /** + * We need to have two offsets, in order to parse both "+0200" and "+02:00". The first is not + * really ISO-8601 compatible with the extended date and time. We need to make parsing strict, in + * order to keep the minute mandatory, otherwise we would be left with an unparsed minute + */ + public static final DateTimeFormatter LENIENT_PARSER = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .parseLenient() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .optionalStart() + .parseStrict() + .appendOffset("+HH:MM:ss", "Z") + .parseLenient() + .optionalEnd() + .optionalStart() + .appendOffset("+HHmmss", "Z") + .optionalEnd() + .toFormatter(); + + /** + * Parses a ISO-8601 string into am OffsetDateTime instance allowing the offset to be both in + * '02:00' and '0200' format. + * @throws ParseException if the string cannot be parsed + */ + public static OffsetDateTime parseLeniently(CharSequence input) throws ParseException { + try { + return OffsetDateTime.parse(input, LENIENT_PARSER); + } catch (DateTimeParseException e) { + throw new ParseException(e.getParsedString(), e.getErrorIndex()); + } + } +} diff --git a/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java index d2188fb1b0b..48c4b1b7b87 100644 --- a/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/FrequencyTransitLeg.java @@ -1,16 +1,9 @@ package org.opentripplanner.model.plan; -import java.time.LocalDate; -import java.time.ZoneId; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; -import javax.annotation.Nullable; import org.opentripplanner.framework.time.ServiceDateUtils; -import org.opentripplanner.model.transfer.ConstrainedTransfer; -import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.model.timetable.TripTimes; /** * One leg of a trip -- that is, a temporally continuous piece of the journey that takes place on a @@ -62,8 +55,8 @@ public List getIntermediateStops() { StopArrival visit = new StopArrival( Place.forStop(stop), - ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, arrivalTime), - ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, departureTime), + LegTime.ofStatic(ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, arrivalTime)), + LegTime.ofStatic(ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, departureTime)), i, tripTimes.gtfsSequenceOfStopIndex(i) ); diff --git a/src/main/java/org/opentripplanner/model/plan/Leg.java b/src/main/java/org/opentripplanner/model/plan/Leg.java index 05213dbdf5e..5a032def979 100644 --- a/src/main/java/org/opentripplanner/model/plan/Leg.java +++ b/src/main/java/org/opentripplanner/model/plan/Leg.java @@ -200,6 +200,16 @@ default Accessibility getTripWheelchairAccessibility() { return null; } + /** + * The time (including realtime information) when the leg starts. + */ + LegTime start(); + + /** + * The time (including realtime information) when the leg ends. + */ + LegTime end(); + /** * The date and time this leg begins. */ diff --git a/src/main/java/org/opentripplanner/model/plan/LegTime.java b/src/main/java/org/opentripplanner/model/plan/LegTime.java new file mode 100644 index 00000000000..354ce4f4b0b --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/LegTime.java @@ -0,0 +1,45 @@ +package org.opentripplanner.model.plan; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A scheduled time of a transit vehicle at a certain location with a optional realtime information. + */ +public record LegTime(@Nonnull ZonedDateTime scheduledTime, @Nullable RealTimeEstimate estimated) { + public LegTime { + Objects.requireNonNull(scheduledTime); + } + + @Nonnull + public static LegTime of(ZonedDateTime realtime, int delaySecs) { + var delay = Duration.ofSeconds(delaySecs); + return new LegTime(realtime.minus(delay), new RealTimeEstimate(realtime, delay)); + } + + @Nonnull + 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/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index 876567035dd..6bf39d5aa4c 100644 --- a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -162,6 +162,24 @@ public Accessibility getTripWheelchairAccessibility() { return tripTimes.getWheelchairAccessibility(); } + @Override + public LegTime start() { + if (getRealTime()) { + return LegTime.of(startTime, getDepartureDelay()); + } else { + return LegTime.ofStatic(startTime); + } + } + + @Override + public LegTime end() { + if (getRealTime()) { + return LegTime.of(endTime, getArrivalDelay()); + } else { + return LegTime.ofStatic(endTime); + } + } + @Override @Nonnull public TransitMode getMode() { @@ -257,17 +275,11 @@ public Place getTo() { @Override public List getIntermediateStops() { List visits = new ArrayList<>(); + var mapper = new StopArrivalMapper(zoneId, serviceDate, tripTimes); for (int i = boardStopPosInPattern + 1; i < alightStopPosInPattern; i++) { StopLocation stop = tripPattern.getStop(i); - - StopArrival visit = new StopArrival( - Place.forStop(stop), - ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, tripTimes.getArrivalTime(i)), - ServiceDateUtils.toZonedDateTime(serviceDate, zoneId, tripTimes.getDepartureTime(i)), - i, - tripTimes.gtfsSequenceOfStopIndex(i) - ); + final StopArrival visit = mapper.map(i, stop, getRealTime()); visits.add(visit); } return visits; diff --git a/src/main/java/org/opentripplanner/model/plan/StopArrival.java b/src/main/java/org/opentripplanner/model/plan/StopArrival.java index 10398768c85..489b122da09 100644 --- a/src/main/java/org/opentripplanner/model/plan/StopArrival.java +++ b/src/main/java/org/opentripplanner/model/plan/StopArrival.java @@ -1,39 +1,31 @@ package org.opentripplanner.model.plan; -import java.time.ZonedDateTime; import org.opentripplanner.framework.tostring.ToStringBuilder; /** * This class is used to represent a stop arrival event mostly for intermediate visits to a stops * along a route. */ -public class StopArrival { +public final class StopArrival { public final Place place; - /** - * The time the rider will arrive at the place. - */ - public final ZonedDateTime arrival; - - /** - * The time the rider will depart the place. - */ - public final ZonedDateTime departure; - - /** - * For transit trips, the stop index (numbered from zero from the start of the trip). - */ + public final LegTime arrival; + public final LegTime departure; public final Integer stopPosInPattern; + public final Integer gtfsStopSequence; /** - * For transit trips, the sequence number of the stop. Per GTFS, these numbers are increasing. + * @param arrival The time the rider will arrive at the place. + * @param departure The time the rider will depart the place. + * @param stopPosInPattern For transit trips, the stop index (numbered from zero from the start of + * the trip). + * @param gtfsStopSequence For transit trips, the sequence number of the stop. Per GTFS, these + * numbers are increasing. */ - public final Integer gtfsStopSequence; - public StopArrival( Place place, - ZonedDateTime arrival, - ZonedDateTime departure, + LegTime arrival, + LegTime departure, Integer stopPosInPattern, Integer gtfsStopSequence ) { @@ -48,8 +40,8 @@ public StopArrival( public String toString() { return ToStringBuilder .of(StopArrival.class) - .addTime("arrival", arrival) - .addTime("departure", departure) + .addObj("arrival", arrival) + .addObj("departure", departure) .addObj("place", place) .toString(); } diff --git a/src/main/java/org/opentripplanner/model/plan/StopArrivalMapper.java b/src/main/java/org/opentripplanner/model/plan/StopArrivalMapper.java new file mode 100644 index 00000000000..25bab2a7c3f --- /dev/null +++ b/src/main/java/org/opentripplanner/model/plan/StopArrivalMapper.java @@ -0,0 +1,53 @@ +package org.opentripplanner.model.plan; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Objects; +import org.opentripplanner.framework.time.ServiceDateUtils; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.TripTimes; + +/** + * Maps leg-related information to an instance of {@link StopArrival}. + */ +class StopArrivalMapper { + + private final ZoneId zoneId; + private final LocalDate serviceDate; + private final TripTimes tripTimes; + + public StopArrivalMapper(ZoneId zoneId, LocalDate serviceDate, TripTimes tripTimes) { + this.zoneId = Objects.requireNonNull(zoneId); + this.serviceDate = Objects.requireNonNull(serviceDate); + this.tripTimes = Objects.requireNonNull(tripTimes); + } + + StopArrival map(int i, StopLocation stop, boolean realTime) { + final var arrivalTime = ServiceDateUtils.toZonedDateTime( + serviceDate, + zoneId, + tripTimes.getArrivalTime(i) + ); + final var departureTime = ServiceDateUtils.toZonedDateTime( + serviceDate, + zoneId, + tripTimes.getDepartureTime(i) + ); + + var arrival = LegTime.ofStatic(arrivalTime); + var departure = LegTime.ofStatic(departureTime); + + if (realTime) { + arrival = LegTime.of(arrivalTime, tripTimes.getArrivalDelay(i)); + departure = LegTime.of(departureTime, tripTimes.getDepartureDelay(i)); + } + + return new StopArrival( + Place.forStop(stop), + arrival, + departure, + i, + tripTimes.gtfsSequenceOfStopIndex(i) + ); + } +} diff --git a/src/main/java/org/opentripplanner/model/plan/StreetLeg.java b/src/main/java/org/opentripplanner/model/plan/StreetLeg.java index 319c9e01f69..6384159a4ec 100644 --- a/src/main/java/org/opentripplanner/model/plan/StreetLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/StreetLeg.java @@ -156,6 +156,16 @@ public boolean hasSameMode(Leg other) { return other instanceof StreetLeg oSL && mode.equals(oSL.mode); } + @Override + public LegTime start() { + return LegTime.ofStatic(startTime); + } + + @Override + public LegTime end() { + return LegTime.ofStatic(endTime); + } + @Override public Leg withTimeShift(Duration duration) { return StreetLegBuilder diff --git a/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java b/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java index 382a09d34e2..df43bbcb411 100644 --- a/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/UnknownTransitPathLeg.java @@ -68,6 +68,16 @@ public boolean hasSameMode(Leg other) { return false; } + @Override + public LegTime start() { + return LegTime.ofStatic(startTime); + } + + @Override + public LegTime end() { + return LegTime.ofStatic(endTime); + } + @Override public double getDistanceMeters() { return UNKNOWN; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/AlertToLegMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/AlertToLegMapper.java index dd08ab6095d..94630be7600 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/AlertToLegMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/AlertToLegMapper.java @@ -91,8 +91,8 @@ public void addTransitAlertsToLeg(Leg leg, boolean isFirstLeg) { ) ); - ZonedDateTime stopArrival = visit.arrival; - ZonedDateTime stopDeparture = visit.departure; + ZonedDateTime stopArrival = visit.arrival.scheduledTime(); + ZonedDateTime stopDeparture = visit.departure.scheduledTime(); addTransitAlertsToLeg(leg, alerts, stopArrival, stopDeparture); } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index daeb57d655e..655a1908a87 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1497,15 +1497,11 @@ type DefaultFareProduct implements FareProduct { } type Itinerary { - """ - Time when the user leaves from the origin. Format: Unix timestamp in milliseconds. - """ - startTime: Long + "Time when the user leaves from the origin." + start: OffsetDateTime - """ - Time when the user arrives to the destination.. Format: Unix timestamp in milliseconds. - """ - endTime: Long + "Time when the user leaves arrives at the destination." + end: OffsetDateTime """Duration of the trip on this itinerary, in seconds.""" duration: Long @@ -1584,6 +1580,16 @@ type Itinerary { and always returns an empty list. Use the leg's `fareProducts` instead. """ fares: [fare] @deprecated(reason: "Use the leg's `fareProducts`.") + + """ + Time when the user leaves from the origin. Format: Unix timestamp in milliseconds. + """ + startTime: Long @deprecated(reason: "Use `start` instead which includes timezone information.") + + """ + Time when the user arrives to the destination. Format: Unix timestamp in milliseconds. + """ + endTime: Long @deprecated(reason: "Use `end` instead which includes timezone information.") } "A currency" @@ -1618,8 +1624,10 @@ type Money { """" An ISO-8601-formatted duration, i.e. `PT2H30M` for 2 hours and 30 minutes. + +Negative durations are formatted like `-PT10M`. """ -scalar Duration +scalar Duration @specifiedBy(url:"https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)") type RideHailingProvider { "The ID of the ride hailing provider." @@ -1640,30 +1648,46 @@ type RideHailingEstimate { productName: String } -type Leg { - """ - The date and time when this leg begins. Format: Unix timestamp in milliseconds. - """ - startTime: Long +""" +An ISO-8601-formatted datetime with offset, i.e. `2023-06-13T14:30+03:00` for 2:30pm on June 13th 2023 at Helsinki's offset from UTC at that time. + +ISO-8601 allows many different formats but OTP will only return the profile specified in RFC3339. +""" +scalar OffsetDateTime @specifiedBy(url: "https://www.rfcreader.com/#rfc3339") +"Real-time estimates for a vehicle at a certain place." +type RealTimeEstimate { + time: OffsetDateTime! """ - The date and time when this leg ends. Format: Unix timestamp in milliseconds. + The delay or "earliness" of the vehicle at a certain place. + + If the vehicle is early then this is a negative duration. """ - endTime: Long + delay: Duration! +} + +""" +Time information about a passenger at a certain place. May contain real-time information if +available. +""" +type LegTime { + "The scheduled time of the event." + scheduledTime: OffsetDateTime! + "The estimated time of the event. If no real-time information is available, this is null," + estimated: RealTimeEstimate +} + +type Leg { """ - For transit leg, the offset from the scheduled departure time of the boarding - stop in this leg, i.e. scheduled time of departure at boarding stop = - `startTime - departureDelay` + The time when the leg starts including real-time information, if available. """ - departureDelay: Int + start: LegTime! """ - For transit leg, the offset from the scheduled arrival time of the alighting - stop in this leg, i.e. scheduled time of arrival at alighting stop = `endTime - - arrivalDelay` + The time when the leg ends including real-time information, if available. """ - arrivalDelay: Int + end: LegTime! """The mode (e.g. `WALK`) used when traversing this leg.""" mode: Mode @@ -1825,6 +1849,31 @@ type Leg { that applies to multiple legs can appear several times. """ fareProducts: [FareProductUse] + + """ + The date and time when this leg begins. Format: Unix timestamp in milliseconds. + """ + startTime: Long @deprecated(reason: "Use `start.estimated.time` instead which contains timezone information.") + + """ + The date and time when this leg ends. Format: Unix timestamp in milliseconds. + """ + endTime: Long @deprecated(reason: "Use `end.estimated.time` instead which contains timezone information.") + + """ + For transit leg, the offset from the scheduled departure time of the boarding + stop in this leg, i.e. scheduled time of departure at boarding stop = + `startTime - departureDelay` + """ + departureDelay: Int @deprecated(reason: "Use `end.estimated.delay` instead.") + + """ + For transit leg, the offset from the scheduled arrival time of the alighting + stop in this leg, i.e. scheduled time of arrival at alighting stop = `endTime + - arrivalDelay` + """ + arrivalDelay: Int @deprecated(reason: "Use `start.estimated.delay` instead.") + } """A span of time.""" @@ -2260,14 +2309,16 @@ type Place { lon: Float! """ - The time the rider will arrive at the place. Format: Unix timestamp in milliseconds. + The time the rider will arrive at the place. This also includes real-time information + if available. """ - arrivalTime: Long! + arrival: LegTime """ - The time the rider will depart the place. Format: Unix timestamp in milliseconds. + The time the rider will depart the place. This also includes real-time information + if available. """ - departureTime: Long! + departure: LegTime """The stop related to the place.""" stop: Stop @@ -2305,6 +2356,16 @@ type Place { """The car parking related to the place""" carPark: CarPark @deprecated(reason: "carPark is deprecated. Use vehicleParking instead.") + + """ + The time the rider will arrive at the place. Format: Unix timestamp in milliseconds. + """ + arrivalTime: Long! @deprecated(reason: "Use `arrival` which includes timezone information.") + + """ + The time the rider will depart the place. Format: Unix timestamp in milliseconds. + """ + departureTime: Long! @deprecated(reason: "Use `departure` which includes timezone information.") } type placeAtDistance implements Node { diff --git a/src/test/java/org/opentripplanner/apis/gtfs/DurationScalarTest.java b/src/test/java/org/opentripplanner/apis/gtfs/DurationScalarTest.java new file mode 100644 index 00000000000..28d1ad08376 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/DurationScalarTest.java @@ -0,0 +1,41 @@ +package org.opentripplanner.apis.gtfs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.of; + +import graphql.schema.CoercingSerializeException; +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class DurationScalarTest { + + static List durationCases() { + return List.of( + of(Duration.ofMinutes(30), "PT30M"), + of(Duration.ofHours(23), "PT23H"), + of(Duration.ofMinutes(-10), "-PT10M"), + of(Duration.ofMinutes(-90), "-PT1H30M"), + of(Duration.ofMinutes(-184), "-PT3H4M") + ); + } + + @ParameterizedTest + @MethodSource("durationCases") + void duration(Duration duration, String expected) { + var string = GraphQLScalars.DURATION_SCALAR.getCoercing().serialize(duration); + assertEquals(expected, string); + } + + @Test + void nonDuration() { + Assertions.assertThrows( + CoercingSerializeException.class, + () -> GraphQLScalars.DURATION_SCALAR.getCoercing().serialize(new Object()) + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLScalarsTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GeoJsonScalarTest.java similarity index 59% rename from src/test/java/org/opentripplanner/apis/gtfs/GraphQLScalarsTest.java rename to src/test/java/org/opentripplanner/apis/gtfs/GeoJsonScalarTest.java index 90b35a31b9f..cc88e22d074 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLScalarsTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GeoJsonScalarTest.java @@ -3,29 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.fasterxml.jackson.core.JsonProcessingException; -import graphql.schema.CoercingSerializeException; -import java.time.Duration; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.opentripplanner.framework.json.ObjectMappers; -class GraphQLScalarsTest { - - @Test - void duration() { - var string = GraphQLScalars.durationScalar.getCoercing().serialize(Duration.ofMinutes(30)); - assertEquals("PT30M", string); - } - - @Test - void nonDuration() { - Assertions.assertThrows( - CoercingSerializeException.class, - () -> GraphQLScalars.durationScalar.getCoercing().serialize(new Object()) - ); - } +class GeoJsonScalarTest { @Test void geoJson() throws JsonProcessingException { @@ -38,7 +21,7 @@ void geoJson() throws JsonProcessingException { new Coordinate(0, 0), } ); - var geoJson = GraphQLScalars.geoJsonScalar.getCoercing().serialize(polygon); + var geoJson = GraphQLScalars.GEOJSON_SCALAR.getCoercing().serialize(polygon); var jsonNode = ObjectMappers .ignoringExtraFields() diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index d89afea6f9d..f630e0cee3c 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -61,7 +61,6 @@ import org.opentripplanner.routing.alertpatch.TimePeriod; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.api.request.RouteRequest; -import org.opentripplanner.routing.core.FareType; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.GraphFinder; import org.opentripplanner.routing.graphfinder.NearbyStop; @@ -86,6 +85,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; @@ -115,6 +115,7 @@ class GraphQLIntegrationTest { .parse("2023-02-15T12:03:28+01:00") .toInstant(); static final Instant ALERT_END_TIME = ALERT_START_TIME.plus(1, ChronoUnit.DAYS); + private static final int TEN_MINUTES = 10 * 60; private static GraphQLRequestContext context; @@ -179,6 +180,8 @@ static void setup() { .carHail(D10m, E) .build(); + add10MinuteDelay(i1); + var busLeg = i1.getTransitLeg(1); var railLeg = (ScheduledTransitLeg) i1.getTransitLeg(2); @@ -280,6 +283,20 @@ public TransitAlertService getTransitAlertService() { ); } + private static void add10MinuteDelay(Itinerary i1) { + i1.transformTransitLegs(tl -> { + if (tl instanceof ScheduledTransitLeg stl) { + var rtt = (RealTimeTripTimes) stl.getTripTimes(); + + for (var i = 0; i < rtt.getNumStops(); i++) { + rtt.updateArrivalTime(i, rtt.getArrivalTime(i) + TEN_MINUTES); + rtt.updateDepartureTime(i, rtt.getDepartureTime(i) + TEN_MINUTES); + } + } + return tl; + }); + } + @FilePatternSource(pattern = "src/test/resources/org/opentripplanner/apis/gtfs/queries/*.graphql") @ParameterizedTest(name = "Check GraphQL query in {0}") void graphQL(Path path) throws IOException { diff --git a/src/test/java/org/opentripplanner/apis/gtfs/OffsetDateTimeScalarTest.java b/src/test/java/org/opentripplanner/apis/gtfs/OffsetDateTimeScalarTest.java new file mode 100644 index 00000000000..45c8e9de837 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/OffsetDateTimeScalarTest.java @@ -0,0 +1,62 @@ +package org.opentripplanner.apis.gtfs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.of; + +import graphql.language.StringValue; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner._support.time.ZoneIds; + +public class OffsetDateTimeScalarTest { + + static final OffsetDateTime OFFSET_DATE_TIME = OffsetDateTime.of( + LocalDate.of(2024, 2, 4), + LocalTime.MIDNIGHT, + ZoneOffset.UTC + ); + + static List offsetDateTimeCases() { + return List.of( + of(OFFSET_DATE_TIME, "2024-02-04T00:00:00Z"), + of(OFFSET_DATE_TIME.plusHours(12).plusMinutes(8).plusSeconds(22), "2024-02-04T12:08:22Z"), + of( + OFFSET_DATE_TIME.atZoneSameInstant(ZoneIds.BERLIN).toOffsetDateTime(), + "2024-02-04T01:00:00+01:00" + ), + of( + OFFSET_DATE_TIME.atZoneSameInstant(ZoneIds.NEW_YORK).toOffsetDateTime(), + "2024-02-03T19:00:00-05:00" + ) + ); + } + + @ParameterizedTest + @MethodSource("offsetDateTimeCases") + void serializeOffsetDateTime(OffsetDateTime odt, String expected) { + var string = GraphQLScalars.OFFSET_DATETIME_SCALAR.getCoercing().serialize(odt); + assertEquals(expected, string); + } + + @ParameterizedTest + @MethodSource("offsetDateTimeCases") + void parseOffsetDateTime(OffsetDateTime expected, String input) { + var odt = GraphQLScalars.OFFSET_DATETIME_SCALAR.getCoercing().parseValue(input); + assertEquals(expected, odt); + } + + @ParameterizedTest + @MethodSource("offsetDateTimeCases") + void parseOffsetDateTimeLiteral(OffsetDateTime expected, String input) { + var odt = GraphQLScalars.OFFSET_DATETIME_SCALAR + .getCoercing() + .parseLiteral(new StringValue(input)); + assertEquals(expected, odt); + } +} diff --git a/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java b/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java index 86953760485..2457aa14b60 100644 --- a/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.of; import static org.opentripplanner.framework.time.DurationUtils.requireNonNegative; import static org.opentripplanner.framework.time.DurationUtils.toIntMilliseconds; @@ -13,6 +14,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.ResourceLock; import org.junit.jupiter.api.parallel.Resources; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class DurationUtilsTest { @@ -151,6 +155,29 @@ public void msToSecondsStr() { } } + static List durationCases() { + return List.of( + of(Duration.ofSeconds(30), "PT30S"), + of(Duration.ofMinutes(30), "PT30M"), + of(Duration.ofHours(23), "PT23H"), + of(Duration.ofSeconds(-30), "-PT30S"), + of(Duration.ofMinutes(-10), "-PT10M"), + of(Duration.ofMinutes(-90), "-PT1H30M"), + of(Duration.ofMinutes(-184), "-PT3H4M") + ); + } + + @ParameterizedTest + @MethodSource("durationCases") + void formatDuration(Duration duration, String expected) { + var string = DurationUtils.formatDurationWithLeadingMinus(duration); + + assertEquals(expected, string); + + var parsed = Duration.parse(expected); + assertEquals(parsed, duration); + } + private static int durationSec(int hour, int min, int sec) { return 60 * (60 * hour + min) + sec; } diff --git a/src/test/java/org/opentripplanner/framework/time/OffsetDateTimeParserTest.java b/src/test/java/org/opentripplanner/framework/time/OffsetDateTimeParserTest.java new file mode 100644 index 00000000000..d8944b9a5f0 --- /dev/null +++ b/src/test/java/org/opentripplanner/framework/time/OffsetDateTimeParserTest.java @@ -0,0 +1,51 @@ +package org.opentripplanner.framework.time; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.text.ParseException; +import java.time.OffsetDateTime; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class OffsetDateTimeParserTest { + + private static final String TIME_STRING = "2023-01-27T12:59:00+01:00"; + private static final OffsetDateTime TIME = OffsetDateTime.parse(TIME_STRING); + + static List successfulCases() { + return List.of( + TIME_STRING, + "2023-01-27T12:59:00.000+01:00", + "2023-01-27T12:59:00+0100", + "2023-01-27T12:59:00+01", + "2023-01-27T11:59:00Z", + "2023-01-27T11:59Z", + "2023-01-27T06:59:00-05:00", + "2023-01-27T06:59:00-0500" + ); + } + + @ParameterizedTest + @MethodSource("successfulCases") + void parse(String input) throws ParseException { + var res = OffsetDateTimeParser.parseLeniently(input); + assertTrue(res.isEqual(TIME)); + } + + static List failedCases() { + return List.of("2023-01-27T11:59:00", "2023-01-27T11", "2023-01-27T11:00"); + } + + @ParameterizedTest + @MethodSource("failedCases") + void failed(String input) { + Assertions.assertThrows( + ParseException.class, + () -> { + OffsetDateTimeParser.parseLeniently(input); + } + ); + } +} diff --git a/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java b/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java index e45beb8ebad..b41ace81e8d 100644 --- a/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java +++ b/src/test/java/org/opentripplanner/model/plan/ScheduledTransitLegTest.java @@ -1,6 +1,8 @@ package org.opentripplanner.model.plan; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import java.time.OffsetDateTime; @@ -9,33 +11,74 @@ import org.junit.jupiter.api.Test; import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.transit.model.timetable.Trip; class ScheduledTransitLegTest { static final ZonedDateTime TIME = OffsetDateTime .parse("2023-04-17T17:49:06+02:00") .toZonedDateTime(); + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final Route ROUTE = TransitModelForTest.route(id("2")).build(); + private static final TripPattern PATTERN = TransitModelForTest + .tripPattern("1", ROUTE) + .withStopPattern(TEST_MODEL.stopPattern(3)) + .build(); + private static final Trip TRIP = TransitModelForTest.trip("trip1").build(); + private static final ScheduledTripTimes TRIP_TIMES = ScheduledTripTimes + .of() + .withArrivalTimes("10:00 11:00 12:00") + .withDepartureTimes("10:01 11:02 12:03") + .withTrip(TRIP) + .build(); @Test void defaultFares() { - var testModel = TransitModelForTest.of(); - var route = TransitModelForTest.route(id("2")).build(); - var pattern = TransitModelForTest - .tripPattern("1", route) - .withStopPattern(testModel.stopPattern(3)) + var leg = builder().build(); + + assertEquals(List.of(), leg.fareProducts()); + } + + @Test + void legTimesWithoutRealTime() { + var leg = builder().withTripTimes(TRIP_TIMES).build(); + + assertNull(leg.start().estimated()); + assertNull(leg.end().estimated()); + } + + @Test + void legTimesWithRealTime() { + var tt = ScheduledTripTimes + .of() + .withArrivalTimes("10:00 11:00 12:00") + .withDepartureTimes("10:01 11:02 12:03") + .withTrip(TRIP) .build(); - var leg = new ScheduledTransitLegBuilder() + + var rtt = RealTimeTripTimes.of(tt); + rtt.updateArrivalTime(0, 111); + + var leg = builder().withTripTimes(rtt).build(); + + assertNotNull(leg.start().estimated()); + assertNotNull(leg.end().estimated()); + } + + private static ScheduledTransitLegBuilder builder() { + return new ScheduledTransitLegBuilder() .withTripTimes(null) - .withTripPattern(pattern) + .withTripPattern(PATTERN) .withBoardStopIndexInPattern(0) .withAlightStopIndexInPattern(2) .withStartTime(TIME) .withEndTime(TIME.plusMinutes(10)) .withServiceDate(TIME.toLocalDate()) .withZoneId(ZoneIds.BERLIN) - .withGeneralizedCost(100) - .build(); - - assertEquals(List.of(), leg.fareProducts()); + .withGeneralizedCost(100); } } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json index 557cdec8659..ea58480be8e 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan-extended.json @@ -3,6 +3,8 @@ "plan" : { "itineraries" : [ { + "start" : "2020-02-02T11:00:00Z", + "end" : "2020-02-02T12:00:00Z", "startTime" : 1580641200000, "endTime" : 1580644800000, "generalizedCost" : 4072, @@ -16,10 +18,26 @@ "legs" : [ { "mode" : "WALK", + "start" : { + "scheduledTime" : "2020-02-02T11:00:00Z", + "estimated" : null + }, + "end" : { + "scheduledTime" : "2020-02-02T11:00:20Z", + "estimated" : null + }, "from" : { "name" : "A", "lat" : 5.0, "lon" : 8.0, + "arrival" : { + "scheduledTime" : "2020-02-02T11:00:00Z", + "estimated" : null + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:00:00Z", + "estimated" : null + }, "departureTime" : 1580641200000, "arrivalTime" : 1580641200000 }, @@ -27,6 +45,14 @@ "name" : "B", "lat" : 6.0, "lon" : 8.5, + "arrival" : { + "scheduledTime" : "2020-02-02T11:00:20Z", + "estimated" : null + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:00:20Z", + "estimated" : null + }, "departureTime" : 1580641220000, "arrivalTime" : 1580641220000 }, @@ -35,16 +61,45 @@ "generalizedCost" : 40, "headsign" : null, "trip" : null, + "intermediatePlaces" : null, "alerts" : [ ], "rideHailingEstimate" : null, "accessibilityScore" : null }, { "mode" : "BUS", + "start" : { + "scheduledTime" : "2020-02-02T10:51:00Z", + "estimated" : { + "time" : "2020-02-02T11:01:00Z", + "delay" : "PT10M" + } + }, + "end" : { + "scheduledTime" : "2020-02-02T11:05:00Z", + "estimated" : { + "time" : "2020-02-02T11:15:00Z", + "delay" : "PT10M" + } + }, "from" : { "name" : "B", "lat" : 6.0, "lon" : 8.5, + "arrival" : { + "scheduledTime" : "2020-02-02T10:51:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:01:00Z" + } + }, + "departure" : { + "scheduledTime" : "2020-02-02T10:51:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:01:00Z" + } + }, "departureTime" : 1580641260000, "arrivalTime" : 1580641260000 }, @@ -52,6 +107,20 @@ "name" : "C", "lat" : 7.0, "lon" : 9.0, + "arrival" : { + "scheduledTime" : "2020-02-02T11:05:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:15:00Z" + } + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:05:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:15:00Z" + } + }, "departureTime" : 1580642100000, "arrivalTime" : 1580642100000 }, @@ -62,16 +131,65 @@ "trip" : { "tripHeadsign" : "Trip headsign 122" }, + "intermediatePlaces" : [ + { + "arrival" : { + "scheduledTime" : "2020-02-02T11:01:00Z", + "estimated" : { + "time" : "2020-02-02T11:11:00Z", + "delay" : "PT10M" + } + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:01:00Z", + "estimated" : { + "time" : "2020-02-02T11:11:00Z", + "delay" : "PT10M" + } + }, + "stop" : { + "name" : "B" + } + } + ], "alerts" : [ ], "rideHailingEstimate" : null, "accessibilityScore" : null }, { "mode" : "RAIL", + "start" : { + "scheduledTime" : "2020-02-02T11:20:00Z", + "estimated" : { + "time" : "2020-02-02T11:30:00Z", + "delay" : "PT10M" + } + }, + "end" : { + "scheduledTime" : "2020-02-02T11:40:00Z", + "estimated" : { + "time" : "2020-02-02T11:50:00Z", + "delay" : "PT10M" + } + }, "from" : { "name" : "C", "lat" : 7.0, "lon" : 9.0, + "arrival" : { + "scheduledTime" : "2020-02-02T11:20:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:30:00Z" + } + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:20:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:30:00Z" + } + }, "departureTime" : 1580643000000, "arrivalTime" : 1580643000000 }, @@ -79,6 +197,20 @@ "name" : "D", "lat" : 8.0, "lon" : 9.5, + "arrival" : { + "scheduledTime" : "2020-02-02T11:40:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:50:00Z" + } + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:40:00Z", + "estimated" : { + "delay" : "PT10M", + "time" : "2020-02-02T11:50:00Z" + } + }, "departureTime" : 1580644200000, "arrivalTime" : 1580644200000 }, @@ -89,6 +221,27 @@ "trip" : { "tripHeadsign" : "Trip headsign 439" }, + "intermediatePlaces" : [ + { + "arrival" : { + "scheduledTime" : "2020-02-02T11:30:00Z", + "estimated" : { + "time" : "2020-02-02T11:40:00Z", + "delay" : "PT10M" + } + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:30:00Z", + "estimated" : { + "time" : "2020-02-02T11:40:00Z", + "delay" : "PT10M" + } + }, + "stop" : { + "name" : "C" + } + } + ], "alerts" : [ { "id" : "QWxlcnQ6Rjphbi1hbGVydA", @@ -115,10 +268,26 @@ }, { "mode" : "CAR", + "start" : { + "scheduledTime" : "2020-02-02T11:50:00Z", + "estimated" : null + }, + "end" : { + "scheduledTime" : "2020-02-02T12:00:00Z", + "estimated" : null + }, "from" : { "name" : "D", "lat" : 8.0, "lon" : 9.5, + "arrival" : { + "scheduledTime" : "2020-02-02T11:50:00Z", + "estimated" : null + }, + "departure" : { + "scheduledTime" : "2020-02-02T11:50:00Z", + "estimated" : null + }, "departureTime" : 1580644200000, "arrivalTime" : 1580644200000 }, @@ -126,6 +295,14 @@ "name" : "E", "lat" : 9.0, "lon" : 10.0, + "arrival" : { + "scheduledTime" : "2020-02-02T12:00:00Z", + "estimated" : null + }, + "departure" : { + "scheduledTime" : "2020-02-02T12:00:00Z", + "estimated" : null + }, "departureTime" : 1580644800000, "arrivalTime" : 1580644800000 }, @@ -134,6 +311,7 @@ "generalizedCost" : 1000, "headsign" : null, "trip" : null, + "intermediatePlaces" : null, "alerts" : [ ], "rideHailingEstimate" : { "provider" : { diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan.json index 06fd20be150..d213443f7cd 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/plan.json @@ -3,47 +3,57 @@ "plan" : { "itineraries" : [ { - "startTime" : 1580641200000, - "endTime" : 1580644800000, + "start" : "2020-02-02T11:00:00Z", + "end" : "2020-02-02T12:00:00Z", "legs" : [ { "mode" : "WALK", - "startTime" : 1580641200000, - "endTime" : 1580641220000, "from" : { "name" : "A", "lat" : 5.0, "lon" : 8.0, - "departureTime" : 1580641200000, - "arrivalTime" : 1580641200000 + "departure" : { + "scheduledTime" : "2020-02-02T11:00:00Z", + "estimated" : null + } }, "to" : { "name" : "B", "lat" : 6.0, "lon" : 8.5, - "departureTime" : 1580641220000, - "arrivalTime" : 1580641220000 + "arrival" : { + "scheduledTime" : "2020-02-02T11:00:20Z", + "estimated" : null + } }, "route" : null, "legGeometry" : null }, { "mode" : "BUS", - "startTime" : 1580641260000, - "endTime" : 1580642100000, "from" : { "name" : "B", "lat" : 6.0, "lon" : 8.5, - "departureTime" : 1580641260000, - "arrivalTime" : 1580641260000 + "departure" : { + "scheduledTime" : "2020-02-02T10:51:00Z", + "estimated" : { + "time" : "2020-02-02T11:01:00Z", + "delay" : "PT10M" + } + } }, "to" : { "name" : "C", "lat" : 7.0, "lon" : 9.0, - "departureTime" : 1580642100000, - "arrivalTime" : 1580642100000 + "arrival" : { + "scheduledTime" : "2020-02-02T11:05:00Z", + "estimated" : { + "time" : "2020-02-02T11:15:00Z", + "delay" : "PT10M" + } + } }, "route" : { "gtfsId" : "F:BUS", @@ -56,21 +66,29 @@ }, { "mode" : "RAIL", - "startTime" : 1580643000000, - "endTime" : 1580644200000, "from" : { "name" : "C", "lat" : 7.0, "lon" : 9.0, - "departureTime" : 1580643000000, - "arrivalTime" : 1580643000000 + "departure" : { + "scheduledTime" : "2020-02-02T11:20:00Z", + "estimated" : { + "time" : "2020-02-02T11:30:00Z", + "delay" : "PT10M" + } + } }, "to" : { "name" : "D", "lat" : 8.0, "lon" : 9.5, - "departureTime" : 1580644200000, - "arrivalTime" : 1580644200000 + "arrival" : { + "scheduledTime" : "2020-02-02T11:40:00Z", + "estimated" : { + "time" : "2020-02-02T11:50:00Z", + "delay" : "PT10M" + } + } }, "route" : { "gtfsId" : "F:2", @@ -83,21 +101,23 @@ }, { "mode" : "CAR", - "startTime" : 1580644200000, - "endTime" : 1580644800000, "from" : { "name" : "D", "lat" : 8.0, "lon" : 9.5, - "departureTime" : 1580644200000, - "arrivalTime" : 1580644200000 + "departure" : { + "scheduledTime" : "2020-02-02T11:50:00Z", + "estimated" : null + } }, "to" : { "name" : "E", "lat" : 9.0, "lon" : 10.0, - "departureTime" : 1580644800000, - "arrivalTime" : 1580644800000 + "arrival" : { + "scheduledTime" : "2020-02-02T12:00:00Z", + "estimated" : null + } }, "route" : null, "legGeometry" : null diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql index db30d8489ef..bcd96892e84 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan-extended.graphql @@ -16,6 +16,9 @@ } ]) { itineraries { + start + end + # next two are deprecated startTime endTime generalizedCost @@ -28,10 +31,38 @@ walkTime legs { mode + start { + scheduledTime + estimated { + time + delay + } + } + end { + scheduledTime + estimated { + time + delay + } + } from { name lat lon + arrival { + scheduledTime + estimated { + delay + time + } + } + departure { + scheduledTime + estimated { + delay + time + } + } departureTime arrivalTime } @@ -39,6 +70,20 @@ name lat lon + arrival { + scheduledTime + estimated { + delay + time + } + } + departure { + scheduledTime + estimated { + delay + time + } + } departureTime arrivalTime } @@ -50,6 +95,25 @@ trip { tripHeadsign } + intermediatePlaces { + arrival { + scheduledTime + estimated { + time + delay + } + } + departure { + scheduledTime + estimated { + time + delay + } + } + stop { + name + } + } alerts { id alertHeaderText diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan.graphql index d71c991234d..39877eef0b7 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/plan.graphql @@ -17,25 +17,33 @@ }, ]) { itineraries { - startTime - endTime + start + end legs { mode - startTime - endTime from { name lat lon - departureTime - arrivalTime + departure { + scheduledTime + estimated { + time + delay + } + } } to { name lat lon - departureTime - arrivalTime + arrival { + scheduledTime + estimated { + time + delay + } + } } route { gtfsId