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
hannesj authored and t2gran committed Dec 6, 2023
1 parent 31909f2 commit 1dc2192
Show file tree
Hide file tree
Showing 23 changed files with 476 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 @@ -228,6 +228,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. | ✓️ | |
| `DebugClient` | Enable the debug web client located at the root of the web server. | ✓️ | |
| `FloatingBike` | Enable floating bike routing. | ✓️ | |
| `HackSorlandsbanen` | Includ Sørlandsbanen | | ✓️ |
| `GtfsGraphQlApi` | Enable GTFS GraphQL API. | ✓️ | |
| `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 @@ -46,6 +46,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe
| 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 |
| escalatorReluctance | `double` | A multiplier for how bad being in an escalator is compared to being in transit for equal lengths of time | *Optional* | `1.5` | 2.4 |
| 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
2 changes: 2 additions & 0 deletions src/ext/graphql/transmodelapi/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,8 @@ type QueryType {
dateTime: DateTime,
"Debug the itinerary-filter-chain. OTP will attach a system notice to itineraries instead of removing them. This is very convenient when tuning the filters."
debugItineraryFilter: Boolean = false @deprecated(reason : "Use `itineraryFilter.debug` instead."),
"FOR TESTING ONLY"
extraSearchCoachReluctance: Float,
"A list of filters for which trips should be included. A trip will be included if it matches with at least one filter. An empty list of filters means that all trips should be included. If a search include this parameter, \"whiteListed\", \"banned\" & \"modes.transportModes\" filters will be ignored."
filters: [TripFilterInput!],
"The start location"
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 RaptorRequest<TripSchedule> enableHack(
RaptorRequest<TripSchedule> 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 @@ -596,6 +596,14 @@ public static GraphQLFieldDefinition create(
)
.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 @@ -97,6 +97,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 @@ -200,6 +200,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
Loading

0 comments on commit 1dc2192

Please sign in to comment.