diff --git a/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java b/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java index 724cc649243..b17fab84347 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java @@ -3,9 +3,9 @@ import java.util.Arrays; import java.util.BitSet; import java.util.Objects; +import java.util.function.IntFunction; import java.util.stream.IntStream; import javax.annotation.Nullable; -import org.opentripplanner.framework.lang.IntUtils; import org.opentripplanner.framework.lang.StringUtils; /** @@ -55,8 +55,28 @@ public int hashCode() { */ @Override public String toString() { - return ( - "(" + (name == null ? "" : name + ", ") + "stops: " + IntUtils.intArrayToString(stops) + ")" - ); + return toString(Integer::toString); + } + + public String toString(IntFunction nameResolver) { + StringBuilder buf = new StringBuilder("("); + if (name != null) { + buf.append(name).append(", "); + } + buf.append("stops: "); + appendStops(buf, ", ", nameResolver); + return buf.append(")").toString(); + } + + public void appendStops(StringBuilder buf, String sep, IntFunction nameResolver) { + boolean skipFirst = true; + for (int stop : stops) { + if (skipFirst) { + skipFirst = false; + } else { + buf.append(sep); + } + buf.append(nameResolver.apply(stop)); + } } } diff --git a/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java b/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java index 7184d70a9c2..6dae7ee2c5a 100644 --- a/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java +++ b/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java @@ -4,6 +4,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; +import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.raptor.api.model.RaptorConstrainedTransfer; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; @@ -18,19 +19,20 @@ /** * The path builder is a utility to build paths. The path builder is responsible for reconstructing - * information that was used in decision making inside Raptor, but not kept due to performance + * information that was used in decision-making inside Raptor, but not kept due to performance * reasons. For example information about the transfer like transfer constraints. *

* The path builder enforces the same logic as Raptor and generates information like the * generalized-cost instead of getting it from the stop-arrivals. This is convenient if a path is - * created OUTSIDE Raptor, which is the case in the {@link org.opentripplanner.routing.algorithm.transferoptimization.OptimizeTransferService}. + * created OUTSIDE Raptor, which is the case in the {@link + * org.opentripplanner.routing.algorithm.transferoptimization.OptimizeTransferService}. *

* The path builder comes in two versions. One which adds new legs to the tail of the path, allowing * us to add legs starting with the access leg and ending with the egress leg. The other adds legs - * in the opposite order, from egress to access. Hence the forward and reverse mappers are + * in the opposite order, from egress to access. Hence, the forward and reverse mappers are * simplified using the head and tail builder respectively. See {@link #headPathBuilder( - * RaptorSlackProvider, RaptorCostCalculator, RaptorStopNameResolver, - * RaptorPathConstrainedTransferSearch)} and {@link #tailPathBuilder(RaptorSlackProvider, + * RaptorSlackProvider, int, RaptorCostCalculator, RaptorStopNameResolver, + * RaptorPathConstrainedTransferSearch)} and {@link #tailPathBuilder(RaptorSlackProvider, int, * RaptorCostCalculator, RaptorStopNameResolver, RaptorPathConstrainedTransferSearch)}. *

* The builder is also used for creating test data in unit test. @@ -53,8 +55,7 @@ public abstract class PathBuilder { @Nullable private final RaptorPathConstrainedTransferSearch transferConstraintsSearch; - @Nullable - private int c2; + private int c2 = RaptorConstants.NOT_SET; // Path leg elements as a double linked list. This makes it easy to look at // legs before and after in the logic and easy to fork, building alternative @@ -177,10 +178,14 @@ public void c2(int c2) { this.c2 = c2; } + public int c2() { + return tail.isC2Set() ? tail.c2() : c2; + } + public RaptorPath build() { updateAggregatedFields(); var pathLegs = createPathLegs(costCalculator, slackProvider); - return new Path<>(iterationDepartureTime, pathLegs, pathLegs.generalizedCostTotal(), c2); + return new Path<>(iterationDepartureTime, pathLegs, pathLegs.generalizedCostTotal(), c2()); } @Override diff --git a/src/main/java/org/opentripplanner/raptor/path/PathBuilderLeg.java b/src/main/java/org/opentripplanner/raptor/path/PathBuilderLeg.java index d4375c97aaf..eebdf69bfd9 100644 --- a/src/main/java/org/opentripplanner/raptor/path/PathBuilderLeg.java +++ b/src/main/java/org/opentripplanner/raptor/path/PathBuilderLeg.java @@ -22,8 +22,8 @@ import org.opentripplanner.raptor.spi.RaptorSlackProvider; /** - * This is the leg implementation for the {@link PathBuilder}. It is a private inner class which - * helps to cache and calculate values before constructing a path. + * This is the leg implementation for the {@link PathBuilder}. It helps to cache and calculate + * values before constructing a path. */ public class PathBuilderLeg { @@ -40,6 +40,7 @@ public class PathBuilderLeg { private int fromTime = NOT_SET; private int toTime = NOT_SET; + private int c2 = RaptorConstants.NOT_SET; private PathBuilderLeg prev = null; private PathBuilderLeg next = null; @@ -54,6 +55,7 @@ private PathBuilderLeg(PathBuilderLeg other) { this.fromTime = other.fromTime; this.toTime = other.toTime; this.leg = other.leg; + this.c2 = other.c2; // Mutable fields if (other.next != null) { @@ -72,7 +74,7 @@ private PathBuilderLeg(MyLeg leg) { } } - /* factory methods */ + /* accessors */ public int fromTime() { return fromTime; @@ -94,8 +96,6 @@ public int toStop() { return leg.toStop(); } - /* accessors */ - public int toStopPos() { return asTransitLeg().toStopPos(); } @@ -104,6 +104,18 @@ public int durationInSec() { return toTime - fromTime; } + public int c2() { + return c2; + } + + public void c2(int c2) { + this.c2 = c2; + } + + public boolean isC2Set() { + return c2 != RaptorConstants.NOT_SET; + } + @Nullable public RaptorConstrainedTransfer constrainedTransferAfterLeg() { return isTransit() ? asTransitLeg().constrainedTransferAfterLeg : null; @@ -146,6 +158,10 @@ public T trip() { return asTransitLeg().trip; } + public PathBuilderLeg prev() { + return prev; + } + public PathBuilderLeg next() { return next; } @@ -294,10 +310,6 @@ static PathBuilderLeg egress(RaptorAccessEgres return new PathBuilderLeg<>(new MyEgressLeg(egress)); } - PathBuilderLeg prev() { - return prev; - } - void setPrev(PathBuilderLeg prev) { this.prev = prev; } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index e8a4c9e3311..b9f9685341c 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -142,17 +142,16 @@ private TransitRouterResult route() { Collection> paths = transitResponse.paths(); if (OTPFeature.OptimizeTransfers.isOn() && !transitResponse.containsUnknownPaths()) { - paths = - TransferOptimizationServiceConfigurator - .createOptimizeTransferService( - transitLayer::getStopByIndex, - requestTransitDataProvider.stopNameResolver(), - serverContext.transitService().getTransferService(), - requestTransitDataProvider, - transitLayer.getStopBoardAlightCosts(), - request.preferences().transfer().optimization() - ) - .optimize(transitResponse.paths()); + var service = TransferOptimizationServiceConfigurator.createOptimizeTransferService( + transitLayer::getStopByIndex, + requestTransitDataProvider.stopNameResolver(), + serverContext.transitService().getTransferService(), + requestTransitDataProvider, + transitLayer.getStopBoardAlightCosts(), + request.preferences().transfer().optimization(), + raptorRequest.multiCriteria() + ); + paths = service.optimize(transitResponse.paths()); } // Create itineraries diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java index 231abf74555..a733927f068 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java @@ -4,17 +4,18 @@ import org.opentripplanner.model.transfer.TransferService; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; +import org.opentripplanner.raptor.api.request.MultiCriteriaRequest; import org.opentripplanner.raptor.spi.RaptorCostCalculator; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; import org.opentripplanner.routing.algorithm.transferoptimization.OptimizeTransferService; import org.opentripplanner.routing.algorithm.transferoptimization.api.TransferOptimizationParameters; -import org.opentripplanner.routing.algorithm.transferoptimization.model.MinCostFilterChain; import org.opentripplanner.routing.algorithm.transferoptimization.model.MinSafeTransferTimeCalculator; -import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; import org.opentripplanner.routing.algorithm.transferoptimization.model.TransferWaitTimeCostCalculator; +import org.opentripplanner.routing.algorithm.transferoptimization.model.costfilter.MinCostPathTailFilterFactory; +import org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.PassThroughPathTailFilter; import org.opentripplanner.routing.algorithm.transferoptimization.services.OptimizePathDomainService; import org.opentripplanner.routing.algorithm.transferoptimization.services.TransferGenerator; -import org.opentripplanner.routing.algorithm.transferoptimization.services.TransferOptimizedFilterFactory; import org.opentripplanner.routing.algorithm.transferoptimization.services.TransferServiceAdaptor; import org.opentripplanner.transit.model.site.StopLocation; @@ -29,6 +30,7 @@ public class TransferOptimizationServiceConfigurator transitDataProvider; private final int[] stopBoardAlightCosts; private final TransferOptimizationParameters config; + private final MultiCriteriaRequest multiCriteriaRequest; private TransferOptimizationServiceConfigurator( IntFunction stopLookup, @@ -36,7 +38,8 @@ private TransferOptimizationServiceConfigurator( TransferService transferService, RaptorTransitDataProvider transitDataProvider, int[] stopBoardAlightCosts, - TransferOptimizationParameters config + TransferOptimizationParameters config, + MultiCriteriaRequest multiCriteriaRequest ) { this.stopLookup = stopLookup; this.stopNameResolver = stopNameResolver; @@ -44,6 +47,7 @@ private TransferOptimizationServiceConfigurator( this.transitDataProvider = transitDataProvider; this.stopBoardAlightCosts = stopBoardAlightCosts; this.config = config; + this.multiCriteriaRequest = multiCriteriaRequest; } /** @@ -57,7 +61,8 @@ > OptimizeTransferService createOptimizeTransferService( TransferService transferService, RaptorTransitDataProvider transitDataProvider, int[] stopBoardAlightCosts, - TransferOptimizationParameters config + TransferOptimizationParameters config, + MultiCriteriaRequest multiCriteriaRequest ) { return new TransferOptimizationServiceConfigurator( stopLookup, @@ -65,24 +70,20 @@ > OptimizeTransferService createOptimizeTransferService( transferService, transitDataProvider, stopBoardAlightCosts, - config + config, + multiCriteriaRequest ) .createOptimizeTransferService(); } private OptimizeTransferService createOptimizeTransferService() { var pathTransferGenerator = createTransferGenerator(config.optimizeTransferPriority()); - var filter = createTransferOptimizedFilter( - config.optimizeTransferPriority(), - config.optimizeTransferWaitTime() - ); if (config.optimizeTransferWaitTime()) { var transferWaitTimeCalculator = createTransferWaitTimeCalculator(); var transfersPermutationService = createOptimizePathService( pathTransferGenerator, - filter, transferWaitTimeCalculator, transitDataProvider.multiCriteriaCostCalculator() ); @@ -95,7 +96,6 @@ private OptimizeTransferService createOptimizeTransferService() { } else { var transfersPermutationService = createOptimizePathService( pathTransferGenerator, - filter, null, transitDataProvider.multiCriteriaCostCalculator() ); @@ -105,7 +105,6 @@ private OptimizeTransferService createOptimizeTransferService() { private OptimizePathDomainService createOptimizePathService( TransferGenerator transferGenerator, - MinCostFilterChain> transferPointFilter, TransferWaitTimeCostCalculator transferWaitTimeCostCalculator, RaptorCostCalculator costCalculator ) { @@ -116,7 +115,7 @@ private OptimizePathDomainService createOptimizePathService( transferWaitTimeCostCalculator, stopBoardAlightCosts, config.extraStopBoardAlightCostsFactor(), - transferPointFilter, + createFilter(), stopNameResolver ); } @@ -140,10 +139,16 @@ private TransferWaitTimeCostCalculator createTransferWaitTimeCalculator() { ); } - private MinCostFilterChain> createTransferOptimizedFilter( - boolean transferPriority, - boolean optimizeWaitTime - ) { - return TransferOptimizedFilterFactory.filter(transferPriority, optimizeWaitTime); + private PathTailFilter createFilter() { + var filter = new MinCostPathTailFilterFactory( + config.optimizeTransferPriority(), + config.optimizeTransferWaitTime() + ) + .createFilter(); + + if (multiCriteriaRequest.hasPassThroughPoints()) { + filter = new PassThroughPathTailFilter<>(filter, multiCriteriaRequest.passThroughPoints()); + } + return filter; } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/PathTailFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/PathTailFilter.java new file mode 100644 index 00000000000..cabc9ac33aa --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/PathTailFilter.java @@ -0,0 +1,31 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model; + +import java.util.Set; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; + +/** + * Filter path tails for a given stopPosition during the optimization process. The algorithm only + * feeds in paths which can be compared. If the head is alighted at a position which gives it an + * advantage over another path (accept boarding at more stops), then the paths are split into + * different sets and the filter is called for each set. + */ +public interface PathTailFilter { + /** + * Filter path while building the paths. The {@code head} of each path is guaranteed to be a + * transit leg, and the {@code boardStopPosition} is guaranteed to be the last position in the + * head leg which will be used for boarding. The {@code boardStopPosition} should be used when + * calculating the property which is used for comparison. If the comparison can be done without + * looking at a stop-position, then this can be ignored. + */ + Set> filterIntermediateResult( + Set> elements, + int boardStopPosition + ); + + /** + * Filter the paths one last time. The {@code head} is not guaranteed to be the access-leg. This + * can be used to insert values into the path or checking if all requirements are meet. + * + */ + Set> filterFinalResult(Set> elements); +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/MinCostFilterChain.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilter.java similarity index 51% rename from src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/MinCostFilterChain.java rename to src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilter.java index 554790ccb4c..3feb4cb5a1c 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/MinCostFilterChain.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilter.java @@ -1,9 +1,12 @@ -package org.opentripplanner.routing.algorithm.transferoptimization.model; +package org.opentripplanner.routing.algorithm.transferoptimization.model.costfilter; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.ToIntFunction; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; /** * This class takes a list of "cost functions" and creates a filter chain for them. The precedence @@ -17,23 +20,35 @@ * * @param The element type of the cost-functions and the filtered list */ -public class MinCostFilterChain { +class MinCostPathTailFilter implements PathTailFilter { - private final List> costFunctions; + private final List>> costFunctions; - public MinCostFilterChain(List> costFunctions) { + MinCostPathTailFilter(List>> costFunctions) { this.costFunctions = costFunctions; } - public Set filter(Set elements) { - for (ToIntFunction costFunction : costFunctions) { + @Override + public Set> filterIntermediateResult( + Set> elements, + int boardStopPosition + ) { + for (var costFunction : costFunctions) { elements = filter(elements, costFunction); } return elements; } - private Set filter(Set elements, ToIntFunction costFunction) { - var result = new HashSet(); + @Override + public Set> filterFinalResult(Set> elements) { + return filterIntermediateResult(elements, 0); + } + + private Set> filter( + Set> elements, + ToIntFunction> costFunction + ) { + var result = new HashSet>(); int minCost = Integer.MAX_VALUE; for (var it : elements) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilterFactory.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilterFactory.java new file mode 100644 index 00000000000..5bc3440c269 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilterFactory.java @@ -0,0 +1,47 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.costfilter; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.ToIntFunction; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; + +public class MinCostPathTailFilterFactory { + + private final boolean transferPriority; + private final boolean optimizeWaitTime; + + public MinCostPathTailFilterFactory(boolean transferPriority, boolean optimizeWaitTime) { + this.transferPriority = transferPriority; + this.optimizeWaitTime = optimizeWaitTime; + } + + public PathTailFilter createFilter() { + List>> filters = new ArrayList<>(3); + + if (transferPriority) { + filters.add(OptimizedPathTail::transferPriorityCost); + } + + if (optimizeWaitTime) { + filters.add(OptimizedPathTail::generalizedCostWaitTimeOptimized); + } else { + filters.add(OptimizedPathTail::generalizedCost); + } + + filters.add(OptimizedPathTail::breakTieCost); + + return new MinCostPathTailFilter<>(filters); + } + + /** + * This factory method is used for unit testing. It allows you to pass in a simple cost function + * instead of the more complicated functions used in the main version of this. + */ + public static PathTailFilter ofCostFunction( + ToIntFunction> costFunction + ) { + return new MinCostPathTailFilter<>(List.of(costFunction)); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPathTailFilter.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPathTailFilter.java new file mode 100644 index 00000000000..f0e6ec8d24b --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPathTailFilter.java @@ -0,0 +1,93 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import static java.util.stream.Collectors.toSet; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.PassThroughPoint; +import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; + +/** + * Create a filter chain function and find the best combination of transfers for the journey + * that also includes all pass-through points. + *

+ * The algorithm starts with the last trip in the journey, then goes backwards looping through all + * possible transfers for each transit leg. For each possible transfer stop position the C2-value + * is calculated. The filter chain function is going to use the c2-value and the cost function to + * determine whether the tail should be included or excluded from result. + *

+ *Example: + *

+ * Let's say we have a trip with 2 transit legs and 3 possible transfer points: AD, BE and CF. + *

+ * There are 3 possible transfer combination with the first and second transit: + *

+ *    Iteration 1 (initial c2 value is 1 since we have one pass-through point):
+ *
+ *      ? ~ transit 2 ~ egress | c2 = 1
+ *
+ *    Iteration 2 (create all possible journey combinations with transfers and calculate c2):
+ *
+ *      // C2 is 0 since we will pass through E if we board Transit 2 at D
+ *      ? ~ transit 1 ~ AD ~ Transit 2 ~ egress | c2 = 0
+ *
+ *      ? ~ transit 1 ~ BE ~ Transit 2 ~ egress | c2 = 0
+ *
+ *      // C2 is 1 since we will not pass through E if we board at F
+ *      ? ~ transit 1 ~ CF ~ Transit 2 ~ egress | c2 = 1
+ *
+ *    Iteration 3 (insert access and filter out all combinations where c2 != 0)
+ *      access ~ transit 1 ~ AD ~ transit 2 ~ egress | C2 = 0
+ *      access ~ transit 1 ~ BE ~ transit 2 ~ egress | C2 = 0
+ * 
+ * Then we're going to fall back the delegate filter to choose between the two options. + */ +public class PassThroughPathTailFilter implements PathTailFilter { + + private final PathTailFilter filterChain; + private final PathTailC2Calculator c2Calculator; + + public PassThroughPathTailFilter( + PathTailFilter filterChain, + List passThroughPoints + ) { + this.filterChain = filterChain; + this.c2Calculator = new PathTailC2Calculator(passThroughPoints); + } + + @Override + public Set> filterIntermediateResult( + Set> elements, + int boardStopPosition + ) { + Map>> elementsByC2Value = elements + .stream() + .collect( + Collectors.groupingBy( + it -> c2Calculator.calculateC2AtStopPos(it, boardStopPosition), + toSet() + ) + ); + var result = new HashSet>(); + for (var set : elementsByC2Value.values()) { + result.addAll(filterChain.filterIntermediateResult(set, boardStopPosition)); + } + return result; + } + + @Override + public Set> filterFinalResult(Set> elements) { + Set> result = elements + .stream() + .peek(c2Calculator::calculateC2) + .filter(it -> it.head().c2() == 0) + .collect(toSet()); + + return filterChain.filterFinalResult(result); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPointsIterator.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPointsIterator.java new file mode 100644 index 00000000000..01ea482d450 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughPointsIterator.java @@ -0,0 +1,57 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import java.util.BitSet; +import java.util.List; +import org.opentripplanner.raptor.api.request.PassThroughPoint; + +/** + * Iterate over the pass-through points. Note! This implementation iterates backwards starting at the last + * pass-through point. + */ +class PassThroughPointsIterator { + + private final List passThroughPoints; + private int index; + private BitSet current; + + private PassThroughPointsIterator(List passThroughPoints, int c2) { + this.passThroughPoints = passThroughPoints; + this.index = c2; + next(); + } + + /** + * Iterate from the given {@code c2} value (pass-through-point number minus one) towards the + * beginning. + */ + static PassThroughPointsIterator tailIterator(List passThroughPoints, int c2) { + return new PassThroughPointsIterator(passThroughPoints, c2); + } + + /** + * Iterate over the pass-though-points starting at the end/destination and towards the beginning + * of the points, until the origin is reached. + */ + static PassThroughPointsIterator tailIterator(List passThroughPoints) { + return new PassThroughPointsIterator(passThroughPoints, passThroughPoints.size()); + } + + /** + * The current c2 value reached by the iterator. + */ + int currC2() { + return index + 1; + } + + /** + * Go to the next element (move to the previous pass-though-point) + */ + void next() { + --index; + this.current = index < 0 ? null : passThroughPoints.get(index).asBitSet(); + } + + boolean isPassThroughPoint(int stopIndex) { + return current != null && current.get(stopIndex); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PathTailC2Calculator.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PathTailC2Calculator.java new file mode 100644 index 00000000000..45605d45062 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PathTailC2Calculator.java @@ -0,0 +1,79 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import java.util.List; +import org.opentripplanner.raptor.api.request.PassThroughPoint; +import org.opentripplanner.raptor.path.PathBuilderLeg; +import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; + +class PathTailC2Calculator { + + private final List passThroughPoints; + + PathTailC2Calculator(List passThroughPoints) { + this.passThroughPoints = passThroughPoints; + } + + int calculateC2(OptimizedPathTail tail) { + return calculateAndSetC2ForLeg(tail.head()).currC2(); + } + + int calculateC2AtStopPos(OptimizedPathTail tail, int fromStopPos) { + return calculateC2ForLeg(tail.head(), fromStopPos); + } + + private int calculateC2ForLeg(PathBuilderLeg curr, int fromStopPos) { + var ptpIter = calculateAndSetC2ForLeg(curr); + calculateC2AtStopPos(curr, fromStopPos, ptpIter); + return ptpIter.currC2(); + } + + private PassThroughPointsIterator calculateAndSetC2ForLeg(PathBuilderLeg tail) { + PassThroughPointsIterator ptpIter; + + var curr = findFirstLegWithC2Set(tail); + + if (curr.isEgress()) { + ptpIter = PassThroughPointsIterator.tailIterator(passThroughPoints); + curr.c2(ptpIter.currC2()); + } else { + ptpIter = PassThroughPointsIterator.tailIterator(passThroughPoints, curr.c2()); + } + + while (curr != tail) { + if (curr.isTransit()) { + calculateC2AtStopPos(curr, curr.fromStopPos(), ptpIter); + } + curr = curr.prev(); + curr.c2(ptpIter.currC2()); + } + curr.c2(ptpIter.currC2()); + return ptpIter; + } + + /** + * Find the first leg that has the c2 value set - starting with the given leg and ending with + * the egress leg. If no c2 value is set in the tail, then the egress leg is returned. + */ + private PathBuilderLeg findFirstLegWithC2Set(PathBuilderLeg tail) { + while (!tail.isEgress()) { + if (tail.isC2Set()) { + return tail; + } + tail = tail.next(); + } + return tail; + } + + private void calculateC2AtStopPos( + PathBuilderLeg leg, + int stopPos, + PassThroughPointsIterator ptpIter + ) { + var pattern = leg.trip().pattern(); + for (int pos = leg.toStopPos(); pos >= stopPos; --pos) { + if (ptpIter.isPassThroughPoint(pattern.stopIndex(pos))) { + ptpIter.next(); + } + } + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java index 7798f3342b8..e587bff8e70 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java @@ -1,5 +1,7 @@ package org.opentripplanner.routing.algorithm.transferoptimization.services; +import static java.util.stream.Collectors.toSet; + import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -14,8 +16,8 @@ import org.opentripplanner.raptor.spi.RaptorCostCalculator; import org.opentripplanner.raptor.spi.RaptorSlackProvider; import org.opentripplanner.routing.algorithm.transferoptimization.api.OptimizedPath; -import org.opentripplanner.routing.algorithm.transferoptimization.model.MinCostFilterChain; import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; import org.opentripplanner.routing.algorithm.transferoptimization.model.TransferWaitTimeCostCalculator; import org.opentripplanner.routing.algorithm.transferoptimization.model.TripToTripTransfer; @@ -31,7 +33,7 @@ * M : number of possible transfer location for a given pair of trips * * Without any pruning the permutations have an order of O(N^M), but by filtering during the path - * construction we get close to O(N*M) - which is acceptable. + * construction we get close to o(N*M) - which is acceptable. * * Example with 3 lines(trips), where the `+` indicate stop places: * @@ -69,7 +71,7 @@ public class OptimizePathDomainService { private final TransferGenerator transferGenerator; private final RaptorCostCalculator costCalculator; private final RaptorSlackProvider slackProvider; - private final MinCostFilterChain> minCostFilterChain; + private final PathTailFilter filter; private final RaptorStopNameResolver stopNameTranslator; @Nullable @@ -87,7 +89,7 @@ public OptimizePathDomainService( @Nullable TransferWaitTimeCostCalculator waitTimeCostCalculator, int[] stopBoardAlightCosts, double extraStopBoardAlightCostsFactor, - MinCostFilterChain> minCostFilterChain, + PathTailFilter filter, RaptorStopNameResolver stopNameTranslator ) { this.transferGenerator = transferGenerator; @@ -96,7 +98,7 @@ public OptimizePathDomainService( this.waitTimeCostCalculator = waitTimeCostCalculator; this.stopBoardAlightCosts = stopBoardAlightCosts; this.extraStopBoardAlightCostsFactor = extraStopBoardAlightCostsFactor; - this.minCostFilterChain = minCostFilterChain; + this.filter = filter; this.stopNameTranslator = stopNameTranslator; } @@ -109,9 +111,11 @@ public Set> findBestTransitPath(RaptorPath originalPath) { ); // Combine transit legs and transfers - var tails = findBestTransferOption(originalPath, transitLegs, possibleTransfers); + var tails = findBestTransferOption(originalPath, transitLegs, possibleTransfers, filter); + + var filteredTails = filter.filterFinalResult(tails); - return tails.stream().map(OptimizedPathTail::build).collect(Collectors.toSet()); + return filteredTails.stream().map(OptimizedPathTail::build).collect(toSet()); } private static T last(List list) { @@ -121,7 +125,8 @@ private static T last(List list) { private Set> findBestTransferOption( RaptorPath originalPath, List> originalTransitLegs, - List>> possibleTransfers + List>> possibleTransfers, + PathTailFilter filter ) { final int iterationDepartureTime = originalPath.rangeRaptorIterationDepartureTime(); // Create a set of tails with the last transit leg in it (one element) @@ -158,19 +163,19 @@ private Set> findBestTransferOption( // create a tailSelector for the tails produced in the last round and use it to filter them // based on the transfer-arrival-time and given filter - var tailSelector = new TransitPathLegSelector<>(minCostFilterChain, tails); + var tailSelector = new TransitPathLegSelector<>(filter, tails); // Reset the result set to an empty set tails = new HashSet<>(); for (TripToTripTransfer tx : transfers) { - // Skip transfers happening before earliest possible board time + // Skip transfers happening before the earliest possible board time if (tx.from().time() <= earliestDepartureTimeFromLeg) { continue; } // Find the best tails that are safe to board with respect to the arrival - var candidateTails = tailSelector.next(tx.to().time()); + var candidateTails = tailSelector.next(tx.to().stopPosition()); for (OptimizedPathTail tail : candidateTails) { // Tail can be used with current transfer @@ -183,8 +188,8 @@ private Set> findBestTransferOption( // Filter tails one final time tails = - new TransitPathLegSelector<>(minCostFilterChain, tails) - .next(originalPath.accessLeg().toTime()); + new TransitPathLegSelector<>(filter, tails) + .next(originalPath.accessLeg().nextTransitLeg().getFromStopPosition()); // Insert the access leg and the following transfer insertAccess(originalPath, tails); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransferOptimizedFilterFactory.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransferOptimizedFilterFactory.java deleted file mode 100644 index 9b950a2663e..00000000000 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransferOptimizedFilterFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.opentripplanner.routing.algorithm.transferoptimization.services; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.ToIntFunction; -import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.routing.algorithm.transferoptimization.model.MinCostFilterChain; -import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; - -public class TransferOptimizedFilterFactory { - - public static MinCostFilterChain> filter( - boolean transferPriority, - boolean optimizeWaitTime - ) { - return new TransferOptimizedFilterFactory().create(transferPriority, optimizeWaitTime); - } - - private MinCostFilterChain> create( - boolean transferPriority, - boolean optimizeWaitTime - ) { - List>> filters = new ArrayList<>(3); - - if (transferPriority) { - filters.add(OptimizedPathTail::transferPriorityCost); - } - - if (optimizeWaitTime) { - filters.add(OptimizedPathTail::generalizedCostWaitTimeOptimized); - } else { - filters.add(OptimizedPathTail::generalizedCost); - } - - filters.add(OptimizedPathTail::breakTieCost); - - return new MinCostFilterChain<>(filters); - } -} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java index b3fc002a87c..9c1af6fc1a4 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelector.java @@ -2,15 +2,15 @@ import java.util.HashSet; import java.util.Set; -import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.routing.algorithm.transferoptimization.model.MinCostFilterChain; import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; /** * This class takes a list of transit legs and returns the best leg based on the {@link - * TransferOptimizedFilterFactory} and the earliest-boarding-time. The filter is used to pick the - * best leg from the legs which can be boarded after the earliest-boarding-time. + * org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter} and + * the earliest-boarding-time. The filter is used to pick the best leg from the legs which can be + * boarded after the earliest-boarding-time. *

* HOW IT WORKS *

@@ -28,38 +28,35 @@ */ class TransitPathLegSelector { - private final MinCostFilterChain> filter; + private final PathTailFilter filter; private Set> remindingLegs; private Set> selectedLegs; - private int lastLimit = Integer.MAX_VALUE; + private int prevStopPosition = Integer.MAX_VALUE; - TransitPathLegSelector( - final MinCostFilterChain> filter, - final Set> legs - ) { + TransitPathLegSelector(final PathTailFilter filter, final Set> legs) { this.filter = filter; this.remindingLegs = Set.copyOf(legs); this.selectedLegs = new HashSet<>(); } - Set> next(final int earliestBoardingTime) { - if (earliestBoardingTime > lastLimit) { + Set> next(final int fromStopPosition) { + if (fromStopPosition > prevStopPosition) { throw new IllegalStateException( "The next method must be called with decreasing time limits. " + - "minTimeLimit=" + - TimeUtils.timeToStrLong(earliestBoardingTime) + - ", lastLimit=" + - TimeUtils.timeToStrLong(lastLimit) + "fromStopPosition=" + + fromStopPosition + + ", previousStopPosition=" + + prevStopPosition ); } - lastLimit = earliestBoardingTime; + prevStopPosition = fromStopPosition; Set> candidates = new HashSet<>(); Set> rest = new HashSet<>(); for (OptimizedPathTail it : remindingLegs) { - if (earliestBoardingTime < it.latestPossibleBoardingTime()) { + if (fromStopPosition < it.head().toStopPos()) { candidates.add(it); } else { rest.add(it); @@ -74,7 +71,7 @@ Set> next(final int earliestBoardingTime) { // Set state remindingLegs = rest; - selectedLegs = filter.filter(candidates); + selectedLegs = filter.filterIntermediateResult(candidates, fromStopPosition); return selectedLegs; } diff --git a/src/test/java/org/opentripplanner/model/modes/FilterFactoryTest.java b/src/test/java/org/opentripplanner/model/modes/PathTailFilterFactoryTest.java similarity index 99% rename from src/test/java/org/opentripplanner/model/modes/FilterFactoryTest.java rename to src/test/java/org/opentripplanner/model/modes/PathTailFilterFactoryTest.java index 72df8256d9f..1431fd8e20a 100644 --- a/src/test/java/org/opentripplanner/model/modes/FilterFactoryTest.java +++ b/src/test/java/org/opentripplanner/model/modes/PathTailFilterFactoryTest.java @@ -13,7 +13,7 @@ import org.opentripplanner.transit.model.basic.SubMode; import org.opentripplanner.transit.model.basic.TransitMode; -class FilterFactoryTest { +class PathTailFilterFactoryTest { private static final SubMode LOCAL_BUS = SubMode.getOrBuildAndCacheForever("localBus"); private static final SubMode EXPRESS_BUS = SubMode.getOrBuildAndCacheForever("expressBus"); diff --git a/src/test/java/org/opentripplanner/raptor/_data/RaptorTestConstants.java b/src/test/java/org/opentripplanner/raptor/_data/RaptorTestConstants.java index e94cca13d62..d293f7326e6 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/RaptorTestConstants.java +++ b/src/test/java/org/opentripplanner/raptor/_data/RaptorTestConstants.java @@ -52,6 +52,10 @@ public interface RaptorTestConstants { int STOP_G = 7; int STOP_H = 8; int STOP_I = 9; + int STOP_J = 10; + int STOP_K = 11; + int STOP_L = 12; + int STOP_M = 13; // Stop position in pattern int STOP_POS_0 = 0; diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/MinCostFilterChainTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/MinCostFilterChainTest.java deleted file mode 100644 index a9aacbf67b3..00000000000 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/MinCostFilterChainTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.opentripplanner.routing.algorithm.transferoptimization.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.List; -import java.util.Objects; -import java.util.Set; -import org.junit.jupiter.api.Test; -import org.opentripplanner.framework.tostring.ValueObjectToStringBuilder; - -class MinCostFilterChainTest { - - private final A v01 = new A("A", 0, 1); - private final A v10 = new A("B", 1, 0); - private final A v02 = new A("C", 0, 2); - private final A w01 = new A("A'", 0, 1); - - static Set setOf(A... as) { - return Set.of(as); - } - - @Test - void filterEmptySet() { - // filter empty set - assertEquals(Set.of(), new MinCostFilterChain(List.of(it -> it.x)).filter(Set.of())); - } - - @Test - void filterOneElement() { - assertEquals(setOf(v01), filter(v01)); - } - - @Test - void filterTwoDistinctEntries() { - assertEquals(setOf(v01), filter(v01, v10)); - // swap order, should not matter - assertEquals(setOf(v01), filter(v10, v01)); - } - - @Test - void filterTwoDistinctEntriesWithTheSameFirstValueX() { - // Keep best y (x is same) - assertEquals(setOf(v01), filter(v01, v02)); - assertEquals(setOf(v01), filter(v02, v01)); - } - - @Test - void filterTwoEqualVectors() { - assertEquals(setOf(v01, w01), filter(v01, w01)); - assertEquals(setOf(v01, w01), filter(w01, v01)); - } - - private Set filter(A... as) { - return new MinCostFilterChain(List.of(it -> it.x, it -> it.y)).filter(setOf(as)); - } - - static class A { - - /** Name is included in eq/hc to be able to add the "same" [x,y] vector to a set. */ - public final String name; - public final int x; - public final int y; - - private A(String name, int x, int y) { - this.name = name; - this.x = x; - this.y = y; - } - - @Override - public int hashCode() { - return Objects.hash(x, y, name); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof A)) { - return false; - } - final A a = (A) o; - return name.equals(a.name) && x == a.x && y == a.y; - } - - @Override - public String toString() { - return ValueObjectToStringBuilder - .of() - .addText(name) - .addText("(") - .addNum(x) - .addText(", ") - .addNum(y) - .addText(")") - .toString(); - } - } -} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java index af643d9810c..bd4e5e12c38 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTailTest.java @@ -1,7 +1,6 @@ package org.opentripplanner.routing.algorithm.transferoptimization.model; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.opentripplanner.routing.algorithm.transferoptimization.services.TestTransferBuilder.txConstrained; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,7 +9,7 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; import org.opentripplanner.raptor.api.path.TransitPathLeg; -import org.opentripplanner.routing.algorithm.transferoptimization.services.TransferGeneratorDummy; +import org.opentripplanner.routing.algorithm.transferoptimization.services.TestTransferBuilder; class OptimizedPathTailTest implements RaptorTestConstants { @@ -27,17 +26,16 @@ class OptimizedPathTailTest implements RaptorTestConstants { private final TransitPathLeg t3 = t2.nextTransitLeg(); @SuppressWarnings("ConstantConditions") - private final TripToTripTransfer tx23 = TransferGeneratorDummy.tx( - txConstrained(t2.trip(), STOP_D, t3.trip(), STOP_D).staySeated() - ); + private final TripToTripTransfer tx23 = TestTransferBuilder + .tx(t2.trip(), STOP_D, t3.trip(), STOP_D) + .staySeated() + .build(); + + private final TripToTripTransfer tx12 = TestTransferBuilder + .tx(t1.trip(), STOP_B, t2.trip(), STOP_C) + .walk(D2m) + .build(); - private final TripToTripTransfer tx12 = TransferGeneratorDummy.tx( - t1.trip(), - STOP_B, - D2m, - STOP_C, - t2.trip() - ); private final TransferWaitTimeCostCalculator waitTimeCalc = new TransferWaitTimeCostCalculator( 1.0, 5.0 diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilterTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilterTest.java new file mode 100644 index 00000000000..910789edbd5 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/costfilter/MinCostPathTailFilterTest.java @@ -0,0 +1,110 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.costfilter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.raptor._data.stoparrival.BasicPathTestCase.COST_CALCULATOR; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.opentripplanner.raptor._data.RaptorTestConstants; +import org.opentripplanner.raptor._data.transit.TestTripSchedule; +import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.TransferWaitTimeCostCalculator; + +class MinCostPathTailFilterTest implements RaptorTestConstants { + + private static final TransferWaitTimeCostCalculator WAIT_TIME_CALC = new TransferWaitTimeCostCalculator( + 1.0, + 5.0 + ); + + private final A v01 = new A("A", 0, 11); + private final A v10 = new A("B", 1, 10); + private final A v02 = new A("C", 0, 12); + private final A w01 = new A("A'", 0, 11); + + @Test + void filterEmptySet() { + // filter empty set + assertEquals( + Set.of(), + new MinCostPathTailFilter(List.of(OptimizedPathTail::generalizedCost)) + .filterIntermediateResult(Set.of(), 0) + ); + } + + @Test + void filterOneElement() { + assertEquals(Set.of(v01), filter(v01)); + } + + @Test + void filterTwoDistinctEntries() { + assertEquals(Set.of(v01), filter(v01, v10)); + // swap order, should not matter + assertEquals(Set.of(v01), filter(v10, v01)); + } + + @Test + void filterTwoDistinctEntriesWithTheSameFirstValueX() { + // Keep best y (x is same) + assertEquals(Set.of(v01), filter(v01, v02)); + assertEquals(Set.of(v01), filter(v02, v01)); + } + + @Test + void filterTwoEqualVectors() { + assertEquals(Set.of(v01, w01), filter(v01, w01)); + assertEquals(Set.of(v01, w01), filter(w01, v01)); + } + + private Set filter(A... as) { + return new MinCostPathTailFilter(List.of(it -> ((A) it).x, it -> ((A) it).y)) + .filterIntermediateResult(Set.of(as), 0) + .stream() + .map(it -> (A) it) + .collect(Collectors.toSet()); + } + + A toA(OptimizedPathTail e) { + return (A) e; + } + + static class A extends OptimizedPathTail { + + /** Name is included in eq/hc to be able to add the "same" [x,y] vector to a set. */ + public final String name; + public final int x; + public final int y; + + private A(String name, int x, int y) { + super(SLACK_PROVIDER, COST_CALCULATOR, T00_00, WAIT_TIME_CALC, null, 0.0, null); + this.name = name; + this.x = x; + this.y = y; + } + + @Override + public int hashCode() { + return Objects.hash(x, y, name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof A a) { + return name.equals(a.name) && x == a.x && y == a.y; + } + return false; + } + + @Override + public String toString() { + return name + "(" + x + ", " + y + ")"; + } + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughNoTransfersTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughNoTransfersTest.java new file mode 100644 index 00000000000..cc071d91799 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughNoTransfersTest.java @@ -0,0 +1,96 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.framework.time.TimeUtils.time; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestCase.testCase; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.first; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.pathBuilder; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.subject; + +import java.util.List; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.raptor._data.RaptorTestConstants; +import org.opentripplanner.raptor._data.transit.TestTripSchedule; + +/** + * This test focus on the PASS-THROUGH functionality with a very simple scenario - one transit leg + * and no transfers. + *

+ * FEATURE UNDER TEST + *

+ * We want the path with the lowest generalized-cost that visits the pass-through points in the + * correct order. + *

+ * TEST SETUP + *

+ * We will use one trip with 5 stops. There is only one possible path - the original. + */ +@SuppressWarnings("SameParameterValue") +public class PassThroughNoTransfersTest implements RaptorTestConstants { + + private static final int ITERATION_START_TIME = time("10:00"); + /** Any stop not part of trip. */ + private static final int ANY_STOP = STOP_I; + + private final TestTripSchedule trip1 = TestTripSchedule + .schedule() + .pattern("Line 1", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E) + .times("10:05 10:10 10:15 10:20 10:25") + .build(); + + static List tripWithoutTransfersTestCases() { + return List.of( + testCase("").build(), + testCase("at board stop B").points(STOP_B).build(), + testCase("at intermediate stop C").points(STOP_C).build(), + testCase("at alight stop D").points(STOP_D).build(), + testCase("at either B or C").points(STOP_B, STOP_C).build(), + testCase("at either C or D").points(STOP_C, STOP_D).build(), + testCase("at C, w/unreachable A").points(STOP_A, STOP_C).build(), + testCase("at C, w/unreachable E").points(STOP_C, STOP_E).build(), + testCase("at C, w/unreachable stop not part of trip").points(STOP_C, ANY_STOP).build(), + testCase("at board stop B & intermediate stop C").points(STOP_B).points(STOP_C).build(), + testCase("at intermediate stop C & alight stop D").points(STOP_C).points(STOP_D).build(), + testCase("at board stop C & the alight stop D").points(STOP_C).points(STOP_D).build(), + testCase("at B, C, and D") + .points(STOP_A, STOP_B) + .points(STOP_C, STOP_I) + .points(STOP_D) + .build() + ); + } + + /** + * The pass-through point used can be the board-, intermediate-, and/or alight-stop. We will also + * test all combinations of these to make sure a pass-through point is only accounted for once. + *

+ * The trip1 used: + *

+   * Origin ~ walk ~ B ~ Trip 1 ~ D ~ walk ~ Destination
+   * 
+ * Note! Stop A and E is not visited. Stop I is part of one transfer-point, but not part of the + * trip. + */ + @ParameterizedTest + @MethodSource("tripWithoutTransfersTestCases") + public void tripWithoutTransfers(TestCase tc) { + var originalPath = pathBuilder() + .access(ITERATION_START_TIME, STOP_B, D1s) + .bus(trip1, STOP_D) + .egress(D1s); + + var subject = subject(tc.points()); + + // When + var result = subject.findBestTransitPath(originalPath); + + assertEquals(1, result.size()); + + // Then expect a set containing the original path + assertEquals( + originalPath.toString(this::stopIndexToName), + first(result).toString(this::stopIndexToName) + ); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughOneTransferTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughOneTransferTest.java new file mode 100644 index 00000000000..054915e84f4 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughOneTransferTest.java @@ -0,0 +1,189 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.framework.time.TimeUtils.time; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestCase.testCase; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.pathBuilder; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.pathFocus; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.subject; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.tx; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.raptor._data.RaptorTestConstants; +import org.opentripplanner.raptor._data.transit.TestTripSchedule; + +/** + * This test focus on the PASS-THROUGH functionality with two transit legs and one transfer in the + * path. But between trip 1 and trip 2, there may be many transfer options to choose from. + * + *

+ * FEATURE UNDER TEST + *

+ * We want the path with the lowest generalized-cost that visit the pass-through points in the + * correct order. + *

+ * TEST SETUP + *

+ * We will use 2 trips with a fixed set of transfers for each test. Each trip has 5 stops and + * plenty of slack to do the transfers for all possible stops combinations. We will set the + * transfer durations to get different generalized-costs for each possible path. We will set the + * cost so the transfers which do not contain any transfer-points have the lowest cost - is optimal + * on generalized-cost. We do this to make sure the subject-under-test is using the pass-through- + * points, and not the generalized cost to choose the correct path. + */ +@SuppressWarnings("SameParameterValue") +public class PassThroughOneTransferTest implements RaptorTestConstants { + + private static final int ITERATION_START_TIME = time("10:00"); + + /** + * We use arrays to store stuff per stop, so this is the max value of all stop indexes used, + * plus one. Gaps are Ok, if they exist. + */ + private static final int N_STOPS = STOP_M + 1; + + private final TestTripSchedule trip1 = TestTripSchedule + .schedule() + .pattern("Line 1", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E) + .times("10:05 10:10 10:15 10:20 10:25") + .build(); + + private final TestTripSchedule trip2 = TestTripSchedule + .schedule() + .pattern("Line 2", STOP_F, STOP_G, STOP_H, STOP_I, STOP_J) + .times("10:30 10:35 10:40 10:45 10:50") + .build(); + + static List tripWithOneTransferTestCases() { + return List.of( + testCase().expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_B).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_C).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_D).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_G).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_H).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_I).expectTransfer(STOP_C, STOP_H), + // Two stops in one pass-through point + testCase().points(STOP_B, STOP_C).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_B, STOP_D).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_B, STOP_G).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_B, STOP_H).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_B, STOP_I).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_C, STOP_D).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_C, STOP_G).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_C, STOP_H).expectTransfer(STOP_D, STOP_G), + testCase().points(STOP_C, STOP_I).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_D, STOP_G).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_D, STOP_H).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_D, STOP_I).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_G, STOP_H).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_G, STOP_I).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_H, STOP_I).expectTransfer(STOP_C, STOP_G), + // Two stops in two pass-through points + testCase().points(STOP_B).points(STOP_C).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_B).points(STOP_D).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_B).points(STOP_G).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_B).points(STOP_H).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_B).points(STOP_I).expectTransfer(STOP_C, STOP_H), + testCase().points(STOP_C).points(STOP_D).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_C).points(STOP_G).expectTransfer(STOP_D, STOP_G), + testCase().points(STOP_C).points(STOP_H).expectTransfer(STOP_D, STOP_G), + testCase().points(STOP_C).points(STOP_I).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_D).points(STOP_G).expectTransfer(STOP_D, STOP_G), + testCase().points(STOP_D).points(STOP_H).expectTransfer(STOP_D, STOP_G), + testCase().points(STOP_D).points(STOP_I).expectTransfer(STOP_D, STOP_H), + testCase().points(STOP_G).points(STOP_H).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_G).points(STOP_I).expectTransfer(STOP_C, STOP_G), + testCase().points(STOP_H).points(STOP_I).expectTransfer(STOP_C, STOP_G) + ); + } + + /** + * In this test we will use trip 1 and 2. We will have one test for each possible pass-through- + * point. We will add 4 transfers between the trips, [from]-[to]: {@code C-G, C-H, D-G, D-H}. We + * will also add transfers between B-F and E-I, these transfers can not be used with the access + * and egress, because we are not allowed to have two walking legs in a row. We include this + * transfers to make sure the implementation ignores them. + *

+ * + *

+   *           Origin
+   *                \
+   *  Trip 1   A --- B --- C --- D --- E
+   *                 |     | \ / |     |
+   *                 |     | / \ |     |
+   *  Trip 2         F --- G --- H --- I --- J
+   *                                    \
+   *                                     Destination
+   * 
+ * With this setup we will try all possible combinations of pass-through points and make sure + * the correct path is chosen. + *

+ * We will adjust the transfer walk duration so that paths containing the transfer-point get a + * high cost, and paths without it get a lower cost. + */ + @ParameterizedTest + @MethodSource("tripWithOneTransferTestCases") + public void tripWithOneTransfer(TestCase tc) { + var txCost = new WalkDurationForStopCombinations(N_STOPS) + .withPassThroughPoints(tc.points(), 10) + // This transfer do not visit D + .addTxCost(STOP_C, STOP_G, 2) + // This transfer do not visit D and G; hence given the lowest cost + .addTxCost(STOP_C, STOP_H, 1) + // This transfer visit all stops; Hence given the highest cost + .addTxCost(STOP_D, STOP_G, 3) + // This transfer do not visit G + .addTxCost(STOP_D, STOP_H, 2); + + // We need *a* path - the transfer here can be any. + var originalPath = pathBuilder() + .access(ITERATION_START_TIME, STOP_B, D1s) + .bus(trip1, STOP_D) + .walk(txCost.walkDuration(STOP_D, STOP_F), STOP_F) + .bus(trip2, STOP_I) + .egress(D1s); + + var expectedPath = pathBuilder() + .access(ITERATION_START_TIME, STOP_B, D1s) + .bus(trip1, tc.stopIndexA()) + .walk(txCost.walkDuration(tc.stopIndexA(), tc.stopIndexB()), tc.stopIndexB()) + .bus(trip2, STOP_I) + .egress(D1s); + + // These are illegal transfers for the given path, we add them here to make sure they + // do not interfere with the result. For simpler debugging problems try commenting out these + // lines, just do not forget to comment them back in when the problem is fixed. + var subject = subject( + tc.points(), + List.of( + tx(trip1, STOP_C, trip2, STOP_G, txCost), + tx(trip1, STOP_C, trip2, STOP_H, txCost), + tx(trip1, STOP_D, trip2, STOP_G, txCost), + tx(trip1, STOP_D, trip2, STOP_H, txCost), + // These are illegal transfers for the given path, we add them here to make sure they + // do not interfere with the result. For simpler debugging problems try comment out these + // lines, just do not forget to comment them back in when the problem is fixed. + tx(trip1, STOP_B, trip2, STOP_F, txCost), + tx(trip1, STOP_E, trip2, STOP_I, txCost) + ) + ); + + // When + var result = subject.findBestTransitPath(originalPath); + + // Then expect a set containing the expected path only + var resultAsString = result + .stream() + .map(it -> it.toString(this::stopIndexToName)) + .collect(Collectors.joining(", ")); + assertEquals( + pathFocus(expectedPath.toString(this::stopIndexToName)), + pathFocus(resultAsString), + resultAsString + ); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughTwoTransfersTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughTwoTransfersTest.java new file mode 100644 index 00000000000..fd2fd827d7c --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/PassThroughTwoTransfersTest.java @@ -0,0 +1,239 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.framework.time.TimeUtils.time; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestCase.testCase; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.pathBuilder; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.pathFocus; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.subject; +import static org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough.TestUtils.tx; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.raptor._data.RaptorTestConstants; +import org.opentripplanner.raptor._data.transit.TestTripSchedule; +import org.opentripplanner.raptor.api.path.RaptorPath; + +/** + * This test focus on the PASS-THROUGH functionality with three transit legs and two transfer in + * the path. + *

+ * FEATURE UNDER TEST + *

+ * We want the path with the lowest generalized-cost that visit the pass-through points in the + * correct order. + *

+ * TEST SETUP + *

+ * We will use 3 trips with a fixed set of transfers for each test. Each trip has 5 stops and + * plenty of slack to do the transfers for all possible stops combinations. There is two + * transfers to choose from between trip 1 and trip 2, and between trip 2 and trip 3. We will set + * the transfer durations to get different generalized-costs for each possible path. We will set + * the cost so the transfers which do not contain any transfer-points have the lowest cost - is + * optimal on generalized-cost. We do this to make sure the subject-under-test is using the + * pass-through-points, and not the generalized cost to choose the correct path. + */ +@SuppressWarnings("SameParameterValue") +public class PassThroughTwoTransfersTest implements RaptorTestConstants { + + private static final int ITERATION_START_TIME = time("10:00"); + + private final TestTripSchedule trip1 = TestTripSchedule + .schedule() + .pattern("Line 1", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E) + .times("10:05 10:10 10:15 10:20 10:25") + .build(); + + private final TestTripSchedule trip2 = TestTripSchedule + .schedule() + .pattern("Line 2", STOP_F, STOP_G, STOP_H, STOP_I, STOP_J) + .times("10:30 10:35 10:40 10:45 10:50") + .build(); + + private final TestTripSchedule trip3 = TestTripSchedule + .schedule() + .pattern("Line 3", STOP_K, STOP_L, STOP_M) + .times("10:55 11:00 11:05") + .build(); + + static List tripWithTwoTransferTestCases() { + return List.of( + // None pass-through-points + testCase("").expectTransfersFrom(STOP_C, STOP_H), + // One pass-through-points + testCase().points(STOP_B).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_C).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_D).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_E).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_I).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_J).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_L).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_K, STOP_D).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_K, STOP_E).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_K, STOP_I).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_K, STOP_J).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_K, STOP_L).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_K, STOP_M).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_G, STOP_B).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G, STOP_C).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G, STOP_D).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G, STOP_E).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G, STOP_I).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G, STOP_J).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G, STOP_L).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G, STOP_M).expectTransfersFrom(STOP_E, STOP_J), + testCase("at D w/unreachable A").points(STOP_D, STOP_A).expectTransfersFrom(STOP_E, STOP_J), + testCase("at E w/unreachable A").points(STOP_E, STOP_A).expectTransfersFrom(STOP_E, STOP_J), + testCase("at I w/unreachable A").points(STOP_I, STOP_A).expectTransfersFrom(STOP_C, STOP_J), + testCase("at J w/unreachable A").points(STOP_J, STOP_A).expectTransfersFrom(STOP_C, STOP_J), + testCase("at L w/unreachable A").points(STOP_L, STOP_A).expectTransfersFrom(STOP_C, STOP_H), + testCase("at D w/unreachable F").points(STOP_D, STOP_F).expectTransfersFrom(STOP_E, STOP_J), + testCase("at E w/unreachable F").points(STOP_E, STOP_F).expectTransfersFrom(STOP_E, STOP_J), + testCase("at I w/unreachable F").points(STOP_I, STOP_F).expectTransfersFrom(STOP_C, STOP_J), + testCase("at J w/unreachable F").points(STOP_J, STOP_F).expectTransfersFrom(STOP_C, STOP_J), + testCase("at L w/unreachable F").points(STOP_L, STOP_F).expectTransfersFrom(STOP_C, STOP_H), + // Two pass-through-points - a few samples + testCase().points(STOP_B).points(STOP_C).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_B).points(STOP_D).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_B).points(STOP_E).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_B).points(STOP_G).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_B).points(STOP_H).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_B).points(STOP_I).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_B).points(STOP_J).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_B).points(STOP_K).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_B).points(STOP_L).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_C).points(STOP_D).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_C).points(STOP_E).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_C).points(STOP_G).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_C).points(STOP_H).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_C).points(STOP_I).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_C).points(STOP_J).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_C).points(STOP_K).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_C).points(STOP_L).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_D).points(STOP_E, STOP_K).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_D).points(STOP_I, STOP_H).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_D).points(STOP_J, STOP_K).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_D).points(STOP_L).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_D).points(STOP_M).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_E).points(STOP_I).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_E).points(STOP_J).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_E).points(STOP_L).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_E).points(STOP_M).expectTransfersFrom(STOP_E, STOP_J), + testCase().points(STOP_G).points(STOP_H).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_G).points(STOP_I).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_G).points(STOP_J).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_G).points(STOP_K).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_G).points(STOP_L).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_G).points(STOP_M).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_H).points(STOP_I).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_H).points(STOP_J).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_H).points(STOP_K).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_H).points(STOP_L).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_H).points(STOP_M).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_I).points(STOP_J).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_I).points(STOP_L).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_I).points(STOP_M).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_J).points(STOP_L).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_J).points(STOP_M).expectTransfersFrom(STOP_C, STOP_J), + testCase().points(STOP_K).points(STOP_L).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_K).points(STOP_M).expectTransfersFrom(STOP_C, STOP_H), + testCase().points(STOP_L).points(STOP_M).expectTransfersFrom(STOP_C, STOP_H) + ); + } + + /** + * In this test we will test a path with *three* transit legs and 2 transfers. For each transfer + * there is two options, and in total three possible paths: + *

    + *
  1. Origin ~ B ~ C ~ G ~ H ~ K ~ M ~ Destination
  2. + *
  3. Origin ~ B ~ C ~ G ~ J ~ L ~ M ~ Destination
  4. + *
  5. Origin ~ B ~ E ~ I ~ J ~ L ~ M ~ Destination
  6. + *
+ * This is how the network look like: + *
+   *          Origin
+   *                \
+   *  Trip 1   A --- B --- C --- D --- E
+   *                        \ 5s[20s]   \ 10s
+   *  Trip 2           F --- G --- H --- I --- J
+   *                               \ 5s[20s]    \ 10s
+   *  Trip 3                        K ---------- L --- M
+   *                                                    \
+   *                                                     Destination
+   * 
+ * The point in this test is to give the path with transfer C-G & H-K an advantage + * (generalized-cost), but at the same time miss out on possible transfer-points (D,E,I,J). + *

+ * If stop G or K is part of a pass-through-point, then we would like to make an exception to the + * generalized-cost by increasing the cost for transfer K -> H-K and G -> C-G to 20s - making + * these transfers less favorable on generalized-cost. + *

+ * We will variate this test with zero, one and two pass-through point and by making unreachable + * stops(A,F) part of the pass-through-points. + */ + @ParameterizedTest + @MethodSource("tripWithTwoTransferTestCases") + public void tripWithTwoTransfer(TestCase tc) { + // We set up the cost so that the maximum number of stops are skipped if we route on + // generalized-cost only. + final int costCG = tc.contains(STOP_G) ? 20 : 5; + final int costEI = 10; + final int costHK = tc.contains(STOP_K) ? 20 : 5; + final int costJL = 10; + + // We need *a* path - the transfer her can be any + var originalPath = pathBuilder() + .access(ITERATION_START_TIME, STOP_B, D1s) + .bus(trip1, STOP_C) + .walk(costCG, STOP_G) + .bus(trip2, STOP_H) + .walk(costHK, STOP_K) + .bus(trip3, STOP_M) + .egress(D1s); + + RaptorPath expectedPath; + { + var b = pathBuilder().access(ITERATION_START_TIME, STOP_B, D1s); + + if (tc.stopIndexA() == STOP_C) { + b.bus(trip1, STOP_C).walk(costCG, STOP_G); + } else { + b.bus(trip1, STOP_E).walk(costEI, STOP_I); + } + if (tc.stopIndexB() == STOP_H) { + b.bus(trip2, STOP_H).walk(costHK, STOP_K); + } else { + b.bus(trip2, STOP_J).walk(costJL, STOP_L); + } + expectedPath = b.bus(trip3, STOP_M).egress(D1s); + } + + var firstTransfers = List.of( + tx(trip1, STOP_C, trip2, STOP_G, costCG), + tx(trip1, STOP_E, trip2, STOP_I, costEI) + ); + var secondTransfers = List.of( + tx(trip2, STOP_H, trip3, STOP_K, costHK), + tx(trip2, STOP_J, trip3, STOP_L, costJL) + ); + + var subject = subject(tc.points(), firstTransfers, secondTransfers); + + // When + + var result = subject.findBestTransitPath(originalPath); + + // Then expect a set containing the expected path only + var resultAsString = result + .stream() + .map(it -> it.toString(this::stopIndexToName)) + .collect(Collectors.joining(", ")); + assertEquals( + pathFocus(expectedPath.toString(this::stopIndexToName)), + pathFocus(resultAsString), + resultAsString + ); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/StopPair.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/StopPair.java new file mode 100644 index 00000000000..0291c3529eb --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/StopPair.java @@ -0,0 +1,3 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +record StopPair(int from, int to) {} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestCase.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestCase.java new file mode 100644 index 00000000000..3b2059cf4d9 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestCase.java @@ -0,0 +1,70 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import java.util.List; +import org.opentripplanner.raptor._data.RaptorTestConstants; +import org.opentripplanner.raptor.api.request.PassThroughPoint; + +record TestCase( + String description, + int stopIndexA, + int stopIndexB, + boolean fromAToB, + List points +) + implements RaptorTestConstants { + static TestCaseBuilder testCase(String description) { + return new TestCaseBuilder(description); + } + + static TestCaseBuilder testCase() { + return new TestCaseBuilder(null); + } + + @Override + public String toString() { + var buf = new StringBuilder(); + buf.append( + switch (points.size()) { + case 0 -> "No pass-through-points"; + case 1 -> "One pass-through-point "; + case 2 -> "Two pass-through-points "; + default -> points.size() + " pass-through-points "; + } + ); + if (description != null) { + buf.append(description); + } else { + if (points.size() == 1) { + buf.append("at "); + appendPoint(buf, 0); + } + if (points.size() > 1) { + buf.append("at ("); + appendPoint(buf, 0); + for (int i = 1; i < points.size(); ++i) { + buf.append(") and ("); + appendPoint(buf, i); + } + buf.append(")"); + } + } + + if (stopIndexA > 0 || stopIndexB > 0) { + buf + .append(". Expects transfer" + (fromAToB ? "" : "s") + " from ") + .append(stopIndexToName(stopIndexA)) + .append(fromAToB ? " to " : " and ") + .append(stopIndexToName(stopIndexB)); + } + buf.append(". ").append(points.stream().map(p -> p.toString(this::stopIndexToName)).toList()); + return buf.toString(); + } + + private void appendPoint(StringBuilder buf, int passThroughPointIndex) { + points.get(passThroughPointIndex).appendStops(buf, " or ", this::stopIndexToName); + } + + boolean contains(int stopIndex) { + return points.stream().anyMatch(it -> it.asBitSet().get(stopIndex)); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestCaseBuilder.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestCaseBuilder.java new file mode 100644 index 00000000000..aed3170df3f --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestCaseBuilder.java @@ -0,0 +1,42 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import java.util.ArrayList; +import java.util.List; +import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.request.PassThroughPoint; + +class TestCaseBuilder { + + final String description; + int stopIndexA = RaptorConstants.NOT_SET; + int stopIndexB = RaptorConstants.NOT_SET; + boolean txFromAToB = false; + final List points = new ArrayList<>(); + + TestCaseBuilder(String description) { + this.description = description; + } + + TestCaseBuilder points(int... stops) { + points.add(new PassThroughPoint("PT" + (points.size() + 1), stops)); + return this; + } + + TestCase expectTransfer(int fromStopIndex, int toStopIndex) { + this.stopIndexA = fromStopIndex; + this.stopIndexB = toStopIndex; + this.txFromAToB = true; + return build(); + } + + TestCase expectTransfersFrom(int fromStopIndexA, int fromStopIndexB) { + this.stopIndexA = fromStopIndexA; + this.stopIndexB = fromStopIndexB; + this.txFromAToB = false; + return build(); + } + + TestCase build() { + return new TestCase(description, stopIndexA, stopIndexB, txFromAToB, points); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestUtils.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestUtils.java new file mode 100644 index 00000000000..57695d2ac5c --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/TestUtils.java @@ -0,0 +1,87 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import static org.opentripplanner.routing.algorithm.transferoptimization.services.TransferGeneratorDummy.dummyTransferGenerator; + +import java.util.Collection; +import java.util.List; +import org.opentripplanner.raptor._data.RaptorTestConstants; +import org.opentripplanner.raptor._data.api.TestPathBuilder; +import org.opentripplanner.raptor._data.transit.TestTransitData; +import org.opentripplanner.raptor._data.transit.TestTripSchedule; +import org.opentripplanner.raptor.api.request.PassThroughPoint; +import org.opentripplanner.raptor.spi.RaptorCostCalculator; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.DefaultCostCalculator; +import org.opentripplanner.routing.algorithm.transferoptimization.model.TripToTripTransfer; +import org.opentripplanner.routing.algorithm.transferoptimization.model.costfilter.MinCostPathTailFilterFactory; +import org.opentripplanner.routing.algorithm.transferoptimization.services.OptimizePathDomainService; +import org.opentripplanner.routing.algorithm.transferoptimization.services.TestTransferBuilder; + +class TestUtils implements RaptorTestConstants { + + private static final int BOARD_COST_SEC = 0; + private static final int TRANSFER_COST_SEC = 0; + private static final double WAIT_RELUCTANCE = 1.0; + + private static final RaptorCostCalculator COST_CALCULATOR = new DefaultCostCalculator<>( + BOARD_COST_SEC, + TRANSFER_COST_SEC, + WAIT_RELUCTANCE, + null, + null + ); + + static TestPathBuilder pathBuilder() { + return new TestPathBuilder(TestTransitData.SLACK_PROVIDER, COST_CALCULATOR); + } + + static TripToTripTransfer tx( + TestTripSchedule fromTrip, + int fromStop, + TestTripSchedule toTrip, + int toStop, + WalkDurationForStopCombinations txCost + ) { + return tx(fromTrip, fromStop, toTrip, toStop, txCost.walkDuration(fromStop, toStop)); + } + + static TripToTripTransfer tx( + TestTripSchedule fromTrip, + int fromStop, + TestTripSchedule toTrip, + int toStop, + int txCost + ) { + return TestTransferBuilder.tx(fromTrip, fromStop, toTrip, toStop).walk(txCost).build(); + } + + static OptimizePathDomainService subject( + List passThroughPoints, + final List>... transfers + ) { + var filter = new MinCostPathTailFilterFactory(false, false).createFilter(); + filter = new PassThroughPathTailFilter<>(filter, passThroughPoints); + var generator = dummyTransferGenerator(transfers); + + return new OptimizePathDomainService<>( + generator, + COST_CALCULATOR, + TestTransitData.SLACK_PROVIDER, + null, + null, + 0.0, + filter, + (new RaptorTestConstants() {})::stopIndexToName + ); + } + + static T first(Collection c) { + return c.stream().findFirst().orElseThrow(); + } + + /** + * Remove stuff we do not care about, like the priority cost and times. + */ + static String pathFocus(String resultString) { + return resultString.replaceAll(" \\$\\d+pri]", "]").replaceAll(" \\d{2}:\\d{2}", ""); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/WalkDurationForStopCombinations.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/WalkDurationForStopCombinations.java new file mode 100644 index 00000000000..cfd8f6c4e53 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/model/passthrough/WalkDurationForStopCombinations.java @@ -0,0 +1,60 @@ +package org.opentripplanner.routing.algorithm.transferoptimization.model.passthrough; + +import java.util.BitSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.raptor.api.request.PassThroughPoint; + +/** + * This class is used to adjust the walk time - giving each path an unique generalized-cost. We want + * the paths which do NOT visit the pass-through points to have a lower generalized-cost than the + * "correct" paths. We do this by adding a small increasing cost to each paths ordered by the number + * of stops visited. Then we add a bigger cost for each stop in the transfer which is a + * pass-through-point. + */ +class WalkDurationForStopCombinations { + + private final int[] stopCost; + private final Map transferCost = new HashMap<>(); + + public WalkDurationForStopCombinations(int nStops) { + stopCost = IntUtils.intArray(nStops, 0); + } + + WalkDurationForStopCombinations withPassThroughPoints( + Collection points, + int passThroughPointExtraCost + ) { + var passThroughStops = new BitSet(); + points.stream().map(PassThroughPoint::asBitSet).forEach(passThroughStops::or); + + for (int i = 0; i < stopCost.length; i++) { + if (passThroughStops.get(i)) { + stopCost[i] += passThroughPointExtraCost; + } + } + return this; + } + + /** + * Use this method to add a small cost({@code costInWalkSec}) to a transfer. Give the path that + * visit the least number of stops the lowest value. In addition, the + * {@link #withPassThroughPoints(Collection, int)} method can be used to add a extra cost to all + * transfers containing a pass-through-point. + */ + WalkDurationForStopCombinations addTxCost(int fromStop, int toStop, int costInWalkSec) { + StopPair tx = new StopPair(fromStop, toStop); + transferCost.put(tx, costTxOnly(tx) + costInWalkSec); + return this; + } + + int walkDuration(int fromStop, int toStop) { + return costTxOnly(new StopPair(fromStop, toStop)) + stopCost[fromStop] + stopCost[toStop]; + } + + int costTxOnly(StopPair tx) { + return transferCost.getOrDefault(tx, 0); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceConstrainedTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceConstrainedTest.java index a85cf2e699d..d6725f4a425 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceConstrainedTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceConstrainedTest.java @@ -7,9 +7,7 @@ import static org.opentripplanner.model.transfer.TransferPriority.NOT_ALLOWED; import static org.opentripplanner.model.transfer.TransferPriority.PREFERRED; import static org.opentripplanner.model.transfer.TransferPriority.RECOMMENDED; -import static org.opentripplanner.routing.algorithm.transferoptimization.services.TestTransferBuilder.txConstrained; import static org.opentripplanner.routing.algorithm.transferoptimization.services.TransferGeneratorDummy.dummyTransferGenerator; -import static org.opentripplanner.routing.algorithm.transferoptimization.services.TransferGeneratorDummy.tx; import java.util.List; import org.junit.jupiter.api.Test; @@ -65,15 +63,15 @@ public class OptimizePathDomainServiceConstrainedTest implements RaptorTestConst TransferGenerator transfers = dummyTransferGenerator( List.of( - tx(txConstrained(trip1, STOP_B, trip2, STOP_C).priority(ALLOWED), D1m), - tx(txConstrained(trip1, STOP_C, trip2, STOP_D).priority(RECOMMENDED), D2m), - tx(txConstrained(trip1, STOP_D, trip2, STOP_E).priority(PREFERRED), D3m), - tx(txConstrained(trip1, STOP_E, trip2, STOP_F).guaranteed(), D4m), - tx(txConstrained(trip1, STOP_F, trip2, STOP_G).staySeated(), D5m), - tx(txConstrained(trip1, STOP_C, trip2, STOP_C).priority(NOT_ALLOWED)), - tx(txConstrained(trip1, STOP_D, trip2, STOP_D).priority(NOT_ALLOWED)), - tx(txConstrained(trip1, STOP_E, trip2, STOP_E).priority(NOT_ALLOWED)), - tx(txConstrained(trip1, STOP_F, trip2, STOP_F).priority(NOT_ALLOWED)) + TestTransferBuilder.tx(trip1, STOP_B, trip2, STOP_C).priority(ALLOWED).walk(D1m).build(), + TestTransferBuilder.tx(trip1, STOP_C, trip2, STOP_D).priority(RECOMMENDED).walk(D2m).build(), + TestTransferBuilder.tx(trip1, STOP_D, trip2, STOP_E).priority(PREFERRED).walk(D3m).build(), + TestTransferBuilder.tx(trip1, STOP_E, trip2, STOP_F).guaranteed().walk(D4m).build(), + TestTransferBuilder.tx(trip1, STOP_F, trip2, STOP_G).staySeated().walk(D5m).build(), + TestTransferBuilder.tx(trip1, STOP_C, trip2, STOP_C).priority(NOT_ALLOWED).build(), + TestTransferBuilder.tx(trip1, STOP_D, trip2, STOP_D).priority(NOT_ALLOWED).build(), + TestTransferBuilder.tx(trip1, STOP_E, trip2, STOP_E).priority(NOT_ALLOWED).build(), + TestTransferBuilder.tx(trip1, STOP_F, trip2, STOP_F).priority(NOT_ALLOWED).build() ) ); diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java index 192cdb84f11..8b7d589852e 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainServiceTest.java @@ -2,9 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.framework.time.TimeUtils.time; -import static org.opentripplanner.routing.algorithm.transferoptimization.services.TestTransferBuilder.txConstrained; +import static org.opentripplanner.routing.algorithm.transferoptimization.services.TestTransferBuilder.tx; import static org.opentripplanner.routing.algorithm.transferoptimization.services.TransferGeneratorDummy.dummyTransferGenerator; -import static org.opentripplanner.routing.algorithm.transferoptimization.services.TransferGeneratorDummy.tx; import java.util.Collection; import java.util.List; @@ -19,6 +18,7 @@ import org.opentripplanner.raptor.spi.RaptorSlackProvider; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.DefaultCostCalculator; import org.opentripplanner.routing.algorithm.transferoptimization.model.TransferWaitTimeCostCalculator; +import org.opentripplanner.routing.algorithm.transferoptimization.model.costfilter.MinCostPathTailFilterFactory; public class OptimizePathDomainServiceTest implements RaptorTestConstants { @@ -112,7 +112,9 @@ public void testTripWithOneTransfer() { .times("10:12 10:22 10:50") .build(); - var transfers = dummyTransferGenerator(List.of(tx(trip1, STOP_C, D30s, STOP_F, trip2))); + var transfers = dummyTransferGenerator( + List.of(tx(trip1, STOP_C, trip2, STOP_F).walk(D30s).build()) + ); // Path: Access ~ B ~ T1 ~ C ~ Walk 30s ~ D ~ T2 ~ E ~ Egress var original = pathBuilder() @@ -168,11 +170,11 @@ public void testPathWithThreeTripsAndMultiplePlacesToTransfer() { var transfers = dummyTransferGenerator( List.of( - tx(trip1, STOP_B, trip2), - tx(trip1, STOP_B, D30s, STOP_C, trip2), - tx(trip1, STOP_D, trip2) + tx(trip1, STOP_B, trip2).build(), + tx(trip1, STOP_B, trip2, STOP_C).walk(D30s).build(), + tx(trip1, STOP_D, trip2).build() ), - List.of(tx(trip2, STOP_D, D30s, STOP_E, trip3), tx(trip2, STOP_F, trip3)) + List.of(tx(trip2, STOP_D, trip3, STOP_E).walk(D30s).build(), tx(trip2, STOP_F, trip3).build()) ); var original = pathBuilder() @@ -243,8 +245,8 @@ public void testConstrainedTransferIsPreferred() { var transfers = dummyTransferGenerator( List.of( - tx(trip1, STOP_B, trip2), - tx(txConstrained(trip1, STOP_C, trip2, STOP_C).guaranteed()) + tx(trip1, STOP_B, trip2).build(), + tx(trip1, STOP_C, trip2, STOP_C).guaranteed().build() ) ); @@ -287,6 +289,11 @@ static OptimizePathDomainService subject( TransferGenerator generator, @Nullable TransferWaitTimeCostCalculator waitTimeCalculator ) { + var filter = new MinCostPathTailFilterFactory( + true, + waitTimeCalculator != null + ) + .createFilter(); return new OptimizePathDomainService<>( generator, COST_CALCULATOR, @@ -294,7 +301,7 @@ static OptimizePathDomainService subject( waitTimeCalculator, null, 0.0, - TransferOptimizedFilterFactory.filter(true, waitTimeCalculator != null), + filter, (new RaptorTestConstants() {})::stopIndexToName ); } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TestTransferBuilder.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TestTransferBuilder.java index 382d81df4e3..4155d46fe63 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TestTransferBuilder.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TestTransferBuilder.java @@ -1,11 +1,16 @@ package org.opentripplanner.routing.algorithm.transferoptimization.services; +import java.util.Objects; import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.model.transfer.ConstrainedTransfer; import org.opentripplanner.model.transfer.TransferConstraint; import org.opentripplanner.model.transfer.TransferPriority; import org.opentripplanner.model.transfer.TripTransferPoint; +import org.opentripplanner.raptor._data.transit.TestTransfer; +import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.routing.algorithm.transferoptimization.model.TripStopTime; +import org.opentripplanner.routing.algorithm.transferoptimization.model.TripToTripTransfer; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.timetable.Trip; @@ -16,17 +21,37 @@ @SuppressWarnings("UnusedReturnValue") public class TestTransferBuilder { - private final T fromTrip; - private final int fromStopIndex; - private final T toTrip; - private final int toStopIndex; - private final TransferConstraint.Builder constraint = TransferConstraint.of(); + private T fromTrip; + private int fromStopIndex = RaptorConstants.NOT_SET; + private T toTrip; + private int toStopIndex = RaptorConstants.NOT_SET; - private TestTransferBuilder(T fromTrip, int fromStopIndex, T toTrip, int toStopIndex) { - this.fromTrip = fromTrip; - this.fromStopIndex = fromStopIndex; - this.toTrip = toTrip; - this.toStopIndex = toStopIndex; + // We set the default walk time to zero - it is not relevant for many tests and zero is easy + private int walkDurationSec = 0; + private TransferConstraint.Builder constraint = null; + + public static TestTransferBuilder tx(T fromTrip, T toTrip) { + return new TestTransferBuilder().fromTrip(fromTrip).toTrip(toTrip); + } + + /** + * Set all required parameters for a transfer. The walk duration is set to zero. + */ + public static TestTransferBuilder tx( + T fromTrip, + int fromStopIndex, + T toTrip, + int toStopIndex + ) { + return tx(fromTrip, toTrip).fromStopIndex(fromStopIndex).toStopIndex(toStopIndex); + } + + public static TestTransferBuilder tx( + T fromTrip, + int sameStopIndex, + T toTrip + ) { + return tx(fromTrip, sameStopIndex, toTrip, sameStopIndex); } public static TestTransferBuilder txConstrained( @@ -35,53 +60,109 @@ public static TestTransferBuilder txConstraine T toTrip, int toStopIndex ) { - return new TestTransferBuilder<>(fromTrip, fromStopIndex, toTrip, toStopIndex); + var builder = tx(fromTrip, fromStopIndex, toTrip, toStopIndex); + // Make sure the constraint is initialized; hence an object generated in the build step. + // If none of the constraints are set this still generates a constraint instance, which + // should behave like a regular transfer, but is not the same structure. + builder.constraint(); + return builder; } - public T getFromTrip() { + public T fromTrip() { return fromTrip; } - public int getFromStopIndex() { + public TestTransferBuilder fromTrip(T fromTrip) { + this.fromTrip = fromTrip; + return this; + } + + public int fromStopIndex() { return fromStopIndex; } - public T getToTrip() { + public TestTransferBuilder fromStopIndex(int fromStopIndex) { + this.fromStopIndex = fromStopIndex; + return this; + } + + public T toTrip() { return toTrip; } - public int getToStopIndex() { + public TestTransferBuilder toTrip(T toTrip) { + this.toTrip = toTrip; + return this; + } + + public int toStopIndex() { return toStopIndex; } + public TestTransferBuilder toStopIndex(int toStopIndex) { + this.toStopIndex = toStopIndex; + return this; + } + + /** + * Walk duration in seconds + */ + public int walk() { + return walkDurationSec; + } + + public TestTransferBuilder walk(int walkDurationSec) { + this.walkDurationSec = walkDurationSec; + return this; + } + public TestTransferBuilder staySeated() { - this.constraint.staySeated(); + this.constraint().staySeated(); return this; } public TestTransferBuilder guaranteed() { - this.constraint.guaranteed(); + this.constraint().guaranteed(); return this; } public TestTransferBuilder maxWaitTime(int maxWaitTime) { - this.constraint.maxWaitTime(maxWaitTime); + this.constraint().maxWaitTime(maxWaitTime); return this; } public TestTransferBuilder priority(TransferPriority priority) { - this.constraint.priority(priority); + this.constraint().priority(priority); return this; } - public ConstrainedTransfer build() { - if (fromTrip == null) { - throw new NullPointerException(); - } - if (toTrip == null) { - throw new NullPointerException(); - } + public TripToTripTransfer build() { + validateFromTo(); + validateWalkDurationSec(); + + var pathTransfer = fromStopIndex == toStopIndex + ? null + : TestTransfer.transfer(toStopIndex, walkDurationSec); + return new TripToTripTransfer<>( + departure(fromTrip, fromStopIndex), + arrival(toTrip, toStopIndex), + pathTransfer, + buildConstrainedTransfer() + ); + } + + private static Trip createDummyTrip(T trip) { + // Set an uniq id: pattern + the first stop departure time + return TransitModelForTest + .trip(trip.pattern().debugInfo() + ":" + TimeUtils.timeToStrCompact(trip.departure(0))) + .build(); + } + + private ConstrainedTransfer buildConstrainedTransfer() { + if (constraint == null) { + return null; + } int fromStopPos = fromTrip.pattern().findStopPositionAfter(0, fromStopIndex); int toStopPos = toTrip.pattern().findStopPositionAfter(0, toStopIndex); @@ -93,10 +174,35 @@ public ConstrainedTransfer build() { ); } - private static Trip createDummyTrip(T trip) { - // Set a uniq id: pattern + the first stop departure time - return TransitModelForTest - .trip(trip.pattern().debugInfo() + ":" + TimeUtils.timeToStrCompact(trip.departure(0))) - .build(); + private static TripStopTime departure(T trip, int stopIndex) { + return TripStopTime.departure(trip, trip.pattern().findStopPositionAfter(0, stopIndex)); + } + + private static TripStopTime arrival(T trip, int stopIndex) { + return TripStopTime.arrival(trip, trip.pattern().findStopPositionAfter(0, stopIndex)); + } + + private void validateFromTo() { + Objects.requireNonNull(fromTrip); + Objects.requireNonNull(toTrip); + if (fromStopIndex < 0) { + throw new IllegalStateException(); + } + if (toStopIndex < 0) { + throw new IllegalStateException(); + } + } + + private void validateWalkDurationSec() { + if (walkDurationSec < 0) { + throw new IllegalStateException(); + } + } + + private TransferConstraint.Builder constraint() { + if (constraint == null) { + constraint = TransferConstraint.of(); + } + return constraint; } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransferGeneratorDummy.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransferGeneratorDummy.java index af29caafb2b..4392e89a83d 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransferGeneratorDummy.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransferGeneratorDummy.java @@ -2,12 +2,9 @@ import java.util.Arrays; import java.util.List; -import org.opentripplanner.raptor._data.transit.TestTransfer; import org.opentripplanner.raptor._data.transit.TestTransitData; import org.opentripplanner.raptor._data.transit.TestTripSchedule; -import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.TransitPathLeg; -import org.opentripplanner.routing.algorithm.transferoptimization.model.TripStopTime; import org.opentripplanner.routing.algorithm.transferoptimization.model.TripToTripTransfer; /** @@ -15,45 +12,8 @@ */ public class TransferGeneratorDummy { - private static final int D0s = 0; - - /** Transfer from trip & stop, walk, to stop & trip */ - public static TripToTripTransfer tx( - TestTripSchedule fromTrip, - int fromStop, - int walkDuration, - int toStop, - TestTripSchedule toTrip - ) { - return createTripToTripTransfer(fromTrip, fromStop, walkDuration, toStop, toTrip); - } - - /** Transfer from trip via same stop to trip */ - public static TripToTripTransfer tx( - TestTripSchedule fromTrip, - int sameStop, - TestTripSchedule toTrip - ) { - return createTripToTripTransfer(fromTrip, sameStop, D0s, sameStop, toTrip); - } - - /** Transfer from transfer constraints - same stop */ - public static TripToTripTransfer tx( - TestTransferBuilder builder - ) { - return createTripToTripTransfer(builder, D0s); - } - - /** Transfer from transfer constraints - with walking */ - public static TripToTripTransfer tx( - TestTransferBuilder builder, - int walkDuration - ) { - return createTripToTripTransfer(builder, walkDuration); - } - @SafeVarargs - static TransferGenerator dummyTransferGenerator( + public static TransferGenerator dummyTransferGenerator( final List>... transfers ) { return new TransferGenerator<>(null, new TestTransitData()) { @@ -65,47 +25,4 @@ public List>> findAllPossibleTransfers } }; } - - /* private methods */ - - private static TripToTripTransfer createTripToTripTransfer( - TestTripSchedule fromTrip, - int fromStop, - int walkDuration, - int toStop, - TestTripSchedule toTrip - ) { - var pathTransfer = fromStop == toStop ? null : TestTransfer.transfer(toStop, walkDuration); - - return new TripToTripTransfer<>( - departure(fromTrip, fromStop), - arrival(toTrip, toStop), - pathTransfer, - null - ); - } - - private static TripToTripTransfer createTripToTripTransfer( - TestTransferBuilder builder, - int walkDuration - ) { - int fromStop = builder.getFromStopIndex(); - int toStop = builder.getToStopIndex(); - var pathTransfer = fromStop == toStop ? null : TestTransfer.transfer(toStop, walkDuration); - - return new TripToTripTransfer<>( - departure(builder.getFromTrip(), builder.getFromStopIndex()), - arrival(builder.getToTrip(), builder.getToStopIndex()), - pathTransfer, - builder.build() - ); - } - - private static TripStopTime departure(T trip, int stopIndex) { - return TripStopTime.departure(trip, trip.pattern().findStopPositionAfter(0, stopIndex)); - } - - private static TripStopTime arrival(T trip, int stopIndex) { - return TripStopTime.arrival(trip, trip.pattern().findStopPositionAfter(0, stopIndex)); - } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelectorTest.java b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelectorTest.java index fee23864fcf..5e92adc4d9e 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelectorTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/transferoptimization/services/TransitPathLegSelectorTest.java @@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collection; -import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -15,13 +14,13 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.path.EgressPathLeg; import org.opentripplanner.raptor.api.path.TransitPathLeg; -import org.opentripplanner.raptor.spi.BoardAndAlightTime; import org.opentripplanner.raptor.spi.DefaultSlackProvider; import org.opentripplanner.raptor.spi.RaptorCostCalculator; import org.opentripplanner.raptor.spi.RaptorSlackProvider; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.DefaultCostCalculator; -import org.opentripplanner.routing.algorithm.transferoptimization.model.MinCostFilterChain; import org.opentripplanner.routing.algorithm.transferoptimization.model.OptimizedPathTail; +import org.opentripplanner.routing.algorithm.transferoptimization.model.PathTailFilter; +import org.opentripplanner.routing.algorithm.transferoptimization.model.costfilter.MinCostPathTailFilterFactory; public class TransitPathLegSelectorTest implements RaptorTestConstants { @@ -39,13 +38,17 @@ public class TransitPathLegSelectorTest implements RaptorTestConstants { null ); - public static final MinCostFilterChain> FILTER_CHAIN = new MinCostFilterChain<>( - List.of(OptimizedPathTail::generalizedCost) + public static final PathTailFilter FILTER_CHAIN = MinCostPathTailFilterFactory.ofCostFunction( + OptimizedPathTail::generalizedCost ); - private final int T10_00 = TimeUtils.time("10:00"); - private final int T10_20 = TimeUtils.time("10:20"); - private final int T10_40 = TimeUtils.time("10:40"); + private final int STOP_TIME_ONE = TimeUtils.time("10:00"); + private final int STOP_TIME_TWO = TimeUtils.time("10:20"); + private final int STOP_TIME_THREE = TimeUtils.time("10:40"); + + private final int STOP_POS_ONE = 0; + private final int STOP_POS_TWO = 1; + private final int STOP_POS_THREE = 2; private final OptimizedPathTail pathTail = new OptimizedPathTail<>( SLACK_PROVIDER, @@ -60,10 +63,10 @@ public class TransitPathLegSelectorTest implements RaptorTestConstants { private final TestTripSchedule TRIP = TestTripSchedule .schedule() .pattern("L1", STOP_A, STOP_C, STOP_E) - .times(T10_00, T10_20, T10_40) + .times(STOP_TIME_ONE, STOP_TIME_TWO, STOP_TIME_THREE) .build(); - private final int EGRESS_START = T10_40 + D1m; + private final int EGRESS_START = STOP_TIME_THREE + D1m; private final int EGRESS_END = EGRESS_START + D5m; @Test @@ -78,10 +81,10 @@ public void testOneElementIsReturnedIfTimeLimitThresholdIsPassed() { var subject = new TransitPathLegSelector<>(FILTER_CHAIN, Set.of(leg)); - var result = subject.next(T10_40); + var result = subject.next(STOP_POS_THREE); assertTrue(result.isEmpty(), result.toString()); - result = subject.next(T10_40 - 1); + result = subject.next(STOP_POS_TWO); assertFalse(result.isEmpty(), result.toString()); } @@ -92,25 +95,14 @@ public void testTwoPathLegs() { var subject = new TransitPathLegSelector<>(FILTER_CHAIN, Set.of(leg1, leg2)); - var result = subject.next(T10_40); + var result = subject.next(STOP_POS_THREE); assertTrue(result.isEmpty(), result.toString()); - result = subject.next(T10_40 - 1); - assertEquals("BUS L1 10:00 10:40", firstRide(result)); - assertEquals(result.size(), 1); - - // No change yet - result = subject.next(T10_20); + result = subject.next(STOP_POS_TWO); assertEquals("BUS L1 10:00 10:40", firstRide(result)); assertEquals(result.size(), 1); - // Get next - result = subject.next(T10_20 - 1); - assertEquals("BUS L1 10:00 10:20", firstRide(result)); - assertEquals(result.size(), 1); - - // Same as previous - result = subject.next(0); + result = subject.next(STOP_POS_ONE); assertEquals("BUS L1 10:00 10:20", firstRide(result)); assertEquals(result.size(), 1); } @@ -132,13 +124,12 @@ private TransitPathLeg transitLeg(int egressStop) { walk.generalizedCost() ); int toTime = TRIP.arrival(TRIP.findArrivalStopPosition(Integer.MAX_VALUE, egressStop)); - var times = BoardAndAlightTime.create(TRIP, STOP_A, T10_00, egressStop, toTime); - int cost = 100 * (T10_40 - T10_00); + int cost = 100 * (STOP_TIME_THREE - STOP_TIME_ONE); return new TransitPathLeg<>( TRIP, - T10_00, + STOP_TIME_ONE, toTime, - TRIP.findDepartureStopPosition(T10_00, STOP_A), + TRIP.findDepartureStopPosition(STOP_TIME_ONE, STOP_A), TRIP.findArrivalStopPosition(toTime, egressStop), null, cost,