Skip to content

Commit

Permalink
hack: Add train option for Sørlandsbanen
Browse files Browse the repository at this point in the history
  • Loading branch information
t2gran authored and vpaturet committed Jun 14, 2024
1 parent d78225c commit ae3b16e
Show file tree
Hide file tree
Showing 24 changed files with 478 additions and 21 deletions.
1 change: 1 addition & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ Here is a list of all features which can be toggled on/off and their default val
| `ConsiderPatternsForDirectTransfers` | Enable limiting transfers so that there is only a single transfer to each pattern. | ✓️ | |
| `DebugUi` | Enable the debug GraphQL client and web UI and located at the root of the web server as well as the debug map tiles it uses. Be aware that the map tiles are not a stable API and can change without notice. Use the [vector tiles feature if](sandbox/MapboxVectorTilesApi.md) you want a stable map tiles API. | ✓️ | |
| `FloatingBike` | Enable floating bike routing. | ✓️ | |
| `HackSorlandsbanen` | Includ Sørlandsbanen | | ✓️ |
| `GtfsGraphQlApi` | Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md). | ✓️ | |
| `GtfsGraphQlApiRentalStationFuzzyMatching` | Does vehicleRentalStation query also allow ids that are not feed scoped. | | |
| `MinimumTransferTimeIsDefinitive` | If the minimum transfer time is a lower bound (default) or the definitive time for the transfer. Set this to `true` if you want to set a transfer time lower than what OTP derives from OSM data. | | |
Expand Down
1 change: 1 addition & 0 deletions docs/RouteRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe
| elevatorBoardTime | `integer` | How long does it take to get on an elevator, on average. | *Optional* | `90` | 2.0 |
| elevatorHopCost | `integer` | What is the cost of travelling one floor on an elevator? | *Optional* | `20` | 2.0 |
| elevatorHopTime | `integer` | How long does it take to advance one floor on an elevator? | *Optional* | `20` | 2.0 |
| extraSearchCoachReluctance | `double` | TODO | *Optional* | `0.0` | 2.1 |
| geoidElevation | `boolean` | If true, the Graph's ellipsoidToGeoidDifference is applied to all elevations returned by this query. | *Optional* | `false` | 2.0 |
| ignoreRealtimeUpdates | `boolean` | When true, real-time updates are ignored during this search. | *Optional* | `false` | 2.0 |
| [intersectionTraversalModel](#rd_intersectionTraversalModel) | `enum` | The model that computes the costs of turns. | *Optional* | `"simple"` | 2.2 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ private BoardAndAlightSlack mapTransit() {
v -> tr.withRaptor(r -> r.withRelaxGeneralizedCostAtDestination(v))
);
}
setIfNotNull(req.extraSearchCoachReluctance, tr::setExtraSearchCoachReluctance);
});

