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

org.opentripplanner.graph_builder.module.osm.WalkableAreaBuilder Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
package org.opentripplanner.graph_builder.module.osm;

import java.util.Map;
import java.util.Objects;
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.GeometryUtils;
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.graph_builder.services.StreetEdgeFactory;
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.AStar;
import org.opentripplanner.routing.algorithm.astar.strategies.SkipEdgeStrategy;
import org.opentripplanner.routing.algorithm.astar.strategies.TrivialRemainingWeightHeuristic;
import org.opentripplanner.routing.api.request.RoutingRequest;
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.util.I18NString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.Set;

/**
 * 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 static Logger LOG = LoggerFactory.getLogger(WalkableAreaBuilder.class); private DataImportIssueStore issueStore; private final int maxAreaNodes; private Graph graph; private OSMDatabase osmdb; private WayPropertySet wayPropertySet; private Map wayPropertiesCache = new HashMap<>(); private StreetEdgeFactory edgeFactory; // This is an awful hack, but this class (WalkableAreaBuilder) ought to be rewritten. private Handler handler; private HashMap areaBoundaryVertexForCoordinate = new HashMap(); private boolean platformEntriesLinking; private List platformLinkingEndpoints; public WalkableAreaBuilder(Graph graph, OSMDatabase osmdb, WayPropertySet wayPropertySet, StreetEdgeFactory edgeFactory, Handler handler, DataImportIssueStore issueStore, int maxAreaNodes, boolean platformEntriesLinking ) { this.graph = graph; this.osmdb = osmdb; this.wayPropertySet = wayPropertySet; this.edgeFactory = edgeFactory; this.handler = handler; this.issueStore = issueStore; this.maxAreaNodes = maxAreaNodes; this.platformEntriesLinking = platformEntriesLinking; 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 * @param group */ public void buildWithoutVisibility(AreaGroup group) { Set edges = new HashSet(); // create polygon and accumulate nodes for area for (Ring ring : group.outermostRings) { AreaEdgeList edgeList = new AreaEdgeList(ring.jtsPolygon); // 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)); } } } } } } 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()); // create polygon and accumulate nodes for area for (Ring ring : group.outermostRings) { Polygon polygon = ring.jtsPolygon; AreaEdgeList edgeList = new AreaEdgeList(polygon); // 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) { OsmVertex 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()))) .collect(Collectors.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); } class ListedEdgesOnly implements SkipEdgeStrategy { private Set edges; public ListedEdgesOnly(Set edges) { this.edges = edges; } @Override public boolean shouldSkipEdge( Set origins, Set targets, State current, Edge edge, ShortestPathTree spt, RoutingRequest traverseOptions ) { return !edges.contains(edge); } } /** * 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. * * @param startingVertices * @param edges */ private void pruneAreaEdges(Collection startingVertices, Set edges, Set edgesToKeep) { if (edges.size() == 0) return; TraverseMode mode; StreetEdge firstEdge = (StreetEdge) edges.iterator().next(); if (firstEdge.getPermission().allows(StreetTraversalPermission.PEDESTRIAN)) { mode = TraverseMode.WALK; } else if (firstEdge.getPermission().allows(StreetTraversalPermission.BICYCLE)) { mode = TraverseMode.BICYCLE; } else { mode = TraverseMode.CAR; } RoutingRequest options = new RoutingRequest(mode); options.dominanceFunction = new DominanceFunction.EarliestArrival(); options.setDummyRoutingContext(graph); AStar search = new AStar(); search.setSkipEdgeStrategy(new ListedEdgesOnly(edges)); Set usedEdges = new HashSet(); for (Vertex vertex : startingVertices) { options.setRoutingContext(graph, vertex, null); options.rctx.remainingWeightHeuristic = new TrivialRemainingWeightHeuristic(); ShortestPathTree spt = search.getShortestPathTree(options); for (Vertex endVertex : startingVertices) { GraphPath path = spt.getPath(endVertex, false); 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.isStop(); } 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 = wayPropertySet.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 = edgeFactory.createAreaEdge(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 = edgeFactory.createAreaEdge(endEndpoint, startEndpoint, (LineString) 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 = wayPropertySet.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 = wayPropertySet.getDataForWay(areaEntity); wayPropertiesCache.put(areaEntity, wayData); } Double safety = wayPropertiesCache.get(areaEntity).getSafetyFeatures().first; namedArea.setBicycleSafetyMultiplier(safety); 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy