-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6182 from entur/otp2_sorlandsbanen
Combine two multi-criteria searches in Raptor
- Loading branch information
Showing
32 changed files
with
737 additions
and
342 deletions.
There are no files selected for viewing
82 changes: 82 additions & 0 deletions
82
application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/CoachCostCalculator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package org.opentripplanner.ext.sorlandsbanen; | ||
|
||
import org.opentripplanner.raptor.api.model.RaptorAccessEgress; | ||
import org.opentripplanner.raptor.api.model.RaptorTransferConstraint; | ||
import org.opentripplanner.raptor.spi.RaptorCostCalculator; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; | ||
import org.opentripplanner.transit.model.basic.TransitMode; | ||
|
||
|
||
/** | ||
* This cost calculator increases the cost on mode coach by adding an extra reluctance. The | ||
* reluctance is hardcoded in this class and cannot be configured. | ||
*/ | ||
class CoachCostCalculator<T extends TripSchedule> implements RaptorCostCalculator<T> { | ||
|
||
private static final int EXTRA_RELUCTANCE_ON_COACH = RaptorCostConverter.toRaptorCost(0.6); | ||
|
||
private final RaptorCostCalculator<T> delegate; | ||
|
||
CoachCostCalculator(RaptorCostCalculator<T> delegate) { | ||
this.delegate = delegate; | ||
} | ||
|
||
@Override | ||
public int boardingCost( | ||
boolean firstBoarding, | ||
int prevArrivalTime, | ||
int boardStop, | ||
int boardTime, | ||
T trip, | ||
RaptorTransferConstraint transferConstraints | ||
) { | ||
return delegate.boardingCost( | ||
firstBoarding, | ||
prevArrivalTime, | ||
boardStop, | ||
boardTime, | ||
trip, | ||
transferConstraints | ||
); | ||
} | ||
|
||
@Override | ||
public int onTripRelativeRidingCost(int boardTime, T tripScheduledBoarded) { | ||
return delegate.onTripRelativeRidingCost(boardTime, tripScheduledBoarded); | ||
} | ||
|
||
@Override | ||
public int transitArrivalCost( | ||
int boardCost, | ||
int alightSlack, | ||
int transitTime, | ||
T trip, | ||
int toStop | ||
) { | ||
int cost = delegate.transitArrivalCost(boardCost, alightSlack, transitTime, trip, toStop); | ||
|
||
// This is a bit ugly, since it relies on the fact that the 'transitReluctanceFactorIndex' | ||
// returns the 'route.getMode().ordinal()' | ||
if(trip.transitReluctanceFactorIndex() == TransitMode.COACH.ordinal()) { | ||
cost += transitTime * EXTRA_RELUCTANCE_ON_COACH; | ||
} | ||
return cost; | ||
} | ||
|
||
@Override | ||
public int waitCost(int waitTimeInSeconds) { | ||
return delegate.waitCost(waitTimeInSeconds); | ||
} | ||
|
||
@Override | ||
public int calculateRemainingMinCost(int minTravelTime, int minNumTransfers, int fromStop) { | ||
return delegate.calculateRemainingMinCost(minTravelTime, minNumTransfers, fromStop); | ||
} | ||
|
||
@Override | ||
public int costEgress(RaptorAccessEgress egress) { | ||
return delegate.costEgress(egress); | ||
} | ||
|
||
} |
54 changes: 54 additions & 0 deletions
54
application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/MergePaths.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package org.opentripplanner.ext.sorlandsbanen; | ||
|
||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.function.BiFunction; | ||
import org.opentripplanner.raptor.api.model.RaptorTripSchedule; | ||
import org.opentripplanner.raptor.api.path.PathLeg; | ||
import org.opentripplanner.raptor.api.path.RaptorPath; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset; | ||
import org.opentripplanner.transit.model.basic.TransitMode; | ||
|
||
/** | ||
* Strategy for merging the main results and the extra rail results from Sorlandsbanen. | ||
* Everything from the main result is kept, and any additional rail results from the alternative | ||
* search are added. | ||
*/ | ||
class MergePaths<T extends RaptorTripSchedule> implements BiFunction<Collection<RaptorPath<T>>, Collection<RaptorPath<T>>, Collection<RaptorPath<T>>> { | ||
|
||
@Override | ||
public Collection<RaptorPath<T>> apply(Collection<RaptorPath<T>> main, Collection<RaptorPath<T>> alternatives) { | ||
Map<PathKey, RaptorPath<T>> result = new HashMap<>(); | ||
addAllToMap(result, main); | ||
addRailToMap(result, alternatives); | ||
return result.values(); | ||
} | ||
|
||
private void addAllToMap(Map<PathKey, RaptorPath<T>> map, Collection<RaptorPath<T>> paths) { | ||
for (var it : paths) { | ||
map.put(new PathKey(it), it); | ||
} | ||
} | ||
|
||
private void addRailToMap(Map<PathKey, RaptorPath<T>> map, Collection<RaptorPath<T>> paths) { | ||
for (var it : paths) { | ||
if (hasRail(it)) { | ||
// Avoid replacing an existing value if it exists, there might be minor differences in the | ||
// path, in which case we want to keep the main result. | ||
map.computeIfAbsent(new PathKey(it), k -> it); | ||
} | ||
} | ||
} | ||
|
||
private static boolean hasRail(RaptorPath<?> path) { | ||
return path | ||
.legStream() | ||
.filter(PathLeg::isTransitLeg) | ||
.anyMatch(leg -> { | ||
var trip = (TripScheduleWithOffset) leg.asTransitLeg().trip(); | ||
var mode = trip.getOriginalTripPattern().getMode(); | ||
return mode == TransitMode.RAIL; | ||
}); | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
application/src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package org.opentripplanner.ext.sorlandsbanen; | ||
|
||
import org.opentripplanner.raptor.api.path.PathLeg; | ||
import org.opentripplanner.raptor.api.path.RaptorPath; | ||
|
||
|
||
/** | ||
* The purpose of this class is to create a key to be able to compare paths so duplicate results | ||
* can be ignored. | ||
* <p> | ||
* Creating a good key for a path is not easy. For example, should a small variation in the street | ||
* routing for an access/egress leg count as a significant difference? The solution here is | ||
* straightforward. It creates a hash of the access-, egress- and transit-legs in the path, | ||
* ignoring transfer legs. This approach may drop valid results if there are hash collisions, | ||
* but since this is a Sandbox module and the investment in this code is minimal, we will accept | ||
* the risk. | ||
*/ | ||
final class PathKey { | ||
|
||
private final int hash; | ||
|
||
PathKey(RaptorPath<?> path) { | ||
this.hash = hash(path); | ||
} | ||
|
||
private static int hash(RaptorPath<?> path) { | ||
int result = 1; | ||
|
||
PathLeg<?> leg = path.accessLeg(); | ||
|
||
while (!leg.isEgressLeg()) { | ||
result = 31 * result + leg.toStop(); | ||
result = 31 * result + leg.toTime(); | ||
|
||
if (leg.isTransitLeg()) { | ||
result = 31 * result + leg.asTransitLeg().trip().pattern().debugInfo().hashCode(); | ||
} | ||
leg = leg.nextLeg(); | ||
} | ||
result = 31 * result + leg.toTime(); | ||
|
||
return result; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) { | ||
return true; | ||
} | ||
if (o.getClass() != PathKey.class) { | ||
return false; | ||
} | ||
return hash == ((PathKey) o).hash; | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return hash; | ||
} | ||
} |
106 changes: 106 additions & 0 deletions
106
...cation/src/ext/java/org/opentripplanner/ext/sorlandsbanen/SorlandsbanenNorwayService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package org.opentripplanner.ext.sorlandsbanen; | ||
|
||
import java.util.Collection; | ||
import java.util.function.BiFunction; | ||
import javax.annotation.Nullable; | ||
import org.opentripplanner.framework.geometry.WgsCoordinate; | ||
import org.opentripplanner.model.GenericLocation; | ||
import org.opentripplanner.raptor.api.path.RaptorPath; | ||
import org.opentripplanner.raptor.spi.ExtraMcRouterSearch; | ||
import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgresses; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; | ||
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData; | ||
import org.opentripplanner.routing.api.request.RouteRequest; | ||
import org.opentripplanner.transit.model.framework.FeedScopedId; | ||
import org.opentripplanner.transit.model.site.StopLocation; | ||
|
||
/** | ||
* This service is responsible for producing results with rail for the south of Norway. The rail | ||
* line is called "Sørlandsbanen". This rail line is slow and goes inland far from where people | ||
* live. Despite this, people and the operator want to show it in the results for log travel along | ||
* the southern part of Norway where it is an option. Tuning the search has proven to be | ||
* challenging. It is solved here by doing two searches. One normal search and one where the rail | ||
* is given a big cost advantage over coach. If train results are found in the second search, then | ||
* it is added to the results of the first search. Everything found in the first search is always | ||
* returned. | ||
*/ | ||
public class SorlandsbanenNorwayService { | ||
|
||
private static final double SOUTH_BORDER_LIMIT = 59.1; | ||
private static final int MIN_DISTANCE_LIMIT = 120_000; | ||
|
||
|
||
@Nullable | ||
public ExtraMcRouterSearch<TripSchedule> createExtraMcRouterSearch(RouteRequest request, AccessEgresses accessEgresses, TransitLayer transitLayer) { | ||
WgsCoordinate from = findStopCoordinate( | ||
request.from(), | ||
accessEgresses.getAccesses(), | ||
transitLayer | ||
); | ||
WgsCoordinate to = findStopCoordinate(request.to(), accessEgresses.getEgresses(), transitLayer); | ||
|
||
if (from.isNorthOf(SOUTH_BORDER_LIMIT) && to.isNorthOf(SOUTH_BORDER_LIMIT)) { | ||
return null; | ||
} | ||
|
||
double distance = from.distanceTo(to); | ||
if (distance < MIN_DISTANCE_LIMIT) { | ||
return null; | ||
} | ||
|
||
return new ExtraMcRouterSearch<>() { | ||
@Override | ||
public RaptorTransitDataProvider<TripSchedule> createTransitDataAlternativeSearch(RaptorTransitDataProvider<TripSchedule> transitDataMainSearch) { | ||
return new RaptorRoutingRequestTransitData( | ||
(RaptorRoutingRequestTransitData)transitDataMainSearch, | ||
new CoachCostCalculator<>(transitDataMainSearch.multiCriteriaCostCalculator()) | ||
); | ||
} | ||
|
||
@Override | ||
public BiFunction<Collection<RaptorPath<TripSchedule>>, Collection<RaptorPath<TripSchedule>>, Collection<RaptorPath<TripSchedule>>> merger() { | ||
return new MergePaths<>(); | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Find a coordinate matching the given location, in order: | ||
* - First return the coordinate of the location if it exists. | ||
* - Then loop through the access/egress stops and try to find the | ||
* stop or station given by the location id, return the stop/station coordinate. | ||
* - Return the stop coordinate of the first access/egress in the list. | ||
*/ | ||
@SuppressWarnings("ConstantConditions") | ||
private static WgsCoordinate findStopCoordinate( | ||
GenericLocation location, | ||
Collection<? extends RoutingAccessEgress> accessEgress, | ||
TransitLayer transitLayer | ||
) { | ||
if (location.lat != null) { | ||
return new WgsCoordinate(location.lat, location.lng); | ||
} | ||
|
||
StopLocation firstStop = null; | ||
for (RoutingAccessEgress it : accessEgress) { | ||
StopLocation stop = transitLayer.getStopByIndex(it.stop()); | ||
if (stop.getId().equals(location.stopId)) { | ||
return stop.getCoordinate(); | ||
} | ||
if (idIsParentStation(stop, location.stopId)) { | ||
return stop.getParentStation().getCoordinate(); | ||
} | ||
if (firstStop == null) { | ||
firstStop = stop; | ||
} | ||
} | ||
return firstStop.getCoordinate(); | ||
} | ||
|
||
private static boolean idIsParentStation(StopLocation stop, FeedScopedId pId) { | ||
return stop.getParentStation() != null && stop.getParentStation().getId().equals(pId); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...c/ext/java/org/opentripplanner/ext/sorlandsbanen/configure/SorlandsbanenNorwayModule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.opentripplanner.ext.sorlandsbanen.configure; | ||
|
||
import dagger.Module; | ||
import dagger.Provides; | ||
import javax.annotation.Nullable; | ||
import org.opentripplanner.ext.sorlandsbanen.SorlandsbanenNorwayService; | ||
import org.opentripplanner.framework.application.OTPFeature; | ||
|
||
@Module | ||
public class SorlandsbanenNorwayModule { | ||
|
||
@Provides | ||
@Nullable | ||
SorlandsbanenNorwayService providesSorlandsbanenNorwayService() { | ||
return OTPFeature.Sorlandsbanen.isOn() ? new SorlandsbanenNorwayService() : null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.