diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java index 60cfb72ef7d..288f429a861 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayer.java @@ -22,6 +22,8 @@ public class TransitLayer { /** * Transit data required for routing, indexed by each local date(Graph TimeZone) it runs through. * A Trip "runs through" a date if any of its arrivals or departures is happening on that date. + * The same trip pattern can therefore have multiple running dates and trip pattern is not + * required to "run" on its service date. */ private final HashMap> tripPatternsRunningOnDate; @@ -94,7 +96,12 @@ public StopLocation getStopByIndex(int stop) { return stop == -1 ? null : this.stopModel.stopByIndex(stop); } - public Collection getTripPatternsForDate(LocalDate date) { + /** + * Returns trip patterns for the given running date. Running date is not necessarily the same + * as the service date. A Trip "runs through" a date if any of its arrivals or departures is + * happening on that date. Trip pattern can have multiple running dates. + */ + public Collection getTripPatternsForRunningDate(LocalDate date) { return tripPatternsRunningOnDate.getOrDefault(date, List.of()); } @@ -112,16 +119,29 @@ public int getStopCount() { return stopModel.stopIndexSize(); } + /** + * Returns a copy of the list of trip patterns for the given running date. Running date is not + * necessarily the same as the service date. A Trip "runs through" a date if any of its arrivals + * or departures is happening on that date. Trip pattern can have multiple running dates. + */ public List getTripPatternsRunningOnDateCopy(LocalDate runningPeriodDate) { List tripPatternForDate = tripPatternsRunningOnDate.get(runningPeriodDate); return tripPatternForDate != null ? new ArrayList<>(tripPatternForDate) : new ArrayList<>(); } - public List getTripPatternsStartingOnDateCopy(LocalDate date) { - List tripPatternsRunningOnDate = getTripPatternsRunningOnDateCopy(date); - return tripPatternsRunningOnDate + /** + * Returns a copy of the list of trip patterns for the given service date. Service date is not + * necessarily the same as any of the trip pattern's running dates. + */ + public List getTripPatternsOnServiceDateCopy(LocalDate date) { + List tripPatternsRunningOnDates = getTripPatternsRunningOnDateCopy(date); + // Trip pattern can run only after midnight. Therefore, we need to get the trip pattern's for + // the next running date as well and filter out duplicates. + tripPatternsRunningOnDates.addAll(getTripPatternsRunningOnDateCopy(date.plusDays(1))); + return tripPatternsRunningOnDates .stream() - .filter(t -> t.getLocalDate().equals(date)) + .filter(t -> t.getServiceDate().equals(date)) + .distinct() .collect(Collectors.toList()); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java index b86314aa42c..1411424dfc6 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java @@ -43,16 +43,16 @@ public class TripPatternForDate implements Comparable { */ private final FrequencyEntry[] frequencies; - /** The date for which the filtering was performed. */ - private final LocalDate localDate; + /** The service date of the trip pattern. */ + private final LocalDate serviceDate; /** - * The date on which the first trip departs. + * The running date on which the first trip departs. Not necessarily the same as the service date. */ private final LocalDate startOfRunningPeriod; /** - * The date on which the last trip arrives. + * The running date on which the last trip arrives. */ private final LocalDate endOfRunningPeriod; @@ -60,19 +60,19 @@ public TripPatternForDate( RoutingTripPattern tripPattern, List tripTimes, List frequencies, - LocalDate localDate + LocalDate serviceDate ) { this.tripPattern = tripPattern; this.tripTimes = tripTimes.toArray(new TripTimes[0]); this.frequencies = frequencies.toArray(new FrequencyEntry[0]); - this.localDate = localDate; + this.serviceDate = serviceDate; // TODO: We expect a pattern only containing trips or frequencies, fix ability to merge if (hasFrequencies()) { this.startOfRunningPeriod = ServiceDateUtils .asDateTime( - localDate, + serviceDate, frequencies .stream() .mapToInt(frequencyEntry -> frequencyEntry.startTime) @@ -84,7 +84,7 @@ public TripPatternForDate( this.endOfRunningPeriod = ServiceDateUtils .asDateTime( - localDate, + serviceDate, frequencies .stream() .mapToInt(frequencyEntry -> frequencyEntry.endTime) @@ -96,11 +96,11 @@ public TripPatternForDate( // These depend on the tripTimes array being sorted var first = tripTimes.get(0); this.startOfRunningPeriod = - ServiceDateUtils.asDateTime(localDate, first.getDepartureTime(0)).toLocalDate(); + ServiceDateUtils.asDateTime(serviceDate, first.getDepartureTime(0)).toLocalDate(); var last = tripTimes.get(tripTimes.size() - 1); this.endOfRunningPeriod = ServiceDateUtils - .asDateTime(localDate, last.getArrivalTime(last.getNumStops() - 1)) + .asDateTime(serviceDate, last.getArrivalTime(last.getNumStops() - 1)) .toLocalDate(); assertValidRunningPeriod(startOfRunningPeriod, endOfRunningPeriod, first, last); } @@ -126,18 +126,31 @@ public TripTimes getTripTimes(int i) { return tripTimes[i]; } - public LocalDate getLocalDate() { - return localDate; + /** + * The service date for which the trip pattern belongs to. Not necessarily the same as the start + * of the running period in cases where the trip pattern only runs after midnight. + */ + public LocalDate getServiceDate() { + return serviceDate; } public int numberOfTripSchedules() { return tripTimes.length; } + /** + * The start of the running period. This is determined by the first departure time for this + * pattern. Not necessarily the same as the service date if the pattern runs after midnight. + */ public LocalDate getStartOfRunningPeriod() { return startOfRunningPeriod; } + /** + * Returns the running dates. A Trip "runs through" a date if any of its arrivals or departures is + * happening on that date. The same trip pattern can therefore have multiple running dates and + * trip pattern is not required to "run" on its service date. + */ public List getRunningPeriodDates() { // Add one day to ensure last day is included return startOfRunningPeriod @@ -151,14 +164,14 @@ public boolean hasFrequencies() { @Override public int compareTo(TripPatternForDate other) { - return localDate.compareTo(other.localDate); + return serviceDate.compareTo(other.serviceDate); } @Override public int hashCode() { return Objects.hash( tripPattern, - localDate, + serviceDate, Arrays.hashCode(tripTimes), Arrays.hashCode(frequencies) ); @@ -176,7 +189,7 @@ public boolean equals(Object o) { return ( tripPattern.equals(that.tripPattern) && - localDate.equals(that.localDate) && + serviceDate.equals(that.serviceDate) && Arrays.equals(tripTimes, that.tripTimes) && Arrays.equals(frequencies, that.frequencies) ); @@ -184,7 +197,9 @@ public boolean equals(Object o) { @Override public String toString() { - return "TripPatternForDate{" + "tripPattern=" + tripPattern + ", localDate=" + localDate + '}'; + return ( + "TripPatternForDate{" + "tripPattern=" + tripPattern + ", serviceDate=" + serviceDate + '}' + ); } @Nullable @@ -214,7 +229,7 @@ public TripPatternForDate newWithFilteredTripTimes(Predicate filter) return this; } - return new TripPatternForDate(tripPattern, filteredTripTimes, filteredFrequencies, localDate); + return new TripPatternForDate(tripPattern, filteredTripTimes, filteredFrequencies, serviceDate); } private static void assertValidRunningPeriod( diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyAlightSearch.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyAlightSearch.java index 451f51b2aa5..2f020e22cf5 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyAlightSearch.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyAlightSearch.java @@ -55,7 +55,7 @@ public RaptorBoardOrAlightEvent search( arrivalTime + headway, headway, offset, - pattern.getLocalDate() + pattern.getServiceDate() ); } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyBoardSearch.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyBoardSearch.java index 897ce370a93..ea58e870547 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyBoardSearch.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/frequency/TripFrequencyBoardSearch.java @@ -54,7 +54,7 @@ public RaptorBoardOrAlightEvent search( departureTime - headway, headway, offset, - pattern.getLocalDate() + pattern.getServiceDate() ); } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java index bed27497587..fad5de83de0 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TransitLayerUpdater.java @@ -96,7 +96,7 @@ public void update( if (!tripPatternsStartingOnDateMapCache.containsKey(date)) { Map map = realtimeTransitLayer - .getTripPatternsStartingOnDateCopy(date) + .getTripPatternsOnServiceDateCopy(date) .stream() .collect(Collectors.toMap(t -> t.getTripPattern().getPattern(), t -> t)); tripPatternsStartingOnDateMapCache.put(date, map); @@ -146,7 +146,7 @@ public void update( } else { LOG.debug( "NEW TripPatternForDate: {} - {}", - newTripPatternForDate.getLocalDate(), + newTripPatternForDate.getServiceDate(), newTripPatternForDate.getTripPattern().debugInfo() ); } @@ -179,7 +179,7 @@ public void update( } for (TripPatternForDate tripPatternForDate : previouslyUsedPatterns) { - if (tripPatternForDate.getLocalDate().equals(date)) { + if (tripPatternForDate.getServiceDate().equals(date)) { TripPattern pattern = tripPatternForDate.getTripPattern().getPattern(); if (!pattern.isCreatedByRealtimeUpdater()) { continue; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java index 863a4ca9ae8..f987e4f7a21 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreator.java @@ -120,7 +120,7 @@ static List merge( // Calculate offsets per date int[] offsets = new int[patternsSorted.length]; for (int i = 0; i < patternsSorted.length; i++) { - LocalDate serviceDate = patternsSorted[i].getLocalDate(); + LocalDate serviceDate = patternsSorted[i].getServiceDate(); if (offsetCache.containsKey(serviceDate)) { offsets[i] = offsetCache.get(serviceDate); } else { @@ -185,7 +185,9 @@ private static List filterActiveTripPatterns( filter.tripTimesPredicate(tripTimes, filter.hasSubModeFilters()); Predicate tripTimesWithoutSubmodesPredicate = tripTimes -> filter.tripTimesPredicate(tripTimes, false); - Collection tripPatternsForDate = transitLayer.getTripPatternsForDate(date); + Collection tripPatternsForDate = transitLayer.getTripPatternsForRunningDate( + date + ); List result = new ArrayList<>(tripPatternsForDate.size()); for (TripPatternForDate p : tripPatternsForDate) { if (firstDay || p.getStartOfRunningPeriod().equals(date)) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripScheduleWithOffset.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripScheduleWithOffset.java index 642dbd2d6e5..aca998b24ca 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripScheduleWithOffset.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripScheduleWithOffset.java @@ -124,7 +124,7 @@ private void findTripTimes() { if (index < numSchedules) { this.tripTimes = tripPatternForDate.getTripTimes(index); - this.serviceDate = tripPatternForDate.getLocalDate(); + this.serviceDate = tripPatternForDate.getServiceDate(); this.secondsOffset = pattern.tripPatternForDateOffsets(i); return; } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayerTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayerTest.java new file mode 100644 index 00000000000..7c674252e6a --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TransitLayerTest.java @@ -0,0 +1,201 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.transit; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.network.RoutingTripPattern; +import org.opentripplanner.transit.model.network.StopPattern; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; + +class TransitLayerTest { + + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final TripTimes TRIP_TIMES; + + private static final RoutingTripPattern TRIP_PATTERN; + + static { + var stop = TEST_MODEL.stop("TEST:STOP", 0, 0).build(); + var stopTime = new StopTime(); + stopTime.setStop(stop); + var stopPattern = new StopPattern(List.of(stopTime)); + var route = TransitModelForTest.route("1").build(); + TRIP_PATTERN = + TripPattern + .of(TransitModelForTest.id("P1")) + .withRoute(route) + .withStopPattern(stopPattern) + .build() + .getRoutingTripPattern(); + TRIP_TIMES = + TripTimesFactory.tripTimes( + TransitModelForTest.trip("1").withRoute(route).build(), + List.of(new StopTime()), + new Deduplicator() + ); + } + + @Test + void testGetTripPatternsRunningOnDateCopy() { + var date = LocalDate.of(2024, 1, 1); + + var tripPatternForDate = new TripPatternForDate( + TRIP_PATTERN, + List.of(TRIP_TIMES), + List.of(), + date + ); + var tripPatterns = List.of(tripPatternForDate); + var transitLayer = new TransitLayer( + Map.of(date, tripPatterns), + null, + null, + null, + null, + null, + null, + null, + null + ); + var runningOnDate = transitLayer.getTripPatternsRunningOnDateCopy(date); + assertEquals(1, runningOnDate.size()); + assertEquals(tripPatterns, runningOnDate); + assertFalse(tripPatterns == runningOnDate); + assertEquals(0, transitLayer.getTripPatternsRunningOnDateCopy(date.minusDays(1)).size()); + assertEquals(0, transitLayer.getTripPatternsRunningOnDateCopy(date.plusDays(1)).size()); + } + + @Test + void testGetTripPatternsForRunningDate() { + var date = LocalDate.of(2024, 1, 1); + + var tripPatternForDate = new TripPatternForDate( + TRIP_PATTERN, + List.of(TRIP_TIMES), + List.of(), + date + ); + var tripPatterns = List.of(tripPatternForDate); + var transitLayer = new TransitLayer( + Map.of(date, tripPatterns), + null, + null, + null, + null, + null, + null, + null, + null + ); + var runningOnDate = transitLayer.getTripPatternsForRunningDate(date); + assertEquals(1, runningOnDate.size()); + assertEquals(tripPatterns, runningOnDate); + assertTrue(tripPatterns == runningOnDate); + assertEquals(0, transitLayer.getTripPatternsForRunningDate(date.minusDays(1)).size()); + assertEquals(0, transitLayer.getTripPatternsForRunningDate(date.plusDays(1)).size()); + } + + @Test + void testGetTripPatternsOnServiceDateCopyWithSameRunningAndServiceDate() { + var date = LocalDate.of(2024, 1, 1); + + var tripPatternForDate = new TripPatternForDate( + TRIP_PATTERN, + List.of(TRIP_TIMES), + List.of(), + date + ); + var transitLayer = new TransitLayer( + Map.of(date, List.of(tripPatternForDate)), + null, + null, + null, + null, + null, + null, + null, + null + ); + var startingOnDate = transitLayer.getTripPatternsOnServiceDateCopy(date); + assertEquals(1, startingOnDate.size()); + assertEquals(tripPatternForDate, startingOnDate.getFirst()); + assertEquals(0, transitLayer.getTripPatternsOnServiceDateCopy(date.minusDays(1)).size()); + assertEquals(0, transitLayer.getTripPatternsOnServiceDateCopy(date.plusDays(1)).size()); + } + + @Test + void testGetTripPatternsOnServiceDateCopyWithServiceRunningAfterMidnight() { + var runningDate = LocalDate.of(2024, 1, 1); + var serviceDate = runningDate.minusDays(1); + + var tripPatternForDate = new TripPatternForDate( + TRIP_PATTERN, + List.of(TRIP_TIMES), + List.of(), + serviceDate + ); + var transitLayer = new TransitLayer( + Map.of(runningDate, List.of(tripPatternForDate)), + null, + null, + null, + null, + null, + null, + null, + null + ); + var startingOnDate = transitLayer.getTripPatternsOnServiceDateCopy(serviceDate); + // starting date should be determined by service date, not running date which refers to the + // normal calendar date that the trip pattern is running on + assertEquals(1, startingOnDate.size()); + assertEquals(tripPatternForDate, startingOnDate.getFirst()); + assertEquals(0, transitLayer.getTripPatternsOnServiceDateCopy(runningDate).size()); + } + + @Test + void testGetTripPatternsOnServiceDateCopyWithServiceRunningBeforeAndAfterMidnight() { + // This is same as the service date + var firstRunningDate = LocalDate.of(2024, 1, 1); + var secondRunningDate = firstRunningDate.plusDays(1); + + var tripPatternForDate = new TripPatternForDate( + TRIP_PATTERN, + List.of(TRIP_TIMES), + List.of(), + firstRunningDate + ); + var transitLayer = new TransitLayer( + Map.ofEntries( + entry(firstRunningDate, List.of(tripPatternForDate)), + entry(secondRunningDate, List.of(tripPatternForDate)) + ), + null, + null, + null, + null, + null, + null, + null, + null + ); + var startingOnDate = transitLayer.getTripPatternsOnServiceDateCopy(firstRunningDate); + // Transit layer indexes trip patterns by running date and to get trip patterns for certain + // service date, we need to look up the trip patterns for the next running date as well, but + // we don't want to return duplicates + assertEquals(1, startingOnDate.size()); + assertEquals(tripPatternForDate, startingOnDate.getFirst()); + assertEquals(0, transitLayer.getTripPatternsOnServiceDateCopy(secondRunningDate).size()); + } +} diff --git a/src/test/java/org/opentripplanner/transit/speed_test/support/AssertSpeedTestSetup.java b/src/test/java/org/opentripplanner/transit/speed_test/support/AssertSpeedTestSetup.java index 031df343a9a..4113d5979b1 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/support/AssertSpeedTestSetup.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/support/AssertSpeedTestSetup.java @@ -15,7 +15,7 @@ public static void assertTestDateHasData( ) { int numberOfPatternForTestDate = transitModel .getTransitLayer() - .getTripPatternsForDate(config.testDate) + .getTripPatternsForRunningDate(config.testDate) .size(); if (numberOfPatternForTestDate < 10) {