Skip to content

Commit

Permalink
Merge pull request #6182 from entur/otp2_sorlandsbanen
Browse files Browse the repository at this point in the history
Combine two multi-criteria searches in Raptor
  • Loading branch information
t2gran authored Nov 5, 2024
2 parents c6b7ef9 + eb55887 commit ee53e50
Show file tree
Hide file tree
Showing 32 changed files with 737 additions and 342 deletions.
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);
}

}
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;
});
}
}
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;
}
}
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);
}
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ public enum OTPFeature {
SandboxAPIGeocoder(false, true, "Enable the Geocoder API."),
SandboxAPIMapboxVectorTilesApi(false, true, "Enable Mapbox vector tiles API."),
SandboxAPIParkAndRideApi(false, true, "Enable park-and-ride endpoint."),
Sorlandsbanen(
false,
true,
"Include train Sørlandsbanen in results when searching in south of Norway. Only relevant in Norway."
),
TransferAnalyzer(false, true, "Analyze transfers during graph build.");

private static final Object TEST_LOCK = new Object();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,24 @@ public WgsCoordinate roundToApproximate100m() {
return new WgsCoordinate(lat, lng);
}

/**
* Compute a fairly accurate distance between two coordinates. Use the fast version in
* {@link SphericalDistanceLibrary} if many computations are needed. Return the distance in
* meters between the two coordinates.
*/
public double distanceTo(WgsCoordinate other) {
return SphericalDistanceLibrary.distance(
this.latitude,
this.longitude,
other.latitude,
other.longitude
);
}

public boolean isNorthOf(double latitudeBorder) {
return latitude > latitudeBorder;
}

/**
* Return a new coordinate that is moved an approximate number of meters east.
*/
Expand Down
Loading

0 comments on commit ee53e50

Please sign in to comment.