/* 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.factory;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Stop;
import org.opentripplanner.common.geometry.DistanceLibrary;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.common.model.P2;
import org.opentripplanner.common.pqueue.BinHeap;
import org.opentripplanner.routing.algorithm.NegativeWeightException;
import org.opentripplanner.routing.core.OptimizeType;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.State;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.edgetype.FrequencyBoard;
import org.opentripplanner.routing.edgetype.PatternEdge;
import org.opentripplanner.routing.edgetype.TransitBoardAlight;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.graph.Edge;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.impl.StreetVertexIndexServiceImpl;
import org.opentripplanner.routing.spt.BasicShortestPathTree;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.geom.Coordinate;
public class LocalStopFinder {
private final static Logger LOG = LoggerFactory.getLogger(LocalStopFinder.class);
private static final double MAX_SUBOPTIMAL_DISTANCE = 100; /* allow a slop of ~10 seconds */
private static final double LOCAL_STOP_SEARCH_RADIUS = 1000; /* how far to search for nearby stops */
private HashSet<TripPattern> patterns;
private Graph graph;
private StreetVertexIndexServiceImpl indexService;
private RoutingRequest walkingOptions;
private RoutingRequest bikingOptions;
private HashMap<Stop, HashMap<TripPattern, P2<Double>>> neighborhoods;
private HashMap<AgencyAndId, TransitStop> transitStops;
private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance();
public LocalStopFinder(StreetVertexIndexServiceImpl indexService, Graph graph) {
this.graph = graph;
this.indexService = indexService;
}
public void markLocalStops() {
LOG.debug("Finding local stops");
patterns = new HashSet<TripPattern>();
transitStops = new HashMap<AgencyAndId, TransitStop>();
int total = 0;
for (Vertex gv : graph.getVertices()) {
if (gv instanceof TransitStop) {
TransitStop ts = (TransitStop) gv;
ts.setLocal(true);
transitStops.put(ts.getStopId(), ts);
total ++;
}
for (Edge e : gv.getOutgoing()) {
if (e instanceof TransitBoardAlight && ((TransitBoardAlight) e).isBoarding()) {
TripPattern pattern = ((TransitBoardAlight) e).getPattern();
patterns.add(pattern);
}
if (e instanceof FrequencyBoard) {
TripPattern pattern = ((FrequencyBoard) e).getPattern();
patterns.add(pattern);
}
}
}
// For each pattern, check if each stop is local
neighborhoods = new HashMap<Stop, HashMap<TripPattern, P2<Double>>>();
walkingOptions = new RoutingRequest(new TraverseModeSet(TraverseMode.WALK));
bikingOptions = new RoutingRequest(new TraverseModeSet(TraverseMode.BICYCLE));
bikingOptions.optimize = OptimizeType.SAFE;
int nonLocal = 0;
for (TripPattern pattern : patterns) {
List<Stop> stops = getStops(pattern);
// a stop is local if, in order to get to all other nearby patterns, it is
// just as good to transfer at the previous stop.
// so, each stop in the system needs a neighborhood of patterns.
HashMap<TripPattern, P2<Double>> previousDistances = null;
HashMap<TripPattern, P2<Double>> distances = null;
for (int i = 0; i < stops.size() - 1; ++i) {
Stop stop = stops.get(i);
TransitStop transitStop = getVertexForStop(stop);
previousDistances = distances;
distances = getNeighborhood(stop);
HashMap<TripPattern, P2<Double>> nextDistances = null;
Stop nextStop = stops.get(i + 1);
nextDistances = getNeighborhood(nextStop);
if (previousDistances == null) {
// first stop is never local
if (transitStop.isLocal()) {
nonLocal ++;
}
transitStop.setLocal(false);
continue;
} else {
boolean local = true;
for (Entry<TripPattern, P2<Double>> entry : distances.entrySet()) {
TripPattern key = entry.getKey();
if (key == pattern) {
continue;
}
P2<Double> distance = entry.getValue();
P2<Double> previousDistance = previousDistances.get(key);
P2<Double> nextDistance = nextDistances.get(key);
if (distance.getFirst() == 0) {
local = false;
} else if (previousDistance == null) {
if (nextDistance == null
|| nextDistance.getFirst() + MAX_SUBOPTIMAL_DISTANCE >= distance
.getFirst()
|| nextDistance.getSecond() + MAX_SUBOPTIMAL_DISTANCE >= distance
.getSecond()) {
local = false;
break;
}
} else if (distance.getFirst() <= previousDistance.getFirst()
+ MAX_SUBOPTIMAL_DISTANCE
&& (nextDistance == null || nextDistance.getFirst()
+ MAX_SUBOPTIMAL_DISTANCE >= distance.getFirst())) {
local = false;
break;
} else if (distance.getSecond() <= previousDistance.getSecond()
+ MAX_SUBOPTIMAL_DISTANCE
&& (nextDistance == null || nextDistance.getSecond()
+ MAX_SUBOPTIMAL_DISTANCE >= distance.getSecond())) {
local = false;
break;
}
}
if (local == false) {
if (transitStop.isLocal()) {
nonLocal ++;
}
transitStop.setLocal(false);
}
}
}
// last stop is never local
Stop stop = stops.get(stops.size() - 1);
TransitStop transitStop = getVertexForStop(stop);
if (transitStop.isLocal()) {
nonLocal ++;
}
transitStop.setLocal(false);
}
LOG.debug("Local stops: " + (total - nonLocal) + " / " + total);
}
private HashMap<TripPattern, P2<Double>> getNeighborhood(Stop stop) {
TransitStop transitStop = getVertexForStop(stop);
HashMap<TripPattern, P2<Double>> neighborhood = neighborhoods.get(stop);
if (neighborhood == null) {
Set<TripPattern> nearbyPatterns = getNearbyPatterns(stop);
HashMap<TripPattern, Double> walkNeighborhood = getBestDistanceForPatterns(graph, transitStop, walkingOptions, nearbyPatterns);
HashMap<TripPattern, Double> bikeNeighborhood = getBestDistanceForPatterns(graph, transitStop, bikingOptions, nearbyPatterns);
neighborhood = new HashMap<TripPattern, P2<Double>>();
for (TripPattern p : nearbyPatterns) {
Double walkDistance = walkNeighborhood.get(p);
if (walkDistance == null) {
continue; /* if you can't walk there, there's no point */
}
Double bikeDistance = bikeNeighborhood.get(p);
if (bikeDistance == null) {
bikeDistance = Double.MAX_VALUE; /* wrong, but will cause stop to not be marked as local on this pattern's account, which is probably right.*/
}
neighborhood.put(p, new P2<Double> (walkDistance, bikeDistance));
}
neighborhoods.put(stop, neighborhood);
}
return neighborhood;
}
/**
* TODO - Any way this can use the existing search code? AStar or Dijkstra
* @param graph
* @param origin
* @param options
* @param nearbyPatterns
* @return
*/
private HashMap<TripPattern, Double> getBestDistanceForPatterns(Graph graph, Vertex origin,
RoutingRequest options, Set<TripPattern> nearbyPatterns) {
// Iteration Variables
HashSet<Vertex> closed = new HashSet<Vertex>();
BinHeap<State> queue = new BinHeap<State>(50);
BasicShortestPathTree spt = new BasicShortestPathTree(options);
State initial = new State(origin, options);
spt.add(initial);
queue.insert(initial, 0);
HashMap<TripPattern, Double> patternCosts = new HashMap<TripPattern, Double>();
int patternsSeen = 0;
while (!queue.empty()) { // Until the priority queue is empty:
State u = queue.extract_min(); // get the lowest-weightSum Vertex 'u',
if (! spt.visit(u))
continue;
Vertex fromv = u.getVertex();
closed.add(fromv);
if (fromv instanceof TransitStop) {
Vertex departureVertex = null;
for (Edge e : fromv.getOutgoing()) {
// preboard edge, to departure vertex
departureVertex = e.getToVertex();
for (Edge e2 : departureVertex.getOutgoing()) {
if ((e2 instanceof TransitBoardAlight
&& ((TransitBoardAlight) e2).isBoarding()) || e2 instanceof FrequencyBoard) {
/* finally, a boarding edge */
TripPattern pattern = ((PatternEdge) e2).getPattern();
if (nearbyPatterns.contains(pattern)) {
Double cost = patternCosts.get(pattern);
if (cost == null) {
patternCosts.put(pattern, u.getWeight());
patternsSeen++;
if (patternsSeen == nearbyPatterns.size()) {
return patternCosts;
}
} else if (cost > u.getWeight()) {
patternCosts.put(pattern, u.getWeight());
}
}
}
}
}
}
if (distanceLibrary .fastDistance(fromv.getCoordinate(), origin.getCoordinate()) > LOCAL_STOP_SEARCH_RADIUS) {
/* we have now traveled far from the origin, so we know that anything we find
* from here on out is going to be too far
*/
return patternCosts;
}
Iterable<Edge> outgoing = fromv.getOutgoing();
for (Edge edge : outgoing) {
State v = edge.traverse(u);
// When an edge leads nowhere (as indicated by returning NULL), the iteration is
// over.
if (v == null)
continue;
double dw = v.getWeight() - u.getWeight();
if (dw < 0)
throw new NegativeWeightException(String.valueOf(dw));
Vertex toVertex = v.getVertex();
if (closed.contains(toVertex))
continue;
if (spt.add(v))
queue.insert(v, v.getWeight());
}
}
return patternCosts;
}
private TransitStop getVertexForStop(Stop stop) {
return transitStops.get(stop.getId());
}
private HashSet<TripPattern> getNearbyPatterns(Stop stop) {
// get all transit stops within about the LOCAL_STOP_SEARCH_RADIUS
Coordinate c = new Coordinate(stop.getLon(), stop.getLat());
List<Vertex> localTransitStops = indexService.getLocalTransitStops(c, LOCAL_STOP_SEARCH_RADIUS);
HashSet<TripPattern> neighborhood = new HashSet<TripPattern>();
for (Vertex v : localTransitStops) {
if (v instanceof TransitStop) {
if (((TransitStop) v).isEntrance()) {
// enter to get to actual stop
for (Edge e : v.getOutgoing()) {
v = e.getToVertex();
break;
}
}
for (Edge e : v.getOutgoing()) {
for (Edge e2 : e.getToVertex().getOutgoing()) {
if (e2 instanceof TransitBoardAlight && ((TransitBoardAlight) e2).isBoarding()) {
neighborhood.add(((TransitBoardAlight) e2).getPattern());
} else if (e2 instanceof FrequencyBoard) {
neighborhood.add(((FrequencyBoard) e2).getPattern());
}
}
}
}
}
return neighborhood;
}
private List<Stop> getStops(TripPattern pattern) {
return pattern.getStops();
}
}