diff --git a/src/main/java/org/opentripplanner/graph_builder/linking/SimpleStreetSplitter.java b/src/main/java/org/opentripplanner/graph_builder/linking/SimpleStreetSplitter.java index 864063be9f6..dc43d9f8d8c 100644 --- a/src/main/java/org/opentripplanner/graph_builder/linking/SimpleStreetSplitter.java +++ b/src/main/java/org/opentripplanner/graph_builder/linking/SimpleStreetSplitter.java @@ -39,19 +39,20 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; /** * This class links transit stops to streets by splitting the streets (unless the stop is extremely close to the street * intersection). - * + *

* It is intended to eventually completely replace the existing stop linking code, which had been through so many * revisions and adaptations to different street and turn representations that it was very glitchy. This new code is * also intended to be deterministic in linking to streets, independent of the order in which the JVM decides to * iterate over Maps and even in the presence of points that are exactly halfway between multiple candidate linking * points. - * + *

* It would be wise to keep this new incarnation of the linking code relatively simple, considering what happened before. - * + *

* See discussion in pull request #1922, follow up issue #1934, and the original issue calling for replacement of * the stop linker, #1305. */ @@ -61,7 +62,9 @@ public class SimpleStreetSplitter { public static final int MAX_SEARCH_RADIUS_METERS = 1000; - /** if there are two ways and the distances to them differ by less than this value, we link to both of them */ + /** + * if there are two ways and the distances to them differ by less than this value, we link to both of them + */ public static final double DUPLICATE_WAY_EPSILON_METERS = 0.001; private Graph graph; @@ -73,9 +76,10 @@ public class SimpleStreetSplitter { /** * Construct a new SimpleStreetSplitter. Be aware that only one SimpleStreetSplitter should be * active on a graph at any given time. + * * @param graph */ - public SimpleStreetSplitter (Graph graph) { + public SimpleStreetSplitter(Graph graph) { this.graph = graph; // build a nice private spatial index, since we're adding and removing edges @@ -86,10 +90,12 @@ public SimpleStreetSplitter (Graph graph) { } } - /** Link all relevant vertices to the street network */ - public void link () { + /** + * Link all relevant vertices to the street network + */ + public void link() { for (Vertex v : graph.getVertices()) { - if (v instanceof TransitStop || v instanceof BikeRentalStationVertex || v instanceof BikeParkVertex) + if (v instanceof TransitStop || v instanceof BikeRentalStationVertex || v instanceof BikeParkVertex) { if (!link(v)) { if (v instanceof TransitStop) LOG.warn(graph.addBuilderAnnotation(new StopUnlinked((TransitStop) v))); @@ -97,45 +103,65 @@ else if (v instanceof BikeRentalStationVertex) LOG.warn(graph.addBuilderAnnotation(new BikeRentalStationUnlinked((BikeRentalStationVertex) v))); else if (v instanceof BikeParkVertex) LOG.warn(graph.addBuilderAnnotation(new BikeParkUnlinked((BikeParkVertex) v))); - }; + } + } } } - /** Link this vertex into the graph */ - public boolean link (Vertex vertex) { + /** + * Iteratively search the area around a location and find all the close by edges that can be traversed. + * This method tries to collect at least 10 candidates per iteration in order to prevent the loss of + * edges that are approximately at the same distance from the center. This number is arbitrary, though. + *

+ * Note that the returned list is unsorted. + * + * @param vertex the center vertex + * @return a (possibly empty) list of edges close to the vertex + */ + private List getCandidateEdges(Vertex vertex) { + List edges; + double maxRadiusDeg = SphericalDistanceLibrary.metersToDegrees(MAX_SEARCH_RADIUS_METERS); + int iteration = 7; // We double the radius at most n times + + final TraverseModeSet traverseModeSet = new TraverseModeSet(TraverseMode.WALK); + final Envelope env = new Envelope(vertex.getCoordinate()); + double xscale = Math.cos(vertex.getLat() * Math.PI / 180); + double radius = maxRadiusDeg / (1 << (iteration - 1)); + env.expandBy(radius / xscale, radius); + + do { + //Do the envelope query and filter edges that can be traversed and that are still in the + //graph + edges = idx.query(env).stream().parallel().filter(edge -> + edge.canTraverse(traverseModeSet) && + edge.getToVertex().getIncoming().contains(edge) + ).collect(Collectors.toList()); + + iteration--; + radius = maxRadiusDeg / (1 << iteration); + env.expandBy(radius / xscale, radius); + } while (edges.size() < 10 && iteration > 0); + + return edges; + } + + /** + * Link this vertex into the graph + */ + public boolean link(Vertex vertex) { // find nearby street edges - // TODO: we used to use an expanding-envelope search, which is more efficient in - // dense areas. but first let's see how inefficient this is. I suspect it's not too - // bad and the gains in simplicity are considerable. final double radiusDeg = SphericalDistanceLibrary.metersToDegrees(MAX_SEARCH_RADIUS_METERS); - Envelope env = new Envelope(vertex.getCoordinate()); - // local equirectangular projection final double xscale = Math.cos(vertex.getLat() * Math.PI / 180); - env.expandBy(radiusDeg / xscale, radiusDeg); - double duplicateDeg = SphericalDistanceLibrary.metersToDegrees(DUPLICATE_WAY_EPSILON_METERS); // We sort the list of candidate edges by distance to the stop // This should remove any issues with things coming out of the spatial index in different orders // Then we link to everything that is within DUPLICATE_WAY_EPSILON_METERS of of the best distance // so that we capture back edges and duplicate ways. - // TODO all the code below looks like a good candidate for Java 8 streams and lambdas - List candidateEdges = new ArrayList( - Collections2.filter(idx.query(env), new Predicate() { - - @Override - public boolean apply(StreetEdge edge) { - // note: not filtering by radius here as distance calculation is expensive - // we do that below. - return edge.canTraverse(new TraverseModeSet(TraverseMode.WALK)) && - // only link to edges still in the graph. - edge.getToVertex().getIncoming().contains(edge); - } - }) - ); + List candidateEdges = getCandidateEdges(vertex); // make a map of distances final TIntDoubleMap distances = new TIntDoubleHashMap(); @@ -145,19 +171,8 @@ public boolean apply(StreetEdge edge) { } // sort the list - Collections.sort(candidateEdges, new Comparator () { - - @Override - public int compare(StreetEdge o1, StreetEdge o2) { - double diff = distances.get(o1.getId()) - distances.get(o2.getId()); - if (diff < 0) - return -1; - if (diff > 0) - return 1; - return 0; - } - - }); + Collections.sort(candidateEdges, (o1, o2) -> + Double.compare(distances.get(o1.getId()), distances.get(o2.getId()))); // find the closest candidate edges if (candidateEdges.isEmpty() || distances.get(candidateEdges.get(0).getId()) > radiusDeg) @@ -183,8 +198,10 @@ public int compare(StreetEdge o1, StreetEdge o2) { return true; } - /** split the edge and link in the transit stop */ - private void link (Vertex tstop, StreetEdge edge, double xscale) { + /** + * split the edge and link in the transit stop + */ + private void link(Vertex tstop, StreetEdge edge, double xscale) { // TODO: we've already built this line string, we should save it LineString orig = edge.getGeometry(); LineString transformed = equirectangularProject(orig, xscale); @@ -207,17 +224,17 @@ else if (ll.getSegmentIndex() == orig.getNumPoints() - 1) { // nPoints - 2: -1 to correct for index vs count, -1 to account for fencepost problem else if (ll.getSegmentIndex() == orig.getNumPoints() - 2 && ll.getSegmentFraction() > 1 - 1e-8) { makeLinkEdges(tstop, (StreetVertex) edge.getToVertex()); - } - - else { + } else { // split the edge, get the split vertex SplitterVertex v0 = split(edge, ll); makeLinkEdges(tstop, v0); } } - /** Split the street edge at the given fraction */ - private SplitterVertex split (StreetEdge edge, LinearLocation ll) { + /** + * Split the street edge at the given fraction + */ + private SplitterVertex split(StreetEdge edge, LinearLocation ll) { LineString geometry = edge.getGeometry(); // create the geometries @@ -244,9 +261,11 @@ private SplitterVertex split (StreetEdge edge, LinearLocation ll) { return v; } - /** Make the appropriate type of link edges from a vertex */ - private void makeLinkEdges (Vertex from, StreetVertex to) { - if (from instanceof TransitStop) + /** + * Make the appropriate type of link edges from a vertex + */ + private void makeLinkEdges(Vertex from, StreetVertex to) { + if (from instanceof TransitStop) makeTransitLinkEdges((TransitStop) from, to); else if (from instanceof BikeRentalStationVertex) makeBikeRentalLinkEdges((BikeRentalStationVertex) from, to); @@ -254,7 +273,9 @@ else if (from instanceof BikeParkVertex) makeBikeParkEdges((BikeParkVertex) from, to); } - /** Make bike park edges */ + /** + * Make bike park edges + */ private void makeBikeParkEdges(BikeParkVertex from, StreetVertex to) { for (StreetBikeParkLink sbpl : Iterables.filter(from.getOutgoing(), StreetBikeParkLink.class)) { if (sbpl.getToVertex() == to) @@ -265,10 +286,10 @@ private void makeBikeParkEdges(BikeParkVertex from, StreetVertex to) { new StreetBikeParkLink(to, from); } - /** + /** * Make street transit link edges, unless they already exist. */ - private void makeTransitLinkEdges (TransitStop tstop, StreetVertex v) { + private void makeTransitLinkEdges(TransitStop tstop, StreetVertex v) { // ensure that the requisite edges do not already exist // this can happen if we link to duplicate ways that have the same start/end vertices. for (StreetTransitLink e : Iterables.filter(tstop.getOutgoing(), StreetTransitLink.class)) { @@ -280,8 +301,10 @@ private void makeTransitLinkEdges (TransitStop tstop, StreetVertex v) { new StreetTransitLink(v, tstop, tstop.hasWheelchairEntrance()); } - /** Make link edges for bike rental */ - private void makeBikeRentalLinkEdges (BikeRentalStationVertex from, StreetVertex to) { + /** + * Make link edges for bike rental + */ + private void makeBikeRentalLinkEdges(BikeRentalStationVertex from, StreetVertex to) { for (StreetBikeRentalLink sbrl : Iterables.filter(from.getOutgoing(), StreetBikeRentalLink.class)) { if (sbrl.getToVertex() == to) return; @@ -291,14 +314,18 @@ private void makeBikeRentalLinkEdges (BikeRentalStationVertex from, StreetVertex new StreetBikeRentalLink(to, from); } - /** projected distance from stop to edge, in latitude degrees */ - private static double distance (Vertex tstop, StreetEdge edge, double xscale) { + /** + * projected distance from stop to edge, in latitude degrees + */ + private static double distance(Vertex tstop, StreetEdge edge, double xscale) { // use JTS internal tools wherever possible LineString transformed = equirectangularProject(edge.getGeometry(), xscale); return transformed.distance(geometryFactory.createPoint(new Coordinate(tstop.getLon() * xscale, tstop.getLat()))); } - /** project this linestring to an equirectangular projection */ + /** + * project this linestring to an equirectangular projection + */ private static LineString equirectangularProject(LineString geometry, double xscale) { Coordinate[] coords = new Coordinate[geometry.getNumPoints()];