All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy