@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.bannedTrips.add(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;
  }
  /**
   * @param options
   * @param walkOptions
   * @param nBoardings
   * @param createdStates
   * @return whether search should continue
   */
  public boolean walkPhase(
      RoutingRequest options,
      RoutingRequest walkOptions,
      int nBoardings,
      List<RaptorState> createdStates) {

    double distanceToNearestTransitStop = 0;
    if (options.rctx.target != null) {
      distanceToNearestTransitStop = options.rctx.target.getDistanceToNearestTransitStop();
    }

    final int boardSlack =
        nBoardings == 1
            ? options.getBoardSlack()
            : (options.getTransferSlack() - options.getAlightSlack());
    ShortestPathTree spt;
    GenericDijkstra dijkstra = new GenericDijkstra(walkOptions);
    dijkstra.setShortestPathTreeFactory(bounder);
    List<State> transitStopStates = new ArrayList<State>();

    if (nBoardings == 0) {
      // TODO: retry min-time bounding with this and with maxtime

      if (options.rctx.target != null
          && bounder.getTargetDistance(options.rctx.origin) < options.getMaxWalkDistance())
        dijkstra.setHeuristic(bounder);

      MaxWalkState start = new MaxWalkState(options.rctx.origin, walkOptions);
      spt = dijkstra.getShortestPathTree(start);
      for (State state : spt.getAllStates()) {
        if (state.getVertex() instanceof TransitStop
            || state.getVertex() instanceof TransitStopArrive
            || state.getVertex() instanceof TransitStopDepart) transitStopStates.add(state);
      }
      // also, compute an initial spt from the target so that we can find out what transit
      // stops are nearby and what
      // the time is to them, so that we can start target bounding earlier
      if (maxTimeDayIndex > 0) {
        RoutingRequest reversedWalkOptions = walkOptions.clone();
        reversedWalkOptions.setArriveBy(!walkOptions.isArriveBy());
        GenericDijkstra destDijkstra = new GenericDijkstra(reversedWalkOptions);
        start = new MaxWalkState(options.rctx.target, reversedWalkOptions);
        ShortestPathTree targetSpt = destDijkstra.getShortestPathTree(start);
        for (State state : targetSpt.getAllStates()) {

          final Vertex vertex = state.getVertex();

          if (!(vertex instanceof TransitStop)) continue;
          RaptorStop stop = data.raptorStopsForStopId.get(((TransitStop) vertex).getStopId());
          if (stop == null) {
            // we have found a stop is totally unused, so skip it
            continue;
          }

          // Skip banned stops
          if (options.getBannedStops().matches(stop.stopVertex.getStop())) {
            continue;
          }
          if (options.getBannedStopsHard().matches(stop.stopVertex.getStop())) {
            continue;
          }

          addStopNearTarget(stop, state.getWalkDistance(), (int) state.getElapsedTimeSeconds());
        }
      }
    } else {

      final List<MaxWalkState> startPoints = new ArrayList<MaxWalkState>();

      for (RaptorState state : createdStates) {

        // bounding states
        // this reduces the number of initial vertices
        // and the state space size

        Vertex stopVertex =
            options.isArriveBy() ? state.stop.departVertex : state.stop.arriveVertex;
        if (stopVertex == null) {
          stopVertex = state.stop.stopVertex;
        }

        if (options.rctx.target != null) {
          double minWalk = distanceToNearestTransitStop;
          double targetDistance = bounder.getTargetDistance(stopVertex);

          if (targetDistance + state.walkDistance > options.getMaxWalkDistance()) {
            // can't walk to destination, so we can't alight at a local vertex
            if (state.stop.stopVertex.isLocal()) continue;
          }

          if (minWalk + state.walkDistance > options.getMaxWalkDistance()) {
            continue;
          }
        }

        StateEditor dijkstraState = new MaxWalkState.MaxWalkStateEditor(walkOptions, stopVertex);
        dijkstraState.setInitialWaitTimeSeconds(state.initialWaitTime);
        dijkstraState.setStartTimeSeconds(options.dateTime);
        dijkstraState.setNumBoardings(state.nBoardings);
        dijkstraState.setWalkDistance(state.walkDistance);
        dijkstraState.setTimeSeconds(state.arrivalTime);
        dijkstraState.setExtension("raptorParent", state);
        dijkstraState.setOptions(walkOptions);
        dijkstraState.incrementWeight(state.weight);
        MaxWalkState newState = (MaxWalkState) dijkstraState.makeState();
        startPoints.add(newState);
      }
      if (startPoints.size() == 0) {
        return false;
      }
      System.out.println("walk starts: " + startPoints.size() + " / " + visitedEver.size());
      dijkstra.setPriorityQueueFactory(
          new PrefilledPriorityQueueFactory(startPoints.subList(1, startPoints.size())));

      bounder.addSptStates(startPoints.subList(1, startPoints.size()));

      bounder.prepareForSearch();

      dijkstra.setSearchTerminationStrategy(bounder);
      if (options.rctx.target != null) {
        dijkstra.setSkipTraverseResultStrategy(bounder);
        dijkstra.setHeuristic(bounder);
      }

      // Do local search
      spt = dijkstra.getShortestPathTree(startPoints.get(0));
      transitStopStates = bounder.getTransitStopsVisited();
    }

    List<? extends State> targetStates = null;
    if (walkOptions.rctx.target != null) targetStates = spt.getStates(walkOptions.rctx.target);
    if (targetStates != null) {
      TARGET:
      for (State targetState : targetStates) {
        RaptorState parent = (RaptorState) targetState.getExtension("raptorParent");
        RaptorState state;
        if (parent != null) {
          state = new RaptorState(parent);
          state.nBoardings = parent.nBoardings;
          state.rentingBike = targetState.isBikeRenting();
        } else {
          state = new RaptorState(options);
        }
        state.weight = targetState.getWeight();
        state.walkDistance = targetState.getWalkDistance();
        state.arrivalTime = (int) targetState.getTimeSeconds();
        state.walkPath = targetState;
        for (Iterator<RaptorState> it = getTargetStates().iterator(); it.hasNext(); ) {
          RaptorState oldState = it.next();
          if (oldState.eDominates(state)) {
            continue TARGET;
          } else if (state.eDominates(oldState)) {
            it.remove();
          }
        }
        addTargetState(state);
        log.debug("Found target at: " + state + " on " + state.getTrips());
      }
    }
    for (State state : bounder.removedBoundingStates) {
      removeTargetState(state);
    }

    SPTSTATE:
    for (State state : transitStopStates) {
      final Vertex vertex = state.getVertex();

      RaptorStop stop = data.raptorStopsForStopId.get(((OffboardVertex) vertex).getStopId());
      if (stop == null) {
        // we have found a stop is totally unused, so skip it
        continue;
      }

      // Skip banned stops
      if (options.getBannedStops().matches(stop.stopVertex.getStop())) {
        continue;
      }
      if (options.getBannedStopsHard().matches(stop.stopVertex.getStop())) {
        continue;
      }

      if (options.rctx.target != null) {
        double minWalk = distanceToNearestTransitStop;

        double targetDistance = bounder.getTargetDistance(vertex);
        final double remainingWalk = options.maxWalkDistance - state.getWalkDistance();

        if (maxTimeDayIndex > 0 && remainingWalk < 3218) {
          double minTime =
              (targetDistance - minWalk) / Raptor.MAX_TRANSIT_SPEED
                  + minWalk / options.getStreetSpeedUpperBound();
          if (targetDistance > remainingWalk) minTime += boardSlack;

          int maxTimeForVertex = 0;
          int region = vertex.getGroupIndex();
          final int elapsedTime = (int) state.getElapsedTimeSeconds();
          for (StopNearTarget stopNearTarget : stopsNearTarget.values()) {
            int destinationRegion = stopNearTarget.stop.stopVertex.getGroupIndex();
            final int maxTimeFromThisRegion =
                data.maxTransitRegions.maxTransit[maxTimeDayIndex][destinationRegion][region];
            int maxTime = elapsedTime + maxTimeFromThisRegion + stopNearTarget.time;

            if (maxTime > maxTimeForVertex) {
              maxTimeForVertex = maxTime;
            }
          }
          if (maxTimeForVertex < maxTime) {
            maxTime = maxTimeForVertex;
          } else {
            if (elapsedTime + minTime > maxTime * 1.5) {
              continue;
            }
          }
        }
      }
      List<RaptorState> states = statesByStop[stop.index];
      if (states == null) {
        states = new ArrayList<RaptorState>();
        statesByStop[stop.index] = states;
      }

      RaptorState parent = (RaptorState) state.getExtension("raptorParent");
      RaptorState newState;
      if (parent != null) {
        newState = new RaptorState(parent);
      } else {
        // this only happens in round 0
        newState = new RaptorState(options);
      }
      newState.weight = state.getWeight();
      newState.nBoardings = nBoardings;
      newState.walkDistance = state.getWalkDistance();
      newState.arrivalTime = (int) state.getTimeSeconds();
      newState.walkPath = state;
      newState.stop = stop;
      newState.rentingBike = state.isBikeRenting();

      for (RaptorState oldState : states) {
        if (oldState.eDominates(newState)) {
          continue SPTSTATE;
        }
      }

      visitedLastRound.add(stop);
      visitedEver.add(stop);
      states.add(newState);
    }
    return true;
  }