/** Two paths are equal if they use the same ordered list of trips */
 public boolean equals(Object o) {
   if (o instanceof GraphPath) {
     GraphPath go = (GraphPath) o;
     return go.getTrips().equals(getTrips());
   }
   return false;
 }
  @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;
  }