@Override
  public State traverse(State s0) {
    RoutingRequest options = s0.getOptions();

    // Ignore this edge if its stop is banned
    if (!options.bannedStops.isEmpty()) {
      if (options.bannedStops.matches(((TransitStop) fromv).getStop())) {
        return null;
      }
    }
    if (!options.bannedStopsHard.isEmpty()) {
      if (options.bannedStopsHard.matches(((TransitStop) fromv).getStop())) {
        return null;
      }
    }

    if (options.arriveBy) {
      /* Traverse backward: not much to do */
      StateEditor s1 = s0.edit(this);
      TransitStop fromVertex = (TransitStop) getFromVertex();

      // apply board slack
      s1.incrementTimeInSeconds(options.boardSlack);
      s1.alightTransit();
      s1.setBackMode(getMode());
      return s1.makeState();
    } else {
      /* Traverse forward: apply stop(pair)-specific costs */

      // Do not pre-board if transit modes are not selected.
      // Return null here rather than in StreetTransitLink so that walk-only
      // options can be used to find transit stops without boarding vehicles.
      if (!options.modes.isTransit()) return null;

      // If we've hit our transfer limit, don't go any further
      if (s0.getNumBoardings() > options.maxTransfers) return null;

      /* apply transfer rules */
      /*
       * look in the global transfer table for the rules from the previous stop to this stop.
       */
      long t0 = s0.getTimeSeconds();

      long slack;
      if (s0.isEverBoarded()) {
        slack = options.transferSlack - options.alightSlack;
      } else {
        slack = options.boardSlack;
      }
      long board_after = t0 + slack;
      long transfer_penalty = 0;

      // penalize transfers more heavily if requested by the user
      if (s0.isEverBoarded()) {
        // this is not the first boarding, therefore we must have "transferred" -- whether
        // via a formal transfer or by walking.
        transfer_penalty += options.transferPenalty;
      }

      StateEditor s1 = s0.edit(this);
      s1.setTimeSeconds(board_after);
      long wait_cost = board_after - t0;
      s1.incrementWeight(wait_cost + transfer_penalty);
      s1.setBackMode(getMode());
      return s1.makeState();
    }
  }
  /**
   * @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;
  }