diff --git a/doc/user/examples/ibi/portland/router-config.json b/doc/user/examples/ibi/portland/router-config.json index 0bf3547dbfd..acbcbc2e0a0 100644 --- a/doc/user/examples/ibi/portland/router-config.json +++ b/doc/user/examples/ibi/portland/router-config.json @@ -59,6 +59,62 @@ "url": "https://gbfs.spin.pm/api/gbfs/v2/portland" } ], + "vectorTiles": { + "basePath": "/rtp/routers/default/vectorTiles", + "attribution": "Regional Partners", + "layers": [ + { + "name": "stops", + "type": "Stop", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600, + "filter": "sunday-to-sunday-service-week" + }, + { + "name": "areaStops", + "type": "AreaStop", + "mapper": "OTPRR", + "maxZoom": 30, + "minZoom": 8, + "cacheMaxSeconds": 600 + }, + { + "name": "stations", + "type": "Station", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 2, + "cacheMaxSeconds": 600 + }, + { + "name": "rentalVehicles", + "type": "VehicleRentalVehicle", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 2, + "cacheMaxSeconds": 60 + }, + { + "name": "rentalStations", + "type": "VehicleRentalStation", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 2, + "cacheMaxSeconds": 600 + }, + { + "name": "vehicleParking", + "type": "VehicleParking", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 10, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + } + ] + }, "rideHailingServices": [ { "type": "uber-car-hailing", diff --git a/doc/user/sandbox/MapboxVectorTilesApi.md b/doc/user/sandbox/MapboxVectorTilesApi.md index 62f3bd36c38..4430a398a5d 100644 --- a/doc/user/sandbox/MapboxVectorTilesApi.md +++ b/doc/user/sandbox/MapboxVectorTilesApi.md @@ -173,6 +173,7 @@ For each layer, the configuration includes: |       type = "stop" | `enum` | Type of the layer. | *Required* | | 2.0 | |       [cacheMaxSeconds](#vectorTiles_layers_0_cacheMaxSeconds) | `integer` | Sets the cache header in the response. | *Optional* | `-1` | 2.0 | |       [expansionFactor](#vectorTiles_layers_0_expansionFactor) | `double` | How far outside its boundaries should the tile contain information. | *Optional* | `0.25` | 2.0 | +|       [filter](#vectorTiles_layers_0_filter) | `enum` | Reduce the result set of a layer further by a specific filter. | *Optional* | `"none"` | 2.6 | |       [mapper](#vectorTiles_layers_0_mapper) | `string` | Describes the mapper converting from the OTP model entities to the vector tile properties. | *Required* | | 2.0 | |       maxZoom | `integer` | Maximum zoom levels the layer is active for. | *Optional* | `20` | 2.0 | |       minZoom | `integer` | Minimum zoom levels the layer is active for. | *Optional* | `9` | 2.0 | @@ -245,6 +246,18 @@ How far outside its boundaries should the tile contain information. The value is a fraction of the tile size. If you are having problem with icons and shapes being clipped at tile edges, then increase this number. +

filter

+ +**Since version:** `2.6` ∙ **Type:** `enum` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"none"` +**Path:** /vectorTiles/layers/[0] +**Enum values:** `none` | `sunday-to-sunday-service-week` + +Reduce the result set of a layer further by a specific filter. + +This is useful for when the schema of a layer, say stops, should remain unchanged but some +elements should not be included in the result. + +

mapper

**Since version:** `2.0` ∙ **Type:** `string` ∙ **Cardinality:** `Required` diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/LayerFiltersTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/LayerFiltersTest.java new file mode 100644 index 00000000000..f12d43b62cf --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/LayerFiltersTest.java @@ -0,0 +1,42 @@ +package org.opentripplanner.ext.vectortiles.layers; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model._data.PatternTestModel; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; + +class LayerFiltersTest { + + private static final RegularStop STOP = TransitModelForTest.of().stop("1").build(); + private static final LocalDate DATE = LocalDate.of(2024, 9, 5); + private static final TripPattern PATTERN = PatternTestModel.pattern(); + + @Test + void includeStopWithinServiceWeek() { + var predicate = LayerFilters.buildCurrentServiceWeekPredicate( + s -> List.of(PATTERN), + trip -> List.of(DATE), + () -> DATE + ); + + assertTrue(predicate.test(STOP)); + } + + @Test + void excludeOutsideServiceWeek() { + var inThreeWeeks = DATE.plusDays(21); + var predicate = LayerFilters.buildCurrentServiceWeekPredicate( + s -> List.of(PATTERN), + trip -> List.of(inThreeWeeks), + () -> DATE + ); + + assertFalse(predicate.test(STOP)); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/LayerFilters.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/LayerFilters.java new file mode 100644 index 00000000000..7d63bbe5398 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/LayerFilters.java @@ -0,0 +1,73 @@ +package org.opentripplanner.ext.vectortiles.layers; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.opentripplanner.apis.gtfs.model.LocalDateRange; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.PatternByServiceDatesFilter; +import org.opentripplanner.transit.service.TransitService; + +/** + * Predicates for filtering elements of vector tile layers. Currently only contains predicates + * for {@link RegularStop}. Once more types need to be filtered, this may need some refactoring. + */ +public class LayerFilters { + + /** + * No filter is applied: all stops are included in the result. + */ + public static final Predicate NO_FILTER = x -> true; + + /** + * Returns a predicate which only includes stop which are visited by a pattern that is in the current + * "service week", which lasts from Sunday to Sunday. + */ + public static Predicate buildCurrentServiceWeekPredicate( + Function> getPatternsForStop, + Function> getServiceDatesForTrip, + Supplier nowSupplier + ) { + var serviceDate = nowSupplier.get(); + var lastSunday = serviceDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)); + var nextSundayPlusOne = serviceDate.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)).plusDays(1); + + var filter = new PatternByServiceDatesFilter( + // reminder, the end of the date range is exclusive so it's the next Sunday plus one day + new LocalDateRange(lastSunday, nextSundayPlusOne), + // not used + route -> List.of(), + getServiceDatesForTrip + ); + + return regularStop -> { + var patterns = getPatternsForStop.apply(regularStop); + var patternsInCurrentWeek = filter.filterPatterns(patterns); + return !patternsInCurrentWeek.isEmpty(); + }; + } + + public static Predicate forType(FilterType type, TransitService transitService) { + return switch (type) { + case NONE -> NO_FILTER; + case SUNDAY_TO_SUNDAY_SERVICE_WEEK -> buildCurrentServiceWeekPredicate( + transitService::getPatternsForStop, + trip -> + transitService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId()), + () -> LocalDate.now(transitService.getTimeZone()) + ); + }; + } + + public enum FilterType { + NONE, + SUNDAY_TO_SUNDAY_SERVICE_WEEK, + } +} diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java index aa664497728..141157f8f3e 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerBuilder.java @@ -5,24 +5,20 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.function.BiFunction; -import java.util.stream.Collectors; +import java.util.function.Predicate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; -import org.opentripplanner.apis.support.mapping.PropertyMapper; import org.opentripplanner.ext.vectortiles.VectorTilesResource; +import org.opentripplanner.ext.vectortiles.layers.LayerFilters; import org.opentripplanner.inspector.vector.LayerBuilder; import org.opentripplanner.inspector.vector.LayerParameters; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.service.TransitService; -public class StopsLayerBuilder extends LayerBuilder { +public class StopsLayerBuilder extends LayerBuilder { - static Map>> mappers = Map.of( - MapperType.Digitransit, - DigitransitStopPropertyMapper::create - ); private final TransitService transitService; + private final Predicate filter; public StopsLayerBuilder( TransitService transitService, @@ -30,7 +26,7 @@ public StopsLayerBuilder( Locale locale ) { super( - (PropertyMapper) Map + Map .ofEntries( entry(MapperType.Digitransit, new DigitransitStopPropertyMapper(transitService, locale)), entry( @@ -43,12 +39,14 @@ public StopsLayerBuilder( layerParameters.expansionFactor() ); this.transitService = transitService; + this.filter = LayerFilters.forType(layerParameters.filterType(), transitService); } protected List getGeometries(Envelope query) { return transitService .findRegularStops(query) .stream() + .filter(filter) .map(stop -> { Geometry point = stop.getGeometry(); @@ -56,7 +54,7 @@ protected List getGeometries(Envelope query) { return point; }) - .collect(Collectors.toList()); + .toList(); } enum MapperType { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index 0e70c13074b..0943fa309fc 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -27,12 +27,12 @@ import org.locationtech.jts.geom.Envelope; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; import org.opentripplanner.apis.gtfs.GraphQLUtils; -import org.opentripplanner.apis.gtfs.PatternByServiceDatesFilter; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLQueryTypeStopsByRadiusArgs; import org.opentripplanner.apis.gtfs.mapping.routerequest.LegacyRouteRequestMapper; import org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapper; +import org.opentripplanner.apis.gtfs.support.filter.PatternByDateFilterUtil; import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; import org.opentripplanner.ext.fares.impl.DefaultFareService; import org.opentripplanner.ext.fares.impl.GtfsFaresService; @@ -615,8 +615,11 @@ public DataFetcher> routes() { } if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { - var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService); - routeStream = filter.filterRoutes(routeStream).stream(); + var filter = PatternByDateFilterUtil.ofGraphQL( + args.getGraphQLServiceDates(), + transitService + ); + routeStream = filter.filterRoutes(routeStream.toList()).stream(); } return routeStream.toList(); }; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java index a3f557951f0..ae6acb0a297 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/RouteImpl.java @@ -9,12 +9,12 @@ import java.util.stream.Collectors; import org.opentripplanner.apis.gtfs.GraphQLRequestContext; import org.opentripplanner.apis.gtfs.GraphQLUtils; -import org.opentripplanner.apis.gtfs.PatternByServiceDatesFilter; 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.support.filter.PatternByDateFilterUtil; import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; @@ -183,7 +183,10 @@ public DataFetcher> patterns() { var args = new GraphQLTypes.GraphQLRoutePatternsArgs(environment.getArguments()); if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { - var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService); + var filter = PatternByDateFilterUtil.ofGraphQL( + args.getGraphQLServiceDates(), + transitService + ); return filter.filterPatterns(patterns); } else { return patterns; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java index 0730d2fbc91..503899ba21a 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopImpl.java @@ -18,6 +18,8 @@ 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.support.filter.PatternByDateFilterUtil; +import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.StopTimesInPattern; import org.opentripplanner.model.TripTimeOnDate; @@ -243,7 +245,19 @@ public DataFetcher platformCode() { @Override public DataFetcher> routes() { - return this::getRoutes; + return env -> { + var args = new GraphQLTypes.GraphQLStopRoutesArgs(env.getArguments()); + var routes = getRoutes(env); + if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) { + var filter = PatternByDateFilterUtil.ofGraphQL( + args.getGraphQLServiceDates(), + getTransitService(env) + ); + return filter.filterRoutes(routes); + } else { + return routes; + } + }; } @Override diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index 67051444cdf..ed3e9afefc9 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -4231,6 +4231,26 @@ public void setGraphQLLanguage(String language) { } } + public static class GraphQLStopRoutesArgs { + + private GraphQLLocalDateRangeInput serviceDates; + + public GraphQLStopRoutesArgs(Map args) { + if (args != null) { + this.serviceDates = + new GraphQLLocalDateRangeInput((Map) args.get("serviceDates")); + } + } + + public GraphQLLocalDateRangeInput getGraphQLServiceDates() { + return this.serviceDates; + } + + public void setGraphQLServiceDates(GraphQLLocalDateRangeInput serviceDates) { + this.serviceDates = serviceDates; + } + } + public static class GraphQLStopStopTimesForPatternArgs { private String id; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/support/filter/PatternByDateFilterUtil.java b/src/main/java/org/opentripplanner/apis/gtfs/support/filter/PatternByDateFilterUtil.java new file mode 100644 index 00000000000..f3c0d6d0352 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/support/filter/PatternByDateFilterUtil.java @@ -0,0 +1,23 @@ +package org.opentripplanner.apis.gtfs.support.filter; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.model.LocalDateRange; +import org.opentripplanner.transit.service.PatternByServiceDatesFilter; +import org.opentripplanner.transit.service.TransitService; + +/** + * Utility methods for instantiating a {@link PatternByServiceDatesFilter}. + */ +public class PatternByDateFilterUtil { + + public static PatternByServiceDatesFilter ofGraphQL( + GraphQLTypes.GraphQLLocalDateRangeInput range, + TransitService transitService + ) { + return new PatternByServiceDatesFilter( + new LocalDateRange(range.getGraphQLStart(), range.getGraphQLEnd()), + transitService::getPatternsForRoute, + trip -> transitService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId()) + ); + } +} diff --git a/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java b/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java index cb849e0f0ac..70cb5a2ce43 100644 --- a/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java +++ b/src/main/java/org/opentripplanner/inspector/vector/LayerParameters.java @@ -1,6 +1,7 @@ package org.opentripplanner.inspector.vector; import org.opentripplanner.apis.support.mapping.PropertyMapper; +import org.opentripplanner.ext.vectortiles.layers.LayerFilters; /** * Configuration options for a single vector tile layer. @@ -53,4 +54,8 @@ default int cacheMaxSeconds() { default double expansionFactor() { return EXPANSION_FACTOR; } + + default LayerFilters.FilterType filterType() { + return LayerFilters.FilterType.NONE; + } } diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java index 96cd49b72bb..26d56dc0dba 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java @@ -6,20 +6,22 @@ import static org.opentripplanner.inspector.vector.LayerParameters.MIN_ZOOM; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_0; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_6; import java.util.Collection; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; import org.opentripplanner.ext.vectortiles.VectorTilesResource; +import org.opentripplanner.ext.vectortiles.VectorTilesResource.LayerType; +import org.opentripplanner.ext.vectortiles.layers.LayerFilters; import org.opentripplanner.inspector.vector.LayerParameters; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; -public class VectorTileConfig - implements VectorTilesResource.LayersParameters { +public class VectorTileConfig implements VectorTilesResource.LayersParameters { public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null, null); - private final List> layers; + private final List> layers; @Nullable private final String basePath; @@ -28,7 +30,7 @@ public class VectorTileConfig private final String attribution; VectorTileConfig( - Collection> layers, + Collection> layers, @Nullable String basePath, @Nullable String attribution ) { @@ -38,7 +40,7 @@ public class VectorTileConfig } @Override - public List> layers() { + public List> layers() { return layers; } @@ -144,7 +146,18 @@ public static Layer mapLayer(NodeAdapter node) { "The value is a fraction of the tile size. If you are having problem with icons and " + "shapes being clipped at tile edges, then increase this number." ) - .asDouble(EXPANSION_FACTOR) + .asDouble(EXPANSION_FACTOR), + node + .of("filter") + .since(V2_6) + .summary("Reduce the result set of a layer further by a specific filter.") + .description( + """ + This is useful for when the schema of a layer, say stops, should remain unchanged but some + elements should not be included in the result. + """ + ) + .asEnum(LayerFilters.FilterType.NONE) ); } @@ -155,7 +168,8 @@ record Layer( int maxZoom, int minZoom, int cacheMaxSeconds, - double expansionFactor + double expansionFactor, + LayerFilters.FilterType filterType ) implements LayerParameters {} } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java b/src/main/java/org/opentripplanner/transit/service/PatternByServiceDatesFilter.java similarity index 79% rename from src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java rename to src/main/java/org/opentripplanner/transit/service/PatternByServiceDatesFilter.java index 8eecfe6273b..21890975acf 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilter.java +++ b/src/main/java/org/opentripplanner/transit/service/PatternByServiceDatesFilter.java @@ -1,16 +1,13 @@ -package org.opentripplanner.apis.gtfs; +package org.opentripplanner.transit.service; import java.time.LocalDate; import java.util.Collection; import java.util.Objects; import java.util.function.Function; -import java.util.stream.Stream; -import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.model.LocalDateRange; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.service.TransitService; /** * Encapsulates the logic to filter patterns by the service dates that they operate on. It also @@ -29,7 +26,7 @@ public class PatternByServiceDatesFilter { * This method is not private to enable unit testing. *

*/ - PatternByServiceDatesFilter( + public PatternByServiceDatesFilter( LocalDateRange range, Function> getPatternsForRoute, Function> getServiceDatesForTrip @@ -45,17 +42,6 @@ public class PatternByServiceDatesFilter { } } - public PatternByServiceDatesFilter( - GraphQLTypes.GraphQLLocalDateRangeInput filterInput, - TransitService transitService - ) { - this( - new LocalDateRange(filterInput.getGraphQLStart(), filterInput.getGraphQLEnd()), - transitService::getPatternsForRoute, - trip -> transitService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId()) - ); - } - /** * Filter the patterns by the service dates that it operates on. */ @@ -67,8 +53,9 @@ public Collection filterPatterns(Collection tripPatter * Filter the routes by listing all their patterns' service dates and checking if they * operate on the specified dates. */ - public Collection filterRoutes(Stream routeStream) { + public Collection filterRoutes(Collection routeStream) { return routeStream + .stream() .filter(r -> { var patterns = getPatternsForRoute.apply(r); return !this.filterPatterns(patterns).isEmpty(); diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 927af19f8b1..78f18f4e654 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -2001,7 +2001,16 @@ type Stop implements Node & PlaceInterface { "Identifier of the platform, usually a number. This value is only present for stops that are part of a station" platformCode: String "Routes which pass through this stop" - routes: [Route!] + routes( + """ + Only include routes which are operational on at least one service date specified by this filter. + + **Note**: A service date is a technical term useful for transit planning purposes and might not + correspond to a how a passenger thinks of a calendar date. For example, a night bus running + on Sunday morning at 1am to 3am, might have the previous Saturday's service date. + """ + serviceDates: LocalDateRangeInput + ): [Route!] "Returns timetable of the specified pattern at this stop" stopTimesForPattern( "Id of the pattern" diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 79590ca2775..0e0eb159c1b 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -28,6 +28,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.stream.Stream; import javax.annotation.Nonnull; import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; @@ -86,6 +87,7 @@ import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.BikeAccess; +import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.RegularStop; @@ -114,6 +116,7 @@ class GraphQLIntegrationTest { .of(A, B, C, D, E, F, G, H) .map(p -> (RegularStop) p.stop) .toList(); + private static final Route ROUTE = TransitModelForTest.route("a-route").build(); private static VehicleRentalStation VEHICLE_RENTAL_STATION = new TestVehicleRentalStationBuilder() .withVehicles(10) @@ -209,6 +212,11 @@ public List getModesOfStopLocation(StopLocation stop) { public TransitAlertService getTransitAlertService() { return alertService; } + + @Override + public Set getRoutesForStop(StopLocation stop) { + return Set.of(ROUTE); + } }; routes.forEach(transitService::addRoutes); diff --git a/src/test/java/org/opentripplanner/transit/model/_data/PatternTestModel.java b/src/test/java/org/opentripplanner/transit/model/_data/PatternTestModel.java new file mode 100644 index 00000000000..c3afd0ddf61 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/_data/PatternTestModel.java @@ -0,0 +1,46 @@ +package org.opentripplanner.transit.model._data; + +import static org.opentripplanner.transit.model._data.TransitModelForTest.id; + +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.StopModel; + +public class PatternTestModel { + + public static final Route ROUTE_1 = TransitModelForTest.route("1").build(); + + private static final FeedScopedId SERVICE_ID = id("service"); + private static final Trip TRIP = TransitModelForTest + .trip("t1") + .withRoute(ROUTE_1) + .withServiceId(SERVICE_ID) + .build(); + private static final TransitModelForTest MODEL = new TransitModelForTest(StopModel.of()); + private static final RegularStop STOP_1 = MODEL.stop("1").build(); + private static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern(STOP_1, STOP_1); + + /** + * Creates a trip pattern that has a stop pattern, trip times and a trip with a service id. + */ + public static TripPattern pattern() { + var pattern = TransitModelForTest + .tripPattern("1", ROUTE_1) + .withStopPattern(STOP_PATTERN) + .build(); + + var tt = ScheduledTripTimes + .of() + .withTrip(TRIP) + .withArrivalTimes("10:00 10:05") + .withDepartureTimes("10:00 10:05") + .build(); + pattern.add(tt); + return pattern; + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java b/src/test/java/org/opentripplanner/transit/service/PatternByServiceDatesFilterTest.java similarity index 66% rename from src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java rename to src/test/java/org/opentripplanner/transit/service/PatternByServiceDatesFilterTest.java index f01bac12006..93f50ce5b1e 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/PatternByServiceDatesFilterTest.java +++ b/src/test/java/org/opentripplanner/transit/service/PatternByServiceDatesFilterTest.java @@ -1,12 +1,12 @@ -package org.opentripplanner.apis.gtfs; +package org.opentripplanner.transit.service; import static java.time.LocalDate.parse; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.opentripplanner.apis.gtfs.PatternByServiceDatesFilterTest.FilterExpectation.NOT_REMOVED; -import static org.opentripplanner.apis.gtfs.PatternByServiceDatesFilterTest.FilterExpectation.REMOVED; -import static org.opentripplanner.transit.model._data.TransitModelForTest.id; +import static org.opentripplanner.transit.model._data.PatternTestModel.ROUTE_1; +import static org.opentripplanner.transit.service.PatternByServiceDatesFilterTest.FilterExpectation.NOT_REMOVED; +import static org.opentripplanner.transit.service.PatternByServiceDatesFilterTest.FilterExpectation.REMOVED; import java.time.LocalDate; import java.util.List; @@ -14,51 +14,18 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.apis.gtfs.model.LocalDateRange; -import org.opentripplanner.transit.model._data.TransitModelForTest; -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.network.Route; -import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model._data.PatternTestModel; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; -import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.service.StopModel; class PatternByServiceDatesFilterTest { - private static final Route ROUTE_1 = TransitModelForTest.route("1").build(); - private static final FeedScopedId SERVICE_ID = id("service"); - private static final Trip TRIP = TransitModelForTest - .trip("t1") - .withRoute(ROUTE_1) - .withServiceId(SERVICE_ID) - .build(); - private static final TransitModelForTest MODEL = new TransitModelForTest(StopModel.of()); - private static final RegularStop STOP_1 = MODEL.stop("1").build(); - private static final StopPattern STOP_PATTERN = TransitModelForTest.stopPattern(STOP_1, STOP_1); - private static final TripPattern PATTERN_1 = pattern(); + private static final TripPattern PATTERN_1 = PatternTestModel.pattern(); enum FilterExpectation { REMOVED, NOT_REMOVED, } - private static TripPattern pattern() { - var pattern = TransitModelForTest - .tripPattern("1", ROUTE_1) - .withStopPattern(STOP_PATTERN) - .build(); - - var tt = ScheduledTripTimes - .of() - .withTrip(TRIP) - .withArrivalTimes("10:00 10:05") - .withDepartureTimes("10:00 10:05") - .build(); - pattern.add(tt); - return pattern; - } - static List invalidRangeCases() { return List.of( Arguments.of(null, null), @@ -140,7 +107,7 @@ void filterRoutes(LocalDate start, LocalDate end, FilterExpectation expectation) var filter = defaultFilter(start, end); var filterInput = List.of(ROUTE_1); - var filterOutput = filter.filterRoutes(filterInput.stream()); + var filterOutput = filter.filterRoutes(filterInput); if (expectation == NOT_REMOVED) { assertEquals(filterOutput, filterInput); diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json index 307b07b58aa..4e3d4c8a18c 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/stops.json @@ -6,56 +6,120 @@ "lat" : 5.0, "lon" : 8.0, "name" : "A", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:B", "lat" : 6.0, "lon" : 8.5, "name" : "B", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:C", "lat" : 7.0, "lon" : 9.0, "name" : "C", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:D", "lat" : 8.0, "lon" : 9.5, "name" : "D", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:E", "lat" : 9.0, "lon" : 10.0, "name" : "E", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:F", "lat" : 9.0, "lon" : 10.5, "name" : "F", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:G", "lat" : 9.5, "lon" : 11.0, "name" : "G", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] }, { "gtfsId" : "F:H", "lat" : 10.0, "lon" : 11.5, "name" : "H", - "vehicleMode" : "BUS" + "vehicleMode" : "BUS", + "allRoutes" : [ + { + "gtfsId" : "F:a-route", + "longName" : null, + "shortName" : "Ra-route" + } + ], + "routesWithinRange" : [ ] } ] } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql index 5f3df71f8e7..af4fd904096 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/stops.graphql @@ -5,5 +5,17 @@ lon name vehicleMode + allRoutes: routes { + gtfsId + longName + shortName + } + routesWithinRange: routes( + serviceDates: { start: "2024-09-10", end: "2024-09-10" } + ) { + gtfsId + longName + shortName + } } }