/* 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.updater.stoptime; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.io.File; import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; import java.util.List; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.onebusaway.gtfs.model.*; import org.onebusaway.gtfs.model.calendar.CalendarServiceData; import org.onebusaway.gtfs.model.calendar.ServiceDate; import org.onebusaway.gtfs.services.GtfsRelationalDao; import org.opentripplanner.ConstantsForTests; import org.opentripplanner.gtfs.GtfsContext; import org.opentripplanner.gtfs.GtfsLibrary; 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.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.impl.DefaultStreetVertexIndexFactory; import org.opentripplanner.routing.trippattern.RealTimeState; import org.opentripplanner.routing.trippattern.TripTimes; import org.opentripplanner.routing.vertextype.TransitStopDepart; import com.google.protobuf.InvalidProtocolBufferException; 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 org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; public class TimetableSnapshotSourceTest { private static byte cancellation[]; private static Graph graph = new Graph(); private static boolean fullDataset = false; private static GtfsContext context; private static ServiceDate serviceDate = new ServiceDate(); private static String feedId; private TimetableSnapshotSource updater; @BeforeClass public static void setUpClass() throws Exception { context = GtfsLibrary.readGtfs(new File(ConstantsForTests.FAKE_GTFS)); GtfsRelationalDao dao = context.getDao(); feedId = context.getFeedId().getId(); for (ShapePoint shapePoint : dao.getAllEntitiesForType(ShapePoint.class)) { shapePoint.getShapeId().setAgencyId(feedId); } for (Route route : dao.getAllEntitiesForType(Route.class)) { route.getId().setAgencyId(feedId); } for (Stop stop : dao.getAllEntitiesForType(Stop.class)) { stop.getId().setAgencyId(feedId); } for (Trip trip : dao.getAllEntitiesForType(Trip.class)) { trip.getId().setAgencyId(feedId); } for (ServiceCalendar serviceCalendar : dao.getAllEntitiesForType(ServiceCalendar.class)) { serviceCalendar.getServiceId().setAgencyId(feedId); } for (ServiceCalendarDate serviceCalendarDate : dao.getAllEntitiesForType(ServiceCalendarDate.class)) { serviceCalendarDate.getServiceId().setAgencyId(feedId); } for (FareAttribute fareAttribute : dao.getAllEntitiesForType(FareAttribute.class)) { fareAttribute.getId().setAgencyId(feedId); } for (Pathway pathway : dao.getAllEntitiesForType(Pathway.class)) { pathway.getId().setAgencyId(feedId); } GTFSPatternHopFactory factory = new GTFSPatternHopFactory(context); factory.run(graph); graph.index(new DefaultStreetVertexIndexFactory()); final TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder(); tripDescriptorBuilder.setTripId("1.1"); tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.CANCELED); final TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); tripUpdateBuilder.setTrip(tripDescriptorBuilder); cancellation = tripUpdateBuilder.build().toByteArray(); } @Before public void setUp() { graph.putService(CalendarServiceData.class, GtfsLibrary.createCalendarServiceData(context.getDao())); updater = new TimetableSnapshotSource(graph); } @Test public void testGetSnapshot() throws InvalidProtocolBufferException { updater.applyTripUpdates(graph, fullDataset, Arrays.asList(TripUpdate.parseFrom(cancellation)), feedId); final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); assertNotNull(snapshot); assertSame(snapshot, updater.getTimetableSnapshot()); updater.applyTripUpdates(graph, fullDataset, Arrays.asList(TripUpdate.parseFrom(cancellation)), feedId); assertSame(snapshot, updater.getTimetableSnapshot()); updater.maxSnapshotFrequency = (-1); final TimetableSnapshot newSnapshot = updater.getTimetableSnapshot(); assertNotNull(newSnapshot); assertNotSame(snapshot, newSnapshot); } @Test public void testHandleCanceledTrip() throws InvalidProtocolBufferException { final AgencyAndId tripId = new AgencyAndId(feedId, "1.1"); final AgencyAndId tripId2 = new AgencyAndId(feedId, "1.2"); final Trip trip = graph.index.tripForId.get(tripId); final TripPattern pattern = graph.index.patternForTrip.get(trip); final int tripIndex = pattern.scheduledTimetable.getTripIndex(tripId); final int tripIndex2 = pattern.scheduledTimetable.getTripIndex(tripId2); updater.applyTripUpdates(graph, fullDataset, Arrays.asList(TripUpdate.parseFrom(cancellation)), feedId); final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); final Timetable forToday = snapshot.resolve(pattern, serviceDate); final Timetable schedule = snapshot.resolve(pattern, null); assertNotSame(forToday, schedule); assertNotSame(forToday.getTripTimes(tripIndex), schedule.getTripTimes(tripIndex)); assertSame(forToday.getTripTimes(tripIndex2), schedule.getTripTimes(tripIndex2)); final TripTimes tripTimes = forToday.getTripTimes(tripIndex); for (int i = 0; i < tripTimes.getNumStops(); i++) { assertEquals(TripTimes.UNAVAILABLE, tripTimes.getDepartureTime(i)); assertEquals(TripTimes.UNAVAILABLE, tripTimes.getArrivalTime(i)); } assertEquals(RealTimeState.CANCELED, tripTimes.getRealTimeState()); } @Test public void testHandleDelayedTrip() { final AgencyAndId tripId = new AgencyAndId(feedId, "1.1"); final AgencyAndId tripId2 = new AgencyAndId(feedId, "1.2"); final Trip trip = graph.index.tripForId.get(tripId); final TripPattern pattern = graph.index.patternForTrip.get(trip); final int tripIndex = pattern.scheduledTimetable.getTripIndex(tripId); final int tripIndex2 = pattern.scheduledTimetable.getTripIndex(tripId2); final TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder(); tripDescriptorBuilder.setTripId("1.1"); tripDescriptorBuilder.setScheduleRelationship( TripDescriptor.ScheduleRelationship.SCHEDULED); final TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); tripUpdateBuilder.setTrip(tripDescriptorBuilder); final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship( StopTimeUpdate.ScheduleRelationship.SCHEDULED); stopTimeUpdateBuilder.setStopSequence(2); final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); arrivalBuilder.setDelay(1); departureBuilder.setDelay(1); final TripUpdate tripUpdate = tripUpdateBuilder.build(); updater.applyTripUpdates(graph, fullDataset, Arrays.asList(tripUpdate), feedId); final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); final Timetable forToday = snapshot.resolve(pattern, serviceDate); final Timetable schedule = snapshot.resolve(pattern, null); assertNotSame(forToday, schedule); assertNotSame(forToday.getTripTimes(tripIndex), schedule.getTripTimes(tripIndex)); assertSame(forToday.getTripTimes(tripIndex2), schedule.getTripTimes(tripIndex2)); assertEquals(1, forToday.getTripTimes(tripIndex).getArrivalDelay(1)); assertEquals(1, forToday.getTripTimes(tripIndex).getDepartureDelay(1)); assertEquals(RealTimeState.SCHEDULED, schedule.getTripTimes(tripIndex).getRealTimeState()); assertEquals(RealTimeState.UPDATED, forToday.getTripTimes(tripIndex).getRealTimeState()); assertEquals(RealTimeState.SCHEDULED, schedule.getTripTimes(tripIndex2).getRealTimeState()); assertEquals(RealTimeState.SCHEDULED, forToday.getTripTimes(tripIndex2).getRealTimeState()); } @Test public void testHandleAddedTrip() throws ParseException { // GIVEN // Get service date of today because old dates will be purged after applying updates final ServiceDate serviceDate = new ServiceDate(Calendar.getInstance()); final String addedTripId = "added_trip"; TripUpdate tripUpdate; { final TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder(); tripDescriptorBuilder.setTripId(addedTripId); tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.ADDED); tripDescriptorBuilder.setStartDate(serviceDate.getAsString()); final Calendar calendar = serviceDate.getAsCalendar(graph.getTimeZone()); final long midnightSecondsSinceEpoch = calendar.getTimeInMillis() / 1000; final TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); tripUpdateBuilder.setTrip(tripDescriptorBuilder); { // Stop A final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); stopTimeUpdateBuilder.setStopId("A"); { // Arrival final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); arrivalBuilder.setTime(midnightSecondsSinceEpoch + (8 * 3600) + (30 * 60)); arrivalBuilder.setDelay(0); } { // Departure final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); departureBuilder.setTime(midnightSecondsSinceEpoch + (8 * 3600) + (30 * 60)); departureBuilder.setDelay(0); } } { // Stop C final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); stopTimeUpdateBuilder.setStopId("C"); { // Arrival final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); arrivalBuilder.setTime(midnightSecondsSinceEpoch + (8 * 3600) + (40 * 60)); arrivalBuilder.setDelay(0); } { // Departure final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); departureBuilder.setTime(midnightSecondsSinceEpoch + (8 * 3600) + (45 * 60)); departureBuilder.setDelay(0); } } { // Stop E final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); stopTimeUpdateBuilder.setStopId("E"); { // Arrival final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); arrivalBuilder.setTime(midnightSecondsSinceEpoch + (8 * 3600) + (55 * 60)); arrivalBuilder.setDelay(0); } { // Departure final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); departureBuilder.setTime(midnightSecondsSinceEpoch + (8 * 3600) + (55 * 60)); departureBuilder.setDelay(0); } } tripUpdate = tripUpdateBuilder.build(); } // WHEN updater.applyTripUpdates(graph, fullDataset, Arrays.asList(tripUpdate), feedId); // THEN // Find new pattern in graph starting from stop A Stop stopA = graph.index.stopForId.get(new AgencyAndId(feedId, "A")); TransitStopDepart transitStopDepartA = graph.index.stopVertexForStop.get(stopA).departVertex; // Get trip pattern of last (most recently added) outgoing edge final List<Edge> outgoingEdges = (List<Edge>) transitStopDepartA.getOutgoing(); final TripPattern tripPattern = ((TransitBoardAlight) outgoingEdges.get(outgoingEdges.size() - 1)).getPattern(); assertNotNull("Added trip pattern should be found", tripPattern); final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); final Timetable forToday = snapshot.resolve(tripPattern, serviceDate); final Timetable schedule = snapshot.resolve(tripPattern, null); assertNotSame(forToday, schedule); final int forTodayAddedTripIndex = forToday.getTripIndex(addedTripId); assertTrue("Added trip should be found in time table for service date", forTodayAddedTripIndex > -1); assertEquals(RealTimeState.ADDED, forToday.getTripTimes(forTodayAddedTripIndex).getRealTimeState()); final int scheduleTripIndex = schedule.getTripIndex(addedTripId); assertEquals("Added trip should not be found in scheduled time table", -1, scheduleTripIndex); } @Test public void testHandleModifiedTrip() throws ParseException { // TODO // GIVEN // Get service date of today because old dates will be purged after applying updates ServiceDate serviceDate = new ServiceDate(Calendar.getInstance()); String modifiedTripId = "10.1"; TripUpdate tripUpdate; { final TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder(); tripDescriptorBuilder.setTripId(modifiedTripId); tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.MODIFIED); tripDescriptorBuilder.setStartDate(serviceDate.getAsString()); final Calendar calendar = serviceDate.getAsCalendar(graph.getTimeZone()); final long midnightSecondsSinceEpoch = calendar.getTimeInMillis() / 1000; final TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); tripUpdateBuilder.setTrip(tripDescriptorBuilder); { // Stop O final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); stopTimeUpdateBuilder.setStopId("O"); stopTimeUpdateBuilder.setStopSequence(10); { // Arrival final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); arrivalBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (30 * 60)); arrivalBuilder.setDelay(0); } { // Departure final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); departureBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (30 * 60)); departureBuilder.setDelay(0); } } { // Stop C final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); stopTimeUpdateBuilder.setStopId("C"); stopTimeUpdateBuilder.setStopSequence(30); { // Arrival final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); arrivalBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (40 * 60)); arrivalBuilder.setDelay(0); } { // Departure final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); departureBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (45 * 60)); departureBuilder.setDelay(0); } } { // Stop D final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SKIPPED); stopTimeUpdateBuilder.setStopId("D"); stopTimeUpdateBuilder.setStopSequence(40); { // Arrival final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); arrivalBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (50 * 60)); arrivalBuilder.setDelay(0); } { // Departure final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); departureBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (51 * 60)); departureBuilder.setDelay(0); } } { // Stop P final StopTimeUpdate.Builder stopTimeUpdateBuilder = tripUpdateBuilder.addStopTimeUpdateBuilder(); stopTimeUpdateBuilder.setScheduleRelationship(StopTimeUpdate.ScheduleRelationship.SCHEDULED); stopTimeUpdateBuilder.setStopId("P"); stopTimeUpdateBuilder.setStopSequence(50); { // Arrival final StopTimeEvent.Builder arrivalBuilder = stopTimeUpdateBuilder.getArrivalBuilder(); arrivalBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (55 * 60)); arrivalBuilder.setDelay(0); } { // Departure final StopTimeEvent.Builder departureBuilder = stopTimeUpdateBuilder.getDepartureBuilder(); departureBuilder.setTime(midnightSecondsSinceEpoch + (12 * 3600) + (55 * 60)); departureBuilder.setDelay(0); } } tripUpdate = tripUpdateBuilder.build(); } // WHEN updater.applyTripUpdates(graph, fullDataset, Arrays.asList(tripUpdate), feedId); // THEN final TimetableSnapshot snapshot = updater.getTimetableSnapshot(); // Original trip pattern { final AgencyAndId tripId = new AgencyAndId(feedId, modifiedTripId); final Trip trip = graph.index.tripForId.get(tripId); final TripPattern originalTripPattern = graph.index.patternForTrip.get(trip); final Timetable originalTimetableForToday = snapshot.resolve(originalTripPattern, serviceDate); final Timetable originalTimetableScheduled = snapshot.resolve(originalTripPattern, null); assertNotSame(originalTimetableForToday, originalTimetableScheduled); final int originalTripIndexScheduled = originalTimetableScheduled.getTripIndex(modifiedTripId); assertTrue("Original trip should be found in scheduled time table", originalTripIndexScheduled > -1); final TripTimes originalTripTimesScheduled = originalTimetableScheduled.getTripTimes(originalTripIndexScheduled); assertFalse("Original trip times should not be canceled in scheduled time table", originalTripTimesScheduled.isCanceled()); assertEquals(RealTimeState.SCHEDULED, originalTripTimesScheduled.getRealTimeState()); final int originalTripIndexForToday = originalTimetableForToday.getTripIndex(modifiedTripId); assertTrue("Original trip should be found in time table for service date", originalTripIndexForToday > -1); final TripTimes originalTripTimesForToday = originalTimetableForToday.getTripTimes(originalTripIndexForToday); assertTrue("Original trip times should be canceled in time table for service date", originalTripTimesForToday.isCanceled()); assertEquals(RealTimeState.CANCELED, originalTripTimesForToday.getRealTimeState()); } // New trip pattern { final TripPattern newTripPattern = snapshot.getLastAddedTripPattern(feedId, modifiedTripId, serviceDate); assertNotNull("New trip pattern should be found", newTripPattern); final Timetable newTimetableForToday = snapshot.resolve(newTripPattern, serviceDate); final Timetable newTimetableScheduled = snapshot.resolve(newTripPattern, null); assertNotSame(newTimetableForToday, newTimetableScheduled); final int newTimetableForTodayModifiedTripIndex = newTimetableForToday.getTripIndex(modifiedTripId); assertTrue("New trip should be found in time table for service date", newTimetableForTodayModifiedTripIndex > -1); assertEquals(RealTimeState.MODIFIED, newTimetableForToday.getTripTimes(newTimetableForTodayModifiedTripIndex).getRealTimeState()); assertEquals("New trip should not be found in scheduled time table", -1, newTimetableScheduled.getTripIndex(modifiedTripId)); } } @Test public void testPurgeExpiredData() throws InvalidProtocolBufferException { final AgencyAndId tripId = new AgencyAndId(feedId, "1.1"); final ServiceDate previously = serviceDate.previous().previous(); // Just to be safe... final Trip trip = graph.index.tripForId.get(tripId); final TripPattern pattern = graph.index.patternForTrip.get(trip); updater.maxSnapshotFrequency = (0); updater.purgeExpiredData = (false); updater.applyTripUpdates(graph, fullDataset, Arrays.asList(TripUpdate.parseFrom(cancellation)), feedId); final TimetableSnapshot snapshotA = updater.getTimetableSnapshot(); updater.purgeExpiredData = (true); final TripDescriptor.Builder tripDescriptorBuilder = TripDescriptor.newBuilder(); tripDescriptorBuilder.setTripId("1.1"); tripDescriptorBuilder.setScheduleRelationship(TripDescriptor.ScheduleRelationship.CANCELED); tripDescriptorBuilder.setStartDate(previously.getAsString()); final TripUpdate.Builder tripUpdateBuilder = TripUpdate.newBuilder(); tripUpdateBuilder.setTrip(tripDescriptorBuilder); final TripUpdate tripUpdate = tripUpdateBuilder.build(); updater.applyTripUpdates(graph, fullDataset, Arrays.asList(tripUpdate), feedId); final TimetableSnapshot snapshotB = updater.getTimetableSnapshot(); assertNotSame(snapshotA, snapshotB); assertSame (snapshotA.resolve(pattern, null ), snapshotB.resolve(pattern, null )); assertSame (snapshotA.resolve(pattern, serviceDate), snapshotB.resolve(pattern, serviceDate)); assertNotSame(snapshotA.resolve(pattern, null ), snapshotA.resolve(pattern, serviceDate)); assertSame (snapshotB.resolve(pattern, null ), snapshotB.resolve(pattern, previously)); } }