/* 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.edgetype.loader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.opentripplanner.common.geometry.DistanceLibrary;
import org.opentripplanner.common.geometry.GeometryUtils;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.common.model.P2;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.TraversalRequirements;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.edgetype.AreaEdge;
import org.opentripplanner.routing.edgetype.PlainStreetEdge;
import org.opentripplanner.routing.edgetype.StreetBikeRentalLink;
import org.opentripplanner.routing.edgetype.StreetEdge;
import org.opentripplanner.routing.edgetype.StreetTransitLink;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.impl.CandidateEdgeBundle;
import org.opentripplanner.routing.vertextype.BikeRentalStationVertex;
import org.opentripplanner.routing.vertextype.IntersectionVertex;
import org.opentripplanner.routing.vertextype.StreetVertex;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.opentripplanner.routing.vertextype.TransitVertex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Point;
/**
* This class keeps track of all of the edges created during a particular case of network linking
*/
public class LinkRequest {
private static Logger LOG = LoggerFactory.getLogger(LinkRequest.class);
NetworkLinkerLibrary linker;
private Boolean result;
private List<Edge> edgesAdded = new ArrayList<Edge>();
private DistanceLibrary distanceLibrary;
public LinkRequest(NetworkLinkerLibrary linker) {
this.linker = linker;
this.distanceLibrary = linker.getDistanceLibrary();
}
/**
* The entry point for networklinker to link each bike rental station.
*
* @param v
* Sets result to true if the links were successfully added, otherwise false
*/
public void connectVertexToStreets(BikeRentalStationVertex v) {
Collection<StreetVertex> nearbyStreetVertices = getNearbyStreetVertices(v, null, null);
if (nearbyStreetVertices == null) {
result = false;
} else {
for (StreetVertex sv : nearbyStreetVertices) {
addEdges(new StreetBikeRentalLink(sv, v),
new StreetBikeRentalLink(v, sv));
}
result = true;
}
}
public boolean getResult() {
if (result == null) {
throw new IllegalStateException("Can't get result of LinkRequest; no operation performed");
}
return result;
}
/**
* For the given vertex, find or create some vertices nearby in the street network.
* Once the vertices are found they are remembered, and subsequent calls to this
* method with the same Vertex argument will return the same collection of vertices.
* This method is potentially called multiple times with the same Vertex as an argument,
* via the "determineIncomingEdgesForVertex" and "determineOutgoingEdgesForVertex" methods.
*
* Used by both the network linker and for adding temporary "extra" edges at the origin
* and destination of a search.
*/
private Collection<StreetVertex> getNearbyStreetVertices(Vertex v, Collection<Edge> nearbyRouteEdges, RoutingRequest options) {
Collection<StreetVertex> existing = linker.splitVertices.get(v);
if (existing != null)
return existing;
String vertexLabel;
if (v instanceof TransitVertex)
vertexLabel = "link for " + ((TransitVertex)v).getStopId();
else
vertexLabel = "link for " + v;
Coordinate coordinate = v.getCoordinate();
/* is there a bundle of edges nearby to use or split? */
GenericLocation location = new GenericLocation(coordinate);
TraversalRequirements reqs = new TraversalRequirements(options);
CandidateEdgeBundle edges = linker.index.getClosestEdges(location, reqs, null, nearbyRouteEdges, true);
if (edges == null || edges.size() < 1) {
// no edges were found nearby, or a bidirectional/loop bundle of edges was not identified
LOG.debug("found too few edges: {} {}", v.getName(), v.getCoordinate());
return null;
}
// if the bundle was caught endwise (T intersections and dead ends),
// get the intersection instead.
if (edges.endwise()) {
List<StreetVertex> list = Arrays.asList(edges.endwiseVertex);
linker.splitVertices.put(v, list);
return list;
} else {
/* is the stop right at an intersection? */
StreetVertex atIntersection = linker.index.getIntersectionAt(coordinate);
if (atIntersection != null) {
// if so, the stop can be linked directly to all vertices at the intersection
if (edges.getScore() > distanceLibrary.distance(atIntersection.getCoordinate(), coordinate))
return Arrays.asList(atIntersection);
}
return getSplitterVertices(vertexLabel, edges.toEdgeList(), coordinate);
}
}
/**
* Given a bundle of parallel, coincident edges, find a vertex splitting the set of edges as close as
* possible to the given coordinate. If necessary, create new edges reflecting the split and update the
* replacement edge lists accordingly.
*
* Split edges are not added to the graph immediately, so that they can be re-split later if another stop
* is located near the same bundle of original edges.
*/
private Collection<StreetVertex> getSplitterVertices(String label, Collection<StreetEdge> edges, Coordinate coordinate) {
// It is assumed that we are splitting at least one edge.
if (edges.size() < 1) {
return null;
}
// Has this set of original edges already been replaced by split edges?
HashSet<StreetEdge> edgeSet = new HashSet<StreetEdge>(edges);
LinkedList<P2<PlainStreetEdge>> replacement = linker.replacements.get(edgeSet);
if (replacement == null) {
replacement = new LinkedList<P2<PlainStreetEdge>>();
Iterator<StreetEdge> iter = edges.iterator();
StreetEdge first = iter.next();
StreetEdge second = null;
while (iter.hasNext()) {
StreetEdge edge = iter.next();
if (edge.getFromVertex() == first.getToVertex() && edge.getToVertex() == first.getFromVertex()) {
second = edge;
}
}
PlainStreetEdge secondClone;
if (second == null) {
secondClone = null;
} else {
secondClone = ((PlainStreetEdge) second).clone();
}
P2<PlainStreetEdge> newEdges = new P2<PlainStreetEdge>(((PlainStreetEdge) first).clone(), secondClone);
replacement.add(newEdges);
linker.replacements.put(edgeSet, replacement);
}
// If the original replacement edge pair has already been split,
// decide out which sub-segment the current coordinate lies on.
double bestDist = Double.MAX_VALUE;
P2<PlainStreetEdge> bestPair = null;
Point p = GeometryUtils.getGeometryFactory().createPoint(coordinate);
for (P2<PlainStreetEdge> pair : replacement) {
PlainStreetEdge e1 = pair.getFirst();
double dist = e1.getGeometry().distance(p);
if (dist < bestDist) {
bestDist = dist;
bestPair = pair;
}
}
// split the (sub)segment edge pair as needed, returning vertices at the split point
return split(replacement, label, bestPair, coordinate);
}
/**
* Split a matched (bidirectional) pair of edges at the given coordinate, unless the coordinate is
* very close to one of the existing endpoints. Returns the vertices located at the split point.
*/
private Collection<StreetVertex> split(LinkedList<P2<PlainStreetEdge>> replacement, String label,
P2<PlainStreetEdge> bestPair, Coordinate coordinate) {
PlainStreetEdge e1 = bestPair.getFirst();
PlainStreetEdge e2 = bestPair.getSecond();
String name = e1.getName();
StreetVertex e1v1 = (StreetVertex) e1.getFromVertex();
StreetVertex e1v2 = (StreetVertex) e1.getToVertex();
LineString forwardGeometry = e1.getGeometry();
StreetVertex e2v1 = null;
StreetVertex e2v2 = null;
P2<LineString> backGeometryPair = null;
if (e2 != null) {
e2v1 = (StreetVertex) e2.getFromVertex();
e2v2 = (StreetVertex) e2.getToVertex();
LineString backGeometry = e2.getGeometry();
backGeometryPair = GeometryUtils.splitGeometryAtPoint(backGeometry,
coordinate);
}
P2<LineString> forwardGeometryPair = GeometryUtils.splitGeometryAtPoint(forwardGeometry,
coordinate);
LineString toMidpoint = forwardGeometryPair.getFirst();
Coordinate midCoord = toMidpoint.getEndPoint().getCoordinate();
// determine how far along the original pair the split would occur
double totalGeomLength = forwardGeometry.getLength();
double lengthRatioIn = toMidpoint.getLength() / totalGeomLength;
// If coordinate is coincident with an endpoint of the edge pair, splitting is unnecessary.
// note: the pair potentially being split was generated by the 'replace' method,
// so the two PlainStreetEdges are known to be pointing in opposite directions.
if (lengthRatioIn < 0.00001) {
ArrayList<StreetVertex> out = new ArrayList<StreetVertex>();
out.add(e1v1);
if (e2 != null) {
out.add(e2v2);
}
return out;
} else if (lengthRatioIn > 0.99999) {
ArrayList<StreetVertex> out = new ArrayList<StreetVertex>();
out.add(e1v2);
if (e2 != null) {
out.add(e1v2);
}
return out;
}
double lengthIn = e1.getLength() * lengthRatioIn;
double lengthOut = e1.getLength() * (1 - lengthRatioIn);
// Split each edge independently. If a only one splitter vertex is used, routing may take
// shortcuts thought the splitter vertex to avoid turn penalties.
IntersectionVertex e1midpoint = new IntersectionVertex(linker.graph, "split 1 at " + label, midCoord.x, midCoord.y, name);
// We are replacing two edges with four edges
PlainStreetEdge forward1 = new PlainStreetEdge(e1v1, e1midpoint, toMidpoint, name, lengthIn,
e1.getPermission(), false);
PlainStreetEdge forward2 = new PlainStreetEdge(e1midpoint, e1v2,
forwardGeometryPair.getSecond(), name, lengthOut, e1.getPermission(), true);
if (e1 instanceof AreaEdge) {
((AreaEdge) e1).getArea().addVertex(e1midpoint, linker.graph);
}
addEdges(forward1, forward2);
PlainStreetEdge backward1 = null;
PlainStreetEdge backward2 = null;
IntersectionVertex e2midpoint = null;
if (e2 != null) {
e2midpoint = new IntersectionVertex(linker.graph, "split 2 at " + label, midCoord.x, midCoord.y, name);
backward1 = new PlainStreetEdge(e2v1, e2midpoint, backGeometryPair.getFirst(),
name, lengthOut, e2.getPermission(), false);
backward2 = new PlainStreetEdge(e2midpoint, e2v2, backGeometryPair.getSecond(),
name, lengthIn, e2.getPermission(), true);
if (e2 instanceof AreaEdge) {
((AreaEdge) e2).getArea().addVertex(e2midpoint, linker.graph);
}
double backwardBseLengthIn = e2.getBicycleSafetyEffectiveLength() * lengthRatioIn;
double backwardBseLengthOut = e2.getBicycleSafetyEffectiveLength() * (1 - lengthRatioIn);
backward1.setBicycleSafetyEffectiveLength(backwardBseLengthIn);
backward2.setBicycleSafetyEffectiveLength(backwardBseLengthOut);
backward1.setElevationProfile(e2.getElevationProfile(0, lengthOut), false);
backward2.setElevationProfile(e2.getElevationProfile(lengthIn, totalGeomLength), false);
addEdges(backward1, backward2);
}
double forwardBseLengthIn = e1.getBicycleSafetyEffectiveLength() * lengthRatioIn;
double forwardBseLengthOut = e1.getBicycleSafetyEffectiveLength() * (1 - lengthRatioIn);
forward1.setBicycleSafetyEffectiveLength(forwardBseLengthIn);
forward2.setBicycleSafetyEffectiveLength(forwardBseLengthOut);
forward1.setElevationProfile(e1.getElevationProfile(0, lengthIn), false);
forward2.setElevationProfile(e1.getElevationProfile(lengthOut, totalGeomLength), false);
// swap the new split edge into the replacements list, and remove the old ones
ListIterator<P2<PlainStreetEdge>> it = replacement.listIterator();
while (it.hasNext()) {
P2<PlainStreetEdge> pair = it.next();
if (pair == bestPair) {
it.set(new P2<PlainStreetEdge>(forward1, backward2));
it.add(new P2<PlainStreetEdge>(forward2, backward1));
break;
}
}
// disconnect the two old edges from the graph
linker.graph.removeTemporaryEdge(e1);
edgesAdded.remove(e1);
//e1.detach();
if (e2 != null) {
linker.graph.removeTemporaryEdge(e2);
edgesAdded.remove(e2);
//e2.detach();
// return the two new splitter vertices
return Arrays.asList((StreetVertex) e1midpoint, e2midpoint);
} else {
// return the one new splitter vertices
return Arrays.asList((StreetVertex) e1midpoint);
}
}
private void addEdges(Edge... newEdges) {
edgesAdded.addAll(Arrays.asList(newEdges));
}
public List<Edge> getEdgesAdded() {
return edgesAdded;
}
public void connectVertexToStreets(TransitStop v, boolean wheelchairAccessible) {
List<Edge> nearbyEdges = null;
if (linker.edgesForRoute != null && linker.transitIndex != null) {
nearbyEdges = new ArrayList<Edge>();
for (AgencyAndId route : linker.transitIndex.getRoutesForStop(v.getStopId())) {
List<Edge> edges = linker.edgesForRoute.get(route);
if (edges != null) {
nearbyEdges.addAll(edges);
}
}
}
TraverseModeSet modes = v.getModes().clone();
modes.setMode(TraverseMode.WALK, true);
RoutingRequest request = new RoutingRequest(modes);
Collection<StreetVertex> nearbyStreetVertices = getNearbyStreetVertices(v, nearbyEdges, request);
if (nearbyStreetVertices == null) {
result = false;
} else {
for (StreetVertex sv : nearbyStreetVertices) {
new StreetTransitLink(sv, v, wheelchairAccessible);
new StreetTransitLink(v, sv, wheelchairAccessible);
}
result = true;
}
}
}