diff --git a/application/src/ext-test/resources/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/router-config.json b/application/src/ext-test/resources/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/router-config.json index e2dc7e202d2..9e38f625cb3 100644 --- a/application/src/ext-test/resources/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/router-config.json +++ b/application/src/ext-test/resources/org/opentripplanner/ext/vehiclerentalservicedirectory/generatedoc/router-config.json @@ -1,9 +1,6 @@ { "vehicleRentalServiceDirectory": { - "url": "https://example.com", - "sourcesName": "systems", - "updaterUrlName": "url", - "updaterNetworkName": "id", + "url": "https://example.com/gbfs/v3/manifest.json", "headers": { "ET-Client-Name": "otp" }, diff --git a/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java b/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java index 179a158a2a9..28f9e2f6389 100644 --- a/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java +++ b/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/VehicleRentalServiceDirectoryFetcher.java @@ -1,18 +1,24 @@ package org.opentripplanner.ext.vehiclerentalservicedirectory; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; +import org.mobilitydata.gbfs.v3_0.manifest.GBFSDataset; +import org.mobilitydata.gbfs.v3_0.manifest.GBFSManifest; +import org.mobilitydata.gbfs.v3_0.manifest.GBFSVersion; import org.opentripplanner.ext.vehiclerentalservicedirectory.api.VehicleRentalServiceDirectoryFetcherParameters; import org.opentripplanner.framework.io.OtpHttpClientException; import org.opentripplanner.framework.io.OtpHttpClientFactory; -import org.opentripplanner.framework.json.JsonUtils; import org.opentripplanner.routing.linking.VertexLinker; import org.opentripplanner.service.vehiclerental.VehicleRentalRepository; import org.opentripplanner.updater.spi.GraphUpdater; @@ -24,8 +30,8 @@ import org.slf4j.LoggerFactory; /** - * Fetches GBFS endpoints from the micromobility aggregation service located at - * https://github.com/entur/lamassu, which is an API for aggregating GBFS endpoints. + * Fetches GBFS endpoints from a GBFS v3 manifest.json file. + * The manifest can be loaded from a remote URL or a local file. */ public class VehicleRentalServiceDirectoryFetcher { @@ -33,6 +39,9 @@ public class VehicleRentalServiceDirectoryFetcher { VehicleRentalServiceDirectoryFetcher.class ); private static final Duration DEFAULT_FREQUENCY = Duration.ofSeconds(15); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private final VertexLinker vertexLinker; private final VehicleRentalRepository repository; @@ -53,15 +62,18 @@ public static List createUpdatersFromEndpoint( VertexLinker vertexLinker, VehicleRentalRepository repository ) { - LOG.info("Fetching list of updaters from {}", parameters.getUrl()); + LOG.info("Fetching GBFS v3 manifest from {}", parameters.getUrl()); - var sources = listSources(parameters); + var manifest = loadManifest(parameters); - if (sources.isEmpty()) { + if ( + manifest == null || manifest.getData() == null || manifest.getData().getDatasets() == null + ) { + LOG.warn("No datasets found in manifest from {}", parameters.getUrl()); return List.of(); } - int maxHttpConnections = sources.size(); + int maxHttpConnections = manifest.getData().getDatasets().size(); var otpHttpClientFactory = new OtpHttpClientFactory(maxHttpConnections); var serviceDirectory = new VehicleRentalServiceDirectoryFetcher( @@ -69,61 +81,76 @@ public static List createUpdatersFromEndpoint( repository, otpHttpClientFactory ); - return serviceDirectory.createUpdatersFromEndpoint(parameters, sources); + return serviceDirectory.createUpdatersFromManifest(parameters, manifest); } - public List createUpdatersFromEndpoint( + public List createUpdatersFromManifest( VehicleRentalServiceDirectoryFetcherParameters parameters, - JsonNode sources + GBFSManifest manifest ) { return fetchUpdaterInfoFromDirectoryAndCreateUpdaters( - buildListOfNetworksFromConfig(parameters, sources) + buildListOfNetworksFromManifest(parameters, manifest) ); } - private static List buildListOfNetworksFromConfig( + private static List buildListOfNetworksFromManifest( VehicleRentalServiceDirectoryFetcherParameters parameters, - JsonNode sources + GBFSManifest manifest ) { List dataSources = new ArrayList<>(); - for (JsonNode source : sources) { - Optional network = JsonUtils.asText(source, parameters.getSourceNetworkName()); - Optional updaterUrl = JsonUtils.asText(source, parameters.getSourceUrlName()); + for (GBFSDataset dataset : manifest.getData().getDatasets()) { + String networkName = dataset.getSystemId(); + Optional gbfsUrl = selectBestVersion(dataset); - if (network.isEmpty() || updaterUrl.isEmpty()) { - LOG.warn( - "Error reading json from {}. Are json tag names configured properly?", - parameters.getUrl() + if (gbfsUrl.isEmpty()) { + LOG.warn("No suitable GBFS version found for system {}", networkName); + continue; + } + + var config = parameters.networkParameters(networkName); + + if (config.isPresent()) { + var networkParams = config.get(); + dataSources.add( + new GbfsVehicleRentalDataSourceParameters( + gbfsUrl.get(), + parameters.getLanguage(), + networkParams.allowKeepingAtDestination(), + parameters.getHeaders(), + networkName, + networkParams.geofencingZones(), + // overloadingAllowed - not part of GBFS, not supported here + false, + // rentalPickupType not supported + RentalPickupType.ALL + ) ); } else { - var networkName = network.get(); - var config = parameters.networkParameters(networkName); - - if (config.isPresent()) { - var networkParams = config.get(); - dataSources.add( - new GbfsVehicleRentalDataSourceParameters( - updaterUrl.get(), - parameters.getLanguage(), - networkParams.allowKeepingAtDestination(), - parameters.getHeaders(), - networkName, - networkParams.geofencingZones(), - // overloadingAllowed - not part of GBFS, not supported here - false, - // rentalPickupType not supported - RentalPickupType.ALL - ) - ); - } else { - LOG.warn("Network not configured in OTP: {}", networkName); - } + LOG.warn("Network not configured in OTP: {}", networkName); } } return dataSources; } + /** + * Selects the best (newest) GBFS version from the available versions for a dataset. + * Prefers v3.0 over v2.x versions. + */ + private static Optional selectBestVersion(GBFSDataset dataset) { + if (dataset.getVersions() == null || dataset.getVersions().isEmpty()) { + return Optional.empty(); + } + + // Sort versions by version number (descending) to prefer newer versions + return dataset + .getVersions() + .stream() + .sorted(Comparator.comparing(GBFSVersion::getVersion).reversed()) + .map(GBFSVersion::getUrl) + .findFirst(); + } + private List fetchUpdaterInfoFromDirectoryAndCreateUpdaters( List dataSources ) { @@ -153,31 +180,31 @@ private VehicleRentalUpdater fetchAndCreateUpdater( return new VehicleRentalUpdater(vehicleRentalParameters, dataSource, vertexLinker, repository); } - private static JsonNode listSources(VehicleRentalServiceDirectoryFetcherParameters parameters) { - JsonNode node; + private static GBFSManifest loadManifest( + VehicleRentalServiceDirectoryFetcherParameters parameters + ) { URI url = parameters.getUrl(); + try { - var otpHttpClient = new OtpHttpClientFactory().create(LOG); - node = otpHttpClient.getAndMapAsJsonNode(url, Map.of(), new ObjectMapper()); - } catch (OtpHttpClientException e) { - LOG.warn("Error fetching list of vehicle rental endpoints from {}", url, e); - return MissingNode.getInstance(); - } - if (node == null) { - LOG.warn("Error reading json from {}. Node is null!", url); - return MissingNode.getInstance(); - } + String manifestContent; + + // Check if URL is a file path + if ("file".equals(url.getScheme())) { + Path filePath = Path.of(url.getPath()); + manifestContent = Files.readString(filePath); + LOG.info("Loaded GBFS manifest from file: {}", filePath); + } else { + // Load from remote URL + var otpHttpClient = new OtpHttpClientFactory().create(LOG); + var jsonNode = otpHttpClient.getAndMapAsJsonNode(url, Map.of(), OBJECT_MAPPER); + manifestContent = OBJECT_MAPPER.writeValueAsString(jsonNode); + LOG.info("Loaded GBFS manifest from URL: {}", url); + } - String sourcesName = parameters.getSourcesName(); - JsonNode sources = node.get(sourcesName); - if (sources == null) { - LOG.warn( - "Error reading json from {}. No JSON node for sources name '{}' found.", - url, - sourcesName - ); - return MissingNode.getInstance(); + return OBJECT_MAPPER.readValue(manifestContent, GBFSManifest.class); + } catch (OtpHttpClientException | IOException e) { + LOG.error("Error loading GBFS manifest from {}", url, e); + return null; } - return sources; } } diff --git a/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/VehicleRentalServiceDirectoryFetcherParameters.java b/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/VehicleRentalServiceDirectoryFetcherParameters.java index eec06aab2fc..b68d41090a5 100644 --- a/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/VehicleRentalServiceDirectoryFetcherParameters.java +++ b/application/src/ext/java/org/opentripplanner/ext/vehiclerentalservicedirectory/api/VehicleRentalServiceDirectoryFetcherParameters.java @@ -8,17 +8,15 @@ import javax.annotation.Nullable; import org.opentripplanner.updater.spi.HttpHeaders; +/** + * Parameters for fetching vehicle rental services from a GBFS v3 manifest.json file. + * The manifest can be loaded from a remote URL or a local file path. + */ public class VehicleRentalServiceDirectoryFetcherParameters { public static final String DEFAULT_NETWORK_NAME = "default-network"; private final URI url; - private final String sourcesName; - - private final String sourceUrlName; - - private final String sourceNetworkName; - private final HttpHeaders headers; private final String language; @@ -30,17 +28,11 @@ public class VehicleRentalServiceDirectoryFetcherParameters { public VehicleRentalServiceDirectoryFetcherParameters( URI url, - String sourcesName, - String updaterUrlName, - String networkName, String language, HttpHeaders headers, Collection networkParameters ) { this.url = url; - this.sourcesName = sourcesName; - this.sourceUrlName = updaterUrlName; - this.sourceNetworkName = networkName; this.language = language; this.headers = headers; this.parametersForNetwork = networkParameters @@ -50,41 +42,16 @@ public VehicleRentalServiceDirectoryFetcherParameters( } /** - * Endpoint for the VehicleRentalServiceDirectory + * URL or file path to the GBFS v3 manifest.json *

- * This is required. + * This is required. Can be either: + * - A remote URL (http/https) + * - A local file path (file://) */ public URI getUrl() { return url; } - /** - * Json tag name for updater sources - *

- * Optional, default values is "systems". - */ - public String getSourcesName() { - return sourcesName; - } - - /** - * Json tag name for endpoint urls for each source - *

- * Optional, default values is "url". - */ - public String getSourceUrlName() { - return sourceUrlName; - } - - /** - * Json tag name for the network name for each source - *

- * Optional, default values is "id". - */ - public String getSourceNetworkName() { - return sourceNetworkName; - } - /** * Json tag name for http headers *

diff --git a/application/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java b/application/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java index e200cdcde9c..34fd415b4bb 100644 --- a/application/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java +++ b/application/src/main/java/org/opentripplanner/standalone/config/sandbox/VehicleRentalServiceDirectoryFetcherConfig.java @@ -20,7 +20,7 @@ public static VehicleRentalServiceDirectoryFetcherParameters create( var c = root .of(parameterName) .since(V2_0) - .summary("Configuration for the vehicle rental service directory.") + .summary("Configuration for the vehicle rental service directory using GBFS v3 manifest.") .asObject(); if (c.isEmpty()) { @@ -28,23 +28,16 @@ public static VehicleRentalServiceDirectoryFetcherParameters create( } return new VehicleRentalServiceDirectoryFetcherParameters( - c.of("url").since(V2_1).summary("Endpoint for the VehicleRentalServiceDirectory").asUri(), c - .of("sourcesName") + .of("url") .since(V2_1) - .summary("Json tag name for updater sources.") - .asString("systems"), - c - .of("updaterUrlName") - .since(V2_1) - .summary("Json tag name for endpoint urls for each source.") - .asString("url"), - c - .of("updaterNetworkName") - .since(V2_1) - .summary("Json tag name for the network name for each source.") - .asString("id"), - c.of("language").since(V2_1).summary("Language code.").asString(null), + .summary("URL or file path to the GBFS v3 manifest.json") + .description( + "Can be either a remote URL (http/https) or a local file path (file://). " + + "The manifest must conform to the GBFS v3.0 specification." + ) + .asUri(), + c.of("language").since(V2_1).summary("Language code for GBFS feeds.").asString(null), HttpHeadersConfig.headers(c, V2_1), mapNetworkParameters("networks", c) ); diff --git a/application/src/test/resources/standalone/config/router-config.json b/application/src/test/resources/standalone/config/router-config.json index 6732fd0cdcc..b9e99c55d69 100644 --- a/application/src/test/resources/standalone/config/router-config.json +++ b/application/src/test/resources/standalone/config/router-config.json @@ -193,10 +193,7 @@ ] }, "vehicleRentalServiceDirectory": { - "url": "https://entur.no/bikeRentalServiceDirectory", - "sourcesName": "systems", - "updaterUrlName": "url", - "updaterNetworkName": "id", + "url": "https://entur.no/bikeRentalServiceDirectory/manifest.json", "headers": { "ET-Client-Name": "MY_ORG_CLIENT_NAME" } diff --git a/doc/templates/sandbox/VehicleRentalServiceDirectory.md b/doc/templates/sandbox/VehicleRentalServiceDirectory.md index 787a17e90e4..54b73d25f5a 100644 --- a/doc/templates/sandbox/VehicleRentalServiceDirectory.md +++ b/doc/templates/sandbox/VehicleRentalServiceDirectory.md @@ -18,7 +18,7 @@ like OTP can connect to the directory and get the necessary configuration from i - Make json tag names configurable [#3447](https://github.com/opentripplanner/OpenTripPlanner/pull/3447) - Enable GBFS geofencing with VehicleRentalServiceDirectory [#5324](https://github.com/opentripplanner/OpenTripPlanner/pull/5324) - Enable `allowKeepingVehicleAtDestination` [#5944](https://github.com/opentripplanner/OpenTripPlanner/pull/5944) - +- Rewrite to use manifest.json from GBFS v3 as the service directory [#6900](https://github.com/opentripplanner/OpenTripPlanner/pull/6900) ## Configuration diff --git a/doc/user/RouterConfiguration.md b/doc/user/RouterConfiguration.md index 2d7e9983f10..7dc0a894d26 100644 --- a/doc/user/RouterConfiguration.md +++ b/doc/user/RouterConfiguration.md @@ -73,7 +73,7 @@ A full list of them can be found in the [RouteRequest](RouteRequest.md). | [triasApi](sandbox/TriasApi.md) | `object` | Configuration for the TRIAS API. | *Optional* | | 2.8 | | [updaters](Realtime-Updaters.md) | `object[]` | Configuration for the updaters that import various types of data into OTP. | *Optional* | | 1.5 | | [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 | +| [vehicleRentalServiceDirectory](sandbox/VehicleRentalServiceDirectory.md) | `object` | Configuration for the vehicle rental service directory using GBFS v3 manifest. | *Optional* | | 2.0 | @@ -675,10 +675,7 @@ Used to group requests when monitoring OTP. ] }, "vehicleRentalServiceDirectory" : { - "url" : "https://entur.no/bikeRentalServiceDirectory", - "sourcesName" : "systems", - "updaterUrlName" : "url", - "updaterNetworkName" : "id", + "url" : "https://entur.no/bikeRentalServiceDirectory/manifest.json", "headers" : { "ET-Client-Name" : "MY_ORG_CLIENT_NAME" } diff --git a/doc/user/examples/entur/router-config.json b/doc/user/examples/entur/router-config.json index 94ff4493c30..2fe5dab221d 100644 --- a/doc/user/examples/entur/router-config.json +++ b/doc/user/examples/entur/router-config.json @@ -99,10 +99,7 @@ } }, "vehicleRentalServiceDirectory": { - "url": "https://example.com", - "sourcesName": "systems", - "updaterUrlName": "url", - "updaterNetworkName": "id", + "url": "https://example.com/manifest.json", "headers": { "ET-Client-Name": "{{ Entur specific header }}" } diff --git a/doc/user/sandbox/VehicleRentalServiceDirectory.md b/doc/user/sandbox/VehicleRentalServiceDirectory.md index b901b5d1aaf..b41e1e91423 100644 --- a/doc/user/sandbox/VehicleRentalServiceDirectory.md +++ b/doc/user/sandbox/VehicleRentalServiceDirectory.md @@ -18,7 +18,7 @@ like OTP can connect to the directory and get the necessary configuration from i - Make json tag names configurable [#3447](https://github.com/opentripplanner/OpenTripPlanner/pull/3447) - Enable GBFS geofencing with VehicleRentalServiceDirectory [#5324](https://github.com/opentripplanner/OpenTripPlanner/pull/5324) - Enable `allowKeepingVehicleAtDestination` [#5944](https://github.com/opentripplanner/OpenTripPlanner/pull/5944) - +- Rewrite to use manifest.json from GBFS v3 as the service directory [#6900](https://github.com/opentripplanner/OpenTripPlanner/pull/6900) ## Configuration @@ -32,11 +32,8 @@ the `router-config.json` | Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | |----------------------------------------------------------------------------------------------------------------------|:---------------:|---------------------------------------------------------------------------------|:----------:|---------------|:-----:| -| language | `string` | Language code. | *Optional* | | 2.1 | -| sourcesName | `string` | Json tag name for updater sources. | *Optional* | `"systems"` | 2.1 | -| updaterNetworkName | `string` | Json tag name for the network name for each source. | *Optional* | `"id"` | 2.1 | -| updaterUrlName | `string` | Json tag name for endpoint urls for each source. | *Optional* | `"url"` | 2.1 | -| url | `uri` | Endpoint for the VehicleRentalServiceDirectory | *Required* | | 2.1 | +| language | `string` | Language code for GBFS feeds. | *Optional* | | 2.1 | +| [url](#vehicleRentalServiceDirectory_url) | `uri` | URL or file path to the GBFS v3 manifest.json | *Required* | | 2.1 | | [headers](#vehicleRentalServiceDirectory_headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.1 | | [networks](#vehicleRentalServiceDirectory_networks) | `object[]` | List all networks to include. Use "network": "default-network" to set defaults. | *Optional* | | 2.4 | |       [allowKeepingVehicleAtDestination](#vehicleRentalServiceDirectory_networks_0_allowKeepingVehicleAtDestination) | `boolean` | Enables `allowKeepingVehicleAtDestination` for the given network. | *Optional* | `false` | 2.5 | @@ -51,6 +48,15 @@ the `router-config.json` +

url

+ +**Since version:** `2.1` ∙ **Type:** `uri` ∙ **Cardinality:** `Required` +**Path:** /vehicleRentalServiceDirectory + +URL or file path to the GBFS v3 manifest.json + +Can be either a remote URL (http/https) or a local file path (file://). The manifest must conform to the GBFS v3.0 specification. +

headers

**Since version:** `2.1` ∙ **Type:** `map of string` ∙ **Cardinality:** `Optional` @@ -106,10 +112,7 @@ See the regular [GBFS documentation](../GBFS-Config.md) for more information. // router-config.json { "vehicleRentalServiceDirectory" : { - "url" : "https://example.com", - "sourcesName" : "systems", - "updaterUrlName" : "url", - "updaterNetworkName" : "id", + "url" : "https://example.com/gbfs/v3/manifest.json", "headers" : { "ET-Client-Name" : "otp" },