diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java new file mode 100644 index 00000000000..b7ce446d430 --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java @@ -0,0 +1,79 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import org.opentripplanner.raptor.api.model.RaptorAccessEgress; +import org.opentripplanner.raptor.api.model.RaptorTransferConstraint; +import org.opentripplanner.raptor.spi.RaptorCostCalculator; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; +import org.opentripplanner.transit.model.basic.TransitMode; + + +/** + * This cost calculator increases the cost on mode coach by adding an extra reluctance. The + * reluctance is hardcoded in this class and cannot be configured. + */ +class CoachCostCalculator implements RaptorCostCalculator { + + private static final int EXTRA_RELUCTANCE_ON_COACH = RaptorCostConverter.toRaptorCost(0.6); + + private final RaptorCostCalculator delegate; + + CoachCostCalculator(RaptorCostCalculator delegate) { + this.delegate = delegate; + } + + @Override + public int boardingCost( + boolean firstBoarding, + int prevArrivalTime, + int boardStop, + int boardTime, + T trip, + RaptorTransferConstraint transferConstraints + ) { + return delegate.boardingCost( + firstBoarding, + prevArrivalTime, + boardStop, + boardTime, + trip, + transferConstraints + ); + } + + @Override + public int onTripRelativeRidingCost(int boardTime, T tripScheduledBoarded) { + return delegate.onTripRelativeRidingCost(boardTime, tripScheduledBoarded); + } + + @Override + public int transitArrivalCost( + int boardCost, + int alightSlack, + int transitTime, + T trip, + int toStop + ) { + int cost = delegate.transitArrivalCost(boardCost, alightSlack, transitTime, trip, toStop); + if(trip.transitReluctanceFactorIndex() == TransitMode.COACH.ordinal()) { + cost += transitTime * EXTRA_RELUCTANCE_ON_COACH; + } + return cost; + } + + @Override + public int waitCost(int waitTimeInSeconds) { + return delegate.waitCost(waitTimeInSeconds); + } + + @Override + public int calculateRemainingMinCost(int minTravelTime, int minNumTransfers, int fromStop) { + return delegate.calculateRemainingMinCost(minTravelTime, minNumTransfers, fromStop); + } + + @Override + public int costEgress(RaptorAccessEgress egress) { + return delegate.costEgress(egress); + } + +} diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturSorlandsbanenService.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturSorlandsbanenService.java new file mode 100644 index 00000000000..a973617f257 --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/EnturSorlandsbanenService.java @@ -0,0 +1,103 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.function.BiFunction; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.model.GenericLocation; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; +import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; +import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgresses; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData; +import org.opentripplanner.routing.api.request.RouteRequest; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; + +/** + * This is basically a big hack to produce results containing "Sørlandsbanen" in Norway. This + * railroad line is slow and goes inland fare from where people live. Despite this, people and the + * operator want to show it in the results for log travel along the southern part of Norway where + * ii is an option. Tuning the search has proven to be challenging. It is solved here by doing + * two searches. One normal search and one where the rail is given a big cost advantage over coach. + * If train results are found in the second search, then it is added to the results of the first + * search. Everything found in the first search is always returned. + */ +public class EnturSorlandsbanenService { + + private static final double SOUTH_BOARDER_LIMIT = 59.1; + private static final int MIN_DISTANCE_LIMIT = 120_000; + + + public ExtraMcRouterSearch createMcRouterFactory(RouteRequest request, AccessEgresses accessEgresses, TransitLayer transitLayer) { + WgsCoordinate from = findStopCoordinate( + request.from(), + accessEgresses.getAccesses(), + transitLayer + ); + WgsCoordinate to = findStopCoordinate(request.to(), accessEgresses.getEgresses(), transitLayer); + + if (from.latitude() > SOUTH_BOARDER_LIMIT && to.latitude() > SOUTH_BOARDER_LIMIT) { + return null; + } + + double distance = from.distanceTo(to); + if (distance < MIN_DISTANCE_LIMIT) { + return null; + } + + return new ExtraMcRouterSearch<>() { + @Override + public RaptorTransitDataProvider createTransitDataAlternativeSearch(RaptorTransitDataProvider transitDataMainSearch) { + return new RaptorRoutingRequestTransitData( + (RaptorRoutingRequestTransitData)transitDataMainSearch, + new CoachCostCalculator<>(transitDataMainSearch.multiCriteriaCostCalculator()) + ); + } + + @Override + public BiFunction>, Collection>, Collection>> merger() { + return new MergePaths<>(); + } + }; + } + + /** + * Find a coordinate matching the given location, in order: + * - First return the coordinate of the location if it exists. + * - Then loop through the access/egress stops and try to find the + * stop or station given by the location id, return the stop/station coordinate. + * - Return the fist stop in the access/egress list coordinate. + */ + @SuppressWarnings("ConstantConditions") + private static WgsCoordinate findStopCoordinate( + GenericLocation location, + Collection accessEgress, + TransitLayer transitLayer + ) { + if (location.lat != null) { + return new WgsCoordinate(location.lat, location.lng); + } + + StopLocation firstStop = null; + for (RoutingAccessEgress it : accessEgress) { + StopLocation stop = transitLayer.getStopByIndex(it.stop()); + if (stop.getId().equals(location.stopId)) { + return stop.getCoordinate(); + } + if (idIsParentStation(stop, location.stopId)) { + return stop.getParentStation().getCoordinate(); + } + if (firstStop == null) { + firstStop = stop; + } + } + return firstStop.getCoordinate(); + } + + private static boolean idIsParentStation(StopLocation stop, FeedScopedId pId) { + return stop.getParentStation() != null && stop.getParentStation().getId().equals(pId); + } +} diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java new file mode 100644 index 00000000000..b5f94019b72 --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java @@ -0,0 +1,52 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; +import org.opentripplanner.transit.model.basic.TransitMode; + +/** + * Strategy for merging the main results and the extra rail results from Sorlandsbanen. + * Everything from the main result is kept, and any additional rail results from the alternative + * search are added. + */ +class MergePaths implements BiFunction>, Collection>, Collection>> { + + @Override + public Collection> apply(Collection> main, Collection> railAlternatives) { + Map> result = new HashMap<>(); + addAllToMap(result, main); + addRailToMap(result, railAlternatives); + return result.values(); + } + + private void addAllToMap(Map> map, Collection> paths) { + for (var it : paths) { + map.put(new PathKey(it), it); + } + } + + private void addRailToMap(Map> map, Collection> paths) { + for (var it : paths) { + if (hasRail(it)) { + map.put(new PathKey(it), it); + } + } + } + + private static boolean hasRail(RaptorPath path) { + return path + .legStream() + .filter(PathLeg::isTransitLeg) + .anyMatch(leg -> { + var trip = (TripScheduleWithOffset) leg.asTransitLeg().trip(); + var mode = trip.getOriginalTripPattern().getMode(); + return mode == TransitMode.RAIL; + }); + } +} diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java new file mode 100644 index 00000000000..28c1b1eac25 --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java @@ -0,0 +1,58 @@ +package org.opentripplanner.ext.sorlandsbanen; + +import org.opentripplanner.raptor.api.path.PathLeg; +import org.opentripplanner.raptor.api.path.RaptorPath; + + +/** + * Uses a hash to create a key for access, egress and transit legs in a path. Transfers + * are not included. The key is used to exclude duplicates. This approach may drop valid results + * when there is a hash collision, but this whole sandbox feature is a hack - so we can tolerate + * this here. + */ +final class PathKey { + + private final int hash; + + PathKey(RaptorPath path) { + this.hash = hash(path); + } + + private static int hash(RaptorPath path) { + if (path == null) { + return 0; + } + int result = 1; + + PathLeg leg = path.accessLeg(); + + while (!leg.isEgressLeg()) { + result = 31 * result + leg.toStop(); + result = 31 * result + leg.toTime(); + + if (leg.isTransitLeg()) { + result = 31 * result + leg.asTransitLeg().trip().pattern().debugInfo().hashCode(); + } + leg = leg.nextLeg(); + } + result = 31 * result + leg.toTime(); + + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o.getClass() != PathKey.class) { + return false; + } + return hash == ((PathKey) o).hash; + } + + @Override + public int hashCode() { + return hash; + } +} diff --git a/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/configure/EnturSorlandsbanenModule.java b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/configure/EnturSorlandsbanenModule.java new file mode 100644 index 00000000000..b1208f2c68b --- /dev/null +++ b/application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/configure/EnturSorlandsbanenModule.java @@ -0,0 +1,17 @@ +package org.opentripplanner.ext.sorlandsbanen.configure; + +import dagger.Module; +import dagger.Provides; +import javax.annotation.Nullable; +import org.opentripplanner.ext.sorlandsbanen.EnturSorlandsbanenService; +import org.opentripplanner.framework.application.OTPFeature; + +@Module +public class EnturSorlandsbanenModule { + + @Provides + @Nullable + EnturSorlandsbanenService providesEnturSorlandsbanenService() { + return OTPFeature.Sorlandsbanen.isOn() ? new EnturSorlandsbanenService() : null; + } +} diff --git a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java index 324f5397673..c4cb86ebfb9 100644 --- a/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java +++ b/application/src/main/java/org/opentripplanner/framework/application/OTPFeature.java @@ -118,6 +118,11 @@ 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."), + Sorlandsbanen( + false, + true, + "Include train Sørlandsbanen in results when searchig in south of Norway. Only relevant in Norway." + ), TransferAnalyzer(false, true, "Analyze transfers during graph build."); private static final Object TEST_LOCK = new Object(); diff --git a/application/src/main/java/org/opentripplanner/framework/geometry/WgsCoordinate.java b/application/src/main/java/org/opentripplanner/framework/geometry/WgsCoordinate.java index 6c3dbf0f9d3..136bb4264c0 100644 --- a/application/src/main/java/org/opentripplanner/framework/geometry/WgsCoordinate.java +++ b/application/src/main/java/org/opentripplanner/framework/geometry/WgsCoordinate.java @@ -163,6 +163,20 @@ public WgsCoordinate roundToApproximate100m() { return new WgsCoordinate(lat, lng); } + /** + * Compute a farly accurate distance between two coordinates. Use the fast version in + * {@link SphericalDistanceLibrary} if many computations are needed. Return the distance in + * meters between the two coordinates. + */ + public double distanceTo(WgsCoordinate other) { + return SphericalDistanceLibrary.distance( + this.latitude, + this.longitude, + other.latitude, + other.longitude + ); + } + /** * Return a new coordinate that is moved an approximate number of meters east. */ diff --git a/application/src/main/java/org/opentripplanner/raptor/RaptorService.java b/application/src/main/java/org/opentripplanner/raptor/RaptorService.java index 70156cbbfbe..599a4104414 100644 --- a/application/src/main/java/org/opentripplanner/raptor/RaptorService.java +++ b/application/src/main/java/org/opentripplanner/raptor/RaptorService.java @@ -1,6 +1,7 @@ package org.opentripplanner.raptor; import java.util.stream.Collectors; +import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.RaptorRequest; import org.opentripplanner.raptor.api.response.RaptorResponse; @@ -8,6 +9,7 @@ import org.opentripplanner.raptor.service.DefaultStopArrivals; import org.opentripplanner.raptor.service.HeuristicSearchTask; import org.opentripplanner.raptor.service.RangeRaptorDynamicSearch; +import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,8 +25,16 @@ public class RaptorService { private final RaptorConfig config; - public RaptorService(RaptorConfig config) { + @Nullable + private final ExtraMcRouterSearch extraMcSearch; + + public RaptorService(RaptorConfig config, @Nullable ExtraMcRouterSearch extraMcSearch) { this.config = config; + this.extraMcSearch = extraMcSearch; + } + + public RaptorService(RaptorConfig config) { + this(config, null); } public RaptorResponse route( @@ -35,7 +45,8 @@ public RaptorResponse route( RaptorResponse response; if (request.isDynamicSearch()) { - response = new RangeRaptorDynamicSearch<>(config, transitData, request).route(); + response = + new RangeRaptorDynamicSearch<>(config, transitData, extraMcSearch, request).route(); } else { response = routeUsingStdWorker(transitData, request); } diff --git a/application/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java b/application/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java index cc488448304..6c86cfca4b0 100644 --- a/application/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java +++ b/application/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java @@ -7,6 +7,7 @@ import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.RaptorRequest; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; +import org.opentripplanner.raptor.rangeraptor.ConcurrentCompositeRaptorRouter; import org.opentripplanner.raptor.rangeraptor.DefaultRangeRaptorWorker; import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.rangeraptor.RangeRaptorWorkerComposite; @@ -15,6 +16,7 @@ import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; import org.opentripplanner.raptor.rangeraptor.internalapi.PassThroughPointsService; import org.opentripplanner.raptor.rangeraptor.internalapi.RangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouter; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerState; import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; @@ -22,19 +24,22 @@ import org.opentripplanner.raptor.rangeraptor.multicriteria.configure.McRangeRaptorConfig; import org.opentripplanner.raptor.rangeraptor.standard.configure.StdRangeRaptorConfig; import org.opentripplanner.raptor.rangeraptor.transit.RaptorSearchWindowCalculator; +import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; /** * This class is responsible for creating a new search and holding application scoped Raptor state. *

