/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.routing.impl; import static org.opentripplanner.common.IterableLibrary.filter; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import java.util.Set; import lombok.Getter; import lombok.Setter; import org.opentripplanner.common.IterableLibrary; import org.opentripplanner.common.geometry.DistanceLibrary; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.common.model.GenericLocation; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.TraversalRequirements; import org.opentripplanner.routing.core.TraverseModeSet; import org.opentripplanner.routing.edgetype.FreeEdge; import org.opentripplanner.routing.edgetype.StreetEdge; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.location.StreetLocation; import org.opentripplanner.routing.services.StreetVertexIndexService; import org.opentripplanner.routing.vertextype.IntersectionVertex; import org.opentripplanner.routing.vertextype.StreetVertex; import org.opentripplanner.routing.vertextype.TransitStop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Iterables; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.index.SpatialIndex; import com.vividsolutions.jts.index.quadtree.Quadtree; import com.vividsolutions.jts.index.strtree.STRtree; /** * Indexes all edges and transit vertices of the graph spatially. Has a variety of query methods used during network linking and trip planning. * * Creates a StreetLocation representing a location on a street that's not at an intersection, based on input latitude and longitude. Instantiating * this class is expensive, because it creates a spatial index of all of the intersections in the graph. */ public class StreetVertexIndexServiceImpl implements StreetVertexIndexService { // Members are protected so that custom subclasses can access them. protected Graph graph; /** * Contains only instances of {@link StreetEdge} */ protected SpatialIndex edgeTree; protected STRtree transitStopTree; protected STRtree intersectionTree; @Getter @Setter protected DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); // private static final double SEARCH_RADIUS_M = 100; // meters // private static final double SEARCH_RADIUS_DEG = DistanceLibrary.metersToDegrees(SEARCH_RADIUS_M); /* all distance constants here are plate-carée Euclidean, 0.001 ~= 100m at equator */ // Edges will only be found if they are closer than this distance public static final double MAX_DISTANCE_FROM_STREET = 0.01000; // Maximum difference in distance for two geometries to be considered coincident public static final double DISTANCE_ERROR = 0.000001; // If a point is within MAX_CORNER_DISTANCE, it is treated as at the corner. // This distance is a euclidean distance in lat/lng space. private static final double MAX_CORNER_DISTANCE = 0.0001; static final Logger LOG = LoggerFactory.getLogger(StreetVertexIndexServiceImpl.class); public StreetVertexIndexServiceImpl(Graph graph) { this.graph = graph; setup(); } public StreetVertexIndexServiceImpl(Graph graph, DistanceLibrary distanceLibrary) { this.graph = graph; this.distanceLibrary = distanceLibrary; setup(); } public void setup_modifiable() { edgeTree = new Quadtree(); postSetup(); } public void setup() { edgeTree = new STRtree(); postSetup(); ((STRtree) edgeTree).build(); } private void postSetup() { transitStopTree = new STRtree(); intersectionTree = new STRtree(); for (Vertex gv : graph.getVertices()) { Vertex v = gv; // We only care about StreetEdges for (StreetEdge e : filter(gv.getOutgoing(), StreetEdge.class)) { if (e.getGeometry() == null) { continue; } Envelope env = e.getGeometry().getEnvelopeInternal(); edgeTree.insert(env, e); } if (v instanceof TransitStop) { // only index transit stops that (a) are entrances, or (b) have no associated // entrances TransitStop ts = (TransitStop) v; if (!ts.isEntrance() && ts.hasEntrances()) { continue; } Envelope env = new Envelope(v.getCoordinate()); transitStopTree.insert(env, v); } if (v instanceof IntersectionVertex) { Envelope env = new Envelope(v.getCoordinate()); intersectionTree.insert(env, v); } } transitStopTree.build(); } /** * Get all transit stops within a given distance of a coordinate * * @param distance in meters */ @SuppressWarnings("unchecked") public List<Vertex> getLocalTransitStops(Coordinate c, double distance) { Envelope env = new Envelope(c); env.expandBy(SphericalDistanceLibrary.metersToDegrees(distance)); List<Vertex> nearby = transitStopTree.query(env); List<Vertex> results = new ArrayList<Vertex>(); for (Vertex v : nearby) { if (distanceLibrary.distance(v.getCoordinate(), c) <= distance) { results.add(v); } } return results; } /** * Convenience helper for when extraEdges is empty/null. */ private Vertex getClosestVertex(final GenericLocation location, RoutingRequest options) { return getClosestVertex(location, options, null); } /** * Returns the closest vertex for this GenericLocation. If necessary, this vertex will be created by splitting nearby edges (non-permanently). * * This method is the heart of the logic that searches for the start and endpoints of RideRequests. As such, it is protected so that subclasses * can override the search behavior. */ protected Vertex getClosestVertex(final GenericLocation location, RoutingRequest options, List<Edge> extraEdges) { LOG.debug("Looking for/making a vertex near {}", location); // first, check for intersections very close by Coordinate coord = location.getCoordinate(); StreetVertex intersection = getIntersectionAt(coord, MAX_CORNER_DISTANCE); String calculatedName = location.getName(); if (intersection != null) { // coordinate is at a street corner or endpoint if (!location.hasName()) { LOG.debug("found intersection {}. not splitting.", intersection); // generate names for corners when no name was given Set<String> uniqueNameSet = new HashSet<String>(); for (Edge e : intersection.getOutgoing()) { if (e instanceof StreetEdge) { uniqueNameSet.add(e.getName()); } } List<String> uniqueNames = new ArrayList<String>(uniqueNameSet); Locale locale; if (options == null) { locale = new Locale("en"); } else { locale = options.getLocale(); } ResourceBundle resources = ResourceBundle.getBundle("internals", locale); String fmt = resources.getString("corner"); if (uniqueNames.size() > 1) { calculatedName = String.format(fmt, uniqueNames.get(0), uniqueNames.get(1)); } else if (uniqueNames.size() == 1) { calculatedName = uniqueNames.get(0); } else { calculatedName = resources.getString("unnamedStreet"); } } StreetLocation closest = new StreetLocation(graph, "corner " + Math.random(), coord, calculatedName); FreeEdge e = new FreeEdge(closest, intersection); closest.getExtra().add(e); e = new FreeEdge(intersection, closest); closest.getExtra().add(e); return closest; } // if no intersection vertices were found, then find the closest transit stop // (we can return stops here because this method is not used when street-transit linking) double closestStopDistance = Double.POSITIVE_INFINITY; Vertex closestStop = null; // elsewhere options=null means no restrictions, find anything. // here we skip examining stops, as they are really only relevant when transit is being used if (options != null && options.getModes().isTransit()) { for (Vertex v : getLocalTransitStops(coord, 1000)) { double d = distanceLibrary.distance(v.getCoordinate(), coord); if (d < closestStopDistance) { closestStopDistance = d; closestStop = v; } } } LOG.debug(" best stop: {} distance: {}", closestStop, closestStopDistance); // then find closest walkable street StreetLocation closestStreet = null; CandidateEdgeBundle bundle = getClosestEdges(location, options, extraEdges, null, false); CandidateEdge candidate = bundle.best; double closestStreetDistance = Double.POSITIVE_INFINITY; if (candidate != null) { StreetEdge bestStreet = candidate.edge; Coordinate nearestPoint = candidate.nearestPointOnEdge; closestStreetDistance = distanceLibrary.distance(coord, nearestPoint); LOG.debug("best street: {} dist: {}", bestStreet.toString(), closestStreetDistance); if (calculatedName == null || "".equals(calculatedName)) { calculatedName = bestStreet.getName(); } String closestName = String.format("%s_%s", bestStreet.getName(), location.toString()); closestStreet = StreetLocation.createStreetLocation(graph, closestName, calculatedName, bundle.toEdgeList(), nearestPoint, coord); } // decide whether to return street, or street + stop if (closestStreet == null) { // no street found, return closest stop or null LOG.debug("returning only transit stop (no street found)"); return closestStop; // which will be null if none was found } else { // street found if (closestStop != null) { // both street and stop found double relativeStopDistance = closestStopDistance / closestStreetDistance; if (relativeStopDistance < 1.5) { LOG.debug("linking transit stop to street (distances are comparable)"); closestStreet.addExtraEdgeTo(closestStop); } } LOG.debug("returning split street"); return closestStreet; } } @SuppressWarnings("unchecked") public Collection<Vertex> getVerticesForEnvelope(Envelope envelope) { return intersectionTree.query(envelope); } @Override public Collection<StreetEdge> getEdgesForEnvelope(Envelope envelope) { return edgeTree.query(envelope); } @Override @SuppressWarnings("unchecked") public CandidateEdgeBundle getClosestEdges(GenericLocation location, TraversalRequirements reqs, List<Edge> extraEdges, Collection<Edge> preferredEdges, boolean possibleTransitLinksOnly) { Coordinate coordinate = location.getCoordinate(); Envelope envelope = new Envelope(coordinate); // Collect the extra StreetEdges to consider. Iterable<StreetEdge> extraStreets = IterableLibrary.filter(graph.getTemporaryEdges(), StreetEdge.class); if (extraEdges != null) { extraStreets = Iterables.concat(IterableLibrary.filter(extraEdges, StreetEdge.class), extraStreets); } double envelopeGrowthAmount = 0.001; // ~= 100 meters double radius = 0; CandidateEdgeBundle candidateEdges = new CandidateEdgeBundle(); while (candidateEdges.size() == 0) { // expand envelope -- assumes many close searches and occasional far ones envelope.expandBy(envelopeGrowthAmount); radius += envelopeGrowthAmount; if (radius > MAX_DISTANCE_FROM_STREET) { return candidateEdges; // empty list } Iterable<StreetEdge> nearbyEdges = edgeTree.query(envelope); if (nearbyEdges != null) { nearbyEdges = Iterables.concat(nearbyEdges, extraStreets); } // oh. This is part of the problem: we're not linking to one-way // streets, even though that is a perfectly reasonable thing to do. // we need to handle that using bundles. for (StreetEdge e : nearbyEdges) { // Ignore invalid edges. if (e == null || e.getFromVertex() == null) { continue; } // Ignore those edges we can't traverse. canBeTraversed checks internally if // walking a bike is possible on this StreetEdge. if (!reqs.canBeTraversed(e)) { continue; } // Compute preference value double preferrence = 1; if (preferredEdges != null && preferredEdges.contains(e)) { preferrence = 3.0; } TraverseModeSet modes = reqs.getModes(); CandidateEdge ce = new CandidateEdge(e, location, preferrence, modes); // Even if an edge is outside the query envelope, bounding boxes can // still intersect. In this case, distance to the edge is greater // than the query envelope size. if (ce.getDistance() < radius) { candidateEdges.add(ce); } } } Collection<CandidateEdgeBundle> bundles = candidateEdges.binByDistanceAndAngle(); // initially set best bundle to the closest bundle CandidateEdgeBundle best = null; for (CandidateEdgeBundle bundle : bundles) { if (best == null || bundle.best.score < best.best.score) { if (possibleTransitLinksOnly) { // assuming all platforms are tagged when they are not car streets... #1077 if (!(bundle.allowsCars() || bundle.isPlatform())) continue; } best = bundle; } } return best; } @Override public CandidateEdgeBundle getClosestEdges(GenericLocation location, TraversalRequirements reqs) { return getClosestEdges(location, reqs, null, null, false); } /** * Find edges closest to the given location. * * TODO(flamholz): consider deleting. * * @param coordinate Point to get edges near * @param request RoutingRequest that must be able to traverse the edge (all edges if null) * @param extraEdges Any edges not in the graph that might be included (allows trips within one block) * @param preferredEdges Any edges to prefer in the search * @param possibleTransitLinksOnly only return edges traversable by cars or are platforms * @return */ protected CandidateEdgeBundle getClosestEdges(GenericLocation location, RoutingRequest request, List<Edge> extraEdges, Collection<Edge> preferredEdges, boolean possibleTransitLinksOnly) { // NOTE(flamholz): if request is null, will initialize TraversalRequirements // that accept all modes of travel. TraversalRequirements reqs = new TraversalRequirements(request); return getClosestEdges(location, reqs, extraEdges, preferredEdges, possibleTransitLinksOnly); } /** * Convenience wrapper that uses MAX_CORNER_DISTANCE. * * @param coordinate * @return */ public StreetVertex getIntersectionAt(Coordinate coordinate) { return getIntersectionAt(coordinate, MAX_CORNER_DISTANCE); } @SuppressWarnings("unchecked") public StreetVertex getIntersectionAt(Coordinate coordinate, double distanceError) { Envelope envelope = new Envelope(coordinate); envelope.expandBy(distanceError * 2); List<StreetVertex> nearby = intersectionTree.query(envelope); StreetVertex nearest = null; double bestDistance = Double.POSITIVE_INFINITY; for (StreetVertex v : nearby) { double distance = coordinate.distance(v.getCoordinate()); if (distance < distanceError) { if (distance < bestDistance) { bestDistance = distance; nearest = v; } } } return nearest; } @Override /** radius is meters */ public List<TransitStop> getNearbyTransitStops(Coordinate coordinate, double radius) { Envelope envelope = new Envelope(coordinate); envelope.expandBy(SphericalDistanceLibrary.metersToDegrees(radius)); List<?> stops = transitStopTree.query(envelope); ArrayList<TransitStop> out = new ArrayList<TransitStop>(); for (Object o : stops) { TransitStop stop = (TransitStop) o; if (distanceLibrary.distance(stop.getCoordinate(), coordinate) < radius) { out.add(stop); } } return out; } @Override /** radius is meters */ public List<TransitStop> getNearbyTransitStops(Coordinate coordinateOne, Coordinate coordinateTwo) { Envelope envelope = new Envelope(coordinateOne, coordinateTwo); List<?> stops = transitStopTree.query(envelope); ArrayList<TransitStop> out = new ArrayList<TransitStop>(); for (Object o : stops) { TransitStop stop = (TransitStop) o; out.add(stop); } return out; } @Override public Vertex getVertexForLocation(GenericLocation location, RoutingRequest options) { return getVertexForLocation(location, options, null); } /** * @param other: non-null when another vertex has already been found. When the from vertex has * already been made/found, that vertex is passed in when finding/creating the to vertex. * TODO: This appears to be for reusing the extra edges list -- is this still needed? */ @Override public Vertex getVertexForLocation(GenericLocation loc, RoutingRequest options, Vertex other) { Coordinate c = loc.getCoordinate(); if (c != null) { if (other instanceof StreetLocation) { return getClosestVertex(loc, options, ((StreetLocation) other).getExtra()); } else { return getClosestVertex(loc, options); } } // No Coordinate available. String place = loc.getPlace(); if (place == null) { return null; } // did not match lat/lon, interpret place as a vertex label. // this should probably only be used in tests, // though it does allow routing from stop to stop. return graph.getVertex(place); } }