diff --git a/.github/workflows/cibuild.yml b/.github/workflows/cibuild.yml index 2853c7ee00b..09150d0d868 100644 --- a/.github/workflows/cibuild.yml +++ b/.github/workflows/cibuild.yml @@ -121,7 +121,7 @@ jobs: - name: Build GTFS GraphQL API documentation run: | - npm install -g @magidoc/cli@4.0.0 + npm install -g @magidoc/cli@4.1.4 magidoc generate - name: Deploy compiled HTML to Github pages diff --git a/client-next/src/hooks/useServerInfo.ts b/client-next/src/hooks/useServerInfo.ts index 54bc67c0db7..23ee23fc283 100644 --- a/client-next/src/hooks/useServerInfo.ts +++ b/client-next/src/hooks/useServerInfo.ts @@ -2,8 +2,7 @@ import { useEffect, useState } from 'react'; import { graphql } from '../gql'; import { request } from 'graphql-request'; // eslint-disable-line import/no-unresolved import { QueryType } from '../gql/graphql.ts'; - -const endpoint = import.meta.env.VITE_API_URL; +import { getApiUrl } from '../util/getApiUrl.ts'; const query = graphql(` query serverInfo { @@ -22,7 +21,7 @@ export const useServerInfo = () => { const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { - setData((await request(endpoint, query)) as QueryType); + setData((await request(getApiUrl(), query)) as QueryType); }; fetchData(); }, []); diff --git a/client-next/src/hooks/useTripQuery.ts b/client-next/src/hooks/useTripQuery.ts index e96a04d7cc3..c9672d0b6d2 100644 --- a/client-next/src/hooks/useTripQuery.ts +++ b/client-next/src/hooks/useTripQuery.ts @@ -2,8 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { graphql } from '../gql'; import { request } from 'graphql-request'; // eslint-disable-line import/no-unresolved import { QueryType, TripQueryVariables } from '../gql/graphql.ts'; - -const endpoint = import.meta.env.VITE_API_URL; +import { getApiUrl } from '../util/getApiUrl.ts'; /** General purpose trip query document for debugging trip searches @@ -96,9 +95,9 @@ export const useTripQuery: TripQueryHook = (variables) => { if (variables) { setLoading(true); if (pageCursor) { - setData((await request(endpoint, query, { ...variables, pageCursor })) as QueryType); + setData((await request(getApiUrl(), query, { ...variables, pageCursor })) as QueryType); } else { - setData((await request(endpoint, query, variables)) as QueryType); + setData((await request(getApiUrl(), query, variables)) as QueryType); } setLoading(false); } else { diff --git a/client-next/src/util/getApiUrl.ts b/client-next/src/util/getApiUrl.ts new file mode 100644 index 00000000000..34d25068342 --- /dev/null +++ b/client-next/src/util/getApiUrl.ts @@ -0,0 +1,12 @@ +const endpoint = import.meta.env.VITE_API_URL; + +export const getApiUrl = () => { + try { + // the URL constructor will throw exception if endpoint is not a valid URL, + // e.g. if it is a relative path + new URL(endpoint); + return endpoint; + } catch (e) { + return `${window.location.origin}${endpoint}`; + } +}; diff --git a/docs/Analysis.md b/docs/Analysis.md index 867e90869c4..f07010c5442 100644 --- a/docs/Analysis.md +++ b/docs/Analysis.md @@ -14,7 +14,6 @@ Much of the analysis code present in the v1.x legacy branch of OTP is essentiall OTP2's new transit router is quite similar to R5 (indeed it was directly influenced by R5) and would not face the same technical problems. Nonetheless, we have decided not to port the OTP1 analysis features over to OTP2 since it would broaden the scope of OTP2 away from passenger information and draw the finite amount of available attention and resources away from existing open source analytics tools. If you would like to apply the routing innovations present in OTP2 in analytics situations, we recommend taking a look at projects like R5 or the R and Python language wrappers for it created by the community. -Some analytics features may still be available as optional "sandbox" features in OTP2 (such as [Travel Time](sandbox/TravelTime.md)), but these do not work in the same way as the features you may have used or read about in OTP1. They are unmaintained and unsupported, and may be removed in the near future. ## Terminology Note diff --git a/docs/Changelog.md b/docs/Changelog.md index c71911a7eb4..a95d931ae51 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -30,6 +30,9 @@ based on merged pull requests. Search GitHub issues and pull requests for smalle - Fix SIRI update travel back in time [#5867](https://github.com/opentripplanner/OpenTripPlanner/pull/5867) - Limit result size and execution time in TransModel GraphQL API [#5883](https://github.com/opentripplanner/OpenTripPlanner/pull/5883) - Fix real-time added patterns persistence with DIFFERENTIAL updates [#5726](https://github.com/opentripplanner/OpenTripPlanner/pull/5726) +- Add plan query that follows the relay connection specification [#5185](https://github.com/opentripplanner/OpenTripPlanner/pull/5185) +- Fix debug client after breaking change in dependency graphql-request [#5899](https://github.com/opentripplanner/OpenTripPlanner/pull/5899) +- Remove TravelTime API [#5890](https://github.com/opentripplanner/OpenTripPlanner/pull/5890) [](AUTOMATIC_CHANGELOG_PLACEHOLDER_DO_NOT_REMOVE) ## 2.5.0 (2024-03-13) diff --git a/docs/Configuration.md b/docs/Configuration.md index ed58f13fa6e..05611e23628 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -248,7 +248,6 @@ Here is a list of all features which can be toggled on/off and their default val | `SandboxAPIGeocoder` | Enable the Geocoder API. | | ✓️ | | `SandboxAPIMapboxVectorTilesApi` | Enable Mapbox vector tiles API. | | ✓️ | | `SandboxAPIParkAndRideApi` | Enable park-and-ride endpoint. | | ✓️ | -| `SandboxAPITravelTime` | Enable the isochrone/travel time surface API. | | ✓️ | | `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | diff --git a/docs/sandbox/TravelTime.md b/docs/sandbox/TravelTime.md deleted file mode 100644 index 817f71506f3..00000000000 --- a/docs/sandbox/TravelTime.md +++ /dev/null @@ -1,61 +0,0 @@ -# Travel Time (Isochrone & Surface) API - -## Contact Info - -- Entur, Norway - -## Changelog - -- 2022-05-09 Initial implementation - -## Documentation - -The API produces a snapshot of travel time form a single place to places around it. The results can -be fetched either as a set of isochrones or a raster map. Please note that as a sandbox feature this -functionality is UNSUPPORTED and neither maintained nor well-understood by most current OTP -developers, and may not be accurate or reliable. Travel time analytics work that began within OTP -has moved years ago to other projects, where it actively continues. See the -[Analysis](../Analysis.md) page for further details. - -### Configuration - -The feature must be enabled in otp-config.json as follows: - -```JSON -// otp-config.json -{ - "otpFeatures" : { - "SandboxAPITravelTime" : true - } -} -``` - -### API parameters - -- `location` Origin of the search, can be either `latitude,longitude` or a stop id -- `time` Departure time as a ISO-8601 time and date (example `2023-04-24T15:40:12+02:00`). The default value is the current time. -- `cutoff` The maximum travel duration as a ISO-8601 duration. The `PT` can be dropped to simplify the value. - This parameter can be given multiple times to include multiple isochrones in a single request. - The default value is one hour. -- `modes` A list of travel modes. WALK is not implemented, use `WALK, TRANSIT` instead. -- `arriveBy` Set to `false` when searching from the location and `true` when searching to the - location - -### Isochrone API - -`/otp/traveltime/isochrone` - -Results is the travel time boundaries at the `cutoff` travel time. - -### Travel time surface API - -`/otp/traveltime/surface` - -The travel time as a GeoTIFF raster file. The file has a single 32-bit int band, which contains the -travel time in seconds. - -### Example Request - -``` -http://localhost:8080/otp/traveltime/isochrone?batch=true&location=52.499959,13.388803&time=2023-04-12T10:19:03%2B02:00&modes=WALK,TRANSIT&arriveBy=false&cutoff=30M17S -``` diff --git a/mkdocs.yml b/mkdocs.yml index da976615d1b..b8c257ceb7d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -110,7 +110,6 @@ nav: - Data Overlay: 'sandbox/DataOverlay.md' - Vehicle Parking Updaters: 'sandbox/VehicleParking.md' - Geocoder API: 'sandbox/GeocoderAPI.md' - - Travel Time Isochrones: 'sandbox/TravelTime.md' - IBI Accessibility Score: 'sandbox/IBIAccessibilityScore.md' - Fares: 'sandbox/Fares.md' - Ride Hailing: 'sandbox/RideHailing.md' diff --git a/pom.xml b/pom.xml index 13988c93969..6f202b592f8 100644 --- a/pom.xml +++ b/pom.xml @@ -545,7 +545,7 @@ com.google.cloud libraries-bom - 26.34.0 + 26.40.0 pom import @@ -852,12 +852,12 @@ com.graphql-java graphql-java - 21.5 + 22.0 com.graphql-java graphql-java-extended-scalars - 21.0 + 22.0 org.apache.httpcomponents.client5 diff --git a/src/client/debug-client-preview/index.html b/src/client/debug-client-preview/index.html index a26303b904d..4bcf452cb3e 100644 --- a/src/client/debug-client-preview/index.html +++ b/src/client/debug-client-preview/index.html @@ -5,8 +5,8 @@ OTP Debug Client - - + +
diff --git a/src/ext/java/org/opentripplanner/ext/actuator/MicrometerGraphQLInstrumentation.java b/src/ext/java/org/opentripplanner/ext/actuator/MicrometerGraphQLInstrumentation.java index cfd1f0185b8..ab76adcc444 100644 --- a/src/ext/java/org/opentripplanner/ext/actuator/MicrometerGraphQLInstrumentation.java +++ b/src/ext/java/org/opentripplanner/ext/actuator/MicrometerGraphQLInstrumentation.java @@ -13,7 +13,6 @@ import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import graphql.execution.instrumentation.parameters.InstrumentationFieldParameters; import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters; import graphql.language.Document; import graphql.schema.GraphQLTypeUtil; @@ -22,7 +21,6 @@ import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Timer; import java.util.List; -import java.util.concurrent.CompletableFuture; /** * Using this instrumentation we can precisely measure how queries and data fetchers are executed @@ -107,21 +105,13 @@ public ExecutionStrategyInstrumentationContext beginExecutionStrategy( ) { return new ExecutionStrategyInstrumentationContext() { @Override - public void onDispatched(CompletableFuture result) {} + public void onDispatched() {} @Override public void onCompleted(ExecutionResult result, Throwable t) {} }; } - @Override - public InstrumentationContext beginField( - InstrumentationFieldParameters parameters, - InstrumentationState state - ) { - return noOp(); - } - @Override public InstrumentationContext beginFieldFetch( InstrumentationFieldFetchParameters parameters, diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 9b7f67526e6..db9c1fcc441 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -15,6 +15,7 @@ import javax.annotation.Nullable; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.model.TimetableSnapshotProvider; import org.opentripplanner.transit.model.framework.DataValidationException; import org.opentripplanner.transit.model.framework.Result; import org.opentripplanner.transit.model.network.TripPattern; @@ -29,7 +30,7 @@ import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateResult; import org.opentripplanner.updater.spi.UpdateSuccess; -import org.opentripplanner.updater.trip.AbstractTimetableSnapshotSource; +import org.opentripplanner.updater.trip.TimetableSnapshotManager; import org.opentripplanner.updater.trip.UpdateIncrementality; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +42,7 @@ * necessary to provide planning threads a consistent constant view of a graph with real-time data at * a specific point in time. */ -public class SiriTimetableSnapshotSource extends AbstractTimetableSnapshotSource { +public class SiriTimetableSnapshotSource implements TimetableSnapshotProvider { private static final Logger LOG = LoggerFactory.getLogger(SiriTimetableSnapshotSource.class); @@ -59,15 +60,18 @@ public class SiriTimetableSnapshotSource extends AbstractTimetableSnapshotSource private final TransitService transitService; + private final TimetableSnapshotManager snapshotManager; + public SiriTimetableSnapshotSource( TimetableSnapshotSourceParameters parameters, TransitModel transitModel ) { - super( - transitModel.getTransitLayerUpdater(), - parameters, - () -> LocalDate.now(transitModel.getTimeZone()) - ); + this.snapshotManager = + new TimetableSnapshotManager( + transitModel.getTransitLayerUpdater(), + parameters, + () -> LocalDate.now(transitModel.getTimeZone()) + ); this.transitModel = transitModel; this.transitService = new DefaultTransitService(transitModel); this.tripPatternCache = @@ -100,10 +104,10 @@ public UpdateResult applyEstimatedTimetable( List> results = new ArrayList<>(); - withLock(() -> { + snapshotManager.withLock(() -> { if (incrementality == FULL_DATASET) { // Remove all updates from the buffer - buffer.clear(feedId); + snapshotManager.clearBuffer(feedId); } for (var etDelivery : updates) { @@ -118,12 +122,17 @@ public UpdateResult applyEstimatedTimetable( LOG.debug("message contains {} trip updates", updates.size()); - purgeAndCommit(); + snapshotManager.purgeAndCommit(); }); return UpdateResult.ofResults(results); } + @Override + public TimetableSnapshot getTimetableSnapshot() { + return snapshotManager.getTimetableSnapshot(); + } + private Result apply( EstimatedVehicleJourney journey, TransitModel transitModel, @@ -228,7 +237,7 @@ private Result handleModifiedTrip( estimatedVehicleJourney, entityResolver, this::getCurrentTimetable, - buffer::getRealtimeAddedTripPattern + snapshotManager::getRealtimeAddedTripPattern ); if (tripAndPattern == null) { @@ -271,7 +280,7 @@ private Result handleModifiedTrip( // Also check whether trip id has been used for previously ADDED/MODIFIED trip message and // remove the previously created trip - this.buffer.revertTripToScheduledTripPattern(trip.getId(), serviceDate); + this.snapshotManager.revertTripToScheduledTripPattern(trip.getId(), serviceDate); return updateResult; } @@ -290,7 +299,7 @@ private Result addTripToGraphAndBuffer(TripUpdate tr serviceDate ); // Add new trip times to buffer, making protective copies as needed. Bubble success/error up. - var result = buffer.update(pattern, tripUpdate.tripTimes(), serviceDate); + var result = snapshotManager.updateBuffer(pattern, tripUpdate.tripTimes(), serviceDate); LOG.debug("Applied real-time data for trip {} on {}", trip, serviceDate); return result; } @@ -315,7 +324,7 @@ private boolean markScheduledTripAsDeleted(Trip trip, final LocalDate serviceDat } else { final RealTimeTripTimes newTripTimes = tripTimes.copyScheduledTimes(); newTripTimes.deleteTrip(); - buffer.update(pattern, newTripTimes, serviceDate); + snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); success = true; } } diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java index 8b5896a1bf6..40c008d0004 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTripPatternCache.java @@ -1,22 +1,13 @@ package org.opentripplanner.ext.siri; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimaps; import java.time.LocalDate; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.function.Function; import javax.annotation.Nonnull; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Threadsafe mechanism for tracking any TripPatterns added to the graph via SIRI realtime messages. @@ -39,23 +30,11 @@ */ public class SiriTripPatternCache { - private static final Logger LOG = LoggerFactory.getLogger(SiriTripPatternCache.class); - // TODO RT_AB: Improve documentation. This seems to be the primary collection of added // TripPatterns, with other collections serving as indexes. Similar to TripPatternCache.cache // in the GTFS version of this class, but with service date as part of the key. private final Map cache = new HashMap<>(); - // TODO RT_AB: Improve documentation. This field appears to be an index that exists only in the - // SIRI version of this class (i.e. this version and not the older TripPatternCache that - // handles GTFS-RT). This index appears to be tailored for use by the Transmodel GraphQL APIs. - private final ListMultimap patternsForStop = Multimaps.synchronizedListMultimap( - ArrayListMultimap.create() - ); - - // TODO RT_AB: clarify name and add documentation to this field. - private final Map updatedTripPatternsForTripCache = new HashMap<>(); - // TODO RT_AB: generalize this so we can generate IDs for SIRI or GTFS-RT sources. private final SiriTripPatternIdGenerator tripPatternIdGenerator; @@ -128,85 +107,8 @@ public synchronized TripPattern getOrCreateTripPattern( cache.put(key, tripPattern); } - /* - When the StopPattern is first modified (e.g. change of platform), then updated (or vice - versa), the stopPattern is altered, and the StopPattern-object for the different states will - not be equal. - - This causes both tripPatterns to be added to all unchanged stops along the route, which again - causes duplicate results in departureRow-searches (one departure for "updated", one for - "modified"). - - Full example: - Planned stops: Stop 1 - Platform 1, Stop 2 - Platform 1 - - StopPattern #rt1: "updated" stopPattern cached in 'patternsForStop': - - Stop 1, Platform 1 - - StopPattern #rt1 - - Stop 2, Platform 1 - - StopPattern #rt1 - - "modified" stopPattern: Stop 1 - Platform 1, Stop 2 - Platform 2 - - StopPattern #rt2: "modified" stopPattern cached in 'patternsForStop' will then be: - - Stop 1, Platform 1 - - StopPattern #rt1, StopPattern #rt2 - - Stop 2, Platform 1 - - StopPattern #rt1 - - Stop 2, Platform 2 - - StopPattern #rt2 - - Therefore, we must clean up the duplicates by deleting the previously added (and thus - outdated) tripPattern for all affected stops. In example above, "StopPattern #rt1" should be - removed from all stops. - - TODO RT_AB: review why this particular case is handled in an ad-hoc manner. It seems like all - such indexes should be constantly rebuilt and versioned along with the TimetableSnapshot. - */ - TripServiceDateKey tripServiceDateKey = new TripServiceDateKey(trip, serviceDate); - if (updatedTripPatternsForTripCache.containsKey(tripServiceDateKey)) { - // Remove previously added TripPatterns for the trip currently being updated - if the stopPattern does not match - TripPattern cachedTripPattern = updatedTripPatternsForTripCache.get(tripServiceDateKey); - if (cachedTripPattern != null && !tripPattern.stopPatternIsEqual(cachedTripPattern)) { - int sizeBefore = patternsForStop.values().size(); - long t1 = System.currentTimeMillis(); - patternsForStop.values().removeAll(Arrays.asList(cachedTripPattern)); - int sizeAfter = patternsForStop.values().size(); - - LOG.debug( - "Removed outdated TripPattern for {} stops in {} ms - tripId: {}", - (sizeBefore - sizeAfter), - (System.currentTimeMillis() - t1), - trip.getId() - ); - // TODO: Also remove previously updated - now outdated - TripPattern from cache ? - // cache.remove(new StopPatternServiceDateKey(cachedTripPattern.stopPattern, serviceDate)); - } - } - - // To make these trip patterns visible for departureRow searches. - for (var stop : tripPattern.getStops()) { - if (!patternsForStop.containsEntry(stop, tripPattern)) { - patternsForStop.put(stop, tripPattern); - } - } - - // Cache the last added tripPattern that has been used to update a specific trip - updatedTripPatternsForTripCache.put(tripServiceDateKey, tripPattern); - return tripPattern; } - - /** - * Returns any new TripPatterns added by real time information for a given stop. - * TODO RT_AB: this appears to be currently unused. Perhaps remove it if the API has changed. - * - * @param stop the stop - * @return list of TripPatterns created by real time sources for the stop. - */ - public List getAddedTripPatternsForStop(RegularStop stop) { - return patternsForStop.get(stop); - } } // TODO RT_AB: move the below classes inside the above class as private static inner classes. @@ -243,33 +145,3 @@ public boolean equals(Object thatObject) { return (this.stopPattern.equals(that.stopPattern) && this.serviceDate.equals(that.serviceDate)); } } - -/** - * An alternative key for looking up realtime-added TripPatterns by trip and service date instead - * of stop pattern and service date. Must define hashcode and equals to confer semantic identity. - * TODO RT_AB: verify whether one map is considered the definitive collection and the other an index. - */ -class TripServiceDateKey { - - Trip trip; - LocalDate serviceDate; - - public TripServiceDateKey(Trip trip, LocalDate serviceDate) { - this.trip = trip; - this.serviceDate = serviceDate; - } - - @Override - public int hashCode() { - return trip.hashCode() + serviceDate.hashCode(); - } - - @Override - public boolean equals(Object thatObject) { - if (!(thatObject instanceof TripServiceDateKey)) { - return false; - } - TripServiceDateKey that = (TripServiceDateKey) thatObject; - return (this.trip.equals(that.trip) && this.serviceDate.equals(that.serviceDate)); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHelper.java b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHelper.java index 618c5e0c31c..0f1bd449075 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHelper.java +++ b/src/ext/java/org/opentripplanner/ext/siri/updater/SiriHelper.java @@ -7,20 +7,15 @@ import java.util.UUID; import javax.xml.stream.XMLStreamException; import org.rutebanken.siri20.util.SiriXml; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import uk.org.siri.siri20.EstimatedTimetableRequestStructure; import uk.org.siri.siri20.MessageQualifierStructure; import uk.org.siri.siri20.RequestorRef; import uk.org.siri.siri20.ServiceRequest; import uk.org.siri.siri20.Siri; import uk.org.siri.siri20.SituationExchangeRequestStructure; -import uk.org.siri.siri20.VehicleMonitoringRequestStructure; public class SiriHelper { - private static final Logger LOG = LoggerFactory.getLogger(SiriHelper.class); - public static Siri unmarshal(InputStream is) throws JAXBException, XMLStreamException { return SiriXml.parseXml(is); } @@ -30,16 +25,6 @@ public static String createSXServiceRequestAsXml(String requestorRef) throws JAX return SiriXml.toXml(request); } - public static String createVMServiceRequestAsXml(String requestorRef) throws JAXBException { - Siri request = createVMServiceRequest(requestorRef); - return SiriXml.toXml(request); - } - - public static String createETServiceRequestAsXml(String requestorRef) throws JAXBException { - Siri request = createETServiceRequest(requestorRef, null); - return SiriXml.toXml(request); - } - public static String createETServiceRequestAsXml(String requestorRef, Duration previewInterval) throws JAXBException { Siri request = createETServiceRequest(requestorRef, previewInterval); @@ -106,29 +91,4 @@ private static Siri createETServiceRequest(String requestorRefValue, Duration pr return request; } - - private static Siri createVMServiceRequest(String requestorRefValue) { - Siri request = createSiriObject(); - - ServiceRequest serviceRequest = new ServiceRequest(); - serviceRequest.setRequestTimestamp(ZonedDateTime.now()); - - RequestorRef requestorRef = new RequestorRef(); - requestorRef.setValue(requestorRefValue); - serviceRequest.setRequestorRef(requestorRef); - - VehicleMonitoringRequestStructure vmRequest = new VehicleMonitoringRequestStructure(); - vmRequest.setRequestTimestamp(ZonedDateTime.now()); - vmRequest.setVersion("2.0"); - - MessageQualifierStructure messageIdentifier = new MessageQualifierStructure(); - messageIdentifier.setValue(UUID.randomUUID().toString()); - - vmRequest.setMessageIdentifier(messageIdentifier); - serviceRequest.getVehicleMonitoringRequests().add(vmRequest); - - request.setServiceRequest(serviceRequest); - - return request; - } } diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/IsochroneData.java b/src/ext/java/org/opentripplanner/ext/traveltime/IsochroneData.java deleted file mode 100644 index 5e367be2eef..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/IsochroneData.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import org.locationtech.jts.geom.Geometry; - -/** - * A conveyor for an isochrone. - * - * @author laurent - */ -public record IsochroneData(long cutoffSec, Geometry geometry, Geometry debugGeometry) {} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/IsochroneRenderer.java b/src/ext/java/org/opentripplanner/ext/traveltime/IsochroneRenderer.java deleted file mode 100644 index 2c4f2ed1ac1..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/IsochroneRenderer.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import org.geojson.MultiPolygon; -import org.geotools.api.feature.simple.SimpleFeatureType; -import org.geotools.data.simple.SimpleFeatureCollection; -import org.geotools.feature.DefaultFeatureCollection; -import org.geotools.feature.simple.SimpleFeatureBuilder; -import org.geotools.feature.simple.SimpleFeatureTypeBuilder; -import org.geotools.referencing.crs.DefaultGeographicCRS; -import org.locationtech.jts.geom.Geometry; -import org.opentripplanner.ext.traveltime.geometry.DelaunayIsolineBuilder; -import org.opentripplanner.ext.traveltime.geometry.ZMetric; -import org.opentripplanner.ext.traveltime.geometry.ZSampleGrid; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class IsochroneRenderer { - - private static final Logger LOG = LoggerFactory.getLogger(IsochroneRenderer.class); - private static final SimpleFeatureType contourSchema = makeContourSchema(); - - static List renderIsochrones( - ZSampleGrid sampleGrid, - TravelTimeRequest traveltimeRequest - ) { - long t0 = System.currentTimeMillis(); - ZMetric zMetric = new IsolineMetric(); - DelaunayIsolineBuilder isolineBuilder = new DelaunayIsolineBuilder<>( - sampleGrid.delaunayTriangulate(), - zMetric - ); - isolineBuilder.setDebug(traveltimeRequest.includeDebugGeometry); - - List isochrones = new ArrayList<>(); - for (Duration cutoff : traveltimeRequest.cutoffs) { - long cutoffSec = cutoff.toSeconds(); - WTWD z0 = new WTWD(); - z0.w = 1.0; - z0.wTime = cutoffSec; - z0.d = traveltimeRequest.offRoadDistanceMeters; - Geometry geometry = isolineBuilder.computeIsoline(z0); - Geometry debugGeometry = null; - if (traveltimeRequest.includeDebugGeometry) { - debugGeometry = isolineBuilder.getDebugGeometry(); - } - - isochrones.add(new IsochroneData(cutoffSec, geometry, debugGeometry)); - } - - long t1 = System.currentTimeMillis(); - LOG.info("Computed {} isochrones in {}msec", isochrones.size(), (int) (t1 - t0)); - - return isochrones; - } - - /** - * Create a geotools feature collection from a list of isochrones in the OTPA internal format. - * Once in a FeatureCollection, they can for example be exported as GeoJSON. - */ - static SimpleFeatureCollection makeContourFeatures(List isochrones) { - DefaultFeatureCollection featureCollection = new DefaultFeatureCollection(null, contourSchema); - SimpleFeatureBuilder fbuilder = new SimpleFeatureBuilder(contourSchema); - for (IsochroneData isochrone : isochrones) { - fbuilder.add(isochrone.geometry()); - fbuilder.add(isochrone.cutoffSec()); - featureCollection.add(fbuilder.buildFeature(null)); - } - return featureCollection; - } - - private static SimpleFeatureType makeContourSchema() { - /* Create the output feature schema. */ - SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder(); - typeBuilder.setName("contours"); - typeBuilder.setCRS(DefaultGeographicCRS.WGS84); - typeBuilder.setDefaultGeometry("the_geom"); - // Do not use "geom" or "geometry" below, it seems to broke shapefile generation - typeBuilder.add("the_geom", MultiPolygon.class); - typeBuilder.add("time", Long.class); - return typeBuilder.buildFeatureType(); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/IsolineMetric.java b/src/ext/java/org/opentripplanner/ext/traveltime/IsolineMetric.java deleted file mode 100644 index 8afa3b938ba..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/IsolineMetric.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import org.opentripplanner.ext.traveltime.geometry.ZMetric; - -public class IsolineMetric implements ZMetric { - - @Override - public int cut(WTWD zA, WTWD zB, WTWD z0) { - double t0 = z0.wTime / z0.w; - double tA = zA.d > z0.d ? Double.POSITIVE_INFINITY : zA.wTime / zA.w; - double tB = zB.d > z0.d ? Double.POSITIVE_INFINITY : zB.wTime / zB.w; - if (tA < t0 && t0 <= tB) return 1; - if (tB < t0 && t0 <= tA) return -1; - return 0; - } - - @Override - public double interpolate(WTWD zA, WTWD zB, WTWD z0) { - if (zA.d > z0.d || zB.d > z0.d) { - if (zA.d > z0.d && zB.d > z0.d) { - throw new AssertionError("dA > d0 && dB > d0"); - } - // Interpolate on d - return zA.d == zB.d ? 0.5 : (z0.d - zA.d) / (zB.d - zA.d); - } else { - // Interpolate on t - double tA = zA.wTime / zA.w; - double tB = zB.wTime / zB.w; - double t0 = z0.wTime / z0.w; - return tA == tB ? 0.5 : (t0 - tA) / (tB - tA); - } - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/PostTransitSkipEdgeStrategy.java b/src/ext/java/org/opentripplanner/ext/traveltime/PostTransitSkipEdgeStrategy.java deleted file mode 100644 index c0175819da5..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/PostTransitSkipEdgeStrategy.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import java.time.Duration; -import java.time.Instant; -import org.opentripplanner.astar.spi.SkipEdgeStrategy; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.search.state.State; - -public class PostTransitSkipEdgeStrategy implements SkipEdgeStrategy { - - private final long maxDurationSeconds; - private final long departureTime; - private final boolean arriveBy; - - public PostTransitSkipEdgeStrategy( - Duration maxEgressTime, - Instant departureTime, - boolean arriveBy - ) { - this.maxDurationSeconds = maxEgressTime.toSeconds(); - this.departureTime = departureTime.getEpochSecond(); - this.arriveBy = arriveBy; - } - - @Override - public boolean shouldSkipEdge(State current, Edge edge) { - long postTransitDepartureTime; - if (current.stateData instanceof TravelTimeStateData travelTimeStateData) { - postTransitDepartureTime = travelTimeStateData.postTransitDepartureTime; - } else { - postTransitDepartureTime = departureTime; - } - long duration; - - if (arriveBy) { - duration = postTransitDepartureTime - current.getTimeSeconds(); - } else { - duration = current.getTimeSeconds() - postTransitDepartureTime; - } - - return duration > maxDurationSeconds; - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/RasterRenderer.java b/src/ext/java/org/opentripplanner/ext/traveltime/RasterRenderer.java deleted file mode 100644 index 6343c6a1f68..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/RasterRenderer.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import static javax.imageio.ImageWriteParam.MODE_EXPLICIT; - -import jakarta.ws.rs.core.StreamingOutput; -import java.awt.image.DataBuffer; -import javax.media.jai.RasterFactory; -import org.geotools.api.parameter.GeneralParameterValue; -import org.geotools.api.parameter.ParameterValueGroup; -import org.geotools.coverage.grid.GridCoverage2D; -import org.geotools.coverage.grid.GridCoverageFactory; -import org.geotools.coverage.grid.GridEnvelope2D; -import org.geotools.coverage.grid.GridGeometry2D; -import org.geotools.coverage.grid.io.AbstractGridFormat; -import org.geotools.gce.geotiff.GeoTiffFormat; -import org.geotools.gce.geotiff.GeoTiffWriteParams; -import org.geotools.gce.geotiff.GeoTiffWriter; -import org.geotools.geometry.jts.ReferencedEnvelope; -import org.geotools.referencing.crs.DefaultGeographicCRS; -import org.geotools.referencing.operation.transform.AffineTransform2D; -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.ext.traveltime.geometry.ZSampleGrid; - -public class RasterRenderer { - - static StreamingOutput createGeoTiffRaster(ZSampleGrid sampleGrid) { - int minX = sampleGrid.getXMin(); - int minY = sampleGrid.getYMin(); - int maxY = sampleGrid.getYMax(); - - int width = sampleGrid.getXMax() - minX + 1; - int height = maxY - minY + 1; - - Coordinate center = sampleGrid.getCenter(); - - double resX = sampleGrid.getCellSize().x; - double resY = sampleGrid.getCellSize().y; - - var raster = RasterFactory.createBandedRaster(DataBuffer.TYPE_INT, width, height, 1, null); - var dataBuffer = raster.getDataBuffer(); - - // Initialize with NO DATA value - for (int i = 0; i < dataBuffer.getSize(); i++) { - dataBuffer.setElem(i, Integer.MIN_VALUE); - } - - for (var s : sampleGrid) { - final WTWD z = s.getZ(); - raster.setSample(s.getX() - minX, maxY - s.getY(), 0, z.wTime / z.w); - } - - ReferencedEnvelope geom = new GridGeometry2D( - new GridEnvelope2D(0, 0, width, height), - new AffineTransform2D(resX, 0, 0, resY, center.x + resX * minX, center.y + resY * minY), - DefaultGeographicCRS.WGS84 - ) - .getEnvelope2D(); - - GridCoverage2D gridCoverage = new GridCoverageFactory().create("traveltime", raster, geom); - - GeoTiffWriteParams wp = new GeoTiffWriteParams(); - wp.setCompressionMode(MODE_EXPLICIT); - wp.setCompressionType("LZW"); - ParameterValueGroup params = new GeoTiffFormat().getWriteParameters(); - params.parameter(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName().toString()).setValue(wp); - return outputStream -> { - GeoTiffWriter writer = new GeoTiffWriter(outputStream); - writer.write(gridCoverage, params.values().toArray(new GeneralParameterValue[1])); - writer.dispose(); - outputStream.close(); - }; - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/SampleGridRenderer.java b/src/ext/java/org/opentripplanner/ext/traveltime/SampleGridRenderer.java deleted file mode 100644 index c72732f3515..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/SampleGridRenderer.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.astar.model.ShortestPathTree; -import org.opentripplanner.ext.traveltime.geometry.AccumulativeGridSampler; -import org.opentripplanner.ext.traveltime.geometry.AccumulativeMetric; -import org.opentripplanner.ext.traveltime.geometry.SparseMatrixZSampleGrid; -import org.opentripplanner.ext.traveltime.geometry.ZSampleGrid; -import org.opentripplanner.ext.traveltime.spt.SPTVisitor; -import org.opentripplanner.ext.traveltime.spt.SPTWalker; -import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.Vertex; -import org.opentripplanner.street.search.state.State; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class SampleGridRenderer { - - private static final Logger LOG = LoggerFactory.getLogger(SampleGridRenderer.class); - - public static ZSampleGrid getSampleGrid( - ShortestPathTree spt, - TravelTimeRequest traveltimeRequest - ) { - final double offRoadDistanceMeters = traveltimeRequest.offRoadDistanceMeters; - final double offRoadWalkSpeedMps = 1.00; // m/s, off-road walk speed - - // Create a sample grid based on the SPT. - long t1 = System.currentTimeMillis(); - Coordinate coordinateOrigin = spt.getAllStates().iterator().next().getVertex().getCoordinate(); - final double gridSizeMeters = traveltimeRequest.precisionMeters; - final double cosLat = Math.cos(Math.toRadians(coordinateOrigin.y)); - double dY = Math.toDegrees(gridSizeMeters / SphericalDistanceLibrary.RADIUS_OF_EARTH_IN_M); - double dX = dY / cosLat; - - SparseMatrixZSampleGrid sampleGrid = new SparseMatrixZSampleGrid<>( - 16, - spt.getVertexCount(), - dX, - dY, - coordinateOrigin - ); - sampleSPT( - spt, - sampleGrid, - gridSizeMeters, - offRoadDistanceMeters, - offRoadWalkSpeedMps, - (int) traveltimeRequest.maxCutoff.getSeconds(), - cosLat - ); - - long t2 = System.currentTimeMillis(); - LOG.info("Computed sampling in {}msec", (int) (t2 - t1)); - - return sampleGrid; - } - - /** - * Sample a SPT using a SPTWalker and an AccumulativeGridSampler. - */ - public static void sampleSPT( - final ShortestPathTree spt, - final ZSampleGrid sampleGrid, - final double gridSizeMeters, - final double offRoadDistanceMeters, - final double offRoadWalkSpeedMps, - final int maxTimeSec, - final double cosLat - ) { - final AccumulativeMetric accMetric = new WTWDAccumulativeMetric( - cosLat, - offRoadDistanceMeters, - offRoadWalkSpeedMps, - gridSizeMeters - ); - final AccumulativeGridSampler gridSampler = new AccumulativeGridSampler<>( - sampleGrid, - accMetric - ); - - // At which distance we split edges along the geometry during sampling. - // For best results, this should be slighly lower than the grid size. - double walkerSplitDistanceMeters = gridSizeMeters * 0.5; - - SPTVisitor visitor = new SampleGridSPTVisitor(maxTimeSec, gridSampler, offRoadWalkSpeedMps); - new SPTWalker(spt).walk(visitor, walkerSplitDistanceMeters); - gridSampler.close(); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/SampleGridSPTVisitor.java b/src/ext/java/org/opentripplanner/ext/traveltime/SampleGridSPTVisitor.java deleted file mode 100644 index 3784bef2e48..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/SampleGridSPTVisitor.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.ext.traveltime.geometry.AccumulativeGridSampler; -import org.opentripplanner.ext.traveltime.spt.SPTVisitor; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.edge.StreetEdge; -import org.opentripplanner.street.search.state.State; - -class SampleGridSPTVisitor implements SPTVisitor { - - private final int maxTimeSec; - private final AccumulativeGridSampler gridSampler; - private final double offRoadWalkSpeedMps; - - public SampleGridSPTVisitor( - int maxTimeSec, - AccumulativeGridSampler gridSampler, - double offRoadWalkSpeedMps - ) { - this.maxTimeSec = maxTimeSec; - this.gridSampler = gridSampler; - this.offRoadWalkSpeedMps = offRoadWalkSpeedMps; - } - - @Override - public boolean accept(Edge e) { - return e instanceof StreetEdge; - } - - @Override - public void visit( - Edge e, - Coordinate c, - State s0, - State s1, - double d0, - double d1, - double speedAlongEdge - ) { - double t0 = s0.getElapsedTimeSeconds() + d0 / speedAlongEdge; - double t1 = s1.getElapsedTimeSeconds() + d1 / speedAlongEdge; - if (t0 < maxTimeSec || t1 < maxTimeSec) { - if (!Double.isInfinite(t0) || !Double.isInfinite(t1)) { - WTWD z = new WTWD(); - z.w = 1.0; - z.d = 0.0; - if (t0 < t1) { - z.wTime = t0; - z.wWalkDist = s0.getWalkDistance() + d0; - } else { - z.wTime = t1; - z.wWalkDist = s1.getWalkDistance() + d1; - } - gridSampler.addSamplingPoint(c, z, offRoadWalkSpeedMps); - } - } - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeRequest.java b/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeRequest.java deleted file mode 100644 index 2b7f8cedb9e..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeRequest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import java.time.Duration; -import java.util.Arrays; -import java.util.List; - -/** - * A request for an travel time map. - * - * @author laurent - */ -public class TravelTimeRequest { - - public final List cutoffs; - - public final boolean includeDebugGeometry = false; - - public final int precisionMeters = 200; - - public final int offRoadDistanceMeters = 150; - - public final Duration maxCutoff; - - public final Duration maxAccessDuration; - public final Duration maxEgressDuration; - - public TravelTimeRequest( - List cutoffList, - Duration defaultAccessDuration, - Duration defaultEgressDuration - ) { - this.cutoffs = cutoffList; - this.maxCutoff = cutoffs.stream().max(Duration::compareTo).orElseThrow(); - if (maxCutoff.compareTo(defaultAccessDuration) < 0) { - maxAccessDuration = maxCutoff; - } else { - maxAccessDuration = defaultAccessDuration; - } - - if (maxCutoff.compareTo(defaultEgressDuration) < 0) { - maxEgressDuration = maxCutoff; - } else { - maxEgressDuration = defaultEgressDuration; - } - } - - @Override - public int hashCode() { - return cutoffs.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (other instanceof TravelTimeRequest otherReq) { - return this.cutoffs.equals(otherReq.cutoffs); - } - return false; - } - - public String toString() { - return String.format( - "", - Arrays.toString(cutoffs.toArray()), - precisionMeters - ); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeResource.java b/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeResource.java deleted file mode 100644 index 88742aebeb1..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeResource.java +++ /dev/null @@ -1,305 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.StreamingOutput; -import java.time.Instant; -import java.time.LocalDate; -import java.time.Period; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import org.geotools.data.geojson.GeoJSONWriter; -import org.opentripplanner.api.common.LocationStringParser; -import org.opentripplanner.api.parameter.QualifiedModeSet; -import org.opentripplanner.astar.model.ShortestPathTree; -import org.opentripplanner.ext.traveltime.geometry.ZSampleGrid; -import org.opentripplanner.framework.time.DurationUtils; -import org.opentripplanner.framework.time.ServiceDateUtils; -import org.opentripplanner.raptor.RaptorService; -import org.opentripplanner.raptor.api.model.RaptorAccessEgress; -import org.opentripplanner.raptor.api.model.SearchDirection; -import org.opentripplanner.raptor.api.request.RaptorProfile; -import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; -import org.opentripplanner.raptor.api.response.RaptorResponse; -import org.opentripplanner.raptor.api.response.StopArrivals; -import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressRouter; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.AccessEgressMapper; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RouteRequestTransitDataProviderFilter; -import org.opentripplanner.routing.api.request.RouteRequest; -import org.opentripplanner.routing.api.request.StreetMode; -import org.opentripplanner.routing.api.request.request.StreetRequest; -import org.opentripplanner.routing.api.request.request.filter.SelectRequest; -import org.opentripplanner.routing.api.request.request.filter.TransitFilterRequest; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.api.OtpServerRequestContext; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.Vertex; -import org.opentripplanner.street.search.StreetSearchBuilder; -import org.opentripplanner.street.search.TemporaryVerticesContainer; -import org.opentripplanner.street.search.request.StreetSearchRequest; -import org.opentripplanner.street.search.request.StreetSearchRequestMapper; -import org.opentripplanner.street.search.state.State; -import org.opentripplanner.street.search.state.StateData; -import org.opentripplanner.street.search.strategy.DominanceFunctions; -import org.opentripplanner.transit.model.basic.MainAndSubMode; -import org.opentripplanner.transit.model.site.RegularStop; -import org.opentripplanner.transit.service.TransitService; - -@Path("/traveltime") -public class TravelTimeResource { - - private final RouteRequest routingRequest; - private final RaptorRoutingRequestTransitData requestTransitDataProvider; - private final Instant startTime; - private final Instant endTime; - private final ZonedDateTime startOfTime; - private final TravelTimeRequest traveltimeRequest; - private final RaptorService raptorService; - private final Graph graph; - private final TransitService transitService; - - public TravelTimeResource( - @Context OtpServerRequestContext serverContext, - @QueryParam("location") String location, - @QueryParam("time") String time, - @QueryParam("cutoff") @DefaultValue("60m") List cutoffs, - @QueryParam("modes") String modes, - @QueryParam("arriveBy") @DefaultValue("false") boolean arriveBy - ) { - this.graph = serverContext.graph(); - this.transitService = serverContext.transitService(); - routingRequest = serverContext.defaultRouteRequest(); - routingRequest.setArriveBy(arriveBy); - - if (modes != null) { - var modeSet = new QualifiedModeSet(modes); - routingRequest.journey().setModes(modeSet.getRequestModes()); - var transitModes = modeSet.getTransitModes().stream().map(MainAndSubMode::new).toList(); - var select = SelectRequest.of().withTransportModes(transitModes).build(); - var request = TransitFilterRequest.of().addSelect(select).build(); - routingRequest.journey().transit().setFilters(List.of(request)); - } - - var durationForMode = routingRequest.preferences().street().accessEgress().maxDuration(); - traveltimeRequest = - new TravelTimeRequest( - cutoffs.stream().map(DurationUtils::duration).toList(), - durationForMode.valueOf(getAccessRequest(routingRequest).mode()), - durationForMode.valueOf(getEgressRequest(routingRequest).mode()) - ); - - var parsedLocation = LocationStringParser.fromOldStyleString(location); - var requestTime = time != null ? Instant.parse(time) : Instant.now(); - routingRequest.setDateTime(requestTime); - - if (routingRequest.arriveBy()) { - startTime = requestTime.minus(traveltimeRequest.maxCutoff); - endTime = requestTime; - routingRequest.setTo(parsedLocation); - } else { - startTime = requestTime; - endTime = startTime.plus(traveltimeRequest.maxCutoff); - routingRequest.setFrom(parsedLocation); - } - - ZoneId zoneId = transitService.getTimeZone(); - LocalDate startDate = LocalDate.ofInstant(startTime, zoneId); - LocalDate endDate = LocalDate.ofInstant(endTime, zoneId); - startOfTime = ServiceDateUtils.asStartOfService(startDate, zoneId); - - requestTransitDataProvider = - new RaptorRoutingRequestTransitData( - transitService.getRealtimeTransitLayer(), - startOfTime, - 0, - (int) Period.between(startDate, endDate).get(ChronoUnit.DAYS), - new RouteRequestTransitDataProviderFilter(routingRequest), - routingRequest - ); - - raptorService = new RaptorService<>(serverContext.raptorConfig()); - } - - @GET - @Path("/isochrone") - @Produces(MediaType.APPLICATION_JSON) - public Response getIsochrones() { - ZSampleGrid sampleGrid = getSampleGrid(); - - var isochrones = IsochroneRenderer.renderIsochrones(sampleGrid, traveltimeRequest); - var features = IsochroneRenderer.makeContourFeatures(isochrones); - - StreamingOutput out = outputStream -> { - try (final GeoJSONWriter geoJSONWriter = new GeoJSONWriter(outputStream)) { - geoJSONWriter.writeFeatureCollection(features); - } - }; - - return Response.ok().entity(out).build(); - } - - @GET - @Path("/surface") - @Produces("image/tiff") - public Response getSurface() { - ZSampleGrid sampleGrid = getSampleGrid(); - StreamingOutput streamingOutput = RasterRenderer.createGeoTiffRaster(sampleGrid); - return Response.ok().entity(streamingOutput).build(); - } - - private ZSampleGrid getSampleGrid() { - try ( - var temporaryVertices = new TemporaryVerticesContainer( - graph, - routingRequest, - getAccessRequest(routingRequest).mode(), - StreetMode.NOT_SET - ) - ) { - var accessList = getAccess(temporaryVertices); - var arrivals = route(accessList).getArrivals(); - var spt = getShortestPathTree(temporaryVertices, arrivals); - return SampleGridRenderer.getSampleGrid(spt, traveltimeRequest); - } - } - - private Collection getAccess(TemporaryVerticesContainer temporaryVertices) { - final Collection accessStops = AccessEgressRouter.streetSearch( - routingRequest, - temporaryVertices, - transitService, - getAccessRequest(routingRequest), - null, - routingRequest.arriveBy(), - traveltimeRequest.maxAccessDuration, - 0 - ); - return AccessEgressMapper.mapNearbyStops(accessStops, routingRequest.arriveBy()); - } - - private ShortestPathTree getShortestPathTree( - TemporaryVerticesContainer temporaryVertices, - StopArrivals arrivals - ) { - return StreetSearchBuilder - .of() - .setSkipEdgeStrategy( - new PostTransitSkipEdgeStrategy( - traveltimeRequest.maxEgressDuration, - routingRequest.dateTime(), - routingRequest.arriveBy() - ) - ) - .setRequest(routingRequest) - .setStreetRequest(getEgressRequest(routingRequest)) - .setVerticesContainer(temporaryVertices) - .setDominanceFunction(new DominanceFunctions.EarliestArrival()) - .setInitialStates(getInitialStates(arrivals, temporaryVertices)) - .getShortestPathTree(); - } - - private List getInitialStates( - StopArrivals arrivals, - TemporaryVerticesContainer temporaryVertices - ) { - List initialStates = new ArrayList<>(); - - StreetSearchRequest directStreetSearchRequest = StreetSearchRequestMapper - .map(routingRequest) - .withMode(routingRequest.journey().direct().mode()) - .withArriveBy(routingRequest.arriveBy()) - .build(); - - List directStateDatas = StateData.getInitialStateDatas(directStreetSearchRequest); - - Set vertices = routingRequest.arriveBy() - ? temporaryVertices.getToVertices() - : temporaryVertices.getFromVertices(); - for (var vertex : vertices) { - for (var stateData : directStateDatas) { - initialStates.add(new State(vertex, startTime, stateData, directStreetSearchRequest)); - } - } - - StreetSearchRequest egressStreetSearchRequest = StreetSearchRequestMapper - .map(routingRequest) - .withMode(getEgressRequest(routingRequest).mode()) - .withArriveBy(routingRequest.arriveBy()) - .build(); - - for (RegularStop stop : transitService.listRegularStops()) { - int index = stop.getIndex(); - if (!arrivals.reachedByTransit(index)) { - continue; - } - final int arrivalTime = arrivals.bestTransitArrivalTime(index); - Vertex v = graph.getStopVertexForStopId(stop.getId()); - if (v == null) { - continue; - } - Instant time = startOfTime.plusSeconds(arrivalTime).toInstant(); - List egressStateDatas = StateData.getInitialStateDatas( - egressStreetSearchRequest, - mode -> new TravelTimeStateData(mode, time.getEpochSecond()) - ); - for (var stopStateData : egressStateDatas) { - State s = new State(v, time, stopStateData, directStreetSearchRequest); - s.weight = - routingRequest.arriveBy() - ? time.until(endTime, ChronoUnit.SECONDS) - : startTime.until(time, ChronoUnit.SECONDS); - initialStates.add(s); - } - } - return initialStates; - } - - private RaptorResponse route(Collection accessList) { - RaptorRequestBuilder builder = new RaptorRequestBuilder<>(); - - builder - .profile(RaptorProfile.BEST_TIME) - .searchParams() - .earliestDepartureTime(ServiceDateUtils.secondsSinceStartOfTime(startOfTime, startTime)) - .latestArrivalTime(ServiceDateUtils.secondsSinceStartOfTime(startOfTime, endTime)) - .searchOneIterationOnly() - .timetable(false) - .allowEmptyAccessEgressPaths(true) - .constrainedTransfers(false); // TODO: Not compatible with best times - - if (routingRequest.arriveBy()) { - builder.searchDirection(SearchDirection.REVERSE).searchParams().addEgressPaths(accessList); - } else { - builder.searchDirection(SearchDirection.FORWARD).searchParams().addAccessPaths(accessList); - } - - return raptorService.route(builder.build(), requestTransitDataProvider); - } - - private StreetRequest getAccessRequest(RouteRequest accessRequest) { - return routingRequest.arriveBy() - ? accessRequest.journey().egress() - : accessRequest.journey().access(); - } - - private StreetRequest getEgressRequest(RouteRequest accessRequest) { - return routingRequest.arriveBy() - ? accessRequest.journey().access() - : accessRequest.journey().egress(); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeStateData.java b/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeStateData.java deleted file mode 100644 index 1c1ad71ae4f..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeStateData.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import org.opentripplanner.routing.api.request.StreetMode; -import org.opentripplanner.street.search.state.StateData; - -public class TravelTimeStateData extends StateData { - - protected final long postTransitDepartureTime; - - public TravelTimeStateData(StreetMode streetMode, long postTransitDepartureTime) { - super(streetMode); - this.postTransitDepartureTime = postTransitDepartureTime; - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/WTWD.java b/src/ext/java/org/opentripplanner/ext/traveltime/WTWD.java deleted file mode 100644 index a86af8c7b3c..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/WTWD.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -/** - * The default TZ data we keep for each sample: Weighted Time and Walk Distance - *

- * For now we keep all possible values in the vector; we may want to remove the values that will not - * be used in the process (for example # of boardings). Currently, the filtering is done afterwards, - * it may be faster and surely less memory-intensive to do the filtering when processing. - * - * @author laurent - */ -public class WTWD { - - /* Total weight */ - public double w; - - // TODO Add generalized cost - - /* Weighted sum of time in seconds */ - public double wTime; - - /* Weighted sum of walk distance in meters */ - public double wWalkDist; - - /* Minimum off-road distance to any sample */ - public double d; - - @Override - public String toString() { - return String.format("[t/w=%f,w=%f,d=%f]", wTime / w, w, d); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/WTWDAccumulativeMetric.java b/src/ext/java/org/opentripplanner/ext/traveltime/WTWDAccumulativeMetric.java deleted file mode 100644 index 14186d54082..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/WTWDAccumulativeMetric.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.opentripplanner.ext.traveltime; - -import java.util.ArrayList; -import java.util.List; -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.ext.traveltime.geometry.AccumulativeMetric; -import org.opentripplanner.ext.traveltime.geometry.ZSamplePoint; -import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; - -/** - * Any given sample is weighted according to the inverse of the squared normalized distance + 1 to - * the grid sample. We add to the sampling time a default off-road walk distance to account for - * off-road sampling. TODO how does this "account" for off-road sampling ? - */ -public class WTWDAccumulativeMetric implements AccumulativeMetric { - - private final double cosLat; - private final double offRoadDistanceMeters; - private final double offRoadSpeed; - private final double gridSizeMeters; - - public WTWDAccumulativeMetric( - double cosLat, - double offRoadDistanceMeters, - double offRoadSpeed, - double gridSizeMeters - ) { - this.cosLat = cosLat; - this.offRoadDistanceMeters = offRoadDistanceMeters; - this.offRoadSpeed = offRoadSpeed; - this.gridSizeMeters = gridSizeMeters; - } - - @Override - public WTWD cumulateSample(Coordinate C0, Coordinate Cs, WTWD z, WTWD zS, double offRoadSpeed) { - double t = z.wTime / z.w; - double wd = z.wWalkDist / z.w; - double d = SphericalDistanceLibrary.fastDistance(C0, Cs, cosLat); - // additional time - double dt = d / offRoadSpeed; - /* - * Compute weight for time. The weight function to distance here is somehow arbitrary. - * Its only purpose is to weight the samples when there is various samples within the - * same "cell", giving more weight to the closest samples to the cell center. - */ - double w = 1 / ((d + gridSizeMeters) * (d + gridSizeMeters)); - if (zS == null) { - zS = new WTWD(); - zS.d = Double.MAX_VALUE; - } - zS.w = zS.w + w; - zS.wTime = zS.wTime + w * (t + dt); - zS.wWalkDist = zS.wWalkDist + w * (wd + d); - if (d < zS.d) { - zS.d = d; - } - return zS; - } - - /** - * A Generated closing sample take 1) as off-road distance, the minimum of the off-road distance - * of all enclosing samples, plus the grid size, and 2) as time the minimum time of all enclosing - * samples plus the grid size * off-road walk speed as additional time. All this are - * approximations. - *

- * TODO Is there a better way of computing this? Here the computation will be different - * based on the order where we close the samples. - */ - @Override - public boolean closeSample(ZSamplePoint point) { - double dMin = Double.MAX_VALUE; - double tMin = Double.MAX_VALUE; - double wdMin = Double.MAX_VALUE; - List zz = new ArrayList<>(4); - if (point.up() != null) zz.add(point.up().getZ()); - if (point.down() != null) zz.add(point.down().getZ()); - if (point.right() != null) zz.add(point.right().getZ()); - if (point.left() != null) zz.add(point.left().getZ()); - for (WTWD z : zz) { - if (z.d < dMin) dMin = z.d; - double t = z.wTime / z.w; - if (t < tMin) tMin = t; - double wd = z.wWalkDist / z.w; - if (wd < wdMin) wdMin = wd; - } - WTWD z = new WTWD(); - z.w = 1.0; - /* - * The computations below are approximation, but we are on the edge anyway and the - * current sample does not correspond to any computed value. - */ - z.wTime = tMin + gridSizeMeters / offRoadSpeed; - z.wWalkDist = wdMin + gridSizeMeters; - z.d = dMin + gridSizeMeters; - point.setZ(z); - return dMin > offRoadDistanceMeters; - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/AccumulativeGridSampler.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/AccumulativeGridSampler.java deleted file mode 100644 index 32a6e3c6c62..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/AccumulativeGridSampler.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import java.util.ArrayList; -import java.util.List; -import org.locationtech.jts.geom.Coordinate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Helper class to fill-in a ZSampleGrid from a given loosely-defined set of sampling points. - * - * The process is customized by an "accumulative" metric which gives the behavior of cumulating - * several values onto one sampling point. - * - * To use this class, create an instance giving an AccumulativeMetric implementation as parameter. - * Then for each source sample, call "addSample" with the its TZ value. At the end, call close() to - * close the sample grid (ie add grid node at the edge to make sure borders are correctly defined, - * the definition of correct is left to the client). - * - * @author laurent - */ -public class AccumulativeGridSampler { - - private static final Logger LOG = LoggerFactory.getLogger(AccumulativeGridSampler.class); - - private final AccumulativeMetric metric; - - private final ZSampleGrid sampleGrid; - - private boolean closed = false; - - /** - * @param metric TZ data "behavior" and "metric". - */ - public AccumulativeGridSampler(ZSampleGrid sampleGrid, AccumulativeMetric metric) { - this.metric = metric; - this.sampleGrid = sampleGrid; - } - - public final void addSamplingPoint(Coordinate C0, TZ z, double offRoadSpeed) { - if (closed) throw new IllegalStateException("Can't add a sample after closing."); - int[] xy = sampleGrid.getLowerLeftIndex(C0); - int x = xy[0]; - int y = xy[1]; - List> ABCD = List.of( - sampleGrid.getOrCreate(x, y), - sampleGrid.getOrCreate(x + 1, y), - sampleGrid.getOrCreate(x, y + 1), - sampleGrid.getOrCreate(x + 1, y + 1) - ); - for (ZSamplePoint P : ABCD) { - Coordinate C = sampleGrid.getCoordinates(P); - P.setZ(metric.cumulateSample(C0, C, z, P.getZ(), offRoadSpeed)); - } - } - - /** - * Surround all existing samples on the edge by a layer of closing samples. - */ - public final void close() { - if (closed) return; - closed = true; - List> processList = new ArrayList<>(sampleGrid.size()); - for (ZSamplePoint A : sampleGrid) { - processList.add(A); - } - int round = 0; - int n = 0; - while (!processList.isEmpty()) { - List> newProcessList = new ArrayList<>(processList.size()); - for (ZSamplePoint A : processList) { - if (A.right() == null) { - ZSamplePoint B = closeSample(A.getX() + 1, A.getY()); - if (B != null) newProcessList.add(B); - n++; - } - if (A.left() == null) { - ZSamplePoint B = closeSample(A.getX() - 1, A.getY()); - if (B != null) newProcessList.add(B); - n++; - } - if (A.up() == null) { - ZSamplePoint B = closeSample(A.getX(), A.getY() + 1); - if (B != null) newProcessList.add(B); - n++; - } - if (A.down() == null) { - ZSamplePoint B = closeSample(A.getX(), A.getY() - 1); - if (B != null) newProcessList.add(B); - n++; - } - } - processList = newProcessList; - LOG.debug("Round {} : next process list {}", round, processList.size()); - round++; - } - LOG.info("Added {} closing samples to get a total of {}.", n, sampleGrid.size()); - } - - private ZSamplePoint closeSample(int x, int y) { - ZSamplePoint A = sampleGrid.getOrCreate(x, y); - boolean ok = metric.closeSample(A); - if (ok) { - return null; - } else { - return A; - } - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/AccumulativeMetric.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/AccumulativeMetric.java deleted file mode 100644 index 2f7d18c409a..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/AccumulativeMetric.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import org.locationtech.jts.geom.Coordinate; - -/** - * An accumulative metric give the behavior of combining several samples to a regular sample grid, - * ie how we should weight and add several TZ values from inside a cell to compute the cell corner - * TZ values. - * - * @author laurent - */ -public interface AccumulativeMetric { - /** - * Callback function to handle a new added sample. - * - * @param C0 The initial position of the sample, as given in the addSample() call. - * @param Cs The position of the sample on the grid, never farther away than (dX,dY) - * @param z The z value of the initial sample, as given in the addSample() call. - * @param zS The previous z value of the sample. Can be null if this is the first time, - * it's up to the caller to initialize the z value. - * @param offRoadSpeed The offroad speed to assume. - * @return The modified z value for the sample. - */ - TZ cumulateSample(Coordinate C0, Coordinate Cs, TZ z, TZ zS, double offRoadSpeed); - - /** - * Callback function to handle a "closing" sample (that is a sample post-created to surround - * existing samples and provide nice and smooth edges for the algorithm). - * - * @param point The point to set Z. - * @return True if the point "close" the set. - */ - boolean closeSample(ZSamplePoint point); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayEdge.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayEdge.java deleted file mode 100644 index ad3a6623044..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayEdge.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -/** - * A DelaunayEdge is a directed segment between two DelaunayPoints of the triangulation. - *

- * The interface is kept minimal for isoline building purposes. - * - * @author laurent - */ -interface DelaunayEdge { - /** - * @return The start point (node) of this edge. - */ - DelaunayPoint getA(); - - /** - * @return The end point (node) of this edge. - */ - DelaunayPoint getB(); - - /** - * @param ccw true (CCW) for A->B left edge, false (CW) for right edge. - * @return The edge starting at B, going right or left. - */ - DelaunayEdge getEdge1(boolean ccw); - - /** - * @param ccw For same value of ccw, will return the same side as getEdge1(). - * @return The edge starting at A, going right or left. - */ - DelaunayEdge getEdge2(boolean ccw); - - /** - * HACK. This should not be here really. But with Java, attaching some user value to an object - * rely on another level of indirection and costly maps/arrays. Exposing this flag directly here - * saves *lots* of processing time. TODO Is there a better way to do that? - * - * @return The flag set by setProcessed. - */ - boolean isProcessed(); - - /** - * - */ - void setProcessed(boolean processed); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayIsolineBuilder.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayIsolineBuilder.java deleted file mode 100644 index f669df864b9..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayIsolineBuilder.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import static org.locationtech.jts.geom.CoordinateArrays.toCoordinateArray; -import static org.locationtech.jts.geom.GeometryFactory.toGeometryArray; -import static org.locationtech.jts.geom.GeometryFactory.toLinearRingArray; -import static org.locationtech.jts.geom.GeometryFactory.toPolygonArray; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; -import org.locationtech.jts.algorithm.Area; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LinearRing; -import org.locationtech.jts.geom.MultiPolygon; -import org.locationtech.jts.geom.Polygon; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Compute isoline based on a Delaunay triangulation of z samplings. - * - * It will compute an isoline for a given z0 value. The isoline is composed of a list of n polygons, - * CW for normal polygons, CCW for "holes". The isoline computation can be called multiple times on - * the same builder for different z0 value: this will reduce the number of Fz sampling as they are - * cached in the builder, and reduce the number of time the Delaunay triangulation has to be built. - * - * The algorithm is rather simple: for each edges of the triangulation check if the edge is - * "cutting" (ie crossing the z0 plane). Then start for each unprocessed cutting edge using a walk - * algorithm, keeping high z0 always one the same side, to build a set of closed polygons. Then - * process each polygon to punch holes: a CW polygon is a hole in a larger CCW polygon, a CCW - * polygon is an island (shell). - * - * @author laurent - */ -public class DelaunayIsolineBuilder implements IsolineBuilder { - - private static final Logger LOG = LoggerFactory.getLogger(DelaunayIsolineBuilder.class); - - private final ZMetric zMetric; - - private final DelaunayTriangulation triangulation; - - private final GeometryFactory geometryFactory = new GeometryFactory(); - - private final List debugGeom = new ArrayList<>(); - - private boolean debug = false; - - /** - * Create an object to compute isolines. One may call several time computeIsoline on the same - * object, with different z0 values. - * - * @param triangulation The triangulation to process. Must be closed (no edge at the border - * should intersect). - * @param zMetric The Z metric (intersection detection and interpolation method). - */ - public DelaunayIsolineBuilder(DelaunayTriangulation triangulation, ZMetric zMetric) { - this.triangulation = triangulation; - this.zMetric = zMetric; - } - - public void setDebug(boolean debug) { - this.debug = debug; - } - - @Override - public Geometry computeIsoline(TZ z0) { - Queue> processQ = new ArrayDeque<>(triangulation.edgesCount()); - for (DelaunayEdge e : triangulation.edges()) { - e.setProcessed(false); - processQ.add(e); - } - - if (debug) generateDebugGeometry(z0); - - List rings = new ArrayList<>(); - while (!processQ.isEmpty()) { - DelaunayEdge e = processQ.remove(); - if (e.isProcessed()) continue; - e.setProcessed(true); - int cut = zMetric.cut(e.getA().getZ(), e.getB().getZ(), z0); - if (cut == 0) { - continue; // While, next edge - } - List polyPoints = new ArrayList<>(); - boolean ccw = cut > 0; - while (true) { - // Add a point to polyline - Coordinate cA = e.getA().getCoordinates(); - Coordinate cB = e.getB().getCoordinates(); - double k = zMetric.interpolate(e.getA().getZ(), e.getB().getZ(), z0); - Coordinate cC = new Coordinate(cA.x * (1.0 - k) + cB.x * k, cA.y * (1.0 - k) + cB.y * k); - polyPoints.add(cC); - e.setProcessed(true); - DelaunayEdge E1 = e.getEdge1(ccw); - DelaunayEdge E2 = e.getEdge2(ccw); - int cut1 = E1 == null ? 0 : zMetric.cut(E1.getA().getZ(), E1.getB().getZ(), z0); - int cut2 = E2 == null ? 0 : zMetric.cut(E2.getA().getZ(), E2.getB().getZ(), z0); - boolean ok1 = cut1 != 0 && !E1.isProcessed(); - boolean ok2 = cut2 != 0 && !E2.isProcessed(); - if (ok1) { - e = E1; - ccw = cut1 > 0; - } else if (ok2) { - e = E2; - ccw = cut2 > 0; - } else { - // This must be the end of the polyline... - break; - } - } - // Close the polyline - polyPoints.add(polyPoints.get(0)); - if (polyPoints.size() > 5) { - // If the ring is smaller than 4 points do not add it, - // that will remove too small islands or holes. - LinearRing ring = geometryFactory.createLinearRing(toCoordinateArray(polyPoints)); - rings.add(ring); - } - } - return punchHoles(rings); - } - - private void generateDebugGeometry(TZ z0) { - debug = false; - for (DelaunayEdge e : triangulation.edges()) { - Coordinate cA = e.getA().getCoordinates(); - Coordinate cB = e.getB().getCoordinates(); - debugGeom.add(geometryFactory.createLineString(new Coordinate[] { cA, cB })); - if (zMetric.cut(e.getA().getZ(), e.getB().getZ(), z0) != 0) { - double k = zMetric.interpolate(e.getA().getZ(), e.getB().getZ(), z0); - Coordinate cC = new Coordinate(cA.x * (1.0 - k) + cB.x * k, cA.y * (1.0 - k) + cB.y * k); - debugGeom.add(geometryFactory.createPoint(cC)); - } - } - } - - public final Geometry getDebugGeometry() { - return geometryFactory.createGeometryCollection(toGeometryArray(debugGeom)); - } - - @SuppressWarnings("unchecked") - private MultiPolygon punchHoles(List rings) { - List shells = new ArrayList<>(rings.size()); - List holes = new ArrayList<>(rings.size() / 2); - // 1. Split the polygon list in two: shells and holes (CCW and CW) - for (LinearRing ring : rings) { - if (Area.ofRingSigned(ring.getCoordinateSequence()) > 0.0) { - holes.add(ring); - } else { - shells.add(geometryFactory.createPolygon(ring)); - } - } - // 2. Sort the shells based on number of points to optimize step 3. - shells.sort((o1, o2) -> o2.getNumPoints() - o1.getNumPoints()); - for (Polygon shell : shells) { - shell.setUserData(new ArrayList()); - } - // 3. For each hole, determine which shell it fits in. - int nHolesFailed = 0; - for (LinearRing hole : holes) { - outer:{ - // Probably most of the time, the first shell will be the one - for (Polygon shell : shells) { - if (shell.contains(hole)) { - ((List) shell.getUserData()).add(hole); - break outer; - } - } - // This should not happen, but do not break bad here - // as loosing a hole is not critical, we still have - // sensible data to return. - nHolesFailed += 1; - } - } - if (nHolesFailed > 0) { - LOG.error("Could not find a shell for {} holes.", nHolesFailed); - } - // 4. Build the list of punched polygons - List punched = new ArrayList<>(shells.size()); - for (Polygon shell : shells) { - List shellHoles = ((List) shell.getUserData()); - punched.add( - geometryFactory.createPolygon(shell.getExteriorRing(), toLinearRingArray(shellHoles)) - ); - } - return geometryFactory.createMultiPolygon(toPolygonArray(punched)); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayPoint.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayPoint.java deleted file mode 100644 index 9bfb5f04882..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayPoint.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import org.locationtech.jts.geom.Coordinate; - -/** - * A DelaunayPoint is the geometrical point of a node of the triangulation. - * - * @author laurent - */ -interface DelaunayPoint { - /** - * @return The geometric location of this point. - */ - Coordinate getCoordinates(); - - /** - * @return The Z value for this point. - */ - TZ getZ(); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayTriangulation.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayTriangulation.java deleted file mode 100644 index cfd3ae3db59..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/DelaunayTriangulation.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -/** - * A Delaunay triangulation (adapted to isoline building). - * - * A simple interface returning a collection (an iterable) of DelaunayEdges. The interface is kept - * minimal for isoline building purposes. - * - * @author laurent - * @param The value stored for each node. - */ -public interface DelaunayTriangulation { - int edgesCount(); - - Iterable> edges(); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/IsolineBuilder.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/IsolineBuilder.java deleted file mode 100644 index 4dd775e948d..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/IsolineBuilder.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import org.locationtech.jts.geom.Geometry; - -/** - * Generic interface for a class that compute an isoline out of a TZ 2D "field". - * - * @author laurent - */ -public interface IsolineBuilder { - Geometry computeIsoline(TZ z0); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/SparseMatrix.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/SparseMatrix.java deleted file mode 100644 index e1a61c224ca..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/SparseMatrix.java +++ /dev/null @@ -1,167 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * A fast sparse 2D matrix holding elements of type T. - * The x and y indexes into the sparse matrix are _signed_ 32-bit integers (negative indexes are allowed). - * Square sub-chunks of size chunkSize x chunkSize are stored in a hashmap, - * keyed on a combination of the x and y coordinates. - * Does not implement the collection interface for simplicity and speed. - * Not thread-safe! - * - * @author laurent - */ -public class SparseMatrix implements Iterable { - - private int shift; // How many low order bits to shift off to get the index of a chunk. - - private final int mask; // The low order bits to retain when finding the index within a chunk. - - private final Map chunks; - - int size = 0; // The number of elements currently stored in this matrix (number of cells containing a T). - - int matSize; // The capacity of a single chunk TODO rename - - int chunkSize; // The dimension of a single chunk in each of two dimensions TODO rename - - public int xMin, xMax, yMin, yMax; // The maximum and minimum indices where an element is stored. - - /** - * @param chunkSize Must be a power of two so chunk indexes can be determined by shifting off low order bits. - * Keep it small (8, 16, 32...). Chunks are square, with this many elements in each of two dimensions, - * so the number of elements in each chunk will be the square of this value. - * @param totalSize Estimated total number of elements to be stored in the matrix (actual use, not capacity). - */ - public SparseMatrix(int chunkSize, int totalSize) { - shift = 0; - this.chunkSize = chunkSize; - mask = chunkSize - 1; // all low order bits below the given power of two - this.matSize = chunkSize * chunkSize; // capacity of a single chunk - /* Find log_2 chunkSize, the number of low order bits to shift off an index to get its chunk index. */ - while (chunkSize > 1) { - if (chunkSize % 2 != 0) throw new IllegalArgumentException("Chunk size must be a power of 2"); - chunkSize /= 2; - shift++; - } - // We assume here that each chunk will be filled at ~25% (thus the x4) - this.chunks = new HashMap<>(totalSize / matSize * 4); - this.xMin = Integer.MAX_VALUE; - this.yMin = Integer.MAX_VALUE; - this.xMax = Integer.MIN_VALUE; - this.yMax = Integer.MIN_VALUE; - } - - public final T get(int x, int y) { - T[] ts = chunks.get(new Key(x, y, shift)); - if (ts == null) { - return null; - } - int index = ((x & mask) << shift) + (y & mask); - return ts[index]; - } - - @SuppressWarnings("unchecked") - public final T put(int x, int y, T t) { - /* Keep a bounding box around all matrix cells in use. */ - if (x < xMin) xMin = x; - if (x > xMax) xMax = x; - if (y < yMin) yMin = y; - if (y > yMax) yMax = y; - Key key = new Key(x, y, shift); - // Java does not allow arrays of generics. - T[] ts = chunks.computeIfAbsent(key, k -> (T[]) (new Object[matSize])); - /* Find index within chunk: concatenated low order bits of x and y. */ - int index = ((x & mask) << shift) + (y & mask); - if (ts[index] == null) size++; - ts[index] = t; - return t; - } - - public int size() { - return size; - } - - /* - * We rely on the map iterator for checking for concurrent modification exceptions. - */ - private class SparseMatrixIterator implements Iterator { - - private final Iterator mapIterator; - - private int chunkIndex = -1; - - private T[] chunk = null; - - private SparseMatrixIterator() { - mapIterator = chunks.values().iterator(); - moveToNext(); - } - - @Override - public boolean hasNext() { - return chunk != null; - } - - @Override - public T next() { - T t = chunk[chunkIndex]; - moveToNext(); - return t; - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove"); - } - - private void moveToNext() { - if (chunk == null) { - chunk = mapIterator.hasNext() ? mapIterator.next() : null; - if (chunk == null) return; // End - } - while (true) { - chunkIndex++; - if (chunkIndex == matSize) { - chunkIndex = 0; - chunk = mapIterator.hasNext() ? mapIterator.next() : null; - if (chunk == null) return; // End - } - if (chunk[chunkIndex] != null) return; - } - } - } - - @Override - public Iterator iterator() { - return new SparseMatrixIterator(); - } - - /** - * We were previously bit-shifting two 32 bit integers into a long. These are used as map keys, so they had to be - * Long objects rather than primitive long ints. This purpose-built key object should be roughly the same in terms - * of space and speed, and more readable. - */ - static class Key { - - int x, y; - - public Key(int x, int y, int shift) { - this.x = x >>> shift; // shift off low order bits (index within chunk) retaining only the chunk number - this.y = y >>> shift; // same for y coordinate - } - - @Override - public int hashCode() { - return x ^ y; - } - - @Override - public boolean equals(Object other) { - return other instanceof Key && ((Key) other).x == x && ((Key) other).y == y; - } - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/SparseMatrixZSampleGrid.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/SparseMatrixZSampleGrid.java deleted file mode 100644 index c2fa3744107..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/SparseMatrixZSampleGrid.java +++ /dev/null @@ -1,342 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import javax.annotation.Nonnull; -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.framework.lang.IntUtils; - -/** - * A generic indexed grid of Z samples. - * - * Internally use a SparseMatrix to store samples. - * - * @author laurent - */ -public final class SparseMatrixZSampleGrid - implements ZSampleGrid, DelaunayTriangulation { - - private final class SparseMatrixSamplePoint implements ZSamplePoint, DelaunayPoint { - - private int x; - - private int y; - - private TZ z; - - private SparseMatrixSamplePoint up, down, right, left; - - private GridDelaunayEdge eUp, eUpRight, eRight; - - @Override - public ZSamplePoint up() { - return up; - } - - @Override - public ZSamplePoint down() { - return down; - } - - @Override - public ZSamplePoint right() { - return right; - } - - @Override - public ZSamplePoint left() { - return left; - } - - @Override - public Coordinate getCoordinates() { - return SparseMatrixZSampleGrid.this.getCoordinates(this); - } - - @Override - public int getX() { - return this.x; - } - - @Override - public int getY() { - return this.y; - } - - @Override - public TZ getZ() { - return this.z; - } - - @Override - public void setZ(TZ z) { - this.z = z; - } - } - - private final class GridDelaunayEdge implements DelaunayEdge { - - private static final int TYPE_VERTICAL = 0; - - private static final int TYPE_HORIZONTAL = 1; - - private static final int TYPE_DIAGONAL = 2; - - private boolean processed; - - private final SparseMatrixSamplePoint A, B; - - private GridDelaunayEdge ccw1, ccw2, cw1, cw2; - - private final int type; - - private GridDelaunayEdge(SparseMatrixSamplePoint A, SparseMatrixSamplePoint B, int type) { - this.A = A; - this.B = B; - switch (type) { - case TYPE_HORIZONTAL -> A.eRight = this; - case TYPE_VERTICAL -> A.eUp = this; - case TYPE_DIAGONAL -> A.eUpRight = this; - } - this.type = type; - } - - @Override - public DelaunayPoint getA() { - return A; - } - - @Override - public DelaunayPoint getB() { - return B; - } - - @Override - public DelaunayEdge getEdge1(boolean ccw) { - return ccw ? ccw1 : cw1; - } - - @Override - public DelaunayEdge getEdge2(boolean ccw) { - return ccw ? ccw2 : cw2; - } - - @Override - public boolean isProcessed() { - return processed; - } - - @Override - public void setProcessed(boolean processed) { - this.processed = processed; - } - - @Override - public String toString() { - return "" + B.getCoordinates() + ">"; - } - } - - private final double dX, dY; - - private final Coordinate center; - - private final SparseMatrix allSamples; - - private List triangulation = null; - - /** - * @param chunkSize SparseMatrix chunk side (eg 8 or 16). See SparseMatrix. - * @param totalSize Total estimated size for pre-allocating. - * @param dX X grid size, same units as center coordinates. - * @param dY Y grid size, same units as center coordinates. - * @param center Center position of the grid. Do not need to be precise. - */ - public SparseMatrixZSampleGrid( - int chunkSize, - int totalSize, - double dX, - double dY, - Coordinate center - ) { - this.center = center; - this.dX = dX; - this.dY = dY; - allSamples = new SparseMatrix<>(chunkSize, totalSize); - } - - public ZSamplePoint getOrCreate(int x, int y) { - SparseMatrixSamplePoint A = allSamples.get(x, y); - if (A != null) return A; - A = new SparseMatrixSamplePoint(); - A.x = x; - A.y = y; - A.z = null; - SparseMatrixSamplePoint Aup = allSamples.get(x, y + 1); - if (Aup != null) { - Aup.down = A; - A.up = Aup; - } - SparseMatrixSamplePoint Adown = allSamples.get(x, y - 1); - if (Adown != null) { - Adown.up = A; - A.down = Adown; - } - SparseMatrixSamplePoint Aright = allSamples.get(x + 1, y); - if (Aright != null) { - Aright.left = A; - A.right = Aright; - } - SparseMatrixSamplePoint Aleft = allSamples.get(x - 1, y); - if (Aleft != null) { - Aleft.right = A; - A.left = Aleft; - } - allSamples.put(x, y, A); - return A; - } - - @Override - @Nonnull - public Iterator> iterator() { - return new Iterator<>() { - private final Iterator iterator = allSamples.iterator(); - - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public ZSamplePoint next() { - return iterator.next(); - } - - @Override - public void remove() { - iterator.remove(); - } - }; - } - - @Override - public Coordinate getCoordinates(ZSamplePoint point) { - // TODO Cache the coordinates in the point? - return new Coordinate(point.getX() * dX + center.x, point.getY() * dY + center.y); - } - - @Override - public int[] getLowerLeftIndex(Coordinate C) { - return new int[] { - IntUtils.round((C.x - center.x - dX / 2) / dX), - IntUtils.round((C.y - center.y - dY / 2) / dY), - }; - } - - @Override - public Coordinate getCenter() { - return center; - } - - @Override - public Coordinate getCellSize() { - return new Coordinate(dX, dY); - } - - @Override - public int getXMin() { - return allSamples.xMin; - } - - @Override - public int getXMax() { - return allSamples.xMax; - } - - @Override - public int getYMin() { - return allSamples.yMin; - } - - @Override - public int getYMax() { - return allSamples.yMax; - } - - @Override - public int size() { - return allSamples.size(); - } - - @Override - public int edgesCount() { - if (triangulation == null) { - delaunify(); - } - return triangulation.size(); - } - - @Override - public Iterable> edges() { - if (triangulation == null) { - delaunify(); - } - return triangulation; - } - - /** - * The conversion from a grid of points to a Delaunay triangulation is trivial. Each square from - * the grid is cut through one diagonal in two triangles, the resulting output is a Delaunay - * triangulation. - */ - private void delaunify() { - triangulation = new ArrayList<>(allSamples.size() * 3); - // 1. Create unlinked edges - for (SparseMatrixSamplePoint A : allSamples) { - SparseMatrixSamplePoint B = (SparseMatrixSamplePoint) A.right(); - SparseMatrixSamplePoint D = (SparseMatrixSamplePoint) A.up(); - SparseMatrixSamplePoint C = (SparseMatrixSamplePoint) ( - B != null ? B.up() : D != null ? D.right() : null - ); - if (B != null) { - triangulation.add(new GridDelaunayEdge(A, B, GridDelaunayEdge.TYPE_HORIZONTAL)); - } - if (D != null) { - triangulation.add(new GridDelaunayEdge(A, D, GridDelaunayEdge.TYPE_VERTICAL)); - } - if (C != null) { - triangulation.add(new GridDelaunayEdge(A, C, GridDelaunayEdge.TYPE_DIAGONAL)); - } - } - // 2. Link edges - for (GridDelaunayEdge e : triangulation) { - switch (e.type) { - case GridDelaunayEdge.TYPE_HORIZONTAL -> { - e.ccw1 = e.B.eUp; - e.ccw2 = e.A.eUpRight; - e.cw1 = e.A.down == null ? null : e.A.down.eUpRight; - e.cw2 = e.A.down == null ? null : e.A.down.eUp; - } - case GridDelaunayEdge.TYPE_VERTICAL -> { - e.ccw1 = e.A.left == null ? null : e.A.left.eUpRight; - e.ccw2 = e.A.left == null ? null : e.A.left.eRight; - e.cw1 = e.B.eRight; - e.cw2 = e.A.eUpRight; - } - case GridDelaunayEdge.TYPE_DIAGONAL -> { - e.ccw1 = e.A.up == null ? null : e.A.up.eRight; - e.ccw2 = e.A.eUp; - e.cw1 = e.A.right == null ? null : e.A.right.eUp; - e.cw2 = e.A.eRight; - } - } - } - } - - @Override - public DelaunayTriangulation delaunayTriangulate() { - // We ourselves are a DelaunayTriangulation - return this; - } -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZMetric.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZMetric.java deleted file mode 100644 index 56592359240..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZMetric.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -/** - * A ZMetric is a metric for some generic TZ value. - *

- * By metric here we understand: - *

    - *
  • Cutting detection on a range, z0 in [Za, Zb] (rely on TZ to be an ordered set)
  • - *
  • Interpolation on a range, z0 in [Za, Zb].
  • - *
- * Cutting detection could be easily implemented using interpolation, but usually interpolating - * is rather more expansive than cutting detection, so we split the two operations. - * - * @author laurent - */ -public interface ZMetric { - /** - * Check if the edge [AB] between two samples A and B "intersect" the zz0 plane. - * - * @param zA z value for the A sample - * @param zB z value for the B sample - * @param z0 z value for the intersecting plane - * @return 0 if no intersection, -1 or +1 if intersection (depending on which is lower, A or B). - */ - int cut(TZ zA, TZ zB, TZ z0); - - /** - * Interpolate a crossing point on an edge [AB]. - * - * @param zA z value for the A sample - * @param zB z value for the B sample - * @param z0 z value for the intersecting plane - * @return k value between 0 and 1, where the crossing occurs. 0=A, 1=B. - */ - double interpolate(TZ zA, TZ zB, TZ z0); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZSampleGrid.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZSampleGrid.java deleted file mode 100644 index 78b694b0f79..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZSampleGrid.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -import org.locationtech.jts.geom.Coordinate; - -/** - * A generic indexed grid of TZ samples. TZ could be anything but is usually a vector of parameters. - * - * We assume some sort of equirectangular project between the index coordinates (x,y) and the - * geographic coordinates (lat, lon). The projection factor (cos phi, standard parallel) is given as - * a cell size in lat,lon degrees (dLat,dLon)). The conversion is given by the following formulae: - * - * - * lon = lon0 + x.dLon; - * lat = lat0 + y.dLat; - * (lat0,lon0) is the center, (dLat,dLon) is the cell size. - * - * @author laurent - */ -public interface ZSampleGrid extends Iterable> { - /** - * @return The sample point located at (x,y). Create a new one if not existing. - */ - ZSamplePoint getOrCreate(int x, int y); - - /** - * @param point The sample point - * @return The (lat,lon) coordinates of this sample point. - */ - Coordinate getCoordinates(ZSamplePoint point); - - /** - * @param C The geographical coordinate - * @return The (x,y) index of the lower-left index of the cell enclosing the point. - */ - int[] getLowerLeftIndex(Coordinate C); - - /** - * @return The base coordinate center (lat0,lon0) - */ - Coordinate getCenter(); - - /** - * @return The cell size (dLat,dLon) - */ - public Coordinate getCellSize(); - - public int getXMin(); - - public int getXMax(); - - public int getYMin(); - - public int getYMax(); - - int size(); - - /** - * TODO The mapping between a ZSampleGrid and a DelaunayTriangulation should not be part of an - * interface but extracted to a converter. This assume that the conversion process does not rely - * on the inner working of the ZSampleGrid implementation, which should be the case. - * - * @return This ZSampleGrid converted as a DelaunayTriangulation. - */ - DelaunayTriangulation delaunayTriangulate(); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZSamplePoint.java b/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZSamplePoint.java deleted file mode 100644 index 3ac2732a8f1..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/geometry/ZSamplePoint.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.opentripplanner.ext.traveltime.geometry; - -public interface ZSamplePoint { - /** - * @return The X index of this sample point. - */ - int getX(); - - /** - * @return The Y index of this sample point. - */ - int getY(); - - /** - * @return The Z value associated with this sample point. - */ - TZ getZ(); - - void setZ(TZ z); - - /** - * @return The neighboring sample point located at (x,y-1) - */ - ZSamplePoint up(); - - /** - * @return The neighboring sample point located at (x,y+1) - */ - ZSamplePoint down(); - - /** - * @return The neighboring sample point located at (x+1,y) - */ - ZSamplePoint right(); - - /** - * @return The neighboring sample point located at (x-1,y) - */ - ZSamplePoint left(); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/spt/SPTVisitor.java b/src/ext/java/org/opentripplanner/ext/traveltime/spt/SPTVisitor.java deleted file mode 100644 index a47c6d7ebda..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/spt/SPTVisitor.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.opentripplanner.ext.traveltime.spt; - -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.search.state.State; - -public interface SPTVisitor { - /** - * @param e The edge to filter. - * @return True to visit this edge, false to skip it. - */ - boolean accept(Edge e); - - /** - * Note: The same state can be visited several times (from different edges). - * - * @param e The edge being visited (filtered from a previous call to accept) - * @param c The coordinate of the point alongside the edge geometry. - * @param s0 The state at the start vertex of this edge - * @param s1 The state at the end vertex of this edge - * @param d0 Curvilinear coordinate of c on [s0-s1], in meters - * @param d1 Curvilinear coordinate of c on [s1-s0], in meters - * @param speed The assumed speed on the edge - */ - void visit(Edge e, Coordinate c, State s0, State s1, double d0, double d1, double speed); -} diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/spt/SPTWalker.java b/src/ext/java/org/opentripplanner/ext/traveltime/spt/SPTWalker.java deleted file mode 100644 index 52da9bd9dd5..00000000000 --- a/src/ext/java/org/opentripplanner/ext/traveltime/spt/SPTWalker.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.opentripplanner.ext.traveltime.spt; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.LineString; -import org.opentripplanner.astar.model.ShortestPathTree; -import org.opentripplanner.framework.geometry.SphericalDistanceLibrary; -import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.edge.StreetEdge; -import org.opentripplanner.street.model.vertex.Vertex; -import org.opentripplanner.street.search.TraverseMode; -import org.opentripplanner.street.search.state.State; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Walk over a SPT tree to geometrically visit all nodes and edge geometry. For each geometry longer - * than the provided base length d0, split it in several steps of equal length and shorter than d0. - * For each walk step call the visitor callback. - * - * @author laurent - */ -public class SPTWalker { - - private static final Logger LOG = LoggerFactory.getLogger(SPTWalker.class); - - private final ShortestPathTree spt; - - public SPTWalker(ShortestPathTree spt) { - this.spt = spt; - } - - /** - * Walk over a SPT. Call a visitor for each visited point. - */ - public void walk(SPTVisitor visitor, double d0) { - int nTotal = 0, nSkippedDupEdge = 0, nSkippedNoGeometry = 0; - Collection allStates = spt.getAllStates(); - Set allVertices = new HashSet<>(spt.getVertexCount()); - for (State s : allStates) { - allVertices.add(s.getVertex()); - } - Set processedEdges = new HashSet<>(allVertices.size()); - for (Vertex v : allVertices) { - State s0 = spt.getState(v); - if (s0 == null || !s0.isFinal()) continue; - for (Edge e : s0.getVertex().getIncoming()) { - // Take only street - if (e != null && visitor.accept(e)) { - State s1 = spt.getState(e.getFromVertex()); - if (s1 == null || !s1.isFinal()) continue; - if (e.getFromVertex() != null && e.getToVertex() != null) { - // Hack alert: e.hashCode() throw NPE - if (processedEdges.contains(e)) { - nSkippedDupEdge++; - continue; - } - processedEdges.add(e); - } - Vertex vx0 = s0.getVertex(); - Vertex vx1 = s1.getVertex(); - LineString lineString = e.getGeometry(); - if (lineString == null) { - nSkippedNoGeometry++; - continue; - } - - // Compute speed along edge - double speedAlongEdge = s0.getPreferences().walk().speed(); - if (e instanceof StreetEdge se) { - /* - * Compute effective speed, taking into account end state mode (car, bike, - * walk...) and edge properties (car max speed, slope, etc...) - */ - TraverseMode mode = s0.currentMode(); - speedAlongEdge = se.calculateSpeed(s0.getPreferences(), mode, s0.isBackWalkingBike()); - if (mode != TraverseMode.CAR) { - speedAlongEdge *= se.getEffectiveBikeDistance() / se.getDistanceMeters(); - } - double avgSpeed = - se.getDistanceMeters() / Math.abs(s0.getTimeSeconds() - s1.getTimeSeconds()); - if (avgSpeed < 1e-10) { - avgSpeed = 1e-10; - } - /* - * We can't go faster than the average speed on the edge. We can go slower - * however, that simply means that one end vertice has a time higher than - * the other end vertice + time to traverse the edge (can happen due to - * max walk clamping). - */ - if (speedAlongEdge > avgSpeed) speedAlongEdge = avgSpeed; - } - - // Length of linestring - double lineStringLen = SphericalDistanceLibrary.fastLength(lineString); - visitor.visit(e, vx0.getCoordinate(), s0, s1, 0.0, lineStringLen, speedAlongEdge); - visitor.visit(e, vx1.getCoordinate(), s0, s1, lineStringLen, 0.0, speedAlongEdge); - nTotal += 2; - Coordinate[] pList = lineString.getCoordinates(); - boolean reverse = vx1.getCoordinate().equals(pList[0]); - // Split the linestring in nSteps - if (lineStringLen > d0) { - int nSteps = (int) Math.floor(lineStringLen / d0) + 1; // Number of steps - double stepLen = lineStringLen / nSteps; // Length of step - double startLen = 0; // Distance at start of current seg - double curLen = stepLen; // Distance cursor - int ns = 1; - for (int i = 0; i < pList.length - 1; i++) { - Coordinate p0 = pList[i]; - Coordinate p1 = pList[i + 1]; - double segLen = SphericalDistanceLibrary.fastDistance(p0, p1); - while (curLen - startLen < segLen) { - double k = (curLen - startLen) / segLen; - Coordinate p = new Coordinate(p0.x * (1 - k) + p1.x * k, p0.y * (1 - k) + p1.y * k); - visitor.visit( - e, - p, - reverse ? s1 : s0, - reverse ? s0 : s1, - curLen, - lineStringLen - curLen, - speedAlongEdge - ); - nTotal++; - curLen += stepLen; - ns++; - } - startLen += segLen; - if (ns >= nSteps) break; - } - } - } - } - } - LOG.info( - "SPTWalker: Generated {} points ({} dup edges, {} no geometry) from {} vertices / {} states.", - nTotal, - nSkippedDupEdge, - nSkippedNoGeometry, - allVertices.size(), - allStates.size() - ); - } -} diff --git a/src/main/java/org/opentripplanner/apis/APIEndpoints.java b/src/main/java/org/opentripplanner/apis/APIEndpoints.java index b6b70eb238e..fe8db5b3911 100644 --- a/src/main/java/org/opentripplanner/apis/APIEndpoints.java +++ b/src/main/java/org/opentripplanner/apis/APIEndpoints.java @@ -11,7 +11,6 @@ import static org.opentripplanner.framework.application.OTPFeature.SandboxAPIGeocoder; import static org.opentripplanner.framework.application.OTPFeature.SandboxAPIMapboxVectorTilesApi; import static org.opentripplanner.framework.application.OTPFeature.SandboxAPIParkAndRideApi; -import static org.opentripplanner.framework.application.OTPFeature.SandboxAPITravelTime; import static org.opentripplanner.framework.application.OTPFeature.TransmodelGraphQlApi; import java.util.ArrayList; @@ -32,7 +31,6 @@ import org.opentripplanner.ext.restapi.resources.IndexAPI; import org.opentripplanner.ext.restapi.resources.PlannerResource; import org.opentripplanner.ext.restapi.resources.Routers; -import org.opentripplanner.ext.traveltime.TravelTimeResource; import org.opentripplanner.ext.vectortiles.VectorTilesResource; import org.opentripplanner.framework.application.OTPFeature; @@ -65,7 +63,6 @@ private APIEndpoints() { addIfEnabled(SandboxAPIGeocoder, GeocoderResource.class); // scheduled to be removed and only here for backwards compatibility addIfEnabled(SandboxAPIGeocoder, GeocoderResource.GeocoderResourceOldPath.class); - addIfEnabled(SandboxAPITravelTime, TravelTimeResource.class); // scheduled to be removed addIfEnabled(APIBikeRental, BikeRental.class); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java index 5ab24c89543..212dbd1b150 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLScalars.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.language.FloatValue; +import graphql.language.IntValue; import graphql.language.StringValue; import graphql.relay.Relay; import graphql.schema.Coercing; @@ -13,18 +15,19 @@ import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Optional; import javax.annotation.Nonnull; import org.locationtech.jts.geom.Geometry; import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory; import org.opentripplanner.framework.json.ObjectMappers; +import org.opentripplanner.framework.model.Cost; import org.opentripplanner.framework.model.Grams; import org.opentripplanner.framework.time.OffsetDateTimeParser; public class GraphQLScalars { - private static final ObjectMapper geoJsonMapper = ObjectMappers.geoJson(); - - public static GraphQLScalarType DURATION_SCALAR = DurationScalarFactory.createDurationScalar(); + private static final ObjectMapper GEOJSON_MAPPER = ObjectMappers.geoJson(); + public static final GraphQLScalarType DURATION_SCALAR = DurationScalarFactory.createDurationScalar(); public static final GraphQLScalarType POLYLINE_SCALAR = GraphQLScalarType .newScalar() @@ -111,6 +114,127 @@ public OffsetDateTime parseLiteral(Object input) throws CoercingParseLiteralExce ) .build(); + public static final GraphQLScalarType COORDINATE_VALUE_SCALAR = GraphQLScalarType + .newScalar() + .name("CoordinateValue") + .coercing( + new Coercing() { + private static final String VALIDATION_ERROR_MESSAGE = "Not a valid WGS84 coordinate value"; + + @Override + public Double serialize(@Nonnull Object dataFetcherResult) + throws CoercingSerializeException { + if (dataFetcherResult instanceof Double doubleValue) { + return doubleValue; + } else if (dataFetcherResult instanceof Float floatValue) { + return floatValue.doubleValue(); + } else { + throw new CoercingSerializeException( + "Cannot serialize object of class %s as a coordinate number".formatted( + dataFetcherResult.getClass().getSimpleName() + ) + ); + } + } + + @Override + public Double parseValue(Object input) throws CoercingParseValueException { + if (input instanceof Double doubleValue) { + return validateCoordinate(doubleValue) + .orElseThrow(() -> new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)); + } + if (input instanceof Integer intValue) { + return validateCoordinate(intValue) + .orElseThrow(() -> new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseValueException( + "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input) + ); + } + + @Override + public Double parseLiteral(Object input) throws CoercingParseLiteralException { + if (input instanceof FloatValue coordinate) { + return validateCoordinate(coordinate.getValue().doubleValue()) + .orElseThrow(() -> new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)); + } + if (input instanceof IntValue coordinate) { + return validateCoordinate(coordinate.getValue().doubleValue()) + .orElseThrow(() -> new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseLiteralException( + "Expected a number, got: " + input.getClass().getSimpleName() + ); + } + + private static Optional validateCoordinate(double coordinate) { + if (coordinate >= -180.001 && coordinate <= 180.001) { + return Optional.of(coordinate); + } + return Optional.empty(); + } + } + ) + .build(); + + public static final GraphQLScalarType COST_SCALAR = GraphQLScalarType + .newScalar() + .name("Cost") + .coercing( + new Coercing() { + private static final int MAX_COST = 1000000; + private static final String VALIDATION_ERROR_MESSAGE = + "Cost cannot be negative or greater than %d".formatted(MAX_COST); + + @Override + public Integer serialize(@Nonnull Object dataFetcherResult) + throws CoercingSerializeException { + if (dataFetcherResult instanceof Integer intValue) { + return intValue; + } else if (dataFetcherResult instanceof Cost costValue) { + return costValue.toSeconds(); + } else { + throw new CoercingSerializeException( + "Cannot serialize object of class %s as a cost".formatted( + dataFetcherResult.getClass().getSimpleName() + ) + ); + } + } + + @Override + public Cost parseValue(Object input) throws CoercingParseValueException { + if (input instanceof Integer intValue) { + return validateCost(intValue) + .orElseThrow(() -> new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseValueException( + "Expected an integer, got %s %s".formatted(input.getClass().getSimpleName(), input) + ); + } + + @Override + public Cost parseLiteral(Object input) throws CoercingParseLiteralException { + if (input instanceof IntValue intValue) { + var value = intValue.getValue().intValue(); + return validateCost(value) + .orElseThrow(() -> new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseLiteralException( + "Expected an integer, got: " + input.getClass().getSimpleName() + ); + } + + private static Optional validateCost(int cost) { + if (cost >= 0 && cost <= MAX_COST) { + return Optional.of(Cost.costOfSeconds(cost)); + } + return Optional.empty(); + } + } + ) + .build(); + public static final GraphQLScalarType GEOJSON_SCALAR = GraphQLScalarType .newScalar() .name("GeoJson") @@ -121,7 +245,7 @@ public OffsetDateTime parseLiteral(Object input) throws CoercingParseLiteralExce public JsonNode serialize(Object dataFetcherResult) throws CoercingSerializeException { if (dataFetcherResult instanceof Geometry) { var geom = (Geometry) dataFetcherResult; - return geoJsonMapper.valueToTree(geom); + return GEOJSON_MAPPER.valueToTree(geom); } return null; } @@ -195,20 +319,159 @@ public Double serialize(Object dataFetcherResult) throws CoercingSerializeExcept @Override public Grams parseValue(Object input) throws CoercingParseValueException { - if (input instanceof Double) { - var grams = (Double) input; - return new Grams(grams); + if (input instanceof Double doubleValue) { + return new Grams(doubleValue); } - return null; + if (input instanceof Integer intValue) { + return new Grams(intValue); + } + throw new CoercingParseValueException( + "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input) + ); } @Override public Grams parseLiteral(Object input) throws CoercingParseLiteralException { - if (input instanceof Double) { - var grams = (Double) input; - return new Grams(grams); + if (input instanceof FloatValue coordinate) { + return new Grams(coordinate.getValue().doubleValue()); } - return null; + if (input instanceof IntValue coordinate) { + return new Grams(coordinate.getValue().doubleValue()); + } + throw new CoercingParseLiteralException( + "Expected a number, got: " + input.getClass().getSimpleName() + ); + } + } + ) + .build(); + + public static final GraphQLScalarType RATIO_SCALAR = GraphQLScalarType + .newScalar() + .name("Ratio") + .coercing( + new Coercing() { + private static final String VALIDATION_ERROR_MESSAGE = + "Value is under 0 or greater than 1."; + + @Override + public Double serialize(@Nonnull Object dataFetcherResult) + throws CoercingSerializeException { + var validationException = new CoercingSerializeException(VALIDATION_ERROR_MESSAGE); + if (dataFetcherResult instanceof Double doubleValue) { + return validateRatio(doubleValue).orElseThrow(() -> validationException); + } else if (dataFetcherResult instanceof Float floatValue) { + return validateRatio(floatValue.doubleValue()).orElseThrow(() -> validationException); + } else { + throw new CoercingSerializeException( + "Cannot serialize object of class %s as a ratio".formatted( + dataFetcherResult.getClass().getSimpleName() + ) + ); + } + } + + @Override + public Double parseValue(Object input) throws CoercingParseValueException { + if (input instanceof Double doubleValue) { + return validateRatio(doubleValue) + .orElseThrow(() -> new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)); + } + if (input instanceof Integer intValue) { + return validateRatio(intValue) + .orElseThrow(() -> new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseValueException( + "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input) + ); + } + + @Override + public Double parseLiteral(Object input) throws CoercingParseLiteralException { + if (input instanceof FloatValue ratio) { + return validateRatio(ratio.getValue().doubleValue()) + .orElseThrow(() -> new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)); + } + if (input instanceof IntValue ratio) { + return validateRatio(ratio.getValue().doubleValue()) + .orElseThrow(() -> new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseLiteralException( + "Expected a number, got: " + input.getClass().getSimpleName() + ); + } + + private static Optional validateRatio(double ratio) { + if (ratio >= -0.001 && ratio <= 1.001) { + return Optional.of(ratio); + } + return Optional.empty(); + } + } + ) + .build(); + + public static final GraphQLScalarType RELUCTANCE_SCALAR = GraphQLScalarType + .newScalar() + .name("Reluctance") + .coercing( + new Coercing() { + private static final double MIN_Reluctance = 0.1; + private static final double MAX_Reluctance = 100000; + private static final String VALIDATION_ERROR_MESSAGE = + "Reluctance needs to be between %s and %s".formatted(MIN_Reluctance, MAX_Reluctance); + + @Override + public Double serialize(@Nonnull Object dataFetcherResult) + throws CoercingSerializeException { + if (dataFetcherResult instanceof Double doubleValue) { + return doubleValue; + } else if (dataFetcherResult instanceof Float floatValue) { + return floatValue.doubleValue(); + } else { + throw new CoercingSerializeException( + "Cannot serialize object of class %s as a reluctance".formatted( + dataFetcherResult.getClass().getSimpleName() + ) + ); + } + } + + @Override + public Double parseValue(Object input) throws CoercingParseValueException { + if (input instanceof Double doubleValue) { + return validateReluctance(doubleValue) + .orElseThrow(() -> new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)); + } + if (input instanceof Integer intValue) { + return validateReluctance(intValue) + .orElseThrow(() -> new CoercingParseValueException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseValueException( + "Expected a number, got %s %s".formatted(input.getClass().getSimpleName(), input) + ); + } + + @Override + public Double parseLiteral(Object input) throws CoercingParseLiteralException { + if (input instanceof FloatValue reluctance) { + return validateReluctance(reluctance.getValue().doubleValue()) + .orElseThrow(() -> new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)); + } + if (input instanceof IntValue reluctance) { + return validateReluctance(reluctance.getValue().doubleValue()) + .orElseThrow(() -> new CoercingParseLiteralException(VALIDATION_ERROR_MESSAGE)); + } + throw new CoercingParseLiteralException( + "Expected a number, got: " + input.getClass().getSimpleName() + ); + } + + private static Optional validateReluctance(double reluctance) { + if (reluctance >= MIN_Reluctance - 0.001 && reluctance <= MAX_Reluctance + 0.001) { + return Optional.of(reluctance); + } + return Optional.empty(); } } ) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index 53d53b9bc4b..9175d3486e1 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -51,6 +51,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.PatternImpl; import org.opentripplanner.apis.gtfs.datafetchers.PlaceImpl; import org.opentripplanner.apis.gtfs.datafetchers.PlaceInterfaceTypeResolver; +import org.opentripplanner.apis.gtfs.datafetchers.PlanConnectionImpl; import org.opentripplanner.apis.gtfs.datafetchers.PlanImpl; import org.opentripplanner.apis.gtfs.datafetchers.QueryTypeImpl; import org.opentripplanner.apis.gtfs.datafetchers.RentalVehicleImpl; @@ -112,7 +113,18 @@ protected static GraphQLSchema buildSchema() { .scalar(GraphQLScalars.GRAPHQL_ID_SCALAR) .scalar(GraphQLScalars.GRAMS_SCALAR) .scalar(GraphQLScalars.OFFSET_DATETIME_SCALAR) + .scalar(GraphQLScalars.RATIO_SCALAR) + .scalar(GraphQLScalars.COORDINATE_VALUE_SCALAR) + .scalar(GraphQLScalars.COST_SCALAR) + .scalar(GraphQLScalars.RELUCTANCE_SCALAR) .scalar(ExtendedScalars.GraphQLLong) + .scalar(ExtendedScalars.Locale) + .scalar( + ExtendedScalars + .newAliasedScalar("Speed") + .aliasedScalar(ExtendedScalars.NonNegativeFloat) + .build() + ) .type("Node", type -> type.typeResolver(new NodeTypeResolver())) .type("PlaceInterface", type -> type.typeResolver(new PlaceInterfaceTypeResolver())) .type("StopPosition", type -> type.typeResolver(new StopPosition() {})) @@ -135,6 +147,7 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(PatternImpl.class)) .type(typeWiring.build(PlaceImpl.class)) .type(typeWiring.build(placeAtDistanceImpl.class)) + .type(typeWiring.build(PlanConnectionImpl.class)) .type(typeWiring.build(PlanImpl.class)) .type(typeWiring.build(QueryTypeImpl.class)) .type(typeWiring.build(RouteImpl.class)) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlanConnectionImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlanConnectionImpl.java new file mode 100644 index 00000000000..96a8557683b --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PlanConnectionImpl.java @@ -0,0 +1,83 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.relay.ConnectionCursor; +import graphql.relay.DefaultConnectionCursor; +import graphql.relay.DefaultEdge; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.time.Duration; +import java.time.OffsetDateTime; +import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.apis.gtfs.model.PlanPageInfo; +import org.opentripplanner.model.plan.Itinerary; +import org.opentripplanner.routing.api.response.RoutingError; +import org.opentripplanner.routing.api.response.RoutingResponse; +import org.opentripplanner.transit.service.TransitService; + +public class PlanConnectionImpl implements GraphQLDataFetchers.GraphQLPlanConnection { + + @Override + public DataFetcher searchDateTime() { + return environment -> { + var transitService = getTransitService(environment); + var instant = getSource(environment).getTripPlan().date; + return instant.atOffset(transitService.getTimeZone().getRules().getOffset(instant)); + }; + } + + @Override + public DataFetcher>> edges() { + return environment -> + getSource(environment) + .getTripPlan() + .itineraries.stream() + .map(itinerary -> new DefaultEdge<>(itinerary, new DefaultConnectionCursor("NoCursor"))) + .toList(); + } + + @Override + public DataFetcher> routingErrors() { + return environment -> getSource(environment).getRoutingErrors(); + } + + @Override + public DataFetcher pageInfo() { + return environment -> { + var startCursor = getSource(environment).getNextPageCursor() != null + ? getSource(environment).getPreviousPageCursor().encode() + : null; + ConnectionCursor startConnectionCursor = null; + if (startCursor != null) { + startConnectionCursor = new DefaultConnectionCursor(startCursor); + } + var endCursor = getSource(environment).getPreviousPageCursor() != null + ? getSource(environment).getNextPageCursor().encode() + : null; + ConnectionCursor endConnectionCursor = null; + if (endCursor != null) { + endConnectionCursor = new DefaultConnectionCursor(endCursor); + } + Duration searchWindowUsed = null; + var metadata = getSource(environment).getMetadata(); + if (metadata != null) { + searchWindowUsed = metadata.searchWindowUsed; + } + return new PlanPageInfo( + startConnectionCursor, + endConnectionCursor, + startCursor != null, + endCursor != null, + searchWindowUsed + ); + }; + } + + private TransitService getTransitService(DataFetchingEnvironment environment) { + return environment.getContext().transitService(); + } + + private RoutingResponse getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java index a4f5cb00e2f..eae4bc2ad33 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/QueryTypeImpl.java @@ -30,7 +30,8 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLQueryTypeStopsByRadiusArgs; -import org.opentripplanner.apis.gtfs.mapping.RouteRequestMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.LegacyRouteRequestMapper; +import org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapper; import org.opentripplanner.ext.fares.impl.DefaultFareService; import org.opentripplanner.ext.fares.impl.GtfsFaresService; import org.opentripplanner.ext.fares.model.FareRuleSet; @@ -478,15 +479,19 @@ public DataFetcher> patterns() { @Override public DataFetcher> plan() { + return environment -> { + GraphQLRequestContext context = environment.getContext(); + RouteRequest request = LegacyRouteRequestMapper.toRouteRequest(environment, context); + return getPlanResult(context, request); + }; + } + + @Override + public DataFetcher> planConnection() { return environment -> { GraphQLRequestContext context = environment.getContext(); RouteRequest request = RouteRequestMapper.toRouteRequest(environment, context); - RoutingResponse res = context.routingService().route(request); - return DataFetcherResult - .newResult() - .data(res) - .localContext(Map.of("locale", request.locale())) - .build(); + return getPlanResult(context, request); }; } @@ -933,6 +938,15 @@ private GraphFinder getGraphFinder(DataFetchingEnvironment environment) { return environment.getContext().graphFinder(); } + private DataFetcherResult getPlanResult(GraphQLRequestContext context, RouteRequest request) { + RoutingResponse res = context.routingService().route(request); + return DataFetcherResult + .newResult() + .data(res) + .localContext(Map.of("locale", request.locale())) + .build(); + } + protected static List filterAlerts( Collection alerts, GraphQLTypes.GraphQLQueryTypeAlertsArgs args diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index 103534689a9..6c66d3992f4 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -2,6 +2,7 @@ package org.opentripplanner.apis.gtfs.generated; import graphql.relay.Connection; +import graphql.relay.DefaultEdge; import graphql.relay.Edge; import graphql.schema.DataFetcher; import graphql.schema.TypeResolver; @@ -21,6 +22,7 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRoutingErrorCode; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; import org.opentripplanner.apis.gtfs.model.FeedPublisher; +import org.opentripplanner.apis.gtfs.model.PlanPageInfo; import org.opentripplanner.apis.gtfs.model.RideHailingProvider; import org.opentripplanner.apis.gtfs.model.StopPosition; import org.opentripplanner.apis.gtfs.model.TripOccupancy; @@ -288,6 +290,16 @@ public interface GraphQLContactInfo { public DataFetcher phoneNumber(); } + /** + * Coordinate (often referred as coordinates), which is used to specify a location using in the + * WGS84 coordinate system. + */ + public interface GraphQLCoordinate { + public DataFetcher latitude(); + + public DataFetcher longitude(); + } + public interface GraphQLCoordinates { public DataFetcher lat(); @@ -683,6 +695,46 @@ public interface GraphQLPlan { public DataFetcher to(); } + /** + * Plan (result of an itinerary search) that follows + * [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + */ + public interface GraphQLPlanConnection { + public DataFetcher>> edges(); + + public DataFetcher pageInfo(); + + public DataFetcher> routingErrors(); + + public DataFetcher searchDateTime(); + } + + /** + * Edge outputted by a plan search. Part of the + * [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + */ + public interface GraphQLPlanEdge { + public DataFetcher cursor(); + + public DataFetcher node(); + } + + /** + * Information about pagination in a connection. Part of the + * [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + */ + public interface GraphQLPlanPageInfo { + public DataFetcher endCursor(); + + public DataFetcher hasNextPage(); + + public DataFetcher hasPreviousPage(); + + public DataFetcher searchWindowUsed(); + + public DataFetcher startCursor(); + } + /** Stop position at a specific stop. */ public interface GraphQLPositionAtStop { public DataFetcher position(); @@ -736,6 +788,8 @@ public interface GraphQLQueryType { public DataFetcher> plan(); + public DataFetcher> planConnection(); + public DataFetcher rentalVehicle(); public DataFetcher> rentalVehicles(); @@ -1191,7 +1245,7 @@ public interface GraphQLVehicleParkingSpaces { public DataFetcher wheelchairAccessibleCarSpaces(); } - /** Realtime vehicle position */ + /** Real-time vehicle position */ public interface GraphQLVehiclePosition { public DataFetcher heading(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index ecc038fec18..3c187ca3bbe 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -19,6 +19,26 @@ public enum GraphQLAbsoluteDirection { WEST, } + public static class GraphQLAccessibilityPreferencesInput { + + private GraphQLWheelchairPreferencesInput wheelchair; + + public GraphQLAccessibilityPreferencesInput(Map args) { + if (args != null) { + this.wheelchair = + new GraphQLWheelchairPreferencesInput((Map) args.get("wheelchair")); + } + } + + public GraphQLWheelchairPreferencesInput getGraphQLWheelchair() { + return this.wheelchair; + } + + public void setGraphQLWheelchair(GraphQLWheelchairPreferencesInput wheelchair) { + this.wheelchair = wheelchair; + } + } + public static class GraphQLAgencyAlertsArgs { private List types; @@ -94,826 +114,1856 @@ public enum GraphQLAlertSeverityLevelType { WARNING, } - public static class GraphQLBikeParkNameArgs { + public static class GraphQLAlightPreferencesInput { - private String language; + private java.time.Duration slack; - public GraphQLBikeParkNameArgs(Map args) { + public GraphQLAlightPreferencesInput(Map args) { if (args != null) { - this.language = (String) args.get("language"); + this.slack = (java.time.Duration) args.get("slack"); } } - public String getGraphQLLanguage() { - return this.language; + public java.time.Duration getGraphQLSlack() { + return this.slack; } - public void setGraphQLLanguage(String language) { - this.language = language; + public void setGraphQLSlack(java.time.Duration slack) { + this.slack = slack; } } - public enum GraphQLBikesAllowed { - ALLOWED, - NOT_ALLOWED, - NO_INFORMATION, - } - - public static class GraphQLCarParkNameArgs { + public static class GraphQLBicycleParkingPreferencesInput { - private String language; + private List filters; + private List preferred; + private org.opentripplanner.framework.model.Cost unpreferredCost; - public GraphQLCarParkNameArgs(Map args) { + public GraphQLBicycleParkingPreferencesInput(Map args) { if (args != null) { - this.language = (String) args.get("language"); + if (args.get("filters") != null) { + this.filters = (List) args.get("filters"); + } + if (args.get("preferred") != null) { + this.preferred = (List) args.get("preferred"); + } + this.unpreferredCost = + (org.opentripplanner.framework.model.Cost) args.get("unpreferredCost"); } } - public String getGraphQLLanguage() { - return this.language; + public List getGraphQLFilters() { + return this.filters; } - public void setGraphQLLanguage(String language) { - this.language = language; + public List getGraphQLPreferred() { + return this.preferred; + } + + public org.opentripplanner.framework.model.Cost getGraphQLUnpreferredCost() { + return this.unpreferredCost; + } + + public void setGraphQLFilters(List filters) { + this.filters = filters; + } + + public void setGraphQLPreferred(List preferred) { + this.preferred = preferred; + } + + public void setGraphQLUnpreferredCost( + org.opentripplanner.framework.model.Cost unpreferredCost + ) { + this.unpreferredCost = unpreferredCost; } } - public static class GraphQLDepartureRowStoptimesArgs { + public static class GraphQLBicyclePreferencesInput { - private Integer numberOfDepartures; - private Boolean omitCanceled; - private Boolean omitNonPickups; - private Long startTime; - private Integer timeRange; + private org.opentripplanner.framework.model.Cost boardCost; + private GraphQLCyclingOptimizationInput optimization; + private GraphQLBicycleParkingPreferencesInput parking; + private Double reluctance; + private GraphQLBicycleRentalPreferencesInput rental; + private Double speed; + private GraphQLBicycleWalkPreferencesInput walk; - public GraphQLDepartureRowStoptimesArgs(Map args) { + public GraphQLBicyclePreferencesInput(Map args) { if (args != null) { - this.numberOfDepartures = (Integer) args.get("numberOfDepartures"); - this.omitCanceled = (Boolean) args.get("omitCanceled"); - this.omitNonPickups = (Boolean) args.get("omitNonPickups"); - this.startTime = (Long) args.get("startTime"); - this.timeRange = (Integer) args.get("timeRange"); + this.boardCost = (org.opentripplanner.framework.model.Cost) args.get("boardCost"); + this.optimization = + new GraphQLCyclingOptimizationInput((Map) args.get("optimization")); + this.parking = + new GraphQLBicycleParkingPreferencesInput((Map) args.get("parking")); + this.reluctance = (Double) args.get("reluctance"); + this.rental = + new GraphQLBicycleRentalPreferencesInput((Map) args.get("rental")); + this.speed = (Double) args.get("speed"); + this.walk = new GraphQLBicycleWalkPreferencesInput((Map) args.get("walk")); } } - public Integer getGraphQLNumberOfDepartures() { - return this.numberOfDepartures; + public org.opentripplanner.framework.model.Cost getGraphQLBoardCost() { + return this.boardCost; } - public Boolean getGraphQLOmitCanceled() { - return this.omitCanceled; + public GraphQLCyclingOptimizationInput getGraphQLOptimization() { + return this.optimization; } - public Boolean getGraphQLOmitNonPickups() { - return this.omitNonPickups; + public GraphQLBicycleParkingPreferencesInput getGraphQLParking() { + return this.parking; } - public Long getGraphQLStartTime() { - return this.startTime; + public Double getGraphQLReluctance() { + return this.reluctance; } - public Integer getGraphQLTimeRange() { - return this.timeRange; + public GraphQLBicycleRentalPreferencesInput getGraphQLRental() { + return this.rental; } - public void setGraphQLNumberOfDepartures(Integer numberOfDepartures) { - this.numberOfDepartures = numberOfDepartures; + public Double getGraphQLSpeed() { + return this.speed; } - public void setGraphQLOmitCanceled(Boolean omitCanceled) { - this.omitCanceled = omitCanceled; + public GraphQLBicycleWalkPreferencesInput getGraphQLWalk() { + return this.walk; } - public void setGraphQLOmitNonPickups(Boolean omitNonPickups) { - this.omitNonPickups = omitNonPickups; + public void setGraphQLBoardCost(org.opentripplanner.framework.model.Cost boardCost) { + this.boardCost = boardCost; } - public void setGraphQLStartTime(Long startTime) { - this.startTime = startTime; + public void setGraphQLOptimization(GraphQLCyclingOptimizationInput optimization) { + this.optimization = optimization; } - public void setGraphQLTimeRange(Integer timeRange) { - this.timeRange = timeRange; + public void setGraphQLParking(GraphQLBicycleParkingPreferencesInput parking) { + this.parking = parking; } - } - - public static class GraphQLFeedAlertsArgs { - - private List types; - public GraphQLFeedAlertsArgs(Map args) { - if (args != null) { - if (args.get("types") != null) { - this.types = - ((List) args.get("types")).stream() - .map(item -> - item instanceof GraphQLFeedAlertType - ? item - : GraphQLFeedAlertType.valueOf((String) item) - ) - .map(GraphQLFeedAlertType.class::cast) - .collect(Collectors.toList()); - } - } + public void setGraphQLReluctance(Double reluctance) { + this.reluctance = reluctance; } - public List getGraphQLTypes() { - return this.types; + public void setGraphQLRental(GraphQLBicycleRentalPreferencesInput rental) { + this.rental = rental; } - public void setGraphQLTypes(List types) { - this.types = types; + public void setGraphQLSpeed(Double speed) { + this.speed = speed; } - } - - /** Entities, which are relevant for a feed and can contain alerts */ - public enum GraphQLFeedAlertType { - AGENCIES, - ROUTE_TYPES, - } - - public enum GraphQLFilterPlaceType { - BICYCLE_RENT, - BIKE_PARK, - CAR_PARK, - DEPARTURE_ROW, - STATION, - STOP, - VEHICLE_RENT, - } - public enum GraphQLFormFactor { - BICYCLE, - CAR, - CARGO_BICYCLE, - MOPED, - OTHER, - SCOOTER, - SCOOTER_SEATED, - SCOOTER_STANDING, + public void setGraphQLWalk(GraphQLBicycleWalkPreferencesInput walk) { + this.walk = walk; + } } - public static class GraphQLInputBannedInput { + public static class GraphQLBicycleRentalPreferencesInput { - private String agencies; - private String routes; - private String stops; - private String stopsHard; - private String trips; + private List allowedNetworks; + private List bannedNetworks; + private GraphQLDestinationBicyclePolicyInput destinationBicyclePolicy; - public GraphQLInputBannedInput(Map args) { + public GraphQLBicycleRentalPreferencesInput(Map args) { if (args != null) { - this.agencies = (String) args.get("agencies"); - this.routes = (String) args.get("routes"); - this.stops = (String) args.get("stops"); - this.stopsHard = (String) args.get("stopsHard"); - this.trips = (String) args.get("trips"); + this.allowedNetworks = (List) args.get("allowedNetworks"); + this.bannedNetworks = (List) args.get("bannedNetworks"); + this.destinationBicyclePolicy = + new GraphQLDestinationBicyclePolicyInput( + (Map) args.get("destinationBicyclePolicy") + ); } } - public String getGraphQLAgencies() { - return this.agencies; + public List getGraphQLAllowedNetworks() { + return this.allowedNetworks; } - public String getGraphQLRoutes() { - return this.routes; + public List getGraphQLBannedNetworks() { + return this.bannedNetworks; } - public String getGraphQLStops() { - return this.stops; + public GraphQLDestinationBicyclePolicyInput getGraphQLDestinationBicyclePolicy() { + return this.destinationBicyclePolicy; } - public String getGraphQLStopsHard() { - return this.stopsHard; + public void setGraphQLAllowedNetworks(List allowedNetworks) { + this.allowedNetworks = allowedNetworks; } - public String getGraphQLTrips() { - return this.trips; + public void setGraphQLBannedNetworks(List bannedNetworks) { + this.bannedNetworks = bannedNetworks; } - public void setGraphQLAgencies(String agencies) { - this.agencies = agencies; + public void setGraphQLDestinationBicyclePolicy( + GraphQLDestinationBicyclePolicyInput destinationBicyclePolicy + ) { + this.destinationBicyclePolicy = destinationBicyclePolicy; } + } - public void setGraphQLRoutes(String routes) { - this.routes = routes; + public static class GraphQLBicycleWalkPreferencesCostInput { + + private org.opentripplanner.framework.model.Cost mountDismountCost; + private Double reluctance; + + public GraphQLBicycleWalkPreferencesCostInput(Map args) { + if (args != null) { + this.mountDismountCost = + (org.opentripplanner.framework.model.Cost) args.get("mountDismountCost"); + this.reluctance = (Double) args.get("reluctance"); + } } - public void setGraphQLStops(String stops) { - this.stops = stops; + public org.opentripplanner.framework.model.Cost getGraphQLMountDismountCost() { + return this.mountDismountCost; } - public void setGraphQLStopsHard(String stopsHard) { - this.stopsHard = stopsHard; + public Double getGraphQLReluctance() { + return this.reluctance; } - public void setGraphQLTrips(String trips) { - this.trips = trips; + public void setGraphQLMountDismountCost( + org.opentripplanner.framework.model.Cost mountDismountCost + ) { + this.mountDismountCost = mountDismountCost; + } + + public void setGraphQLReluctance(Double reluctance) { + this.reluctance = reluctance; } } - public static class GraphQLInputCoordinatesInput { + public static class GraphQLBicycleWalkPreferencesInput { - private String address; - private Double lat; - private Integer locationSlack; - private Double lon; + private GraphQLBicycleWalkPreferencesCostInput cost; + private java.time.Duration mountDismountTime; + private Double speed; - public GraphQLInputCoordinatesInput(Map args) { + public GraphQLBicycleWalkPreferencesInput(Map args) { if (args != null) { - this.address = (String) args.get("address"); - this.lat = (Double) args.get("lat"); - this.locationSlack = (Integer) args.get("locationSlack"); - this.lon = (Double) args.get("lon"); + this.cost = + new GraphQLBicycleWalkPreferencesCostInput((Map) args.get("cost")); + this.mountDismountTime = (java.time.Duration) args.get("mountDismountTime"); + this.speed = (Double) args.get("speed"); } } - public String getGraphQLAddress() { - return this.address; + public GraphQLBicycleWalkPreferencesCostInput getGraphQLCost() { + return this.cost; } - public Double getGraphQLLat() { - return this.lat; + public java.time.Duration getGraphQLMountDismountTime() { + return this.mountDismountTime; } - public Integer getGraphQLLocationSlack() { - return this.locationSlack; + public Double getGraphQLSpeed() { + return this.speed; } - public Double getGraphQLLon() { - return this.lon; + public void setGraphQLCost(GraphQLBicycleWalkPreferencesCostInput cost) { + this.cost = cost; } - public void setGraphQLAddress(String address) { - this.address = address; + public void setGraphQLMountDismountTime(java.time.Duration mountDismountTime) { + this.mountDismountTime = mountDismountTime; } - public void setGraphQLLat(Double lat) { - this.lat = lat; + public void setGraphQLSpeed(Double speed) { + this.speed = speed; } + } - public void setGraphQLLocationSlack(Integer locationSlack) { - this.locationSlack = locationSlack; + public static class GraphQLBikeParkNameArgs { + + private String language; + + public GraphQLBikeParkNameArgs(Map args) { + if (args != null) { + this.language = (String) args.get("language"); + } } - public void setGraphQLLon(Double lon) { - this.lon = lon; + public String getGraphQLLanguage() { + return this.language; + } + + public void setGraphQLLanguage(String language) { + this.language = language; } } - public enum GraphQLInputField { - DATE_TIME, - FROM, - TO, + public enum GraphQLBikesAllowed { + ALLOWED, + NOT_ALLOWED, + NO_INFORMATION, } - public static class GraphQLInputFiltersInput { + public static class GraphQLBoardPreferencesInput { - private List bikeParks; - private List bikeRentalStations; - private List carParks; - private List routes; - private List stations; - private List stops; + private java.time.Duration slack; + private Double waitReluctance; - public GraphQLInputFiltersInput(Map args) { + public GraphQLBoardPreferencesInput(Map args) { if (args != null) { - this.bikeParks = (List) args.get("bikeParks"); - this.bikeRentalStations = (List) args.get("bikeRentalStations"); - this.carParks = (List) args.get("carParks"); - this.routes = (List) args.get("routes"); - this.stations = (List) args.get("stations"); - this.stops = (List) args.get("stops"); + this.slack = (java.time.Duration) args.get("slack"); + this.waitReluctance = (Double) args.get("waitReluctance"); } } - public List getGraphQLBikeParks() { - return this.bikeParks; + public java.time.Duration getGraphQLSlack() { + return this.slack; } - public List getGraphQLBikeRentalStations() { - return this.bikeRentalStations; - } - - public List getGraphQLCarParks() { - return this.carParks; - } - - public List getGraphQLRoutes() { - return this.routes; - } - - public List getGraphQLStations() { - return this.stations; + public Double getGraphQLWaitReluctance() { + return this.waitReluctance; } - public List getGraphQLStops() { - return this.stops; + public void setGraphQLSlack(java.time.Duration slack) { + this.slack = slack; } - public void setGraphQLBikeParks(List bikeParks) { - this.bikeParks = bikeParks; + public void setGraphQLWaitReluctance(Double waitReluctance) { + this.waitReluctance = waitReluctance; } + } - public void setGraphQLBikeRentalStations(List bikeRentalStations) { - this.bikeRentalStations = bikeRentalStations; - } + public static class GraphQLCarParkNameArgs { - public void setGraphQLCarParks(List carParks) { - this.carParks = carParks; - } + private String language; - public void setGraphQLRoutes(List routes) { - this.routes = routes; + public GraphQLCarParkNameArgs(Map args) { + if (args != null) { + this.language = (String) args.get("language"); + } } - public void setGraphQLStations(List stations) { - this.stations = stations; + public String getGraphQLLanguage() { + return this.language; } - public void setGraphQLStops(List stops) { - this.stops = stops; + public void setGraphQLLanguage(String language) { + this.language = language; } } - public static class GraphQLInputModeWeightInput { + public static class GraphQLCarParkingPreferencesInput { - private Double AIRPLANE; - private Double BUS; - private Double CABLE_CAR; - private Double FERRY; - private Double FUNICULAR; - private Double GONDOLA; - private Double RAIL; - private Double SUBWAY; - private Double TRAM; + private List filters; + private List preferred; + private org.opentripplanner.framework.model.Cost unpreferredCost; - public GraphQLInputModeWeightInput(Map args) { + public GraphQLCarParkingPreferencesInput(Map args) { if (args != null) { - this.AIRPLANE = (Double) args.get("AIRPLANE"); - this.BUS = (Double) args.get("BUS"); - this.CABLE_CAR = (Double) args.get("CABLE_CAR"); - this.FERRY = (Double) args.get("FERRY"); - this.FUNICULAR = (Double) args.get("FUNICULAR"); - this.GONDOLA = (Double) args.get("GONDOLA"); - this.RAIL = (Double) args.get("RAIL"); - this.SUBWAY = (Double) args.get("SUBWAY"); - this.TRAM = (Double) args.get("TRAM"); + if (args.get("filters") != null) { + this.filters = (List) args.get("filters"); + } + if (args.get("preferred") != null) { + this.preferred = (List) args.get("preferred"); + } + this.unpreferredCost = + (org.opentripplanner.framework.model.Cost) args.get("unpreferredCost"); } } - public Double getGraphQLAirplane() { - return this.AIRPLANE; + public List getGraphQLFilters() { + return this.filters; } - public Double getGraphQLBus() { - return this.BUS; + public List getGraphQLPreferred() { + return this.preferred; } - public Double getGraphQLCable_Car() { - return this.CABLE_CAR; + public org.opentripplanner.framework.model.Cost getGraphQLUnpreferredCost() { + return this.unpreferredCost; } - public Double getGraphQLFerry() { - return this.FERRY; + public void setGraphQLFilters(List filters) { + this.filters = filters; } - public Double getGraphQLFunicular() { - return this.FUNICULAR; + public void setGraphQLPreferred(List preferred) { + this.preferred = preferred; } - public Double getGraphQLGondola() { - return this.GONDOLA; + public void setGraphQLUnpreferredCost( + org.opentripplanner.framework.model.Cost unpreferredCost + ) { + this.unpreferredCost = unpreferredCost; } + } - public Double getGraphQLRail() { - return this.RAIL; + public static class GraphQLCarPreferencesInput { + + private GraphQLCarParkingPreferencesInput parking; + private Double reluctance; + private GraphQLCarRentalPreferencesInput rental; + + public GraphQLCarPreferencesInput(Map args) { + if (args != null) { + this.parking = + new GraphQLCarParkingPreferencesInput((Map) args.get("parking")); + this.reluctance = (Double) args.get("reluctance"); + this.rental = + new GraphQLCarRentalPreferencesInput((Map) args.get("rental")); + } } - public Double getGraphQLSubway() { - return this.SUBWAY; + public GraphQLCarParkingPreferencesInput getGraphQLParking() { + return this.parking; } - public Double getGraphQLTram() { - return this.TRAM; + public Double getGraphQLReluctance() { + return this.reluctance; } - public void setGraphQLAirplane(Double AIRPLANE) { - this.AIRPLANE = AIRPLANE; + public GraphQLCarRentalPreferencesInput getGraphQLRental() { + return this.rental; } - public void setGraphQLBus(Double BUS) { - this.BUS = BUS; + public void setGraphQLParking(GraphQLCarParkingPreferencesInput parking) { + this.parking = parking; } - public void setGraphQLCable_Car(Double CABLE_CAR) { - this.CABLE_CAR = CABLE_CAR; + public void setGraphQLReluctance(Double reluctance) { + this.reluctance = reluctance; } - public void setGraphQLFerry(Double FERRY) { - this.FERRY = FERRY; + public void setGraphQLRental(GraphQLCarRentalPreferencesInput rental) { + this.rental = rental; } + } - public void setGraphQLFunicular(Double FUNICULAR) { - this.FUNICULAR = FUNICULAR; + public static class GraphQLCarRentalPreferencesInput { + + private List allowedNetworks; + private List bannedNetworks; + + public GraphQLCarRentalPreferencesInput(Map args) { + if (args != null) { + this.allowedNetworks = (List) args.get("allowedNetworks"); + this.bannedNetworks = (List) args.get("bannedNetworks"); + } } - public void setGraphQLGondola(Double GONDOLA) { - this.GONDOLA = GONDOLA; + public List getGraphQLAllowedNetworks() { + return this.allowedNetworks; } - public void setGraphQLRail(Double RAIL) { - this.RAIL = RAIL; + public List getGraphQLBannedNetworks() { + return this.bannedNetworks; } - public void setGraphQLSubway(Double SUBWAY) { - this.SUBWAY = SUBWAY; + public void setGraphQLAllowedNetworks(List allowedNetworks) { + this.allowedNetworks = allowedNetworks; } - public void setGraphQLTram(Double TRAM) { - this.TRAM = TRAM; + public void setGraphQLBannedNetworks(List bannedNetworks) { + this.bannedNetworks = bannedNetworks; } } - public static class GraphQLInputPreferredInput { + public static class GraphQLCyclingOptimizationInput { - private String agencies; - private Integer otherThanPreferredRoutesPenalty; - private String routes; + private GraphQLTriangleCyclingFactorsInput triangle; + private GraphQLCyclingOptimizationType type; - public GraphQLInputPreferredInput(Map args) { + public GraphQLCyclingOptimizationInput(Map args) { if (args != null) { - this.agencies = (String) args.get("agencies"); - this.otherThanPreferredRoutesPenalty = - (Integer) args.get("otherThanPreferredRoutesPenalty"); - this.routes = (String) args.get("routes"); + this.triangle = + new GraphQLTriangleCyclingFactorsInput((Map) args.get("triangle")); + if (args.get("type") instanceof GraphQLCyclingOptimizationType) { + this.type = (GraphQLCyclingOptimizationType) args.get("type"); + } else if (args.get("type") != null) { + this.type = GraphQLCyclingOptimizationType.valueOf((String) args.get("type")); + } } } - public String getGraphQLAgencies() { - return this.agencies; - } - - public Integer getGraphQLOtherThanPreferredRoutesPenalty() { - return this.otherThanPreferredRoutesPenalty; + public GraphQLTriangleCyclingFactorsInput getGraphQLTriangle() { + return this.triangle; } - public String getGraphQLRoutes() { - return this.routes; + public GraphQLCyclingOptimizationType getGraphQLType() { + return this.type; } - public void setGraphQLAgencies(String agencies) { - this.agencies = agencies; + public void setGraphQLTriangle(GraphQLTriangleCyclingFactorsInput triangle) { + this.triangle = triangle; } - public void setGraphQLOtherThanPreferredRoutesPenalty(Integer otherThanPreferredRoutesPenalty) { - this.otherThanPreferredRoutesPenalty = otherThanPreferredRoutesPenalty; + public void setGraphQLType(GraphQLCyclingOptimizationType type) { + this.type = type; } + } - public void setGraphQLRoutes(String routes) { - this.routes = routes; - } + /** + * Predefined optimization alternatives for bicycling routing. For more customization, + * one can use the triangle factors. + */ + public enum GraphQLCyclingOptimizationType { + FLAT_STREETS, + SAFEST_STREETS, + SAFE_STREETS, + SHORTEST_DURATION, } - public static class GraphQLInputTriangleInput { + public static class GraphQLDepartureRowStoptimesArgs { - private Double safetyFactor; - private Double slopeFactor; - private Double timeFactor; + private Integer numberOfDepartures; + private Boolean omitCanceled; + private Boolean omitNonPickups; + private Long startTime; + private Integer timeRange; - public GraphQLInputTriangleInput(Map args) { + public GraphQLDepartureRowStoptimesArgs(Map args) { if (args != null) { - this.safetyFactor = (Double) args.get("safetyFactor"); - this.slopeFactor = (Double) args.get("slopeFactor"); - this.timeFactor = (Double) args.get("timeFactor"); + this.numberOfDepartures = (Integer) args.get("numberOfDepartures"); + this.omitCanceled = (Boolean) args.get("omitCanceled"); + this.omitNonPickups = (Boolean) args.get("omitNonPickups"); + this.startTime = (Long) args.get("startTime"); + this.timeRange = (Integer) args.get("timeRange"); } } - public Double getGraphQLSafetyFactor() { - return this.safetyFactor; + public Integer getGraphQLNumberOfDepartures() { + return this.numberOfDepartures; } - public Double getGraphQLSlopeFactor() { - return this.slopeFactor; + public Boolean getGraphQLOmitCanceled() { + return this.omitCanceled; } - public Double getGraphQLTimeFactor() { - return this.timeFactor; + public Boolean getGraphQLOmitNonPickups() { + return this.omitNonPickups; } - public void setGraphQLSafetyFactor(Double safetyFactor) { - this.safetyFactor = safetyFactor; + public Long getGraphQLStartTime() { + return this.startTime; } - public void setGraphQLSlopeFactor(Double slopeFactor) { - this.slopeFactor = slopeFactor; + public Integer getGraphQLTimeRange() { + return this.timeRange; } - public void setGraphQLTimeFactor(Double timeFactor) { - this.timeFactor = timeFactor; + public void setGraphQLNumberOfDepartures(Integer numberOfDepartures) { + this.numberOfDepartures = numberOfDepartures; + } + + public void setGraphQLOmitCanceled(Boolean omitCanceled) { + this.omitCanceled = omitCanceled; + } + + public void setGraphQLOmitNonPickups(Boolean omitNonPickups) { + this.omitNonPickups = omitNonPickups; + } + + public void setGraphQLStartTime(Long startTime) { + this.startTime = startTime; + } + + public void setGraphQLTimeRange(Integer timeRange) { + this.timeRange = timeRange; } } - public static class GraphQLInputUnpreferredInput { + public static class GraphQLDestinationBicyclePolicyInput { - private String agencies; - private String routes; - private String unpreferredCost; - private Integer useUnpreferredRoutesPenalty; + private Boolean allowKeeping; + private org.opentripplanner.framework.model.Cost keepingCost; - public GraphQLInputUnpreferredInput(Map args) { + public GraphQLDestinationBicyclePolicyInput(Map args) { if (args != null) { - this.agencies = (String) args.get("agencies"); - this.routes = (String) args.get("routes"); - this.unpreferredCost = (String) args.get("unpreferredCost"); - this.useUnpreferredRoutesPenalty = (Integer) args.get("useUnpreferredRoutesPenalty"); + this.allowKeeping = (Boolean) args.get("allowKeeping"); + this.keepingCost = (org.opentripplanner.framework.model.Cost) args.get("keepingCost"); } } - public String getGraphQLAgencies() { - return this.agencies; + public Boolean getGraphQLAllowKeeping() { + return this.allowKeeping; } - public String getGraphQLRoutes() { - return this.routes; + public org.opentripplanner.framework.model.Cost getGraphQLKeepingCost() { + return this.keepingCost; } - public String getGraphQLUnpreferredCost() { - return this.unpreferredCost; + public void setGraphQLAllowKeeping(Boolean allowKeeping) { + this.allowKeeping = allowKeeping; } - public Integer getGraphQLUseUnpreferredRoutesPenalty() { - return this.useUnpreferredRoutesPenalty; + public void setGraphQLKeepingCost(org.opentripplanner.framework.model.Cost keepingCost) { + this.keepingCost = keepingCost; } + } - public void setGraphQLAgencies(String agencies) { - this.agencies = agencies; + public static class GraphQLDestinationScooterPolicyInput { + + private Boolean allowKeeping; + private org.opentripplanner.framework.model.Cost keepingCost; + + public GraphQLDestinationScooterPolicyInput(Map args) { + if (args != null) { + this.allowKeeping = (Boolean) args.get("allowKeeping"); + this.keepingCost = (org.opentripplanner.framework.model.Cost) args.get("keepingCost"); + } } - public void setGraphQLRoutes(String routes) { - this.routes = routes; + public Boolean getGraphQLAllowKeeping() { + return this.allowKeeping; } - public void setGraphQLUnpreferredCost(String unpreferredCost) { - this.unpreferredCost = unpreferredCost; + public org.opentripplanner.framework.model.Cost getGraphQLKeepingCost() { + return this.keepingCost; } - public void setGraphQLUseUnpreferredRoutesPenalty(Integer useUnpreferredRoutesPenalty) { - this.useUnpreferredRoutesPenalty = useUnpreferredRoutesPenalty; + public void setGraphQLAllowKeeping(Boolean allowKeeping) { + this.allowKeeping = allowKeeping; + } + + public void setGraphQLKeepingCost(org.opentripplanner.framework.model.Cost keepingCost) { + this.keepingCost = keepingCost; } } - public static class GraphQLLegNextLegsArgs { + public static class GraphQLFeedAlertsArgs { - private List destinationModesWithParentStation; + private List types; + + public GraphQLFeedAlertsArgs(Map args) { + if (args != null) { + if (args.get("types") != null) { + this.types = + ((List) args.get("types")).stream() + .map(item -> + item instanceof GraphQLFeedAlertType + ? item + : GraphQLFeedAlertType.valueOf((String) item) + ) + .map(GraphQLFeedAlertType.class::cast) + .collect(Collectors.toList()); + } + } + } + + public List getGraphQLTypes() { + return this.types; + } + + public void setGraphQLTypes(List types) { + this.types = types; + } + } + + /** Entities, which are relevant for a feed and can contain alerts */ + public enum GraphQLFeedAlertType { + AGENCIES, + ROUTE_TYPES, + } + + public enum GraphQLFilterPlaceType { + BICYCLE_RENT, + BIKE_PARK, + CAR_PARK, + DEPARTURE_ROW, + STATION, + STOP, + VEHICLE_RENT, + } + + public enum GraphQLFormFactor { + BICYCLE, + CAR, + CARGO_BICYCLE, + MOPED, + OTHER, + SCOOTER, + SCOOTER_SEATED, + SCOOTER_STANDING, + } + + public static class GraphQLInputBannedInput { + + private String agencies; + private String routes; + private String stops; + private String stopsHard; + private String trips; + + public GraphQLInputBannedInput(Map args) { + if (args != null) { + this.agencies = (String) args.get("agencies"); + this.routes = (String) args.get("routes"); + this.stops = (String) args.get("stops"); + this.stopsHard = (String) args.get("stopsHard"); + this.trips = (String) args.get("trips"); + } + } + + public String getGraphQLAgencies() { + return this.agencies; + } + + public String getGraphQLRoutes() { + return this.routes; + } + + public String getGraphQLStops() { + return this.stops; + } + + public String getGraphQLStopsHard() { + return this.stopsHard; + } + + public String getGraphQLTrips() { + return this.trips; + } + + public void setGraphQLAgencies(String agencies) { + this.agencies = agencies; + } + + public void setGraphQLRoutes(String routes) { + this.routes = routes; + } + + public void setGraphQLStops(String stops) { + this.stops = stops; + } + + public void setGraphQLStopsHard(String stopsHard) { + this.stopsHard = stopsHard; + } + + public void setGraphQLTrips(String trips) { + this.trips = trips; + } + } + + public static class GraphQLInputCoordinatesInput { + + private String address; + private Double lat; + private Integer locationSlack; + private Double lon; + + public GraphQLInputCoordinatesInput(Map args) { + if (args != null) { + this.address = (String) args.get("address"); + this.lat = (Double) args.get("lat"); + this.locationSlack = (Integer) args.get("locationSlack"); + this.lon = (Double) args.get("lon"); + } + } + + public String getGraphQLAddress() { + return this.address; + } + + public Double getGraphQLLat() { + return this.lat; + } + + public Integer getGraphQLLocationSlack() { + return this.locationSlack; + } + + public Double getGraphQLLon() { + return this.lon; + } + + public void setGraphQLAddress(String address) { + this.address = address; + } + + public void setGraphQLLat(Double lat) { + this.lat = lat; + } + + public void setGraphQLLocationSlack(Integer locationSlack) { + this.locationSlack = locationSlack; + } + + public void setGraphQLLon(Double lon) { + this.lon = lon; + } + } + + public enum GraphQLInputField { + DATE_TIME, + FROM, + TO, + } + + public static class GraphQLInputFiltersInput { + + private List bikeParks; + private List bikeRentalStations; + private List carParks; + private List routes; + private List stations; + private List stops; + + public GraphQLInputFiltersInput(Map args) { + if (args != null) { + this.bikeParks = (List) args.get("bikeParks"); + this.bikeRentalStations = (List) args.get("bikeRentalStations"); + this.carParks = (List) args.get("carParks"); + this.routes = (List) args.get("routes"); + this.stations = (List) args.get("stations"); + this.stops = (List) args.get("stops"); + } + } + + public List getGraphQLBikeParks() { + return this.bikeParks; + } + + public List getGraphQLBikeRentalStations() { + return this.bikeRentalStations; + } + + public List getGraphQLCarParks() { + return this.carParks; + } + + public List getGraphQLRoutes() { + return this.routes; + } + + public List getGraphQLStations() { + return this.stations; + } + + public List getGraphQLStops() { + return this.stops; + } + + public void setGraphQLBikeParks(List bikeParks) { + this.bikeParks = bikeParks; + } + + public void setGraphQLBikeRentalStations(List bikeRentalStations) { + this.bikeRentalStations = bikeRentalStations; + } + + public void setGraphQLCarParks(List carParks) { + this.carParks = carParks; + } + + public void setGraphQLRoutes(List routes) { + this.routes = routes; + } + + public void setGraphQLStations(List stations) { + this.stations = stations; + } + + public void setGraphQLStops(List stops) { + this.stops = stops; + } + } + + public static class GraphQLInputModeWeightInput { + + private Double AIRPLANE; + private Double BUS; + private Double CABLE_CAR; + private Double FERRY; + private Double FUNICULAR; + private Double GONDOLA; + private Double RAIL; + private Double SUBWAY; + private Double TRAM; + + public GraphQLInputModeWeightInput(Map args) { + if (args != null) { + this.AIRPLANE = (Double) args.get("AIRPLANE"); + this.BUS = (Double) args.get("BUS"); + this.CABLE_CAR = (Double) args.get("CABLE_CAR"); + this.FERRY = (Double) args.get("FERRY"); + this.FUNICULAR = (Double) args.get("FUNICULAR"); + this.GONDOLA = (Double) args.get("GONDOLA"); + this.RAIL = (Double) args.get("RAIL"); + this.SUBWAY = (Double) args.get("SUBWAY"); + this.TRAM = (Double) args.get("TRAM"); + } + } + + public Double getGraphQLAirplane() { + return this.AIRPLANE; + } + + public Double getGraphQLBus() { + return this.BUS; + } + + public Double getGraphQLCable_Car() { + return this.CABLE_CAR; + } + + public Double getGraphQLFerry() { + return this.FERRY; + } + + public Double getGraphQLFunicular() { + return this.FUNICULAR; + } + + public Double getGraphQLGondola() { + return this.GONDOLA; + } + + public Double getGraphQLRail() { + return this.RAIL; + } + + public Double getGraphQLSubway() { + return this.SUBWAY; + } + + public Double getGraphQLTram() { + return this.TRAM; + } + + public void setGraphQLAirplane(Double AIRPLANE) { + this.AIRPLANE = AIRPLANE; + } + + public void setGraphQLBus(Double BUS) { + this.BUS = BUS; + } + + public void setGraphQLCable_Car(Double CABLE_CAR) { + this.CABLE_CAR = CABLE_CAR; + } + + public void setGraphQLFerry(Double FERRY) { + this.FERRY = FERRY; + } + + public void setGraphQLFunicular(Double FUNICULAR) { + this.FUNICULAR = FUNICULAR; + } + + public void setGraphQLGondola(Double GONDOLA) { + this.GONDOLA = GONDOLA; + } + + public void setGraphQLRail(Double RAIL) { + this.RAIL = RAIL; + } + + public void setGraphQLSubway(Double SUBWAY) { + this.SUBWAY = SUBWAY; + } + + public void setGraphQLTram(Double TRAM) { + this.TRAM = TRAM; + } + } + + public static class GraphQLInputPreferredInput { + + private String agencies; + private Integer otherThanPreferredRoutesPenalty; + private String routes; + + public GraphQLInputPreferredInput(Map args) { + if (args != null) { + this.agencies = (String) args.get("agencies"); + this.otherThanPreferredRoutesPenalty = + (Integer) args.get("otherThanPreferredRoutesPenalty"); + this.routes = (String) args.get("routes"); + } + } + + public String getGraphQLAgencies() { + return this.agencies; + } + + public Integer getGraphQLOtherThanPreferredRoutesPenalty() { + return this.otherThanPreferredRoutesPenalty; + } + + public String getGraphQLRoutes() { + return this.routes; + } + + public void setGraphQLAgencies(String agencies) { + this.agencies = agencies; + } + + public void setGraphQLOtherThanPreferredRoutesPenalty(Integer otherThanPreferredRoutesPenalty) { + this.otherThanPreferredRoutesPenalty = otherThanPreferredRoutesPenalty; + } + + public void setGraphQLRoutes(String routes) { + this.routes = routes; + } + } + + public static class GraphQLInputTriangleInput { + + private Double safetyFactor; + private Double slopeFactor; + private Double timeFactor; + + public GraphQLInputTriangleInput(Map args) { + if (args != null) { + this.safetyFactor = (Double) args.get("safetyFactor"); + this.slopeFactor = (Double) args.get("slopeFactor"); + this.timeFactor = (Double) args.get("timeFactor"); + } + } + + public Double getGraphQLSafetyFactor() { + return this.safetyFactor; + } + + public Double getGraphQLSlopeFactor() { + return this.slopeFactor; + } + + public Double getGraphQLTimeFactor() { + return this.timeFactor; + } + + public void setGraphQLSafetyFactor(Double safetyFactor) { + this.safetyFactor = safetyFactor; + } + + public void setGraphQLSlopeFactor(Double slopeFactor) { + this.slopeFactor = slopeFactor; + } + + public void setGraphQLTimeFactor(Double timeFactor) { + this.timeFactor = timeFactor; + } + } + + public static class GraphQLInputUnpreferredInput { + + private String agencies; + private String routes; + private String unpreferredCost; + private Integer useUnpreferredRoutesPenalty; + + public GraphQLInputUnpreferredInput(Map args) { + if (args != null) { + this.agencies = (String) args.get("agencies"); + this.routes = (String) args.get("routes"); + this.unpreferredCost = (String) args.get("unpreferredCost"); + this.useUnpreferredRoutesPenalty = (Integer) args.get("useUnpreferredRoutesPenalty"); + } + } + + public String getGraphQLAgencies() { + return this.agencies; + } + + public String getGraphQLRoutes() { + return this.routes; + } + + public String getGraphQLUnpreferredCost() { + return this.unpreferredCost; + } + + public Integer getGraphQLUseUnpreferredRoutesPenalty() { + return this.useUnpreferredRoutesPenalty; + } + + public void setGraphQLAgencies(String agencies) { + this.agencies = agencies; + } + + public void setGraphQLRoutes(String routes) { + this.routes = routes; + } + + public void setGraphQLUnpreferredCost(String unpreferredCost) { + this.unpreferredCost = unpreferredCost; + } + + public void setGraphQLUseUnpreferredRoutesPenalty(Integer useUnpreferredRoutesPenalty) { + this.useUnpreferredRoutesPenalty = useUnpreferredRoutesPenalty; + } + } + + /** + * Enable this to attach a system notice to itineraries instead of removing them. This is very + * convenient when tuning the itinerary-filter-chain. + */ + public enum GraphQLItineraryFilterDebugProfile { + LIMIT_TO_NUMBER_OF_ITINERARIES, + LIMIT_TO_SEARCH_WINDOW, + LIST_ALL, + OFF, + } + + public static class GraphQLLegNextLegsArgs { + + private List destinationModesWithParentStation; private Integer numberOfLegs; private List originModesWithParentStation; - public GraphQLLegNextLegsArgs(Map args) { + public GraphQLLegNextLegsArgs(Map args) { + if (args != null) { + if (args.get("destinationModesWithParentStation") != null) { + this.destinationModesWithParentStation = + ((List) args.get("destinationModesWithParentStation")).stream() + .map(item -> + item instanceof GraphQLTransitMode + ? item + : GraphQLTransitMode.valueOf((String) item) + ) + .map(GraphQLTransitMode.class::cast) + .collect(Collectors.toList()); + } + this.numberOfLegs = (Integer) args.get("numberOfLegs"); + if (args.get("originModesWithParentStation") != null) { + this.originModesWithParentStation = + ((List) args.get("originModesWithParentStation")).stream() + .map(item -> + item instanceof GraphQLTransitMode + ? item + : GraphQLTransitMode.valueOf((String) item) + ) + .map(GraphQLTransitMode.class::cast) + .collect(Collectors.toList()); + } + } + } + + public List getGraphQLDestinationModesWithParentStation() { + return this.destinationModesWithParentStation; + } + + public Integer getGraphQLNumberOfLegs() { + return this.numberOfLegs; + } + + public List getGraphQLOriginModesWithParentStation() { + return this.originModesWithParentStation; + } + + public void setGraphQLDestinationModesWithParentStation( + List destinationModesWithParentStation + ) { + this.destinationModesWithParentStation = destinationModesWithParentStation; + } + + public void setGraphQLNumberOfLegs(Integer numberOfLegs) { + this.numberOfLegs = numberOfLegs; + } + + public void setGraphQLOriginModesWithParentStation( + List originModesWithParentStation + ) { + this.originModesWithParentStation = originModesWithParentStation; + } + } + + /** Identifies whether this stop represents a stop or station. */ + public enum GraphQLLocationType { + ENTRANCE, + STATION, + STOP, + } + + public enum GraphQLMode { + AIRPLANE, + BICYCLE, + BUS, + CABLE_CAR, + CAR, + CARPOOL, + COACH, + FERRY, + FLEX, + FLEXIBLE, + FUNICULAR, + GONDOLA, + LEG_SWITCH, + MONORAIL, + RAIL, + SCOOTER, + SUBWAY, + TAXI, + TRAM, + TRANSIT, + TROLLEYBUS, + WALK, + } + + /** Occupancy status of a vehicle. */ + public enum GraphQLOccupancyStatus { + CRUSHED_STANDING_ROOM_ONLY, + EMPTY, + FEW_SEATS_AVAILABLE, + FULL, + MANY_SEATS_AVAILABLE, + NOT_ACCEPTING_PASSENGERS, + NO_DATA_AVAILABLE, + STANDING_ROOM_ONLY, + } + + public static class GraphQLOpeningHoursDatesArgs { + + private List dates; + + public GraphQLOpeningHoursDatesArgs(Map args) { + if (args != null) { + this.dates = (List) args.get("dates"); + } + } + + public List getGraphQLDates() { + return this.dates; + } + + public void setGraphQLDates(List dates) { + this.dates = dates; + } + } + + /** Optimization type for bicycling legs */ + public enum GraphQLOptimizeType { + FLAT, + GREENWAYS, + QUICK, + SAFE, + TRIANGLE, + } + + public static class GraphQLParkingFilterInput { + + private List not; + private List select; + + public GraphQLParkingFilterInput(Map args) { if (args != null) { - if (args.get("destinationModesWithParentStation") != null) { - this.destinationModesWithParentStation = - ((List) args.get("destinationModesWithParentStation")).stream() - .map(item -> - item instanceof GraphQLTransitMode - ? item - : GraphQLTransitMode.valueOf((String) item) - ) - .map(GraphQLTransitMode.class::cast) - .collect(Collectors.toList()); + if (args.get("not") != null) { + this.not = (List) args.get("not"); } - this.numberOfLegs = (Integer) args.get("numberOfLegs"); - if (args.get("originModesWithParentStation") != null) { - this.originModesWithParentStation = - ((List) args.get("originModesWithParentStation")).stream() + if (args.get("select") != null) { + this.select = (List) args.get("select"); + } + } + } + + public List getGraphQLNot() { + return this.not; + } + + public List getGraphQLSelect() { + return this.select; + } + + public void setGraphQLNot(List not) { + this.not = not; + } + + public void setGraphQLSelect(List select) { + this.select = select; + } + } + + public static class GraphQLParkingFilterOperationInput { + + private List tags; + + public GraphQLParkingFilterOperationInput(Map args) { + if (args != null) { + this.tags = (List) args.get("tags"); + } + } + + public List getGraphQLTags() { + return this.tags; + } + + public void setGraphQLTags(List tags) { + this.tags = tags; + } + } + + public static class GraphQLPatternAlertsArgs { + + private List types; + + public GraphQLPatternAlertsArgs(Map args) { + if (args != null) { + if (args.get("types") != null) { + this.types = + ((List) args.get("types")).stream() .map(item -> - item instanceof GraphQLTransitMode + item instanceof GraphQLPatternAlertType ? item - : GraphQLTransitMode.valueOf((String) item) + : GraphQLPatternAlertType.valueOf((String) item) ) - .map(GraphQLTransitMode.class::cast) + .map(GraphQLPatternAlertType.class::cast) .collect(Collectors.toList()); } } } - public List getGraphQLDestinationModesWithParentStation() { - return this.destinationModesWithParentStation; + public List getGraphQLTypes() { + return this.types; } - public Integer getGraphQLNumberOfLegs() { - return this.numberOfLegs; + public void setGraphQLTypes(List types) { + this.types = types; } + } - public List getGraphQLOriginModesWithParentStation() { - return this.originModesWithParentStation; + public static class GraphQLPatternTripsForDateArgs { + + private String serviceDate; + + public GraphQLPatternTripsForDateArgs(Map args) { + if (args != null) { + this.serviceDate = (String) args.get("serviceDate"); + } } - public void setGraphQLDestinationModesWithParentStation( - List destinationModesWithParentStation - ) { - this.destinationModesWithParentStation = destinationModesWithParentStation; + public String getGraphQLServiceDate() { + return this.serviceDate; } - public void setGraphQLNumberOfLegs(Integer numberOfLegs) { - this.numberOfLegs = numberOfLegs; + public void setGraphQLServiceDate(String serviceDate) { + this.serviceDate = serviceDate; } + } - public void setGraphQLOriginModesWithParentStation( - List originModesWithParentStation - ) { - this.originModesWithParentStation = originModesWithParentStation; + /** Entities, which are relevant for a pattern and can contain alerts */ + public enum GraphQLPatternAlertType { + AGENCY, + PATTERN, + ROUTE, + ROUTE_TYPE, + STOPS_ON_PATTERN, + STOPS_ON_TRIPS, + TRIPS, + } + + public enum GraphQLPickupDropoffType { + CALL_AGENCY, + COORDINATE_WITH_DRIVER, + NONE, + SCHEDULED, + } + + /** Street modes that can be used for access to the transit network from origin. */ + public enum GraphQLPlanAccessMode { + BICYCLE, + BICYCLE_PARKING, + BICYCLE_RENTAL, + CAR_DROP_OFF, + CAR_PARKING, + CAR_RENTAL, + FLEX, + SCOOTER_RENTAL, + WALK, + } + + public static class GraphQLPlanCoordinateInput { + + private Double latitude; + private Double longitude; + + public GraphQLPlanCoordinateInput(Map args) { + if (args != null) { + this.latitude = (Double) args.get("latitude"); + this.longitude = (Double) args.get("longitude"); + } + } + + public Double getGraphQLLatitude() { + return this.latitude; + } + + public Double getGraphQLLongitude() { + return this.longitude; + } + + public void setGraphQLLatitude(Double latitude) { + this.latitude = latitude; + } + + public void setGraphQLLongitude(Double longitude) { + this.longitude = longitude; } } - /** Identifies whether this stop represents a stop or station. */ - public enum GraphQLLocationType { - ENTRANCE, - STATION, - STOP, + public static class GraphQLPlanDateTimeInput { + + private java.time.OffsetDateTime earliestDeparture; + private java.time.OffsetDateTime latestArrival; + + public GraphQLPlanDateTimeInput(Map args) { + if (args != null) { + this.earliestDeparture = (java.time.OffsetDateTime) args.get("earliestDeparture"); + this.latestArrival = (java.time.OffsetDateTime) args.get("latestArrival"); + } + } + + public java.time.OffsetDateTime getGraphQLEarliestDeparture() { + return this.earliestDeparture; + } + + public java.time.OffsetDateTime getGraphQLLatestArrival() { + return this.latestArrival; + } + + public void setGraphQLEarliestDeparture(java.time.OffsetDateTime earliestDeparture) { + this.earliestDeparture = earliestDeparture; + } + + public void setGraphQLLatestArrival(java.time.OffsetDateTime latestArrival) { + this.latestArrival = latestArrival; + } } - public enum GraphQLMode { - AIRPLANE, + /** Street mode that is used when searching for itineraries that don't use any transit. */ + public enum GraphQLPlanDirectMode { BICYCLE, - BUS, - CABLE_CAR, + BICYCLE_PARKING, + BICYCLE_RENTAL, CAR, - CARPOOL, - COACH, - FERRY, + CAR_PARKING, + CAR_RENTAL, FLEX, - FLEXIBLE, - FUNICULAR, - GONDOLA, - LEG_SWITCH, - MONORAIL, - RAIL, - SCOOTER, - SUBWAY, - TAXI, - TRAM, - TRANSIT, - TROLLEYBUS, + SCOOTER_RENTAL, + WALK, + } + + /** Street modes that can be used for egress from the transit network to destination. */ + public enum GraphQLPlanEgressMode { + BICYCLE, + BICYCLE_RENTAL, + CAR_PICKUP, + CAR_RENTAL, + FLEX, + SCOOTER_RENTAL, WALK, } - /** Occupancy status of a vehicle. */ - public enum GraphQLOccupancyStatus { - CRUSHED_STANDING_ROOM_ONLY, - EMPTY, - FEW_SEATS_AVAILABLE, - FULL, - MANY_SEATS_AVAILABLE, - NOT_ACCEPTING_PASSENGERS, - NO_DATA_AVAILABLE, - STANDING_ROOM_ONLY, + public static class GraphQLPlanItineraryFilterInput { + + private Double groupSimilarityKeepOne; + private Double groupSimilarityKeepThree; + private Double groupedOtherThanSameLegsMaxCostMultiplier; + private GraphQLItineraryFilterDebugProfile itineraryFilterDebugProfile; + + public GraphQLPlanItineraryFilterInput(Map args) { + if (args != null) { + this.groupSimilarityKeepOne = (Double) args.get("groupSimilarityKeepOne"); + this.groupSimilarityKeepThree = (Double) args.get("groupSimilarityKeepThree"); + this.groupedOtherThanSameLegsMaxCostMultiplier = + (Double) args.get("groupedOtherThanSameLegsMaxCostMultiplier"); + if (args.get("itineraryFilterDebugProfile") instanceof GraphQLItineraryFilterDebugProfile) { + this.itineraryFilterDebugProfile = + (GraphQLItineraryFilterDebugProfile) args.get("itineraryFilterDebugProfile"); + } else if (args.get("itineraryFilterDebugProfile") != null) { + this.itineraryFilterDebugProfile = + GraphQLItineraryFilterDebugProfile.valueOf( + (String) args.get("itineraryFilterDebugProfile") + ); + } + } + } + + public Double getGraphQLGroupSimilarityKeepOne() { + return this.groupSimilarityKeepOne; + } + + public Double getGraphQLGroupSimilarityKeepThree() { + return this.groupSimilarityKeepThree; + } + + public Double getGraphQLGroupedOtherThanSameLegsMaxCostMultiplier() { + return this.groupedOtherThanSameLegsMaxCostMultiplier; + } + + public GraphQLItineraryFilterDebugProfile getGraphQLItineraryFilterDebugProfile() { + return this.itineraryFilterDebugProfile; + } + + public void setGraphQLGroupSimilarityKeepOne(Double groupSimilarityKeepOne) { + this.groupSimilarityKeepOne = groupSimilarityKeepOne; + } + + public void setGraphQLGroupSimilarityKeepThree(Double groupSimilarityKeepThree) { + this.groupSimilarityKeepThree = groupSimilarityKeepThree; + } + + public void setGraphQLGroupedOtherThanSameLegsMaxCostMultiplier( + Double groupedOtherThanSameLegsMaxCostMultiplier + ) { + this.groupedOtherThanSameLegsMaxCostMultiplier = groupedOtherThanSameLegsMaxCostMultiplier; + } + + public void setGraphQLItineraryFilterDebugProfile( + GraphQLItineraryFilterDebugProfile itineraryFilterDebugProfile + ) { + this.itineraryFilterDebugProfile = itineraryFilterDebugProfile; + } + } + + public static class GraphQLPlanLabeledLocationInput { + + private String label; + private GraphQLPlanLocationInput location; + + public GraphQLPlanLabeledLocationInput(Map args) { + if (args != null) { + this.label = (String) args.get("label"); + this.location = new GraphQLPlanLocationInput((Map) args.get("location")); + } + } + + public String getGraphQLLabel() { + return this.label; + } + + public GraphQLPlanLocationInput getGraphQLLocation() { + return this.location; + } + + public void setGraphQLLabel(String label) { + this.label = label; + } + + public void setGraphQLLocation(GraphQLPlanLocationInput location) { + this.location = location; + } + } + + public static class GraphQLPlanLocationInput { + + private GraphQLPlanCoordinateInput coordinate; + private GraphQLPlanStopLocationInput stopLocation; + + public GraphQLPlanLocationInput(Map args) { + if (args != null) { + this.coordinate = + new GraphQLPlanCoordinateInput((Map) args.get("coordinate")); + this.stopLocation = + new GraphQLPlanStopLocationInput((Map) args.get("stopLocation")); + } + } + + public GraphQLPlanCoordinateInput getGraphQLCoordinate() { + return this.coordinate; + } + + public GraphQLPlanStopLocationInput getGraphQLStopLocation() { + return this.stopLocation; + } + + public void setGraphQLCoordinate(GraphQLPlanCoordinateInput coordinate) { + this.coordinate = coordinate; + } + + public void setGraphQLStopLocation(GraphQLPlanStopLocationInput stopLocation) { + this.stopLocation = stopLocation; + } + } + + public static class GraphQLPlanModesInput { + + private List direct; + private Boolean directOnly; + private GraphQLPlanTransitModesInput transit; + private Boolean transitOnly; + + public GraphQLPlanModesInput(Map args) { + if (args != null) { + if (args.get("direct") != null) { + this.direct = + ((List) args.get("direct")).stream() + .map(item -> + item instanceof GraphQLPlanDirectMode + ? item + : GraphQLPlanDirectMode.valueOf((String) item) + ) + .map(GraphQLPlanDirectMode.class::cast) + .collect(Collectors.toList()); + } + this.directOnly = (Boolean) args.get("directOnly"); + this.transit = new GraphQLPlanTransitModesInput((Map) args.get("transit")); + this.transitOnly = (Boolean) args.get("transitOnly"); + } + } + + public List getGraphQLDirect() { + return this.direct; + } + + public Boolean getGraphQLDirectOnly() { + return this.directOnly; + } + + public GraphQLPlanTransitModesInput getGraphQLTransit() { + return this.transit; + } + + public Boolean getGraphQLTransitOnly() { + return this.transitOnly; + } + + public void setGraphQLDirect(List direct) { + this.direct = direct; + } + + public void setGraphQLDirectOnly(Boolean directOnly) { + this.directOnly = directOnly; + } + + public void setGraphQLTransit(GraphQLPlanTransitModesInput transit) { + this.transit = transit; + } + + public void setGraphQLTransitOnly(Boolean transitOnly) { + this.transitOnly = transitOnly; + } } - public static class GraphQLOpeningHoursDatesArgs { + public static class GraphQLPlanPreferencesInput { - private List dates; + private GraphQLAccessibilityPreferencesInput accessibility; + private GraphQLPlanStreetPreferencesInput street; + private GraphQLTransitPreferencesInput transit; - public GraphQLOpeningHoursDatesArgs(Map args) { + public GraphQLPlanPreferencesInput(Map args) { if (args != null) { - this.dates = (List) args.get("dates"); + this.accessibility = + new GraphQLAccessibilityPreferencesInput((Map) args.get("accessibility")); + this.street = + new GraphQLPlanStreetPreferencesInput((Map) args.get("street")); + this.transit = + new GraphQLTransitPreferencesInput((Map) args.get("transit")); } } - public List getGraphQLDates() { - return this.dates; + public GraphQLAccessibilityPreferencesInput getGraphQLAccessibility() { + return this.accessibility; } - public void setGraphQLDates(List dates) { - this.dates = dates; + public GraphQLPlanStreetPreferencesInput getGraphQLStreet() { + return this.street; + } + + public GraphQLTransitPreferencesInput getGraphQLTransit() { + return this.transit; + } + + public void setGraphQLAccessibility(GraphQLAccessibilityPreferencesInput accessibility) { + this.accessibility = accessibility; + } + + public void setGraphQLStreet(GraphQLPlanStreetPreferencesInput street) { + this.street = street; + } + + public void setGraphQLTransit(GraphQLTransitPreferencesInput transit) { + this.transit = transit; } } - /** Optimization type for bicycling legs */ - public enum GraphQLOptimizeType { - FLAT, - GREENWAYS, - QUICK, - SAFE, - TRIANGLE, + public static class GraphQLPlanStopLocationInput { + + private String stopLocationId; + + public GraphQLPlanStopLocationInput(Map args) { + if (args != null) { + this.stopLocationId = (String) args.get("stopLocationId"); + } + } + + public String getGraphQLStopLocationId() { + return this.stopLocationId; + } + + public void setGraphQLStopLocationId(String stopLocationId) { + this.stopLocationId = stopLocationId; + } } - public static class GraphQLParkingFilterInput { + public static class GraphQLPlanStreetPreferencesInput { - private List not; - private List select; + private GraphQLBicyclePreferencesInput bicycle; + private GraphQLCarPreferencesInput car; + private GraphQLScooterPreferencesInput scooter; + private GraphQLWalkPreferencesInput walk; - public GraphQLParkingFilterInput(Map args) { + public GraphQLPlanStreetPreferencesInput(Map args) { if (args != null) { - if (args.get("not") != null) { - this.not = (List) args.get("not"); - } - if (args.get("select") != null) { - this.select = (List) args.get("select"); - } + this.bicycle = + new GraphQLBicyclePreferencesInput((Map) args.get("bicycle")); + this.car = new GraphQLCarPreferencesInput((Map) args.get("car")); + this.scooter = + new GraphQLScooterPreferencesInput((Map) args.get("scooter")); + this.walk = new GraphQLWalkPreferencesInput((Map) args.get("walk")); } } - public List getGraphQLNot() { - return this.not; + public GraphQLBicyclePreferencesInput getGraphQLBicycle() { + return this.bicycle; } - public List getGraphQLSelect() { - return this.select; + public GraphQLCarPreferencesInput getGraphQLCar() { + return this.car; } - public void setGraphQLNot(List not) { - this.not = not; + public GraphQLScooterPreferencesInput getGraphQLScooter() { + return this.scooter; } - public void setGraphQLSelect(List select) { - this.select = select; + public GraphQLWalkPreferencesInput getGraphQLWalk() { + return this.walk; + } + + public void setGraphQLBicycle(GraphQLBicyclePreferencesInput bicycle) { + this.bicycle = bicycle; + } + + public void setGraphQLCar(GraphQLCarPreferencesInput car) { + this.car = car; + } + + public void setGraphQLScooter(GraphQLScooterPreferencesInput scooter) { + this.scooter = scooter; + } + + public void setGraphQLWalk(GraphQLWalkPreferencesInput walk) { + this.walk = walk; } } - public static class GraphQLParkingFilterOperationInput { + public enum GraphQLPlanTransferMode { + BICYCLE, + WALK, + } - private List tags; + public static class GraphQLPlanTransitModePreferenceInput { - public GraphQLParkingFilterOperationInput(Map args) { + private GraphQLTransitModePreferenceCostInput cost; + private GraphQLTransitMode mode; + + public GraphQLPlanTransitModePreferenceInput(Map args) { if (args != null) { - this.tags = (List) args.get("tags"); + this.cost = + new GraphQLTransitModePreferenceCostInput((Map) args.get("cost")); + if (args.get("mode") instanceof GraphQLTransitMode) { + this.mode = (GraphQLTransitMode) args.get("mode"); + } else if (args.get("mode") != null) { + this.mode = GraphQLTransitMode.valueOf((String) args.get("mode")); + } } } - public List getGraphQLTags() { - return this.tags; + public GraphQLTransitModePreferenceCostInput getGraphQLCost() { + return this.cost; } - public void setGraphQLTags(List tags) { - this.tags = tags; + public GraphQLTransitMode getGraphQLMode() { + return this.mode; + } + + public void setGraphQLCost(GraphQLTransitModePreferenceCostInput cost) { + this.cost = cost; + } + + public void setGraphQLMode(GraphQLTransitMode mode) { + this.mode = mode; } } - public static class GraphQLPatternAlertsArgs { + public static class GraphQLPlanTransitModesInput { - private List types; + private List access; + private List egress; + private List transfer; + private List transit; - public GraphQLPatternAlertsArgs(Map args) { + public GraphQLPlanTransitModesInput(Map args) { if (args != null) { - if (args.get("types") != null) { - this.types = - ((List) args.get("types")).stream() + if (args.get("access") != null) { + this.access = + ((List) args.get("access")).stream() .map(item -> - item instanceof GraphQLPatternAlertType + item instanceof GraphQLPlanAccessMode ? item - : GraphQLPatternAlertType.valueOf((String) item) + : GraphQLPlanAccessMode.valueOf((String) item) ) - .map(GraphQLPatternAlertType.class::cast) + .map(GraphQLPlanAccessMode.class::cast) + .collect(Collectors.toList()); + } + if (args.get("egress") != null) { + this.egress = + ((List) args.get("egress")).stream() + .map(item -> + item instanceof GraphQLPlanEgressMode + ? item + : GraphQLPlanEgressMode.valueOf((String) item) + ) + .map(GraphQLPlanEgressMode.class::cast) + .collect(Collectors.toList()); + } + if (args.get("transfer") != null) { + this.transfer = + ((List) args.get("transfer")).stream() + .map(item -> + item instanceof GraphQLPlanTransferMode + ? item + : GraphQLPlanTransferMode.valueOf((String) item) + ) + .map(GraphQLPlanTransferMode.class::cast) .collect(Collectors.toList()); } + if (args.get("transit") != null) { + this.transit = (List) args.get("transit"); + } } } - public List getGraphQLTypes() { - return this.types; + public List getGraphQLAccess() { + return this.access; } - public void setGraphQLTypes(List types) { - this.types = types; + public List getGraphQLEgress() { + return this.egress; } - } - - public static class GraphQLPatternTripsForDateArgs { - private String serviceDate; + public List getGraphQLTransfer() { + return this.transfer; + } - public GraphQLPatternTripsForDateArgs(Map args) { - if (args != null) { - this.serviceDate = (String) args.get("serviceDate"); - } + public List getGraphQLTransit() { + return this.transit; } - public String getGraphQLServiceDate() { - return this.serviceDate; + public void setGraphQLAccess(List access) { + this.access = access; } - public void setGraphQLServiceDate(String serviceDate) { - this.serviceDate = serviceDate; + public void setGraphQLEgress(List egress) { + this.egress = egress; } - } - /** Entities, which are relevant for a pattern and can contain alerts */ - public enum GraphQLPatternAlertType { - AGENCY, - PATTERN, - ROUTE, - ROUTE_TYPE, - STOPS_ON_PATTERN, - STOPS_ON_TRIPS, - TRIPS, - } + public void setGraphQLTransfer(List transfer) { + this.transfer = transfer; + } - public enum GraphQLPickupDropoffType { - CALL_AGENCY, - COORDINATE_WITH_DRIVER, - NONE, - SCHEDULED, + public void setGraphQLTransit(List transit) { + this.transit = transit; + } } public enum GraphQLPropulsionType { @@ -1644,7 +2694,7 @@ public GraphQLQueryTypePlanArgs(Map args) { this.omitCanceled = (Boolean) args.get("omitCanceled"); if (args.get("optimize") instanceof GraphQLOptimizeType) { this.optimize = (GraphQLOptimizeType) args.get("optimize"); - } else { + } else if (args.get("optimize") != null) { this.optimize = GraphQLOptimizeType.valueOf((String) args.get("optimize")); } this.pageCursor = (String) args.get("pageCursor"); @@ -2142,48 +3192,180 @@ public void setGraphQLTransferPenalty(Integer transferPenalty) { this.transferPenalty = transferPenalty; } - public void setGraphQLTransportModes(List transportModes) { - this.transportModes = transportModes; + public void setGraphQLTransportModes(List transportModes) { + this.transportModes = transportModes; + } + + public void setGraphQLTriangle(GraphQLInputTriangleInput triangle) { + this.triangle = triangle; + } + + public void setGraphQLUnpreferred(GraphQLInputUnpreferredInput unpreferred) { + this.unpreferred = unpreferred; + } + + public void setGraphQLWaitAtBeginningFactor(Double waitAtBeginningFactor) { + this.waitAtBeginningFactor = waitAtBeginningFactor; + } + + public void setGraphQLWaitReluctance(Double waitReluctance) { + this.waitReluctance = waitReluctance; + } + + public void setGraphQLWalkBoardCost(Integer walkBoardCost) { + this.walkBoardCost = walkBoardCost; + } + + public void setGraphQLWalkOnStreetReluctance(Double walkOnStreetReluctance) { + this.walkOnStreetReluctance = walkOnStreetReluctance; + } + + public void setGraphQLWalkReluctance(Double walkReluctance) { + this.walkReluctance = walkReluctance; + } + + public void setGraphQLWalkSafetyFactor(Double walkSafetyFactor) { + this.walkSafetyFactor = walkSafetyFactor; + } + + public void setGraphQLWalkSpeed(Double walkSpeed) { + this.walkSpeed = walkSpeed; + } + + public void setGraphQLWheelchair(Boolean wheelchair) { + this.wheelchair = wheelchair; + } + } + + public static class GraphQLQueryTypePlanConnectionArgs { + + private String after; + private String before; + private GraphQLPlanDateTimeInput dateTime; + private GraphQLPlanLabeledLocationInput destination; + private Integer first; + private GraphQLPlanItineraryFilterInput itineraryFilter; + private Integer last; + private java.util.Locale locale; + private GraphQLPlanModesInput modes; + private GraphQLPlanLabeledLocationInput origin; + private GraphQLPlanPreferencesInput preferences; + private java.time.Duration searchWindow; + + public GraphQLQueryTypePlanConnectionArgs(Map args) { + if (args != null) { + this.after = (String) args.get("after"); + this.before = (String) args.get("before"); + this.dateTime = new GraphQLPlanDateTimeInput((Map) args.get("dateTime")); + this.destination = + new GraphQLPlanLabeledLocationInput((Map) args.get("destination")); + this.first = (Integer) args.get("first"); + this.itineraryFilter = + new GraphQLPlanItineraryFilterInput((Map) args.get("itineraryFilter")); + this.last = (Integer) args.get("last"); + this.locale = (java.util.Locale) args.get("locale"); + this.modes = new GraphQLPlanModesInput((Map) args.get("modes")); + this.origin = new GraphQLPlanLabeledLocationInput((Map) args.get("origin")); + this.preferences = + new GraphQLPlanPreferencesInput((Map) args.get("preferences")); + this.searchWindow = (java.time.Duration) args.get("searchWindow"); + } + } + + public String getGraphQLAfter() { + return this.after; + } + + public String getGraphQLBefore() { + return this.before; + } + + public GraphQLPlanDateTimeInput getGraphQLDateTime() { + return this.dateTime; + } + + public GraphQLPlanLabeledLocationInput getGraphQLDestination() { + return this.destination; + } + + public Integer getGraphQLFirst() { + return this.first; + } + + public GraphQLPlanItineraryFilterInput getGraphQLItineraryFilter() { + return this.itineraryFilter; + } + + public Integer getGraphQLLast() { + return this.last; + } + + public java.util.Locale getGraphQLLocale() { + return this.locale; + } + + public GraphQLPlanModesInput getGraphQLModes() { + return this.modes; + } + + public GraphQLPlanLabeledLocationInput getGraphQLOrigin() { + return this.origin; + } + + public GraphQLPlanPreferencesInput getGraphQLPreferences() { + return this.preferences; + } + + public java.time.Duration getGraphQLSearchWindow() { + return this.searchWindow; + } + + public void setGraphQLAfter(String after) { + this.after = after; + } + + public void setGraphQLBefore(String before) { + this.before = before; } - public void setGraphQLTriangle(GraphQLInputTriangleInput triangle) { - this.triangle = triangle; + public void setGraphQLDateTime(GraphQLPlanDateTimeInput dateTime) { + this.dateTime = dateTime; } - public void setGraphQLUnpreferred(GraphQLInputUnpreferredInput unpreferred) { - this.unpreferred = unpreferred; + public void setGraphQLDestination(GraphQLPlanLabeledLocationInput destination) { + this.destination = destination; } - public void setGraphQLWaitAtBeginningFactor(Double waitAtBeginningFactor) { - this.waitAtBeginningFactor = waitAtBeginningFactor; + public void setGraphQLFirst(Integer first) { + this.first = first; } - public void setGraphQLWaitReluctance(Double waitReluctance) { - this.waitReluctance = waitReluctance; + public void setGraphQLItineraryFilter(GraphQLPlanItineraryFilterInput itineraryFilter) { + this.itineraryFilter = itineraryFilter; } - public void setGraphQLWalkBoardCost(Integer walkBoardCost) { - this.walkBoardCost = walkBoardCost; + public void setGraphQLLast(Integer last) { + this.last = last; } - public void setGraphQLWalkOnStreetReluctance(Double walkOnStreetReluctance) { - this.walkOnStreetReluctance = walkOnStreetReluctance; + public void setGraphQLLocale(java.util.Locale locale) { + this.locale = locale; } - public void setGraphQLWalkReluctance(Double walkReluctance) { - this.walkReluctance = walkReluctance; + public void setGraphQLModes(GraphQLPlanModesInput modes) { + this.modes = modes; } - public void setGraphQLWalkSafetyFactor(Double walkSafetyFactor) { - this.walkSafetyFactor = walkSafetyFactor; + public void setGraphQLOrigin(GraphQLPlanLabeledLocationInput origin) { + this.origin = origin; } - public void setGraphQLWalkSpeed(Double walkSpeed) { - this.walkSpeed = walkSpeed; + public void setGraphQLPreferences(GraphQLPlanPreferencesInput preferences) { + this.preferences = preferences; } - public void setGraphQLWheelchair(Boolean wheelchair) { - this.wheelchair = wheelchair; + public void setGraphQLSearchWindow(java.time.Duration searchWindow) { + this.searchWindow = searchWindow; } } @@ -2772,6 +3954,146 @@ public enum GraphQLRoutingErrorCode { WALKING_BETTER_THAN_TRANSIT, } + public static class GraphQLScooterOptimizationInput { + + private GraphQLTriangleScooterFactorsInput triangle; + private GraphQLScooterOptimizationType type; + + public GraphQLScooterOptimizationInput(Map args) { + if (args != null) { + this.triangle = + new GraphQLTriangleScooterFactorsInput((Map) args.get("triangle")); + if (args.get("type") instanceof GraphQLScooterOptimizationType) { + this.type = (GraphQLScooterOptimizationType) args.get("type"); + } else if (args.get("type") != null) { + this.type = GraphQLScooterOptimizationType.valueOf((String) args.get("type")); + } + } + } + + public GraphQLTriangleScooterFactorsInput getGraphQLTriangle() { + return this.triangle; + } + + public GraphQLScooterOptimizationType getGraphQLType() { + return this.type; + } + + public void setGraphQLTriangle(GraphQLTriangleScooterFactorsInput triangle) { + this.triangle = triangle; + } + + public void setGraphQLType(GraphQLScooterOptimizationType type) { + this.type = type; + } + } + + /** + * Predefined optimization alternatives for scooter routing. For more customization, + * one can use the triangle factors. + */ + public enum GraphQLScooterOptimizationType { + FLAT_STREETS, + SAFEST_STREETS, + SAFE_STREETS, + SHORTEST_DURATION, + } + + public static class GraphQLScooterPreferencesInput { + + private GraphQLScooterOptimizationInput optimization; + private Double reluctance; + private GraphQLScooterRentalPreferencesInput rental; + private Double speed; + + public GraphQLScooterPreferencesInput(Map args) { + if (args != null) { + this.optimization = + new GraphQLScooterOptimizationInput((Map) args.get("optimization")); + this.reluctance = (Double) args.get("reluctance"); + this.rental = + new GraphQLScooterRentalPreferencesInput((Map) args.get("rental")); + this.speed = (Double) args.get("speed"); + } + } + + public GraphQLScooterOptimizationInput getGraphQLOptimization() { + return this.optimization; + } + + public Double getGraphQLReluctance() { + return this.reluctance; + } + + public GraphQLScooterRentalPreferencesInput getGraphQLRental() { + return this.rental; + } + + public Double getGraphQLSpeed() { + return this.speed; + } + + public void setGraphQLOptimization(GraphQLScooterOptimizationInput optimization) { + this.optimization = optimization; + } + + public void setGraphQLReluctance(Double reluctance) { + this.reluctance = reluctance; + } + + public void setGraphQLRental(GraphQLScooterRentalPreferencesInput rental) { + this.rental = rental; + } + + public void setGraphQLSpeed(Double speed) { + this.speed = speed; + } + } + + public static class GraphQLScooterRentalPreferencesInput { + + private List allowedNetworks; + private List bannedNetworks; + private GraphQLDestinationScooterPolicyInput destinationScooterPolicy; + + public GraphQLScooterRentalPreferencesInput(Map args) { + if (args != null) { + this.allowedNetworks = (List) args.get("allowedNetworks"); + this.bannedNetworks = (List) args.get("bannedNetworks"); + this.destinationScooterPolicy = + new GraphQLDestinationScooterPolicyInput( + (Map) args.get("destinationScooterPolicy") + ); + } + } + + public List getGraphQLAllowedNetworks() { + return this.allowedNetworks; + } + + public List getGraphQLBannedNetworks() { + return this.bannedNetworks; + } + + public GraphQLDestinationScooterPolicyInput getGraphQLDestinationScooterPolicy() { + return this.destinationScooterPolicy; + } + + public void setGraphQLAllowedNetworks(List allowedNetworks) { + this.allowedNetworks = allowedNetworks; + } + + public void setGraphQLBannedNetworks(List bannedNetworks) { + this.bannedNetworks = bannedNetworks; + } + + public void setGraphQLDestinationScooterPolicy( + GraphQLDestinationScooterPolicyInput destinationScooterPolicy + ) { + this.destinationScooterPolicy = destinationScooterPolicy; + } + } + public static class GraphQLStopAlertsArgs { private List types; @@ -3133,6 +4455,99 @@ public void setGraphQLLanguage(String language) { } } + public static class GraphQLTimetablePreferencesInput { + + private Boolean excludeRealTimeUpdates; + private Boolean includePlannedCancellations; + private Boolean includeRealTimeCancellations; + + public GraphQLTimetablePreferencesInput(Map args) { + if (args != null) { + this.excludeRealTimeUpdates = (Boolean) args.get("excludeRealTimeUpdates"); + this.includePlannedCancellations = (Boolean) args.get("includePlannedCancellations"); + this.includeRealTimeCancellations = (Boolean) args.get("includeRealTimeCancellations"); + } + } + + public Boolean getGraphQLExcludeRealTimeUpdates() { + return this.excludeRealTimeUpdates; + } + + public Boolean getGraphQLIncludePlannedCancellations() { + return this.includePlannedCancellations; + } + + public Boolean getGraphQLIncludeRealTimeCancellations() { + return this.includeRealTimeCancellations; + } + + public void setGraphQLExcludeRealTimeUpdates(Boolean excludeRealTimeUpdates) { + this.excludeRealTimeUpdates = excludeRealTimeUpdates; + } + + public void setGraphQLIncludePlannedCancellations(Boolean includePlannedCancellations) { + this.includePlannedCancellations = includePlannedCancellations; + } + + public void setGraphQLIncludeRealTimeCancellations(Boolean includeRealTimeCancellations) { + this.includeRealTimeCancellations = includeRealTimeCancellations; + } + } + + public static class GraphQLTransferPreferencesInput { + + private org.opentripplanner.framework.model.Cost cost; + private Integer maximumAdditionalTransfers; + private Integer maximumTransfers; + private java.time.Duration slack; + + public GraphQLTransferPreferencesInput(Map args) { + if (args != null) { + this.cost = (org.opentripplanner.framework.model.Cost) args.get("cost"); + this.maximumAdditionalTransfers = (Integer) args.get("maximumAdditionalTransfers"); + this.maximumTransfers = (Integer) args.get("maximumTransfers"); + this.slack = (java.time.Duration) args.get("slack"); + } + } + + public org.opentripplanner.framework.model.Cost getGraphQLCost() { + return this.cost; + } + + public Integer getGraphQLMaximumAdditionalTransfers() { + return this.maximumAdditionalTransfers; + } + + public Integer getGraphQLMaximumTransfers() { + return this.maximumTransfers; + } + + public java.time.Duration getGraphQLSlack() { + return this.slack; + } + + public void setGraphQLCost(org.opentripplanner.framework.model.Cost cost) { + this.cost = cost; + } + + public void setGraphQLMaximumAdditionalTransfers(Integer maximumAdditionalTransfers) { + this.maximumAdditionalTransfers = maximumAdditionalTransfers; + } + + public void setGraphQLMaximumTransfers(Integer maximumTransfers) { + this.maximumTransfers = maximumTransfers; + } + + public void setGraphQLSlack(java.time.Duration slack) { + this.slack = slack; + } + } + + /** + * Transit modes include modes that are used within organized transportation networks + * run by public transportation authorities, taxi companies etc. + * Equivalent to GTFS route_type or to NeTEx TransportMode. + */ public enum GraphQLTransitMode { AIRPLANE, BUS, @@ -3150,6 +4565,76 @@ public enum GraphQLTransitMode { TROLLEYBUS, } + public static class GraphQLTransitModePreferenceCostInput { + + private Double reluctance; + + public GraphQLTransitModePreferenceCostInput(Map args) { + if (args != null) { + this.reluctance = (Double) args.get("reluctance"); + } + } + + public Double getGraphQLReluctance() { + return this.reluctance; + } + + public void setGraphQLReluctance(Double reluctance) { + this.reluctance = reluctance; + } + } + + public static class GraphQLTransitPreferencesInput { + + private GraphQLAlightPreferencesInput alight; + private GraphQLBoardPreferencesInput board; + private GraphQLTimetablePreferencesInput timetable; + private GraphQLTransferPreferencesInput transfer; + + public GraphQLTransitPreferencesInput(Map args) { + if (args != null) { + this.alight = new GraphQLAlightPreferencesInput((Map) args.get("alight")); + this.board = new GraphQLBoardPreferencesInput((Map) args.get("board")); + this.timetable = + new GraphQLTimetablePreferencesInput((Map) args.get("timetable")); + this.transfer = + new GraphQLTransferPreferencesInput((Map) args.get("transfer")); + } + } + + public GraphQLAlightPreferencesInput getGraphQLAlight() { + return this.alight; + } + + public GraphQLBoardPreferencesInput getGraphQLBoard() { + return this.board; + } + + public GraphQLTimetablePreferencesInput getGraphQLTimetable() { + return this.timetable; + } + + public GraphQLTransferPreferencesInput getGraphQLTransfer() { + return this.transfer; + } + + public void setGraphQLAlight(GraphQLAlightPreferencesInput alight) { + this.alight = alight; + } + + public void setGraphQLBoard(GraphQLBoardPreferencesInput board) { + this.board = board; + } + + public void setGraphQLTimetable(GraphQLTimetablePreferencesInput timetable) { + this.timetable = timetable; + } + + public void setGraphQLTransfer(GraphQLTransferPreferencesInput transfer) { + this.transfer = transfer; + } + } + public static class GraphQLTransportModeInput { private GraphQLMode mode; @@ -3159,12 +4644,12 @@ public GraphQLTransportModeInput(Map args) { if (args != null) { if (args.get("mode") instanceof GraphQLMode) { this.mode = (GraphQLMode) args.get("mode"); - } else { + } else if (args.get("mode") != null) { this.mode = GraphQLMode.valueOf((String) args.get("mode")); } if (args.get("qualifier") instanceof GraphQLQualifier) { this.qualifier = (GraphQLQualifier) args.get("qualifier"); - } else { + } else if (args.get("qualifier") != null) { this.qualifier = GraphQLQualifier.valueOf((String) args.get("qualifier")); } } @@ -3187,6 +4672,84 @@ public void setGraphQLQualifier(GraphQLQualifier qualifier) { } } + public static class GraphQLTriangleCyclingFactorsInput { + + private Double flatness; + private Double safety; + private Double time; + + public GraphQLTriangleCyclingFactorsInput(Map args) { + if (args != null) { + this.flatness = (Double) args.get("flatness"); + this.safety = (Double) args.get("safety"); + this.time = (Double) args.get("time"); + } + } + + public Double getGraphQLFlatness() { + return this.flatness; + } + + public Double getGraphQLSafety() { + return this.safety; + } + + public Double getGraphQLTime() { + return this.time; + } + + public void setGraphQLFlatness(Double flatness) { + this.flatness = flatness; + } + + public void setGraphQLSafety(Double safety) { + this.safety = safety; + } + + public void setGraphQLTime(Double time) { + this.time = time; + } + } + + public static class GraphQLTriangleScooterFactorsInput { + + private Double flatness; + private Double safety; + private Double time; + + public GraphQLTriangleScooterFactorsInput(Map args) { + if (args != null) { + this.flatness = (Double) args.get("flatness"); + this.safety = (Double) args.get("safety"); + this.time = (Double) args.get("time"); + } + } + + public Double getGraphQLFlatness() { + return this.flatness; + } + + public Double getGraphQLSafety() { + return this.safety; + } + + public Double getGraphQLTime() { + return this.time; + } + + public void setGraphQLFlatness(Double flatness) { + this.flatness = flatness; + } + + public void setGraphQLSafety(Double safety) { + this.safety = safety; + } + + public void setGraphQLTime(Double time) { + this.time = time; + } + } + public static class GraphQLTripAlertsArgs { private List types; @@ -3408,9 +4971,77 @@ public enum GraphQLVertexType { TRANSIT, } + public static class GraphQLWalkPreferencesInput { + + private org.opentripplanner.framework.model.Cost boardCost; + private Double reluctance; + private Double safetyFactor; + private Double speed; + + public GraphQLWalkPreferencesInput(Map args) { + if (args != null) { + this.boardCost = (org.opentripplanner.framework.model.Cost) args.get("boardCost"); + this.reluctance = (Double) args.get("reluctance"); + this.safetyFactor = (Double) args.get("safetyFactor"); + this.speed = (Double) args.get("speed"); + } + } + + public org.opentripplanner.framework.model.Cost getGraphQLBoardCost() { + return this.boardCost; + } + + public Double getGraphQLReluctance() { + return this.reluctance; + } + + public Double getGraphQLSafetyFactor() { + return this.safetyFactor; + } + + public Double getGraphQLSpeed() { + return this.speed; + } + + public void setGraphQLBoardCost(org.opentripplanner.framework.model.Cost boardCost) { + this.boardCost = boardCost; + } + + public void setGraphQLReluctance(Double reluctance) { + this.reluctance = reluctance; + } + + public void setGraphQLSafetyFactor(Double safetyFactor) { + this.safetyFactor = safetyFactor; + } + + public void setGraphQLSpeed(Double speed) { + this.speed = speed; + } + } + public enum GraphQLWheelchairBoarding { NOT_POSSIBLE, NO_INFORMATION, POSSIBLE, } + + public static class GraphQLWheelchairPreferencesInput { + + private Boolean enabled; + + public GraphQLWheelchairPreferencesInput(Map args) { + if (args != null) { + this.enabled = (Boolean) args.get("enabled"); + } + } + + public Boolean getGraphQLEnabled() { + return this.enabled; + } + + public void setGraphQLEnabled(Boolean enabled) { + this.enabled = enabled; + } + } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index 70455ec3dad..ff9af6f6aa0 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -26,8 +26,15 @@ config: Polyline: String GeoJson: org.locationtech.jts.geom.Geometry Grams: org.opentripplanner.framework.model.Grams - OffsetDateTime: java.time.OffsetDateTime Duration: java.time.Duration + Cost: org.opentripplanner.framework.model.Cost + CoordinateValue: Double + Locale: java.util.Locale + OffsetDateTime: java.time.OffsetDateTime + Speed: Double + Reluctance: Double + Ratio: Double + mappers: AbsoluteDirection: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLAbsoluteDirection#GraphQLAbsoluteDirection Agency: org.opentripplanner.transit.model.organization.Agency#Agency @@ -73,6 +80,9 @@ config: placeAtDistanceConnection: graphql.relay.Connection#Connection placeAtDistanceEdge: graphql.relay.Edge#Edge Plan: graphql.execution.DataFetcherResult + PlanConnection: graphql.execution.DataFetcherResult + PlanEdge: graphql.relay.DefaultEdge#DefaultEdge + PlanPageInfo: org.opentripplanner.apis.gtfs.model.PlanPageInfo#PlanPageInfo RealtimeState: String RelativeDirection: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRelativeDirection#GraphQLRelativeDirection Route: org.opentripplanner.transit.model.network.Route#Route diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json b/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json index db865eaa003..6a840640ca9 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/package.json @@ -12,7 +12,7 @@ "dependencies": { "@graphql-codegen/add": "5.0.2", "@graphql-codegen/cli": "5.0.2", - "@graphql-codegen/java": "4.0.0", + "@graphql-codegen/java": "4.0.1", "@graphql-codegen/java-resolvers": "3.0.0", "graphql": "16.8.1" } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock b/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock index fffe602db18..77829ecc911 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/yarn.lock @@ -810,10 +810,10 @@ "@graphql-codegen/visitor-plugin-common" "2.13.1" tslib "~2.6.0" -"@graphql-codegen/java@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@graphql-codegen/java/-/java-4.0.0.tgz#245e4403f19c390d5b6a03956558e34c3c4d7848" - integrity sha512-7pxwkgm0eFRDpq6PZx3n2tErBLr1wsG75USvCDnkkoz0145UCErk9GMAEfYgGx1mAm9+oUT+1wjZozjEYWjSow== +"@graphql-codegen/java@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@graphql-codegen/java/-/java-4.0.1.tgz#2f62a7361e702691500c83903c6c6f829496fa5c" + integrity sha512-p51hsOgnuJInSNy+X2Vb+Su9U41iK1xU0YeciVx7JSnOyiT5nQRuDzsjDFvUIL9YZTM+KGbsomaRISOPM6Yq/Q== dependencies: "@graphql-codegen/java-common" "^3.0.0" "@graphql-codegen/plugin-helpers" "^3.0.0" diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/TransitModeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/TransitModeMapper.java new file mode 100644 index 00000000000..2128263d8de --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/TransitModeMapper.java @@ -0,0 +1,29 @@ +package org.opentripplanner.apis.gtfs.mapping; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.transit.model.basic.TransitMode; + +/** + * Maps transit mode from API to internal model. + */ +public class TransitModeMapper { + + public static TransitMode map(GraphQLTypes.GraphQLTransitMode mode) { + return switch (mode) { + case AIRPLANE -> TransitMode.AIRPLANE; + case BUS -> TransitMode.BUS; + case CABLE_CAR -> TransitMode.CABLE_CAR; + case COACH -> TransitMode.COACH; + case FERRY -> TransitMode.FERRY; + case FUNICULAR -> TransitMode.FUNICULAR; + case GONDOLA -> TransitMode.GONDOLA; + case RAIL -> TransitMode.RAIL; + case SUBWAY -> TransitMode.SUBWAY; + case TRAM -> TransitMode.TRAM; + case CARPOOL -> TransitMode.CARPOOL; + case TAXI -> TransitMode.TAXI; + case TROLLEYBUS -> TransitMode.TROLLEYBUS; + case MONORAIL -> TransitMode.MONORAIL; + }; + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java new file mode 100644 index 00000000000..ac4c90a1a56 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/AccessModeMapper.java @@ -0,0 +1,24 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.StreetMode; + +/** + * Maps access street mode from API to internal model. + */ +public class AccessModeMapper { + + public static StreetMode map(GraphQLTypes.GraphQLPlanAccessMode mode) { + return switch (mode) { + case BICYCLE -> StreetMode.BIKE; + case BICYCLE_RENTAL -> StreetMode.BIKE_RENTAL; + case BICYCLE_PARKING -> StreetMode.BIKE_TO_PARK; + case CAR_RENTAL -> StreetMode.CAR_RENTAL; + case CAR_PARKING -> StreetMode.CAR_TO_PARK; + case CAR_DROP_OFF -> StreetMode.CAR_PICKUP; + case FLEX -> StreetMode.FLEXIBLE; + case SCOOTER_RENTAL -> StreetMode.SCOOTER_RENTAL; + case WALK -> StreetMode.WALK; + }; + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ArgumentUtils.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ArgumentUtils.java new file mode 100644 index 00000000000..b04affeaaf2 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ArgumentUtils.java @@ -0,0 +1,124 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import graphql.schema.DataFetchingEnvironment; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; + +public class ArgumentUtils { + + /** + * This methods returns list of modes and their costs from the argument structure: + * modes.transit.transit. This methods circumvents a bug in graphql-codegen as getting a list of + * input objects is not possible through using the generated types in {@link GraphQLTypes}. + *

+ * TODO this ugliness can be removed when the bug gets fixed + */ + @Nullable + static List> getTransitModes(DataFetchingEnvironment environment) { + if (environment.containsArgument("modes")) { + Map modesArgs = environment.getArgument("modes"); + if (modesArgs.containsKey("transit")) { + Map transitArgs = (Map) modesArgs.get("transit"); + if (transitArgs.containsKey("transit")) { + return (List>) transitArgs.get("transit"); + } + } + } + return null; + } + + /** + * This methods returns parking preferences of the given type from argument structure: + * preferences.street.type.parking. This methods circumvents a bug in graphql-codegen as getting a + * list of input objects is not possible through using the generated types in + * {@link GraphQLTypes}. + *

+ * TODO this ugliness can be removed when the bug gets fixed + */ + @Nullable + static Map getParking(DataFetchingEnvironment environment, String type) { + return ( + (Map) ( + (Map) ( + (Map) ((Map) environment.getArgument("preferences")).get( + "street" + ) + ).get(type) + ).get("parking") + ); + } + + /** + * This methods returns required/banned parking tags of the given type from argument structure: + * preferences.street.type.parking.filters. This methods circumvents a bug in graphql-codegen as + * getting a list of input objects is not possible through using the generated types in + * {@link GraphQLTypes}. + *

+ * TODO this ugliness can be removed when the bug gets fixed + */ + @Nonnull + static Collection> getParkingFilters( + DataFetchingEnvironment environment, + String type + ) { + var parking = getParking(environment, type); + var filters = parking != null && parking.containsKey("filters") + ? getParking(environment, type).get("filters") + : null; + return filters != null ? (Collection>) filters : List.of(); + } + + /** + * This methods returns preferred/unpreferred parking tags of the given type from argument + * structure: preferences.street.type.parking.preferred. This methods circumvents a bug in + * graphql-codegen as getting a list of input objects is not possible through using the generated + * types in {@link GraphQLTypes}. + *

+ * TODO this ugliness can be removed when the bug gets fixed + */ + @Nonnull + static Collection> getParkingPreferred( + DataFetchingEnvironment environment, + String type + ) { + var parking = getParking(environment, type); + var preferred = parking != null && parking.containsKey("preferred") + ? getParking(environment, type).get("preferred") + : null; + return preferred != null ? (Collection>) preferred : List.of(); + } + + static Set parseNotFilters(Collection> filters) { + return parseFilters(filters, "not"); + } + + static Set parseSelectFilters(Collection> filters) { + return parseFilters(filters, "select"); + } + + @Nonnull + private static Set parseFilters(Collection> filters, String key) { + return filters + .stream() + .flatMap(f -> + parseOperation((Collection>>) f.getOrDefault(key, List.of())) + ) + .collect(Collectors.toSet()); + } + + private static Stream parseOperation(Collection>> map) { + return map + .stream() + .flatMap(f -> { + var tags = f.getOrDefault("tags", List.of()); + return tags.stream(); + }); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/BicyclePreferencesMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/BicyclePreferencesMapper.java new file mode 100644 index 00000000000..7205757a569 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/BicyclePreferencesMapper.java @@ -0,0 +1,166 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.getParkingFilters; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.getParkingPreferred; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.parseNotFilters; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.parseSelectFilters; + +import graphql.schema.DataFetchingEnvironment; +import java.util.Set; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.routing.api.request.preference.BikePreferences; +import org.opentripplanner.routing.api.request.preference.VehicleParkingPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleRentalPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleWalkingPreferences; + +public class BicyclePreferencesMapper { + + static void setBicyclePreferences( + BikePreferences.Builder preferences, + GraphQLTypes.GraphQLBicyclePreferencesInput args, + DataFetchingEnvironment environment + ) { + if (args == null) { + return; + } + + var speed = args.getGraphQLSpeed(); + if (speed != null) { + preferences.withSpeed(speed); + } + var reluctance = args.getGraphQLReluctance(); + if (reluctance != null) { + preferences.withReluctance(reluctance); + } + var boardCost = args.getGraphQLBoardCost(); + if (boardCost != null) { + preferences.withBoardCost(boardCost.toSeconds()); + } + preferences.withWalking(walk -> setBicycleWalkPreferences(walk, args.getGraphQLWalk())); + preferences.withParking(parking -> + setBicycleParkingPreferences(parking, args.getGraphQLParking(), environment) + ); + preferences.withRental(rental -> setBicycleRentalPreferences(rental, args.getGraphQLRental())); + setBicycleOptimization(preferences, args.getGraphQLOptimization()); + } + + private static void setBicycleWalkPreferences( + VehicleWalkingPreferences.Builder preferences, + GraphQLTypes.GraphQLBicycleWalkPreferencesInput args + ) { + if (args == null) { + return; + } + + var speed = args.getGraphQLSpeed(); + if (speed != null) { + preferences.withSpeed(speed); + } + var mountTime = args.getGraphQLMountDismountTime(); + if (mountTime != null) { + preferences.withMountDismountTime( + DurationUtils.requireNonNegativeShort(mountTime, "bicycle mount dismount time") + ); + } + var cost = args.getGraphQLCost(); + if (cost != null) { + var reluctance = cost.getGraphQLReluctance(); + if (reluctance != null) { + preferences.withReluctance(reluctance); + } + var mountCost = cost.getGraphQLMountDismountCost(); + if (mountCost != null) { + preferences.withMountDismountCost(mountCost.toSeconds()); + } + } + } + + private static void setBicycleParkingPreferences( + VehicleParkingPreferences.Builder preferences, + GraphQLTypes.GraphQLBicycleParkingPreferencesInput args, + DataFetchingEnvironment environment + ) { + if (args == null) { + return; + } + + var unpreferredCost = args.getGraphQLUnpreferredCost(); + if (unpreferredCost != null) { + preferences.withUnpreferredVehicleParkingTagCost(unpreferredCost.toSeconds()); + } + var filters = getParkingFilters(environment, "bicycle"); + preferences.withRequiredVehicleParkingTags(parseSelectFilters(filters)); + preferences.withBannedVehicleParkingTags(parseNotFilters(filters)); + var preferred = getParkingPreferred(environment, "bicycle"); + preferences.withPreferredVehicleParkingTags(parseSelectFilters(preferred)); + preferences.withNotPreferredVehicleParkingTags(parseNotFilters(preferred)); + } + + private static void setBicycleRentalPreferences( + VehicleRentalPreferences.Builder preferences, + GraphQLTypes.GraphQLBicycleRentalPreferencesInput args + ) { + if (args == null) { + return; + } + + var allowedNetworks = args.getGraphQLAllowedNetworks(); + if (allowedNetworks != null) { + if (allowedNetworks.isEmpty()) { + throw new IllegalArgumentException("Allowed bicycle rental networks must not be empty."); + } + preferences.withAllowedNetworks(Set.copyOf(allowedNetworks)); + } + var bannedNetworks = args.getGraphQLBannedNetworks(); + if (bannedNetworks != null) { + preferences.withBannedNetworks(Set.copyOf(bannedNetworks)); + } + var destinationPolicy = args.getGraphQLDestinationBicyclePolicy(); + if (destinationPolicy != null) { + var allowed = destinationPolicy.getGraphQLAllowKeeping(); + if (allowed != null) { + preferences.withAllowArrivingInRentedVehicleAtDestination(allowed); + } + var cost = destinationPolicy.getGraphQLKeepingCost(); + if (cost != null) { + preferences.withArrivingInRentalVehicleAtDestinationCost(cost.toSeconds()); + } + } + } + + private static void setBicycleOptimization( + BikePreferences.Builder preferences, + GraphQLTypes.GraphQLCyclingOptimizationInput args + ) { + if (args == null) { + return; + } + + var type = args.getGraphQLType(); + var mappedType = type != null ? VehicleOptimizationTypeMapper.map(type) : null; + if (mappedType != null) { + preferences.withOptimizeType(mappedType); + } + var triangleArgs = args.getGraphQLTriangle(); + if (isBicycleTriangleSet(triangleArgs)) { + preferences.withForcedOptimizeTriangle(triangle -> { + triangle + .withSlope(triangleArgs.getGraphQLFlatness()) + .withSafety(triangleArgs.getGraphQLSafety()) + .withTime(triangleArgs.getGraphQLTime()); + }); + } + } + + private static boolean isBicycleTriangleSet( + GraphQLTypes.GraphQLTriangleCyclingFactorsInput args + ) { + return ( + args != null && + args.getGraphQLFlatness() != null && + args.getGraphQLSafety() != null && + args.getGraphQLTime() != null + ); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/CarPreferencesMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/CarPreferencesMapper.java new file mode 100644 index 00000000000..01a78153c9b --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/CarPreferencesMapper.java @@ -0,0 +1,77 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.getParkingFilters; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.getParkingPreferred; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.parseNotFilters; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.parseSelectFilters; + +import graphql.schema.DataFetchingEnvironment; +import java.util.Set; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.preference.CarPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleParkingPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleRentalPreferences; + +public class CarPreferencesMapper { + + static void setCarPreferences( + CarPreferences.Builder preferences, + GraphQLTypes.GraphQLCarPreferencesInput args, + DataFetchingEnvironment environment + ) { + if (args == null) { + return; + } + + var reluctance = args.getGraphQLReluctance(); + if (reluctance != null) { + preferences.withReluctance(reluctance); + } + preferences.withParking(parking -> + setCarParkingPreferences(parking, args.getGraphQLParking(), environment) + ); + preferences.withRental(rental -> setCarRentalPreferences(rental, args.getGraphQLRental())); + } + + private static void setCarParkingPreferences( + VehicleParkingPreferences.Builder preferences, + GraphQLTypes.GraphQLCarParkingPreferencesInput args, + DataFetchingEnvironment environment + ) { + if (args == null) { + return; + } + + var unpreferredCost = args.getGraphQLUnpreferredCost(); + if (unpreferredCost != null) { + preferences.withUnpreferredVehicleParkingTagCost(unpreferredCost.toSeconds()); + } + var filters = getParkingFilters(environment, "car"); + preferences.withRequiredVehicleParkingTags(parseSelectFilters(filters)); + preferences.withBannedVehicleParkingTags(parseNotFilters(filters)); + var preferred = getParkingPreferred(environment, "car"); + preferences.withPreferredVehicleParkingTags(parseSelectFilters(preferred)); + preferences.withNotPreferredVehicleParkingTags(parseNotFilters(preferred)); + } + + private static void setCarRentalPreferences( + VehicleRentalPreferences.Builder preferences, + GraphQLTypes.GraphQLCarRentalPreferencesInput args + ) { + if (args == null) { + return; + } + + var allowedNetworks = args.getGraphQLAllowedNetworks(); + if (allowedNetworks != null) { + if (allowedNetworks.isEmpty()) { + throw new IllegalArgumentException("Allowed car rental networks must not be empty."); + } + preferences.withAllowedNetworks(Set.copyOf(allowedNetworks)); + } + var bannedNetworks = args.getGraphQLBannedNetworks(); + if (bannedNetworks != null) { + preferences.withBannedNetworks(Set.copyOf(bannedNetworks)); + } + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/DirectModeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/DirectModeMapper.java new file mode 100644 index 00000000000..e34e7ad2aed --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/DirectModeMapper.java @@ -0,0 +1,24 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.StreetMode; + +/** + * Maps direct street mode from API to internal model. + */ +public class DirectModeMapper { + + public static StreetMode map(GraphQLTypes.GraphQLPlanDirectMode mode) { + return switch (mode) { + case BICYCLE -> StreetMode.BIKE; + case BICYCLE_RENTAL -> StreetMode.BIKE_RENTAL; + case BICYCLE_PARKING -> StreetMode.BIKE_TO_PARK; + case CAR -> StreetMode.CAR; + case CAR_RENTAL -> StreetMode.CAR_RENTAL; + case CAR_PARKING -> StreetMode.CAR_TO_PARK; + case FLEX -> StreetMode.FLEXIBLE; + case SCOOTER_RENTAL -> StreetMode.SCOOTER_RENTAL; + case WALK -> StreetMode.WALK; + }; + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java new file mode 100644 index 00000000000..f03b160ac97 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/EgressModeMapper.java @@ -0,0 +1,22 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.StreetMode; + +/** + * Maps egress street mode from API to internal model. + */ +public class EgressModeMapper { + + public static StreetMode map(GraphQLTypes.GraphQLPlanEgressMode mode) { + return switch (mode) { + case BICYCLE -> StreetMode.BIKE; + case BICYCLE_RENTAL -> StreetMode.BIKE_RENTAL; + case CAR_RENTAL -> StreetMode.CAR_RENTAL; + case CAR_PICKUP -> StreetMode.CAR_PICKUP; + case FLEX -> StreetMode.FLEXIBLE; + case SCOOTER_RENTAL -> StreetMode.SCOOTER_RENTAL; + case WALK -> StreetMode.WALK; + }; + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ItineraryFilterDebugProfileMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ItineraryFilterDebugProfileMapper.java new file mode 100644 index 00000000000..7a83388ff1e --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ItineraryFilterDebugProfileMapper.java @@ -0,0 +1,21 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.preference.ItineraryFilterDebugProfile; + +/** + * Maps ItineraryFilterDebugProfile from API to internal model. + */ +public class ItineraryFilterDebugProfileMapper { + + public static ItineraryFilterDebugProfile map( + GraphQLTypes.GraphQLItineraryFilterDebugProfile profile + ) { + return switch (profile) { + case LIMIT_TO_NUMBER_OF_ITINERARIES -> ItineraryFilterDebugProfile.LIMIT_TO_NUM_OF_ITINERARIES; + case LIMIT_TO_SEARCH_WINDOW -> ItineraryFilterDebugProfile.LIMIT_TO_SEARCH_WINDOW; + case LIST_ALL -> ItineraryFilterDebugProfile.LIST_ALL; + case OFF -> ItineraryFilterDebugProfile.OFF; + }; + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapper.java similarity index 93% rename from src/main/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapper.java rename to src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapper.java index 4cd6c04b044..f602871162f 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapper.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapper.java @@ -1,4 +1,7 @@ -package org.opentripplanner.apis.gtfs.mapping; +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.parseNotFilters; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.parseSelectFilters; import graphql.schema.DataFetchingEnvironment; import java.time.Duration; @@ -7,10 +10,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.Nonnull; import org.opentripplanner.api.common.LocationStringParser; import org.opentripplanner.api.parameter.QualifiedMode; @@ -32,7 +33,7 @@ import org.opentripplanner.transit.model.basic.MainAndSubMode; import org.opentripplanner.transit.model.basic.TransitMode; -public class RouteRequestMapper { +public class LegacyRouteRequestMapper { @Nonnull public static RouteRequest toRouteRequest( @@ -256,33 +257,6 @@ public static RouteRequest toRouteRequest( return request; } - private static Set parseNotFilters(Collection> filters) { - return parseFilters(filters, "not"); - } - - private static Set parseSelectFilters(Collection> filters) { - return parseFilters(filters, "select"); - } - - @Nonnull - private static Set parseFilters(Collection> filters, String key) { - return filters - .stream() - .flatMap(f -> - parseOperation((Collection>>) f.getOrDefault(key, List.of())) - ) - .collect(Collectors.toSet()); - } - - private static Stream parseOperation(Collection>> map) { - return map - .stream() - .flatMap(f -> { - var tags = f.getOrDefault("tags", List.of()); - return tags.stream(); - }); - } - private static boolean hasArgument(Map m, String name) { return m.containsKey(name) && m.get(name) != null; } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java new file mode 100644 index 00000000000..32d3456df57 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ModePreferencesMapper.java @@ -0,0 +1,172 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.getTransitModes; + +import graphql.schema.DataFetchingEnvironment; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.mapping.TransitModeMapper; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.routing.api.request.request.JourneyRequest; +import org.opentripplanner.routing.api.request.request.filter.SelectRequest; +import org.opentripplanner.routing.api.request.request.filter.TransitFilterRequest; +import org.opentripplanner.transit.model.basic.MainAndSubMode; + +public class ModePreferencesMapper { + + /** + * TODO this doesn't support multiple street modes yet + */ + static void setModes( + JourneyRequest journey, + GraphQLTypes.GraphQLPlanModesInput modesInput, + DataFetchingEnvironment environment + ) { + var direct = modesInput.getGraphQLDirect(); + if (Boolean.TRUE.equals(modesInput.getGraphQLTransitOnly())) { + journey.direct().setMode(StreetMode.NOT_SET); + } else if (direct != null) { + if (direct.isEmpty()) { + throw new IllegalArgumentException("Direct modes must not be empty."); + } + var streetModes = direct.stream().map(DirectModeMapper::map).toList(); + journey.direct().setMode(getStreetMode(streetModes)); + } + + var transit = modesInput.getGraphQLTransit(); + if (Boolean.TRUE.equals(modesInput.getGraphQLDirectOnly())) { + journey.transit().disable(); + } else if (transit != null) { + var access = transit.getGraphQLAccess(); + if (access != null) { + if (access.isEmpty()) { + throw new IllegalArgumentException("Access modes must not be empty."); + } + var streetModes = access.stream().map(AccessModeMapper::map).toList(); + journey.access().setMode(getStreetMode(streetModes)); + } + + var egress = transit.getGraphQLEgress(); + if (egress != null) { + if (egress.isEmpty()) { + throw new IllegalArgumentException("Egress modes must not be empty."); + } + var streetModes = egress.stream().map(EgressModeMapper::map).toList(); + journey.egress().setMode(getStreetMode(streetModes)); + } + + var transfer = transit.getGraphQLTransfer(); + if (transfer != null) { + if (transfer.isEmpty()) { + throw new IllegalArgumentException("Transfer modes must not be empty."); + } + var streetModes = transfer.stream().map(TransferModeMapper::map).toList(); + journey.transfer().setMode(getStreetMode(streetModes)); + } + validateStreetModes(journey); + + var transitModes = getTransitModes(environment); + if (transitModes != null) { + if (transitModes.isEmpty()) { + throw new IllegalArgumentException("Transit modes must not be empty."); + } + var filterRequestBuilder = TransitFilterRequest.of(); + var mainAndSubModes = transitModes + .stream() + .map(mode -> + new MainAndSubMode( + TransitModeMapper.map( + GraphQLTypes.GraphQLTransitMode.valueOf((String) mode.get("mode")) + ) + ) + ) + .toList(); + filterRequestBuilder.addSelect( + SelectRequest.of().withTransportModes(mainAndSubModes).build() + ); + journey.transit().setFilters(List.of(filterRequestBuilder.build())); + } + } + } + + /** + * Current support: + * 1. If only one mode is defined, it needs to be WALK, BICYCLE, CAR or some parking mode. + * 2. If two modes are defined, they can't be BICYCLE or CAR, and WALK needs to be one of them. + * 3. More than two modes can't be defined for the same leg. + *

+ * TODO future support: + * 1. Any mode can be defined alone. If it's not used in a leg, the leg gets filtered away. + * 2. If two modes are defined, they can't be BICYCLE or CAR. Usually WALK is required as the second + * mode but in some cases it's possible to define other modes as well such as BICYCLE_RENTAL together + * with SCOOTER_RENTAL. In that case, legs which don't use BICYCLE_RENTAL or SCOOTER_RENTAL would be filtered + * out. + * 3. When more than two modes are used, some combinations are supported such as WALK, BICYCLE_RENTAL and SCOOTER_RENTAL. + */ + private static StreetMode getStreetMode(List modes) { + if (modes.size() > 2) { + throw new IllegalArgumentException( + "Only one or two modes can be specified for a leg, got: %.".formatted(modes) + ); + } + if (modes.size() == 1) { + var mode = modes.getFirst(); + // TODO in the future, we will support defining other modes alone as well and filter out legs + // which don't contain the only specified mode as opposed to also returning legs which contain + // only walking. + if (!isAlwaysPresentInLeg(mode)) { + throw new IllegalArgumentException( + "For the time being, %s needs to be combined with WALK mode for the same leg.".formatted( + mode + ) + ); + } + return mode; + } + if (modes.contains(StreetMode.BIKE)) { + throw new IllegalArgumentException( + "Bicycle can't be combined with other modes for the same leg: %s.".formatted(modes) + ); + } + if (modes.contains(StreetMode.CAR)) { + throw new IllegalArgumentException( + "Car can't be combined with other modes for the same leg: %s.".formatted(modes) + ); + } + if (!modes.contains(StreetMode.WALK)) { + throw new IllegalArgumentException( + "For the time being, WALK needs to be added as a mode for a leg when using %s and these two can't be used in the same leg.".formatted( + modes + ) + ); + } + // Walk is currently always used as an implied mode when mode is not car. + return modes.stream().filter(mode -> mode != StreetMode.WALK).findFirst().get(); + } + + private static boolean isAlwaysPresentInLeg(StreetMode mode) { + return ( + mode == StreetMode.BIKE || + mode == StreetMode.CAR || + mode == StreetMode.WALK || + mode.includesParking() + ); + } + + /** + * TODO this doesn't support multiple street modes yet + */ + private static void validateStreetModes(JourneyRequest journey) { + Set modes = new HashSet(); + modes.add(journey.access().mode()); + modes.add(journey.egress().mode()); + modes.add(journey.transfer().mode()); + if (modes.contains(StreetMode.BIKE) && modes.size() != 1) { + throw new IllegalArgumentException( + "If BICYCLE is used for access, egress or transfer, then it should be used for all." + ); + } + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/OptimizationTypeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/OptimizationTypeMapper.java similarity index 91% rename from src/main/java/org/opentripplanner/apis/gtfs/mapping/OptimizationTypeMapper.java rename to src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/OptimizationTypeMapper.java index 7f4f5200256..dc17891f9d2 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/mapping/OptimizationTypeMapper.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/OptimizationTypeMapper.java @@ -1,4 +1,4 @@ -package org.opentripplanner.apis.gtfs.mapping; +package org.opentripplanner.apis.gtfs.mapping.routerequest; import javax.annotation.Nonnull; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapper.java new file mode 100644 index 00000000000..ada358d98b3 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapper.java @@ -0,0 +1,183 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static org.opentripplanner.apis.gtfs.mapping.routerequest.BicyclePreferencesMapper.setBicyclePreferences; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.CarPreferencesMapper.setCarPreferences; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ModePreferencesMapper.setModes; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ScooterPreferencesMapper.setScooterPreferences; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.TransitPreferencesMapper.setTransitPreferences; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.WalkPreferencesMapper.setWalkPreferences; + +import graphql.schema.DataFetchingEnvironment; +import java.time.Instant; +import javax.annotation.Nonnull; +import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.framework.graphql.GraphQLUtils; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.model.GenericLocation; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.preference.ItineraryFilterPreferences; +import org.opentripplanner.routing.api.request.preference.RoutingPreferences; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +public class RouteRequestMapper { + + @Nonnull + public static RouteRequest toRouteRequest( + DataFetchingEnvironment environment, + GraphQLRequestContext context + ) { + RouteRequest request = context.defaultRouteRequest(); + var args = new GraphQLTypes.GraphQLQueryTypePlanConnectionArgs(environment.getArguments()); + var dateTime = args.getGraphQLDateTime(); + if (dateTime.getGraphQLEarliestDeparture() != null) { + request.setDateTime(args.getGraphQLDateTime().getGraphQLEarliestDeparture().toInstant()); + } else if (dateTime.getGraphQLLatestArrival() != null) { + request.setDateTime(args.getGraphQLDateTime().getGraphQLLatestArrival().toInstant()); + request.setArriveBy(true); + } else { + request.setDateTime(Instant.now()); + } + request.setFrom(parseGenericLocation(args.getGraphQLOrigin())); + request.setTo(parseGenericLocation(args.getGraphQLDestination())); + request.setLocale(GraphQLUtils.getLocale(environment, args.getGraphQLLocale())); + if (args.getGraphQLSearchWindow() != null) { + request.setSearchWindow( + DurationUtils.requireNonNegativeLong(args.getGraphQLSearchWindow(), "searchWindow") + ); + } + + if (args.getGraphQLBefore() != null) { + request.setPageCursorFromEncoded(args.getGraphQLBefore()); + if (args.getGraphQLLast() != null) { + request.setNumItineraries(args.getGraphQLLast()); + } + } else if (args.getGraphQLAfter() != null) { + request.setPageCursorFromEncoded(args.getGraphQLAfter()); + if (args.getGraphQLFirst() != null) { + request.setNumItineraries(args.getGraphQLFirst()); + } + } else if (args.getGraphQLFirst() != null) { + request.setNumItineraries(args.getGraphQLFirst()); + } + + request.withPreferences(preferences -> setPreferences(preferences, request, args, environment)); + + setModes(request.journey(), args.getGraphQLModes(), environment); + + return request; + } + + private static void setPreferences( + RoutingPreferences.Builder prefs, + RouteRequest request, + GraphQLTypes.GraphQLQueryTypePlanConnectionArgs args, + DataFetchingEnvironment environment + ) { + var preferenceArgs = args.getGraphQLPreferences(); + prefs.withItineraryFilter(filters -> + setItineraryFilters(filters, args.getGraphQLItineraryFilter()) + ); + prefs.withTransit(transit -> { + prefs.withTransfer(transfer -> setTransitPreferences(transit, transfer, args, environment)); + }); + setStreetPreferences(prefs, request, preferenceArgs.getGraphQLStreet(), environment); + setAccessibilityPreferences(request, preferenceArgs.getGraphQLAccessibility()); + } + + private static void setItineraryFilters( + ItineraryFilterPreferences.Builder filterPreferences, + GraphQLTypes.GraphQLPlanItineraryFilterInput filters + ) { + if (filters.getGraphQLItineraryFilterDebugProfile() != null) { + filterPreferences.withDebug( + ItineraryFilterDebugProfileMapper.map(filters.getGraphQLItineraryFilterDebugProfile()) + ); + } + if (filters.getGraphQLGroupSimilarityKeepOne() != null) { + filterPreferences.withGroupSimilarityKeepOne(filters.getGraphQLGroupSimilarityKeepOne()); + } + if (filters.getGraphQLGroupSimilarityKeepThree() != null) { + filterPreferences.withGroupSimilarityKeepThree(filters.getGraphQLGroupSimilarityKeepThree()); + } + if (filters.getGraphQLGroupedOtherThanSameLegsMaxCostMultiplier() != null) { + filterPreferences.withGroupedOtherThanSameLegsMaxCostMultiplier( + filters.getGraphQLGroupedOtherThanSameLegsMaxCostMultiplier() + ); + } + } + + private static void setStreetPreferences( + RoutingPreferences.Builder preferences, + RouteRequest request, + GraphQLTypes.GraphQLPlanStreetPreferencesInput args, + DataFetchingEnvironment environment + ) { + setRentalAvailabilityPreferences(preferences, request); + + if (args == null) { + return; + } + + preferences.withBike(bicycle -> + setBicyclePreferences(bicycle, args.getGraphQLBicycle(), environment) + ); + preferences.withCar(car -> setCarPreferences(car, args.getGraphQLCar(), environment)); + preferences.withScooter(scooter -> setScooterPreferences(scooter, args.getGraphQLScooter())); + preferences.withWalk(walk -> setWalkPreferences(walk, args.getGraphQLWalk())); + } + + private static void setRentalAvailabilityPreferences( + RoutingPreferences.Builder preferences, + RouteRequest request + ) { + preferences.withBike(bike -> + bike.withRental(rental -> rental.withUseAvailabilityInformation(request.isTripPlannedForNow()) + ) + ); + preferences.withCar(car -> + car.withRental(rental -> rental.withUseAvailabilityInformation(request.isTripPlannedForNow())) + ); + preferences.withScooter(scooter -> + scooter.withRental(rental -> + rental.withUseAvailabilityInformation(request.isTripPlannedForNow()) + ) + ); + } + + private static void setAccessibilityPreferences( + RouteRequest request, + GraphQLTypes.GraphQLAccessibilityPreferencesInput preferenceArgs + ) { + if (preferenceArgs != null && preferenceArgs.getGraphQLWheelchair() != null) { + request.setWheelchair(preferenceArgs.getGraphQLWheelchair().getGraphQLEnabled()); + } + } + + private static GenericLocation parseGenericLocation( + GraphQLTypes.GraphQLPlanLabeledLocationInput locationInput + ) { + var stopLocation = locationInput.getGraphQLLocation().getGraphQLStopLocation(); + if (stopLocation.getGraphQLStopLocationId() != null) { + var stopId = stopLocation.getGraphQLStopLocationId(); + if (FeedScopedId.isValidString(stopId)) { + return new GenericLocation( + locationInput.getGraphQLLabel(), + FeedScopedId.parse(stopId), + null, + null + ); + } else { + throw new IllegalArgumentException("Stop id %s is not of valid format.".formatted(stopId)); + } + } + + var coordinate = locationInput.getGraphQLLocation().getGraphQLCoordinate(); + return new GenericLocation( + locationInput.getGraphQLLabel(), + null, + coordinate.getGraphQLLatitude(), + coordinate.getGraphQLLongitude() + ); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ScooterPreferencesMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ScooterPreferencesMapper.java new file mode 100644 index 00000000000..f29ebd0de63 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/ScooterPreferencesMapper.java @@ -0,0 +1,96 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import java.util.Set; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.preference.ScooterPreferences; +import org.opentripplanner.routing.api.request.preference.VehicleRentalPreferences; + +public class ScooterPreferencesMapper { + + static void setScooterPreferences( + ScooterPreferences.Builder preferences, + GraphQLTypes.GraphQLScooterPreferencesInput args + ) { + if (args == null) { + return; + } + + var speed = args.getGraphQLSpeed(); + if (speed != null) { + preferences.withSpeed(speed); + } + var reluctance = args.getGraphQLReluctance(); + if (reluctance != null) { + preferences.withReluctance(reluctance); + } + preferences.withRental(rental -> setScooterRentalPreferences(rental, args.getGraphQLRental())); + setScooterOptimization(preferences, args.getGraphQLOptimization()); + } + + private static void setScooterRentalPreferences( + VehicleRentalPreferences.Builder preferences, + GraphQLTypes.GraphQLScooterRentalPreferencesInput args + ) { + if (args == null) { + return; + } + + var allowedNetworks = args.getGraphQLAllowedNetworks(); + if (allowedNetworks != null) { + if (allowedNetworks.isEmpty()) { + throw new IllegalArgumentException("Allowed scooter rental networks must not be empty."); + } + preferences.withAllowedNetworks(Set.copyOf(allowedNetworks)); + } + var bannedNetworks = args.getGraphQLBannedNetworks(); + if (bannedNetworks != null) { + preferences.withBannedNetworks(Set.copyOf(bannedNetworks)); + } + var destinationPolicy = args.getGraphQLDestinationScooterPolicy(); + if (destinationPolicy != null) { + var allowed = destinationPolicy.getGraphQLAllowKeeping(); + if (allowed != null) { + preferences.withAllowArrivingInRentedVehicleAtDestination(allowed); + } + var cost = destinationPolicy.getGraphQLKeepingCost(); + if (cost != null) { + preferences.withArrivingInRentalVehicleAtDestinationCost(cost.toSeconds()); + } + } + } + + private static void setScooterOptimization( + ScooterPreferences.Builder preferences, + GraphQLTypes.GraphQLScooterOptimizationInput args + ) { + if (args == null) { + return; + } + + var type = args.getGraphQLType(); + var mappedType = type != null ? VehicleOptimizationTypeMapper.map(type) : null; + if (mappedType != null) { + preferences.withOptimizeType(mappedType); + } + var triangleArgs = args.getGraphQLTriangle(); + if (isScooterTriangleSet(triangleArgs)) { + preferences.withForcedOptimizeTriangle(triangle -> { + triangle + .withSlope(triangleArgs.getGraphQLFlatness()) + .withSafety(triangleArgs.getGraphQLSafety()) + .withTime(triangleArgs.getGraphQLTime()); + }); + } + } + + private static boolean isScooterTriangleSet( + GraphQLTypes.GraphQLTriangleScooterFactorsInput args + ) { + return ( + args != null && + args.getGraphQLFlatness() != null && + args.getGraphQLSafety() != null && + args.getGraphQLTime() != null + ); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java new file mode 100644 index 00000000000..ffa7363e3a7 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransferModeMapper.java @@ -0,0 +1,17 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.StreetMode; + +/** + * Maps transfer street mode from API to internal model. + */ +public class TransferModeMapper { + + public static StreetMode map(GraphQLTypes.GraphQLPlanTransferMode mode) { + return switch (mode) { + case BICYCLE -> StreetMode.BIKE; + case WALK -> StreetMode.WALK; + }; + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransitPreferencesMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransitPreferencesMapper.java new file mode 100644 index 00000000000..f542639ae36 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/TransitPreferencesMapper.java @@ -0,0 +1,110 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static org.opentripplanner.apis.gtfs.mapping.routerequest.ArgumentUtils.getTransitModes; + +import graphql.schema.DataFetchingEnvironment; +import java.util.Map; +import java.util.stream.Collectors; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.mapping.TransitModeMapper; +import org.opentripplanner.framework.collection.CollectionUtils; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.routing.api.request.preference.TransferPreferences; +import org.opentripplanner.routing.api.request.preference.TransitPreferences; + +public class TransitPreferencesMapper { + + static void setTransitPreferences( + TransitPreferences.Builder transitPreferences, + TransferPreferences.Builder transferPreferences, + GraphQLTypes.GraphQLQueryTypePlanConnectionArgs args, + DataFetchingEnvironment environment + ) { + var modes = args.getGraphQLModes(); + var transit = getTransitModes(environment); + if (!Boolean.TRUE.equals(modes.getGraphQLDirectOnly()) && !CollectionUtils.isEmpty(transit)) { + var reluctanceForMode = transit + .stream() + .filter(mode -> mode.containsKey("cost")) + .collect( + Collectors.toMap( + mode -> + TransitModeMapper.map( + GraphQLTypes.GraphQLTransitMode.valueOf((String) mode.get("mode")) + ), + mode -> (Double) ((Map) mode.get("cost")).get("reluctance") + ) + ); + transitPreferences.setReluctanceForMode(reluctanceForMode); + } + var transitArgs = args.getGraphQLPreferences().getGraphQLTransit(); + if (transitArgs == null) { + return; + } + + var board = transitArgs.getGraphQLBoard(); + if (board != null) { + var slack = board.getGraphQLSlack(); + if (slack != null) { + transitPreferences.withDefaultBoardSlackSec( + (int) DurationUtils.requireNonNegativeMedium(slack, "board slack").toSeconds() + ); + } + var waitReluctance = board.getGraphQLWaitReluctance(); + if (waitReluctance != null) { + transferPreferences.withWaitReluctance(waitReluctance); + } + } + var alight = transitArgs.getGraphQLAlight(); + if (alight != null) { + var slack = alight.getGraphQLSlack(); + if (slack != null) { + transitPreferences.withDefaultAlightSlackSec( + (int) DurationUtils.requireNonNegativeMedium(slack, "alight slack").toSeconds() + ); + } + } + var transfer = transitArgs.getGraphQLTransfer(); + if (transfer != null) { + var cost = transfer.getGraphQLCost(); + if (cost != null) { + transferPreferences.withCost(cost.toSeconds()); + } + var slack = transfer.getGraphQLSlack(); + if (slack != null) { + transferPreferences.withSlack( + (int) DurationUtils.requireNonNegativeMedium(slack, "transfer slack").toSeconds() + ); + } + var maxTransfers = transfer.getGraphQLMaximumTransfers(); + if (maxTransfers != null) { + if (maxTransfers < 0) { + throw new IllegalArgumentException("Maximum transfers must be non-negative."); + } + transferPreferences.withMaxTransfers(maxTransfers + 1); + } + var additionalTransfers = transfer.getGraphQLMaximumAdditionalTransfers(); + if (additionalTransfers != null) { + if (additionalTransfers < 0) { + throw new IllegalArgumentException("Maximum additional transfers must be non-negative."); + } + transferPreferences.withMaxAdditionalTransfers(additionalTransfers); + } + } + var timetable = transitArgs.getGraphQLTimetable(); + if (timetable != null) { + var excludeUpdates = timetable.getGraphQLExcludeRealTimeUpdates(); + if (excludeUpdates != null) { + transitPreferences.setIgnoreRealtimeUpdates(excludeUpdates); + } + var includePlannedCancellations = timetable.getGraphQLIncludePlannedCancellations(); + if (includePlannedCancellations != null) { + transitPreferences.setIncludePlannedCancellations(includePlannedCancellations); + } + var includeRealtimeCancellations = timetable.getGraphQLIncludeRealTimeCancellations(); + if (includeRealtimeCancellations != null) { + transitPreferences.setIncludeRealtimeCancellations(includeRealtimeCancellations); + } + } + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/VehicleOptimizationTypeMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/VehicleOptimizationTypeMapper.java new file mode 100644 index 00000000000..c47cbd3cf7a --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/VehicleOptimizationTypeMapper.java @@ -0,0 +1,28 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +/** + * Maps vehicle optimization type from API to internal model. + */ +public class VehicleOptimizationTypeMapper { + + public static VehicleRoutingOptimizeType map(GraphQLTypes.GraphQLCyclingOptimizationType type) { + return switch (type) { + case SHORTEST_DURATION -> VehicleRoutingOptimizeType.SHORTEST_DURATION; + case FLAT_STREETS -> VehicleRoutingOptimizeType.FLAT_STREETS; + case SAFE_STREETS -> VehicleRoutingOptimizeType.SAFE_STREETS; + case SAFEST_STREETS -> VehicleRoutingOptimizeType.SAFEST_STREETS; + }; + } + + public static VehicleRoutingOptimizeType map(GraphQLTypes.GraphQLScooterOptimizationType type) { + return switch (type) { + case SHORTEST_DURATION -> VehicleRoutingOptimizeType.SHORTEST_DURATION; + case FLAT_STREETS -> VehicleRoutingOptimizeType.FLAT_STREETS; + case SAFE_STREETS -> VehicleRoutingOptimizeType.SAFE_STREETS; + case SAFEST_STREETS -> VehicleRoutingOptimizeType.SAFEST_STREETS; + }; + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/WalkPreferencesMapper.java b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/WalkPreferencesMapper.java new file mode 100644 index 00000000000..1795e999ac6 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/mapping/routerequest/WalkPreferencesMapper.java @@ -0,0 +1,33 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.routing.api.request.preference.WalkPreferences; + +public class WalkPreferencesMapper { + + static void setWalkPreferences( + WalkPreferences.Builder preferences, + GraphQLTypes.GraphQLWalkPreferencesInput args + ) { + if (args == null) { + return; + } + + var speed = args.getGraphQLSpeed(); + if (speed != null) { + preferences.withSpeed(speed); + } + var reluctance = args.getGraphQLReluctance(); + if (reluctance != null) { + preferences.withReluctance(reluctance); + } + var walkSafetyFactor = args.getGraphQLSafetyFactor(); + if (walkSafetyFactor != null) { + preferences.withSafetyFactor(walkSafetyFactor); + } + var boardCost = args.getGraphQLBoardCost(); + if (boardCost != null) { + preferences.withBoardCost(boardCost.toSeconds()); + } + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/model/PlanPageInfo.java b/src/main/java/org/opentripplanner/apis/gtfs/model/PlanPageInfo.java new file mode 100644 index 00000000000..dd017971f8a --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/model/PlanPageInfo.java @@ -0,0 +1,87 @@ +package org.opentripplanner.apis.gtfs.model; + +import graphql.relay.ConnectionCursor; +import java.time.Duration; +import java.util.Objects; + +public class PlanPageInfo { + + private final ConnectionCursor startCursor; + private final ConnectionCursor endCursor; + private final boolean hasPreviousPage; + private final boolean hasNextPage; + private final Duration searchWindowUsed; + + public PlanPageInfo( + ConnectionCursor startCursor, + ConnectionCursor endCursor, + boolean hasPreviousPage, + boolean hasNextPage, + Duration searchWindowUsed + ) { + this.startCursor = startCursor; + this.endCursor = endCursor; + this.hasPreviousPage = hasPreviousPage; + this.hasNextPage = hasNextPage; + this.searchWindowUsed = searchWindowUsed; + } + + public ConnectionCursor startCursor() { + return startCursor; + } + + public ConnectionCursor endCursor() { + return endCursor; + } + + public boolean hasPreviousPage() { + return hasPreviousPage; + } + + public boolean hasNextPage() { + return hasNextPage; + } + + public Duration searchWindowUsed() { + return searchWindowUsed; + } + + @Override + public int hashCode() { + return Objects.hash( + startCursor == null ? null : startCursor.getValue(), + endCursor == null ? null : endCursor.getValue(), + hasPreviousPage, + hasNextPage, + searchWindowUsed + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlanPageInfo that = (PlanPageInfo) o; + return ( + equalsCursors(startCursor, that.startCursor) && + equalsCursors(endCursor, that.endCursor) && + Objects.equals(searchWindowUsed, that.searchWindowUsed) && + hasPreviousPage == that.hasPreviousPage && + hasNextPage == that.hasNextPage + ); + } + + /** + * Only checks that the values of the cursors are equal and ignores rest of the fields. + */ + private static boolean equalsCursors(ConnectionCursor a, ConnectionCursor b) { + return ( + (a == null && b == null) || + (a != null && b != null && Objects.equals(a.getValue(), b.getValue())) + ); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java b/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java index d143d65421c..a925aff0a1d 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/AbortOnTimeoutExecutionStrategy.java @@ -1,6 +1,7 @@ package org.opentripplanner.apis.transmodel.support; import graphql.execution.AsyncExecutionStrategy; +import graphql.execution.ExecutionStrategyParameters; import graphql.schema.DataFetchingEnvironment; import java.io.Closeable; import java.util.concurrent.CompletableFuture; @@ -27,13 +28,14 @@ public class AbortOnTimeoutExecutionStrategy extends AsyncExecutionStrategy impl @Override protected CompletableFuture handleFetchingException( DataFetchingEnvironment environment, + ExecutionStrategyParameters params, Throwable e ) { if (e instanceof OTPRequestTimeoutException te) { logTimeoutProgress(); throw te; } - return super.handleFetchingException(environment, e); + return super.handleFetchingException(environment, params, e); } @SuppressWarnings("Convert2MethodRef") diff --git a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 4ae5004cf6b..29489de19f2 100644 --- a/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -104,7 +104,6 @@ public enum OTPFeature { SandboxAPIGeocoder(false, true, "Enable the Geocoder API."), SandboxAPIMapboxVectorTilesApi(false, true, "Enable Mapbox vector tiles API."), SandboxAPIParkAndRideApi(false, true, "Enable park-and-ride endpoint."), - SandboxAPITravelTime(false, true, "Enable the isochrone/travel time surface API."), TransferAnalyzer(false, true, "Analyze transfers during graph build."); private static final Object TEST_LOCK = new Object(); diff --git a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java index 1edbfc527d1..b34db13e270 100644 --- a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java +++ b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java @@ -32,4 +32,17 @@ public static String toString(@Nullable Collection c, String nullText) { } return stream.collect(Collectors.joining(", ", "[", "]")); } + + /** + * A null-safe version of isEmpty() for a collection. + *

+ * If the collection is {@code null} then {@code true} is returned. + *

+ * If the collection is empty then {@code true} is returned. + *

+ * Otherwise {@code false} is returned. + */ + public static boolean isEmpty(@Nullable Collection c) { + return c == null || c.isEmpty(); + } } diff --git a/src/main/java/org/opentripplanner/framework/graphql/GraphQLUtils.java b/src/main/java/org/opentripplanner/framework/graphql/GraphQLUtils.java index 906ba37a892..334513fd419 100644 --- a/src/main/java/org/opentripplanner/framework/graphql/GraphQLUtils.java +++ b/src/main/java/org/opentripplanner/framework/graphql/GraphQLUtils.java @@ -17,7 +17,12 @@ public static String getTranslation(I18NString input, DataFetchingEnvironment en } public static Locale getLocale(DataFetchingEnvironment environment) { - return getLocale(environment, environment.getArgument("language")); + var localeString = environment.getArgument("language"); + if (localeString != null) { + return Locale.forLanguageTag((String) localeString); + } + + return getLocaleFromEnvironment(environment); } public static Locale getLocale(DataFetchingEnvironment environment, String localeString) { @@ -25,6 +30,18 @@ public static Locale getLocale(DataFetchingEnvironment environment, String local return Locale.forLanguageTag(localeString); } + return getLocaleFromEnvironment(environment); + } + + public static Locale getLocale(DataFetchingEnvironment environment, Locale locale) { + if (locale != null) { + return locale; + } + + return getLocaleFromEnvironment(environment); + } + + public static Locale getLocaleFromEnvironment(DataFetchingEnvironment environment) { // This can come from the accept-language header var userLocale = environment.getLocale(); var defaultLocale = getDefaultLocale(environment); diff --git a/src/main/java/org/opentripplanner/framework/time/DurationUtils.java b/src/main/java/org/opentripplanner/framework/time/DurationUtils.java index 9a01d308a1c..9d16ed7d269 100644 --- a/src/main/java/org/opentripplanner/framework/time/DurationUtils.java +++ b/src/main/java/org/opentripplanner/framework/time/DurationUtils.java @@ -14,7 +14,7 @@ /** * This class extend the Java {@link Duration} with utility functionality to parse and convert - * integer and text to a {@link Duration}. + * integer and text to a {@link Duration}. This class also contains methods to validate durations. *

* OTP make have use of the Duration in a lenient ISO-8601 duration format. For example: *

@@ -181,7 +181,67 @@ public static String msToSecondsStr(long timeMs) {
   public static Duration requireNonNegative(Duration value) {
     Objects.requireNonNull(value);
     if (value.isNegative()) {
-      throw new IllegalArgumentException("Duration can no be negative: " + value);
+      throw new IllegalArgumentException("Duration can't be negative: " + value);
+    }
+    return value;
+  }
+
+  /**
+   * Checks that duration is not negative and not over 2 days.
+   *
+   * @param subject used to identify name of the problematic value when throwing an exception.
+   */
+  public static Duration requireNonNegativeLong(Duration value, String subject) {
+    Objects.requireNonNull(value);
+    if (value.isNegative()) {
+      throw new IllegalArgumentException(
+        "Duration %s can't be negative: %s.".formatted(subject, value)
+      );
+    }
+    if (value.compareTo(Duration.ofDays(2)) > 0) {
+      throw new IllegalArgumentException(
+        "Duration %s can't be longer than two days: %s.".formatted(subject, value)
+      );
+    }
+    return value;
+  }
+
+  /**
+   * Checks that duration is not negative and not over 2 hours.
+   *
+   * @param subject used to identify name of the problematic value when throwing an exception.
+   */
+  public static Duration requireNonNegativeMedium(Duration value, String subject) {
+    Objects.requireNonNull(value);
+    if (value.isNegative()) {
+      throw new IllegalArgumentException(
+        "Duration %s can't be negative: %s.".formatted(subject, value)
+      );
+    }
+    if (value.compareTo(Duration.ofHours(2)) > 0) {
+      throw new IllegalArgumentException(
+        "Duration %s can't be longer than two hours: %s.".formatted(subject, value)
+      );
+    }
+    return value;
+  }
+
+  /**
+   * Checks that duration is not negative and not over 30 minutes.
+   *
+   * @param subject used to identify name of the problematic value when throwing an exception.
+   */
+  public static Duration requireNonNegativeShort(Duration value, String subject) {
+    Objects.requireNonNull(value);
+    if (value.isNegative()) {
+      throw new IllegalArgumentException(
+        "Duration %s can't be negative: %s.".formatted(subject, value)
+      );
+    }
+    if (value.compareTo(Duration.ofMinutes(30)) > 0) {
+      throw new IllegalArgumentException(
+        "Duration %s can't be longer than 30 minutes: %s.".formatted(subject, value)
+      );
     }
     return value;
   }
diff --git a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java
index 933f89b75a5..5c018412572 100644
--- a/src/main/java/org/opentripplanner/model/TimetableSnapshot.java
+++ b/src/main/java/org/opentripplanner/model/TimetableSnapshot.java
@@ -14,6 +14,7 @@
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
+import javax.annotation.Nullable;
 import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater;
 import org.opentripplanner.transit.model.framework.FeedScopedId;
 import org.opentripplanner.transit.model.framework.Result;
@@ -147,6 +148,7 @@ public Timetable resolve(TripPattern pattern, LocalDate serviceDate) {
    *
    * @return trip pattern created by the updater; null if trip is on the original trip pattern
    */
+  @Nullable
   public TripPattern getRealtimeAddedTripPattern(FeedScopedId tripId, LocalDate serviceDate) {
     TripIdAndServiceDate tripIdAndServiceDate = new TripIdAndServiceDate(tripId, serviceDate);
     return realtimeAddedTripPattern.get(tripIdAndServiceDate);
@@ -344,6 +346,8 @@ public boolean revertTripToScheduledTripPattern(FeedScopedId tripId, LocalDate s
 
   /**
    * Removes all Timetables which are valid for a ServiceDate on-or-before the one supplied.
+   *
+   * @return true if any data has been modified and false if no purging has happened.
    */
   public boolean purgeExpiredData(LocalDate serviceDate) {
     if (readOnly) {
diff --git a/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java b/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java
index 7dabe8e2a1c..9bad7cf7222 100644
--- a/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java
+++ b/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java
@@ -26,7 +26,6 @@ public class SearchParams {
   private final boolean constrainedTransfers;
   private final Collection accessPaths;
   private final Collection egressPaths;
-  private final boolean allowEmptyAccessEgressPaths;
 
   /**
    * Default values are defined in the default constructor.
@@ -42,7 +41,6 @@ private SearchParams() {
     constrainedTransfers = false;
     accessPaths = List.of();
     egressPaths = List.of();
-    allowEmptyAccessEgressPaths = false;
   }
 
   SearchParams(SearchParamsBuilder builder) {
@@ -56,7 +54,6 @@ private SearchParams() {
     this.constrainedTransfers = builder.constrainedTransfers();
     this.accessPaths = List.copyOf(builder.accessPaths());
     this.egressPaths = List.copyOf(builder.egressPaths());
-    this.allowEmptyAccessEgressPaths = builder.allowEmptyAccessEgressPaths();
   }
 
   /**
@@ -198,14 +195,6 @@ public Collection egressPaths() {
     return egressPaths;
   }
 
-  /**
-   * If enabled, the check for access and egress paths is skipped. This is required when wanting to
-   * eg. run a separate heuristic search, with no pre-defined destinations.
-   */
-  public boolean allowEmptyAccessEgressPaths() {
-    return allowEmptyAccessEgressPaths;
-  }
-
   /**
    * Get the maximum duration of any access or egress path in seconds.
    */
@@ -279,14 +268,8 @@ void verify() {
       isEarliestDepartureTimeSet() || isLatestArrivalTimeSet(),
       "'earliestDepartureTime' or 'latestArrivalTime' is required."
     );
-    assertProperty(
-      allowEmptyAccessEgressPaths || !accessPaths.isEmpty(),
-      "At least one 'accessPath' is required."
-    );
-    assertProperty(
-      allowEmptyAccessEgressPaths || !egressPaths.isEmpty(),
-      "At least one 'egressPath' is required."
-    );
+    assertProperty(!accessPaths.isEmpty(), "At least one 'accessPath' is required.");
+    assertProperty(!egressPaths.isEmpty(), "At least one 'egressPath' is required.");
     assertProperty(
       !(preferLateArrival && !isLatestArrivalTimeSet()),
       "The 'latestArrivalTime' is required when 'departAsLateAsPossible' is set."
diff --git a/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java b/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java
index 517780eb69c..5774a92d6e1 100644
--- a/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java
+++ b/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java
@@ -29,7 +29,6 @@ public class SearchParamsBuilder {
   private int maxNumberOfTransfers;
   private boolean timetable;
   private boolean constrainedTransfers;
-  private boolean allowEmptyAccessEgressPaths;
 
   public SearchParamsBuilder(RaptorRequestBuilder parent, SearchParams defaults) {
     this.parent = parent;
@@ -43,7 +42,6 @@ public SearchParamsBuilder(RaptorRequestBuilder parent, SearchParams defaults
     this.constrainedTransfers = defaults.constrainedTransfers();
     this.accessPaths.addAll(defaults.accessPaths());
     this.egressPaths.addAll(defaults.egressPaths());
-    this.allowEmptyAccessEgressPaths = defaults.allowEmptyAccessEgressPaths();
   }
 
   public int earliestDepartureTime() {
@@ -162,15 +160,6 @@ public SearchParamsBuilder addEgressPaths(RaptorAccessEgress... egressPaths)
     return addEgressPaths(Arrays.asList(egressPaths));
   }
 
-  public SearchParamsBuilder allowEmptyAccessEgressPaths(boolean allowEmptyEgressPaths) {
-    this.allowEmptyAccessEgressPaths = allowEmptyEgressPaths;
-    return this;
-  }
-
-  public boolean allowEmptyAccessEgressPaths() {
-    return allowEmptyAccessEgressPaths;
-  }
-
   public RaptorRequest build() {
     return parent.build();
   }
diff --git a/src/main/java/org/opentripplanner/street/search/state/StateData.java b/src/main/java/org/opentripplanner/street/search/state/StateData.java
index d1db14d91f4..648b2f3110c 100644
--- a/src/main/java/org/opentripplanner/street/search/state/StateData.java
+++ b/src/main/java/org/opentripplanner/street/search/state/StateData.java
@@ -8,7 +8,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
-import java.util.function.Function;
 import org.opentripplanner.routing.api.request.StreetMode;
 import org.opentripplanner.street.model.RentalFormFactor;
 import org.opentripplanner.street.search.TraverseMode;
@@ -21,8 +20,6 @@
  */
 public class StateData implements Cloneable {
 
-  // TODO OTP2 Many of these could be replaced by a more generic state machine implementation
-
   protected boolean vehicleParked;
 
   protected VehicleRentalState vehicleRentalState;
@@ -56,7 +53,7 @@ public class StateData implements Cloneable {
   public Set noRentalDropOffZonesAtStartOfReverseSearch = Set.of();
 
   /** Private constructor, use static methods to get a set of initial states. */
-  protected StateData(StreetMode requestMode) {
+  private StateData(StreetMode requestMode) {
     currentMode =
       switch (requestMode) {
         // when renting or using a flex vehicle, you start on foot until you have found the vehicle
@@ -72,25 +69,13 @@ protected StateData(StreetMode requestMode) {
    * Returns a set of initial StateDatas based on the options from the RouteRequest
    */
   public static List getInitialStateDatas(StreetSearchRequest request) {
-    return getInitialStateDatas(request, StateData::new);
-  }
-
-  /**
-   * Returns a set of initial StateDatas based on the options from the RouteRequest, with a custom
-   * StateData implementation.
-   */
-  public static List getInitialStateDatas(
-    StreetSearchRequest request,
-    Function stateDataConstructor
-  ) {
     var rentalPreferences = request.preferences().rental(request.mode());
     return getInitialStateDatas(
       request.mode(),
       request.arriveBy(),
       rentalPreferences != null
         ? rentalPreferences.allowArrivingInRentedVehicleAtDestination()
-        : false,
-      stateDataConstructor
+        : false
     );
   }
 
@@ -106,8 +91,7 @@ public static StateData getBaseCaseStateData(StreetSearchRequest request) {
       request.arriveBy(),
       rentalPreferences != null
         ? rentalPreferences.allowArrivingInRentedVehicleAtDestination()
-        : false,
-      StateData::new
+        : false
     );
 
     var baseCaseDatas =
@@ -143,11 +127,10 @@ public static StateData getBaseCaseStateData(StreetSearchRequest request) {
   private static List getInitialStateDatas(
     StreetMode requestMode,
     boolean arriveBy,
-    boolean allowArrivingInRentedVehicleAtDestination,
-    Function stateDataConstructor
+    boolean allowArrivingInRentedVehicleAtDestination
   ) {
     List res = new ArrayList<>();
-    var proto = stateDataConstructor.apply(requestMode);
+    var proto = new StateData(requestMode);
 
     // carPickup searches may start and end in two distinct states:
     //   - CAR / IN_CAR where pickup happens directly at the bus stop
@@ -233,7 +216,7 @@ private static RentalFormFactor toFormFactor(StreetMode streetMode) {
     };
   }
 
-  public StateData clone() {
+  protected StateData clone() {
     try {
       return (StateData) super.clone();
     } catch (CloneNotSupportedException e1) {
diff --git a/src/main/java/org/opentripplanner/updater/trip/AbstractTimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java
similarity index 66%
rename from src/main/java/org/opentripplanner/updater/trip/AbstractTimetableSnapshotSource.java
rename to src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java
index 3df4f2aba59..624d749664c 100644
--- a/src/main/java/org/opentripplanner/updater/trip/AbstractTimetableSnapshotSource.java
+++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotManager.java
@@ -4,26 +4,31 @@
 import java.util.Objects;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Supplier;
+import javax.annotation.Nullable;
 import org.opentripplanner.framework.time.CountdownTimer;
+import org.opentripplanner.model.Timetable;
 import org.opentripplanner.model.TimetableSnapshot;
-import org.opentripplanner.model.TimetableSnapshotProvider;
 import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater;
+import org.opentripplanner.transit.model.framework.FeedScopedId;
+import org.opentripplanner.transit.model.framework.Result;
+import org.opentripplanner.transit.model.network.TripPattern;
+import org.opentripplanner.transit.model.timetable.TripTimes;
 import org.opentripplanner.updater.TimetableSnapshotSourceParameters;
+import org.opentripplanner.updater.spi.UpdateError;
+import org.opentripplanner.updater.spi.UpdateSuccess;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * A base class for which abstracts away locking, updating, committing and purging of the timetable snapshot.
+ * A class which abstracts away locking, updating, committing and purging of the timetable snapshot.
  * In order to keep code reviews easier this is an intermediate stage and will be refactored further.
  * In particular the following refactorings are planned:
- *
- * - use composition instead of inheritance
- * - make the buffer private to this class and add an API for its access
+ * 

* - create only one "snapshot manager" per transit model that is shared between Siri/GTFS-RT updaters */ -public class AbstractTimetableSnapshotSource implements TimetableSnapshotProvider { +public final class TimetableSnapshotManager { - private static final Logger LOG = LoggerFactory.getLogger(AbstractTimetableSnapshotSource.class); + private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotManager.class); private final TransitLayerUpdater transitLayerUpdater; /** * Lock to indicate that buffer is in use @@ -35,7 +40,7 @@ public class AbstractTimetableSnapshotSource implements TimetableSnapshotProvide * only be modified by a thread that holds a lock on {@link #bufferLock}. All public methods that * might modify this buffer will correctly acquire the lock. */ - protected final TimetableSnapshot buffer = new TimetableSnapshot(); + private final TimetableSnapshot buffer = new TimetableSnapshot(); /** * The working copy of the timetable snapshot. Should not be visible to routing threads. Should @@ -75,7 +80,7 @@ public class AbstractTimetableSnapshotSource implements TimetableSnapshotProvide * @param localDateNow This supplier allows you to inject a custom lambda to override what is * considered 'today'. This is useful for unit testing. */ - public AbstractTimetableSnapshotSource( + public TimetableSnapshotManager( TransitLayerUpdater transitLayerUpdater, TimetableSnapshotSourceParameters parameters, Supplier localDateNow @@ -94,7 +99,7 @@ public AbstractTimetableSnapshotSource( * provided a consistent view of all TripTimes. The routing thread need only release its reference * to the snapshot to release resources. */ - public final TimetableSnapshot getTimetableSnapshot() { + public TimetableSnapshot getTimetableSnapshot() { // Try to get a lock on the buffer if (bufferLock.tryLock()) { // Make a new snapshot if necessary @@ -118,7 +123,7 @@ public final TimetableSnapshot getTimetableSnapshot() { * * @param force Force the committing of a new snapshot even if the above conditions are not met. */ - public final void commitTimetableSnapshot(final boolean force) { + public void commitTimetableSnapshot(final boolean force) { if (force || snapshotFrequencyThrottle.timeIsUp()) { if (force || buffer.isDirty()) { LOG.debug("Committing {}", buffer); @@ -136,12 +141,25 @@ public final void commitTimetableSnapshot(final boolean force) { } } + /** + * Get the current trip pattern given a trip id and a service date, if it has been changed from + * the scheduled pattern with an update, for which the stopPattern is different. + * + * @param tripId trip id + * @param serviceDate service date + * @return trip pattern created by the updater; null if pattern has not been changed for this trip. + */ + @Nullable + public TripPattern getRealtimeAddedTripPattern(FeedScopedId tripId, LocalDate serviceDate) { + return buffer.getRealtimeAddedTripPattern(tripId, serviceDate); + } + /** * Make a snapshot after each message in anticipation of incoming requests. * Purge data if necessary (and force new snapshot if anything was purged). * Make sure that the public (locking) getTimetableSnapshot function is not called. */ - protected void purgeAndCommit() { + public void purgeAndCommit() { if (purgeExpiredData) { final boolean modified = purgeExpiredData(); commitTimetableSnapshot(modified); @@ -150,14 +168,28 @@ protected void purgeAndCommit() { } } + /** + * If a previous realtime update has changed which trip pattern is associated with the given trip + * on the given service date, this method will dissociate the trip from that pattern and remove + * the trip's timetables from that pattern on that particular service date. + *

+ * For this service date, the trip will revert to its original trip pattern from the scheduled + * data, remaining on that pattern unless it's changed again by a future realtime update. + */ + public void revertTripToScheduledTripPattern(FeedScopedId tripId, LocalDate serviceDate) { + buffer.revertTripToScheduledTripPattern(tripId, serviceDate); + } + /** * Remove realtime data from previous service dates from the snapshot. This is useful so that * instances that run for multiple days don't accumulate a lot of realtime data for past * dates which would increase memory consumption. * If your OTP instances are restarted throughout the day, this is less useful and can be * turned off. + * + * @return true if any data has been modified and false if no purging has happened. */ - protected final boolean purgeExpiredData() { + private boolean purgeExpiredData() { final LocalDate today = localDateNow.get(); // TODO: Base this on numberOfDaysOfLongestTrip for tripPatterns final LocalDate previously = today.minusDays(2); // Just to be safe... @@ -174,16 +206,12 @@ protected final boolean purgeExpiredData() { return buffer.purgeExpiredData(previously); } - protected final LocalDate localDateNow() { - return localDateNow.get(); - } - /** * Execute a {@code Runnable} with a locked snapshot buffer and release the lock afterwards. While * the action of locking and unlocking is not complicated to do for calling code, this method * exists so that the lock instance is a private field. */ - protected final void withLock(Runnable action) { + public void withLock(Runnable action) { bufferLock.lock(); try { @@ -193,4 +221,36 @@ protected final void withLock(Runnable action) { bufferLock.unlock(); } } + + /** + * Clear all data of snapshot for the provided feed id + */ + public void clearBuffer(String feedId) { + buffer.clear(feedId); + } + + /** + * Update the TripTimes of one Trip in a Timetable of a TripPattern. If the Trip of the TripTimes + * does not exist yet in the Timetable, add it. This method will make a protective copy + * of the Timetable if such a copy has not already been made while building up this snapshot, + * handling both cases where patterns were pre-existing in static data or created by realtime data. + * + * @param serviceDate service day for which this update is valid + * @return whether the update was actually applied + */ + public Result updateBuffer( + TripPattern pattern, + TripTimes tripTimes, + LocalDate serviceDate + ) { + return buffer.update(pattern, tripTimes, serviceDate); + } + + /** + * Returns an updated timetable for the specified pattern if one is available in this snapshot, or + * the originally scheduled timetable if there are no updates in this snapshot. + */ + public Timetable resolve(TripPattern pattern, LocalDate serviceDate) { + return buffer.resolve(pattern, serviceDate); + } } diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index 7e69c35e04e..cc39d82369b 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -40,6 +40,8 @@ import org.opentripplanner.gtfs.mapping.TransitModeMapper; import org.opentripplanner.model.StopTime; import org.opentripplanner.model.Timetable; +import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.model.TimetableSnapshotProvider; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.DataValidationException; import org.opentripplanner.transit.model.framework.Deduplicator; @@ -73,7 +75,7 @@ * necessary to provide planning threads a consistent constant view of a graph with realtime data at * a specific point in time. */ -public class TimetableSnapshotSource extends AbstractTimetableSnapshotSource { +public class TimetableSnapshotSource implements TimetableSnapshotProvider { private static final Logger LOG = LoggerFactory.getLogger(TimetableSnapshotSource.class); @@ -92,6 +94,9 @@ public class TimetableSnapshotSource extends AbstractTimetableSnapshotSource { private final Map serviceCodes; + private final TimetableSnapshotManager snapshotManager; + private final Supplier localDateNow; + public TimetableSnapshotSource( TimetableSnapshotSourceParameters parameters, TransitModel transitModel @@ -108,11 +113,13 @@ public TimetableSnapshotSource( TransitModel transitModel, Supplier localDateNow ) { - super(transitModel.getTransitLayerUpdater(), parameters, localDateNow); + this.snapshotManager = + new TimetableSnapshotManager(transitModel.getTransitLayerUpdater(), parameters, localDateNow); this.timeZone = transitModel.getTimeZone(); this.transitService = new DefaultTransitService(transitModel); this.deduplicator = transitModel.getDeduplicator(); this.serviceCodes = transitModel.getServiceCodes(); + this.localDateNow = localDateNow; // Inject this into the transit model transitModel.initTimetableSnapshotProvider(this); @@ -146,10 +153,10 @@ public UpdateResult applyTripUpdates( Map failuresByRelationship = new HashMap<>(); List> results = new ArrayList<>(); - withLock(() -> { + snapshotManager.withLock(() -> { if (updateIncrementality == FULL_DATASET) { // Remove all updates from the buffer - buffer.clear(feedId); + snapshotManager.clearBuffer(feedId); } LOG.debug("message contains {} trip updates", updates.size()); @@ -189,7 +196,7 @@ public UpdateResult applyTripUpdates( } else { // TODO: figure out the correct service date. For the special case that a trip // starts for example at 40:00, yesterday would probably be a better guess. - serviceDate = localDateNow(); + serviceDate = localDateNow.get(); } // Determine what kind of trip update this is final TripDescriptor.ScheduleRelationship tripScheduleRelationship = determineTripScheduleRelationship( @@ -257,7 +264,7 @@ public UpdateResult applyTripUpdates( } } - purgeAndCommit(); + snapshotManager.purgeAndCommit(); }); var updateResult = UpdateResult.ofResults(results); @@ -279,7 +286,7 @@ private void purgePatternModifications( FeedScopedId tripId, LocalDate serviceDate ) { - final TripPattern pattern = buffer.getRealtimeAddedTripPattern(tripId, serviceDate); + final TripPattern pattern = snapshotManager.getRealtimeAddedTripPattern(tripId, serviceDate); if ( !isPreviouslyAddedTrip(tripId, pattern, serviceDate) || ( @@ -290,7 +297,7 @@ private void purgePatternModifications( // Remove previous realtime updates for this trip. This is necessary to avoid previous // stop pattern modifications from persisting. If a trip was previously added with the ScheduleRelationship // ADDED and is now cancelled or deleted, we still want to keep the realtime added trip pattern. - this.buffer.revertTripToScheduledTripPattern(tripId, serviceDate); + this.snapshotManager.revertTripToScheduledTripPattern(tripId, serviceDate); } } @@ -302,7 +309,7 @@ private boolean isPreviouslyAddedTrip( if (pattern == null) { return false; } - var timetable = buffer.resolve(pattern, serviceDate); + var timetable = snapshotManager.resolve(pattern, serviceDate); if (timetable == null) { return false; } @@ -313,6 +320,11 @@ private boolean isPreviouslyAddedTrip( return tripTimes.getRealTimeState() == RealTimeState.ADDED; } + @Override + public TimetableSnapshot getTimetableSnapshot() { + return snapshotManager.getTimetableSnapshot(); + } + private static void logUpdateResult( String feedId, Map failuresByRelationship, @@ -425,10 +437,10 @@ private Result handleScheduledTrip( ); cancelScheduledTrip(tripId, serviceDate, CancelationType.DELETE); - return buffer.update(newPattern, updatedTripTimes, serviceDate); + return snapshotManager.updateBuffer(newPattern, updatedTripTimes, serviceDate); } else { // Set the updated trip times in the buffer - return buffer.update(pattern, updatedTripTimes, serviceDate); + return snapshotManager.updateBuffer(pattern, updatedTripTimes, serviceDate); } } @@ -870,7 +882,7 @@ private Result addTripToGraphAndBuffer( pattern.lastStop().getName() ); // Add new trip times to the buffer - return buffer.update(pattern, newTripTimes, serviceDate); + return snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); } /** @@ -901,7 +913,7 @@ private boolean cancelScheduledTrip( case CANCEL -> newTripTimes.cancelTrip(); case DELETE -> newTripTimes.deleteTrip(); } - buffer.update(pattern, newTripTimes, serviceDate); + snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); success = true; } } @@ -925,10 +937,10 @@ private boolean cancelPreviouslyAddedTrip( ) { boolean cancelledAddedTrip = false; - final TripPattern pattern = buffer.getRealtimeAddedTripPattern(tripId, serviceDate); + final TripPattern pattern = snapshotManager.getRealtimeAddedTripPattern(tripId, serviceDate); if (isPreviouslyAddedTrip(tripId, pattern, serviceDate)) { // Cancel trip times for this trip in this pattern - final Timetable timetable = buffer.resolve(pattern, serviceDate); + final Timetable timetable = snapshotManager.resolve(pattern, serviceDate); final int tripIndex = timetable.getTripIndex(tripId); if (tripIndex == -1) { debug(tripId, "Could not cancel previously added trip on {}", serviceDate); @@ -940,7 +952,7 @@ private boolean cancelPreviouslyAddedTrip( case CANCEL -> newTripTimes.cancelTrip(); case DELETE -> newTripTimes.deleteTrip(); } - buffer.update(pattern, newTripTimes, serviceDate); + snapshotManager.updateBuffer(pattern, newTripTimes, serviceDate); cancelledAddedTrip = true; } } diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index 393d5f014db..79a794b3607 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -8,10 +8,26 @@ This is only worth it when the execution is long running, i.e. more than ~50 mil """ directive @async on FIELD_DEFINITION +""" +Exactly one of the fields on an input object must be set and non-null while all others are omitted. +""" +directive @oneOf on INPUT_OBJECT + schema { query: QueryType } +""" +Plan accessibilty preferences. This can be expanded to contain preferences for various accessibility use cases +in the future. Currently only generic wheelchair preferences are available. +""" +input AccessibilityPreferencesInput { + """ + Wheelchair related preferences. Note, currently this is the only accessibility mode that is available. + """ + wheelchair: WheelchairPreferencesInput +} + """A public transport agency""" type Agency implements Node { """ @@ -273,6 +289,16 @@ enum AlertSeverityLevelType { SEVERE } +""" +Preferences related to alighting from a transit vehicle. +""" +input AlightPreferencesInput { + """ + What is the required minimum time alighting from a vehicle. + """ + slack: Duration +} + type OpeningHours { """ OSM-formatted string of the opening hours. @@ -333,6 +359,42 @@ type BikePark implements Node & PlaceInterface { openingHours: OpeningHours } +""" +Is it possible to arrive to the destination with a rented bicycle and does it +come with an extra cost. +""" +input DestinationBicyclePolicyInput { + """ + For networks that require station drop-off, should the routing engine offer results that go directly to the destination without dropping off the rental bicycle first. + """ + allowKeeping: Boolean + + """ + Cost associated with arriving to the destination with a rented bicycle. + No cost is applied if arriving to the destination after dropping off the rented + bicycle. + """ + keepingCost: Cost +} + +""" +Is it possible to arrive to the destination with a rented scooter and does it +come with an extra cost. +""" +input DestinationScooterPolicyInput { + """ + For networks that require station drop-off, should the routing engine offer results that go directly to the destination without dropping off the rental scooter first. + """ + allowKeeping: Boolean + + """ + Cost associated with arriving to the destination with a rented scooter. + No cost is applied if arriving to the destination after dropping off the rented + scooter. + """ + keepingCost: Cost +} + """Vehicle parking represents a location where bicycles or cars can be parked.""" type VehicleParking implements Node & PlaceInterface { """ @@ -784,6 +846,66 @@ enum BikesAllowed { NOT_ALLOWED } +""" +Preferences related to travel with a bicycle. +""" +input BicyclePreferencesInput { + """ + A multiplier for how bad cycling is compared to being in transit for equal lengths of time. + """ + reluctance: Reluctance + + """ + Walking preferences when walking a bicycle. + """ + walk: BicycleWalkPreferencesInput + + """ + Maximum speed on flat ground while riding a bicycle. Note, this speed is higher than + the average speed will be in itineraries as this is the maximum speed but there are + factors that slow down cycling such as crossings, intersections and elevation changes. + """ + speed: Speed + + """ + What criteria should be used when optimizing a cycling route. + """ + optimization: CyclingOptimizationInput + + """ + Cost of boarding a vehicle with a bicycle. + """ + boardCost: Cost + + """ + Bicycle rental related preferences. + """ + rental: BicycleRentalPreferencesInput + + """ + Bicycle parking related preferences. + """ + parking: BicycleParkingPreferencesInput +} + +""" +Preferences related to boarding a transit vehicle. Note, board costs for each street mode +can be found under the street mode preferences. +""" +input BoardPreferencesInput { + """ + A multiplier for how bad waiting at a stop is compared to being in transit for equal lengths of time. + """ + waitReluctance: Reluctance + + """ + What is the required minimum waiting time at a stop. Setting this value as `PT0S`, for example, can lead + to passenger missing a connection when the vehicle leaves ahead of time or the passenger arrives to the + stop later than expected. + """ + slack: Duration +} + """Car park represents a location where cars can be parked.""" type CarPark implements Node & PlaceInterface { """ @@ -828,6 +950,26 @@ type CarPark implements Node & PlaceInterface { openingHours: OpeningHours } +""" +Preferences related to traveling on a car (excluding car travel on transit services such as taxi). +""" +input CarPreferencesInput { + """ + A multiplier for how bad travelling on car is compared to being in transit for equal lengths of time. + """ + reluctance: Reluctance + + """ + Car rental related preferences. + """ + rental: CarRentalPreferencesInput + + """ + Car parking related preferences. + """ + parking: CarParkingPreferencesInput +} + """Cluster is a list of stops grouped by name and proximity""" type Cluster implements Node { """ @@ -855,6 +997,33 @@ type Cluster implements Node { stops: [Stop!] } +""" +A static cost that is applied to a certain event or entity. Cost is a positive integer, +for example `450`. One cost unit should roughly match a one second travel on transit. +""" +scalar Cost + +""" +Coordinate (often referred as coordinates), which is used to specify a location using in the +WGS84 coordinate system. +""" +type Coordinate { + """ + Latitude as a WGS84 format number. + """ + latitude: CoordinateValue! + + """ + Longitude as a WGS84 format number. + """ + longitude: CoordinateValue! +} + +""" +Either a latitude or a longitude as a WGS84 format floating point number. +""" +scalar CoordinateValue @specifiedBy(url: "https://earth-info.nga.mil/?dir=wgs84&action=wgs84") + type Coordinates { """Latitude (WGS 84)""" lat: Float @@ -863,6 +1032,27 @@ type Coordinates { lon: Float } +""" +Plan date time options. Only one of the values should be defined. +""" +input PlanDateTimeInput @oneOf { + """ + Earliest departure date time. The returned itineraries should not + depart before this instant unless one is using paging to find earlier + itineraries. Note, it is not currently possible to define both + `earliestDeparture` and `latestArrival`. + """ + earliestDeparture: OffsetDateTime + + """ + Latest arrival time date time. The returned itineraries should not + arrive to the destination after this instant unless one is using + paging to find later itineraries. Note, it is not currently possible + to define both `earliestDeparture` and `latestArrival`. + """ + latestArrival: OffsetDateTime +} + type debugOutput { totalTime: Long pathCalculationTime: Long @@ -1064,7 +1254,7 @@ type Geometry { points: Polyline } -scalar GeoJson +scalar GeoJson @specifiedBy(url: "https://www.rfcreader.com/#rfc7946") scalar Grams @@ -1082,6 +1272,20 @@ type StopGeometries { googleEncoded: [Geometry] } +""" +A coordinate used for a location in a plan query. +""" +input PlanCoordinateInput { + """ + Latitude as a WGS84 format number. + """ + latitude: CoordinateValue! + """ + Longitude as a WGS84 format number. + """ + longitude: CoordinateValue! +} + input InputBanned { """A comma-separated list of banned route ids""" routes: String @@ -1208,6 +1412,59 @@ input InputPreferred { otherThanPreferredRoutesPenalty: Int } +""" +Locale in the format defined in [RFC5646](https://datatracker.ietf.org/doc/html/rfc5646). For example, `en` or `en-US`. +""" +scalar Locale @specifiedBy(url: "https://www.rfcreader.com/#rfc5646") + +""" +Preferences for car parking facilities used during the routing. +""" +input CarParkingPreferencesInput { + """ + Selection filters to include or exclude parking facilities. + An empty list will include all facilities in the routing search. + """ + filters: [ParkingFilter!] + + """ + If `preferred` is non-empty, using a parking facility that doesn't contain + at least one of the preferred conditions, will receive this extra cost and therefore avoided if + preferred options are available. + """ + unpreferredCost: Cost + + """ + If non-empty every parking facility that doesn't match this set of conditions will + receive an extra cost (defined by `unpreferredCost`) and therefore avoided. + """ + preferred: [ParkingFilter!] +} + +""" +Preferences for bicycle parking facilities used during the routing. +""" +input BicycleParkingPreferencesInput { + """ + Selection filters to include or exclude parking facilities. + An empty list will include all facilities in the routing search. + """ + filters: [ParkingFilter!] + + """ + If `preferred` is non-empty, using a parking facility that doesn't contain + at least one of the preferred conditions, will receive this extra cost and therefore avoided if + preferred options are available. + """ + unpreferredCost: Cost + + """ + If non-empty every parking facility that doesn't match this set of conditions will + receive an extra cost (defined by `unpreferredCost`) and therefore avoided. + """ + preferred: [ParkingFilter!] +} + """ Preferences for parking facilities used during the routing. """ @@ -1268,6 +1525,213 @@ input ParkingFilter { select: [ParkingFilterOperation!] } +""" +Settings that control the behavior of itinerary filtering. **These are advanced settings and +should not be set by a user through user preferences.** +""" +input PlanItineraryFilterInput { + """ + Itinerary filter debug profile used to control the behaviour of itinerary filters. + """ + itineraryFilterDebugProfile: ItineraryFilterDebugProfile = OFF + + """ + Pick one itinerary from each group after putting itineraries that are `85%` similar together, + if the given value is `0.85`, for example. Itineraries are grouped together based on relative + the distance of transit travel that is identical between the itineraries (access, egress and + transfers are ignored). The value must be at least `0.5`. + """ + groupSimilarityKeepOne: Ratio = 0.85 + + """ + Pick three itineraries from each group after putting itineraries that are `68%` similar together, + if the given value is `0.68`, for example. Itineraries are grouped together based on relative + the distance of transit travel that is identical between the itineraries (access, egress and + transfers are ignored). The value must be at least `0.5`. + """ + groupSimilarityKeepThree: Ratio = 0.68 + + """ + Of the itineraries grouped to maximum of three itineraries, how much worse can the non-grouped + legs be compared to the lowest cost. `2.0` means that they can be double the cost, and any + itineraries having a higher cost will be filtered away. Use a value lower than `1.0` to turn the + grouping off. + """ + groupedOtherThanSameLegsMaxCostMultiplier: Float = 2.0 +} + +""" +Plan location settings. Location must be set. Label is optional +and used for naming the location. +""" +input PlanLabeledLocationInput { + """ + A location that has to be used in an itinerary. + """ + location: PlanLocationInput! + + """ + A label that can be attached to the location. This label is then returned with the location + in the itineraries. + """ + label: String +} + +""" +Plan location. Either a coordinate or a stop location should be defined. +""" +input PlanLocationInput @oneOf { + """ + Coordinate of the location. Note, either a coordinate or a stop location should be defined. + """ + coordinate: PlanCoordinateInput + + """ + Stop, station, a group of stop places or multimodal stop place that should be used as + a location for the search. The trip doesn't have to use the given stop location for a + transit connection as it's possible to start walking to another stop from the given + location. If a station or a group of stop places is provided, a stop that makes the most + sense for the journey is picked as the location within the station or group of stop places. + """ + stopLocation: PlanStopLocationInput +} + +""" +Wrapper type for different types of preferences related to plan query. +""" +input PlanPreferencesInput { + """ + Street routing preferences used for ingress, egress and transfers. These do not directly affect + the transit legs but can change how preferable walking or cycling, for example, is compared to + transit. + """ + street: PlanStreetPreferencesInput + + """ + Transit routing preferences used for transit legs. + """ + transit: TransitPreferencesInput + + """ + Accessibility preferences that affect both the street and transit routing. + """ + accessibility: AccessibilityPreferencesInput +} + +""" +Stop, station, a group of stop places or multimodal stop place that should be used as +a location for the search. The trip doesn't have to use the given stop location for a +transit connection as it's possible to start walking to another stop from the given +location. If a station or a group of stop places is provided, a stop that makes the most +sense for the journey is picked as the location within the station or group of stop places. +""" +input PlanStopLocationInput { + """ + ID of the stop, station, a group of stop places or multimodal stop place. Format + should be `FeedId:StopLocationId`. + """ + stopLocationId: String! +} + +""" +Mode selections for the plan search. +""" +input PlanModesInput { + """ + Should only the direct search without any transit be done. + """ + directOnly: Boolean = false + + """ + Should only the transit search be done and never suggest itineraries that don't + contain any transit legs. + """ + transitOnly: Boolean = false + + """ + Street mode that is used when searching for itineraries that don't use any transit. + If more than one mode is selected, at least one of them must be used but not necessarily all. + There are modes that automatically also use walking such as the rental modes. To force rental + to be used, this should only include the rental mode and not `WALK` in addition. + The default access mode is `WALK`. + """ + direct: [PlanDirectMode!] + + """ + Modes for different phases of an itinerary when transit is included. Also + includes street mode selections related to connecting to the transit network + and transfers. By default, all transit modes are usable and `WALK` is used for + access, egress and transfers. + """ + transit: PlanTransitModesInput +} + +""" +Modes for different phases of an itinerary when transit is included. Also includes street +mode selections related to connecting to the transit network and transfers. +""" +input PlanTransitModesInput { + """ + Street mode that is used when searching for access to the transit network from origin. + If more than one mode is selected, at least one of them must be used but not necessarily all. + There are modes that automatically also use walking such as the rental modes. To force rental + to be used, this should only include the rental mode and not `WALK` in addition. + The default access mode is `WALK`. + """ + access: [PlanAccessMode!] + + """ + Street mode that is used when searching for egress to destination from the transit network. + If more than one mode is selected, at least one of them must be used but not necessarily all. + There are modes that automatically also use walking such as the rental modes. To force rental + to be used, this should only include the rental mode and not `WALK` in addition. + The default access mode is `WALK`. + """ + egress: [PlanEgressMode!] + + """ + Street mode that is used when searching for transfers. Selection of only one allowed for now. + The default transfer mode is `WALK`. + """ + transfer: [PlanTransferMode!] + + """ + Transit modes and reluctances associated with them. Each defined mode can be used in + an itinerary but doesn't have to be. If direct search is not disabled, there can be an + itinerary without any transit legs. By default, all transit modes are usable. + """ + transit: [PlanTransitModePreferenceInput!] +} + +""" +Street routing preferences used for ingress, egress and transfers. These do not directly affect +the transit legs but can change how preferable walking or cycling, for example, is compared to +transit. +""" +input PlanStreetPreferencesInput { + """ + Cycling related preferences. + """ + bicycle: BicyclePreferencesInput + + """ + Scooter (kick or electrical) related preferences. + """ + scooter: ScooterPreferencesInput + + """ + Car related preferences. These are not used for car travel as part of transit, such as + taxi travel. + """ + car: CarPreferencesInput + + """ + Walk related preferences. These are not used when walking a bicycle or a scooter as they + have their own preferences. + """ + walk: WalkPreferencesInput +} + """ Relative importances of optimization factors. Only effective for bicycling legs. Invariant: `timeFactor + slopeFactor + safetyFactor == 1` @@ -1283,6 +1747,44 @@ input InputTriangle { timeFactor: Float } +""" +Relative importance of optimization factors. Only effective for bicycling legs. +Invariant: `safety + flatness + time == 1` +""" +input TriangleCyclingFactorsInput { + """ + Relative importance of cycling safety, but this factor can also include other + concerns such as convenience and general cyclist preferences by taking into account + road surface etc. + """ + safety: Ratio! + + """Relative importance of flat terrain""" + flatness: Ratio! + + """Relative importance of duration""" + time: Ratio! +} + +""" +Relative importance of optimization factors. Only effective for scooter legs. +Invariant: `safety + flatness + time == 1` +""" +input TriangleScooterFactorsInput { + """ + Relative importance of scooter safety, but this factor can also include other + concerns such as convenience and general scooter preferences by taking into account + road surface etc. + """ + safety: Ratio! + + """Relative importance of flat terrain""" + flatness: Ratio! + + """Relative importance of duration""" + time: Ratio! +} + input InputUnpreferred { """A comma-separated list of ids of the routes unpreferred by the user.""" routes: String @@ -1970,7 +2472,7 @@ enum Mode { """TRAM""" TRAM - """"Private car trips shared with others.""" + """Private car trips shared with others.""" CARPOOL """A taxi, possibly operated by a public transport agency.""" @@ -1989,38 +2491,39 @@ enum Mode { MONORAIL } +""" +Transit modes include modes that are used within organized transportation networks +run by public transportation authorities, taxi companies etc. +Equivalent to GTFS route_type or to NeTEx TransportMode. +""" enum TransitMode { - """AIRPLANE""" AIRPLANE - """BUS""" BUS - """CABLE_CAR""" CABLE_CAR - """COACH""" COACH - """FERRY""" FERRY - """FUNICULAR""" FUNICULAR - """GONDOLA""" GONDOLA - """RAIL""" + """ + This includes long or short distance trains. + """ RAIL - """SUBWAY""" + """ + Subway or metro, depending on the local terminology. + """ SUBWAY - """TRAM""" TRAM - """"Private car trips shared with others.""" + """Private car trips shared with others.""" CARPOOL """A taxi, possibly operated by a public transport agency.""" @@ -2076,7 +2579,239 @@ type PageInfo { endCursor: String } -"""Realtime vehicle position""" +""" +Information about pagination in a connection. Part of the +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). +""" +type PlanPageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String + + """The search window that was used for the search in the current page.""" + searchWindowUsed: Duration +} + +""" +Street modes that can be used for access to the transit network from origin. +""" +enum PlanAccessMode { + """ + Cycling to a stop and boarding a vehicle with the bicycle. + Note, this can include walking when it's needed to walk the bicycle. + Access can use cycling only if the mode used for transfers + and egress is also `BICYCLE`. + """ + BICYCLE + + """ + Bicycle rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, access will include only walking. Also, this + can include walking before picking up or after dropping off the + bicycle or when it's needed to walk the bicycle. + """ + BICYCLE_RENTAL + + """ + Starting the itinerary with a bicycle and parking the bicycle to + a parking location. Note, this can include walking after parking + the bicycle or when it's needed to walk the bicycle. + """ + BICYCLE_PARKING + + """ + Car rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, access will include only walking. Also, this + can include walking before picking up or after dropping off the + car. + """ + CAR_RENTAL + + """ + Starting the itinerary with a car and parking the car to a parking location. + Note, this can include walking after parking the car. + """ + CAR_PARKING + + """ + Getting dropped off by a car to a location that is accessible with a car. + Note, this can include walking after the drop-off. + """ + CAR_DROP_OFF + + """ + Flexible transit. This can include different forms of flexible transit that + can be defined in GTFS-Flex or in Netex. Note, this can include walking before + or after the flexible transit leg. + """ + FLEX + + """ + Scooter rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, access will include only walking. Also, this + can include walking before picking up or after dropping off the + scooter. + """ + SCOOTER_RENTAL + + """ + Walking to a stop. + """ + WALK +} + +""" +Street mode that is used when searching for itineraries that don't use any transit. +""" +enum PlanDirectMode { + """ + Cycling from the origin to the destination. Note, this can include walking + when it's needed to walk the bicycle. + """ + BICYCLE + + """ + Bicycle rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, itinerary will include only walking. + Also, it can include walking before picking up or after dropping off the + bicycle or when it's needed to walk the bicycle. + """ + BICYCLE_RENTAL + + """ + Starting the itinerary with a bicycle and parking the bicycle to + a parking location. Note, this can include walking after parking + the bicycle or when it's needed to walk the bicycle. + """ + BICYCLE_PARKING + + """ + Driving a car from the origin to the destination. + """ + CAR + + """ + Car rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, itinerary will include only walking. Also, this + can include walking before picking up or after dropping off the + car. + """ + CAR_RENTAL + + """ + Starting the itinerary with a car and parking the car to a parking location. + Note, this can include walking after parking the car. + """ + CAR_PARKING + + """ + Flexible transit. This can include different forms of flexible transit that + can be defined in GTFS-Flex or in Netex. Note, this can include walking before + or after the flexible transit leg. + """ + FLEX + + """ + Scooter rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, itinerary will include only walking. Also, this + can include walking before picking up or after dropping off the + scooter. + """ + SCOOTER_RENTAL + + """ + Walking from the origin to the destination. Note, this can include walking + when it's needed to walk the bicycle. + """ + WALK +} + +""" +Street modes that can be used for egress from the transit network to destination. +""" +enum PlanEgressMode { + """ + Cycling from a stop to the destination. Note, this can include walking when + it's needed to walk the bicycle. Egress can use cycling only if the mode used + for access and transfers is also `BICYCLE`. + """ + BICYCLE + + """ + Bicycle rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, egress will include only walking. Also, this + can include walking before picking up or after dropping off the + bicycle or when it's needed to walk the bicycle. + """ + BICYCLE_RENTAL + + """ + Car rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, egress will include only walking. Also, this + can include walking before picking up or after dropping off the + car. + """ + CAR_RENTAL + + """ + Getting picked up by a car from a location that is accessible with a car. + Note, this can include walking before the pickup. + """ + CAR_PICKUP + + """ + Flexible transit. This can include different forms of flexible transit that + can be defined in GTFS-Flex or in Netex. Note, this can include walking before + or after the flexible transit leg. + """ + FLEX + + """ + Scooter rental can use either station based systems or "floating" + vehicles which are not linked to a rental station. Note, if there are no + rental options available, egress will include only walking. Also, this + can include walking before picking up or after dropping off the + scooter. + """ + SCOOTER_RENTAL + + """ + Walking from a stop to the destination. + """ + WALK +} + +enum PlanTransferMode { + """ + Cycling between transit vehicles (typically between stops). Note, this can + include walking when it's needed to walk the bicycle. Transfers can only use + cycling if the mode used for access and egress is also `BICYCLE`. + """ + BICYCLE + + """ + Walking between transit vehicles (typically between stops). + """ + WALK +} + +"""Real-time vehicle position""" type VehiclePosition { """Feed-scoped ID that uniquely identifies the vehicle in the format FeedId:VehicleId""" vehicleId: String, @@ -2194,7 +2929,7 @@ type Pattern implements Node { ): [Alert] """ - Realtime-updated position of vehicles that are serving this pattern. + Real-time updated position of vehicles that are serving this pattern. """ vehiclePositions: [VehiclePosition!] @@ -2267,27 +3002,134 @@ type BookingTime { } """ -Booking information for a stop time which has special requirements to use, like calling ahead or -using an app. +Booking information for a stop time which has special requirements to use, like calling ahead or +using an app. +""" +type BookingInfo { + "Contact information for reaching the service provider" + contactInfo: ContactInfo + "When is the earliest time the service can be booked." + earliestBookingTime: BookingTime + "When is the latest time the service can be booked" + latestBookingTime: BookingTime + "Minimum number of seconds before travel to make the request" + minimumBookingNoticeSeconds: Long + "Maximum number of seconds before travel to make the request" + maximumBookingNoticeSeconds: Long + "A general message for those booking the service" + message: String + "A message specific to the pick up" + pickupMessage: String + "A message specific to the drop off" + dropOffMessage: String +} + +""" +What criteria should be used when optimizing a cycling route. +""" +input CyclingOptimizationInput @oneOf { + """ + Use one of the predefined optimization types. + """ + type: CyclingOptimizationType + + """ + Define optimization by weighing three criteria. + """ + triangle: TriangleCyclingFactorsInput +} + +""" +Predefined optimization alternatives for bicycling routing. For more customization, +one can use the triangle factors. +""" +enum CyclingOptimizationType { + """ + Search for routes with the shortest duration while ignoring the cycling safety + of the streets (the routes should still follow local regulations). Routes can include + steep streets, if they are the fastest alternatives. This option was previously called + `QUICK`. + """ + SHORTEST_DURATION + + """ + Emphasize flatness over safety or duration of the route. This option was previously called `FLAT`. + """ + FLAT_STREETS + + """ + Emphasize cycling safety over flatness or duration of the route. Safety can also include other + concerns such as convenience and general cyclist preferences by taking into account + road surface etc. This option was previously called `SAFE`. + """ + SAFE_STREETS + + """ + Completely ignore the elevation differences and prefer the streets, that are evaluated + to be the safest, even more than with the `SAFE_STREETS` option. + Safety can also include other concerns such as convenience and general cyclist preferences + by taking into account road surface etc. This option was previously called `GREENWAYS`. + """ + SAFEST_STREETS +} + +""" +What criteria should be used when optimizing a scooter route. +""" +input ScooterOptimizationInput @oneOf { + """ + Use one of the predefined optimization types. + """ + type: ScooterOptimizationType + + """ + Define optimization by weighing three criteria. + """ + triangle: TriangleScooterFactorsInput +} + +""" +Predefined optimization alternatives for scooter routing. For more customization, +one can use the triangle factors. +""" +enum ScooterOptimizationType { + """ + Search for routes with the shortest duration while ignoring the scooter safety + of the streets. The routes should still follow local regulations, but currently scooters + are only allowed on the same streets as bicycles which might not be accurate for each country + or with different types of scooters. Routes can include steep streets, if they are + the fastest alternatives. This option was previously called `QUICK`. + """ + SHORTEST_DURATION + + """ + Emphasize flatness over safety or duration of the route. This option was previously called `FLAT`. + """ + FLAT_STREETS + + """ + Emphasize scooter safety over flatness or duration of the route. Safety can also include other + concerns such as convenience and general preferences by taking into account road surface etc. + Note, currently the same criteria is used both for cycling and scooter travel to determine how + safe streets are for cycling or scooter. This option was previously called `SAFE`. + """ + SAFE_STREETS + + """ + Completely ignore the elevation differences and prefer the streets, that are evaluated + to be safest for scooters, even more than with the `SAFE_STREETS` option. + Safety can also include other concerns such as convenience and general preferences by taking + into account road surface etc. Note, currently the same criteria is used both for cycling and + scooter travel to determine how safe streets are for cycling or scooter. + This option was previously called `GREENWAYS`. + """ + SAFEST_STREETS +} + +""" +Speed in meters per seconds. Values are positive floating point numbers (for example, 2.34). """ -type BookingInfo { - "Contact information for reaching the service provider" - contactInfo: ContactInfo - "When is the earliest time the service can be booked." - earliestBookingTime: BookingTime - "When is the latest time the service can be booked" - latestBookingTime: BookingTime - "Minimum number of seconds before travel to make the request" - minimumBookingNoticeSeconds: Long - "Maximum number of seconds before travel to make the request" - maximumBookingNoticeSeconds: Long - "A general message for those booking the service" - message: String - "A message specific to the pick up" - pickupMessage: String - "A message specific to the drop off" - dropOffMessage: String -} +scalar Speed "The board/alight position in between two stops of the pattern of a trip with continuous pickup/drop off." type PositionBetweenStops { @@ -2344,7 +3186,7 @@ type Place { The purpose of this field is to identify the stop within the pattern so it can be cross-referenced between it and the itinerary. It is safe to cross-reference when done quickly, i.e. within seconds. - However, it should be noted that realtime updates can change the values, so don't store it for + However, it should be noted that real-time updates can change the values, so don't store it for longer amounts of time. Depending on the source data, this might not be the GTFS `stop_sequence` but another value, perhaps @@ -2463,7 +3305,7 @@ type Plan { This is the suggested search time for the "previous page" or time window. Insert it together with the searchWindowUsed in the request to get a new set of trips preceding in the search-window BEFORE the current search. No duplicate trips should be returned, unless a trip - is delayed and new realtime-data is available. + is delayed and new real-time data is available. """ prevDateTime: Long @deprecated(reason: "Use previousPageCursor instead") @@ -2471,7 +3313,7 @@ type Plan { This is the suggested search time for the "next page" or time window. Insert it together with the searchWindowUsed in the request to get a new set of trips following in the search-window AFTER the current search. No duplicate trips should be returned, unless a trip - is delayed and new realtime-data is available. + is delayed and new real-time data is available. """ nextDateTime: Long @deprecated(reason: "Use nextPageCursor instead") @@ -2487,12 +3329,58 @@ type Plan { debugOutput: debugOutput! } +""" +Plan (result of an itinerary search) that follows +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). +""" +type PlanConnection { + """ + What was the starting point for the itinerary search. + """ + searchDateTime: OffsetDateTime + + """ + Errors faced during the routing search. + """ + routingErrors: [RoutingError!]! + + """ + Edges which contain the itineraries. Part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + edges: [PlanEdge] + + """ + Contains cursors to continue the search and the information if there are more itineraries available. + Part of the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + pageInfo: PlanPageInfo! +} + +""" +Edge outputted by a plan search. Part of the +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). +""" +type PlanEdge { + """ + An itinerary suggestion. Part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + node: Itinerary! + + """ + The cursor of the edge. Part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + cursor: String! +} + """ List of coordinates in an encoded polyline format (see https://developers.google.com/maps/documentation/utilities/polylinealgorithm). The value appears in JSON as a string. """ -scalar Polyline +scalar Polyline @specifiedBy(url: "https://developers.google.com/maps/documentation/utilities/polylinealgorithm") """ Additional qualifier for a transport mode. @@ -3336,6 +4224,109 @@ type QueryType { """ startTransitTripId: String @deprecated(reason: "Not implemented in OTP2") ): Plan @async + + """ + Plan (itinerary) search that follows + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). + """ + planConnection( + """ + Datetime of the search. It's possible to either define the earliest departure time + or the latest arrival time. By default, earliest departure time is set as now. + """ + dateTime: PlanDateTimeInput + + """ + Duration of the search window. This either starts at the defined earliest departure + time or ends at the latest arrival time. If this is not provided, a reasonable + search window is automatically generated. When searching for earlier or later itineraries + with paging, this search window is no longer used and the new window will be based + on how many suggestions were returned in the previous search. The new search window can be + shorter or longer than the original search window. Note, itineraries are returned faster + with a smaller search window and search window limitation is done mainly for performance reasons. + + Setting this parameter makes especially sense if the transportation network is as sparse or dense + in the whole itinerary search area. Otherwise, letting the system decide what is the search window + is in combination of using paging can lead to better performance and to getting a more consistent + number of itineraries in each search. + """ + searchWindow: Duration + + """ + The origin where the search starts. Usually coordinates but can also be a stop location. + """ + origin: PlanLabeledLocationInput! + + """ + The destination where the search ends. Usually coordinates but can also be a stop location. + """ + destination: PlanLabeledLocationInput! + + """ + Street and transit modes used during the search. This also includes options to only return + an itinerary that contains no transit legs or force transit to be used in all itineraries. + By default, all transit modes are usable and `WALK` is used for direct street suggestions, + access, egress and transfers. + """ + modes: PlanModesInput + + """ + Preferences that affect what itineraries are returned. Preferences are split into categories. + """ + preferences: PlanPreferencesInput + + """ + Settings that control the behavior of itinerary filtering. These are advanced settings and + should not be set by a user through user preferences. + """ + itineraryFilter: PlanItineraryFilterInput + + """ + Locale used for translations. Note, there might not necessarily be translations available. + It's possible and recommended to use the ´accept-language´ header instead of this parameter. + """ + locale: Locale + + """ + Takes in cursor from a previous search. Used for forward pagination. If earliest departure time + is used in the original query, the new search then returns itineraries that depart after + the start time of the last itinerary that was returned, or at the same time if there are multiple + itinerary options that can depart at that moment in time. + If latest arrival time is defined, the new search returns itineraries that arrive before the end + time of the last itinerary that was returned in the previous search, or at the same time if there + are multiple itinerary options that can arrive at that moment in time. This parameter is part of + the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) and + should be used together with the `first` parameter. + """ + after: String + + """ + How many new itineraries should at maximum be returned in either the first search or with + forward pagination. This parameter is part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) + and should be used together with the `after` parameter (although `after` shouldn't be defined + in the first search). + """ + first: Int + + """ + Takes in cursor from a previous search. Used for backwards pagination. If earliest departure time + is used in the original query, the new search then returns itineraries that depart before that time. + If latest arrival time is defined, the new search returns itineraries that arrive after that time. + This parameter is part of the [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) + and should be used together with the `last` parameter. + """ + before: String + + """ + How many new itineraries should at maximum be returned in backwards pagination. It's recommended to + use the same value as was used for the `first` parameter in the original search for optimal + performance. This parameter is part of the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) + and should be used together with the `before` parameter. + """ + last: Int + ): PlanConnection @async @deprecated(reason: "Experimental and can include breaking changes, use plan instead") } enum RealtimeState { @@ -3364,6 +4355,71 @@ enum RealtimeState { MODIFIED } +""" +A cost multiplier for how bad something is compared to being in transit for equal lengths of time. +The value should be greater than 0. 1 means neutral and values below 1 mean that something is +preferred over transit. + +""" +scalar Reluctance + +""" +Preferences related to scooter rental (station based or floating scooter rental). +""" +input ScooterRentalPreferencesInput { + """ + Is it possible to arrive to the destination with a rented scooter and does it + come with an extra cost. + """ + destinationScooterPolicy: DestinationScooterPolicyInput + + """ + Rental networks which can be potentially used as part of an itinerary. + """ + allowedNetworks: [String!] + + """ + Rental networks which cannot be used as part of an itinerary. + """ + bannedNetworks: [String!] +} + +""" +Preferences related to car rental (station based or floating car rental). +""" +input CarRentalPreferencesInput { + """ + Rental networks which can be potentially used as part of an itinerary. + """ + allowedNetworks: [String!] + + """ + Rental networks which cannot be used as part of an itinerary. + """ + bannedNetworks: [String!] +} + +""" +Preferences related to bicycle rental (station based or floating bicycle rental). +""" +input BicycleRentalPreferencesInput { + """ + Is it possible to arrive to the destination with a rented bicycle and does it + come with an extra cost. + """ + destinationBicyclePolicy: DestinationBicyclePolicyInput + + """ + Rental networks which can be potentially used as part of an itinerary. + """ + allowedNetworks: [String!] + + """ + Rental networks which cannot be used as part of an itinerary. + """ + bannedNetworks: [String!] +} + """ Route represents a public transportation service, usually from point A to point B and *back*, shown to customers under a single name, e.g. bus 550. Routes @@ -3612,6 +4668,34 @@ enum OccupancyStatus { NOT_ACCEPTING_PASSENGERS } +""" +Preferences related to travel with a scooter (kick or e-scooter). +""" +input ScooterPreferencesInput { + """ + A multiplier for how bad riding a scooter is compared to being in transit + for equal lengths of time. + """ + reluctance: Reluctance + + """ + Maximum speed on flat ground while riding a scooter. Note, this speed is higher than + the average speed will be in itineraries as this is the maximum speed but there are + factors that slow down the travel such as crossings, intersections and elevation changes. + """ + speed: Speed + + """ + What criteria should be used when optimizing a scooter route. + """ + optimization: ScooterOptimizationInput + + """ + Scooter rental related preferences. + """ + rental: ScooterRentalPreferencesInput +} + type step { """The distance in meters that this step takes.""" distance: Float @@ -3957,7 +5041,7 @@ type Stoptime { The purpose of this field is to identify the stop within the pattern so it can be cross-referenced between it and the itinerary. It is safe to cross-reference when done quickly, i.e. within seconds. - However, it should be noted that realtime updates can change the values, so don't store it for + However, it should be noted that real-time updates can change the values, so don't store it for longer amounts of time. Depending on the source data, this might not be the GTFS `stop_sequence` but another value, perhaps @@ -3971,7 +5055,7 @@ type Stoptime { scheduledArrival: Int """ - Realtime prediction of arrival time. Format: seconds since midnight of the departure date + Real-time prediction of arrival time. Format: seconds since midnight of the departure date """ realtimeArrival: Int @@ -3987,7 +5071,7 @@ type Stoptime { scheduledDeparture: Int """ - Realtime prediction of departure time. Format: seconds since midnight of the departure date + Real-time prediction of departure time. Format: seconds since midnight of the departure date """ realtimeDeparture: Int @@ -4072,6 +5156,115 @@ type TicketType implements Node { zones: [String!] } +input TimetablePreferencesInput { + """ + When false, real-time updates are considered during the routing. + In practice, when this option is set as true, some of the suggestions might not be + realistic as the transfers could be invalid due to delays, + trips can be cancelled or stops can be skipped. + """ + excludeRealTimeUpdates: Boolean + + """ + When true, departures that have been cancelled ahead of time will be + included during the routing. This means that an itinerary can include + a cancelled departure while some other alternative that contains no cancellations + could be filtered out as the alternative containing a cancellation would normally + be better. + """ + includePlannedCancellations: Boolean + + """ + When true, departures that have been cancelled through a real-time feed will be + included during the routing. This means that an itinerary can include + a cancelled departure while some other alternative that contains no cancellations + could be filtered out as the alternative containing a cancellation would normally + be better. This option can't be set to true while `includeRealTimeUpdates` is false. + """ + includeRealTimeCancellations: Boolean +} + +""" +Preferences related to transfers between transit vehicles (typically between stops). +""" +input TransferPreferencesInput { + """ + A static cost that is added for each transfer on top of other costs. + """ + cost: Cost + + """ + A global minimum transfer time (in seconds) that specifies the minimum amount of time + that must pass between exiting one transit vehicle and boarding another. This time is + in addition to time it might take to walk between transit stops. Setting this value + as `PT0S`, for example, can lead to passenger missing a connection when the vehicle leaves + ahead of time or the passenger arrives to the stop later than expected. + """ + slack: Duration + + """ + How many additional transfers there can be at maximum compared to the itinerary with the + least number of transfers. + """ + maximumAdditionalTransfers: Int + + """ + How many transfers there can be at maximum in an itinerary. + """ + maximumTransfers: Int +} + +""" +Transit mode and a reluctance associated with it. +""" +input PlanTransitModePreferenceInput { + """ + Transit mode that could be (but doesn't have to be) used in an itinerary. + """ + mode: TransitMode! + + """ + Costs related to using a transit mode. + """ + cost: TransitModePreferenceCostInput +} + +""" +Costs related to using a transit mode. +""" +input TransitModePreferenceCostInput { + """ + A cost multiplier of transit leg travel time. + """ + reluctance: Reluctance! +} + +""" +Transit routing preferences used for transit legs. +""" +input TransitPreferencesInput { + """ + Preferences related to boarding a transit vehicle. Note, board costs for each street mode + can be found under the street mode preferences. + """ + board: BoardPreferencesInput + + """ + Preferences related to alighting from a transit vehicle. + """ + alight: AlightPreferencesInput + + """ + Preferences related to transfers between transit vehicles (typically between stops). + """ + transfer: TransferPreferencesInput + + """ + Preferences related to cancellations and real-time. + """ + timetable: TimetablePreferencesInput +} + """Text with language""" type TranslatedString { text: String @@ -4192,12 +5385,38 @@ type Trip implements Node { ): [Alert] """ - The latest realtime occupancy information for the latest occurance of this + The latest real-time occupancy information for the latest occurance of this trip. """ occupancy: TripOccupancy } +""" +Enable this to attach a system notice to itineraries instead of removing them. This is very +convenient when tuning the itinerary-filter-chain. +""" +enum ItineraryFilterDebugProfile { + """ + Only return the requested number of itineraries, counting both actual and deleted ones. + The top `numItineraries` using the request sort order is returned. This does not work + with paging, itineraries after the limit, but inside the search-window are skipped when + moving to the next page. + """ + LIMIT_TO_NUMBER_OF_ITINERARIES + + """ + Return all itineraries, including deleted ones, inside the actual search-window used + (the requested search-window may differ). + """ + LIMIT_TO_SEARCH_WINDOW + + "List all itineraries, including all deleted itineraries." + LIST_ALL + + "By default, the debug itinerary filters is turned off." + OFF +} + """Entities, which are relevant for a trip and can contain alerts""" enum TripAlertType { """Alerts affecting the trip""" @@ -4273,6 +5492,79 @@ enum VertexType { PARKANDRIDE } +""" +Preferences for walking a bicycle. +""" +input BicycleWalkPreferencesInput { + """ + Maximum walk speed on flat ground. Note, this speed is higher than the average speed + will be in itineraries as this is the maximum speed but there are + factors that slow down walking such as crossings, intersections and elevation changes. + """ + speed: Speed + + """ + Costs related to walking a bicycle. + """ + cost: BicycleWalkPreferencesCostInput + + """" + How long it takes to hop on or off a bicycle when switching to walking the bicycle + or when getting on the bicycle again. However, this is not applied when getting + on a rented bicycle for the first time or off the bicycle when returning the bicycle. + """ + mountDismountTime: Duration +} + +""" +Costs related to walking a bicycle. +""" +input BicycleWalkPreferencesCostInput { + """ + A static cost that is added each time hopping on or off a bicycle to start or end + bicycle walking. However, this cost is not applied when getting on a rented bicycle + for the first time or when getting off the bicycle when returning the bicycle. + """ + mountDismountCost: Cost + + """ + A cost multiplier of bicycle walking travel time. The multiplier is for how bad + walking the bicycle is compared to being in transit for equal lengths of time. + """ + reluctance: Reluctance +} + +""" +Preferences related to walking (excluding walking a bicycle or a scooter). +""" +input WalkPreferencesInput { + """ + Maximum walk speed on flat ground. Note, this speed is higher than the average speed + will be in itineraries as this is the maximum speed but there are + factors that slow down walking such as crossings, intersections and elevation changes. + """ + speed: Speed + + """ + A multiplier for how bad walking is compared to being in transit for equal lengths of time. + """ + reluctance: Reluctance + + """ + Factor for how much the walk safety is considered in routing. Value should be between 0 and 1. + If the value is set to be 0, safety is ignored. + """ + safetyFactor: Ratio + + """ + The cost of boarding a vehicle while walking. + """ + boardCost: Cost +} + +"""A fractional multiplier between 0 and 1, for example 0.25. 0 means 0% and 1 means 100%.""" +scalar Ratio + enum WheelchairBoarding { """There is no accessibility information for the stop.""" NO_INFORMATION @@ -4285,3 +5577,15 @@ enum WheelchairBoarding { """Wheelchair boarding is not possible at this stop.""" NOT_POSSIBLE } + +""" +Wheelchair related preferences. Note, this is the only from of accessibilty available +currently and is sometimes is used for other accessibility needs as well. +""" +input WheelchairPreferencesInput { + """ + Is wheelchair accessibility considered in routing. Note, this does not guarantee + that the itineraries are wheelchair accessible as there can be data issues. + """ + enabled: Boolean +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/CoordinateValueScalarTest.java b/src/test/java/org/opentripplanner/apis/gtfs/CoordinateValueScalarTest.java new file mode 100644 index 00000000000..99042498342 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/CoordinateValueScalarTest.java @@ -0,0 +1,102 @@ +package org.opentripplanner.apis.gtfs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import java.math.BigDecimal; +import java.math.BigInteger; +import org.junit.jupiter.api.Test; + +class CoordinateValueScalarTest { + + private static final Double COORDINATE = 10.0; + private static final Integer COORDINATE_INT = 10; + private static final double COORDINATE_MAX = 180.0; + private static final double COORDINATE_MIN = 180.0; + private static final double TOO_HIGH = 190; + private static final double TOO_LOW = -190; + private static final String TEXT = "foo"; + private static final double DELTA = 0.0001; + + @Test + void testSerialize() { + var coordinate = (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR + .getCoercing() + .serialize(COORDINATE); + assertEquals(COORDINATE, coordinate, DELTA); + coordinate = + (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR + .getCoercing() + .serialize(COORDINATE.floatValue()); + assertEquals(COORDINATE, coordinate, DELTA); + assertThrows( + CoercingSerializeException.class, + () -> GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().serialize(TEXT) + ); + } + + @Test + void testParseValue() { + var coordinate = (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR + .getCoercing() + .parseValue(COORDINATE); + assertEquals(COORDINATE, coordinate, DELTA); + coordinate = + (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().parseValue(COORDINATE_MIN); + assertEquals(COORDINATE_MIN, coordinate, DELTA); + coordinate = + (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().parseValue(COORDINATE_MAX); + assertEquals(COORDINATE_MAX, coordinate, DELTA); + coordinate = + (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().parseValue(COORDINATE_INT); + assertEquals(COORDINATE_INT, coordinate, DELTA); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().parseValue(TEXT) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().parseValue(TOO_LOW) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().parseValue(TOO_HIGH) + ); + } + + @Test + void testParseLiteral() { + var coordinateDouble = (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(COORDINATE))); + assertEquals(COORDINATE, coordinateDouble, DELTA); + var coordinateInt = (Double) GraphQLScalars.COORDINATE_VALUE_SCALAR + .getCoercing() + .parseLiteral(new IntValue(BigInteger.valueOf(COORDINATE.intValue()))); + assertEquals(COORDINATE, coordinateInt, DELTA); + assertThrows( + CoercingParseLiteralException.class, + () -> GraphQLScalars.COORDINATE_VALUE_SCALAR.getCoercing().parseLiteral(new StringValue(TEXT)) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.COORDINATE_VALUE_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(TOO_HIGH))) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.COORDINATE_VALUE_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(TOO_LOW))) + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/CostScalarTest.java b/src/test/java/org/opentripplanner/apis/gtfs/CostScalarTest.java new file mode 100644 index 00000000000..bb8b5bfe445 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/CostScalarTest.java @@ -0,0 +1,78 @@ +package org.opentripplanner.apis.gtfs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import graphql.language.IntValue; +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import java.math.BigInteger; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; + +class CostScalarTest { + + private static final Cost COST_THIRTY = Cost.costOfSeconds(30); + private static final int THIRTY = 30; + private static final int NEGATIVE_THIRTY = -30; + private static final int TOO_HIGH = 300000000; + private static final String TEXT = "foo"; + + @Test + void testSerialize() { + var cost = GraphQLScalars.COST_SCALAR.getCoercing().serialize(COST_THIRTY); + assertEquals(THIRTY, cost); + var costNumber = GraphQLScalars.COST_SCALAR.getCoercing().serialize(THIRTY); + assertEquals(THIRTY, costNumber); + assertThrows( + CoercingSerializeException.class, + () -> GraphQLScalars.COST_SCALAR.getCoercing().serialize(TEXT) + ); + } + + @Test + void testParseValue() { + var cost = GraphQLScalars.COST_SCALAR.getCoercing().parseValue(THIRTY); + assertEquals(COST_THIRTY, cost); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.COST_SCALAR.getCoercing().parseValue(TEXT) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.COST_SCALAR.getCoercing().parseValue(NEGATIVE_THIRTY) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.COST_SCALAR.getCoercing().parseValue(TOO_HIGH) + ); + } + + @Test + void testParseLiteral() { + var cost = GraphQLScalars.COST_SCALAR + .getCoercing() + .parseLiteral(new IntValue(BigInteger.valueOf(THIRTY))); + assertEquals(COST_THIRTY, cost); + assertThrows( + CoercingParseLiteralException.class, + () -> GraphQLScalars.COST_SCALAR.getCoercing().parseLiteral(new StringValue(TEXT)) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.COST_SCALAR + .getCoercing() + .parseLiteral(new IntValue(BigInteger.valueOf(NEGATIVE_THIRTY))) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.COST_SCALAR + .getCoercing() + .parseLiteral(new IntValue(BigInteger.valueOf(TOO_HIGH))) + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/RatioScalarTest.java b/src/test/java/org/opentripplanner/apis/gtfs/RatioScalarTest.java new file mode 100644 index 00000000000..b628ef3f5d1 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/RatioScalarTest.java @@ -0,0 +1,89 @@ +package org.opentripplanner.apis.gtfs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import java.math.BigDecimal; +import java.math.BigInteger; +import org.junit.jupiter.api.Test; + +class RatioScalarTest { + + private static final Double HALF = 0.5; + private static final Integer ZERO = 0; + private static final Integer ONE = 1; + private static final double TOO_HIGH = 1.1; + private static final double TOO_LOW = -1.1; + private static final String TEXT = "foo"; + private static final double DELTA = 0.0001; + + @Test + void testSerialize() { + var ratio = (Double) GraphQLScalars.RATIO_SCALAR.getCoercing().serialize(HALF); + assertEquals(HALF, ratio, DELTA); + ratio = (Double) GraphQLScalars.RATIO_SCALAR.getCoercing().serialize(HALF.floatValue()); + assertEquals(HALF, ratio, DELTA); + assertThrows( + CoercingSerializeException.class, + () -> GraphQLScalars.RATIO_SCALAR.getCoercing().serialize(TEXT) + ); + } + + @Test + void testParseValue() { + var ratio = (Double) GraphQLScalars.RATIO_SCALAR.getCoercing().parseValue(HALF); + assertEquals(HALF, ratio, DELTA); + ratio = (Double) GraphQLScalars.RATIO_SCALAR.getCoercing().parseValue(ZERO); + assertEquals(ZERO, ratio, DELTA); + ratio = (Double) GraphQLScalars.RATIO_SCALAR.getCoercing().parseValue(ONE); + assertEquals(ONE, ratio, DELTA); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.RATIO_SCALAR.getCoercing().parseValue(TEXT) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.RATIO_SCALAR.getCoercing().parseValue(TOO_LOW) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.RATIO_SCALAR.getCoercing().parseValue(TOO_HIGH) + ); + } + + @Test + void testParseLiteral() { + var ratioDouble = (Double) GraphQLScalars.RATIO_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(HALF))); + assertEquals(HALF, ratioDouble, DELTA); + var ratioInt = (Double) GraphQLScalars.RATIO_SCALAR + .getCoercing() + .parseLiteral(new IntValue(BigInteger.valueOf(HALF.intValue()))); + assertEquals(HALF.intValue(), ratioInt, DELTA); + assertThrows( + CoercingParseLiteralException.class, + () -> GraphQLScalars.RATIO_SCALAR.getCoercing().parseLiteral(new StringValue(TEXT)) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.RATIO_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(TOO_HIGH))) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.RATIO_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(TOO_LOW))) + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/ReluctanceScalarTest.java b/src/test/java/org/opentripplanner/apis/gtfs/ReluctanceScalarTest.java new file mode 100644 index 00000000000..40684e85885 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/ReluctanceScalarTest.java @@ -0,0 +1,87 @@ +package org.opentripplanner.apis.gtfs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import java.math.BigDecimal; +import java.math.BigInteger; +import org.junit.jupiter.api.Test; + +class ReluctanceScalarTest { + + private static final Double HALF = 0.5; + private static final Integer ONE = 1; + private static final double TOO_HIGH = 100001; + private static final double TOO_LOW = 0; + private static final String TEXT = "foo"; + private static final double DELTA = 0.0001; + + @Test + void testSerialize() { + var reluctance = (Double) GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().serialize(HALF); + assertEquals(HALF, reluctance, DELTA); + reluctance = + (Double) GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().serialize(HALF.floatValue()); + assertEquals(HALF, reluctance, DELTA); + assertThrows( + CoercingSerializeException.class, + () -> GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().serialize(TEXT) + ); + } + + @Test + void testParseValue() { + var reluctanceDouble = (Double) GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().parseValue(HALF); + assertEquals(HALF, reluctanceDouble, DELTA); + var reluctanceInt = (Double) GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().parseValue(ONE); + assertEquals(ONE, reluctanceInt, DELTA); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().parseValue(TEXT) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().parseValue(TOO_LOW) + ); + assertThrows( + CoercingParseValueException.class, + () -> GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().parseValue(TOO_HIGH) + ); + } + + @Test + void testParseLiteral() { + var reluctanceDouble = (Double) GraphQLScalars.RELUCTANCE_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(HALF))); + assertEquals(HALF, reluctanceDouble, DELTA); + var reluctanceInt = (Double) GraphQLScalars.RELUCTANCE_SCALAR + .getCoercing() + .parseLiteral(new IntValue(BigInteger.valueOf(ONE))); + assertEquals(ONE, reluctanceInt, DELTA); + assertThrows( + CoercingParseLiteralException.class, + () -> GraphQLScalars.RELUCTANCE_SCALAR.getCoercing().parseLiteral(new StringValue(TEXT)) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.RELUCTANCE_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(TOO_HIGH))) + ); + assertThrows( + CoercingParseLiteralException.class, + () -> + GraphQLScalars.RELUCTANCE_SCALAR + .getCoercing() + .parseLiteral(new FloatValue(BigDecimal.valueOf(TOO_LOW))) + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapperTest.java similarity index 88% rename from src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java rename to src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapperTest.java index 22a8583d705..382543fc501 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/LegacyRouteRequestMapperTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.apis.gtfs.mapping; +package org.opentripplanner.apis.gtfs.mapping.routerequest; import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -38,7 +38,7 @@ import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; -class RouteRequestMapperTest implements PlanTestConstants { +class LegacyRouteRequestMapperTest implements PlanTestConstants { static final GraphQLRequestContext context; @@ -83,7 +83,7 @@ void parkingFilters() { var env = executionContext(arguments); - var routeRequest = RouteRequestMapper.toRouteRequest(env, context); + var routeRequest = LegacyRouteRequestMapper.toRouteRequest(env, context); assertNotNull(routeRequest); @@ -118,7 +118,10 @@ static Stream banningCases() { void banning(Map banned, String expectedFilters) { Map arguments = Map.of("banned", banned); - var routeRequest = RouteRequestMapper.toRouteRequest(executionContext(arguments), context); + var routeRequest = LegacyRouteRequestMapper.toRouteRequest( + executionContext(arguments), + context + ); assertNotNull(routeRequest); assertEquals(expectedFilters, routeRequest.journey().transit().filters().toString()); @@ -144,7 +147,10 @@ static Stream transportModesCases() { void modes(List> modes, String expectedFilters) { Map arguments = Map.of("transportModes", modes); - var routeRequest = RouteRequestMapper.toRouteRequest(executionContext(arguments), context); + var routeRequest = LegacyRouteRequestMapper.toRouteRequest( + executionContext(arguments), + context + ); assertNotNull(routeRequest); assertEquals(expectedFilters, routeRequest.journey().transit().filters().toString()); @@ -157,7 +163,10 @@ private static Map mode(String mode) { @Test void defaultBikeOptimize() { Map arguments = Map.of(); - var routeRequest = RouteRequestMapper.toRouteRequest(executionContext(arguments), context); + var routeRequest = LegacyRouteRequestMapper.toRouteRequest( + executionContext(arguments), + context + ); assertEquals(SAFE_STREETS, routeRequest.preferences().bike().optimizeType()); } @@ -170,7 +179,10 @@ void bikeTriangle() { Map.of("safetyFactor", 0.2, "slopeFactor", 0.1, "timeFactor", 0.7) ); - var routeRequest = RouteRequestMapper.toRouteRequest(executionContext(arguments), context); + var routeRequest = LegacyRouteRequestMapper.toRouteRequest( + executionContext(arguments), + context + ); assertEquals(TRIANGLE, routeRequest.preferences().bike().optimizeType()); assertEquals( @@ -196,7 +208,10 @@ void noTriangle(GraphQLTypes.GraphQLOptimizeType bot) { Map.of("safetyFactor", 0.2, "slopeFactor", 0.1, "timeFactor", 0.7) ); - var routeRequest = RouteRequestMapper.toRouteRequest(executionContext(arguments), context); + var routeRequest = LegacyRouteRequestMapper.toRouteRequest( + executionContext(arguments), + context + ); assertEquals(OptimizationTypeMapper.map(bot), routeRequest.preferences().bike().optimizeType()); assertEquals( @@ -210,10 +225,16 @@ void walkReluctance() { var reluctance = 119d; Map arguments = Map.of("walkReluctance", reluctance); - var routeRequest = RouteRequestMapper.toRouteRequest(executionContext(arguments), context); + var routeRequest = LegacyRouteRequestMapper.toRouteRequest( + executionContext(arguments), + context + ); assertEquals(reluctance, routeRequest.preferences().walk().reluctance()); - var noParamsRequest = RouteRequestMapper.toRouteRequest(executionContext(Map.of()), context); + var noParamsRequest = LegacyRouteRequestMapper.toRouteRequest( + executionContext(Map.of()), + context + ); assertNotEquals(reluctance, noParamsRequest.preferences().walk().reluctance()); } diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperAccessibilityTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperAccessibilityTest.java new file mode 100644 index 00000000000..99377aa7e46 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperAccessibilityTest.java @@ -0,0 +1,31 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.createArgsCopy; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.executionContext; + +import java.util.Locale; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class RouteRequestMapperAccessibilityTest { + + @Test + void testWheelchairPreferences() { + var args = createArgsCopy(RouteRequestMapperTest.ARGS); + var wheelchairEnabled = true; + args.put( + "preferences", + Map.ofEntries( + entry( + "accessibility", + Map.ofEntries(entry("wheelchair", Map.ofEntries(entry("enabled", wheelchairEnabled)))) + ) + ) + ); + var env = executionContext(args, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + assertEquals(wheelchairEnabled, routeRequest.wheelchair()); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperBicycleTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperBicycleTest.java new file mode 100644 index 00000000000..698390557e1 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperBicycleTest.java @@ -0,0 +1,318 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.createArgsCopy; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.executionContext; + +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +class RouteRequestMapperBicycleTest { + + @Test + void testBasicBikePreferences() { + var bicycleArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var reluctance = 7.5; + var speed = 15d; + var boardCost = Cost.costOfSeconds(50); + bicycleArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries( + entry("reluctance", reluctance), + entry("speed", speed), + entry("boardCost", boardCost) + ) + ) + ) + ) + ) + ); + var env = executionContext(bicycleArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var bikePreferences = routeRequest.preferences().bike(); + assertEquals(reluctance, bikePreferences.reluctance()); + assertEquals(speed, bikePreferences.speed()); + assertEquals(boardCost.toSeconds(), bikePreferences.boardCost()); + } + + @Test + void testBikeWalkPreferences() { + var bicycleArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var walkSpeed = 7d; + var mountDismountTime = Duration.ofSeconds(23); + var mountDismountCost = Cost.costOfSeconds(35); + var walkReluctance = 6.3; + bicycleArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries( + entry( + "walk", + Map.ofEntries( + entry("speed", walkSpeed), + entry("mountDismountTime", mountDismountTime), + entry( + "cost", + Map.ofEntries( + entry("mountDismountCost", mountDismountCost), + entry("reluctance", walkReluctance) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(bicycleArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var bikeWalkingPreferences = routeRequest.preferences().bike().walking(); + assertEquals(walkSpeed, bikeWalkingPreferences.speed()); + assertEquals(mountDismountTime, bikeWalkingPreferences.mountDismountTime()); + assertEquals(mountDismountCost, bikeWalkingPreferences.mountDismountCost()); + assertEquals(walkReluctance, bikeWalkingPreferences.reluctance()); + } + + @Test + void testBikeTrianglePreferences() { + var bicycleArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var bikeSafety = 0.3; + var bikeFlatness = 0.5; + var bikeTime = 0.2; + bicycleArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries( + entry( + "optimization", + Map.ofEntries( + entry( + "triangle", + Map.ofEntries( + entry("safety", bikeSafety), + entry("flatness", bikeFlatness), + entry("time", bikeTime) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(bicycleArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var bikePreferences = routeRequest.preferences().bike(); + assertEquals(VehicleRoutingOptimizeType.TRIANGLE, bikePreferences.optimizeType()); + var bikeTrianglePreferences = bikePreferences.optimizeTriangle(); + assertEquals(bikeSafety, bikeTrianglePreferences.safety()); + assertEquals(bikeFlatness, bikeTrianglePreferences.slope()); + assertEquals(bikeTime, bikeTrianglePreferences.time()); + } + + @Test + void testBikeOptimizationPreferences() { + var bicycleArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + bicycleArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries(entry("optimization", Map.ofEntries(entry("type", "SAFEST_STREETS")))) + ) + ) + ) + ) + ); + var env = executionContext(bicycleArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var bikePreferences = routeRequest.preferences().bike(); + assertEquals(VehicleRoutingOptimizeType.SAFEST_STREETS, bikePreferences.optimizeType()); + } + + @Test + void testBikeRentalPreferences() { + var bicycleArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var allowed = Set.of("foo", "bar"); + var banned = Set.of("not"); + var allowKeeping = true; + var keepingCost = Cost.costOfSeconds(150); + bicycleArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries( + entry( + "rental", + Map.ofEntries( + entry("allowedNetworks", allowed.stream().toList()), + entry("bannedNetworks", banned.stream().toList()), + entry( + "destinationBicyclePolicy", + Map.ofEntries( + entry("allowKeeping", allowKeeping), + entry("keepingCost", keepingCost) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(bicycleArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var bikeRentalPreferences = routeRequest.preferences().bike().rental(); + assertEquals(allowed, bikeRentalPreferences.allowedNetworks()); + assertEquals(banned, bikeRentalPreferences.bannedNetworks()); + assertEquals(allowKeeping, bikeRentalPreferences.allowArrivingInRentedVehicleAtDestination()); + assertEquals(keepingCost, bikeRentalPreferences.arrivingInRentalVehicleAtDestinationCost()); + } + + @Test + void testEmptyBikeRentalPreferences() { + var bikeArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var empty = Set.of(); + bikeArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries( + entry("rental", Map.ofEntries(entry("allowedNetworks", empty.stream().toList()))) + ) + ) + ) + ) + ) + ); + var allowedEnv = executionContext(bikeArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(allowedEnv, RouteRequestMapperTest.CONTEXT) + ); + + bikeArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + bikeArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries( + entry("rental", Map.ofEntries(entry("bannedNetworks", empty.stream().toList()))) + ) + ) + ) + ) + ) + ); + var bannedEnv = executionContext(bikeArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(bannedEnv, RouteRequestMapperTest.CONTEXT); + var bikeRentalPreferences = routeRequest.preferences().bike().rental(); + assertEquals(empty, bikeRentalPreferences.bannedNetworks()); + } + + @Test + void testBikeParkingPreferences() { + var bicycleArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var unpreferredCost = Cost.costOfSeconds(150); + var notFilter = List.of("wheelbender"); + var selectFilter = List.of("locker", "roof"); + var unpreferred = List.of("bad"); + var preferred = List.of("a", "b"); + bicycleArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "bicycle", + Map.ofEntries( + entry( + "parking", + Map.ofEntries( + entry("unpreferredCost", unpreferredCost), + entry( + "filters", + List.of( + Map.ofEntries( + entry("not", List.of(Map.of("tags", notFilter))), + entry("select", List.of(Map.of("tags", selectFilter))) + ) + ) + ), + entry( + "preferred", + List.of( + Map.ofEntries( + entry("not", List.of(Map.of("tags", unpreferred))), + entry("select", List.of(Map.of("tags", preferred))) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(bicycleArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var bikeParkingPreferences = routeRequest.preferences().bike().parking(); + assertEquals(unpreferredCost, bikeParkingPreferences.unpreferredVehicleParkingTagCost()); + assertEquals( + "VehicleParkingFilter{not: [tags=%s], select: [tags=%s]}".formatted(notFilter, selectFilter), + bikeParkingPreferences.filter().toString() + ); + assertEquals( + "VehicleParkingFilter{not: [tags=%s], select: [tags=%s]}".formatted(unpreferred, preferred), + bikeParkingPreferences.preferred().toString() + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperCarTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperCarTest.java new file mode 100644 index 00000000000..6c8109676f0 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperCarTest.java @@ -0,0 +1,175 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.createArgsCopy; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.executionContext; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; + +class RouteRequestMapperCarTest { + + @Test + void testBasicCarPreferences() { + var carArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var reluctance = 7.5; + carArgs.put( + "preferences", + Map.ofEntries(entry("street", Map.ofEntries(entry("car", Map.of("reluctance", reluctance))))) + ); + var env = executionContext(carArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var carPreferences = routeRequest.preferences().car(); + assertEquals(reluctance, carPreferences.reluctance()); + } + + @Test + void testCarRentalPreferences() { + var carArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var allowed = Set.of("foo", "bar"); + var banned = Set.of("not"); + carArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "car", + Map.ofEntries( + entry( + "rental", + Map.ofEntries( + entry("allowedNetworks", allowed.stream().toList()), + entry("bannedNetworks", banned.stream().toList()) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(carArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var carRentalPreferences = routeRequest.preferences().car().rental(); + assertEquals(allowed, carRentalPreferences.allowedNetworks()); + assertEquals(banned, carRentalPreferences.bannedNetworks()); + } + + @Test + void testEmptyCarRentalPreferences() { + var carArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var empty = Set.of(); + carArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "car", + Map.ofEntries( + entry("rental", Map.ofEntries(entry("allowedNetworks", empty.stream().toList()))) + ) + ) + ) + ) + ) + ); + var allowedEnv = executionContext(carArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(allowedEnv, RouteRequestMapperTest.CONTEXT) + ); + + carArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + carArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "car", + Map.ofEntries( + entry("rental", Map.ofEntries(entry("bannedNetworks", empty.stream().toList()))) + ) + ) + ) + ) + ) + ); + var bannedEnv = executionContext(carArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(bannedEnv, RouteRequestMapperTest.CONTEXT); + var carRentalPreferences = routeRequest.preferences().car().rental(); + assertEquals(empty, carRentalPreferences.bannedNetworks()); + } + + @Test + void testCarParkingPreferences() { + var carArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var unpreferredCost = Cost.costOfSeconds(150); + var notFilter = List.of("wheelbender"); + var selectFilter = List.of("locker", "roof"); + var unpreferred = List.of("bad"); + var preferred = List.of("a", "b"); + carArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "car", + Map.ofEntries( + entry( + "parking", + Map.ofEntries( + entry("unpreferredCost", unpreferredCost), + entry( + "filters", + List.of( + Map.ofEntries( + entry("not", List.of(Map.of("tags", notFilter))), + entry("select", List.of(Map.of("tags", selectFilter))) + ) + ) + ), + entry( + "preferred", + List.of( + Map.ofEntries( + entry("not", List.of(Map.of("tags", unpreferred))), + entry("select", List.of(Map.of("tags", preferred))) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(carArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var carParkingPreferences = routeRequest.preferences().car().parking(); + assertEquals(unpreferredCost, carParkingPreferences.unpreferredVehicleParkingTagCost()); + assertEquals( + "VehicleParkingFilter{not: [tags=%s], select: [tags=%s]}".formatted(notFilter, selectFilter), + carParkingPreferences.filter().toString() + ); + assertEquals( + "VehicleParkingFilter{not: [tags=%s], select: [tags=%s]}".formatted(unpreferred, preferred), + carParkingPreferences.preferred().toString() + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperModesTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperModesTest.java new file mode 100644 index 00000000000..910bb916a7b --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperModesTest.java @@ -0,0 +1,233 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.createArgsCopy; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.executionContext; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opentripplanner.routing.api.request.StreetMode; +import org.opentripplanner.transit.model.basic.TransitMode; + +class RouteRequestMapperModesTest { + + @Test + void testDirectOnly() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + modesArgs.put("modes", Map.ofEntries(entry("directOnly", true))); + var env = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + assertFalse(routeRequest.journey().transit().enabled()); + } + + @Test + void testTransitOnly() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + modesArgs.put("modes", Map.ofEntries(entry("transitOnly", true))); + var env = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + assertEquals(StreetMode.NOT_SET, routeRequest.journey().direct().mode()); + } + + @Test + void testStreetModesWithOneValidMode() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var bicycle = List.of("BICYCLE"); + modesArgs.put( + "modes", + Map.ofEntries( + entry("direct", List.of("CAR")), + entry( + "transit", + Map.ofEntries( + entry("access", bicycle), + entry("egress", bicycle), + entry("transfer", bicycle) + ) + ) + ) + ); + var env = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + assertEquals(StreetMode.CAR, routeRequest.journey().direct().mode()); + assertEquals(StreetMode.BIKE, routeRequest.journey().access().mode()); + assertEquals(StreetMode.BIKE, routeRequest.journey().egress().mode()); + assertEquals(StreetMode.BIKE, routeRequest.journey().transfer().mode()); + } + + @Test + void testStreetModesWithOneInvalidMode() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var bicycleRental = List.of("BICYCLE_RENTAL"); + modesArgs.put("modes", Map.ofEntries(entry("direct", bicycleRental))); + var env = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT) + ); + } + + @Test + void testStreetModesWithTwoValidModes() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var bicycleRental = List.of("BICYCLE_RENTAL", "WALK"); + modesArgs.put( + "modes", + Map.ofEntries( + entry("direct", bicycleRental), + entry( + "transit", + Map.ofEntries(entry("access", bicycleRental), entry("egress", bicycleRental)) + ) + ) + ); + var env = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + assertEquals(StreetMode.BIKE_RENTAL, routeRequest.journey().direct().mode()); + assertEquals(StreetMode.BIKE_RENTAL, routeRequest.journey().access().mode()); + assertEquals(StreetMode.BIKE_RENTAL, routeRequest.journey().egress().mode()); + assertEquals(StreetMode.WALK, routeRequest.journey().transfer().mode()); + } + + @Test + void testStreetModesWithTwoInvalidModes() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var rentals = List.of("BICYCLE_RENTAL", "CAR_RENTAL"); + modesArgs.put( + "modes", + Map.ofEntries( + entry("direct", rentals), + entry("transit", Map.ofEntries(entry("access", rentals), entry("egress", rentals))) + ) + ); + var rentalEnv = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(rentalEnv, RouteRequestMapperTest.CONTEXT) + ); + + modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var bicycleWalk = List.of("BICYCLE", "WALK"); + modesArgs.put( + "modes", + Map.ofEntries( + entry("transit", Map.ofEntries(entry("access", bicycleWalk), entry("egress", bicycleWalk))) + ) + ); + var bicycleWalkEnv = executionContext( + modesArgs, + Locale.ENGLISH, + RouteRequestMapperTest.CONTEXT + ); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(bicycleWalkEnv, RouteRequestMapperTest.CONTEXT) + ); + } + + @Test + void testStreetModesWithThreeModes() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var rentals = List.of("WALK", "BICYCLE_RENTAL", "CAR_RENTAL"); + modesArgs.put( + "modes", + Map.ofEntries( + entry("direct", rentals), + entry("transit", Map.ofEntries(entry("access", rentals), entry("egress", rentals))) + ) + ); + var env = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT) + ); + } + + @Test + void testTransitModes() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var tramCost = 1.5; + modesArgs.put( + "modes", + Map.ofEntries( + entry( + "transit", + Map.ofEntries( + entry( + "transit", + List.of( + Map.ofEntries( + entry("mode", "TRAM"), + entry("cost", Map.ofEntries(entry("reluctance", tramCost))) + ), + Map.ofEntries(entry("mode", "FERRY")) + ) + ) + ) + ) + ) + ); + var env = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var reluctanceForMode = routeRequest.preferences().transit().reluctanceForMode(); + assertEquals(tramCost, reluctanceForMode.get(TransitMode.TRAM)); + assertNull(reluctanceForMode.get(TransitMode.FERRY)); + assertEquals( + "[TransitFilterRequest{select: [SelectRequest{transportModes: [FERRY, TRAM]}]}]", + routeRequest.journey().transit().filters().toString() + ); + } + + @Test + void testStreetModesWithEmptyModes() { + var modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var empty = List.of(); + modesArgs.put("modes", Map.ofEntries(entry("direct", empty))); + var directEnv = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(directEnv, RouteRequestMapperTest.CONTEXT) + ); + + modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + modesArgs.put("modes", Map.ofEntries(entry("transit", Map.ofEntries(entry("access", empty))))); + var accessEnv = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(accessEnv, RouteRequestMapperTest.CONTEXT) + ); + + modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + modesArgs.put("modes", Map.ofEntries(entry("transit", Map.ofEntries(entry("egress", empty))))); + var egressEnv = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(egressEnv, RouteRequestMapperTest.CONTEXT) + ); + + modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + modesArgs.put( + "modes", + Map.ofEntries(entry("transit", Map.ofEntries(entry("transfer", empty)))) + ); + var transferEnv = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(transferEnv, RouteRequestMapperTest.CONTEXT) + ); + + modesArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + modesArgs.put("modes", Map.ofEntries(entry("transit", Map.ofEntries(entry("transit", empty))))); + var transitEnv = executionContext(modesArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(transitEnv, RouteRequestMapperTest.CONTEXT) + ); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperScooterTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperScooterTest.java new file mode 100644 index 00000000000..de15a45d0c9 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperScooterTest.java @@ -0,0 +1,204 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.createArgsCopy; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.executionContext; + +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; +import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; + +class RouteRequestMapperScooterTest { + + @Test + void testBasicScooterPreferences() { + var scooterArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var reluctance = 7.5; + var speed = 15d; + scooterArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry("scooter", Map.ofEntries(entry("reluctance", reluctance), entry("speed", speed))) + ) + ) + ) + ); + var env = executionContext(scooterArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var scooterPreferences = routeRequest.preferences().scooter(); + assertEquals(reluctance, scooterPreferences.reluctance()); + assertEquals(speed, scooterPreferences.speed()); + } + + @Test + void testScooterTrianglePreferences() { + var scooterArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var scooterSafety = 0.3; + var scooterFlatness = 0.5; + var scooterTime = 0.2; + scooterArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "scooter", + Map.ofEntries( + entry( + "optimization", + Map.ofEntries( + entry( + "triangle", + Map.ofEntries( + entry("safety", scooterSafety), + entry("flatness", scooterFlatness), + entry("time", scooterTime) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(scooterArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var scooterPreferences = routeRequest.preferences().scooter(); + assertEquals(VehicleRoutingOptimizeType.TRIANGLE, scooterPreferences.optimizeType()); + var scooterTrianglePreferences = scooterPreferences.optimizeTriangle(); + assertEquals(scooterSafety, scooterTrianglePreferences.safety()); + assertEquals(scooterFlatness, scooterTrianglePreferences.slope()); + assertEquals(scooterTime, scooterTrianglePreferences.time()); + } + + @Test + void testScooterOptimizationPreferences() { + var scooterArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + scooterArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "scooter", + Map.ofEntries(entry("optimization", Map.ofEntries(entry("type", "SAFEST_STREETS")))) + ) + ) + ) + ) + ); + var env = executionContext(scooterArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var scooterPreferences = routeRequest.preferences().scooter(); + assertEquals(VehicleRoutingOptimizeType.SAFEST_STREETS, scooterPreferences.optimizeType()); + } + + @Test + void testScooterRentalPreferences() { + var scooterArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var allowed = Set.of("foo", "bar"); + var banned = Set.of("not"); + var allowKeeping = true; + var keepingCost = Cost.costOfSeconds(150); + scooterArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "scooter", + Map.ofEntries( + entry( + "rental", + Map.ofEntries( + entry("allowedNetworks", allowed.stream().toList()), + entry("bannedNetworks", banned.stream().toList()), + entry( + "destinationScooterPolicy", + Map.ofEntries( + entry("allowKeeping", allowKeeping), + entry("keepingCost", keepingCost) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ); + var env = executionContext(scooterArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var scooterRentalPreferences = routeRequest.preferences().scooter().rental(); + assertEquals(allowed, scooterRentalPreferences.allowedNetworks()); + assertEquals(banned, scooterRentalPreferences.bannedNetworks()); + assertEquals( + allowKeeping, + scooterRentalPreferences.allowArrivingInRentedVehicleAtDestination() + ); + assertEquals(keepingCost, scooterRentalPreferences.arrivingInRentalVehicleAtDestinationCost()); + } + + @Test + void testEmptyScooterRentalPreferences() { + var scooterArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var empty = Set.of(); + scooterArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "scooter", + Map.ofEntries( + entry("rental", Map.ofEntries(entry("allowedNetworks", empty.stream().toList()))) + ) + ) + ) + ) + ) + ); + var allowedEnv = executionContext(scooterArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + assertThrows( + IllegalArgumentException.class, + () -> RouteRequestMapper.toRouteRequest(allowedEnv, RouteRequestMapperTest.CONTEXT) + ); + + scooterArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + scooterArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "scooter", + Map.ofEntries( + entry("rental", Map.ofEntries(entry("bannedNetworks", empty.stream().toList()))) + ) + ) + ) + ) + ) + ); + var bannedEnv = executionContext(scooterArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(bannedEnv, RouteRequestMapperTest.CONTEXT); + var scooterRentalPreferences = routeRequest.preferences().scooter().rental(); + assertEquals(empty, scooterRentalPreferences.bannedNetworks()); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTest.java new file mode 100644 index 00000000000..f47f7a7004a --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTest.java @@ -0,0 +1,320 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static graphql.Assert.assertTrue; +import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder; +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import graphql.ExecutionInput; +import graphql.execution.ExecutionId; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetchingEnvironmentImpl; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.opentripplanner._support.time.ZoneIds; +import org.opentripplanner.apis.gtfs.GraphQLRequestContext; +import org.opentripplanner.apis.gtfs.TestRoutingService; +import org.opentripplanner.ext.fares.impl.DefaultFareService; +import org.opentripplanner.model.plan.paging.cursor.PageCursor; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.routing.api.request.preference.ItineraryFilterDebugProfile; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graphfinder.GraphFinder; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; +import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.service.DefaultTransitService; +import org.opentripplanner.transit.service.TransitModel; + +class RouteRequestMapperTest { + + private static final Coordinate ORIGIN = new Coordinate(1.0, 2.0); + private static final Coordinate DESTINATION = new Coordinate(2.0, 1.0); + private static final Locale LOCALE = Locale.GERMAN; + static final GraphQLRequestContext CONTEXT; + static final Map ARGS = Map.ofEntries( + entry( + "origin", + Map.ofEntries( + entry("location", Map.of("coordinate", Map.of("latitude", ORIGIN.x, "longitude", ORIGIN.y))) + ) + ), + entry( + "destination", + Map.ofEntries( + entry( + "location", + Map.of("coordinate", Map.of("latitude", DESTINATION.x, "longitude", DESTINATION.y)) + ) + ) + ) + ); + + static { + Graph graph = new Graph(); + var transitModel = new TransitModel(); + transitModel.initTimeZone(ZoneIds.BERLIN); + final DefaultTransitService transitService = new DefaultTransitService(transitModel); + CONTEXT = + new GraphQLRequestContext( + new TestRoutingService(List.of()), + transitService, + new DefaultFareService(), + graph.getVehicleParkingService(), + new DefaultVehicleRentalService(), + new DefaultRealtimeVehicleService(transitService), + GraphFinder.getInstance(graph, transitService::findRegularStops), + new RouteRequest() + ); + } + + @Test + void testMinimalArgs() { + var env = executionContext(ARGS, LOCALE, CONTEXT); + var defaultRequest = new RouteRequest(); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(ORIGIN.x, routeRequest.from().lat); + assertEquals(ORIGIN.y, routeRequest.from().lng); + assertEquals(DESTINATION.x, routeRequest.to().lat); + assertEquals(DESTINATION.y, routeRequest.to().lng); + assertEquals(LOCALE, routeRequest.locale()); + assertEquals(defaultRequest.wheelchair(), routeRequest.wheelchair()); + assertEquals(defaultRequest.arriveBy(), routeRequest.arriveBy()); + assertEquals(defaultRequest.isTripPlannedForNow(), routeRequest.isTripPlannedForNow()); + assertEquals(defaultRequest.numItineraries(), routeRequest.numItineraries()); + assertEquals(defaultRequest.searchWindow(), routeRequest.searchWindow()); + assertEquals(defaultRequest.journey().modes(), routeRequest.journey().modes()); + assertEquals(1, defaultRequest.journey().transit().filters().size()); + assertEquals(1, routeRequest.journey().transit().filters().size()); + assertTrue(routeRequest.journey().transit().enabled()); + assertEquals( + defaultRequest.journey().transit().filters().toString(), + routeRequest.journey().transit().filters().toString() + ); + assertTrue( + Duration + .between(defaultRequest.dateTime(), routeRequest.dateTime()) + .compareTo(Duration.ofSeconds(10)) < + 0 + ); + + // Using current time as datetime changes rental availability use preferences, therefore to + // check that the preferences are equal, we need to use a future time. + var futureArgs = createArgsCopy(ARGS); + var futureTime = OffsetDateTime.of( + LocalDate.of(3000, 3, 15), + LocalTime.MIDNIGHT, + ZoneOffset.UTC + ); + futureArgs.put("dateTime", Map.ofEntries(entry("earliestDeparture", futureTime))); + var futureEnv = executionContext(futureArgs, LOCALE, CONTEXT); + var futureRequest = RouteRequestMapper.toRouteRequest(futureEnv, CONTEXT); + assertEquals(defaultRequest.preferences(), futureRequest.preferences()); + } + + @Test + void testEarliestDeparture() { + var dateTimeArgs = createArgsCopy(ARGS); + var dateTime = OffsetDateTime.of(LocalDate.of(2020, 3, 15), LocalTime.MIDNIGHT, ZoneOffset.UTC); + dateTimeArgs.put("dateTime", Map.ofEntries(entry("earliestDeparture", dateTime))); + var env = executionContext(dateTimeArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(dateTime.toInstant(), routeRequest.dateTime()); + assertFalse(routeRequest.arriveBy()); + assertFalse(routeRequest.isTripPlannedForNow()); + } + + @Test + void testLatestArrival() { + var dateTimeArgs = createArgsCopy(ARGS); + var dateTime = OffsetDateTime.of(LocalDate.of(2020, 3, 15), LocalTime.MIDNIGHT, ZoneOffset.UTC); + dateTimeArgs.put("dateTime", Map.ofEntries(entry("latestArrival", dateTime))); + var env = executionContext(dateTimeArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(dateTime.toInstant(), routeRequest.dateTime()); + assertTrue(routeRequest.arriveBy()); + assertFalse(routeRequest.isTripPlannedForNow()); + } + + @Test + void testRentalAvailability() { + var nowArgs = createArgsCopy(ARGS); + var nowEnv = executionContext(nowArgs, LOCALE, CONTEXT); + var nowRequest = RouteRequestMapper.toRouteRequest(nowEnv, CONTEXT); + assertTrue(nowRequest.preferences().bike().rental().useAvailabilityInformation()); + assertTrue(nowRequest.preferences().car().rental().useAvailabilityInformation()); + assertTrue(nowRequest.preferences().scooter().rental().useAvailabilityInformation()); + + var futureArgs = createArgsCopy(ARGS); + var futureTime = OffsetDateTime.of( + LocalDate.of(3000, 3, 15), + LocalTime.MIDNIGHT, + ZoneOffset.UTC + ); + futureArgs.put("dateTime", Map.ofEntries(entry("earliestDeparture", futureTime))); + var futureEnv = executionContext(futureArgs, LOCALE, CONTEXT); + var futureRequest = RouteRequestMapper.toRouteRequest(futureEnv, CONTEXT); + assertFalse(futureRequest.preferences().bike().rental().useAvailabilityInformation()); + assertFalse(futureRequest.preferences().car().rental().useAvailabilityInformation()); + assertFalse(futureRequest.preferences().scooter().rental().useAvailabilityInformation()); + } + + @Test + void testStopLocationAndLabel() { + Map stopLocationArgs = new HashMap<>(); + var stopA = "foo:1"; + var stopB = "foo:2"; + var originLabel = "start"; + var destinationLabel = "end"; + stopLocationArgs.put( + "origin", + Map.ofEntries( + entry("location", Map.of("stopLocation", Map.of("stopLocationId", stopA))), + entry("label", originLabel) + ) + ); + stopLocationArgs.put( + "destination", + Map.ofEntries( + entry("location", Map.of("stopLocation", Map.of("stopLocationId", stopB))), + entry("label", destinationLabel) + ) + ); + var env = executionContext(stopLocationArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(FeedScopedId.parse(stopA), routeRequest.from().stopId); + assertEquals(originLabel, routeRequest.from().label); + assertEquals(FeedScopedId.parse(stopB), routeRequest.to().stopId); + assertEquals(destinationLabel, routeRequest.to().label); + } + + @Test + void testLocale() { + var englishLocale = Locale.ENGLISH; + var localeArgs = createArgsCopy(ARGS); + localeArgs.put("locale", englishLocale); + var env = executionContext(localeArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(englishLocale, routeRequest.locale()); + } + + @Test + void testSearchWindow() { + var searchWindow = Duration.ofHours(5); + var searchWindowArgs = createArgsCopy(ARGS); + searchWindowArgs.put("searchWindow", searchWindow); + var env = executionContext(searchWindowArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(searchWindow, routeRequest.searchWindow()); + } + + @Test + void testBefore() { + var before = PageCursor.decode( + "MXxQUkVWSU9VU19QQUdFfDIwMjQtMDMtMTVUMTM6MzU6MzlafHw0MG18U1RSRUVUX0FORF9BUlJJVkFMX1RJTUV8ZmFsc2V8MjAyNC0wMy0xNVQxNDoyODoxNFp8MjAyNC0wMy0xNVQxNToxNDoyMlp8MXw0MjUzfA==" + ); + var last = 8; + var beforeArgs = createArgsCopy(ARGS); + beforeArgs.put("before", before.encode()); + beforeArgs.put("first", 3); + beforeArgs.put("last", last); + beforeArgs.put("numberOfItineraries", 3); + var env = executionContext(beforeArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(before, routeRequest.pageCursor()); + assertEquals(last, routeRequest.numItineraries()); + } + + @Test + void testAfter() { + var after = PageCursor.decode( + "MXxORVhUX1BBR0V8MjAyNC0wMy0xNVQxNDo0MzoxNFp8fDQwbXxTVFJFRVRfQU5EX0FSUklWQUxfVElNRXxmYWxzZXwyMDI0LTAzLTE1VDE0OjI4OjE0WnwyMDI0LTAzLTE1VDE1OjE0OjIyWnwxfDQyNTN8" + ); + var first = 8; + var afterArgs = createArgsCopy(ARGS); + afterArgs.put("after", after.encode()); + afterArgs.put("first", first); + afterArgs.put("last", 3); + afterArgs.put("numberOfItineraries", 3); + var env = executionContext(afterArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(after, routeRequest.pageCursor()); + assertEquals(first, routeRequest.numItineraries()); + } + + @Test + void testNumberOfItinerariesForSearchWithoutPaging() { + var itineraries = 8; + var itinArgs = createArgsCopy(ARGS); + itinArgs.put("first", itineraries); + itinArgs.put("last", 3); + var env = executionContext(itinArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + assertEquals(itineraries, routeRequest.numItineraries()); + } + + @Test + void testItineraryFilters() { + var filterArgs = createArgsCopy(ARGS); + var profile = ItineraryFilterDebugProfile.LIMIT_TO_NUM_OF_ITINERARIES; + var keepOne = 0.4; + var keepThree = 0.6; + var multiplier = 3.5; + filterArgs.put( + "itineraryFilter", + Map.ofEntries( + entry("itineraryFilterDebugProfile", "LIMIT_TO_NUMBER_OF_ITINERARIES"), + entry("groupSimilarityKeepOne", keepOne), + entry("groupSimilarityKeepThree", keepThree), + entry("groupedOtherThanSameLegsMaxCostMultiplier", multiplier) + ) + ); + var env = executionContext(filterArgs, LOCALE, CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, CONTEXT); + var itinFilter = routeRequest.preferences().itineraryFilter(); + assertEquals(profile, itinFilter.debug()); + assertEquals(keepOne, itinFilter.groupSimilarityKeepOne()); + assertEquals(keepThree, itinFilter.groupSimilarityKeepThree()); + assertEquals(multiplier, itinFilter.groupedOtherThanSameLegsMaxCostMultiplier()); + } + + static Map createArgsCopy(Map arguments) { + Map newArgs = new HashMap<>(); + newArgs.putAll(arguments); + return newArgs; + } + + static DataFetchingEnvironment executionContext( + Map arguments, + Locale locale, + GraphQLRequestContext requestContext + ) { + ExecutionInput executionInput = ExecutionInput + .newExecutionInput() + .query("") + .operationName("planConnection") + .context(requestContext) + .locale(locale) + .build(); + + var executionContext = newExecutionContextBuilder() + .executionInput(executionInput) + .executionId(ExecutionId.from("planConnectionTest")) + .build(); + return DataFetchingEnvironmentImpl + .newDataFetchingEnvironment(executionContext) + .arguments(arguments) + .localContext(Map.of("locale", locale)) + .build(); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTransitTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTransitTest.java new file mode 100644 index 00000000000..5b2149afe32 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperTransitTest.java @@ -0,0 +1,126 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.createArgsCopy; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.executionContext; + +import java.time.Duration; +import java.util.Locale; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; +import org.opentripplanner.transit.model.basic.TransitMode; + +class RouteRequestMapperTransitTest { + + @Test + void testBoardPreferences() { + var args = createArgsCopy(RouteRequestMapperTest.ARGS); + var reluctance = 7.5; + var slack = Duration.ofSeconds(125); + args.put( + "preferences", + Map.ofEntries( + entry( + "transit", + Map.ofEntries( + entry( + "board", + Map.ofEntries(entry("waitReluctance", reluctance), entry("slack", slack)) + ) + ) + ) + ) + ); + var env = executionContext(args, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var transferPreferences = routeRequest.preferences().transfer(); + assertEquals(reluctance, transferPreferences.waitReluctance()); + var transitPreferences = routeRequest.preferences().transit(); + assertEquals(slack, transitPreferences.boardSlack().valueOf(TransitMode.BUS)); + } + + @Test + void testAlightPreferences() { + var args = createArgsCopy(RouteRequestMapperTest.ARGS); + var slack = Duration.ofSeconds(125); + args.put( + "preferences", + Map.ofEntries( + entry("transit", Map.ofEntries(entry("alight", Map.ofEntries(entry("slack", slack))))) + ) + ); + var env = executionContext(args, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var transitPreferences = routeRequest.preferences().transit(); + assertEquals(slack, transitPreferences.alightSlack().valueOf(TransitMode.BUS)); + } + + @Test + void testTransferPreferences() { + var args = createArgsCopy(RouteRequestMapperTest.ARGS); + var cost = Cost.costOfSeconds(75); + var slack = Duration.ofSeconds(125); + var maximumAdditionalTransfers = 1; + var maximumTransfers = 3; + args.put( + "preferences", + Map.ofEntries( + entry( + "transit", + Map.ofEntries( + entry( + "transfer", + Map.ofEntries( + entry("cost", cost), + entry("slack", slack), + entry("maximumAdditionalTransfers", maximumAdditionalTransfers), + entry("maximumTransfers", maximumTransfers) + ) + ) + ) + ) + ) + ); + var env = executionContext(args, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var transferPreferences = routeRequest.preferences().transfer(); + assertEquals(cost.toSeconds(), transferPreferences.cost()); + assertEquals(slack.toSeconds(), transferPreferences.slack()); + assertEquals(maximumAdditionalTransfers, transferPreferences.maxAdditionalTransfers()); + assertEquals(maximumTransfers + 1, transferPreferences.maxTransfers()); + } + + @Test + void testTimetablePreferences() { + var args = createArgsCopy(RouteRequestMapperTest.ARGS); + var excludeRealTimeUpdates = true; + var includePlannedCancellations = true; + var includeRealTimeCancellations = true; + args.put( + "preferences", + Map.ofEntries( + entry( + "transit", + Map.ofEntries( + entry( + "timetable", + Map.ofEntries( + entry("excludeRealTimeUpdates", excludeRealTimeUpdates), + entry("includePlannedCancellations", includePlannedCancellations), + entry("includeRealTimeCancellations", includeRealTimeCancellations) + ) + ) + ) + ) + ) + ); + var env = executionContext(args, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var transitPreferences = routeRequest.preferences().transit(); + assertEquals(excludeRealTimeUpdates, transitPreferences.ignoreRealtimeUpdates()); + assertEquals(includePlannedCancellations, transitPreferences.includePlannedCancellations()); + assertEquals(includeRealTimeCancellations, transitPreferences.includeRealtimeCancellations()); + } +} diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperWalkTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperWalkTest.java new file mode 100644 index 00000000000..86d249419e1 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/routerequest/RouteRequestMapperWalkTest.java @@ -0,0 +1,49 @@ +package org.opentripplanner.apis.gtfs.mapping.routerequest; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.createArgsCopy; +import static org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapperTest.executionContext; + +import java.util.Locale; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.model.Cost; + +class RouteRequestMapperWalkTest { + + @Test + void testWalkPreferences() { + var walkArgs = createArgsCopy(RouteRequestMapperTest.ARGS); + var reluctance = 7.5; + var speed = 15d; + var boardCost = Cost.costOfSeconds(50); + var safetyFactor = 0.4; + walkArgs.put( + "preferences", + Map.ofEntries( + entry( + "street", + Map.ofEntries( + entry( + "walk", + Map.ofEntries( + entry("reluctance", reluctance), + entry("speed", speed), + entry("boardCost", boardCost), + entry("safetyFactor", safetyFactor) + ) + ) + ) + ) + ) + ); + var env = executionContext(walkArgs, Locale.ENGLISH, RouteRequestMapperTest.CONTEXT); + var routeRequest = RouteRequestMapper.toRouteRequest(env, RouteRequestMapperTest.CONTEXT); + var walkPreferences = routeRequest.preferences().walk(); + assertEquals(reluctance, walkPreferences.reluctance()); + assertEquals(speed, walkPreferences.speed()); + assertEquals(boardCost.toSeconds(), walkPreferences.boardCost()); + assertEquals(safetyFactor, walkPreferences.safetyFactor()); + } +} diff --git a/src/test/java/org/opentripplanner/framework/graphql/GraphQLUtilsTest.java b/src/test/java/org/opentripplanner/framework/graphql/GraphQLUtilsTest.java index 9a1e2a2ba90..b72cb6e5a0d 100644 --- a/src/test/java/org/opentripplanner/framework/graphql/GraphQLUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/graphql/GraphQLUtilsTest.java @@ -73,9 +73,11 @@ void testGetLocaleWithDefinedLocaleArg() { var frenchLocale = Locale.FRENCH; - var locale = GraphQLUtils.getLocale(env, frenchLocale.toString()); + var localeWithString = GraphQLUtils.getLocale(env, frenchLocale.toString()); + assertEquals(frenchLocale, localeWithString); - assertEquals(frenchLocale, locale); + var localeWithLocale = GraphQLUtils.getLocale(env, frenchLocale); + assertEquals(frenchLocale, localeWithLocale); } @Test diff --git a/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java b/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java index 2457aa14b60..97b8afc52c0 100644 --- a/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java @@ -4,6 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.params.provider.Arguments.of; import static org.opentripplanner.framework.time.DurationUtils.requireNonNegative; +import static org.opentripplanner.framework.time.DurationUtils.requireNonNegativeLong; +import static org.opentripplanner.framework.time.DurationUtils.requireNonNegativeMedium; +import static org.opentripplanner.framework.time.DurationUtils.requireNonNegativeShort; import static org.opentripplanner.framework.time.DurationUtils.toIntMilliseconds; import java.time.Duration; @@ -124,6 +127,45 @@ public void testRequireNonNegative() { assertThrows(IllegalArgumentException.class, () -> requireNonNegative(Duration.ofSeconds(-1))); } + @Test + public void testRequireNonNegativeLong() { + assertThrows(NullPointerException.class, () -> requireNonNegativeLong(null, "test")); + assertThrows( + IllegalArgumentException.class, + () -> requireNonNegativeLong(Duration.ofSeconds(-1), "test") + ); + assertThrows( + IllegalArgumentException.class, + () -> requireNonNegativeLong(Duration.ofDays(3), "test") + ); + } + + @Test + public void testRequireNonNegativeMedium() { + assertThrows(NullPointerException.class, () -> requireNonNegativeMedium(null, "test")); + assertThrows( + IllegalArgumentException.class, + () -> requireNonNegativeMedium(Duration.ofSeconds(-1), "test") + ); + assertThrows( + IllegalArgumentException.class, + () -> requireNonNegativeMedium(Duration.ofHours(3), "test") + ); + } + + @Test + public void testRequireNonNegativeShort() { + assertThrows(NullPointerException.class, () -> requireNonNegativeShort(null, "test")); + assertThrows( + IllegalArgumentException.class, + () -> requireNonNegativeShort(Duration.ofSeconds(-1), "test") + ); + assertThrows( + IllegalArgumentException.class, + () -> requireNonNegativeShort(Duration.ofMinutes(31), "test") + ); + } + @Test public void testToIntMilliseconds() { assertEquals(20, toIntMilliseconds(null, 20)); diff --git a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java new file mode 100644 index 00000000000..8f5c278b586 --- /dev/null +++ b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotManagerTest.java @@ -0,0 +1,123 @@ +package org.opentripplanner.updater.trip; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.updater.trip.TimetableSnapshotManagerTest.SameAssert.NotSame; +import static org.opentripplanner.updater.trip.TimetableSnapshotManagerTest.SameAssert.Same; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.Month; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.model.TimetableSnapshot; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.RealTimeTripTimes; +import org.opentripplanner.transit.model.timetable.ScheduledTripTimes; +import org.opentripplanner.updater.TimetableSnapshotSourceParameters; + +class TimetableSnapshotManagerTest { + + private static final LocalDate TODAY = LocalDate.of(2024, Month.MAY, 30); + private static final LocalDate TOMORROW = TODAY.plusDays(1); + private static final LocalDate YESTERDAY = TODAY.minusDays(1); + + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final TripPattern PATTERN = TransitModelForTest + .tripPattern("pattern", TransitModelForTest.route("r1").build()) + .withStopPattern( + TransitModelForTest.stopPattern(TEST_MODEL.stop("1").build(), TEST_MODEL.stop("2").build()) + ) + .build(); + private static final RealTimeTripTimes TRIP_TIMES = RealTimeTripTimes.of( + ScheduledTripTimes + .of() + .withArrivalTimes("00:00 00:01") + .withTrip(TransitModelForTest.trip("trip").build()) + .build() + ); + + enum SameAssert { + Same { + public void test(Object a, Object b) { + assertSame(a, b); + } + }, + NotSame { + public void test(Object a, Object b) { + assertNotSame(a, b); + } + }; + + abstract void test(Object a, Object b); + + SameAssert not() { + return this == Same ? NotSame : Same; + } + } + + static Stream purgeExpiredDataTestCases() { + return Stream.of( + // purgeExpiredData maxSnapshotFrequency || snapshots PatternSnapshotA PatternSnapshotB + Arguments.of(Boolean.TRUE, -1, NotSame, NotSame), + Arguments.of(Boolean.FALSE, -1, NotSame, Same), + Arguments.of(Boolean.TRUE, 1000, NotSame, NotSame), + Arguments.of(Boolean.FALSE, 1000, Same, Same) + ); + } + + @ParameterizedTest(name = "purgeExpired: {0}, maxFrequency: {1} || {2} {3}") + @MethodSource("purgeExpiredDataTestCases") + public void testPurgeExpiredData( + boolean purgeExpiredData, + int maxSnapshotFrequency, + SameAssert expSnapshots, + SameAssert expPatternAeqB + ) { + // We will simulate the clock turning midnight into tomorrow, data on + // yesterday is candidate to expire + final AtomicReference clock = new AtomicReference<>(YESTERDAY); + + var snapshotManager = new TimetableSnapshotManager( + null, + TimetableSnapshotSourceParameters.DEFAULT + .withPurgeExpiredData(purgeExpiredData) + .withMaxSnapshotFrequency(Duration.ofMillis(maxSnapshotFrequency)), + clock::get + ); + + var res1 = snapshotManager.updateBuffer(PATTERN, TRIP_TIMES, YESTERDAY); + assertTrue(res1.isSuccess()); + + snapshotManager.commitTimetableSnapshot(true); + final TimetableSnapshot snapshotA = snapshotManager.getTimetableSnapshot(); + + // Turn the clock to tomorrow + clock.set(TOMORROW); + + var res2 = snapshotManager.updateBuffer(PATTERN, TRIP_TIMES, TODAY); + assertTrue(res2.isSuccess()); + + snapshotManager.purgeAndCommit(); + + final TimetableSnapshot snapshotB = snapshotManager.getTimetableSnapshot(); + + expSnapshots.test(snapshotA, snapshotB); + expPatternAeqB.test( + snapshotA.resolve(PATTERN, YESTERDAY), + snapshotB.resolve(PATTERN, YESTERDAY) + ); + expPatternAeqB + .not() + .test(snapshotB.resolve(PATTERN, null), snapshotB.resolve(PATTERN, YESTERDAY)); + + // Expect the same results regardless of the config for these + assertNotSame(snapshotA.resolve(PATTERN, null), snapshotA.resolve(PATTERN, YESTERDAY)); + assertSame(snapshotA.resolve(PATTERN, null), snapshotB.resolve(PATTERN, null)); + } +} diff --git a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java index cbe183932d2..1dad90b416b 100644 --- a/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java +++ b/src/test/java/org/opentripplanner/updater/trip/TimetableSnapshotSourceTest.java @@ -11,8 +11,6 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.updater.trip.BackwardsDelayPropagationType.REQUIRED_NO_DATA; -import static org.opentripplanner.updater.trip.TimetableSnapshotSourceTest.SameAssert.NotSame; -import static org.opentripplanner.updater.trip.TimetableSnapshotSourceTest.SameAssert.Same; import static org.opentripplanner.updater.trip.UpdateIncrementality.DIFFERENTIAL; import com.google.transit.realtime.GtfsRealtime.TripDescriptor; @@ -24,15 +22,11 @@ import java.time.Duration; import java.time.LocalDate; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import javax.annotation.Nonnull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.ConstantsForTests; import org.opentripplanner.TestOtpModel; import org.opentripplanner._support.time.ZoneIds; @@ -784,108 +778,4 @@ private TimetableSnapshotSource defaultUpdater() { () -> SERVICE_DATE ); } - - enum SameAssert { - Same { - public void test(Object a, Object b) { - assertSame(a, b); - } - }, - NotSame { - public void test(Object a, Object b) { - assertNotSame(a, b); - } - }; - - abstract void test(Object a, Object b); - - SameAssert not() { - return this == Same ? NotSame : Same; - } - } - - static Stream purgeExpiredDataTestCases() { - return Stream.of( - // purgeExpiredData maxSnapshotFrequency || snapshots PatternSnapshotA PatternSnapshotB - Arguments.of(Boolean.TRUE, -1, NotSame, NotSame), - Arguments.of(Boolean.FALSE, -1, NotSame, Same), - Arguments.of(Boolean.TRUE, 1000, NotSame, NotSame), - Arguments.of(Boolean.FALSE, 1000, Same, Same) - ); - } - - @ParameterizedTest(name = "purgeExpired: {0}, maxFrequency: {1} || {2} {3}") - @MethodSource("purgeExpiredDataTestCases") - public void testPurgeExpiredData( - boolean purgeExpiredData, - int maxSnapshotFrequency, - SameAssert expSnapshots, - SameAssert expPatternAeqB - ) { - final FeedScopedId tripId = new FeedScopedId(feedId, "1.1"); - final Trip trip = transitModel.getTransitModelIndex().getTripForId().get(tripId); - final TripPattern pattern = transitModel.getTransitModelIndex().getPatternForTrip().get(trip); - - // We will simulate the clock turning midnight into tomorrow, data on - // yesterday is candidate to expire - final LocalDate yesterday = SERVICE_DATE.minusDays(1); - final LocalDate tomorrow = SERVICE_DATE.plusDays(1); - final AtomicReference clock = new AtomicReference<>(yesterday); - - var tripDescriptorBuilder = TripDescriptor.newBuilder(); - tripDescriptorBuilder.setTripId("1.1"); - tripDescriptorBuilder.setScheduleRelationship(ScheduleRelationship.CANCELED); - - tripDescriptorBuilder.setStartDate(ServiceDateUtils.asCompactString(yesterday)); - var tripUpdateYesterday = TripUpdate.newBuilder().setTrip(tripDescriptorBuilder).build(); - - // Update pattern on today, even if the time the update is performed is tomorrow - tripDescriptorBuilder.setStartDate(ServiceDateUtils.asCompactString(SERVICE_DATE)); - var tripUpdateToday = TripUpdate.newBuilder().setTrip(tripDescriptorBuilder).build(); - - var updater = new TimetableSnapshotSource( - TimetableSnapshotSourceParameters.DEFAULT - .withPurgeExpiredData(purgeExpiredData) - .withMaxSnapshotFrequency(Duration.ofMillis(maxSnapshotFrequency)), - transitModel, - clock::get - ); - - // Apply update when clock is yesterday - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdateYesterday), - feedId - ); - updater.commitTimetableSnapshot(true); - - final TimetableSnapshot snapshotA = updater.getTimetableSnapshot(); - - // Turn the clock to tomorrow - clock.set(tomorrow); - - updater.applyTripUpdates( - TRIP_MATCHER_NOOP, - REQUIRED_NO_DATA, - DIFFERENTIAL, - List.of(tripUpdateToday), - feedId - ); - final TimetableSnapshot snapshotB = updater.getTimetableSnapshot(); - - expSnapshots.test(snapshotA, snapshotB); - expPatternAeqB.test( - snapshotA.resolve(pattern, yesterday), - snapshotB.resolve(pattern, yesterday) - ); - expPatternAeqB - .not() - .test(snapshotB.resolve(pattern, null), snapshotB.resolve(pattern, yesterday)); - - // Expect the same results regardless of the config for these - assertNotSame(snapshotA.resolve(pattern, null), snapshotA.resolve(pattern, yesterday)); - assertSame(snapshotA.resolve(pattern, null), snapshotB.resolve(pattern, null)); - } } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/planConnection.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/planConnection.json new file mode 100644 index 00000000000..53ab016cc93 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/planConnection.json @@ -0,0 +1,38 @@ +{ + "data" : { + "planConnection" : { + "searchDateTime" : "2023-01-27T21:08:35+01:00", + "routingErrors" : [ ], + "pageInfo" : { + "hasNextPage" : false, + "hasPreviousPage" : false, + "startCursor" : null, + "endCursor" : null, + "searchWindowUsed" : null + }, + "edges" : [ + { + "cursor" : "NoCursor", + "node" : { + "start" : "2020-02-02T11:00:00Z", + "end" : "2020-02-02T12:00:00Z", + "legs" : [ + { + "mode" : "WALK" + }, + { + "mode" : "BUS" + }, + { + "mode" : "RAIL" + }, + { + "mode" : "CAR" + } + ] + } + } + ] + } + } +} diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/planConnection.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/planConnection.graphql new file mode 100644 index 00000000000..4013479ded0 --- /dev/null +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/planConnection.graphql @@ -0,0 +1,163 @@ +{ + planConnection( + dateTime: { + earliestDeparture: "2023-06-13T14:30+03:00" + } + searchWindow: "PT2H30M" + first: 5 + origin: { + location: { + coordinate: { + latitude: 45.5552 + longitude: -122.6534 + } + } + label: "Home" + } + destination: { + location: { + coordinate: { + latitude: 45.4908 + longitude: -122.5519 + } + } + label: "Work" + } + modes: { + directOnly: false + transitOnly: false + direct: [WALK] + transit: { + access: [BICYCLE_RENTAL, WALK] + transfer: [WALK] + egress: [BICYCLE_RENTAL, WALK] + transit: [ + { + mode: TRAM + cost: { + reluctance: 1.3 + } + }, + { + mode: BUS + } + ] + } + } + preferences: { + accessibility: { + wheelchair: { + enabled: true + } + } + street: { + car: { + reluctance: 6.5 + rental: { + allowedNetworks: ["foo", "bar"] + bannedNetworks: ["foobar"] + } + parking: { + unpreferredCost: 200 + preferred: [{ + select: [{ + tags: ["best-park"] + }] + }] + filters: [{ + not: [{ + tags: ["worst-park"] + }] + }] + } + } + bicycle: { + reluctance: 3.0 + speed: 7.4 + optimization: { + type: SAFEST_STREETS + } + boardCost: 200 + walk: { + speed: 1.3 + cost: { + mountDismountCost: 100 + reluctance: 3.5 + } + mountDismountTime: "PT5S" + } + rental: { + destinationBicyclePolicy: { + allowKeeping: true + keepingCost: 300 + } + allowedNetworks: ["foo", "bar"] + bannedNetworks: ["foobar"] + } + parking: { + unpreferredCost: 200 + preferred: [{ + select: [{ + tags: ["best-park"] + }] + }] + filters: [{ + not: [{ + tags: ["worst-park"] + }] + }] + } + } + walk: { + speed: 2.4 + reluctance: 1.5 + safetyFactor: 0.5 + boardCost: 200 + } + } + transit: { + board: { + waitReluctance: 3.2 + slack: "PT1M30S" + } + alight: { + slack: "PT0S" + } + transfer: { + cost: 200 + slack: "PT2M" + maximumAdditionalTransfers: 2 + maximumTransfers: 5 + } + timetable: { + excludeRealTimeUpdates: false + includePlannedCancellations: false + includeRealTimeCancellations: true + } + } + } + locale: "en" + ) { + searchDateTime + routingErrors { + code + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + searchWindowUsed + } + edges { + cursor + node { + start + end + legs { + mode + } + } + } + } +} \ No newline at end of file