
org.opentripplanner.graph_builder.module.osm.WalkableAreaBuilder Maven / Gradle / Ivy
The newest version!
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.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.osm.model.OsmEntity;
import org.opentripplanner.osm.model.OsmNode;
import org.opentripplanner.osm.model.OsmRelation;
import org.opentripplanner.osm.model.OsmRelationMember;
import org.opentripplanner.osm.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.service.osminfo.OsmInfoGraphBuildRepository;
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.AreaEdgeBuilder;
import org.opentripplanner.street.model.edge.AreaGroup;
import org.opentripplanner.street.model.edge.Edge;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class WalkableAreaBuilder {
private final DataImportIssueStore issueStore;
private final int maxAreaNodes;
private final Graph graph;
private final OsmDatabase osmdb;
private final OsmInfoGraphBuildRepository osmInfoGraphBuildRepository;
private final Map wayPropertiesCache = new HashMap<>();
private final VertexGenerator vertexBuilder;
private final HashMap areaBoundaryVertexForCoordinate =
new HashMap<>();
private final boolean platformEntriesLinking;
private final List platformLinkingPoints;
private final Set boardingLocationRefTags;
private final EdgeNamer namer;
private final SafetyValueNormalizer normalizer;
// template for AreaEdge names
private static final String labelTemplate = "way (area) %s from %s to %s";
private static final Logger LOG = LoggerFactory.getLogger(WalkableAreaBuilder.class);
public WalkableAreaBuilder(
Graph graph,
OsmDatabase osmdb,
OsmInfoGraphBuildRepository osmInfoGraphBuildRepository,
VertexGenerator vertexBuilder,
EdgeNamer namer,
SafetyValueNormalizer normalizer,
DataImportIssueStore issueStore,
int maxAreaNodes,
boolean platformEntriesLinking,
Set boardingLocationRefTags
) {
this.graph = graph;
this.osmdb = osmdb;
this.osmInfoGraphBuildRepository = osmInfoGraphBuildRepository;
this.vertexBuilder = vertexBuilder;
this.namer = namer;
this.normalizer = normalizer;
this.issueStore = issueStore;
this.maxAreaNodes = maxAreaNodes;
this.platformEntriesLinking = platformEntriesLinking;
this.boardingLocationRefTags = boardingLocationRefTags;
this.platformLinkingPoints = platformEntriesLinking
? graph
.getVertices()
.stream()
.filter(OsmVertex.class::isInstance)
.map(OsmVertex.class::cast)
.filter(this::isPlatformLinkingPoint)
.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(OsmAreaGroup group) {
// create polygon and accumulate nodes for area
for (Ring ring : group.outermostRings) {
Set edges = new HashSet<>();
AreaGroup areaGroup = new AreaGroup(ring.jtsPolygon);
HashSet alreadyAddedEdges = new HashSet<>();
for (OsmArea 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(areaGroup, area, outerRing, i, alreadyAddedEdges)
);
}
for (Ring innerRing : outerRing.getHoles()) {
for (int j = 0; j < innerRing.nodes.size(); ++j) {
edges.addAll(
createEdgesForRingSegment(areaGroup, area, innerRing, j, alreadyAddedEdges)
);
}
}
}
}
var vertices = edges
.stream()
.flatMap(v ->
Stream.of(v.getFromVertex(), v.getToVertex())
.filter(IntersectionVertex.class::isInstance)
.map(IntersectionVertex.class::cast)
)
.collect(Collectors.toSet());
areaGroup.addVisibilityVertices(vertices);
createAreas(areaGroup, ring, group.areas);
}
}
public void buildWithVisibility(OsmAreaGroup 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 startingVertices = new HashSet<>();
// List of edges belonging to the walkable area
Set edges = new HashSet<>();
// Edges which are part of the rings
Set ringEdges = new HashSet<>();
// OSM ways that this area group consists of
Set osmWayIds = group.areas
.stream()
.map(area -> area.parent)
.flatMap(osmEntity ->
osmEntity instanceof OsmRelation relation
? relation.getMembers().stream().map(OsmRelationMember::getRef)
: Stream.of(osmEntity.getId())
)
.collect(Collectors.toSet());
// create polygon and accumulate nodes for area
for (Ring ring : group.outermostRings) {
Polygon polygon = ring.jtsPolygon;
AreaGroup areaGroup = new AreaGroup(polygon);
// the points corresponding to concave or hole vertices or those linked to ways
HashSet alreadyAddedEdges = new HashSet<>();
HashSet platformLinkingVertices = new HashSet<>();
HashSet visibilityVertices = new HashSet<>();
GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
OsmEntity areaEntity = group.getSomeOsmObject();
for (OsmArea area : group.areas) {
// test if area is inside the current ring
if (!group.isSimpleAreaGroup()) {
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);
visibilityVertices.add(vertex);
startingVertices.add(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 verticesWithin = platformLinkingPoints
.stream()
.filter(t ->
outerRing.jtsPolygon.contains(geometryFactory.createPoint(t.getCoordinate()))
)
.toList();
platformLinkingVertices.addAll(verticesWithin);
for (OsmVertex v : verticesWithin) {
startingVertices.add(v);
visibilityVertices.add(v);
linkPointsAdded = true;
}
}
for (int i = 0; i < outerRing.nodes.size(); ++i) {
OsmNode node = outerRing.nodes.get(i);
Set newEdges = createEdgesForRingSegment(
areaGroup,
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))
) {
visibilityVertices.add(vertexBuilder.getVertexForOsmNode(node, areaEntity));
}
if (isStartingNode(node, osmWayIds)) {
var v = vertexBuilder.getVertexForOsmNode(node, areaEntity);
startingVertices.add(v);
visibilityVertices.add(v);
}
}
for (Ring innerRing : outerRing.getHoles()) {
for (int j = 0; j < innerRing.nodes.size(); ++j) {
OsmNode node = innerRing.nodes.get(j);
var newEdges = createEdgesForRingSegment(
areaGroup,
area,
innerRing,
j,
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.
// For holes, we must swap the convexity condition
if (!innerRing.isNodeConvex(j)) {
visibilityVertices.add(vertexBuilder.getVertexForOsmNode(node, areaEntity));
}
if (isStartingNode(node, osmWayIds)) {
var v = vertexBuilder.getVertexForOsmNode(node, areaEntity);
startingVertices.add(v);
visibilityVertices.add(v);
}
}
}
}
}
if (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;
}
areaGroup.addVisibilityVertices(visibilityVertices);
createAreas(areaGroup, ring, group.areas);
if (visibilityVertices.size() > maxAreaNodes) {
issueStore.add(new AreaTooComplicated(group, visibilityVertices.size(), maxAreaNodes));
}
// if area is too complex, consider only part of visibility nodes
// so that at least some edges passing through the area are added
// otherwise routing can use only area boundary edges
float skip_ratio = (float) maxAreaNodes / (float) visibilityVertices.size();
int i = 0;
float sum_i = 0;
for (IntersectionVertex vertex1 : visibilityVertices) {
sum_i += skip_ratio;
if (Math.floor(sum_i) < i + 1) {
continue;
}
i = (int) Math.floor(sum_i);
int j = 0;
float sum_j = 0;
for (IntersectionVertex vertex2 : visibilityVertices) {
sum_j += skip_ratio;
if (Math.floor(sum_j) < j + 1) {
continue;
}
j = (int) Math.floor(sum_j);
if (shouldSkipEdge(vertex1, vertex2, alreadyAddedEdges)) {
continue;
}
Coordinate[] coordinates = new Coordinate[] {
vertex1.getCoordinate(),
vertex2.getCoordinate(),
};
LineString line = geometryFactory.createLineString(coordinates);
if (polygon.contains(line)) {
Set segments = createSegments(vertex1, vertex2, group.areas, areaGroup, true);
edges.addAll(segments);
if (platformLinkingVertices.contains(vertex1)) {
ringEdges.addAll(segments);
}
if (platformLinkingVertices.contains(vertex2)) {
ringEdges.addAll(segments);
}
}
}
}
}
pruneAreaEdges(startingVertices, edges, ringEdges);
}
/**
* 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 WayProperties findAreaProperties(OsmEntity entity) {
if (!wayPropertiesCache.containsKey(entity)) {
var wayData = entity.getOsmProvider().getWayPropertySet().getDataForWay(entity);
wayPropertiesCache.put(entity, wayData);
return wayData;
} else {
return wayPropertiesCache.get(entity);
}
}
private Set createEdgesForRingSegment(
AreaGroup areaGroup,
OsmArea area,
Ring ring,
int i,
HashSet alreadyAddedEdges
) {
OsmNode node = ring.nodes.get(i);
OsmNode nextNode = ring.nodes.get((i + 1) % ring.nodes.size());
IntersectionVertex v1 = vertexBuilder.getVertexForOsmNode(node, area.parent);
IntersectionVertex v2 = vertexBuilder.getVertexForOsmNode(nextNode, area.parent);
if (shouldSkipEdge(v1, v2, alreadyAddedEdges)) {
return Set.of();
}
return createSegments(v1, v2, List.of(area), areaGroup, false);
}
private Set createSegments(
IntersectionVertex vertex1,
IntersectionVertex vertex2,
Collection areas,
AreaGroup areaGroup,
boolean testIntersection
) {
Coordinate[] coordinates = new Coordinate[] {
vertex1.getCoordinate(),
vertex2.getCoordinate(),
};
double length = SphericalDistanceLibrary.distance(
vertex1.getCoordinate(),
vertex2.getCoordinate()
);
if (length < 0.01) {
// vertex1 and vertex2 are in the same position
return Set.of();
}
GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
LineString line = geometryFactory.createLineString(coordinates);
OsmEntity parent = null;
WayProperties wayData = null;
StreetTraversalPermission areaPermissions = StreetTraversalPermission.ALL;
boolean wheelchairAccessible = true;
// combine properties of intersected areas
for (OsmArea area : areas) {
MultiPolygon polygon = area.jtsMultiPolygon;
boolean crosses = testIntersection ? polygon.intersection(line).getLength() > 0.000001 : true;
if (crosses) {
parent = area.parent;
wayData = findAreaProperties(parent);
areaPermissions = areaPermissions.intersection(wayData.getPermission());
wheelchairAccessible = wheelchairAccessible && parent.isWheelchairAccessible();
}
}
if (parent == null) {
// No intersections - not really possible
return Set.of();
}
String label = String.format(
labelTemplate,
parent.getId(),
vertex1.getLabel(),
vertex2.getLabel()
);
float carSpeed = parent.getOsmProvider().getOsmTagMapper().getCarSpeedForWay(parent, false);
I18NString name = namer.getNameForWay(parent, label);
AreaEdgeBuilder streetEdgeBuilder = new AreaEdgeBuilder()
.withFromVertex(vertex1)
.withToVertex(vertex2)
.withGeometry(line)
.withName(name)
.withMeterLength(length)
.withPermission(areaPermissions)
.withBack(false)
.withArea(areaGroup)
.withCarSpeed(carSpeed)
.withBogusName(parent.hasNoName())
.withWheelchairAccessible(wheelchairAccessible)
.withLink(parent.isLink());
label = String.format(labelTemplate, parent.getId(), vertex2.getLabel(), vertex1.getLabel());
name = namer.getNameForWay(parent, label);
AreaEdgeBuilder backStreetEdgeBuilder = new AreaEdgeBuilder()
.withFromVertex(vertex2)
.withToVertex(vertex1)
.withGeometry(line.reverse())
.withName(name)
.withMeterLength(length)
.withPermission(areaPermissions)
.withBack(true)
.withArea(areaGroup)
.withCarSpeed(carSpeed)
.withBogusName(parent.hasNoName())
.withWheelchairAccessible(wheelchairAccessible)
.withLink(parent.isLink());
AreaEdge street = streetEdgeBuilder.buildAndConnect();
AreaEdge backStreet = backStreetEdgeBuilder.buildAndConnect();
normalizer.applyWayProperties(street, backStreet, wayData, parent);
return Set.of(street, backStreet);
}
private void createAreas(AreaGroup areaGroup, Ring ring, Collection areas) {
Polygon containingArea = ring.jtsPolygon;
for (OsmArea area : areas) {
Geometry intersection = containingArea.intersection(area.jtsMultiPolygon);
if (intersection.getArea() == 0) {
continue;
}
Area namedArea = new Area();
OsmEntity areaEntity = area.parent;
String id = "way (area) " + areaEntity.getId();
I18NString name = namer.getNameForWay(areaEntity, id);
namedArea.setName(name);
WayProperties wayData = findAreaProperties(areaEntity);
double bicycleSafety = wayData.bicycleSafety().forward();
namedArea.setBicycleSafetyMultiplier(bicycleSafety);
double walkSafety = wayData.walkSafety().forward();
namedArea.setWalkSafetyMultiplier(walkSafety);
namedArea.setOriginalEdges(intersection);
namedArea.setPermission(wayData.getPermission());
areaGroup.addArea(namedArea);
if (areaEntity.isBoardingLocation()) {
var references = areaEntity.getMultiTagValues(boardingLocationRefTags);
if (!references.isEmpty()) {
var platform = new Platform(name, area.findInteriorPoint(), references);
osmInfoGraphBuildRepository.addPlatform(namedArea, platform);
}
}
}
}
private boolean isPlatformLinkingPoint(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 isLinkingPoint = true;
for (Edge se : osmVertex.getOutgoing()) {
if (
!se.getToVertex().getCoordinate().equals(start.getCoordinate()) &&
!(se instanceof AreaEdge)
) {
isLinkingPoint = false;
}
}
return isLinkingPoint;
}
return false;
}
record ListedEdgesOnly(Set edges) implements SkipEdgeStrategy {
@Override
public boolean shouldSkipEdge(State current, Edge edge) {
return !edges.contains(edge);
}
}
private boolean shouldSkipEdge(
IntersectionVertex v1,
IntersectionVertex v2,
HashSet alreadyAddedEdges
) {
if (v1 == v2) {
return true;
}
NodeEdge edge = new NodeEdge(v1, v2);
if (alreadyAddedEdges.contains(edge) || alreadyAddedEdges.contains(new NodeEdge(v2, v1))) {
return true;
}
alreadyAddedEdges.add(edge);
return false;
}
private record NodeEdge(IntersectionVertex from, IntersectionVertex to) {}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy