diff --git a/doc-templates/sandbox/MapboxVectorTilesApi.md b/doc-templates/sandbox/MapboxVectorTilesApi.md index dfec1ed085a..35211eff00b 100644 --- a/doc-templates/sandbox/MapboxVectorTilesApi.md +++ b/doc-templates/sandbox/MapboxVectorTilesApi.md @@ -49,6 +49,15 @@ The feature must be configured in `router-config.json` as follows "minZoom": 14, "cacheMaxSeconds": 600 }, + // flex zones + { + "name": "areaStops", + "type": "AreaStop", + "mapper": "OTPRR", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, { "name": "stations", "type": "Station", @@ -136,6 +145,7 @@ For each layer, the configuration includes: - `name` which is used in the url to fetch tiles, and as the layer name in the vector tiles. - `type` which tells the type of the layer. Currently supported: - `Stop` + - `AreaStop`: Flex zones - `Station` - `VehicleRental`: all rental places: stations and free-floating vehicles - `VehicleRentalVehicle`: free-floating rental vehicles @@ -191,4 +201,5 @@ key, and a function to create the mapper, with a `Graph` object as a parameter, * Added DigitransitRealtime for vehicle rental stations * Changed old vehicle parking mapper to be Stadtnavi * Added a new Digitransit vehicle parking mapper with no real-time information and less fields -- 2024-01-22: Make `basePath` configurable [#5627](https://github.com/opentripplanner/OpenTripPlanner/pull/5627) \ No newline at end of file +- 2024-01-22: Make `basePath` configurable [#5627](https://github.com/opentripplanner/OpenTripPlanner/pull/5627) +- 2024-02-27: Add layer for flex zones [#5704](https://github.com/opentripplanner/OpenTripPlanner/pull/5704) diff --git a/docs/RouterConfiguration.md b/docs/RouterConfiguration.md index ed09483f33c..503b8c7370f 100644 --- a/docs/RouterConfiguration.md +++ b/docs/RouterConfiguration.md @@ -649,6 +649,14 @@ Used to group requests when monitoring OTP. "minZoom" : 14, "cacheMaxSeconds" : 600 }, + { + "name" : "areaStops", + "type" : "AreaStop", + "mapper" : "OTPRR", + "maxZoom" : 20, + "minZoom" : 14, + "cacheMaxSeconds" : 600 + }, { "name" : "stations", "type" : "Station", diff --git a/docs/sandbox/MapboxVectorTilesApi.md b/docs/sandbox/MapboxVectorTilesApi.md index 537f1b800dd..45feec03d47 100644 --- a/docs/sandbox/MapboxVectorTilesApi.md +++ b/docs/sandbox/MapboxVectorTilesApi.md @@ -49,6 +49,15 @@ The feature must be configured in `router-config.json` as follows "minZoom": 14, "cacheMaxSeconds": 600 }, + // flex zones + { + "name": "areaStops", + "type": "AreaStop", + "mapper": "OTPRR", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, { "name": "stations", "type": "Station", @@ -136,6 +145,7 @@ For each layer, the configuration includes: - `name` which is used in the url to fetch tiles, and as the layer name in the vector tiles. - `type` which tells the type of the layer. Currently supported: - `Stop` + - `AreaStop`: Flex zones - `Station` - `VehicleRental`: all rental places: stations and free-floating vehicles - `VehicleRentalVehicle`: free-floating rental vehicles @@ -286,4 +296,5 @@ key, and a function to create the mapper, with a `Graph` object as a parameter, * Added DigitransitRealtime for vehicle rental stations * Changed old vehicle parking mapper to be Stadtnavi * Added a new Digitransit vehicle parking mapper with no real-time information and less fields -- 2024-01-22: Make `basePath` configurable [#5627](https://github.com/opentripplanner/OpenTripPlanner/pull/5627) \ No newline at end of file +- 2024-01-22: Make `basePath` configurable [#5627](https://github.com/opentripplanner/OpenTripPlanner/pull/5627) +- 2024-02-27: Add layer for flex zones [#5704](https://github.com/opentripplanner/OpenTripPlanner/pull/5704) diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapperTest.java new file mode 100644 index 00000000000..2cf0804b630 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapperTest.java @@ -0,0 +1,44 @@ +package org.opentripplanner.ext.vectortiles.layers.areastops; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.inspector.vector.KeyValue.kv; + +import java.util.List; +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.geometry.Polygons; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.service.StopModel; + +class AreaStopPropertyMapperTest { + + private static final TransitModelForTest MODEL = new TransitModelForTest(StopModel.of()); + private static final AreaStop STOP = MODEL.areaStopForTest("123", Polygons.BERLIN); + private static final Route ROUTE_WITH_COLOR = TransitModelForTest + .route("123") + .withColor("ffffff") + .build(); + private static final Route ROUTE_WITHOUT_COLOR = TransitModelForTest.route("456").build(); + + @Test + void map() { + var mapper = new AreaStopPropertyMapper( + ignored -> List.of(ROUTE_WITH_COLOR, ROUTE_WITHOUT_COLOR), + Locale.ENGLISH + ); + + var kv = mapper.map(STOP); + + assertEquals( + List.of( + kv("gtfsId", "F:123"), + kv("name", "123"), + kv("code", null), + kv("routeColors", "ffffff") + ), + kv + ); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopsLayerBuilderTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopsLayerBuilderTest.java new file mode 100644 index 00000000000..2e6c4e16c40 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopsLayerBuilderTest.java @@ -0,0 +1,74 @@ +package org.opentripplanner.ext.vectortiles.layers.areastops; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.standalone.config.framework.json.JsonSupport.newNodeAdapterForTest; + +import java.util.List; +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.geometry.Polygons; +import org.opentripplanner.ext.vectortiles.VectorTilesResource; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.inspector.vector.LayerParameters; +import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.StopModel; +import org.opentripplanner.transit.service.StopModelBuilder; +import org.opentripplanner.transit.service.TransitModel; + +class AreaStopsLayerBuilderTest { + + private static final FeedScopedId ID = new FeedScopedId("FEED", "ID"); + private static final I18NString NAME = I18NString.of("Test stop"); + private static final String CONFIG = + """ + { + "vectorTiles": { + "layers" : [ + { + "name": "areaStops", + "type": "AreaStop", + "mapper": "OTPRR", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0 + } + ] + } + } + """; + private static final LayerParameters LAYER_CONFIG = VectorTileConfig + .mapVectorTilesParameters(newNodeAdapterForTest(CONFIG), "vectorTiles") + .layers() + .getFirst(); + + private final StopModelBuilder stopModelBuilder = StopModel.of(); + + private final AreaStop AREA_STOP = stopModelBuilder + .areaStop(ID) + .withName(NAME) + .withGeometry(Polygons.BERLIN) + .build(); + + private final TransitModel transitModel = new TransitModel( + stopModelBuilder.withAreaStop(AREA_STOP).build(), + new Deduplicator() + ); + + @Test + void getAreaStops() { + transitModel.index(); + + var subject = new AreaStopsLayerBuilder( + new DefaultTransitService(transitModel), + LAYER_CONFIG, + Locale.ENGLISH + ); + var geometries = subject.getGeometries(AREA_STOP.getGeometry().getEnvelopeInternal()); + assertEquals(List.of(Polygons.BERLIN), geometries); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java index a1e9a83c85a..29701ee2307 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java @@ -18,6 +18,7 @@ import java.util.function.Predicate; import org.glassfish.grizzly.http.server.Request; import org.opentripplanner.apis.support.TileJson; +import org.opentripplanner.ext.vectortiles.layers.areastops.AreaStopsLayerBuilder; import org.opentripplanner.ext.vectortiles.layers.stations.StationsLayerBuilder; import org.opentripplanner.ext.vectortiles.layers.stops.StopsLayerBuilder; import org.opentripplanner.ext.vectortiles.layers.vehicleparkings.VehicleParkingGroupsLayerBuilder; @@ -68,7 +69,7 @@ public Response tileGet( locale, Arrays.asList(requestedLayers.split(",")), serverContext.vectorTileConfig().layers(), - VectorTilesResource::crateLayerBuilder, + VectorTilesResource::createLayerBuilder, serverContext ); } @@ -115,7 +116,7 @@ private List getFeedInfos() { .toList(); } - private static LayerBuilder crateLayerBuilder( + private static LayerBuilder createLayerBuilder( LayerParameters layerParameters, Locale locale, OtpServerRequestContext context @@ -123,6 +124,7 @@ private static LayerBuilder crateLayerBuilder( return switch (layerParameters.type()) { case Stop -> new StopsLayerBuilder(context.transitService(), layerParameters, locale); case Station -> new StationsLayerBuilder(context.transitService(), layerParameters, locale); + case AreaStop -> new AreaStopsLayerBuilder(context.transitService(), layerParameters, locale); case VehicleRental -> new VehicleRentalPlacesLayerBuilder( context.vehicleRentalService(), layerParameters, @@ -153,6 +155,7 @@ private static LayerBuilder crateLayerBuilder( public enum LayerType { Stop, Station, + AreaStop, VehicleRental, VehicleRentalVehicle, VehicleRentalStation, diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapper.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapper.java new file mode 100644 index 00000000000..ea6f9225e11 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapper.java @@ -0,0 +1,52 @@ +package org.opentripplanner.ext.vectortiles.layers.areastops; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.opentripplanner.apis.support.mapping.PropertyMapper; +import org.opentripplanner.framework.i18n.I18NStringMapper; +import org.opentripplanner.inspector.vector.KeyValue; +import org.opentripplanner.transit.model.network.Route; +import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.service.TransitService; + +public class AreaStopPropertyMapper extends PropertyMapper { + + private final Function> getRoutesForStop; + private final I18NStringMapper i18NStringMapper; + + protected AreaStopPropertyMapper( + Function> getRoutesForStop, + Locale locale + ) { + this.getRoutesForStop = getRoutesForStop; + this.i18NStringMapper = new I18NStringMapper(locale); + } + + protected static AreaStopPropertyMapper create(TransitService transitService, Locale locale) { + return new AreaStopPropertyMapper(transitService::getRoutesForStop, locale); + } + + @Override + protected Collection map(AreaStop stop) { + var routeColors = getRoutesForStop + .apply(stop) + .stream() + .map(Route::getColor) + .filter(Objects::nonNull) + .distinct() + // the MVT spec explicitly doesn't cover how to encode arrays + // https://docs.mapbox.com/data/tilesets/guides/vector-tiles-standards/#what-the-spec-doesnt-cover + .collect(Collectors.joining(",")); + return List.of( + new KeyValue("gtfsId", stop.getId().toString()), + new KeyValue("name", i18NStringMapper.mapNonnullToApi(stop.getName())), + new KeyValue("code", stop.getCode()), + new KeyValue("routeColors", routeColors) + ); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopsLayerBuilder.java b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopsLayerBuilder.java new file mode 100644 index 00000000000..31952752dbc --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopsLayerBuilder.java @@ -0,0 +1,53 @@ +package org.opentripplanner.ext.vectortiles.layers.areastops; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.BiFunction; +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.inspector.vector.LayerBuilder; +import org.opentripplanner.inspector.vector.LayerParameters; +import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.service.TransitService; + +public class AreaStopsLayerBuilder extends LayerBuilder { + + static Map>> mappers = Map.of( + MapperType.OTPRR, + AreaStopPropertyMapper::create + ); + private final TransitService transitService; + + public AreaStopsLayerBuilder( + TransitService transitService, + LayerParameters layerParameters, + Locale locale + ) { + super( + mappers.get(MapperType.valueOf(layerParameters.mapper())).apply(transitService, locale), + layerParameters.name(), + layerParameters.expansionFactor() + ); + this.transitService = transitService; + } + + protected List getGeometries(Envelope query) { + return transitService + .findAreaStops(query) + .stream() + .filter(g -> g.getGeometry() != null) + .map(stop -> { + Geometry point = stop.getGeometry().copy(); + point.setUserData(stop); + return point; + }) + .toList(); + } + + enum MapperType { + OTPRR, + } +} diff --git a/src/test/resources/standalone/config/router-config.json b/src/test/resources/standalone/config/router-config.json index 0fc39e9a99a..3a07cddfeb8 100644 --- a/src/test/resources/standalone/config/router-config.json +++ b/src/test/resources/standalone/config/router-config.json @@ -212,6 +212,14 @@ "minZoom": 14, "cacheMaxSeconds": 600 }, + { + "name": "areaStops", + "type": "AreaStop", + "mapper": "OTPRR", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, { "name": "stations", "type": "Station",