diff --git a/src/main/java/ch/naviqore/app/service/ServiceConfigParser.java b/src/main/java/ch/naviqore/app/service/ServiceConfigParser.java index 6259cddd..1c991235 100644 --- a/src/main/java/ch/naviqore/app/service/ServiceConfigParser.java +++ b/src/main/java/ch/naviqore/app/service/ServiceConfigParser.java @@ -20,7 +20,8 @@ public ServiceConfigParser(@Value("${gtfs.static.uri}") String gtfsStaticUri, @Value("${walking.calculator.type}") String walkingCalculatorType, @Value("${walking.speed}") double walkingSpeed, @Value("${walking.duration.minimum}") int walkingDurationMinimum, - @Value("${cache.size}") int cacheSize, + @Value("${raptor.days.to.scan}") int raptorDaysToScan, + @Value("${cache.service.day.size}") int cacheServiceDaySize, @Value("${cache.eviction.strategy}") String cacheEvictionStrategy) { ServiceConfig.WalkCalculatorType walkCalculatorTypeEnum = ServiceConfig.WalkCalculatorType.valueOf( @@ -30,7 +31,7 @@ public ServiceConfigParser(@Value("${gtfs.static.uri}") String gtfsStaticUri, this.serviceConfig = new ServiceConfig(gtfsStaticUri, gtfsStaticUpdateCron, transferTimeSameStopDefault, transferTimeBetweenStopsMinimum, transferTimeAccessEgress, walkingSearchRadius, walkCalculatorTypeEnum, - walkingSpeed, walkingDurationMinimum, cacheSize, cacheEvictionStrategyEnum); + walkingSpeed, walkingDurationMinimum, raptorDaysToScan, cacheServiceDaySize, cacheEvictionStrategyEnum); } } diff --git a/src/main/java/ch/naviqore/raptor/RaptorAlgorithm.java b/src/main/java/ch/naviqore/raptor/RaptorAlgorithm.java index b9cddd06..33c36bf5 100644 --- a/src/main/java/ch/naviqore/raptor/RaptorAlgorithm.java +++ b/src/main/java/ch/naviqore/raptor/RaptorAlgorithm.java @@ -1,15 +1,17 @@ package ch.naviqore.raptor; +import ch.naviqore.raptor.router.RaptorConfig; import ch.naviqore.raptor.router.RaptorRouterBuilder; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Map; public interface RaptorAlgorithm { - static RaptorRouterBuilder builder(int sameStopTransferTime) { - return new RaptorRouterBuilder(sameStopTransferTime); + static RaptorRouterBuilder builder(RaptorConfig config) { + return new RaptorRouterBuilder(config); } /** @@ -48,4 +50,7 @@ List routeLatestDeparture(Map departureStops, Map routeIsolines(Map sourceStops, TimeType timeType, QueryConfig config); + // TODO: Discuss if this should be added to the interface (for now added for benchmark test) + void prepareStopTimesForDate(LocalDate date); + } diff --git a/src/main/java/ch/naviqore/raptor/router/DateTimeUtils.java b/src/main/java/ch/naviqore/raptor/router/DateTimeUtils.java index cdedb8a8..5637851e 100644 --- a/src/main/java/ch/naviqore/raptor/router/DateTimeUtils.java +++ b/src/main/java/ch/naviqore/raptor/router/DateTimeUtils.java @@ -11,21 +11,13 @@ class DateTimeUtils { - static LocalDate getReferenceDate(Map sourceStops, TimeType timeType) { + static LocalDateTime getReferenceDate(Map sourceStops, TimeType timeType) { if (timeType == TimeType.DEPARTURE) { // get minimum departure time - return sourceStops.values() - .stream() - .min(Comparator.naturalOrder()) - .map(LocalDateTime::toLocalDate) - .orElseThrow(); + return sourceStops.values().stream().min(Comparator.naturalOrder()).orElseThrow(); } else { // get maximum arrival time - return sourceStops.values() - .stream() - .max(Comparator.naturalOrder()) - .map(LocalDateTime::toLocalDate) - .orElseThrow(); + return sourceStops.values().stream().max(Comparator.naturalOrder()).orElseThrow(); } } diff --git a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java index f014742b..a3032a48 100644 --- a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java +++ b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java @@ -20,7 +20,7 @@ class LabelPostprocessor { private final Stop[] stops; - private final StopTime[] stopTimes; + private final int[] stopTimes; private final Route[] routes; private final RouteStop[] routeStops; @@ -375,7 +375,8 @@ private boolean canStopTimeBeTarget(StopTime stopTime, int routeSourceTime, Time return null; } - return stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset]; + int stopTimeIndex = firstStopTimeIdx + 2 * (tripOffset * numberOfStops + stopOffset) + 2; + return new StopTime(stopTimes[stopTimeIndex], stopTimes[stopTimeIndex + 1]); } private @Nullable StopLabelsAndTimes.Label getBestLabelForStop(List bestLabelsPerRound, diff --git a/src/main/java/ch/naviqore/raptor/router/Lookup.java b/src/main/java/ch/naviqore/raptor/router/Lookup.java index 7d8092b0..60c9fb88 100644 --- a/src/main/java/ch/naviqore/raptor/router/Lookup.java +++ b/src/main/java/ch/naviqore/raptor/router/Lookup.java @@ -2,5 +2,5 @@ import java.util.Map; -record Lookup(Map stops, Map routes) { +record Lookup(Map stops, Map routes, Map routeTripIds) { } diff --git a/src/main/java/ch/naviqore/raptor/router/Query.java b/src/main/java/ch/naviqore/raptor/router/Query.java index 1413e576..8410e4c0 100644 --- a/src/main/java/ch/naviqore/raptor/router/Query.java +++ b/src/main/java/ch/naviqore/raptor/router/Query.java @@ -4,6 +4,7 @@ import ch.naviqore.raptor.TimeType; import lombok.extern.slf4j.Slf4j; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -31,6 +32,9 @@ class Query { private final int cutoffTime; private final StopLabelsAndTimes stopLabelsAndTimes; + private final LocalDateTime referenceDate; + private final int maxDaysToScan; + /** * @param raptorData the current raptor data structures. * @param sourceStopIndices the indices of the source stops. @@ -39,9 +43,12 @@ class Query { * @param walkingDurationsToTarget the walking durations to the target stops. * @param timeType the time type (arrival or departure) of the query. * @param config the query configuration. + * @param referenceDate the reference date for the query. + * @param raptorConfig the raptor configuration. */ Query(RaptorData raptorData, int[] sourceStopIndices, int[] targetStopIndices, int[] sourceTimes, - int[] walkingDurationsToTarget, QueryConfig config, TimeType timeType) { + int[] walkingDurationsToTarget, QueryConfig config, TimeType timeType, LocalDateTime referenceDate, + RaptorConfig raptorConfig) { if (sourceStopIndices.length != sourceTimes.length) { throw new IllegalArgumentException("Source stops and departure/arrival times must have the same size."); @@ -58,6 +65,8 @@ class Query { this.walkingDurationsToTarget = walkingDurationsToTarget; this.config = config; this.timeType = timeType; + this.referenceDate = referenceDate; + this.maxDaysToScan = raptorConfig.getDaysToScan(); targetStops = new int[targetStopIndices.length * 2]; cutoffTime = determineCutoffTime(); @@ -85,7 +94,7 @@ List run() { FootpathRelaxer footpathRelaxer = new FootpathRelaxer(stopLabelsAndTimes, raptorData, config.getMinimumTransferDuration(), config.getMaximumWalkingDuration(), timeType); RouteScanner routeScanner = new RouteScanner(stopLabelsAndTimes, raptorData, - config.getMinimumTransferDuration(), timeType); + config.getMinimumTransferDuration(), timeType, referenceDate, maxDaysToScan); // initially relax all source stops and add the newly improved stops by relaxation to the marked stops Set markedStops = initialize(); @@ -118,7 +127,7 @@ List run() { * @return the initially marked stops. */ Set initialize() { - log.info("Initializing global best times per stop and best labels per round"); + log.debug("Initializing global best times per stop and best labels per round"); // fill target stops for (int i = 0; i < targetStops.length; i += 2) { diff --git a/src/main/java/ch/naviqore/raptor/router/RaptorConfig.java b/src/main/java/ch/naviqore/raptor/router/RaptorConfig.java new file mode 100644 index 00000000..01da8c41 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/router/RaptorConfig.java @@ -0,0 +1,89 @@ +package ch.naviqore.raptor.router; + +import ch.naviqore.utils.cache.EvictionCache; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +@Getter +@NoArgsConstructor +@ToString +public class RaptorConfig { + + @Setter + private RaptorTripMaskProvider maskProvider = new NoMaskProvider(); + + private int daysToScan = 1; + private int defaultSameStopTransferTime = 120; + + private int stopTimeCacheSize = 5; + @Setter + private EvictionCache.Strategy stopTimeCacheStrategy = EvictionCache.Strategy.LRU; + + public RaptorConfig(int daysToScan, int defaultSameStopTransferTime, int stopTimeCacheSize, + EvictionCache.Strategy stopTimeCacheStrategy, RaptorTripMaskProvider maskProvider) { + setDaysToScan(daysToScan); + setDefaultSameStopTransferTime(defaultSameStopTransferTime); + setStopTimeCacheSize(stopTimeCacheSize); + setStopTimeCacheStrategy(stopTimeCacheStrategy); + setMaskProvider(maskProvider); + } + + public void setDaysToScan(int daysToScan) { + if (daysToScan <= 0) { + throw new IllegalArgumentException("Days to scan must be greater than 0."); + } + this.daysToScan = daysToScan; + } + + public void setDefaultSameStopTransferTime(int defaultSameStopTransferTime) { + if (defaultSameStopTransferTime < 0) { + throw new IllegalArgumentException("Default same stop transfer time must be greater than or equal to 0."); + } + this.defaultSameStopTransferTime = defaultSameStopTransferTime; + } + + public void setStopTimeCacheSize(int stopTimeCacheSize) { + if (stopTimeCacheSize <= 0) { + throw new IllegalArgumentException("Stop time cache size must be greater than 0."); + } + this.stopTimeCacheSize = stopTimeCacheSize; + } + + /** + * No mask provider as default mask provider (no masking of trips). + */ + @Setter + @NoArgsConstructor + static class NoMaskProvider implements RaptorTripMaskProvider { + + Map tripIds = null; + + @Override + public String getServiceIdForDate(LocalDate date) { + return "NoMask"; + } + + @Override + public DayTripMask getDayTripMask(LocalDate date) { + Map tripMasks = new HashMap<>(); + for (Map.Entry entry : tripIds.entrySet()) { + String routeId = entry.getKey(); + String[] tripIds = entry.getValue(); + boolean[] tripMask = new boolean[tripIds.length]; + for (int i = 0; i < tripIds.length; i++) { + tripMask[i] = true; + } + tripMasks.put(routeId, new RouteTripMask(tripMask)); + } + + return new DayTripMask(getServiceIdForDate(date), date, tripMasks); + } + } + +} diff --git a/src/main/java/ch/naviqore/raptor/router/RaptorData.java b/src/main/java/ch/naviqore/raptor/router/RaptorData.java index 8419e5ce..c1bb1da1 100644 --- a/src/main/java/ch/naviqore/raptor/router/RaptorData.java +++ b/src/main/java/ch/naviqore/raptor/router/RaptorData.java @@ -11,4 +11,6 @@ interface RaptorData { RouteTraversal getRouteTraversal(); + StopTimeProvider getStopTimeProvider(); + } diff --git a/src/main/java/ch/naviqore/raptor/router/RaptorRouter.java b/src/main/java/ch/naviqore/raptor/router/RaptorRouter.java index 80c2c388..7647b7f1 100644 --- a/src/main/java/ch/naviqore/raptor/router/RaptorRouter.java +++ b/src/main/java/ch/naviqore/raptor/router/RaptorRouter.java @@ -28,15 +28,29 @@ class RaptorRouter implements RaptorAlgorithm, RaptorData { @Getter private final RouteTraversal routeTraversal; + @Getter + private final StopTimeProvider stopTimeProvider; + + private final RaptorConfig config; + private final InputValidator validator; - RaptorRouter(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal) { + RaptorRouter(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal, RaptorConfig config) { this.lookup = lookup; this.stopContext = stopContext; this.routeTraversal = routeTraversal; + this.config = config; + config.getMaskProvider().setTripIds(lookup.routeTripIds()); + this.stopTimeProvider = new StopTimeProvider(this, config.getMaskProvider(), config.getStopTimeCacheSize(), + config.getStopTimeCacheStrategy()); validator = new InputValidator(lookup.stops()); } + @Override + public void prepareStopTimesForDate(LocalDate date) { + stopTimeProvider.getStopTimesForDate(date); + } + @Override public List routeEarliestArrival(Map departureStops, Map arrivalStops, QueryConfig config) { @@ -68,14 +82,15 @@ public Map routeIsolines(Map sourceSt InputValidator.validateSourceStopTimes(sourceStops); log.info("Routing isolines from {} with {}", sourceStops.keySet(), timeType); - LocalDate referenceDate = DateTimeUtils.getReferenceDate(sourceStops, timeType); + LocalDateTime referenceDateTime = DateTimeUtils.getReferenceDate(sourceStops, timeType); + LocalDate referenceDate = referenceDateTime.toLocalDate(); Map validatedSourceStopIdx = validator.validateStopsAndGetIndices( DateTimeUtils.mapLocalDateTimeToTimestamp(sourceStops, referenceDate)); int[] sourceStopIndices = validatedSourceStopIdx.keySet().stream().mapToInt(Integer::intValue).toArray(); int[] refStopTimes = validatedSourceStopIdx.values().stream().mapToInt(Integer::intValue).toArray(); List bestLabelsPerRound = new Query(this, sourceStopIndices, new int[]{}, - refStopTimes, new int[]{}, config, timeType).run(); + refStopTimes, new int[]{}, config, timeType, referenceDateTime, this.config).run(); return new LabelPostprocessor(this, timeType).reconstructIsolines(bestLabelsPerRound, referenceDate); } @@ -98,7 +113,8 @@ public Map routeIsolines(Map sourceSt private List getConnections(Map sourceStops, Map targetStops, TimeType timeType, QueryConfig config) { InputValidator.validateSourceStopTimes(sourceStops); - LocalDate referenceDate = DateTimeUtils.getReferenceDate(sourceStops, timeType); + LocalDateTime referenceDateTime = DateTimeUtils.getReferenceDate(sourceStops, timeType); + LocalDate referenceDate = referenceDateTime.toLocalDate(); Map sourceStopsSecondsOfDay = DateTimeUtils.mapLocalDateTimeToTimestamp(sourceStops, referenceDate); Map validatedSourceStops = validator.validateStopsAndGetIndices(sourceStopsSecondsOfDay); @@ -111,7 +127,7 @@ private List getConnections(Map sourceStops, int[] walkingDurationsToTarget = validatedTargetStops.values().stream().mapToInt(Integer::intValue).toArray(); List bestLabelsPerRound = new Query(this, sourceStopIndices, targetStopIndices, - sourceTimes, walkingDurationsToTarget, config, timeType).run(); + sourceTimes, walkingDurationsToTarget, config, timeType, referenceDateTime, this.config).run(); return new LabelPostprocessor(this, timeType).reconstructParetoOptimalSolutions(bestLabelsPerRound, validatedTargetStops, referenceDate); @@ -190,7 +206,7 @@ private Map validateStopsAndGetIndices(Map st if (stopsToIdx.containsKey(stopId)) { validStopIds.put(stopsToIdx.get(stopId), time); } else { - log.warn("Stop ID {} not found in lookup removing from query.", entry.getKey()); + log.debug("Stop ID {} not found in lookup removing from query.", entry.getKey()); } } diff --git a/src/main/java/ch/naviqore/raptor/router/RaptorRouterBuilder.java b/src/main/java/ch/naviqore/raptor/router/RaptorRouterBuilder.java index a7c930a6..211d26e9 100644 --- a/src/main/java/ch/naviqore/raptor/router/RaptorRouterBuilder.java +++ b/src/main/java/ch/naviqore/raptor/router/RaptorRouterBuilder.java @@ -8,8 +8,6 @@ import static ch.naviqore.raptor.router.StopLabelsAndTimes.NO_INDEX; -// TODO remove duplicated step of generating same stop transfers - /** * Builds the Raptor and its internal data structures. Ensures that all stops, routes, trips, stop times, and transfers * are correctly added and validated before constructing the Raptor model: @@ -26,7 +24,7 @@ @Slf4j public class RaptorRouterBuilder { - private final int defaultSameStopTransferTime; + private final RaptorConfig config; private final Map stops = new HashMap<>(); private final Map routeBuilders = new HashMap<>(); private final Map> transfers = new HashMap<>(); @@ -37,8 +35,8 @@ public class RaptorRouterBuilder { int routeStopSize = 0; int transferSize = 0; - public RaptorRouterBuilder(int defaultSameStopTransferTime) { - this.defaultSameStopTransferTime = defaultSameStopTransferTime; + public RaptorRouterBuilder(RaptorConfig config) { + this.config = config; } public RaptorRouterBuilder addStop(String id) { @@ -111,7 +109,7 @@ public RaptorRouterBuilder addTransfer(String sourceStopId, String targetStopId, } public RaptorAlgorithm build() { - log.info("Initialize Raptor with {} stops, {} routes, {} route stops, {} stop times, {} transfers", + log.info("Initializing Raptor with {} stops, {} routes, {} route stops, {} stop times, {} transfers", stops.size(), routeBuilders.size(), routeStopSize, stopTimeSize, transferSize); // build route containers and the raptor array-based data structures @@ -120,7 +118,7 @@ public RaptorAlgorithm build() { StopContext stopContext = buildStopContext(lookup); RouteTraversal routeTraversal = buildRouteTraversal(routeContainers); - return new RaptorRouter(lookup, stopContext, routeTraversal); + return new RaptorRouter(lookup, stopContext, routeTraversal, config); } private @NotNull List buildAndSortRouteContainers() { @@ -130,14 +128,16 @@ public RaptorAlgorithm build() { private Lookup buildLookup(List routeContainers) { log.debug("Building lookup with {} stops and {} routes", stops.size(), routeContainers.size()); Map routes = new HashMap<>(routeContainers.size()); + Map routeTripIds = new HashMap<>(); // assign idx to routes based on sorted order for (int i = 0; i < routeContainers.size(); i++) { RouteBuilder.RouteContainer routeContainer = routeContainers.get(i); routes.put(routeContainer.id(), i); + routeTripIds.put(routeContainer.id(), routeContainer.trips().keySet().toArray(new String[0])); } - return new Lookup(Map.copyOf(stops), Map.copyOf(routes)); + return new Lookup(Map.copyOf(stops), Map.copyOf(routes), Map.copyOf(routeTripIds)); } private StopContext buildStopContext(Lookup lookup) { @@ -165,7 +165,7 @@ private StopContext buildStopContext(Lookup lookup) { List currentTransfers = transfers.get(stopId); int numberOfTransfers = currentTransfers == null ? 0 : currentTransfers.size(); - int sameStopTransferTime = sameStopTransfers.getOrDefault(stopId, defaultSameStopTransferTime); + int sameStopTransferTime = sameStopTransfers.getOrDefault(stopId, config.getDefaultSameStopTransferTime()); // add stop entry to stop array stopArr[stopIdx] = new Stop(stopId, stopRouteIdx, currentStopRoutes.size(), sameStopTransferTime, @@ -194,11 +194,16 @@ private RouteTraversal buildRouteTraversal(List rou // allocate arrays in needed size Route[] routeArr = new Route[routeContainers.size()]; RouteStop[] routeStopArr = new RouteStop[routeStopSize]; - StopTime[] stopTimeArr = new StopTime[stopTimeSize]; + int[] stopTimeArr = new int[2 + (stopTimeSize * 2) + (routeContainers.size() * 2)]; // iterate over routes and populate arrays int routeStopCnt = 0; - int stopTimeCnt = 0; + + // placeholders for min/max value of day + stopTimeArr[0] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + stopTimeArr[1] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + + int stopTimeCnt = 2; for (int routeIdx = 0; routeIdx < routeContainers.size(); routeIdx++) { RouteBuilder.RouteContainer routeContainer = routeContainers.get(routeIdx); @@ -208,6 +213,10 @@ private RouteTraversal buildRouteTraversal(List rou routeArr[routeIdx] = new Route(routeContainer.id(), routeStopCnt, numberOfStops, stopTimeCnt, numberOfTrips, routeContainer.trips().keySet().toArray(new String[0])); + // will be route day min/max values + stopTimeArr[stopTimeCnt++] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + stopTimeArr[stopTimeCnt++] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + // add stops to route stop array Map stopSequence = routeContainer.stopSequence(); for (int position = 0; position < numberOfStops; position++) { @@ -218,7 +227,8 @@ private RouteTraversal buildRouteTraversal(List rou // add times to stop time array for (StopTime[] stopTimes : routeContainer.trips().values()) { for (StopTime stopTime : stopTimes) { - stopTimeArr[stopTimeCnt++] = stopTime; + stopTimeArr[stopTimeCnt++] = stopTime.arrival(); + stopTimeArr[stopTimeCnt++] = stopTime.departure(); } } } diff --git a/src/main/java/ch/naviqore/raptor/router/RaptorTripMaskProvider.java b/src/main/java/ch/naviqore/raptor/router/RaptorTripMaskProvider.java new file mode 100644 index 00000000..dd58fb7a --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/router/RaptorTripMaskProvider.java @@ -0,0 +1,68 @@ +package ch.naviqore.raptor.router; + +import java.time.LocalDate; +import java.util.Map; + +/** + * Interface to provide trip masks for the raptor routing. + *

+ * The trip mask provider should be able to provide information if a route trip is taking place on a given date. + * Internally this will then be used to create a stop time array for the route scanner. + */ +public interface RaptorTripMaskProvider { + /** + * Set the trip ids for each route. + *

+ * This method is called when the raptor data is loaded by the raptor instance. And passes the reference to the trip + * mask provider containing a map of route ids to an array of trip ids. + * + * @param routeTripIds a map of route ids to an array of trip ids. + */ + void setTripIds(Map routeTripIds); + + /** + * Get the service id for a date. + *

+ * Each date has a service id associated with it. This service id is used to cache the trip mask for the given date. + * And allow using the same trip mask for multiple dates if the service id is the same. + * + * @param date the date for which the service id should be returned. + * @return the service id for the given date. + */ + String getServiceIdForDate(LocalDate date); + + /** + * Get the trip mask for a given date. + *

+ * This method should return a map of route ids to trip masks for the given date. + * + * @param date the date for which the trip mask should be returned. + * @return the raptor day mask of the day. + */ + DayTripMask getDayTripMask(LocalDate date); + + /** + * This represents a service day trip mask for a given day. + *

+ * The service day mask holds the date it's valid for, a serviceId which can be identical for multiple days if the + * service is the same. And a map of route ids to {@link RouteTripMask}. + * + * @param serviceId the service id for the day + * @param date the date of the day + * @param tripMask a map of route ids to route trip masks for the day + */ + record DayTripMask(String serviceId, LocalDate date, Map tripMask) { + } + + /** + * Represents a route trip mask for a given day and route. + * + * @param routeTripMask the route trip mask for the day, where each index represents trip (sorted by departure + * times) and the boolean value at that index indicates if the trip is taking place on the + * given day. + */ + record RouteTripMask(boolean[] routeTripMask) { + public static final int NO_TRIP = Integer.MIN_VALUE; + } + +} diff --git a/src/main/java/ch/naviqore/raptor/router/RouteScanner.java b/src/main/java/ch/naviqore/raptor/router/RouteScanner.java index 26a20a62..0e7d8da6 100644 --- a/src/main/java/ch/naviqore/raptor/router/RouteScanner.java +++ b/src/main/java/ch/naviqore/raptor/router/RouteScanner.java @@ -4,6 +4,9 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @@ -15,39 +18,84 @@ @Slf4j class RouteScanner { + private static final int SECONDS_IN_DAY = 86400; + private final Stop[] stops; private final int[] stopRoutes; - private final StopTime[] stopTimes; private final Route[] routes; + private final int[] rawStopTimes; private final RouteStop[] routeStops; - private final StopLabelsAndTimes stopLabelsAndTimes; - /** - * the minimum transfer duration time, since this is intended as rest period it is added to the walk time. - */ private final int minTransferDuration; private final TimeType timeType; + private final int[][] stopTimes; + private final int actualDaysToScan; + private final int startDayOffset; + /** * @param stopLabelsAndTimes the best time per stop and label per stop and round. * @param raptorData the current raptor data structures. * @param minimumTransferDuration The minimum transfer duration time. * @param timeType the time type (arrival or departure). + * @param referenceDateTime the reference date for the query. + * @param maxDaysToScan the maximum number of days to scan. */ RouteScanner(StopLabelsAndTimes stopLabelsAndTimes, RaptorData raptorData, int minimumTransferDuration, - TimeType timeType) { + TimeType timeType, LocalDateTime referenceDateTime, int maxDaysToScan) { // constant data structures this.stops = raptorData.getStopContext().stops(); this.stopRoutes = raptorData.getStopContext().stopRoutes(); - this.stopTimes = raptorData.getRouteTraversal().stopTimes(); this.routes = raptorData.getRouteTraversal().routes(); + this.rawStopTimes = raptorData.getRouteTraversal().stopTimes(); this.routeStops = raptorData.getRouteTraversal().routeStops(); // note: will also change outside of scanner, due to footpath relaxation this.stopLabelsAndTimes = stopLabelsAndTimes; // constant configuration of scanner this.minTransferDuration = minimumTransferDuration; this.timeType = timeType; + + LocalDate referenceDate = referenceDateTime.toLocalDate(); + + if (maxDaysToScan < 1) { + throw new IllegalArgumentException("maxDaysToScan must be greater than 0."); + } else if (maxDaysToScan == 1) { + stopTimes = new int[1][]; + stopTimes[0] = raptorData.getStopTimeProvider().getStopTimesForDate(referenceDate); + actualDaysToScan = 1; + startDayOffset = 0; + } else { + // there is no need to scan the next day for arrival trips but previous day is maybe needed in departure trips + if (timeType == TimeType.DEPARTURE) { + LocalDate previousDay = referenceDate.minusDays(1); + int[] previousDayStopTimes = raptorData.getStopTimeProvider().getStopTimesForDate(previousDay); + + int departureTimeInPreviousDaySeconds = (int) Duration.between(previousDay.atStartOfDay(), + referenceDateTime).getSeconds(); + + // if latest stop time of previous day is after / equal the departure time, we need to include the + // previous day to scanning + if (previousDayStopTimes[1] >= departureTimeInPreviousDaySeconds) { + startDayOffset = -1; + actualDaysToScan = maxDaysToScan; + } else { + startDayOffset = 0; + actualDaysToScan = maxDaysToScan - 1; + } + } else { + actualDaysToScan = maxDaysToScan - 1; + startDayOffset = 0; + } + + stopTimes = new int[actualDaysToScan][]; + for (int i = 0; i < actualDaysToScan; i++) { + int dayOffset = i + startDayOffset; + LocalDate date = timeType == TimeType.DEPARTURE ? referenceDate.plusDays( + dayOffset) : referenceDate.minusDays(dayOffset); + stopTimes[i] = raptorData.getStopTimeProvider().getStopTimesForDate(date); + } + } } /** @@ -108,6 +156,11 @@ private void scanRoute(int currentRouteIdx, int round, Set markedStops, final int firstStopTimeIdx = currentRoute.firstStopTimeIdx(); final int numberOfStops = currentRoute.numberOfStops(); + if (!isRouteActiveInDaysToScan(currentRoute)) { + log.debug("Route {} is not active in time range.", currentRoute.id()); + return; + } + ActiveTrip activeTrip = null; int startOffset = forward ? 0 : numberOfStops - 1; @@ -118,16 +171,18 @@ private void scanRoute(int currentRouteIdx, int round, Set markedStops, int stopIdx = routeStops[firstRouteStopIdx + stopOffset].stopIndex(); Stop stop = stops[stopIdx]; int bestStopTime = stopLabelsAndTimes.getComparableBestTime(stopIdx); - // find first marked stop in route if (activeTrip == null) { - if (!canEnterAtStop(stop, bestStopTime, markedStops, stopIdx, stopOffset, numberOfStops)) { + if (!canEnterAtStop(stop, bestStopTime, markedStops, stopIdx, stopOffset, currentRoute)) { continue; } } else { // in this case we are on a trip and need to check if time has improved - StopTime stopTimeObj = stopTimes[firstStopTimeIdx + activeTrip.tripOffset * numberOfStops + stopOffset]; - if (!checkIfTripIsPossibleAndUpdateMarks(stopTimeObj, activeTrip, stop, bestStopTime, stopIdx, round, + int stopTimeIndex = firstStopTimeIdx + 2 * (activeTrip.tripOffset * numberOfStops + stopOffset) + 2; + // the stopTimeIndex points to the arrival time of the stop and stopTimeIndex + 1 to the departure time + int targetTime = rawStopTimes[(timeType == TimeType.DEPARTURE) ? stopTimeIndex : stopTimeIndex + 1]; + targetTime += activeTrip.dayTimeOffset; + if (!checkIfTripIsPossibleAndUpdateMarks(targetTime, activeTrip, stop, bestStopTime, stopIdx, round, lastRound, markedStopsNext, currentRouteIdx)) { continue; } @@ -136,21 +191,32 @@ private void scanRoute(int currentRouteIdx, int round, Set markedStops, } } + private boolean isRouteActiveInDaysToScan(Route route) { + for (int i = 0; i < actualDaysToScan; i++) { + int stopTimeStartIndex = route.firstStopTimeIdx(); + // This means the earliest and latest trip time are set for the route (route is active on given day) + if (stopTimes[i][stopTimeStartIndex] != RaptorTripMaskProvider.RouteTripMask.NO_TRIP && stopTimes[i][stopTimeStartIndex + 1] != RaptorTripMaskProvider.RouteTripMask.NO_TRIP) { + return true; + } + } + return false; + } + /** * This method checks if a trip can be entered at the stop in the current round. A trip can be entered if the stop * was reached in a previous round, and is not the first (targetTime) / last (sourceTime) stop of a trip or (for * performance reasons) assuming that this check is only run when not travelling with an active trip, the stop was * not marked in a previous round (i.e., the lasts round trip query would be repeated). * - * @param stop the stop to check if a trip can be entered. - * @param stopTime the time at the stop. - * @param markedStops the set of marked stops from the previous round. - * @param stopIdx the index of the stop to check if a trip can be entered. - * @param stopOffset the offset of the stop in the route. - * @param numberOfStops the number of stops in the route. + * @param stop the stop to check if a trip can be entered. + * @param stopTime the time at the stop. + * @param markedStops the set of marked stops from the previous round. + * @param stopIdx the index of the stop to check if a trip can be entered. + * @param stopOffset the offset of the stop in the route. + * @param currentRoute the current route. */ private boolean canEnterAtStop(Stop stop, int stopTime, Set markedStops, int stopIdx, int stopOffset, - int numberOfStops) { + Route currentRoute) { int unreachableValue = timeType == TimeType.DEPARTURE ? INFINITY : -INFINITY; if (stopTime == unreachableValue) { @@ -158,13 +224,22 @@ private boolean canEnterAtStop(Stop stop, int stopTime, Set markedStops return false; } + int furthestStopTime = getFurthestTripTimeOfRoute(currentRoute, timeType); + if (timeType == TimeType.DEPARTURE && furthestStopTime < stopTime) { + log.debug("No trips departing after best stop time on route {} for stop {}", currentRoute.id(), stop.id()); + return false; + } else if (timeType == TimeType.ARRIVAL && furthestStopTime > stopTime) { + log.debug("No trips arriving before best stop time on route {} for stop {}", currentRoute.id(), stop.id()); + return false; + } + if (!markedStops.contains(stopIdx)) { // this stop has already been scanned in previous round without improved target time log.debug("Stop {} was not improved in previous round, continue", stop.id()); return false; } - if (timeType == TimeType.DEPARTURE && (stopOffset + 1 == numberOfStops)) { + if (timeType == TimeType.DEPARTURE && (stopOffset + 1 == currentRoute.numberOfStops())) { // last stop in route, does not make sense to check for trip to enter log.debug("Stop {} is last stop in route, continue", stop.id()); return false; @@ -180,6 +255,31 @@ private boolean canEnterAtStop(Stop stop, int stopTime, Set markedStops return true; } + /** + * Get the latest possible stop time for a route on all days to scan (for time type DEPARTURE) or the earliest + * possible stop time for a route on all days to scan (for time type ARRIVAL). + *

+ * Returns -INFINITY for DEPARTURE and INFINITY for ARRIVAL if no trip is possible. + * + * @param route the route to get the furthest trip time from. + * @param timeType the time type (arrival or departure). + * @return the furthest trip time of the route. + */ + private int getFurthestTripTimeOfRoute(Route route, TimeType timeType) { + // get index of latest trip for departure and earliest trip for arrival + int stopTimeIdx = timeType == TimeType.DEPARTURE ? route.firstStopTimeIdx() + 1 : route.firstStopTimeIdx(); + for (int dayIndex = stopTimes.length - 1; dayIndex >= 0; dayIndex--) { + int dayOffset = dayIndex + startDayOffset; + int time = stopTimes[dayIndex][stopTimeIdx]; + if (time != RaptorTripMaskProvider.RouteTripMask.NO_TRIP) { + int timeOffset = (timeType == TimeType.DEPARTURE ? 1 : -1) * dayOffset * SECONDS_IN_DAY; + return time + timeOffset; + } + } + + return timeType == TimeType.DEPARTURE ? -INFINITY : INFINITY; + } + /** *

This method checks if the time at a stop can be improved by arriving or departing with the active trip, if so * the stop is marked for the next round and the time is updated. If the time is improved it is clear that an @@ -187,7 +287,7 @@ private boolean canEnterAtStop(Stop stop, int stopTime, Set markedStops *

If the time was not improved, an additional check will be needed to figure out if an earlier or later trip * from the stop is possible within the current round, thus the method returns true.

* - * @param stopTime the stop time to check for an earlier or later trip. + * @param targetTime the stop time to check for an earlier or later trip. * @param activeTrip the active trip to check for an earlier or later trip. * @param stop the stop to check for an earlier or later trip. * @param bestStopTime the earliest or latest time at the stop based on the TimeType. @@ -196,19 +296,17 @@ private boolean canEnterAtStop(Stop stop, int stopTime, Set markedStops * @param currentRouteIdx the index of the current route. * @return true if an earlier or later trip is possible, false otherwise. */ - private boolean checkIfTripIsPossibleAndUpdateMarks(StopTime stopTime, ActiveTrip activeTrip, Stop stop, + private boolean checkIfTripIsPossibleAndUpdateMarks(int targetTime, ActiveTrip activeTrip, Stop stop, int bestStopTime, int stopIdx, int thisRound, int lastRound, Set markedStopsNext, int currentRouteIdx) { - boolean isImproved = (timeType == TimeType.DEPARTURE) ? stopTime.arrival() < bestStopTime : stopTime.departure() > bestStopTime; + boolean isImproved = (timeType == TimeType.DEPARTURE) ? targetTime < bestStopTime : targetTime > bestStopTime; if (isImproved) { log.debug("Stop {} was improved", stop.id()); - stopLabelsAndTimes.setBestTime(stopIdx, - (timeType == TimeType.DEPARTURE) ? stopTime.arrival() : stopTime.departure()); + stopLabelsAndTimes.setBestTime(stopIdx, targetTime); - StopLabelsAndTimes.Label label = new StopLabelsAndTimes.Label(activeTrip.entryTime(), - (timeType == TimeType.DEPARTURE) ? stopTime.arrival() : stopTime.departure(), + StopLabelsAndTimes.Label label = new StopLabelsAndTimes.Label(activeTrip.entryTime, targetTime, StopLabelsAndTimes.LabelType.ROUTE, currentRouteIdx, activeTrip.tripOffset, stopIdx, activeTrip.previousLabel); stopLabelsAndTimes.setLabel(thisRound, stopIdx, label); @@ -219,7 +317,7 @@ private boolean checkIfTripIsPossibleAndUpdateMarks(StopTime stopTime, ActiveTri log.debug("Stop {} was not improved", stop.id()); StopLabelsAndTimes.Label previous = stopLabelsAndTimes.getLabel(lastRound, stopIdx); - boolean isImprovedInSameRound = (timeType == TimeType.DEPARTURE) ? previous == null || previous.targetTime() >= stopTime.arrival() : previous == null || previous.targetTime() <= stopTime.departure(); + boolean isImprovedInSameRound = previous == null || ((timeType == TimeType.DEPARTURE) ? previous.targetTime() >= targetTime : previous.targetTime() <= targetTime); if (isImprovedInSameRound) { log.debug("Stop {} has been improved in same round, trip not possible within this round", stop.id()); return false; @@ -231,9 +329,9 @@ private boolean checkIfTripIsPossibleAndUpdateMarks(StopTime stopTime, ActiveTri } /** - * Find the possible trip on the route. This loops through all trips departing or arriving from a given stop for a - * given route and returns details about the first or last trip that can be taken (departing after or arriving - * before the time of the previous round at this stop and accounting for transfer constraints). + * Find the possible trip on the route for the given trip mask. This loops through all trips departing or arriving + * from a given stop for a given route and returns details about the first or last trip that can be taken (departing + * after or arriving before the time of the previous round at this stop and accounting for transfer constraints). * * @param stopIdx the index of the stop to find the possible trip from. * @param stop the stop to find the possible trip from. @@ -247,8 +345,6 @@ private boolean checkIfTripIsPossibleAndUpdateMarks(StopTime stopTime, ActiveTri int numberOfStops = route.numberOfStops(); int numberOfTrips = route.numberOfTrips(); - int tripOffset = (timeType == TimeType.DEPARTURE) ? 0 : numberOfTrips - 1; - int entryTime = 0; StopLabelsAndTimes.Label previousLabel = stopLabelsAndTimes.getLabel(lastRound, stopIdx); // this is the reference time, where we can depart after or arrive earlier @@ -258,25 +354,41 @@ private boolean checkIfTripIsPossibleAndUpdateMarks(StopTime stopTime, ActiveTri minTransferDuration) : -Math.max(stop.sameStopTransferTime(), minTransferDuration); } - while ((timeType == TimeType.DEPARTURE) ? tripOffset < numberOfTrips : tripOffset >= 0) { - StopTime currentStopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset]; - if ((timeType == TimeType.DEPARTURE) ? currentStopTime.departure() >= referenceTime : currentStopTime.arrival() <= referenceTime) { - log.debug("Found active trip ({}) on route {}", tripOffset, route.id()); - entryTime = (timeType == TimeType.DEPARTURE) ? currentStopTime.departure() : currentStopTime.arrival(); - break; + for (int dayIndex = 0; dayIndex < actualDaysToScan; dayIndex++) { + int dayOffset = dayIndex + startDayOffset; + int timeOffset = (timeType == TimeType.DEPARTURE ? 1 : -1) * dayOffset * SECONDS_IN_DAY; + int earliestTripTime = stopTimes[dayIndex][firstStopTimeIdx] + timeOffset; + int latestTripTime = stopTimes[dayIndex][firstStopTimeIdx + 1] + timeOffset; + + // check if the day has any trips relevant + if ((timeType == TimeType.DEPARTURE ? latestTripTime < referenceTime : referenceTime < earliestTripTime)) { + log.debug("No usable trips on route {} for stop {} on day {}", route.id(), stop.id(), dayIndex); + continue; } - if ((timeType == TimeType.DEPARTURE) ? tripOffset < numberOfTrips - 1 : tripOffset > 0) { - tripOffset += (timeType == TimeType.DEPARTURE) ? 1 : -1; - } else { - // no active trip found - log.debug("No active trip found on route {}", route.id()); - return null; + + for (int i = 0; i < numberOfTrips; i++) { + int tripOffset = (timeType == TimeType.DEPARTURE) ? i : numberOfTrips - 1 - i; + int stopTimeIndex = firstStopTimeIdx + 2 * (tripOffset * numberOfStops + stopOffset) + 2; + // the stopTimeIndex points to the arrival time of the stop and stopTimeIndex + 1 to the departure time + int relevantStopTime = stopTimes[dayIndex][(timeType == TimeType.DEPARTURE) ? stopTimeIndex + 1 : stopTimeIndex]; + // Trip is not active + if (relevantStopTime == RaptorTripMaskProvider.RouteTripMask.NO_TRIP) { + continue; + } + relevantStopTime += timeOffset; + if ((timeType == TimeType.DEPARTURE) ? relevantStopTime >= referenceTime : relevantStopTime <= referenceTime) { + log.debug("Found active trip ({}) on route {}", i, route.id()); + return new ActiveTrip(tripOffset, relevantStopTime, timeOffset, previousLabel); + } } } - return new ActiveTrip(tripOffset, entryTime, previousLabel); + // no active trip found + log.debug("No active trip found on route {}", route.id()); + return null; } - private record ActiveTrip(int tripOffset, int entryTime, StopLabelsAndTimes.Label previousLabel) { + private record ActiveTrip(int tripOffset, int entryTime, int dayTimeOffset, + StopLabelsAndTimes.Label previousLabel) { } } diff --git a/src/main/java/ch/naviqore/raptor/router/RouteTraversal.java b/src/main/java/ch/naviqore/raptor/router/RouteTraversal.java index 8d8d67bf..e10e22da 100644 --- a/src/main/java/ch/naviqore/raptor/router/RouteTraversal.java +++ b/src/main/java/ch/naviqore/raptor/router/RouteTraversal.java @@ -7,5 +7,5 @@ * @param routes routes * @param routeStops route stops */ -record RouteTraversal(StopTime[] stopTimes, Route[] routes, RouteStop[] routeStops) { +record RouteTraversal(int[] stopTimes, Route[] routes, RouteStop[] routeStops) { } diff --git a/src/main/java/ch/naviqore/raptor/router/StopTimeProvider.java b/src/main/java/ch/naviqore/raptor/router/StopTimeProvider.java new file mode 100644 index 00000000..c1bcc20d --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/router/StopTimeProvider.java @@ -0,0 +1,122 @@ +package ch.naviqore.raptor.router; + +import ch.naviqore.utils.cache.EvictionCache; + +import java.time.LocalDate; +import java.util.Map; + +/** + * Provider for stop time int arrays for a given date. + *

+ * This provider uses the {@link RaptorTripMaskProvider} to create the stop time arrays for a given date. The stop time + * arrays are then cached based on the service id of the date, allowing to handle multiple days with same service id + * efficiently. + */ +class StopTimeProvider { + + /** + * The cache for the stop times. Stop time arrays are mapped to service ids, because multiple dates may have the + * same service id. + */ + private final EvictionCache stopTimeCache; + + private final RaptorData data; + private final RaptorTripMaskProvider tripMaskProvider; + + StopTimeProvider(RaptorData data, RaptorTripMaskProvider tripMaskProvider, int cacheSize, + EvictionCache.Strategy cacheStrategy) { + this.data = data; + this.tripMaskProvider = tripMaskProvider; + this.stopTimeCache = new EvictionCache<>(cacheSize, cacheStrategy); + } + + /** + * Create the stop times for a given date. + *

+ * The stop time array is built based on the trip mask provided by the {@link RaptorTripMaskProvider} and is + * structured as follows: + *

    + *
  • 0: earliest overall stop time (in seconds relative to service date)
  • + *
  • 1: latest overall stop time (in seconds relative to service date)
  • + *
  • n: for each route the stop times are stored in the following order: + *
      + *
    • 0: earliest route stop time of day(in seconds relative to service date)
    • + *
    • 1: latest route stop time od day (in seconds relative to service date)
    • + *
    • n: each trip of the route stored as a sequence of 2 x number of stops on trip, in following logic: + * stop 1: arrival time, stop 1: departure time, stop 2 arrival time, stop 2 departure time, ... + * + * @param date the date for which the stop times should be created (or retrieved from cache) + * @return the stop times for the given date. + */ + int[] getStopTimesForDate(LocalDate date) { + String serviceId = tripMaskProvider.getServiceIdForDate(date); + return stopTimeCache.computeIfAbsent(serviceId, () -> createStopTimesForDate(date)); + } + + private int[] createStopTimesForDate(LocalDate date) { + RaptorTripMaskProvider.DayTripMask mask = tripMaskProvider.getDayTripMask(date); + + int[] originalStopTimesArray = data.getRouteTraversal().stopTimes(); + int[] newStopTimesArray = new int[originalStopTimesArray.length]; + + // set the global start and end times for the day (initially set to NO_TRIP) + newStopTimesArray[0] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + newStopTimesArray[1] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + + // set the stop times for each route + for (Map.Entry entry : mask.tripMask().entrySet()) { + String routeId = entry.getKey(); + RaptorTripMaskProvider.RouteTripMask tripMask = entry.getValue(); + int routeIdx = data.getLookup().routes().get(routeId); + Route route = data.getRouteTraversal().routes()[routeIdx]; + int numStops = route.numberOfStops(); + int stopTimeIndex = route.firstStopTimeIdx(); + + boolean[] booleanMask = tripMask.routeTripMask(); + + int earliestRouteStopTime = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + int latestRouteStopTime = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + + int tripOffset = 0; + for (boolean tripActive : booleanMask) { + for (int stopOffset = 0; stopOffset < numStops; stopOffset++) { + int arrivalIndex = stopTimeIndex + (tripOffset * numStops * 2) + stopOffset * 2 + 2; + int departureIndex = arrivalIndex + 1; + // arrival and departure + if (tripActive) { + newStopTimesArray[arrivalIndex] = originalStopTimesArray[arrivalIndex]; + newStopTimesArray[departureIndex] = originalStopTimesArray[departureIndex]; + if (earliestRouteStopTime == RaptorTripMaskProvider.RouteTripMask.NO_TRIP) { + earliestRouteStopTime = originalStopTimesArray[arrivalIndex]; + } + latestRouteStopTime = originalStopTimesArray[departureIndex]; + + } else { + newStopTimesArray[arrivalIndex] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + newStopTimesArray[departureIndex] = RaptorTripMaskProvider.RouteTripMask.NO_TRIP; + } + } + tripOffset++; + } + + // set the earliest and latest stop times for the route + newStopTimesArray[stopTimeIndex] = earliestRouteStopTime; + newStopTimesArray[stopTimeIndex + 1] = latestRouteStopTime; + + // maybe update the global start/end times for day + if (earliestRouteStopTime != RaptorTripMaskProvider.RouteTripMask.NO_TRIP && latestRouteStopTime != RaptorTripMaskProvider.RouteTripMask.NO_TRIP) { + // set the global earliest stop time if not set or if the new time is earlier + if (newStopTimesArray[0] == RaptorTripMaskProvider.RouteTripMask.NO_TRIP || earliestRouteStopTime < newStopTimesArray[0]) { + newStopTimesArray[0] = earliestRouteStopTime; + } + // set the global latest stop time if not set or if the new time is later + if (newStopTimesArray[1] == RaptorTripMaskProvider.RouteTripMask.NO_TRIP || latestRouteStopTime > newStopTimesArray[1]) { + newStopTimesArray[1] = latestRouteStopTime; + } + } + + } + + return newStopTimesArray; + } +} diff --git a/src/main/java/ch/naviqore/service/config/ServiceConfig.java b/src/main/java/ch/naviqore/service/config/ServiceConfig.java index d94a1d11..3d57ff5c 100644 --- a/src/main/java/ch/naviqore/service/config/ServiceConfig.java +++ b/src/main/java/ch/naviqore/service/config/ServiceConfig.java @@ -22,6 +22,8 @@ public class ServiceConfig { public static final double DEFAULT_WALKING_SPEED = 1.4; public static final int DEFAULT_WALKING_DURATION_MINIMUM = 120; + public static final int DEFAULT_MAX_DAYS_TO_SCAN = 3; + public static final int DEFAULT_CACHE_SIZE = 5; public static final CacheEvictionStrategy DEFAULT_CACHE_EVICTION_STRATEGY = CacheEvictionStrategy.LRU; @@ -34,26 +36,28 @@ public class ServiceConfig { private final WalkCalculatorType walkingCalculatorType; private final double walkingSpeed; private final int walkingDurationMinimum; - private final int cacheSize; + private final int raptorDaysToScan; + private final int cacheServiceDaySize; private final CacheEvictionStrategy cacheEvictionStrategy; public ServiceConfig(String gtfsStaticUri, String gtfsStaticUpdateCron, int transferTimeSameStopDefault, int transferTimeBetweenStopsMinimum, int transferTimeAccessEgress, int walkingSearchRadius, WalkCalculatorType walkingCalculatorType, double walkingSpeed, int walkingDurationMinimum, - int cacheSize, CacheEvictionStrategy cacheEvictionStrategy) { + int raptorDaysToScan, int cacheServiceDaySize, CacheEvictionStrategy cacheEvictionStrategy) { this.gtfsStaticUri = validateNonNull(gtfsStaticUri, "gtfsStaticUrl"); this.gtfsStaticUpdateCron = validateNonNull(gtfsStaticUpdateCron, "gtfsStaticUpdateCron"); this.transferTimeSameStopDefault = validateNonNegative(transferTimeSameStopDefault, "transferTimeSameStopDefault"); - this.transferTimeBetweenStopsMinimum = validateNonNegative(transferTimeBetweenStopsMinimum, - "transferTimeBetweenStopsMinimum"); + // negative values imply that transfers should not be generated + this.transferTimeBetweenStopsMinimum = transferTimeBetweenStopsMinimum; this.transferTimeAccessEgress = validateNonNegative(transferTimeAccessEgress, "transferTimeAccessEgress"); this.walkingSearchRadius = validateNonNegative(walkingSearchRadius, "walkingSearchRadius"); this.walkingCalculatorType = validateNonNull(walkingCalculatorType, "walkingCalculatorType"); this.walkingSpeed = validatePositive(walkingSpeed, "walkingSpeed"); this.walkingDurationMinimum = validateNonNegative(walkingDurationMinimum, "walkingDurationMinimum"); - this.cacheSize = validatePositive(cacheSize, "cacheSize"); + this.raptorDaysToScan = validatePositive(raptorDaysToScan, "raptorDaysToScan"); + this.cacheServiceDaySize = validatePositive(cacheServiceDaySize, "cacheServiceDaySize"); this.cacheEvictionStrategy = validateNonNull(cacheEvictionStrategy, "cacheEvictionStrategy"); } @@ -64,7 +68,8 @@ public ServiceConfig(String gtfsStaticUri) { this(gtfsStaticUri, DEFAULT_GTFS_STATIC_UPDATE_CRON, DEFAULT_TRANSFER_TIME_SAME_STOP_DEFAULT, DEFAULT_TRANSFER_TIME_BETWEEN_STOPS_MINIMUM, DEFAULT_TRANSFER_TIME_ACCESS_EGRESS, DEFAULT_WALKING_SEARCH_RADIUS, DEFAULT_WALKING_CALCULATOR_TYPE, DEFAULT_WALKING_SPEED, - DEFAULT_WALKING_DURATION_MINIMUM, DEFAULT_CACHE_SIZE, DEFAULT_CACHE_EVICTION_STRATEGY); + DEFAULT_WALKING_DURATION_MINIMUM, DEFAULT_MAX_DAYS_TO_SCAN, DEFAULT_CACHE_SIZE, + DEFAULT_CACHE_EVICTION_STRATEGY); } private static T validateNonNull(T value, String name) { diff --git a/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java b/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java index bf665ddc..6514f332 100644 --- a/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java +++ b/src/main/java/ch/naviqore/service/impl/PublicTransitServiceImpl.java @@ -2,6 +2,7 @@ import ch.naviqore.gtfs.schedule.model.GtfsSchedule; import ch.naviqore.raptor.RaptorAlgorithm; +import ch.naviqore.raptor.router.RaptorConfig; import ch.naviqore.service.*; import ch.naviqore.service.config.ConnectionQueryConfig; import ch.naviqore.service.config.ServiceConfig; @@ -10,6 +11,7 @@ import ch.naviqore.service.exception.TripNotActiveException; import ch.naviqore.service.exception.TripNotFoundException; import ch.naviqore.service.impl.convert.GtfsToRaptorConverter; +import ch.naviqore.service.impl.convert.GtfsTripMaskProvider; import ch.naviqore.service.impl.transfer.TransferGenerator; import ch.naviqore.service.walk.WalkCalculator; import ch.naviqore.utils.cache.EvictionCache; @@ -36,8 +38,8 @@ public class PublicTransitServiceImpl implements PublicTransitService { private final KDTree spatialStopIndex; private final SearchIndex stopSearchIndex; private final WalkCalculator walkCalculator; - private final List additionalTransfers; - private final RaptorCache cache; + private final RaptorAlgorithm raptorAlgorithm; + private final GtfsTripMaskProvider tripMaskProvider; PublicTransitServiceImpl(ServiceConfig config, GtfsSchedule schedule, KDTree spatialStopIndex, @@ -48,11 +50,15 @@ public class PublicTransitServiceImpl implements PublicTransitService { this.spatialStopIndex = spatialStopIndex; this.stopSearchIndex = stopSearchIndex; this.walkCalculator = walkCalculator; - this.additionalTransfers = List.copyOf(additionalTransfers); - // initialize raptor instances cache - cache = new RaptorCache(config.getCacheSize(), - EvictionCache.Strategy.valueOf(config.getCacheEvictionStrategy().name())); + EvictionCache.Strategy cacheStrategy = EvictionCache.Strategy.valueOf(config.getCacheEvictionStrategy().name()); + tripMaskProvider = new GtfsTripMaskProvider(schedule, config.getCacheServiceDaySize(), cacheStrategy); + + // build raptor algorithm + RaptorConfig raptorConfig = new RaptorConfig(config.getRaptorDaysToScan(), + config.getTransferTimeSameStopDefault(), config.getCacheServiceDaySize(), cacheStrategy, + tripMaskProvider); + raptorAlgorithm = new GtfsToRaptorConverter(schedule, additionalTransfers, raptorConfig).convert(); } @Override @@ -186,12 +192,11 @@ private List getConnections(@Nullable ch.naviqore.gtfs.schedule.mode } // query connection from raptor - RaptorAlgorithm raptor = cache.getRaptor(time.toLocalDate()); List connections; if (isDeparture) { - connections = raptor.routeEarliestArrival(sourceStops, targetStops, map(config)); + connections = raptorAlgorithm.routeEarliestArrival(sourceStops, targetStops, map(config)); } else { - connections = raptor.routeLatestDeparture(targetStops, sourceStops, map(config)); + connections = raptorAlgorithm.routeLatestDeparture(targetStops, sourceStops, map(config)); } // assemble connection results @@ -210,7 +215,7 @@ private List getConnections(@Nullable ch.naviqore.gtfs.schedule.mode lastMile = getLastWalk(targetLocation, connection.getToStopId(), arrivalTime); } - Connection serviceConnection = map(connection, firstMile, lastMile, time.toLocalDate(), schedule); + Connection serviceConnection = map(connection, firstMile, lastMile, schedule); // Filter needed because the raptor algorithm does not consider the firstMile and lastMile walk time if (Duration.between(serviceConnection.getDepartureTime(), serviceConnection.getArrivalTime()) @@ -275,9 +280,7 @@ public Map getIsoLines(GeoCoordinate source, LocalDateTime tim Map sourceStops = getStopsWithWalkTimeFromLocation(source, time, config.getMaximumWalkingDuration(), timeType); - RaptorAlgorithm raptor = cache.getRaptor(time.toLocalDate()); - - return mapToStopConnectionMap(raptor.routeIsolines(sourceStops, map(timeType), map(config)), source, time, + return mapToStopConnectionMap(raptorAlgorithm.routeIsolines(sourceStops, map(timeType), map(config)), source, config, timeType); } @@ -285,15 +288,14 @@ public Map getIsoLines(GeoCoordinate source, LocalDateTime tim public Map getIsoLines(Stop source, LocalDateTime time, TimeType timeType, ConnectionQueryConfig config) { Map sourceStops = getAllChildStopsFromStop(source, time); - RaptorAlgorithm raptor = cache.getRaptor(time.toLocalDate()); - return mapToStopConnectionMap(raptor.routeIsolines(sourceStops, map(timeType), map(config)), null, time, config, - timeType); + return mapToStopConnectionMap(raptorAlgorithm.routeIsolines(sourceStops, map(timeType), map(config)), null, + config, timeType); } private Map mapToStopConnectionMap(Map isoLines, - @Nullable GeoCoordinate source, LocalDateTime startTime, - ConnectionQueryConfig config, TimeType timeType) { + @Nullable GeoCoordinate source, ConnectionQueryConfig config, + TimeType timeType) { Map result = new HashMap<>(); for (Map.Entry entry : isoLines.entrySet()) { @@ -308,7 +310,7 @@ private Map mapToStopConnectionMap(Map - * TODO: Not always create a new raptor, use mask on stop times based on active trips. - */ - private class RaptorCache { - - private final EvictionCache, RaptorAlgorithm> raptorCache; - private final EvictionCache> activeServices; - - /** - * @param cacheSize the maximum number of Raptor instances to be cached. - * @param strategy the cache eviction strategy. - */ - RaptorCache(int cacheSize, EvictionCache.Strategy strategy) { - raptorCache = new EvictionCache<>(cacheSize, strategy); - activeServices = new EvictionCache<>(Math.min(365, cacheSize * 20), strategy); - } - - // get cached or create and cache new raptor instance, based on the active calendars on a date - private RaptorAlgorithm getRaptor(LocalDate date) { - Set activeServices = this.activeServices.computeIfAbsent(date, - () -> getActiveServices(date)); - return raptorCache.computeIfAbsent(activeServices, - () -> new GtfsToRaptorConverter(schedule, additionalTransfers, - config.getTransferTimeSameStopDefault()).convert(date)); - } - - // get all active calendars form the gtfs for given date, serves as key for caching raptor instances - private Set getActiveServices(LocalDate date) { - return schedule.getCalendars() - .values() - .stream() - .filter(calendar -> calendar.isServiceAvailable(date)) - .collect(Collectors.toSet()); - } - - // clear the cache, needs to be called when the GTFS schedule changes - private void clear() { - activeServices.clear(); - raptorCache.clear(); - } - + // clear the trip mask cache, since new the cached instances are now outdated + tripMaskProvider.clearCache(); } } diff --git a/src/main/java/ch/naviqore/service/impl/PublicTransitServiceInitializer.java b/src/main/java/ch/naviqore/service/impl/PublicTransitServiceInitializer.java index 4bff9659..1a6c1e0f 100644 --- a/src/main/java/ch/naviqore/service/impl/PublicTransitServiceInitializer.java +++ b/src/main/java/ch/naviqore/service/impl/PublicTransitServiceInitializer.java @@ -40,8 +40,12 @@ public PublicTransitServiceInitializer(ServiceConfig config, GtfsSchedule schedu this.walkCalculator = initializeWalkCalculator(config); this.stopSearchIndex = generateStopSearchIndex(schedule); this.spatialStopIndex = generateSpatialStopIndex(schedule); - this.additionalTransfers = generateTransfers(schedule, - createTransferGenerators(config, walkCalculator, spatialStopIndex)); + if (config.getTransferTimeBetweenStopsMinimum() >= 0) { + this.additionalTransfers = generateTransfers(schedule, + createTransferGenerators(config, walkCalculator, spatialStopIndex)); + } else { + this.additionalTransfers = new ArrayList<>(); + } } private static WalkCalculator initializeWalkCalculator(ServiceConfig config) { diff --git a/src/main/java/ch/naviqore/service/impl/TypeMapper.java b/src/main/java/ch/naviqore/service/impl/TypeMapper.java index d9d066e3..db7fe79b 100644 --- a/src/main/java/ch/naviqore/service/impl/TypeMapper.java +++ b/src/main/java/ch/naviqore/service/impl/TypeMapper.java @@ -13,12 +13,15 @@ import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.List; @NoArgsConstructor(access = AccessLevel.NONE) final class TypeMapper { + private static final int SECONDS_IN_DAY = 86400; + public static Stop map(ch.naviqore.gtfs.schedule.model.Stop stop) { if (stop == null) { return null; @@ -72,7 +75,7 @@ public static Walk createWalk(int distance, int duration, WalkType walkType, Loc } public static Connection map(ch.naviqore.raptor.Connection connection, @Nullable Walk firstMile, - @Nullable Walk lastMile, LocalDate date, GtfsSchedule schedule) { + @Nullable Walk lastMile, GtfsSchedule schedule) { List legs = new ArrayList<>(); if (firstMile != null) { @@ -80,7 +83,7 @@ public static Connection map(ch.naviqore.raptor.Connection connection, @Nullable } for (ch.naviqore.raptor.Leg leg : connection.getLegs()) { - legs.add(map(leg, date, schedule)); + legs.add(map(leg, schedule)); } if (lastMile != null) { @@ -90,7 +93,7 @@ public static Connection map(ch.naviqore.raptor.Connection connection, @Nullable return new ConnectionImpl(legs); } - public static Leg map(ch.naviqore.raptor.Leg leg, LocalDate date, GtfsSchedule schedule) { + public static Leg map(ch.naviqore.raptor.Leg leg, GtfsSchedule schedule) { int duration = (int) Duration.between(leg.getDepartureTime(), leg.getArrivalTime()).toSeconds(); Stop sourceStop = map(schedule.getStops().get(leg.getFromStopId())); Stop targetStop = map(schedule.getStops().get(leg.getToStopId())); @@ -100,7 +103,7 @@ public static Leg map(ch.naviqore.raptor.Leg leg, LocalDate date, GtfsSchedule s case WALK_TRANSFER -> new TransferImpl(distance, duration, leg.getDepartureTime(), leg.getArrivalTime(), sourceStop, targetStop); - case ROUTE -> createPublicTransitLeg(leg, date, schedule, distance); + case ROUTE -> createPublicTransitLeg(leg, schedule, distance); }; } @@ -116,11 +119,11 @@ public static ch.naviqore.raptor.TimeType map(TimeType timeType) { }; } - private static Leg createPublicTransitLeg(ch.naviqore.raptor.Leg leg, LocalDate date, GtfsSchedule schedule, - int distance) { - int duration = (int) Duration.between(leg.getDepartureTime(), leg.getArrivalTime()).toSeconds(); + private static Leg createPublicTransitLeg(ch.naviqore.raptor.Leg leg, GtfsSchedule schedule, int distance) { ch.naviqore.gtfs.schedule.model.Trip gtfsTrip = schedule.getTrips().get(leg.getTripId()); - Trip trip = map(gtfsTrip, date); + LocalDate serviceDay = getServiceDay(leg, gtfsTrip); + int duration = (int) Duration.between(leg.getDepartureTime(), leg.getArrivalTime()).toSeconds(); + Trip trip = map(gtfsTrip, serviceDay); assert gtfsTrip.getStopTimes().size() == trip.getStopTimes() .size() : "GTFS trip and trip implementation in service must have the same number of stop times."; @@ -132,14 +135,16 @@ private static Leg createPublicTransitLeg(ch.naviqore.raptor.Leg leg, LocalDate var gtfsStopTime = gtfsTrip.getStopTimes().get(i); // if the from stop id and the departure time matches, set the departure stop time if (gtfsStopTime.stop().getId().equals(leg.getFromStopId()) && gtfsStopTime.departure() - .getTotalSeconds() == getSecondsOfDay(leg.getDepartureTime(), date)) { + .toLocalTime() + .equals(leg.getDepartureTime().toLocalTime())) { departure = trip.getStopTimes().get(i); continue; } // if the to stop id and the arrival time matches, set the arrival stop time if (gtfsStopTime.stop().getId().equals(leg.getToStopId()) && gtfsStopTime.arrival() - .getTotalSeconds() == getSecondsOfDay(leg.getArrivalTime(), date)) { + .toLocalTime() + .equals(leg.getArrivalTime().toLocalTime())) { arrival = trip.getStopTimes().get(i); break; } @@ -154,4 +159,16 @@ private static Leg createPublicTransitLeg(ch.naviqore.raptor.Leg leg, LocalDate private static int getSecondsOfDay(LocalDateTime time, LocalDate refDay) { return (int) Duration.between(refDay.atStartOfDay(), time).toSeconds(); } + + private static LocalDate getServiceDay(ch.naviqore.raptor.Leg leg, ch.naviqore.gtfs.schedule.model.Trip trip) { + String StopId = leg.getFromStopId(); + LocalTime departureTime = leg.getDepartureTime().toLocalTime(); + for (ch.naviqore.gtfs.schedule.model.StopTime stopTime : trip.getStopTimes()) { + if (stopTime.stop().getId().equals(StopId) && stopTime.departure().toLocalTime().equals(departureTime)) { + int dayShift = stopTime.departure().getTotalSeconds() / SECONDS_IN_DAY; + return leg.getDepartureTime().toLocalDate().minusDays(dayShift); + } + } + throw new IllegalStateException("Could not find service day for leg"); + } } diff --git a/src/main/java/ch/naviqore/service/impl/convert/GtfsRoutePartitioner.java b/src/main/java/ch/naviqore/service/impl/convert/GtfsRoutePartitioner.java index f47ddc47..0ba747fa 100644 --- a/src/main/java/ch/naviqore/service/impl/convert/GtfsRoutePartitioner.java +++ b/src/main/java/ch/naviqore/service/impl/convert/GtfsRoutePartitioner.java @@ -24,7 +24,7 @@ public class GtfsRoutePartitioner { public GtfsRoutePartitioner(GtfsSchedule schedule) { log.info("Partitioning GTFS schedule with {} routes into sub-routes", schedule.getRoutes().size()); schedule.getRoutes().values().forEach(this::processRoute); - log.info("Got {} sub-routes in schedule", subRoutes.values().stream().mapToInt(Map::size).sum()); + log.debug("Got {} sub-routes in schedule", subRoutes.values().stream().mapToInt(Map::size).sum()); } private void processRoute(Route route) { diff --git a/src/main/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverter.java b/src/main/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverter.java index af788b54..cfb1dd31 100644 --- a/src/main/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverter.java +++ b/src/main/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverter.java @@ -3,11 +3,12 @@ import ch.naviqore.gtfs.schedule.model.*; import ch.naviqore.gtfs.schedule.type.TransferType; import ch.naviqore.raptor.RaptorAlgorithm; +import ch.naviqore.raptor.router.RaptorConfig; import ch.naviqore.raptor.router.RaptorRouterBuilder; import ch.naviqore.service.impl.transfer.TransferGenerator; import lombok.extern.slf4j.Slf4j; -import java.time.LocalDate; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -32,23 +33,23 @@ public class GtfsToRaptorConverter { private final List additionalTransfers; private final GtfsSchedule schedule; - public GtfsToRaptorConverter(GtfsSchedule schedule, int sameStopTransferTime) { - this(schedule, List.of(), sameStopTransferTime); + public GtfsToRaptorConverter(GtfsSchedule schedule, RaptorConfig config) { + this(schedule, List.of(), config); } public GtfsToRaptorConverter(GtfsSchedule schedule, List additionalTransfers, - int sameStopTransferTime) { + RaptorConfig config) { this.partitioner = new GtfsRoutePartitioner(schedule); this.additionalTransfers = additionalTransfers; this.schedule = schedule; - this.builder = RaptorAlgorithm.builder(sameStopTransferTime); + this.builder = RaptorAlgorithm.builder(config); } - public RaptorAlgorithm convert(LocalDate date) { - List activeTrips = schedule.getActiveTrips(date); - log.info("Converting {} active trips from GTFS schedule to Raptor model", activeTrips.size()); + public RaptorAlgorithm convert() { + Collection trips = schedule.getTrips().values(); + log.info("Converting {} trips from GTFS schedule to Raptor model", trips.size()); - for (Trip trip : activeTrips) { + for (Trip trip : trips) { GtfsRoutePartitioner.SubRoute subRoute = partitioner.getSubRoute(trip); // add route if not already diff --git a/src/main/java/ch/naviqore/service/impl/convert/GtfsTripMaskProvider.java b/src/main/java/ch/naviqore/service/impl/convert/GtfsTripMaskProvider.java new file mode 100644 index 00000000..1542a24f --- /dev/null +++ b/src/main/java/ch/naviqore/service/impl/convert/GtfsTripMaskProvider.java @@ -0,0 +1,100 @@ +package ch.naviqore.service.impl.convert; + +import ch.naviqore.gtfs.schedule.model.GtfsSchedule; +import ch.naviqore.raptor.router.RaptorTripMaskProvider; +import ch.naviqore.service.config.ServiceConfig; +import ch.naviqore.utils.cache.EvictionCache; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class GtfsTripMaskProvider implements RaptorTripMaskProvider { + + private final GtfsSchedule schedule; + private final GtfsTripMaskProvider.MaskCache cache; + @Setter + private Map tripIds = null; + + public GtfsTripMaskProvider(GtfsSchedule schedule) { + this(schedule, ServiceConfig.DEFAULT_CACHE_SIZE, + EvictionCache.Strategy.valueOf(ServiceConfig.DEFAULT_CACHE_EVICTION_STRATEGY.name())); + } + + public GtfsTripMaskProvider(GtfsSchedule schedule, int cacheSize, EvictionCache.Strategy strategy) { + this.schedule = schedule; + this.cache = new GtfsTripMaskProvider.MaskCache(cacheSize, strategy); + } + + public void clearCache() { + cache.clear(); + } + + @Override + public String getServiceIdForDate(LocalDate date) { + return cache.getActiveServices(date); + } + + @Override + public DayTripMask getDayTripMask(LocalDate date) { + if (tripIds == null) { + throw new IllegalStateException("Trip ids not set"); + } + return buildTripMask(date, cache.getActiveServices(date)); + } + + private DayTripMask buildTripMask(LocalDate date, String serviceId) { + Map tripMasks = new HashMap<>(); + + for (Map.Entry entry : tripIds.entrySet()) { + String routeId = entry.getKey(); + String[] tripIds = entry.getValue(); + boolean[] tripMask = new boolean[tripIds.length]; + for (int i = 0; i < tripIds.length; i++) { + tripMask[i] = schedule.getTrips().get(tripIds[i]).getCalendar().isServiceAvailable(date); + } + + tripMasks.put(routeId, new RouteTripMask(tripMask)); + } + + return new DayTripMask(serviceId, date, tripMasks); + } + + /** + * Caches for active services (= GTFS calendars) per date and raptor trip mask instances. + */ + private class MaskCache { + private final EvictionCache activeServices; + + /** + * @param cacheSize the maximum number of trip mask instances to be cached. + * @param strategy the cache eviction strategy. + */ + MaskCache(int cacheSize, EvictionCache.Strategy strategy) { + activeServices = new EvictionCache<>(Math.min(365, cacheSize * 20), strategy); + } + + public String getActiveServices(LocalDate date) { + return activeServices.computeIfAbsent(date, () -> getActiveServicesFromSchedule(date)); + } + + // get all active calendars form the gtfs for given date, serves as key for caching raptor instances + private String getActiveServicesFromSchedule(LocalDate date) { + return schedule.getCalendars() + .values() + .stream() + .filter(calendar -> calendar.isServiceAvailable(date)) + .map(ch.naviqore.gtfs.schedule.model.Calendar::getId) + .collect(Collectors.joining(",")); + } + + // clear the cache, needs to be called when the GTFS schedule changes + private void clear() { + activeServices.clear(); + } + + } + +} diff --git a/src/main/java/ch/naviqore/service/impl/transfer/SameStopTransferGenerator.java b/src/main/java/ch/naviqore/service/impl/transfer/SameStopTransferGenerator.java deleted file mode 100644 index cf15b3f9..00000000 --- a/src/main/java/ch/naviqore/service/impl/transfer/SameStopTransferGenerator.java +++ /dev/null @@ -1,40 +0,0 @@ -package ch.naviqore.service.impl.transfer; - -import ch.naviqore.gtfs.schedule.model.GtfsSchedule; -import ch.naviqore.gtfs.schedule.model.Stop; -import lombok.extern.slf4j.Slf4j; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Slf4j -public class SameStopTransferGenerator implements TransferGenerator { - - private final int minimumTransferTime; - - /** - * Creates a new generator - * - * @param minimumTransferTime minimum transfer time between two trips at the same stop in seconds. - */ - public SameStopTransferGenerator(int minimumTransferTime) { - if (minimumTransferTime < 0) { - throw new IllegalArgumentException("minimumTransferTime is negative"); - } - this.minimumTransferTime = minimumTransferTime; - } - - @Override - public List generateTransfers(GtfsSchedule schedule) { - List transfers = new ArrayList<>(); - Map stops = schedule.getStops(); - - log.info("Generating same stop transfers for {} stops", stops.size()); - for (Stop fromStop : stops.values()) { - transfers.add(new TransferGenerator.Transfer(fromStop, fromStop, minimumTransferTime)); - } - - return transfers; - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2b57e796..eba656d9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -22,10 +22,11 @@ gtfs.static.update.cron=${GTFS_STATIC_UPDATE_CRON:0 0 4 * * *} # Default transfer time between same stop transfers in seconds. If GTFS already provides same stop transfer times, # those have precedence over this default. If the value is set to 0, no additional same stop transfer times are set. transfer.time.same.stop.default=${TRANSFER_TIME_SAME_STOP_DEFAULT:120} -# Minimum transfer time between the same stops in seconds. If stops are closer than this, this time has precedence over -# the actual walking time, which accounts for leaving the station building, stairways, etc. If GTFS already provides a -# transfer time between two stops, the GTFS time has precedence over this minimum. -transfer.time.between.stops.minimum=${TRANSFER_TIME_BETWEEN_STOPS_MINIMUM:180} +# Minimum transfer time between the different stops in seconds. If stops are closer than this, this time has precedence +# over the actual walking time, which accounts for leaving the station building, stairways, etc. If GTFS already +# provides a transfer time between two stops, the GTFS time has precedence over this minimum. If the value is set to -1, +# no additional transfers will be created. +transfer.time.between.stops.minimum=${TRANSFER_TIME_BETWEEN_STOPS_MINIMUM:-1} # Time in seconds required to access or egress from a public transit trip. This time is added twice to the walking # duration of transfers between two stops and once to first and last mile walking legs. transfer.time.access.egress=${TRANSFER_TIME_ACCESS_EGRESS:15} @@ -46,10 +47,18 @@ walking.speed=${WALKING_SPEED:1.4} # very short walks inside stations that give a false sense of accuracy. walking.duration.minimum=${WALKING_DURATION_MINIMUM:60} # ============================================== +# RAPTOR +# ============================================== +# Number of dates surrounding the queried date should be included in the scan. This is useful if the previous GTFS +# service day includes trips on the queried date. Or the connection is so long that it continues into the next service +# day. The default value is 3, which includes the previous, current, and next service day. +raptor.days.to.scan=${RAPTOR_DAYS_TO_SCAN:3} +# ============================================== # CACHE # ============================================== -# Number of Raptor instances to cache. For each type of service day, a new Raptor is needed, which consumes resources -# while building. Therefore, Raptor instances are cached for days with the same active services (trips). -cache.size=${CACHE_SIZE:5} +# Number of service day instances to cache. For each service day, a new stop time array / trip mask is needed, which +# consumes resources while building. Therefore, trip masks and stop time int arrays are cached grouped by service id. +# (e.g. if all Mondays in the schedule have the same active services only one instance for all Mondays is cached). +cache.service.day.size=${CACHE_SERVICE_DAY_SIZE:5} # Cache eviction strategy: LRU (Least Recently Used) or MRU (Most Recently Used). cache.eviction.strategy=${CACHE_EVICTION_STRATEGY:LRU} diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 01b66a56..42faaff3 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -9,13 +9,10 @@ import ch.naviqore.raptor.Connection; import ch.naviqore.raptor.QueryConfig; import ch.naviqore.raptor.RaptorAlgorithm; +import ch.naviqore.raptor.router.RaptorConfig; import ch.naviqore.service.impl.convert.GtfsToRaptorConverter; -import ch.naviqore.service.impl.transfer.SameStopTransferGenerator; -import ch.naviqore.service.impl.transfer.TransferGenerator; -import ch.naviqore.service.impl.transfer.WalkTransferGenerator; -import ch.naviqore.service.walk.BeeLineWalkCalculator; -import ch.naviqore.utils.spatial.index.KDTree; -import ch.naviqore.utils.spatial.index.KDTreeBuilder; +import ch.naviqore.service.impl.convert.GtfsTripMaskProvider; +import ch.naviqore.utils.cache.EvictionCache; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -62,11 +59,8 @@ final class Benchmark { private static final long MONITORING_INTERVAL_MS = 30000; private static final int NS_TO_MS_CONVERSION_FACTOR = 1_000_000; private static final int NOT_AVAILABLE = -1; - private static final int WALKING_SPEED = 3000; - private static final int MINIMUM_TRANSFER_TIME = 120; private static final int SAME_STOP_TRANSFER_TIME = 120; - private static final int ACCESS_EGRESS_TIME = 15; - private static final int SEARCH_RADIUS = 500; + private static final int MAX_DAYS_TO_SCAN = 3; public static void main(String[] args) throws IOException, InterruptedException { GtfsSchedule schedule = initializeSchedule(); @@ -84,21 +78,16 @@ private static GtfsSchedule initializeSchedule() throws IOException, Interrupted } private static RaptorAlgorithm initializeRaptor(GtfsSchedule schedule) throws InterruptedException { - // TODO: This should be implemented in the new integration service and should not need to run everytime a raptor - // instance is created. Ideally this will be handled as an attribute with a list of transfer generators. With - // this approach, transfers can be generated according to different rules with the first applicable one taking - // precedence. - KDTree spatialStopIndex = new KDTreeBuilder().addLocations(schedule.getStops().values()).build(); - BeeLineWalkCalculator walkCalculator = new BeeLineWalkCalculator(WALKING_SPEED); - WalkTransferGenerator transferGenerator = new WalkTransferGenerator(walkCalculator, MINIMUM_TRANSFER_TIME, - ACCESS_EGRESS_TIME, SEARCH_RADIUS, spatialStopIndex); - List additionalGeneratedTransfers = transferGenerator.generateTransfers(schedule); - SameStopTransferGenerator sameStopTransferGenerator = new SameStopTransferGenerator(SAME_STOP_TRANSFER_TIME); - additionalGeneratedTransfers.addAll(sameStopTransferGenerator.generateTransfers(schedule)); - - RaptorAlgorithm raptor = new GtfsToRaptorConverter(schedule, additionalGeneratedTransfers, - SAME_STOP_TRANSFER_TIME).convert(SCHEDULE_DATE); + RaptorConfig config = new RaptorConfig(MAX_DAYS_TO_SCAN, SAME_STOP_TRANSFER_TIME, MAX_DAYS_TO_SCAN, + EvictionCache.Strategy.LRU, new GtfsTripMaskProvider(schedule)); + RaptorAlgorithm raptor = new GtfsToRaptorConverter(schedule, config).convert(); manageResources(); + + for (int dayIndex = 0; dayIndex < MAX_DAYS_TO_SCAN; dayIndex++) { + raptor.prepareStopTimesForDate(SCHEDULE_DATE.plusDays(dayIndex - 1)); + } + manageResources(); + return raptor; } diff --git a/src/test/java/ch/naviqore/app/controller/DummyService.java b/src/test/java/ch/naviqore/app/controller/DummyService.java index 927841d7..6d0570f5 100644 --- a/src/test/java/ch/naviqore/app/controller/DummyService.java +++ b/src/test/java/ch/naviqore/app/controller/DummyService.java @@ -28,10 +28,6 @@ class DummyService implements PublicTransitService { static final List STOPS = List.of(STOP_A, STOP_B, STOP_C, STOP_D, STOP_E, STOP_F, STOP_G, STOP_H); - - private record RouteData(DummyServiceModels.Route route, List stops) { - } - private static final RouteData ROUTE_1 = new RouteData( new DummyServiceModels.Route("1", "Route 1", "R1", "BUS", "Agency 1"), List.of(STOP_A, STOP_B, STOP_C, STOP_D, STOP_E, STOP_F, STOP_G)); @@ -41,7 +37,6 @@ private record RouteData(DummyServiceModels.Route route, List ROUTES = List.of(ROUTE_1, ROUTE_2, ROUTE_3); @Override @@ -88,11 +83,9 @@ public Map getIsoLines(GeoCoordinate source, LocalDateTime tim for (Stop stop : STOPS) { try { if (timeType == TimeType.DEPARTURE) { - connections.put(stop, - DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + connections.put(stop, DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); } else { - connections.put(stop, - DummyConnectionGenerators.getSimpleConnection(stop, source, time, timeType)); + connections.put(stop, DummyConnectionGenerators.getSimpleConnection(stop, source, time, timeType)); } } catch (IllegalArgumentException e) { // ignore @@ -111,11 +104,9 @@ public Map getIsoLines(Stop source, LocalDateTime time, TimeTy } try { if (timeType == TimeType.DEPARTURE) { - connections.put(stop, - DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); + connections.put(stop, DummyConnectionGenerators.getSimpleConnection(source, stop, time, timeType)); } else { - connections.put(stop, - DummyConnectionGenerators.getSimpleConnection(stop, source, time, timeType)); + connections.put(stop, DummyConnectionGenerators.getSimpleConnection(stop, source, time, timeType)); } } catch (IllegalArgumentException e) { // ignore @@ -178,6 +169,9 @@ public Route getRouteById(String routeId) throws RouteNotFoundException { .orElseThrow(() -> new RouteNotFoundException(routeId)); } + private record RouteData(DummyServiceModels.Route route, List stops) { + } + static class DummyConnectionGenerators { private static final int SECONDS_BETWEEN_STOPS = 300; private static final int DISTANCE_BETWEEN_STOPS = 100; diff --git a/src/test/java/ch/naviqore/app/service/ServiceConfigParserIT.java b/src/test/java/ch/naviqore/app/service/ServiceConfigParserIT.java index fd774c70..2f0e3864 100644 --- a/src/test/java/ch/naviqore/app/service/ServiceConfigParserIT.java +++ b/src/test/java/ch/naviqore/app/service/ServiceConfigParserIT.java @@ -18,10 +18,7 @@ public class ServiceConfigParserIT { static Stream provideTestCombinations() { return Stream.of(Arguments.of(-1, DEFAULT_TRANSFER_TIME_BETWEEN_STOPS_MINIMUM, DEFAULT_WALKING_SEARCH_RADIUS, - DEFAULT_WALKING_SPEED, "BEE_LINE_DISTANCE", "Minimum Transfer Time cannot be smaller than zero."), - Arguments.of(DEFAULT_TRANSFER_TIME_SAME_STOP_DEFAULT, -1, DEFAULT_WALKING_SEARCH_RADIUS, - DEFAULT_WALKING_SPEED, "BEE_LINE_DISTANCE", - "Same Stop Transfer Time cannot be smaller than zero."), + DEFAULT_WALKING_SPEED, "BEE_LINE_DISTANCE", "Same Stop Transfer Time cannot be smaller than zero."), Arguments.of(DEFAULT_TRANSFER_TIME_SAME_STOP_DEFAULT, DEFAULT_TRANSFER_TIME_BETWEEN_STOPS_MINIMUM, -1, DEFAULT_WALKING_SPEED, "BEE_LINE_DISTANCE", "Maximum Walking Distance cannot be smaller than zero."), @@ -40,7 +37,7 @@ private static ServiceConfig getServiceConfig() { DEFAULT_TRANSFER_TIME_SAME_STOP_DEFAULT, DEFAULT_TRANSFER_TIME_BETWEEN_STOPS_MINIMUM, DEFAULT_TRANSFER_TIME_ACCESS_EGRESS, DEFAULT_WALKING_SEARCH_RADIUS, DEFAULT_WALKING_CALCULATOR_TYPE.name(), DEFAULT_WALKING_SPEED, DEFAULT_WALKING_DURATION_MINIMUM, - DEFAULT_CACHE_SIZE, DEFAULT_CACHE_EVICTION_STRATEGY.name()); + DEFAULT_MAX_DAYS_TO_SCAN, DEFAULT_CACHE_SIZE, DEFAULT_CACHE_EVICTION_STRATEGY.name()); return parser.getServiceConfig(); } @@ -55,7 +52,8 @@ void testServiceConfigParser_withValidInputs() { assertEquals(DEFAULT_WALKING_SPEED, config.getWalkingSpeed()); assertEquals(DEFAULT_WALKING_SEARCH_RADIUS, config.getWalkingSearchRadius()); assertEquals(DEFAULT_WALKING_DURATION_MINIMUM, config.getWalkingDurationMinimum()); - assertEquals(DEFAULT_CACHE_SIZE, config.getCacheSize()); + assertEquals(DEFAULT_MAX_DAYS_TO_SCAN, config.getRaptorDaysToScan()); + assertEquals(DEFAULT_CACHE_SIZE, config.getCacheServiceDaySize()); assertEquals(CacheEvictionStrategy.LRU, config.getCacheEvictionStrategy()); } @@ -65,8 +63,8 @@ void testServiceConfigParser_withInvalidWalkCalculatorType() { () -> new ServiceConfigParser(GTFS_STATIC_URI, DEFAULT_GTFS_STATIC_UPDATE_CRON, DEFAULT_TRANSFER_TIME_BETWEEN_STOPS_MINIMUM, DEFAULT_TRANSFER_TIME_SAME_STOP_DEFAULT, DEFAULT_TRANSFER_TIME_ACCESS_EGRESS, DEFAULT_WALKING_SEARCH_RADIUS, "INVALID", - DEFAULT_WALKING_SPEED, DEFAULT_WALKING_DURATION_MINIMUM, DEFAULT_CACHE_SIZE, - DEFAULT_CACHE_EVICTION_STRATEGY.name())); + DEFAULT_WALKING_SPEED, DEFAULT_WALKING_DURATION_MINIMUM, DEFAULT_MAX_DAYS_TO_SCAN, + DEFAULT_CACHE_SIZE, DEFAULT_CACHE_EVICTION_STRATEGY.name())); } @ParameterizedTest(name = "{5}") @@ -78,7 +76,7 @@ void testServiceConfigParser_withInvalidInputs(int transferTimeSameStopDefault, () -> new ServiceConfigParser(GTFS_STATIC_URI, DEFAULT_GTFS_STATIC_UPDATE_CRON, transferTimeSameStopDefault, transferTimeBetweenStopsMinimum, DEFAULT_TRANSFER_TIME_ACCESS_EGRESS, walkingSearchRadius, walkingCalculatorType.toUpperCase(), - walkingSpeed, DEFAULT_WALKING_DURATION_MINIMUM, DEFAULT_CACHE_SIZE, + walkingSpeed, DEFAULT_WALKING_DURATION_MINIMUM, DEFAULT_MAX_DAYS_TO_SCAN, DEFAULT_CACHE_SIZE, DEFAULT_CACHE_EVICTION_STRATEGY.name()), message); } diff --git a/src/test/java/ch/naviqore/raptor/router/RaptorRouterMultiDayTest.java b/src/test/java/ch/naviqore/raptor/router/RaptorRouterMultiDayTest.java new file mode 100644 index 00000000..a82be763 --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/router/RaptorRouterMultiDayTest.java @@ -0,0 +1,250 @@ +package ch.naviqore.raptor.router; + +import ch.naviqore.raptor.Connection; +import ch.naviqore.raptor.RaptorAlgorithm; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test class for the RaptorRouter with multi-day schedules. + */ +@ExtendWith(RaptorRouterTestExtension.class) +public class RaptorRouterMultiDayTest { + + private static final String STOP_A = "A"; + private static final String STOP_G = "G"; + private static final String STOP_Q = "Q"; + + private static final LocalDateTime REFERENCE_DAY = LocalDateTime.of(2021, 1, 1, 0, 0); + private static final LocalDateTime PREVIOUS_DAY = REFERENCE_DAY.minusDays(1); + private static final LocalDateTime NEXT_DAY = REFERENCE_DAY.plusDays(1); + + @NoArgsConstructor + static class RoutePerDayMasker implements RaptorTripMaskProvider { + final Map> blockedRoutes = new HashMap<>(); + @Setter + Map tripIds = null; + @Setter + int dayStartHour = 0; + @Setter + int dayEndHour = 24; + + void deactivateRouteOnDate(String routeId, LocalDate date) { + String forwardRouteId = routeId + "-F"; + String reverseRouteId = routeId + "-R"; + blockedRoutes.computeIfAbsent(date, k -> new HashSet<>()).add(forwardRouteId); + blockedRoutes.get(date).add(reverseRouteId); + } + + @Override + public String getServiceIdForDate(LocalDate date) { + return date.toString(); + } + + @Override + public DayTripMask getDayTripMask(LocalDate date) { + + Set blockedRouteIds = blockedRoutes.getOrDefault(date, Set.of()); + + Map tripMasks = new HashMap<>(); + for (Map.Entry entry : tripIds.entrySet()) { + String routeId = entry.getKey(); + String[] tripIds = entry.getValue(); + if (blockedRouteIds.contains(routeId)) { + tripMasks.put(routeId, new RouteTripMask(new boolean[tripIds.length])); + } else { + boolean[] tripMask = new boolean[tripIds.length]; + for (int i = 0; i < tripIds.length; i++) { + tripMask[i] = true; + } + tripMasks.put(routeId, new RouteTripMask(tripMask)); + } + } + + return new DayTripMask(getServiceIdForDate(date), date, tripMasks); + } + } + + @Nested + class PreviousDay { + @Test + void findDepartureConnectionFromPreviousDayService(RaptorRouterTestBuilder builder) { + + RaptorAlgorithm multiDayRaptor = builder.withAddRoute1_AG().withMaxDaysToScan(3).build(5, 26); + RaptorAlgorithm singleDayRaptor = builder.withMaxDaysToScan(1).build(5, 26); + + // connection from A to G should leave at 00:00 am. (this trip is part of the previous day service) + List multiDayConnections = RaptorRouterTestHelpers.routeEarliestArrival(multiDayRaptor, STOP_A, + STOP_G, REFERENCE_DAY); + // connection from A to G should leave at 05:00 am. (this trip is part of the reference day service) + List singleDayConnections = RaptorRouterTestHelpers.routeEarliestArrival(singleDayRaptor, + STOP_A, STOP_G, REFERENCE_DAY); + + assertEquals(1, multiDayConnections.size()); + Connection connection = multiDayConnections.getFirst(); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connection, STOP_A, STOP_G, REFERENCE_DAY, 0, 0, 1, + multiDayRaptor); + assertEquals(REFERENCE_DAY, connection.getDepartureTime()); + + assertEquals(1, singleDayConnections.size()); + Connection singleDayConnection = singleDayConnections.getFirst(); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(singleDayConnection, STOP_A, STOP_G, REFERENCE_DAY, + 0, 0, 1, multiDayRaptor); + assertEquals(REFERENCE_DAY.plusHours(5), singleDayConnection.getDepartureTime()); + } + + @Test + void findArrivalConnectionFromPreviousDayService(RaptorRouterTestBuilder builder) { + RaptorAlgorithm multiDayRaptor = builder.withAddRoute1_AG().withMaxDaysToScan(3).build(5, 26); + RaptorAlgorithm singleDayRaptor = builder.withMaxDaysToScan(1).build(5, 26); + + LocalDateTime requestedArrivalTime = REFERENCE_DAY.plusMinutes(5); + + // connection from A to G should arrive at 00:05 am. (this trip is part of the previous day service) + List multiDayConnections = RaptorRouterTestHelpers.routeLatestDeparture(multiDayRaptor, STOP_A, + STOP_G, requestedArrivalTime); + // connection from A to G should not be possible if only the reference day is considered + List singleDayConnections = RaptorRouterTestHelpers.routeLatestDeparture(singleDayRaptor, + STOP_A, STOP_G, requestedArrivalTime); + + assertEquals(1, multiDayConnections.size()); + assertEquals(0, singleDayConnections.size()); + Connection connection = multiDayConnections.getFirst(); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connection, STOP_A, STOP_G, requestedArrivalTime, 0, + 0, 1, multiDayRaptor); + assertEquals(requestedArrivalTime, connection.getArrivalTime()); + assertEquals(PREVIOUS_DAY.toLocalDate(), connection.getDepartureTime().toLocalDate()); + } + } + + @Nested + class NextDay { + + @Test + void findConnectionUsingNextDayService(RaptorRouterTestBuilder builder) { + int startOfDay = 5; + int endOfDay = 20; + + // service runs only until 20:00! + RaptorAlgorithm raptor = builder.withAddRoute1_AG().withMaxDaysToScan(3).build(startOfDay, endOfDay); + + // departure time is 22:00 hence the connection from A to G should leave at the start of the next service + // day + LocalDateTime departureTime = REFERENCE_DAY.plusHours(22); + + // connection from A to G should leave at 00:00 am. (this trip is part of the next day service) + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_G, + departureTime); + + assertEquals(1, connections.size()); + Connection connection = connections.getFirst(); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connection, STOP_A, STOP_G, departureTime, 0, 0, 1, + raptor); + assertEquals(NEXT_DAY.plusHours(startOfDay), connection.getDepartureTime()); + + // confirm that this connection is not possible with 1 service day only + RaptorAlgorithm raptorWithLessDays = builder.withMaxDaysToScan(1).build(startOfDay, endOfDay); + List connectionsWithLessDays = RaptorRouterTestHelpers.routeEarliestArrival(raptorWithLessDays, + STOP_A, STOP_G, departureTime); + assertEquals(0, connectionsWithLessDays.size()); + } + + } + + @Nested + class MultiDay { + + @Test + void findConnectionUsingTwoServiceDays(RaptorRouterTestBuilder builder) { + + int startOfDay = 6; + int endOfDay = 22; + + LocalDateTime departureTime = REFERENCE_DAY.plusHours(16); + + RoutePerDayMasker tripMaskProvider = new RoutePerDayMasker(); + tripMaskProvider.setDayStartHour(startOfDay); + tripMaskProvider.setDayEndHour(endOfDay); + tripMaskProvider.deactivateRouteOnDate("R3", REFERENCE_DAY.toLocalDate()); + tripMaskProvider.deactivateRouteOnDate("R1", NEXT_DAY.toLocalDate()); + + RaptorAlgorithm raptor = builder.withAddRoute1_AG() + .withAddRoute3_MQ() + .withAddTransfer1_ND() + .withMaxDaysToScan(3) + .withTripMaskProvider(tripMaskProvider) + .build(startOfDay, endOfDay); + + // connection from A to D should leave at 16:00 pm of reference day. Walk transfer to N should happen right + // after arrival at D. The next trip from N to Q will only be running on the next day. + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + departureTime); + + assertEquals(1, connections.size()); + Connection connection = connections.getFirst(); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connection, STOP_A, STOP_Q, departureTime, 0, 1, 2, + raptor); + assertTrue(connection.getDepartureTime().isBefore(REFERENCE_DAY.plusHours(endOfDay))); + assertTrue(connection.getArrivalTime().isAfter(NEXT_DAY.plusHours(startOfDay))); + } + + @Test + void findConnectionUsingFiveServiceDays(RaptorRouterTestBuilder builder) { + + int startOfDay = 6; + int endOfDay = 22; + + int numDaysInFuture = 5; + + LocalDateTime departureTime = REFERENCE_DAY.plusHours(16); + + RoutePerDayMasker tripMaskProvider = new RoutePerDayMasker(); + tripMaskProvider.setDayStartHour(startOfDay); + tripMaskProvider.setDayEndHour(endOfDay); + + // deactivate all days except for the day "numDaysInFuture" days in the future + for (int i = -1; i < numDaysInFuture; i++) { + tripMaskProvider.deactivateRouteOnDate("R1", REFERENCE_DAY.plusDays(i).toLocalDate()); + } + + RaptorAlgorithm raptor = builder.withAddRoute1_AG() + .withMaxDaysToScan(numDaysInFuture + 2) // +2 for reference day and previous day + .withTripMaskProvider(tripMaskProvider) + .build(startOfDay, endOfDay); + + // connection from A to G should leave at start of day of 5th day in the future, since that will be the + // first active trip on this connection. + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_G, + departureTime); + + assertEquals(1, connections.size()); + Connection connection = connections.getFirst(); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connection, STOP_A, STOP_G, departureTime, 0, 0, 1, + raptor); + assertTrue(connection.getDepartureTime() + .isEqual(REFERENCE_DAY.plusDays(numDaysInFuture).plusHours(startOfDay))); + assertTrue( + connection.getArrivalTime().isAfter(REFERENCE_DAY.plusDays(numDaysInFuture).plusHours(startOfDay))); + + // confirm that no connection is found if the raptor is built with fewer days to scan + RaptorAlgorithm raptorWithLessDays = builder.withMaxDaysToScan(numDaysInFuture + 1) // one day less + .build(startOfDay, endOfDay); + List connectionsWithLessDays = RaptorRouterTestHelpers.routeEarliestArrival(raptorWithLessDays, + STOP_A, STOP_G, departureTime); + assertEquals(0, connectionsWithLessDays.size()); + + } + + } + +} diff --git a/src/test/java/ch/naviqore/raptor/router/RaptorRouterTest.java b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTest.java index 813167cf..54835acf 100644 --- a/src/test/java/ch/naviqore/raptor/router/RaptorRouterTest.java +++ b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTest.java @@ -1,6 +1,9 @@ package ch.naviqore.raptor.router; -import ch.naviqore.raptor.*; +import ch.naviqore.raptor.Connection; +import ch.naviqore.raptor.Leg; +import ch.naviqore.raptor.QueryConfig; +import ch.naviqore.raptor.RaptorAlgorithm; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -8,7 +11,6 @@ import java.time.Duration; import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.List; import java.util.Map; @@ -20,8 +22,6 @@ @ExtendWith(RaptorRouterTestExtension.class) class RaptorRouterTest { - private static final int INFINITY = Integer.MAX_VALUE; - private static final String STOP_A = "A"; private static final String STOP_B = "B"; private static final String STOP_C = "C"; @@ -43,190 +43,6 @@ class RaptorRouterTest { private static final LocalDateTime EIGHT_AM = START_OF_DAY.plusHours(8); private static final LocalDateTime NINE_AM = START_OF_DAY.plusHours(9); - static class ConvenienceMethods { - - static Map getIsoLines(RaptorAlgorithm raptor, Map sourceStops) { - return getIsoLines(raptor, sourceStops, new QueryConfig()); - } - - static Map getIsoLines(RaptorAlgorithm raptor, Map sourceStops, - QueryConfig config) { - return raptor.routeIsolines(sourceStops, TimeType.DEPARTURE, config); - } - - static List routeEarliestArrival(RaptorAlgorithm raptor, String sourceStopId, String targetStopId, - LocalDateTime departureTime) { - return routeEarliestArrival(raptor, createStopMap(sourceStopId, departureTime), - createStopMap(targetStopId, 0)); - } - - static List routeEarliestArrival(RaptorAlgorithm raptor, String sourceStopId, String targetStopId, - LocalDateTime departureTime, QueryConfig config) { - return routeEarliestArrival(raptor, createStopMap(sourceStopId, departureTime), - createStopMap(targetStopId, 0), config); - } - - static Map createStopMap(String stopId, LocalDateTime value) { - return Map.of(stopId, value); - } - - static Map createStopMap(String stopId, int value) { - return Map.of(stopId, value); - } - - static List routeEarliestArrival(RaptorAlgorithm raptor, Map sourceStops, - Map targetStopIds) { - return routeEarliestArrival(raptor, sourceStops, targetStopIds, new QueryConfig()); - } - - static List routeEarliestArrival(RaptorAlgorithm raptor, Map sourceStops, - Map targetStopIds, QueryConfig config) { - return raptor.routeEarliestArrival(sourceStops, targetStopIds, config); - } - - static List routeLatestDeparture(RaptorAlgorithm raptor, String sourceStopId, String targetStopId, - LocalDateTime arrivalTime) { - return routeLatestDeparture(raptor, createStopMap(sourceStopId, 0), - createStopMap(targetStopId, arrivalTime)); - } - - static List routeLatestDeparture(RaptorAlgorithm raptor, Map sourceStops, - Map targetStops) { - return routeLatestDeparture(raptor, sourceStops, targetStops, new QueryConfig()); - } - - static List routeLatestDeparture(RaptorAlgorithm raptor, Map sourceStops, - Map targetStops, QueryConfig config) { - return raptor.routeLatestDeparture(sourceStops, targetStops, config); - } - - } - - static class Helpers { - private static void assertEarliestArrivalConnection(Connection connection, String sourceStop, String targetStop, - LocalDateTime requestedDepartureTime, - int numSameStopTransfers, int numWalkTransfers, - int numTrips, RaptorAlgorithm raptor) { - assertEquals(sourceStop, connection.getFromStopId()); - assertEquals(targetStop, connection.getToStopId()); - - assertFalse(connection.getDepartureTime().isBefore(requestedDepartureTime), - "Departure time should be greater equal than searched for departure time"); - assertNotNull(connection.getArrivalTime(), "Arrival time should not be null"); - - assertEquals(numSameStopTransfers, connection.getNumberOfSameStopTransfers(), - "Number of same stop transfers should match"); - assertEquals(numWalkTransfers, connection.getWalkTransfers().size(), - "Number of walk transfers should match"); - assertEquals(numSameStopTransfers + numWalkTransfers, connection.getNumberOfTotalTransfers(), - "Number of transfers should match"); - - assertEquals(numTrips, connection.getRouteLegs().size(), "Number of trips should match"); - assertReverseDirectionConnection(connection, TimeType.ARRIVAL, raptor); - } - - private static void assertLatestDepartureConnection(Connection connection, String sourceStop, String targetStop, - LocalDateTime requestedArrivalTime, - int numSameStopTransfers, int numWalkTransfers, - int numTrips, RaptorAlgorithm raptor) { - assertEquals(sourceStop, connection.getFromStopId()); - assertEquals(targetStop, connection.getToStopId()); - - assertNotNull(connection.getDepartureTime(), "Departure time should not be null"); - assertFalse(connection.getArrivalTime().isAfter(requestedArrivalTime), - "Arrival time should be smaller equal than searched for arrival time"); - - assertEquals(numSameStopTransfers, connection.getNumberOfSameStopTransfers(), - "Number of same station transfers should match"); - assertEquals(numWalkTransfers, connection.getWalkTransfers().size(), - "Number of walk transfers should match"); - assertEquals(numSameStopTransfers + numWalkTransfers, connection.getNumberOfTotalTransfers(), - "Number of transfers should match"); - - assertEquals(numTrips, connection.getRouteLegs().size(), "Number of trips should match"); - assertReverseDirectionConnection(connection, TimeType.DEPARTURE, raptor); - } - - private static void assertReverseDirectionConnection(Connection connection, TimeType timeType, - RaptorAlgorithm raptor) { - List connections; - if (timeType == TimeType.DEPARTURE) { - connections = ConvenienceMethods.routeEarliestArrival(raptor, connection.getFromStopId(), - connection.getToStopId(), connection.getDepartureTime()); - } else { - connections = ConvenienceMethods.routeLatestDeparture(raptor, connection.getFromStopId(), - connection.getToStopId(), connection.getArrivalTime()); - } - - // find the connections with the same amount of rounds (this one should match) - Connection matchingConnection = connections.stream() - .filter(c -> c.getRouteLegs().size() == connection.getRouteLegs().size()) - .findFirst() - .orElse(null); - - assertNotNull(matchingConnection, "Matching connection should be found"); - assertEquals(connection.getFromStopId(), matchingConnection.getFromStopId(), "From stop should match"); - assertEquals(connection.getToStopId(), matchingConnection.getToStopId(), "To stop should match"); - if (timeType == TimeType.DEPARTURE) { - assertEquals(connection.getDepartureTime(), matchingConnection.getDepartureTime(), - "Departure time should match"); - - // there is no guarantee that the arrival time is the same, but it should not be later (worse) than - // the arrival time of the matching connection - if (connection.getArrivalTime().isBefore(matchingConnection.getArrivalTime())) { - return; - } - } else { - assertEquals(connection.getArrivalTime(), matchingConnection.getArrivalTime(), - "Arrival time should match"); - // there is no guarantee that the departure time is the same, but it should not be earlier (worse) than - // the departure time of the matching connection - if (connection.getDepartureTime().isBefore(matchingConnection.getArrivalTime())) { - return; - } - } - - assertEquals(connection.getDepartureTime(), matchingConnection.getDepartureTime(), - "Departure time should match"); - assertEquals(connection.getArrivalTime(), matchingConnection.getArrivalTime(), "Arrival time should match"); - assertEquals(connection.getNumberOfSameStopTransfers(), matchingConnection.getNumberOfSameStopTransfers(), - "Number of same stop transfers should match"); - assertEquals(connection.getWalkTransfers().size(), matchingConnection.getWalkTransfers().size(), - "Number of walk transfers should match"); - assertEquals(connection.getNumberOfTotalTransfers(), matchingConnection.getNumberOfTotalTransfers(), - "Number of transfers should match"); - assertEquals(connection.getRouteLegs().size(), matchingConnection.getRouteLegs().size(), - "Number of trips should match"); - } - - private static void checkIfConnectionsAreParetoOptimal(List connections) { - Connection previousConnection = connections.getFirst(); - for (int i = 1; i < connections.size(); i++) { - Connection currentConnection = connections.get(i); - assertTrue(previousConnection.getDurationInSeconds() > currentConnection.getDurationInSeconds(), - "Previous connection should be slower than current connection"); - assertTrue(previousConnection.getRouteLegs().size() < currentConnection.getRouteLegs().size(), - "Previous connection should have fewer route legs than current connection"); - previousConnection = currentConnection; - } - } - - private static void assertIsoLines(Map isoLines, int expectedIsoLines) { - assertEquals(expectedIsoLines, isoLines.size()); - assertFalse(isoLines.containsKey(STOP_A), "Source stop should not be in iso lines"); - for (Map.Entry entry : isoLines.entrySet()) { - int arrivalTimeStamp = (int) entry.getValue().getArrivalTime().toEpochSecond(ZoneOffset.UTC); - assertFalse(entry.getValue().getDepartureTime().isBefore(EIGHT_AM), - "Departure time should be greater than or equal to departure time"); - assertFalse(entry.getValue().getArrivalTime().isBefore(EIGHT_AM), - "Arrival time should be greater than or equal to departure time"); - assertTrue(arrivalTimeStamp < INFINITY, "Arrival time should be less than INFINITY"); - assertEquals(STOP_A, entry.getValue().getFromStopId(), "From stop should be source stop"); - assertEquals(entry.getKey(), entry.getValue().getToStopId(), "To stop should be key of map entry"); - } - } - } - @Nested class EarliestArrival { @@ -244,22 +60,27 @@ void findConnectionsBetweenIntersectingRoutes(RaptorRouterTestBuilder builder) { // - Route R3-F from P to Q RaptorAlgorithm raptor = builder.buildWithDefaults(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM); // check if 2 connections were found assertEquals(2, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, 1, 2, raptor); - Helpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_Q, EIGHT_AM, 2, 0, 3, raptor); - Helpers.checkIfConnectionsAreParetoOptimal(connections); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, + 1, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_Q, EIGHT_AM, 2, 0, + 3, raptor); + RaptorRouterTestHelpers.checkIfConnectionsAreParetoOptimal(connections); } @Test void routeBetweenTwoStopsOnSameRoute(RaptorRouterTestBuilder builder) { RaptorAlgorithm raptor = builder.buildWithDefaults(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_B, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_B, + EIGHT_AM); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_B, EIGHT_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_B, EIGHT_AM, 0, + 0, 1, raptor); } @Test @@ -267,15 +88,18 @@ void routeWithSelfIntersectingRoute(RaptorRouterTestBuilder builder) { builder.withAddRoute5_AH_selfIntersecting(); RaptorAlgorithm raptor = builder.build(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_H, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_H, + EIGHT_AM); assertEquals(2, connections.size()); // First Connection Should have no transfers but ride the entire loop (slow) - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_H, EIGHT_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_H, EIGHT_AM, 0, + 0, 1, raptor); // Second Connection Should Change at Stop B and take the earlier trip of the same route there (faster) - Helpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_H, EIGHT_AM, 1, 0, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_H, EIGHT_AM, 1, 0, + 2, raptor); - Helpers.checkIfConnectionsAreParetoOptimal(connections); + RaptorRouterTestHelpers.checkIfConnectionsAreParetoOptimal(connections); } @Test @@ -286,9 +110,11 @@ void routeFromTwoSourceStopsWithSameDepartureTime(RaptorRouterTestBuilder builde Map targetStops = Map.of(STOP_H, 0); // fastest and only connection should be B -> H - List connections = ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, + targetStops); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_B, STOP_H, EIGHT_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_B, STOP_H, EIGHT_AM, 0, + 0, 1, raptor); } @Test @@ -300,10 +126,13 @@ void routeFromTwoSourceStopsWithLaterDepartureTimeOnCloserStop(RaptorRouterTestB // B -> H has no transfers but later arrival time (due to departure time one hour later) // A -> H has one transfer but earlier arrival time - List connections = ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, + targetStops); assertEquals(2, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_B, STOP_H, NINE_AM, 0, 0, 1, raptor); - Helpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_H, EIGHT_AM, 1, 0, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_B, STOP_H, NINE_AM, 0, + 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_H, EIGHT_AM, 1, 0, + 2, raptor); assertTrue(connections.getFirst().getArrivalTime().isAfter(connections.get(1).getArrivalTime()), "Connection from A should arrive earlier than connection from B"); } @@ -316,9 +145,11 @@ void routeFromStopToTwoTargetStopsNoWalkTimeToTarget(RaptorRouterTestBuilder bui Map targetStops = Map.of(STOP_F, 0, STOP_S, 0); // fastest and only connection should be A -> F - List connections = ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, + targetStops); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_F, EIGHT_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_F, EIGHT_AM, 0, + 0, 1, raptor); } @Test @@ -331,10 +162,13 @@ void routeFromStopToTwoTargetStopsWithWalkTimeToTarget(RaptorRouterTestBuilder b // since F is closer to A than S, the fastest connection should be A -> F, but because of the hour // walk time to target, the connection A -> S should be faster (no additional walk time) - List connections = ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, + targetStops); assertEquals(2, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_F, EIGHT_AM, 0, 0, 1, raptor); - Helpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_S, EIGHT_AM, 1, 0, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_F, EIGHT_AM, 0, + 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_S, EIGHT_AM, 1, 0, + 2, raptor); // Note since the required walk time to target is not added as a leg, the solutions will not be pareto // optimal without additional post-processing. @@ -345,7 +179,8 @@ void notFindConnectionBetweenNotLinkedStops(RaptorRouterTestBuilder builder) { // Omit route R2/R4 and transfers to make stop Q (on R3) unreachable from A (on R1) RaptorAlgorithm raptor = builder.withAddRoute1_AG().withAddRoute3_MQ().build(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM); assertTrue(connections.isEmpty(), "No connection should be found"); } @@ -356,12 +191,14 @@ void takeFasterRouteOfOverlappingRoutes(RaptorRouterTestBuilder builder) { .withAddRoute1_AG("R1X", RaptorRouterTestBuilder.DEFAULT_OFFSET, RaptorRouterTestBuilder.DEFAULT_HEADWAY_TIME, 3, RaptorRouterTestBuilder.DEFAULT_DWELL_TIME) .build(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_G, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_G, + EIGHT_AM); // Both Routes leave at 8:00 at Stop A, but R1 arrives at G at 8:35 whereas R1X arrives at G at 8:23 // R1X should be taken assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_G, EIGHT_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_G, EIGHT_AM, 0, + 0, 1, raptor); // check departure at 8:00 Connection connection = connections.getFirst(); assertEquals(EIGHT_AM, connection.getDepartureTime()); @@ -377,12 +214,14 @@ void takeSlowerRouteOfOverlappingRoutesDueToEarlierDepartureTime(RaptorRouterTes RaptorAlgorithm raptor = builder.withAddRoute1_AG() .withAddRoute1_AG("R1X", 15, 30, 3, RaptorRouterTestBuilder.DEFAULT_DWELL_TIME) .build(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_G, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_G, + EIGHT_AM); // Route R1 leaves at 8:00 at Stop A and arrives at G at 8:35 whereas R1X leaves at 8:15 from Stop A and // arrives at G at 8:38. R1 should be used. assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_G, EIGHT_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_G, EIGHT_AM, 0, + 0, 1, raptor); // check departure at 8:00 Connection connection = connections.getFirst(); assertEquals(EIGHT_AM, connection.getDepartureTime()); @@ -411,22 +250,27 @@ void findConnectionsBetweenIntersectingRoutes(RaptorRouterTestBuilder builder) { // - Route R3-F from P to Q RaptorAlgorithm raptor = builder.buildWithDefaults(); - List connections = ConvenienceMethods.routeLatestDeparture(raptor, STOP_A, STOP_Q, NINE_AM); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, STOP_A, STOP_Q, + NINE_AM); // check if 2 connections were found assertEquals(2, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_Q, NINE_AM, 0, 1, 2, raptor); - Helpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_Q, NINE_AM, 2, 0, 3, raptor); - Helpers.checkIfConnectionsAreParetoOptimal(connections); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_Q, NINE_AM, 0, + 1, 2, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_Q, NINE_AM, 2, 0, + 3, raptor); + RaptorRouterTestHelpers.checkIfConnectionsAreParetoOptimal(connections); } @Test void routeBetweenTwoStopsOnSameRoute(RaptorRouterTestBuilder builder) { RaptorAlgorithm raptor = builder.buildWithDefaults(); - List connections = ConvenienceMethods.routeLatestDeparture(raptor, STOP_A, STOP_B, NINE_AM); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, STOP_A, STOP_B, + NINE_AM); assertEquals(1, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_B, NINE_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_B, NINE_AM, 0, + 0, 1, raptor); } @Test @@ -434,15 +278,18 @@ void routeWithSelfIntersectingRoute(RaptorRouterTestBuilder builder) { builder.withAddRoute5_AH_selfIntersecting(); RaptorAlgorithm raptor = builder.build(); - List connections = ConvenienceMethods.routeLatestDeparture(raptor, STOP_A, STOP_H, NINE_AM); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, STOP_A, STOP_H, + NINE_AM); assertEquals(2, connections.size()); // First Connection Should have no transfers but ride the entire loop (slow) - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_H, NINE_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_H, NINE_AM, 0, + 0, 1, raptor); // Second Connection Should Change at Stop B and take the earlier trip of the same route there (faster) - Helpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_H, NINE_AM, 1, 0, 2, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_H, NINE_AM, 1, 0, + 2, raptor); - Helpers.checkIfConnectionsAreParetoOptimal(connections); + RaptorRouterTestHelpers.checkIfConnectionsAreParetoOptimal(connections); } @Test @@ -453,9 +300,11 @@ void routeToTwoSourceStopsWitNoWalkTime(RaptorRouterTestBuilder builder) { Map targetStops = Map.of(STOP_H, NINE_AM); // fastest and only connection should be B -> H - List connections = ConvenienceMethods.routeLatestDeparture(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, sourceStops, + targetStops); assertEquals(1, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_B, STOP_H, NINE_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_B, STOP_H, NINE_AM, 0, + 0, 1, raptor); } @Test @@ -467,10 +316,13 @@ void routeToTwoSourceStopsWithWalkTimeOnCloserStop(RaptorRouterTestBuilder build // B -> H has no transfers but (theoretical) worse departure time (due to extra one-hour walk time) // A -> H has one transfer but (theoretical) better departure time (no additional walk time - List connections = ConvenienceMethods.routeLatestDeparture(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, sourceStops, + targetStops); assertEquals(2, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_B, STOP_H, NINE_AM, 0, 0, 1, raptor); - Helpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_H, NINE_AM, 1, 0, 2, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_B, STOP_H, NINE_AM, 0, + 0, 1, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_H, NINE_AM, 1, 0, + 2, raptor); } @Test @@ -481,9 +333,11 @@ void routeFromTwoTargetStopsToTargetNoWalkTime(RaptorRouterTestBuilder builder) Map targetStops = Map.of(STOP_F, NINE_AM, STOP_S, NINE_AM); // fastest and only connection should be A -> F - List connections = ConvenienceMethods.routeLatestDeparture(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, sourceStops, + targetStops); assertEquals(1, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_F, NINE_AM, 0, 0, 1, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_F, NINE_AM, 0, + 0, 1, raptor); } @Test @@ -496,10 +350,13 @@ void routeFromToTargetStopsWithDifferentArrivalTimes(RaptorRouterTestBuilder bui // since F is closer to A than S, the fastest connection should be A -> F, but because of the hour // earlier arrival time, the connection A -> S should be faster (no additional walk time) - List connections = ConvenienceMethods.routeLatestDeparture(raptor, sourceStops, targetStops); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, sourceStops, + targetStops); assertEquals(2, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_F, EIGHT_AM, 0, 0, 1, raptor); - Helpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_S, NINE_AM, 1, 0, 2, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_F, EIGHT_AM, 0, + 0, 1, raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.get(1), STOP_A, STOP_S, NINE_AM, 1, 0, + 2, raptor); } @Test @@ -507,7 +364,8 @@ void notFindConnectionBetweenNotLinkedStops(RaptorRouterTestBuilder builder) { // Omit route R2/R4 and transfers to make stop Q (on R3) unreachable from A (on R1) RaptorAlgorithm raptor = builder.withAddRoute1_AG().withAddRoute3_MQ().build(); - List connections = ConvenienceMethods.routeLatestDeparture(raptor, STOP_A, STOP_Q, NINE_AM); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, STOP_A, STOP_Q, + NINE_AM); assertTrue(connections.isEmpty(), "No connection should be found"); } @@ -522,11 +380,12 @@ void takeFasterRouteOfOverlappingRoutes(RaptorRouterTestBuilder builder) { // Both Routes arrive at 8:35 at Stop G, but R1 leaves A at 8:00 whereas R1X leaves at A at 8:12 // R1X should be taken LocalDateTime arrivalTime = EIGHT_AM.plusMinutes(35); - List connections = ConvenienceMethods.routeLatestDeparture(raptor, STOP_A, STOP_G, arrivalTime); + List connections = RaptorRouterTestHelpers.routeLatestDeparture(raptor, STOP_A, STOP_G, + arrivalTime); assertEquals(1, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_G, arrivalTime, 0, 0, 1, - raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_G, arrivalTime, + 0, 0, 1, raptor); // check departure at 8:12 Connection connection = connections.getFirst(); assertEquals(EIGHT_AM.plusMinutes(12), connection.getDepartureTime()); @@ -542,14 +401,15 @@ void takeSlowerRouteOfOverlappingRoutesDueToLaterDepartureTime(RaptorRouterTestB RaptorAlgorithm raptor = builder.withAddRoute1_AG() .withAddRoute1_AG("R1X", 15, 30, 3, RaptorRouterTestBuilder.DEFAULT_DWELL_TIME) .build(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_G, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_G, + EIGHT_AM); // Route R1 leaves at 8:00 at Stop A and arrives at G at 8:35 whereas R1X leaves at 7:45 from Stop A and // arrives at G at 8:08. R1 should be used. LocalDateTime arrivalTime = EIGHT_AM.plusMinutes(35); assertEquals(1, connections.size()); - Helpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_G, arrivalTime, 0, 0, 1, - raptor); + RaptorRouterTestHelpers.assertLatestDepartureConnection(connections.getFirst(), STOP_A, STOP_G, arrivalTime, + 0, 0, 1, raptor); // check departure at 8:00 Connection connection = connections.getFirst(); assertEquals(EIGHT_AM, connection.getDepartureTime()); @@ -573,9 +433,11 @@ void findConnectionBetweenOnlyFootpath(RaptorRouterTestBuilder builder) { .withAddTransfer2_LR() .build(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_N, STOP_D, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_N, STOP_D, + EIGHT_AM); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_N, STOP_D, EIGHT_AM, 0, 1, 0, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_N, STOP_D, EIGHT_AM, 0, + 1, 0, raptor); } @Test @@ -586,10 +448,12 @@ void findConnectionBetweenWithFootpath(RaptorRouterTestBuilder builder) { // - Route R1-F from A to D // - Foot Transfer from D to N // - Route R3-F from N to Q - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, 1, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, + 1, 2, raptor); } @Test @@ -607,7 +471,8 @@ void findConnectionWithZeroTravelTimeTripsAndConsequentWalkTransfer(RaptorRouter // Connection C <-> O will be 1-stop leg from C to D, a walk transfer to N and a 1-stop leg from N to O. // I.e. departure time at C will be equal to walk transfer departure at D and arrival time at O will be equal // to departure time at N. - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_C, STOP_O, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_C, STOP_O, + EIGHT_AM); assertEquals(1, connections.size()); Connection connection = connections.getFirst(); Leg firstRouteLeg = connection.getLegs().getFirst(); @@ -618,7 +483,8 @@ void findConnectionWithZeroTravelTimeTripsAndConsequentWalkTransfer(RaptorRouter "Departure time at C should be equal to arrival time at D"); assertEquals(firstRouteLeg.getDepartureTime(), walkTransferLeg.getDepartureTime(), "Departure time at C should be equal to walk departure time at D"); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_C, STOP_O, EIGHT_AM, 0, 1, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_C, STOP_O, EIGHT_AM, 0, + 1, 2, raptor); } @Test @@ -634,12 +500,12 @@ void ensureUnnecessaryWalkTransferIsNotAdded(RaptorRouterTestBuilder builder) { // earliest arrival at B is 8:16. However, in this case the traveller still has to wait until 8:21 to depart // from B. The walk transfer should not be added in this case. LocalDateTime requestedDepartureTime = EIGHT_AM.plusMinutes(1); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_C, + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_C, requestedDepartureTime); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_C, requestedDepartureTime, 0, - 0, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_C, + requestedDepartureTime, 0, 0, 1, raptor); } @Test @@ -655,8 +521,10 @@ void ensureFinalLegDoesNotFavorWalkTransferBecauseOfSameStopTransferTime(RaptorR // 8:05 and then at C at 8:11. If the walk from B to C takes 7 minutes the "comparable" arrival time at C // will be also 8:10. However, since the "real" arrival time will be 8:12, the walk transfer should not be // favored. - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_C, EIGHT_AM); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_C, EIGHT_AM, 0, 0, 1, raptor); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_C, + EIGHT_AM); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_C, EIGHT_AM, 0, + 0, 1, raptor); } @Test @@ -671,8 +539,10 @@ void ensureFinalWalkTransferIsAddedIfArrivesEarlierThanRouteLeg(RaptorRouterTest // Route connection from A <-> C can be connected by a route trip starting at 8:00 at A, arriving at B at // 8:05 and then at C at 8:11. If the walk from B to C takes 5 minutes the arrival time at C is 8:10 and // should be favored over the route leg arriving at 8:11. - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_C, EIGHT_AM); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_C, EIGHT_AM, 0, 1, 1, raptor); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_C, + EIGHT_AM); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_C, EIGHT_AM, 0, + 1, 1, raptor); } @Test @@ -684,10 +554,12 @@ void initialWalkTransferShouldLeaveAsLateAsPossible(RaptorRouterTestBuilder buil // Connection from N to E can be connected by a walk transfer from N to D and then a route trip from D to E. // The route trip from D to E leaves at 8:18. The walk transfer requires 15 minutes of walking, thus should // leave N at 8:03 to reach D on time (8:18). The requested earliest departure time is 8:00. - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_N, STOP_E, EIGHT_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_N, STOP_E, + EIGHT_AM); assertEquals(1, connections.size()); assertEquals(EIGHT_AM.plusMinutes(3), connections.getFirst().getDepartureTime()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_N, STOP_E, EIGHT_AM, 0, 1, 1, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_N, STOP_E, EIGHT_AM, 0, + 1, 1, raptor); } } @@ -704,7 +576,8 @@ void takeFirstTripWithoutAddingSameStopTransferTimeAtFirstStop(RaptorRouterTestB // There should be a connection leaving stop A at 5:00 am and this test should ensure that the same stop // transfer time is not added at the first stop, i.e. departure time at 5:00 am should allow to board the // first trip at 5:00 am - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_H, + FIVE_AM); assertEquals(1, connections.size()); assertEquals(FIVE_AM, connections.getFirst().getDepartureTime()); } @@ -718,7 +591,8 @@ void missConnectingTripBecauseOfSameStopTransferTime(RaptorRouterTestBuilder bui .build(); // There should be a connection leaving stop A at 5:19 am and arriving at stop B at 5:24 am // Connection at 5:24 from B to H should be missed because of the same stop transfer time (120s) - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_H, + FIVE_AM); assertEquals(1, connections.size()); @@ -735,7 +609,8 @@ void catchConnectingTripBecauseOfNoSameStopTransferTime(RaptorRouterTestBuilder .build(); // There should be a connection leaving stop A at 5:19 am and arriving at stop B at 5:24 am // Connection at 5:24 from B to H should not be missed because of no same stop transfer time - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_H, + FIVE_AM); assertEquals(1, connections.size()); assertEquals(FIVE_AM.plusMinutes(19), connections.getFirst().getDepartureTime()); @@ -751,7 +626,8 @@ void catchConnectingTripWithSameStopTransferTime(RaptorRouterTestBuilder builder .build(); // There should be a connection leaving stop A at 5:17 am and arriving at stop B at 5:22 am // Connection at 5:24 from B to H should be cached when the same stop transfer time is 120s - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_H, + FIVE_AM); assertEquals(1, connections.size()); assertEquals(FIVE_AM.plusMinutes(17), connections.getFirst().getDepartureTime()); @@ -784,14 +660,16 @@ void findWalkableTransferWithMaxWalkingTime(RaptorRouterTestBuilder builder) { // - Route R1-F from A to F // - Route R4-R from F to P // - Route R3-F from P to Q - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM, - queryConfig); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM, queryConfig); // check if 2 connections were found assertEquals(2, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, 1, 2, raptor); - Helpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_Q, EIGHT_AM, 2, 0, 3, raptor); - Helpers.checkIfConnectionsAreParetoOptimal(connections); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, + 1, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.get(1), STOP_A, STOP_Q, EIGHT_AM, 2, 0, + 3, raptor); + RaptorRouterTestHelpers.checkIfConnectionsAreParetoOptimal(connections); } @Test @@ -802,10 +680,11 @@ void notFindWalkableTransferWithMaxWalkingTime(RaptorRouterTestBuilder builder) // Should only find three route leg connections, since direct transfer between D and N is longer than // allowed maximum walking distance (60 minutes): - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM, - queryConfig); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM, queryConfig); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 2, 0, 3, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 2, + 0, 3, raptor); } @Test @@ -820,10 +699,11 @@ void findConnectionWithMaxTransferNumber(RaptorRouterTestBuilder builder) { // - Foot Transfer from D to N // - Route R3-F from N to Q // 2. Connection with two transfers (see above) should not be found - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM, - queryConfig); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM, queryConfig); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, 1, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 0, + 1, 2, raptor); } @Test @@ -836,10 +716,11 @@ void findConnectionWithMaxTravelTime(RaptorRouterTestBuilder builder) { // - Route R1-F from A to F // - Route R4-R from F to P // - Route R3-F from P to Q - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM, - queryConfig); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM, queryConfig); assertEquals(1, connections.size()); - Helpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 2, 0, 3, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(connections.getFirst(), STOP_A, STOP_Q, EIGHT_AM, 2, + 0, 3, raptor); } @Test @@ -854,7 +735,7 @@ void useSameStopTransferTimeWithZeroMinimumTransferDuration(RaptorRouterTestBuil // There should be a connection leaving stop A at 5:19 am and arriving at stop B at 5:24 am. Connection // at 5:24 (next 5:39) from B to C should be missed because of the same stop transfer time (120s), // regardless of minimum same transfer duration at 0s - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM, + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM, queryConfig); assertEquals(1, connections.size()); @@ -873,7 +754,7 @@ void useMinimumTransferTime(RaptorRouterTestBuilder builder) { .build(); // There should be a connection leaving stop A at 5:19 am and arriving at stop B at 5:24 am. Connection // at 5:24 and 5:39 from B to C should be missed because of the minimum transfer duration (20 minutes) - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM, + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_H, FIVE_AM, queryConfig); assertEquals(1, connections.size()); @@ -887,12 +768,13 @@ void addMinimumTransferTimeToWalkTransferDuration(RaptorRouterTestBuilder builde queryConfig.setMinimumTransferDuration(20 * 60); // 20 minutes RaptorAlgorithm raptor = builder.buildWithDefaults(); - List connections = ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_Q, EIGHT_AM, - queryConfig); + List connections = RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_Q, + EIGHT_AM, queryConfig); assertEquals(2, connections.size()); Connection firstConnection = connections.getFirst(); - Helpers.assertEarliestArrivalConnection(firstConnection, STOP_A, STOP_Q, EIGHT_AM, 0, 1, 2, raptor); + RaptorRouterTestHelpers.assertEarliestArrivalConnection(firstConnection, STOP_A, STOP_Q, EIGHT_AM, 0, 1, 2, + raptor); // The walk transfer from D to N takes 60 minutes and the route from N to Q leaves every 75 minutes. Leg firstLeg = firstConnection.getRouteLegs().getFirst(); @@ -909,23 +791,23 @@ class IsoLines { @Test void createIsoLinesToAllStops(RaptorRouterTestBuilder builder) { RaptorAlgorithm raptor = builder.buildWithDefaults(); - Map isoLines = ConvenienceMethods.getIsoLines(raptor, Map.of(STOP_A, EIGHT_AM)); + Map isoLines = RaptorRouterTestHelpers.getIsoLines(raptor, Map.of(STOP_A, EIGHT_AM)); int stopsInSystem = 19; int expectedIsoLines = stopsInSystem - 1; - Helpers.assertIsoLines(isoLines, expectedIsoLines); + RaptorRouterTestHelpers.assertIsoLines(isoLines, STOP_A, EIGHT_AM, expectedIsoLines); } @Test void createIsoLinesToSomeStopsNotAllConnected(RaptorRouterTestBuilder builder) { // Route 1 and 3 are not connected, thus all Stops of Route 3 should not be reachable from A RaptorAlgorithm raptor = builder.withAddRoute1_AG().withAddRoute3_MQ().build(); - Map isoLines = ConvenienceMethods.getIsoLines(raptor, Map.of(STOP_A, EIGHT_AM)); + Map isoLines = RaptorRouterTestHelpers.getIsoLines(raptor, Map.of(STOP_A, EIGHT_AM)); List reachableStops = List.of(STOP_B, STOP_C, STOP_D, STOP_E, STOP_F, STOP_G); // Not Reachable Stops: M, K, N, O, P, Q - Helpers.assertIsoLines(isoLines, reachableStops.size()); + RaptorRouterTestHelpers.assertIsoLines(isoLines, STOP_A, EIGHT_AM, reachableStops.size()); for (String stop : reachableStops) { assertTrue(isoLines.containsKey(stop), "Stop " + stop + " should be reachable"); @@ -936,12 +818,12 @@ void createIsoLinesToSomeStopsNotAllConnected(RaptorRouterTestBuilder builder) { void createIsoLinesToStopsOfOtherLineOnlyConnectedByFootpath(RaptorRouterTestBuilder builder) { // Route 1 and Route 3 are only connected by Footpath between Stops D and N RaptorAlgorithm raptor = builder.withAddRoute1_AG().withAddRoute3_MQ().withAddTransfer1_ND().build(); - Map isoLines = ConvenienceMethods.getIsoLines(raptor, Map.of(STOP_A, EIGHT_AM)); + Map isoLines = RaptorRouterTestHelpers.getIsoLines(raptor, Map.of(STOP_A, EIGHT_AM)); List reachableStops = List.of(STOP_B, STOP_C, STOP_D, STOP_E, STOP_F, STOP_G, STOP_M, STOP_K, STOP_N, STOP_O, STOP_P, STOP_Q); - Helpers.assertIsoLines(isoLines, reachableStops.size()); + RaptorRouterTestHelpers.assertIsoLines(isoLines, STOP_A, EIGHT_AM, reachableStops.size()); for (String stop : reachableStops) { assertTrue(isoLines.containsKey(stop), "Stop " + stop + " should be reachable"); @@ -957,7 +839,7 @@ void createIsoLinesFromTwoNotConnectedSourceStops(RaptorRouterTestBuilder builde START_OF_DAY.plusHours(16)); List reachableStopsFromStopM = List.of(STOP_K, STOP_N, STOP_O, STOP_P, STOP_Q); - Map isoLines = ConvenienceMethods.getIsoLines(raptor, sourceStops); + Map isoLines = RaptorRouterTestHelpers.getIsoLines(raptor, sourceStops); assertEquals(reachableStopsFromStopA.size() + reachableStopsFromStopM.size(), isoLines.size()); @@ -997,7 +879,7 @@ void setUp(RaptorRouterTestBuilder builder) { void throwErrorWhenSourceStopNotExists() { String sourceStop = "NonExistentStop"; assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStop, STOP_A, EIGHT_AM), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStop, STOP_A, EIGHT_AM), "Source stop has to exists"); } @@ -1005,7 +887,7 @@ void throwErrorWhenSourceStopNotExists() { void throwErrorWhenTargetStopNotExists() { String targetStop = "NonExistentStop"; assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, targetStop, EIGHT_AM), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, targetStop, EIGHT_AM), "Target stop has to exists"); } @@ -1014,7 +896,7 @@ void notThrowErrorForValidAndNonExistingSourceStop() { Map sourceStops = Map.of(STOP_A, EIGHT_AM, "NonExistentStop", EIGHT_AM); Map targetStops = Map.of(STOP_H, 0); - assertDoesNotThrow(() -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops), + assertDoesNotThrow(() -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, targetStops), "Source stops can contain non-existing stops, if one entry is valid"); } @@ -1023,7 +905,7 @@ void notThrowErrorForValidAndNonExistingTargetStop() { Map sourceStops = Map.of(STOP_H, EIGHT_AM); Map targetStops = Map.of(STOP_A, 0, "NonExistentStop", 0); - assertDoesNotThrow(() -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops), + assertDoesNotThrow(() -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, targetStops), "Target stops can contain non-existing stops, if one entry is valid"); } @@ -1033,7 +915,7 @@ void throwErrorForInvalidWalkToTargetTimeFromOneOfManyTargetStops() { Map targetStops = Map.of(STOP_A, 0, STOP_B, -1); assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, targetStops), "Departure time has to be valid for all valid source stops"); } @@ -1043,7 +925,7 @@ void throwErrorNullSourceStops() { Map targetStops = Map.of(STOP_H, 0); assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, targetStops), "Source stops cannot be null"); } @@ -1053,7 +935,7 @@ void throwErrorNullTargetStops() { Map targetStops = null; assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, targetStops), "Target stops cannot be null"); } @@ -1063,7 +945,7 @@ void throwErrorEmptyMapSourceStops() { Map targetStops = Map.of(STOP_H, 0); assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, targetStops), "Source and target stops cannot be null"); } @@ -1073,14 +955,14 @@ void throwErrorEmptyMapTargetStops() { Map targetStops = Map.of(); assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, sourceStops, targetStops), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, sourceStops, targetStops), "Source and target stops cannot be null"); } @Test void throwErrorWhenRequestBetweenSameStop() { assertThrows(IllegalArgumentException.class, - () -> ConvenienceMethods.routeEarliestArrival(raptor, STOP_A, STOP_A, EIGHT_AM), + () -> RaptorRouterTestHelpers.routeEarliestArrival(raptor, STOP_A, STOP_A, EIGHT_AM), "Stops cannot be the same"); } diff --git a/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java index 0e8b6960..fd45dc7b 100644 --- a/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java +++ b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java @@ -53,11 +53,14 @@ public class RaptorRouterTestBuilder { private final List routes = new ArrayList<>(); private final List transfers = new ArrayList<>(); - private int sameStopTransferTime = 120; + + private int daysToScan = 1; + private int defaultSameStopTransferTime = 120; + private RaptorTripMaskProvider tripMaskProvider = new RaptorConfig.NoMaskProvider(); private static RaptorAlgorithm build(List routes, List transfers, int dayStart, int dayEnd, - int sameStopTransferTime) { - RaptorRouterBuilder builder = new RaptorRouterBuilder(sameStopTransferTime); + RaptorConfig config) { + RaptorRouterBuilder builder = new RaptorRouterBuilder(config); Set addedStops = new HashSet<>(); for (Route route : routes) { @@ -177,12 +180,31 @@ public RaptorRouterTestBuilder withAddTransfer(String sourceStop, String targetS } public RaptorRouterTestBuilder withSameStopTransferTime(int time) { - this.sameStopTransferTime = time; + this.defaultSameStopTransferTime = time; + return this; + } + + public RaptorRouterTestBuilder withMaxDaysToScan(int days) { + this.daysToScan = days; + return this; + } + + public RaptorRouterTestBuilder withTripMaskProvider(RaptorTripMaskProvider provider) { + this.tripMaskProvider = provider; return this; } public RaptorAlgorithm build() { - return build(routes, transfers, DAY_START_HOUR, DAY_END_HOUR, sameStopTransferTime); + return build(DAY_START_HOUR, DAY_END_HOUR); + } + + RaptorAlgorithm build(int startOfDay, int endOfDay) { + RaptorConfig config = new RaptorConfig(); + config.setDaysToScan(daysToScan); + config.setDefaultSameStopTransferTime(defaultSameStopTransferTime); + config.setMaskProvider(tripMaskProvider); + config.setStopTimeCacheSize(daysToScan); + return build(routes, transfers, startOfDay, endOfDay, config); } public RaptorAlgorithm buildWithDefaults() { diff --git a/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestHelpers.java b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestHelpers.java new file mode 100644 index 00000000..6bda7dfe --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestHelpers.java @@ -0,0 +1,185 @@ +package ch.naviqore.raptor.router; + +import ch.naviqore.raptor.Connection; +import ch.naviqore.raptor.QueryConfig; +import ch.naviqore.raptor.RaptorAlgorithm; +import ch.naviqore.raptor.TimeType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class RaptorRouterTestHelpers { + + static Map getIsoLines(RaptorAlgorithm raptor, Map sourceStops) { + return getIsoLines(raptor, sourceStops, new QueryConfig()); + } + + static Map getIsoLines(RaptorAlgorithm raptor, Map sourceStops, + QueryConfig config) { + return raptor.routeIsolines(sourceStops, TimeType.DEPARTURE, config); + } + + static List routeEarliestArrival(RaptorAlgorithm raptor, String sourceStopId, String targetStopId, + LocalDateTime departureTime) { + return routeEarliestArrival(raptor, createStopMap(sourceStopId, departureTime), createStopMap(targetStopId, 0)); + } + + static List routeEarliestArrival(RaptorAlgorithm raptor, String sourceStopId, String targetStopId, + LocalDateTime departureTime, QueryConfig config) { + return routeEarliestArrival(raptor, createStopMap(sourceStopId, departureTime), createStopMap(targetStopId, 0), + config); + } + + static Map createStopMap(String stopId, LocalDateTime value) { + return Map.of(stopId, value); + } + + static Map createStopMap(String stopId, int value) { + return Map.of(stopId, value); + } + + static List routeEarliestArrival(RaptorAlgorithm raptor, Map sourceStops, + Map targetStopIds) { + return routeEarliestArrival(raptor, sourceStops, targetStopIds, new QueryConfig()); + } + + static List routeEarliestArrival(RaptorAlgorithm raptor, Map sourceStops, + Map targetStopIds, QueryConfig config) { + return raptor.routeEarliestArrival(sourceStops, targetStopIds, config); + } + + static List routeLatestDeparture(RaptorAlgorithm raptor, String sourceStopId, String targetStopId, + LocalDateTime arrivalTime) { + return routeLatestDeparture(raptor, createStopMap(sourceStopId, 0), createStopMap(targetStopId, arrivalTime)); + } + + static List routeLatestDeparture(RaptorAlgorithm raptor, Map sourceStops, + Map targetStops) { + return routeLatestDeparture(raptor, sourceStops, targetStops, new QueryConfig()); + } + + static List routeLatestDeparture(RaptorAlgorithm raptor, Map sourceStops, + Map targetStops, QueryConfig config) { + return raptor.routeLatestDeparture(sourceStops, targetStops, config); + } + + static void assertEarliestArrivalConnection(Connection connection, String sourceStop, String targetStop, + LocalDateTime requestedDepartureTime, int numSameStopTransfers, + int numWalkTransfers, int numTrips, RaptorAlgorithm raptor) { + assertEquals(sourceStop, connection.getFromStopId()); + assertEquals(targetStop, connection.getToStopId()); + + assertFalse(connection.getDepartureTime().isBefore(requestedDepartureTime), + "Departure time should be greater equal than searched for departure time"); + assertNotNull(connection.getArrivalTime(), "Arrival time should not be null"); + + assertEquals(numSameStopTransfers, connection.getNumberOfSameStopTransfers(), + "Number of same stop transfers should match"); + assertEquals(numWalkTransfers, connection.getWalkTransfers().size(), "Number of walk transfers should match"); + assertEquals(numSameStopTransfers + numWalkTransfers, connection.getNumberOfTotalTransfers(), + "Number of transfers should match"); + + assertEquals(numTrips, connection.getRouteLegs().size(), "Number of trips should match"); + assertReverseDirectionConnection(connection, TimeType.ARRIVAL, raptor); + } + + static void assertLatestDepartureConnection(Connection connection, String sourceStop, String targetStop, + LocalDateTime requestedArrivalTime, int numSameStopTransfers, + int numWalkTransfers, int numTrips, RaptorAlgorithm raptor) { + assertEquals(sourceStop, connection.getFromStopId()); + assertEquals(targetStop, connection.getToStopId()); + + assertNotNull(connection.getDepartureTime(), "Departure time should not be null"); + assertFalse(connection.getArrivalTime().isAfter(requestedArrivalTime), + "Arrival time should be smaller equal than searched for arrival time"); + + assertEquals(numSameStopTransfers, connection.getNumberOfSameStopTransfers(), + "Number of same station transfers should match"); + assertEquals(numWalkTransfers, connection.getWalkTransfers().size(), "Number of walk transfers should match"); + assertEquals(numSameStopTransfers + numWalkTransfers, connection.getNumberOfTotalTransfers(), + "Number of transfers should match"); + + assertEquals(numTrips, connection.getRouteLegs().size(), "Number of trips should match"); + assertReverseDirectionConnection(connection, TimeType.DEPARTURE, raptor); + } + + static void assertReverseDirectionConnection(Connection connection, TimeType timeType, RaptorAlgorithm raptor) { + List connections; + if (timeType == TimeType.DEPARTURE) { + connections = routeEarliestArrival(raptor, connection.getFromStopId(), connection.getToStopId(), + connection.getDepartureTime()); + } else { + connections = routeLatestDeparture(raptor, connection.getFromStopId(), connection.getToStopId(), + connection.getArrivalTime()); + } + + // find the connections with the same amount of rounds (this one should match) + Connection matchingConnection = connections.stream() + .filter(c -> c.getRouteLegs().size() == connection.getRouteLegs().size()) + .findFirst() + .orElse(null); + + assertNotNull(matchingConnection, "Matching connection should be found"); + assertEquals(connection.getFromStopId(), matchingConnection.getFromStopId(), "From stop should match"); + assertEquals(connection.getToStopId(), matchingConnection.getToStopId(), "To stop should match"); + if (timeType == TimeType.DEPARTURE) { + assertEquals(connection.getDepartureTime(), matchingConnection.getDepartureTime(), + "Departure time should match"); + + // there is no guarantee that the arrival time is the same, but it should not be later (worse) than + // the arrival time of the matching connection + if (connection.getArrivalTime().isBefore(matchingConnection.getArrivalTime())) { + return; + } + } else { + assertEquals(connection.getArrivalTime(), matchingConnection.getArrivalTime(), "Arrival time should match"); + // there is no guarantee that the departure time is the same, but it should not be earlier (worse) than + // the departure time of the matching connection + if (connection.getDepartureTime().isBefore(matchingConnection.getArrivalTime())) { + return; + } + } + + assertEquals(connection.getDepartureTime(), matchingConnection.getDepartureTime(), + "Departure time should match"); + assertEquals(connection.getArrivalTime(), matchingConnection.getArrivalTime(), "Arrival time should match"); + assertEquals(connection.getNumberOfSameStopTransfers(), matchingConnection.getNumberOfSameStopTransfers(), + "Number of same stop transfers should match"); + assertEquals(connection.getWalkTransfers().size(), matchingConnection.getWalkTransfers().size(), + "Number of walk transfers should match"); + assertEquals(connection.getNumberOfTotalTransfers(), matchingConnection.getNumberOfTotalTransfers(), + "Number of transfers should match"); + assertEquals(connection.getRouteLegs().size(), matchingConnection.getRouteLegs().size(), + "Number of trips should match"); + } + + static void checkIfConnectionsAreParetoOptimal(List connections) { + Connection previousConnection = connections.getFirst(); + for (int i = 1; i < connections.size(); i++) { + Connection currentConnection = connections.get(i); + assertTrue(previousConnection.getDurationInSeconds() > currentConnection.getDurationInSeconds(), + "Previous connection should be slower than current connection"); + assertTrue(previousConnection.getRouteLegs().size() < currentConnection.getRouteLegs().size(), + "Previous connection should have fewer route legs than current connection"); + previousConnection = currentConnection; + } + } + + static void assertIsoLines(Map isoLines, String startStop, LocalDateTime departureTime, + int expectedIsoLines) { + assertEquals(expectedIsoLines, isoLines.size()); + assertFalse(isoLines.containsKey(startStop), "Source stop should not be in iso lines"); + for (Map.Entry entry : isoLines.entrySet()) { + assertFalse(entry.getValue().getDepartureTime().isBefore(departureTime), + "Departure time should be greater than or equal to departure time"); + assertFalse(entry.getValue().getArrivalTime().isBefore(departureTime), + "Arrival time should be greater than or equal to departure time"); + assertNotNull(entry.getValue().getArrivalTime(), "Arrival time must be set."); + assertEquals(startStop, entry.getValue().getFromStopId(), "From stop should be source stop"); + assertEquals(entry.getKey(), entry.getValue().getToStopId(), "To stop should be key of map entry"); + } + } +} diff --git a/src/test/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverterIT.java b/src/test/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverterIT.java index f20215ee..1a43b2b6 100644 --- a/src/test/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverterIT.java +++ b/src/test/java/ch/naviqore/service/impl/convert/GtfsToRaptorConverterIT.java @@ -4,6 +4,7 @@ import ch.naviqore.gtfs.schedule.GtfsScheduleTestData; import ch.naviqore.gtfs.schedule.model.GtfsSchedule; import ch.naviqore.raptor.RaptorAlgorithm; +import ch.naviqore.raptor.router.RaptorConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -11,14 +12,11 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.time.LocalDate; import static org.assertj.core.api.Assertions.assertThat; class GtfsToRaptorConverterIT { - private static final LocalDate DATE = LocalDate.of(2009, 4, 26); - private static final int SAME_STOP_TRANSFER_TIME = 120; private GtfsSchedule schedule; @BeforeEach @@ -29,8 +27,8 @@ void setUp(@TempDir Path tempDir) throws IOException { @Test void shouldConvertGtfsScheduleToRaptor() { - GtfsToRaptorConverter mapper = new GtfsToRaptorConverter(schedule, SAME_STOP_TRANSFER_TIME); - RaptorAlgorithm raptor = mapper.convert(DATE); + GtfsToRaptorConverter mapper = new GtfsToRaptorConverter(schedule, new RaptorConfig()); + RaptorAlgorithm raptor = mapper.convert(); assertThat(raptor).isNotNull(); } } \ No newline at end of file diff --git a/src/test/java/ch/naviqore/service/impl/transfer/SameStopTransferGeneratorTest.java b/src/test/java/ch/naviqore/service/impl/transfer/SameStopTransferGeneratorTest.java deleted file mode 100644 index 01f86353..00000000 --- a/src/test/java/ch/naviqore/service/impl/transfer/SameStopTransferGeneratorTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package ch.naviqore.service.impl.transfer; - -import ch.naviqore.gtfs.schedule.model.GtfsSchedule; -import ch.naviqore.gtfs.schedule.model.GtfsScheduleBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -public class SameStopTransferGeneratorTest { - - @Nested - class Constructor { - @Test - void simpleTransferGenerator() { - assertDoesNotThrow(() -> new SameStopTransferGenerator(120)); - } - - @Test - void negativeSameStopTransferTime_shouldThrowException() { - assertThrows(IllegalArgumentException.class, () -> new SameStopTransferGenerator(-1)); - } - - @Test - void zeroSameStopTransferTime_shouldNotThrowException() { - assertDoesNotThrow(() -> new SameStopTransferGenerator(0)); - } - } - - @Nested - class CreateTransfers { - - private GtfsSchedule schedule; - - @BeforeEach - void setUp() { - GtfsScheduleBuilder builder = GtfsSchedule.builder(); - builder.addStop("stop1", "Zürich, Stadelhofen", 47.366542, 8.548384); - builder.addStop("stop2", "Zürich, Opernhaus", 47.365030, 8.547976); - schedule = builder.build(); - } - - @Test - void shouldCreateTransfers_withPositiveSameStopTransferTime() { - SameStopTransferGenerator generator = new SameStopTransferGenerator(120); - List transfers = generator.generateTransfers(schedule); - - assertEquals(2, transfers.size()); - for (TransferGenerator.Transfer transfer : transfers) { - assertEquals(transfer.from(), transfer.to()); - assertEquals(120, transfer.duration()); - } - } - - @Test - void shouldCreateTransfers_withZeroSameStopTransferTime() { - SameStopTransferGenerator generator = new SameStopTransferGenerator(0); - List transfers = generator.generateTransfers(schedule); - - assertEquals(2, transfers.size()); - for (TransferGenerator.Transfer transfer : transfers) { - assertEquals(transfer.from(), transfer.to()); - assertEquals(0, transfer.duration()); - } - } - } -}