Skip to content

Commit

Permalink
Add ability to create special transfer requests for cars and bikes wi…
Browse files Browse the repository at this point in the history
…th the 'carsAllowedStopMaxTransferDurationsForMode' build config parameter.
  • Loading branch information
VillePihlava committed Oct 30, 2024
1 parent 53a2132 commit f6adb13
Show file tree
Hide file tree
Showing 10 changed files with 634 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,8 @@ public RequestModes getRequestModes() {
mBuilder.withEgressMode(StreetMode.CAR_HAILING);
mBuilder.withDirectMode(StreetMode.WALK);
} else {
mBuilder.withAccessMode(StreetMode.WALK);
mBuilder.withTransferMode(StreetMode.WALK);
mBuilder.withEgressMode(StreetMode.WALK);
mBuilder.withDirectMode(StreetMode.CAR);
// This is used in transfer cache request calculations.
mBuilder.withAllStreetModes(StreetMode.CAR);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimaps;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.framework.logging.ProgressTracker;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
Expand All @@ -18,6 +21,8 @@
import org.opentripplanner.graph_builder.module.nearbystops.StreetNearbyStopFinder;
import org.opentripplanner.model.PathTransfer;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.StreetMode;
import org.opentripplanner.routing.api.request.framework.DurationForEnum;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graphfinder.NearbyStop;
import org.opentripplanner.street.model.edge.Edge;
Expand All @@ -44,6 +49,7 @@ public class DirectTransferGenerator implements GraphBuilderModule {
private final Duration radiusByDuration;

private final List<RouteRequest> transferRequests;
private final DurationForEnum<StreetMode> carsAllowedStopMaxTransferDurationsForMode;
private final Graph graph;
private final TimetableRepository timetableRepository;
private final DataImportIssueStore issueStore;
Expand All @@ -60,6 +66,23 @@ public DirectTransferGenerator(
this.issueStore = issueStore;
this.radiusByDuration = radiusByDuration;
this.transferRequests = transferRequests;
this.carsAllowedStopMaxTransferDurationsForMode = DurationForEnum.of(StreetMode.class).build();
}

public DirectTransferGenerator(
Graph graph,
TimetableRepository timetableRepository,
DataImportIssueStore issueStore,
Duration radiusByDuration,
List<RouteRequest> transferRequests,
DurationForEnum<StreetMode> carsAllowedStopMaxTransferDurationsForMode
) {
this.graph = graph;
this.timetableRepository = timetableRepository;
this.issueStore = issueStore;
this.radiusByDuration = radiusByDuration;
this.transferRequests = transferRequests;
this.carsAllowedStopMaxTransferDurationsForMode = carsAllowedStopMaxTransferDurationsForMode;
}

@Override
Expand All @@ -68,9 +91,16 @@ public void buildGraph() {
timetableRepository.index();

/* The linker will use streets if they are available, or straight-line distance otherwise. */
NearbyStopFinder nearbyStopFinder = createNearbyStopFinder();
NearbyStopFinder nearbyStopFinder = createNearbyStopFinder(radiusByDuration);

List<TransitStopVertex> stops = graph.getVerticesOfType(TransitStopVertex.class);
Set<TransitStopVertex> carsAllowedStops = timetableRepository
.getStopLocationsUsedForCarsAllowedTrips()
.stream()
.map(StopLocation::getId)
.map(graph::getStopVertexForStopId)
.filter(TransitStopVertex.class::isInstance) // filter out null values if no TransitStopVertex is found for ID
.collect(Collectors.toSet());

ProgressTracker progress = ProgressTracker.track(
"Create transfer edges for stops",
Expand All @@ -86,6 +116,34 @@ public void buildGraph() {
HashMultimap.create()
);

List<RouteRequest> filteredTransferRequests = new ArrayList<RouteRequest>();
List<RouteRequest> carsAllowedStopTransferRequests = new ArrayList<RouteRequest>();
HashMap<StreetMode, NearbyStopFinder> carsAllowedStopNearbyStopFinders = new HashMap<>();

// Split transfer requests into normal and carsAllowedStop requests.
for (RouteRequest transferProfile : transferRequests) {
StreetMode mode = transferProfile.journey().transfer().mode();
if (carsAllowedStopMaxTransferDurationsForMode.containsKey(mode)) {
carsAllowedStopNearbyStopFinders.put(
mode,
createNearbyStopFinder(carsAllowedStopMaxTransferDurationsForMode.valueOf(mode))
);

carsAllowedStopTransferRequests.add(transferProfile);
// For bikes, also normal transfer requests are wanted.
if (mode == StreetMode.BIKE) {
filteredTransferRequests.add(transferProfile);
}
} else if (mode == StreetMode.CAR) {
// Special transfers are always created for cars.
// If a duration is not specified for cars, the default is used.
carsAllowedStopNearbyStopFinders.put(mode, nearbyStopFinder);
carsAllowedStopTransferRequests.add(transferProfile);
} else {
filteredTransferRequests.add(transferProfile);
}
}

stops
.stream()
.parallel()
Expand All @@ -101,25 +159,8 @@ public void buildGraph() {

LOG.debug("Linking stop '{}' {}", stop, ts0);

for (RouteRequest transferProfile : transferRequests) {
for (NearbyStop sd : nearbyStopFinder.findNearbyStops(
ts0,
transferProfile,
transferProfile.journey().transfer(),
false
)) {
// Skip the origin stop, loop transfers are not needed.
if (sd.stop == stop) {
continue;
}
if (sd.stop.transfersNotAllowed()) {
continue;
}
distinctTransfers.put(
new TransferKey(stop, sd.stop, sd.edges),
new PathTransfer(stop, sd.stop, sd.distance, sd.edges)
);
}
for (RouteRequest transferProfile : filteredTransferRequests) {
findNearbyStops(nearbyStopFinder, ts0, transferProfile, stop, distinctTransfers);
if (OTPFeature.FlexRouting.isOn()) {
// This code is for finding transfers from AreaStops to Stops, transfers
// from Stops to AreaStops and between Stops are already covered above.
Expand All @@ -143,6 +184,21 @@ public void buildGraph() {
}
}
}
// This calculates transfers between stops that can use trips with cars.
for (RouteRequest transferProfile : carsAllowedStopTransferRequests) {
StreetMode mode = transferProfile.journey().transfer().mode();
if (
carsAllowedStops.contains(ts0) && carsAllowedStopNearbyStopFinders.containsKey(mode)
) {
findNearbyStops(
carsAllowedStopNearbyStopFinders.get(mode),
ts0,
transferProfile,
stop,
distinctTransfers
);
}
}

LOG.debug(
"Linked stop {} with {} transfers to stops with different patterns.",
Expand Down Expand Up @@ -179,7 +235,7 @@ public void buildGraph() {
* whether the graph has a street network and if ConsiderPatternsForDirectTransfers feature is
* enabled.
*/
private NearbyStopFinder createNearbyStopFinder() {
private NearbyStopFinder createNearbyStopFinder(Duration radiusByDuration) {
var transitService = new DefaultTransitService(timetableRepository);
NearbyStopFinder finder;
if (!graph.hasStreets) {
Expand All @@ -199,5 +255,32 @@ private NearbyStopFinder createNearbyStopFinder() {
}
}

private void findNearbyStops(
NearbyStopFinder nearbyStopFinder,
TransitStopVertex ts0,
RouteRequest transferProfile,
RegularStop stop,
Map<TransferKey, PathTransfer> distinctTransfers
) {
for (NearbyStop sd : nearbyStopFinder.findNearbyStops(
ts0,
transferProfile,
transferProfile.journey().transfer(),
false
)) {
// Skip the origin stop, loop transfers are not needed.
if (sd.stop == stop) {
continue;
}
if (sd.stop.transfersNotAllowed()) {
continue;
}
distinctTransfers.put(
new TransferKey(stop, sd.stop, sd.edges),
new PathTransfer(stop, sd.stop, sd.distance, sd.edges)
);
}
}

private record TransferKey(StopLocation source, StopLocation target, List<Edge> edges) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@ static DirectTransferGenerator provideDirectTransferGenerator(
timetableRepository,
issueStore,
config.maxTransferDuration,
config.transferRequests
config.transferRequests,
config.carsAllowedStopMaxTransferDurationsForMode
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.search.StreetSearchBuilder;
import org.opentripplanner.street.search.TraverseMode;
import org.opentripplanner.street.search.request.StreetSearchRequest;
import org.opentripplanner.street.search.request.StreetSearchRequestMapper;
import org.opentripplanner.street.search.state.State;
import org.opentripplanner.street.search.strategy.DominanceFunctions;
import org.opentripplanner.transit.model.site.AreaStop;
Expand All @@ -42,6 +40,7 @@ public class StreetNearbyStopFinder implements NearbyStopFinder {
private final int maxStopCount;
private final DataOverlayContext dataOverlayContext;
private final Set<Vertex> ignoreVertices;
private final Set<Vertex> findOnlyVertices;

/**
* Construct a NearbyStopFinder for the given graph and search radius.
Expand All @@ -54,7 +53,7 @@ public StreetNearbyStopFinder(
int maxStopCount,
DataOverlayContext dataOverlayContext
) {
this(durationLimit, maxStopCount, dataOverlayContext, Set.of());
this(durationLimit, maxStopCount, dataOverlayContext, Set.of(), Set.of());
}

/**
Expand All @@ -69,11 +68,31 @@ public StreetNearbyStopFinder(
int maxStopCount,
DataOverlayContext dataOverlayContext,
Set<Vertex> ignoreVertices
) {
this(durationLimit, maxStopCount, dataOverlayContext, ignoreVertices, Set.of());
}

/**
* Construct a NearbyStopFinder for the given graph and search radius.
*
* @param maxStopCount The maximum stops to return. 0 means no limit. Regardless of the maxStopCount
* we will always return all the directly connected stops.
* @param ignoreVertices A set of stop vertices to ignore and not return NearbyStops for.
*
* @param findOnlyVertices Only return NearbyStops that are in this set.
*/
public StreetNearbyStopFinder(
Duration durationLimit,
int maxStopCount,
DataOverlayContext dataOverlayContext,
Set<Vertex> ignoreVertices,
Set<Vertex> findOnlyVertices
) {
this.dataOverlayContext = dataOverlayContext;
this.durationLimit = durationLimit;
this.maxStopCount = maxStopCount;
this.ignoreVertices = ignoreVertices;
this.findOnlyVertices = findOnlyVertices;
}

/**
Expand Down Expand Up @@ -146,7 +165,10 @@ public Collection<NearbyStop> findNearbyStops(
continue;
}
if (targetVertex instanceof TransitStopVertex tsv && state.isFinal()) {
stopsFound.add(NearbyStop.nearbyStopForState(state, tsv.getStop()));
// If a set of findOnlyVertices is provided, only add stops that are in the set.
if (findOnlyVertices.isEmpty() || findOnlyVertices.contains(targetVertex)) {
stopsFound.add(NearbyStop.nearbyStopForState(state, tsv.getStop()));
}
}
if (
OTPFeature.FlexRouting.isOn() &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public Duration defaultValue() {
return defaultValue;
}

public boolean containsKey(E key) {
return valueForEnum.containsKey(key);
}

/**
* Utility method to get {@link #defaultValue} as an number in unit seconds. Equivalent to
* {@code (int) defaultValue.toSeconds()}. The downcast is safe since we only allow days, hours,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@
import org.opentripplanner.model.calendar.ServiceDateInterval;
import org.opentripplanner.netex.config.NetexFeedParameters;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.StreetMode;
import org.opentripplanner.routing.api.request.framework.DurationForEnum;
import org.opentripplanner.routing.fares.FareServiceFactory;
import org.opentripplanner.standalone.config.buildconfig.CarsAllowedStopMaxTransferDurationsForMode;
import org.opentripplanner.standalone.config.buildconfig.DemConfig;
import org.opentripplanner.standalone.config.buildconfig.GtfsConfig;
import org.opentripplanner.standalone.config.buildconfig.IslandPruningConfig;
Expand Down Expand Up @@ -151,6 +154,7 @@ public class BuildConfig implements OtpDataStoreConfig {
public final IslandPruningConfig islandPruning;

public final Duration maxTransferDuration;
public final DurationForEnum<StreetMode> carsAllowedStopMaxTransferDurationsForMode;
public final NetexFeedParameters netexDefaults;
public final GtfsFeedParameters gtfsDefaults;

Expand Down Expand Up @@ -287,6 +291,12 @@ When set to true (it is false by default), the elevation module will include the
"Transfers up to this duration with the default walk speed value will be pre-calculated and included in the Graph."
)
.asDuration(Duration.ofMinutes(30));
carsAllowedStopMaxTransferDurationsForMode =
CarsAllowedStopMaxTransferDurationsForMode.map(
root,
"carsAllowedStopMaxTransferDurationsForMode",
maxTransferDuration
);
maxStopToShapeSnapDistance =
root
.of("maxStopToShapeSnapDistance")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.opentripplanner.standalone.config.buildconfig;

import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7;

import java.time.Duration;
import java.util.Map;
import org.opentripplanner.routing.api.request.StreetMode;
import org.opentripplanner.routing.api.request.framework.DurationForEnum;
import org.opentripplanner.standalone.config.framework.json.NodeAdapter;

public class CarsAllowedStopMaxTransferDurationsForMode {

public static DurationForEnum<StreetMode> map(
NodeAdapter root,
String parameterName,
Duration maxTransferDuration
) {
Map<StreetMode, Duration> values = root
.of(parameterName)
.since(V2_7)
.summary(
"This is used for specifying a `maxTransferDuration` value for bikes and cars to use with transfers between stops that have trips with cars."
)
.description(
"""
This is a special parameter that only works on transfers between stops that have trips that allow cars.
The duration can be set for either 'BIKE' or 'CAR'.
For cars, transfers are only calculated between stops that have trips that allow cars.
For cars, this overrides the default `maxTransferDuration`.
For bicycles, this indicates that additional transfers should be calculated with the specified duration between stops that have trips that allow cars.
**Example**
```JSON
// build-config.json
{
"carsAllowedStopMaxTransferDurationsForMode": {
"CAR": "2h",
"BIKE": "3h"
}
}
```
"""
)
.asEnumMap(StreetMode.class, Duration.class);
for (StreetMode mode : values.keySet()) {
if (mode != StreetMode.BIKE && mode != StreetMode.CAR) {
throw new IllegalArgumentException(
"Only the CAR and BIKE modes are allowed in the carsAllowedStopMaxTransferDurationsForMode parameter."
);
}
}
return DurationForEnum.of(StreetMode.class).withValues(values).build();
}
}
Loading

0 comments on commit f6adb13

Please sign in to comment.