/* This program is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.routing.core;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import junit.framework.TestCase;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.Stop;
import org.onebusaway.gtfs.model.Trip;
import org.onebusaway.gtfs.model.calendar.CalendarServiceData;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.opentripplanner.ConstantsForTests;
import org.opentripplanner.gtfs.GtfsContext;
import org.opentripplanner.gtfs.GtfsLibrary;
import org.opentripplanner.routing.algorithm.AStar;
import org.opentripplanner.routing.edgetype.SimpleTransfer;
import org.opentripplanner.routing.edgetype.TimedTransferEdge;
import org.opentripplanner.routing.edgetype.Timetable;
import org.opentripplanner.routing.edgetype.TimetableSnapshot;
import org.opentripplanner.routing.edgetype.TransitBoardAlight;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.edgetype.factory.GTFSPatternHopFactory;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.Vertex;
import org.opentripplanner.routing.impl.DefaultStreetVertexIndexFactory;
import org.opentripplanner.routing.spt.GraphPath;
import org.opentripplanner.routing.spt.ShortestPathTree;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.opentripplanner.updater.stoptime.TimetableSnapshotSource;
import org.opentripplanner.util.TestUtils;
import com.google.transit.realtime.GtfsRealtime.TripDescriptor;
import com.google.transit.realtime.GtfsRealtime.TripUpdate;
import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent;
import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate;
import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship;
/**
* This is a singleton class to hold graph data between test runs, since loading it is slow.
*/
class Context {
public String feedId;
public Graph graph;
public AStar aStar;
private static Context instance = null;
public static Context getInstance() throws IOException {
if (instance == null) {
instance = new Context();
}
return instance;
}
public Context() throws IOException {
// Create a star search
aStar = new AStar();
// Create graph
GtfsContext context = GtfsLibrary.readGtfs(new File(ConstantsForTests.FAKE_GTFS));
graph = spy(new Graph());
GTFSPatternHopFactory factory = new GTFSPatternHopFactory(context);
factory.run(graph);
graph.index(new DefaultStreetVertexIndexFactory());
graph.putService(CalendarServiceData.class,
GtfsLibrary.createCalendarServiceData(context.getDao()));
feedId = context.getFeedId().getId();
// Add simple transfer to make transfer possible between N-K and F-H
createSimpleTransfer(feedId + ":K", feedId + ":F", 100);
// Add simple transfer to make transfer possible between O-P and U-V
createSimpleTransfer(feedId + ":P", feedId + ":U", 100);
// Add simple transfer to make transfer possible between U-V and I-J
createSimpleTransfer(feedId + ":V", feedId + ":I", 100);
// Create dummy TimetableSnapshot
TimetableSnapshot snapshot = new TimetableSnapshot();
// Mock TimetableSnapshotSource to return dummy TimetableSnapshot
TimetableSnapshotSource timetableSnapshotSource = mock(TimetableSnapshotSource.class);
when(timetableSnapshotSource.getTimetableSnapshot()).thenReturn(snapshot);
graph.timetableSnapshotSource = (timetableSnapshotSource);
}
/**
* Create simple transfer edge between two vertices given their labels
* @param from is label of from vertex
* @param to is label of to vertex
* @param distance is distance of transfer
*/
private void createSimpleTransfer(String from, String to, int distance) {
TransitStop fromv = ((TransitStop) graph.getVertex(from));
TransitStop tov = ((TransitStop) graph.getVertex(to));
new SimpleTransfer(fromv, tov, distance, null);
}
}
/**
* Test transfers, mostly stop-to-stop transfers.
*/
public class TestTransfers extends TestCase {
private Graph graph;
private AStar aStar;
private String feedId;
public void setUp() throws Exception {
// Get graph, a star & feed id from singleton class
graph = Context.getInstance().graph;
aStar = Context.getInstance().aStar;
feedId = Context.getInstance().feedId;
}
/**
* Plan journey without optimization and return list of states and edges
* @param options are options to use for planning the journey
* @return ordered list of states and edges in the journey
*/
private GraphPath planJourney(RoutingRequest options) {
return planJourney(options, false);
}
/**
* Plan journey and return list of states and edges
* @param options are options to use for planning the journey
* @param optimize is true when optimization should be used
* @return ordered list of states and edges in the journey
*/
private GraphPath planJourney(RoutingRequest options, boolean optimize) {
// Calculate route and convert to path
ShortestPathTree spt = aStar.getShortestPathTree(options);
GraphPath path = spt.getPath(options.rctx.target, optimize);
// Return list of states and edges in the journey
return path;
}
private List<Trip> extractTrips(GraphPath path) {
// Get all trips in order
List<Trip> trips = new ArrayList<Trip>();
if (path != null) {
for (State s : path.states) {
if (s.getBackMode() != null && s.getBackMode().isTransit()) {
Trip trip = s.getBackTrip();
if (trip != null && !trips.contains(trip)) {
trips.add(trip);
}
}
}
}
// Return trips
return trips;
}
/**
* Apply an update to a table trip pattern and check whether the update was applied correctly
*/
private void applyUpdateToTripPattern(TripPattern pattern, String tripId, String stopId,
int stopSeq, int arrive, int depart, ScheduleRelationship scheduleRelationship,
int timestamp, ServiceDate serviceDate) throws ParseException {
TimetableSnapshot snapshot = graph.timetableSnapshotSource.getTimetableSnapshot();
Timetable timetable = snapshot.resolve(pattern, serviceDate);
TimeZone timeZone = new SimpleTimeZone(-7, "PST");
long today = serviceDate.getAsDate(timeZone).getTime() / 1000;
TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder();
tripDescriptorBuilder.setTripId(tripId);
StopTimeEvent.Builder departStopTimeEventBuilder = StopTimeEvent.newBuilder();
StopTimeEvent.Builder arriveStopTimeEventBuilder = StopTimeEvent.newBuilder();
departStopTimeEventBuilder.setTime(today + depart);
arriveStopTimeEventBuilder.setTime(today + arrive);
StopTimeUpdate.Builder stopTimeUpdateBuilder = StopTimeUpdate.newBuilder();
stopTimeUpdateBuilder.setStopSequence(stopSeq);
stopTimeUpdateBuilder.setDeparture(departStopTimeEventBuilder);
stopTimeUpdateBuilder.setArrival(arriveStopTimeEventBuilder);
stopTimeUpdateBuilder.setScheduleRelationship(scheduleRelationship);
TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder();
tripUpdateBuilder.setTrip(tripDescriptorBuilder);
tripUpdateBuilder.addStopTimeUpdate(0, stopTimeUpdateBuilder);
TripUpdate tripUpdate = tripUpdateBuilder.build();
TripTimes updatedTripTimes = timetable.createUpdatedTripTimes(tripUpdate, timeZone, serviceDate);
assertNotNull(updatedTripTimes);
int tripIndex = timetable.getTripIndex(tripId);
assertTrue(tripIndex != -1);
timetable.setTripTimes(tripIndex, updatedTripTimes);
}
public void testStopToStopTransfer() throws Exception {
// Replace the transfer table with an empty table
TransferTable table = new TransferTable();
when(graph.getTransferTable()).thenReturn(table);
// Compute a normal path between two stops
Vertex origin = graph.getVertex(feedId + ":N");
Vertex destination = graph.getVertex(feedId + ":H");
// Set options like time and routing context
RoutingRequest options = new RoutingRequest();
options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 7, 11, 11, 11, 0);
options.setRoutingContext(graph, origin, destination);
// Plan journey
GraphPath path;
List<Trip> trips;
path = planJourney(options);
trips = extractTrips(path);
// Validate result
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.2", trips.get(1).getId().getId());
// Add transfer to table, transfer time was 27600 seconds
Stop stopK = new Stop();
stopK.setId(new AgencyAndId(feedId, "K"));
Stop stopF = new Stop();
stopF.setId(new AgencyAndId(feedId, "F"));
table.addTransferTime(stopK, stopF, null, null, null, null, 27601);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check whether a later second trip was taken
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.3", trips.get(1).getId().getId());
// Revert the graph, thus using the original transfer table again
reset(graph);
}
public void testStopToStopTransferInReverse() throws Exception {
// Replace the transfer table with an empty table
TransferTable table = new TransferTable();
when(graph.getTransferTable()).thenReturn(table);
// Compute a normal path between two stops
Vertex origin = graph.getVertex(feedId + ":N");
Vertex destination = graph.getVertex(feedId + ":H");
// Set options like time and routing context
RoutingRequest options = new RoutingRequest();
options.setArriveBy(true);
options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 7, 12, 1, 0, 0);
options.setRoutingContext(graph, origin, destination);
// Plan journey
GraphPath path;
List<Trip> trips;
path = planJourney(options, true);
trips = extractTrips(path);
// Validate result
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.2", trips.get(1).getId().getId());
// Add transfer to table, transfer time was 27600 seconds
Stop stopK = new Stop();
stopK.setId(new AgencyAndId(feedId, "K"));
Stop stopF = new Stop();
stopF.setId(new AgencyAndId(feedId, "F"));
table.addTransferTime(stopK, stopF, null, null, null, null, 27601);
// Plan journey
path = planJourney(options, true);
trips = extractTrips(path);
// Check whether a later second trip was taken
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.3", trips.get(1).getId().getId());
// Revert the graph, thus using the original transfer table again
reset(graph);
}
public void testStopToStopTransferWithFrequency() throws Exception {
// Replace the transfer table with an empty table
TransferTable table = new TransferTable();
when(graph.getTransferTable()).thenReturn(table);
// Compute a normal path between two stops
Vertex origin = graph.getVertex(feedId + ":O");
Vertex destination = graph.getVertex(feedId + ":V");
// Set options like time and routing context
RoutingRequest options = new RoutingRequest();
options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 7, 11, 13, 11, 0);
options.setRoutingContext(graph, origin, destination);
// Plan journey
GraphPath path;
List<Trip> trips;
path = planJourney(options);
trips = extractTrips(path);
// Validate result
assertEquals("10.5", trips.get(0).getId().getId());
assertEquals("15.1", trips.get(1).getId().getId());
// Find state with FrequencyBoard back edge and save time of that state
long time = -1;
for (State s : path.states) {
if (s.getBackEdge() instanceof TransitBoardAlight && ((TransitBoardAlight)s.getBackEdge()).boarding) {
time = s.getTimeSeconds(); // find the final board edge, don't break
}
}
assertTrue(time >= 0);
// Add transfer to table such that the next trip will be chosen
// (there are 3600 seconds between trips), transfer time was 75 seconds
Stop stopP = new Stop();
stopP.setId(new AgencyAndId(feedId, "P"));
Stop stopU = new Stop();
stopU.setId(new AgencyAndId(feedId, "U"));
table.addTransferTime(stopP, stopU, null, null, null, null, 3675);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check whether a later second trip was taken
assertEquals("10.5", trips.get(0).getId().getId());
assertEquals("15.1", trips.get(1).getId().getId());
// Find state with FrequencyBoard back edge and save time of that state
long newTime = -1;
for (State s : path.states) {
if (s.getBackEdge() instanceof TransitBoardAlight && ((TransitBoardAlight)s.getBackEdge()).boarding) {
newTime = s.getTimeSeconds(); // find the final board edge, don't break
}
}
assertTrue(newTime >= 0);
assertTrue(newTime > time);
assertEquals(3600, newTime - time);
// Revert the graph, thus using the original transfer table again
reset(graph);
}
public void testStopToStopTransferWithFrequencyInReverse() throws Exception {
// Replace the transfer table with an empty table
TransferTable table = new TransferTable();
when(graph.getTransferTable()).thenReturn(table);
// Compute a normal path between two stops
Vertex origin = graph.getVertex(feedId + ":U");
Vertex destination = graph.getVertex(feedId + ":J");
// Set options like time and routing context
RoutingRequest options = new RoutingRequest();
options.setArriveBy(true);
options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 7, 11, 11, 11, 0);
options.setRoutingContext(graph, origin, destination);
// Plan journey
GraphPath path;
List<Trip> trips;
path = planJourney(options);
trips = extractTrips(path);
// Validate result
assertEquals("15.1", trips.get(0).getId().getId());
assertEquals("5.1", trips.get(1).getId().getId());
// Find state with FrequencyBoard back edge and save time of that state
long time = -1;
for (State s : path.states) {
if (s.getBackEdge() instanceof TransitBoardAlight
&& s.getBackState() != null) {
time = s.getBackState().getTimeSeconds();
break;
}
}
assertTrue(time >= 0);
// Add transfer to table such that the next trip will be chosen
// (there are 3600 seconds between trips), transfer time was 75 seconds
Stop stopV = new Stop();
stopV.setId(new AgencyAndId(feedId, "V"));
Stop stopI = new Stop();
stopI.setId(new AgencyAndId(feedId, "I"));
table.addTransferTime(stopV, stopI, null, null, null, null, 3675);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check whether a later second trip was taken
assertEquals("15.1", trips.get(0).getId().getId());
assertEquals("5.1", trips.get(1).getId().getId());
// Find state with FrequencyBoard back edge and save time of that state
long newTime = -1;
for (State s : path.states) {
if (s.getBackEdge() instanceof TransitBoardAlight
&& s.getBackState() != null) {
newTime = s.getBackState().getTimeSeconds();
break;
}
}
assertTrue(newTime >= 0);
assertTrue(newTime < time);
assertEquals(3600, time - newTime);
// Revert the graph, thus using the original transfer table again
reset(graph);
}
public void testForbiddenStopToStopTransfer() throws Exception {
// Replace the transfer table with an empty table
TransferTable table = new TransferTable();
when(graph.getTransferTable()).thenReturn(table);
// Compute a normal path between two stops
Vertex origin = graph.getVertex(feedId + ":N");
Vertex destination = graph.getVertex(feedId + ":H");
// Set options like time and routing context
RoutingRequest options = new RoutingRequest();
options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 7, 11, 11, 11, 0);
options.setRoutingContext(graph, origin, destination);
// Plan journey
GraphPath path;
List<Trip> trips;
path = planJourney(options);
trips = extractTrips(path);
// Validate result
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.2", trips.get(1).getId().getId());
// Add forbidden transfer to table
Stop stopK = new Stop();
stopK.setId(new AgencyAndId(feedId, "K"));
Stop stopF = new Stop();
stopF.setId(new AgencyAndId(feedId, "F"));
table.addTransferTime(stopK, stopF, null, null, null, null,
StopTransfer.FORBIDDEN_TRANSFER);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check that no trip was returned
assertEquals(0, trips.size());
// Revert the graph, thus using the original transfer table again
reset(graph);
}
public void testForbiddenStopToStopTransferWithFrequencyInReverse() throws Exception {
// Replace the transfer table with an empty table
TransferTable table = new TransferTable();
when(graph.getTransferTable()).thenReturn(table);
// Compute a normal path between two stops
Vertex origin = graph.getVertex(feedId + ":U");
Vertex destination = graph.getVertex(feedId + ":J");
// Set options like time and routing context
RoutingRequest options = new RoutingRequest();
options.setArriveBy(true);
options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 7, 11, 11, 11, 0);
options.setRoutingContext(graph, origin, destination);
// Plan journey
GraphPath path;
List<Trip> trips;
path = planJourney(options);
trips = extractTrips(path);
// Validate result
assertEquals("15.1", trips.get(0).getId().getId());
assertEquals("5.1", trips.get(1).getId().getId());
// Add forbidden transfer to table
Stop stopV = new Stop();
stopV.setId(new AgencyAndId(feedId, "V"));
Stop stopI = new Stop();
stopI.setId(new AgencyAndId(feedId, "I"));
table.addTransferTime(stopV, stopI, null, null, null, null,
StopTransfer.FORBIDDEN_TRANSFER);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check that no trip was returned
assertEquals(0, trips.size());
// Revert the graph, thus using the original transfer table again
reset(graph);
}
public void testTimedStopToStopTransfer() throws Exception {
ServiceDate serviceDate = new ServiceDate(2009, 07, 11);
// Replace the transfer table with an empty table
TransferTable table = new TransferTable();
when(graph.getTransferTable()).thenReturn(table);
// Compute a normal path between two stops
Vertex origin = graph.getVertex(feedId + ":N");
Vertex destination = graph.getVertex(feedId + ":H");
// Set options like time and routing context
RoutingRequest options = new RoutingRequest();
options.dateTime = TestUtils.dateInSeconds("America/New_York", 2009, 7, 11, 11, 11, 0);
options.setRoutingContext(graph, origin, destination);
// Plan journey
GraphPath path;
List<Trip> trips;
path = planJourney(options);
trips = extractTrips(path);
// Validate result
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.2", trips.get(1).getId().getId());
// Add timed transfer to table
Stop stopK = new Stop();
stopK.setId(new AgencyAndId(feedId, "K"));
Stop stopF = new Stop();
stopF.setId(new AgencyAndId(feedId, "F"));
table.addTransferTime(stopK, stopF, null, null, null, null, StopTransfer.TIMED_TRANSFER);
// Don't forget to also add a TimedTransferEdge
Vertex fromVertex = graph.getVertex(feedId + ":K_arrive");
Vertex toVertex = graph.getVertex(feedId + ":F_depart");
TimedTransferEdge timedTransferEdge = new TimedTransferEdge(fromVertex, toVertex);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check whether the trips are still the same
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.2", trips.get(1).getId().getId());
// Now apply a real-time update: let the to-trip be early by 27600 seconds,
// resulting in a transfer time of 0 seconds
Trip trip = graph.index.tripForId.get(new AgencyAndId("agency", "4.2"));
TripPattern pattern = graph.index.patternForTrip.get(trip);
applyUpdateToTripPattern(pattern, "4.2", "F", 1, 55200, 55200,
ScheduleRelationship.SCHEDULED, 0, serviceDate);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check whether the trips are still the same
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.2", trips.get(1).getId().getId());
// Now apply a real-time update: let the to-trip be early by 27601 seconds,
// resulting in a transfer time of -1 seconds
applyUpdateToTripPattern(pattern, "4.2", "F", 1, 55199, 55199,
ScheduleRelationship.SCHEDULED, 0, serviceDate);
// Plan journey
path = planJourney(options);
trips = extractTrips(path);
// Check whether a later second trip was taken
assertEquals("8.1", trips.get(0).getId().getId());
assertEquals("4.3", trips.get(1).getId().getId());
// "Revert" the real-time update
applyUpdateToTripPattern(pattern, "4.2", "F", 1, 82800, 82800,
ScheduleRelationship.SCHEDULED, 0, serviceDate);
// Remove the timed transfer from the graph
graph.removeEdge(timedTransferEdge);
// Revert the graph, thus using the original transfer table again
reset(graph);
}
}