
org.opentripplanner.graph_builder.module.osm.WalkableAreaBuilder Maven / Gradle / Ivy
Show all versions of otp Show documentation
package org.opentripplanner.graph_builder.module.osm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.opentripplanner.astar.model.GraphPath;
import org.opentripplanner.astar.model.ShortestPathTree;
import org.opentripplanner.astar.spi.SkipEdgeStrategy;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.framework.geometry.SphericalDistanceLibrary;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.graph_builder.services.osm.EdgeNamer;
import org.opentripplanner.openstreetmap.model.OSMNode;
import org.opentripplanner.openstreetmap.model.OSMRelation;
import org.opentripplanner.openstreetmap.model.OSMRelationMember;
import org.opentripplanner.openstreetmap.model.OSMWithTags;
import org.opentripplanner.openstreetmap.wayproperty.WayProperties;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.StreetMode;
import org.opentripplanner.routing.api.request.request.StreetRequest;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.street.model.StreetTraversalPermission;
import org.opentripplanner.street.model.edge.AreaEdge;
import org.opentripplanner.street.model.edge.AreaEdgeBuilder;
import org.opentripplanner.street.model.edge.AreaEdgeList;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.NamedArea;
import org.opentripplanner.street.model.edge.StreetEdge;
import org.opentripplanner.street.model.vertex.IntersectionVertex;
import org.opentripplanner.street.model.vertex.OsmVertex;
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.model.vertex.VertexFactory;
import org.opentripplanner.street.search.StreetSearchBuilder;
import org.opentripplanner.street.search.state.State;
import org.opentripplanner.street.search.strategy.DominanceFunctions;
/**
* Theoretically, it is not correct to build the visibility graph on the joined polygon of areas
* with different levels of bike safety. That's because in the optimal path, you might end up
* changing direction at area boundaries. The problem is known as "weighted planar subdivisions",
* and the best known algorithm is O(N^3). That's not much worse than general visibility graph
* construction, but it would have to be done at runtime to account for the differences in bike
* safety preferences. Ted Chiang's "Story Of Your Life" describes how a very similar problem in
* optics gives rise to Snell's Law. It is the second-best story about a law of physics that I know
* of (Chiang's "Exhalation" is the first).
*
* Anyway, since we're not going to run an O(N^3) algorithm at runtime just to give people who don't
* understand Snell's Law weird paths that they can complain about, this should be just fine.
*
*
* TODO this approach could be replaced by building a walkable grid of edges for an area, so the
* number of edges for an area wouldn't be determined by the nodes. The current approach can lead
* to an excessive number of edges, or to no edges at all if maxAreaNodes is surpassed.
*/
class WalkableAreaBuilder {
private final DataImportIssueStore issueStore;
private final int maxAreaNodes;
private final Graph graph;
private final OsmDatabase osmdb;
private final Map wayPropertiesCache = new HashMap<>();
private final VertexGenerator vertexBuilder;
private final HashMap areaBoundaryVertexForCoordinate = new HashMap<>();
private final boolean platformEntriesLinking;
private final List platformLinkingEndpoints;
private final Set boardingLocationRefTags;
private final EdgeNamer namer;
private final SafetyValueNormalizer normalizer;
private final VertexFactory vertexFactory;
public WalkableAreaBuilder(
Graph graph,
OsmDatabase osmdb,
VertexGenerator vertexBuilder,
EdgeNamer namer,
SafetyValueNormalizer normalizer,
DataImportIssueStore issueStore,
int maxAreaNodes,
boolean platformEntriesLinking,
Set boardingLocationRefTags
) {
this.graph = graph;
this.osmdb = osmdb;
this.vertexBuilder = vertexBuilder;
this.namer = namer;
this.normalizer = normalizer;
this.issueStore = issueStore;
this.maxAreaNodes = maxAreaNodes;
this.platformEntriesLinking = platformEntriesLinking;
this.boardingLocationRefTags = boardingLocationRefTags;
this.platformLinkingEndpoints =
platformEntriesLinking
? graph
.getVertices()
.stream()
.filter(OsmVertex.class::isInstance)
.map(OsmVertex.class::cast)
.filter(this::isPlatformLinkingEndpoint)
.collect(Collectors.toList())
: List.of();
this.vertexFactory = new VertexFactory(graph);
}
/**
* For all areas just use outermost rings as edges so that areas can be routable without
* visibility calculations
*/
public void buildWithoutVisibility(AreaGroup group) {
var references = getStopReferences(group);
// create polygon and accumulate nodes for area
for (Ring ring : group.outermostRings) {
Set edges = new HashSet<>();
AreaEdgeList edgeList = new AreaEdgeList(ring.jtsPolygon, references);
// the points corresponding to concave or hole vertices
// or those linked to ways
HashSet alreadyAddedEdges = new HashSet<>();
// we also want to fill in the edges of this area anyway, because we can,
// and to avoid the numerical problems that they tend to cause
for (Area area : group.areas) {
if (!ring.jtsPolygon.contains(area.jtsMultiPolygon)) {
continue;
}
for (Ring outerRing : area.outermostRings) {
for (int i = 0; i < outerRing.nodes.size(); ++i) {
edges.addAll(
createEdgesForRingSegment(edgeList, area, outerRing, i, alreadyAddedEdges)
);
}
//TODO: is this actually needed?
for (Ring innerRing : outerRing.getHoles()) {
for (int j = 0; j < innerRing.nodes.size(); ++j) {
edges.addAll(
createEdgesForRingSegment(edgeList, area, innerRing, j, alreadyAddedEdges)
);
}
}
}
}
edges
.stream()
.flatMap(v ->
Stream
.of(v.getFromVertex(), v.getToVertex())
.filter(IntersectionVertex.class::isInstance)
.map(IntersectionVertex.class::cast)
)
.forEach(edgeList::addVisibilityVertex);
createNamedAreas(edgeList, ring, group.areas);
}
}
public void buildWithVisibility(AreaGroup group) {
// These sets contain the nodes/vertices which can be used to traverse from the rest of the
// street network onto the walkable area
Set startingNodes = new HashSet<>();
Set startingVertices = new HashSet<>();
// List of edges belonging to the walkable area
Set edges = new HashSet<>();
// Edges which are part of the rings. We want to keep there for linking even tough they
// might not be part of the visibility edges.
Set ringEdges = new HashSet<>();
// OSM ways that this area group consists of
Set osmWayIds = group.areas
.stream()
.map(area -> area.parent)
.flatMap(osmWithTags ->
osmWithTags instanceof OSMRelation
? ((OSMRelation) osmWithTags).getMembers().stream().map(OSMRelationMember::getRef)
: Stream.of(osmWithTags.getId())
)
.collect(Collectors.toSet());
var references = getStopReferences(group);
// create polygon and accumulate nodes for area
for (Ring ring : group.outermostRings) {
Polygon polygon = ring.jtsPolygon;
AreaEdgeList edgeList = new AreaEdgeList(polygon, references);
// the points corresponding to concave or hole vertices
// or those linked to ways
HashSet visibilityNodes = new HashSet<>();
HashSet alreadyAddedEdges = new HashSet<>();
HashSet platformLinkingVertices = new HashSet<>();
// we need to accumulate visibility points from all contained areas
// inside this ring, but only for shared nodes; we don't care about
// convexity, which we'll handle for the grouped area only.
GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
OSMWithTags areaEntity = group.getSomeOSMObject();
// we also want to fill in the edges of this area anyway, because we can,
// and to avoid the numerical problems that they tend to cause
for (Area area : group.areas) {
if (!polygon.contains(area.jtsMultiPolygon)) {
continue;
}
// Add stops/entrances from public transit relations into the area
// they may provide the only entrance to a platform
// which otherwise would be pruned as unconnected island
Collection entrances = osmdb.getStopsInArea(area.parent);
for (OSMNode node : entrances) {
var vertex = vertexBuilder.getVertexForOsmNode(node, areaEntity);
platformLinkingVertices.add(vertex);
visibilityNodes.add(node);
startingNodes.add(node);
edgeList.addVisibilityVertex(vertex);
}
for (Ring outerRing : area.outermostRings) {
// variable to indicate if some additional entrance points have been added to area
boolean linkPointsAdded = !entrances.isEmpty();
// Add unconnected entries to area if platformEntriesLinking parameter is true
if (platformEntriesLinking && area.parent.isPlatform()) {
List endpointsWithin = platformLinkingEndpoints
.stream()
.filter(t ->
outerRing.jtsPolygon.contains(geometryFactory.createPoint(t.getCoordinate()))
)
.toList();
platformLinkingVertices.addAll(endpointsWithin);
for (OsmVertex v : endpointsWithin) {
OSMNode node = osmdb.getNode(v.nodeId);
visibilityNodes.add(node);
startingNodes.add(node);
edgeList.addVisibilityVertex(v);
linkPointsAdded = true;
}
}
for (int i = 0; i < outerRing.nodes.size(); ++i) {
OSMNode node = outerRing.nodes.get(i);
Set newEdges = createEdgesForRingSegment(
edgeList,
area,
outerRing,
i,
alreadyAddedEdges
);
edges.addAll(newEdges);
ringEdges.addAll(newEdges);
// A node can only be a visibility node only if it is an entrance to the
// area or a convex point, i.e. the angle is over 180 degrees.
// Also, if additional linking points have been defined, add some points from outer
// edge to ensure that platform geometry gets connected
if (
outerRing.isNodeConvex(i) ||
(linkPointsAdded && (i == 0 || i == outerRing.nodes.size() / 2))
) {
visibilityNodes.add(node);
edgeList.addVisibilityVertex(vertexBuilder.getVertexForOsmNode(node, areaEntity));
}
if (isStartingNode(node, osmWayIds)) {
visibilityNodes.add(node);
startingNodes.add(node);
edgeList.addVisibilityVertex(vertexBuilder.getVertexForOsmNode(node, areaEntity));
}
}
for (Ring innerRing : outerRing.getHoles()) {
for (int j = 0; j < innerRing.nodes.size(); ++j) {
OSMNode node = innerRing.nodes.get(j);
edges.addAll(
createEdgesForRingSegment(edgeList, area, innerRing, j, alreadyAddedEdges)
);
// A node can only be a visibility node only if it is an entrance to the
// area or a convex point, i.e. the angle is over 180 degrees.
// For holes, the internal angle is calculated, so we must swap the sign
if (!innerRing.isNodeConvex(j)) {
visibilityNodes.add(node);
edgeList.addVisibilityVertex(vertexBuilder.getVertexForOsmNode(node, areaEntity));
}
if (isStartingNode(node, osmWayIds)) {
visibilityNodes.add(node);
startingNodes.add(node);
edgeList.addVisibilityVertex(vertexBuilder.getVertexForOsmNode(node, areaEntity));
}
}
}
}
}
if (edgeList.visibilityVertices().isEmpty()) {
issueStore.add(new UnconnectedArea(group));
// Area is not connected to graph. Remove it immediately before it causes any trouble.
for (Edge edge : edges) {
graph.removeEdge(edge);
}
continue;
}
createNamedAreas(edgeList, ring, group.areas);
if (visibilityNodes.size() > maxAreaNodes) {
issueStore.add(new AreaTooComplicated(group, visibilityNodes.size(), maxAreaNodes));
}
// if area is too complex, consider only part of visibility nodes
// so that at least some edges passing through the area is added
// otherwise routing can use only area boundary edges
float skip_ratio = (float) maxAreaNodes / (float) visibilityNodes.size();
int i = 0;
float sum_i = 0;
for (OSMNode nodeI : visibilityNodes) {
sum_i += skip_ratio;
if (Math.floor(sum_i) < i + 1) {
continue;
}
i = (int) Math.floor(sum_i);
IntersectionVertex startEndpoint = vertexBuilder.getVertexForOsmNode(nodeI, areaEntity);
if (startingNodes.contains(nodeI)) {
startingVertices.add(startEndpoint);
}
int j = 0;
float sum_j = 0;
for (OSMNode nodeJ : visibilityNodes) {
sum_j += skip_ratio;
if (Math.floor(sum_j) < j + 1) {
continue;
}
j = (int) Math.floor(sum_j);
NodeEdge edge = new NodeEdge(nodeI, nodeJ);
if (alreadyAddedEdges.contains(edge)) continue;
IntersectionVertex endEndpoint = vertexBuilder.getVertexForOsmNode(nodeJ, areaEntity);
Coordinate[] coordinates = new Coordinate[] {
startEndpoint.getCoordinate(),
endEndpoint.getCoordinate(),
};
LineString line = geometryFactory.createLineString(coordinates);
if (polygon.contains(line)) {
Set segments = createSegments(
startEndpoint,
endEndpoint,
group.areas,
edgeList
);
edges.addAll(segments);
if (platformLinkingVertices.contains(startEndpoint)) {
ringEdges.addAll(segments);
}
if (platformLinkingVertices.contains(endEndpoint)) {
ringEdges.addAll(segments);
}
}
}
}
}
pruneAreaEdges(startingVertices, edges, ringEdges);
}
private Set getStopReferences(AreaGroup group) {
return group.areas
.stream()
.filter(g -> g.parent.isBoardingLocation())
.flatMap(g -> g.parent.getMultiTagValues(boardingLocationRefTags).stream())
.collect(Collectors.toSet());
}
/**
* Do an all-pairs shortest path search from a list of vertices over a specified set of edges, and
* retain only those edges which are actually used in some shortest path.
*/
private void pruneAreaEdges(
Collection startingVertices,
Set edges,
Set edgesToKeep
) {
if (edges.isEmpty()) return;
StreetMode mode;
StreetEdge firstEdge = (StreetEdge) edges.iterator().next();
if (firstEdge.getPermission().allows(StreetTraversalPermission.PEDESTRIAN)) {
mode = StreetMode.WALK;
} else if (firstEdge.getPermission().allows(StreetTraversalPermission.BICYCLE)) {
mode = StreetMode.BIKE;
} else {
mode = StreetMode.CAR;
}
RouteRequest options = new RouteRequest();
Set usedEdges = new HashSet<>();
for (Vertex vertex : startingVertices) {
ShortestPathTree spt = StreetSearchBuilder
.of()
.setSkipEdgeStrategy(new ListedEdgesOnly(edges))
.setDominanceFunction(new DominanceFunctions.EarliestArrival())
.setRequest(options)
.setStreetRequest(new StreetRequest(mode))
.setFrom(vertex)
.getShortestPathTree();
for (Vertex endVertex : startingVertices) {
GraphPath path = spt.getPath(endVertex);
if (path != null) {
usedEdges.addAll(path.edges);
}
}
}
for (Edge edge : edges) {
if (!usedEdges.contains(edge) && !edgesToKeep.contains(edge)) {
graph.removeEdge(edge);
}
}
}
private boolean isStartingNode(OSMNode node, Set osmWayIds) {
return (
osmdb.isNodeBelongsToWay(node.getId()) ||
// Do not add if part of same areaGroup
!osmdb
.getAreasForNode(node.getId())
.stream()
.allMatch(osmWay -> osmWayIds.contains(osmWay.getId())) ||
node.isBoardingLocation()
);
}
private Set createEdgesForRingSegment(
AreaEdgeList edgeList,
Area area,
Ring ring,
int i,
HashSet alreadyAddedEdges
) {
OSMNode node = ring.nodes.get(i);
OSMNode nextNode = ring.nodes.get((i + 1) % ring.nodes.size());
NodeEdge nodeEdge = new NodeEdge(node, nextNode);
if (alreadyAddedEdges.contains(nodeEdge)) {
return Set.of();
}
alreadyAddedEdges.add(nodeEdge);
IntersectionVertex startEndpoint = vertexBuilder.getVertexForOsmNode(node, area.parent);
IntersectionVertex endEndpoint = vertexBuilder.getVertexForOsmNode(nextNode, area.parent);
return createSegments(startEndpoint, endEndpoint, List.of(area), edgeList);
}
private Set createSegments(
IntersectionVertex startEndpoint,
IntersectionVertex endEndpoint,
Collection areas,
AreaEdgeList edgeList
) {
List intersects = new ArrayList<>();
Coordinate[] coordinates = new Coordinate[] {
startEndpoint.getCoordinate(),
endEndpoint.getCoordinate(),
};
GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
LineString line = geometryFactory.createLineString(coordinates);
for (Area area : areas) {
MultiPolygon polygon = area.jtsMultiPolygon;
Geometry intersection = polygon.intersection(line);
if (intersection.getLength() > 0.000001) {
intersects.add(area);
}
}
if (intersects.isEmpty()) {
// apparently our intersection here was bogus
return Set.of();
}
// do we need to recurse?
if (intersects.size() == 1) {
Area area = intersects.getFirst();
OSMWithTags areaEntity = area.parent;
StreetTraversalPermission areaPermissions = areaEntity.overridePermissions(
StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE
);
float carSpeed = areaEntity
.getOsmProvider()
.getOsmTagMapper()
.getCarSpeedForWay(areaEntity, false);
double length = SphericalDistanceLibrary.distance(
startEndpoint.getCoordinate(),
endEndpoint.getCoordinate()
);
String label =
"way (area) " +
areaEntity.getId() +
" from " +
startEndpoint.getLabel() +
" to " +
endEndpoint.getLabel();
I18NString name = namer.getNameForWay(areaEntity, label);
AreaEdgeBuilder streetEdgeBuilder = new AreaEdgeBuilder()
.withFromVertex(startEndpoint)
.withToVertex(endEndpoint)
.withGeometry(line)
.withName(name)
.withMeterLength(length)
.withPermission(areaPermissions)
.withBack(false)
.withArea(edgeList)
.withCarSpeed(carSpeed)
.withBogusName(areaEntity.hasNoName())
.withWheelchairAccessible(areaEntity.isWheelchairAccessible())
.withLink(areaEntity.isLink());
label =
"way (area) " +
areaEntity.getId() +
" from " +
endEndpoint.getLabel() +
" to " +
startEndpoint.getLabel();
name = namer.getNameForWay(areaEntity, label);
AreaEdgeBuilder backStreetEdgeBuilder = new AreaEdgeBuilder()
.withFromVertex(endEndpoint)
.withToVertex(startEndpoint)
.withGeometry(line.reverse())
.withName(name)
.withMeterLength(length)
.withPermission(areaPermissions)
.withBack(true)
.withArea(edgeList)
.withCarSpeed(carSpeed)
.withBogusName(areaEntity.hasNoName())
.withWheelchairAccessible(areaEntity.isWheelchairAccessible())
.withLink(areaEntity.isLink());
if (!wayPropertiesCache.containsKey(areaEntity)) {
WayProperties wayData = areaEntity
.getOsmProvider()
.getWayPropertySet()
.getDataForWay(areaEntity);
wayPropertiesCache.put(areaEntity, wayData);
}
AreaEdge street = streetEdgeBuilder.buildAndConnect();
AreaEdge backStreet = backStreetEdgeBuilder.buildAndConnect();
normalizer.applyWayProperties(
street,
backStreet,
wayPropertiesCache.get(areaEntity),
areaEntity
);
return Set.of(street, backStreet);
} else {
// take the part that intersects with the start vertex
Coordinate startCoordinate = startEndpoint.getCoordinate();
Point startPoint = geometryFactory.createPoint(startCoordinate);
Set edges = new HashSet<>();
for (Area area : intersects) {
MultiPolygon polygon = area.jtsMultiPolygon;
if (
!(polygon.intersects(startPoint) || polygon.getBoundary().intersects(startPoint))
) continue;
Geometry lineParts = line.intersection(polygon);
if (lineParts.getLength() > 0.000001) {
Coordinate edgeCoordinate = null;
// this is either a LineString or a MultiLineString (we hope)
if (lineParts instanceof MultiLineString mls) {
boolean found = false;
for (int i = 0; i < mls.getNumGeometries(); ++i) {
LineString segment = (LineString) mls.getGeometryN(i);
if (found) {
edgeCoordinate = segment.getEndPoint().getCoordinate();
break;
}
if (segment.contains(startPoint) || segment.getBoundary().contains(startPoint)) {
found = true;
if (segment.getLength() > 0.000001) {
edgeCoordinate = segment.getEndPoint().getCoordinate();
break;
}
}
}
} else if (lineParts instanceof LineString lineString) {
edgeCoordinate = lineString.getEndPoint().getCoordinate();
} else {
continue;
}
IntersectionVertex newEndpoint = areaBoundaryVertexForCoordinate.get(edgeCoordinate);
if (newEndpoint == null) {
newEndpoint = vertexFactory.intersection(edgeCoordinate);
areaBoundaryVertexForCoordinate.put(edgeCoordinate, newEndpoint);
}
edges.addAll(createSegments(startEndpoint, newEndpoint, List.of(area), edgeList));
edges.addAll(createSegments(newEndpoint, endEndpoint, intersects, edgeList));
return edges;
}
}
}
return Set.of();
}
private void createNamedAreas(AreaEdgeList edgeList, Ring ring, Collection areas) {
Polygon containingArea = ring.jtsPolygon;
for (Area area : areas) {
Geometry intersection = containingArea.intersection(area.jtsMultiPolygon);
if (intersection.getArea() == 0) {
continue;
}
NamedArea namedArea = new NamedArea();
OSMWithTags areaEntity = area.parent;
String id = "way (area) " + areaEntity.getId() + " (splitter linking)";
I18NString name = namer.getNameForWay(areaEntity, id);
namedArea.setName(name);
if (!wayPropertiesCache.containsKey(areaEntity)) {
WayProperties wayData = areaEntity
.getOsmProvider()
.getWayPropertySet()
.getDataForWay(areaEntity);
wayPropertiesCache.put(areaEntity, wayData);
}
double bicycleSafety = wayPropertiesCache.get(areaEntity).bicycleSafety().forward();
namedArea.setBicycleSafetyMultiplier(bicycleSafety);
double walkSafety = wayPropertiesCache.get(areaEntity).walkSafety().forward();
namedArea.setWalkSafetyMultiplier(walkSafety);
namedArea.setOriginalEdges(intersection);
StreetTraversalPermission permission = areaEntity.overridePermissions(
StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE
);
namedArea.setPermission(permission);
edgeList.addArea(namedArea);
}
}
private boolean isPlatformLinkingEndpoint(OsmVertex osmVertex) {
boolean isCandidate = false;
Vertex start = null;
for (Edge e : osmVertex.getIncoming()) {
if (e instanceof StreetEdge se && !(e instanceof AreaEdge)) {
if (Arrays.asList(1, 2, 3).contains(se.getPermission().code)) {
isCandidate = true;
start = se.getFromVertex();
break;
}
}
}
if (isCandidate && start != null) {
boolean isEndpoint = true;
for (Edge se : osmVertex.getOutgoing()) {
if (
!se.getToVertex().getCoordinate().equals(start.getCoordinate()) &&
!(se instanceof AreaEdge)
) {
isEndpoint = false;
}
}
return isEndpoint;
}
return false;
}
record ListedEdgesOnly(Set edges) implements SkipEdgeStrategy {
@Override
public boolean shouldSkipEdge(State current, Edge edge) {
return !edges.contains(edge);
}
}
private record NodeEdge(OSMNode from, OSMNode to) {}
}