/* 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.raptor; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.apache.commons.math3.util.FastMath; import org.opentripplanner.common.geometry.DistanceLibrary; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.routing.algorithm.strategies.RemainingWeightHeuristic; import org.opentripplanner.routing.algorithm.strategies.SearchTerminationStrategy; import org.opentripplanner.routing.algorithm.strategies.SkipTraverseResultStrategy; import org.opentripplanner.routing.algorithm.strategies.TransitLocalStreetService; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.edgetype.PreAlightEdge; import org.opentripplanner.routing.edgetype.PreBoardEdge; import org.opentripplanner.routing.edgetype.StreetEdge; import org.opentripplanner.routing.edgetype.TransitBoardAlight; import org.opentripplanner.routing.graph.AbstractVertex; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.spt.ArrayMultiShortestPathTree; import org.opentripplanner.routing.spt.ShortestPathTree; import org.opentripplanner.routing.spt.ShortestPathTreeFactory; import org.opentripplanner.routing.vertextype.TransitStop; import org.opentripplanner.routing.vertextype.TransitStopArrive; import org.opentripplanner.routing.vertextype.TransitStopDepart; import com.vividsolutions.jts.geom.Coordinate; public class TargetBound implements SearchTerminationStrategy, SkipTraverseResultStrategy, RemainingWeightHeuristic, ShortestPathTreeFactory { private static final long serialVersionUID = -5296036164138922096L; private static final long WORST_TIME_DIFFERENCE = 3600; private static final double WORST_WEIGHT_DIFFERENCE_FACTOR = 1.3; List<State> bounders; private Vertex realTarget; private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); private Coordinate realTargetCoordinate; private double distanceToNearestTransitStop; private TransitLocalStreetService transitLocalStreets; private double speedUpperBound; //this is saved so that it can be reused in various skipping functions private double targetDistance; private double speedWeight; /** * How much longer the worst path can be than the best in terms of time. * Setting this lower will cut off some less-walking more-time paths. * Setting it higher will slow down the search a lot. */ private double timeBoundFactor = 2; private List<Integer> previousArrivalTime = new ArrayList<Integer>(); private RoutingRequest options; public ShortestPathTree spt = new ArrayMultiShortestPathTree(options); double[] distance = new double[AbstractVertex.getMaxIndex()]; public double bestTargetDistance = Double.POSITIVE_INFINITY; public List<State> removedBoundingStates = new ArrayList<State>(); private List<State> transitStopsVisited = new ArrayList<State>(); private double transferTimeInWalkDistance; public TargetBound(RoutingRequest options) { this.options = options; if (options.rctx.target != null) { this.realTarget = options.rctx.target; this.realTargetCoordinate = realTarget.getCoordinate(); this.distanceToNearestTransitStop = realTarget.getDistanceToNearestTransitStop(); bounders = new ArrayList<State>(); transitLocalStreets = options.rctx.graph.getService(TransitLocalStreetService.class); speedUpperBound = options.getStreetSpeedUpperBound(); this.speedWeight = options.getWalkReluctance() / speedUpperBound; this.transferTimeInWalkDistance = options.getTransferSlack() / options.getWalkSpeed(); } } @Override public boolean shouldSearchContinue(Vertex origin, Vertex target, State current, ShortestPathTree spt, RoutingRequest traverseOptions) { final Vertex vertex = current.getVertex(); if (vertex instanceof TransitStop || vertex instanceof TransitStopDepart || vertex instanceof TransitStopArrive) { transitStopsVisited.add(current); } if (vertex == realTarget) { addBounder(current); } return true; } public void addBounder(State bounder) { for (Iterator<State> it = bounders.iterator(); it.hasNext(); ) { State old = it.next(); //exact dup if (old.getNumBoardings() == bounder.getNumBoardings() && old.getTimeSeconds() == bounder.getTimeSeconds() && old.getWalkDistance() == bounder.getWalkDistance()) return; if (bounder.dominates(old)) { it.remove(); removedBoundingStates.add(old); } else if (bounder.getNumBoardings() <= old.getNumBoardings() && options.arriveBy ? ( bounder.getTimeSeconds() - WORST_TIME_DIFFERENCE > old.getTimeSeconds()) : (bounder.getTimeSeconds() + WORST_TIME_DIFFERENCE < old.getTimeSeconds())) { it.remove(); removedBoundingStates.add(old); } } bounders.add(bounder); RaptorState state = (RaptorState) bounder.getExtension("raptorParent"); if (state == null) { previousArrivalTime.add(-1); return; } RaptorStop stop = state.stop; //get previous alight at stop if (options.isArriveBy()) { final int nextDepartTime = getNextDepartTime(options, (state.arrivalTime - options.getBoardSlack()) - 2, stop.stopVertex); previousArrivalTime.add((int) ((nextDepartTime - options.getAlightSlack()) - bounder.getElapsedTimeSeconds())); } else { final int previousArriveTime = getPreviousArriveTime(options, state.arrivalTime - options.getAlightSlack() + 2, stop.stopVertex); previousArrivalTime.add((int) (previousArriveTime + options.getAlightSlack() + bounder.getElapsedTimeSeconds())); } } @Override public boolean shouldSkipTraversalResult(Vertex origin, Vertex target, State parent, State current, ShortestPathTree spt, RoutingRequest traverseOptions) { if (realTarget == null) return false; final Vertex vertex = current.getVertex(); int vertexIndex = vertex.getIndex(); if (vertexIndex < distance.length) { if (distance[vertexIndex] > 0.0) { targetDistance = distance[vertexIndex]; } else { targetDistance = distanceLibrary.fastDistance(realTargetCoordinate.y, realTargetCoordinate.x, vertex.getY(), vertex.getX()); distance[vertexIndex] = targetDistance; if (vertex instanceof TransitStop && targetDistance < bestTargetDistance) { bestTargetDistance = targetDistance; } } } else { targetDistance = distanceLibrary.fastDistance(realTargetCoordinate.y, realTargetCoordinate.x, vertex.getY(), vertex.getX()); } final double remainingWalk = traverseOptions.maxWalkDistance - current.getWalkDistance(); final double minWalk; double minTime = 0; if (targetDistance > remainingWalk) { // then we must have some transit + some walk. minWalk = this.distanceToNearestTransitStop + vertex.getDistanceToNearestTransitStop(); minTime = options.isArriveBy() ? traverseOptions.getAlightSlack() : traverseOptions.getBoardSlack(); if (current.getBackEdge() instanceof StreetEdge && transitLocalStreets != null && !transitLocalStreets.transferrable(vertex)) { return true; } } else { // could walk directly to destination if (targetDistance < distanceToNearestTransitStop || transitLocalStreets == null || !transitLocalStreets.transferrable(vertex)) minWalk = targetDistance; else minWalk = distanceToNearestTransitStop; } if (minWalk > remainingWalk) return true; final double optimisticDistance = current.getWalkDistance() + minWalk; final double walkTime = minWalk / speedUpperBound; minTime += (targetDistance - minWalk) / Raptor.MAX_TRANSIT_SPEED + walkTime; double stateTime = current.getOptimizedElapsedTimeSeconds() + minTime; double walkDistance = FastMath.max(optimisticDistance * Raptor.WALK_EPSILON, optimisticDistance + transferTimeInWalkDistance); int i = 0; boolean prevBounded = !bounders.isEmpty(); for (State bounder : bounders) { if (removedBoundingStates.contains(bounder)) continue; if (current.getWeight() + minTime + walkTime * (options.getWalkReluctance() - 1) > bounder.getWeight() * WORST_WEIGHT_DIFFERENCE_FACTOR) { return true; } int prevTime = previousArrivalTime.get(i++); if (walkDistance > bounder.getWalkDistance() && current.getNumBoardings() >= bounder.getNumBoardings()) { if (current.getElapsedTimeSeconds() + minTime >= bounder.getElapsedTimeSeconds()) { return true; } else if (prevTime > 0 && (options.arriveBy ? (current.getTimeSeconds() - minTime >= prevTime) : ((current.getTimeSeconds() + minTime) <= prevTime))) { prevBounded = false; } } else { prevBounded = false; } //check that the new path is not much longer in time than the bounding path if (bounder.getOptimizedElapsedTimeSeconds() * timeBoundFactor < stateTime) { return true; } } return prevBounded; } public static int getNextDepartTime(RoutingRequest request, int departureTime, Vertex stopVertex) { int bestArrivalTime = Integer.MAX_VALUE; request.arriveBy = false; // find the boards for (Edge preboard : stopVertex.getOutgoing()) { if (preboard instanceof PreBoardEdge) { Vertex departure = preboard.getToVertex(); // this is the departure vertex for (Edge board : departure.getOutgoing()) { if (board instanceof TransitBoardAlight) { State state = new State(board.getFromVertex(), departureTime, request); State result = board.traverse(state); if (result == null) continue; int time = (int) result.getTimeSeconds(); if (time < bestArrivalTime) { bestArrivalTime = time; } } } } } request.arriveBy = true; return bestArrivalTime; } public static int getPreviousArriveTime(RoutingRequest request, int arrivalTime, Vertex stopVertex) { int bestArrivalTime = -1; request.arriveBy = true; // find the alights for (Edge prealight : stopVertex.getIncoming()) { if (prealight instanceof PreAlightEdge) { Vertex arrival = prealight.getFromVertex(); // this is the arrival vertex for (Edge alight : arrival.getIncoming()) { if (alight instanceof TransitBoardAlight) { State state = new State(alight.getToVertex(), arrivalTime, request); State result = alight.traverse(state); if (result == null) continue; int time = (int) result.getTimeSeconds(); if (time > bestArrivalTime) { bestArrivalTime = time; } } } } } request.arriveBy = false; return bestArrivalTime; } /** * This actually does have to be admissible, since when we find the target, it used to bound the rest of the search. */ @Override public double computeForwardWeight(State s, Vertex target) { return targetDistance * speedWeight; } @Override public double computeReverseWeight(State s, Vertex target) { return computeForwardWeight(s, target); } @Override public void reset() {} @Override public void initialize(State s, Vertex target) {} public double getTimeBoundFactor() { return timeBoundFactor; } public void setTimeBoundFactor(double timeBoundFactor) { this.timeBoundFactor = timeBoundFactor; } @Override public ShortestPathTree create(RoutingRequest options) { return spt; } public void addSptStates(List<MaxWalkState> states) { for (MaxWalkState state : states) { if (state.getVertex() instanceof TransitStop) { transitStopsVisited.add(state); } spt.add(state); } } public double getTargetDistance(Vertex vertex) { int vertexIndex = vertex.getIndex(); if (vertexIndex < distance.length) { if (distance[vertexIndex] > 0.0) { return distance[vertexIndex]; } else { double d = distanceLibrary.fastDistance(realTargetCoordinate.y, realTargetCoordinate.x, vertex.getY(), vertex.getX()); distance[vertexIndex] = d; return d; } } else { return distanceLibrary.fastDistance(realTargetCoordinate.y, realTargetCoordinate.x, vertex.getY(), vertex.getX()); } } public List<State> getTransitStopsVisited() { return transitStopsVisited; } public void prepareForSearch() { transitStopsVisited.clear(); } public void reset(RoutingRequest options) { this.options = options; if (realTarget != options.rctx.target) { this.realTarget = options.rctx.target; this.realTargetCoordinate = realTarget.getCoordinate(); this.distanceToNearestTransitStop = realTarget.getDistanceToNearestTransitStop(); bounders = new ArrayList<State>(); Arrays.fill(distance, -1); } spt = new ArrayMultiShortestPathTree(options); transitLocalStreets = options.rctx.graph.getService(TransitLocalStreetService.class); speedUpperBound = options.getStreetSpeedUpperBound(); this.speedWeight = options.getWalkReluctance() / speedUpperBound; } @Override public void abort() {} }