Skip to content

Commit d931077

Browse files
committed
restore scheduled timetables to TransitLayer when buffer is cleared
1 parent 3a4753e commit d931077

File tree

3 files changed

+192
-2
lines changed

3 files changed

+192
-2
lines changed

application/src/main/java/org/opentripplanner/model/Timetable.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Iterator;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Objects;
2324
import java.util.Optional;
2425
import javax.annotation.Nullable;
2526
import org.opentripplanner.transit.model.framework.DataValidationException;
@@ -483,4 +484,33 @@ private static TripTimes getRepresentativeTripTimes(
483484
return null;
484485
}
485486
}
487+
488+
/**
489+
* Get a copy of the scheduled timetable valid for the specified service date only
490+
*/
491+
public Timetable copyForServiceDate(LocalDate date) {
492+
if (serviceDate != null) {
493+
throw new RuntimeException(
494+
"Can only copy scheduled timetable for a specific date if a date hasn't been specified yet."
495+
);
496+
}
497+
return copyOf().withServiceDate(date).build();
498+
}
499+
500+
@Override
501+
public boolean equals(Object o) {
502+
if (o == null || getClass() != o.getClass()) return false;
503+
Timetable timetable = (Timetable) o;
504+
return (
505+
Objects.equals(pattern, timetable.pattern) &&
506+
Objects.equals(tripTimes, timetable.tripTimes) &&
507+
Objects.equals(frequencyEntries, timetable.frequencyEntries) &&
508+
Objects.equals(serviceDate, timetable.serviceDate)
509+
);
510+
}
511+
512+
@Override
513+
public int hashCode() {
514+
return Objects.hash(pattern, tripTimes, frequencyEntries, serviceDate);
515+
}
486516
}