- * This class should have APPLICATION scope. It manage a threadPool, and hold a reference to the - * application tuning parameters. + * This class should have APPLICATION scope. It keep a reference to the threadPool used by Raptor, + * and holds a reference to the application tuning parameters. * * @param The TripSchedule type defined by the user of the raptor API. */ public class RaptorConfig { + @Nullable private final ExecutorService threadPool; + private final RaptorTuningParameters tuningParameters; /** The service is not final, because it depends on the request. */ @@ -58,19 +63,41 @@ public SearchContext context(RaptorTransitDataProvider transit, RaptorRequ return SearchContext.of(request, tuningParameters, transit, acceptC2AtDestination).build(); } - public RangeRaptor createRangeRaptorWithStdWorker( + public RaptorRouter createRangeRaptorWithStdWorker( RaptorTransitDataProvider transitData, RaptorRequest request ) { var context = context(transitData, request); var stdConfig = new StdRangeRaptorConfig<>(context); - return createRangeRaptor( - context, - createWorker(context.legs().getFirst(), stdConfig.state(), stdConfig.strategy()) + var worker = createWorker(context.legs().getFirst(), stdConfig.state(), stdConfig.strategy()); + return createRangeRaptor(context, worker); + } + + public RaptorRouter createRangeRaptorWithMcWorker( + RaptorTransitDataProvider transitData, + RaptorRequest request, + Heuristics heuristics, + @Nullable ExtraMcRouterSearch extraMcSearch + ) { + var mainSearch = createRangeRaptorWithMcWorker(transitData, request, heuristics); + + if (extraMcSearch == null) { + return mainSearch; + } + var alternativeSearch = createRangeRaptorWithMcWorker( + extraMcSearch.createTransitDataAlternativeSearch(transitData), + request, + heuristics + ); + return new ConcurrentCompositeRaptorRouter<>( + mainSearch, + alternativeSearch, + extraMcSearch.merger(), + threadPool() ); } - public RangeRaptor createRangeRaptorWithMcWorker( + private RaptorRouter createRangeRaptorWithMcWorker( RaptorTransitDataProvider transitData, RaptorRequest request, Heuristics heuristics @@ -93,11 +120,10 @@ public RangeRaptor createRangeRaptorWithMcWorker( var c = new McRangeRaptorConfig<>(leg, passThroughPointsService).withHeuristics(heuristics); worker = createWorker(leg, c.state(), c.strategy()); } - return createRangeRaptor(context, worker); } - public RangeRaptor createRangeRaptorWithHeuristicSearch( + public RaptorRouter createRangeRaptorWithHeuristicSearch( RaptorTransitDataProvider transitData, RaptorRequest request ) { @@ -117,6 +143,7 @@ public boolean isMultiThreaded() { return threadPool != null; } + @Nullable public ExecutorService threadPool() { return threadPool; } @@ -146,7 +173,7 @@ private RangeRaptorWorker createWorker( return new DefaultRangeRaptorWorker<>( workerState, routingStrategy, - ctx.transit(), + ctx.transitData(), ctx.slackProvider(), ctxLeg.accessPaths(), ctx.calculator(), @@ -156,10 +183,10 @@ private RangeRaptorWorker createWorker( ); } - private RangeRaptor createRangeRaptor(SearchContext ctx, RangeRaptorWorker worker) { + private RaptorRouter createRangeRaptor(SearchContext ctx, RangeRaptorWorker worker) { return new RangeRaptor<>( worker, - ctx.transit(), + ctx.transitData(), ctx.legs().getFirst().accessPaths(), ctx.roundTracker(), ctx.calculator(), diff --git a/application/src/main/java/org/opentripplanner/raptor/rangeraptor/CompositeResult.java b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/CompositeResult.java new file mode 100644 index 00000000000..d92f0df79bb --- /dev/null +++ b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/CompositeResult.java @@ -0,0 +1,71 @@ +package org.opentripplanner.raptor.rangeraptor; + +import java.util.Collection; +import java.util.function.BiFunction; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; + +/** + * Join two results together. + *

    + *
  • Everything from the first result is added
  • + *
  • The result is merged with the injected merge strategy.
  • + *
  • Some of the methods ONLY return the result of the main search!
  • + *
+ */ +class CompositeResult implements RaptorRouterResult { + + private final RaptorRouterResult mainResult; + private final Collection> result; + + CompositeResult( + RaptorRouterResult mainResult, + RaptorRouterResult alternativeResult, + BiFunction>, Collection>, Collection>> merger + ) { + this.mainResult = mainResult; + this.result = merger.apply(mainResult.extractPaths(), alternativeResult.extractPaths()); + } + + /** + * Return the merged result. + */ + @Override + public Collection> extractPaths() { + return result; + } + + /** + * Return the main result only. + */ + @Override + public SingleCriteriaStopArrivals extractBestOverallArrivals() { + return mainResult.extractBestOverallArrivals(); + } + + /** + * Return the main result only. + */ + @Override + public SingleCriteriaStopArrivals extractBestTransitArrivals() { + return mainResult.extractBestTransitArrivals(); + } + + /** + * Return the main result only. + */ + @Override + public SingleCriteriaStopArrivals extractBestNumberOfTransfers() { + return mainResult.extractBestNumberOfTransfers(); + } + + /** + * Return true if either the main or the alternative search has reached the destination. + */ + @Override + public boolean isDestinationReached() { + return !result.isEmpty(); + } +} diff --git a/application/src/main/java/org/opentripplanner/raptor/rangeraptor/ConcurrentCompositeRaptorRouter.java b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/ConcurrentCompositeRaptorRouter.java new file mode 100644 index 00000000000..dbf0c2d89c7 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/ConcurrentCompositeRaptorRouter.java @@ -0,0 +1,64 @@ +package org.opentripplanner.raptor.rangeraptor; + +import java.util.Collection; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.function.BiFunction; +import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.RaptorPath; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouter; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; + +/** + * Run two Raptor routers and join the result. The two searches are run concurrent if an + * {@link ExecutorService} is provided. + * @see CompositeResult for joining results. + */ +public class ConcurrentCompositeRaptorRouter + implements RaptorRouter { + + private final RaptorRouter mainWorker; + private final RaptorRouter alternativeWorker; + private final BiFunction>, Collection>, Collection>> merger; + private final ExecutorService executorService; + + public ConcurrentCompositeRaptorRouter( + RaptorRouter mainWorker, + RaptorRouter alternativeWorker, + BiFunction>, Collection>, Collection>> merger, + ExecutorService executorService + ) { + this.mainWorker = mainWorker; + this.alternativeWorker = alternativeWorker; + this.merger = merger; + this.executorService = executorService; + } + + @Override + public RaptorRouterResult route() { + if (executorService == null) { + var mainResult = mainWorker.route(); + var alternativeResult = alternativeWorker.route(); + return new CompositeResult<>(mainResult, alternativeResult, merger); + } + + var mainResultFuture = executorService.submit(mainWorker::route); + var alternativeResultFuture = executorService.submit(alternativeWorker::route); + + try { + var mainResult = mainResultFuture.get(); + var alternativeResult = alternativeResultFuture.get(); + return new CompositeResult<>(mainResult, alternativeResult, merger); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // propagate interruption to the running task. + + mainResultFuture.cancel(true); + alternativeResultFuture.cancel(true); + throw new OTPRequestTimeoutException(); + } catch (ExecutionException e) { + throw (e.getCause() instanceof RuntimeException re) ? re : new RuntimeException(e); + } + } +} diff --git a/application/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java index e322881d786..e197bad8461 100644 --- a/application/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java +++ b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java @@ -59,7 +59,7 @@ public class SearchContext { /** * the transit data role needed for routing */ - protected final RaptorTransitDataProvider transit; + protected final RaptorTransitDataProvider transitData; private final RaptorTransitCalculator calculator; private final RaptorTuningParameters tuningParameters; @@ -75,10 +75,10 @@ public class SearchContext { /** Lazy initialized */ private RaptorCostCalculator costCalculator = null; - public SearchContext( + SearchContext( RaptorRequest request, RaptorTuningParameters tuningParameters, - RaptorTransitDataProvider transit, + RaptorTransitDataProvider transitData, AccessPaths accessPaths, List viaConnections, EgressPaths egressPaths, @@ -86,7 +86,7 @@ public SearchContext( ) { this.request = request; this.tuningParameters = tuningParameters; - this.transit = transit; + this.transitData = transitData; this.calculator = createCalculator(request, tuningParameters); this.roundTracker = @@ -133,8 +133,8 @@ public MultiCriteriaRequest multiCriteria() { return request.multiCriteria(); } - public RaptorTransitDataProvider transit() { - return transit; + public RaptorTransitDataProvider transitData() { + return transitData; } public RaptorTransitCalculator calculator() { @@ -150,7 +150,7 @@ public SlackProvider slackProvider() { } public RaptorSlackProvider raptorSlackProvider() { - return transit.slackProvider(); + return transitData.slackProvider(); } /** @@ -167,7 +167,7 @@ public ToIntFunction boardSlackProvider() { @Nullable public RaptorCostCalculator costCalculator() { if (costCalculator == null) { - this.costCalculator = transit.multiCriteriaCostCalculator(); + this.costCalculator = transitData.multiCriteriaCostCalculator(); } return costCalculator; } @@ -187,7 +187,7 @@ public IntPredicate acceptC2AtDestination() { /** Number of stops in transit graph. */ public int nStops() { - return transit.numberOfStops(); + return transitData.numberOfStops(); } /** Calculate the maximum number of rounds to perform. */ @@ -224,7 +224,7 @@ public boolean useConstrainedTransfers() { /* private methods */ public RaptorStopNameResolver stopNameResolver() { - return transit.stopNameResolver(); + return transitData.stopNameResolver(); } public TimeBasedBoardingSupport createTimeBasedBoardingSupport() { diff --git a/application/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java index 43d504ced7d..24c4d8a3bed 100644 --- a/application/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java +++ b/application/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java @@ -102,7 +102,7 @@ private PathMapper createPathMapper(boolean includeCost) { ctx.raptorSlackProvider(), includeCost ? ctx.costCalculator() : null, ctx.stopNameResolver(), - ctx.transit().transferConstraintsSearch(), + ctx.transitData().transferConstraintsSearch(), ctx.lifeCycle() ); } diff --git a/application/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java b/application/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java index 7c0158d2192..0de84283af8 100644 --- a/application/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java +++ b/application/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java @@ -10,6 +10,7 @@ import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouter; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; import org.slf4j.Logger; @@ -33,7 +34,7 @@ public class HeuristicSearchTask { private final RaptorTransitDataProvider transitData; private boolean run = false; - private RangeRaptor search = null; + private RaptorRouter search = null; private RaptorRequest originalRequest; private RaptorRequest heuristicRequest; private RaptorRouterResult result = null; diff --git a/application/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java b/application/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java index 5353804f414..cba79f3a115 100644 --- a/application/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java +++ b/application/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java @@ -19,9 +19,10 @@ import org.opentripplanner.raptor.api.request.SearchParamsBuilder; import org.opentripplanner.raptor.api.response.RaptorResponse; import org.opentripplanner.raptor.configure.RaptorConfig; -import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouter; import org.opentripplanner.raptor.rangeraptor.transit.RaptorSearchWindowCalculator; +import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,12 +46,16 @@ public class RangeRaptorDynamicSearch { private final RaptorRequest originalRequest; private final RaptorSearchWindowCalculator dynamicSearchWindowCalculator; + @Nullable + private final ExtraMcRouterSearch extraMcSearch; + private final HeuristicSearchTask fwdHeuristics; private final HeuristicSearchTask revHeuristics; public RangeRaptorDynamicSearch( RaptorConfig config, RaptorTransitDataProvider transitData, + @Nullable ExtraMcRouterSearch extraMcSearch, RaptorRequest originalRequest ) { this.config = config; @@ -58,6 +63,7 @@ public RangeRaptorDynamicSearch( this.originalRequest = originalRequest; this.dynamicSearchWindowCalculator = config.searchWindowCalculator().withSearchParams(originalRequest.searchParams()); + this.extraMcSearch = extraMcSearch; this.fwdHeuristics = new HeuristicSearchTask<>(FORWARD, "Forward", config, transitData); this.revHeuristics = new HeuristicSearchTask<>(REVERSE, "Reverse", config, transitData); @@ -129,18 +135,23 @@ private void runHeuristics() { private RaptorResponse createAndRunDynamicRRWorker(RaptorRequest request) { LOG.debug("Main request: {}", request); - RangeRaptor rangeRaptorRouter; + RaptorRouter raptorRouter; // Create worker if (request.profile().is(MULTI_CRITERIA)) { - rangeRaptorRouter = - config.createRangeRaptorWithMcWorker(transitData, request, getDestinationHeuristics()); + raptorRouter = + config.createRangeRaptorWithMcWorker( + transitData, + request, + getDestinationHeuristics(), + extraMcSearch + ); } else { - rangeRaptorRouter = config.createRangeRaptorWithStdWorker(transitData, request); + raptorRouter = config.createRangeRaptorWithStdWorker(transitData, request); } // Route - var result = rangeRaptorRouter.route(); + var result = raptorRouter.route(); // create and return response return new RaptorResponse<>( diff --git a/application/src/main/java/org/opentripplanner/raptor/service/ViaRangeRaptorDynamicSearch.java b/application/src/main/java/org/opentripplanner/raptor/service/ViaRangeRaptorDynamicSearch.java deleted file mode 100644 index 4476e40464f..00000000000 --- a/application/src/main/java/org/opentripplanner/raptor/service/ViaRangeRaptorDynamicSearch.java +++ /dev/null @@ -1,298 +0,0 @@ -package org.opentripplanner.raptor.service; - -import static org.opentripplanner.raptor.api.model.SearchDirection.FORWARD; -import static org.opentripplanner.raptor.api.model.SearchDirection.REVERSE; -import static org.opentripplanner.raptor.api.request.RaptorProfile.MULTI_CRITERIA; -import static org.opentripplanner.raptor.service.HeuristicToRunResolver.resolveHeuristicToRunBasedOnOptimizationsAndSearchParameters; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import org.opentripplanner.framework.application.OTPRequestTimeoutException; -import org.opentripplanner.raptor.RaptorService; -import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.api.request.RaptorRequest; -import org.opentripplanner.raptor.api.request.SearchParams; -import org.opentripplanner.raptor.api.request.SearchParamsBuilder; -import org.opentripplanner.raptor.api.response.RaptorResponse; -import org.opentripplanner.raptor.configure.RaptorConfig; -import org.opentripplanner.raptor.rangeraptor.RangeRaptor; -import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; -import org.opentripplanner.raptor.rangeraptor.transit.RaptorSearchWindowCalculator; -import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This search helps the {@link RaptorService} to configure - * heuristics and set dynamic search parameters like EDT, LAT and raptor-search-window. - *

- * If possible the forward and reverse heuristics will be run in parallel. - *

- * Depending on which optimization is enabled and which search parameters are set a forward and/or a - * reverse "single-iteration" raptor search is performed and heuristics are collected. This is used - * to configure the "main" multi-iteration RangeRaptor search. - */ -public class ViaRangeRaptorDynamicSearch { - - private static final Logger LOG = LoggerFactory.getLogger(ViaRangeRaptorDynamicSearch.class); - - private final RaptorConfig config; - private final RaptorTransitDataProvider transitData; - private final RaptorRequest originalRequest; - private final RaptorSearchWindowCalculator dynamicSearchWindowCalculator; - - private final HeuristicSearchTask fwdHeuristics; - private final HeuristicSearchTask revHeuristics; - - public ViaRangeRaptorDynamicSearch( - RaptorConfig config, - RaptorTransitDataProvider transitData, - RaptorRequest originalRequest - ) { - this.config = config; - this.transitData = transitData; - this.originalRequest = originalRequest; - this.dynamicSearchWindowCalculator = - config.searchWindowCalculator().withSearchParams(originalRequest.searchParams()); - - this.fwdHeuristics = new HeuristicSearchTask<>(FORWARD, "Forward", config, transitData); - this.revHeuristics = new HeuristicSearchTask<>(REVERSE, "Reverse", config, transitData); - } - - public RaptorResponse route() { - try { - enableHeuristicSearchBasedOnOptimizationsAndSearchParameters(); - - // Run heuristics, if no destination is reached - runHeuristics(); - - // Set search-window and other dynamic calculated parameters - var dynamicRequest = requestWithDynamicSearchParams(originalRequest); - - return createAndRunDynamicRRWorker(dynamicRequest); - } catch (DestinationNotReachedException e) { - return new RaptorResponse<>( - Collections.emptyList(), - null, - // If a trip exists(forward heuristics succeed), but is outside the calculated - // search-window, then set the search-window params as if the request was - // performed. This enables the client to page to the next window - requestWithDynamicSearchParams(originalRequest), - false - ); - } - } - - /** - * Only exposed for testing purposes - */ - @Nullable - public Heuristics getDestinationHeuristics() { - if (!originalRequest.useDestinationPruning()) { - return null; - } - LOG.debug("RangeRaptor - Destination pruning enabled."); - return revHeuristics.result(); - } - - /** - * Create and prepare heuristic search (both FORWARD and REVERSE) based on optimizations and input - * search parameters. This is done for Standard and Multi-criteria profiles only. - */ - private void enableHeuristicSearchBasedOnOptimizationsAndSearchParameters() { - // We delegate this to a static method to be able to write unit test on this logic - resolveHeuristicToRunBasedOnOptimizationsAndSearchParameters( - originalRequest, - fwdHeuristics::enable, - revHeuristics::enable - ); - } - - /** - * Run standard "singe-iteration" raptor search to calculate heuristics - this should be really - * fast to run compared with a (multi-criteria) range-raptor search. - * - * @throws DestinationNotReachedException if destination is not reached. - */ - private void runHeuristics() { - if (isItPossibleToRunHeuristicsInParallel()) { - runHeuristicsInParallel(); - } else { - runHeuristicsSequentially(); - } - fwdHeuristics.debugCompareResult(revHeuristics); - } - - private RaptorResponse createAndRunDynamicRRWorker(RaptorRequest request) { - LOG.debug("Main request: {}", request); - RangeRaptor rangeRaptorRouter; - - // Create worker - if (request.profile().is(MULTI_CRITERIA)) { - rangeRaptorRouter = - config.createRangeRaptorWithMcWorker(transitData, request, getDestinationHeuristics()); - } else { - rangeRaptorRouter = config.createRangeRaptorWithStdWorker(transitData, request); - } - - // Route - var result = rangeRaptorRouter.route(); - - // create and return response - return new RaptorResponse<>( - result.extractPaths(), - new DefaultStopArrivals(result), - request, - // This method is not run unless the heuristic reached the destination - true - ); - } - - private boolean isItPossibleToRunHeuristicsInParallel() { - SearchParams s = originalRequest.searchParams(); - return ( - config.isMultiThreaded() && - originalRequest.runInParallel() && - s.isEarliestDepartureTimeSet() && - s.isLatestArrivalTimeSet() && - fwdHeuristics.isEnabled() && - revHeuristics.isEnabled() - ); - } - - /** - * @throws DestinationNotReachedException if destination is not reached - */ - private void runHeuristicsInParallel() { - fwdHeuristics.withRequest(originalRequest); - revHeuristics.withRequest(originalRequest); - Future asyncResult = null; - try { - asyncResult = config.threadPool().submit(fwdHeuristics::run); - revHeuristics.run(); - asyncResult.get(); - LOG.debug( - "Route using RangeRaptor - " + "REVERSE and FORWARD heuristic search performed in parallel." - ); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - // propagate interruption to the running task. - asyncResult.cancel(true); - throw new OTPRequestTimeoutException(); - } catch (ExecutionException e) { - if (e.getCause() instanceof DestinationNotReachedException) { - throw new DestinationNotReachedException(); - } - LOG.error(e.getMessage() + ". Request: " + originalRequest, e); - throw new IllegalStateException( - "Failed to run FORWARD/REVERSE heuristic search in parallel. Details: " + e.getMessage() - ); - } - } - - /** - * @throws DestinationNotReachedException if destination is not reached - */ - private void runHeuristicsSequentially() { - List> tasks = listTasksInOrder(); - - if (tasks.isEmpty()) { - return; - } - - // Run the first heuristic search - Heuristics result = runHeuristicSearchTask(tasks.get(0)); - calculateDynamicSearchParametersFromHeuristics(result); - - if (tasks.size() == 1) { - return; - } - - // Run the second heuristic search - runHeuristicSearchTask(tasks.get(1)); - } - - private Heuristics runHeuristicSearchTask(HeuristicSearchTask task) { - RaptorRequest request = task.getDirection().isForward() - ? requestForForwardHeurSearchWithDynamicSearchParams() - : requestForReverseHeurSearchWithDynamicSearchParams(); - - task.withRequest(request).run(); - - return task.result(); - } - - /** - * If the earliest-departure-time(EDT) is set, the task order should be: - *

    - *
  1. {@code FORWARD}
  2. - *
  3. {@code REVERSE}
  4. - *
- * If no EDT is set, the latest-arrival-time is set, and the order should be the opposite, - * with {@code REVERSE} first - */ - private List> listTasksInOrder() { - boolean performForwardFirst = originalRequest.searchParams().isEarliestDepartureTimeSet(); - - List> list = performForwardFirst - ? List.of(fwdHeuristics, revHeuristics) - : List.of(revHeuristics, fwdHeuristics); - - return list.stream().filter(HeuristicSearchTask::isEnabled).collect(Collectors.toList()); - } - - private RaptorRequest requestForForwardHeurSearchWithDynamicSearchParams() { - if (originalRequest.searchParams().isEarliestDepartureTimeSet()) { - return originalRequest; - } - return originalRequest - .mutate() - .searchParams() - .earliestDepartureTime(transitData.getValidTransitDataStartTime()) - .build(); - } - - private RaptorRequest requestForReverseHeurSearchWithDynamicSearchParams() { - if (originalRequest.searchParams().isLatestArrivalTimeSet()) { - return originalRequest; - } - return originalRequest - .mutate() - .searchParams() - .latestArrivalTime( - transitData.getValidTransitDataEndTime() + - originalRequest.searchParams().accessEgressMaxDurationSeconds() - ) - .build(); - } - - private RaptorRequest requestWithDynamicSearchParams(RaptorRequest request) { - SearchParamsBuilder builder = request.mutate().searchParams(); - - if (!request.searchParams().isEarliestDepartureTimeSet()) { - builder.earliestDepartureTime(dynamicSearchWindowCalculator.getEarliestDepartureTime()); - } - if (!request.searchParams().isSearchWindowSet()) { - builder.searchWindowInSeconds(dynamicSearchWindowCalculator.getSearchWindowSeconds()); - } - // We do not set the latest-arrival-time, because we do not want to limit the forward - // multi-criteria search, it does not have much effect on the performance - we only risk - // losing optimal results. - return builder.build(); - } - - private void calculateDynamicSearchParametersFromHeuristics(@Nullable Heuristics heuristics) { - if (heuristics != null) { - dynamicSearchWindowCalculator - .withHeuristics( - heuristics.bestOverallJourneyTravelDuration(), - heuristics.minWaitTimeForJourneysReachingDestination() - ) - .calculate(); - } - } -} diff --git a/application/src/main/java/org/opentripplanner/raptor/spi/ExtraMcRouterSearch.java b/application/src/main/java/org/opentripplanner/raptor/spi/ExtraMcRouterSearch.java new file mode 100644 index 00000000000..afebc107832 --- /dev/null +++ b/application/src/main/java/org/opentripplanner/raptor/spi/ExtraMcRouterSearch.java @@ -0,0 +1,35 @@ +package org.opentripplanner.raptor.spi; + +import java.util.Collection; +import java.util.function.BiFunction; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.path.RaptorPath; + +/** + * This interface is used to run two mulit-criteria searches and merging the result. Raptor will + * run the heuristics as normal. Then create two multi-criteria searches, the main search and the + * alternative search. The caller must provide a {@code merger} and + * {@link RaptorTransitDataProvider}. The transit data is used for the alternative search. This + * allows the caller to filter the transit data or change the cost-calculator. + *

+ * When changing the transit data, you may also invalidate the heuristics created by Raptor. If this + * is the case, you need to turn off the {@link org.opentripplanner.raptor.api.request.Optimization#PARETO_CHECK_AGAINST_DESTINATION}. + * For the heuristics to work, you may add extra cost or filter away data. + *

+ * @param The TripSchedule type defined by the user of the raptor API. + */ +public interface ExtraMcRouterSearch { + /** + * The returned transit-data is used in the ALTERNATIVE search. The given transit data is used in + * the main search. It is the same data passed into Raptor. + */ + RaptorTransitDataProvider createTransitDataAlternativeSearch( + RaptorTransitDataProvider transitDataMainSearch + ); + + /** + * You must provide a merge strategy to merge the main result (first argument) with the + * alternative result(second argument). Make sure the end result does not have any duplicates. + */ + BiFunction>, Collection>, Collection>> merger(); +} diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index e72d8ee1427..d6c03c4d773 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -12,12 +12,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.stream.IntStream; +import javax.annotation.Nullable; import org.opentripplanner.ext.ridehailing.RideHailingAccessShifter; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.raptor.RaptorService; import org.opentripplanner.raptor.api.path.RaptorPath; import org.opentripplanner.raptor.api.response.RaptorResponse; +import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; import org.opentripplanner.routing.algorithm.mapping.RaptorPathToItineraryMapper; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressPenaltyDecorator; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressRouter; @@ -142,7 +144,10 @@ private TransitRouterResult route() { ); // Route transit - var raptorService = new RaptorService<>(serverContext.raptorConfig()); + var raptorService = new RaptorService<>( + serverContext.raptorConfig(), + createMcRouterFactory(accessEgresses, transitLayer) + ); var transitResponse = raptorService.route(raptorRequest, requestTransitDataProvider); checkIfTransitConnectionExists(transitResponse); @@ -387,4 +392,21 @@ private IntStream listStopIndexes(FeedScopedId stopLocationId) { } return stops.stream().mapToInt(StopLocation::getIndex); } + + /** + * An optional factory for creating a decorator around the multi-criteria RangeRaptor instance. + */ + @Nullable + private ExtraMcRouterSearch createMcRouterFactory( + AccessEgresses accessEgresses, + TransitLayer transitLayer + ) { + if (OTPFeature.Sorlandsbanen.isOff()) { + return null; + } + var service = serverContext.enturSorlandsbanenService(); + return service == null + ? null + : service.createMcRouterFactory(request, accessEgresses, transitLayer); + } } diff --git a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java index 0182598ca35..f1b462e031a 100644 --- a/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java +++ b/application/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java @@ -128,6 +128,23 @@ public RaptorRoutingRequestTransitData( ); } + public RaptorRoutingRequestTransitData( + RaptorRoutingRequestTransitData original, + RaptorCostCalculator newCostCalculator + ) { + this.transitLayer = original.transitLayer; + this.transitSearchTimeZero = original.transitSearchTimeZero; + this.activeTripPatternsPerStop = original.activeTripPatternsPerStop; + this.patternIndex = original.patternIndex; + this.transferIndex = original.transferIndex; + this.transferService = original.transferService; + this.constrainedTransfers = original.constrainedTransfers; + this.validTransitDataStartTime = original.validTransitDataStartTime; + this.validTransitDataEndTime = original.validTransitDataEndTime; + this.generalizedCostCalculator = newCostCalculator; + this.slackProvider = original.slackProvider(); + } + @Override public Iterator getTransfersFromStop(int stopIndex) { return transferIndex.getForwardTransfers(stopIndex).iterator(); diff --git a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index 7ec71e589c7..765262840ee 100644 --- a/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -10,6 +10,7 @@ import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.ext.ridehailing.RideHailingService; +import org.opentripplanner.ext.sorlandsbanen.EnturSorlandsbanenService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.inspector.raster.TileRendererManager; @@ -101,16 +102,10 @@ public interface OtpServerRequestContext { List rideHailingServices(); - @Nullable - StopConsolidationService stopConsolidationService(); - StreetLimitationParametersService streetLimitationParametersService(); MeterRegistry meterRegistry(); - @Nullable - EmissionsService emissionsService(); - /** Inspector/debug services */ TileRendererManager tileRendererManager(); @@ -129,6 +124,8 @@ default GraphFinder graphFinder() { VectorTileConfig vectorTileConfig(); + /* Sandbox modules */ + @Nullable default DataOverlayContext dataOverlayContext(RouteRequest request) { return OTPFeature.DataOverlay.isOnElseNull(() -> @@ -139,6 +136,15 @@ default DataOverlayContext dataOverlayContext(RouteRequest request) { ); } + @Nullable + EmissionsService emissionsService(); + @Nullable LuceneIndex lucenceIndex(); + + @Nullable + StopConsolidationService stopConsolidationService(); + + @Nullable + EnturSorlandsbanenService enturSorlandsbanenService(); } diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index e828406a35e..c93cca4887d 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -10,6 +10,8 @@ import org.opentripplanner.ext.geocoder.configure.GeocoderModule; import org.opentripplanner.ext.interactivelauncher.configuration.InteractiveLauncherModule; import org.opentripplanner.ext.ridehailing.configure.RideHailingServicesModule; +import org.opentripplanner.ext.sorlandsbanen.EnturSorlandsbanenService; +import org.opentripplanner.ext.sorlandsbanen.configure.EnturSorlandsbanenModule; import org.opentripplanner.ext.stopconsolidation.StopConsolidationRepository; import org.opentripplanner.ext.stopconsolidation.configure.StopConsolidationServiceModule; import org.opentripplanner.graph_builder.issue.api.DataImportIssueSummary; @@ -55,6 +57,7 @@ ConstructApplicationModule.class, RideHailingServicesModule.class, EmissionsServiceModule.class, + EnturSorlandsbanenModule.class, StopConsolidationServiceModule.class, InteractiveLauncherModule.class, StreetLimitationParametersServiceModule.class, @@ -90,6 +93,9 @@ public interface ConstructApplicationFactory { StreetLimitationParameters streetLimitationParameters(); + @Nullable + EnturSorlandsbanenService enturSorlandsbanenService(); + @Nullable LuceneIndex luceneIndex(); diff --git a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index 6c830054c49..75e2d0940a3 100644 --- a/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/application/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -10,6 +10,7 @@ import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.ext.interactivelauncher.api.LauncherRequestDecorator; import org.opentripplanner.ext.ridehailing.RideHailingService; +import org.opentripplanner.ext.sorlandsbanen.EnturSorlandsbanenService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; @@ -41,6 +42,7 @@ OtpServerRequestContext providesServerContext( StreetLimitationParametersService streetLimitationParametersService, @Nullable TraverseVisitor traverseVisitor, EmissionsService emissionsService, + @Nullable EnturSorlandsbanenService enturSorlandsbanenService, LauncherRequestDecorator launcherRequestDecorator, @Nullable LuceneIndex luceneIndex ) { @@ -58,6 +60,7 @@ OtpServerRequestContext providesServerContext( realtimeVehicleService, vehicleRentalService, emissionsService, + enturSorlandsbanenService, routerConfig.flexParameters(), rideHailingServices, stopConsolidationService, diff --git a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index 0e81193d787..2c35dfe6263 100644 --- a/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/application/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -9,6 +9,7 @@ import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.geocoder.LuceneIndex; import org.opentripplanner.ext.ridehailing.RideHailingService; +import org.opentripplanner.ext.sorlandsbanen.EnturSorlandsbanenService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.inspector.raster.TileRendererManager; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; @@ -48,6 +49,10 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { private final RealtimeVehicleService realtimeVehicleService; private final VehicleRentalService vehicleRentalService; private final EmissionsService emissionsService; + + @Nullable + private final EnturSorlandsbanenService enturSorlandsbanenService; + private final StopConsolidationService stopConsolidationService; private final StreetLimitationParametersService streetLimitationParametersService; private final LuceneIndex luceneIndex; @@ -67,12 +72,13 @@ private DefaultServerRequestContext( WorldEnvelopeService worldEnvelopeService, RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, - EmissionsService emissionsService, + @Nullable EmissionsService emissionsService, + @Nullable EnturSorlandsbanenService enturSorlandsbanenService, List rideHailingServices, - StopConsolidationService stopConsolidationService, + @Nullable StopConsolidationService stopConsolidationService, StreetLimitationParametersService streetLimitationParametersService, FlexParameters flexParameters, - TraverseVisitor traverseVisitor, + @Nullable TraverseVisitor traverseVisitor, @Nullable LuceneIndex luceneIndex ) { this.graph = graph; @@ -90,6 +96,7 @@ private DefaultServerRequestContext( this.realtimeVehicleService = realtimeVehicleService; this.rideHailingServices = rideHailingServices; this.emissionsService = emissionsService; + this.enturSorlandsbanenService = enturSorlandsbanenService; this.stopConsolidationService = stopConsolidationService; this.streetLimitationParametersService = streetLimitationParametersService; this.luceneIndex = luceneIndex; @@ -110,6 +117,7 @@ public static DefaultServerRequestContext create( RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, @Nullable EmissionsService emissionsService, + @Nullable EnturSorlandsbanenService enturSorlandsbanenService, FlexParameters flexParameters, List rideHailingServices, @Nullable StopConsolidationService stopConsolidationService, @@ -130,6 +138,7 @@ public static DefaultServerRequestContext create( realtimeVehicleService, vehicleRentalService, emissionsService, + enturSorlandsbanenService, rideHailingServices, stopConsolidationService, streetLimitationParametersService, @@ -251,4 +260,9 @@ public LuceneIndex lucenceIndex() { public EmissionsService emissionsService() { return emissionsService; } + + @Nullable + public EnturSorlandsbanenService enturSorlandsbanenService() { + return enturSorlandsbanenService; + } } diff --git a/application/src/test/java/org/opentripplanner/TestServerContext.java b/application/src/test/java/org/opentripplanner/TestServerContext.java index 04a222c4434..0b541d7fb85 100644 --- a/application/src/test/java/org/opentripplanner/TestServerContext.java +++ b/application/src/test/java/org/opentripplanner/TestServerContext.java @@ -51,6 +51,7 @@ public static OtpServerRequestContext createServerContext( createRealtimeVehicleService(transitService), createVehicleRentalService(), createEmissionsService(), + null, routerConfig.flexParameters(), List.of(), null, diff --git a/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index b8eeb2d13d9..5feb40f71b3 100644 --- a/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/application/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -145,6 +145,7 @@ void setup() { new DefaultRealtimeVehicleService(transitService), new DefaultVehicleRentalService(), new DefaultEmissionsService(new EmissionsDataModel()), + null, RouterConfig.DEFAULT.flexParameters(), List.of(), null, diff --git a/application/src/test/java/org/opentripplanner/framework/geometry/WgsCoordinateTest.java b/application/src/test/java/org/opentripplanner/framework/geometry/WgsCoordinateTest.java index 5cd7ac66123..4bb27fa6d36 100644 --- a/application/src/test/java/org/opentripplanner/framework/geometry/WgsCoordinateTest.java +++ b/application/src/test/java/org/opentripplanner/framework/geometry/WgsCoordinateTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.framework.geometry.WgsCoordinate.GREENWICH; import java.util.List; import org.junit.jupiter.api.Test; @@ -101,8 +102,8 @@ void add() { @Test void testGreenwich() { - assertEquals(51.48d, WgsCoordinate.GREENWICH.latitude()); - assertEquals(0d, WgsCoordinate.GREENWICH.longitude()); + assertEquals(51.48d, GREENWICH.latitude()); + assertEquals(0d, GREENWICH.longitude()); } @Test @@ -120,4 +121,9 @@ void roundingTo100m() { assertEquals(10, rounded.longitude()); assertEquals(53.557, rounded.latitude()); } + + @Test + void testDistanceTo() { + assertEquals(131_394, (int) GREENWICH.distanceTo(GREENWICH.add(-1.0, 1.0))); + } } diff --git a/application/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java b/application/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java index 657e060c30b..13f341279f7 100644 --- a/application/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java +++ b/application/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java @@ -77,7 +77,7 @@ void enforcePackageDependenciesRaptorAPI() { @Test void enforcePackageDependenciesRaptorSPI() { - RAPTOR.subPackage("spi").dependsOn(FRAMEWORK_UTILS, API_MODEL, API_PATH).verify(); + RAPTOR_SPI.dependsOn(FRAMEWORK_UTILS, API_MODEL, API_PATH).verify(); } @Test diff --git a/application/src/test/java/org/opentripplanner/raptor/moduletests/I01_HeuristicTest.java b/application/src/test/java/org/opentripplanner/raptor/moduletests/I01_HeuristicTest.java index 7fe9cfa2c15..1e2f79cce44 100644 --- a/application/src/test/java/org/opentripplanner/raptor/moduletests/I01_HeuristicTest.java +++ b/application/src/test/java/org/opentripplanner/raptor/moduletests/I01_HeuristicTest.java @@ -90,7 +90,7 @@ public void setup() { public void regular() { var request = requestBuilder.build(); - var search = new RangeRaptorDynamicSearch<>(config, data, request); + var search = new RangeRaptorDynamicSearch<>(config, data, null, request); search.route(); @@ -105,7 +105,7 @@ public void withConstrainedTransfers() { var request = requestBuilder.build(); - var search = new RangeRaptorDynamicSearch<>(config, data, request); + var search = new RangeRaptorDynamicSearch<>(config, data, null, request); search.route(); diff --git a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index 38fb11712b8..b47d91ae999 100644 --- a/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/application/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -117,6 +117,7 @@ public SpeedTest( TestServerContext.createRealtimeVehicleService(transitService), TestServerContext.createVehicleRentalService(), TestServerContext.createEmissionsService(), + null, config.flexConfig, List.of(), null, diff --git a/doc/user/Configuration.md b/doc/user/Configuration.md index bca974f8617..a45e4253a06 100644 --- a/doc/user/Configuration.md +++ b/doc/user/Configuration.md @@ -250,6 +250,7 @@ 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. | | ✓️ | +| `Sorlandsbanen` | Include train Sørlandsbanen in results when searchig in south of Norway. Only relevant in Norway. | | ✓️ | | `TransferAnalyzer` | Analyze transfers during graph build. | | ✓️ | diff --git a/doc/user/sandbox/Sorlandsbanen.md b/doc/user/sandbox/Sorlandsbanen.md new file mode 100644 index 00000000000..4898a29af37 --- /dev/null +++ b/doc/user/sandbox/Sorlandsbanen.md @@ -0,0 +1,44 @@ +# Sørlandsbanen - The southern railroad in Norway + +**This sandbox module is only working in Norway**, in particular only in the south of Norway. The +feature flag to turn it *on* should only be enabled if you are routing using the norwegian data set. + +The railroad in southern Norway is very slow and does not go by the cost where most people live. It +is easily beaten by coaches in the area. Despite this, we need to include it in results where it is +relevant. + +When the feature flag is enabled, two Raptor searches are performed. The first is the regular +search - unmodified, as requested by the user. The second search is modified to include train +results with Sørlandsbanen. This is achieved by setting a high COACH reluctance. We then take any +rail results(if they exist) from the second search and add it two to the results from the first +search. The new set of results will contain everything we found in the first search, plus the train +results in the second results. + +Note! This is a hack and the logic to enable this look at the origin and destination coordinates +in addition to the feature flag. + + +## Contact Info + +- Entur, Norway + +## Changelog + +- 2024-10-14: We have used this feature for som time, but now want it in the Sandbox so we do not + need to merge it everytime we create a new entur release. + + +### Configuration + +This is turned _off_ by default. To turn it on enable the `Sorlandsbanen` feature. + +```json +// otp-config.json +{ + "otpFeatures": { + "Sorlandsbanen": true + } +} +``` + + diff --git a/mkdocs.yml b/mkdocs.yml index 1364be7be1f..7d1c25bd45d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -122,3 +122,4 @@ nav: - Ride Hailing: 'sandbox/RideHailing.md' - Emissions: 'sandbox/Emissions.md' - Stop Consolidation: 'sandbox/StopConsolidation.md' + - Sørlandsbanen: 'sandbox/Sorlandsbanen.md'