
org.opentripplanner.graph_builder.module.DirectTransferGenerator Maven / Gradle / Ivy
package org.opentripplanner.graph_builder.module;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimaps;
import java.time.Duration;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.graph_builder.issues.StopNotLinkedForTransfers;
import org.opentripplanner.graph_builder.model.GraphBuilderModule;
import org.opentripplanner.graph_builder.module.nearbystops.NearbyStopFinder;
import org.opentripplanner.graph_builder.module.nearbystops.PatternConsideringNearbyStopFinder;
import org.opentripplanner.graph_builder.module.nearbystops.StraightLineNearbyStopFinder;
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.graph.Graph;
import org.opentripplanner.routing.graphfinder.NearbyStop;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.vertex.TransitStopVertex;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.service.DefaultTransitService;
import org.opentripplanner.transit.service.TimetableRepository;
import org.opentripplanner.utils.logging.ProgressTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link GraphBuilderModule} module that links up the
* stops of a transit network among themselves. This is necessary for routing in long-distance
* mode.
*
* It will use the street network if OSM data has already been loaded into the graph. Otherwise it
* will use straight-line distance between stops.
*/
public class DirectTransferGenerator implements GraphBuilderModule {
private static final Logger LOG = LoggerFactory.getLogger(DirectTransferGenerator.class);
private final Duration defaultMaxTransferDuration;
private final List transferRequests;
private final Map transferParametersForMode;
private final Graph graph;
private final TimetableRepository timetableRepository;
private final DataImportIssueStore issueStore;
/**
* Constructor used in tests. This initializes transferParametersForMode as an empty map.
*/
public DirectTransferGenerator(
Graph graph,
TimetableRepository timetableRepository,
DataImportIssueStore issueStore,
Duration defaultMaxTransferDuration,
List transferRequests
) {
this.graph = graph;
this.timetableRepository = timetableRepository;
this.issueStore = issueStore;
this.defaultMaxTransferDuration = defaultMaxTransferDuration;
this.transferRequests = transferRequests;
this.transferParametersForMode = Map.of();
}
public DirectTransferGenerator(
Graph graph,
TimetableRepository timetableRepository,
DataImportIssueStore issueStore,
Duration defaultMaxTransferDuration,
List transferRequests,
Map transferParametersForMode
) {
this.graph = graph;
this.timetableRepository = timetableRepository;
this.issueStore = issueStore;
this.defaultMaxTransferDuration = defaultMaxTransferDuration;
this.transferRequests = transferRequests;
this.transferParametersForMode = transferParametersForMode;
}
@Override
public void buildGraph() {
// Initialize transit model index which is needed by the nearby stop finder.
timetableRepository.index();
// The linker will use streets if they are available, or straight-line distance otherwise.
NearbyStopFinder nearbyStopFinder = createNearbyStopFinder(defaultMaxTransferDuration);
List stops = graph.getVerticesOfType(TransitStopVertex.class);
Set carsAllowedStops =
timetableRepository.getStopLocationsUsedForCarsAllowedTrips();
LOG.info("Creating transfers based on requests:");
transferRequests.forEach(transferProfile -> LOG.info(transferProfile.toString()));
if (transferParametersForMode.isEmpty()) {
LOG.info("No mode-specific transfer configurations provided.");
} else {
LOG.info("Using transfer configurations for modes:");
transferParametersForMode.forEach((mode, transferParameters) ->
LOG.info(mode + ": " + transferParameters)
);
}
ProgressTracker progress = ProgressTracker.track(
"Create transfer edges for stops",
1000,
stops.size()
);
AtomicInteger nTransfersTotal = new AtomicInteger();
AtomicInteger nLinkedStops = new AtomicInteger();
// This is a synchronizedMultimap so that a parallel stream may be used to insert elements.
var transfersByStop = Multimaps.synchronizedMultimap(
HashMultimap.create()
);
// Parse the transfer configuration from the parameters given in the build config.
TransferConfiguration transferConfiguration = parseTransferParameters(nearbyStopFinder);
stops
.stream()
.parallel()
.forEach(ts0 -> {
/* Make transfers to each nearby stop that has lowest weight on some trip pattern.
* Use map based on the list of edges, so that only distinct transfers are stored. */
Map distinctTransfers = new HashMap<>();
RegularStop stop = ts0.getStop();
if (stop.transfersNotAllowed()) {
return;
}
LOG.debug("Linking stop '{}' {}", stop, ts0);
calculateDefaultTransfers(transferConfiguration, ts0, stop, distinctTransfers);
calculateFlexTransfers(transferConfiguration, ts0, stop, distinctTransfers);
calculateCarsAllowedTransfers(
transferConfiguration,
ts0,
stop,
distinctTransfers,
carsAllowedStops
);
LOG.debug(
"Linked stop {} with {} transfers to stops with different patterns.",
stop,
distinctTransfers.size()
);
if (distinctTransfers.isEmpty()) {
issueStore.add(new StopNotLinkedForTransfers(ts0));
} else {
distinctTransfers
.values()
.forEach(transfer -> transfersByStop.put(transfer.from, transfer));
nLinkedStops.incrementAndGet();
nTransfersTotal.addAndGet(distinctTransfers.size());
}
//Keep lambda! A method-ref would causes incorrect class and line number to be logged
//noinspection Convert2MethodRef
progress.step(m -> LOG.info(m));
});
timetableRepository.addAllTransfersByStops(transfersByStop);
LOG.info(progress.completeMessage());
LOG.info(
"Done connecting stops to one another. Created a total of {} transfers from {} stops.",
nTransfersTotal,
nLinkedStops
);
transferRequests
.stream()
.map(transferProfile -> transferProfile.journey().transfer().mode())
.distinct()
.forEach(mode ->
LOG.info(
"Created {} transfers for mode {}.",
timetableRepository.findTransfers(mode).size(),
mode
)
);
}
/**
* Factory method for creating a NearbyStopFinder. Will create different finders depending on
* whether the graph has a street network and if ConsiderPatternsForDirectTransfers feature is
* enabled.
*/
private NearbyStopFinder createNearbyStopFinder(Duration radiusAsDuration) {
var transitService = new DefaultTransitService(timetableRepository);
NearbyStopFinder finder;
if (!graph.hasStreets) {
LOG.info(
"Creating direct transfer edges between stops using straight line distance (not streets)..."
);
finder = new StraightLineNearbyStopFinder(transitService, radiusAsDuration);
} else {
LOG.info("Creating direct transfer edges between stops using the street network from OSM...");
finder = new StreetNearbyStopFinder(radiusAsDuration, 0, null);
}
if (OTPFeature.ConsiderPatternsForDirectTransfers.isOn()) {
return new PatternConsideringNearbyStopFinder(transitService, finder);
} else {
return finder;
}
}
private void createPathTransfer(
StopLocation from,
StopLocation to,
NearbyStop sd,
Map distinctTransfers,
StreetMode mode
) {
TransferKey transferKey = new TransferKey(from, to, sd.edges);
PathTransfer pathTransfer = distinctTransfers.get(transferKey);
if (pathTransfer == null) {
// If the PathTransfer can't be found, it is created.
distinctTransfers.put(
transferKey,
new PathTransfer(from, to, sd.distance, sd.edges, EnumSet.of(mode))
);
} else {
// If the PathTransfer is found, a new PathTransfer with the added mode is created.
distinctTransfers.put(transferKey, pathTransfer.withAddedMode(mode));
}
}
/**
* This method parses the given transfer parameters into a transfer configuration and checks for invalid input.
*/
private TransferConfiguration parseTransferParameters(NearbyStopFinder nearbyStopFinder) {
List defaultTransferRequests = new ArrayList<>();
List carsAllowedStopTransferRequests = new ArrayList<>();
List flexTransferRequests = new ArrayList<>();
HashMap defaultNearbyStopFinderForMode = new HashMap<>();
// These are used for calculating transfers only between carsAllowedStops.
HashMap carsAllowedStopNearbyStopFinderForMode = new HashMap<>();
// Check that the mode specified in transferParametersForMode can also be found in transferRequests.
for (StreetMode mode : transferParametersForMode.keySet()) {
if (
!transferRequests
.stream()
.anyMatch(transferProfile -> transferProfile.journey().transfer().mode() == mode)
) {
throw new IllegalArgumentException(
String.format(
"Mode %s is used in transferParametersForMode but not in transferRequests",
mode
)
);
}
}
for (RouteRequest transferProfile : transferRequests) {
StreetMode mode = transferProfile.journey().transfer().mode();
TransferParameters transferParameters = transferParametersForMode.get(mode);
if (transferParameters != null) {
// WALK mode transfers can not be disabled. For example, flex transfers need them.
if (transferParameters.disableDefaultTransfers() && mode == StreetMode.WALK) {
throw new IllegalArgumentException("WALK mode transfers can not be disabled");
}
// Disable normal transfer calculations for the specific mode, if disableDefaultTransfers is set in the build config.
if (!transferParameters.disableDefaultTransfers()) {
defaultTransferRequests.add(transferProfile);
// Set mode-specific maxTransferDuration, if it is set in the build config.
Duration maxTransferDuration = transferParameters.maxTransferDuration();
if (maxTransferDuration != null) {
defaultNearbyStopFinderForMode.put(mode, createNearbyStopFinder(maxTransferDuration));
} else {
defaultNearbyStopFinderForMode.put(mode, nearbyStopFinder);
}
}
// Create transfers between carsAllowedStops for the specific mode if carsAllowedStopMaxTransferDuration is set in the build config.
Duration carsAllowedStopMaxTransferDuration =
transferParameters.carsAllowedStopMaxTransferDuration();
if (carsAllowedStopMaxTransferDuration != null) {
carsAllowedStopTransferRequests.add(transferProfile);
carsAllowedStopNearbyStopFinderForMode.put(
mode,
createNearbyStopFinder(carsAllowedStopMaxTransferDuration)
);
}
} else {
defaultTransferRequests.add(transferProfile);
defaultNearbyStopFinderForMode.put(mode, nearbyStopFinder);
}
}
// Flex transfer requests only use the WALK mode.
if (OTPFeature.FlexRouting.isOn()) {
flexTransferRequests.addAll(
transferRequests
.stream()
.filter(transferProfile -> transferProfile.journey().transfer().mode() == StreetMode.WALK)
.toList()
);
}
return new TransferConfiguration(
defaultTransferRequests,
carsAllowedStopTransferRequests,
flexTransferRequests,
defaultNearbyStopFinderForMode,
carsAllowedStopNearbyStopFinderForMode
);
}
/**
* This method calculates default transfers.
*/
private void calculateDefaultTransfers(
TransferConfiguration transferConfiguration,
TransitStopVertex ts0,
RegularStop stop,
Map distinctTransfers
) {
for (RouteRequest transferProfile : transferConfiguration.defaultTransferRequests()) {
StreetMode mode = transferProfile.journey().transfer().mode();
var nearbyStops = transferConfiguration
.defaultNearbyStopFinderForMode()
.get(mode)
.findNearbyStops(ts0, transferProfile, transferProfile.journey().transfer(), false);
for (NearbyStop sd : nearbyStops) {
// Skip the origin stop, loop transfers are not needed.
if (sd.stop == stop) {
continue;
}
if (sd.stop.transfersNotAllowed()) {
continue;
}
createPathTransfer(stop, sd.stop, sd, distinctTransfers, mode);
}
}
}
/**
* This method calculates flex transfers if flex routing is enabled.
*/
private void calculateFlexTransfers(
TransferConfiguration transferConfiguration,
TransitStopVertex ts0,
RegularStop stop,
Map distinctTransfers
) {
for (RouteRequest transferProfile : transferConfiguration.flexTransferRequests()) {
// Flex transfer requests only use the WALK mode.
StreetMode mode = StreetMode.WALK;
var nearbyStops = transferConfiguration
.defaultNearbyStopFinderForMode()
.get(mode)
.findNearbyStops(ts0, transferProfile, transferProfile.journey().transfer(), true);
// This code is for finding transfers from AreaStops to Stops, transfers
// from Stops to AreaStops and between Stops are already covered above.
for (NearbyStop sd : nearbyStops) {
// Skip the origin stop, loop transfers are not needed.
if (sd.stop == stop) {
continue;
}
if (sd.stop instanceof RegularStop) {
continue;
}
// The TransferKey and PathTransfer are created differently for flex routing.
createPathTransfer(sd.stop, stop, sd, distinctTransfers, mode);
}
}
}
/**
* This method calculates transfers between stops that are visited by trips that allow cars, if configured.
*/
private void calculateCarsAllowedTransfers(
TransferConfiguration transferConfiguration,
TransitStopVertex ts0,
RegularStop stop,
Map distinctTransfers,
Set carsAllowedStops
) {
if (carsAllowedStops.contains(stop)) {
for (RouteRequest transferProfile : transferConfiguration.carsAllowedStopTransferRequests()) {
StreetMode mode = transferProfile.journey().transfer().mode();
var nearbyStops = transferConfiguration
.carsAllowedStopNearbyStopFinderForMode()
.get(mode)
.findNearbyStops(ts0, transferProfile, transferProfile.journey().transfer(), false);
for (NearbyStop sd : nearbyStops) {
// Skip the origin stop, loop transfers are not needed.
if (sd.stop == stop) {
continue;
}
if (sd.stop.transfersNotAllowed()) {
continue;
}
// Only calculate transfers between carsAllowedStops.
if (!carsAllowedStops.contains(sd.stop)) {
continue;
}
createPathTransfer(stop, sd.stop, sd, distinctTransfers, mode);
}
}
}
}
private record TransferConfiguration(
List defaultTransferRequests,
List carsAllowedStopTransferRequests,
List flexTransferRequests,
HashMap defaultNearbyStopFinderForMode,
HashMap carsAllowedStopNearbyStopFinderForMode
) {}
private record TransferKey(StopLocation source, StopLocation target, List edges) {}
}