/* 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.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import javax.annotation.PostConstruct; 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.routing.core.RoutingRequest; import org.opentripplanner.routing.core.ServiceDay; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.core.TraverseModeSet; import org.opentripplanner.routing.edgetype.PatternDwell; import org.opentripplanner.routing.edgetype.PatternHop; import org.opentripplanner.routing.edgetype.PatternInterlineDwell; import org.opentripplanner.routing.edgetype.PreAlightEdge; import org.opentripplanner.routing.edgetype.PreBoardEdge; import org.opentripplanner.routing.edgetype.TransitBoardAlight; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.impl.RetryingPathServiceImpl; import org.opentripplanner.routing.pathparser.BasicPathParser; import org.opentripplanner.routing.pathparser.NoThruTrafficPathParser; import org.opentripplanner.routing.pathparser.PathParser; import org.opentripplanner.routing.services.GraphService; import org.opentripplanner.routing.services.PathService; import org.opentripplanner.routing.services.SPTService; import org.opentripplanner.routing.spt.GraphPath; import org.opentripplanner.routing.vertextype.TransitStop; import org.opentripplanner.routing.vertextype.TransitVertex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; public class Raptor implements PathService { private static final Logger log = LoggerFactory.getLogger(Raptor.class); static final double MAX_TRANSIT_SPEED = 25; private static final int MAX_WALK_MULTIPLE = 8; public static final double WALK_EPSILON = 1.10; @Autowired private GraphService graphService; private List<ServiceDay> cachedServiceDays; private RaptorData cachedRaptorData; private double multiPathTimeout = 0; // seconds /** * This is used for short paths (under shortPathCutoff). */ RetryingPathServiceImpl shortPathService = new RetryingPathServiceImpl(); /** * The max length, in meters, that will use the shortPathService. */ private double shortPathCutoff = 10000; @PostConstruct public void setup() { shortPathService.setGraphService(graphService); shortPathService.setSptService(sptService); } /** * Stop searching for additional itineraries (beyond the first one) after this many seconds * have elapsed, relative to the beginning of the search for the first itinerary. * A negative or zero value means search forever. */ public void setMultiPathTimeout (double seconds) { multiPathTimeout = seconds; } //fallback for nontransit trips @Autowired public SPTService sptService; private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); @Override public List<GraphPath> getPaths(RoutingRequest options) { final Graph graph = graphService.getGraph(options.getRouterId()); if (options.rctx == null) { options.setRoutingContext(graph); options.rctx.pathParsers = new PathParser[] { new BasicPathParser(), new NoThruTrafficPathParser() }; } if (!options.getModes().isTransit()) { return sptService.getShortestPathTree(options).getPaths(); } //also fall back to A* for short trips double distance = distanceLibrary.distance(options.rctx.origin.getCoordinate(), options.rctx.target.getCoordinate()); if (distance < shortPathCutoff) { log.debug("Falling back to A* for very short path"); return shortPathService.getPaths(options); } RaptorDataService service = graph.getService(RaptorDataService.class); if (service == null) { log.warn("No raptor data. Rebuild with RaptorDataBuilder"); return Collections.emptyList(); } RaptorData data = service.getData(); //we multiply the initial walk distance to account for epsilon dominance. double initialWalk = options.getMaxWalkDistance() * WALK_EPSILON; options.setMaxWalkDistance(initialWalk); //do not even bother with obviously impossible walks double minWalk = options.rctx.origin.getDistanceToNearestTransitStop() + options.rctx.target.getDistanceToNearestTransitStop(); if (options.getMaxWalkDistance() < minWalk) { options.setMaxWalkDistance(minWalk); } RoutingRequest walkOptions = options.clone(); walkOptions.rctx.pathParsers = new PathParser[0]; TraverseModeSet modes = options.getModes().clone(); modes.setTransit(false); walkOptions.setModes(modes); RaptorSearch search = new RaptorSearch(data, options); if (data.maxTransitRegions != null) { Calendar tripDate = Calendar.getInstance(graph.getTimeZone()); tripDate.setTime(new Date(1000L * options.dateTime)); Calendar maxTransitStart = Calendar.getInstance(graph.getTimeZone()); maxTransitStart.set(Calendar.YEAR, data.maxTransitRegions.startYear); maxTransitStart.set(Calendar.MONTH, data.maxTransitRegions.startMonth); maxTransitStart.set(Calendar.DAY_OF_MONTH, data.maxTransitRegions.startDay); int day = 0; while (tripDate.after(maxTransitStart)) { day++; tripDate.add(Calendar.DAY_OF_MONTH, -1); } if (day > data.maxTransitRegions.maxTransit.length || options.isWheelchairAccessible()) { day = -1; } search.maxTimeDayIndex = day; } int rushAheadRound = preliminaryRaptorSearch(data, options, walkOptions, search); long searchBeginTime = System.currentTimeMillis(); double expectedWorstTime = 1.5 * distanceLibrary.distance(options.rctx.origin.getCoordinate(), options.rctx.target.getCoordinate()) / options.getWalkSpeed(); int foundSoFar = 0; double firstWalkDistance = 0; List<RaptorState> targetStates = new ArrayList<RaptorState>(); do { int bestElapsedTime = Integer.MAX_VALUE; RETRY: do { for (int round = 0; round < options.getMaxTransfers() + 2; ++round) { if (!round(data, options, walkOptions, search, round)) break; long elapsed = System.currentTimeMillis() - searchBeginTime; if (elapsed > multiPathTimeout * 1000 && multiPathTimeout > 0 && targetStates.size() > 0) break RETRY; ArrayList<RaptorState> toRemove = new ArrayList<RaptorState>(); for (RaptorState state : search.getTargetStates()) { if (state.nBoardings == 0 && options.getMaxWalkDistance() > initialWalk) { toRemove.add(state); } } if (search.getTargetStates().size() > 0) { if (firstWalkDistance == 0) { firstWalkDistance = options.getMaxWalkDistance(); } for (RaptorState state : toRemove) { search.removeTargetState(state.walkPath); } } if (targetStates.size() >= options.getNumItineraries() && round >= rushAheadRound) { int oldBest = bestElapsedTime; for (RaptorState state : search.getTargetStates()) { final int elapsedTime = (int) Math.abs(state.arrivalTime - options.dateTime); if (elapsedTime < bestElapsedTime) { bestElapsedTime = elapsedTime; } } int improvement = oldBest - bestElapsedTime; if (improvement < 600 && bestElapsedTime < expectedWorstTime) break RETRY; } } if (foundSoFar < search.getTargetStates().size()) { foundSoFar = search.getTargetStates().size(); } else if (foundSoFar > 0) { // we didn't find anything new in this round, and we already have // some paths, so bail out break; } options = options.clone(); walkOptions = walkOptions.clone(); if (search.getTargetStates().size() > 0 && bestElapsedTime < expectedWorstTime) { // we have found some paths so we no longer want to expand the max walk distance break RETRY; } else { options.setMaxWalkDistance(options.getMaxWalkDistance() * 2); walkOptions.setMaxWalkDistance(options.getMaxWalkDistance()); options.setWalkReluctance(options.getWalkReluctance() * 2); walkOptions.setWalkReluctance(options.getWalkReluctance()); } search.reset(options); } while (options.getMaxWalkDistance() < initialWalk * MAX_WALK_MULTIPLE && initialWalk < Double.MAX_VALUE); options = options.clone(); walkOptions = walkOptions.clone(); for (RaptorState state : search.getTargetStates()) { for (AgencyAndId trip : state.getTrips()) { options.banTrip(trip); } } if (search.getTargetStates().size() == 0) break; // no paths found; searching more won't help options.setMaxWalkDistance(firstWalkDistance); walkOptions.setMaxWalkDistance(firstWalkDistance); targetStates.addAll(search.getTargetStates()); search = new RaptorSearch(data, options); } while (targetStates.size() < options.getNumItineraries()); collectRoutesUsed(data, options, targetStates); if (targetStates.isEmpty()) { log.info("RAPTOR found no paths"); } Collections.sort(targetStates); if (targetStates.size() > options.getNumItineraries()) targetStates = targetStates.subList(0, options.getNumItineraries()); List<GraphPath> paths = new ArrayList<GraphPath>(); for (RaptorState targetState : targetStates) { // reconstruct path ArrayList<RaptorState> states = new ArrayList<RaptorState>(); RaptorState cur = targetState; while (cur != null) { states.add(cur); cur = cur.getParent(); } // states is in reverse order of time State state = getState(targetState.getRequest(), data, states); paths.add(new GraphPath(state, true)); } return paths; } private void collectRoutesUsed(RaptorData data, RoutingRequest options, List<RaptorState> targetStates) { // find start/end regions List<Integer> startRegions = getRegionsForVertex(data.regionData, options.rctx.fromVertex); int startRegion; if (startRegions.size() == 1) { startRegion = startRegions.get(0); } else { // on boundary return; } List<Integer> endRegions = getRegionsForVertex(data.regionData, options.rctx.toVertex); int endRegion; if (endRegions.size() == 1) { endRegion = endRegions.get(0); } else { // on boundary return; } HashSet<RaptorRoute> routes = data.regionData.routes[startRegion][endRegion]; HashSet<RaptorStop> stops = data.regionData.stops[startRegion][endRegion]; TARGETSTATE: for (RaptorState state : targetStates) { for (RaptorState dom : targetStates) { if (dom.nBoardings <= state.nBoardings && dom.arrivalTime < state.arrivalTime) { continue TARGETSTATE; } } synchronized(data) { while (state != null) { if (state.route != null) routes.add(state.route); if (state.stop != null) { stops.add(state.stop); } state = state.getParent(); } } } } /** * This does preliminary search over just routes and stops that have been used in the * past between these regions. */ private int preliminaryRaptorSearch(RaptorData data, RoutingRequest options, RoutingRequest walkOptions, RaptorSearch search) { //find start/end regions List<Integer> startRegions = getRegionsForVertex(data.regionData, options.rctx.fromVertex); int startRegion; //for trips that span regions, we can safely pick either region startRegion = startRegions.get(0); List<Integer> endRegions = getRegionsForVertex(data.regionData, options.rctx.toVertex); int endRegion; endRegion = endRegions.get(0); // create a reduced set of RaptorData with only the stops/routes previously seen on trips // from the start region to the end region RaptorData trimmedData = new RaptorData(); trimmedData.raptorStopsForStopId = new HashMap<AgencyAndId, RaptorStop>(); HashSet<RaptorStop> stops = data.regionData.stops[startRegion][endRegion]; for (RaptorStop stop : stops) { trimmedData.raptorStopsForStopId.put(stop.stopVertex.getStopId(), stop); } trimmedData.regionData = data.regionData; trimmedData.routes = data.regionData.routes[startRegion][endRegion]; trimmedData.stops = data.stops; //trimmedData.allowedStops = stops; trimmedData.routesForStop = data.routesForStop; double walkDistance = options.getMaxWalkDistance(); options = options.clone(); walkOptions = walkOptions.clone(); if (walkDistance > 4000) { // this is a really long walk. We'll almost never actually need this. So let's do our // preliminary search over just 4km first. options.setMaxWalkDistance(4000); walkOptions.setMaxWalkDistance(4000); } int round; if (trimmedData.routes.size() > 0) { log.debug("Doing preliminary search on limited route set (" + trimmedData.routes.size() + ", " + stops.size() + ")"); round = doPreliminarySearch(options, walkOptions, search, trimmedData); } else { round = 0; } if (search.getTargetStates().size() == 0 && walkDistance > 5000) { // nothing found in preliminary search // so we'll do a search with full set of routes & stops, but still limited distance log.debug("Doing preliminary search at limited distance"); round = doPreliminarySearch(options, walkOptions, search, data); } return round; } private int doPreliminarySearch(RoutingRequest options, RoutingRequest walkOptions, RaptorSearch search, RaptorData trimmedData) { RaptorSearch rushSearch = new RaptorSearch(trimmedData, options); int bestElapsedTime = Integer.MAX_VALUE; int round; for (round = 0; round < options.getMaxTransfers() + 2; round++) { if (!round(trimmedData, options, walkOptions, rushSearch, round)) break; if (rushSearch.getTargetStates().size() > 0) { int oldBest = bestElapsedTime; for (RaptorState state : rushSearch.getTargetStates()) { final int elapsedTime = (int) Math .abs(state.arrivalTime - options.dateTime); if (elapsedTime < bestElapsedTime) { bestElapsedTime = elapsedTime; } } int improvement = oldBest - bestElapsedTime; if (improvement < 600) break; } } for (RaptorState state : rushSearch.getTargetStates()) { search.bounder.addBounder(state.walkPath); search.addTargetState(state); } return round; } /** * Some vertices aren't associated with a region, because they're synthetic, or * maybe for some other region. So instead, we check their connected vertices, * recursively, to try to find their region. * * @param regionData * @param vertex * @return */ static List<Integer> getRegionsForVertex(RegionData regionData, Vertex vertex) { return new ArrayList<Integer>(getRegionsForVertex(regionData, vertex, new HashSet<Vertex>(), 0)); } /** * Internals of getRegionsForVertex; keeps track of seen vertices to avoid loops. * @param regionData * @param vertex * @param seen * @param depth * @return */ private static HashSet<Integer> getRegionsForVertex(RegionData regionData, Vertex vertex, HashSet<Vertex> seen, int depth) { seen.add(vertex); HashSet<Integer> regions = new HashSet<Integer>(); int region = vertex.getGroupIndex(); if (region >= 0) { regions.add(region); } else { for (Edge e: vertex.getOutgoing()) { final Vertex tov = e.getToVertex(); if (!seen.contains(tov)) regions.addAll(getRegionsForVertex(regionData, tov, seen, depth + 1)); } for (Edge e: vertex.getIncoming()) { final Vertex fromv = e.getFromVertex(); if (!seen.contains(fromv)) regions.addAll(getRegionsForVertex(regionData, fromv, seen, depth + 1)); } } return regions; } private State getState(RoutingRequest options, RaptorData data, ArrayList<RaptorState> states) { if (options.arriveBy) { return getStateArriveBy(data, states); } else { return getStateDepartAt(data, states); } } private State getStateDepartAt(RaptorData data, ArrayList<RaptorState> states) { State state = new State(states.get(0).getRequest()); for (int i = states.size() - 1; i >= 0; --i) { RaptorState cur = states.get(i); if (cur.walkPath != null) { //a walking step GraphPath path = new GraphPath(cur.walkPath, false); Edge edge0 = path.edges.getFirst(); if (edge0.getFromVertex() != state.getVertex()) { state = state.getBackState(); } for (Edge e : path.edges) { state = e.traverse(state); } } else { // so, cur is at this point at a transit stop; we have a route to board if (cur.getParent() == null || ! cur.getParent().interlining) { for (Edge e : state.getVertex().getOutgoing()) { if (e instanceof PreAlightEdge) { state = e.traverse(state); break; } } for (Edge e : state.getVertex().getOutgoing()) { if (e instanceof PreBoardEdge) { state = e.traverse(state); break; } } TransitBoardAlight board = cur.getRoute().boards[cur.boardStopSequence][cur.patternIndex]; state = board.traverse(state); } // now traverse the hops and dwells until we find the alight we're looking for HOP: while (true) { for (Edge e : state.getVertex().getOutgoing()) { if (e instanceof PatternDwell) { state = e.traverse(state); } else if (e instanceof PatternHop) { state = e.traverse(state); if (cur.interlining) { for (Edge e2 : state.getVertex().getOutgoing()) { RaptorState next = states.get(i - 1); if (e2 instanceof PatternInterlineDwell) { Stop toStop = ((TransitVertex) e2.getToVertex()).getStop(); Stop expectedStop = next.boardStop.stopVertex.getStop(); if (toStop.equals(expectedStop)) { State newState = e2.traverse(state); if (newState == null) continue; if (newState.getTripId() != next.tripId) continue; state = newState; break HOP; } } } } else { for (Edge e2 : state.getVertex().getOutgoing()) { if (e2 instanceof TransitBoardAlight) { for (Edge e3 : e2.getToVertex().getOutgoing()) { if (e3 instanceof PreAlightEdge) { if (data.raptorStopsForStopId.get(((TransitStop) e3 .getToVertex()).getStopId()) == cur.stop) { state = e2.traverse(state); state = e3.traverse(state); break HOP; } } } } } } } } } } } return state; } private State getStateArriveBy(RaptorData data, ArrayList<RaptorState> states) { RoutingRequest options = states.get(0).getRequest(); State state = new State(options.rctx.origin, options); for (int i = states.size() - 1; i >= 0; --i) { RaptorState cur = states.get(i); if (cur.walkPath != null) { GraphPath path = new GraphPath(cur.walkPath, false); Edge edge0 = path.edges.getLast(); if (edge0.getToVertex() != state.getVertex()) { state = state.getBackState(); } for (ListIterator<Edge> it = path.edges.listIterator(path.edges.size()); it.hasPrevious();) { Edge e = it.previous(); state = e.traverse(state); } } else { // so, cur is at this point at a transit stop departure; we have a route to alight from if (cur.getParent() == null || ! cur.getParent().interlining) { for (Edge e : state.getVertex().getIncoming()) { if (e instanceof PreAlightEdge) { state = e.traverse(state); } } TransitBoardAlight alight = cur.getRoute().alights[cur.boardStopSequence - 1][cur.patternIndex]; state = alight.traverse(state); } // now traverse the hops and dwells until we find the board we're looking for HOP: while (true) { for (Edge e : state.getVertex().getIncoming()) { if (e instanceof PatternDwell) { state = e.traverse(state); } else if (e instanceof PatternHop) { state = e.traverse(state); if (cur.interlining) { for (Edge e2 : state.getVertex().getIncoming()) { RaptorState next = states.get(i - 1); if (e2 instanceof PatternInterlineDwell) { Stop fromStop = ((TransitVertex) e2.getFromVertex()).getStop(); Stop expectedStop = next.boardStop.stopVertex.getStop(); if (fromStop.equals(expectedStop)) { State newState = e2.traverse(state); if (newState == null) continue; if (newState.getTripId() != next.tripId) continue; state = newState; break HOP; } } } } else { for (Edge e2 : state.getVertex().getIncoming()) { if (e2 instanceof TransitBoardAlight) { for (Edge e3 : e2.getFromVertex().getIncoming()) { if (e3 instanceof PreBoardEdge) { if (data.raptorStopsForStopId.get(((TransitStop) e3 .getFromVertex()).getStopId()) == cur.stop) { state = e2.traverse(state); state = e3.traverse(state); break HOP; } } } } } } } } } } } return state; } /** * Prune raptor data to include only routes and boardings which have trips today. Doesn't * actually improve speed */ @SuppressWarnings("unchecked") private RaptorData pruneDataForServiceDays(Graph graph, ArrayList<ServiceDay> serviceDays) { if (serviceDays.equals(cachedServiceDays)) return cachedRaptorData; RaptorData data = graph.getService(RaptorDataService.class).getData(); RaptorData pruned = new RaptorData(); pruned.raptorStopsForStopId = data.raptorStopsForStopId; pruned.stops = data.stops; pruned.routes = new ArrayList<RaptorRoute>(); pruned.routesForStop = new List[pruned.stops.length]; for (RaptorRoute route : data.routes) { ArrayList<Integer> keep = new ArrayList<Integer>(); for (int i = 0; i < route.boards[0].length; ++i) { Edge board = route.boards[0][i]; int serviceId; if (board instanceof TransitBoardAlight) { serviceId = ((TransitBoardAlight) board).getPattern().getServiceId(); } else { log.debug("Unexpected nonboard among boards"); continue; } for (ServiceDay day : serviceDays) { if (day.serviceIdRunning(serviceId)) { keep.add(i); break; } } } if (keep.isEmpty()) continue; int nPatterns = keep.size(); RaptorRoute prunedRoute = new RaptorRoute(route.getNStops(), nPatterns); for (int stop = 0; stop < route.getNStops() - 1; ++stop) { for (int pattern = 0; pattern < nPatterns; ++pattern) { prunedRoute.boards[stop][pattern] = route.boards[stop][keep.get(pattern)]; } } pruned.routes.add(route); for (RaptorStop stop : route.stops) { List<RaptorRoute> routes = pruned.routesForStop[stop.index]; if (routes == null) { routes = new ArrayList<RaptorRoute>(); pruned.routesForStop[stop.index] = routes; } routes.add(route); } } for (RaptorStop stop : data.stops) { if (pruned.routesForStop[stop.index] == null) { pruned.routesForStop[stop.index] = Collections.emptyList(); } } cachedServiceDays = serviceDays; cachedRaptorData = pruned; return pruned; } private boolean round(RaptorData data, RoutingRequest options, RoutingRequest walkOptions, final RaptorSearch search, int nBoardings) { log.debug("Round " + nBoardings); /* Phase 2: handle transit */ List<RaptorState> createdStates = search.transitPhase(options, nBoardings); /* Phase 3: handle walking paths */ return search.walkPhase(options, walkOptions, nBoardings, createdStates); } public RaptorStateSet getStateSet(RoutingRequest options) { final Graph graph; if (options.rctx == null) { graph = graphService.getGraph(options.getRouterId()); options.setRoutingContext(graph); options.rctx.pathParsers = new PathParser[] { new BasicPathParser(), new NoThruTrafficPathParser() }; } else { graph = options.rctx.graph; } RaptorData data = graph.getService(RaptorDataService.class).getData(); //we multiply the initial walk distance to account for epsilon dominance. options.setMaxWalkDistance(options.getMaxWalkDistance() * WALK_EPSILON); RoutingRequest walkOptions = options.clone(); walkOptions.rctx.pathParsers = new PathParser[0]; TraverseModeSet modes = options.getModes().clone(); modes.setTransit(false); walkOptions.setModes(modes); RaptorSearch search = new RaptorSearch(data, options); for (int i = 0; i < options.getMaxTransfers() + 2; ++i) { if (!round(data, options, walkOptions, search, i)) break; } RaptorStateSet result = new RaptorStateSet(); result.statesByStop = search.statesByStop; return result; } public double getShortPathCutoff() { return shortPathCutoff; } public void setShortPathCutoff(double shortPathCutoff) { this.shortPathCutoff = shortPathCutoff; } }