package org.opentripplanner.graph_builder.linking;
import com.google.common.collect.Iterables;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.index.SpatialIndex;
import com.vividsolutions.jts.linearref.LinearLocation;
import com.vividsolutions.jts.linearref.LocationIndexedLine;
import gnu.trove.map.TIntDoubleMap;
import gnu.trove.map.hash.TIntDoubleHashMap;
import jersey.repackaged.com.google.common.collect.Lists;
import org.opentripplanner.common.geometry.GeometryUtils;
import org.opentripplanner.common.geometry.HashGridSpatialIndex;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.common.model.P2;
import org.opentripplanner.graph_builder.annotation.BikeParkUnlinked;
import org.opentripplanner.graph_builder.annotation.BikeRentalStationUnlinked;
import org.opentripplanner.graph_builder.annotation.StopUnlinked;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.edgetype.StreetBikeParkLink;
import org.opentripplanner.routing.edgetype.StreetBikeRentalLink;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.edgetype.StreetTransitLink;
import org.opentripplanner.routing.edgetype.TemporaryFreeEdge;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.location.TemporaryStreetLocation;
import org.opentripplanner.routing.vertextype.BikeParkVertex;
import org.opentripplanner.routing.vertextype.BikeRentalStationVertex;
import org.opentripplanner.routing.vertextype.SplitterVertex;
import org.opentripplanner.routing.vertextype.StreetVertex;
import org.opentripplanner.routing.vertextype.TemporarySplitterVertex;
import org.opentripplanner.routing.vertextype.TemporaryVertex;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.opentripplanner.util.NonLocalizedString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
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.
*/
public class SimpleStreetSplitter {
private static final Logger LOG = LoggerFactory.getLogger(SimpleStreetSplitter.class);
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 */
public static final double DUPLICATE_WAY_EPSILON_METERS = 0.001;
private Graph graph;
private HashGridSpatialIndex<Edge> idx;
private SpatialIndex transitStopIndex;
private static GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
//If true edges are split and new edges are created (used when linking transit stops etc. during graph building)
//If false new temporary edges are created and no edges are deleted (Used when searching for origin/destination)
private final boolean destructiveSplitting;
/**
* Construct a new SimpleStreetSplitter. Be aware that only one SimpleStreetSplitter should be
* active on a graph at any given time.
* @param graph
* @param hashGridSpatialIndex If not null this index is used instead of creating new one
* @param transitStopIndex Index of all transitStops which is generated in {@link org.opentripplanner.routing.impl.StreetVertexIndexServiceImpl}
* @param destructiveSplitting If true splitting is permanent (Used when linking transit stops etc.) when false Splitting is only for duration of a request. Since they are made from temporary vertices and edges.
*/
public SimpleStreetSplitter(Graph graph, HashGridSpatialIndex<Edge> hashGridSpatialIndex,
SpatialIndex transitStopIndex, boolean destructiveSplitting) {
this.graph = graph;
this.transitStopIndex = transitStopIndex;
this.destructiveSplitting = destructiveSplitting;
//We build a spatial index if it isn't provided
if (hashGridSpatialIndex == null) {
// build a nice private spatial index, since we're adding and removing edges
idx = new HashGridSpatialIndex<Edge>();
for (StreetEdge se : Iterables.filter(graph.getEdges(), StreetEdge.class)) {
idx.insert(se.getGeometry(), se);
}
} else {
idx = hashGridSpatialIndex;
}
}
/**
* Construct a new SimpleStreetSplitter. Be aware that only one SimpleStreetSplitter should be
* active on a graph at any given time.
*
* SimpleStreetSplitter generates index on graph and splits destructively (used in transit splitter)
* @param graph
*/
public SimpleStreetSplitter(Graph graph) {
this(graph, null, null, true);
}
/** 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 (!link(v)) {
if (v instanceof TransitStop)
LOG.warn(graph.addBuilderAnnotation(new StopUnlinked((TransitStop) v)));
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 to the closest walkable edge */
public boolean link (Vertex vertex) {
return link(vertex, TraverseMode.WALK, null);
}
/** Link this vertex into the graph */
public boolean link(Vertex vertex, TraverseMode traverseMode, RoutingRequest options) {
// 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);
final TraverseModeSet traverseModeSet;
if (traverseMode == TraverseMode.BICYCLE) {
traverseModeSet = new TraverseModeSet(traverseMode, TraverseMode.WALK);
} else {
traverseModeSet = new TraverseModeSet(traverseMode);
}
// 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.
List<StreetEdge> candidateEdges = idx.query(env).stream()
.filter(streetEdge -> streetEdge instanceof StreetEdge)
.map(edge -> (StreetEdge) edge)
// note: not filtering by radius here as distance calculation is expensive
// we do that below.
.filter(edge -> edge.canTraverse(traverseModeSet) &&
// only link to edges still in the graph.
edge.getToVertex().getIncoming().contains(edge))
.collect(Collectors.toList());
// make a map of distances
final TIntDoubleMap distances = new TIntDoubleHashMap();
for (StreetEdge e : candidateEdges) {
distances.put(e.getId(), distance(vertex, e, xscale));
}
// sort the list
Collections.sort(candidateEdges, (o1, o2) -> {
double diff = distances.get(o1.getId()) - distances.get(o2.getId());
if (diff < 0)
return -1;
if (diff > 0)
return 1;
return 0;
});
// find the closest candidate edges
if (candidateEdges.isEmpty() || distances.get(candidateEdges.get(0).getId()) > radiusDeg) {
//We only link to stops if we are searching for origin/destination and for that we need transitStopIndex
if (destructiveSplitting || transitStopIndex == null) {
return false;
}
LOG.debug("No street edge was found for {}", vertex);
//we search for closest stops (since this is only used in origin/destination linking if no edges were found)
//in same way as closest edges are found
List<TransitStop> candidateStops = new ArrayList<>();
transitStopIndex.query(env).forEach(candidateStop ->
candidateStops.add((TransitStop) candidateStop)
);
final TIntDoubleMap stopDistances = new TIntDoubleHashMap();
for (TransitStop t : candidateStops) {
stopDistances.put(t.getIndex(), distance(vertex, t, xscale));
}
Collections.sort(candidateStops, (o1, o2) -> {
double diff = stopDistances.get(o1.getIndex()) - stopDistances.get(o2.getIndex());
if (diff < 0) {
return -1;
}
if (diff > 0) {
return 1;
}
return 0;
});
if (candidateStops.isEmpty() || stopDistances.get(candidateStops.get(0).getIndex()) > radiusDeg) {
LOG.debug("Stops aren't close either!");
return false;
} else {
List<TransitStop> bestStops = Lists.newArrayList();
// add stops until there is a break of epsilon meters.
// we do this to enforce determinism. if there are a lot of stops that are all extremely close to each other,
// we want to be sure that we deterministically link to the same ones every time. Any hard cutoff means things can
// fall just inside or beyond the cutoff depending on floating-point operations.
int i = 0;
do {
bestStops.add(candidateStops.get(i++));
} while (i < candidateStops.size() &&
stopDistances.get(candidateStops.get(i).getIndex()) - stopDistances
.get(candidateStops.get(i - 1).getIndex()) < duplicateDeg);
for (TransitStop stop: bestStops) {
LOG.debug("Linking vertex to stop: {}", stop.getName());
makeTemporaryEdges((TemporaryStreetLocation)vertex, stop);
}
return true;
}
} else {
// find the best edges
List<StreetEdge> bestEdges = Lists.newArrayList();
// add edges until there is a break of epsilon meters.
// we do this to enforce determinism. if there are a lot of edges that are all extremely close to each other,
// we want to be sure that we deterministically link to the same ones every time. Any hard cutoff means things can
// fall just inside or beyond the cutoff depending on floating-point operations.
int i = 0;
do {
bestEdges.add(candidateEdges.get(i++));
} while (i < candidateEdges.size() &&
distances.get(candidateEdges.get(i).getId()) - distances
.get(candidateEdges.get(i - 1).getId()) < duplicateDeg);
for (StreetEdge edge : bestEdges) {
link(vertex, edge, xscale, options);
}
return true;
}
}
/** split the edge and link in the transit stop */
private void link(Vertex tstop, StreetEdge edge, double xscale, RoutingRequest options) {
// TODO: we've already built this line string, we should save it
LineString orig = edge.getGeometry();
LineString transformed = equirectangularProject(orig, xscale);
LocationIndexedLine il = new LocationIndexedLine(transformed);
LinearLocation ll = il.project(new Coordinate(tstop.getLon() * xscale, tstop.getLat()));
// if we're very close to one end of the line or the other, or endwise, don't bother to split,
// cut to the chase and link directly
// We use a really tiny epsilon here because we only want points that actually snap to exactly the same location on the
// street to use the same vertices. Otherwise the order the stops are loaded in will affect where they are snapped.
if (ll.getSegmentIndex() == 0 && ll.getSegmentFraction() < 1e-8) {
makeLinkEdges(tstop, (StreetVertex) edge.getFromVertex());
}
// -1 converts from count to index. Because of the fencepost problem, npoints - 1 is the "segment"
// past the last point
else if (ll.getSegmentIndex() == orig.getNumPoints() - 1) {
makeLinkEdges(tstop, (StreetVertex) edge.getToVertex());
}
// 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 {
TemporaryVertex temporaryVertex = null;
boolean endVertex = false;
if (tstop instanceof TemporaryVertex) {
temporaryVertex = (TemporaryVertex) tstop;
endVertex = temporaryVertex.isEndVertex();
}
//This throws runtime TrivialPathException if same edge is split in origin and destination link
//It is only used in origin/destination linking since otherwise options is null
if (options != null) {
options.canSplitEdge(edge);
}
// split the edge, get the split vertex
SplitterVertex v0 = split(edge, ll, temporaryVertex != null, endVertex);
makeLinkEdges(tstop, v0);
}
}
/**
* Split the street edge at the given fraction
*
* @param edge to be split
* @param ll fraction at which to split the edge
* @param temporarySplit if true this is temporary split at origin/destinations search and only temporary edges vertices are created
* @param endVertex if this is temporary edge this is true if this is end vertex otherwise it doesn't matter
* @return Splitter vertex with added new edges
*/
private SplitterVertex split (StreetEdge edge, LinearLocation ll, boolean temporarySplit, boolean endVertex) {
LineString geometry = edge.getGeometry();
// create the geometries
Coordinate splitPoint = ll.getCoordinate(geometry);
// every edge can be split exactly once, so this is a valid label
SplitterVertex v;
if (temporarySplit) {
v = new TemporarySplitterVertex(graph, "split from " + edge.getId(), splitPoint.x, splitPoint.y,
edge, endVertex);
if (edge.isWheelchairAccessible()) {
((TemporarySplitterVertex) v).setWheelchairAccessible(true);
} else {
((TemporarySplitterVertex) v).setWheelchairAccessible(false);
}
} else {
v = new SplitterVertex(graph, "split from " + edge.getId(), splitPoint.x, splitPoint.y,
edge);
}
// make the edges
// TODO this is using the StreetEdge implementation of split, which will discard elevation information
// on edges that have it
P2<StreetEdge> edges = edge.split(v, !temporarySplit);
if (destructiveSplitting) {
// update indices of new edges
idx.insert(edges.first.getGeometry(), edges.first);
idx.insert(edges.second.getGeometry(), edges.second);
// (no need to remove original edge, we filter it when it comes out of the index)
// remove original edge from the graph
edge.getToVertex().removeIncoming(edge);
edge.getFromVertex().removeOutgoing(edge);
}
return v;
}
/** Make the appropriate type of link edges from a vertex */
private void makeLinkEdges(Vertex from, StreetVertex to) {
if (from instanceof TemporaryStreetLocation) {
makeTemporaryEdges((TemporaryStreetLocation) from, to);
} else if (from instanceof TransitStop) {
makeTransitLinkEdges((TransitStop) from, to);
} else if (from instanceof BikeRentalStationVertex) {
makeBikeRentalLinkEdges((BikeRentalStationVertex) from, to);
} else if (from instanceof BikeParkVertex) {
makeBikeParkEdges((BikeParkVertex) from, to);
}
}
/** Make temporary edges to origin/destination vertex in origin/destination search **/
private void makeTemporaryEdges(TemporaryStreetLocation from, Vertex to) {
if (destructiveSplitting) {
throw new RuntimeException("Destructive splitting is used on temporary edges. Something is wrong!");
}
if (to instanceof TemporarySplitterVertex) {
from.setWheelchairAccessible(((TemporarySplitterVertex) to).isWheelchairAccessible());
}
if (from.isEndVertex()) {
LOG.debug("Linking end vertex to {} -> {}", to, from);
new TemporaryFreeEdge(to, from);
} else {
LOG.debug("Linking start vertex to {} -> {}", from, to);
new TemporaryFreeEdge(from, to);
}
}
/** Make bike park edges */
private void makeBikeParkEdges(BikeParkVertex from, StreetVertex to) {
if (!destructiveSplitting) {
throw new RuntimeException("Bike park edges are created with non destructive splitting!");
}
for (StreetBikeParkLink sbpl : Iterables.filter(from.getOutgoing(), StreetBikeParkLink.class)) {
if (sbpl.getToVertex() == to)
return;
}
new StreetBikeParkLink(from, to);
new StreetBikeParkLink(to, from);
}
/**
* Make street transit link edges, unless they already exist.
*/
private void makeTransitLinkEdges (TransitStop tstop, StreetVertex v) {
if (!destructiveSplitting) {
throw new RuntimeException("Transitedges are created with non destructive splitting!");
}
// 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)) {
if (e.getToVertex() == v)
return;
}
new StreetTransitLink(tstop, v, tstop.hasWheelchairEntrance());
new StreetTransitLink(v, tstop, tstop.hasWheelchairEntrance());
}
/** Make link edges for bike rental */
private void makeBikeRentalLinkEdges (BikeRentalStationVertex from, StreetVertex to) {
if (!destructiveSplitting) {
throw new RuntimeException("Bike rental edges are created with non destructive splitting!");
}
for (StreetBikeRentalLink sbrl : Iterables.filter(from.getOutgoing(), StreetBikeRentalLink.class)) {
if (sbrl.getToVertex() == to)
return;
}
new StreetBikeRentalLink(from, to);
new StreetBikeRentalLink(to, from);
}
/** 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())));
}
/** projected distance from stop to edge, in latitude degrees */
private static double distance (Vertex tstop, Vertex tstop2, double xscale) {
// use JTS internal tools wherever possible
return new Coordinate(tstop.getLon() * xscale, tstop.getLat()).distance(new Coordinate(tstop2.getLon() * xscale, tstop2.getLat()));
}
/** project this linestring to an equirectangular projection */
private static LineString equirectangularProject(LineString geometry, double xscale) {
Coordinate[] coords = new Coordinate[geometry.getNumPoints()];
for (int i = 0; i < coords.length; i++) {
Coordinate c = geometry.getCoordinateN(i);
c = (Coordinate) c.clone();
c.x *= xscale;
coords[i] = c;
}
return geometryFactory.createLineString(coords);
}
/**
* Used to link origin and destination points to graph non destructively.
*
* Split edges don't replace existing ones and only temporary edges and vertices are created.
*
* Will throw ThrivialPathException if origin and destination Location are on the same edge
*
* @param location
* @param options
* @param endVertex true if this is destination vertex
* @return
*/
public Vertex getClosestVertex(GenericLocation location, RoutingRequest options,
boolean endVertex) {
if (destructiveSplitting) {
throw new RuntimeException("Origin and destination search is used with destructive splitting. Something is wrong!");
}
if (endVertex) {
LOG.debug("Finding end vertex for {}", location);
} else {
LOG.debug("Finding start vertex for {}", location);
}
Coordinate coord = location.getCoordinate();
//TODO: add nice name
String name;
if (location.name == null || location.name.isEmpty()) {
if (endVertex) {
name = "Destination";
} else {
name = "Origin";
}
} else {
name = location.name;
}
TemporaryStreetLocation closest = new TemporaryStreetLocation(UUID.randomUUID().toString(),
coord, new NonLocalizedString(name), endVertex);
TraverseMode nonTransitMode = TraverseMode.WALK;
//It can be null in tests
if (options != null) {
TraverseModeSet modes = options.modes;
if (modes.getCar())
// for park and ride we will start in car mode and walk to the end vertex
if (endVertex && (options.parkAndRide || options.kissAndRide)) {
nonTransitMode = TraverseMode.WALK;
} else {
nonTransitMode = TraverseMode.CAR;
}
else if (modes.getWalk())
nonTransitMode = TraverseMode.WALK;
else if (modes.getBicycle())
nonTransitMode = TraverseMode.BICYCLE;
}
if(!link(closest, nonTransitMode, options)) {
LOG.warn("Couldn't link {}", location);
}
return closest;
}
}