diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/DefaultValueInjector.java b/application/src/main/java/org/opentripplanner/apis/gtfs/DefaultValueInjector.java new file mode 100644 index 00000000000..172ad530a60 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/DefaultValueInjector.java @@ -0,0 +1,565 @@ +package org.opentripplanner.apis.gtfs; + +import graphql.language.ArrayValue; +import graphql.language.BooleanValue; +import graphql.language.EnumValue; +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLNamedSchemaElement; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeVisitor; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.mapping.TransitModeMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.AccessModeMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.DirectModeMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.EgressModeMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.StreetModeMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.TransferModeMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.VehicleOptimizationTypeMapper; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.preference.BikePreferences; +import org.opentripplanner.routing.api.request.preference.CarPreferences; +import org.opentripplanner.routing.api.request.preference.ScooterPreferences; +import org.opentripplanner.routing.api.request.preference.TimeSlopeSafetyTriangle; +import org.opentripplanner.routing.api.request.preference.TransferPreferences; +import org.opentripplanner.routing.api.request.preference.TransitPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleParkingPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleRentalPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleWalkingPreferences; +import org.opentripplanner.routing.api.request.preference.WalkPreferences; +import org.opentripplanner.routing.api.request.preference.filter.VehicleParkingFilter; +import org.opentripplanner.routing.api.request.preference.filter.VehicleParkingSelect; +import org.opentripplanner.routing.api.request.request.JourneyRequest; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; +import org.opentripplanner.transit.model.basic.TransitMode; + +/** + * GraphQL type visitor that injects default values to input fields and query arguments from code + * and configuration. + */ +public class DefaultValueInjector extends GraphQLTypeVisitorStub implements GraphQLTypeVisitor { + + private final Map> defaultForKey; + + public DefaultValueInjector(RouteRequest defaultRouteRequest) { + this.defaultForKey = createDefaultMapping(defaultRouteRequest); + } + + @Override + public TraversalControl visitGraphQLArgument( + GraphQLArgument argument, + TraverserContext context + ) { + var defaultValue = getDefaultValueForSchemaObject(context, argument.getName()); + if (defaultValue != null) { + return changeNode( + context, + argument.transform(builder -> builder.defaultValueLiteral(defaultValue).build()) + ); + } + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitGraphQLInputObjectField( + GraphQLInputObjectField field, + TraverserContext context + ) { + var defaultValue = getDefaultValueForSchemaObject(context, field.getName()); + if (defaultValue != null) { + return changeNode( + context, + field.transform(builder -> builder.defaultValueLiteral(defaultValue).build()) + ); + } + return TraversalControl.CONTINUE; + } + + private Value getDefaultValueForSchemaObject( + TraverserContext context, + String name + ) { + // Arguments and input fields always have a parent + var parent = (GraphQLNamedSchemaElement) context.getParentNode(); + var parentName = parent.getName(); + var key = parentName + "." + name; + return defaultForKey.get(key); + } + + private static Map> createDefaultMapping(RouteRequest defaultRouteRequest) { + var builder = new DefaultMappingBuilder() + .intReq("planConnection.first", defaultRouteRequest.numItineraries()) + .stringOpt("planConnection.searchWindow", defaultRouteRequest.searchWindow()); + setBikeDefaults(defaultRouteRequest.preferences().bike(), builder); + setCarDefaults(defaultRouteRequest.preferences().car(), builder); + setModeDefaults( + defaultRouteRequest.journey(), + defaultRouteRequest.preferences().transit(), + builder + ); + setScooterDefaults(defaultRouteRequest.preferences().scooter(), builder); + setTransitDefaults(defaultRouteRequest.preferences().transit(), builder); + setTransferDefaults(defaultRouteRequest.preferences().transfer(), builder); + setWalkDefaults(defaultRouteRequest.preferences().walk(), builder); + setWheelchairDefaults(defaultRouteRequest, builder); + return builder.build(); + } + + private static void setBikeDefaults(BikePreferences bike, DefaultMappingBuilder builder) { + builder + .intReq("BicyclePreferencesInput.boardCost", bike.boardCost()) + .floatReq("BicyclePreferencesInput.reluctance", bike.reluctance()) + .floatReq("BicyclePreferencesInput.speed", bike.speed()) + .objectReq( + "BicyclePreferencesInput.optimization", + mapVehicleOptimize( + bike.optimizeType(), + bike.optimizeTriangle(), + VehicleOptimizationTypeMapper::mapForBicycle + ) + ); + setBikeParkingDefaults(bike.parking(), builder); + setBikeRentalDefaults(bike.rental(), builder); + setBikeWalkingDefaults(bike.walking(), builder); + } + + private static void setBikeParkingDefaults( + VehicleParkingPreferences parking, + DefaultMappingBuilder builder + ) { + builder + .intReq( + "BicycleParkingPreferencesInput.unpreferredCost", + parking.unpreferredVehicleParkingTagCost().toSeconds() + ) + .arrayReq("BicycleParkingPreferencesInput.filters", mapVehicleParkingFilter(parking.filter())) + .arrayReq( + "BicycleParkingPreferencesInput.preferred", + mapVehicleParkingFilter(parking.preferred()) + ); + } + + private static void setBikeRentalDefaults( + VehicleRentalPreferences rental, + DefaultMappingBuilder builder + ) { + builder + .arrayStringsOpt( + "BicycleRentalPreferencesInput.allowedNetworks", + rental.allowedNetworks().isEmpty() ? null : rental.allowedNetworks() + ) + .arrayStringsReq("BicycleRentalPreferencesInput.bannedNetworks", rental.bannedNetworks()) + .boolReq( + "DestinationBicyclePolicyInput.allowKeeping", + rental.allowArrivingInRentedVehicleAtDestination() + ) + .intReq( + "DestinationBicyclePolicyInput.keepingCost", + rental.arrivingInRentalVehicleAtDestinationCost().toSeconds() + ); + } + + private static void setBikeWalkingDefaults( + VehicleWalkingPreferences walking, + DefaultMappingBuilder builder + ) { + builder + .intReq( + "BicycleWalkPreferencesCostInput.mountDismountCost", + walking.mountDismountCost().toSeconds() + ) + .floatReq("BicycleWalkPreferencesCostInput.reluctance", walking.reluctance()) + .stringReq("BicycleWalkPreferencesInput.mountDismountTime", walking.mountDismountTime()) + .floatReq("BicycleWalkPreferencesInput.speed", walking.speed()); + } + + private static void setCarDefaults(CarPreferences car, DefaultMappingBuilder builder) { + builder + .floatReq("CarPreferencesInput.reluctance", car.reluctance()) + .intReq("CarPreferencesInput.boardCost", car.boardCost()); + setCarParkingDefaults(car.parking(), builder); + setCarRentalDefaults(car.rental(), builder); + } + + private static void setCarParkingDefaults( + VehicleParkingPreferences parking, + DefaultMappingBuilder builder + ) { + builder + .intReq( + "CarParkingPreferencesInput.unpreferredCost", + parking.unpreferredVehicleParkingTagCost().toSeconds() + ) + .arrayReq("CarParkingPreferencesInput.filters", mapVehicleParkingFilter(parking.filter())) + .arrayReq( + "CarParkingPreferencesInput.preferred", + mapVehicleParkingFilter(parking.preferred()) + ); + } + + private static void setCarRentalDefaults( + VehicleRentalPreferences rental, + DefaultMappingBuilder builder + ) { + builder + .arrayStringsOpt( + "CarRentalPreferencesInput.allowedNetworks", + rental.allowedNetworks().isEmpty() ? null : rental.allowedNetworks() + ) + .arrayStringsReq("CarRentalPreferencesInput.bannedNetworks", rental.bannedNetworks()); + } + + private static void setModeDefaults( + JourneyRequest journey, + TransitPreferences transit, + DefaultMappingBuilder builder + ) { + builder + .enumListReq( + "PlanModesInput.direct", + StreetModeMapper + .getStreetModesForApi(journey.direct().mode()) + .stream() + .map(mode -> (Enum) DirectModeMapper.map(mode)) + .toList() + ) + .enumListReq( + "PlanTransitModesInput.access", + StreetModeMapper + .getStreetModesForApi(journey.access().mode()) + .stream() + .map(mode -> (Enum) AccessModeMapper.map(mode)) + .toList() + ) + .enumListReq( + "PlanTransitModesInput.egress", + StreetModeMapper + .getStreetModesForApi(journey.egress().mode()) + .stream() + .map(mode -> (Enum) EgressModeMapper.map(mode)) + .toList() + ) + .enumListReq( + "PlanTransitModesInput.transfer", + StreetModeMapper + .getStreetModesForApi(journey.transfer().mode()) + .stream() + .map(mode -> (Enum) TransferModeMapper.map(mode)) + .toList() + ) + .arrayReq("PlanTransitModesInput.transit", mapTransitModes(transit.reluctanceForMode())); + } + + private static void setScooterDefaults( + ScooterPreferences scooter, + DefaultMappingBuilder builder + ) { + builder + .floatReq("ScooterPreferencesInput.reluctance", scooter.reluctance()) + .floatReq("ScooterPreferencesInput.speed", scooter.speed()) + .objectReq( + "ScooterPreferencesInput.optimization", + mapVehicleOptimize( + scooter.optimizeType(), + scooter.optimizeTriangle(), + VehicleOptimizationTypeMapper::mapForScooter + ) + ); + setScooterRentalDefaults(scooter.rental(), builder); + } + + private static void setScooterRentalDefaults( + VehicleRentalPreferences rental, + DefaultMappingBuilder builder + ) { + builder + .arrayStringsOpt( + "ScooterRentalPreferencesInput.allowedNetworks", + rental.allowedNetworks().isEmpty() ? null : rental.allowedNetworks() + ) + .arrayStringsReq("ScooterRentalPreferencesInput.bannedNetworks", rental.bannedNetworks()) + .boolReq( + "DestinationScooterPolicyInput.allowKeeping", + rental.allowArrivingInRentedVehicleAtDestination() + ) + .intReq( + "DestinationScooterPolicyInput.keepingCost", + rental.arrivingInRentalVehicleAtDestinationCost().toSeconds() + ); + } + + private static void setTransitDefaults( + TransitPreferences transit, + DefaultMappingBuilder builder + ) { + builder + .stringReq("AlightPreferencesInput.slack", transit.alightSlack().defaultValue()) + .stringReq("BoardPreferencesInput.slack", transit.boardSlack().defaultValue()) + .boolReq("TimetablePreferencesInput.excludeRealTimeUpdates", transit.ignoreRealtimeUpdates()) + .boolReq( + "TimetablePreferencesInput.includePlannedCancellations", + transit.includePlannedCancellations() + ) + .boolReq( + "TimetablePreferencesInput.includeRealTimeCancellations", + transit.includeRealtimeCancellations() + ); + } + + private static void setTransferDefaults( + TransferPreferences transfer, + DefaultMappingBuilder builder + ) { + builder + .floatReq("BoardPreferencesInput.waitReluctance", transfer.waitReluctance()) + .intReq("TransferPreferencesInput.cost", transfer.cost()) + .intReq( + "TransferPreferencesInput.maximumAdditionalTransfers", + transfer.maxAdditionalTransfers() + ) + // Max transfers are wrong in the internal model but fixed in the API mapping + .intReq("TransferPreferencesInput.maximumTransfers", transfer.maxTransfers() - 1) + .stringReq("TransferPreferencesInput.slack", transfer.slack()); + } + + private static void setWalkDefaults(WalkPreferences walk, DefaultMappingBuilder builder) { + builder + .intReq("WalkPreferencesInput.boardCost", walk.boardCost()) + .floatReq("WalkPreferencesInput.reluctance", walk.reluctance()) + .floatReq("WalkPreferencesInput.safetyFactor", walk.safetyFactor()) + .floatReq("WalkPreferencesInput.speed", walk.speed()); + } + + private static void setWheelchairDefaults( + RouteRequest defaultRouteRequest, + DefaultMappingBuilder builder + ) { + builder.boolReq("WheelchairPreferencesInput.enabled", defaultRouteRequest.wheelchair()); + } + + private static ArrayValue mapTransitModes(Map reluctanceForMode) { + var modesWithReluctance = Arrays + .stream(GraphQLTypes.GraphQLTransitMode.values()) + .map(mode -> mapTransitMode(mode, reluctanceForMode.get(TransitModeMapper.map(mode)))) + .toList(); + return ArrayValue.newArrayValue().values(modesWithReluctance).build(); + } + + private static Value mapTransitMode( + GraphQLTypes.GraphQLTransitMode mode, + @Nullable Double reluctance + ) { + var objectBuilder = ObjectValue + .newObjectValue() + .objectField( + ObjectField.newObjectField().name("mode").value(EnumValue.of(mode.name())).build() + ); + if (reluctance != null) { + objectBuilder.objectField( + ObjectField + .newObjectField() + .name("cost") + .value( + ObjectValue + .newObjectValue() + .objectField( + ObjectField + .newObjectField() + .name("reluctance") + .value(FloatValue.of(reluctance)) + .build() + ) + .build() + ) + .build() + ); + } + return objectBuilder.build(); + } + + private static ObjectValue mapVehicleOptimize( + VehicleRoutingOptimizeType type, + TimeSlopeSafetyTriangle triangle, + Function typeMapper + ) { + var optimizationField = type == VehicleRoutingOptimizeType.TRIANGLE + ? ObjectField + .newObjectField() + .name("triangle") + .value( + ObjectValue + .newObjectValue() + .objectField( + ObjectField + .newObjectField() + .name("flatness") + .value(FloatValue.of(triangle.slope())) + .build() + ) + .objectField( + ObjectField + .newObjectField() + .name("safety") + .value(FloatValue.of(triangle.safety())) + .build() + ) + .objectField( + ObjectField + .newObjectField() + .name("time") + .value(FloatValue.of(triangle.time())) + .build() + ) + .build() + ) + .build() + : ObjectField + .newObjectField() + .name("type") + .value(EnumValue.of(typeMapper.apply(type).name())) + .build(); + return ObjectValue.newObjectValue().objectField(optimizationField).build(); + } + + private static ArrayValue mapVehicleParkingFilter(VehicleParkingFilter filter) { + var arrayBuilder = ArrayValue.newArrayValue(); + if (!filter.not().isEmpty() || !filter.select().isEmpty()) { + arrayBuilder.value( + ObjectValue + .newObjectValue() + .objectField(mapVehicleParkingSelects("not", filter.not())) + .objectField(mapVehicleParkingSelects("select", filter.select())) + .build() + ); + } + return arrayBuilder.build(); + } + + private static ObjectField mapVehicleParkingSelects( + String fieldName, + List selectList + ) { + var selects = selectList + .stream() + .map(select -> + (Value) ObjectValue + .newObjectValue() + .objectField( + ObjectField + .newObjectField() + .name("tags") + .value( + ArrayValue + .newArrayValue() + .values(select.tags().stream().map(tag -> (Value) StringValue.of(tag)).toList()) + .build() + ) + .build() + ) + .build() + ) + .toList(); + return ObjectField + .newObjectField() + .name(fieldName) + .value(ArrayValue.newArrayValue().values(selects).build()) + .build(); + } + + private static class DefaultMappingBuilder { + + private final Map> defaultValueForKey = new HashMap<>(); + + public DefaultMappingBuilder intReq(String key, int value) { + defaultValueForKey.put(key, IntValue.of(value)); + return this; + } + + public DefaultMappingBuilder floatReq(String key, double value) { + defaultValueForKey.put(key, FloatValue.of(value)); + return this; + } + + public DefaultMappingBuilder stringReq(String key, Object value) { + defaultValueForKey.put(key, StringValue.of(value.toString())); + return this; + } + + public DefaultMappingBuilder stringOpt(String key, @Nullable Object value) { + if (value != null) { + defaultValueForKey.put(key, StringValue.of(value.toString())); + } + return this; + } + + public DefaultMappingBuilder boolReq(String key, boolean value) { + defaultValueForKey.put(key, BooleanValue.of(value)); + return this; + } + + public DefaultMappingBuilder enumListReq(String key, List valueList) { + defaultValueForKey.put( + key, + ArrayValue + .newArrayValue() + .values((valueList.stream().map(value -> (Value) new EnumValue(value.name())).toList())) + .build() + ); + return this; + } + + public DefaultMappingBuilder objectReq(String key, ObjectValue value) { + defaultValueForKey.put(key, value); + return this; + } + + public DefaultMappingBuilder arrayReq(String key, ArrayValue value) { + defaultValueForKey.put(key, value); + return this; + } + + public DefaultMappingBuilder arrayStringsReq(String key, Collection values) { + defaultValueForKey.put( + key, + ArrayValue + .newArrayValue() + .values(values.stream().map(value -> (Value) StringValue.of(value)).toList()) + .build() + ); + return this; + } + + public DefaultMappingBuilder arrayStringsOpt(String key, @Nullable Collection values) { + if (values != null) { + defaultValueForKey.put( + key, + ArrayValue + .newArrayValue() + .values(values.stream().map(value -> (Value) StringValue.of(value)).toList()) + .build() + ); + } + return this; + } + + public Map> build() { + return defaultValueForKey; + } + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java b/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java index c3ca214b62f..9a3580f474c 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java @@ -1,5 +1,6 @@ package org.opentripplanner.apis.gtfs; +import graphql.schema.GraphQLSchema; import org.opentripplanner.routing.api.RoutingService; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.fares.FareService; @@ -17,6 +18,7 @@ public record GraphQLRequestContext( VehicleRentalService vehicleRentalService, VehicleParkingService vehicleParkingService, RealtimeVehicleService realTimeVehicleService, + GraphQLSchema schema, GraphFinder graphFinder, RouteRequest defaultRouteRequest ) { @@ -28,6 +30,7 @@ public static GraphQLRequestContext ofServerContext(OtpServerRequestContext cont context.vehicleRentalService(), context.vehicleParkingService(), context.realtimeVehicleService(), + context.schema(), context.graphFinder(), context.defaultRouteRequest() ); 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 fe63add7d49..3b4d6235369 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLUtils.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GraphQLUtils.java @@ -7,7 +7,6 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLFormFactor; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLInputField; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRoutingErrorCode; -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; @@ -16,7 +15,6 @@ import org.opentripplanner.routing.graphfinder.PlaceType; import org.opentripplanner.street.model.RentalFormFactor; import org.opentripplanner.transit.model.basic.Accessibility; -import org.opentripplanner.transit.model.basic.TransitMode; public class GraphQLUtils { @@ -51,26 +49,6 @@ public static GraphQLInputField toGraphQL(InputField inputField) { }; } - public static GraphQLTransitMode toGraphQL(TransitMode mode) { - if (mode == null) return null; - return switch (mode) { - case RAIL -> GraphQLTransitMode.RAIL; - case COACH -> GraphQLTransitMode.COACH; - case SUBWAY -> GraphQLTransitMode.SUBWAY; - case BUS -> GraphQLTransitMode.BUS; - case TRAM -> GraphQLTransitMode.TRAM; - case FERRY -> GraphQLTransitMode.FERRY; - case AIRPLANE -> GraphQLTransitMode.AIRPLANE; - case CABLE_CAR -> GraphQLTransitMode.CABLE_CAR; - case GONDOLA -> GraphQLTransitMode.GONDOLA; - case FUNICULAR -> GraphQLTransitMode.FUNICULAR; - case TROLLEYBUS -> GraphQLTransitMode.TROLLEYBUS; - case MONORAIL -> GraphQLTransitMode.MONORAIL; - case CARPOOL -> GraphQLTransitMode.CARPOOL; - case TAXI -> GraphQLTransitMode.TAXI; - }; - } - public static RentalFormFactor toModel(GraphQLFormFactor formFactor) { if (formFactor == null) return null; return switch (formFactor) { 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 721ad3e3ee7..afb6bfc8802 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -7,209 +7,22 @@ import graphql.execution.AbortExecutionException; import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; -import graphql.scalars.ExtendedScalars; -import graphql.schema.GraphQLSchema; -import graphql.schema.idl.RuntimeWiring; -import graphql.schema.idl.SchemaGenerator; -import graphql.schema.idl.SchemaParser; -import graphql.schema.idl.TypeDefinitionRegistry; import io.micrometer.core.instrument.Metrics; import jakarta.ws.rs.core.Response; -import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.opentripplanner.apis.gtfs.datafetchers.AgencyImpl; -import org.opentripplanner.apis.gtfs.datafetchers.AlertEntityTypeResolver; -import org.opentripplanner.apis.gtfs.datafetchers.AlertImpl; -import org.opentripplanner.apis.gtfs.datafetchers.BikeParkImpl; -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.EntranceImpl; -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; -import org.opentripplanner.apis.gtfs.datafetchers.PatternImpl; -import org.opentripplanner.apis.gtfs.datafetchers.PlaceImpl; -import org.opentripplanner.apis.gtfs.datafetchers.PlaceInterfaceTypeResolver; -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.RentalVehicleFuelImpl; -import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleImpl; -import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleTypeImpl; -import org.opentripplanner.apis.gtfs.datafetchers.RideHailingEstimateImpl; -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.StepFeatureTypeResolver; -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; -import org.opentripplanner.apis.gtfs.datafetchers.StopOnTripImpl; -import org.opentripplanner.apis.gtfs.datafetchers.StopRelationshipImpl; -import org.opentripplanner.apis.gtfs.datafetchers.StoptimeImpl; -import org.opentripplanner.apis.gtfs.datafetchers.StoptimesInPatternImpl; -import org.opentripplanner.apis.gtfs.datafetchers.SystemNoticeImpl; -import org.opentripplanner.apis.gtfs.datafetchers.TicketTypeImpl; -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; -import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalNetworkImpl; -import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalStationImpl; -import org.opentripplanner.apis.gtfs.datafetchers.debugOutputImpl; -import org.opentripplanner.apis.gtfs.datafetchers.elevationProfileComponentImpl; -import org.opentripplanner.apis.gtfs.datafetchers.placeAtDistanceImpl; -import org.opentripplanner.apis.gtfs.datafetchers.serviceTimeRangeImpl; -import org.opentripplanner.apis.gtfs.datafetchers.stepImpl; -import org.opentripplanner.apis.gtfs.datafetchers.stopAtDistanceImpl; -import org.opentripplanner.apis.gtfs.model.StopPosition; import org.opentripplanner.apis.support.graphql.LoggingDataFetcherExceptionHandler; import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.graphql.GraphQLResponseSerializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; class GtfsGraphQLIndex { - static final Logger LOG = LoggerFactory.getLogger(GtfsGraphQLIndex.class); - - private static final GraphQLSchema indexSchema = buildSchema(); - - protected static GraphQLSchema buildSchema() { - try { - URL url = Objects.requireNonNull(GtfsGraphQLIndex.class.getResource("schema.graphqls")); - TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(url.openStream()); - IntrospectionTypeWiring typeWiring = new IntrospectionTypeWiring(typeRegistry); - RuntimeWiring runtimeWiring = RuntimeWiring - .newRuntimeWiring() - .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(GraphQLScalars.RATIO_SCALAR) - .scalar(GraphQLScalars.COORDINATE_VALUE_SCALAR) - .scalar(GraphQLScalars.COST_SCALAR) - .scalar(GraphQLScalars.RELUCTANCE_SCALAR) - .scalar(GraphQLScalars.LOCAL_DATE_SCALAR) - .scalar(ExtendedScalars.GraphQLLong) - .scalar(ExtendedScalars.Locale) - .scalar( - ExtendedScalars - .newAliasedScalar("Speed") - .aliasedScalar(ExtendedScalars.NonNegativeFloat) - .build() - ) - .type("Node", type -> type.typeResolver(new NodeTypeResolver())) - .type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver())) - .type("RentalPlace", type -> type.typeResolver(new RentalPlaceTypeResolver())) - .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("StepFeature", type -> type.typeResolver(new StepFeatureTypeResolver())) - .type(typeWiring.build(AgencyImpl.class)) - .type(typeWiring.build(AlertImpl.class)) - .type(typeWiring.build(BikeParkImpl.class)) - .type(typeWiring.build(VehicleParkingImpl.class)) - .type(typeWiring.build(BikeRentalStationImpl.class)) - .type(typeWiring.build(CarParkImpl.class)) - .type(typeWiring.build(CoordinatesImpl.class)) - .type(typeWiring.build(debugOutputImpl.class)) - .type(typeWiring.build(DepartureRowImpl.class)) - .type(typeWiring.build(elevationProfileComponentImpl.class)) - .type(typeWiring.build(FeedImpl.class)) - .type(typeWiring.build(GeometryImpl.class)) - .type(typeWiring.build(ItineraryImpl.class)) - .type(typeWiring.build(LegImpl.class)) - .type(typeWiring.build(PatternImpl.class)) - .type(typeWiring.build(PlaceImpl.class)) - .type(typeWiring.build(placeAtDistanceImpl.class)) - .type(typeWiring.build(PlanConnectionImpl.class)) - .type(typeWiring.build(PlanImpl.class)) - .type(typeWiring.build(QueryTypeImpl.class)) - .type(typeWiring.build(RouteImpl.class)) - .type(typeWiring.build(serviceTimeRangeImpl.class)) - .type(typeWiring.build(stepImpl.class)) - .type(typeWiring.build(StopImpl.class)) - .type(typeWiring.build(stopAtDistanceImpl.class)) - .type(typeWiring.build(StoptimeImpl.class)) - .type(typeWiring.build(StoptimesInPatternImpl.class)) - .type(typeWiring.build(TicketTypeImpl.class)) - .type(typeWiring.build(TranslatedStringImpl.class)) - .type(typeWiring.build(TripImpl.class)) - .type(typeWiring.build(SystemNoticeImpl.class)) - .type(typeWiring.build(ContactInfoImpl.class)) - .type(typeWiring.build(BookingTimeImpl.class)) - .type(typeWiring.build(BookingInfoImpl.class)) - .type(typeWiring.build(VehicleRentalStationImpl.class)) - .type(typeWiring.build(VehicleRentalNetworkImpl.class)) - .type(typeWiring.build(RentalVehicleImpl.class)) - .type(typeWiring.build(RentalVehicleTypeImpl.class)) - .type(typeWiring.build(StopOnRouteImpl.class)) - .type(typeWiring.build(StopOnTripImpl.class)) - .type(typeWiring.build(UnknownImpl.class)) - .type(typeWiring.build(RouteTypeImpl.class)) - .type(typeWiring.build(RoutingErrorImpl.class)) - .type(typeWiring.build(StopGeometriesImpl.class)) - .type(typeWiring.build(VehiclePositionImpl.class)) - .type(typeWiring.build(StopRelationshipImpl.class)) - .type(typeWiring.build(OpeningHoursImpl.class)) - .type(typeWiring.build(RideHailingEstimateImpl.class)) - .type(typeWiring.build(MoneyImpl.class)) - .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)) - .type(typeWiring.build(EntranceImpl.class)) - .type(typeWiring.build(RentalVehicleFuelImpl.class)) - .build(); - SchemaGenerator schemaGenerator = new SchemaGenerator(); - return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring); - } catch (Exception e) { - LOG.error("Unable to build GTFS GraphQL Schema", e); - } - return null; - } - static ExecutionResult getGraphQLExecutionResult( String query, Map variables, @@ -230,7 +43,7 @@ static ExecutionResult getGraphQLExecutionResult( } GraphQL graphQL = GraphQL - .newGraphQL(indexSchema) + .newGraphQL(requestContext.schema()) .instrumentation(instrumentation) .defaultDataFetcherExceptionHandler(new LoggingDataFetcherExceptionHandler()) .build(); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java b/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java new file mode 100644 index 00000000000..6909d1dd98d --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/SchemaFactory.java @@ -0,0 +1,214 @@ +package org.opentripplanner.apis.gtfs; + +import graphql.scalars.ExtendedScalars; +import graphql.schema.GraphQLSchema; +import graphql.schema.SchemaTransformer; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import java.net.URL; +import java.util.Objects; +import org.opentripplanner.apis.gtfs.datafetchers.AgencyImpl; +import org.opentripplanner.apis.gtfs.datafetchers.AlertEntityTypeResolver; +import org.opentripplanner.apis.gtfs.datafetchers.AlertImpl; +import org.opentripplanner.apis.gtfs.datafetchers.BikeParkImpl; +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.EntranceImpl; +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; +import org.opentripplanner.apis.gtfs.datafetchers.PatternImpl; +import org.opentripplanner.apis.gtfs.datafetchers.PlaceImpl; +import org.opentripplanner.apis.gtfs.datafetchers.PlaceInterfaceTypeResolver; +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.RentalVehicleFuelImpl; +import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleImpl; +import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleTypeImpl; +import org.opentripplanner.apis.gtfs.datafetchers.RideHailingEstimateImpl; +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.StepFeatureTypeResolver; +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; +import org.opentripplanner.apis.gtfs.datafetchers.StopOnTripImpl; +import org.opentripplanner.apis.gtfs.datafetchers.StopRelationshipImpl; +import org.opentripplanner.apis.gtfs.datafetchers.StoptimeImpl; +import org.opentripplanner.apis.gtfs.datafetchers.StoptimesInPatternImpl; +import org.opentripplanner.apis.gtfs.datafetchers.SystemNoticeImpl; +import org.opentripplanner.apis.gtfs.datafetchers.TicketTypeImpl; +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; +import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalNetworkImpl; +import org.opentripplanner.apis.gtfs.datafetchers.VehicleRentalStationImpl; +import org.opentripplanner.apis.gtfs.datafetchers.debugOutputImpl; +import org.opentripplanner.apis.gtfs.datafetchers.elevationProfileComponentImpl; +import org.opentripplanner.apis.gtfs.datafetchers.placeAtDistanceImpl; +import org.opentripplanner.apis.gtfs.datafetchers.serviceTimeRangeImpl; +import org.opentripplanner.apis.gtfs.datafetchers.stepImpl; +import org.opentripplanner.apis.gtfs.datafetchers.stopAtDistanceImpl; +import org.opentripplanner.apis.gtfs.model.StopPosition; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Used to construct {@link GraphQLSchema} instances. + */ +public class SchemaFactory { + + static final Logger LOG = LoggerFactory.getLogger(SchemaFactory.class); + + /** + * Creates schema from schema file and injects default values from code/configuration. + * + * @param defaultRouteRequest used to inject defaults into the schema. + */ + public static GraphQLSchema createSchemaWithDefaultInjection(RouteRequest defaultRouteRequest) { + var originalSchema = createSchema(); + return SchemaTransformer.transformSchema( + originalSchema, + new DefaultValueInjector(defaultRouteRequest) + ); + } + + /** + * Creates schema from schema file without injecting default values from code/configuration. This + * is meant for formatting the schema without editing it or for testing without default + * injection. + */ + public static GraphQLSchema createSchema() { + try { + URL url = Objects.requireNonNull(SchemaFactory.class.getResource("schema.graphqls")); + TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(url.openStream()); + IntrospectionTypeWiring typeWiring = new IntrospectionTypeWiring(typeRegistry); + RuntimeWiring runtimeWiring = RuntimeWiring + .newRuntimeWiring() + .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(GraphQLScalars.RATIO_SCALAR) + .scalar(GraphQLScalars.COORDINATE_VALUE_SCALAR) + .scalar(GraphQLScalars.COST_SCALAR) + .scalar(GraphQLScalars.RELUCTANCE_SCALAR) + .scalar(GraphQLScalars.LOCAL_DATE_SCALAR) + .scalar(ExtendedScalars.GraphQLLong) + .scalar(ExtendedScalars.Locale) + .scalar( + ExtendedScalars + .newAliasedScalar("Speed") + .aliasedScalar(ExtendedScalars.NonNegativeFloat) + .build() + ) + .type("Node", type -> type.typeResolver(new NodeTypeResolver())) + .type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver())) + .type("RentalPlace", type -> type.typeResolver(new RentalPlaceTypeResolver())) + .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("StepFeature", type -> type.typeResolver(new StepFeatureTypeResolver())) + .type(typeWiring.build(AgencyImpl.class)) + .type(typeWiring.build(AlertImpl.class)) + .type(typeWiring.build(BikeParkImpl.class)) + .type(typeWiring.build(VehicleParkingImpl.class)) + .type(typeWiring.build(BikeRentalStationImpl.class)) + .type(typeWiring.build(CarParkImpl.class)) + .type(typeWiring.build(CoordinatesImpl.class)) + .type(typeWiring.build(debugOutputImpl.class)) + .type(typeWiring.build(DepartureRowImpl.class)) + .type(typeWiring.build(elevationProfileComponentImpl.class)) + .type(typeWiring.build(FeedImpl.class)) + .type(typeWiring.build(GeometryImpl.class)) + .type(typeWiring.build(ItineraryImpl.class)) + .type(typeWiring.build(LegImpl.class)) + .type(typeWiring.build(PatternImpl.class)) + .type(typeWiring.build(PlaceImpl.class)) + .type(typeWiring.build(placeAtDistanceImpl.class)) + .type(typeWiring.build(PlanConnectionImpl.class)) + .type(typeWiring.build(PlanImpl.class)) + .type(typeWiring.build(QueryTypeImpl.class)) + .type(typeWiring.build(RouteImpl.class)) + .type(typeWiring.build(serviceTimeRangeImpl.class)) + .type(typeWiring.build(stepImpl.class)) + .type(typeWiring.build(StopImpl.class)) + .type(typeWiring.build(stopAtDistanceImpl.class)) + .type(typeWiring.build(StoptimeImpl.class)) + .type(typeWiring.build(StoptimesInPatternImpl.class)) + .type(typeWiring.build(TicketTypeImpl.class)) + .type(typeWiring.build(TranslatedStringImpl.class)) + .type(typeWiring.build(TripImpl.class)) + .type(typeWiring.build(SystemNoticeImpl.class)) + .type(typeWiring.build(ContactInfoImpl.class)) + .type(typeWiring.build(BookingTimeImpl.class)) + .type(typeWiring.build(BookingInfoImpl.class)) + .type(typeWiring.build(VehicleRentalStationImpl.class)) + .type(typeWiring.build(VehicleRentalNetworkImpl.class)) + .type(typeWiring.build(RentalVehicleImpl.class)) + .type(typeWiring.build(RentalVehicleTypeImpl.class)) + .type(typeWiring.build(StopOnRouteImpl.class)) + .type(typeWiring.build(StopOnTripImpl.class)) + .type(typeWiring.build(UnknownImpl.class)) + .type(typeWiring.build(RouteTypeImpl.class)) + .type(typeWiring.build(RoutingErrorImpl.class)) + .type(typeWiring.build(StopGeometriesImpl.class)) + .type(typeWiring.build(VehiclePositionImpl.class)) + .type(typeWiring.build(StopRelationshipImpl.class)) + .type(typeWiring.build(OpeningHoursImpl.class)) + .type(typeWiring.build(RideHailingEstimateImpl.class)) + .type(typeWiring.build(MoneyImpl.class)) + .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)) + .type(typeWiring.build(EntranceImpl.class)) + .type(typeWiring.build(RentalVehicleFuelImpl.class)) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring); + } catch (Exception e) { + LOG.error("Unable to build GTFS GraphQL Schema", e); + return null; + } + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/configure/SchemaModule.java b/application/src/main/java/org/opentripplanner/apis/gtfs/configure/SchemaModule.java new file mode 100644 index 00000000000..45db5a366dd --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/configure/SchemaModule.java @@ -0,0 +1,29 @@ +package org.opentripplanner.apis.gtfs.configure; + +import dagger.Module; +import dagger.Provides; +import graphql.schema.GraphQLSchema; +import jakarta.inject.Singleton; +import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.SchemaFactory; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.routing.api.request.RouteRequest; + +/** + * The schema is used during application serve phase, not loading, and it depends on the default + * route request, which is injected from the + * {@link org.opentripplanner.standalone.config.RouterConfig}. The {@link GraphQLSchema} is only + * constructed if the API feature flag is on. + */ +@Module +public class SchemaModule { + + @Provides + @Singleton + @Nullable + public GraphQLSchema provideSchema(RouteRequest defaultRouteRequest) { + return OTPFeature.GtfsGraphQlApi.isOn() + ? SchemaFactory.createSchemaWithDefaultInjection(defaultRouteRequest) + : null; + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java index 9b98b71bd79..4fb3548eb63 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java @@ -8,12 +8,12 @@ import java.util.List; import java.util.stream.Collectors; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; -import org.opentripplanner.apis.gtfs.GraphQLUtils; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; import org.opentripplanner.apis.gtfs.mapping.BikesAllowedMapper; +import org.opentripplanner.apis.gtfs.mapping.TransitModeMapper; import org.opentripplanner.apis.gtfs.support.filter.PatternByDateFilterUtil; import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; import org.opentripplanner.routing.alertpatch.EntitySelector; @@ -171,7 +171,7 @@ public DataFetcher longName() { @Override public DataFetcher mode() { - return environment -> GraphQLUtils.toGraphQL(getSource(environment).getMode()); + return environment -> TransitModeMapper.map(getSource(environment).getMode()); } @Override diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/TransitModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/TransitModeMapper.java index 2128263d8de..a736379e394 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/TransitModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/TransitModeMapper.java @@ -4,7 +4,7 @@ import org.opentripplanner.transit.model.basic.TransitMode; /** - * Maps transit mode from API to internal model. + * Maps transit mode from API to internal model or vice versa. */ public class TransitModeMapper { @@ -26,4 +26,23 @@ public static TransitMode map(GraphQLTypes.GraphQLTransitMode mode) { case MONORAIL -> TransitMode.MONORAIL; }; } + + public static GraphQLTypes.GraphQLTransitMode map(TransitMode mode) { + return switch (mode) { + case AIRPLANE -> GraphQLTypes.GraphQLTransitMode.AIRPLANE; + case BUS -> GraphQLTypes.GraphQLTransitMode.BUS; + case CABLE_CAR -> GraphQLTypes.GraphQLTransitMode.CABLE_CAR; + case COACH -> GraphQLTypes.GraphQLTransitMode.COACH; + case FERRY -> GraphQLTypes.GraphQLTransitMode.FERRY; + case FUNICULAR -> GraphQLTypes.GraphQLTransitMode.FUNICULAR; + case GONDOLA -> GraphQLTypes.GraphQLTransitMode.GONDOLA; + case RAIL -> GraphQLTypes.GraphQLTransitMode.RAIL; + case SUBWAY -> GraphQLTypes.GraphQLTransitMode.SUBWAY; + case TRAM -> GraphQLTypes.GraphQLTransitMode.TRAM; + case CARPOOL -> GraphQLTypes.GraphQLTransitMode.CARPOOL; + case TAXI -> GraphQLTypes.GraphQLTransitMode.TAXI; + case TROLLEYBUS -> GraphQLTypes.GraphQLTransitMode.TROLLEYBUS; + case MONORAIL -> GraphQLTypes.GraphQLTransitMode.MONORAIL; + }; + } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java index e0e3ac0bbb2..631a6017153 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java @@ -4,7 +4,7 @@ import org.opentripplanner.routing.api.request.StreetMode; /** - * Maps access street mode from API to internal model. + * Maps access street mode from API to internal model or vice versa. */ public class AccessModeMapper { @@ -22,4 +22,19 @@ public static StreetMode map(GraphQLTypes.GraphQLPlanAccessMode mode) { case WALK -> StreetMode.WALK; }; } + + public static GraphQLTypes.GraphQLPlanAccessMode map(StreetMode mode) { + return switch (mode) { + case BIKE -> GraphQLTypes.GraphQLPlanAccessMode.BICYCLE; + case BIKE_RENTAL -> GraphQLTypes.GraphQLPlanAccessMode.BICYCLE_RENTAL; + case BIKE_TO_PARK -> GraphQLTypes.GraphQLPlanAccessMode.BICYCLE_PARKING; + case CAR -> GraphQLTypes.GraphQLPlanAccessMode.CAR; + case CAR_RENTAL -> GraphQLTypes.GraphQLPlanAccessMode.CAR_RENTAL; + case CAR_TO_PARK -> GraphQLTypes.GraphQLPlanAccessMode.CAR_PARKING; + case CAR_PICKUP -> GraphQLTypes.GraphQLPlanAccessMode.CAR_DROP_OFF; + case FLEXIBLE -> GraphQLTypes.GraphQLPlanAccessMode.FLEX; + case SCOOTER_RENTAL -> GraphQLTypes.GraphQLPlanAccessMode.SCOOTER_RENTAL; + case WALK, CAR_HAILING, NOT_SET -> GraphQLTypes.GraphQLPlanAccessMode.WALK; + }; + } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/DirectModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/DirectModeMapper.java index e34e7ad2aed..0285c8b1427 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/DirectModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/DirectModeMapper.java @@ -4,7 +4,7 @@ import org.opentripplanner.routing.api.request.StreetMode; /** - * Maps direct street mode from API to internal model. + * Maps direct street mode from API to internal model or vice versa. */ public class DirectModeMapper { @@ -21,4 +21,18 @@ public static StreetMode map(GraphQLTypes.GraphQLPlanDirectMode mode) { case WALK -> StreetMode.WALK; }; } + + public static GraphQLTypes.GraphQLPlanDirectMode map(StreetMode mode) { + return switch (mode) { + case BIKE -> GraphQLTypes.GraphQLPlanDirectMode.BICYCLE; + case BIKE_RENTAL -> GraphQLTypes.GraphQLPlanDirectMode.BICYCLE_RENTAL; + case BIKE_TO_PARK -> GraphQLTypes.GraphQLPlanDirectMode.BICYCLE_PARKING; + case CAR -> GraphQLTypes.GraphQLPlanDirectMode.CAR; + case CAR_RENTAL -> GraphQLTypes.GraphQLPlanDirectMode.CAR_RENTAL; + case CAR_TO_PARK -> GraphQLTypes.GraphQLPlanDirectMode.CAR_PARKING; + case FLEXIBLE -> GraphQLTypes.GraphQLPlanDirectMode.FLEX; + case SCOOTER_RENTAL -> GraphQLTypes.GraphQLPlanDirectMode.SCOOTER_RENTAL; + case WALK, CAR_HAILING, CAR_PICKUP, NOT_SET -> GraphQLTypes.GraphQLPlanDirectMode.WALK; + }; + } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java index ddcaa255f2a..53da10a7177 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java @@ -4,7 +4,7 @@ import org.opentripplanner.routing.api.request.StreetMode; /** - * Maps egress street mode from API to internal model. + * Maps egress street mode from API to internal model or vice versa. */ public class EgressModeMapper { @@ -20,4 +20,21 @@ public static StreetMode map(GraphQLTypes.GraphQLPlanEgressMode mode) { case WALK -> StreetMode.WALK; }; } + + public static GraphQLTypes.GraphQLPlanEgressMode map(StreetMode mode) { + return switch (mode) { + case BIKE -> GraphQLTypes.GraphQLPlanEgressMode.BICYCLE; + case BIKE_RENTAL -> GraphQLTypes.GraphQLPlanEgressMode.BICYCLE_RENTAL; + case CAR -> GraphQLTypes.GraphQLPlanEgressMode.CAR; + case CAR_RENTAL -> GraphQLTypes.GraphQLPlanEgressMode.CAR_RENTAL; + case CAR_PICKUP -> GraphQLTypes.GraphQLPlanEgressMode.CAR_PICKUP; + case FLEXIBLE -> GraphQLTypes.GraphQLPlanEgressMode.FLEX; + case SCOOTER_RENTAL -> GraphQLTypes.GraphQLPlanEgressMode.SCOOTER_RENTAL; + case WALK, + CAR_HAILING, + CAR_TO_PARK, + BIKE_TO_PARK, + NOT_SET -> GraphQLTypes.GraphQLPlanEgressMode.WALK; + }; + } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java index 663e93acca9..1355d9f1009 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java @@ -1,11 +1,11 @@ package org.opentripplanner.apis.gtfs.mapping.routerequest; import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.getTransitModes; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.StreetModeMapper.getStreetModeForRouting; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.StreetModeMapper.validateStreetModes; import graphql.schema.DataFetchingEnvironment; -import java.util.HashSet; import java.util.List; -import java.util.Set; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.mapping.TransitModeMapper; import org.opentripplanner.routing.api.request.StreetMode; @@ -32,7 +32,7 @@ static void setModes( throw new IllegalArgumentException("Direct modes must not be empty."); } var streetModes = direct.stream().map(DirectModeMapper::map).toList(); - journey.direct().setMode(getStreetMode(streetModes)); + journey.direct().setMode(getStreetModeForRouting(streetModes)); } var transit = modesInput.getGraphQLTransit(); @@ -45,7 +45,7 @@ static void setModes( throw new IllegalArgumentException("Access modes must not be empty."); } var streetModes = access.stream().map(AccessModeMapper::map).toList(); - journey.access().setMode(getStreetMode(streetModes)); + journey.access().setMode(getStreetModeForRouting(streetModes)); } var egress = transit.getGraphQLEgress(); @@ -54,7 +54,7 @@ static void setModes( throw new IllegalArgumentException("Egress modes must not be empty."); } var streetModes = egress.stream().map(EgressModeMapper::map).toList(); - journey.egress().setMode(getStreetMode(streetModes)); + journey.egress().setMode(getStreetModeForRouting(streetModes)); } var transfer = transit.getGraphQLTransfer(); @@ -63,7 +63,7 @@ static void setModes( throw new IllegalArgumentException("Transfer modes must not be empty."); } var streetModes = transfer.stream().map(TransferModeMapper::map).toList(); - journey.transfer().setMode(getStreetMode(streetModes)); + journey.transfer().setMode(getStreetModeForRouting(streetModes)); } validateStreetModes(journey); @@ -90,88 +90,4 @@ static void setModes( } } } - - /** - * Current support: - * 1. If only one mode is defined, it needs to be WALK, BICYCLE, CAR or some parking mode. - * 2. If two modes are defined, they can't be BICYCLE or CAR, and WALK needs to be one of them. - * 3. More than two modes can't be defined for the same leg. - *

- * TODO future support: - * 1. Any mode can be defined alone. If it's not used in a leg, the leg gets filtered away. - * 2. If two modes are defined, they can't be BICYCLE or CAR. Usually WALK is required as the second - * mode but in some cases it's possible to define other modes as well such as BICYCLE_RENTAL together - * with SCOOTER_RENTAL. In that case, legs which don't use BICYCLE_RENTAL or SCOOTER_RENTAL would be filtered - * out. - * 3. When more than two modes are used, some combinations are supported such as WALK, BICYCLE_RENTAL and SCOOTER_RENTAL. - */ - private static StreetMode getStreetMode(List modes) { - if (modes.size() > 2) { - throw new IllegalArgumentException( - "Only one or two modes can be specified for a leg, got: %.".formatted(modes) - ); - } - if (modes.size() == 1) { - var mode = modes.getFirst(); - // TODO in the future, we will support defining other modes alone as well and filter out legs - // which don't contain the only specified mode as opposed to also returning legs which contain - // only walking. - if (!isAlwaysPresentInLeg(mode)) { - throw new IllegalArgumentException( - "For the time being, %s needs to be combined with WALK mode for the same leg.".formatted( - mode - ) - ); - } - return mode; - } - if (modes.contains(StreetMode.BIKE)) { - throw new IllegalArgumentException( - "Bicycle can't be combined with other modes for the same leg: %s.".formatted(modes) - ); - } - if (modes.contains(StreetMode.CAR)) { - throw new IllegalArgumentException( - "Car can't be combined with other modes for the same leg: %s.".formatted(modes) - ); - } - if (!modes.contains(StreetMode.WALK)) { - throw new IllegalArgumentException( - "For the time being, WALK needs to be added as a mode for a leg when using %s and these two can't be used in the same leg.".formatted( - modes - ) - ); - } - // Walk is currently always used as an implied mode when mode is not car. - return modes.stream().filter(mode -> mode != StreetMode.WALK).findFirst().get(); - } - - private static boolean isAlwaysPresentInLeg(StreetMode mode) { - return ( - mode == StreetMode.BIKE || - mode == StreetMode.CAR || - mode == StreetMode.WALK || - mode.includesParking() - ); - } - - /** - * TODO this doesn't support multiple street modes yet - */ - private static void validateStreetModes(JourneyRequest journey) { - Set modes = new HashSet(); - modes.add(journey.access().mode()); - modes.add(journey.egress().mode()); - modes.add(journey.transfer().mode()); - if (modes.contains(StreetMode.BIKE) && modes.size() != 1) { - throw new IllegalArgumentException( - "If BICYCLE is used for access, egress or transfer, then it should be used for all." - ); - } - if (modes.contains(StreetMode.CAR) && modes.size() != 1) { - throw new IllegalArgumentException( - "If CAR is used for access, egress or transfer, then it should be used for all." - ); - } - } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapper.java index 23e42f9a70c..718005f4b76 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapper.java @@ -41,11 +41,11 @@ public static RouteRequest toRouteRequest( request.setFrom(parseGenericLocation(args.getGraphQLOrigin())); request.setTo(parseGenericLocation(args.getGraphQLDestination())); request.setLocale(GraphQLUtils.getLocale(environment, args.getGraphQLLocale())); - if (args.getGraphQLSearchWindow() != null) { - request.setSearchWindow( - DurationUtils.requireNonNegativeMax2days(args.getGraphQLSearchWindow(), "searchWindow") - ); - } + request.setSearchWindow( + args.getGraphQLSearchWindow() != null + ? DurationUtils.requireNonNegativeMax2days(args.getGraphQLSearchWindow(), "searchWindow") + : null + ); if (args.getGraphQLBefore() != null) { request.setPageCursorFromEncoded(args.getGraphQLBefore()); diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/StreetModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/StreetModeMapper.java new file mode 100644 index 00000000000..3d13ee12421 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/StreetModeMapper.java @@ -0,0 +1,111 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.routing.api.request.request.JourneyRequest; + +/** + * Mapping and validation methods for StreetModes. + */ +public class StreetModeMapper { + + /** + * This is meant to be used when mapping from StreetModes provided in API calls to + * {@link JourneyRequest} StreetMode. Current support: + * 1. If only one mode is defined, it needs to be WALK, BICYCLE, CAR or some parking mode. + * 2. If two modes are defined, they can't be BICYCLE or CAR, and WALK needs to be one of them. + * 3. More than two modes can't be defined for the same leg. + *

+ * TODO future support: + * 1. Any mode can be defined alone. If it's not used in a leg, the leg gets filtered away. + * 2. If two modes are defined, they can't be BICYCLE or CAR. Usually WALK is required as the second + * mode but in some cases it's possible to define other modes as well such as BICYCLE_RENTAL together + * with SCOOTER_RENTAL. In that case, legs which don't use BICYCLE_RENTAL or SCOOTER_RENTAL would be filtered + * out. + * 3. When more than two modes are used, some combinations are supported such as WALK, BICYCLE_RENTAL and SCOOTER_RENTAL. + */ + public static StreetMode getStreetModeForRouting(List modes) { + if (modes.size() > 2) { + throw new IllegalArgumentException( + "Only one or two modes can be specified for a leg, got: %.".formatted(modes) + ); + } + if (modes.size() == 1) { + var mode = modes.getFirst(); + // TODO in the future, we will support defining other modes alone as well and filter out legs + // which don't contain the only specified mode as opposed to also returning legs which contain + // only walking. + if (!isAlwaysPresentInLeg(mode)) { + throw new IllegalArgumentException( + "For the time being, %s needs to be combined with WALK mode for the same leg.".formatted( + mode + ) + ); + } + return mode; + } + if (modes.contains(StreetMode.BIKE)) { + throw new IllegalArgumentException( + "Bicycle can't be combined with other modes for the same leg: %s.".formatted(modes) + ); + } + if (modes.contains(StreetMode.CAR)) { + throw new IllegalArgumentException( + "Car can't be combined with other modes for the same leg: %s.".formatted(modes) + ); + } + if (!modes.contains(StreetMode.WALK)) { + throw new IllegalArgumentException( + "For the time being, WALK needs to be added as a mode for a leg when using %s and these two can't be used in the same leg.".formatted( + modes + ) + ); + } + // Walk is currently always used as an implied mode when mode is not car. + return modes.stream().filter(mode -> mode != StreetMode.WALK).findFirst().get(); + } + + /** + * This is meant to be used when mapping from {@link JourneyRequest} StreetMode into StreetMode + * combinations currently used by the API. The logic is as follows: + * 1. If the mode is WALK, BICYCLE, CAR or some parking mode, then it is returned alone. + * 2. Otherwise, return WALK + the mode. + */ + public static List getStreetModesForApi(StreetMode mode) { + if (isAlwaysPresentInLeg(mode)) { + return List.of(mode); + } + return List.of(StreetMode.WALK, mode); + } + + /** + * TODO this doesn't support multiple street modes yet + */ + public static void validateStreetModes(JourneyRequest journey) { + Set modes = new HashSet(); + modes.add(journey.access().mode()); + modes.add(journey.egress().mode()); + modes.add(journey.transfer().mode()); + if (modes.contains(StreetMode.BIKE) && modes.size() != 1) { + throw new IllegalArgumentException( + "If BICYCLE is used for access, egress or transfer, then it should be used for all." + ); + } + if (modes.contains(StreetMode.CAR) && modes.size() != 1) { + throw new IllegalArgumentException( + "If CAR is used for access, egress or transfer, then it should be used for all." + ); + } + } + + private static boolean isAlwaysPresentInLeg(StreetMode mode) { + return ( + mode == StreetMode.BIKE || + mode == StreetMode.CAR || + mode == StreetMode.WALK || + mode.includesParking() + ); + } +} diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java index 18d2c0e3811..4329ae695bd 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java @@ -4,7 +4,7 @@ import org.opentripplanner.routing.api.request.StreetMode; /** - * Maps transfer street mode from API to internal model. + * Maps transfer street mode from API to internal model or vice versa. */ public class TransferModeMapper { @@ -15,4 +15,21 @@ public static StreetMode map(GraphQLTypes.GraphQLPlanTransferMode mode) { case WALK -> StreetMode.WALK; }; } + + public static GraphQLTypes.GraphQLPlanTransferMode map(StreetMode mode) { + return switch (mode) { + case BIKE -> GraphQLTypes.GraphQLPlanTransferMode.BICYCLE; + case CAR -> GraphQLTypes.GraphQLPlanTransferMode.CAR; + case WALK, + BIKE_RENTAL, + CAR_HAILING, + CAR_RENTAL, + CAR_PICKUP, + CAR_TO_PARK, + BIKE_TO_PARK, + FLEXIBLE, + SCOOTER_RENTAL, + NOT_SET -> GraphQLTypes.GraphQLPlanTransferMode.WALK; + }; + } } diff --git a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/VehicleOptimizationTypeMapper.java b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/VehicleOptimizationTypeMapper.java index c47cbd3cf7a..2ae2807b392 100644 --- a/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/VehicleOptimizationTypeMapper.java +++ b/application/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/VehicleOptimizationTypeMapper.java @@ -1,5 +1,6 @@ package org.opentripplanner.apis.gtfs.mapping.routerequest; +import javax.annotation.Nullable; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; @@ -25,4 +26,30 @@ public static VehicleRoutingOptimizeType map(GraphQLTypes.GraphQLScooterOptimiza case SAFEST_STREETS -> VehicleRoutingOptimizeType.SAFEST_STREETS; }; } + + @Nullable + public static GraphQLTypes.GraphQLCyclingOptimizationType mapForBicycle( + VehicleRoutingOptimizeType type + ) { + return switch (type) { + case SHORTEST_DURATION -> GraphQLTypes.GraphQLCyclingOptimizationType.SHORTEST_DURATION; + case FLAT_STREETS -> GraphQLTypes.GraphQLCyclingOptimizationType.FLAT_STREETS; + case SAFE_STREETS -> GraphQLTypes.GraphQLCyclingOptimizationType.SAFE_STREETS; + case SAFEST_STREETS -> GraphQLTypes.GraphQLCyclingOptimizationType.SAFEST_STREETS; + case TRIANGLE -> null; + }; + } + + @Nullable + public static GraphQLTypes.GraphQLScooterOptimizationType mapForScooter( + VehicleRoutingOptimizeType type + ) { + return switch (type) { + case SHORTEST_DURATION -> GraphQLTypes.GraphQLScooterOptimizationType.SHORTEST_DURATION; + case FLAT_STREETS -> GraphQLTypes.GraphQLScooterOptimizationType.FLAT_STREETS; + case SAFE_STREETS -> GraphQLTypes.GraphQLScooterOptimizationType.SAFE_STREETS; + case SAFEST_STREETS -> GraphQLTypes.GraphQLScooterOptimizationType.SAFEST_STREETS; + case TRIANGLE -> null; + }; + } } diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java b/application/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java index 4e36b1b58db..daafe70f9e7 100644 --- a/application/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java +++ b/application/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java @@ -320,7 +320,7 @@ public Duration searchWindow() { return searchWindow; } - public void setSearchWindow(Duration searchWindow) { + public void setSearchWindow(@Nullable Duration searchWindow) { if (searchWindow != null) { if (hasMaxSearchWindow() && searchWindow.toSeconds() > maxSearchWindow.toSeconds()) { throw new IllegalArgumentException("The search window cannot exceed " + maxSearchWindow); diff --git a/application/src/main/java/org/opentripplanner/routing/api/request/preference/filter/VehicleParkingSelect.java b/application/src/main/java/org/opentripplanner/routing/api/request/preference/filter/VehicleParkingSelect.java index d30f0c6795d..de8e01d8ebf 100644 --- a/application/src/main/java/org/opentripplanner/routing/api/request/preference/filter/VehicleParkingSelect.java +++ b/application/src/main/java/org/opentripplanner/routing/api/request/preference/filter/VehicleParkingSelect.java @@ -1,6 +1,7 @@ package org.opentripplanner.routing.api.request.preference.filter; import java.util.Collections; +import java.util.List; import java.util.Set; import org.opentripplanner.service.vehicleparking.model.VehicleParking; @@ -19,7 +20,23 @@ public sealed interface VehicleParkingSelect { */ boolean isEmpty(); - record TagsSelect(Set tags) implements VehicleParkingSelect { + /** + * Get the tags from the select. Is not meant for checking for matches. + */ + List tags(); + + final class TagsSelect implements VehicleParkingSelect { + + private final Set tags; + + public TagsSelect(Set tags) { + this.tags = tags; + } + + public List tags() { + return tags.stream().toList(); + } + @Override public boolean matches(VehicleParking p) { return !Collections.disjoint(tags, p.getTags()); diff --git a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index 139bac6dcc9..1884032c952 100644 --- a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -1,5 +1,6 @@ package org.opentripplanner.standalone.api; +import graphql.schema.GraphQLSchema; import io.micrometer.core.instrument.MeterRegistry; import java.util.List; import java.util.Locale; @@ -153,4 +154,7 @@ default DataOverlayContext dataOverlayContext(RouteRequest request) { @Nullable SorlandsbanenNorwayService sorlandsbanenService(); + + @Nullable + GraphQLSchema schema(); } diff --git a/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java index 4a623f02c3b..5d49f53a12b 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/routerequest/RouteRequestConfig.java @@ -82,10 +82,12 @@ public static RouteRequest mapRouteRequest(NodeAdapter c, RouteRequest dft) { c .of("modes") .since(V2_0) - .summary("The set of access/egress/direct/transit modes to be used for the route search.") + .summary( + "The set of access/egress/direct/transfer modes (separated by a comma) to be used for the route search." + ) .asCustomStringType( RequestModes.defaultRequestModes(), - "TRANSIT,WALK", + "WALK", s -> new QualifiedModeSet(s).getRequestModes() ) ); diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index f2edde933e9..7a664e8db77 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -109,6 +109,7 @@ public class ConstructApplication { .dataImportIssueSummary(issueSummary) .stopConsolidationRepository(stopConsolidationRepository) .streetLimitationParameters(streetLimitationParameters) + .schema(config.routerConfig().routingRequestDefaults()) .build(); } diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index 6b2fdff0947..102b6312df9 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -2,8 +2,10 @@ import dagger.BindsInstance; import dagger.Component; +import graphql.schema.GraphQLSchema; import jakarta.inject.Singleton; import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.configure.SchemaModule; import org.opentripplanner.ext.emissions.EmissionsDataModel; import org.opentripplanner.ext.emissions.EmissionsServiceModule; import org.opentripplanner.ext.geocoder.LuceneIndex; @@ -17,6 +19,7 @@ import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; +import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; @@ -64,6 +67,7 @@ VehicleRentalRepositoryModule.class, VehicleRentalServiceModule.class, SorlandsbanenNorwayModule.class, + SchemaModule.class, StopConsolidationServiceModule.class, StreetLimitationParametersServiceModule.class, WorldEnvelopeServiceModule.class, @@ -104,6 +108,9 @@ public interface ConstructApplicationFactory { @Nullable SorlandsbanenNorwayService enturSorlandsbanenService(); + @Nullable + GraphQLSchema schema(); + @Nullable LuceneIndex luceneIndex(); @@ -138,6 +145,9 @@ Builder stopConsolidationRepository( @BindsInstance Builder emissionsDataModel(EmissionsDataModel emissionsDataModel); + @BindsInstance + Builder schema(RouteRequest defaultRouteRequest); + @BindsInstance Builder streetLimitationParameters(StreetLimitationParameters streetLimitationParameters); diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index 916ba834d68..7893b715d61 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -2,6 +2,7 @@ import dagger.Module; import dagger.Provides; +import graphql.schema.GraphQLSchema; import io.micrometer.core.instrument.Metrics; import java.util.List; import javax.annotation.Nullable; @@ -47,6 +48,7 @@ OtpServerRequestContext providesServerContext( StreetLimitationParametersService streetLimitationParametersService, @Nullable TraverseVisitor traverseVisitor, EmissionsService emissionsService, + @Nullable GraphQLSchema schema, @Nullable SorlandsbanenNorwayService sorlandsbanenService, LauncherRequestDecorator launcherRequestDecorator, @Nullable LuceneIndex luceneIndex @@ -78,6 +80,7 @@ OtpServerRequestContext providesServerContext( worldEnvelopeService, emissionsService, luceneIndex, + schema, sorlandsbanenService, stopConsolidationService, tileRendererManager, diff --git a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index 6112c3fc1bf..7f42c066c22 100644 --- a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -1,5 +1,6 @@ package org.opentripplanner.standalone.server; +import graphql.schema.GraphQLSchema; import io.micrometer.core.instrument.MeterRegistry; import java.util.List; import java.util.Locale; @@ -65,6 +66,9 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { @Nullable private final TileRendererManager tileRendererManager; + @Nullable + private final GraphQLSchema schema; + @Nullable private final SorlandsbanenNorwayService sorlandsbanenService; @@ -101,6 +105,7 @@ public DefaultServerRequestContext( WorldEnvelopeService worldEnvelopeService, @Nullable EmissionsService emissionsService, @Nullable LuceneIndex luceneIndex, + @Nullable GraphQLSchema schema, @Nullable SorlandsbanenNorwayService sorlandsbanenService, @Nullable StopConsolidationService stopConsolidationService, @Nullable TileRendererManager tileRendererManager, @@ -125,6 +130,7 @@ public DefaultServerRequestContext( // Optional fields this.emissionsService = emissionsService; this.luceneIndex = luceneIndex; + this.schema = schema; this.sorlandsbanenService = sorlandsbanenService; this.stopConsolidationService = stopConsolidationService; this.tileRendererManager = tileRendererManager; @@ -203,6 +209,12 @@ public List rideHailingServices() { return rideHailingServices; } + @Nullable + @Override + public GraphQLSchema schema() { + return schema; + } + @Override public StopConsolidationService stopConsolidationService() { return stopConsolidationService; 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 183ad23d43d..9b1628577b6 100644 --- a/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/application/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -1720,8 +1720,8 @@ type QueryType { preferences: PlanPreferencesInput, """ Duration of the search window. This either starts at the defined earliest departure - time or ends at the latest arrival time. If this is not provided, a reasonable - search window is automatically generated. When searching for earlier or later itineraries + time or ends at the latest arrival time. If this is not provided or the value is set as null, + a reasonable search window is automatically generated. When searching for earlier or later itineraries with paging, this search window is no longer used and the new window will be based on how many suggestions were returned in the previous search. The new search window can be shorter or longer than the original search window. Note, itineraries are returned faster @@ -3913,7 +3913,10 @@ input BicyclePreferencesInput { "Preferences related to bicycle rental (station based or floating bicycle rental)." input BicycleRentalPreferencesInput { - "Rental networks which can be potentially used as part of an itinerary." + """ + Rental networks which can be potentially used as part of an itinerary. If this field has no default value, + it means that all networks are allowed unless some are banned with `bannedNetworks`. + """ allowedNetworks: [String!] "Rental networks which cannot be used as part of an itinerary." bannedNetworks: [String!] @@ -4006,7 +4009,10 @@ input CarPreferencesInput { "Preferences related to car rental (station based or floating car rental)." input CarRentalPreferencesInput { - "Rental networks which can be potentially used as part of an itinerary." + """ + Rental networks which can be potentially used as part of an itinerary. If this field has no default value, + it means that all networks are allowed unless some are banned with `bannedNetworks`. + """ allowedNetworks: [String!] "Rental networks which cannot be used as part of an itinerary." bannedNetworks: [String!] @@ -4321,7 +4327,6 @@ input PlanModesInput { If more than one mode is selected, at least one of them must be used but not necessarily all. There are modes that automatically also use walking such as the rental modes. To force rental to be used, this should only include the rental mode and not `WALK` in addition. - The default access mode is `WALK`. """ direct: [PlanDirectMode!] "Should only the direct search without any transit be done." @@ -4329,8 +4334,7 @@ input PlanModesInput { """ Modes for different phases of an itinerary when transit is included. Also includes street mode selections related to connecting to the transit network - and transfers. By default, all transit modes are usable and `WALK` is used for - access, egress and transfers. + and transfers. By default, all transit modes are usable. """ transit: PlanTransitModesInput """ @@ -4423,7 +4427,6 @@ input PlanTransitModesInput { If more than one mode is selected, at least one of them must be used but not necessarily all. There are modes that automatically also use walking such as the rental modes. To force rental to be used, this should only include the rental mode and not `WALK` in addition. - The default access mode is `WALK`. """ access: [PlanAccessMode!] """ @@ -4431,13 +4434,9 @@ input PlanTransitModesInput { If more than one mode is selected, at least one of them must be used but not necessarily all. There are modes that automatically also use walking such as the rental modes. To force rental to be used, this should only include the rental mode and not `WALK` in addition. - The default access mode is `WALK`. """ egress: [PlanEgressMode!] - """ - Street mode that is used when searching for transfers. Selection of only one allowed for now. - The default transfer mode is `WALK`. - """ + "Street mode that is used when searching for transfers. Selection of only one allowed for now." transfer: [PlanTransferMode!] """ Transit modes and reluctances associated with them. Each defined mode can be used in @@ -4510,7 +4509,10 @@ input ScooterPreferencesInput { "Preferences related to scooter rental (station based or floating scooter rental)." input ScooterRentalPreferencesInput { - "Rental networks which can be potentially used as part of an itinerary." + """ + Rental networks which can be potentially used as part of an itinerary. If this field has no default value, + it means that all networks are allowed unless some are banned with `bannedNetworks`. + """ allowedNetworks: [String!] "Rental networks which cannot be used as part of an itinerary." bannedNetworks: [String!] @@ -4559,11 +4561,11 @@ input TransferPreferencesInput { "How many transfers there can be at maximum in an itinerary." maximumTransfers: Int """ - A global minimum transfer time (in seconds) that specifies the minimum amount of time - that must pass between exiting one transit vehicle and boarding another. This time is - in addition to time it might take to walk between transit stops. Setting this value - as `PT0S`, for example, can lead to passenger missing a connection when the vehicle leaves - ahead of time or the passenger arrives to the stop later than expected. + A global minimum transfer time that specifies the minimum amount of time that must pass + between exiting one transit vehicle and boarding another. This time is in addition to + time it might take to walk between transit stops. Setting this value as `PT0S`, for + example, can lead to passenger missing a connection when the vehicle leaves ahead of time + or the passenger arrives to the stop later than expected. """ slack: Duration } diff --git a/application/src/test/java/org/opentripplanner/TestServerContext.java b/application/src/test/java/org/opentripplanner/TestServerContext.java index 9732ee69a89..c655303cdeb 100644 --- a/application/src/test/java/org/opentripplanner/TestServerContext.java +++ b/application/src/test/java/org/opentripplanner/TestServerContext.java @@ -107,6 +107,7 @@ public static OtpServerRequestContext createServerContext( null, null, null, + null, null ); } diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLFormattingTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLFormattingTest.java index 7c05fc8e361..24055a7d0f6 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLFormattingTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLFormattingTest.java @@ -17,7 +17,7 @@ public class GraphQLFormattingTest { @Test public void format() { String original = readFile(SCHEMA_FILE); - var schema = GtfsGraphQLIndex.buildSchema(); + var schema = SchemaFactory.createSchema(); writeFile(SCHEMA_FILE, new SchemaPrinter().print(schema)); assertFileEquals(original, SCHEMA_FILE); } diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIndexTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIndexTest.java deleted file mode 100644 index 0013f7df202..00000000000 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIndexTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.opentripplanner.apis.gtfs; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; - -import graphql.schema.AsyncDataFetcher; -import graphql.schema.DataFetcher; -import graphql.schema.FieldCoordinates; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLSchema; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.opentripplanner.framework.application.OTPFeature; - -public class GraphQLIndexTest { - - @Test - public void build() { - var schema = GtfsGraphQLIndex.buildSchema(); - assertNotNull(schema); - } - - @ValueSource(strings = { "plan", "nearest" }) - @ParameterizedTest(name = "\"{0}\" must be a an async fetcher") - void asyncDataFetchers(String fieldName) { - OTPFeature.AsyncGraphQLFetchers.testOn(() -> { - var schema = GtfsGraphQLIndex.buildSchema(); - var fetcher = getQueryType(fieldName, schema); - assertSame(fetcher.getClass(), AsyncDataFetcher.class); - }); - OTPFeature.AsyncGraphQLFetchers.testOff(() -> { - var schema = GtfsGraphQLIndex.buildSchema(); - var fetcher = getQueryType(fieldName, schema); - assertNotSame(fetcher.getClass(), AsyncDataFetcher.class); - }); - } - - private static DataFetcher getQueryType(String fieldName, GraphQLSchema schema) { - return schema - .getCodeRegistry() - .getDataFetcher( - FieldCoordinates.coordinates("QueryType", fieldName), - GraphQLFieldDefinition - .newFieldDefinition() - .name(fieldName) - .type(GraphQLObjectType.newObject().name(fieldName).build()) - .build() - ); - } -} 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 d27401d8b53..f9ff0cef4d7 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -372,6 +372,7 @@ public Set findRoutes(StopLocation stop) { defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE_1); defaultVehicleRentalService.addVehicleRentalStation(RENTAL_VEHICLE_2); + var routeRequest = new RouteRequest(); context = new GraphQLRequestContext( new TestRoutingService(List.of(i1)), @@ -380,8 +381,9 @@ public Set findRoutes(StopLocation stop) { defaultVehicleRentalService, new DefaultVehicleParkingService(parkingRepository), realtimeVehicleService, + SchemaFactory.createSchemaWithDefaultInjection(routeRequest), finder, - new RouteRequest() + routeRequest ); } diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/SchemaFactoryTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/SchemaFactoryTest.java new file mode 100644 index 00000000000..dac20e3af37 --- /dev/null +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/SchemaFactoryTest.java @@ -0,0 +1,115 @@ +package org.opentripplanner.apis.gtfs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.Value; +import graphql.schema.AsyncDataFetcher; +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.routing.api.request.RouteRequest; + +public class SchemaFactoryTest { + + @Test + void createSchema() { + var schema = SchemaFactory.createSchemaWithDefaultInjection(new RouteRequest()); + assertNotNull(schema); + } + + @Test + void testDefaultValueInjection() { + var routeRequest = new RouteRequest(); + double walkSpeed = 15; + routeRequest.withPreferences(preferences -> + preferences.withWalk(walk -> walk.withSpeed(walkSpeed)) + ); + var maxTransfers = 2; + routeRequest.withPreferences(preferences -> + preferences.withTransfer(transfer -> transfer.withMaxTransfers(maxTransfers + 1)) + ); + var numItineraries = 63; + routeRequest.setNumItineraries(numItineraries); + var schema = SchemaFactory.createSchemaWithDefaultInjection(routeRequest); + assertNotNull(schema); + + var defaultSpeed = (FloatValue) getDefaultValueForField( + schema, + "WalkPreferencesInput", + "speed" + ); + assertEquals(walkSpeed, defaultSpeed.getValue().doubleValue(), 0.01f); + + var defaultMaxTransfers = (IntValue) getDefaultValueForField( + schema, + "TransferPreferencesInput", + "maximumTransfers" + ); + assertEquals(maxTransfers, defaultMaxTransfers.getValue().intValue()); + + var defaultNumberOfItineraries = (IntValue) getDefaultValueForArgument( + schema, + "planConnection", + "first" + ); + assertEquals(numItineraries, defaultNumberOfItineraries.getValue().intValue()); + } + + @ValueSource(strings = { "plan", "nearest" }) + @ParameterizedTest(name = "\"{0}\" must be a an async fetcher") + void asyncDataFetchers(String fieldName) { + OTPFeature.AsyncGraphQLFetchers.testOn(() -> { + var schema = SchemaFactory.createSchemaWithDefaultInjection(new RouteRequest()); + var fetcher = getQueryType(fieldName, schema); + assertSame(fetcher.getClass(), AsyncDataFetcher.class); + }); + OTPFeature.AsyncGraphQLFetchers.testOff(() -> { + var schema = SchemaFactory.createSchemaWithDefaultInjection(new RouteRequest()); + var fetcher = getQueryType(fieldName, schema); + assertNotSame(fetcher.getClass(), AsyncDataFetcher.class); + }); + } + + private static DataFetcher getQueryType(String fieldName, GraphQLSchema schema) { + return schema + .getCodeRegistry() + .getDataFetcher( + FieldCoordinates.coordinates("QueryType", fieldName), + GraphQLFieldDefinition + .newFieldDefinition() + .name(fieldName) + .type(GraphQLObjectType.newObject().name(fieldName).build()) + .build() + ); + } + + private static Value getDefaultValueForField( + GraphQLSchema schema, + String inputObjectName, + String fieldName + ) { + GraphQLInputObjectType inputObject = schema.getTypeAs(inputObjectName); + return (Value) inputObject.getField(fieldName).getInputFieldDefaultValue().getValue(); + } + + private static Value getDefaultValueForArgument( + GraphQLSchema schema, + String queryName, + String argumentName + ) { + var query = schema.getQueryType().getField(queryName); + return (Value) query.getArgument(argumentName).getArgumentDefaultValue().getValue(); + } +} diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapperTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapperTest.java index 17193ad02c1..bf019023eaa 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapperTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapperTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.SchemaFactory; import org.opentripplanner.apis.gtfs.TestRoutingService; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.ext.fares.impl.DefaultFareService; @@ -57,6 +58,7 @@ class LegacyRouteRequestMapperTest implements PlanTestConstants { var timetableRepository = new TimetableRepository(stopModelBuilder.build(), new Deduplicator()); timetableRepository.initTimeZone(ZoneIds.BERLIN); final DefaultTransitService transitService = new DefaultTransitService(timetableRepository); + var routeRequest = new RouteRequest(); context = new GraphQLRequestContext( new TestRoutingService(List.of()), @@ -65,8 +67,9 @@ class LegacyRouteRequestMapperTest implements PlanTestConstants { new DefaultVehicleRentalService(), new DefaultVehicleParkingService(new DefaultVehicleParkingRepository()), new DefaultRealtimeVehicleService(transitService), + SchemaFactory.createSchemaWithDefaultInjection(routeRequest), GraphFinder.getInstance(graph, transitService::findRegularStopsByBoundingBox), - new RouteRequest() + routeRequest ); } diff --git a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTest.java b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTest.java index 925e5009e77..0aab9473371 100644 --- a/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTest.java +++ b/application/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTest.java @@ -23,6 +23,7 @@ import org.locationtech.jts.geom.Coordinate; import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.SchemaFactory; import org.opentripplanner.apis.gtfs.TestRoutingService; import org.opentripplanner.ext.fares.impl.DefaultFareService; import org.opentripplanner.model.plan.paging.cursor.PageCursor; @@ -67,6 +68,7 @@ class RouteRequestMapperTest { var timetableRepository = new TimetableRepository(); timetableRepository.initTimeZone(ZoneIds.BERLIN); final DefaultTransitService transitService = new DefaultTransitService(timetableRepository); + var routeRequest = new RouteRequest(); CONTEXT = new GraphQLRequestContext( new TestRoutingService(List.of()), @@ -75,8 +77,9 @@ class RouteRequestMapperTest { new DefaultVehicleRentalService(), new DefaultVehicleParkingService(new DefaultVehicleParkingRepository()), new DefaultRealtimeVehicleService(transitService), + SchemaFactory.createSchemaWithDefaultInjection(routeRequest), GraphFinder.getInstance(graph, transitService::findRegularStopsByBoundingBox), - new RouteRequest() + routeRequest ); } diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index 6170f84ae71..7263e95eeed 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -133,6 +133,7 @@ public SpeedTest( null, null, null, + null, null ); // Creating transitLayerForRaptor should be integrated into the TimetableRepository, but for now diff --git a/doc/user/RouteRequest.md b/doc/user/RouteRequest.md index 3e50dbd6fbd..5398a7c458b 100644 --- a/doc/user/RouteRequest.md +++ b/doc/user/RouteRequest.md @@ -29,7 +29,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe | locale | `locale` | TODO | *Optional* | `"en_US"` | 2.0 | | [maxDirectStreetDuration](#rd_maxDirectStreetDuration) | `duration` | This is the maximum duration for a direct street search for each mode. | *Optional* | `"PT4H"` | 2.1 | | [maxJourneyDuration](#rd_maxJourneyDuration) | `duration` | The expected maximum time a journey can last across all possible journeys for the current deployment. | *Optional* | `"PT24H"` | 2.1 | -| modes | `string` | The set of access/egress/direct/transit modes to be used for the route search. | *Optional* | `"TRANSIT,WALK"` | 2.0 | +| modes | `string` | The set of access/egress/direct/transfer modes (separated by a comma) to be used for the route search. | *Optional* | `"WALK"` | 2.0 | | nonpreferredTransferPenalty | `integer` | Penalty (in seconds) for using a non-preferred transfer. | *Optional* | `180` | 2.0 | | numItineraries | `integer` | The maximum number of itineraries to return. | *Optional* | `50` | 2.0 | | [otherThanPreferredRoutesPenalty](#rd_otherThanPreferredRoutesPenalty) | `integer` | Penalty added for using every route that is not preferred if user set any route as preferred. | *Optional* | `300` | 2.0 |