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 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.LinearRing;
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.AreaNotEpsilonValid;
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.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.util.I18NString;
import org.opentripplanner.visibility.Environment;
import org.opentripplanner.visibility.VLPoint;
import org.opentripplanner.visibility.VLPolygon;
import org.opentripplanner.visibility.VisibilityPolygon;
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. *

* */ public class WalkableAreaBuilder { private static Logger LOG = LoggerFactory.getLogger(WalkableAreaBuilder.class); private DataImportIssueStore issueStore; public static final int MAX_AREA_NODES = 500; public static final double VISIBILITY_EPSILON = 0.000000001; private Graph graph; private OSMDatabase osmdb; private WayPropertySet wayPropertySet; private StreetEdgeFactory edgeFactory; // This is an awful hack, but this class (WalkableAreaBuilder) ought to be rewritten. private Handler handler; private HashMap areaBoundaryVertexForCoordinate = new HashMap(); public WalkableAreaBuilder(Graph graph, OSMDatabase osmdb, WayPropertySet wayPropertySet, StreetEdgeFactory edgeFactory, Handler handler, DataImportIssueStore issueStore ) { this.graph = graph; this.osmdb = osmdb; this.wayPropertySet = wayPropertySet; this.edgeFactory = edgeFactory; this.handler = handler; this.issueStore = issueStore; } /** * 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(); // 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.toJtsPolygon().contains(area.toJTSMultiPolygon())) { continue; } for (Ring outerRing : area.outermostRings) { for (int i = 0; i < outerRing.nodes.size(); ++i) { createEdgesForRingSegment(edges, edgeList, area, outerRing, i, alreadyAddedEdges); } //TODO: is this actually needed? for (Ring innerRing : outerRing.holes) { for (int j = 0; j < innerRing.nodes.size(); ++j) { createEdgesForRingSegment(edges, edgeList, area, innerRing, j, alreadyAddedEdges); } } } } } } public void buildWithVisibility(AreaGroup group, boolean platformEntriesLinking) { Set startingNodes = new HashSet(); Set startingVertices = new HashSet(); Set edges = new HashSet(); // create polygon and accumulate nodes for area for (Ring ring : group.outermostRings) { AreaEdgeList edgeList = new AreaEdgeList(); // the points corresponding to concave or hole vertices // or those linked to ways ArrayList visibilityPoints = new ArrayList(); ArrayList visibilityNodes = new ArrayList(); HashSet> alreadyAddedEdges = 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. // 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) { // public transform platforms will be handled separately if platformEntriesLinking // parameter is true if(platformEntriesLinking && "platform".equals(area.parent.getTag("public_transport"))) { continue; } if (!ring.toJtsPolygon().contains(area.toJTSMultiPolygon())) { continue; } // Add stops from public transit relations into the area Collection nodes = osmdb.getStopsInArea(area.parent); if (nodes != null) { for (OSMNode node : nodes) { addtoVisibilityAndStartSets(startingNodes, visibilityPoints, visibilityNodes, node); } } for (Ring outerRing : area.outermostRings) { for (int i = 0; i < outerRing.nodes.size(); ++i) { OSMNode node = outerRing.nodes.get(i); createEdgesForRingSegment(edges, edgeList, area, outerRing, i, alreadyAddedEdges); addtoVisibilityAndStartSets(startingNodes, visibilityPoints, visibilityNodes, node); } for (Ring innerRing : outerRing.holes) { for (int j = 0; j < innerRing.nodes.size(); ++j) { OSMNode node = innerRing.nodes.get(j); createEdgesForRingSegment(edges, edgeList, area, innerRing, j, alreadyAddedEdges); addtoVisibilityAndStartSets(startingNodes, visibilityPoints, visibilityNodes, node); } } } } List nodes = new ArrayList(); List vertices = new ArrayList(); accumulateRingNodes(ring, nodes, vertices); VLPolygon polygon = makeStandardizedVLPolygon(vertices, nodes, false); accumulateVisibilityPoints(ring.nodes, polygon, visibilityPoints, visibilityNodes, false); ArrayList polygons = new ArrayList(); polygons.add(polygon); // holes for (Ring innerRing : ring.holes) { ArrayList holeNodes = new ArrayList(); vertices = new ArrayList(); accumulateRingNodes(innerRing, holeNodes, vertices); VLPolygon hole = makeStandardizedVLPolygon(vertices, holeNodes, true); accumulateVisibilityPoints(innerRing.nodes, hole, visibilityPoints, visibilityNodes, true); nodes.addAll(holeNodes); polygons.add(hole); } Environment areaEnv = new Environment(polygons); // FIXME: temporary hard limit on size of // areas to prevent way explosion if (visibilityPoints.size() > MAX_AREA_NODES) { issueStore.add( new AreaTooComplicated( group.getSomeOSMObject().getId(), visibilityPoints.size())); continue; } if (!areaEnv.is_valid(VISIBILITY_EPSILON)) { issueStore.add( new AreaNotEpsilonValid(group.getSomeOSMObject().getId())); continue; } edgeList.setOriginalEdges(ring.toJtsPolygon()); createNamedAreas(edgeList, ring, group.areas); OSMWithTags areaEntity = group.getSomeOSMObject(); for (int i = 0; i < visibilityNodes.size(); ++i) { OSMNode nodeI = visibilityNodes.get(i); VisibilityPolygon visibilityPolygon = new VisibilityPolygon( visibilityPoints.get(i), areaEnv, VISIBILITY_EPSILON); Polygon poly = toJTSPolygon(visibilityPolygon); for (int j = 0; j < visibilityNodes.size(); ++j) { OSMNode nodeJ = visibilityNodes.get(j); P2 nodePair = new P2(nodeI, nodeJ); if (alreadyAddedEdges.contains(nodePair)) continue; IntersectionVertex startEndpoint = handler.getVertexForOsmNode(nodeI, areaEntity); IntersectionVertex endEndpoint = handler.getVertexForOsmNode(nodeJ, areaEntity); Coordinate[] coordinates = new Coordinate[] { startEndpoint.getCoordinate(), endEndpoint.getCoordinate() }; GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory(); LineString line = geometryFactory.createLineString(coordinates); if (poly != null && poly.contains(line)) { createSegments(nodeI, nodeJ, startEndpoint, endEndpoint, group.areas, edgeList, edges); if (startingNodes.contains(nodeI)) { startingVertices.add(startEndpoint); } if (startingNodes.contains(nodeJ)) { startingVertices.add(endEndpoint); } } } } } pruneAreaEdges(startingVertices, edges); } class ListedEdgesOnly implements SkipEdgeStrategy { private Set edges; public ListedEdgesOnly(Set edges) { this.edges = edges; } @Override public boolean shouldSkipEdge(Vertex origin, Vertex target, 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) { 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) { for (Edge edge : path.edges) { usedEdges.add(edge); } } } } for (Edge edge : edges) { if (!usedEdges.contains(edge)) { graph.removeEdge(edge); } } } private void addtoVisibilityAndStartSets(Set startingNodes, ArrayList visibilityPoints, ArrayList visibilityNodes, OSMNode node) { if (osmdb.isNodeBelongsToWay(node.getId()) || osmdb.isNodeSharedByMultipleAreas(node.getId()) || node.isStop()) { startingNodes.add(node); VLPoint point = new VLPoint(node.lon, node.lat); if (!visibilityPoints.contains(point)) { visibilityPoints.add(point); visibilityNodes.add(node); } } } private Polygon toJTSPolygon(VLPolygon visibilityPolygon) { if (visibilityPolygon.vertices.isEmpty()) { return null; } // incomprehensibly, visilibity's routines for figuring out point-polygon containment are // too broken // to use here, so we have to fall back to JTS. Coordinate[] coordinates = new Coordinate[visibilityPolygon.n() + 1]; for (int p = 0; p < coordinates.length; ++p) { VLPoint vlPoint = visibilityPolygon.get(p); coordinates[p] = new Coordinate(vlPoint.x, vlPoint.y); } LinearRing shell = GeometryUtils.getGeometryFactory().createLinearRing(coordinates); Polygon poly = GeometryUtils.getGeometryFactory().createPolygon(shell, new LinearRing[0]); return poly; } private void createEdgesForRingSegment(Set edges, 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; } alreadyAddedEdges.add(nodePair); IntersectionVertex startEndpoint = handler.getVertexForOsmNode(node, area.parent); IntersectionVertex endEndpoint = handler.getVertexForOsmNode(nextNode, area.parent); createSegments(node, nextNode, startEndpoint, endEndpoint, Arrays.asList(area), edgeList, edges); } private void createSegments(OSMNode fromNode, OSMNode toNode, IntersectionVertex startEndpoint, IntersectionVertex endEndpoint, Collection areas, AreaEdgeList edgeList, Set edges) { 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.toJTSMultiPolygon(); Geometry intersection = polygon.intersection(line); if (intersection.getLength() > 0.000001) { intersects.add(area); } } if (intersects.size() == 0) { // apparently our intersection here was bogus return; } // 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); edges.add(street); 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); edges.add(backStreet); WayProperties wayData = wayPropertySet.getDataForWay(areaEntity); handler.applyWayProperties(street, backStreet, wayData, areaEntity); } else { // take the part that intersects with the start vertex Coordinate startCoordinate = startEndpoint.getCoordinate(); Point startPoint = geometryFactory.createPoint(startCoordinate); for (Area area : intersects) { MultiPolygon polygon = area.toJTSMultiPolygon(); 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); } createSegments(fromNode, toNode, startEndpoint, newEndpoint, Arrays.asList(area), edgeList, edges); createSegments(fromNode, toNode, newEndpoint, endEndpoint, intersects, edgeList, edges); break; } } } } private void createNamedAreas(AreaEdgeList edgeList, Ring ring, Collection areas) { Polygon containingArea = ring.toJtsPolygon(); for (Area area : areas) { Geometry intersection = containingArea.intersection(area.toJTSMultiPolygon()); 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); WayProperties wayData = wayPropertySet.getDataForWay(areaEntity); Double safety = wayData.getSafetyFeatures().first; namedArea.setBicycleSafetyMultiplier(safety); namedArea.setOriginalEdges(intersection); StreetTraversalPermission permission = OSMFilter.getPermissionsForEntity(areaEntity, StreetTraversalPermission.PEDESTRIAN_AND_BICYCLE); namedArea.setPermission(permission); edgeList.addArea(namedArea); } } private void accumulateRingNodes(Ring ring, List nodes, List vertices) { for (OSMNode node : ring.nodes) { if (nodes.contains(node)) { // hopefully, this only happens in order to // close polygons continue; } VLPoint point = new VLPoint(node.lon, node.lat); nodes.add(node); vertices.add(point); } } private void accumulateVisibilityPoints(List nodes, VLPolygon polygon, List visibilityPoints, List visibilityNodes, boolean hole) { int n = polygon.vertices.size(); for (int i = 0; i < n; ++i) { OSMNode curNode = nodes.get(i); VLPoint cur = polygon.vertices.get(i); VLPoint prev = polygon.vertices.get((i + n - 1) % n); VLPoint next = polygon.vertices.get((i + 1) % n); if (hole || (cur.x - prev.x) * (next.y - cur.y) - (cur.y - prev.y) * (next.x - cur.x) > 0) { // that math up there is a cross product to check // if the point is concave. Note that the sign is reversed because // visilibity is either ccw or latitude-major if (!visibilityNodes.contains(curNode)) { visibilityPoints.add(cur); visibilityNodes.add(curNode); } } } } private VLPolygon makeStandardizedVLPolygon(List vertices, List nodes, boolean reversed) { VLPolygon polygon = new VLPolygon(vertices); if ((reversed && polygon.area() > 0) || (!reversed && polygon.area() < 0)) { polygon.reverse(); // need to reverse nodes as well reversePolygonOfOSMNodes(nodes); } if (!polygon.is_in_standard_form()) { standardize(polygon.vertices, nodes); } return polygon; } private void standardize(ArrayList vertices, List nodes) { // based on code from VisiLibity int point_count = vertices.size(); if (point_count > 1) { // if more than one point in the polygon. ArrayList vertices_temp = new ArrayList(point_count); ArrayList nodes_temp = new ArrayList(point_count); // Find index of lexicographically smallest point. int index_of_smallest = 0; for (int i = 1; i < point_count; i++) if (vertices.get(i).compareTo(vertices.get(index_of_smallest)) < 0) index_of_smallest = i; // minor optimization for already-standardized polygons if (index_of_smallest == 0) return; // Fill vertices_temp starting with lex. smallest. for (int i = index_of_smallest; i < point_count; i++) { vertices_temp.add(vertices.get(i)); nodes_temp.add(nodes.get(i)); } for (int i = 0; i < index_of_smallest; i++) { vertices_temp.add(vertices.get(i)); nodes_temp.add(nodes.get(i)); } for (int i = 0; i < point_count; ++i) { vertices.set(i, vertices_temp.get(i)); nodes.set(i, nodes_temp.get(i)); } } } private void reversePolygonOfOSMNodes(List nodes) { for (int i = 1; i < (nodes.size() + 1) / 2; ++i) { OSMNode tmp = nodes.get(i); int opposite = nodes.size() - i; nodes.set(i, nodes.get(opposite)); nodes.set(opposite, tmp); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy