diff --git a/doc-templates/sandbox/MapboxVectorTilesApi.md b/doc-templates/sandbox/MapboxVectorTilesApi.md new file mode 100644 index 00000000000..dfec1ed085a --- /dev/null +++ b/doc-templates/sandbox/MapboxVectorTilesApi.md @@ -0,0 +1,194 @@ +# Mapbox Vector Tiles API + +## Contact Info + +- HSL, Finland +- Arcadis, US + +## Documentation + +This API produces [Mapbox vector tiles](https://docs.mapbox.com/vector-tiles/reference/), which are +used by [Digitransit-ui](https://github.com/HSLdevcom/digitransit-ui) and +[`otp-react-redux`](https://github.com/opentripplanner/otp-react-redux) to show information about +public transit entities on the map. + +The tiles can be fetched from `/otp/routers/{routerId}/vectorTiles/{layers}/{z}/{x}/{y}.pbf`, +where `layers` is a comma separated list of layer names from the configuration. + +Maplibre/Mapbox GL JS also requires a tilejson.json endpoint which is available at +`/otp/routers/{routerId}/vectorTiles/{layers}/tilejson.json`. + +Translatable fields in the tiles are translated based on the `accept-language` header in requests. +Currently, only the language with the highest priority from the header is used. + +### Configuration + +To enable this you need to add the feature `otp-config.json`. + +```json +// otp-config.json +{ + "otpFeatures": { + "SandboxAPIMapboxVectorTilesApi": true + } +} +``` + +The feature must be configured in `router-config.json` as follows + +```JSON +{ + "vectorTiles": { + "basePath": "/only/configure/if/required", + "layers": [ + { + "name": "stops", + "type": "Stop", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, + { + "name": "stations", + "type": "Station", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 12, + "cacheMaxSeconds": 600 + }, + // all rental places: stations and free-floating vehicles + { + "name": "citybikes", + "type": "VehicleRental", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + }, + // just free-floating vehicles + { + "name": "rentalVehicles", + "type": "VehicleRentalVehicle", + "mapper": "DigitransitRealtime", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60 + }, + // just rental stations + { + "name": "rentalStations", + "type": "VehicleRentalStation", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, + // Contains just stations and real-time information for them + { + "name": "realtimeRentalStations", + "type": "VehicleRentalStation", + "mapper": "DigitransitRealtime", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60 + }, + // This exists for backwards compatibility. At some point, we might want + // to add a new real-time parking mapper with better translation support + // and less unnecessary fields. + { + "name": "stadtnaviVehicleParking", + "type": "VehicleParking", + "mapper": "Stadtnavi", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + }, + // no real-time, translatable fields are translated based on accept-language header + // and contains less fields than the Stadtnavi mapper + { + "name": "vehicleParking", + "type": "VehicleParking", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600, + "expansionFactor": 0.25 + }, + { + "name": "vehicleParkingGroups", + "type": "VehicleParkingGroup", + "mapper": "Digitransit", + "maxZoom": 17, + "minZoom": 14, + "cacheMaxSeconds": 600, + "expansionFactor": 0.25 + } + ] + } +} +``` + +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` + - `Station` + - `VehicleRental`: all rental places: stations and free-floating vehicles + - `VehicleRentalVehicle`: free-floating rental vehicles + - `VehicleRentalStation`: rental stations + - `VehicleParking` + - `VehicleParkingGroup` + + + +### Extending + +If more generic layers are created for this API, the code should be moved out from the sandbox, into +the core, perhaps potentially leaving specific property mappers in place. + +#### Creating a new layer + +In order to create a new type of layer, you need to create a new class extending `LayerBuilder`. +You need to implement two methods, `List getGeometries(Envelope query)`, which returns a +list of geometries, with an object of type `T` as their userData in the geometry, +and `double getExpansionFactor()`, which describes how much information outside the tile bounds +should be included. This layer then needs to be added into `VectorTilesResource.layers`, with a +new `LayerType` enum as the key, and the class constructor as the value. + +A new mapper needs to be added every time a new layer is added. See below for information. + +#### Creating a new mapper + +The mapping contains information of what data to include in the vector tiles. The mappers are +defined per layer. + +In order to create a new mapper for a layer, you need to create a new class +extending `PropertyMapper`. In that class, you need to implement the +method `Collection> map(T input)`. The type T is dependent on the layer for which +you implement the mapper for. It needs to return a list of attributes, as key-value pairs which will +be written into the vector tile. + +The mapper needs to be added to the `mappers` map in the layer, with a new `MapperType` enum as the +key, and a function to create the mapper, with a `Graph` object as a parameter, as the value. + +## Changelog + +- 2020-07-09: Initial version of Mapbox vector tiles API +- 2021-05-12: Make expansion factor configurable +- 2021-09-07: Rename `BikeRental` to `VehicleRental` +- 2021-10-13: Correctly serialize the vehicle rental name [#3648](https://github.com/opentripplanner/OpenTripPlanner/pull/3648) +- 2022-01-03: Add support for VehicleParking entities +- 2022-04-27: Read the headsign for frequency-only patterns correctly [#4122](https://github.com/opentripplanner/OpenTripPlanner/pull/4122) +- 2022-08-23: Remove patterns and add route gtfsTypes to stop layer [#4404](https://github.com/opentripplanner/OpenTripPlanner/pull/4404) +- 2022-10-11: Added layer for VehicleParkingGroups [#4510](https://github.com/opentripplanner/OpenTripPlanner/pull/4510) +- 2022-10-14: Add separate layers for vehicle rental place types [#4516](https://github.com/opentripplanner/OpenTripPlanner/pull/4516) +- 2022-10-19 [#4529](https://github.com/opentripplanner/OpenTripPlanner/pull/4529): + * Translatable fields are now translated based on accept-language header + * 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 diff --git a/docs/RouterConfiguration.md b/docs/RouterConfiguration.md index 23e2e3763d4..62fb5d9617a 100644 --- a/docs/RouterConfiguration.md +++ b/docs/RouterConfiguration.md @@ -67,7 +67,7 @@ A full list of them can be found in the [RouteRequest](RouteRequest.md). |    [hideFeedId](#transmodelApi_hideFeedId) | `boolean` | Hide the FeedId in all API output, and add it to input. | *Optional* | `false` | na | |    [tracingHeaderTags](#transmodelApi_tracingHeaderTags) | `string[]` | Used to group requests when monitoring OTP. | *Optional* | | na | | [updaters](UpdaterConfig.md) | `object[]` | Configuration for the updaters that import various types of data into OTP. | *Optional* | | 1.5 | -| [vectorTileLayers](sandbox/MapboxVectorTilesApi.md) | `object[]` | Configuration of the individual layers for the Mapbox vector tiles. | *Optional* | | 2.0 | +| [vectorTiles](sandbox/MapboxVectorTilesApi.md) | `object` | Vector tile configuration | *Optional* | | na | | [vehicleRentalServiceDirectory](sandbox/VehicleRentalServiceDirectory.md) | `object` | Configuration for the vehicle rental service directory. | *Optional* | | 2.0 | @@ -625,58 +625,61 @@ Used to group requests when monitoring OTP. "transmodelApi" : { "hideFeedId" : true }, - "vectorTileLayers" : [ - { - "name" : "stops", - "type" : "Stop", - "mapper" : "Digitransit", - "maxZoom" : 20, - "minZoom" : 14, - "cacheMaxSeconds" : 600 - }, - { - "name" : "stations", - "type" : "Station", - "mapper" : "Digitransit", - "maxZoom" : 20, - "minZoom" : 12, - "cacheMaxSeconds" : 600 - }, - { - "name" : "rentalPlaces", - "type" : "VehicleRental", - "mapper" : "Digitransit", - "maxZoom" : 20, - "minZoom" : 14, - "cacheMaxSeconds" : 60, - "expansionFactor" : 0.25 - }, - { - "name" : "rentalVehicle", - "type" : "VehicleRentalVehicle", - "mapper" : "Digitransit", - "maxZoom" : 20, - "minZoom" : 14, - "cacheMaxSeconds" : 60 - }, - { - "name" : "rentalStation", - "type" : "VehicleRentalStation", - "mapper" : "Digitransit", - "maxZoom" : 20, - "minZoom" : 14, - "cacheMaxSeconds" : 600 - }, - { - "name" : "vehicleParking", - "type" : "VehicleParking", - "mapper" : "Digitransit", - "maxZoom" : 20, - "minZoom" : 14, - "cacheMaxSeconds" : 60, - "expansionFactor" : 0.25 - } - ], + "vectorTiles" : { + "basePath" : "/otp_ct/vectorTiles", + "layers" : [ + { + "name" : "stops", + "type" : "Stop", + "mapper" : "Digitransit", + "maxZoom" : 20, + "minZoom" : 14, + "cacheMaxSeconds" : 600 + }, + { + "name" : "stations", + "type" : "Station", + "mapper" : "Digitransit", + "maxZoom" : 20, + "minZoom" : 12, + "cacheMaxSeconds" : 600 + }, + { + "name" : "rentalPlaces", + "type" : "VehicleRental", + "mapper" : "Digitransit", + "maxZoom" : 20, + "minZoom" : 14, + "cacheMaxSeconds" : 60, + "expansionFactor" : 0.25 + }, + { + "name" : "rentalVehicle", + "type" : "VehicleRentalVehicle", + "mapper" : "Digitransit", + "maxZoom" : 20, + "minZoom" : 14, + "cacheMaxSeconds" : 60 + }, + { + "name" : "rentalStation", + "type" : "VehicleRentalStation", + "mapper" : "Digitransit", + "maxZoom" : 20, + "minZoom" : 14, + "cacheMaxSeconds" : 600 + }, + { + "name" : "vehicleParking", + "type" : "VehicleParking", + "mapper" : "Digitransit", + "maxZoom" : 20, + "minZoom" : 14, + "cacheMaxSeconds" : 60, + "expansionFactor" : 0.25 + } + ] + }, "timetableUpdates" : { "purgeExpiredData" : false, "maxSnapshotFrequency" : "2s" diff --git a/docs/examples/ibi/portland/build-config.json b/docs/examples/ibi/portland/build-config.json index 4b3a232ffba..46309d21c59 100644 --- a/docs/examples/ibi/portland/build-config.json +++ b/docs/examples/ibi/portland/build-config.json @@ -6,7 +6,7 @@ "transitFeeds": [ { "type": "gtfs", - "feedId": "trimet", + "feedId": "TriMet", "source": "https://developer.trimet.org/schedule/gtfs.zip" } ] diff --git a/docs/sandbox/MapboxVectorTilesApi.md b/docs/sandbox/MapboxVectorTilesApi.md index 8ef8ee179e7..da9fd1120e1 100644 --- a/docs/sandbox/MapboxVectorTilesApi.md +++ b/docs/sandbox/MapboxVectorTilesApi.md @@ -3,18 +3,21 @@ ## Contact Info - HSL, Finland -- Kyyti Group Oy, Finland -- Hannes Junnila +- Arcadis, US ## Documentation This API produces [Mapbox vector tiles](https://docs.mapbox.com/vector-tiles/reference/), which are -used by eg. [Digitransit-ui](https://github.com/HSLdevcom/digitransit-ui) to show information about +used by [Digitransit-ui](https://github.com/HSLdevcom/digitransit-ui) and +[`otp-react-redux`](https://github.com/opentripplanner/otp-react-redux) to show information about public transit entities on the map. The tiles can be fetched from `/otp/routers/{routerId}/vectorTiles/{layers}/{z}/{x}/{y}.pbf`, where `layers` is a comma separated list of layer names from the configuration. +Maplibre/Mapbox GL JS also requires a tilejson.json endpoint which is available at +`/otp/routers/{routerId}/vectorTiles/{layers}/tilejson.json`. + Translatable fields in the tiles are translated based on the `accept-language` header in requests. Currently, only the language with the highest priority from the header is used. @@ -35,93 +38,96 @@ The feature must be configured in `router-config.json` as follows ```JSON { - "vectorTileLayers": [ - { - "name": "stops", - "type": "Stop", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 600 - }, - { - "name": "stations", - "type": "Station", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 12, - "cacheMaxSeconds": 600 - }, - // all rental places: stations and free-floating vehicles - { - "name": "citybikes", - "type": "VehicleRental", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60, - "expansionFactor": 0.25 - }, - // just free-floating vehicles - { - "name": "rentalVehicles", - "type": "VehicleRentalVehicle", - "mapper": "DigitransitRealtime", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60 - }, - // just rental stations - { - "name": "rentalStations", - "type": "VehicleRentalStation", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 600 - }, - // Contains just stations and real-time information for them - { - "name": "realtimeRentalStations", - "type": "VehicleRentalStation", - "mapper": "DigitransitRealtime", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60 - }, - // This exists for backwards compatibility. At some point, we might want - // to add a new real-time parking mapper with better translation support - // and less unnecessary fields. - { - "name": "stadtnaviVehicleParking", - "type": "VehicleParking", - "mapper": "Stadtnavi", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60, - "expansionFactor": 0.25 - }, - // no real-time, translatable fields are translated based on accept-language header - // and contains less fields than the Stadtnavi mapper - { - "name": "vehicleParking", - "type": "VehicleParking", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 600, - "expansionFactor": 0.25 - }, - { - "name": "vehicleParkingGroups", - "type": "VehicleParkingGroup", - "mapper": "Digitransit", - "maxZoom": 17, - "minZoom": 14, - "cacheMaxSeconds": 600, - "expansionFactor": 0.25 - } - ] + "vectorTiles": { + "basePath": "/only/configure/if/required", + "layers": [ + { + "name": "stops", + "type": "Stop", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, + { + "name": "stations", + "type": "Station", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 12, + "cacheMaxSeconds": 600 + }, + // all rental places: stations and free-floating vehicles + { + "name": "citybikes", + "type": "VehicleRental", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + }, + // just free-floating vehicles + { + "name": "rentalVehicles", + "type": "VehicleRentalVehicle", + "mapper": "DigitransitRealtime", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60 + }, + // just rental stations + { + "name": "rentalStations", + "type": "VehicleRentalStation", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, + // Contains just stations and real-time information for them + { + "name": "realtimeRentalStations", + "type": "VehicleRentalStation", + "mapper": "DigitransitRealtime", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60 + }, + // This exists for backwards compatibility. At some point, we might want + // to add a new real-time parking mapper with better translation support + // and less unnecessary fields. + { + "name": "stadtnaviVehicleParking", + "type": "VehicleParking", + "mapper": "Stadtnavi", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + }, + // no real-time, translatable fields are translated based on accept-language header + // and contains less fields than the Stadtnavi mapper + { + "name": "vehicleParking", + "type": "VehicleParking", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600, + "expansionFactor": 0.25 + }, + { + "name": "vehicleParkingGroups", + "type": "VehicleParkingGroup", + "mapper": "Digitransit", + "maxZoom": 17, + "minZoom": 14, + "cacheMaxSeconds": 600, + "expansionFactor": 0.25 + } + ] + } } ``` @@ -136,19 +142,91 @@ For each layer, the configuration includes: - `VehicleRentalStation`: rental stations - `VehicleParking` - `VehicleParkingGroup` -- `mapper` which describes the mapper converting the properties from the OTP model entities to the - vector tile properties. Currently `Digitransit` is supported for all layer types. -- `minZoom` and `maxZoom` which describe the zoom levels the layer is active for. -- `cacheMaxSeconds` which sets the cache header in the response. The lowest value of the layers - included is selected. -- `expansionFactor` 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. + + + + +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|----------------------------------------------------------------|:----------:|--------------------------------------------------------------------------------------------|:----------:|---------------|:-----:| +| [basePath](#vectorTiles_basePath) | `string` | The path of the vector tile source URLs in `tilejson.json`. | *Optional* | | 2.5 | +| [layers](#vectorTiles_layers) | `object[]` | Configuration of the individual layers for the Mapbox vector tiles. | *Optional* | | 2.0 | +|       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 | +|       [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 | +|       name | `string` | Used in the url to fetch tiles, and as the layer name in the vector tiles. | *Required* | | 2.0 | + + +#### Details + +

basePath

+ +**Since version:** `2.5` ∙ **Type:** `string` ∙ **Cardinality:** `Optional` +**Path:** /vectorTiles + +The path of the vector tile source URLs in `tilejson.json`. + +This is useful if you have a proxy setup and rewrite the path that is passed to OTP. + +If you don't configure this optional value then the path returned in `tilejson.json` is in +the format `/otp/routers/default/vectorTiles/layer1,layer2/{z}/{x}/{x}.pbf`. +If you, for example, set a value of `/otp_test/tiles` then the returned path changes to +`/otp_test/tiles/layer1,layer2/{z}/{x}/{x}.pbf`. + +The protocol and host are always read from the incoming HTTP request. If you run OTP behind +a proxy then make sure to set the headers `X-Forwarded-Proto` and `X-Forwarded-Host` to make OTP +return the protocol and host for the original request and not the proxied one. + +**Note:** This does _not_ change the path that OTP itself serves the tiles or `tilejson.json` +responses but simply changes the URLs listed in `tilejson.json`. The rewriting of the path +is expected to be handled by a proxy. + + +

layers

+ +**Since version:** `2.0` ∙ **Type:** `object[]` ∙ **Cardinality:** `Optional` +**Path:** /vectorTiles + +Configuration of the individual layers for the Mapbox vector tiles. + +

cacheMaxSeconds

+ +**Since version:** `2.0` ∙ **Type:** `integer` ∙ **Cardinality:** `Optional` ∙ **Default value:** `-1` +**Path:** /vectorTiles/layers/[0] + +Sets the cache header in the response. + +The lowest value of the layers included is selected. + +

expansionFactor

+ +**Since version:** `2.0` ∙ **Type:** `double` ∙ **Cardinality:** `Optional` ∙ **Default value:** `0.25` +**Path:** /vectorTiles/layers/[0] + +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. + +

mapper

+ +**Since version:** `2.0` ∙ **Type:** `string` ∙ **Cardinality:** `Required` +**Path:** /vectorTiles/layers/[0] + +Describes the mapper converting from the OTP model entities to the vector tile properties. + +Currently `Digitransit` is supported for all layer types. + + + + + ### Extending -If more generic layers are created for this API, it should be moved out from the sandbox, into the -core code, with potentially leaving specific property mappers in place. +If more generic layers are created for this API, the code should be moved out from the sandbox, into +the core, perhaps potentially leaving specific property mappers in place. #### Creating a new layer @@ -168,7 +246,7 @@ defined per layer. In order to create a new mapper for a layer, you need to create a new class extending `PropertyMapper`. In that class, you need to implement the -method `Collection> map(T input)`. The type T is dependent on the layer for which +method `Collection> map(T input)`. The type T is dependent on the layer for which you implement the mapper for. It needs to return a list of attributes, as key-value pairs which will be written into the vector tile. @@ -191,3 +269,4 @@ 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 diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/VectorTilesConfigDocTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/VectorTilesConfigDocTest.java new file mode 100644 index 00000000000..233e3fa3737 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/VectorTilesConfigDocTest.java @@ -0,0 +1,74 @@ +package org.opentripplanner.ext.vectortiles; + +import static org.opentripplanner.framework.io.FileUtils.assertFileEquals; +import static org.opentripplanner.framework.io.FileUtils.readFile; +import static org.opentripplanner.framework.io.FileUtils.writeFile; +import static org.opentripplanner.framework.text.MarkdownFormatter.HEADER_4; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.DOCS_ROOT; +import static org.opentripplanner.generate.doc.framework.DocsTestConstants.TEMPLATE_ROOT; +import static org.opentripplanner.generate.doc.framework.TemplateUtil.replaceSection; +import static org.opentripplanner.standalone.config.framework.json.JsonSupport.jsonNodeFromPath; + +import java.io.File; +import org.junit.jupiter.api.Test; +import org.opentripplanner.generate.doc.framework.DocBuilder; +import org.opentripplanner.generate.doc.framework.GeneratesDocumentation; +import org.opentripplanner.generate.doc.framework.ParameterDetailsList; +import org.opentripplanner.generate.doc.framework.ParameterSummaryTable; +import org.opentripplanner.generate.doc.framework.SkipNodes; +import org.opentripplanner.standalone.config.RouterConfig; +import org.opentripplanner.standalone.config.framework.json.NodeAdapter; +import org.opentripplanner.test.support.ResourceLoader; + +@GeneratesDocumentation +public class VectorTilesConfigDocTest { + + private static final String DOCUMENT = "sandbox/MapboxVectorTilesApi.md"; + private static final File TEMPLATE = new File(TEMPLATE_ROOT, DOCUMENT); + private static final File OUT_FILE = new File(DOCS_ROOT, DOCUMENT); + private static final SkipNodes SKIP_NODES = SkipNodes.of().build(); + + @Test + public void updateDoc() { + NodeAdapter node = readVectorTiles(); + + // Read and close input file (same as output file) + String template = readFile(TEMPLATE); + String original = readFile(OUT_FILE); + + template = replaceSection(template, "parameters", vectorTilesDoc(node)); + + writeFile(OUT_FILE, template); + assertFileEquals(original, OUT_FILE); + } + + private NodeAdapter readVectorTiles() { + var path = ResourceLoader.of(this).file("router-config.json").toPath(); + var json = jsonNodeFromPath(path); + var conf = new RouterConfig(json, path.toString(), false); + return conf.asNodeAdapter().child("vectorTiles"); + } + + private String vectorTilesDoc(NodeAdapter node) { + DocBuilder buf = new DocBuilder(); + addParameterSummaryTable(buf, node); + addDetailsSection(buf, node); + return buf.toString(); + } + + private void addParameterSummaryTable(DocBuilder buf, NodeAdapter node) { + buf.addSection(new ParameterSummaryTable(SKIP_NODES).createTable(node).toMarkdownTable()); + } + + private void addDetailsSection(DocBuilder buf, NodeAdapter node) { + String details = getParameterDetailsTable(node); + + if (!details.isBlank()) { + buf.header(4, "Details", null).addSection(details); + } + } + + private String getParameterDetailsTable(NodeAdapter node) { + return ParameterDetailsList.listParametersWithDetails(node, SKIP_NODES, HEADER_4); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/VectorTilesResourceTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/VectorTilesResourceTest.java new file mode 100644 index 00000000000..ff9509a8474 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/VectorTilesResourceTest.java @@ -0,0 +1,31 @@ +package org.opentripplanner.ext.vectortiles; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.glassfish.grizzly.http.server.Request; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opentripplanner.TestServerContext; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.test.support.HttpForTest; +import org.opentripplanner.transit.service.TransitModel; + +class VectorTilesResourceTest { + + @Test + void tileJson() { + // the Grizzly request is awful to instantiate, using Mockito + var grizzlyRequest = Mockito.mock(Request.class); + var resource = new VectorTilesResource( + TestServerContext.createServerContext(new Graph(), new TransitModel()), + grizzlyRequest, + "default" + ); + var req = HttpForTest.containerRequest(); + var tileJson = resource.getTileJson(req.getUriInfo(), req, "layer1,layer2"); + assertEquals( + "https://localhost:8080/otp/routers/default/vectorTiles/layer1,layer2/{z}/{x}/{y}.pbf", + tileJson.tiles[0] + ); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java index 1442b57fd60..1ec7d042894 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java @@ -97,21 +97,23 @@ public void vehicleParkingGroupGeometryTest() { var config = """ { - "vectorTileLayers": [ - { - "name": "vehicleParkingGroups", - "type": "VehicleParkingGroup", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 600, - "expansionFactor": 0 - } - ] + "vectorTiles": { + "layers" :[ + { + "name": "vehicleParkingGroups", + "type": "VehicleParkingGroup", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600, + "expansionFactor": 0 + } + ] + } } """; var nodeAdapter = newNodeAdapterForTest(config); - var tiles = VectorTileConfig.mapVectorTilesParameters(nodeAdapter, "vectorTileLayers"); + var tiles = VectorTileConfig.mapVectorTilesParameters(nodeAdapter, "vectorTiles"); assertEquals(1, tiles.layers().size()); var builder = new VehicleParkingGroupsLayerBuilderWithPublicGeometry( graph, diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java index b4988ab398d..fdb723b3dc7 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java @@ -93,23 +93,25 @@ public void vehicleParkingGeometryTest() { var config = """ { - "vectorTileLayers": [ - { - "name": "vehicleParking", - "type": "VehicleParking", - "mapper": "Stadtnavi", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60, - "expansionFactor": 0 - } - ] + "vectorTiles": { + "layers" : [ + { + "name": "vehicleParking", + "type": "VehicleParking", + "mapper": "Stadtnavi", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0 + } + ] + } } """; var nodeAdapter = newNodeAdapterForTest(config); - var tiles = VectorTileConfig.mapVectorTilesParameters(nodeAdapter, "vectorTileLayers"); + var tiles = VectorTileConfig.mapVectorTilesParameters(nodeAdapter, "vectorTiles"); assertEquals(1, tiles.layers().size()); - var builder = new VehicleParkingsLayerBuilder(graph, tiles.layers().get(0), Locale.US); + var builder = new VehicleParkingsLayerBuilder(graph, tiles.layers().getFirst(), Locale.US); List geometries = builder.getGeometries(new Envelope(0.99, 1.01, 1.99, 2.01)); diff --git a/src/ext-test/java/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/VehicleRentalServiceDirectoryConfigDocTest.java b/src/ext-test/java/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/VehicleRentalServiceDirectoryConfigDocTest.java index 51f72c05e3e..4936bb4dd44 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/VehicleRentalServiceDirectoryConfigDocTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/VehicleRentalServiceDirectoryConfigDocTest.java @@ -37,7 +37,7 @@ public class VehicleRentalServiceDirectoryConfigDocTest { public void updateConfigurationDoc() { NodeAdapter node = readConfigDefaults(); - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String doc = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java index af2715d6928..772db7394f3 100644 --- a/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java +++ b/src/ext/java/org/opentripplanner/ext/vectortiles/VectorTilesResource.java @@ -66,7 +66,7 @@ public Response tileGet( z, locale, Arrays.asList(requestedLayers.split(",")), - serverContext.vectorTileLayers().layers(), + serverContext.vectorTileConfig().layers(), VectorTilesResource::crateLayerBuilder, serverContext ); @@ -89,15 +89,19 @@ public TileJson getTileJson( .filter(Predicate.not(Objects::isNull)) .toList(); - return new TileJson( - uri, - headers, - requestedLayers, - ignoreRouterId, - "vectorTiles", - envelope, - feedInfos - ); + List rLayers = Arrays.asList(requestedLayers.split(",")); + + var url = serverContext + .vectorTileConfig() + .basePath() + .map(overrideBasePath -> + TileJson.urlFromOverriddenBasePath(uri, headers, overrideBasePath, rLayers) + ) + .orElseGet(() -> + TileJson.urlWithDefaultPath(uri, headers, rLayers, ignoreRouterId, "vectorTiles") + ); + + return new TileJson(url, envelope, feedInfos); } private static LayerBuilder crateLayerBuilder( diff --git a/src/ext/resources/org/opentripplanner/ext/vectortiles/router-config.json b/src/ext/resources/org/opentripplanner/ext/vectortiles/router-config.json new file mode 100644 index 00000000000..df325d076a3 --- /dev/null +++ b/src/ext/resources/org/opentripplanner/ext/vectortiles/router-config.json @@ -0,0 +1,16 @@ +{ + "vectorTiles": { + "basePath": "/otp_ct/vectorTiles", + "layers": [ + { + "name": "stops", + "type": "Stop", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + } + ] + } +} + diff --git a/src/main/java/org/opentripplanner/apis/support/TileJson.java b/src/main/java/org/opentripplanner/apis/support/TileJson.java index 2259d72d828..75aabb2b6c6 100644 --- a/src/main/java/org/opentripplanner/apis/support/TileJson.java +++ b/src/main/java/org/opentripplanner/apis/support/TileJson.java @@ -4,7 +4,9 @@ import jakarta.ws.rs.core.UriInfo; import java.io.Serializable; import java.util.Collection; +import java.util.List; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.opentripplanner.framework.io.HttpUtils; import org.opentripplanner.model.FeedInfo; import org.opentripplanner.service.worldenvelope.model.WorldEnvelope; @@ -34,15 +36,7 @@ public class TileJson implements Serializable { public final double[] bounds; public final double[] center; - public TileJson( - UriInfo uri, - HttpHeaders headers, - String layers, - String ignoreRouterId, - String path, - WorldEnvelope envelope, - Collection feedInfos - ) { + public TileJson(String tileUrl, WorldEnvelope envelope, Collection feedInfos) { attribution = feedInfos .stream() @@ -51,15 +45,7 @@ public TileJson( ) .collect(Collectors.joining(", ")); - tiles = - new String[] { - "%s/otp/routers/%s/%s/%s/{z}/{x}/{y}.pbf".formatted( - HttpUtils.getBaseAddress(uri, headers), - ignoreRouterId, - path, - layers - ), - }; + tiles = new String[] { tileUrl }; bounds = new double[] { @@ -72,4 +58,42 @@ public TileJson( var c = envelope.center(); center = new double[] { c.longitude(), c.latitude(), 9 }; } + + /** + * Creates a vector source layer URL from a hard-coded path plus information from the incoming + * HTTP request. + */ + public static String urlWithDefaultPath( + UriInfo uri, + HttpHeaders headers, + List layers, + String ignoreRouterId, + String path + ) { + return "%s/otp/routers/%s/%s/%s/{z}/{x}/{y}.pbf".formatted( + HttpUtils.getBaseAddress(uri, headers), + ignoreRouterId, + path, + String.join(",", layers) + ); + } + + /** + * Creates a vector source layer URL from a configured base path plus information from the incoming + * HTTP request. + */ + public static String urlFromOverriddenBasePath( + UriInfo uri, + HttpHeaders headers, + String overridePath, + List layers + ) { + var strippedPath = StringUtils.stripStart(overridePath, "/"); + strippedPath = StringUtils.stripEnd(strippedPath, "/"); + return "%s/%s/%s/{z}/{x}/{y}.pbf".formatted( + HttpUtils.getBaseAddress(uri, headers), + strippedPath, + String.join(",", layers) + ); + } } diff --git a/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java b/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java index b94482af711..03f4357e540 100644 --- a/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java +++ b/src/main/java/org/opentripplanner/apis/vectortiles/GraphInspectorVectorTileResource.java @@ -110,16 +110,16 @@ public TileJson getTileJson( ) { var envelope = serverContext.worldEnvelopeService().envelope().orElseThrow(); List feedInfos = feedInfos(); + List rlayer = Arrays.asList(requestedLayers.split(",")); - return new TileJson( + var url = TileJson.urlWithDefaultPath( uri, headers, - requestedLayers, + rlayer, ignoreRouterId, - "inspector/vectortile", - envelope, - feedInfos + "inspector/vectortile" ); + return new TileJson(url, envelope, feedInfos); } @GET diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index fa6ead99c5e..fa3a7069e2d 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -9,7 +9,6 @@ import org.opentripplanner.ext.emissions.EmissionsService; import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; -import org.opentripplanner.ext.vectortiles.VectorTilesResource; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.inspector.raster.TileRendererManager; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; @@ -23,6 +22,7 @@ import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; +import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.search.state.State; @@ -119,7 +119,7 @@ default GraphFinder graphFinder() { FlexConfig flexConfig(); - VectorTilesResource.LayersParameters vectorTileLayers(); + VectorTileConfig vectorTileConfig(); default DataOverlayContext dataOverlayContext(RouteRequest request) { return OTPFeature.DataOverlay.isOnElseNull(() -> diff --git a/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java b/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java index ae92486037d..bf97155b747 100644 --- a/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java @@ -9,7 +9,6 @@ import java.io.Serializable; import java.util.List; import org.opentripplanner.ext.ridehailing.RideHailingServiceParameters; -import org.opentripplanner.ext.vectortiles.VectorTilesResource; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; import org.opentripplanner.standalone.config.routerconfig.RideHailingServicesConfig; @@ -48,7 +47,7 @@ public class RouterConfig implements Serializable { private final RideHailingServicesConfig rideHailingConfig; private final FlexConfig flexConfig; private final TransmodelAPIConfig transmodelApi; - private final VectorTileConfig vectorTileLayers; + private final VectorTileConfig vectorTileConfig; public RouterConfig(JsonNode node, String source, boolean logUnusedParams) { this(new NodeAdapter(node, source), logUnusedParams); @@ -72,7 +71,7 @@ public RouterConfig(JsonNode node, String source, boolean logUnusedParams) { this.routingRequestDefaults.setMaxSearchWindow(transitConfig.maxSearchWindow()); this.updatersParameters = new UpdatersConfig(root); this.rideHailingConfig = new RideHailingServicesConfig(root); - this.vectorTileLayers = VectorTileConfig.mapVectorTilesParameters(root, "vectorTileLayers"); + this.vectorTileConfig = VectorTileConfig.mapVectorTilesParameters(root, "vectorTiles"); this.flexConfig = new FlexConfig(root, "flex"); if (logUnusedParams && LOG.isWarnEnabled()) { @@ -124,8 +123,8 @@ public List rideHailingServiceParameters() { return rideHailingConfig.rideHailingServiceParameters(); } - public VectorTilesResource.LayersParameters vectorTileLayers() { - return vectorTileLayers; + public VectorTileConfig vectorTileConfig() { + return vectorTileConfig; } public FlexConfig flexConfig() { 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 a0342910be9..6f7d6967ce8 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/VectorTileConfig.java @@ -5,9 +5,12 @@ import static org.opentripplanner.inspector.vector.LayerParameters.MAX_ZOOM; 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 java.util.Collection; import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; import org.opentripplanner.ext.vectortiles.VectorTilesResource; import org.opentripplanner.inspector.vector.LayerParameters; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; @@ -15,12 +18,18 @@ public class VectorTileConfig implements VectorTilesResource.LayersParameters { - List> layers; + public static final VectorTileConfig DEFAULT = new VectorTileConfig(List.of(), null); + private final List> layers; - public VectorTileConfig( - Collection> layers + @Nullable + private final String basePath; + + VectorTileConfig( + Collection> layers, + @Nullable String basePath ) { this.layers = List.copyOf(layers); + this.basePath = basePath; } @Override @@ -28,16 +37,41 @@ public List> layers() { return layers; } - public static VectorTileConfig mapVectorTilesParameters( - NodeAdapter root, - String vectorTileLayers - ) { + public Optional basePath() { + return Optional.ofNullable(basePath); + } + + public static VectorTileConfig mapVectorTilesParameters(NodeAdapter node, String paramName) { + var root = node.of(paramName).summary("Vector tile configuration").asObject(); return new VectorTileConfig( root - .of(vectorTileLayers) + .of("layers") .since(V2_0) .summary("Configuration of the individual layers for the Mapbox vector tiles.") - .asObjects(VectorTileConfig::mapLayer) + .asObjects(VectorTileConfig::mapLayer), + root + .of("basePath") + .since(V2_5) + .summary("The path of the vector tile source URLs in `tilejson.json`.") + .description( + """ + This is useful if you have a proxy setup and rewrite the path that is passed to OTP. + + If you don't configure this optional value then the path returned in `tilejson.json` is in + the format `/otp/routers/default/vectorTiles/layer1,layer2/{z}/{x}/{x}.pbf`. + If you, for example, set a value of `/otp_test/tiles` then the returned path changes to + `/otp_test/tiles/layer1,layer2/{z}/{x}/{x}.pbf`. + + The protocol and host are always read from the incoming HTTP request. If you run OTP behind + a proxy then make sure to set the headers `X-Forwarded-Proto` and `X-Forwarded-Host` to make OTP + return the protocol and host for the original request and not the proxied one. + + **Note:** This does _not_ change the path that OTP itself serves the tiles or `tilejson.json` + responses but simply changes the URLs listed in `tilejson.json`. The rewriting of the path + is expected to be handled by a proxy. + """ + ) + .asString(DEFAULT.basePath) ); } diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index c9d7253b0be..5d8efcd3a5b 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -49,7 +49,7 @@ OtpServerRequestContext providesServerContext( graph, transitService, Metrics.globalRegistry, - routerConfig.vectorTileLayers(), + routerConfig.vectorTileConfig(), worldEnvelopeService, realtimeVehicleService, vehicleRentalService, diff --git a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index f14fea66693..9a586219ba1 100644 --- a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -8,7 +8,6 @@ import org.opentripplanner.ext.emissions.EmissionsService; import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; -import org.opentripplanner.ext.vectortiles.VectorTilesResource; import org.opentripplanner.inspector.raster.TileRendererManager; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; import org.opentripplanner.raptor.configure.RaptorConfig; @@ -24,6 +23,7 @@ import org.opentripplanner.standalone.api.HttpRequestScoped; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.standalone.config.routerconfig.TransitRoutingConfig; +import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.transit.service.TransitService; @@ -39,7 +39,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { private final MeterRegistry meterRegistry; private final RaptorConfig raptorConfig; private final TileRendererManager tileRendererManager; - private final VectorTilesResource.LayersParameters vectorTileLayers; + private final VectorTileConfig vectorTileConfig; private final FlexConfig flexConfig; private final TraverseVisitor traverseVisitor; private final WorldEnvelopeService worldEnvelopeService; @@ -59,7 +59,7 @@ private DefaultServerRequestContext( MeterRegistry meterRegistry, RaptorConfig raptorConfig, TileRendererManager tileRendererManager, - VectorTilesResource.LayersParameters vectorTileLayers, + VectorTileConfig vectorTileConfig, WorldEnvelopeService worldEnvelopeService, RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, @@ -75,7 +75,7 @@ private DefaultServerRequestContext( this.meterRegistry = meterRegistry; this.raptorConfig = raptorConfig; this.tileRendererManager = tileRendererManager; - this.vectorTileLayers = vectorTileLayers; + this.vectorTileConfig = vectorTileConfig; this.vehicleRentalService = vehicleRentalService; this.flexConfig = flexConfig; this.traverseVisitor = traverseVisitor; @@ -97,7 +97,7 @@ public static DefaultServerRequestContext create( Graph graph, TransitService transitService, MeterRegistry meterRegistry, - VectorTilesResource.LayersParameters vectorTileLayers, + VectorTileConfig vectorTileConfig, WorldEnvelopeService worldEnvelopeService, RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, @@ -115,7 +115,7 @@ public static DefaultServerRequestContext create( meterRegistry, raptorConfig, new TileRendererManager(graph, routeRequestDefaults.preferences()), - vectorTileLayers, + vectorTileConfig, worldEnvelopeService, realtimeVehicleService, vehicleRentalService, @@ -220,8 +220,8 @@ public FlexConfig flexConfig() { } @Override - public VectorTilesResource.LayersParameters vectorTileLayers() { - return vectorTileLayers; + public VectorTileConfig vectorTileConfig() { + return vectorTileConfig; } @Override diff --git a/src/test/java/org/opentripplanner/TestServerContext.java b/src/test/java/org/opentripplanner/TestServerContext.java index 1f3e6491232..5d74dbba240 100644 --- a/src/test/java/org/opentripplanner/TestServerContext.java +++ b/src/test/java/org/opentripplanner/TestServerContext.java @@ -16,6 +16,7 @@ import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; import org.opentripplanner.service.worldenvelope.internal.DefaultWorldEnvelopeRepository; import org.opentripplanner.service.worldenvelope.internal.DefaultWorldEnvelopeService; +import org.opentripplanner.service.worldenvelope.model.WorldEnvelope; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.standalone.config.RouterConfig; import org.opentripplanner.standalone.server.DefaultServerRequestContext; @@ -42,7 +43,7 @@ public static OtpServerRequestContext createServerContext( graph, new DefaultTransitService(transitModel), Metrics.globalRegistry, - routerConfig.vectorTileLayers(), + routerConfig.vectorTileConfig(), createWorldEnvelopeService(), createRealtimeVehicleService(transitService), createVehicleRentalService(), @@ -58,7 +59,14 @@ public static OtpServerRequestContext createServerContext( /** Static factory method to create a service for test purposes. */ public static WorldEnvelopeService createWorldEnvelopeService() { - return new DefaultWorldEnvelopeService(new DefaultWorldEnvelopeRepository()); + var repository = new DefaultWorldEnvelopeRepository(); + var envelope = WorldEnvelope + .of() + .expandToIncludeStreetEntities(0, 0) + .expandToIncludeStreetEntities(1, 1) + .build(); + repository.saveEnvelope(envelope); + return new DefaultWorldEnvelopeService(repository); } public static RealtimeVehicleService createRealtimeVehicleService(TransitService transitService) { diff --git a/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java new file mode 100644 index 00000000000..ac3b7bca522 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/support/TileJsonTest.java @@ -0,0 +1,43 @@ +package org.opentripplanner.apis.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.glassfish.jersey.server.internal.routing.UriRoutingContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentripplanner.test.support.HttpForTest; + +class TileJsonTest { + + private static final List LAYERS = List.of("stops", "rentalVehicles"); + + @ParameterizedTest + @ValueSource( + strings = { + "/otp_ct/vectorTiles", + "otp_ct/vectorTiles/", + "otp_ct/vectorTiles///", + "///otp_ct/vectorTiles/", + } + ) + void overrideBasePath(String basePath) { + var req = HttpForTest.containerRequest(); + var uriInfo = new UriRoutingContext(req); + assertEquals( + "https://localhost:8080/otp_ct/vectorTiles/stops,rentalVehicles/{z}/{x}/{y}.pbf", + TileJson.urlFromOverriddenBasePath(uriInfo, req, basePath, LAYERS) + ); + } + + @Test + void defaultPath() { + var req = HttpForTest.containerRequest(); + var uriInfo = new UriRoutingContext(req); + assertEquals( + "https://localhost:8080/otp/routers/default/vectorTiles/stops,rentalVehicles/{z}/{x}/{y}.pbf", + TileJson.urlWithDefaultPath(uriInfo, req, LAYERS, "default", "vectorTiles") + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index 9a01a36cbcb..127fe66f0ea 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -124,7 +124,7 @@ public class TripRequestMapperTest implements PlanTestConstants { graph, transitService, Metrics.globalRegistry, - RouterConfig.DEFAULT.vectorTileLayers(), + RouterConfig.DEFAULT.vectorTileConfig(), new DefaultWorldEnvelopeService(new DefaultWorldEnvelopeRepository()), new DefaultRealtimeVehicleService(transitService), new DefaultVehicleRentalService(), diff --git a/src/test/java/org/opentripplanner/generate/doc/BuildConfigurationDocTest.java b/src/test/java/org/opentripplanner/generate/doc/BuildConfigurationDocTest.java index 287b0145292..4009a455abe 100644 --- a/src/test/java/org/opentripplanner/generate/doc/BuildConfigurationDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/BuildConfigurationDocTest.java @@ -49,7 +49,7 @@ public class BuildConfigurationDocTest { public void updateBuildConfigurationDoc() { NodeAdapter node = readBuildConfig(); - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String doc = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/ConfigurationDocTest.java b/src/test/java/org/opentripplanner/generate/doc/ConfigurationDocTest.java index 2f972e8300a..19a562d8b3f 100644 --- a/src/test/java/org/opentripplanner/generate/doc/ConfigurationDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/ConfigurationDocTest.java @@ -36,7 +36,7 @@ public class ConfigurationDocTest { */ @Test public void updateConfigurationDoc() { - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String doc = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/FlexConfigurationDocTest.java b/src/test/java/org/opentripplanner/generate/doc/FlexConfigurationDocTest.java index 2b440d7545a..fd2d7092dc5 100644 --- a/src/test/java/org/opentripplanner/generate/doc/FlexConfigurationDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/FlexConfigurationDocTest.java @@ -33,7 +33,7 @@ public class FlexConfigurationDocTest { public void updateFlexDoc() { NodeAdapter node = readFlexConfig(); - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String template = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/GraphQLTutorialDocTest.java b/src/test/java/org/opentripplanner/generate/doc/GraphQLTutorialDocTest.java index bf2b092e092..5e858ff4d61 100644 --- a/src/test/java/org/opentripplanner/generate/doc/GraphQLTutorialDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/GraphQLTutorialDocTest.java @@ -33,7 +33,7 @@ public class GraphQLTutorialDocTest { */ @Test public void updateTutorialDoc() throws IOException { - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String doc = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/RouteRequestDocTest.java b/src/test/java/org/opentripplanner/generate/doc/RouteRequestDocTest.java index 9a39b3cfa3f..76642db3e5a 100644 --- a/src/test/java/org/opentripplanner/generate/doc/RouteRequestDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/RouteRequestDocTest.java @@ -44,7 +44,7 @@ public class RouteRequestDocTest { public void updateRouteRequestConfigurationDoc() { NodeAdapter node = readRoutingDefaults(); - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String doc = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/RouterConfigurationDocTest.java b/src/test/java/org/opentripplanner/generate/doc/RouterConfigurationDocTest.java index d13f423ffc4..90cdd9de975 100644 --- a/src/test/java/org/opentripplanner/generate/doc/RouterConfigurationDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/RouterConfigurationDocTest.java @@ -33,7 +33,7 @@ public class RouterConfigurationDocTest { .skip("flex", "sandbox/Flex.md") .skip("routingDefaults", "RouteRequest.md") .skip("updaters", "UpdaterConfig.md") - .skip("vectorTileLayers", "sandbox/MapboxVectorTilesApi.md") + .skip("vectorTiles", "sandbox/MapboxVectorTilesApi.md") .skipNestedElements("transferCacheRequests", "RouteRequest.md") .skip("rideHailingServices", "sandbox/RideHailing.md") .skip("vehicleRentalServiceDirectory", "sandbox/VehicleRentalServiceDirectory.md") @@ -51,7 +51,7 @@ public class RouterConfigurationDocTest { public void updateBuildConfigurationDoc() { NodeAdapter node = readRouterConfig(); - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String doc = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/RoutingModeDocTest.java b/src/test/java/org/opentripplanner/generate/doc/RoutingModeDocTest.java index e08de453630..0c6edc7e16b 100644 --- a/src/test/java/org/opentripplanner/generate/doc/RoutingModeDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/RoutingModeDocTest.java @@ -24,7 +24,7 @@ public class RoutingModeDocTest { @Test public void updateDocs() { - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String doc = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/UpdaterConfigDocTest.java b/src/test/java/org/opentripplanner/generate/doc/UpdaterConfigDocTest.java index 3d0cb547b7a..fa5abca7814 100644 --- a/src/test/java/org/opentripplanner/generate/doc/UpdaterConfigDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/UpdaterConfigDocTest.java @@ -50,7 +50,7 @@ public class UpdaterConfigDocTest { public void updateRouterConfigurationDoc() { NodeAdapter node = readBuildConfig(); - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String template = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/generate/doc/VehicleParkingDocTest.java b/src/test/java/org/opentripplanner/generate/doc/VehicleParkingDocTest.java index de9b921c27a..abc9ceee806 100644 --- a/src/test/java/org/opentripplanner/generate/doc/VehicleParkingDocTest.java +++ b/src/test/java/org/opentripplanner/generate/doc/VehicleParkingDocTest.java @@ -33,7 +33,7 @@ public class VehicleParkingDocTest { public void updateVehicleParkingDoc() { NodeAdapter node = readVehicleUpdaters(); - // Read and close inout file (same as output file) + // Read and close input file (same as output file) String template = readFile(TEMPLATE); String original = readFile(OUT_FILE); diff --git a/src/test/java/org/opentripplanner/standalone/server/EtagRequestFilterTest.java b/src/test/java/org/opentripplanner/standalone/server/EtagRequestFilterTest.java index 1451a218852..5adf8264d8e 100644 --- a/src/test/java/org/opentripplanner/standalone/server/EtagRequestFilterTest.java +++ b/src/test/java/org/opentripplanner/standalone/server/EtagRequestFilterTest.java @@ -8,7 +8,6 @@ import java.nio.charset.StandardCharsets; import java.util.stream.Stream; import javax.annotation.Nonnull; -import org.glassfish.jersey.internal.MapPropertiesDelegate; import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; import org.glassfish.jersey.message.internal.OutboundMessageContext; import org.glassfish.jersey.message.internal.Statuses; @@ -17,6 +16,7 @@ import org.jets3t.service.utils.Mimetypes; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.opentripplanner.test.support.HttpForTest; import org.opentripplanner.test.support.VariableSource; class EtagRequestFilterTest { @@ -44,7 +44,7 @@ void writeEtag( byte[] entity, String expectedEtag ) throws IOException { - var request = request(method); + var request = HttpForTest.containerRequest(method); var response = response(status, request); var headers = response.getHeaders(); headers.add(EtagRequestFilter.HEADER_CONTENT_TYPE, responseContentType); @@ -65,7 +65,7 @@ void writeEtag( @VariableSource("ifNoneMatchCases") void ifNoneMatch(String ifNoneMatch, int expectedStatus, byte[] expectedEntity) throws IOException { - var request = request("GET"); + var request = HttpForTest.containerRequest("GET"); request.header(EtagRequestFilter.HEADER_IF_NONE_MATCH, ifNoneMatch); var response = response(200, request); var headers = response.getHeaders(); @@ -92,9 +92,4 @@ private static ContainerResponse response(int status, ContainerRequest request) private static byte[] bytes(String input) { return input.getBytes(StandardCharsets.UTF_8); } - - @Nonnull - private static ContainerRequest request(String method) { - return new ContainerRequest(null, null, method, null, new MapPropertiesDelegate(), null); - } } diff --git a/src/test/java/org/opentripplanner/test/support/HttpForTest.java b/src/test/java/org/opentripplanner/test/support/HttpForTest.java new file mode 100644 index 00000000000..7bbe272572d --- /dev/null +++ b/src/test/java/org/opentripplanner/test/support/HttpForTest.java @@ -0,0 +1,22 @@ +package org.opentripplanner.test.support; + +import java.net.URI; +import java.net.URISyntaxException; +import org.glassfish.jersey.internal.MapPropertiesDelegate; +import org.glassfish.jersey.server.ContainerRequest; + +public class HttpForTest { + + public static ContainerRequest containerRequest(String method) { + try { + URI uri = new URI("https://localhost:8080"); + return new ContainerRequest(uri, uri, method, null, new MapPropertiesDelegate(), null); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + public static ContainerRequest containerRequest() { + return containerRequest("GET"); + } +} diff --git a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index 642e192539c..7a11c35bace 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -29,6 +29,7 @@ import org.opentripplanner.standalone.config.BuildConfig; import org.opentripplanner.standalone.config.ConfigModel; import org.opentripplanner.standalone.config.OtpConfigLoader; +import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; import org.opentripplanner.standalone.server.DefaultServerRequestContext; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; @@ -111,7 +112,7 @@ public SpeedTest( graph, new DefaultTransitService(transitModel), timer.getRegistry(), - List::of, + VectorTileConfig.DEFAULT, TestServerContext.createWorldEnvelopeService(), TestServerContext.createRealtimeVehicleService(transitService), TestServerContext.createVehicleRentalService(), diff --git a/src/test/resources/standalone/config/router-config.json b/src/test/resources/standalone/config/router-config.json index 863b9bec279..9293cfad8f4 100644 --- a/src/test/resources/standalone/config/router-config.json +++ b/src/test/resources/standalone/config/router-config.json @@ -80,11 +80,14 @@ "accessEgress": { "maxDuration": "45m", "maxDurationForMode": { - "BIKE_RENTAL": "20m" + "BIKE_RENTAL": "20m" }, "maxStopCount": 500, "penalty": { - "FLEXIBLE" : { "timePenalty": "2m + 1.1t", "costFactor": 1.7 } + "FLEXIBLE": { + "timePenalty": "2m + 1.1t", + "costFactor": 1.7 + } } }, "itineraryFilters": { @@ -103,8 +106,12 @@ "geoidElevation": false, "maxJourneyDuration": "36h", "unpreferred": { - "agencies": ["HSL:123"], - "routes": ["HSL:456"] + "agencies": [ + "HSL:123" + ], + "routes": [ + "HSL:456" + ] }, "unpreferredCost": "10m + 2.0 x", "streetRoutingTimeout": "5s", @@ -158,8 +165,15 @@ "PREFERRED": 0 }, "transferCacheRequests": [ - { "modes": "WALK" }, - { "modes": "WALK", "wheelchairAccessibility": { "enabled": true } } + { + "modes": "WALK" + }, + { + "modes": "WALK", + "wheelchairAccessibility": { + "enabled": true + } + } ] }, "vehicleRentalServiceDirectory": { @@ -174,61 +188,64 @@ "transmodelApi": { "hideFeedId": true }, - "vectorTileLayers": [ - { - "name": "stops", - "type": "Stop", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 600 - }, - { - "name": "stations", - "type": "Station", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 12, - "cacheMaxSeconds": 600 - }, - { - "name": "rentalPlaces", - // all rental places: stations and free-floating vehicles - "type": "VehicleRental", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60, - "expansionFactor": 0.25 - }, - { - "name": "rentalVehicle", - // just free-floating vehicles - "type": "VehicleRentalVehicle", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60 - }, - { - "name": "rentalStation", - // just rental stations - "type": "VehicleRentalStation", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 600 - }, - { - "name": "vehicleParking", - "type": "VehicleParking", - "mapper": "Digitransit", - "maxZoom": 20, - "minZoom": 14, - "cacheMaxSeconds": 60, - "expansionFactor": 0.25 - } - ], + "vectorTiles": { + "basePath": "/otp_ct/vectorTiles", + "layers": [ + { + "name": "stops", + "type": "Stop", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, + { + "name": "stations", + "type": "Station", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 12, + "cacheMaxSeconds": 600 + }, + { + "name": "rentalPlaces", + // all rental places: stations and free-floating vehicles + "type": "VehicleRental", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + }, + { + "name": "rentalVehicle", + // just free-floating vehicles + "type": "VehicleRentalVehicle", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60 + }, + { + "name": "rentalStation", + // just rental stations + "type": "VehicleRentalStation", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 600 + }, + { + "name": "vehicleParking", + "type": "VehicleParking", + "mapper": "Digitransit", + "maxZoom": 20, + "minZoom": 14, + "cacheMaxSeconds": 60, + "expansionFactor": 0.25 + } + ] + }, "timetableUpdates": { "purgeExpiredData": false, "maxSnapshotFrequency": "2s" @@ -324,7 +341,9 @@ "Header-Name": "Header-Value" }, "fuzzyTripMatching": false, - "features": ["position"] + "features": [ + "position" + ] }, // Siri-ET over HTTP { @@ -368,8 +387,10 @@ "clientSecret": "very-secret", "wheelchairAccessibleProductId": "545de0c4-659f-49c6-be65-0d5e448dffd5", "bannedProductIds": [ - "1196d0dd-423b-4a81-a1d8-615367d3a365", "f58761e5-8dd5-4940-a472-872f1236c596" + "1196d0dd-423b-4a81-a1d8-615367d3a365", + "f58761e5-8dd5-4940-a472-872f1236c596" ] } ] } +