@Override
  public List<GraphPath> plan(
      String fromPlace,
      String toPlace,
      List<String> intermediates,
      Date targetTime,
      TraverseOptions options) {

    if (options.getModes().contains(TraverseMode.TRANSIT)) {
      throw new UnsupportedOperationException("TSP is not supported for transit trips");
    }

    ArrayList<String> notFound = new ArrayList<String>();
    Vertex fromVertex = getVertexForPlace(fromPlace, options);
    if (fromVertex == null) {
      notFound.add("from");
    }
    Vertex toVertex = getVertexForPlace(toPlace, options);
    if (toVertex == null) {
      notFound.add("to");
    }
    ArrayList<Vertex> intermediateVertices = new ArrayList<Vertex>();

    int i = 0;
    for (String intermediate : intermediates) {
      Vertex vertex = getVertexForPlace(intermediate, options);
      if (vertex == null) {
        notFound.add("intermediate." + i);
      } else {
        intermediateVertices.add(vertex);
      }
      i += 1;
    }

    if (notFound.size() > 0) {
      throw new VertexNotFoundException(notFound);
    }

    if (_graphService.getCalendarService() != null)
      options.setCalendarService(_graphService.getCalendarService());

    options.setTransferTable(_graphService.getGraph().getTransferTable());
    GraphPath path =
        _routingService.route(
            fromVertex,
            toVertex,
            intermediateVertices,
            (int) (targetTime.getTime() / 1000),
            options);

    return Arrays.asList(path);
  }
  @Override
  public List<GraphPath> plan(State origin, Vertex target, int nItineraries) {

    Date targetTime = new Date(origin.getTime() * 1000);
    TraverseOptions options = origin.getOptions();

    if (_graphService.getCalendarService() != null)
      options.setCalendarService(_graphService.getCalendarService());
    options.setTransferTable(_graphService.getGraph().getTransferTable());
    options.setServiceDays(targetTime.getTime() / 1000);
    if (options.getModes().getTransit()
        && !_graphService.getGraph().transitFeedCovers(targetTime)) {
      // user wants a path through the transit network,
      // but the date provided is outside those covered by the transit feed.
      throw new TransitTimesException();
    }
    // decide which A* heuristic to use
    options.remainingWeightHeuristic =
        _remainingWeightHeuristicFactory.getInstanceForSearch(options, target);
    LOG.debug("Applied A* heuristic: {}", options.remainingWeightHeuristic);

    // If transit is not to be used, disable walk limit and only search for one itinerary.
    if (!options.getModes().getTransit()) {
      nItineraries = 1;
      options.setMaxWalkDistance(Double.MAX_VALUE);
    }

    ArrayList<GraphPath> paths = new ArrayList<GraphPath>();

    // The list of options specifying various modes, banned routes, etc to try for multiple
    // itineraries
    Queue<TraverseOptions> optionQueue = new LinkedList<TraverseOptions>();
    optionQueue.add(options);

    /* if the user wants to travel by transit, create a bus-only set of options */
    if (options.getModes().getTrainish() && options.getModes().contains(TraverseMode.BUS)) {
      TraverseOptions busOnly = options.clone();
      busOnly.setModes(options.getModes().clone());
      busOnly.getModes().setTrainish(false);
      // Moved inside block to avoid double insertion in list ? (AMB)
      // optionQueue.add(busOnly);
    }

    double maxWeight = Double.MAX_VALUE;
    long maxTime = options.isArriveBy() ? 0 : Long.MAX_VALUE;
    while (paths.size() < nItineraries) {
      options = optionQueue.poll();
      if (options == null) {
        break;
      }
      StateEditor editor = new StateEditor(origin, null);
      editor.setTraverseOptions(options);
      origin = editor.makeState();

      // options.worstTime = maxTime;
      // options.maxWeight = maxWeight;
      long searchBeginTime = System.currentTimeMillis();
      LOG.debug("BEGIN SEARCH");
      List<GraphPath> somePaths = _routingService.route(origin, target);
      LOG.debug("END SEARCH {} msec", System.currentTimeMillis() - searchBeginTime);
      if (maxWeight == Double.MAX_VALUE) {
        /*
         * the worst trip we are willing to accept is at most twice as bad or twice as long.
         */
        if (somePaths.isEmpty()) {
          // if there is no first path, there won't be any other paths
          return null;
        }
        GraphPath path = somePaths.get(0);
        long duration = path.getDuration();
        LOG.debug("Setting max time and weight for subsequent searches.");
        LOG.debug("First path start time:  {}", path.getStartTime());
        maxTime =
            path.getStartTime() + MAX_TIME_FACTOR * (options.isArriveBy() ? -duration : duration);
        LOG.debug("First path duration:  {}", duration);
        LOG.debug("Max time set to:  {}", maxTime);
        maxWeight = path.getWeight() * MAX_WEIGHT_FACTOR;
        LOG.debug("Max weight set to:  {}", maxWeight);
      }
      if (somePaths.isEmpty()) {
        LOG.debug("NO PATHS FOUND");
        continue;
      }
      for (GraphPath path : somePaths) {
        if (!paths.contains(path)) {
          // DEBUG
          // path.dump();
          paths.add(path);
          // now, create a list of options, one with each trip in this journey banned.

          LOG.debug("New trips: {}", path.getTrips());
          TraverseOptions newOptions = options.clone();
          for (AgencyAndId trip : path.getTrips()) {
            newOptions.bannedTrips.add(trip);
          }

          if (!optionQueue.contains(newOptions)) {
            optionQueue.add(newOptions);
          }
          /*
           * // now, create a list of options, one with each route in this trip banned. //
           * the HashSet banned is not strictly necessary as the optionsQueue will //
           * already remove duplicate options, but it might be slightly faster as //
           * hashing TraverseOptions is slow. LOG.debug("New routespecs: {}",
           * path.getRouteSpecs()); for (RouteSpec spec : path.getRouteSpecs()) {
           * TraverseOptions newOptions = options.clone();
           * newOptions.bannedRoutes.add(spec); if (!optionQueue.contains(newOptions)) {
           * optionQueue.add(newOptions); } }
           */
        }
      }
      LOG.debug("{} / {} itineraries", paths.size(), nItineraries);
    }
    if (paths.size() == 0) {
      return null;
    }
    // We order the list of returned paths by the time of arrival or departure (not path duration)
    Collections.sort(paths, new PathComparator(origin.getOptions().isArriveBy()));
    return paths;
  }
  @Override
  public List<GraphPath> plan(State origin, Vertex target, int nItineraries) {

    TraverseOptions options = origin.getOptions();

    if (_graphService.getCalendarService() != null)
      options.setCalendarService(_graphService.getCalendarService());
    options.setTransferTable(_graphService.getGraph().getTransferTable());

    options.setServiceDays(origin.getTime(), _graphService.getGraph().getAgencyIds());
    if (options.getModes().getTransit()
        && !_graphService.getGraph().transitFeedCovers(new Date(origin.getTime() * 1000))) {
      // user wants a path through the transit network,
      // but the date provided is outside those covered by the transit feed.
      throw new TransitTimesException();
    }

    // always use the bidirectional heuristic because the others are not precise enough
    RemainingWeightHeuristic heuristic =
        new BidirectionalRemainingWeightHeuristic(_graphService.getGraph());

    // the states that will eventually be turned into paths and returned
    List<State> returnStates = new LinkedList<State>();

    // Populate any extra edges
    final ExtraEdgesStrategy extraEdgesStrategy = options.extraEdgesStrategy;
    OverlayGraph extraEdges = new OverlayGraph();
    extraEdgesStrategy.addEdgesFor(extraEdges, origin.getVertex());
    extraEdgesStrategy.addEdgesFor(extraEdges, target);

    BinHeap<State> pq = new BinHeap<State>();
    //        List<State> boundingStates = new ArrayList<State>();

    // initialize heuristic outside loop so table can be reused
    heuristic.computeInitialWeight(origin, target);

    // increase maxWalk repeatedly in case hard limiting is in use
    WALK:
    for (double maxWalk = options.getMaxWalkDistance();
        maxWalk < 100000 && returnStates.isEmpty();
        maxWalk *= 2) {
      LOG.debug("try search with max walk {}", maxWalk);
      // increase maxWalk if settings make trip impossible
      if (maxWalk
          < Math.min(
              origin.getVertex().distance(target),
              origin.getVertex().getDistanceToNearestTransitStop()
                  + target.getDistanceToNearestTransitStop())) continue WALK;
      options.setMaxWalkDistance(maxWalk);
      // reinitialize states for each retry
      HashMap<Vertex, List<State>> states = new HashMap<Vertex, List<State>>();
      pq.reset();
      pq.insert(origin, 0);
      long startTime = System.currentTimeMillis();
      long endTime = startTime + (int) (_timeouts[0] * 1000);
      LOG.debug("starttime {} endtime {}", startTime, endTime);
      QUEUE:
      while (!pq.empty()) {

        if (System.currentTimeMillis() > endTime) {
          LOG.debug("timeout at {} msec", System.currentTimeMillis() - startTime);
          if (returnStates.isEmpty()) continue WALK;
          else {
            storeMemory();
            break WALK;
          }
        }

        State su = pq.extract_min();

        //                for (State bs : boundingStates) {
        //                    if (eDominates(bs, su)) {
        //                        continue QUEUE;
        //                    }
        //                }

        Vertex u = su.getVertex();

        if (traverseVisitor != null) {
          traverseVisitor.visitVertex(su);
        }

        if (u.equals(target)) {
          //                    boundingStates.add(su);
          returnStates.add(su);
          if (!options.getModes().getTransit()) break QUEUE;
          // options should contain max itineraries
          if (returnStates.size() >= _maxPaths) break QUEUE;
          if (returnStates.size() < _timeouts.length) {
            endTime = startTime + (int) (_timeouts[returnStates.size()] * 1000);
            LOG.debug(
                "{} path, set timeout to {}",
                returnStates.size(),
                _timeouts[returnStates.size()] * 1000);
          }
          continue QUEUE;
        }

        for (Edge e : u.getEdges(extraEdges, null, options.isArriveBy())) {
          STATE:
          for (State new_sv = e.traverse(su); new_sv != null; new_sv = new_sv.getNextResult()) {
            if (traverseVisitor != null) {
              traverseVisitor.visitEdge(e, new_sv);
            }

            double h = heuristic.computeForwardWeight(new_sv, target);
            //                    for (State bs : boundingStates) {
            //                        if (eDominates(bs, new_sv)) {
            //                            continue STATE;
            //                        }
            //                    }
            Vertex v = new_sv.getVertex();
            List<State> old_states = states.get(v);
            if (old_states == null) {
              old_states = new LinkedList<State>();
              states.put(v, old_states);
            } else {
              for (State old_sv : old_states) {
                if (eDominates(old_sv, new_sv)) {
                  continue STATE;
                }
              }
              Iterator<State> iter = old_states.iterator();
              while (iter.hasNext()) {
                State old_sv = iter.next();
                if (eDominates(new_sv, old_sv)) {
                  iter.remove();
                }
              }
            }
            if (traverseVisitor != null) traverseVisitor.visitEnqueue(new_sv);

            old_states.add(new_sv);
            pq.insert(new_sv, new_sv.getWeight() + h);
          }
        }
      }
    }
    storeMemory();

    // Make the states into paths and return them
    List<GraphPath> paths = new LinkedList<GraphPath>();
    for (State s : returnStates) {
      LOG.debug(s.toStringVerbose());
      paths.add(new GraphPath(s, true));
    }
    // sort by arrival time, though paths are already in order of increasing difficulty
    // Collections.sort(paths, new PathComparator(origin.getOptions().isArriveBy()));
    return paths;
  }