diff --git a/src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsTest.java b/src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsTest.java index 043a91038d0..82058567c38 100644 --- a/src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsTest.java @@ -26,7 +26,7 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; class EmissionsTest { @@ -130,7 +130,7 @@ private ScheduledTransitLeg createTransitLeg(Route route) { .withRoute(route) .build(); return new ScheduledTransitLegBuilder<>() - .withTripTimes(new TripTimes(trip, stopTimes, new Deduplicator())) + .withTripTimes(TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator())) .withTripPattern(pattern) .withBoardStopIndexInPattern(0) .withAlightStopIndexInPattern(2) diff --git a/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java b/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java index 38e60335fc8..43be9c6d735 100644 --- a/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/realtimeresolver/RealtimeResolverTest.java @@ -137,7 +137,7 @@ private static TripPattern delay(TripPattern pattern1, int seconds) { } private static TripTimes delay(TripTimes tt, int seconds) { - var delayed = new TripTimes(tt); + var delayed = tt.copyOfScheduledTimes(); IntStream .range(0, delayed.getNumStops()) .forEach(i -> { diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/ModifiedTripBuilderTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/ModifiedTripBuilderTest.java index a9e9ad73c53..2bba22b3283 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/ModifiedTripBuilderTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/ModifiedTripBuilderTest.java @@ -29,6 +29,7 @@ import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; @@ -111,7 +112,7 @@ class ModifiedTripBuilderTest { STOP_TIME_C_1.setStopSequence(1); } - private static final TripTimes TRIP_TIMES = new TripTimes( + private static final TripTimes TRIP_TIMES = TripTimesFactory.tripTimes( TRIP, List.of(STOP_TIME_A_1, STOP_TIME_B_1, STOP_TIME_C_1), DEDUPLICATOR diff --git a/src/ext-test/java/org/opentripplanner/ext/siri/TimetableHelperTest.java b/src/ext-test/java/org/opentripplanner/ext/siri/TimetableHelperTest.java index 2c8e25e7308..97bd3be2398 100644 --- a/src/ext-test/java/org/opentripplanner/ext/siri/TimetableHelperTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/siri/TimetableHelperTest.java @@ -21,6 +21,7 @@ import org.opentripplanner.transit.model.timetable.OccupancyStatus; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.StopModel; import uk.org.siri.siri20.OccupancyEnumeration; @@ -74,7 +75,7 @@ public void setUp() { .build(); Trip trip = Trip.of(new FeedScopedId(FEED_ID, "TRIP_ID")).withRoute(route).build(); - tripTimes = new TripTimes(trip, List.of(stopTime), new Deduplicator()); + tripTimes = TripTimesFactory.tripTimes(trip, List.of(stopTime), new Deduplicator()); } @Test diff --git a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java index 23d7cbb1705..2d7789422ea 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/siri/AddedTripBuilder.java @@ -29,6 +29,7 @@ import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.updater.spi.TripTimesValidationMapper; import org.opentripplanner.updater.spi.UpdateError; @@ -201,10 +202,14 @@ Result build() { .withStopPattern(stopPattern) .build(); - TripTimes tripTimes = new TripTimes(trip, aimedStopTimes, transitModel.getDeduplicator()); + TripTimes tripTimes = TripTimesFactory.tripTimes( + trip, + aimedStopTimes, + transitModel.getDeduplicator() + ); tripTimes.setServiceCode(transitModel.getServiceCodes().get(trip.getServiceId())); pattern.add(tripTimes); - TripTimes updatedTripTimes = new TripTimes(tripTimes); + TripTimes updatedTripTimes = tripTimes.copyOfScheduledTimes(); // Loop through calls again and apply updates for (int stopSequence = 0; stopSequence < calls.size(); stopSequence++) { diff --git a/src/ext/java/org/opentripplanner/ext/siri/ModifiedTripBuilder.java b/src/ext/java/org/opentripplanner/ext/siri/ModifiedTripBuilder.java index c7d6d9b3373..f56d3c6a75f 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/ModifiedTripBuilder.java +++ b/src/ext/java/org/opentripplanner/ext/siri/ModifiedTripBuilder.java @@ -94,7 +94,7 @@ public ModifiedTripBuilder( * in form the SIRI-ET update. */ public Result build() { - TripTimes newTimes = new TripTimes(existingTripTimes); + TripTimes newTimes = existingTripTimes.copyOfScheduledTimes(); StopPattern stopPattern = createStopPattern(pattern, calls, entityResolver); diff --git a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java index 3e6b4a62fd4..0cedc491f79 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java +++ b/src/ext/java/org/opentripplanner/ext/siri/SiriTimetableSnapshotSource.java @@ -395,7 +395,7 @@ private boolean markScheduledTripAsDeleted(Trip trip, final LocalDate serviceDat if (tripTimes == null) { LOG.warn("Could not mark scheduled trip as deleted {}", trip.getId()); } else { - final TripTimes newTripTimes = new TripTimes(tripTimes); + final TripTimes newTripTimes = tripTimes.copyOfScheduledTimes(); newTripTimes.deleteTrip(); buffer.update(pattern, newTripTimes, serviceDate); success = true; diff --git a/src/main/java/org/opentripplanner/framework/lang/IntUtils.java b/src/main/java/org/opentripplanner/framework/lang/IntUtils.java index 01040bf0f93..04eeff4695e 100644 --- a/src/main/java/org/opentripplanner/framework/lang/IntUtils.java +++ b/src/main/java/org/opentripplanner/framework/lang/IntUtils.java @@ -59,9 +59,9 @@ public static int[] intArray(int size, int initialValue) { * Copy int array and shift all values by the given {@code offset}. */ public static int[] shiftArray(int offset, int[] array) { - int[] a = new int[array.length]; - for (int i = 0; i < array.length; i++) { - a[i] = array[i] + offset; + int[] a = Arrays.copyOf(array, array.length); + for (int i = 0; i < array.length; ++i) { + a[i] += offset; } return a; } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java b/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java index 59766ac092b..eb3674bf76a 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/TimeZoneAdjusterModule.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; import org.opentripplanner.graph_builder.model.GraphBuilderModule; -import org.opentripplanner.model.Timetable; import org.opentripplanner.transit.service.TransitModel; /** @@ -46,13 +45,9 @@ public void buildGraph() { return; } - final Timetable scheduledTimetable = pattern.getScheduledTimetable(); - - scheduledTimetable.getTripTimes().forEach(tripTimes -> tripTimes.timeShift(timeShift)); - - scheduledTimetable - .getFrequencyEntries() - .forEach(frequencyEntry -> frequencyEntry.tripTimes.timeShift(timeShift)); + pattern + .getScheduledTimetable() + .updateAllTripTimes(it -> it.adjustTimesToGraphTimeZone(timeShift)); }); } } diff --git a/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java b/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java index 5e09915b43a..1cb3c4c0186 100644 --- a/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java +++ b/src/main/java/org/opentripplanner/gtfs/GenerateTripPatternsOperation.java @@ -25,6 +25,7 @@ import org.opentripplanner.transit.model.timetable.FrequencyEntry; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -132,7 +133,7 @@ private void buildTripPatternForTrip(Trip trip) { TripPattern tripPattern = findOrCreateTripPattern(stopPattern, trip, direction); // Create a TripTimes object for this list of stoptimes, which form one trip. - TripTimes tripTimes = new TripTimes(trip, stopTimes, deduplicator); + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, deduplicator); // If this trip is referenced by one or more lines in frequencies.txt, wrap it in a FrequencyEntry. List frequencies = frequenciesForTrip.get(trip); diff --git a/src/main/java/org/opentripplanner/model/Timetable.java b/src/main/java/org/opentripplanner/model/Timetable.java index 2496fa11a41..170ed49092d 100644 --- a/src/main/java/org/opentripplanner/model/Timetable.java +++ b/src/main/java/org/opentripplanner/model/Timetable.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.UnaryOperator; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.opentripplanner.framework.time.ServiceDateUtils; @@ -202,7 +203,7 @@ public Result createUpdatedTripTimesFromGTFSRT( LOG.trace("tripId {} found at index {} in timetable.", tripId, tripIndex); } - TripTimes newTimes = new TripTimes(getTripTimes(tripIndex)); + TripTimes newTimes = getTripTimes(tripIndex).copyOfScheduledTimes(); List skippedStopIndices = new ArrayList<>(); // The GTFS-RT reference specifies that StopTimeUpdates are sorted by stop_sequence. @@ -396,6 +397,25 @@ public void addTripTimes(TripTimes tt) { tripTimes.add(tt); } + /** + * Apply the same update to all trip-times inculuding scheduled and frequency based + * trip times. + *

+ * THIS IS NOT THREAD-SAFE - ONLY USE THIS METHOD DURING GRAPH-BUILD! + */ + public void updateAllTripTimes(UnaryOperator update) { + tripTimes.replaceAll(update); + frequencyEntries.replaceAll(it -> + new FrequencyEntry( + it.startTime, + it.endTime, + it.headway, + it.exactTimes, + update.apply(it.tripTimes) + ) + ); + } + /** * Add a frequency entry to this Timetable. See addTripTimes method. Maybe Frequency Entries * should just be TripTimes for simplicity. diff --git a/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java b/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java index 0133e0fdda2..675b9eab5c9 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/TripPatternMapper.java @@ -30,6 +30,7 @@ import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.rutebanken.netex.model.DatedServiceJourney; import org.rutebanken.netex.model.DatedServiceJourneyRefStructure; import org.rutebanken.netex.model.DestinationDisplay; @@ -366,7 +367,11 @@ private void createTripTimes(List trips, TripPattern tripPattern) { trip.getId() ); } else { - TripTimes tripTimes = new TripTimes(trip, result.tripStopTimes.get(trip), deduplicator); + TripTimes tripTimes = TripTimesFactory.tripTimes( + trip, + result.tripStopTimes.get(trip), + deduplicator + ); tripPattern.add(tripTimes); } } diff --git a/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java b/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java index b223ac7412f..587ad2871cd 100644 --- a/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java +++ b/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java @@ -20,7 +20,7 @@ * Does the same thing as String.intern, but for several different types. Java's String.intern uses * perm gen space and is broken anyway. */ -public class Deduplicator implements Serializable { +public class Deduplicator implements DeduplicatorService, Serializable { private static final String ZERO_COUNT = sizeAndCount(0, 0); @@ -50,6 +50,7 @@ public void reset() { canonicalLists.clear(); } + @Override @Nullable public BitSet deduplicateBitSet(BitSet original) { if (original == null) { @@ -64,7 +65,7 @@ public BitSet deduplicateBitSet(BitSet original) { return canonical; } - /** Used to deduplicate time and stop sequence arrays. The same times may occur in many trips. */ + @Override @Nullable public int[] deduplicateIntArray(int[] original) { if (original == null) { @@ -80,6 +81,7 @@ public int[] deduplicateIntArray(int[] original) { return canonical.array; } + @Override @Nullable public String deduplicateString(String original) { if (original == null) { @@ -90,6 +92,7 @@ public String deduplicateString(String original) { return canonical == null ? original : canonical; } + @Override @Nullable public String[] deduplicateStringArray(String[] original) { if (original == null) { @@ -104,10 +107,7 @@ public String[] deduplicateStringArray(String[] original) { return canonical.array; } - /** - * Used to deduplicate list of via places for stop destinationDisplay. Stops may have the same via - * arrays. - */ + @Override @Nullable public String[][] deduplicateString2DArray(String[][] original) { if (original == null) { @@ -122,6 +122,7 @@ public String[][] deduplicateString2DArray(String[][] original) { return canonical.array; } + @Override @SuppressWarnings("unchecked") @Nullable public T deduplicateObject(Class cl, T original) { @@ -137,6 +138,7 @@ public T deduplicateObject(Class cl, T original) { return canonical == null ? original : canonical; } + @Override @Nullable public T[] deduplicateObjectArray(Class type, T[] original) { if (original == null) { @@ -160,6 +162,7 @@ public T[] deduplicateObjectArray(Class type, T[] original) { return canonical.array(); } + @Override @Nullable public List deduplicateImmutableList(Class clazz, List original) { if (original == null) { diff --git a/src/main/java/org/opentripplanner/transit/model/framework/DeduplicatorNoop.java b/src/main/java/org/opentripplanner/transit/model/framework/DeduplicatorNoop.java new file mode 100644 index 00000000000..568772c29fc --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/framework/DeduplicatorNoop.java @@ -0,0 +1,56 @@ +package org.opentripplanner.transit.model.framework; + +import java.util.BitSet; +import java.util.List; +import javax.annotation.Nullable; + +class DeduplicatorNoop implements DeduplicatorService { + + @Nullable + @Override + public BitSet deduplicateBitSet(BitSet original) { + return original; + } + + @Nullable + @Override + public int[] deduplicateIntArray(int[] original) { + return original; + } + + @Nullable + @Override + public String deduplicateString(String original) { + return original; + } + + @Nullable + @Override + public String[] deduplicateStringArray(String[] original) { + return original; + } + + @Nullable + @Override + public String[][] deduplicateString2DArray(String[][] original) { + return original; + } + + @Nullable + @Override + public T deduplicateObject(Class cl, T original) { + return original; + } + + @Nullable + @Override + public T[] deduplicateObjectArray(Class type, T[] original) { + return original; + } + + @Nullable + @Override + public List deduplicateImmutableList(Class clazz, List original) { + return original; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/framework/DeduplicatorService.java b/src/main/java/org/opentripplanner/transit/model/framework/DeduplicatorService.java new file mode 100644 index 00000000000..d8c4a9ceb82 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/framework/DeduplicatorService.java @@ -0,0 +1,44 @@ +package org.opentripplanner.transit.model.framework; + +import java.util.BitSet; +import java.util.List; +import javax.annotation.Nullable; + +/** + * The deduplication service is used to reduce memory consumption by returning the + * same instance if to value-objects are the same. The value-objects must implement + * hashCode and equals. + *

+ * This is also used with arrays of primitive types. Arrays are mutable, so be sure they are + * well protected and do not change if you deduplicate them. + *

+ * Note! The deduplicator should ONLY be used with immutable types and well protected + * fields - guaranteed not to be changed. + */ +public interface DeduplicatorService { + DeduplicatorService NOOP = new DeduplicatorNoop(); + + @Nullable + BitSet deduplicateBitSet(BitSet original); + + @Nullable + int[] deduplicateIntArray(int[] original); + + @Nullable + String deduplicateString(String original); + + @Nullable + String[] deduplicateStringArray(String[] original); + + @Nullable + String[][] deduplicateString2DArray(String[][] original); + + @Nullable + T deduplicateObject(Class cl, T original); + + @Nullable + T[] deduplicateObjectArray(Class type, T[] original); + + @Nullable + List deduplicateImmutableList(Class clazz, List original); +} diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java new file mode 100644 index 00000000000..c5a5d8f097c --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java @@ -0,0 +1,346 @@ +package org.opentripplanner.transit.model.timetable; + +import static org.opentripplanner.transit.model.timetable.ValidationError.ErrorCode.NEGATIVE_DWELL_TIME; +import static org.opentripplanner.transit.model.timetable.ValidationError.ErrorCode.NEGATIVE_HOP_TIME; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.function.Supplier; +import javax.annotation.Nullable; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.model.BookingInfo; +import org.opentripplanner.transit.model.basic.Accessibility; +import org.opentripplanner.transit.model.framework.Deduplicator; + +final class ScheduledTripTimes implements Serializable, Comparable { + + /** + * Implementation notes: This allows re-using the same scheduled arrival and departure time + * arrays for many ScheduledTripTimes. It is also used in materializing frequency-based + * ScheduledTripTimes. + */ + private final int timeShift; + private final int serviceCode; + private final int[] arrivalTimes; + private final int[] departureTimes; + private final BitSet timepoints; + private final Trip trip; + private final List dropOffBookingInfos; + private final List pickupBookingInfos; + + @Nullable + private final I18NString[] headsigns; + + /** + * Implementation notes: This is 2D array since there can be more than one via name/stop per each + * record in stop sequence). Outer array may be null if there are no vias in stop sequence. Inner + * array may be null if there are no vias for particular stop. This is done in order to save + * space. + */ + @Nullable + private final String[][] headsignVias; + + private final int[] originalGtfsStopSequence; + + ScheduledTripTimes(ScheduledTripTimesBuilder builder) { + this.timeShift = builder.timeShift; + this.serviceCode = builder.serviceCode; + this.arrivalTimes = builder.arrivalTimes; + this.departureTimes = builder.departureTimes; + this.timepoints = builder.timepoints; + this.trip = builder.trip; + this.pickupBookingInfos = builder.pickupBookingInfos; + this.dropOffBookingInfos = builder.dropOffBookingInfos; + this.headsigns = builder.headsigns; + this.headsignVias = builder.headsignVias; + this.originalGtfsStopSequence = builder.originalGtfsStopSequence; + } + + public static ScheduledTripTimesBuilder of(@Nullable Deduplicator deduplicator) { + return new ScheduledTripTimesBuilder(deduplicator); + } + + /** + * Create a builder with or without deduplication. + *

+ * Always provide a deduplicator when building the graph. No deduplication is ok when changing + * simple fields like {@code timeShift} and {@code serviceCode} or even the prefered way in a + * unittest. + */ + public ScheduledTripTimesBuilder copyOf(@Nullable Deduplicator deduplicator) { + return new ScheduledTripTimesBuilder( + timeShift, + serviceCode, + arrivalTimes, + departureTimes, + timepoints, + trip, + dropOffBookingInfos, + pickupBookingInfos, + headsigns, + headsignVias, + originalGtfsStopSequence, + deduplicator + ); + } + + /** + * @see #copyOf(Deduplicator) copyOf(null) + */ + public ScheduledTripTimesBuilder copyOfNoDuplication() { + return copyOf(null); + } + + /** The code for the service on which this trip runs. For departure search optimizations. */ + public int getServiceCode() { + return serviceCode; + } + + /** + * The time in seconds after midnight at which the vehicle should arrive at the given stop + * according to the original schedule. + */ + public int getScheduledArrivalTime(final int stop) { + return arrivalTimes[stop] + timeShift; + } + + /** + * The time in seconds after midnight at which the vehicle arrives at each stop, accounting for + * any real-time updates. + */ + public int getArrivalTime(final int stop) { + return getScheduledArrivalTime(stop); + } + + /** @return the difference between the scheduled and actual arrival times at this stop. */ + public int getArrivalDelay(final int stop) { + return getArrivalTime(stop) - (arrivalTimes[stop] + timeShift); + } + + /** + * The time in seconds after midnight at which the vehicle should leave the given stop according + * to the original schedule. + */ + public int getScheduledDepartureTime(final int stop) { + return departureTimes[stop] + timeShift; + } + + /** + * The time in seconds after midnight at which the vehicle leaves each stop, accounting for any + * real-time updates. + */ + public int getDepartureTime(final int stop) { + return getScheduledDepartureTime(stop); + } + + /** @return the difference between the scheduled and actual departure times at this stop. */ + public int getDepartureDelay(final int stop) { + return getDepartureTime(stop) - (departureTimes[stop] + timeShift); + } + + /** + * Whether or not stopIndex is considered a GTFS timepoint. + */ + public boolean isTimepoint(final int stopIndex) { + return timepoints.get(stopIndex); + } + + /** The trips whose arrivals and departures are represented by this class */ + public Trip getTrip() { + return trip; + } + + /** + * Return an integer which can be used to sort TripTimes in order of departure/arrivals. + *

+ * This sorted trip times is used to search for trips. OTP assume one trip do NOT pass another + * trip down the line. + */ + public int sortIndex() { + return getDepartureTime(0); + } + + public BookingInfo getDropOffBookingInfo(int stop) { + return dropOffBookingInfos.get(stop); + } + + public BookingInfo getPickupBookingInfo(int stop) { + return pickupBookingInfos.get(stop); + } + + /** + * Return {@code true} if the trip is unmodified, a scheduled trip from a published timetable. + * Return {@code false} if the trip is an updated, cancelled, or otherwise modified one. This + * method differs from {@link #getRealTimeState()} in that it checks whether real-time + * information is actually available. + */ + public boolean isScheduled() { + return true; + } + + /** + * Return {@code true} if canceled or soft-deleted + */ + public boolean isCanceledOrDeleted() { + return false; + } + + /** + * Return {@code true} if canceled + */ + public boolean isCanceled() { + return false; + } + + /** + * Return true if trip is soft-deleted, and should not be visible to the user + */ + public boolean isDeleted() { + return false; + } + + public RealTimeState getRealTimeState() { + return RealTimeState.SCHEDULED; + } + + /** + * @return the whole trip's headsign. Individual stops can have different headsigns. + */ + public I18NString getTripHeadsign() { + return trip.getHeadsign(); + } + + /** + * Both trip_headsign and stop_headsign (per stop on a particular trip) are optional GTFS fields. + * A trip may not have a headsign, in which case we should fall back on a Timetable or + * Pattern-level headsign. Such a string will be available when we give TripPatterns or + * StopPatterns unique human-readable route variant names, but a ScheduledTripTimes currently + * does not have a pointer to its enclosing timetable or pattern. + */ + @Nullable + public I18NString getHeadsign(final int stop) { + return (headsigns != null && headsigns[stop] != null) + ? headsigns[stop] + : getTrip().getHeadsign(); + } + + /** + * Return list of via names per particular stop. This field provides info about intermediate stops + * between current stop and final trip destination. Mapped from NeTEx DestinationDisplay.vias. No + * GTFS mapping at the moment. + * + * @return Empty list if there are no vias registered for a stop. + */ + public List getHeadsignVias(final int stop) { + if (headsignVias == null || headsignVias[stop] == null) { + return List.of(); + } + return List.of(headsignVias[stop]); + } + + public int getNumStops() { + return arrivalTimes.length; + } + + public Accessibility getWheelchairAccessibility() { + return trip.getWheelchairBoarding(); + } + + /** + * This is only for API-purposes (does not affect routing). + */ + public OccupancyStatus getOccupancyStatus(int stop) { + return OccupancyStatus.NO_DATA_AVAILABLE; + } + + /** + * When creating ScheduledTripTimes or wrapping it in updates, we could potentially imply + * negative running or dwell times. We really don't want those being used in routing. This method + * checks that all times are increasing. + * + * @return empty if times were found to be increasing, the first validation error otherwise + */ + public Optional validateNonIncreasingTimes() { + final int nStops = arrivalTimes.length; + int prevDep = -9_999_999; + for (int s = 0; s < nStops; s++) { + final int arr = getArrivalTime(s); + final int dep = getDepartureTime(s); + + if (dep < arr) { + return Optional.of(new ValidationError(NEGATIVE_DWELL_TIME, s)); + } + if (prevDep > arr) { + return Optional.of(new ValidationError(NEGATIVE_HOP_TIME, s)); + } + prevDep = dep; + } + return Optional.empty(); + } + + /** Sort trips based on first departure time. */ + @Override + public int compareTo(final ScheduledTripTimes other) { + return this.getDepartureTime(0) - other.getDepartureTime(0); + } + + /** + * Returns the GTFS sequence number of the given 0-based stop position. + * + * These are the GTFS stop sequence numbers, which show the order in which the vehicle visits the + * stops. Despite the fact that the StopPattern or TripPattern enclosing this class provides + * an ordered list of Stops, the original stop sequence numbers may still be needed for matching + * with GTFS-RT update messages. Unfortunately, each individual trip can have totally different + * sequence numbers for the same stops, so we need to store them at the individual trip level. An + * effort is made to re-use the sequence number arrays when they are the same across different + * trips in the same pattern. + */ + public int gtfsSequenceOfStopIndex(final int stop) { + return originalGtfsStopSequence[stop]; + } + + /** + * Returns the 0-based stop index of the given GTFS sequence number. + */ + public OptionalInt stopIndexOfGtfsSequence(int stopSequence) { + if (originalGtfsStopSequence == null) { + return OptionalInt.empty(); + } + for (int i = 0; i < originalGtfsStopSequence.length; i++) { + var sequence = originalGtfsStopSequence[i]; + if (sequence == stopSequence) { + return OptionalInt.of(i); + } + } + return OptionalInt.empty(); + } + + @Override + public boolean equals(Object o) { + throw new UnsupportedOperationException("Not implemented, implement if needed!"); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException("Not implemented, implement if needed!"); + } + + /* package local - only visible to timetable classes */ + + int[] copyArrivalTimes() { + return IntUtils.shiftArray(timeShift, arrivalTimes); + } + + int[] copyDepartureTimes() { + return IntUtils.shiftArray(timeShift, departureTimes); + } + + I18NString[] copyHeadsigns(Supplier defaultValue) { + return headsigns == null ? defaultValue.get() : Arrays.copyOf(headsigns, headsigns.length); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimesBuilder.java b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimesBuilder.java new file mode 100644 index 00000000000..d6515321f74 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimesBuilder.java @@ -0,0 +1,129 @@ +package org.opentripplanner.transit.model.timetable; + +import java.util.BitSet; +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.model.BookingInfo; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.framework.DeduplicatorService; + +public class ScheduledTripTimesBuilder { + + private final int NOT_SET = -1; + + int timeShift; + int serviceCode = NOT_SET; + int[] arrivalTimes; + int[] departureTimes; + BitSet timepoints; + Trip trip; + List dropOffBookingInfos; + List pickupBookingInfos; + I18NString[] headsigns; + String[][] headsignVias; + int[] originalGtfsStopSequence; + private final DeduplicatorService deduplicator; + + ScheduledTripTimesBuilder(@Nullable DeduplicatorService deduplicator) { + this.deduplicator = deduplicator == null ? DeduplicatorService.NOOP : deduplicator; + } + + ScheduledTripTimesBuilder( + int timeShift, + int serviceCode, + int[] arrivalTimes, + int[] departureTimes, + BitSet timepoints, + Trip trip, + List dropOffBookingInfos, + List pickupBookingInfos, + I18NString[] headsigns, + String[][] headsignVias, + int[] originalGtfsStopSequence, + Deduplicator deduplicator + ) { + this(deduplicator); + this.timeShift = timeShift; + this.serviceCode = serviceCode; + this.arrivalTimes = arrivalTimes; + this.departureTimes = departureTimes; + this.timepoints = timepoints; + this.trip = trip; + this.dropOffBookingInfos = dropOffBookingInfos; + this.pickupBookingInfos = pickupBookingInfos; + this.headsigns = headsigns; + this.headsignVias = headsignVias; + this.originalGtfsStopSequence = originalGtfsStopSequence; + } + + public ScheduledTripTimesBuilder withTimeShift(int timeShift) { + this.timeShift = timeShift; + return this; + } + + /** + * Add the {@code delta} to the existing timeShift. This is useful when moving a trip + * from one time-zone to another. + */ + public ScheduledTripTimesBuilder plusTimeShift(int delta) { + this.timeShift += delta; + return this; + } + + public ScheduledTripTimesBuilder withServiceCode(int serviceCode) { + this.serviceCode = serviceCode; + return this; + } + + public ScheduledTripTimesBuilder withArrivalTimes(int[] arrivalTimes) { + this.arrivalTimes = deduplicator.deduplicateIntArray(arrivalTimes); + return this; + } + + public ScheduledTripTimesBuilder withDepartureTimes(int[] departureTimes) { + this.departureTimes = deduplicator.deduplicateIntArray(departureTimes); + return this; + } + + public ScheduledTripTimesBuilder withTimepoints(BitSet timepoints) { + this.timepoints = deduplicator.deduplicateBitSet(timepoints); + return this; + } + + public ScheduledTripTimesBuilder withTrip(Trip trip) { + this.trip = trip; + return this; + } + + public ScheduledTripTimesBuilder withDropOffBookingInfos(List dropOffBookingInfos) { + this.dropOffBookingInfos = + deduplicator.deduplicateImmutableList(BookingInfo.class, dropOffBookingInfos); + return this; + } + + public ScheduledTripTimesBuilder withPickupBookingInfos(List pickupBookingInfos) { + this.pickupBookingInfos = + deduplicator.deduplicateImmutableList(BookingInfo.class, pickupBookingInfos); + return this; + } + + public ScheduledTripTimesBuilder withHeadsigns(I18NString[] headsigns) { + this.headsigns = deduplicator.deduplicateObjectArray(I18NString.class, headsigns); + return this; + } + + public ScheduledTripTimesBuilder withHeadsignVias(String[][] headsignVias) { + this.headsignVias = deduplicator.deduplicateString2DArray(headsignVias); + return this; + } + + public ScheduledTripTimesBuilder withOriginalGtfsStopSequence(int[] originalGtfsStopSequence) { + this.originalGtfsStopSequence = deduplicator.deduplicateIntArray(originalGtfsStopSequence); + return this; + } + + public ScheduledTripTimes build() { + return new ScheduledTripTimes(this); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/StopTimeToScheduledTripTimesMapper.java b/src/main/java/org/opentripplanner/transit/model/timetable/StopTimeToScheduledTripTimesMapper.java new file mode 100644 index 00000000000..bf98c6e77b8 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/StopTimeToScheduledTripTimesMapper.java @@ -0,0 +1,138 @@ +package org.opentripplanner.transit.model.timetable; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.List; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.model.BookingInfo; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.transit.model.framework.Deduplicator; + +class StopTimeToScheduledTripTimesMapper { + + private final Trip trip; + private final ScheduledTripTimesBuilder builder; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private StopTimeToScheduledTripTimesMapper(Trip trip, Deduplicator deduplicator) { + this.trip = trip; + this.builder = ScheduledTripTimes.of(deduplicator).withTrip(trip); + } + + /** + * The provided stopTimes are assumed to be pre-filtered, valid, and monotonically increasing. + * The non-interpolated stoptimes should already be marked at timepoints by a previous filtering + * step. + */ + public static ScheduledTripTimes map( + Trip trip, + Collection stopTimes, + Deduplicator deduplicator + ) { + return new StopTimeToScheduledTripTimesMapper(trip, deduplicator).doMap(stopTimes); + } + + private ScheduledTripTimes doMap(Collection stopTimes) { + final int nStops = stopTimes.size(); + final int[] departures = new int[nStops]; + final int[] arrivals = new int[nStops]; + final int[] sequences = new int[nStops]; + final BitSet timepoints = new BitSet(nStops); + // Times are always shifted to zero. This is essential for frequencies and deduplication. + int timeShift = stopTimes.iterator().next().getArrivalTime(); + builder.withTimeShift(timeShift); + + final List dropOffBookingInfos = new ArrayList<>(); + final List pickupBookingInfos = new ArrayList<>(); + int s = 0; + for (final StopTime st : stopTimes) { + departures[s] = st.getDepartureTime() - timeShift; + arrivals[s] = st.getArrivalTime() - timeShift; + sequences[s] = st.getStopSequence(); + timepoints.set(s, st.getTimepoint() == 1); + + dropOffBookingInfos.add(st.getDropOffBookingInfo()); + pickupBookingInfos.add(st.getPickupBookingInfo()); + s++; + } + builder + .withDepartureTimes(departures) + .withArrivalTimes(arrivals) + .withOriginalGtfsStopSequence(sequences) + .withHeadsigns(makeHeadsignsArray(stopTimes)) + .withHeadsignVias(makeHeadsignViasArray(stopTimes)) + .withDropOffBookingInfos(dropOffBookingInfos) + .withPickupBookingInfos(pickupBookingInfos) + .withTimepoints(timepoints); + + return builder.build(); + } + + /** + * @return either an array of headsigns (one for each stop on this trip) or null if the headsign + * is the same at all stops (including null) and can be found in the Trip object. + */ + private I18NString[] makeHeadsignsArray(final Collection stopTimes) { + final I18NString tripHeadsign = trip.getHeadsign(); + boolean useStopHeadsigns = false; + if (tripHeadsign == null) { + useStopHeadsigns = true; + } else { + for (final StopTime st : stopTimes) { + if (!(tripHeadsign.equals(st.getStopHeadsign()))) { + useStopHeadsigns = true; + break; + } + } + } + if (!useStopHeadsigns) { + return null; //defer to trip_headsign + } + boolean allNull = true; + int i = 0; + final I18NString[] hs = new I18NString[stopTimes.size()]; + for (final StopTime st : stopTimes) { + final I18NString headsign = st.getStopHeadsign(); + hs[i++] = headsign; + if (headsign != null) allNull = false; + } + if (allNull) { + return null; + } else { + return hs; + } + } + + /** + * Create 2D String array for via names for each stop in sequence. + * + * @return May be null if no vias are present in stop sequence. + */ + private String[][] makeHeadsignViasArray(final Collection stopTimes) { + if ( + stopTimes + .stream() + .allMatch(st -> st.getHeadsignVias() == null || st.getHeadsignVias().isEmpty()) + ) { + return null; + } + + String[][] vias = new String[stopTimes.size()][]; + + int i = 0; + for (final StopTime st : stopTimes) { + if (st.getHeadsignVias() == null) { + vias[i] = EMPTY_STRING_ARRAY; + i++; + continue; + } + + vias[i] = st.getHeadsignVias().toArray(EMPTY_STRING_ARRAY); + i++; + } + + return vias; + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java index e280300787a..b4ca4850a18 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java @@ -3,23 +3,17 @@ import static org.opentripplanner.transit.model.timetable.ValidationError.ErrorCode.NEGATIVE_DWELL_TIME; import static org.opentripplanner.transit.model.timetable.ValidationError.ErrorCode.NEGATIVE_HOP_TIME; -import jakarta.annotation.Nullable; import java.io.Serializable; import java.time.Duration; -import java.util.ArrayList; import java.util.Arrays; -import java.util.BitSet; -import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.OptionalInt; +import java.util.function.IntUnaryOperator; +import javax.annotation.Nullable; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.model.BookingInfo; -import org.opentripplanner.model.StopTime; import org.opentripplanner.transit.model.basic.Accessibility; -import org.opentripplanner.transit.model.framework.Deduplicator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A TripTimes represents the arrival and departure times for a single trip in an Timetable. It is @@ -27,180 +21,91 @@ * when realtime updates have been applied. All times are expressed as seconds since midnight (as in * GTFS). */ -public class TripTimes implements Serializable, Comparable { +public final class TripTimes implements Serializable, Comparable { + + private ScheduledTripTimes scheduledTripTimes; - private static final Logger LOG = LoggerFactory.getLogger(TripTimes.class); - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - /** The trips whose arrivals and departures are represented by this TripTimes */ - private final Trip trip; - /** - * Both trip_headsign and stop_headsign (per stop on a particular trip) are optional GTFS fields. - * If the headsigns array is null, we will report the trip_headsign (which may also be null) at - * every stop on the trip. If all the stop_headsigns are the same as the trip_headsign we may also - * set the headsigns array to null to save space. Field is private to force use of the getter - * method which does the necessary fallbacks. - */ - private I18NString[] headsigns; - /** - * Contains a list of via names for each stop. This field provides info about intermediate stops - * between current stop and final trip destination. This is 2D array since there can be more than - * one via name/stop per each record in stop sequence). This is mapped from NeTEx - * DestinationDisplay.vias. No GTFS mapping at the moment. Outer array may be null if there are no - * vias in stop sequence. Inner array may be null if there are no vias for particular stop. This - * is done in order to save space. Field is private to force use of the getter method which does - * the necessary fallbacks. - */ - private final String[][] headsignVias; - /** - * The time in seconds after midnight at which the vehicle should arrive at each stop according to - * the original schedule. - */ - private final int[] scheduledArrivalTimes; - /** - * The time in seconds after midnight at which the vehicle should leave each stop according to the - * original schedule. - */ - private final int[] scheduledDepartureTimes; - private final List dropOffBookingInfos; - private final List pickupBookingInfos; - /** - * These are the GTFS stop sequence numbers, which show the order in which the vehicle visits the - * stops. Despite the face that the StopPattern or TripPattern enclosing this TripTimes provides - * an ordered list of Stops, the original stop sequence numbers may still be needed for matching - * with GTFS-RT update messages. Unfortunately, each individual trip can have totally different - * sequence numbers for the same stops, so we need to store them at the individual trip level. An - * effort is made to re-use the sequence number arrays when they are the same across different - * trips in the same pattern. - */ - private final int[] originalGtfsStopSequence; - /** A Set of stop indexes that are marked as timepoints in the GTFS input. */ - private final BitSet timepoints; - /** - * This allows re-using the same scheduled arrival and departure time arrays for many different - * TripTimes. It is also used in materializing frequency-based TripTimes. - */ - private int timeShift; - // not final because these are set later, after TripTimes construction. - private int serviceCode = -1; - /** - * The time in seconds after midnight at which the vehicle arrives at each stop, accounting for - * any real-time updates. Non-final to allow updates. - */ private int[] arrivalTimes; - /** - * The time in seconds after midnight at which the vehicle leaves each stop, accounting for any - * real-time updates. Non-final to allow updates. - */ private int[] departureTimes; - - /** - * States of the stops in the trip. If the state is DEFAULT for a stop, {@link #realTimeState} - * should determine the realtime state of the stop. - *

- * This is only for API-purposes (does not affect routing). Non-final to allow updates. - */ + private RealTimeState realTimeState; private StopRealTimeState[] stopRealTimeStates; - - /** - * This is only for API-purposes (does not affect routing). Non-final to allow updates. - */ + private I18NString[] headsigns; private OccupancyStatus[] occupancyStatus; - - /** - * The real-time state of this TripTimes. - */ - private RealTimeState realTimeState = RealTimeState.SCHEDULED; - private Accessibility wheelchairAccessibility; - /** - * The provided stopTimes are assumed to be pre-filtered, valid, and monotonically increasing. The - * non-interpolated stoptimes should already be marked at timepoints by a previous filtering - * step. - */ - public TripTimes( - final Trip trip, - final Collection stopTimes, - final Deduplicator deduplicator + private TripTimes(final TripTimes original, int timeShiftDelta) { + this( + original, + original.scheduledTripTimes.copyOfNoDuplication().plusTimeShift(timeShiftDelta).build() + ); + } + + TripTimes(ScheduledTripTimes scheduledTripTimes) { + this( + scheduledTripTimes, + scheduledTripTimes.getRealTimeState(), + null, + null, + null, + scheduledTripTimes.getWheelchairAccessibility() + ); + } + + private TripTimes(TripTimes original, ScheduledTripTimes scheduledTripTimes) { + this( + scheduledTripTimes, + original.realTimeState, + original.stopRealTimeStates, + original.headsigns, + original.occupancyStatus, + original.wheelchairAccessibility + ); + } + + private TripTimes( + ScheduledTripTimes scheduledTripTimes, + RealTimeState realTimeState, + StopRealTimeState[] stopRealTimeStates, + I18NString[] headsigns, + OccupancyStatus[] occupancyStatus, + Accessibility wheelchairAccessibility ) { - this.trip = trip; - final int nStops = stopTimes.size(); - final int[] departures = new int[nStops]; - final int[] arrivals = new int[nStops]; - final int[] sequences = new int[nStops]; - final BitSet timepoints = new BitSet(nStops); - // Times are always shifted to zero. This is essential for frequencies and deduplication. - this.timeShift = stopTimes.iterator().next().getArrivalTime(); - final List dropOffBookingInfos = new ArrayList<>(); - final List pickupBookingInfos = new ArrayList<>(); - int s = 0; - for (final StopTime st : stopTimes) { - departures[s] = st.getDepartureTime() - timeShift; - arrivals[s] = st.getArrivalTime() - timeShift; - sequences[s] = st.getStopSequence(); - timepoints.set(s, st.getTimepoint() == 1); - - dropOffBookingInfos.add(st.getDropOffBookingInfo()); - pickupBookingInfos.add(st.getPickupBookingInfo()); - s++; - } - this.scheduledDepartureTimes = deduplicator.deduplicateIntArray(departures); - this.scheduledArrivalTimes = deduplicator.deduplicateIntArray(arrivals); - this.originalGtfsStopSequence = deduplicator.deduplicateIntArray(sequences); - this.headsigns = - deduplicator.deduplicateObjectArray(I18NString.class, makeHeadsignsArray(stopTimes)); - this.headsignVias = deduplicator.deduplicateString2DArray(makeHeadsignViasArray(stopTimes)); - - this.dropOffBookingInfos = - deduplicator.deduplicateImmutableList(BookingInfo.class, dropOffBookingInfos); - this.pickupBookingInfos = - deduplicator.deduplicateImmutableList(BookingInfo.class, pickupBookingInfos); + this.scheduledTripTimes = scheduledTripTimes; + this.realTimeState = realTimeState; + this.stopRealTimeStates = stopRealTimeStates; + this.headsigns = headsigns; + this.occupancyStatus = occupancyStatus; + this.wheelchairAccessibility = wheelchairAccessibility; + // We set these to null to indicate that this is a non-updated/scheduled TripTimes. - // We cannot point to the scheduled times because they are shifted, and updated times are not. + // We cannot point to the scheduled times because we do not want to make an unnecessary copy. this.arrivalTimes = null; this.departureTimes = null; - this.stopRealTimeStates = null; - this.timepoints = deduplicator.deduplicateBitSet(timepoints); - this.wheelchairAccessibility = trip.getWheelchairBoarding(); - LOG.trace("trip {} has timepoint at indexes {}", trip, timepoints); - } - - /** This copy constructor does not copy the actual times, only the scheduled times. */ - public TripTimes(final TripTimes object) { - this.timeShift = object.timeShift; - this.trip = object.trip; - this.serviceCode = object.serviceCode; - this.headsigns = object.headsigns; - this.headsignVias = object.headsignVias; - this.scheduledArrivalTimes = object.scheduledArrivalTimes; - this.scheduledDepartureTimes = object.scheduledDepartureTimes; - this.arrivalTimes = null; - this.departureTimes = null; - this.stopRealTimeStates = object.stopRealTimeStates; - this.pickupBookingInfos = object.pickupBookingInfos; - this.dropOffBookingInfos = object.dropOffBookingInfos; - this.originalGtfsStopSequence = object.originalGtfsStopSequence; - this.realTimeState = object.realTimeState; - this.timepoints = object.timepoints; - this.wheelchairAccessibility = object.wheelchairAccessibility; - this.occupancyStatus = object.occupancyStatus; + } + + public static TripTimes of(ScheduledTripTimes scheduledTripTimes) { + return new TripTimes(scheduledTripTimes); } /** - * Trips may also have null headsigns, in which case we should fall back on a Timetable or + * Copy scheduled times, but not the actual times. + */ + public TripTimes copyOfScheduledTimes() { + return new TripTimes(this, scheduledTripTimes); + } + + /** + * Both trip_headsign and stop_headsign (per stop on a particular trip) are optional GTFS fields. + * A trip may not have a headsign, in which case we should fall back on a Timetable or * Pattern-level headsign. Such a string will be available when we give TripPatterns or * StopPatterns unique human readable route variant names, but a TripTimes currently does not have * a pointer to its enclosing timetable or pattern. */ @Nullable public I18NString getHeadsign(final int stop) { - I18NString tripHeadsign = getTrip().getHeadsign(); - if (headsigns == null) { - return tripHeadsign; - } else { - I18NString stopHeadsign = headsigns[stop]; - return stopHeadsign != null ? stopHeadsign : tripHeadsign; - } + return (headsigns != null && headsigns[stop] != null) + ? headsigns[stop] + : scheduledTripTimes.getHeadsign(stop); } /** @@ -211,27 +116,30 @@ public I18NString getHeadsign(final int stop) { * @return Empty list if there are no vias registered for a stop. */ public List getHeadsignVias(final int stop) { - if (headsignVias == null || headsignVias[stop] == null) { - return List.of(); - } - return List.of(headsignVias[stop]); + return scheduledTripTimes.getHeadsignVias(stop); } /** * @return the whole trip's headsign. Individual stops can have different headsigns. */ public I18NString getTripHeadsign() { - return trip.getHeadsign(); + return scheduledTripTimes.getTripHeadsign(); } - /** @return the time in seconds after midnight that the vehicle arrives at the stop. */ + /** + * The time in seconds after midnight at which the vehicle should arrive at the given stop + * according to the original schedule. + */ public int getScheduledArrivalTime(final int stop) { - return scheduledArrivalTimes[stop] + timeShift; + return scheduledTripTimes.getScheduledArrivalTime(stop); } - /** @return the amount of time in seconds that the vehicle waits at the stop. */ + /** + * The time in seconds after midnight at which the vehicle should leave the given stop according + * to the original schedule. + */ public int getScheduledDepartureTime(final int stop) { - return scheduledDepartureTimes[stop] + timeShift; + return scheduledTripTimes.getScheduledDepartureTime(stop); } /** @@ -244,77 +152,62 @@ public int sortIndex() { return getDepartureTime(0); } - /** @return the time in seconds after midnight that the vehicle arrives at the stop. */ + /** + * The time in seconds after midnight at which the vehicle arrives at each stop, accounting for + * any real-time updates. + */ public int getArrivalTime(final int stop) { - if (arrivalTimes == null) { - return getScheduledArrivalTime(stop); - } else return arrivalTimes[stop]; // updated times are not time shifted. + return getOrElse(stop, arrivalTimes, scheduledTripTimes::getScheduledArrivalTime); } - /** @return the amount of time in seconds that the vehicle waits at the stop. */ + /** + * The time in seconds after midnight at which the vehicle leaves each stop, accounting for any + * real-time updates. + */ public int getDepartureTime(final int stop) { - if (departureTimes == null) { - return getScheduledDepartureTime(stop); - } else return departureTimes[stop]; // updated times are not time shifted. + return getOrElse(stop, departureTimes, scheduledTripTimes::getScheduledDepartureTime); } /** @return the difference between the scheduled and actual arrival times at this stop. */ public int getArrivalDelay(final int stop) { - return getArrivalTime(stop) - (scheduledArrivalTimes[stop] + timeShift); + return getArrivalTime(stop) - scheduledTripTimes.getScheduledArrivalTime(stop); } /** @return the difference between the scheduled and actual departure times at this stop. */ public int getDepartureDelay(final int stop) { - return getDepartureTime(stop) - (scheduledDepartureTimes[stop] + timeShift); + return getDepartureTime(stop) - scheduledTripTimes.getScheduledDepartureTime(stop); } public void setRecorded(int stop) { - prepareForRealTimeUpdates(); - stopRealTimeStates[stop] = StopRealTimeState.RECORDED; + setStopRealTimeStates(stop, StopRealTimeState.RECORDED); } public void setCancelled(int stop) { - prepareForRealTimeUpdates(); - stopRealTimeStates[stop] = StopRealTimeState.CANCELLED; + setStopRealTimeStates(stop, StopRealTimeState.CANCELLED); } public void setNoData(int stop) { - prepareForRealTimeUpdates(); - stopRealTimeStates[stop] = StopRealTimeState.NO_DATA; + setStopRealTimeStates(stop, StopRealTimeState.NO_DATA); } public void setPredictionInaccurate(int stop) { - prepareForRealTimeUpdates(); - stopRealTimeStates[stop] = StopRealTimeState.INACCURATE_PREDICTIONS; + setStopRealTimeStates(stop, StopRealTimeState.INACCURATE_PREDICTIONS); } public boolean isCancelledStop(int stop) { - if (stopRealTimeStates == null) { - return false; - } - return stopRealTimeStates[stop] == StopRealTimeState.CANCELLED; + return isStopRealTimeStates(stop, StopRealTimeState.CANCELLED); } - // TODO OTP2 - Unused, but will be used by Transmodel API public boolean isRecordedStop(int stop) { - if (stopRealTimeStates == null) { - return false; - } - return stopRealTimeStates[stop] == StopRealTimeState.RECORDED; + return isStopRealTimeStates(stop, StopRealTimeState.RECORDED); } public boolean isNoDataStop(int stop) { - if (stopRealTimeStates == null) { - return false; - } - return stopRealTimeStates[stop] == StopRealTimeState.NO_DATA; + return isStopRealTimeStates(stop, StopRealTimeState.NO_DATA); } public boolean isPredictionInaccurate(int stop) { - if (stopRealTimeStates == null) { - return false; - } - return stopRealTimeStates[stop] == StopRealTimeState.INACCURATE_PREDICTIONS; + return isStopRealTimeStates(stop, StopRealTimeState.INACCURATE_PREDICTIONS); } public void setOccupancyStatus(int stop, OccupancyStatus occupancyStatus) { @@ -322,6 +215,9 @@ public void setOccupancyStatus(int stop, OccupancyStatus occupancyStatus) { this.occupancyStatus[stop] = occupancyStatus; } + /** + * This is only for API-purposes (does not affect routing). + */ public OccupancyStatus getOccupancyStatus(int stop) { if (this.occupancyStatus == null) { return OccupancyStatus.NO_DATA_AVAILABLE; @@ -330,47 +226,44 @@ public OccupancyStatus getOccupancyStatus(int stop) { } public BookingInfo getDropOffBookingInfo(int stop) { - return dropOffBookingInfos.get(stop); + return scheduledTripTimes.getDropOffBookingInfo(stop); } public BookingInfo getPickupBookingInfo(int stop) { - return pickupBookingInfos.get(stop); + return scheduledTripTimes.getPickupBookingInfo(stop); } /** - * @return true if this TripTimes represents an unmodified, scheduled trip from a published - * timetable or false if it is a updated, cancelled, or otherwise modified one. This method - * differs from {@link #getRealTimeState()} in that it checks whether real-time information is - * actually available in this TripTimes. + * Return {@code true} if the trip is unmodified, a scheduled trip from a published timetable. + * Return {@code false} if the trip is an updated, cancelled, or otherwise modified one. This + * method differs from {@link #getRealTimeState()} in that it checks whether real-time + * information is actually available. */ public boolean isScheduled() { return realTimeState == RealTimeState.SCHEDULED; } /** - * @return true if this TripTimes is canceled or soft-deleted + * Return {@code true} if canceled or soft-deleted */ public boolean isCanceledOrDeleted() { return isCanceled() || isDeleted(); } /** - * @return true if this TripTimes is canceled + * Return {@code true} if canceled */ public boolean isCanceled() { return realTimeState == RealTimeState.CANCELED; } /** - * @return true if this TripTimes is soft-deleted, and should not be visible to the user + * Return true if trip is soft-deleted, and should not be visible to the user */ public boolean isDeleted() { return realTimeState == RealTimeState.DELETED; } - /** - * @return the real-time state of this TripTimes - */ public RealTimeState getRealTimeState() { return realTimeState; } @@ -385,10 +278,10 @@ public void setRealTimeState(final RealTimeState realTimeState) { * checks that all internal times are increasing. Thus, this check should be used at the end of * updating trip times, after any propagating or interpolating delay operations. * - * @return empty if times were found to be increasing, stop index of the first error otherwise + * @return empty if times were found to be increasing, the first validation error otherwise */ public Optional validateNonIncreasingTimes() { - final int nStops = scheduledArrivalTimes.length; + final int nStops = arrivalTimes.length; int prevDep = -9_999_999; for (int s = 0; s < nStops; s++) { final int arr = getArrivalTime(s); @@ -499,7 +392,7 @@ public void updateDepartureTime(final int stop, final int time) { public void updateDepartureDelay(final int stop, final int delay) { prepareForRealTimeUpdates(); - departureTimes[stop] = scheduledDepartureTimes[stop] + timeShift + delay; + departureTimes[stop] = scheduledTripTimes.getScheduledDepartureTime(stop) + delay; } public void updateArrivalTime(final int stop, final int time) { @@ -509,10 +402,12 @@ public void updateArrivalTime(final int stop, final int time) { public void updateArrivalDelay(final int stop, final int delay) { prepareForRealTimeUpdates(); - arrivalTimes[stop] = scheduledArrivalTimes[stop] + timeShift + delay; + arrivalTimes[stop] = scheduledTripTimes.getScheduledArrivalTime(stop) + delay; } + @Nullable public Accessibility getWheelchairAccessibility() { + // No need to fall back to scheduled state, since it is copied over in the constructor return wheelchairAccessibility; } @@ -521,10 +416,10 @@ public void updateWheelchairAccessibility(Accessibility wheelchairAccessibility) } public int getNumStops() { - return scheduledArrivalTimes.length; + return scheduledTripTimes.getNumStops(); } - /** Sort TripTimes based on first departure time. */ + /** Sort trips based on first departure time. */ @Override public int compareTo(final TripTimes other) { return this.getDepartureTime(0) - other.getDepartureTime(0); @@ -540,59 +435,67 @@ public TripTimes timeShift(final int stop, final int time, final boolean depart) if (arrivalTimes != null || departureTimes != null) { return null; } - final TripTimes shifted = new TripTimes(this); // Adjust 0-based times to match desired stoptime. final int shift = time - (depart ? getDepartureTime(stop) : getArrivalTime(stop)); - // existing shift should usually (always?) be 0 on freqs - shifted.timeShift = shifted.timeShift + shift; - return shifted; + + return new TripTimes( + this, + scheduledTripTimes.copyOfNoDuplication().plusTimeShift(shift).build() + ); } - // Time-shift all times on this trip. This is used when updating the time zone for the trip. - public void timeShift(Duration duration) { - timeShift += duration.toSeconds(); + /** + * Time-shift all times on this trip. This is used when updating the time zone for the trip. + */ + public TripTimes adjustTimesToGraphTimeZone(Duration shiftDelta) { + return new TripTimes( + this, + scheduledTripTimes.copyOfNoDuplication().plusTimeShift((int) shiftDelta.toSeconds()).build() + ); } /** - * Returns the GTFS sequence number of the given 0-based stop index. + * Returns the GTFS sequence number of the given 0-based stop position. + * + * These are the GTFS stop sequence numbers, which show the order in which the vehicle visits the + * stops. Despite the fact that the StopPattern or TripPattern enclosing this TripTimes provides + * an ordered list of Stops, the original stop sequence numbers may still be needed for matching + * with GTFS-RT update messages. Unfortunately, each individual trip can have totally different + * sequence numbers for the same stops, so we need to store them at the individual trip level. An + * effort is made to re-use the sequence number arrays when they are the same across different + * trips in the same pattern. */ public int gtfsSequenceOfStopIndex(final int stop) { - return originalGtfsStopSequence[stop]; + return scheduledTripTimes.gtfsSequenceOfStopIndex(stop); } /** * Returns the 0-based stop index of the given GTFS sequence number. */ public OptionalInt stopIndexOfGtfsSequence(int stopSequence) { - if (originalGtfsStopSequence == null) { - return OptionalInt.empty(); - } - for (int i = 0; i < originalGtfsStopSequence.length; i++) { - var sequence = originalGtfsStopSequence[i]; - if (sequence == stopSequence) { - return OptionalInt.of(i); - } - } - return OptionalInt.empty(); + return scheduledTripTimes.stopIndexOfGtfsSequence(stopSequence); } - /** @return whether or not stopIndex is considered a timepoint in this TripTimes. */ + /** + * Whether or not stopIndex is considered a GTFS timepoint. + */ public boolean isTimepoint(final int stopIndex) { - return timepoints.get(stopIndex); + return scheduledTripTimes.isTimepoint(stopIndex); } /** The code for the service on which this trip runs. For departure search optimizations. */ public int getServiceCode() { - return serviceCode; + return scheduledTripTimes.getServiceCode(); } public void setServiceCode(int serviceCode) { - this.serviceCode = serviceCode; + this.scheduledTripTimes = + scheduledTripTimes.copyOfNoDuplication().withServiceCode(serviceCode).build(); } - /** The trips whose arrivals and departures are represented by this TripTimes */ + /** The trips whose arrivals and departures are represented by this class */ public Trip getTrip() { - return trip; + return scheduledTripTimes.getTrip(); } /** @@ -657,48 +560,29 @@ public boolean adjustTimesBeforeWhenRequired(int firstUpdatedIndex, boolean setN return hasAdjustedTimes; } + /* private member methods */ + + private void setStopRealTimeStates(int stop, StopRealTimeState state) { + prepareForRealTimeUpdates(); + this.stopRealTimeStates[stop] = state; + } + /** - * @return either an array of headsigns (one for each stop on this trip) or null if the headsign - * is the same at all stops (including null) and can be found in the Trip object. + * The real-time states for a given stops. If the state is DEFAULT for a stop, + * the {@link #getRealTimeState()} should determine the realtime state of the stop. + *

+ * This is only for API-purposes (does not affect routing). */ - private I18NString[] makeHeadsignsArray(final Collection stopTimes) { - final I18NString tripHeadsign = trip.getHeadsign(); - boolean useStopHeadsigns = false; - if (tripHeadsign == null) { - useStopHeadsigns = true; - } else { - for (final StopTime st : stopTimes) { - if (!(tripHeadsign.equals(st.getStopHeadsign()))) { - useStopHeadsigns = true; - break; - } - } - } - if (!useStopHeadsigns) { - return null; //defer to trip_headsign - } - boolean allNull = true; - int i = 0; - final I18NString[] hs = new I18NString[stopTimes.size()]; - for (final StopTime st : stopTimes) { - final I18NString headsign = st.getStopHeadsign(); - hs[i++] = headsign; - if (headsign != null) allNull = false; - } - if (allNull) { - return null; - } else { - return hs; - } + private boolean isStopRealTimeStates(int stop, StopRealTimeState state) { + return stopRealTimeStates != null && stopRealTimeStates[stop] == state; } public void setHeadsign(int index, I18NString headsign) { if (headsigns == null) { - if (headsign.equals(trip.getHeadsign())) { + if (headsign.equals(getTrip().getHeadsign())) { return; } - - this.headsigns = new I18NString[scheduledArrivalTimes.length]; + this.headsigns = scheduledTripTimes.copyHeadsigns(() -> new I18NString[getNumStops()]); this.headsigns[index] = headsign; return; } @@ -707,35 +591,8 @@ public void setHeadsign(int index, I18NString headsign) { headsigns[index] = headsign; } - /** - * Create 2D String array for via names for each stop in sequence. - * - * @return May be null if no vias are present in stop sequence. - */ - private String[][] makeHeadsignViasArray(final Collection stopTimes) { - if ( - stopTimes - .stream() - .allMatch(st -> st.getHeadsignVias() == null || st.getHeadsignVias().isEmpty()) - ) { - return null; - } - - String[][] vias = new String[stopTimes.size()][]; - - int i = 0; - for (final StopTime st : stopTimes) { - if (st.getHeadsignVias() == null) { - vias[i] = EMPTY_STRING_ARRAY; - i++; - continue; - } - - vias[i] = st.getHeadsignVias().toArray(EMPTY_STRING_ARRAY); - i++; - } - - return vias; + private static int getOrElse(int index, int[] array, IntUnaryOperator defaultValue) { + return array != null ? array[index] : defaultValue.applyAsInt(index); } /** @@ -746,23 +603,16 @@ private String[][] makeHeadsignViasArray(final Collection stopTimes) { */ private void prepareForRealTimeUpdates() { if (arrivalTimes == null) { - this.arrivalTimes = Arrays.copyOf(scheduledArrivalTimes, scheduledArrivalTimes.length); - this.departureTimes = Arrays.copyOf(scheduledDepartureTimes, scheduledDepartureTimes.length); + this.arrivalTimes = scheduledTripTimes.copyArrivalTimes(); + this.departureTimes = scheduledTripTimes.copyDepartureTimes(); + // Update the real-time state + this.realTimeState = RealTimeState.UPDATED; this.stopRealTimeStates = new StopRealTimeState[arrivalTimes.length]; + Arrays.fill(stopRealTimeStates, StopRealTimeState.DEFAULT); + this.headsigns = scheduledTripTimes.copyHeadsigns(() -> null); this.occupancyStatus = new OccupancyStatus[arrivalTimes.length]; - if (headsigns != null) { - headsigns = Arrays.copyOf(headsigns, headsigns.length); - } - - for (int i = 0; i < arrivalTimes.length; i++) { - arrivalTimes[i] += timeShift; - departureTimes[i] += timeShift; - stopRealTimeStates[i] = StopRealTimeState.DEFAULT; - occupancyStatus[i] = OccupancyStatus.NO_DATA_AVAILABLE; - } - - // Update the real-time state - realTimeState = RealTimeState.UPDATED; + Arrays.fill(occupancyStatus, OccupancyStatus.NO_DATA_AVAILABLE); + // skip immutable types: scheduledTripTimes & wheelchairAccessibility } } } diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimesFactory.java b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimesFactory.java new file mode 100644 index 00000000000..1bb953b81c5 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimesFactory.java @@ -0,0 +1,28 @@ +package org.opentripplanner.transit.model.timetable; + +import java.util.Collection; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.transit.model.framework.Deduplicator; + +/** + * The responsibility of this class is to create TripTimes based on StopTimes. The + * TripTimes should not have a dependency to StopTimes, so this class act as a middleman. + * Eventually this class should not be needed - the intermediate step to map feeds into + * StopTimes and then map stop-times into TripTimes is unnecessary - we should map the + * feeds directly into {@link ScheduledTripTimes} using the builder instead. + */ +public class TripTimesFactory { + + /** + * The provided stopTimes are assumed to be pre-filtered, valid, and monotonically increasing. The + * non-interpolated stoptimes should already be marked at timepoints by a previous filtering + * step. + */ + public static TripTimes tripTimes( + Trip trip, + Collection stopTimes, + Deduplicator deduplicator + ) { + return new TripTimes(StopTimeToScheduledTripTimesMapper.map(trip, stopTimes, deduplicator)); + } +} diff --git a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java index d9a3a683c1a..6cda95eb983 100644 --- a/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java +++ b/src/main/java/org/opentripplanner/updater/trip/TimetableSnapshotSource.java @@ -55,6 +55,7 @@ import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitEditorService; import org.opentripplanner.transit.service.TransitModel; @@ -885,7 +886,7 @@ private Result addTripToGraphAndBuffer( ); // Create new trip times - final TripTimes newTripTimes = new TripTimes(trip, stopTimes, deduplicator); + final TripTimes newTripTimes = TripTimesFactory.tripTimes(trip, stopTimes, deduplicator); // Update all times to mark trip times as realtime // TODO: should we incorporate the delay field if present? @@ -945,7 +946,7 @@ private boolean cancelScheduledTrip( if (tripIndex == -1) { debug(tripId, "Could not cancel scheduled trip because it's not in the timetable"); } else { - final TripTimes newTripTimes = new TripTimes(timetable.getTripTimes(tripIndex)); + final TripTimes newTripTimes = timetable.getTripTimes(tripIndex).copyOfScheduledTimes(); switch (cancelationType) { case CANCEL -> newTripTimes.cancelTrip(); case DELETE -> newTripTimes.deleteTrip(); @@ -985,7 +986,7 @@ private boolean cancelPreviouslyAddedTrip( if (tripIndex == -1) { debug(tripId, "Could not cancel previously added trip on {}", serviceDate); } else { - final TripTimes newTripTimes = new TripTimes(timetable.getTripTimes(tripIndex)); + final TripTimes newTripTimes = timetable.getTripTimes(tripIndex).copyOfScheduledTimes(); switch (cancelationType) { case CANCEL -> newTripTimes.cancelTrip(); case DELETE -> newTripTimes.deleteTrip(); diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index 16b078b3e77..a293c940713 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -82,7 +82,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; @@ -138,7 +138,7 @@ static void setup() { final TripPattern pattern = TEST_MODEL.pattern(BUS).build(); var trip = TransitModelForTest.trip("123").withHeadsign(I18NString.of("Trip Headsign")).build(); var stopTimes = TEST_MODEL.stopTimesEvery5Minutes(3, trip, T11_00); - var tripTimes = new TripTimes(trip, stopTimes, DEDUPLICATOR); + var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, DEDUPLICATOR); pattern.add(tripTimes); transitModel.addTripPattern(id("pattern-1"), pattern); diff --git a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java index 8e9a803c94b..1e931589204 100644 --- a/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java +++ b/src/test/java/org/opentripplanner/gtfs/interlining/InterlineProcessorTest.java @@ -21,7 +21,7 @@ import org.opentripplanner.transit.model.framework.FeedScopedId; 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 InterlineProcessorTest implements PlanTestConstants { @@ -167,7 +167,7 @@ private static TripPattern tripPattern(String tripId, String blockId, String ser .withRoute(trip.getRoute()) .withStopPattern(stopPattern) .build(); - var tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); tp.add(tripTimes); return tp; } diff --git a/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java b/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java index d3c21909e74..78002e46eae 100644 --- a/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java +++ b/src/test/java/org/opentripplanner/model/TripTimeOnDateTest.java @@ -8,7 +8,7 @@ import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.framework.Deduplicator; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; class TripTimeOnDateTest implements PlanTestConstants { @@ -19,7 +19,7 @@ void gtfsSequence() { var trip = TransitModelForTest.trip("123").build(); var stopTimes = testModel.stopTimesEvery5Minutes(3, trip, T11_00); - var tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); var subject = new TripTimeOnDate(tripTimes, 2, pattern); diff --git a/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java b/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java index 90a077eb205..5d1fef21271 100644 --- a/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java +++ b/src/test/java/org/opentripplanner/model/impl/OtpTransitServiceBuilderLimitPeriodTest.java @@ -25,7 +25,7 @@ import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.timetable.Direction; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.StopModel; /** @@ -193,7 +193,7 @@ private TripPattern createTripPattern(Collection trips) { .build(); for (Trip trip : trips) { - p.add(new TripTimes(trip, STOP_TIMES, DEDUPLICATOR)); + p.add(TripTimesFactory.tripTimes(trip, STOP_TIMES, DEDUPLICATOR)); } return p; } diff --git a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java index 3eccdaaabd1..dafcc639ceb 100644 --- a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java +++ b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java @@ -44,6 +44,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; /** * This is a helper class to allow unit-testing on Itineraries. The builder does not necessarily @@ -475,7 +476,7 @@ public TestItineraryBuilder transit( .withRoute(route) .withStopPattern(stopPattern) .build(); - final TripTimes tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + final TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); tripPattern.add(tripTimes); ScheduledTransitLeg leg; diff --git a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java index 22252093aba..a3fab1838cb 100644 --- a/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java +++ b/src/test/java/org/opentripplanner/model/plan/legreference/ScheduledTransitLegReferenceTest.java @@ -23,7 +23,7 @@ import org.opentripplanner.transit.model.site.Station; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; @@ -64,7 +64,7 @@ static void buildTransitService() { stopIdAtPosition0 = tripPattern.getStop(0).getId(); stopIdAtPosition1 = tripPattern.getStop(1).getId(); stopIdAtPosition2 = tripPattern.getStop(2).getId(); - TripTimes tripTimes = new TripTimes( + var tripTimes = TripTimesFactory.tripTimes( trip, TEST_MODEL.stopTimesEvery5Minutes(5, trip, PlanTestConstants.T11_00), new Deduplicator() diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDateTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDateTest.java index ed13d6cbf19..ecd75f5d273 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDateTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDateTest.java @@ -20,13 +20,14 @@ import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.timetable.FrequencyEntry; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; class TripPatternForDateTest { private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); private static final RegularStop STOP = TEST_MODEL.stop("TEST:STOP", 0, 0).build(); private static final Route ROUTE = TransitModelForTest.route("1").build(); - private static final TripTimes tripTimes = new TripTimes( + private static final TripTimes tripTimes = TripTimesFactory.tripTimes( TransitModelForTest.trip("1").withRoute(ROUTE).build(), List.of(new StopTime()), new Deduplicator() diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java index 6ee2d7bcd11..020dc6305a4 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TripPatternForDateMapperTest.java @@ -19,6 +19,7 @@ import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; public class TripPatternForDateMapperTest { @@ -37,7 +38,7 @@ public static void setUp() throws Exception { var pattern = TEST_MODEL.pattern(BUS).build(); timetable = new Timetable(pattern); var trip = TransitModelForTest.trip("1").build(); - var tripTimes = new TripTimes( + var tripTimes = TripTimesFactory.tripTimes( trip, TEST_MODEL.stopTimesEvery5Minutes(5, trip, PlanTestConstants.T11_00), new Deduplicator() diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java index b5fbcf55e1e..20ca21a50a6 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitDataCreatorTest.java @@ -22,6 +22,7 @@ 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; public class RaptorRoutingRequestTransitDataCreatorTest { @@ -103,7 +104,7 @@ private TripTimes createTripTimesForTest() { stopTime1.setDepartureTime(0); stopTime2.setArrivalTime(7200); - return new TripTimes( + return TripTimesFactory.tripTimes( TransitModelForTest.trip("Test").build(), Arrays.asList(stopTime1, stopTime2), new Deduplicator() diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java index 77d5cce6980..86d5f9404ed 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RouteRequestTransitDataProviderFilterTest.java @@ -44,6 +44,7 @@ import org.opentripplanner.transit.model.timetable.TripAlteration; import org.opentripplanner.transit.model.timetable.TripBuilder; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; class RouteRequestTransitDataProviderFilterTest { @@ -811,7 +812,7 @@ private TripPatternForDate createTestTripPatternForDate() { .build() .getRoutingTripPattern(); - TripTimes tripTimes = new TripTimes( + TripTimes tripTimes = TripTimesFactory.tripTimes( TransitModelForTest.trip("1").withRoute(route).build(), List.of(new StopTime()), new Deduplicator() @@ -884,7 +885,7 @@ private TripTimes createTestTripTimes( stopTime.setDepartureTime(60); stopTime.setStopSequence(0); - return new TripTimes(trip, List.of(stopTime), new Deduplicator()); + return TripTimesFactory.tripTimes(trip, List.of(stopTime), new Deduplicator()); } private TripTimes createTestTripTimesWithSubmode(String submode) { diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java index 2c14a8b0b9d..a7a0c2fd160 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TestRouteData.java @@ -26,6 +26,7 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; public class TestRouteData { @@ -139,7 +140,7 @@ private Trip parseTripInfo( .build(); var stopTimes = stopTimes(trip, stops, tripTimes); this.stopTimesByTrip.put(trip, stopTimes); - this.tripTimesByTrip.put(trip, new TripTimes(trip, stopTimes, deduplicator)); + this.tripTimesByTrip.put(trip, TripTimesFactory.tripTimes(trip, stopTimes, deduplicator)); return trip; } diff --git a/src/test/java/org/opentripplanner/routing/trippattern/FrequencyEntryTest.java b/src/test/java/org/opentripplanner/routing/trippattern/FrequencyEntryTest.java index bfc4887f756..49ca6d9221b 100644 --- a/src/test/java/org/opentripplanner/routing/trippattern/FrequencyEntryTest.java +++ b/src/test/java/org/opentripplanner/routing/trippattern/FrequencyEntryTest.java @@ -14,6 +14,7 @@ import org.opentripplanner.transit.model.timetable.FrequencyEntry; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; public class FrequencyEntryTest { @@ -43,7 +44,7 @@ public class FrequencyEntryTest { stopTimes.add(stopTime); } - tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); } @Test diff --git a/src/test/java/org/opentripplanner/transit/model/timetable/TripTimesTest.java b/src/test/java/org/opentripplanner/transit/model/timetable/TripTimesTest.java index 6456dd732eb..4c2cc41cef2 100644 --- a/src/test/java/org/opentripplanner/transit/model/timetable/TripTimesTest.java +++ b/src/test/java/org/opentripplanner/transit/model/timetable/TripTimesTest.java @@ -23,7 +23,7 @@ class TripTimesTest { - private static TransitModelForTest TEST_MODEL = TransitModelForTest.of(); + private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); private static final String TRIP_ID = "testTripId"; @@ -54,7 +54,7 @@ static TripTimes createInitialTripTimes() { stopTimes.add(stopTime); } - return new TripTimes(trip, stopTimes, new Deduplicator()); + return TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); } @Nested @@ -71,7 +71,7 @@ void shouldHandleBothNullScenario() { Trip trip = TransitModelForTest.trip("TRIP").build(); Collection stopTimes = List.of(EMPTY_STOPPOINT, EMPTY_STOPPOINT, EMPTY_STOPPOINT); - TripTimes tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); I18NString headsignFirstStop = tripTimes.getHeadsign(0); assertNull(headsignFirstStop); @@ -82,7 +82,7 @@ void shouldHandleTripOnlyHeadSignScenario() { Trip trip = TransitModelForTest.trip("TRIP").withHeadsign(DIRECTION).build(); Collection stopTimes = List.of(EMPTY_STOPPOINT, EMPTY_STOPPOINT, EMPTY_STOPPOINT); - TripTimes tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); I18NString headsignFirstStop = tripTimes.getHeadsign(0); assertEquals(DIRECTION, headsignFirstStop); @@ -99,7 +99,7 @@ void shouldHandleStopsOnlyHeadSignScenario() { stopWithHeadsign ); - TripTimes tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); I18NString headsignFirstStop = tripTimes.getHeadsign(0); assertEquals(STOP_TEST_DIRECTION, headsignFirstStop); @@ -116,7 +116,7 @@ void shouldHandleStopsEqualToTripHeadSignScenario() { stopWithHeadsign ); - TripTimes tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); I18NString headsignFirstStop = tripTimes.getHeadsign(0); assertEquals(DIRECTION, headsignFirstStop); @@ -129,7 +129,7 @@ void shouldHandleDifferingTripAndStopHeadSignScenario() { stopWithHeadsign.setStopHeadsign(STOP_TEST_DIRECTION); Collection stopTimes = List.of(stopWithHeadsign, EMPTY_STOPPOINT, EMPTY_STOPPOINT); - TripTimes tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + TripTimes tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); I18NString headsignFirstStop = tripTimes.getHeadsign(0); assertEquals(STOP_TEST_DIRECTION, headsignFirstStop); @@ -141,7 +141,7 @@ void shouldHandleDifferingTripAndStopHeadSignScenario() { @Test public void testStopUpdate() { - TripTimes updatedTripTimesA = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesA = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesA.updateArrivalTime(3, 190); updatedTripTimesA.updateDepartureTime(3, 190); @@ -156,7 +156,7 @@ public void testStopUpdate() { @Test public void testPassedUpdate() { - TripTimes updatedTripTimesA = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesA = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesA.updateDepartureTime(0, 30); @@ -166,7 +166,7 @@ public void testPassedUpdate() { @Test public void testNegativeDwellTime() { - TripTimes updatedTripTimesA = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesA = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesA.updateArrivalTime(1, 60); updatedTripTimesA.updateDepartureTime(1, 59); @@ -179,7 +179,7 @@ public void testNegativeDwellTime() { @Test public void testNegativeHopTime() { - TripTimes updatedTripTimesB = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesB = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesB.updateDepartureTime(6, 421); updatedTripTimesB.updateArrivalTime(7, 420); @@ -200,7 +200,7 @@ public void testNegativeHopTime() { */ @Test public void testNegativeHopTimeWithStopCancellations() { - TripTimes updatedTripTimes = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimes = createInitialTripTimes(); updatedTripTimes.updateDepartureTime(5, 421); updatedTripTimes.updateArrivalTime(6, 481); @@ -227,7 +227,7 @@ public void testNegativeHopTimeWithStopCancellations() { */ @Test public void testPositiveHopTimeWithStopCancellationsLate() { - TripTimes updatedTripTimes = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimes = createInitialTripTimes(); updatedTripTimes.updateDepartureTime(5, 400); updatedTripTimes.updateArrivalTime(6, 460); @@ -253,7 +253,7 @@ public void testPositiveHopTimeWithStopCancellationsLate() { */ @Test public void testPositiveHopTimeWithStopCancellationsEarly() { - TripTimes updatedTripTimes = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimes = createInitialTripTimes(); updatedTripTimes.updateDepartureTime(5, 300); updatedTripTimes.setCancelled(6); @@ -278,7 +278,7 @@ public void testPositiveHopTimeWithStopCancellationsEarly() { */ @Test public void testPositiveHopTimeWithTerminalCancellation() { - TripTimes updatedTripTimes = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimes = createInitialTripTimes(); updatedTripTimes.setCancelled(0); updatedTripTimes.setCancelled(1); @@ -304,7 +304,7 @@ public void testPositiveHopTimeWithTerminalCancellation() { */ @Test public void testInterpolationWithTerminalCancellation() { - TripTimes updatedTripTimes = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimes = createInitialTripTimes(); updatedTripTimes.setCancelled(6); updatedTripTimes.setCancelled(7); @@ -323,7 +323,7 @@ public void testInterpolationWithTerminalCancellation() { */ @Test public void testInterpolationWithMultipleStopCancellations() { - TripTimes updatedTripTimes = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimes = createInitialTripTimes(); updatedTripTimes.setCancelled(1); updatedTripTimes.setCancelled(2); @@ -352,7 +352,7 @@ public void testInterpolationWithMultipleStopCancellations() { */ @Test public void testInterpolationWithMultipleStopCancellations2() { - TripTimes updatedTripTimes = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimes = createInitialTripTimes(); updatedTripTimes.setCancelled(1); updatedTripTimes.setCancelled(2); @@ -375,7 +375,7 @@ public void testInterpolationWithMultipleStopCancellations2() { @Test public void testNonIncreasingUpdateCrossingMidnight() { - TripTimes updatedTripTimesA = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesA = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesA.updateArrivalTime(0, -300); //"Yesterday" updatedTripTimesA.updateDepartureTime(0, 50); @@ -385,7 +385,7 @@ public void testNonIncreasingUpdateCrossingMidnight() { @Test public void testDelay() { - TripTimes updatedTripTimesA = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesA = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesA.updateDepartureDelay(0, 10); updatedTripTimesA.updateArrivalDelay(6, 13); @@ -395,14 +395,14 @@ public void testDelay() { @Test public void testCancel() { - TripTimes updatedTripTimesA = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesA = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesA.cancelTrip(); assertEquals(RealTimeState.CANCELED, updatedTripTimesA.getRealTimeState()); } @Test public void testNoData() { - TripTimes updatedTripTimesA = new TripTimes(createInitialTripTimes()); + TripTimes updatedTripTimesA = createInitialTripTimes().copyOfScheduledTimes(); updatedTripTimesA.setNoData(1); assertFalse(updatedTripTimesA.isNoDataStop(0)); assertTrue(updatedTripTimesA.isNoDataStop(1)); @@ -463,9 +463,9 @@ public void testApply() { stopTimes.add(stopTime1); stopTimes.add(stopTime2); - TripTimes differingTripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + TripTimes differingTripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); - TripTimes updatedTripTimesA = new TripTimes(differingTripTimes); + TripTimes updatedTripTimesA = differingTripTimes.copyOfScheduledTimes(); updatedTripTimesA.updateArrivalTime(1, 89); updatedTripTimesA.updateDepartureTime(1, 98); diff --git a/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java b/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java index 0654211d1e1..29d03844260 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java @@ -37,7 +37,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.OccupancyStatus; import org.opentripplanner.transit.model.timetable.Trip; -import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.TripTimesFactory; public class RealtimeVehicleMatcherTest { @@ -345,7 +345,7 @@ void inferServiceDayOfTripAt6(String time, String expectedDate) { testModel.stopTime(trip, 1, fivePast6) ); - var tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); var instant = OffsetDateTime.parse(time).toInstant(); var inferredDate = RealtimeVehiclePatternMatcher.inferServiceDate(tripTimes, zoneId, instant); @@ -364,7 +364,7 @@ void inferServiceDateCloseToMidnight() { testModel.stopTime(trip, 1, fivePastMidnight) ); - var tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); + var tripTimes = TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator()); var time = OffsetDateTime.parse("2022-04-05T00:04:00+02:00").toInstant(); @@ -384,7 +384,7 @@ private static TripPattern tripPattern(Trip trip, List stopTimes) { .build(); pattern .getScheduledTimetable() - .addTripTimes(new TripTimes(trip, stopTimes, new Deduplicator())); + .addTripTimes(TripTimesFactory.tripTimes(trip, stopTimes, new Deduplicator())); return pattern; }