
org.opentripplanner.graph_builder.module.osm.WalkableAreaBuilder Maven / Gradle / Ivy
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.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.common.model.P2;
import org.opentripplanner.graph_builder.DataImportIssueStore;
import org.opentripplanner.graph_builder.issues.AreaTooComplicated;
import org.opentripplanner.graph_builder.module.osm.OpenStreetMapModule.Handler;
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.routing.algorithm.astar.AStarBuilder;
import org.opentripplanner.routing.algorithm.astar.strategies.SkipEdgeStrategy;
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.core.State;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.edgetype.AreaEdge;
import org.opentripplanner.routing.edgetype.AreaEdgeList;
import org.opentripplanner.routing.edgetype.NamedArea;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.edgetype.StreetTraversalPermission;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.spt.DominanceFunction;
import org.opentripplanner.routing.spt.GraphPath;
import org.opentripplanner.routing.spt.ShortestPathTree;
import org.opentripplanner.routing.vertextype.IntersectionVertex;
import org.opentripplanner.routing.vertextype.OsmVertex;
import org.opentripplanner.transit.model.basic.I18NString;
import org.opentripplanner.util.geometry.GeometryUtils;
/**
* 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.
*/
public class WalkableAreaBuilder {
private final DataImportIssueStore issueStore;
private final int maxAreaNodes;
private final Graph graph;
private final OSMDatabase osmdb;
private final Map wayPropertiesCache = new HashMap<>();
// This is an awful hack, but this class (WalkableAreaBuilder) ought to be rewritten.
private final Handler handler;
private final HashMap areaBoundaryVertexForCoordinate = new HashMap<>();
private final boolean platformEntriesLinking;
private final List platformLinkingEndpoints;
private final Set boardingLocationRefTags;
public WalkableAreaBuilder(
Graph graph,
OSMDatabase osmdb,
Handler handler,
DataImportIssueStore issueStore,
int maxAreaNodes,
boolean platformEntriesLinking,
Set boardingLocationRefTags
) {
this.graph = graph;
this.osmdb = osmdb;
this.handler = handler;
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();
}
/**
* 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.visibilityVertices::add);
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 from public transit relations into the area
Collection nodes = osmdb.getStopsInArea(area.parent);
if (nodes != null) {
for (OSMNode node : nodes) {
var vertex = handler.getVertexForOsmNode(node, areaEntity);
platformLinkingVertices.add(vertex);
visibilityNodes.add(node);
startingNodes.add(node);
edgeList.visibilityVertices.add(vertex);
}
}
for (Ring outerRing : area.outermostRings) {
// Add unconnected entries to area if platformEntriesLinking parameter is true
if (platformEntriesLinking && "platform".equals(area.parent.getTag("public_transport"))) {
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.visibilityVertices.add(v);
}
}
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.
if (outerRing.isNodeConvex(i)) {
visibilityNodes.add(node);
}
if (isStartingNode(node, osmWayIds)) {
visibilityNodes.add(node);
startingNodes.add(node);
edgeList.visibilityVertices.add(handler.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);
}
if (isStartingNode(node, osmWayIds)) {
visibilityNodes.add(node);
startingNodes.add(node);
edgeList.visibilityVertices.add(handler.getVertexForOsmNode(node, areaEntity));
}
}
}
}
}
// FIXME: temporary hard limit on size of
// areas to prevent way explosion
if (polygon.getNumPoints() > maxAreaNodes) {
issueStore.add(
new AreaTooComplicated(
group.getSomeOSMObject().getId(),
visibilityNodes.size(),
maxAreaNodes
)
);
continue;
}
if (edgeList.visibilityVertices.size() == 0) {
issueStore.add(
"UnconnectedArea",
"Area %s has no connection to street network",
osmWayIds.stream().map(Object::toString).collect(Collectors.joining(", "))
);
}
createNamedAreas(edgeList, ring, group.areas);
for (OSMNode nodeI : visibilityNodes) {
IntersectionVertex startEndpoint = handler.getVertexForOsmNode(nodeI, areaEntity);
if (startingNodes.contains(nodeI)) {
startingVertices.add(startEndpoint);
}
for (OSMNode nodeJ : visibilityNodes) {
P2 nodePair = new P2<>(nodeI, nodeJ);
if (alreadyAddedEdges.contains(nodePair)) continue;
IntersectionVertex endEndpoint = handler.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.size() == 0) 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 = AStarBuilder
.allDirections(new ListedEdgesOnly(edges))
.setDominanceFunction(new DominanceFunction.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());
P2 nodePair = new P2<>(node, nextNode);
if (alreadyAddedEdges.contains(nodePair)) {
return Set.of();
}
alreadyAddedEdges.add(nodePair);
IntersectionVertex startEndpoint = handler.getVertexForOsmNode(node, area.parent);
IntersectionVertex endEndpoint = handler.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.size() == 0) {
// apparently our intersection here was bogus
return Set.of();
}
// do we need to recurse?
if (intersects.size() == 1) {
Area area = intersects.get(0);
OSMWithTags areaEntity = area.parent;
StreetTraversalPermission areaPermissions = OSMFilter.getPermissionsForEntity(
areaEntity,
StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE
);
float carSpeed = areaEntity
.getOsmProvider()
.getWayPropertySet()
.getCarSpeedForWay(areaEntity, false);
double length = SphericalDistanceLibrary.distance(
startEndpoint.getCoordinate(),
endEndpoint.getCoordinate()
);
int cls = StreetEdge.CLASS_OTHERPATH;
cls |= OSMFilter.getStreetClasses(areaEntity);
String label =
"way (area) " +
areaEntity.getId() +
" from " +
startEndpoint.getLabel() +
" to " +
endEndpoint.getLabel();
I18NString name = handler.getNameForWay(areaEntity, label);
AreaEdge street = new AreaEdge(
startEndpoint,
endEndpoint,
line,
name,
length,
areaPermissions,
false,
edgeList
);
street.setCarSpeed(carSpeed);
if (!areaEntity.hasTag("name") && !areaEntity.hasTag("ref")) {
street.setHasBogusName(true);
}
if (areaEntity.isTagFalse("wheelchair")) {
street.setWheelchairAccessible(false);
}
street.setStreetClass(cls);
label =
"way (area) " +
areaEntity.getId() +
" from " +
endEndpoint.getLabel() +
" to " +
startEndpoint.getLabel();
name = handler.getNameForWay(areaEntity, label);
AreaEdge backStreet = new AreaEdge(
endEndpoint,
startEndpoint,
line.reverse(),
name,
length,
areaPermissions,
true,
edgeList
);
backStreet.setCarSpeed(carSpeed);
if (!areaEntity.hasTag("name") && !areaEntity.hasTag("ref")) {
backStreet.setHasBogusName(true);
}
if (areaEntity.isTagFalse("wheelchair")) {
backStreet.setWheelchairAccessible(false);
}
backStreet.setStreetClass(cls);
if (!wayPropertiesCache.containsKey(areaEntity)) {
WayProperties wayData = areaEntity
.getOsmProvider()
.getWayPropertySet()
.getDataForWay(areaEntity);
wayPropertiesCache.put(areaEntity, wayData);
}
handler.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) {
MultiLineString mls = (MultiLineString) lineParts;
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) {
edgeCoordinate = ((LineString) lineParts).getEndPoint().getCoordinate();
} else {
continue;
}
IntersectionVertex newEndpoint = areaBoundaryVertexForCoordinate.get(edgeCoordinate);
if (newEndpoint == null) {
newEndpoint =
new IntersectionVertex(
graph,
"area splitter at " + edgeCoordinate,
edgeCoordinate.x,
edgeCoordinate.y
);
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;
int cls = StreetEdge.CLASS_OTHERPATH;
cls |= OSMFilter.getStreetClasses(areaEntity);
namedArea.setStreetClass(cls);
String id = "way (area) " + areaEntity.getId() + " (splitter linking)";
I18NString name = handler.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)
.getBicycleSafetyFeatures()
.forward();
namedArea.setBicycleSafetyMultiplier(bicycleSafety);
Double walkSafety = wayPropertiesCache.get(areaEntity).getWalkSafetyFeatures().forward();
namedArea.setWalkSafetyMultiplier(walkSafety);
namedArea.setOriginalEdges(intersection);
StreetTraversalPermission permission = OSMFilter.getPermissionsForEntity(
areaEntity,
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 && !(e instanceof AreaEdge)) {
StreetEdge se = (StreetEdge) e;
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);
}
}
}