application/src/main/java/org/opentripplanner/model/TimetableSnapshot.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,39 @@ public boolean isEmpty() {
552552
* @return true if the timetable changed as a result of the call
553553
*/
554554
private boolean clearTimetables(String feedId) {
555-
return timetables.keySet().removeIf(tripPattern -> feedId.equals(tripPattern.getFeedId()));
555+
var dirty = false;
556+
dirtyTimetables.clear();
557+
558+
for (var entry : timetables.entrySet()) {
559+
var pattern = entry.getKey();
560+
561+
if (feedId.equals(pattern.getFeedId())) {
562+
var timetablesForPattern = entry.getValue();
563+
var scheduledTimetable = pattern.getScheduledTimetable();
564+
565+
// remove scheduled timetables from the entry
566+
var updatedTimetables = timetablesForPattern
567+
.stream()
568+
.filter(timetable ->
569+
!timetable.equals(scheduledTimetable.copyForServiceDate(timetable.getServiceDate()))
570+
);
571+
572+
// then restore updated timetables to scheduled timetables
573+
var restoredTimetables = updatedTimetables
574+
.map(timetable -> scheduledTimetable.copyForServiceDate(timetable.getServiceDate()))
575+
.collect(ImmutableSortedSet.toImmutableSortedSet(new SortedTimetableComparator()));
576+
dirty = dirty || !restoredTimetables.isEmpty();
577+
restoredTimetables.forEach(updated ->
578+
dirtyTimetables.put(
579+
new TripPatternAndServiceDate(pattern, updated.getServiceDate()),
580+
updated
581+
)
582+
);
583+
entry.setValue(restoredTimetables);
584+
}
585+
}
586+
587+
return dirty;
556588
}
557589

558590
/**

application/src/test/java/org/opentripplanner/model/TimetableSnapshotTest.java

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.HashMap;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Objects;
2324
import java.util.SortedSet;
2425
import java.util.concurrent.atomic.AtomicBoolean;
2526
import org.junit.jupiter.api.BeforeAll;
@@ -31,7 +32,10 @@
3132
import org.opentripplanner.transit.model.framework.Deduplicator;
3233
import org.opentripplanner.transit.model.framework.FeedScopedId;
3334
import org.opentripplanner.transit.model.framework.Result;
35+
import org.opentripplanner.transit.model.network.StopPattern;
3436
import org.opentripplanner.transit.model.network.TripPattern;
37+
import org.opentripplanner.transit.model.site.RegularStop;
38+
import org.opentripplanner.transit.model.timetable.RealTimeState;
3539
import org.opentripplanner.transit.model.timetable.Trip;
3640
import org.opentripplanner.transit.model.timetable.TripIdAndServiceDate;
3741
import org.opentripplanner.transit.model.timetable.TripOnServiceDate;
@@ -46,7 +50,7 @@ public class TimetableSnapshotTest {
4650
private static final ZoneId timeZone = ZoneIds.GMT;
4751
public static final LocalDate SERVICE_DATE = LocalDate.of(2024, 1, 1);
4852
private static Map<FeedScopedId, TripPattern> patternIndex;
49-
static String feedId;
53+
private static String feedId;
5054

5155
@BeforeAll
5256
public static void setUp() throws Exception {
@@ -412,6 +416,130 @@ void testClear() {
412416
assertNotNull(snapshot.getRealtimeAddedRoute(pattern.getRoute().getId()));
413417
}
414418

419+
/**
420+
* This test checks that the original timetable is given to TransitLayerUpdater for previously
421+
* added patterns after the buffer is cleared.
422+
* <p>
423+
* Refer to bug #6197 for details.
424+
*/
425+
@Test
426+
void testTransitLayerUpdateAfterClear() {
427+
var resolver = new TimetableSnapshot();
428+
429+
// make an updated trip
430+
var pattern = patternIndex.get(new FeedScopedId(feedId, "1.1"));
431+
var trip = pattern.scheduledTripsAsStream().findFirst().orElseThrow();
432+
var scheduledTimetable = pattern.getScheduledTimetable();
433+
var updatedTripTimes = Objects
434+
.requireNonNull(scheduledTimetable.getTripTimes(trip))
435+
.copyScheduledTimes();
436+
for (var i = 0; i < updatedTripTimes.getNumStops(); ++i) {
437+
updatedTripTimes.updateArrivalDelay(i, 30);
438+
updatedTripTimes.updateDepartureDelay(i, 30);
439+
}
440+
updatedTripTimes.setRealTimeState(RealTimeState.UPDATED);
441+
var realTimeTripUpdate = new RealTimeTripUpdate(
442+
pattern,
443+
updatedTripTimes,
444+
SERVICE_DATE,
445+
null,
446+
false,
447+
false
448+
);
449+
450+
var addedDepartureStopTime = new StopTime();
451+
var addedArrivalStopTime = new StopTime();
452+
addedDepartureStopTime.setDepartureTime(0);
453+
addedDepartureStopTime.setArrivalTime(0);
454+
addedDepartureStopTime.setStop(RegularStop.of(new FeedScopedId(feedId, "XX"), () -> 0).build());
455+
addedArrivalStopTime.setDepartureTime(300);
456+
addedArrivalStopTime.setArrivalTime(300);
457+
addedArrivalStopTime.setStop(RegularStop.of(new FeedScopedId(feedId, "YY"), () -> 1).build());
458+
var addedStopTimes = List.of(addedDepartureStopTime, addedArrivalStopTime);
459+
var addedStopPattern = new StopPattern(addedStopTimes);
460+
var route = patternIndex.values().stream().findFirst().orElseThrow().getRoute();
461+
var addedTripPattern = TripPattern
462+
.of(new FeedScopedId(feedId, "1.1"))
463+
.withRoute(route)
464+
.withStopPattern(addedStopPattern)
465+
.withCreatedByRealtimeUpdater(true)
466+
.build();
467+
var addedTripTimes = TripTimesFactory.tripTimes(
468+
Trip.of(new FeedScopedId(feedId, "addedTrip")).withRoute(route).build(),
469+
addedStopTimes,
470+
new Deduplicator()
471+
);
472+
var addedTripUpdate = new RealTimeTripUpdate(
473+
addedTripPattern,
474+
addedTripTimes,
475+
SERVICE_DATE,
476+
null,
477+
true,
478+
false
479+
);
480+
481+
var transitLayerUpdater = new TransitLayerUpdater(null) {
482+
int count = 0;
483+
484+
/**
485+
* Test that the TransitLayerUpdater receives correct updated timetables upon commit
486+
* <p>
487+
* This method is called 3 times.
488+
* When count = 0, the buffer contains one added and one updated trip, and the timetables
489+
* should reflect this fact.
490+
* When count = 1, the buffer is empty, however, this method should still receive the
491+
* timetables of the previous added and updated patterns in order to restore them to the
492+
* initial scheduled timetable.
493+
* When count = 2, the buffer is still empty, and no changes should be made.
494+
*/
495+
@Override
496+
public void update(
497+
Collection<Timetable> updatedTimetables,
498+
Map<TripPattern, SortedSet<Timetable>> timetables
499+
) {
500+
assertThat(updatedTimetables).hasSize(count == 2 ? 0 : 2);
501+
502+
updatedTimetables.forEach(timetable -> {
503+
var timetablePattern = timetable.getPattern();
504+
assertEquals(SERVICE_DATE, timetable.getServiceDate());
505+
if (timetablePattern == pattern) {
506+
if (count == 1) {
507+
// the timetable for the existing pattern should revert to the original
508+
assertEquals(scheduledTimetable.getTripTimes(), timetable.getTripTimes());
509+
} else {
510+
// the timetable for the existing pattern should contain the modified times
511+
assertEquals(
512+
RealTimeState.UPDATED,
513+
Objects.requireNonNull(timetable.getTripTimes(trip)).getRealTimeState()
514+
);
515+
}
516+
} else if (timetablePattern == addedTripPattern) {
517+
if (count == 1) {
518+
// the timetable for the added pattern should be empty after clearing
519+
assertThat(timetable.getTripTimes()).isEmpty();
520+
} else {
521+
// the timetable for the added pattern should contain the times for 1 added trip
522+
assertThat(timetable.getTripTimes()).hasSize(1);
523+
}
524+
} else {
525+
throw new RuntimeException("unknown pattern passed to transit layer updater");
526+
}
527+
});
528+
++count;
529+
}
530+
};
531+
532+
resolver.update(realTimeTripUpdate);
533+
resolver.update(addedTripUpdate);
534+
resolver.commit(transitLayerUpdater, true);
535+
536+
resolver.clear(feedId);
537+
resolver.commit(transitLayerUpdater, true);
538+
539+
resolver.clear(feedId);
540+
resolver.commit(transitLayerUpdater, true);
541+
}
542+
415543
private static TimetableSnapshot createCommittedSnapshot() {
416544
TimetableSnapshot timetableSnapshot = new TimetableSnapshot();
417545
return timetableSnapshot.commit(null, true);

0 commit comments

Comments
 (0)