
org.opentripplanner.graph_builder.module.OsmBoardingLocationsModule Maven / Gradle / Ivy
package org.opentripplanner.graph_builder.module;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Point;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.framework.geometry.SphericalDistanceLibrary;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.framework.i18n.LocalizedString;
import org.opentripplanner.graph_builder.model.GraphBuilderModule;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.index.StreetIndex;
import org.opentripplanner.routing.linking.VertexLinker;
import org.opentripplanner.service.osminfo.OsmInfoGraphBuildService;
import org.opentripplanner.service.osminfo.model.Platform;
import org.opentripplanner.street.model.StreetTraversalPermission;
import org.opentripplanner.street.model.edge.Area;
import org.opentripplanner.street.model.edge.AreaEdge;
import org.opentripplanner.street.model.edge.BoardingLocationToStopLink;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.LinkingDirection;
import org.opentripplanner.street.model.edge.StreetEdge;
import org.opentripplanner.street.model.edge.StreetEdgeBuilder;
import org.opentripplanner.street.model.edge.StreetTransitStopLink;
import org.opentripplanner.street.model.vertex.OsmBoardingLocationVertex;
import org.opentripplanner.street.model.vertex.StreetVertex;
import org.opentripplanner.street.model.vertex.TransitStopVertex;
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.model.vertex.VertexFactory;
import org.opentripplanner.street.search.TraverseMode;
import org.opentripplanner.street.search.TraverseModeSet;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StationElement;
import org.opentripplanner.transit.service.TimetableRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This module takes advantage of the fact that in some cities, an authoritative linking location
* for GTFS stops is provided by tags in the OSM data.
*
* When OSM data is being loaded, certain entities that represent transit stops are made into
* {@link OsmBoardingLocationVertex} instances. In some cities, these nodes have a ref=* tag which
* gives the corresponding GTFS stop ID for the stop but the exact tag name is configurable. See
* the OSM wiki page.
*
* This module will attempt to link all transit stops and platforms to such nodes or way centroids
* in the OSM data, based on the stop ID or stop code and ref tag. It is run before the main transit
* stop linker, and if no linkage was created here, the main linker should create one based on
* distance or other heuristics.
*/
public class OsmBoardingLocationsModule implements GraphBuilderModule {
private static final Logger LOG = LoggerFactory.getLogger(OsmBoardingLocationsModule.class);
private static final LocalizedString LOCALIZED_PLATFORM_NAME = new LocalizedString(
"name.platform"
);
private final double searchRadiusDegrees = SphericalDistanceLibrary.metersToDegrees(250);
private final Graph graph;
private final OsmInfoGraphBuildService osmInfoGraphBuildService;
private final TimetableRepository timetableRepository;
private final VertexFactory vertexFactory;
private VertexLinker linker;
@Inject
public OsmBoardingLocationsModule(
Graph graph,
OsmInfoGraphBuildService osmInfoGraphBuildService,
TimetableRepository timetableRepository
) {
this.graph = graph;
this.osmInfoGraphBuildService = osmInfoGraphBuildService;
this.timetableRepository = timetableRepository;
this.vertexFactory = new VertexFactory(graph);
}
@Override
public void buildGraph() {
LOG.info("Improving boarding locations by checking OSM entities...");
StreetIndex streetIndex = graph.getStreetIndexSafe(timetableRepository.getSiteRepository());
this.linker = streetIndex.getVertexLinker();
int successes = 0;
for (TransitStopVertex ts : graph.getVerticesOfType(TransitStopVertex.class)) {
// if the street is already linked there is no need to linked it again,
// could happened if using the prune isolated island
boolean alreadyLinked = false;
for (Edge e : ts.getOutgoing()) {
if (e instanceof StreetTransitStopLink) {
alreadyLinked = true;
break;
}
}
if (alreadyLinked) continue;
// only connect transit stops that are not part of a pathway network
if (!ts.hasPathways()) {
if (!connectVertexToStop(ts, streetIndex)) {
LOG.debug("Could not connect {} at {}", ts.getStop().getCode(), ts.getCoordinate());
} else {
successes++;
}
}
}
LOG.info("Found {} OSM references which match a stop's id or code", successes);
}
private boolean connectVertexToStop(TransitStopVertex ts, StreetIndex index) {
if (connectVertexToNode(ts, index)) return true;
if (connectVertexToWay(ts, index)) return true;
return connectVertexToArea(ts, index);
}
private Envelope getEnvelope(TransitStopVertex ts) {
Envelope envelope = new Envelope(ts.getCoordinate());
double xscale = Math.cos((ts.getCoordinate().y * Math.PI) / 180);
envelope.expandBy(searchRadiusDegrees / xscale, searchRadiusDegrees);
return envelope;
}
/**
* Connect a transit stop vertex into a boarding location area in the index.
*
* A centroid vertex is generated and connected to the visibility vertices on the area edge.
*
* @return if the vertex has been connected
*/
private boolean connectVertexToArea(TransitStopVertex ts, StreetIndex index) {
RegularStop stop = ts.getStop();
var nearbyAreaGroups = index
.getEdgesForEnvelope(getEnvelope(ts))
.stream()
.filter(AreaEdge.class::isInstance)
.map(AreaEdge.class::cast)
.map(AreaEdge::getArea)
.collect(Collectors.toSet());
// Find a nearby area representing transit stop in OSM, linking to it if
// stop code or id in ref= tag matches the GTFS stop code of this StopVertex.
for (var areaGroup : nearbyAreaGroups) {
for (Area area : areaGroup.getAreas()) {
var platOpt = osmInfoGraphBuildService.findPlatform(area);
if (platOpt.isPresent()) {
var platform = platOpt.get();
if (matchesReference(stop, platform.references())) {
var boardingLocation = makeBoardingLocation(
stop,
platform.geometry().getCentroid(),
platform.references(),
area.getName()
);
linker.addPermanentAreaVertex(boardingLocation, areaGroup);
linkBoardingLocationToStop(ts, stop.getCode(), boardingLocation);
return true;
}
}
}
}
return false;
}
/**
* Connect a transit stop vertex to a boarding location way in the index.
*
* The vertex is connected to the center of the way if one is found, splitting it if needed.
*
* @return if the vertex has been connected
*/
private boolean connectVertexToWay(TransitStopVertex ts, StreetIndex index) {
var stop = ts.getStop();
var nearbyEdges = new HashMap>();
for (var edge : index.getEdgesForEnvelope(getEnvelope(ts))) {
osmInfoGraphBuildService
.findPlatform(edge)
.ifPresent(platform -> {
if (matchesReference(stop, platform.references())) {
if (!nearbyEdges.containsKey(platform)) {
var list = new ArrayList();
list.add(edge);
nearbyEdges.put(platform, list);
} else {
nearbyEdges.get(platform).add(edge);
}
}
});
}
for (var platformEdgeList : nearbyEdges.entrySet()) {
Platform platform = platformEdgeList.getKey();
var name = platform.name();
var boardingLocation = makeBoardingLocation(
stop,
platform.geometry().getCentroid(),
platform.references(),
name
);
for (var vertex : linker.linkToSpecificStreetEdgesPermanently(
boardingLocation,
new TraverseModeSet(TraverseMode.WALK),
LinkingDirection.BIDIRECTIONAL,
platformEdgeList.getValue().stream().map(StreetEdge.class::cast).collect(Collectors.toSet())
)) {
linkBoardingLocationToStop(ts, stop.getCode(), vertex);
}
return true;
}
return false;
}
/**
* Connect a transit stop vertex to a boarding location node.
*
* The node is generated in the OSM processing step but we need to link it here.
*
* @return If the vertex has been connected.
*/
private boolean connectVertexToNode(TransitStopVertex ts, StreetIndex index) {
var nearbyBoardingLocations = index
.getVerticesForEnvelope(getEnvelope(ts))
.stream()
.filter(OsmBoardingLocationVertex.class::isInstance)
.map(OsmBoardingLocationVertex.class::cast)
.collect(Collectors.toSet());
for (var boardingLocation : nearbyBoardingLocations) {
if (matchesReference(ts.getStop(), boardingLocation.references)) {
if (!boardingLocation.isConnectedToStreetNetwork()) {
linker.linkVertexPermanently(
boardingLocation,
new TraverseModeSet(TraverseMode.WALK),
LinkingDirection.BIDIRECTIONAL,
(osmBoardingLocationVertex, splitVertex) ->
getConnectingEdges(boardingLocation, osmBoardingLocationVertex, splitVertex)
);
}
linkBoardingLocationToStop(ts, ts.getStop().getCode(), boardingLocation);
return true;
}
}
return false;
}
private OsmBoardingLocationVertex makeBoardingLocation(
RegularStop stop,
Point centroid,
Set refs,
I18NString name
) {
var label = "platform-centroid/%s".formatted(stop.getId().toString());
return vertexFactory.osmBoardingLocation(
new Coordinate(centroid.getX(), centroid.getY()),
label,
refs,
name
);
}
private List getConnectingEdges(
OsmBoardingLocationVertex boardingLocation,
Vertex osmBoardingLocationVertex,
StreetVertex splitVertex
) {
if (osmBoardingLocationVertex == splitVertex) {
return List.of();
}
// the OSM boarding location vertex is not connected to the street network, so we
// need to link it first
return List.of(
linkBoardingLocationToStreetNetwork(boardingLocation, splitVertex),
linkBoardingLocationToStreetNetwork(splitVertex, boardingLocation)
);
}
private StreetEdge linkBoardingLocationToStreetNetwork(StreetVertex from, StreetVertex to) {
var line = GeometryUtils.makeLineString(List.of(from.getCoordinate(), to.getCoordinate()));
return new StreetEdgeBuilder<>()
.withFromVertex(from)
.withToVertex(to)
.withGeometry(line)
.withName(LOCALIZED_PLATFORM_NAME)
.withMeterLength(SphericalDistanceLibrary.length(line))
.withPermission(StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE)
.withBack(false)
.buildAndConnect();
}
private void linkBoardingLocationToStop(
TransitStopVertex ts,
@Nullable String stopCode,
StreetVertex boardingLocation
) {
BoardingLocationToStopLink.createBoardingLocationToStopLink(ts, boardingLocation);
BoardingLocationToStopLink.createBoardingLocationToStopLink(boardingLocation, ts);
LOG.debug(
"Connected {} ({}) to {} at {}",
ts,
stopCode,
boardingLocation.getLabel(),
boardingLocation.getCoordinate()
);
}
private boolean matchesReference(StationElement, ?> stop, Collection references) {
var stopCode = stop.getCode();
var stopId = stop.getId().getId();
return (stopCode != null && references.contains(stopCode)) || references.contains(stopId);
}
}