/** 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;
 }
  public void testPickupDropoff() throws Exception {
    Vertex stop_o = graph.getVertex("agency_O_depart");
    Vertex stop_p = graph.getVertex("agency_P");
    int i = 0;
    for (@SuppressWarnings("unused") Edge e : stop_o.getOutgoing()) {
      ++i;
    }
    assertTrue(i == 3);

    long startTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 19, 12, 0, 0);
    RoutingRequest options = new RoutingRequest();
    options.dateTime = startTime;
    options.setRoutingContext(graph, stop_o, stop_p);
    ShortestPathTree spt = aStar.getShortestPathTree(options);
    GraphPath path = spt.getPath(stop_p, false);
    assertNotNull(path);
    long endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 19, 12, 10, 0);
    assertEquals(endTime, path.getEndTime());

    startTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 19, 12, 0, 1);
    options.dateTime = startTime;
    options.setRoutingContext(graph, stop_o, stop_p);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_p, false);
    assertNotNull(path);
    endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 19, 15, 10, 0);
    assertEquals(endTime, path.getEndTime());
  }
  public void testTransfers() throws Exception {
    TransferTable transferTable = graph.getTransferTable();

    // create dummy routes and trips
    Route fromRoute = new Route();
    fromRoute.setId(new AgencyAndId("agency", "1"));
    Trip fromTrip = new Trip();
    fromTrip.setId(new AgencyAndId("agency", "1.1"));
    fromTrip.setRoute(fromRoute);
    Route toRoute = new Route();
    toRoute.setId(new AgencyAndId("agency", "2"));
    Trip toTrip = new Trip();
    toTrip.setId(new AgencyAndId("agency", "2.1"));
    toTrip.setRoute(toRoute);
    Trip toTrip2 = new Trip();
    toTrip2.setId(new AgencyAndId("agency", "2.2"));
    toTrip2.setRoute(toRoute);

    // find stops
    Stop stopK = ((TransitStopArrive) graph.getVertex("agency_K_arrive")).getStop();
    Stop stopN = ((TransitStopDepart) graph.getVertex("agency_N_depart")).getStop();
    Stop stopM = ((TransitStopDepart) graph.getVertex("agency_M_depart")).getStop();

    assertTrue(transferTable.hasPreferredTransfers());
    assertEquals(
        StopTransfer.UNKNOWN_TRANSFER,
        transferTable.getTransferTime(stopN, stopM, fromTrip, toTrip));
    assertEquals(
        StopTransfer.FORBIDDEN_TRANSFER,
        transferTable.getTransferTime(stopK, stopM, fromTrip, toTrip));
    assertEquals(
        StopTransfer.PREFERRED_TRANSFER,
        transferTable.getTransferTime(stopN, stopK, toTrip, toTrip2));
    assertEquals(
        StopTransfer.TIMED_TRANSFER, transferTable.getTransferTime(stopN, stopK, fromTrip, toTrip));
    assertEquals(15, transferTable.getTransferTime(stopN, stopK, fromTrip, toTrip2));

    Vertex e_arrive = graph.getVertex("agency_E_arrive");
    Vertex f_depart = graph.getVertex("agency_F_depart");
    Edge edge = new TransferEdge(e_arrive, f_depart, 10000, 10000);

    long startTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 18, 0, 50, 0);
    Vertex stop_b = graph.getVertex("agency_B_depart");
    Vertex stop_g = graph.getVertex("agency_G_arrive");
    RoutingRequest options = new RoutingRequest();
    options.dateTime = startTime;
    options.setRoutingContext(graph, stop_b, stop_g);
    ShortestPathTree spt = aStar.getShortestPathTree(options);

    GraphPath path = spt.getPath(stop_g, false);
    assertNotNull(path);

    assertTrue(
        "expected to use much later trip due to min transfer time",
        path.getEndTime() - startTime > 4.5 * 60 * 60);

    /* cleanup */
    e_arrive.removeOutgoing(edge);
    f_depart.removeIncoming(edge);
  }
  public void testFewestTransfers() {
    Vertex stop_c = graph.getVertex("agency:C");
    Vertex stop_d = graph.getVertex("agency:D");
    RoutingRequest options = new RoutingRequest();
    options.optimize = OptimizeType.QUICK;
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 16, 0, 0);
    options.setRoutingContext(graph, stop_c, stop_d);

    ShortestPathTree spt = aStar.getShortestPathTree(options);

    // when optimizing for speed, take the fast two-bus path
    GraphPath path = spt.getPath(stop_d, false);
    assertNotNull(path);
    assertEquals(
        TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 16, 20, 0), path.getEndTime());

    // when optimizing for fewest transfers, take the slow one-bus path
    options.transferPenalty = 1800;
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_d, false);
    assertNotNull(path);
    assertEquals(
        TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 16, 50, 0), path.getEndTime());
  }
  public void testRoutingOverMidnight() throws Exception {
    // this route only runs on weekdays
    Vertex stop_g = graph.getVertex("agency:G_depart");
    Vertex stop_h = graph.getVertex("agency:H_arrive");

    ShortestPathTree spt;
    GraphPath path;
    RoutingRequest options = new RoutingRequest();

    // Friday evening
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 18, 23, 20, 0);
    options.setRoutingContext(graph, stop_g, stop_h);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_h, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());

    // Saturday morning
    long startTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 19, 0, 5, 0);
    options.dateTime = startTime;
    options.setRoutingContext(graph, stop_g.getLabel(), stop_h.getLabel());
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_h, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());
    long endTime = path.getEndTime();
    assertTrue(endTime < startTime + 60 * 60);
  }
  /** Generates a TripPlan from a set of paths */
  public TripPlan generatePlan(List<GraphPath> paths, RoutingRequest request) {

    GraphPath exemplar = paths.get(0);
    Vertex tripStartVertex = exemplar.getStartVertex();
    Vertex tripEndVertex = exemplar.getEndVertex();
    String startName = tripStartVertex.getName();
    String endName = tripEndVertex.getName();

    // Use vertex labels if they don't have names
    if (startName == null) {
      startName = tripStartVertex.getLabel();
    }
    if (endName == null) {
      endName = tripEndVertex.getLabel();
    }
    Place from = new Place(tripStartVertex.getX(), tripStartVertex.getY(), startName);
    Place to = new Place(tripEndVertex.getX(), tripEndVertex.getY(), endName);

    TripPlan plan = new TripPlan(from, to, request.getDateTime());

    for (GraphPath path : paths) {
      Itinerary itinerary = generateItinerary(path, request.getShowIntermediateStops());
      plan.addItinerary(itinerary);
    }
    return plan;
  }
  public void testFrequencies() {
    Vertex stop_u = graph.getVertex("agency_U_depart");
    Vertex stop_v = graph.getVertex("agency_V_arrive");

    ShortestPathTree spt;
    GraphPath path;

    RoutingRequest options = new RoutingRequest();
    options.setModes(new TraverseModeSet("TRANSIT"));
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 0, 0, 0);
    options.setRoutingContext(graph, stop_u, stop_v);

    // U to V - original stop times - shouldn't be used
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_v, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());
    long endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 6, 40, 0);
    assertEquals(endTime, path.getEndTime());

    // U to V - first frequency
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 7, 0, 0);
    options.setRoutingContext(graph, stop_u, stop_v);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_v, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());
    endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 7, 40, 0);
    assertEquals(endTime, path.getEndTime());

    // U to V - second frequency
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 14, 0, 0);
    options.setRoutingContext(graph, stop_u, stop_v);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_v, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());
    endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 14, 40, 0);
    assertEquals(endTime, path.getEndTime());

    boolean boarded = false;
    for (FrequencyBoard e : filter(stop_u.getOutgoing(), FrequencyBoard.class)) {
      boarded = true;
      FrequencyBoard board = (FrequencyBoard) e;
      FrequencyBasedTripPattern pattern = board.getPattern();
      int previousArrivalTime = pattern.getPreviousArrivalTime(0, 0, false, false, false);
      assertTrue(previousArrivalTime < 0);

      previousArrivalTime = pattern.getPreviousArrivalTime(0, 60 * 60 * 7 - 1, false, false, false);
      assertEquals(60 * 60 * 6, previousArrivalTime);

      previousArrivalTime = pattern.getPreviousArrivalTime(0, 60 * 60 * 11, false, false, false);
      assertEquals(60 * 60 * 10, previousArrivalTime);

      previousArrivalTime = pattern.getPreviousArrivalTime(0, 60 * 60 * 18, false, false, false);
      assertEquals(60 * 60 * 16, previousArrivalTime);
    }
    assertTrue(boarded);
  }
  public void testRouting() throws Exception {

    Vertex stop_a = graph.getVertex("agency:A");
    Vertex stop_b = graph.getVertex("agency:B");
    Vertex stop_c = graph.getVertex("agency:C");
    Vertex stop_d = graph.getVertex("agency:D");
    Vertex stop_e = graph.getVertex("agency:E");

    RoutingRequest options = new RoutingRequest();
    // test feed is designed for instantaneous transfers
    options.transferSlack = (0);

    long startTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 0, 0, 0);
    options.dateTime = startTime;

    ShortestPathTree spt;
    GraphPath path;

    // A to B
    options.setRoutingContext(graph, stop_a, stop_b);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_b, false);
    assertNotNull(path);
    assertEquals(6, path.states.size());

    // A to C
    options.setRoutingContext(graph, stop_a, stop_c);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_c, false);
    assertNotNull(path);
    assertEquals(8, path.states.size());

    // A to D (change at C)
    options.setRoutingContext(graph, stop_a, stop_d);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_d, false);
    assertNotNull(path);
    // there are two paths of different lengths
    // both arrive at 40 minutes after midnight
    List<TransitStop> stops = extractStopVertices(path);
    assertEquals(stops.size(), 3);
    assertEquals(stops.get(1), stop_c);
    long endTime = startTime + 40 * 60;
    assertEquals(endTime, path.getEndTime());

    // A to E (change at C)
    options.setRoutingContext(graph, stop_a, stop_e);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_e, false);
    assertNotNull(path);
    stops = extractStopVertices(path);
    assertEquals(stops.size(), 3);
    assertEquals(stops.get(1), stop_c);
    endTime = startTime + 70 * 60;
    assertEquals(endTime, path.getEndTime());
  }
  public void testWheelchairAccessible() throws Exception {
    Vertex near_a = graph.getVertex("near_1_agency_entrance_a");
    Vertex near_b = graph.getVertex("near_1_agency_entrance_b");
    Vertex near_c = graph.getVertex("near_1_agency_C");
    Vertex near_e = graph.getVertex("near_1_agency_E");

    Vertex stop_d = graph.getVertex("agency:D");
    Vertex split_d = null;
    for (StreetTransitLink e : Iterables.filter(stop_d.getOutgoing(), StreetTransitLink.class)) {
      split_d = e.getToVertex();
    }

    RoutingRequest options = new RoutingRequest();
    options.wheelchairAccessible = true;
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 18, 0, 0, 0);

    ShortestPathTree spt;
    GraphPath path;

    // stop B is accessible, so there should be a path.
    options.setRoutingContext(graph, near_a, near_b);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(near_b, false);
    assertNotNull(path);

    // stop C is not accessible, so there should be no path.
    options.setRoutingContext(graph, near_a, near_c);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(near_c, false);
    assertNull(path);

    // stop E has no accessibility information, but we should still be able to route to it.
    options.setRoutingContext(graph, near_a, near_e);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(near_e, false);
    assertNotNull(path);

    // from stop A to stop D would normally be trip 1.1 to trip 2.1, arriving at 00:30. But trip
    // 2 is not accessible, so we'll do 1.1 to 3.1, arriving at 01:00
    GregorianCalendar time = new GregorianCalendar(2009, 8, 18, 0, 0, 0);
    time.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    options.dateTime = TestUtils.toSeconds(time);
    options.setRoutingContext(graph, near_a, split_d);
    spt = aStar.getShortestPathTree(options);

    time.add(Calendar.HOUR, 1);
    time.add(Calendar.SECOND, 1); // for the StreetTransitLink
    path = spt.getPath(split_d, false);
    assertNotNull(path);
    assertEquals(TestUtils.toSeconds(time), path.getEndTime());
  }
  public void testTimelessStops() throws Exception {
    Vertex stop_d = graph.getVertex("agency:D");
    Vertex stop_c = graph.getVertex("agency:C");
    RoutingRequest options = new RoutingRequest();
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 10, 0, 0);
    options.setRoutingContext(graph, stop_d, stop_c);
    ShortestPathTree spt = aStar.getShortestPathTree(options);

    GraphPath path = spt.getPath(stop_c, false);
    assertNotNull(path);
    assertEquals(
        TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 11, 0, 0), path.getEndTime());
  }
  public void testRouting() throws Exception {

    Vertex stop_a = graph.getVertex("agency_A");
    Vertex stop_b = graph.getVertex("agency_B");
    Vertex stop_c = graph.getVertex("agency_C");
    Vertex stop_d = graph.getVertex("agency_D");
    Vertex stop_e = graph.getVertex("agency_E");

    RoutingRequest options = new RoutingRequest();
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 0, 0, 0);

    ShortestPathTree spt;
    GraphPath path;

    // A to B
    options.setRoutingContext(graph, stop_a, stop_b);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_b, false);
    assertNotNull(path);
    assertEquals(extractStopVertices(path), Lists.newArrayList(stop_a, stop_b));

    // A to C
    options.setRoutingContext(graph, stop_a, stop_c);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_c, false);
    assertNotNull(path);
    assertEquals(extractStopVertices(path), Lists.newArrayList(stop_a, stop_c));

    // A to D
    options.setRoutingContext(graph, stop_a, stop_d);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_d, false);
    assertNotNull(path);
    assertEquals(extractStopVertices(path), Lists.newArrayList(stop_a, stop_c, stop_d));
    long endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 0, 0, 0) + 40 * 60;
    assertEquals(endTime, path.getEndTime());

    // A to E
    options.setRoutingContext(graph, stop_a, stop_e);
    spt = aStar.getShortestPathTree(options);

    path = spt.getPath(stop_e, false);
    assertNotNull(path);
    assertEquals(extractStopVertices(path), Lists.newArrayList(stop_a, stop_c, stop_e));
    endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 0, 0, 0) + 70 * 60;
    assertEquals(endTime, path.getEndTime());
  }
  public void testDwell() throws Exception {
    Vertex stop_a = graph.getVertex("agency_A_depart");
    Vertex stop_c = graph.getVertex("agency_C_arrive");

    RoutingRequest options = new RoutingRequest();
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 8, 0, 0);
    options.setRoutingContext(graph, stop_a, stop_c);
    ShortestPathTree spt = aStar.getShortestPathTree(options);

    GraphPath path = spt.getPath(stop_c, false);
    assertNotNull(path);
    assertEquals(6, path.states.size());
    long endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 8, 30, 0);
    assertEquals(endTime, path.getEndTime());
  }
  public void testPathways() throws Exception {

    Vertex entrance = graph.getVertex("agency:entrance_a");
    assertNotNull(entrance);
    Vertex stop = graph.getVertex("agency:A");
    assertNotNull(stop);

    RoutingRequest options = new RoutingRequest();
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 16, 0, 0);
    options.setRoutingContext(graph, entrance, stop);
    ShortestPathTree spt = aStar.getShortestPathTree(options);

    GraphPath path = spt.getPath(stop, false);
    assertNotNull(path);
    assertEquals(
        TestUtils.dateInSeconds("America/New_York", 2009, 8, 1, 16, 0, 34), path.getEndTime());
  }
  public void testFrequencies() {
    Vertex stop_u = graph.getVertex("agency:U_depart");
    Vertex stop_v = graph.getVertex("agency:V_arrive");

    ShortestPathTree spt;
    GraphPath path;

    RoutingRequest options = new RoutingRequest();
    options.setModes(new TraverseModeSet("TRANSIT"));
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 0, 0, 0);
    options.setRoutingContext(graph, stop_u, stop_v);

    // U to V - original stop times - shouldn't be used
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_v, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());
    long endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 6, 40, 0);
    assertEquals(endTime, path.getEndTime());

    // U to V - first frequency
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 7, 0, 0);
    options.setRoutingContext(graph, stop_u, stop_v);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_v, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());
    endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 7, 40, 0);
    assertEquals(endTime, path.getEndTime());

    // U to V - second frequency
    options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 14, 0, 0);
    options.setRoutingContext(graph, stop_u, stop_v);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_v, false);
    assertNotNull(path);
    assertEquals(4, path.states.size());
    endTime = TestUtils.dateInSeconds("America/New_York", 2009, 8, 7, 14, 40, 0);
    assertEquals(endTime, path.getEndTime());

    // TODO more detailed testing of frequencies

  }
  public void testRunForTrain() {
    /**
     * This is the notorious Byrd bug: we're going from Q to T at 8:30. There's a trip from S to T
     * at 8:50 and a second one at 9:50. To get to S by 8:50, we need to take trip 12.1 from Q to R,
     * and 13.1 from R to S. If we take the direct-but-slower 11.1, we'll miss the 8:50 and have to
     * catch the 9:50.
     */
    Vertex destination = graph.getVertex("agency:T");
    RoutingRequest options = new RoutingRequest();
    // test is designed such that transfers must be instantaneous
    options.transferSlack = (0);
    GregorianCalendar startTime = new GregorianCalendar(2009, 11, 2, 8, 30, 0);
    startTime.setTimeZone(TimeZone.getTimeZone("America/New_York"));
    options.dateTime = TestUtils.toSeconds(startTime);
    options.setRoutingContext(graph, "agency:Q", destination.getLabel());
    ShortestPathTree spt = aStar.getShortestPathTree(options);
    GraphPath path = spt.getPath(destination, false);

    long endTime = path.getEndTime();
    Calendar c = new GregorianCalendar();
    c.setTimeInMillis(endTime * 1000L);
    assertTrue(endTime - TestUtils.toSeconds(startTime) < 7200);
  }
  /**
   * Makes a new empty Itinerary for a given path.
   *
   * @return
   */
  private Itinerary makeEmptyItinerary(GraphPath path) {
    Itinerary itinerary = new Itinerary();

    State startState = path.states.getFirst();
    State endState = path.states.getLast();

    itinerary.startTime = makeCalendar(startState);
    itinerary.endTime = makeCalendar(endState);
    itinerary.duration = endState.getTimeInMillis() - startState.getTimeInMillis();

    Graph graph = path.getRoutingContext().graph;
    FareService fareService = graph.getService(FareService.class);
    if (fareService != null) {
      itinerary.fare = fareService.getCost(path);
    }
    itinerary.transfers = -1;
    return itinerary;
  }
  public void testHalfEdges() {
    // the shortest half-edge from the start vertex takes you down, but the shortest total path
    // is up and over

    TraverseOptions options = new TraverseOptions();

    HashSet<Edge> turns = new HashSet<Edge>(graph.getOutgoing(left));
    turns.addAll(graph.getOutgoing(leftBack));

    StreetLocation start =
        StreetLocation.createStreetLocation(
            "start",
            "start",
            cast(turns, StreetEdge.class),
            new LinearLocation(0, 0.4).getCoordinate(left.getGeometry()));

    HashSet<Edge> endTurns = new HashSet<Edge>(graph.getOutgoing(right));
    endTurns.addAll(graph.getOutgoing(rightBack));

    StreetLocation end =
        StreetLocation.createStreetLocation(
            "end",
            "end",
            cast(endTurns, StreetEdge.class),
            new LinearLocation(0, 0.8).getCoordinate(right.getGeometry()));

    assertTrue(start.getX() < end.getX());
    assertTrue(start.getY() < end.getY());

    List<DirectEdge> extra = end.getExtra();

    assertEquals(12, extra.size());

    GregorianCalendar startTime = new GregorianCalendar(2009, 11, 1, 12, 34, 25);

    ShortestPathTree spt1 =
        AStar.getShortestPathTree(graph, brOut, end, TestUtils.toSeconds(startTime), options);

    GraphPath pathBr = spt1.getPath(end, false);
    assertNotNull("There must be a path from br to end", pathBr);

    ShortestPathTree spt2 =
        AStar.getShortestPathTree(graph, trOut, end, TestUtils.toSeconds(startTime), options);

    GraphPath pathTr = spt2.getPath(end, false);
    assertNotNull("There must be a path from tr to end", pathTr);
    assertTrue(
        "path from bottom to end must be longer than path from top to end",
        pathBr.getWeight() > pathTr.getWeight());

    ShortestPathTree spt =
        AStar.getShortestPathTree(graph, start, end, TestUtils.toSeconds(startTime), options);

    GraphPath path = spt.getPath(end, false);
    assertNotNull("There must be a path from start to end", path);

    // the bottom is not part of the shortest path
    for (State s : path.states) {
      assertNotSame(s.getVertex(), graph.getVertex("bottom"));
      assertNotSame(s.getVertex(), graph.getVertex("bottomBack"));
    }

    startTime = new GregorianCalendar(2009, 11, 1, 12, 34, 25);

    options.setArriveBy(true);
    spt = AStar.getShortestPathTree(graph, start, end, TestUtils.toSeconds(startTime), options);

    path = spt.getPath(start, false);
    assertNotNull("There must be a path from start to end (looking back)", path);

    // the bottom edge is not part of the shortest path
    for (State s : path.states) {
      assertNotSame(s.getVertex(), graph.getVertex("bottom"));
      assertNotSame(s.getVertex(), graph.getVertex("bottomBack"));
    }

    /* Now, the right edge is not bikeable.  But the user can walk their bike.  So here are some tests
     * that prove (a) that walking bikes works, but that (b) it is not preferred to riding a tiny bit longer.
     */

    options = new TraverseOptions(new TraverseModeSet(TraverseMode.BICYCLE));
    start =
        StreetLocation.createStreetLocation(
            "start1",
            "start1",
            cast(turns, StreetEdge.class),
            new LinearLocation(0, 0.95).getCoordinate(top.getGeometry()));
    end =
        StreetLocation.createStreetLocation(
            "end1",
            "end1",
            cast(turns, StreetEdge.class),
            new LinearLocation(0, 0.95).getCoordinate(bottom.getGeometry()));
    spt = AStar.getShortestPathTree(graph, start, end, TestUtils.toSeconds(startTime), options);

    path = spt.getPath(start, false);
    assertNotNull("There must be a path from top to bottom along the right", path);

    // the left edge is not part of the shortest path (even though the bike must be walked along the
    // right)
    for (State s : path.states) {
      assertNotSame(s.getVertex(), graph.getVertex("left"));
      assertNotSame(s.getVertex(), graph.getVertex("leftBack"));
    }

    start =
        StreetLocation.createStreetLocation(
            "start2",
            "start2",
            cast(turns, StreetEdge.class),
            new LinearLocation(0, 0.55).getCoordinate(top.getGeometry()));
    end =
        StreetLocation.createStreetLocation(
            "end2",
            "end2",
            cast(turns, StreetEdge.class),
            new LinearLocation(0, 0.55).getCoordinate(bottom.getGeometry()));
    spt = AStar.getShortestPathTree(graph, start, end, TestUtils.toSeconds(startTime), options);

    path = spt.getPath(start, false);
    assertNotNull("There must be a path from top to bottom", path);

    // the right edge is not part of the shortest path, e
    for (State s : path.states) {
      assertNotSame(s.getVertex(), graph.getVertex("right"));
      assertNotSame(s.getVertex(), graph.getVertex("rightBack"));
    }
  }
  /**
   * Generate an itinerary from a @{link GraphPath}. The algorithm here is to walk over each state
   * in the graph path, accumulating geometry, time, and length data from the incoming edge. When
   * the incoming edge and outgoing edge have different modes (or when a vehicle changes names due
   * to interlining) a new leg is generated. Street legs undergo an additional processing step to
   * generate turn-by-turn directions.
   *
   * @param path
   * @param showIntermediateStops whether intermediate stops are included in the generated itinerary
   * @return itinerary
   */
  private Itinerary generateItinerary(GraphPath path, boolean showIntermediateStops) {
    Graph graph = path.getRoutingContext().graph;
    TransitIndexService transitIndex = graph.getService(TransitIndexService.class);

    Itinerary itinerary = makeEmptyItinerary(path);
    EdgeNarrative postponedAlerts = null;
    Leg leg = null;
    CoordinateArrayListSequence coordinates = new CoordinateArrayListSequence();
    double previousElevation = Double.MAX_VALUE;
    int startWalk = -1;
    int i = -1;
    PlanGenState pgstate = PlanGenState.START;
    String nextName = null;
    for (State state : path.states) {
      i += 1;
      Edge backEdge = state.getBackEdge();
      EdgeNarrative backEdgeNarrative = state.getBackEdgeNarrative();
      if (backEdge == null) {
        continue;
      }

      TraverseMode mode = backEdgeNarrative.getMode();
      if (mode != null) {
        long dt = state.getAbsTimeDeltaSec();
        if (mode == TraverseMode.BOARDING
            || mode == TraverseMode.ALIGHTING
            || mode == TraverseMode.STL) {
          itinerary.waitingTime += dt;
        } else if (mode.isOnStreetNonTransit()) {
          itinerary.walkDistance += backEdgeNarrative.getDistance();
          itinerary.walkTime += dt;
        } else if (mode.isTransit()) {
          itinerary.transitTime += dt;
        }
      }

      if (backEdge instanceof FreeEdge) {
        if (backEdge instanceof PreBoardEdge) {
          // Add boarding alerts to the next leg
          postponedAlerts = backEdgeNarrative;
        } else if (backEdge instanceof PreAlightEdge) {
          // Add alighting alerts to the previous leg
          addNotesToLeg(itinerary.legs.get(itinerary.legs.size() - 1), backEdgeNarrative);
        }
        continue;
      }

      if (backEdge instanceof EdgeWithElevation) {
        PackedCoordinateSequence profile = ((EdgeWithElevation) backEdge).getElevationProfile();
        previousElevation = applyElevation(profile, itinerary, previousElevation);
      }

      switch (pgstate) {
        case START:
          if (mode == TraverseMode.WALK) {
            pgstate = PlanGenState.WALK;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            startWalk = i;
          } else if (mode == TraverseMode.BICYCLE) {
            pgstate = PlanGenState.BICYCLE;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            startWalk = i;
          } else if (mode == TraverseMode.CAR) {
            pgstate = PlanGenState.CAR;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            startWalk = i;
          } else if (mode == TraverseMode.BOARDING) {
            // this itinerary starts with transit
            pgstate = PlanGenState.PRETRANSIT;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            startWalk = -1;
          } else if (mode == TraverseMode.STL) {
            // this comes after an alight; do nothing
          } else if (mode == TraverseMode.TRANSFER) {
            // handle the whole thing in one step
            leg = makeLeg(itinerary, state);
            coordinates = new CoordinateArrayListSequence();
            coordinates.add(state.getBackState().getVertex().getCoordinate());
            coordinates.add(state.getVertex().getCoordinate());
            finalizeLeg(leg, state, path.states, i, i, coordinates);
            coordinates.clear();
          } else {
            LOG.error("Unexpected state (in START): " + mode);
          }
          break;
        case WALK:
          if (leg == null) {
            leg = makeLeg(itinerary, state);
          }
          if (mode == TraverseMode.WALK) {
            // do nothing
          } else if (mode == TraverseMode.BICYCLE) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates);
            startWalk = i;
            leg = makeLeg(itinerary, state);
            if (backEdge instanceof RentABikeOnEdge) {
              leg.rentedBike = true;
            }
            pgstate = PlanGenState.BICYCLE;
          } else if (mode == TraverseMode.STL) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates);
            leg = null;
            pgstate = PlanGenState.PRETRANSIT;
          } else if (mode == TraverseMode.BOARDING) {
            // this only happens in case of a timed transfer.
            pgstate = PlanGenState.PRETRANSIT;
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates);
            leg = makeLeg(itinerary, state);
            itinerary.transfers++;
          } else if (backEdgeNarrative instanceof LegSwitchingEdge) {
            nextName = state.getBackState().getBackState().getBackState().getVertex().getName();
            finalizeLeg(leg, state, path.states, startWalk, i - 1, coordinates);
            leg = null;
            pgstate = PlanGenState.START;
          } else {
            LOG.error("Unexpected state (in WALK): " + mode);
          }
          break;
        case BICYCLE:
          if (leg == null) {
            leg = makeLeg(itinerary, state);
          }
          if (mode == TraverseMode.BICYCLE) {
            // do nothing
          } else if (mode == TraverseMode.WALK) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates);
            leg = makeLeg(itinerary, state);
            startWalk = i;
            pgstate = PlanGenState.WALK;
          } else if (mode == TraverseMode.STL) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates);
            leg = null;
            pgstate = PlanGenState.PRETRANSIT;
          } else if (backEdgeNarrative instanceof LegSwitchingEdge) {
            finalizeLeg(leg, state, path.states, startWalk, i - 1, coordinates);
            leg = null;
            pgstate = PlanGenState.START;
          } else {
            LOG.error("Unexpected state (in BICYCLE): " + mode);
          }
          break;
        case CAR:
          if (leg == null) {
            leg = makeLeg(itinerary, state);
          }
          if (mode == TraverseMode.CAR) {
            // do nothing
          } else if (mode == TraverseMode.STL) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates);
            leg = null;
            pgstate = PlanGenState.PRETRANSIT;
          } else if (backEdgeNarrative instanceof LegSwitchingEdge) {
            finalizeLeg(leg, state, path.states, startWalk, i - 1, coordinates);
            leg = null;
            pgstate = PlanGenState.START;
          } else {
            LOG.error("Unexpected state (in CAR): " + mode);
          }
          break;
        case PRETRANSIT:
          if (mode == TraverseMode.BOARDING) {
            if (leg != null) {
              LOG.error("leg unexpectedly not null (boarding loop)");
            } else {
              leg = makeLeg(itinerary, state);
              leg.stop = new ArrayList<Place>();
              itinerary.transfers++;
              leg.boardRule = (String) state.getExtension("boardAlightRule");
            }
          } else if (backEdge instanceof HopEdge) {
            pgstate = PlanGenState.TRANSIT;
            fixupTransitLeg(leg, state, transitIndex);
            leg.stop = new ArrayList<Place>();
          } else {
            LOG.error("Unexpected state (in PRETRANSIT): " + mode);
          }
          break;
        case TRANSIT:
          String route = backEdgeNarrative.getName();
          if (mode == TraverseMode.ALIGHTING) {
            if (showIntermediateStops && leg.stop != null && leg.stop.size() > 0) {
              if (leg.stop.isEmpty()) {
                leg.stop = null;
              }
            }
            leg.alightRule = (String) state.getExtension("boardAlightRule");
            finalizeLeg(leg, state, null, -1, -1, coordinates);
            leg = null;
            pgstate = PlanGenState.START;
          } else if (mode.toString().equals(leg.mode)) {
            // no mode change, handle intermediate stops
            if (showIntermediateStops) {
              /*
               * any further transit edge, add "from" vertex to intermediate stops
               */
              if (!(backEdge instanceof DwellEdge)) {
                Place stop = makePlace(state.getBackState(), true);
                leg.stop.add(stop);
              } else if (leg.stop.size() > 0) {
                leg.stop.get(leg.stop.size() - 1).departure = makeCalendar(state);
              }
            }
            if (!route.equals(leg.route)) {
              // interline dwell
              finalizeLeg(leg, state, null, -1, -1, coordinates);
              leg = makeLeg(itinerary, state);
              leg.stop = new ArrayList<Place>();
              fixupTransitLeg(leg, state, transitIndex);
              leg.startTime = makeCalendar(state);
              leg.interlineWithPreviousLeg = true;
            }
          } else {
            LOG.error("Unexpected state (in TRANSIT): " + mode);
          }
          break;
      }
      if (leg != null) {
        leg.distance += backEdgeNarrative.getDistance();
        Geometry edgeGeometry = backEdgeNarrative.getGeometry();
        if (edgeGeometry != null) {
          Coordinate[] edgeCoordinates = edgeGeometry.getCoordinates();
          if (coordinates.size() > 0
              && coordinates.getCoordinate(coordinates.size() - 1).equals(edgeCoordinates[0])) {
            coordinates.extend(edgeCoordinates, 1);
          } else {
            coordinates.extend(edgeCoordinates);
          }
        }

        if (postponedAlerts != null) {
          addNotesToLeg(leg, postponedAlerts);
          postponedAlerts = null;
        }

        addNotesToLeg(leg, backEdgeNarrative);
      }
    } /* end loop over graphPath edge list */

    if (leg != null) {
      finalizeLeg(leg, path.states.getLast(), path.states, startWalk, i, coordinates);
    }
    itinerary.removeBogusLegs();
    itinerary.fixupDates(graph.getService(CalendarServiceData.class));
    if (itinerary.legs.size() == 0) throw new TrivialPathException();
    return itinerary;
  }
  /**
   * Generate an itinerary from a @{link GraphPath}. The algorithm here is to walk over each state
   * in the graph path, accumulating geometry, time, and length data from the incoming edge. When
   * the incoming edge and outgoing edge have different modes (or when a vehicle changes names due
   * to interlining) a new leg is generated. Street legs undergo an additional processing step to
   * generate turn-by-turn directions.
   *
   * @param path
   * @param showIntermediateStops whether intermediate stops are included in the generated itinerary
   * @return itinerary
   */
  private Itinerary generateItinerary(GraphPath path, boolean showIntermediateStops) {
    Graph graph = path.getRoutingContext().graph;
    TransitIndexService transitIndex = graph.getService(TransitIndexService.class);

    Itinerary itinerary = makeEmptyItinerary(path);
    Set<Alert> postponedAlerts = null;
    Leg leg = null;
    CoordinateArrayListSequence coordinates = new CoordinateArrayListSequence();
    double previousElevation = Double.MAX_VALUE;
    int startWalk = -1;
    int i = -1;
    boolean foldingElevatorLegIntoCycleLeg = false;
    PlanGenState pgstate = PlanGenState.START;
    String nextName = null;
    for (State state : path.states) {
      i += 1;
      Edge backEdge = state.getBackEdge();
      if (backEdge == null) {
        continue;
      }

      // debug: push vehicle late status out to UI
      //            if (backEdge instanceof PatternHop) {
      //                TripTimes tt = state.getTripTimes();
      //                int hop = ((PatternHop)backEdge).stopIndex;
      //                LOG.info("{} {}", tt.getTrip().toString(), hop);
      //                if ( ! tt.isScheduled()) {
      //                    int delay = tt.getDepartureDelay(hop);
      //                    String d = "on time";
      //                    if (Math.abs(delay) > 10) {
      //                        d = String.format("%2.1f min %s", delay / 60.0,
      //                                (delay < 0) ? "early" : "late");
      //                    }
      //                    d = "Using real-time delay information: ".concat(d);
      //                    leg.addAlert(Alert.createSimpleAlerts(d));
      //                    LOG.info(d);
      //                }
      //                else {
      //                    leg.addAlert(Alert.createSimpleAlerts("Using published timetables."));
      //                    LOG.info("sched");
      //                }
      //            }

      TraverseMode mode = state.getBackMode();
      if (mode != null) {
        long dt = state.getAbsTimeDeltaSec();
        if (mode == TraverseMode.BOARDING
            || mode == TraverseMode.ALIGHTING
            || mode == TraverseMode.STL) {
          itinerary.waitingTime += dt;
        } else if (mode.isOnStreetNonTransit()) {
          itinerary.walkDistance += backEdge.getDistance();
          itinerary.walkTime += dt;
        } else if (mode.isTransit()) {
          itinerary.transitTime += dt;
        }
      }

      if (backEdge instanceof FreeEdge) {
        if (backEdge instanceof PreBoardEdge) {
          // Add boarding alerts to the next leg
          postponedAlerts = state.getBackAlerts();
        } else if (backEdge instanceof PreAlightEdge) {
          // Add alighting alerts to the previous leg
          addNotesToLeg(itinerary.legs.get(itinerary.legs.size() - 1), state.getBackAlerts());
        }
        continue;
      }

      if (backEdge instanceof EdgeWithElevation) {
        PackedCoordinateSequence profile = ((EdgeWithElevation) backEdge).getElevationProfile();
        previousElevation = applyElevation(profile, itinerary, previousElevation);
      }

      switch (pgstate) {
        case START:
          if (mode == TraverseMode.WALK) {
            pgstate = PlanGenState.WALK;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            startWalk = i;
          } else if (mode == TraverseMode.BICYCLE) {
            pgstate = PlanGenState.BICYCLE;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            startWalk = i;
          } else if (mode == TraverseMode.CAR) {
            pgstate = PlanGenState.CAR;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            startWalk = i;
          } else if (mode == TraverseMode.BOARDING) {
            // this itinerary starts with transit
            pgstate = PlanGenState.PRETRANSIT;
            leg = makeLeg(itinerary, state);
            leg.from.orig = nextName;
            itinerary.transfers++;
            startWalk = -1;
          } else if (mode == TraverseMode.STL) {
            // this comes after an alight; do nothing
          } else if (mode == TraverseMode.TRANSFER) {
            // handle the whole thing in one step
            leg = makeLeg(itinerary, state);
            coordinates = new CoordinateArrayListSequence();
            coordinates.add(state.getBackState().getVertex().getCoordinate());
            coordinates.add(state.getVertex().getCoordinate());
            finalizeLeg(leg, state, path.states, i, i, coordinates, itinerary);
            coordinates.clear();
          } else {
            LOG.error("Unexpected state (in START): " + mode);
          }
          break;
        case WALK:
          if (leg == null) {
            leg = makeLeg(itinerary, state);
          }
          if (mode == TraverseMode.WALK) {
            // do nothing
          } else if (mode == TraverseMode.BICYCLE) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates, itinerary);
            startWalk = i;
            leg = makeLeg(itinerary, state);
            pgstate = PlanGenState.BICYCLE;
          } else if (mode == TraverseMode.STL) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates, itinerary);
            leg = null;
            pgstate = PlanGenState.PRETRANSIT;
          } else if (mode == TraverseMode.BOARDING) {
            // this only happens in case of a timed transfer.
            pgstate = PlanGenState.PRETRANSIT;
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates, itinerary);
            leg = makeLeg(itinerary, state);
            itinerary.transfers++;
          } else if (backEdge instanceof LegSwitchingEdge) {
            nextName = state.getBackState().getBackState().getBackState().getVertex().getName();
            finalizeLeg(leg, state, path.states, startWalk, i - 1, coordinates, itinerary);
            leg = null;
            pgstate = PlanGenState.START;
          } else {
            LOG.error("Unexpected state (in WALK): " + mode);
          }
          break;
        case BICYCLE:
          if (leg == null) {
            leg = makeLeg(itinerary, state);
          }

          // If there are elevator edges that have mode == BICYCLE on both sides, they should
          // be folded into the bicycle leg. But ones with walk on one side or the other should
          // not
          if (state.getBackEdge() instanceof ElevatorBoardEdge) {
            int j = i + 1;
            // proceed forward from the current state until we find one that isn't on an
            // elevator, and check the traverse mode
            while (path.states.get(j).getBackEdge() instanceof ElevatorEdge) j++;

            // path.states[j] is not an elevator edge
            if (path.states.get(j).getBackMode() == TraverseMode.BICYCLE)
              foldingElevatorLegIntoCycleLeg = true;
          }

          if (foldingElevatorLegIntoCycleLeg) {
            if (state.getBackEdge() instanceof ElevatorEdge) {
              break; // from the case
            } else {
              foldingElevatorLegIntoCycleLeg = false;
              // do not break but allow it to be processed below (which will do nothing)
            }
          }

          if (mode == TraverseMode.BICYCLE) {
            // do nothing
          } else if (mode == TraverseMode.WALK) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates, itinerary);
            leg = makeLeg(itinerary, state);
            startWalk = i;
            pgstate = PlanGenState.WALK;
          } else if (mode == TraverseMode.STL) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates, itinerary);
            startWalk = i;
            leg = null;
            pgstate = PlanGenState.PRETRANSIT;
          } else if (backEdge instanceof LegSwitchingEdge) {
            finalizeLeg(leg, state, path.states, startWalk, i - 1, coordinates, itinerary);
            leg = null;
            pgstate = PlanGenState.START;
          } else {
            LOG.error("Unexpected state (in BICYCLE): " + mode);
          }
          break;
        case CAR:
          if (leg == null) {
            leg = makeLeg(itinerary, state);
          }
          if (mode == TraverseMode.CAR) {
            // do nothing
          } else if (mode == TraverseMode.STL) {
            finalizeLeg(leg, state, path.states, startWalk, i, coordinates, itinerary);
            leg = null;
            pgstate = PlanGenState.PRETRANSIT;
          } else if (backEdge instanceof LegSwitchingEdge) {
            finalizeLeg(leg, state, path.states, startWalk, i - 1, coordinates, itinerary);
            leg = null;
            pgstate = PlanGenState.START;
          } else {
            LOG.error("Unexpected state (in CAR): " + mode);
          }
          break;
        case PRETRANSIT:
          if (mode == TraverseMode.BOARDING) {
            if (leg != null) {
              LOG.error("leg unexpectedly not null (boarding loop)");
            } else {
              leg = makeLeg(itinerary, state);
              leg.from.stopIndex = ((OnBoardForwardEdge) backEdge).getStopIndex();
              leg.stop = new ArrayList<Place>();
              itinerary.transfers++;
              leg.boardRule = (String) state.getExtension("boardAlightRule");
            }
          } else if (backEdge instanceof HopEdge) {
            pgstate = PlanGenState.TRANSIT;
            fixupTransitLeg(leg, state, transitIndex);
            leg.stop = new ArrayList<Place>();
          } else {
            LOG.error("Unexpected state (in PRETRANSIT): " + mode);
          }
          break;
        case TRANSIT:
          String route = backEdge.getName();
          if (mode == TraverseMode.ALIGHTING) {
            if (showIntermediateStops && leg.stop != null && leg.stop.size() > 0) {
              if (leg.stop.isEmpty()) {
                leg.stop = null;
              }
            }
            leg.alightRule = (String) state.getExtension("boardAlightRule");
            finalizeLeg(leg, state, null, -1, -1, coordinates, itinerary);
            leg = null;
            pgstate = PlanGenState.START;
          } else if (mode.toString().equals(leg.mode)) {
            // no mode change, handle intermediate stops
            if (showIntermediateStops) {
              /*
               * any further transit edge, add "from" vertex to intermediate stops
               */
              if (!(backEdge instanceof DwellEdge)) {
                Place stop =
                    makePlace(
                        state.getBackState(), state.getBackState().getVertex().getName(), true);
                leg.stop.add(stop);
              } else if (leg.stop.size() > 0) {
                leg.stop.get(leg.stop.size() - 1).departure = makeCalendar(state);
              }
            }
            if (!route.equals(leg.route)) {
              // interline dwell
              finalizeLeg(leg, state, null, -1, -1, coordinates, itinerary);
              leg = makeLeg(itinerary, state);
              leg.stop = new ArrayList<Place>();
              fixupTransitLeg(leg, state, transitIndex);
              leg.startTime = makeCalendar(state);
              leg.interlineWithPreviousLeg = true;
            }
          } else {
            LOG.error("Unexpected state (in TRANSIT): " + mode);
          }
          break;
      }
      if (leg != null) {
        leg.distance += backEdge.getDistance();
        Geometry edgeGeometry = backEdge.getGeometry();
        if (edgeGeometry != null) {
          Coordinate[] edgeCoordinates = edgeGeometry.getCoordinates();
          if (coordinates.size() > 0
              && coordinates.getCoordinate(coordinates.size() - 1).equals(edgeCoordinates[0])) {
            coordinates.extend(edgeCoordinates, 1);
          } else {
            coordinates.extend(edgeCoordinates);
          }
        }

        if (postponedAlerts != null) {
          addNotesToLeg(leg, postponedAlerts);
          postponedAlerts = null;
        }

        addNotesToLeg(leg, state.getBackAlerts());
      }
    } /* end loop over graphPath edge list */

    if (leg != null) {
      finalizeLeg(leg, path.states.getLast(), path.states, startWalk, i, coordinates, itinerary);
    }
    itinerary.removeBogusLegs();
    itinerary.fixupDates(graph.getService(CalendarServiceData.class));
    if (itinerary.legs.size() == 0) throw new TrivialPathException();
    return itinerary;
  }
  @Test
  public void testUpdate() {
    TripUpdate tripUpdate;
    TripUpdate.Builder tripUpdateBuilder;
    TripDescriptor.Builder tripDescriptorBuilder;
    StopTimeUpdate.Builder stopTimeUpdateBuilder;
    StopTimeEvent.Builder stopTimeEventBuilder;

    int trip_1_1_index = timetable.getTripIndex(new AgencyAndId("agency", "1.1"));

    Vertex stop_a = graph.getVertex("agency_A");
    Vertex stop_c = graph.getVertex("agency_C");
    RoutingRequest options = new RoutingRequest();

    ShortestPathTree spt;
    GraphPath path;

    // non-existing trip
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("b");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.CANCELED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    tripUpdate = tripUpdateBuilder.build();
    assertFalse(timetable.update(tripUpdate, "a", timeZone, serviceDate));

    // update trip with bad data
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopSequence(0);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SKIPPED);
    tripUpdate = tripUpdateBuilder.build();
    assertFalse(timetable.update(tripUpdate, "agency", timeZone, serviceDate));

    // update trip with non-increasing data
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopSequence(2);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder();
    stopTimeEventBuilder.setTime(
        TestUtils.dateInSeconds("America/New_York", 2009, AUGUST, 7, 0, 10, 1));
    stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder();
    stopTimeEventBuilder.setTime(
        TestUtils.dateInSeconds("America/New_York", 2009, AUGUST, 7, 0, 10, 0));
    tripUpdate = tripUpdateBuilder.build();
    assertFalse(timetable.update(tripUpdate, "agency", timeZone, serviceDate));

    // ---
    long startTime = TestUtils.dateInSeconds("America/New_York", 2009, AUGUST, 7, 0, 0, 0);
    long endTime;
    options.dateTime = startTime;

    // ---
    options.setRoutingContext(graph, stop_a, stop_c);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_c, false);
    assertNotNull(path);
    endTime = startTime + 20 * 60;
    assertEquals(endTime, path.getEndTime());

    // update trip
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopSequence(1);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder();
    stopTimeEventBuilder.setTime(
        TestUtils.dateInSeconds("America/New_York", 2009, AUGUST, 7, 0, 2, 0));
    stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder();
    stopTimeEventBuilder.setTime(
        TestUtils.dateInSeconds("America/New_York", 2009, AUGUST, 7, 0, 2, 0));
    tripUpdate = tripUpdateBuilder.build();
    assertEquals(20 * 60, timetable.getTripTimes(trip_1_1_index).getArrivalTime(2));
    assertTrue(timetable.update(tripUpdate, "agency", timeZone, serviceDate));
    assertEquals(20 * 60 + 120, timetable.getTripTimes(trip_1_1_index).getArrivalTime(2));

    // ---
    options.setRoutingContext(graph, stop_a, stop_c);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_c, false);
    assertNotNull(path);
    endTime = startTime + 20 * 60 + 120;
    assertEquals(endTime, path.getEndTime());

    // cancel trip
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.CANCELED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    tripUpdate = tripUpdateBuilder.build();
    assertTrue(timetable.update(tripUpdate, "agency", timeZone, serviceDate));

    TripTimes tripTimes = timetable.getTripTimes(trip_1_1_index);
    for (int i = 0; i < tripTimes.getNumStops(); i++) {
      assertEquals(TripTimes.UNAVAILABLE, tripTimes.getDepartureTime(i));
      assertEquals(TripTimes.UNAVAILABLE, tripTimes.getArrivalTime(i));
    }

    // ---
    options.setRoutingContext(graph, stop_a, stop_c);
    spt = aStar.getShortestPathTree(options);
    path = spt.getPath(stop_c, false);
    assertNotNull(path);
    endTime = startTime + 40 * 60;
    assertEquals(endTime, path.getEndTime());

    // update trip arrival time incorrectly
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopSequence(1);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder();
    stopTimeEventBuilder.setDelay(0);
    tripUpdate = tripUpdateBuilder.build();
    assertTrue(timetable.update(tripUpdate, "agency", timeZone, serviceDate));

    // update trip arrival time only
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopSequence(2);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder();
    stopTimeEventBuilder.setDelay(1);
    tripUpdate = tripUpdateBuilder.build();
    assertTrue(timetable.update(tripUpdate, "agency", timeZone, serviceDate));

    // update trip departure time only
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopSequence(2);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder();
    stopTimeEventBuilder.setDelay(-1);
    tripUpdate = tripUpdateBuilder.build();
    assertTrue(timetable.update(tripUpdate, "agency", timeZone, serviceDate));

    // update trip using stop id
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopId("B");
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder();
    stopTimeEventBuilder.setDelay(-1);
    tripUpdate = tripUpdateBuilder.build();
    assertTrue(timetable.update(tripUpdate, "agency", timeZone, serviceDate));

    // update trip arrival time at first stop and make departure time incoherent at second stop
    tripDescriptorBuilder = TripDescriptor.newBuilder();
    tripDescriptorBuilder.setTripId("1.1");
    tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
    tripUpdateBuilder = TripUpdate.newBuilder();
    tripUpdateBuilder.setTrip(tripDescriptorBuilder);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(0);
    stopTimeUpdateBuilder.setStopSequence(1);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getArrivalBuilder();
    stopTimeEventBuilder.setDelay(0);
    stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(1);
    stopTimeUpdateBuilder.setStopSequence(2);
    stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED);
    stopTimeEventBuilder = stopTimeUpdateBuilder.getDepartureBuilder();
    stopTimeEventBuilder.setDelay(-1);
    tripUpdate = tripUpdateBuilder.build();
    assertFalse(timetable.update(tripUpdate, "agency", timeZone, serviceDate));
  }
  @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;
  }