Skip to content

Commit 8032df7

Browse files
authored
Merge pull request opentripplanner#7346 from entur/add-triptimes-index-to-timetable
Add FeedScopedId-keyed index to Timetable for O(1) TripTimes lookup
2 parents ac0610d + d21da39 commit 8032df7

3 files changed

Lines changed: 88 additions & 33 deletions

File tree

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.List;
99
import java.util.Map;
1010
import java.util.Optional;
11+
import java.util.stream.Collectors;
1112
import javax.annotation.Nullable;
1213
import org.opentripplanner.core.model.id.FeedScopedId;
1314
import org.opentripplanner.transit.model.network.TripPattern;
@@ -25,6 +26,8 @@ public class Timetable implements Serializable {
2526

2627
private List<TripTimes> tripTimes;
2728

29+
private Map<FeedScopedId, TripTimes> tripTimesIndex;
30+
2831
private List<FrequencyEntry> frequencyEntries;
2932

3033
@Nullable
@@ -36,6 +39,7 @@ public class Timetable implements Serializable {
3639
this.pattern = timetableBuilder.getPattern();
3740
this.serviceDate = timetableBuilder.getServiceDate();
3841
this.tripTimes = timetableBuilder.createImmutableOrderedListOfTripTimes();
42+
this.tripTimesIndex = timetableBuilder.createImmutableTripTimesIndex();
3943
this.frequencyEntries = List.copyOf(timetableBuilder.getFrequencies());
4044
this.maxTripSpanDays = computeMaxTripSpanDays(this.tripTimes);
4145
}
@@ -54,21 +58,12 @@ public TimetableBuilder copyOf() {
5458

5559
@Nullable
5660
public TripTimes getTripTimes(Trip trip) {
57-
for (TripTimes tt : tripTimes) {
58-
if (tt.getTrip() == trip) {
59-
return tt;
60-
}
61-
}
62-
return null;
61+
return getTripTimes(trip.getId());
6362
}
6463

64+
@Nullable
6565
public TripTimes getTripTimes(FeedScopedId tripId) {
66-
for (TripTimes tt : tripTimes) {
67-
if (tt.getTrip().getId().equals(tripId)) {
68-
return tt;
69-
}
70-
}
71-
return null;
66+
return tripTimesIndex.get(tripId);
7267
}
7368

7469
public boolean isValidFor(LocalDate serviceDate) {
@@ -82,6 +77,9 @@ public void setServiceCodes(Map<FeedScopedId, Integer> serviceCodes) {
8277
.stream()
8378
.map(tt -> tt.withServiceCode(serviceCodes.get(tt.getTrip().getServiceId())))
8479
.toList();
80+
tripTimesIndex = tripTimes
81+
.stream()
82+
.collect(Collectors.toUnmodifiableMap(tt -> tt.getTrip().getId(), tt -> tt));
8583
// Repeated code... bad sign...
8684
frequencyEntries = frequencyEntries
8785
.stream()

application/src/main/java/org/opentripplanner/transit/model/timetable/TimetableBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ List<TripTimes> createImmutableOrderedListOfTripTimes() {
111111
return tripTimes.values().stream().sorted().toList();
112112
}
113113

114+
Map<FeedScopedId, TripTimes> createImmutableTripTimesIndex() {
115+
return Map.copyOf(tripTimes);
116+
}
117+
114118
TripPattern getPattern() {
115119
return pattern;
116120
}
Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
package org.opentripplanner.transit.model.timetable;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.junit.jupiter.api.Assertions.assertSame;
46
import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
57

8+
import org.junit.jupiter.api.Test;
69
import org.junit.jupiter.params.ParameterizedTest;
710
import org.junit.jupiter.params.provider.CsvSource;
8-
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
9-
import org.opentripplanner.transit.model.network.Route;
10-
import org.opentripplanner.transit.model.network.TripPattern;
11+
import org.opentripplanner.transit.model._data.TransitTestEnvironment;
12+
import org.opentripplanner.transit.model._data.TransitTestEnvironmentBuilder;
13+
import org.opentripplanner.transit.model._data.TripInput;
1114
import org.opentripplanner.transit.model.site.RegularStop;
1215

1316
class TimetableTest {
1417

15-
private static final TimetableRepositoryForTest TEST_MODEL = TimetableRepositoryForTest.of();
16-
17-
private static final Route ROUTE = TimetableRepositoryForTest.route("routeId").build();
18-
public static final RegularStop STOP_A = TEST_MODEL.stop("A").build();
19-
public static final RegularStop STOP_C = TEST_MODEL.stop("C").build();
18+
private final TransitTestEnvironmentBuilder envBuilder = TransitTestEnvironment.of();
19+
private final RegularStop stopA = envBuilder.stop("A");
20+
private final RegularStop stopC = envBuilder.stop("C");
2021

2122
@ParameterizedTest
2223
@CsvSource(
@@ -32,20 +33,72 @@ class TimetableTest {
3233
useHeadersInDisplayName = true
3334
)
3435
void maxTripSpanDays(String testCaseName, String schedule, int expectedNumberOfDays) {
35-
var timetable = TripPattern.of(id(testCaseName))
36-
.withRoute(ROUTE)
37-
.withStopPattern(TimetableRepositoryForTest.stopPattern(STOP_A, STOP_C))
38-
.withScheduledTimeTableBuilder(builder ->
39-
builder.addTripTimes(
40-
ScheduledTripTimes.of()
41-
.withTrip(TimetableRepositoryForTest.trip("t1").build())
42-
.withDepartureTimes(schedule)
43-
.build()
44-
)
45-
)
46-
.build()
47-
.getScheduledTimetable();
36+
var times = schedule.trim().split("\\s+");
37+
var env = envBuilder
38+
.addTrip(TripInput.of("t1").addStop(stopA, times[0]).addStop(stopC, times[1]))
39+
.build();
4840

41+
var timetable = env.tripData("t1").tripPattern().getScheduledTimetable();
4942
assertEquals(expectedNumberOfDays, timetable.getMaxTripSpanDays());
5043
}
44+
45+
@Test
46+
void getTripTimesByTrip() {
47+
var env = envBuilder
48+
.addTrip(TripInput.of("trip1").addStop(stopA, "08:00").addStop(stopC, "09:00"))
49+
.addTrip(TripInput.of("trip2").addStop(stopA, "10:00").addStop(stopC, "11:00"))
50+
.build();
51+
52+
var timetable = env.tripData("trip1").tripPattern().getScheduledTimetable();
53+
var trip1 = env.tripData("trip1").trip();
54+
var trip2 = env.tripData("trip2").trip();
55+
56+
assertSame(timetable.getTripTimes(trip1).getTrip(), trip1);
57+
assertSame(timetable.getTripTimes(trip2).getTrip(), trip2);
58+
}
59+
60+
@Test
61+
void getTripTimesByFeedScopedId() {
62+
var env = envBuilder
63+
.addTrip(TripInput.of("trip1").addStop(stopA, "08:00").addStop(stopC, "09:00"))
64+
.addTrip(TripInput.of("trip2").addStop(stopA, "10:00").addStop(stopC, "11:00"))
65+
.build();
66+
67+
var timetable = env.tripData("trip1").tripPattern().getScheduledTimetable();
68+
var trip1 = env.tripData("trip1").trip();
69+
var trip2 = env.tripData("trip2").trip();
70+
71+
assertSame(timetable.getTripTimes(trip1.getId()).getTrip(), trip1);
72+
assertSame(timetable.getTripTimes(trip2.getId()).getTrip(), trip2);
73+
}
74+
75+
@Test
76+
void getTripTimesUsesEqualsNotIdentity() {
77+
var env = envBuilder
78+
.addTrip(TripInput.of("sameId").addStop(stopA, "08:00").addStop(stopC, "09:00"))
79+
.build();
80+
81+
var timetable = env.tripData("sameId").tripPattern().getScheduledTimetable();
82+
var trip1 = env.tripData("sameId").trip();
83+
84+
// Create a different Trip instance with the same ID
85+
var trip2 = Trip.of(id("sameId")).withRoute(envBuilder.route("lookupRoute")).build();
86+
assertEquals(trip1.getId(), trip2.getId());
87+
88+
// Lookup using the second instance should find the TripTimes stored with the first
89+
assertSame(trip1, timetable.getTripTimes(trip2).getTrip());
90+
}
91+
92+
@Test
93+
void getTripTimesReturnsNullForUnknownTrip() {
94+
var env = envBuilder
95+
.addTrip(TripInput.of("trip1").addStop(stopA, "08:00").addStop(stopC, "09:00"))
96+
.build();
97+
98+
var timetable = env.tripData("trip1").tripPattern().getScheduledTimetable();
99+
100+
var unknownTrip = Trip.of(id("unknown")).withRoute(envBuilder.route("lookupRoute2")).build();
101+
assertNull(timetable.getTripTimes(unknownTrip));
102+
assertNull(timetable.getTripTimes(id("unknown")));
103+
}
51104
}

0 commit comments

Comments
 (0)