return new BoardAndAlightSlack(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ public abstract class RoutingResource {
@QueryParam("carReluctance")
protected Double carReluctance;

@QueryParam("extraSearchCoachReluctance")
protected Double extraSearchCoachReluctance;

/**
* How much worse is waiting for a transit vehicle than being on a transit vehicle, as a
* multiplier. The default value treats wait and on-vehicle time as the same.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.opentripplanner.ext.sorlandsbanen;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.raptor.api.model.RaptorTripSchedule;
import org.opentripplanner.raptor.api.path.PathLeg;
import org.opentripplanner.raptor.api.path.RaptorPath;
import org.opentripplanner.raptor.api.response.StopArrivals;
import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker;
import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult;
import org.opentripplanner.raptor.rangeraptor.multicriteria.McRaptorWorkerResult;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class ConcurrentCompositeWorker<T extends RaptorTripSchedule> implements RaptorWorker<T> {

private static final Logger LOG = LoggerFactory.getLogger(ConcurrentCompositeWorker.class);

private final RaptorWorker<T> mainWorker;
private final RaptorWorker<T> alternativeWorker;

ConcurrentCompositeWorker(RaptorWorker<T> mainWorker, RaptorWorker<T> alternativeWorker) {
this.mainWorker = mainWorker;
this.alternativeWorker = alternativeWorker;
}

@Override
public RaptorWorkerResult<T> route() {
if (OTPFeature.ParallelRouting.isOn()) {
var mainResultFuture = CompletableFuture.supplyAsync(mainWorker::route);
var alternativeResultFuture = CompletableFuture.supplyAsync(alternativeWorker::route);

try {
return new RaptorWorkerResultComposite<>(
mainResultFuture.get(),
alternativeResultFuture.get()
);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
} else {
var mainResult = mainWorker.route();
var alternativeResult = alternativeWorker.route();
return new RaptorWorkerResultComposite<>(mainResult, alternativeResult);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.opentripplanner.ext.sorlandsbanen;

import java.util.Collection;
import java.util.function.Function;
import org.opentripplanner.framework.geometry.SphericalDistanceLibrary;
import org.opentripplanner.framework.geometry.WgsCoordinate;
import org.opentripplanner.model.GenericLocation;
import org.opentripplanner.raptor.api.model.RaptorAccessEgress;
import org.opentripplanner.raptor.api.model.RaptorTripSchedule;
import org.opentripplanner.raptor.api.request.RaptorRequest;
import org.opentripplanner.raptor.api.request.SearchParams;
import org.opentripplanner.raptor.configure.RaptorConfig;
import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics;
import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker;
import org.opentripplanner.raptor.spi.RaptorTransitDataProvider;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.FactorStrategy;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.IndexBasedFactorStrategy;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.site.StopLocation;

public class EnturHackSorlandsBanen {

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

public static <T extends RaptorTripSchedule> boolean match(RaptorRequest<T> mcRequest) {
return mcRequest.extraSearchCoachReluctance > 0.1;
}

public static <T extends RaptorTripSchedule> RaptorWorker<T> worker(
RaptorConfig<T> config,
RaptorTransitDataProvider<T> transitData,
RaptorRequest<T> mcRequest,
Heuristics destinationHeuristics
) {
//noinspection unchecked
RaptorTransitDataProvider<T> altTransitData = (RaptorTransitDataProvider<T>) (
(RaptorRoutingRequestTransitData) transitData
).enturHackSorlandsbanen(mapFactors(mcRequest.extraSearchCoachReluctance));

return new ConcurrentCompositeWorker<>(
config.createMcWorker(transitData, mcRequest, destinationHeuristics),
config.createMcWorker(altTransitData, mcRequest, destinationHeuristics)
);
}

public static <T extends RaptorTripSchedule> RaptorRequest<T> enableHack(
RaptorRequest<T> raptorRequest,
RouteRequest request,
TransitLayer transitLayer
) {
if (request.preferences().transit().extraSearchCoachReluctance() < 0.1) {
return raptorRequest;
}

SearchParams params = raptorRequest.searchParams();

WgsCoordinate from = findStopCoordinate(request.from(), params.accessPaths(), transitLayer);
WgsCoordinate to = findStopCoordinate(request.to(), params.egressPaths(), transitLayer);

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

double distanceMeters = SphericalDistanceLibrary.distance(
from.latitude(),
from.longitude(),
to.latitude(),
to.longitude()
);

if (distanceMeters < MIN_DISTANCE_LIMIT) {
return raptorRequest;
}

raptorRequest.extraSearchCoachReluctance =
request.preferences().transit().extraSearchCoachReluctance();
return raptorRequest;
}

/* private methods */

private static Function<FactorStrategy, FactorStrategy> mapFactors(
final double extraSearchCoachReluctance
) {
return (FactorStrategy originalFactors) -> {
int[] modeReluctance = new int[TransitMode.values().length];
for (TransitMode mode : TransitMode.values()) {
int index = mode.ordinal();
int originalFactor = originalFactors.factor(index);
modeReluctance[index] =
mode == TransitMode.COACH
? (int) (extraSearchCoachReluctance * originalFactor + 0.5)
: originalFactor;
}
return new IndexBasedFactorStrategy(modeReluctance);
};
}

/**
* 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<RaptorAccessEgress> accessEgress,
TransitLayer transitLayer
) {
if (location.lat != null) {
return new WgsCoordinate(location.lat, location.lng);
}

StopLocation firstStop = null;
for (RaptorAccessEgress 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);
}
}
51 changes: 51 additions & 0 deletions src/ext/java/org/opentripplanner/ext/sorlandsbanen/PathKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.opentripplanner.ext.sorlandsbanen;

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

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,88 @@
package org.opentripplanner.ext.sorlandsbanen;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.opentripplanner.raptor.api.model.RaptorTripSchedule;
import org.opentripplanner.raptor.api.path.PathLeg;
import org.opentripplanner.raptor.api.path.RaptorPath;
import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult;
import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleWithOffset;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RaptorWorkerResultComposite<T extends RaptorTripSchedule>
implements RaptorWorkerResult<T> {

private static final Logger LOG = LoggerFactory.getLogger(RaptorWorkerResultComposite.class);

private RaptorWorkerResult<T> mainResult;
private RaptorWorkerResult<T> alternativeResult;

public RaptorWorkerResultComposite(
RaptorWorkerResult<T> mainResult,
RaptorWorkerResult<T> alternativeResult
) {
this.mainResult = mainResult;
this.alternativeResult = alternativeResult;
}

@Override
public Collection<RaptorPath<T>> extractPaths() {
Map<PathKey, RaptorPath<T>> paths = new HashMap<>();
addAll(paths, mainResult.extractPaths());
addExtraRail(paths, alternativeResult.extractPaths());
return paths.values();
}

@Override
public SingleCriteriaStopArrivals extractBestOverallArrivals() {
return mainResult.extractBestOverallArrivals();
}

@Override
public SingleCriteriaStopArrivals extractBestTransitArrivals() {
return mainResult.extractBestTransitArrivals();
}

@Override
public SingleCriteriaStopArrivals extractBestNumberOfTransfers() {
return mainResult.extractBestNumberOfTransfers();
}

@Override
public boolean isDestinationReached() {
return mainResult.isDestinationReached();
}

private void addExtraRail(Map<PathKey, RaptorPath<T>> map, Collection<RaptorPath<T>> paths) {
paths.forEach(p -> {
if (hasRail(p)) {
var v = map.put(new PathKey(p), p);
LOG.debug("Ex.Rail {} : {}", (v == null ? "ADD " : "SKIP"), p);
} else {
LOG.debug("Ex. NOT Rail : {}", p);
}
});
}

private void addAll(Map<PathKey, RaptorPath<T>> map, Collection<RaptorPath<T>> paths) {
paths.forEach(p -> {
var v = map.put(new PathKey(p), p);
LOG.debug("Normal {} : {}", (v == null ? "ADD " : "SKIP"), p);
});
}

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
Expand Up @@ -616,6 +616,14 @@ Normally this is when the search is performed (now), plus a small grace period t
)
.build()
)
.argument(
GraphQLArgument
.newArgument()
.name("extraSearchCoachReluctance")
.description("FOR TESTING ONLY")
.type(Scalars.GraphQLFloat)
.build()
)
.dataFetcher(environment -> new TransmodelGraphQLPlanner().plan(environment))
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum OTPFeature {
"""
),
FloatingBike(true, false, "Enable floating bike routing."),
HackSorlandsbanen(false, true, "Includ Sørlandsbanen"),
GtfsGraphQlApi(true, false, "Enable the [GTFS GraphQL API](apis/GTFS-GraphQL-API.md)."),
GtfsGraphQlApiRentalStationFuzzyMatching(
false,
Expand Down
Loading

0 comments on commit ae3b16e

Please sign in to comment.