Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,15 +30,18 @@
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 {

private static final Logger LOG = LoggerFactory.getLogger(
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;
Expand All @@ -53,77 +62,95 @@ public static List<GraphUpdater> 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(
vertexLinker,
repository,
otpHttpClientFactory
);
return serviceDirectory.createUpdatersFromEndpoint(parameters, sources);
return serviceDirectory.createUpdatersFromManifest(parameters, manifest);
}

public List<GraphUpdater> createUpdatersFromEndpoint(
public List<GraphUpdater> createUpdatersFromManifest(
VehicleRentalServiceDirectoryFetcherParameters parameters,
JsonNode sources
GBFSManifest manifest
) {
return fetchUpdaterInfoFromDirectoryAndCreateUpdaters(
buildListOfNetworksFromConfig(parameters, sources)
buildListOfNetworksFromManifest(parameters, manifest)
);
}

private static List<GbfsVehicleRentalDataSourceParameters> buildListOfNetworksFromConfig(
private static List<GbfsVehicleRentalDataSourceParameters> buildListOfNetworksFromManifest(
VehicleRentalServiceDirectoryFetcherParameters parameters,
JsonNode sources
GBFSManifest manifest
) {
List<GbfsVehicleRentalDataSourceParameters> dataSources = new ArrayList<>();

for (JsonNode source : sources) {
Optional<String> network = JsonUtils.asText(source, parameters.getSourceNetworkName());
Optional<String> updaterUrl = JsonUtils.asText(source, parameters.getSourceUrlName());
for (GBFSDataset dataset : manifest.getData().getDatasets()) {
String networkName = dataset.getSystemId();
Optional<String> 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<String> 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<GraphUpdater> fetchUpdaterInfoFromDirectoryAndCreateUpdaters(
List<GbfsVehicleRentalDataSourceParameters> dataSources
) {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,17 +28,11 @@ public class VehicleRentalServiceDirectoryFetcherParameters {

public VehicleRentalServiceDirectoryFetcherParameters(
URI url,
String sourcesName,
String updaterUrlName,
String networkName,
String language,
HttpHeaders headers,
Collection<NetworkParameters> networkParameters
) {
this.url = url;
this.sourcesName = sourcesName;
this.sourceUrlName = updaterUrlName;
this.sourceNetworkName = networkName;
this.language = language;
this.headers = headers;
this.parametersForNetwork = networkParameters
Expand All @@ -50,41 +42,16 @@ public VehicleRentalServiceDirectoryFetcherParameters(
}

/**
* Endpoint for the VehicleRentalServiceDirectory
* URL or file path to the GBFS v3 manifest.json
* <p>
* 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
* <p>
* Optional, default values is "systems".
*/
public String getSourcesName() {
return sourcesName;
}

/**
* Json tag name for endpoint urls for each source
* <p>
* Optional, default values is "url".
*/
public String getSourceUrlName() {
return sourceUrlName;
}

/**
* Json tag name for the network name for each source
* <p>
* Optional, default values is "id".
*/
public String getSourceNetworkName() {
return sourceNetworkName;
}

/**
* Json tag name for http headers
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,24 @@ 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()) {
return null;
}

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)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
2 changes: 1 addition & 1 deletion doc/templates/sandbox/VehicleRentalServiceDirectory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading