Skip to content

Commit

Permalink
feature: Add sandbox feature Sorlandsbanen.
Browse files Browse the repository at this point in the history
  • Loading branch information
t2gran committed Oct 21, 2024
1 parent a20988a commit e416a17
Show file tree
Hide file tree
Showing 18 changed files with 441 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.opentripplanner.ext.sorlandsbanen;

import org.opentripplanner.raptor.api.model.RaptorAccessEgress;
import org.opentripplanner.raptor.api.model.RaptorTransferConstraint;
import org.opentripplanner.raptor.spi.RaptorCostCalculator;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter;
import org.opentripplanner.transit.model.basic.TransitMode;


/**
* This cost calculator increases the cost on mode coach by adding an extra reluctance. The
* reluctance is hardcoded in this class and cannot be configured.
*/
class CoachCostCalculator<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);
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,103 @@
package org.opentripplanner.ext.sorlandsbanen;

import java.util.Collection;
import java.util.function.BiFunction;
import org.opentripplanner.framework.geometry.WgsCoordinate;
import org.opentripplanner.model.GenericLocation;
import org.opentripplanner.raptor.api.path.RaptorPath;
import org.opentripplanner.raptor.spi.ExtraMcRouterSearch;
import org.opentripplanner.raptor.spi.RaptorTransitDataProvider;
import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgresses;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.site.StopLocation;

/**
* This is basically a big hack to produce results containing "Sørlandsbanen" in Norway. This
* railroad line is slow and goes inland fare from where people live. Despite this, people and the
* operator want to show it in the results for log travel along the southern part of Norway where
* ii is an option. Tuning the search has proven to be challenging. It is solved here by doing
* two searches. One normal search and one where the rail is given a big cost advantage over coach.
* If train results are found in the second search, then it is added to the results of the first
* search. Everything found in the first search is always returned.
*/
public class EnturSorlandsbanenService {

private static final double SOUTH_BOARDER_LIMIT = 59.1;
private static final int MIN_DISTANCE_LIMIT = 120_000;


public ExtraMcRouterSearch<TripSchedule> createMcRouterFactory(RouteRequest request, AccessEgresses accessEgresses, TransitLayer transitLayer) {
WgsCoordinate from = findStopCoordinate(
request.from(),
accessEgresses.getAccesses(),
transitLayer
);
WgsCoordinate to = findStopCoordinate(request.to(), accessEgresses.getEgresses(), transitLayer);

if (from.latitude() > SOUTH_BOARDER_LIMIT && to.latitude() > SOUTH_BOARDER_LIMIT) {
return null;
}

double distance = from.distanceTo(to);
if (distance < MIN_DISTANCE_LIMIT) {
return null;
}

return new ExtraMcRouterSearch<>() {
@Override
public RaptorTransitDataProvider<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 fist stop in the access/egress list coordinate.
*/
@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,52 @@
package org.opentripplanner.ext.sorlandsbanen;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import org.opentripplanner.raptor.api.model.RaptorTripSchedule;
import org.opentripplanner.raptor.api.path.PathLeg;
import org.opentripplanner.raptor.api.path.RaptorPath;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset;
import org.opentripplanner.transit.model.basic.TransitMode;

/**
* Strategy for merging the main results and the extra rail results from Sorlandsbanen.
* Everything from the main result is kept, and any additional rail results from the alternative
* search are added.
*/
class MergePaths<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>> railAlternatives) {
Map<PathKey, RaptorPath<T>> result = new HashMap<>();
addAllToMap(result, main);
addRailToMap(result, railAlternatives);
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)) {
map.put(new PathKey(it), it);
}
}
}

