
org.opentripplanner.graph_builder.module.StreetLinkerModule Maven / Gradle / Ivy
package org.opentripplanner.graph_builder.module;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.graph_builder.issues.ParkAndRideEntranceRemoved;
import org.opentripplanner.graph_builder.model.GraphBuilderModule;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.service.vehicleparking.VehicleParkingRepository;
import org.opentripplanner.service.vehicleparking.model.VehicleParking;
import org.opentripplanner.service.vehicleparking.model.VehicleParkingHelper;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.LinkingDirection;
import org.opentripplanner.street.model.edge.StreetStationCentroidLink;
import org.opentripplanner.street.model.edge.StreetTransitEntranceLink;
import org.opentripplanner.street.model.edge.StreetTransitStopLink;
import org.opentripplanner.street.model.edge.StreetVehicleParkingLink;
import org.opentripplanner.street.model.edge.VehicleParkingEdge;
import org.opentripplanner.street.model.vertex.StationCentroidVertex;
import org.opentripplanner.street.model.vertex.StreetVertex;
import org.opentripplanner.street.model.vertex.TransitEntranceVertex;
import org.opentripplanner.street.model.vertex.TransitStopVertex;
import org.opentripplanner.street.model.vertex.VehicleParkingEntranceVertex;
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.search.TraverseMode;
import org.opentripplanner.street.search.TraverseModeSet;
import org.opentripplanner.transit.model.site.GroupStop;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.service.TimetableRepository;
import org.opentripplanner.utils.logging.ProgressTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link GraphBuilderModule} plugin that links various
* objects in the graph to the street network. It should be run after both the transit network and
* street network are loaded. It links four things: transit stops, transit entrances, bike rental
* stations, and bike parks. Therefore, it should be run even when there's no GTFS data present to
* make bike rental services and bike parks usable.
*/
public class StreetLinkerModule implements GraphBuilderModule {
private static final Logger LOG = LoggerFactory.getLogger(StreetLinkerModule.class);
private static final TraverseModeSet CAR_ONLY = new TraverseModeSet(TraverseMode.CAR);
private static final TraverseModeSet WALK_ONLY = new TraverseModeSet(TraverseMode.WALK);
private final Graph graph;
private final VehicleParkingRepository parkingRepository;
private final TimetableRepository timetableRepository;
private final DataImportIssueStore issueStore;
private final Boolean addExtraEdgesToAreas;
public StreetLinkerModule(
Graph graph,
VehicleParkingRepository parkingRepository,
TimetableRepository timetableRepository,
DataImportIssueStore issueStore,
boolean addExtraEdgesToAreas
) {
this.graph = graph;
this.parkingRepository = parkingRepository;
this.timetableRepository = timetableRepository;
this.issueStore = issueStore;
this.addExtraEdgesToAreas = addExtraEdgesToAreas;
}
@Override
public void buildGraph() {
timetableRepository.index();
graph.index(timetableRepository.getSiteRepository());
graph.getLinker().setAddExtraEdgesToAreas(this.addExtraEdgesToAreas);
if (graph.hasStreets) {
linkTransitStops(graph, timetableRepository);
linkTransitEntrances(graph);
linkStationCentroids(graph);
linkVehicleParks(graph, issueStore);
}
// Calculates convex hull of a graph which is shown in routerInfo API point
graph.calculateConvexHull();
}
public void linkTransitStops(Graph graph, TimetableRepository timetableRepository) {
List vertices = graph.getVerticesOfType(TransitStopVertex.class);
var progress = ProgressTracker.track("Linking transit stops to graph", 5000, vertices.size());
LOG.info(progress.startMessage());
Set stopLocationsUsedForFlexTrips = Set.of();
if (OTPFeature.FlexRouting.isOn()) {
stopLocationsUsedForFlexTrips = getStopLocationsUsedForFlexTrips(timetableRepository);
}
Set stopLocationsUsedForCarsAllowedTrips =
timetableRepository.getStopLocationsUsedForCarsAllowedTrips();
for (TransitStopVertex tStop : vertices) {
// Stops with pathways do not need to be connected to the street network, since there are explicit entrances defined for that
if (tStop.hasPathways()) {
continue;
}
// check if stop is already linked, to allow multiple idempotent linking cycles
if (isAlreadyLinked(tStop, stopLocationsUsedForFlexTrips)) {
continue;
}
// ordinarily stops only need to be accessible by foot
StopLinkType linkType = StopLinkType.WALK_ONLY;
if (
(OTPFeature.FlexRouting.isOn() &&
stopLocationsUsedForFlexTrips.contains(tStop.getStop())) ||
stopLocationsUsedForCarsAllowedTrips.contains(tStop.getStop())
) {
linkType = StopLinkType.WALK_AND_CAR;
}
linkStopToStreetNetwork(tStop, linkType);
//noinspection Convert2MethodRef
progress.step(m -> LOG.info(m));
}
LOG.info(progress.completeMessage());
}
/**
* Determines if a given transit stop vertex is already linked to the street network, taking into
* account that flex stops need special linking to both a walkable and drivable edge. For example,
* the {@link OsmBoardingLocationsModule}, which runs before this one, often links stops to
* walkable edges only.
*
* @param stopVertex The transit stop vertex to be checked.
* @param stopLocationsUsedForFlexTrips A set of stop locations that are used for flexible trips.
*/
private static boolean isAlreadyLinked(
TransitStopVertex stopVertex,
Set stopLocationsUsedForFlexTrips
) {
if (stopLocationsUsedForFlexTrips.contains(stopVertex.getStop())) {
return stopVertex.isLinkedToDrivableEdge() && stopVertex.isLinkedToWalkableEdge();
} else {
return stopVertex.isConnectedToGraph();
}
}
/**
* Link a stop to the nearest "relevant" edges.
*
* These are mostly walk edges but if a stop is used by a flex pattern it also needs to be
* car-accessible. Therefore, flex stops are ensured to be connected to the car-accessible
* edge. This may lead to several links being created.
*/
private void linkStopToStreetNetwork(TransitStopVertex tStop, StopLinkType linkType) {
graph
.getLinker()
.linkVertexPermanently(
tStop,
WALK_ONLY,
LinkingDirection.BIDIRECTIONAL,
(transitVertex, streetVertex) -> {
var linkEdges = createStopLinkEdges((TransitStopVertex) transitVertex, streetVertex);
if (linkType == StopLinkType.WALK_AND_CAR && !streetVertex.isConnectedToDriveableEdge()) {
linkToDriveableEdge(tStop);
}
return linkEdges;
}
);
}
/**
* If regular stops or group stops are used for flex trips, they also need to be connected to car
* routable street edges.
*
* This does not apply to zones as street vertices store which zones they are part of.
*
* @see https://github.com/opentripplanner/OpenTripPlanner/issues/5498
*/
private void linkToDriveableEdge(TransitStopVertex tStop) {
graph
.getLinker()
.linkVertexPermanently(
tStop,
CAR_ONLY,
LinkingDirection.BIDIRECTIONAL,
(transitVertex, streetVertex) ->
createStopLinkEdges((TransitStopVertex) transitVertex, streetVertex)
);
}
private static List createStopLinkEdges(
TransitStopVertex vertex,
StreetVertex streetVertex
) {
return List.of(
StreetTransitStopLink.createStreetTransitStopLink(vertex, streetVertex),
StreetTransitStopLink.createStreetTransitStopLink(streetVertex, vertex)
);
}
private static void linkVehicleParkingWithLinker(
Graph graph,
VehicleParkingEntranceVertex vehicleParkingVertex
) {
if (vehicleParkingVertex.isWalkAccessible()) {
graph
.getLinker()
.linkVertexPermanently(
vehicleParkingVertex,
new TraverseModeSet(TraverseMode.WALK),
LinkingDirection.BIDIRECTIONAL,
(vertex, streetVertex) ->
List.of(
StreetVehicleParkingLink.createStreetVehicleParkingLink(
(VehicleParkingEntranceVertex) vertex,
streetVertex
),
StreetVehicleParkingLink.createStreetVehicleParkingLink(
streetVertex,
(VehicleParkingEntranceVertex) vertex
)
)
);
}
if (vehicleParkingVertex.isCarAccessible()) {
graph
.getLinker()
.linkVertexPermanently(
vehicleParkingVertex,
new TraverseModeSet(TraverseMode.CAR),
LinkingDirection.BIDIRECTIONAL,
(vertex, streetVertex) ->
List.of(
StreetVehicleParkingLink.createStreetVehicleParkingLink(
(VehicleParkingEntranceVertex) vertex,
streetVertex
),
StreetVehicleParkingLink.createStreetVehicleParkingLink(
streetVertex,
(VehicleParkingEntranceVertex) vertex
)
)
);
}
}
private void linkTransitEntrances(Graph graph) {
LOG.info("Linking transit entrances to graph...");
for (TransitEntranceVertex tEntrance : graph.getVerticesOfType(TransitEntranceVertex.class)) {
graph
.getLinker()
.linkVertexPermanently(
tEntrance,
new TraverseModeSet(TraverseMode.WALK),
LinkingDirection.BIDIRECTIONAL,
(vertex, streetVertex) ->
List.of(
StreetTransitEntranceLink.createStreetTransitEntranceLink(
(TransitEntranceVertex) vertex,
streetVertex
),
StreetTransitEntranceLink.createStreetTransitEntranceLink(
streetVertex,
(TransitEntranceVertex) vertex
)
)
);
}
}
private void linkStationCentroids(Graph graph) {
BiFunction> stationAndStreetVertexLinker = (
theStation,
streetVertex
) ->
List.of(
StreetStationCentroidLink.createStreetStationLink(
(StationCentroidVertex) theStation,
streetVertex
),
StreetStationCentroidLink.createStreetStationLink(
streetVertex,
(StationCentroidVertex) theStation
)
);
for (StationCentroidVertex station : graph.getVerticesOfType(StationCentroidVertex.class)) {
graph
.getLinker()
.linkVertexPermanently(
station,
new TraverseModeSet(TraverseMode.WALK),
LinkingDirection.BIDIRECTIONAL,
stationAndStreetVertexLinker
);
}
}
private void linkVehicleParks(Graph graph, DataImportIssueStore issueStore) {
LOG.info("Linking vehicle parks to graph...");
List vehicleParkingToRemove = new ArrayList<>();
for (VehicleParkingEntranceVertex vehicleParkingEntranceVertex : graph.getVerticesOfType(
VehicleParkingEntranceVertex.class
)) {
if (vehicleParkingEntranceVertex.isLinkedToGraph()) {
continue;
}
if (vehicleParkingEntranceVertex.getParkingEntrance().getVertex() == null) {
linkVehicleParkingWithLinker(graph, vehicleParkingEntranceVertex);
continue;
}
if (graph.containsVertex(vehicleParkingEntranceVertex.getParkingEntrance().getVertex())) {
VehicleParkingHelper.linkToGraph(vehicleParkingEntranceVertex);
continue;
}
issueStore.add(
new ParkAndRideEntranceRemoved(vehicleParkingEntranceVertex.getParkingEntrance())
);
var vehicleParking = removeVehicleParkingEntranceVertexFromGraph(
vehicleParkingEntranceVertex,
graph
);
if (vehicleParking != null) {
vehicleParkingToRemove.add(vehicleParking);
}
}
if (!vehicleParkingToRemove.isEmpty()) {
parkingRepository.updateVehicleParking(List.of(), vehicleParkingToRemove);
}
}
/**
* Removes vehicle parking entrance vertex from graph.
*
* @return vehicle parking for removal if the removed entrance was its only entrance.
*/
private VehicleParking removeVehicleParkingEntranceVertexFromGraph(
VehicleParkingEntranceVertex vehicleParkingEntranceVertex,
Graph graph
) {
var vehicleParkingEdge = vehicleParkingEntranceVertex
.getOutgoing()
.stream()
.filter(VehicleParkingEdge.class::isInstance)
.map(VehicleParkingEdge.class::cast)
.findFirst()
.orElseThrow(() ->
new IllegalStateException(
"VehicleParkingEdge missing from vertex: " + vehicleParkingEntranceVertex
)
);
var entrance = vehicleParkingEntranceVertex.getParkingEntrance();
var vehicleParking = vehicleParkingEdge.getVehicleParking();
boolean removeVehicleParking =
vehicleParking.getEntrances().size() == 1 &&
vehicleParking.getEntrances().get(0).equals(entrance);
vehicleParkingEntranceVertex.getIncoming().forEach(graph::removeEdge);
vehicleParkingEntranceVertex.getOutgoing().forEach(graph::removeEdge);
graph.remove(vehicleParkingEntranceVertex);
if (removeVehicleParking) {
return vehicleParking;
} else {
vehicleParking.getEntrances().remove(entrance);
return null;
}
}
private Set getStopLocationsUsedForFlexTrips(
TimetableRepository timetableRepository
) {
Set stopLocations = timetableRepository
.getAllFlexTrips()
.stream()
.flatMap(t -> t.getStops().stream())
.collect(Collectors.toSet());
stopLocations.addAll(
stopLocations
.stream()
.filter(GroupStop.class::isInstance)
.map(GroupStop.class::cast)
.flatMap(g -> g.getChildLocations().stream().filter(RegularStop.class::isInstance))
.toList()
);
return stopLocations;
}
private enum StopLinkType {
/**
* Only ensure that the link leads to a walkable edge.
* (The same edge may also be drivable but this is not guaranteed.)
*/
WALK_ONLY,
/**
* Make sure that the stop is linked to an edge each that is walkable and drivable.
* This may lead to several links being created.
*/
WALK_AND_CAR,
}
}