private static boolean hasRail(RaptorPath<?> path) {
return path
.legStream()
.filter(PathLeg::isTransitLeg)
.anyMatch(leg -> {
var trip = (TripScheduleWithOffset) leg.asTransitLeg().trip();
var mode = trip.getOriginalTripPattern().getMode();
return mode == TransitMode.RAIL;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.opentripplanner.ext.sorlandsbanen;

import org.opentripplanner.raptor.api.path.PathLeg;
import org.opentripplanner.raptor.api.path.RaptorPath;


/**
* Uses a hash to create a key for access, egress and transit legs in a path. Transfers
* are not included. The key is used to exclude duplicates. This approach may drop valid results
* when there is a hash collision, but this whole sandbox feature is a hack - so we can tolerate
* this here.
*/
final class PathKey {

private final int hash;

PathKey(RaptorPath<?> path) {
this.hash = hash(path);
}

private static int hash(RaptorPath<?> path) {
if (path == null) {
return 0;
}
int result = 1;

PathLeg<?> leg = path.accessLeg();

while (!leg.isEgressLeg()) {
result = 31 * result + leg.toStop();
result = 31 * result + leg.toTime();

if (leg.isTransitLeg()) {
result = 31 * result + leg.asTransitLeg().trip().pattern().debugInfo().hashCode();
}
leg = leg.nextLeg();
}
result = 31 * result + leg.toTime();

return result;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o.getClass() != PathKey.class) {
return false;
}
return hash == ((PathKey) o).hash;
}

@Override
public int hashCode() {
return hash;
}
}
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.EnturSorlandsbanenService;
import org.opentripplanner.framework.application.OTPFeature;

@Module
public class EnturSorlandsbanenModule {

@Provides
@Nullable
EnturSorlandsbanenService providesEnturSorlandsbanenService() {
return OTPFeature.Sorlandsbanen.isOn() ? new EnturSorlandsbanenService() : 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 searchig in south of Norway. Only relevant in Norway."
),
TransferAnalyzer(false, true, "Analyze transfers during graph build.");

private static final Object TEST_LOCK = new Object();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.IntStream;
import javax.annotation.Nullable;
import org.opentripplanner.ext.ridehailing.RideHailingAccessShifter;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.raptor.RaptorService;
import org.opentripplanner.raptor.api.path.RaptorPath;
import org.opentripplanner.raptor.api.response.RaptorResponse;
import org.opentripplanner.raptor.spi.ExtraMcRouterSearch;
import org.opentripplanner.routing.algorithm.mapping.RaptorPathToItineraryMapper;
import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressPenaltyDecorator;
import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressRouter;
Expand Down Expand Up @@ -142,7 +144,10 @@ private TransitRouterResult route() {
);

// Route transit
var raptorService = new RaptorService<>(serverContext.raptorConfig());
var raptorService = new RaptorService<>(
serverContext.raptorConfig(),
createMcRouterFactory(accessEgresses, transitLayer)
);
var transitResponse = raptorService.route(raptorRequest, requestTransitDataProvider);

checkIfTransitConnectionExists(transitResponse);
Expand Down Expand Up @@ -387,4 +392,21 @@ private IntStream listStopIndexes(FeedScopedId stopLocationId) {
}
return stops.stream().mapToInt(StopLocation::getIndex);
}

/**
* An optional factory for creating a decorator around the multi-criteria RangeRaptor instance.
*/
@Nullable
private ExtraMcRouterSearch<TripSchedule> createMcRouterFactory(
AccessEgresses accessEgresses,
TransitLayer transitLayer
) {
if (OTPFeature.Sorlandsbanen.isOff()) {
return null;
}
var service = serverContext.enturSorlandsbanenService();
return service == null
? null
: service.createMcRouterFactory(request, accessEgresses, transitLayer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ public RaptorRoutingRequestTransitData(
);
}

public RaptorRoutingRequestTransitData(
RaptorRoutingRequestTransitData original,
RaptorCostCalculator<TripSchedule> newCostCalculator
) {
this.transitLayer = original.transitLayer;
this.transitSearchTimeZero = original.transitSearchTimeZero;
this.activeTripPatternsPerStop = original.activeTripPatternsPerStop;
this.patternIndex = original.patternIndex;
this.transferIndex = original.transferIndex;
this.transferService = original.transferService;
this.constrainedTransfers = original.constrainedTransfers;
this.validTransitDataStartTime = original.validTransitDataStartTime;
this.validTransitDataEndTime = original.validTransitDataEndTime;
this.generalizedCostCalculator = newCostCalculator;
this.slackProvider = original.slackProvider();
}

@Override
public Iterator<RaptorTransfer> getTransfersFromStop(int stopIndex) {
return transferIndex.getForwardTransfers(stopIndex).iterator();
Expand Down
Loading

0 comments on commit e416a17

Please sign in to comment.