From 6e9d5c420697f0391ceb8978b847fbdc7b807026 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Mon, 29 Apr 2024 21:21:28 +0200 Subject: [PATCH 01/35] Format --- src/main/java/ch/naviqore/raptor/model/Raptor.java | 7 ++++++- src/test/java/ch/naviqore/Benchmark.java | 9 ++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index a293b343..34ddb719 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -46,6 +46,11 @@ public static RaptorBuilder builder() { public void routeEarliestArrival(String sourceStop, String targetStop, int departureTime) { log.debug("Routing earliest arrival from {} to {} at {}", sourceStop, targetStop, departureTime); + if (!stopsToIdx.containsKey(sourceStop) || !stopsToIdx.containsKey(targetStop)) { + log.error("Source or target stop not found in lookup {}-{}", sourceStop, targetStop); + return; + } + final int sourceIdx = stopsToIdx.get(sourceStop); final int targetIdx = stopsToIdx.get(targetStop); @@ -138,7 +143,7 @@ public void routeEarliestArrival(String sourceStop, String targetStop, int depar System.out.println(stop.id() + ": " + earliestArrival[stopsToIdx.get(stop.id())]); } - log.debug("Earliest arrival at {}: {}", targetStop, earliestArrival[stopsToIdx.get(targetStop)]); + log.debug("Earliest arrival at {}: {}", targetStop, earliestArrival[targetIdx]); } } diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 585e3ed9..5b0dfaac 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -33,8 +33,8 @@ final class Benchmark { private static final int N = 10000; private static final Dataset DATASET = Dataset.SWITZERLAND; - private static final LocalDate DATE = LocalDate.of(2024, 4, 26); - private static final int MAX_SECONDS_IN_DAY = 86400; + private static final LocalDate SCHEDULE_DATE = LocalDate.of(2024, 4, 26); + private static final int SECONDS_IN_DAY = 86400; private static final long MONITORING_INTERVAL_MS = 30000; private static final int NS_TO_MS_CONVERSION_FACTOR = 1_000_000; @@ -45,7 +45,6 @@ public static void main(String[] args) throws IOException, InterruptedException RouteRequest[] requests = sampleRouteRequests(stopIds); RoutingResult[] results = processRequests(raptor, requests); writeResultsToCsv(results); - } private static GtfsSchedule initializeSchedule() throws IOException, InterruptedException { @@ -56,7 +55,7 @@ private static GtfsSchedule initializeSchedule() throws IOException, Interrupted } private static Raptor initializeRaptor(GtfsSchedule schedule) throws InterruptedException { - Raptor raptor = new GtfsToRaptorConverter(Raptor.builder()).convert(schedule, DATE); + Raptor raptor = new GtfsToRaptorConverter(Raptor.builder()).convert(schedule, SCHEDULE_DATE); manageResources(); return raptor; } @@ -73,7 +72,7 @@ private static RouteRequest[] sampleRouteRequests(List stopIds) { int sourceIndex = random.nextInt(stopIds.size()); int destinationIndex = getRandomDestinationIndex(stopIds.size(), sourceIndex, random); requests[i] = new RouteRequest(stopIds.get(sourceIndex), stopIds.get(destinationIndex), - random.nextInt(MAX_SECONDS_IN_DAY)); + random.nextInt(SECONDS_IN_DAY)); } return requests; } From ae186addaa68aac9098d5a659fda0e7ef28dd0f9 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Fri, 17 May 2024 15:40:17 +0200 Subject: [PATCH 02/35] ENH: NAV-17 - First raptor implementation --- .../java/ch/naviqore/raptor/model/Raptor.java | 199 ++++++++++-------- .../ch/naviqore/raptor/model/RaptorTest.java | 4 +- 2 files changed, 119 insertions(+), 84 deletions(-) diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index 34ddb719..e3357eb5 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -2,10 +2,7 @@ import lombok.extern.log4j.Log4j2; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Raptor algorithm implementation @@ -16,6 +13,7 @@ public class Raptor { public final static int NO_INDEX = -1; + public final static int SAME_STOP_TRANSFER_TIME = 120; // lookup private final Map stopsToIdx; private final Map routesToIdx; @@ -46,104 +44,141 @@ public static RaptorBuilder builder() { public void routeEarliestArrival(String sourceStop, String targetStop, int departureTime) { log.debug("Routing earliest arrival from {} to {} at {}", sourceStop, targetStop, departureTime); - if (!stopsToIdx.containsKey(sourceStop) || !stopsToIdx.containsKey(targetStop)) { - log.error("Source or target stop not found in lookup {}-{}", sourceStop, targetStop); - return; - } + // TODO: Input validation, same stop, nulls, not exising stops. final int sourceIdx = stopsToIdx.get(sourceStop); final int targetIdx = stopsToIdx.get(targetStop); // initialization - int[] earliestArrival = new int[stops.length]; - Arrays.fill(earliestArrival, Integer.MAX_VALUE); - earliestArrival[sourceIdx] = departureTime; - - // add first stop to marked stops - Set marked = new HashSet<>(); - marked.add(sourceIdx); - - // perform rounds - int round = 0; - while (!marked.isEmpty()) { - log.info("Processing round {} (= transfers), marked: {}", round, - marked.stream().map(stopIdx -> stops[stopIdx].id()).toList()); - Set nextMarked = new HashSet<>(); - for (int stopIdx : marked) { - log.debug("Processing marked stop {} - {}", stopIdx, stops[stopIdx].id()); - Stop stop = stops[stopIdx]; - for (int i = stop.stopRouteIdx(); i < stop.numberOfRoutes(); i++) { - int routeIdx = stopRoutes[i]; - Route route = routes[routeIdx]; - log.debug("Scanning route {} - {}", routeIdx, route.id()); - // iterate until current stop index is found on route - int stopOffset = 0; - for (int j = route.firstRouteStopIdx(); j < route.firstRouteStopIdx() + route.numberOfStops(); j++) { - if (routeStops[j].stopIndex() == stopIdx) { - break; + final List earliestArrivalsPerRound = new ArrayList<>(); + earliestArrivalsPerRound.add(new Arrival[stops.length]); + earliestArrivalsPerRound.getFirst()[sourceIdx] = new Arrival(departureTime, ArrivalType.INITIAL, NO_INDEX, + NO_INDEX); + + Set markedStops = new HashSet<>(); + markedStops.add(sourceIdx); + + int round = 1; + while (!markedStops.isEmpty()) { + log.debug("Scanning routes for round {} (=trips)", round); + log.debug("Marked stops: {}", markedStops); + Set markedStopsNext = new HashSet<>(); + + // initialize the earliest arrivals for current round + Arrival[] earliestArrivalsLastRound = earliestArrivalsPerRound.get(round - 1); + earliestArrivalsPerRound.add(earliestArrivalsLastRound.clone()); + Arrival[] earliestArrivalsThisRound = earliestArrivalsPerRound.get(round); + + // get routes of marked stops + Set routesToScan = new HashSet<>(); + for (int stopIdx : markedStops) { + Stop currentStop = stops[stopIdx]; + int stopRouteIdx = currentStop.stopRouteIdx(); + int stopRouteEndIdx = stopRouteIdx + currentStop.numberOfRoutes(); + while (stopRouteIdx < stopRouteEndIdx) { + routesToScan.add(routes[stopRoutes[stopRouteIdx]]); + stopRouteIdx++; + } + } + log.debug("Routes to scan: {}", routesToScan); + + // scan routes + for (Route currentRoute : routesToScan) { + log.debug("Scanning route {}", currentRoute.id()); + final int firstRouteStopIdx = currentRoute.firstRouteStopIdx(); + final int firstStopTimeIdx = currentRoute.firstStopTimeIdx(); + final int numberOfStops = currentRoute.numberOfStops(); + final int numberOfTrips = currentRoute.numberOfTrips(); + int tripOffset = 0; + boolean enteredTrip = false; + + // iterate over stops in route + for (int stopOffset = 0; stopOffset < numberOfStops; stopOffset++) { + int earliestDepartureTime; + int stopIdx = routeStops[firstRouteStopIdx + stopOffset].stopIndex(); + Stop stop = stops[stopIdx]; + + Arrival currentArrivalLastRound = earliestArrivalsLastRound[routeStops[firstRouteStopIdx + stopOffset].stopIndex()]; + + // find first marked stop in route + if (!enteredTrip) { + if (currentArrivalLastRound == null) { + // when current arrival is null, then the stop cannot be reached + log.debug("Stop {} cannot be reached, continue", stop.id()); + continue; } - stopOffset++; - } - log.debug("Stop offset on route {} is {} - {}", route.id(), stopOffset, - stops[routeStops[route.firstRouteStopIdx() + stopOffset].stopIndex()].id()); - // find active trip: check if possible to hop on trip - int arrivalTimeAtCurrentStop = earliestArrival[stopIdx]; - int activeTrip = 0; - for (int k = route.firstStopTimeIdx() + stopOffset; k < route.firstStopTimeIdx() + route.numberOfTrips() * route.numberOfStops(); k += route.numberOfStops()) { - // TODO: Consider dwell time - if (stopTimes[k].departure() >= arrivalTimeAtCurrentStop) { - break; + + if (!markedStops.contains(stopIdx)) { + log.debug("marked stops: {}, stopidx: {}", markedStops, currentArrivalLastRound); + // this stop has already been scanned in previous round without improved arrival time + log.debug("Stop {} was not improved in previous round, continue", stop.id()); + continue; } - activeTrip++; - } - log.debug("Scanning active trip number {} on route {} - {}", activeTrip, routeIdx, route.id()); - int from = route.firstStopTimeIdx() + activeTrip * route.numberOfStops() + stopOffset; - int to = route.firstStopTimeIdx() + (activeTrip + 1) * route.numberOfStops(); - int currentRouteStopIdx = route.firstRouteStopIdx() + stopOffset; - for (int k = from; k < to; k++) { - int currentStopIdx = routeStops[currentRouteStopIdx].stopIndex(); - if (stopTimes[k].arrival() < earliestArrival[currentStopIdx]) { - earliestArrival[currentStopIdx] = stopTimes[k].arrival(); - nextMarked.add(currentStopIdx); + + // got first marked stop in the route + log.debug("Got first entry point at stop {} at {} (type: {})", stop.id(), + currentArrivalLastRound.time, currentArrivalLastRound.type()); + enteredTrip = true; + earliestDepartureTime = currentArrivalLastRound.time(); + } else { + // in this case we are on a trip and need to check if arrival time has improved + // get time of arrival on current trip + StopTime stopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset]; + if (currentArrivalLastRound == null || stopTime.arrival() < currentArrivalLastRound.time) { + log.debug("Stop {} was improved", stop.id()); + // TODO: Get correct route idx + earliestArrivalsThisRound[stopIdx] = new Arrival(stopTime.arrival(), ArrivalType.ROUTE, -1, + stopIdx); + // mark stop improvement for next round + markedStopsNext.add(stopIdx); + // Because earlier trip is not possible + continue; } - currentRouteStopIdx++; + earliestDepartureTime = currentArrivalLastRound.time; } - } - } - // relax transfers (= footpaths) - for (int stopIdx : marked) { - Stop stop = stops[stopIdx]; - if (stop.transferIdx() == NO_INDEX) { - continue; - } - for (int k = stop.transferIdx(); k < stop.numberOfTransfers(); k++) { - Transfer transfer = transfers[k]; - int targetStopIdx = transfer.targetStopIdx(); - int arrivalTimeAfterTransfer = earliestArrival[stopIdx] + transfer.duration(); - if (arrivalTimeAfterTransfer < earliestArrival[targetStopIdx]) { - earliestArrival[targetStopIdx] = arrivalTimeAfterTransfer; - nextMarked.add(targetStopIdx); + // find active trip, increase trip offset + tripOffset = 0; + while (tripOffset < numberOfTrips) { + StopTime currentStopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset]; + if (currentStopTime.departure() >= earliestDepartureTime + SAME_STOP_TRANSFER_TIME) { + // active trip: possible to enter this trip + log.debug("Found active trip ({}) on route {}", tripOffset, currentRoute.id()); + break; + } + tripOffset++; } } } - // prepare for next round - marked = nextMarked; + // TODO: Relax footpath transfers + + // prepare next round + markedStops = markedStopsNext; round++; } - // print results for debugging - for (int i = 0; i < earliestArrival.length; i++) { - if (earliestArrival[i] == Integer.MAX_VALUE) { - earliestArrival[i] = -1; + // get pareto-optimal solutions + int legs = -1; + for (Arrival[] earliestArrival : earliestArrivalsPerRound) { + Arrival targetArrival = earliestArrival[targetIdx]; + if (targetArrival != null) { + log.info("Found connection with {} legs: {}", legs, targetArrival); + } else { + log.info("Found no connection with {} legs", legs); } + legs++; } - for (Stop stop : stops) { - System.out.println(stop.id() + ": " + earliestArrival[stopsToIdx.get(stop.id())]); - } - log.debug("Earliest arrival at {}: {}", targetStop, earliestArrival[targetIdx]); + } + + enum ArrivalType { + INITIAL, + ROUTE, + TRANSFER + } + + record Arrival(int time, ArrivalType type, int enteredArrivalIdx, int enteredAtStopIdx) { } } diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java index ee4c989d..780e3b55 100644 --- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java @@ -15,10 +15,10 @@ *
  *                      M
  *                      |
- *        I ---- J ---- K ---- L      R
+ *        I ---- J ---- K ---- L #### R
  *        |             |             |
  *        |             N ---- O ---- P ---- Q
- *        |                           |
+ *        |             #             |
  * A ---- B ---- C ---- D ---- E ---- F ---- G
  *        |                           |
  *        H                           S

From 85317b6a96f950b70262d01eef711cf065659f5c Mon Sep 17 00:00:00 2001
From: Merlin Unterfinger 
Date: Fri, 17 May 2024 16:58:02 +0200
Subject: [PATCH 03/35] ENH: NAV-17 - Reconstruction of pareto optimal
 solutions

---
 .../java/ch/naviqore/raptor/model/Raptor.java | 87 ++++++++++++++-----
 src/test/java/ch/naviqore/Benchmark.java      |  5 +-
 .../ch/naviqore/raptor/model/RaptorTest.java  |  4 +-
 3 files changed, 71 insertions(+), 25 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index e3357eb5..dc4ae983 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -1,5 +1,8 @@
 package ch.naviqore.raptor.model;
 
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.extern.java.Log;
 import lombok.extern.log4j.Log4j2;
 
 import java.util.*;
@@ -41,22 +44,22 @@ public static RaptorBuilder builder() {
         return new RaptorBuilder();
     }
 
-    public void routeEarliestArrival(String sourceStop, String targetStop, int departureTime) {
-        log.debug("Routing earliest arrival from {} to {} at {}", sourceStop, targetStop, departureTime);
+    public List routeEarliestArrival(String sourceStopId, String targetStopId, int departureTime) {
+        log.info("Routing earliest arrival from {} to {} at {}", sourceStopId, targetStopId, departureTime);
 
         // TODO: Input validation, same stop, nulls, not exising stops.
 
-        final int sourceIdx = stopsToIdx.get(sourceStop);
-        final int targetIdx = stopsToIdx.get(targetStop);
+        final int sourceStopIdx = stopsToIdx.get(sourceStopId);
+        final int targetStopIdx = stopsToIdx.get(targetStopId);
 
         // initialization
         final List earliestArrivalsPerRound = new ArrayList<>();
         earliestArrivalsPerRound.add(new Arrival[stops.length]);
-        earliestArrivalsPerRound.getFirst()[sourceIdx] = new Arrival(departureTime, ArrivalType.INITIAL, NO_INDEX,
+        earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new Arrival(departureTime, ArrivalType.INITIAL, NO_INDEX,
                 NO_INDEX);
 
         Set markedStops = new HashSet<>();
-        markedStops.add(sourceIdx);
+        markedStops.add(sourceStopIdx);
 
         int round = 1;
         while (!markedStops.isEmpty()) {
@@ -70,20 +73,21 @@ public void routeEarliestArrival(String sourceStop, String targetStop, int depar
             Arrival[] earliestArrivalsThisRound = earliestArrivalsPerRound.get(round);
 
             // get routes of marked stops
-            Set routesToScan = new HashSet<>();
+            Set routesToScan = new HashSet<>();
             for (int stopIdx : markedStops) {
                 Stop currentStop = stops[stopIdx];
                 int stopRouteIdx = currentStop.stopRouteIdx();
                 int stopRouteEndIdx = stopRouteIdx + currentStop.numberOfRoutes();
                 while (stopRouteIdx < stopRouteEndIdx) {
-                    routesToScan.add(routes[stopRoutes[stopRouteIdx]]);
+                    routesToScan.add(stopRoutes[stopRouteIdx]);
                     stopRouteIdx++;
                 }
             }
             log.debug("Routes to scan: {}", routesToScan);
 
             // scan routes
-            for (Route currentRoute : routesToScan) {
+            for (int currentRouteIdx : routesToScan) {
+                Route currentRoute = routes[currentRouteIdx];
                 log.debug("Scanning route {}", currentRoute.id());
                 final int firstRouteStopIdx = currentRoute.firstRouteStopIdx();
                 final int firstStopTimeIdx = currentRoute.firstStopTimeIdx();
@@ -99,7 +103,7 @@ public void routeEarliestArrival(String sourceStop, String targetStop, int depar
                     Stop stop = stops[stopIdx];
 
                     Arrival currentArrivalLastRound = earliestArrivalsLastRound[routeStops[firstRouteStopIdx + stopOffset].stopIndex()];
-
+                    int enteredAtIndex;
                     // find first marked stop in route
                     if (!enteredTrip) {
                         if (currentArrivalLastRound == null) {
@@ -126,9 +130,8 @@ public void routeEarliestArrival(String sourceStop, String targetStop, int depar
                         StopTime stopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset];
                         if (currentArrivalLastRound == null || stopTime.arrival() < currentArrivalLastRound.time) {
                             log.debug("Stop {} was improved", stop.id());
-                            // TODO: Get correct route idx
-                            earliestArrivalsThisRound[stopIdx] = new Arrival(stopTime.arrival(), ArrivalType.ROUTE, -1,
-                                    stopIdx);
+                            earliestArrivalsThisRound[stopIdx] = new Arrival(stopTime.arrival(), ArrivalType.ROUTE,
+                                    currentRouteIdx, stopIdx);
                             // mark stop improvement for next round
                             markedStopsNext.add(stopIdx);
                             // Because earlier trip is not possible
@@ -159,17 +162,59 @@ public void routeEarliestArrival(String sourceStop, String targetStop, int depar
         }
 
         // get pareto-optimal solutions
-        int legs = -1;
-        for (Arrival[] earliestArrival : earliestArrivalsPerRound) {
-            Arrival targetArrival = earliestArrival[targetIdx];
-            if (targetArrival != null) {
-                log.info("Found connection with {} legs: {}", legs, targetArrival);
-            } else {
-                log.info("Found no connection with {} legs", legs);
+        return reconstructParetoOptimalSolutions(earliestArrivalsPerRound, targetStopIdx);
+    }
+
+    private List reconstructParetoOptimalSolutions(List earliestArrivalsPerRound,
+                                                               int targetStopIdx) {
+        final List connections = new ArrayList<>();
+
+        // iterate over all rounds
+        for (int i = 1; i < earliestArrivalsPerRound.size(); i++) {
+            Arrival arrival = earliestArrivalsPerRound.get(i)[targetStopIdx];
+
+            // target stop not reached in this round
+            if (arrival == null) {
+                continue;
+            }
+
+            // iterate through arrivals starting at target stop
+            Connection connection = new Connection();
+            int currentStopIdx = targetStopIdx;
+
+            while (arrival.type != ArrivalType.INITIAL) {
+                if (arrival.type == ArrivalType.ROUTE) {
+                    connection.legs.add(new Leg(stops[arrival.enteredAtStopIdx].id(), stops[currentStopIdx].id(),
+                            routes[arrival.enteredArrivalIdx()].id()));
+                    currentStopIdx = arrival.enteredAtStopIdx;
+                    arrival = earliestArrivalsPerRound.get(i - 1)[arrival.enteredAtStopIdx()];
+                    if (arrival == null)
+                    {
+                        log.debug("....");
+                    }
+                } else if (arrival.type == ArrivalType.TRANSFER) {
+                    throw new IllegalStateException("No transfers yet!");
+                }
+            }
+
+            // reverse order of legs and add connection
+            if (!connection.legs.isEmpty()) {
+                Collections.reverse(connection.legs);
+                connections.add(connection);
             }
-            legs++;
+
         }
 
+        return connections;
+    }
+
+    @NoArgsConstructor
+    @Getter
+    public static class Connection {
+        private final List legs = new ArrayList<>();
+    }
+
+    public record Leg(String fromStopId, String toStopId, String routeId) {
     }
 
     enum ArrivalType {
diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java
index c661a00d..39545d75 100644
--- a/src/test/java/ch/naviqore/Benchmark.java
+++ b/src/test/java/ch/naviqore/Benchmark.java
@@ -34,11 +34,12 @@
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 final class Benchmark {
 
+    private static final long SEED = 1234;
     private static final int N = 10000;
     private static final Dataset DATASET = Dataset.SWITZERLAND;
     private static final LocalDate SCHEDULE_DATE = LocalDate.of(2024, 4, 26);
     private static final int SECONDS_IN_DAY = 86400;
-    private static final long MONITORING_INTERVAL_MS = 30000;
+    private static final long MONITORING_INTERVAL_MS = 30;
     private static final int NS_TO_MS_CONVERSION_FACTOR = 1_000_000;
 
     public static void main(String[] args) throws IOException, InterruptedException {
@@ -69,7 +70,7 @@ private static void manageResources() throws InterruptedException {
     }
 
     private static RouteRequest[] sampleRouteRequests(List stopIds) {
-        Random random = new Random();
+        Random random = new Random(SEED);
         RouteRequest[] requests = new RouteRequest[Benchmark.N];
         for (int i = 0; i < Benchmark.N; i++) {
             int sourceIndex = random.nextInt(stopIds.size());
diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
index 780e3b55..a6cd67e7 100644
--- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
+++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
@@ -99,8 +99,8 @@ record Transfer(String sourceStop, String targetStop, int duration) {
     class EarliestArrival {
         @Test
         void testRoutingBetweenIntersectingRoutes() {
-            raptor.routeEarliestArrival("A", "Q", 8 * 60 * 60);
-
+            List connections = raptor.routeEarliestArrival("A", "Q", 8 * 60 * 60);
+            System.out.println(connections);
             // TODO: assertThat...
         }
     }

From 193e7e3163a1cfa92217f993d650fa39a51e3d42 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Fri, 17 May 2024 17:32:29 +0200
Subject: [PATCH 04/35] FIX: NAV-17 - Fix reconstruct connections.

---
 .../java/ch/naviqore/raptor/model/Raptor.java | 38 +++++++++----------
 1 file changed, 18 insertions(+), 20 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index dc4ae983..53f5f69c 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -2,7 +2,7 @@
 
 import lombok.Getter;
 import lombok.NoArgsConstructor;
-import lombok.extern.java.Log;
+import lombok.ToString;
 import lombok.extern.log4j.Log4j2;
 
 import java.util.*;
@@ -56,7 +56,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
         final List earliestArrivalsPerRound = new ArrayList<>();
         earliestArrivalsPerRound.add(new Arrival[stops.length]);
         earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new Arrival(departureTime, ArrivalType.INITIAL, NO_INDEX,
-                NO_INDEX);
+                sourceStopIdx, null);
 
         Set markedStops = new HashSet<>();
         markedStops.add(sourceStopIdx);
@@ -95,6 +95,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                 final int numberOfTrips = currentRoute.numberOfTrips();
                 int tripOffset = 0;
                 boolean enteredTrip = false;
+                Arrival enteredAtArrival = null;
 
                 // iterate over stops in route
                 for (int stopOffset = 0; stopOffset < numberOfStops; stopOffset++) {
@@ -103,7 +104,6 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                     Stop stop = stops[stopIdx];
 
                     Arrival currentArrivalLastRound = earliestArrivalsLastRound[routeStops[firstRouteStopIdx + stopOffset].stopIndex()];
-                    int enteredAtIndex;
                     // find first marked stop in route
                     if (!enteredTrip) {
                         if (currentArrivalLastRound == null) {
@@ -131,7 +131,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                         if (currentArrivalLastRound == null || stopTime.arrival() < currentArrivalLastRound.time) {
                             log.debug("Stop {} was improved", stop.id());
                             earliestArrivalsThisRound[stopIdx] = new Arrival(stopTime.arrival(), ArrivalType.ROUTE,
-                                    currentRouteIdx, stopIdx);
+                                    currentRouteIdx, stopIdx, enteredAtArrival);
                             // mark stop improvement for next round
                             markedStopsNext.add(stopIdx);
                             // Because earlier trip is not possible
@@ -142,6 +142,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
 
                     // find active trip, increase trip offset
                     tripOffset = 0;
+                    enteredAtArrival = currentArrivalLastRound;
                     while (tripOffset < numberOfTrips) {
                         StopTime currentStopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset];
                         if (currentStopTime.departure() >= earliestDepartureTime + SAME_STOP_TRANSFER_TIME) {
@@ -180,18 +181,14 @@ private List reconstructParetoOptimalSolutions(List earli
 
             // iterate through arrivals starting at target stop
             Connection connection = new Connection();
-            int currentStopIdx = targetStopIdx;
 
             while (arrival.type != ArrivalType.INITIAL) {
                 if (arrival.type == ArrivalType.ROUTE) {
-                    connection.legs.add(new Leg(stops[arrival.enteredAtStopIdx].id(), stops[currentStopIdx].id(),
-                            routes[arrival.enteredArrivalIdx()].id()));
-                    currentStopIdx = arrival.enteredAtStopIdx;
-                    arrival = earliestArrivalsPerRound.get(i - 1)[arrival.enteredAtStopIdx()];
-                    if (arrival == null)
-                    {
-                        log.debug("....");
-                    }
+                    String fromStopId = stops[arrival.previous.stopIdx].id();
+                    String toStopId = stops[arrival.stopIdx].id();
+                    String routeId = routes[arrival.routeOrTransferIdx].id();
+                    connection.legs.add(new Leg(fromStopId, toStopId, routeId));
+                    arrival = arrival.previous;
                 } else if (arrival.type == ArrivalType.TRANSFER) {
                     throw new IllegalStateException("No transfers yet!");
                 }
@@ -208,8 +205,15 @@ private List reconstructParetoOptimalSolutions(List earli
         return connections;
     }
 
+    enum ArrivalType {
+        INITIAL,
+        ROUTE,
+        TRANSFER
+    }
+
     @NoArgsConstructor
     @Getter
+    @ToString
     public static class Connection {
         private final List legs = new ArrayList<>();
     }
@@ -217,13 +221,7 @@ public static class Connection {
     public record Leg(String fromStopId, String toStopId, String routeId) {
     }
 
-    enum ArrivalType {
-        INITIAL,
-        ROUTE,
-        TRANSFER
-    }
-
-    record Arrival(int time, ArrivalType type, int enteredArrivalIdx, int enteredAtStopIdx) {
+    record Arrival(int time, ArrivalType type, int routeOrTransferIdx, int stopIdx, Arrival previous) {
     }
 
 }

From d0632d08a5b8b5761defd22f078475a3be778243 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Fri, 17 May 2024 18:09:29 +0200
Subject: [PATCH 05/35] FIX: NAV-17 - Fix number per trips per route and handle
 end of day gracefully.

---
 src/main/java/ch/naviqore/raptor/model/Raptor.java        | 8 +++++++-
 src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java | 2 +-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 53f5f69c..80a7a246 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -150,7 +150,13 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                             log.debug("Found active trip ({}) on route {}", tripOffset, currentRoute.id());
                             break;
                         }
-                        tripOffset++;
+                        if( tripOffset < numberOfTrips - 1 ){
+                            tripOffset++;
+                        } else {
+                            // no active trip found
+                            log.debug("No active trip found on route {}", currentRoute.id());
+                            break;
+                        }
                     }
                 }
             }
diff --git a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java
index ddbd5474..0251c7db 100644
--- a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java
+++ b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java
@@ -174,7 +174,7 @@ private RouteTraversal buildRouteTraversal() {
             }
 
             routeArr[routeIdx] = new Route(routeId, routeStopCnt - currentRouteStops.size(), currentRouteStops.size(),
-                    stopTimeCnt - currentStopTimes.size(), currentStopTimes.size());
+                    stopTimeCnt - currentStopTimes.size(), currentStopTimes.size() / currentRouteStops.size());
         }
         return new RouteTraversal(stopTimeArr, routeArr, routeStopArr);
     }

From d3a1dad6eabac5d23037f3cf04e10d1e2b19794a Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Fri, 17 May 2024 18:10:07 +0200
Subject: [PATCH 06/35] FIX: NAV-17 - Return empty list when invalid stops
 requested.

---
 src/main/java/ch/naviqore/raptor/model/Raptor.java | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 80a7a246..a8c51cdc 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -47,10 +47,17 @@ public static RaptorBuilder builder() {
     public List routeEarliestArrival(String sourceStopId, String targetStopId, int departureTime) {
         log.info("Routing earliest arrival from {} to {} at {}", sourceStopId, targetStopId, departureTime);
 
-        // TODO: Input validation, same stop, nulls, not exising stops.
+        int sourceStopIdx;
+        int targetStopIdx;
 
-        final int sourceStopIdx = stopsToIdx.get(sourceStopId);
-        final int targetStopIdx = stopsToIdx.get(targetStopId);
+        // TODO: Input validation, same stop, nulls, not exising stops.
+        try {
+            sourceStopIdx = stopsToIdx.get(sourceStopId);
+            targetStopIdx = stopsToIdx.get(targetStopId);
+        } catch (Exception e) {
+            log.error("Error routing earliest arrival from {} to {} at {}", sourceStopId, targetStopId, departureTime);
+            return new ArrayList<>();
+        }
 
         // initialization
         final List earliestArrivalsPerRound = new ArrayList<>();

From 8beefb42a9c3f3dbd0aa3e190a1c916db653caa8 Mon Sep 17 00:00:00 2001
From: Merlin Unterfinger 
Date: Fri, 17 May 2024 20:15:54 +0200
Subject: [PATCH 07/35] ENH: NAV-17 - Adapt benchmark

---
 src/test/java/ch/naviqore/Benchmark.java | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)

diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java
index 39545d75..60006112 100644
--- a/src/test/java/ch/naviqore/Benchmark.java
+++ b/src/test/java/ch/naviqore/Benchmark.java
@@ -35,7 +35,7 @@
 final class Benchmark {
 
     private static final long SEED = 1234;
-    private static final int N = 10000;
+    private static final int N = 1000;
     private static final Dataset DATASET = Dataset.SWITZERLAND;
     private static final LocalDate SCHEDULE_DATE = LocalDate.of(2024, 4, 26);
     private static final int SECONDS_IN_DAY = 86400;
@@ -91,18 +91,18 @@ private static RoutingResult[] processRequests(Raptor raptor, RouteRequest[] req
         RoutingResult[] responses = new RoutingResult[requests.length];
         for (int i = 0; i < requests.length; i++) {
             long startTime = System.nanoTime();
-            // TODO: RaptorResponse result =
-            raptor.routeEarliestArrival(requests[i].sourceStop(), requests[i].targetStop(),
-                    requests[i].departureTime());
+            List connections = raptor.routeEarliestArrival(requests[i].sourceStop(),
+                    requests[i].targetStop(), requests[i].departureTime());
             long endTime = System.nanoTime();
             responses[i] = new RoutingResult(requests[i].sourceStop(), requests[i].targetStop(),
-                    requests[i].departureTime(), 0, 0, 0, (endTime - startTime) / NS_TO_MS_CONVERSION_FACTOR);
+                    requests[i].departureTime(), connections, 0, 0, 0,
+                    (endTime - startTime) / NS_TO_MS_CONVERSION_FACTOR);
         }
         return responses;
     }
 
     private static void writeResultsToCsv(RoutingResult[] results) throws IOException {
-        String header = "source_stop,target_stop,requested_departure_time,departure_time,arrival_time,transfers,processing_time_ms";
+        String header = "source_stop,target_stop,requested_departure_time,connections,departure_time,arrival_time,transfers,processing_time_ms";
         String folderPath = String.format("benchmark/output/%s", DATASET.name().toLowerCase());
         String fileName = String.format("%s_raptor_results.csv",
                 LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd_HH_mm_ss")));
@@ -117,9 +117,9 @@ private static void writeResultsToCsv(RoutingResult[] results) throws IOExceptio
             writer.println(header);
 
             for (RoutingResult result : results) {
-                writer.printf("%s,%s,%d,%d,%d,%d,%d%n", result.sourceStop(), result.targetStop(),
-                        result.requestedDepartureTime(), result.departureTime(), result.arrivalTime(),
-                        result.transfers(), result.time());
+                writer.printf("%s,%s,%d,%d,%d,%d,%d,%d%n", result.sourceStop, result.targetStop,
+                        result.requestedDepartureTime, result.connections.size(), result.departureTime,
+                        result.arrivalTime, result.transfers, result.time);
             }
         }
     }
@@ -127,7 +127,8 @@ private static void writeResultsToCsv(RoutingResult[] results) throws IOExceptio
     record RouteRequest(String sourceStop, String targetStop, int departureTime) {
     }
 
-    record RoutingResult(String sourceStop, String targetStop, int requestedDepartureTime, int departureTime,
-                         int arrivalTime, int transfers, long time) {
+    record RoutingResult(String sourceStop, String targetStop, int requestedDepartureTime,
+                         List connections, int departureTime, int arrivalTime, int transfers,
+                         long time) {
     }
 }

From dcfd0de7bd1f44a42397806525471869adf127de Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Fri, 17 May 2024 23:56:46 +0200
Subject: [PATCH 08/35] ENH: NAV-17 - Make routeEarliestArrival return only
 paretoOptimalConnections without duplicates.

---
 .../java/ch/naviqore/raptor/model/Raptor.java | 28 +++++++++----------
 1 file changed, 13 insertions(+), 15 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index a8c51cdc..8f76889c 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -60,6 +60,10 @@ public List routeEarliestArrival(String sourceStopId, String targetS
         }
 
         // initialization
+        final int[] earliestArrivals = new int[stops.length];
+        Arrays.fill(earliestArrivals, Integer.MAX_VALUE);
+        earliestArrivals[sourceStopIdx] = departureTime;
+
         final List earliestArrivalsPerRound = new ArrayList<>();
         earliestArrivalsPerRound.add(new Arrival[stops.length]);
         earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new Arrival(departureTime, ArrivalType.INITIAL, NO_INDEX,
@@ -76,7 +80,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
 
             // initialize the earliest arrivals for current round
             Arrival[] earliestArrivalsLastRound = earliestArrivalsPerRound.get(round - 1);
-            earliestArrivalsPerRound.add(earliestArrivalsLastRound.clone());
+            earliestArrivalsPerRound.add(new Arrival[stops.length]);
             Arrival[] earliestArrivalsThisRound = earliestArrivalsPerRound.get(round);
 
             // get routes of marked stops
@@ -106,36 +110,32 @@ public List routeEarliestArrival(String sourceStopId, String targetS
 
                 // iterate over stops in route
                 for (int stopOffset = 0; stopOffset < numberOfStops; stopOffset++) {
-                    int earliestDepartureTime;
                     int stopIdx = routeStops[firstRouteStopIdx + stopOffset].stopIndex();
                     Stop stop = stops[stopIdx];
+                    int earliestArrivalTime = earliestArrivals[stopIdx];
 
-                    Arrival currentArrivalLastRound = earliestArrivalsLastRound[routeStops[firstRouteStopIdx + stopOffset].stopIndex()];
                     // find first marked stop in route
                     if (!enteredTrip) {
-                        if (currentArrivalLastRound == null) {
-                            // when current arrival is null, then the stop cannot be reached
+                        if (earliestArrivalTime == Integer.MAX_VALUE) {
+                            // when current arrival is infinity (Integer.MAX_VALUE), then the stop cannot be reached
                             log.debug("Stop {} cannot be reached, continue", stop.id());
                             continue;
                         }
 
                         if (!markedStops.contains(stopIdx)) {
-                            log.debug("marked stops: {}, stopidx: {}", markedStops, currentArrivalLastRound);
                             // this stop has already been scanned in previous round without improved arrival time
                             log.debug("Stop {} was not improved in previous round, continue", stop.id());
                             continue;
                         }
 
                         // got first marked stop in the route
-                        log.debug("Got first entry point at stop {} at {} (type: {})", stop.id(),
-                                currentArrivalLastRound.time, currentArrivalLastRound.type());
+                        log.debug("Got first entry point at stop {} at {}", stop.id(), earliestArrivalTime);
                         enteredTrip = true;
-                        earliestDepartureTime = currentArrivalLastRound.time();
                     } else {
                         // in this case we are on a trip and need to check if arrival time has improved
                         // get time of arrival on current trip
                         StopTime stopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset];
-                        if (currentArrivalLastRound == null || stopTime.arrival() < currentArrivalLastRound.time) {
+                        if (stopTime.arrival() < earliestArrivalTime) {
                             log.debug("Stop {} was improved", stop.id());
                             earliestArrivalsThisRound[stopIdx] = new Arrival(stopTime.arrival(), ArrivalType.ROUTE,
                                     currentRouteIdx, stopIdx, enteredAtArrival);
@@ -144,20 +144,18 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                             // Because earlier trip is not possible
                             continue;
                         }
-                        earliestDepartureTime = currentArrivalLastRound.time;
                     }
 
                     // find active trip, increase trip offset
                     tripOffset = 0;
-                    enteredAtArrival = currentArrivalLastRound;
+                    enteredAtArrival = earliestArrivalsLastRound[stopIdx];
                     while (tripOffset < numberOfTrips) {
                         StopTime currentStopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset];
-                        if (currentStopTime.departure() >= earliestDepartureTime + SAME_STOP_TRANSFER_TIME) {
-                            // active trip: possible to enter this trip
+                        if (currentStopTime.departure() >= earliestArrivalTime + SAME_STOP_TRANSFER_TIME) {
                             log.debug("Found active trip ({}) on route {}", tripOffset, currentRoute.id());
                             break;
                         }
-                        if( tripOffset < numberOfTrips - 1 ){
+                        if (tripOffset < numberOfTrips - 1) {
                             tripOffset++;
                         } else {
                             // no active trip found

From 193ed7a9b48b44579dc05e3a08ba85e53e9df39f Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Fri, 17 May 2024 23:58:04 +0200
Subject: [PATCH 09/35] ENH: NAV-17 - Add flag to make algorithm quit if
 improved time mark is greater than best arrival time.

---
 src/main/java/ch/naviqore/raptor/model/Raptor.java | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 8f76889c..6b5d6330 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -2,6 +2,7 @@
 
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import lombok.Setter;
 import lombok.ToString;
 import lombok.extern.log4j.Log4j2;
 
@@ -29,6 +30,10 @@ public class Raptor {
     private final Route[] routes;
     private final RouteStop[] routeStops;
 
+    @Setter
+    @Getter
+    private boolean quitAfterBestTimeSolution = true;
+
     Raptor(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal) {
         this.stopsToIdx = lookup.stops();
         this.routesToIdx = lookup.routes();
@@ -137,6 +142,14 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                         StopTime stopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset];
                         if (stopTime.arrival() < earliestArrivalTime) {
                             log.debug("Stop {} was improved", stop.id());
+
+                            // check if search should be stopped after finding the best time
+                            if (quitAfterBestTimeSolution && stopTime.arrival() >= earliestArrivals[targetStopIdx]) {
+                                log.debug("Stop {} is not better than best time, continue", stop.id());
+                                continue;
+                            }
+
+                            earliestArrivals[stopIdx] = stopTime.arrival();
                             earliestArrivalsThisRound[stopIdx] = new Arrival(stopTime.arrival(), ArrivalType.ROUTE,
                                     currentRouteIdx, stopIdx, enteredAtArrival);
                             // mark stop improvement for next round

From 331fd7db0e814c9ef6c1beb989700533afe046e8 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Sat, 18 May 2024 00:04:24 +0200
Subject: [PATCH 10/35] ENH: NAV-17 - Don't check for trips to enter on last
 stop of route.

---
 src/main/java/ch/naviqore/raptor/model/Raptor.java | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 6b5d6330..252b628c 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -133,6 +133,12 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                             continue;
                         }
 
+                        if ( stopOffset + 1 == 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());
+                            continue;
+                        }
+
                         // got first marked stop in the route
                         log.debug("Got first entry point at stop {} at {}", stop.id(), earliestArrivalTime);
                         enteredTrip = true;

From 82e2177d3df172c5495a88d2fc652b5199956cc1 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Sat, 18 May 2024 00:15:34 +0200
Subject: [PATCH 11/35] ENH: NAV-17 - Seperate methods to allow getting
 earliestConnections for all stops or target stop.

---
 .../java/ch/naviqore/raptor/model/Raptor.java | 24 ++++++++++++-------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 252b628c..1c054a98 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -2,7 +2,6 @@
 
 import lombok.Getter;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
 import lombok.ToString;
 import lombok.extern.log4j.Log4j2;
 
@@ -30,10 +29,6 @@ public class Raptor {
     private final Route[] routes;
     private final RouteStop[] routeStops;
 
-    @Setter
-    @Getter
-    private boolean quitAfterBestTimeSolution = true;
-
     Raptor(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal) {
         this.stopsToIdx = lookup.stops();
         this.routesToIdx = lookup.routes();
@@ -64,6 +59,18 @@ public List routeEarliestArrival(String sourceStopId, String targetS
             return new ArrayList<>();
         }
 
+        // get pareto-optimal solutions
+        return reconstructParetoOptimalSolutions(spawnFromSourceStop(sourceStopIdx, targetStopIdx, departureTime),
+                targetStopIdx);
+    }
+
+    // this implementation will spawn from source stop until all stops are reached with all pareto optimal connections
+    private List spawnFromSourceStop(int sourceStopIdx, int departureTime) {
+        return spawnFromSourceStop(sourceStopIdx, NO_INDEX, departureTime);
+    }
+
+    // if targetStopIdx is set (>= 0), then the search will stop when target stop cannot be pareto optimized
+    private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, int departureTime) {
         // initialization
         final int[] earliestArrivals = new int[stops.length];
         Arrays.fill(earliestArrivals, Integer.MAX_VALUE);
@@ -133,7 +140,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                             continue;
                         }
 
-                        if ( stopOffset + 1 == numberOfStops ) {
+                        if (stopOffset + 1 == 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());
                             continue;
@@ -150,7 +157,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
                             log.debug("Stop {} was improved", stop.id());
 
                             // check if search should be stopped after finding the best time
-                            if (quitAfterBestTimeSolution && stopTime.arrival() >= earliestArrivals[targetStopIdx]) {
+                            if (targetStopIdx >= 0 && stopTime.arrival() >= earliestArrivals[targetStopIdx]) {
                                 log.debug("Stop {} is not better than best time, continue", stop.id());
                                 continue;
                             }
@@ -192,8 +199,7 @@ public List routeEarliestArrival(String sourceStopId, String targetS
             round++;
         }
 
-        // get pareto-optimal solutions
-        return reconstructParetoOptimalSolutions(earliestArrivalsPerRound, targetStopIdx);
+        return earliestArrivalsPerRound;
     }
 
     private List reconstructParetoOptimalSolutions(List earliestArrivalsPerRound,

From e5fde8922edf3a72c2af03126270783050da8ee4 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Sat, 18 May 2024 00:43:27 +0200
Subject: [PATCH 12/35] ENH: NAV-17 - Implement footpaths in RAPTOR.

---
 .../java/ch/naviqore/raptor/model/Raptor.java | 33 ++++++++++++++++---
 1 file changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 1c054a98..855f2975 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -192,7 +192,29 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx
                 }
             }
 
-            // TODO: Relax footpath transfers
+            // relax footpaths for all markedStops
+            // temp variable to add any new stops to markedStopsNext
+            Set newStops = new HashSet<>();
+            for (int stopIdx : markedStopsNext) {
+                Stop currentStop = stops[stopIdx];
+                if (currentStop.numberOfTransfers() == 0) {
+                    continue;
+                }
+                for (int i = currentStop.transferIdx(); i < currentStop.numberOfTransfers(); i++) {
+                    Transfer transfer = transfers[i];
+                    // TODO: Handle variable SAME_STOP_TRANSFER_TIMEs
+                    int newTargetStopArrivalTime = earliestArrivals[stopIdx] + transfer.duration() - SAME_STOP_TRANSFER_TIME;
+
+                    // update improved arrival time
+                    if( earliestArrivals[transfer.targetStopIdx()] > newTargetStopArrivalTime ) {
+                        log.debug("Stop {} was improved by transfer from stop {}", stops[transfer.targetStopIdx()].id(), stops[stopIdx].id());
+                        earliestArrivals[transfer.targetStopIdx()] = newTargetStopArrivalTime;
+                        earliestArrivalsThisRound[transfer.targetStopIdx()] = new Arrival(newTargetStopArrivalTime, ArrivalType.TRANSFER, i, transfer.targetStopIdx(), earliestArrivalsThisRound[stopIdx]);
+                        newStops.add(transfer.targetStopIdx());
+                    }
+                }
+            }
+            markedStopsNext.addAll(newStops);
 
             // prepare next round
             markedStops = markedStopsNext;
@@ -219,15 +241,16 @@ private List reconstructParetoOptimalSolutions(List earli
             Connection connection = new Connection();
 
             while (arrival.type != ArrivalType.INITIAL) {
+                String fromStopId = stops[arrival.previous.stopIdx].id();
+                String toStopId = stops[arrival.stopIdx].id();
                 if (arrival.type == ArrivalType.ROUTE) {
-                    String fromStopId = stops[arrival.previous.stopIdx].id();
-                    String toStopId = stops[arrival.stopIdx].id();
                     String routeId = routes[arrival.routeOrTransferIdx].id();
                     connection.legs.add(new Leg(fromStopId, toStopId, routeId));
-                    arrival = arrival.previous;
                 } else if (arrival.type == ArrivalType.TRANSFER) {
-                    throw new IllegalStateException("No transfers yet!");
+                    String transferId = "TRANSFER: " + fromStopId + " to " + toStopId;
+                    connection.legs.add(new Leg(fromStopId, toStopId, transferId));
                 }
+                arrival = arrival.previous;
             }
 
             // reverse order of legs and add connection

From 6e1ec84870adb3f32e672326c7aac3be2ec60252 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Sat, 18 May 2024 01:02:00 +0200
Subject: [PATCH 13/35] ENH: NAV-17 - Add departure/arrival times to connection
 leg records.

---
 .../java/ch/naviqore/raptor/model/Raptor.java | 62 +++++++++++--------
 1 file changed, 36 insertions(+), 26 deletions(-)

diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 855f2975..06d44bdb 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -65,35 +65,34 @@ public List routeEarliestArrival(String sourceStopId, String targetS
     }
 
     // this implementation will spawn from source stop until all stops are reached with all pareto optimal connections
-    private List spawnFromSourceStop(int sourceStopIdx, int departureTime) {
+    private List spawnFromSourceStop(int sourceStopIdx, int departureTime) {
         return spawnFromSourceStop(sourceStopIdx, NO_INDEX, departureTime);
     }
 
     // if targetStopIdx is set (>= 0), then the search will stop when target stop cannot be pareto optimized
-    private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, int departureTime) {
+    private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, int departureTime) {
         // initialization
         final int[] earliestArrivals = new int[stops.length];
         Arrays.fill(earliestArrivals, Integer.MAX_VALUE);
         earliestArrivals[sourceStopIdx] = departureTime;
 
-        final List earliestArrivalsPerRound = new ArrayList<>();
-        earliestArrivalsPerRound.add(new Arrival[stops.length]);
-        earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new Arrival(departureTime, ArrivalType.INITIAL, NO_INDEX,
-                sourceStopIdx, null);
+        final List earliestArrivalsPerRound = new ArrayList<>();
+        earliestArrivalsPerRound.add(new RaptorLeg[stops.length]);
+        earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new RaptorLeg(0, departureTime, ArrivalType.INITIAL,
+                NO_INDEX, sourceStopIdx, null);
 
         Set markedStops = new HashSet<>();
         markedStops.add(sourceStopIdx);
 
         int round = 1;
         while (!markedStops.isEmpty()) {
-            log.debug("Scanning routes for round {} (=trips)", round);
-            log.debug("Marked stops: {}", markedStops);
+            log.debug("Scanning routes for round {}", round);
             Set markedStopsNext = new HashSet<>();
 
             // initialize the earliest arrivals for current round
-            Arrival[] earliestArrivalsLastRound = earliestArrivalsPerRound.get(round - 1);
-            earliestArrivalsPerRound.add(new Arrival[stops.length]);
-            Arrival[] earliestArrivalsThisRound = earliestArrivalsPerRound.get(round);
+            RaptorLeg[] earliestArrivalsLastRound = earliestArrivalsPerRound.get(round - 1);
+            earliestArrivalsPerRound.add(new RaptorLeg[stops.length]);
+            RaptorLeg[] earliestArrivalsThisRound = earliestArrivalsPerRound.get(round);
 
             // get routes of marked stops
             Set routesToScan = new HashSet<>();
@@ -118,7 +117,8 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx
                 final int numberOfTrips = currentRoute.numberOfTrips();
                 int tripOffset = 0;
                 boolean enteredTrip = false;
-                Arrival enteredAtArrival = null;
+                int tripEntryTime = 0;
+                RaptorLeg enteredAtArrival = null;
 
                 // iterate over stops in route
                 for (int stopOffset = 0; stopOffset < numberOfStops; stopOffset++) {
@@ -163,8 +163,8 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx
                             }
 
                             earliestArrivals[stopIdx] = stopTime.arrival();
-                            earliestArrivalsThisRound[stopIdx] = new Arrival(stopTime.arrival(), ArrivalType.ROUTE,
-                                    currentRouteIdx, stopIdx, enteredAtArrival);
+                            earliestArrivalsThisRound[stopIdx] = new RaptorLeg(tripEntryTime, stopTime.arrival(),
+                                    ArrivalType.ROUTE, currentRouteIdx, stopIdx, enteredAtArrival);
                             // mark stop improvement for next round
                             markedStopsNext.add(stopIdx);
                             // Because earlier trip is not possible
@@ -179,6 +179,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx
                         StopTime currentStopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset];
                         if (currentStopTime.departure() >= earliestArrivalTime + SAME_STOP_TRANSFER_TIME) {
                             log.debug("Found active trip ({}) on route {}", tripOffset, currentRoute.id());
+                            tripEntryTime = currentStopTime.departure();
                             break;
                         }
                         if (tripOffset < numberOfTrips - 1) {
@@ -206,10 +207,13 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx
                     int newTargetStopArrivalTime = earliestArrivals[stopIdx] + transfer.duration() - SAME_STOP_TRANSFER_TIME;
 
                     // update improved arrival time
-                    if( earliestArrivals[transfer.targetStopIdx()] > newTargetStopArrivalTime ) {
-                        log.debug("Stop {} was improved by transfer from stop {}", stops[transfer.targetStopIdx()].id(), stops[stopIdx].id());
+                    if (earliestArrivals[transfer.targetStopIdx()] > newTargetStopArrivalTime) {
+                        log.debug("Stop {} was improved by transfer from stop {}", stops[transfer.targetStopIdx()].id(),
+                                stops[stopIdx].id());
                         earliestArrivals[transfer.targetStopIdx()] = newTargetStopArrivalTime;
-                        earliestArrivalsThisRound[transfer.targetStopIdx()] = new Arrival(newTargetStopArrivalTime, ArrivalType.TRANSFER, i, transfer.targetStopIdx(), earliestArrivalsThisRound[stopIdx]);
+                        earliestArrivalsThisRound[transfer.targetStopIdx()] = new RaptorLeg(earliestArrivals[stopIdx],
+                                newTargetStopArrivalTime, ArrivalType.TRANSFER, i, transfer.targetStopIdx(),
+                                earliestArrivalsThisRound[stopIdx]);
                         newStops.add(transfer.targetStopIdx());
                     }
                 }
@@ -224,13 +228,13 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx
         return earliestArrivalsPerRound;
     }
 
-    private List reconstructParetoOptimalSolutions(List earliestArrivalsPerRound,
+    private List reconstructParetoOptimalSolutions(List earliestArrivalsPerRound,
                                                                int targetStopIdx) {
         final List connections = new ArrayList<>();
 
         // iterate over all rounds
         for (int i = 1; i < earliestArrivalsPerRound.size(); i++) {
-            Arrival arrival = earliestArrivalsPerRound.get(i)[targetStopIdx];
+            RaptorLeg arrival = earliestArrivalsPerRound.get(i)[targetStopIdx];
 
             // target stop not reached in this round
             if (arrival == null) {
@@ -241,15 +245,19 @@ private List reconstructParetoOptimalSolutions(List earli
             Connection connection = new Connection();
 
             while (arrival.type != ArrivalType.INITIAL) {
+                String description;
                 String fromStopId = stops[arrival.previous.stopIdx].id();
                 String toStopId = stops[arrival.stopIdx].id();
+                int departureTime = arrival.departureTime;
+                int arrivalTime = arrival.arrivalTime;
                 if (arrival.type == ArrivalType.ROUTE) {
-                    String routeId = routes[arrival.routeOrTransferIdx].id();
-                    connection.legs.add(new Leg(fromStopId, toStopId, routeId));
+                    description = routes[arrival.routeOrTransferIdx].id();
                 } else if (arrival.type == ArrivalType.TRANSFER) {
-                    String transferId = "TRANSFER: " + fromStopId + " to " + toStopId;
-                    connection.legs.add(new Leg(fromStopId, toStopId, transferId));
+                    description = "TRANSFER: " + fromStopId + " to " + toStopId;
+                } else {
+                    throw new IllegalStateException("Unknown arrival type");
                 }
+                connection.legs.add(new ConnectionLeg(fromStopId, toStopId, description, departureTime, arrivalTime));
                 arrival = arrival.previous;
             }
 
@@ -274,13 +282,15 @@ enum ArrivalType {
     @Getter
     @ToString
     public static class Connection {
-        private final List legs = new ArrayList<>();
+        private final List legs = new ArrayList<>();
     }
 
-    public record Leg(String fromStopId, String toStopId, String routeId) {
+    public record ConnectionLeg(String fromStopId, String toStopId, String routeId, int departureTime,
+                                int arrivalTime) {
     }
 
-    record Arrival(int time, ArrivalType type, int routeOrTransferIdx, int stopIdx, Arrival previous) {
+    record RaptorLeg(int departureTime, int arrivalTime, ArrivalType type, int routeOrTransferIdx, int stopIdx,
+                     RaptorLeg previous) {
     }
 
 }

From ba1f0cfc175126beb120bf80548208f10722ff22 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Sat, 18 May 2024 09:54:07 +0200
Subject: [PATCH 14/35] TEST: NAV-17 - Refactor test utilities to add more
 versatility to raptor build for testing.

---
 .../ch/naviqore/raptor/model/RaptorTest.java  | 116 ++++++++++--------
 1 file changed, 67 insertions(+), 49 deletions(-)

diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
index a6cd67e7..96d492c0 100644
--- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
+++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
@@ -1,6 +1,5 @@
 package ch.naviqore.raptor.model;
 
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 
@@ -42,64 +41,83 @@
  */
 class RaptorTest {
 
-    private static final int DAY_START = 5 * 60 * 60;
-    private static final int DAY_END = 25 * 60 * 60;
-    private static final int TRAVEL_TIME = 60 * 5;
-    private static final int DWELL_TIME = 60 * 2;
-    private static final List ROUTES = List.of(
-            new Route("R1", List.of("A", "B", "C", "D", "E", "F", "G"), 15 * 60, 60),
-            new Route("R2", List.of("H", "B", "I", "J", "K", "L"), 30 * 60, 5 * 60),
-            new Route("R3", List.of("M", "K", "N", "O", "P", "Q"), 15 * 60, 7 * 60),
-            new Route("R4", List.of("R", "P", "F", "S"), 60 * 60, 0));
-    private static final List TRANSFERS = List.of(new Transfer("N", "D", 60 * 10),
-            new Transfer("L", "R", 60 * 3));
-    private Raptor raptor;
+    private static final int SECONDS_IN_HOUR = 3600;
+    private static final int DAY_START_HOUR = 5;
+    private static final int DAY_END_HOUR = 25;
+    private static final List ROUTES = List.of(
+            new Utilities.Route("R1", List.of("A", "B", "C", "D", "E", "F", "G")),
+            new Utilities.Route("R2", List.of("H", "B", "I", "J", "K", "L")),
+            new Utilities.Route("R3", List.of("M", "K", "N", "O", "P", "Q")),
+            new Utilities.Route("R4", List.of("R", "P", "F", "S")));
+    private static final List TRANSFERS = List.of(new Utilities.Transfer("N", "D", 60),
+            new Utilities.Transfer("L", "R", 30));
 
-    @BeforeEach
-    void setUp() {
-        Set addedStops = new HashSet<>();
-        RaptorBuilder builder = Raptor.builder();
-        ROUTES.forEach(route -> {
-            builder.addRoute(route.id + "-F");
-            builder.addRoute(route.id + "-R");
-            route.stops.forEach(stop -> {
-                if (!addedStops.contains(stop)) {
-                    builder.addStop(stop);
-                    addedStops.add(stop);
-                }
-            });
-            for (int i = 0; i < route.stops.size(); i++) {
-                builder.addRouteStop(route.stops.get(i), route.id + "-F");
-                builder.addRouteStop(route.stops.get(route.stops.size() - 1 - i), route.id + "-R");
-            }
-            int time = DAY_START + route.offset;
-            while (time < DAY_END) {
+    static class Utilities {
+
+        public static Raptor buildRaptor() {
+            return buildRaptor(ROUTES, TRANSFERS, DAY_START_HOUR, DAY_END_HOUR);
+        }
+
+        public static Raptor buildRaptor(List routes, List transfers, int dayStart, int dayEnd) {
+            Set addedStops = new HashSet<>();
+            RaptorBuilder builder = Raptor.builder();
+            routes.forEach(route -> {
+                builder.addRoute(route.id + "-F");
+                builder.addRoute(route.id + "-R");
+                route.stops.forEach(stop -> {
+                    if (!addedStops.contains(stop)) {
+                        builder.addStop(stop);
+                        addedStops.add(stop);
+                    }
+                });
                 for (int i = 0; i < route.stops.size(); i++) {
-                    builder.addStopTime(route.stops.get(i), route.id + "-F", time, time + TRAVEL_TIME);
-                    builder.addStopTime(route.stops.get(route.stops.size() - 1 - i), route.id + "-R", time,
-                            time + TRAVEL_TIME);
-                    time += TRAVEL_TIME + DWELL_TIME;
+                    builder.addRouteStop(route.stops.get(i), route.id + "-F");
+                    builder.addRouteStop(route.stops.get(route.stops.size() - 1 - i), route.id + "-R");
                 }
-            }
-        });
-        TRANSFERS.forEach(transfer -> {
-            builder.addTransfer(transfer.sourceStop, transfer.targetStop, transfer.duration);
-            builder.addTransfer(transfer.targetStop, transfer.sourceStop, transfer.duration);
-        });
-        raptor = builder.build();
-    }
+                int time = dayStart * SECONDS_IN_HOUR + route.firstDepartureOffsetInMinutes * 60;
+                while (time < dayEnd * SECONDS_IN_HOUR) {
+                    int departureTime = time;
+                    // first stop of trip has no arrival time
+                    int arrivalTime = 0;
+                    for (int i = 0; i < route.stops.size(); i++) {
+                        if (i + 1 == route.stops.size()) {
+                            // last stop of trip has no departure time
+                            departureTime = 0;
+                        }
+                        builder.addStopTime(route.stops.get(i), route.id + "-F", arrivalTime, departureTime);
+                        builder.addStopTime(route.stops.get(route.stops.size() - 1 - i), route.id + "-R", arrivalTime,
+                                departureTime);
 
-    record Route(String id, List stops, int headway, int offset) {
-    }
+                        arrivalTime = departureTime + route.timeBetweenStopsInMinutes * 60;
+                        departureTime = arrivalTime + route.dwellTimeInMinutes * 60;
+                    }
+                    time += route.timeBetweenDeparturesInMinutes * 60;
+                }
+            });
+            transfers.forEach(transfer -> {
+                builder.addTransfer(transfer.sourceStop, transfer.targetStop, transfer.durationInMinutes * 60);
+                builder.addTransfer(transfer.targetStop, transfer.sourceStop, transfer.durationInMinutes * 60);
+            });
+            return builder.build();
+        }
 
-    record Transfer(String sourceStop, String targetStop, int duration) {
+        record Route(String id, List stops, int firstDepartureOffsetInMinutes,
+                     int timeBetweenDeparturesInMinutes, int timeBetweenStopsInMinutes, int dwellTimeInMinutes) {
+            public Route(String id, List stops) {
+                this(id, stops, 0, 15, 5, 1);
+            }
+        }
+
+        record Transfer(String sourceStop, String targetStop, int durationInMinutes) {
+        }
     }
 
     @Nested
     class EarliestArrival {
         @Test
-        void testRoutingBetweenIntersectingRoutes() {
-            List connections = raptor.routeEarliestArrival("A", "Q", 8 * 60 * 60);
+        void routingBetweenIntersectingRoutes() {
+            Raptor raptor = Utilities.buildRaptor();
+            List connections = raptor.routeEarliestArrival("A", "Q", 8 * SECONDS_IN_HOUR);
             System.out.println(connections);
             // TODO: assertThat...
         }

From 3f48079e7c00e255e78167c714b4ab568398ddf0 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Sat, 18 May 2024 10:28:08 +0200
Subject: [PATCH 15/35] ENH: NAV-17 - Add connection class.

---
 .../ch/naviqore/raptor/model/Connection.java  | 107 ++++++++++++++++++
 .../java/ch/naviqore/raptor/model/Raptor.java |  57 ++++------
 src/test/java/ch/naviqore/Benchmark.java      |   5 +-
 3 files changed, 132 insertions(+), 37 deletions(-)
 create mode 100644 src/main/java/ch/naviqore/raptor/model/Connection.java

diff --git a/src/main/java/ch/naviqore/raptor/model/Connection.java b/src/main/java/ch/naviqore/raptor/model/Connection.java
new file mode 100644
index 00000000..998dbd22
--- /dev/null
+++ b/src/main/java/ch/naviqore/raptor/model/Connection.java
@@ -0,0 +1,107 @@
+package ch.naviqore.raptor.model;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@ToString
+public class Connection {
+
+    private final List legs = new ArrayList<>();
+
+    public Connection(List legs) {
+        this.addLegs(legs);
+    }
+
+    public void addLeg(String description, String fromStopId, String toStopId, int departureTime, int arrivalTime,
+                       LegType type) {
+        addLeg(new Leg(description, fromStopId, toStopId, departureTime, arrivalTime, type));
+    }
+
+    public void addLeg(Leg leg) {
+        legs.add(leg);
+        update();
+    }
+
+    public void addLegs(List legs) {
+        this.legs.addAll(legs);
+        update();
+    }
+
+    public void update() {
+        // sort legs by departure time
+        legs.sort(Comparator.comparingInt(l -> l.departureTime));
+        // make sure that the legs are connected and times are consistent
+        for (int i = 0; i < legs.size() - 1; i++) {
+            Leg current = legs.get(i);
+            Leg next = legs.get(i + 1);
+            if (!current.toStopId.equals(next.fromStopId)) {
+                throw new IllegalArgumentException("Legs are not connected: " + current + " -> " + next);
+            }
+            if (current.arrivalTime < current.departureTime) {
+                throw new IllegalArgumentException("Arrival time must be after departure time: " + current);
+            }
+            if (current.arrivalTime > next.departureTime) {
+                throw new IllegalArgumentException(
+                        "Arrival time must be before next departure time: " + current + " -> " + next);
+            }
+        }
+    }
+
+    public int getDepartureTime() {
+        return legs.getFirst().departureTime;
+    }
+
+    public int getArrivalTime() {
+        return legs.getLast().arrivalTime;
+    }
+
+    public String getFromStopId() {
+        return legs.getFirst().fromStopId;
+    }
+
+    public String getToStopId() {
+        return legs.getLast().toStopId;
+    }
+
+    public int getDuration() {
+        return getArrivalTime() - getDepartureTime();
+    }
+
+    public int getNumFootPathTransfers() {
+        return (int) legs.stream().filter(l -> l.type == LegType.TRANSFER).count();
+    }
+
+    public int getNumSameStationTransfers() {
+        return getNumTransfers() - getNumFootPathTransfers();
+    }
+
+    public int getNumTransfers() {
+        if (legs.isEmpty()) {
+            return 0;
+        }
+        return getNumRouteLegs() - 1;
+    }
+
+    public int getNumRouteLegs() {
+        return (int) legs.stream().filter(l -> l.type == LegType.ROUTE).count();
+    }
+
+
+
+    public enum LegType {
+        TRANSFER,
+        ROUTE
+    }
+
+    public record Leg(String routeId, String fromStopId, String toStopId, int departureTime, int arrivalTime,
+                      LegType type) {
+    }
+
+}
diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java
index 06d44bdb..b41ed8d6 100644
--- a/src/main/java/ch/naviqore/raptor/model/Raptor.java
+++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java
@@ -1,8 +1,5 @@
 package ch.naviqore.raptor.model;
 
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.ToString;
 import lombok.extern.log4j.Log4j2;
 
 import java.util.*;
@@ -65,20 +62,20 @@ public List routeEarliestArrival(String sourceStopId, String targetS
     }
 
     // this implementation will spawn from source stop until all stops are reached with all pareto optimal connections
-    private List spawnFromSourceStop(int sourceStopIdx, int departureTime) {
+    private List spawnFromSourceStop(int sourceStopIdx, int departureTime) {
         return spawnFromSourceStop(sourceStopIdx, NO_INDEX, departureTime);
     }
 
     // if targetStopIdx is set (>= 0), then the search will stop when target stop cannot be pareto optimized
-    private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, int departureTime) {
+    private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, int departureTime) {
         // initialization
         final int[] earliestArrivals = new int[stops.length];
         Arrays.fill(earliestArrivals, Integer.MAX_VALUE);
         earliestArrivals[sourceStopIdx] = departureTime;
 
-        final List earliestArrivalsPerRound = new ArrayList<>();
-        earliestArrivalsPerRound.add(new RaptorLeg[stops.length]);
-        earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new RaptorLeg(0, departureTime, ArrivalType.INITIAL,
+        final List earliestArrivalsPerRound = new ArrayList<>();
+        earliestArrivalsPerRound.add(new Leg[stops.length]);
+        earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new Leg(0, departureTime, ArrivalType.INITIAL,
                 NO_INDEX, sourceStopIdx, null);
 
         Set markedStops = new HashSet<>();
@@ -90,9 +87,9 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopI
             Set markedStopsNext = new HashSet<>();
 
             // initialize the earliest arrivals for current round
-            RaptorLeg[] earliestArrivalsLastRound = earliestArrivalsPerRound.get(round - 1);
-            earliestArrivalsPerRound.add(new RaptorLeg[stops.length]);
-            RaptorLeg[] earliestArrivalsThisRound = earliestArrivalsPerRound.get(round);
+            Leg[] earliestArrivalsLastRound = earliestArrivalsPerRound.get(round - 1);
+            earliestArrivalsPerRound.add(new Leg[stops.length]);
+            Leg[] earliestArrivalsThisRound = earliestArrivalsPerRound.get(round);
 
             // get routes of marked stops
             Set routesToScan = new HashSet<>();
@@ -118,7 +115,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopI
                 int tripOffset = 0;
                 boolean enteredTrip = false;
                 int tripEntryTime = 0;
-                RaptorLeg enteredAtArrival = null;
+                Leg enteredAtArrival = null;
 
                 // iterate over stops in route
                 for (int stopOffset = 0; stopOffset < numberOfStops; stopOffset++) {
@@ -163,7 +160,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopI
                             }
 
                             earliestArrivals[stopIdx] = stopTime.arrival();
-                            earliestArrivalsThisRound[stopIdx] = new RaptorLeg(tripEntryTime, stopTime.arrival(),
+                            earliestArrivalsThisRound[stopIdx] = new Leg(tripEntryTime, stopTime.arrival(),
                                     ArrivalType.ROUTE, currentRouteIdx, stopIdx, enteredAtArrival);
                             // mark stop improvement for next round
                             markedStopsNext.add(stopIdx);
@@ -211,7 +208,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopI
                         log.debug("Stop {} was improved by transfer from stop {}", stops[transfer.targetStopIdx()].id(),
                                 stops[stopIdx].id());
                         earliestArrivals[transfer.targetStopIdx()] = newTargetStopArrivalTime;
-                        earliestArrivalsThisRound[transfer.targetStopIdx()] = new RaptorLeg(earliestArrivals[stopIdx],
+                        earliestArrivalsThisRound[transfer.targetStopIdx()] = new Leg(earliestArrivals[stopIdx],
                                 newTargetStopArrivalTime, ArrivalType.TRANSFER, i, transfer.targetStopIdx(),
                                 earliestArrivalsThisRound[stopIdx]);
                         newStops.add(transfer.targetStopIdx());
@@ -228,13 +225,13 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopI
         return earliestArrivalsPerRound;
     }
 
-    private List reconstructParetoOptimalSolutions(List earliestArrivalsPerRound,
+    private List reconstructParetoOptimalSolutions(List earliestArrivalsPerRound,
                                                                int targetStopIdx) {
         final List connections = new ArrayList<>();
 
         // iterate over all rounds
         for (int i = 1; i < earliestArrivalsPerRound.size(); i++) {
-            RaptorLeg arrival = earliestArrivalsPerRound.get(i)[targetStopIdx];
+            Leg arrival = earliestArrivalsPerRound.get(i)[targetStopIdx];
 
             // target stop not reached in this round
             if (arrival == null) {
@@ -242,29 +239,30 @@ private List reconstructParetoOptimalSolutions(List ear
             }
 
             // iterate through arrivals starting at target stop
-            Connection connection = new Connection();
-
+            List legs = new ArrayList<>();
             while (arrival.type != ArrivalType.INITIAL) {
                 String description;
                 String fromStopId = stops[arrival.previous.stopIdx].id();
                 String toStopId = stops[arrival.stopIdx].id();
+                Connection.LegType type;
                 int departureTime = arrival.departureTime;
                 int arrivalTime = arrival.arrivalTime;
                 if (arrival.type == ArrivalType.ROUTE) {
                     description = routes[arrival.routeOrTransferIdx].id();
+                    type = Connection.LegType.ROUTE;
                 } else if (arrival.type == ArrivalType.TRANSFER) {
                     description = "TRANSFER: " + fromStopId + " to " + toStopId;
+                    type = Connection.LegType.TRANSFER;
                 } else {
                     throw new IllegalStateException("Unknown arrival type");
                 }
-                connection.legs.add(new ConnectionLeg(fromStopId, toStopId, description, departureTime, arrivalTime));
+                legs.add(new Connection.Leg(description, fromStopId, toStopId, departureTime, arrivalTime, type));
                 arrival = arrival.previous;
             }
 
             // reverse order of legs and add connection
-            if (!connection.legs.isEmpty()) {
-                Collections.reverse(connection.legs);
-                connections.add(connection);
+            if (!legs.isEmpty()) {
+                connections.add(new Connection(legs));
             }
 
         }
@@ -278,19 +276,8 @@ enum ArrivalType {
         TRANSFER
     }
 
-    @NoArgsConstructor
-    @Getter
-    @ToString
-    public static class Connection {
-        private final List legs = new ArrayList<>();
-    }
-
-    public record ConnectionLeg(String fromStopId, String toStopId, String routeId, int departureTime,
-                                int arrivalTime) {
-    }
-
-    record RaptorLeg(int departureTime, int arrivalTime, ArrivalType type, int routeOrTransferIdx, int stopIdx,
-                     RaptorLeg previous) {
+    record Leg(int departureTime, int arrivalTime, ArrivalType type, int routeOrTransferIdx, int stopIdx,
+               Leg previous) {
     }
 
 }
diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java
index 60006112..6b90b489 100644
--- a/src/test/java/ch/naviqore/Benchmark.java
+++ b/src/test/java/ch/naviqore/Benchmark.java
@@ -5,6 +5,7 @@
 import ch.naviqore.gtfs.schedule.model.GtfsSchedule;
 import ch.naviqore.raptor.GtfsToRaptorConverter;
 import ch.naviqore.raptor.model.Raptor;
+import ch.naviqore.raptor.model.Connection;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 
@@ -91,7 +92,7 @@ private static RoutingResult[] processRequests(Raptor raptor, RouteRequest[] req
         RoutingResult[] responses = new RoutingResult[requests.length];
         for (int i = 0; i < requests.length; i++) {
             long startTime = System.nanoTime();
-            List connections = raptor.routeEarliestArrival(requests[i].sourceStop(),
+            List connections = raptor.routeEarliestArrival(requests[i].sourceStop(),
                     requests[i].targetStop(), requests[i].departureTime());
             long endTime = System.nanoTime();
             responses[i] = new RoutingResult(requests[i].sourceStop(), requests[i].targetStop(),
@@ -128,7 +129,7 @@ record RouteRequest(String sourceStop, String targetStop, int departureTime) {
     }
 
     record RoutingResult(String sourceStop, String targetStop, int requestedDepartureTime,
-                         List connections, int departureTime, int arrivalTime, int transfers,
+                         List connections, int departureTime, int arrivalTime, int transfers,
                          long time) {
     }
 }

From acc09417338e632ff36b3c70fd32f7194434a7a1 Mon Sep 17 00:00:00 2001
From: Lukas Connolly 
Date: Sat, 18 May 2024 10:39:48 +0200
Subject: [PATCH 16/35] TEST: NAV-17 - Add first Raptor test with asserts.

---
 .../ch/naviqore/raptor/model/RaptorTest.java  | 50 ++++++++++++++++++-
 1 file changed, 48 insertions(+), 2 deletions(-)

diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
index 96d492c0..85209f0f 100644
--- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
+++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java
@@ -7,6 +7,9 @@
 import java.util.List;
 import java.util.Set;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 /**
  * Tests for the Raptor class.
  * 

@@ -116,10 +119,53 @@ record Transfer(String sourceStop, String targetStop, int durationInMinutes) { class EarliestArrival { @Test void routingBetweenIntersectingRoutes() { + // Should return two pareto optimal connections: + // 1. Connection (with two route legs and one transfer (including footpath) --> slower but fewer transfers) + // - Route R1-F from A to D + // - Foot Transfer from D to N + // - Route R3-F from N to Q + + // 2. Connection (with three route legs and two transfers (same station) --> faster but more transfers) + // - Route R1-F from A to F + // - Route R4-R from F to P + // - Route R3-F from P to Q Raptor raptor = Utilities.buildRaptor(); - List connections = raptor.routeEarliestArrival("A", "Q", 8 * SECONDS_IN_HOUR); + String sourceStop = "A"; + String targetStop = "Q"; + int departureTime = 8 * SECONDS_IN_HOUR; + List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); System.out.println(connections); - // TODO: assertThat... + + // check if 2 connections were found + assertEquals(2, connections.size()); + + // check if the first connection is correct + Connection connection1 = connections.getFirst(); + assertEquals(sourceStop, connection1.getFromStopId()); + assertEquals(targetStop, connection1.getToStopId()); + assertTrue(connection1.getDepartureTime() >= departureTime, + "Departure time should be greater equal than searched for departure time"); + // check that transfers make sense + assertEquals(1, connection1.getNumFootPathTransfers()); + assertEquals(1, connection1.getNumTransfers()); + assertEquals(0, connection1.getNumSameStationTransfers()); + + // check second connection + Connection connection2 = connections.get(1); + assertEquals(sourceStop, connection2.getFromStopId()); + assertEquals(targetStop, connection2.getToStopId()); + assertTrue(connection2.getDepartureTime() >= departureTime, + "Departure time should be greater equal than searched for departure time"); + // check that transfers make sense + assertEquals(0, connection2.getNumFootPathTransfers()); + assertEquals(2, connection2.getNumTransfers()); + assertEquals(2, connection2.getNumSameStationTransfers()); + + // compare two connections (make sure they are pareto optimal) + assertTrue(connection1.getDuration() > connection2.getDuration(), + "First connection should be slower than second connection"); + assertTrue(connection1.getNumRouteLegs() < connection2.getNumRouteLegs(), + "First connection should have fewer route legs than second connection"); } } From 46d12e56dee1ebef7e736db30347aa721ed475a4 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Sat, 18 May 2024 12:03:33 +0200 Subject: [PATCH 17/35] TEST: NAV-17 - Add some more raptor tests. --- .../ch/naviqore/raptor/model/RaptorTest.java | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java index 85209f0f..48589a95 100644 --- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -134,7 +135,6 @@ void routingBetweenIntersectingRoutes() { String targetStop = "Q"; int departureTime = 8 * SECONDS_IN_HOUR; List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); - System.out.println(connections); // check if 2 connections were found assertEquals(2, connections.size()); @@ -167,6 +167,68 @@ void routingBetweenIntersectingRoutes() { assertTrue(connection1.getNumRouteLegs() < connection2.getNumRouteLegs(), "First connection should have fewer route legs than second connection"); } + + @Test + void routeBetweenNotLinkedStops() { + // Remove route R2/R4 to make stop Q (on R3) unreachable from A (on R1) + List routes = ROUTES.stream().filter(route -> !route.id.equals("R2") && !route.id.equals("R4")).toList(); + List transfers = new ArrayList<>(); + Raptor raptor = Utilities.buildRaptor(routes, transfers, DAY_START_HOUR, DAY_END_HOUR); + String sourceStop = "A"; + String targetStop = "Q"; + int departureTime = 8 * SECONDS_IN_HOUR; + List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); + assertTrue(connections.isEmpty(), "No connection should be found"); + } + + @Test + void routeBetweenOnlyFootpath(){ + List transfers = List.of(new Utilities.Transfer("N", "D", 1)); + Raptor raptor = Utilities.buildRaptor(ROUTES, transfers, DAY_START_HOUR, DAY_END_HOUR); + String sourceStop = "N"; + String targetStop = "D"; + int departureTime = 8 * SECONDS_IN_HOUR; + List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); + assertEquals(1, connections.size()); + Connection connection = connections.getFirst(); + assertEquals(sourceStop, connection.getFromStopId()); + assertEquals(targetStop, connection.getToStopId()); + assertTrue(connection.getDepartureTime() >= departureTime, + "Departure time should be greater equal than searched for departure time"); + assertEquals(1, connection.getNumFootPathTransfers()); + assertEquals(1, connection.getNumTransfers()); + assertEquals(0, connection.getNumSameStationTransfers()); + assertEquals(0, connection.getNumRouteLegs()); + } + + @Test + void routeBetweenSameStop(){ + Raptor raptor = Utilities.buildRaptor(); + String sourceStop = "A"; + String targetStop = "A"; + int departureTime = 8 * SECONDS_IN_HOUR; + List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); + assertEquals(0, connections.size()); + } + + @Test + void routeBetweenTwoStopsOnSameRoute(){ + Raptor raptor = Utilities.buildRaptor(); + String sourceStop = "A"; + String targetStop = "B"; + int departureTime = 8 * SECONDS_IN_HOUR; + List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); + System.out.println(connections); + assertEquals(1, connections.size()); + Connection connection = connections.getFirst(); + assertEquals(sourceStop, connection.getFromStopId()); + assertEquals(targetStop, connection.getToStopId()); + assertTrue(connection.getDepartureTime() >= departureTime, + "Departure time should be greater equal than searched for departure time"); + assertEquals(0, connection.getNumFootPathTransfers()); + assertEquals(0, connection.getNumTransfers()); + assertEquals(0, connection.getNumSameStationTransfers()); + } } } From d03fc885ade1471533199d59b852ac37e9d3a513 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sun, 19 May 2024 10:29:53 +0200 Subject: [PATCH 18/35] REFACTOR: NAV-17 - Refactor connection creation - Connection and Leg implement comparable, change to sorting based on this class property. Connections are sorted according to the earliest arrival time. Legs are sorted according to departure time. - Change access of Raptor.Leg and Raptor.ArrivalType to private since they are not needed from outside the class. - Refactor the connections class, make it initializable and immutable. Hide building methods outside the package. --- .../ch/naviqore/raptor/model/Connection.java | 82 +++++++++++-------- .../java/ch/naviqore/raptor/model/Raptor.java | 24 +++--- src/test/java/ch/naviqore/Benchmark.java | 7 +- .../ch/naviqore/raptor/model/RaptorTest.java | 47 ++++++----- 4 files changed, 88 insertions(+), 72 deletions(-) diff --git a/src/main/java/ch/naviqore/raptor/model/Connection.java b/src/main/java/ch/naviqore/raptor/model/Connection.java index 998dbd22..133b55a0 100644 --- a/src/main/java/ch/naviqore/raptor/model/Connection.java +++ b/src/main/java/ch/naviqore/raptor/model/Connection.java @@ -3,55 +3,55 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; -import java.util.Comparator; +import java.util.Collections; import java.util.List; -@Getter +/** + * A connection is a sequence of legs to travel from an origin stop to destination stop. + */ @NoArgsConstructor +@Getter @ToString -public class Connection { - - private final List legs = new ArrayList<>(); - - public Connection(List legs) { - this.addLegs(legs); - } +public class Connection implements Comparable { - public void addLeg(String description, String fromStopId, String toStopId, int departureTime, int arrivalTime, - LegType type) { - addLeg(new Leg(description, fromStopId, toStopId, departureTime, arrivalTime, type)); - } + private List legs = new ArrayList<>(); - public void addLeg(Leg leg) { - legs.add(leg); - update(); + private static void validateLegOrder(Leg current, Leg next) { + if (!current.toStopId.equals(next.fromStopId)) { + throw new IllegalStateException("Legs are not connected: " + current + " -> " + next); + } + if (current.arrivalTime < current.departureTime) { + throw new IllegalStateException("Arrival time must be after departure time: " + current); + } + if (current.arrivalTime > next.departureTime) { + throw new IllegalStateException( + "Arrival time must be before next departure time: " + current + " -> " + next); + } } - public void addLegs(List legs) { - this.legs.addAll(legs); - update(); + void addLeg(Leg leg) { + this.legs.add(leg); } - public void update() { + void initialize() { // sort legs by departure time - legs.sort(Comparator.comparingInt(l -> l.departureTime)); + Collections.sort(legs); // make sure that the legs are connected and times are consistent for (int i = 0; i < legs.size() - 1; i++) { Leg current = legs.get(i); Leg next = legs.get(i + 1); - if (!current.toStopId.equals(next.fromStopId)) { - throw new IllegalArgumentException("Legs are not connected: " + current + " -> " + next); - } - if (current.arrivalTime < current.departureTime) { - throw new IllegalArgumentException("Arrival time must be after departure time: " + current); - } - if (current.arrivalTime > next.departureTime) { - throw new IllegalArgumentException( - "Arrival time must be before next departure time: " + current + " -> " + next); - } + validateLegOrder(current, next); } + // make legs immutable and remove unnecessary allocated memory + this.legs = List.copyOf(legs); + } + + @Override + public int compareTo(@NotNull Connection other) { + return Integer.compare(this.getArrivalTime(), other.getArrivalTime()); } public int getDepartureTime() { @@ -75,7 +75,7 @@ public int getDuration() { } public int getNumFootPathTransfers() { - return (int) legs.stream().filter(l -> l.type == LegType.TRANSFER).count(); + return (int) legs.stream().filter(l -> l.type == LegType.FOOTPATH).count(); } public int getNumSameStationTransfers() { @@ -93,15 +93,25 @@ public int getNumRouteLegs() { return (int) legs.stream().filter(l -> l.type == LegType.ROUTE).count(); } - - + /** + * Types of legs in a connection. + */ public enum LegType { - TRANSFER, + FOOTPATH, ROUTE } + /** + * A leg is a part of a connection that is travelled on the same route and transport mode, without a transfer. + */ public record Leg(String routeId, String fromStopId, String toStopId, int departureTime, int arrivalTime, - LegType type) { + LegType type) implements Comparable { + + @Override + public int compareTo(@NotNull Connection.Leg other) { + return Integer.compare(this.departureTime, other.departureTime); + } + } } diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index b41ed8d6..74380a3b 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -75,8 +75,8 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in final List earliestArrivalsPerRound = new ArrayList<>(); earliestArrivalsPerRound.add(new Leg[stops.length]); - earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new Leg(0, departureTime, ArrivalType.INITIAL, - NO_INDEX, sourceStopIdx, null); + earliestArrivalsPerRound.getFirst()[sourceStopIdx] = new Leg(0, departureTime, ArrivalType.INITIAL, NO_INDEX, + sourceStopIdx, null); Set markedStops = new HashSet<>(); markedStops.add(sourceStopIdx); @@ -239,7 +239,7 @@ private List reconstructParetoOptimalSolutions(List earliestA } // iterate through arrivals starting at target stop - List legs = new ArrayList<>(); + Connection connection = new Connection(); while (arrival.type != ArrivalType.INITIAL) { String description; String fromStopId = stops[arrival.previous.stopIdx].id(); @@ -252,17 +252,19 @@ private List reconstructParetoOptimalSolutions(List earliestA type = Connection.LegType.ROUTE; } else if (arrival.type == ArrivalType.TRANSFER) { description = "TRANSFER: " + fromStopId + " to " + toStopId; - type = Connection.LegType.TRANSFER; + type = Connection.LegType.FOOTPATH; } else { throw new IllegalStateException("Unknown arrival type"); } - legs.add(new Connection.Leg(description, fromStopId, toStopId, departureTime, arrivalTime, type)); + connection.addLeg( + new Connection.Leg(description, fromStopId, toStopId, departureTime, arrivalTime, type)); arrival = arrival.previous; } - // reverse order of legs and add connection - if (!legs.isEmpty()) { - connections.add(new Connection(legs)); + // initialize connection: Reverse order of legs and add connection + if (!connection.getLegs().isEmpty()) { + connection.initialize(); + connections.add(connection); } } @@ -270,14 +272,14 @@ private List reconstructParetoOptimalSolutions(List earliestA return connections; } - enum ArrivalType { + private enum ArrivalType { INITIAL, ROUTE, TRANSFER } - record Leg(int departureTime, int arrivalTime, ArrivalType type, int routeOrTransferIdx, int stopIdx, - Leg previous) { + private record Leg(int departureTime, int arrivalTime, ArrivalType type, int routeOrTransferIdx, int stopIdx, + Leg previous) { } } diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 6b90b489..2fc1fb6f 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -4,8 +4,8 @@ import ch.naviqore.gtfs.schedule.GtfsScheduleReader; import ch.naviqore.gtfs.schedule.model.GtfsSchedule; import ch.naviqore.raptor.GtfsToRaptorConverter; -import ch.naviqore.raptor.model.Raptor; import ch.naviqore.raptor.model.Connection; +import ch.naviqore.raptor.model.Raptor; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -128,8 +128,7 @@ private static void writeResultsToCsv(RoutingResult[] results) throws IOExceptio record RouteRequest(String sourceStop, String targetStop, int departureTime) { } - record RoutingResult(String sourceStop, String targetStop, int requestedDepartureTime, - List connections, int departureTime, int arrivalTime, int transfers, - long time) { + record RoutingResult(String sourceStop, String targetStop, int requestedDepartureTime, List connections, + int departureTime, int arrivalTime, int transfers, long time) { } } diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java index 48589a95..9ad6f1e2 100644 --- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java @@ -48,15 +48,15 @@ class RaptorTest { private static final int SECONDS_IN_HOUR = 3600; private static final int DAY_START_HOUR = 5; private static final int DAY_END_HOUR = 25; - private static final List ROUTES = List.of( - new Utilities.Route("R1", List.of("A", "B", "C", "D", "E", "F", "G")), - new Utilities.Route("R2", List.of("H", "B", "I", "J", "K", "L")), - new Utilities.Route("R3", List.of("M", "K", "N", "O", "P", "Q")), - new Utilities.Route("R4", List.of("R", "P", "F", "S"))); - private static final List TRANSFERS = List.of(new Utilities.Transfer("N", "D", 60), - new Utilities.Transfer("L", "R", 30)); + private static final List ROUTES = List.of( + new Utils.Route("R1", List.of("A", "B", "C", "D", "E", "F", "G")), + new Utils.Route("R2", List.of("H", "B", "I", "J", "K", "L")), + new Utils.Route("R3", List.of("M", "K", "N", "O", "P", "Q")), + new Utils.Route("R4", List.of("R", "P", "F", "S"))); + private static final List TRANSFERS = List.of(new Utils.Transfer("N", "D", 60), + new Utils.Transfer("L", "R", 30)); - static class Utilities { + static class Utils { public static Raptor buildRaptor() { return buildRaptor(ROUTES, TRANSFERS, DAY_START_HOUR, DAY_END_HOUR); @@ -119,7 +119,7 @@ record Transfer(String sourceStop, String targetStop, int durationInMinutes) { @Nested class EarliestArrival { @Test - void routingBetweenIntersectingRoutes() { + void shouldFindConnectionsBetweenIntersectingRoutes() { // Should return two pareto optimal connections: // 1. Connection (with two route legs and one transfer (including footpath) --> slower but fewer transfers) // - Route R1-F from A to D @@ -130,7 +130,7 @@ void routingBetweenIntersectingRoutes() { // - Route R1-F from A to F // - Route R4-R from F to P // - Route R3-F from P to Q - Raptor raptor = Utilities.buildRaptor(); + Raptor raptor = Utils.buildRaptor(); String sourceStop = "A"; String targetStop = "Q"; int departureTime = 8 * SECONDS_IN_HOUR; @@ -169,11 +169,13 @@ void routingBetweenIntersectingRoutes() { } @Test - void routeBetweenNotLinkedStops() { + void shouldNotFindConnectionBetweenNotLinkedStops() { // Remove route R2/R4 to make stop Q (on R3) unreachable from A (on R1) - List routes = ROUTES.stream().filter(route -> !route.id.equals("R2") && !route.id.equals("R4")).toList(); - List transfers = new ArrayList<>(); - Raptor raptor = Utilities.buildRaptor(routes, transfers, DAY_START_HOUR, DAY_END_HOUR); + List routes = ROUTES.stream() + .filter(route -> !route.id.equals("R2") && !route.id.equals("R4")) + .toList(); + List transfers = new ArrayList<>(); + Raptor raptor = Utils.buildRaptor(routes, transfers, DAY_START_HOUR, DAY_END_HOUR); String sourceStop = "A"; String targetStop = "Q"; int departureTime = 8 * SECONDS_IN_HOUR; @@ -182,9 +184,11 @@ void routeBetweenNotLinkedStops() { } @Test - void routeBetweenOnlyFootpath(){ - List transfers = List.of(new Utilities.Transfer("N", "D", 1)); - Raptor raptor = Utilities.buildRaptor(ROUTES, transfers, DAY_START_HOUR, DAY_END_HOUR); + void shouldFindConnectionBetweenOnlyFootpath() { + // TODO: Fix this test case; The connection returned is R3-R (N -> K), R2-R (K -> B) and R1-F (B -> D) + // instead of a footpath (N -> D) only. + List transfers = List.of(new Utils.Transfer("N", "D", 1)); + Raptor raptor = Utils.buildRaptor(ROUTES, transfers, DAY_START_HOUR, DAY_END_HOUR); String sourceStop = "N"; String targetStop = "D"; int departureTime = 8 * SECONDS_IN_HOUR; @@ -202,8 +206,9 @@ void routeBetweenOnlyFootpath(){ } @Test - void routeBetweenSameStop(){ - Raptor raptor = Utilities.buildRaptor(); + void shouldThrowErrorWhenRequestBetweenSameStop() { + // TODO: Throw error + Raptor raptor = Utils.buildRaptor(); String sourceStop = "A"; String targetStop = "A"; int departureTime = 8 * SECONDS_IN_HOUR; @@ -212,8 +217,8 @@ void routeBetweenSameStop(){ } @Test - void routeBetweenTwoStopsOnSameRoute(){ - Raptor raptor = Utilities.buildRaptor(); + void routeBetweenTwoStopsOnSameRoute() { + Raptor raptor = Utils.buildRaptor(); String sourceStop = "A"; String targetStop = "B"; int departureTime = 8 * SECONDS_IN_HOUR; From dff4cb42130e212859d4bd11b5f02a5661b916c4 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sun, 19 May 2024 12:02:41 +0200 Subject: [PATCH 19/35] TEST: NAV-17 - Add raptor test extension - Introduce raptor test builder with test extension for parameter injection. - Refactor input validation in raptor and add test cases. --- .../java/ch/naviqore/raptor/model/Raptor.java | 68 +++-- .../ch/naviqore/raptor/model/RaptorTest.java | 241 +++++++----------- .../raptor/model/RaptorTestBuilder.java | 153 +++++++++++ .../raptor/model/RaptorTestExtension.java | 22 ++ 4 files changed, 320 insertions(+), 164 deletions(-) create mode 100644 src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java create mode 100644 src/test/java/ch/naviqore/raptor/model/RaptorTestExtension.java diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index 74380a3b..90fa6d42 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -1,5 +1,6 @@ package ch.naviqore.raptor.model; +import lombok.NonNull; import lombok.extern.log4j.Log4j2; import java.util.*; @@ -25,6 +26,7 @@ public class Raptor { private final StopTime[] stopTimes; private final Route[] routes; private final RouteStop[] routeStops; + private final InputValidator validator; Raptor(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal) { this.stopsToIdx = lookup.stops(); @@ -35,30 +37,25 @@ public class Raptor { this.stopTimes = routeTraversal.stopTimes(); this.routes = routeTraversal.routes(); this.routeStops = routeTraversal.routeStops(); + this.validator = new InputValidator(); } public static RaptorBuilder builder() { return new RaptorBuilder(); } - public List routeEarliestArrival(String sourceStopId, String targetStopId, int departureTime) { - log.info("Routing earliest arrival from {} to {} at {}", sourceStopId, targetStopId, departureTime); - - int sourceStopIdx; - int targetStopIdx; + public List routeEarliestArrival(@NonNull String sourceStopId, @NonNull String targetStopId, + int departureTime) { + InputValidator.validateDepartureTime(departureTime); + InputValidator.validateStopIds(sourceStopId, targetStopId); + int sourceStopIdx = validator.validateAndGetStopIdx(sourceStopId); + int targetStopIdx = validator.validateAndGetStopIdx(targetStopId); - // TODO: Input validation, same stop, nulls, not exising stops. - try { - sourceStopIdx = stopsToIdx.get(sourceStopId); - targetStopIdx = stopsToIdx.get(targetStopId); - } catch (Exception e) { - log.error("Error routing earliest arrival from {} to {} at {}", sourceStopId, targetStopId, departureTime); - return new ArrayList<>(); - } + log.info("Routing earliest arrival from {} to {} at {}", sourceStopId, targetStopId, departureTime); + List earliestArrivalsPerRound = spawnFromSourceStop(sourceStopIdx, targetStopIdx, departureTime); // get pareto-optimal solutions - return reconstructParetoOptimalSolutions(spawnFromSourceStop(sourceStopIdx, targetStopIdx, departureTime), - targetStopIdx); + return reconstructParetoOptimalSolutions(earliestArrivalsPerRound, targetStopIdx); } // this implementation will spawn from source stop until all stops are reached with all pareto optimal connections @@ -81,6 +78,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in Set markedStops = new HashSet<>(); markedStops.add(sourceStopIdx); + // continue with further rounds as long as there are new marked stops int round = 1; while (!markedStops.isEmpty()) { log.debug("Scanning routes for round {}", round); @@ -164,7 +162,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in ArrivalType.ROUTE, currentRouteIdx, stopIdx, enteredAtArrival); // mark stop improvement for next round markedStopsNext.add(stopIdx); - // Because earlier trip is not possible + // earlier trip is not possible continue; } } @@ -241,23 +239,22 @@ private List reconstructParetoOptimalSolutions(List earliestA // iterate through arrivals starting at target stop Connection connection = new Connection(); while (arrival.type != ArrivalType.INITIAL) { - String description; + String id; String fromStopId = stops[arrival.previous.stopIdx].id(); String toStopId = stops[arrival.stopIdx].id(); Connection.LegType type; int departureTime = arrival.departureTime; int arrivalTime = arrival.arrivalTime; if (arrival.type == ArrivalType.ROUTE) { - description = routes[arrival.routeOrTransferIdx].id(); + id = routes[arrival.routeOrTransferIdx].id(); type = Connection.LegType.ROUTE; } else if (arrival.type == ArrivalType.TRANSFER) { - description = "TRANSFER: " + fromStopId + " to " + toStopId; + id = String.format("transfer_%s_%s", fromStopId, toStopId); type = Connection.LegType.FOOTPATH; } else { throw new IllegalStateException("Unknown arrival type"); } - connection.addLeg( - new Connection.Leg(description, fromStopId, toStopId, departureTime, arrivalTime, type)); + connection.addLeg(new Connection.Leg(id, fromStopId, toStopId, departureTime, arrivalTime, type)); arrival = arrival.previous; } @@ -282,4 +279,33 @@ private record Leg(int departureTime, int arrivalTime, ArrivalType type, int rou Leg previous) { } + /** + * Validate inputs to raptor. + */ + private class InputValidator { + private static final int MIN_DEPARTURE_TIME = 0; + private static final int MAX_DEPARTURE_TIME = 48 * 60 * 60; + + private static void validateStopIds(String sourceStopId, String targetStopId) { + if (sourceStopId.equals(targetStopId)) { + throw new IllegalArgumentException("Source and target stop IDs must not be the same."); + } + } + + private static void validateDepartureTime(int departureTime) { + if (departureTime < MIN_DEPARTURE_TIME || departureTime > MAX_DEPARTURE_TIME) { + throw new IllegalArgumentException( + "Departure time must be between " + MIN_DEPARTURE_TIME + " and " + MAX_DEPARTURE_TIME + " seconds."); + } + } + + private int validateAndGetStopIdx(String stopId) { + try { + return stopsToIdx.get(stopId); + } catch (NullPointerException e) { + throw new IllegalArgumentException("Stop id " + stopId + " not found."); + } + } + } + } diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java index 9ad6f1e2..3b2b5deb 100644 --- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java @@ -1,125 +1,26 @@ package ch.naviqore.raptor.model; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static ch.naviqore.raptor.model.RaptorTestBuilder.SECONDS_IN_HOUR; +import static org.junit.jupiter.api.Assertions.*; /** * Tests for the Raptor class. - *

- * Simple example schedule for testing: - *

- *                      M
- *                      |
- *        I ---- J ---- K ---- L #### R
- *        |             |             |
- *        |             N ---- O ---- P ---- Q
- *        |             #             |
- * A ---- B ---- C ---- D ---- E ---- F ---- G
- *        |                           |
- *        H                           S
- * 
- *

- * Routes: - *

    - *
  • R1: A, B, C, D, E, F, G
  • - *
  • R2: H, B, I, J, K, L
  • - *
  • R3: M, K, N, O, P, Q
  • - *
  • R4: R, P, F, S
  • - *
- *

- * Transfers: - *

    - *
  • N <--> D: 60*10
  • - *
  • L <--> R: 60*3
  • - *
- * - * @author munterfi */ +@ExtendWith(RaptorTestExtension.class) class RaptorTest { - private static final int SECONDS_IN_HOUR = 3600; - private static final int DAY_START_HOUR = 5; - private static final int DAY_END_HOUR = 25; - private static final List ROUTES = List.of( - new Utils.Route("R1", List.of("A", "B", "C", "D", "E", "F", "G")), - new Utils.Route("R2", List.of("H", "B", "I", "J", "K", "L")), - new Utils.Route("R3", List.of("M", "K", "N", "O", "P", "Q")), - new Utils.Route("R4", List.of("R", "P", "F", "S"))); - private static final List TRANSFERS = List.of(new Utils.Transfer("N", "D", 60), - new Utils.Transfer("L", "R", 30)); - - static class Utils { - - public static Raptor buildRaptor() { - return buildRaptor(ROUTES, TRANSFERS, DAY_START_HOUR, DAY_END_HOUR); - } - - public static Raptor buildRaptor(List routes, List transfers, int dayStart, int dayEnd) { - Set addedStops = new HashSet<>(); - RaptorBuilder builder = Raptor.builder(); - routes.forEach(route -> { - builder.addRoute(route.id + "-F"); - builder.addRoute(route.id + "-R"); - route.stops.forEach(stop -> { - if (!addedStops.contains(stop)) { - builder.addStop(stop); - addedStops.add(stop); - } - }); - for (int i = 0; i < route.stops.size(); i++) { - builder.addRouteStop(route.stops.get(i), route.id + "-F"); - builder.addRouteStop(route.stops.get(route.stops.size() - 1 - i), route.id + "-R"); - } - int time = dayStart * SECONDS_IN_HOUR + route.firstDepartureOffsetInMinutes * 60; - while (time < dayEnd * SECONDS_IN_HOUR) { - int departureTime = time; - // first stop of trip has no arrival time - int arrivalTime = 0; - for (int i = 0; i < route.stops.size(); i++) { - if (i + 1 == route.stops.size()) { - // last stop of trip has no departure time - departureTime = 0; - } - builder.addStopTime(route.stops.get(i), route.id + "-F", arrivalTime, departureTime); - builder.addStopTime(route.stops.get(route.stops.size() - 1 - i), route.id + "-R", arrivalTime, - departureTime); - - arrivalTime = departureTime + route.timeBetweenStopsInMinutes * 60; - departureTime = arrivalTime + route.dwellTimeInMinutes * 60; - } - time += route.timeBetweenDeparturesInMinutes * 60; - } - }); - transfers.forEach(transfer -> { - builder.addTransfer(transfer.sourceStop, transfer.targetStop, transfer.durationInMinutes * 60); - builder.addTransfer(transfer.targetStop, transfer.sourceStop, transfer.durationInMinutes * 60); - }); - return builder.build(); - } - - record Route(String id, List stops, int firstDepartureOffsetInMinutes, - int timeBetweenDeparturesInMinutes, int timeBetweenStopsInMinutes, int dwellTimeInMinutes) { - public Route(String id, List stops) { - this(id, stops, 0, 15, 5, 1); - } - } - - record Transfer(String sourceStop, String targetStop, int durationInMinutes) { - } - } - @Nested class EarliestArrival { + @Test - void shouldFindConnectionsBetweenIntersectingRoutes() { + void shouldFindConnectionsBetweenIntersectingRoutes(RaptorTestBuilder builder) { // Should return two pareto optimal connections: // 1. Connection (with two route legs and one transfer (including footpath) --> slower but fewer transfers) // - Route R1-F from A to D @@ -130,7 +31,8 @@ void shouldFindConnectionsBetweenIntersectingRoutes() { // - Route R1-F from A to F // - Route R4-R from F to P // - Route R3-F from P to Q - Raptor raptor = Utils.buildRaptor(); + Raptor raptor = builder.buildWithDefaults(); + String sourceStop = "A"; String targetStop = "Q"; int departureTime = 8 * SECONDS_IN_HOUR; @@ -169,71 +71,124 @@ void shouldFindConnectionsBetweenIntersectingRoutes() { } @Test - void shouldNotFindConnectionBetweenNotLinkedStops() { - // Remove route R2/R4 to make stop Q (on R3) unreachable from A (on R1) - List routes = ROUTES.stream() - .filter(route -> !route.id.equals("R2") && !route.id.equals("R4")) - .toList(); - List transfers = new ArrayList<>(); - Raptor raptor = Utils.buildRaptor(routes, transfers, DAY_START_HOUR, DAY_END_HOUR); - String sourceStop = "A"; - String targetStop = "Q"; - int departureTime = 8 * SECONDS_IN_HOUR; - List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); - assertTrue(connections.isEmpty(), "No connection should be found"); - } + void routeBetweenTwoStopsOnSameRoute(RaptorTestBuilder builder) { + Raptor raptor = builder.buildWithDefaults(); - @Test - void shouldFindConnectionBetweenOnlyFootpath() { - // TODO: Fix this test case; The connection returned is R3-R (N -> K), R2-R (K -> B) and R1-F (B -> D) - // instead of a footpath (N -> D) only. - List transfers = List.of(new Utils.Transfer("N", "D", 1)); - Raptor raptor = Utils.buildRaptor(ROUTES, transfers, DAY_START_HOUR, DAY_END_HOUR); - String sourceStop = "N"; - String targetStop = "D"; + String sourceStop = "A"; + String targetStop = "B"; int departureTime = 8 * SECONDS_IN_HOUR; List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); + System.out.println(connections); assertEquals(1, connections.size()); Connection connection = connections.getFirst(); assertEquals(sourceStop, connection.getFromStopId()); assertEquals(targetStop, connection.getToStopId()); assertTrue(connection.getDepartureTime() >= departureTime, "Departure time should be greater equal than searched for departure time"); - assertEquals(1, connection.getNumFootPathTransfers()); - assertEquals(1, connection.getNumTransfers()); + assertEquals(0, connection.getNumFootPathTransfers()); + assertEquals(0, connection.getNumTransfers()); assertEquals(0, connection.getNumSameStationTransfers()); - assertEquals(0, connection.getNumRouteLegs()); } @Test - void shouldThrowErrorWhenRequestBetweenSameStop() { - // TODO: Throw error - Raptor raptor = Utils.buildRaptor(); + void shouldNotFindConnectionBetweenNotLinkedStops(RaptorTestBuilder builder) { + // Omit route R2/R4 and transfers to make stop Q (on R3) unreachable from A (on R1) + Raptor raptor = builder.withAddRoute1_AG().withAddRoute3_MQ().build(); + String sourceStop = "A"; - String targetStop = "A"; + String targetStop = "Q"; int departureTime = 8 * SECONDS_IN_HOUR; List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); - assertEquals(0, connections.size()); + assertTrue(connections.isEmpty(), "No connection should be found"); } @Test - void routeBetweenTwoStopsOnSameRoute() { - Raptor raptor = Utils.buildRaptor(); - String sourceStop = "A"; - String targetStop = "B"; + void shouldFindConnectionBetweenOnlyFootpath(RaptorTestBuilder builder) { + // TODO: Fix this test case; The connection returned is R3-R (N -> K), R2-R (K -> B) and R1-F (B -> D) + // instead of a footpath (N -> D) only. + + Raptor raptor = builder.withAddRoute1_AG() + .withAddRoute2_HL() + .withAddRoute3_MQ() + .withAddRoute4_RS() + .withAddTransfer1_ND(1) + .withAddTransfer2_LR() + .build(); + + String sourceStop = "N"; + String targetStop = "D"; int departureTime = 8 * SECONDS_IN_HOUR; List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); - System.out.println(connections); assertEquals(1, connections.size()); Connection connection = connections.getFirst(); assertEquals(sourceStop, connection.getFromStopId()); assertEquals(targetStop, connection.getToStopId()); assertTrue(connection.getDepartureTime() >= departureTime, "Departure time should be greater equal than searched for departure time"); - assertEquals(0, connection.getNumFootPathTransfers()); - assertEquals(0, connection.getNumTransfers()); + assertEquals(1, connection.getNumFootPathTransfers()); + assertEquals(1, connection.getNumTransfers()); assertEquals(0, connection.getNumSameStationTransfers()); + assertEquals(0, connection.getNumRouteLegs()); } + + @Nested + class InputValidation { + + private Raptor raptor; + + @BeforeEach + void setUp(RaptorTestBuilder builder) { + raptor = builder.buildWithDefaults(); + } + + @Test + void shouldThrowErrorWhenSourceStopNotExists() { + String sourceStop = "NonExistentStop"; + String targetStop = "A"; + int departureTime = 8 * SECONDS_IN_HOUR; + + assertThrows(IllegalArgumentException.class, + () -> raptor.routeEarliestArrival(sourceStop, targetStop, departureTime), + "Source stop has to exists"); + } + + @Test + void shouldThrowErrorWhenTargetStopNotExists() { + String sourceStop = "A"; + String targetStop = "NonExistentStop"; + int departureTime = 8 * SECONDS_IN_HOUR; + + assertThrows(IllegalArgumentException.class, + () -> raptor.routeEarliestArrival(sourceStop, targetStop, departureTime), + "Target stop has to exists"); + } + + @Test + void shouldThrowErrorWhenDepartureTimeIsOutOfRange() { + String sourceStop = "A"; + String targetStop = "B"; + + assertThrows(IllegalArgumentException.class, + () -> raptor.routeEarliestArrival(sourceStop, targetStop, -1), + "Departure time cannot be negative"); + assertThrows(IllegalArgumentException.class, + () -> raptor.routeEarliestArrival(sourceStop, targetStop, 49 * SECONDS_IN_HOUR), + "Departure time cannot be greater than two days"); + } + + @Test + void shouldThrowErrorWhenRequestBetweenSameStop() { + String sourceStop = "A"; + String targetStop = "A"; + int departureTime = 8 * SECONDS_IN_HOUR; + + assertThrows(IllegalArgumentException.class, + () -> raptor.routeEarliestArrival(sourceStop, targetStop, departureTime), + "Stops cannot be the same"); + } + + } + } } diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java b/src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java new file mode 100644 index 00000000..96203665 --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java @@ -0,0 +1,153 @@ +package ch.naviqore.raptor.model; + +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Test builder to set up a raptor for testing purposes. + *

+ * Simple example schedule for testing: + *

+ *                      M
+ *                      |
+ *        I ---- J ---- K ---- L #### R
+ *        |             |             |
+ *        |             N ---- O ---- P ---- Q
+ *        |             #             |
+ * A ---- B ---- C ---- D ---- E ---- F ---- G
+ *        |                           |
+ *        H                           S
+ * 
+ *

+ * Routes: + *

    + *
  • R1: A, B, C, D, E, F, G
  • + *
  • R2: H, B, I, J, K, L
  • + *
  • R3: M, K, N, O, P, Q
  • + *
  • R4: R, P, F, S
  • + *
+ *

+ * Transfers: + *

    + *
  • N <--> D: 60*10
  • + *
  • L <--> R: 60*3
  • + *
+ */ +@NoArgsConstructor +public class RaptorTestBuilder { + + static final int SECONDS_IN_HOUR = 3600; + private static final int DAY_START_HOUR = 5; + private static final int DAY_END_HOUR = 25; + + private final List routes = new ArrayList<>(); + private final List transfers = new ArrayList<>(); + + private static Raptor build(List routes, List transfers, int dayStart, int dayEnd) { + Set addedStops = new HashSet<>(); + RaptorBuilder builder = Raptor.builder(); + routes.forEach(route -> { + builder.addRoute(route.id + "-F"); + builder.addRoute(route.id + "-R"); + route.stops.forEach(stop -> { + if (!addedStops.contains(stop)) { + builder.addStop(stop); + addedStops.add(stop); + } + }); + for (int i = 0; i < route.stops.size(); i++) { + builder.addRouteStop(route.stops.get(i), route.id + "-F"); + builder.addRouteStop(route.stops.get(route.stops.size() - 1 - i), route.id + "-R"); + } + int time = dayStart * SECONDS_IN_HOUR + route.firstDepartureOffsetInMinutes * 60; + while (time < dayEnd * SECONDS_IN_HOUR) { + int departureTime = time; + // first stop of trip has no arrival time + int arrivalTime = 0; + for (int i = 0; i < route.stops.size(); i++) { + if (i + 1 == route.stops.size()) { + // last stop of trip has no departure time + departureTime = 0; + } + builder.addStopTime(route.stops.get(i), route.id + "-F", arrivalTime, departureTime); + builder.addStopTime(route.stops.get(route.stops.size() - 1 - i), route.id + "-R", arrivalTime, + departureTime); + + arrivalTime = departureTime + route.timeBetweenStopsInMinutes * 60; + departureTime = arrivalTime + route.dwellTimeInMinutes * 60; + } + time += route.timeBetweenDeparturesInMinutes * 60; + } + }); + transfers.forEach(transfer -> { + builder.addTransfer(transfer.sourceStop, transfer.targetStop, transfer.durationInMinutes * 60); + builder.addTransfer(transfer.targetStop, transfer.sourceStop, transfer.durationInMinutes * 60); + }); + return builder.build(); + } + + public RaptorTestBuilder withAddRoute1_AG() { + routes.add(new Route("R1", List.of("A", "B", "C", "D", "E", "F", "G"))); + return this; + } + + public RaptorTestBuilder withAddRoute2_HL() { + routes.add(new Route("R2", List.of("H", "B", "I", "J", "K", "L"))); + return this; + } + + public RaptorTestBuilder withAddRoute3_MQ() { + routes.add(new Route("R3", List.of("M", "K", "N", "O", "P", "Q"))); + return this; + } + + public RaptorTestBuilder withAddRoute4_RS() { + routes.add(new Route("R4", List.of("R", "P", "F", "S"))); + return this; + } + + public RaptorTestBuilder withAddTransfer1_ND() { + return withAddTransfer1_ND(60); + } + + public RaptorTestBuilder withAddTransfer1_ND(int duration) { + transfers.add(new Transfer("N", "D", duration)); + return this; + } + + public RaptorTestBuilder withAddTransfer2_LR() { + transfers.add(new Transfer("L", "R", 30)); + return this; + } + + public Raptor build() { + return build(routes, transfers, DAY_START_HOUR, DAY_END_HOUR); + } + + public Raptor buildWithDefaults() { + return this.withAddRoute1_AG() + .withAddRoute2_HL() + .withAddRoute3_MQ() + .withAddRoute4_RS() + .withAddTransfer1_ND() + .withAddTransfer2_LR() + .build(); + } + + private record Route(String id, List stops, int firstDepartureOffsetInMinutes, + int timeBetweenDeparturesInMinutes, int timeBetweenStopsInMinutes, int dwellTimeInMinutes) { + + public Route(String id, List stops) { + this(id, stops, 0, 15, 5, 1); + } + + } + + private record Transfer(String sourceStop, String targetStop, int durationInMinutes) { + } + +} diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTestExtension.java b/src/test/java/ch/naviqore/raptor/model/RaptorTestExtension.java new file mode 100644 index 00000000..368a3963 --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTestExtension.java @@ -0,0 +1,22 @@ +package ch.naviqore.raptor.model; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * Extension for JUnit 5 tests for injecting RaptorTestBuilder instances. This extension allows test methods to receive + * a RaptorTestBuilder instance as a parameter. + */ +public class RaptorTestExtension implements ParameterResolver { + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType().equals(RaptorTestBuilder.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return new RaptorTestBuilder(); + } +} \ No newline at end of file From 35539f16bd6eea4199e605bbe5eb877366ed910c Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Sun, 19 May 2024 23:09:52 +0200 Subject: [PATCH 20/35] FIX: NAV-17 - Ignore missing stops in benchmark --- src/test/java/ch/naviqore/Benchmark.java | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 2fc1fb6f..ce5bbb90 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -8,6 +8,7 @@ import ch.naviqore.raptor.model.Raptor; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; import java.io.IOException; import java.io.PrintWriter; @@ -33,6 +34,7 @@ * @author munterfi */ @NoArgsConstructor(access = AccessLevel.PRIVATE) +@Log4j2 final class Benchmark { private static final long SEED = 1234; @@ -72,8 +74,8 @@ private static void manageResources() throws InterruptedException { private static RouteRequest[] sampleRouteRequests(List stopIds) { Random random = new Random(SEED); - RouteRequest[] requests = new RouteRequest[Benchmark.N]; - for (int i = 0; i < Benchmark.N; i++) { + RouteRequest[] requests = new RouteRequest[N]; + for (int i = 0; i < N; i++) { int sourceIndex = random.nextInt(stopIds.size()); int destinationIndex = getRandomDestinationIndex(stopIds.size(), sourceIndex, random); requests[i] = new RouteRequest(stopIds.get(sourceIndex), stopIds.get(destinationIndex), @@ -92,12 +94,17 @@ private static RoutingResult[] processRequests(Raptor raptor, RouteRequest[] req RoutingResult[] responses = new RoutingResult[requests.length]; for (int i = 0; i < requests.length; i++) { long startTime = System.nanoTime(); - List connections = raptor.routeEarliestArrival(requests[i].sourceStop(), - requests[i].targetStop(), requests[i].departureTime()); - long endTime = System.nanoTime(); - responses[i] = new RoutingResult(requests[i].sourceStop(), requests[i].targetStop(), - requests[i].departureTime(), connections, 0, 0, 0, - (endTime - startTime) / NS_TO_MS_CONVERSION_FACTOR); + try { + List connections = raptor.routeEarliestArrival(requests[i].sourceStop(), + requests[i].targetStop(), requests[i].departureTime()); + long endTime = System.nanoTime(); + responses[i] = new RoutingResult(requests[i].sourceStop(), requests[i].targetStop(), + requests[i].departureTime(), connections, 0, 0, 0, + (endTime - startTime) / NS_TO_MS_CONVERSION_FACTOR); + } catch (IllegalArgumentException e) { + log.error("Could not process route request: {}", e.getMessage()); + } + } return responses; } From 0529582b121098c6b08f291bc19b5437d9b3ad05 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Mon, 20 May 2024 09:26:42 +0200 Subject: [PATCH 21/35] ENH: NAV-43 - Generic value object cache - Introduce generic value object cache in the utils package. - Memory usage stays the same. Closes: #9 --- .../schedule/model/GtfsScheduleBuilder.java | 39 +++++----------- .../utils/cache/ValueObjectCache.java | 38 ++++++++++++++++ src/test/java/ch/naviqore/Benchmark.java | 2 +- .../utils/cache/ValueObjectCacheTest.java | 44 +++++++++++++++++++ 4 files changed, 94 insertions(+), 29 deletions(-) create mode 100644 src/main/java/ch/naviqore/utils/cache/ValueObjectCache.java create mode 100644 src/test/java/ch/naviqore/utils/cache/ValueObjectCacheTest.java diff --git a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java index 83f1302d..ad4468fb 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleBuilder.java @@ -4,6 +4,7 @@ import ch.naviqore.gtfs.schedule.type.RouteType; import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import ch.naviqore.gtfs.schedule.type.TransferType; +import ch.naviqore.utils.cache.ValueObjectCache; import ch.naviqore.utils.spatial.GeoCoordinate; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -15,7 +16,6 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** * Implements a builder pattern for constructing instances of {@link GtfsSchedule}. This builder helps assemble a GTFS @@ -30,7 +30,8 @@ @Log4j2 public class GtfsScheduleBuilder { - private final Cache cache = new Cache(); + private final ValueObjectCache localDateCache = new ValueObjectCache<>(); + private final ValueObjectCache serviceDayTimeCache = new ValueObjectCache<>(); private final Map agencies = new HashMap<>(); private final Map calendars = new HashMap<>(); private final Map stops = new HashMap<>(); @@ -80,7 +81,8 @@ public GtfsScheduleBuilder addCalendar(String id, EnumSet serviceDays throw new IllegalArgumentException("Calendar " + id + " already exists"); } log.debug("Adding calendar {}", id); - calendars.put(id, new Calendar(id, serviceDays, cache.getOrAdd(startDate), cache.getOrAdd(endDate))); + calendars.put(id, + new Calendar(id, serviceDays, localDateCache.getOrAdd(startDate), localDateCache.getOrAdd(endDate))); return this; } @@ -91,7 +93,7 @@ public GtfsScheduleBuilder addCalendarDate(String calendarId, LocalDate date, Ex throw new IllegalArgumentException("Calendar " + calendarId + " does not exist"); } log.debug("Adding calendar {}-{}", calendarId, date); - CalendarDate calendarDate = new CalendarDate(calendar, cache.getOrAdd(date), type); + CalendarDate calendarDate = new CalendarDate(calendar, localDateCache.getOrAdd(date), type); calendar.addCalendarDate(calendarDate); return this; } @@ -129,7 +131,8 @@ public GtfsScheduleBuilder addStopTime(String tripId, String stopId, ServiceDayT throw new IllegalArgumentException("Stop " + stopId + " does not exist"); } log.debug("Adding stop time at {} to trip {} ({}-{})", stopId, tripId, arrival, departure); - StopTime stopTime = new StopTime(stop, trip, cache.getOrAdd(arrival), cache.getOrAdd(departure)); + StopTime stopTime = new StopTime(stop, trip, serviceDayTimeCache.getOrAdd(arrival), + serviceDayTimeCache.getOrAdd(departure)); stop.addStopTime(stopTime); trip.addStopTime(stopTime); return this; @@ -187,13 +190,14 @@ public void reset() { } private void clear() { - log.debug("Clearing maps and cache of the builder"); + log.debug("Clearing cache and maps of the builder"); + localDateCache.clear(); + serviceDayTimeCache.clear(); agencies.clear(); calendars.clear(); stops.clear(); routes.clear(); trips.clear(); - cache.clear(); } private void checkNotBuilt() { @@ -201,25 +205,4 @@ private void checkNotBuilt() { throw new IllegalStateException("Cannot modify builder after build() has been called."); } } - - /** - * Cache for value objects - */ - static class Cache { - private final Map localDates = new ConcurrentHashMap<>(); - private final Map serviceDayTimes = new ConcurrentHashMap<>(); - - public LocalDate getOrAdd(LocalDate value) { - return localDates.computeIfAbsent(value, k -> value); - } - - public ServiceDayTime getOrAdd(ServiceDayTime value) { - return serviceDayTimes.computeIfAbsent(value, k -> value); - } - - public void clear() { - localDates.clear(); - serviceDayTimes.clear(); - } - } } diff --git a/src/main/java/ch/naviqore/utils/cache/ValueObjectCache.java b/src/main/java/ch/naviqore/utils/cache/ValueObjectCache.java new file mode 100644 index 00000000..a09f68fe --- /dev/null +++ b/src/main/java/ch/naviqore/utils/cache/ValueObjectCache.java @@ -0,0 +1,38 @@ +package ch.naviqore.utils.cache; + +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * A generic cache for immutable value objects. + *

+ * The cache stores value objects of type T, ensuring that only one instance of each unique value is retained in + * memory. + * + * @param the type of the value objects to be cached + */ +@NoArgsConstructor +public class ValueObjectCache { + + private final Map cache = new HashMap<>(); + + /** + * Retrieves the value from the cache or adds it if it does not exist. + * + * @param value the value to be cached + * @return the cached value + */ + public T getOrAdd(T value) { + return cache.computeIfAbsent(value, k -> value); + } + + /** + * Clear the cache. + */ + public void clear() { + cache.clear(); + } + +} diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index ce5bbb90..7c077599 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -42,7 +42,7 @@ final class Benchmark { private static final Dataset DATASET = Dataset.SWITZERLAND; private static final LocalDate SCHEDULE_DATE = LocalDate.of(2024, 4, 26); private static final int SECONDS_IN_DAY = 86400; - private static final long MONITORING_INTERVAL_MS = 30; + private static final long MONITORING_INTERVAL_MS = 30000; private static final int NS_TO_MS_CONVERSION_FACTOR = 1_000_000; public static void main(String[] args) throws IOException, InterruptedException { diff --git a/src/test/java/ch/naviqore/utils/cache/ValueObjectCacheTest.java b/src/test/java/ch/naviqore/utils/cache/ValueObjectCacheTest.java new file mode 100644 index 00000000..6204784f --- /dev/null +++ b/src/test/java/ch/naviqore/utils/cache/ValueObjectCacheTest.java @@ -0,0 +1,44 @@ +package ch.naviqore.utils.cache; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +class ValueObjectCacheTest { + + @Test + void shouldReturnSameInstanceForSameValue() { + ValueObjectCache cache = new ValueObjectCache<>(); + LocalDate date1 = LocalDate.of(2023, 1, 1); + LocalDate date2 = LocalDate.of(2023, 1, 1); + + // add date1 to the cache + LocalDate cachedDate1 = cache.getOrAdd(date1); + assertEquals(date1, cachedDate1); + + // add date2 to the cache and check if it's the same instance as date1 + LocalDate cachedDate2 = cache.getOrAdd(date2); + assertSame(cachedDate1, cachedDate2); + } + + @Test + void shouldClearCacheAndReturnNewInstanceForSameValue() { + ValueObjectCache cache = new ValueObjectCache<>(); + LocalDate date1 = LocalDate.of(2023, 1, 1); + LocalDate date2 = LocalDate.of(2023, 1, 1); + + // add date1 to the cache + LocalDate cachedDate1 = cache.getOrAdd(date1); + + // clear the cache and add date2 to the cache + cache.clear(); + LocalDate cachedDate2 = cache.getOrAdd(date2); + + // ensure the cache adds the instance as new + assertNotSame(cachedDate1, cachedDate2); + // ensure the date values are the same + assertEquals(cachedDate1, cachedDate2); + } +} \ No newline at end of file From 7fe10c3c372f98afd7ee34b83b32a43832eac665 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Mon, 20 May 2024 11:05:05 +0200 Subject: [PATCH 22/35] ENH: NAV-42 - Trip validation - Add trip validator wit test cases. - Add validation in stop time construction. - Place todo in raptor builder. --- .../java/ch/naviqore/raptor/model/Raptor.java | 3 +- .../naviqore/raptor/model/RaptorBuilder.java | 21 ++- .../ch/naviqore/raptor/model/StopTime.java | 7 + .../naviqore/raptor/model/TripValidator.java | 74 +++++++++ .../raptor/model/TripValidatorTest.java | 141 ++++++++++++++++++ 5 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ch/naviqore/raptor/model/TripValidator.java create mode 100644 src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index 90fa6d42..1513724d 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -15,6 +15,7 @@ public class Raptor { public final static int NO_INDEX = -1; public final static int SAME_STOP_TRANSFER_TIME = 120; + private final InputValidator validator = new InputValidator(); // lookup private final Map stopsToIdx; private final Map routesToIdx; @@ -26,7 +27,6 @@ public class Raptor { private final StopTime[] stopTimes; private final Route[] routes; private final RouteStop[] routeStops; - private final InputValidator validator; Raptor(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal) { this.stopsToIdx = lookup.stops(); @@ -37,7 +37,6 @@ public class Raptor { this.stopTimes = routeTraversal.stopTimes(); this.routes = routeTraversal.routes(); this.routeStops = routeTraversal.routeStops(); - this.validator = new InputValidator(); } public static RaptorBuilder builder() { diff --git a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java index 0251c7db..bcc87686 100644 --- a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java +++ b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java @@ -64,13 +64,28 @@ public RaptorBuilder addRouteStop(String stopId, String routeId) { return this; } + public RaptorBuilder addTrip(String tripId, String routeId, List stopIds) { + if (!routes.containsKey(routeId)) { + throw new IllegalArgumentException("Route " + routeId + " does not exist"); + } + for (String stopId : stopIds) { + if (!stops.containsKey(stopId)) { + throw new IllegalArgumentException("Stop " + stopId + " does not exist"); + } + } + // TODO: Create and track object that ensures: + // - all trips of a route have the same stop sequence + // - stopTimes are added to each trip in the correct order (see addStopTime) + // - each stopTime of a trip has an departure which is temporally after the previous stopTime arrival + // This object is not relevant for the creation of the raptor data itself, but it validates the inputs. + return this; + } + public RaptorBuilder addStopTime(String stopId, String routeId, int arrival, int departure) { log.debug("Adding stop time: stopId={}, routeId={}, arrival={}, departure={}", stopId, routeId, arrival, departure); if (!stops.containsKey(stopId)) { - log.error("Stop {} does not exist", stopId); - // TODO: Reactivate after test for consistency of route stops. - // throw new IllegalArgumentException("Stop " + stopId + " does not exist"); + throw new IllegalArgumentException("Stop " + stopId + " does not exist"); } if (!routes.containsKey(routeId)) { throw new IllegalArgumentException("Route " + routeId + " does not exist"); diff --git a/src/main/java/ch/naviqore/raptor/model/StopTime.java b/src/main/java/ch/naviqore/raptor/model/StopTime.java index f499e04b..732505d8 100644 --- a/src/main/java/ch/naviqore/raptor/model/StopTime.java +++ b/src/main/java/ch/naviqore/raptor/model/StopTime.java @@ -1,4 +1,11 @@ package ch.naviqore.raptor.model; public record StopTime(int arrival, int departure) { + + public StopTime { + if (arrival > departure) { + throw new IllegalArgumentException("Arrival time must be before departure time."); + } + } + } diff --git a/src/main/java/ch/naviqore/raptor/model/TripValidator.java b/src/main/java/ch/naviqore/raptor/model/TripValidator.java new file mode 100644 index 00000000..479e6bf4 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/TripValidator.java @@ -0,0 +1,74 @@ +package ch.naviqore.raptor.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Validates trips of a route. + */ +class TripValidator { + private final Map stopSequence = new HashMap<>(); + private final Map trips = new HashMap<>(); + + TripValidator(List stopIds) { + for (int i = 0; i < stopIds.size(); i++) { + stopSequence.put(stopIds.get(i), i); + } + } + + void addTrip(String tripId) { + if (trips.containsKey(tripId)) { + throw new IllegalArgumentException("Trip " + tripId + " already exists."); + } + trips.put(tripId, new StopTime[stopSequence.size()]); + } + + void addStopTime(String tripId, String stopId, StopTime stopTime) { + StopTime[] stopTimes = trips.get(tripId); + if (stopTimes == null) { + throw new IllegalArgumentException("Trip " + tripId + " does not exist."); + } + + Integer stopIdx = stopSequence.get(stopId); + if (stopIdx == null) { + throw new IllegalArgumentException("Stop " + stopId + " does not exist."); + } + + if (stopTimes[stopIdx] != null) { + throw new IllegalArgumentException("Stop time for stop " + stopId + " already exists."); + } + + if (stopIdx > 0) { + StopTime previousStopTime = stopTimes[stopIdx - 1]; + if (previousStopTime != null && previousStopTime.departure() > stopTime.arrival()) { + throw new IllegalArgumentException( + "Departure time at previous stop is greater than arrival time at current stop."); + } + } + + if (stopIdx < stopTimes.length - 1) { + StopTime nextStopTime = stopTimes[stopIdx + 1]; + if (nextStopTime != null && stopTime.departure() > nextStopTime.arrival()) { + throw new IllegalArgumentException( + "Departure time at current stop is greater than arrival time at next stop."); + } + } + + stopTimes[stopIdx] = stopTime; + } + + void validate() { + for (Map.Entry trip : trips.entrySet()) { + StopTime[] stopTimes = trip.getValue(); + for (Map.Entry stop : stopSequence.entrySet()) { + if (stopTimes[stop.getValue()] == null) { + throw new IllegalStateException( + "Stop time at stop " + stop.getKey() + " on trip " + trip.getKey() + " not set."); + } + } + } + } +} + + diff --git a/src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java b/src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java new file mode 100644 index 00000000..88963892 --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java @@ -0,0 +1,141 @@ +package ch.naviqore.raptor.model; + +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.*; + +class TripValidatorTest { + + private static final List STOP_IDS = List.of("stop1", "stop2", "stop3"); + private TripValidator tripValidator; + + @BeforeEach + void setUp() { + tripValidator = new TripValidator(STOP_IDS); + } + + @Nested + class AddTrip { + + @Test + void shouldAddValidTrips() { + tripValidator.addTrip("trip1"); + assertDoesNotThrow(() -> tripValidator.addTrip("trip2")); + } + + @Test + void shouldNotAddDuplicateTrip() { + tripValidator.addTrip("trip1"); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tripValidator.addTrip("trip1")); + assertEquals("Trip trip1 already exists.", exception.getMessage()); + } + } + + @Nested + class AddStopTime { + + @Test + void shouldAddValidStopTimes() { + tripValidator.addTrip("trip1"); + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + StopTime stopTime3 = new StopTime(500, 600); + + assertDoesNotThrow(() -> tripValidator.addStopTime("trip1", "stop1", stopTime1)); + assertDoesNotThrow(() -> tripValidator.addStopTime("trip1", "stop2", stopTime2)); + assertDoesNotThrow(() -> tripValidator.addStopTime("trip1", "stop3", stopTime3)); + } + + @Test + void shouldNotAddStopTimeToNonExistentTrip() { + StopTime stopTime = new StopTime(100, 200); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tripValidator.addStopTime("trip1", "stop1", stopTime)); + assertEquals("Trip trip1 does not exist.", exception.getMessage()); + } + + @Test + void shouldNotAddStopTimeForNonExistentStop() { + tripValidator.addTrip("trip1"); + StopTime stopTime = new StopTime(100, 200); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tripValidator.addStopTime("trip1", "nonexistentStop", stopTime)); + assertEquals("Stop nonexistentStop does not exist.", exception.getMessage()); + } + + @Test + void shouldNotAddDuplicateStopTimes() { + tripValidator.addTrip("trip1"); + StopTime stopTime = new StopTime(100, 200); + tripValidator.addStopTime("trip1", "stop1", stopTime); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tripValidator.addStopTime("trip1", "stop1", stopTime)); + assertEquals("Stop time for stop stop1 already exists.", exception.getMessage()); + } + + @Test + void shouldNotAddStopTimesWithOverlapOnPreviousStop() { + tripValidator.addTrip("trip1"); + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(150, 250); // overlapping times + + tripValidator.addStopTime("trip1", "stop1", stopTime1); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tripValidator.addStopTime("trip1", "stop2", stopTime2)); + assertEquals("Departure time at previous stop is greater than arrival time at current stop.", + exception.getMessage()); + } + + @Test + void shouldNotAddStopTimesWithOverlapOnNextStop() { + tripValidator.addTrip("trip1"); + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(250, 350); + StopTime stopTime3 = new StopTime(300, 400); + + tripValidator.addStopTime("trip1", "stop1", stopTime1); + tripValidator.addStopTime("trip1", "stop3", stopTime3); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> tripValidator.addStopTime("trip1", "stop2", stopTime2)); + assertEquals("Departure time at current stop is greater than arrival time at next stop.", + exception.getMessage()); + } + } + + @Nested + class Validate { + + @Test + void shouldValidateCompleteRoute() { + tripValidator.addTrip("trip1"); + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + StopTime stopTime3 = new StopTime(500, 600); + + tripValidator.addStopTime("trip1", "stop1", stopTime1); + tripValidator.addStopTime("trip1", "stop2", stopTime2); + tripValidator.addStopTime("trip1", "stop3", stopTime3); + + assertDoesNotThrow(() -> tripValidator.validate()); + } + + @Test + void shouldNotValidateWhenStopTimeIsMissing() { + tripValidator.addTrip("trip1"); + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + + tripValidator.addStopTime("trip1", "stop1", stopTime1); + tripValidator.addStopTime("trip1", "stop2", stopTime2); + + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> tripValidator.validate()); + assertTrue(exception.getMessage().contains("Stop time at stop stop3 on trip trip1 not set.")); + } + } +} From a6e7adfd4b9c8adbb320d1fac5f34e1559bd3e35 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Tue, 21 May 2024 21:45:58 +0200 Subject: [PATCH 23/35] FIX: NAV-17 - Fix case where targetStop can be reached in round 0 by footpath. --- .../ch/naviqore/raptor/model/Connection.java | 15 ++++++--- .../java/ch/naviqore/raptor/model/Raptor.java | 33 +++++++++++++++++-- .../ch/naviqore/raptor/model/RaptorTest.java | 3 -- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/main/java/ch/naviqore/raptor/model/Connection.java b/src/main/java/ch/naviqore/raptor/model/Connection.java index 133b55a0..b0b6e9fa 100644 --- a/src/main/java/ch/naviqore/raptor/model/Connection.java +++ b/src/main/java/ch/naviqore/raptor/model/Connection.java @@ -79,14 +79,19 @@ public int getNumFootPathTransfers() { } public int getNumSameStationTransfers() { - return getNumTransfers() - getNumFootPathTransfers(); + int transferCounter = 0; + for (int i = 0; i < legs.size() - 1; i++) { + Leg current = legs.get(i); + Leg next = legs.get(i + 1); + if (current.type == LegType.ROUTE && next.type == LegType.ROUTE) { + transferCounter++; + } + } + return transferCounter; } public int getNumTransfers() { - if (legs.isEmpty()) { - return 0; - } - return getNumRouteLegs() - 1; + return getNumFootPathTransfers() + getNumSameStationTransfers(); } public int getNumRouteLegs() { diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index 90fa6d42..6a48c0bc 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -68,7 +68,8 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in // initialization final int[] earliestArrivals = new int[stops.length]; Arrays.fill(earliestArrivals, Integer.MAX_VALUE); - earliestArrivals[sourceStopIdx] = departureTime; + // subtract same stop transfer time, as this will be added by default before scanning routes + earliestArrivals[sourceStopIdx] = departureTime - SAME_STOP_TRANSFER_TIME; final List earliestArrivalsPerRound = new ArrayList<>(); earliestArrivalsPerRound.add(new Leg[stops.length]); @@ -78,6 +79,10 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in Set markedStops = new HashSet<>(); markedStops.add(sourceStopIdx); + // expand footpaths for source stop + expandFootpathsForSourceStop(earliestArrivals, earliestArrivalsPerRound, markedStops, sourceStopIdx, + departureTime); + // continue with further rounds as long as there are new marked stops int round = 1; while (!markedStops.isEmpty()) { @@ -228,8 +233,8 @@ private List reconstructParetoOptimalSolutions(List earliestA final List connections = new ArrayList<>(); // iterate over all rounds - for (int i = 1; i < earliestArrivalsPerRound.size(); i++) { - Leg arrival = earliestArrivalsPerRound.get(i)[targetStopIdx]; + for (Leg[] legs : earliestArrivalsPerRound) { + Leg arrival = legs[targetStopIdx]; // target stop not reached in this round if (arrival == null) { @@ -251,6 +256,8 @@ private List reconstructParetoOptimalSolutions(List earliestA } else if (arrival.type == ArrivalType.TRANSFER) { id = String.format("transfer_%s_%s", fromStopId, toStopId); type = Connection.LegType.FOOTPATH; + // include same stop transfer time (which is subtracted before scanning routes) + arrivalTime += SAME_STOP_TRANSFER_TIME; } else { throw new IllegalStateException("Unknown arrival type"); } @@ -269,6 +276,26 @@ private List reconstructParetoOptimalSolutions(List earliestA return connections; } + private void expandFootpathsForSourceStop(int[] earliestArrivals, List earliestArrivalsPerRound, + Set markedStops, int sourceStopIdx, int departureTime) { + // if stop has no transfers, then no footpaths can be expanded + if (stops[sourceStopIdx].numberOfTransfers() == 0) { + return; + } + // mark all transfer stops, no checks needed for since all transfers will improve arrival time and can be + // marked + Stop sourceStop = stops[sourceStopIdx]; + for (int i = sourceStop.transferIdx(); i < sourceStop.transferIdx() + sourceStop.numberOfTransfers(); i++) { + Transfer transfer = transfers[i]; + int newTargetStopArrivalTime = departureTime + transfer.duration() - SAME_STOP_TRANSFER_TIME; + earliestArrivals[transfer.targetStopIdx()] = newTargetStopArrivalTime; + earliestArrivalsPerRound.getFirst()[transfer.targetStopIdx()] = new Leg(departureTime, + newTargetStopArrivalTime, ArrivalType.TRANSFER, i, transfer.targetStopIdx(), + earliestArrivalsPerRound.getFirst()[sourceStopIdx]); + markedStops.add(transfer.targetStopIdx()); + } + } + private enum ArrivalType { INITIAL, ROUTE, diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java index 3b2b5deb..1c52ef02 100644 --- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java @@ -104,9 +104,6 @@ void shouldNotFindConnectionBetweenNotLinkedStops(RaptorTestBuilder builder) { @Test void shouldFindConnectionBetweenOnlyFootpath(RaptorTestBuilder builder) { - // TODO: Fix this test case; The connection returned is R3-R (N -> K), R2-R (K -> B) and R1-F (B -> D) - // instead of a footpath (N -> D) only. - Raptor raptor = builder.withAddRoute1_AG() .withAddRoute2_HL() .withAddRoute3_MQ() From a4f2266e486a3d002aa38a790813fad1536f1e87 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Wed, 22 May 2024 00:17:16 +0200 Subject: [PATCH 24/35] FIX: NAV-17 - Fix case where stop arrival time was improved in same round (entering to earlier trip not possible in same round) --- src/main/java/ch/naviqore/raptor/model/Raptor.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index 6a48c0bc..e8ac1823 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -169,6 +169,15 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in markedStopsNext.add(stopIdx); // earlier trip is not possible continue; + } else { + log.debug("Stop {} was not improved", stop.id()); + Leg previous = earliestArrivalsLastRound[stopIdx]; + if( previous == null || previous.arrivalTime >= stopTime.arrival() ) { + log.debug("Stop {} has been improved in same round, earlier trip not possible within this round", stop.id()); + continue; + } else { + log.debug("Checking for earlier trips at stop {}", stop.id()); + } } } From 4c53f55a32de6ea48a3ad6d76d4ec132718fa8f7 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Wed, 22 May 2024 20:25:16 +0200 Subject: [PATCH 25/35] ENH: NAV-42 - Use route builder to validate consistent trips in raptor builder - Raptor builder validates trips and stop times. The order of adding trips and stop times does not matter anymore. - Adjusted GtfsToRaptorConverter to new Raptor builder. --- .../raptor/GtfsToRaptorConverter.java | 48 +-- .../naviqore/raptor/model/RaptorBuilder.java | 215 +++++++------ .../naviqore/raptor/model/RouteBuilder.java | 125 ++++++++ .../naviqore/raptor/model/TripValidator.java | 74 ----- .../raptor/model/RaptorTestBuilder.java | 76 +++-- .../raptor/model/RouteBuilderTest.java | 285 ++++++++++++++++++ .../raptor/model/TripValidatorTest.java | 141 --------- 7 files changed, 604 insertions(+), 360 deletions(-) create mode 100644 src/main/java/ch/naviqore/raptor/model/RouteBuilder.java delete mode 100644 src/main/java/ch/naviqore/raptor/model/TripValidator.java create mode 100644 src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java delete mode 100644 src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java diff --git a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java index 78d5ecf7..bd3fc6ed 100644 --- a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java +++ b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java @@ -24,8 +24,8 @@ @Log4j2 public class GtfsToRaptorConverter { - private final Set subRoutes = new HashSet<>(); - private final Set stops = new HashSet<>(); + private final Set addedSubRoutes = new HashSet<>(); + private final Set addedStops = new HashSet<>(); private final RaptorBuilder builder = Raptor.builder(); private final GtfsRoutePartitioner partitioner; private final GtfsSchedule schedule; @@ -42,15 +42,29 @@ public Raptor convert(LocalDate date) { for (Trip trip : activeTrips) { GtfsRoutePartitioner.SubRoute subRoute = partitioner.getSubRoute(trip); - if (!subRoutes.contains(subRoute)) { - subRoutes.add(subRoute); - builder.addRoute(subRoute.getId()); - addRouteStops(trip, subRoute); + // add route if not already + if (!addedSubRoutes.contains(subRoute)) { + List stopIds = subRoute.getStopsSequence().stream().map(Stop::getId).toList(); + + // add stops of that are not already added + for (String stopId : stopIds) { + if (!addedStops.contains(stopId)) { + builder.addStop(stopId); + addedStops.add(stopId); + } + } + + builder.addRoute(subRoute.getId(), stopIds); + addedSubRoutes.add(subRoute); } - for (StopTime stopTime : trip.getStopTimes()) { - builder.addStopTime(stopTime.stop().getId(), subRoute.getId(), stopTime.arrival().getTotalSeconds(), - stopTime.departure().getTotalSeconds()); + // add current trip + builder.addTrip(trip.getId(), subRoute.getId()); + List stopTimes = trip.getStopTimes(); + for (int i = 0; i < stopTimes.size(); i++) { + StopTime stopTime = stopTimes.get(i); + builder.addStopTime(subRoute.getId(), trip.getId(), i, stopTime.stop().getId(), + stopTime.arrival().getTotalSeconds(), stopTime.departure().getTotalSeconds()); } } @@ -59,21 +73,9 @@ public Raptor convert(LocalDate date) { return builder.build(); } - private void addRouteStops(Trip trip, GtfsRoutePartitioner.SubRoute subRoute) { - for (StopTime stopTime : trip.getStopTimes()) { - Stop stop = stopTime.stop(); - - if (!stops.contains(stop)) { - stops.add(stop); - builder.addStop(stop.getId()); - } - - builder.addRouteStop(stop.getId(), subRoute.getId()); - } - } - private void addTransfers() { - for (Stop stop : stops) { + for (String stopId : addedStops) { + Stop stop = schedule.getStops().get(stopId); for (Transfer transfer : stop.getTransfers()) { if (transfer.getTransferType() == TransferType.MINIMUM_TIME && stop != transfer.getToStop() && transfer.getMinTransferTime() .isPresent()) { diff --git a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java index bcc87686..6df7f13a 100644 --- a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java +++ b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java @@ -3,6 +3,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; import java.util.*; @@ -18,179 +19,199 @@ public class RaptorBuilder { private final Map stops = new HashMap<>(); - private final Map routes = new HashMap<>(); - private final Map> routeStops = new HashMap<>(); - private final Map> stopTimes = new HashMap<>(); - private final Map> stopRoutes = new HashMap<>(); + private final Map routeBuilders = new HashMap<>(); private final Map> transfers = new HashMap<>(); + private final Map> stopRoutes = new HashMap<>(); - private int stopSize = 0; - private int routeSize = 0; - private int routeStopSize = 0; - private int stopTimeSize = 0; - private int transferSize = 0; + int stopTimeSize = 0; + int routeStopSize = 0; + int transferSize = 0; public RaptorBuilder addStop(String id) { if (stops.containsKey(id)) { throw new IllegalArgumentException("Stop " + id + " already exists"); } + log.debug("Adding stop: id={}", id); stops.put(id, stops.size()); - stopSize++; - return this; - } + stopRoutes.put(id, new HashSet<>()); - public RaptorBuilder addRoute(String id) { - if (routes.containsKey(id)) { - throw new IllegalArgumentException("Route " + id + " already exists"); - } - log.debug("Adding route: id={}", id); - routes.put(id, routes.size()); - routeSize++; return this; } - public RaptorBuilder addRouteStop(String stopId, String routeId) { - log.debug("Adding route stop: stopId={}, routeId={}", stopId, routeId); - if (!stops.containsKey(stopId)) { - throw new IllegalArgumentException("Stop " + stopId + " does not exist"); - } - if (!routes.containsKey(routeId)) { - throw new IllegalArgumentException("Route " + routeId + " does not exist"); + public RaptorBuilder addRoute(String id, List stopIds) { + if (routeBuilders.containsKey(id)) { + throw new IllegalArgumentException("Route " + id + " already exists"); } - routeStops.computeIfAbsent(routeId, k -> new ArrayList<>()).add(stopId); - stopRoutes.computeIfAbsent(stopId, k -> new HashSet<>()).add(routeId); - routeStopSize++; - return this; - } - public RaptorBuilder addTrip(String tripId, String routeId, List stopIds) { - if (!routes.containsKey(routeId)) { - throw new IllegalArgumentException("Route " + routeId + " does not exist"); - } for (String stopId : stopIds) { if (!stops.containsKey(stopId)) { throw new IllegalArgumentException("Stop " + stopId + " does not exist"); } + stopRoutes.get(stopId).add(id); } - // TODO: Create and track object that ensures: - // - all trips of a route have the same stop sequence - // - stopTimes are added to each trip in the correct order (see addStopTime) - // - each stopTime of a trip has an departure which is temporally after the previous stopTime arrival - // This object is not relevant for the creation of the raptor data itself, but it validates the inputs. + + log.debug("Adding route: id={}, stopSequence={}", id, stopIds); + routeBuilders.put(id, new RouteBuilder(id, stopIds)); + routeStopSize += stopIds.size(); + return this; } - public RaptorBuilder addStopTime(String stopId, String routeId, int arrival, int departure) { - log.debug("Adding stop time: stopId={}, routeId={}, arrival={}, departure={}", stopId, routeId, arrival, - departure); - if (!stops.containsKey(stopId)) { - throw new IllegalArgumentException("Stop " + stopId + " does not exist"); - } - if (!routes.containsKey(routeId)) { - throw new IllegalArgumentException("Route " + routeId + " does not exist"); - } - stopTimes.computeIfAbsent(routeId, k -> new ArrayList<>()).add(new StopTime(arrival, departure)); + public RaptorBuilder addTrip(String tripId, String routeId) { + getRouteBuilder(routeId).addTrip(tripId); + return this; + } + + public RaptorBuilder addStopTime(String routeId, String tripId, int position, String stopId, int arrival, + int departure) { + StopTime stopTime = new StopTime(arrival, departure); + getRouteBuilder(routeId).addStopTime(tripId, position, stopId, stopTime); stopTimeSize++; + return this; } public RaptorBuilder addTransfer(String sourceStopId, String targetStopId, int duration) { log.debug("Adding transfer: sourceStopId={}, targetStopId={}, duration={}", sourceStopId, targetStopId, duration); + if (!stops.containsKey(sourceStopId)) { throw new IllegalArgumentException("Source stop " + sourceStopId + " does not exist"); } + if (!stops.containsKey(targetStopId)) { throw new IllegalArgumentException("Target stop " + targetStopId + " does not exist"); } + transfers.computeIfAbsent(sourceStopId, k -> new ArrayList<>()) .add(new Transfer(stops.get(targetStopId), duration)); transferSize++; + return this; } public Raptor build() { - Lookup lookup = buildLookup(); - StopContext stopContext = buildStopContext(); - RouteTraversal routeTraversal = buildRouteTraversal(); - log.info("Initialize Raptor with {} stops, {} routes, {} route stops, {} stop times, {} transfers", stopSize, - routeSize, routeStopSize, stopTimeSize, transferSize); + log.info("Initialize 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 + List routeContainers = buildAndSortRouteContainers(); + Lookup lookup = buildLookup(routeContainers); + StopContext stopContext = buildStopContext(lookup); + RouteTraversal routeTraversal = buildRouteTraversal(routeContainers); + return new Raptor(lookup, stopContext, routeTraversal); } - private Lookup buildLookup() { - log.debug("Building lookup with {} stops and {} routes", stopSize, routeSize); - return new Lookup(new HashMap<>(stops), new HashMap<>(routes)); + private @NotNull List buildAndSortRouteContainers() { + return routeBuilders.values().parallelStream().map(RouteBuilder::build).sorted().toList(); } - private StopContext buildStopContext() { - log.debug("Building stop context with {} stops and {} transfers", stopSize, transferSize); - Stop[] stopArr = new Stop[stopSize]; + private Lookup buildLookup(List routeContainers) { + log.debug("Building lookup with {} stops and {} routes", stops.size(), routeContainers.size()); + Map routes = new HashMap<>(routeContainers.size()); + + // 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); + } + + return new Lookup(Map.copyOf(stops), Map.copyOf(routes)); + } + + private StopContext buildStopContext(Lookup lookup) { + log.debug("Building stop context with {} stops and {} transfers", stops.size(), transferSize); + + // allocate arrays in needed size + Stop[] stopArr = new Stop[stops.size()]; int[] stopRouteArr = new int[stopRoutes.values().stream().mapToInt(Set::size).sum()]; Transfer[] transferArr = new Transfer[transferSize]; - int transferCnt = 0; - int stopRouteCnt = 0; + // iterate over stops and populate arrays + int transferIdx = 0; + int stopRouteIdx = 0; for (Map.Entry entry : stops.entrySet()) { String stopId = entry.getKey(); int stopIdx = entry.getValue(); + // check if stop has no routes: Unserved stops are useless in the raptor data structure + Set currentStopRoutes = stopRoutes.get(stopId); + if (currentStopRoutes == null) { + throw new IllegalStateException("Stop " + stopId + " has no routes"); + } + + // get the number of (optional) transfers List currentTransfers = transfers.get(stopId); - int currentTransferCnt = 0; + int numberOfTransfers = currentTransfers == null ? 0 : currentTransfers.size(); + + // add stop entry to stop array + stopArr[stopIdx] = new Stop(stopId, stopRouteIdx, currentStopRoutes.size(), + numberOfTransfers == 0 ? Raptor.NO_INDEX : transferIdx, numberOfTransfers); + + // add transfer entry to transfer array if there are any if (currentTransfers != null) { for (Transfer transfer : currentTransfers) { - transferArr[transferCnt++] = transfer; - currentTransferCnt++; + transferArr[transferIdx++] = transfer; } } - Set currentStopRoutes = stopRoutes.get(stopId); - if (currentStopRoutes == null) { - throw new IllegalStateException("Stop " + stopId + " has no routes"); - } + // add route index entries to stop route array for (String routeId : currentStopRoutes) { - stopRouteArr[stopRouteCnt++] = routes.get(routeId); + stopRouteArr[stopRouteIdx++] = lookup.routes().get(routeId); } - - stopArr[stopIdx] = new Stop(stopId, stopRouteCnt - currentStopRoutes.size(), currentStopRoutes.size(), - currentTransferCnt == 0 ? Raptor.NO_INDEX : transferCnt - currentTransferCnt, currentTransferCnt); } + return new StopContext(transferArr, stopArr, stopRouteArr); } - private RouteTraversal buildRouteTraversal() { - log.debug("Building route traversal with {} routes, {} route stops, {} stop times", routeSize, routeStopSize, - stopTimeSize); - Route[] routeArr = new Route[routeSize]; + private RouteTraversal buildRouteTraversal(List routeContainers) { + log.debug("Building route traversal with {} routes, {} route stops, {} stop times", routeContainers.size(), + routeStopSize, stopTimeSize); + + // allocate arrays in needed size + Route[] routeArr = new Route[routeContainers.size()]; RouteStop[] routeStopArr = new RouteStop[routeStopSize]; StopTime[] stopTimeArr = new StopTime[stopTimeSize]; + // iterate over routes and populate arrays int routeStopCnt = 0; int stopTimeCnt = 0; - for (Map.Entry entry : routes.entrySet()) { - String routeId = entry.getKey(); - int routeIdx = entry.getValue(); - - List currentRouteStops = routeStops.get(routeId); - if (currentRouteStops == null) { - throw new IllegalStateException("Route " + routeId + " has no route stops"); - } - for (String routeStop : currentRouteStops) { - routeStopArr[routeStopCnt++] = new RouteStop(stops.get(routeStop), routeIdx); + for (int routeIdx = 0; routeIdx < routeContainers.size(); routeIdx++) { + RouteBuilder.RouteContainer routeContainer = routeContainers.get(routeIdx); + + // add route entry to route array + final int numberOfStops = routeContainer.stopSequence().size(); + final int numberOfTrips = routeContainer.trips().size(); + routeArr[routeIdx] = new Route(routeContainer.id(), routeStopCnt, numberOfStops, stopTimeCnt, + numberOfTrips); + + // add stops to route stop array + Map stopSequence = routeContainer.stopSequence(); + for (int position = 0; position < numberOfStops; position++) { + int stopIdx = stops.get(stopSequence.get(position)); + routeStopArr[routeStopCnt++] = new RouteStop(stopIdx, routeIdx); } - List currentStopTimes = stopTimes.get(routeId); - if (currentStopTimes == null) { - throw new IllegalStateException("Route " + routeId + " has no stop times"); - } - for (StopTime stopTime : currentStopTimes) { - stopTimeArr[stopTimeCnt++] = stopTime; + // add times to stop time array + for (StopTime[] stopTimes : routeContainer.trips().values()) { + for (StopTime stopTime : stopTimes) { + stopTimeArr[stopTimeCnt++] = stopTime; + } } - - routeArr[routeIdx] = new Route(routeId, routeStopCnt - currentRouteStops.size(), currentRouteStops.size(), - stopTimeCnt - currentStopTimes.size(), currentStopTimes.size() / currentRouteStops.size()); } + return new RouteTraversal(stopTimeArr, routeArr, routeStopArr); } + + private @NotNull RouteBuilder getRouteBuilder(String routeId) { + RouteBuilder routeBuilder = routeBuilders.get(routeId); + if (routeBuilder == null) { + throw new IllegalArgumentException("Route " + routeId + " does not exist"); + } + + return routeBuilder; + } + } diff --git a/src/main/java/ch/naviqore/raptor/model/RouteBuilder.java b/src/main/java/ch/naviqore/raptor/model/RouteBuilder.java new file mode 100644 index 00000000..9a74cb52 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/RouteBuilder.java @@ -0,0 +1,125 @@ +package ch.naviqore.raptor.model; + +import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Builds route containers that hold valid trips for a given route. + *

+ * The builder ensures that: + *

    + *
  • All trips of a route have the same stop sequence.
  • + *
  • Each stop time of a trip has a departure time that is temporally after the previous stop time's arrival time.
  • + *
  • In the final route container all trips are sorted according to their departure time.
  • + *
+ */ +@Log4j2 +class RouteBuilder { + + private final String routeId; + private final Map stopSequence = new HashMap<>(); + private final Map trips = new HashMap<>(); + + RouteBuilder(String routeId, List stopIds) { + this.routeId = routeId; + for (int i = 0; i < stopIds.size(); i++) { + stopSequence.put(i, stopIds.get(i)); + } + } + + void addTrip(String tripId) { + log.debug("Adding trip: id={} routeId={}", tripId, routeId); + if (trips.containsKey(tripId)) { + throw new IllegalArgumentException("Trip " + tripId + " already exists."); + } + trips.put(tripId, new StopTime[stopSequence.size()]); + } + + void addStopTime(String tripId, int position, String stopId, StopTime stopTime) { + log.debug("Adding stop time: tripId={}, position={}, stopId={}, stopTime={}", tripId, position, stopId, + stopTime); + + if (position < 0 || position >= stopSequence.size()) { + throw new IllegalArgumentException( + "Position " + position + " is out of bounds [0, " + stopSequence.size() + ")."); + } + + StopTime[] stopTimes = trips.get(tripId); + if (stopTimes == null) { + throw new IllegalArgumentException("Trip " + tripId + " does not exist."); + } + + if (!stopSequence.get(position).equals(stopId)) { + throw new IllegalArgumentException("Stop " + stopId + " does not match stop " + stopSequence.get( + position) + " at position " + position + "."); + } + + if (stopTimes[position] != null) { + throw new IllegalArgumentException("Stop time for stop " + stopId + " already exists."); + } + + if (position > 0) { + StopTime previousStopTime = stopTimes[position - 1]; + if (previousStopTime != null && previousStopTime.departure() > stopTime.arrival()) { + throw new IllegalArgumentException( + "Departure time at previous stop is greater than arrival time at current stop."); + } + } + + if (position < stopTimes.length - 1) { + StopTime nextStopTime = stopTimes[position + 1]; + if (nextStopTime != null && stopTime.departure() > nextStopTime.arrival()) { + throw new IllegalArgumentException( + "Departure time at current stop is greater than arrival time at next stop."); + } + } + + stopTimes[position] = stopTime; + } + + private void validate() { + for (Map.Entry trip : trips.entrySet()) { + StopTime[] stopTimes = trip.getValue(); + for (Map.Entry stop : stopSequence.entrySet()) { + // ensure all stop times are set and therefore all trips must have the same stops + if (stopTimes[stop.getKey()] == null) { + throw new IllegalStateException( + "Stop time at stop " + stop.getKey() + " on trip " + trip.getKey() + " not set."); + } + } + } + } + + RouteContainer build() { + log.debug("Validating and building route {}", routeId); + validate(); + + // sort trips by the departure time of the first stop + List> sortedEntries = new ArrayList<>(trips.entrySet()); + sortedEntries.sort(Comparator.comparingInt(entry -> entry.getValue()[0].departure())); + + // populate sortedTrips in the same order as sortedEntries, LinkedHashMap stores insertion order + LinkedHashMap sortedTrips = new LinkedHashMap<>(sortedEntries.size()); + for (Map.Entry entry : sortedEntries) { + sortedTrips.put(entry.getKey(), entry.getValue()); + } + + return new RouteContainer(routeId, stopSequence, sortedTrips); + } + + record RouteContainer(String id, Map stopSequence, + LinkedHashMap trips) implements Comparable { + + @Override + public int compareTo(@NotNull RouteContainer o) { + StopTime thisFirstStopTime = this.trips.values().iterator().next()[0]; + StopTime otherFirstStopTime = o.trips.values().iterator().next()[0]; + return Integer.compare(thisFirstStopTime.departure(), otherFirstStopTime.departure()); + } + + } +} + + diff --git a/src/main/java/ch/naviqore/raptor/model/TripValidator.java b/src/main/java/ch/naviqore/raptor/model/TripValidator.java deleted file mode 100644 index 479e6bf4..00000000 --- a/src/main/java/ch/naviqore/raptor/model/TripValidator.java +++ /dev/null @@ -1,74 +0,0 @@ -package ch.naviqore.raptor.model; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Validates trips of a route. - */ -class TripValidator { - private final Map stopSequence = new HashMap<>(); - private final Map trips = new HashMap<>(); - - TripValidator(List stopIds) { - for (int i = 0; i < stopIds.size(); i++) { - stopSequence.put(stopIds.get(i), i); - } - } - - void addTrip(String tripId) { - if (trips.containsKey(tripId)) { - throw new IllegalArgumentException("Trip " + tripId + " already exists."); - } - trips.put(tripId, new StopTime[stopSequence.size()]); - } - - void addStopTime(String tripId, String stopId, StopTime stopTime) { - StopTime[] stopTimes = trips.get(tripId); - if (stopTimes == null) { - throw new IllegalArgumentException("Trip " + tripId + " does not exist."); - } - - Integer stopIdx = stopSequence.get(stopId); - if (stopIdx == null) { - throw new IllegalArgumentException("Stop " + stopId + " does not exist."); - } - - if (stopTimes[stopIdx] != null) { - throw new IllegalArgumentException("Stop time for stop " + stopId + " already exists."); - } - - if (stopIdx > 0) { - StopTime previousStopTime = stopTimes[stopIdx - 1]; - if (previousStopTime != null && previousStopTime.departure() > stopTime.arrival()) { - throw new IllegalArgumentException( - "Departure time at previous stop is greater than arrival time at current stop."); - } - } - - if (stopIdx < stopTimes.length - 1) { - StopTime nextStopTime = stopTimes[stopIdx + 1]; - if (nextStopTime != null && stopTime.departure() > nextStopTime.arrival()) { - throw new IllegalArgumentException( - "Departure time at current stop is greater than arrival time at next stop."); - } - } - - stopTimes[stopIdx] = stopTime; - } - - void validate() { - for (Map.Entry trip : trips.entrySet()) { - StopTime[] stopTimes = trip.getValue(); - for (Map.Entry stop : stopSequence.entrySet()) { - if (stopTimes[stop.getValue()] == null) { - throw new IllegalStateException( - "Stop time at stop " + stop.getKey() + " on trip " + trip.getKey() + " not set."); - } - } - } - } -} - - diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java b/src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java index 96203665..3d6f7bb6 100644 --- a/src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTestBuilder.java @@ -48,45 +48,63 @@ public class RaptorTestBuilder { private final List transfers = new ArrayList<>(); private static Raptor build(List routes, List transfers, int dayStart, int dayEnd) { - Set addedStops = new HashSet<>(); RaptorBuilder builder = Raptor.builder(); - routes.forEach(route -> { - builder.addRoute(route.id + "-F"); - builder.addRoute(route.id + "-R"); - route.stops.forEach(stop -> { + Set addedStops = new HashSet<>(); + + for (Route route : routes) { + // define route ids + String routeIdF = route.id + "-F"; + String routeIdR = route.id + "-R"; + + // add stops + for (String stop : route.stops) { if (!addedStops.contains(stop)) { builder.addStop(stop); addedStops.add(stop); } - }); - for (int i = 0; i < route.stops.size(); i++) { - builder.addRouteStop(route.stops.get(i), route.id + "-F"); - builder.addRouteStop(route.stops.get(route.stops.size() - 1 - i), route.id + "-R"); } - int time = dayStart * SECONDS_IN_HOUR + route.firstDepartureOffsetInMinutes * 60; + + // add routes + builder.addRoute(routeIdF, route.stops); + builder.addRoute(routeIdR, route.stops.reversed()); + + // add trips + int tripCount = 0; + int time = dayStart * SECONDS_IN_HOUR + route.firstDepartureOffset * 60; while (time < dayEnd * SECONDS_IN_HOUR) { + + // add trips + String tripIdF = String.format("%s-F-%s", route.id, tripCount); + String tripIdR = String.format("%s-R-%s", route.id, tripCount); + builder.addTrip(tripIdF, routeIdF).addTrip(tripIdR, routeIdR); + tripCount++; + + // add stop times int departureTime = time; // first stop of trip has no arrival time - int arrivalTime = 0; + int arrivalTime = departureTime; for (int i = 0; i < route.stops.size(); i++) { if (i + 1 == route.stops.size()) { // last stop of trip has no departure time - departureTime = 0; + departureTime = arrivalTime; } - builder.addStopTime(route.stops.get(i), route.id + "-F", arrivalTime, departureTime); - builder.addStopTime(route.stops.get(route.stops.size() - 1 - i), route.id + "-R", arrivalTime, + builder.addStopTime(routeIdF, tripIdF, i, route.stops.get(i), arrivalTime, departureTime); + builder.addStopTime(routeIdR, tripIdR, i, route.stops.get(route.stops.size() - 1 - i), arrivalTime, departureTime); - arrivalTime = departureTime + route.timeBetweenStopsInMinutes * 60; - departureTime = arrivalTime + route.dwellTimeInMinutes * 60; + arrivalTime = departureTime + route.travelTimeBetweenStops * 60; + departureTime = arrivalTime + route.dwellTimeAtSTop * 60; } - time += route.timeBetweenDeparturesInMinutes * 60; + + time += route.headWayTime * 60; } - }); - transfers.forEach(transfer -> { - builder.addTransfer(transfer.sourceStop, transfer.targetStop, transfer.durationInMinutes * 60); - builder.addTransfer(transfer.targetStop, transfer.sourceStop, transfer.durationInMinutes * 60); - }); + } + + for (Transfer transfer : transfers) { + builder.addTransfer(transfer.sourceStop, transfer.targetStop, transfer.duration * 60); + builder.addTransfer(transfer.targetStop, transfer.sourceStop, transfer.duration * 60); + } + return builder.build(); } @@ -138,8 +156,13 @@ public Raptor buildWithDefaults() { .build(); } - private record Route(String id, List stops, int firstDepartureOffsetInMinutes, - int timeBetweenDeparturesInMinutes, int timeBetweenStopsInMinutes, int dwellTimeInMinutes) { + /** + * Route, times are in minutes. + * + * @param headWayTime the time between the trip departures. + */ + private record Route(String id, List stops, int firstDepartureOffset, int headWayTime, + int travelTimeBetweenStops, int dwellTimeAtSTop) { public Route(String id, List stops) { this(id, stops, 0, 15, 5, 1); @@ -147,7 +170,10 @@ public Route(String id, List stops) { } - private record Transfer(String sourceStop, String targetStop, int durationInMinutes) { + /** + * Transfer, times are in minutes. + */ + private record Transfer(String sourceStop, String targetStop, int duration) { } } diff --git a/src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java b/src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java new file mode 100644 index 00000000..e4492640 --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java @@ -0,0 +1,285 @@ +package ch.naviqore.raptor.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class RouteBuilderTest { + + private static final String ROUTE_1 = "route1"; + private static final String TRIP_1 = "trip1"; + private static final String STOP_1 = "stop1"; + private static final String STOP_2 = "stop2"; + private static final String STOP_3 = "stop3"; + private RouteBuilder builder; + + @Nested + class LinearRoute { + + private static final List STOP_IDS = List.of(STOP_1, STOP_2, STOP_3); + + @BeforeEach + void setUp() { + builder = new RouteBuilder(ROUTE_1, STOP_IDS); + builder.addTrip(TRIP_1); + } + + @Nested + class AddTrip { + + @Test + void shouldAddValidTrips() { + assertDoesNotThrow(() -> builder.addTrip("trip2")); + } + + @Test + void shouldNotAddDuplicateTrip() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addTrip(TRIP_1)); + assertEquals("Trip trip1 already exists.", exception.getMessage()); + } + } + + @Nested + class AddStopTime { + + @Test + void shouldAddValidStopTimes() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + StopTime stopTime3 = new StopTime(500, 600); + + assertDoesNotThrow(() -> builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1)); + assertDoesNotThrow(() -> builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2)); + assertDoesNotThrow(() -> builder.addStopTime(TRIP_1, 2, STOP_3, stopTime3)); + } + + @Test + void shouldNotAddStopTimeToNonExistentTrip() { + StopTime stopTime = new StopTime(100, 200); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime("trip2", 0, STOP_1, stopTime)); + assertEquals("Trip trip2 does not exist.", exception.getMessage()); + } + + @Test + void shouldNotAddStopTimeWithNegativePosition() { + StopTime stopTime = new StopTime(100, 200); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, -1, STOP_1, stopTime)); + assertEquals("Position -1 is out of bounds [0, " + STOP_IDS.size() + ").", exception.getMessage()); + } + + @Test + void shouldNotAddStopTimeWithPositionOutOfBounds() { + StopTime stopTime = new StopTime(100, 200); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, STOP_IDS.size(), STOP_1, stopTime)); + assertEquals("Position " + STOP_IDS.size() + " is out of bounds [0, " + STOP_IDS.size() + ").", + exception.getMessage()); + } + + @Test + void shouldNotAddStopTimeForNonMatchingStop() { + StopTime stopTime = new StopTime(100, 200); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, 0, "nonexistentStop", stopTime)); + assertEquals("Stop nonexistentStop does not match stop stop1 at position 0.", exception.getMessage()); + } + + @Test + void shouldNotAddDuplicateStopTimes() { + StopTime stopTime = new StopTime(100, 200); + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, 0, STOP_1, stopTime)); + assertEquals("Stop time for stop stop1 already exists.", exception.getMessage()); + } + + @Test + void shouldNotAddStopTimesWithOverlapOnPreviousStop() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(150, 250); // overlapping times + + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2)); + assertEquals("Departure time at previous stop is greater than arrival time at current stop.", + exception.getMessage()); + } + + @Test + void shouldNotAddStopTimesWithOverlapOnNextStop() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(250, 350); + StopTime stopTime3 = new StopTime(300, 400); + + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1); + builder.addStopTime(TRIP_1, 2, STOP_3, stopTime3); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2)); + assertEquals("Departure time at current stop is greater than arrival time at next stop.", + exception.getMessage()); + } + } + } + + @Nested + class CircularRoute { + + private static final List STOP_IDS = List.of(STOP_1, STOP_2, STOP_3, STOP_1); + + @BeforeEach + void setUp() { + builder = new RouteBuilder(ROUTE_1, STOP_IDS); + builder.addTrip(TRIP_1); + } + + @Test + void shouldAddSameStopTwice() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + StopTime stopTime3 = new StopTime(500, 600); + StopTime stopTime4 = new StopTime(700, 800); + + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1); + builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2); + builder.addStopTime(TRIP_1, 2, STOP_3, stopTime3); + builder.addStopTime(TRIP_1, 3, STOP_1, stopTime4); + + assertDoesNotThrow(builder::build); + } + + @Test + void shouldAddSameStopTwiceRandomOrder() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + StopTime stopTime3 = new StopTime(500, 600); + StopTime stopTime4 = new StopTime(700, 800); + + builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2); + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1); + builder.addStopTime(TRIP_1, 3, STOP_1, stopTime4); + builder.addStopTime(TRIP_1, 2, STOP_3, stopTime3); + + assertDoesNotThrow(builder::build); + } + + @Test + void shouldNotAddStopTimesWithOverlapOnNextStop() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + StopTime stopTime3 = new StopTime(500, 650); + StopTime stopTime4 = new StopTime(600, 700); + + builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2); + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1); + builder.addStopTime(TRIP_1, 2, STOP_3, stopTime3); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, 3, STOP_1, stopTime4)); + assertEquals("Departure time at previous stop is greater than arrival time at current stop.", + exception.getMessage()); + } + } + + @Nested + class Build { + + private static final List STOP_IDS_1 = List.of(STOP_1, STOP_2, STOP_3); + + @BeforeEach + void setUp() { + builder = new RouteBuilder(ROUTE_1, STOP_IDS_1); + builder.addTrip(TRIP_1); + } + + @Nested + class Validate { + + @Test + void shouldValidateCompleteRoute() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + StopTime stopTime3 = new StopTime(500, 600); + + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1); + builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2); + builder.addStopTime(TRIP_1, 2, STOP_3, stopTime3); + + assertDoesNotThrow(() -> builder.build()); + } + + @Test + void shouldNotValidateWhenStopTimeIsMissing() { + StopTime stopTime1 = new StopTime(100, 200); + StopTime stopTime2 = new StopTime(300, 400); + + builder.addStopTime(TRIP_1, 0, STOP_1, stopTime1); + builder.addStopTime(TRIP_1, 1, STOP_2, stopTime2); + + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> builder.build()); + assertTrue(exception.getMessage().contains("Stop time at stop 2 on trip trip1 not set.")); + } + } + + @Nested + class MultipleRoutes { + + private static final String ROUTE_2 = "route2"; + private static final String TRIP_2 = "trip2"; + private static final List STOP_IDS_2 = List.of(STOP_3, STOP_2, STOP_1); + private RouteBuilder builder2; + + @BeforeEach + void setUp() { + builder.addTrip(TRIP_2); + builder2 = new RouteBuilder(ROUTE_2, STOP_IDS_2); + builder2.addTrip(TRIP_1); + builder2.addTrip(TRIP_2); + } + + @Test + void shouldSortRouteContainersByFirstTripDepartureTime() { + // add everything in reverse order: latest stops first + // route1, trip2: third trip departure + builder.addStopTime(TRIP_2, 2, STOP_3, new StopTime(900, 900)); + builder.addStopTime(TRIP_2, 1, STOP_2, new StopTime(600, 700)); + builder.addStopTime(TRIP_2, 0, STOP_1, new StopTime(400, 500)); + + // route2, trip2: fourth trip departure + builder2.addStopTime(TRIP_2, 2, STOP_1, new StopTime(950, 950)); + builder2.addStopTime(TRIP_2, 1, STOP_2, new StopTime(650, 750)); + builder2.addStopTime(TRIP_2, 0, STOP_3, new StopTime(450, 550)); + + // route1, trip1: second trip departure + builder.addStopTime(TRIP_1, 2, STOP_3, new StopTime(550, 650)); + builder.addStopTime(TRIP_1, 1, STOP_2, new StopTime(350, 450)); + builder.addStopTime(TRIP_1, 0, STOP_1, new StopTime(150, 250)); + + // route2, trip1: first trip departure + builder2.addStopTime(TRIP_1, 2, STOP_1, new StopTime(500, 600)); + builder2.addStopTime(TRIP_1, 1, STOP_2, new StopTime(300, 400)); + builder2.addStopTime(TRIP_1, 0, STOP_3, new StopTime(100, 200)); + + // build route containers + List containers = new ArrayList<>(); + containers.add(builder.build()); + containers.add(builder2.build()); + containers.sort(Comparator.naturalOrder()); + + // check order + List expectedOrder = List.of(ROUTE_2, ROUTE_1); + List actualOrder = containers.stream().map(RouteBuilder.RouteContainer::id).toList(); + assertEquals(expectedOrder, actualOrder); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java b/src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java deleted file mode 100644 index 88963892..00000000 --- a/src/test/java/ch/naviqore/raptor/model/TripValidatorTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package ch.naviqore.raptor.model; - -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.*; - -class TripValidatorTest { - - private static final List STOP_IDS = List.of("stop1", "stop2", "stop3"); - private TripValidator tripValidator; - - @BeforeEach - void setUp() { - tripValidator = new TripValidator(STOP_IDS); - } - - @Nested - class AddTrip { - - @Test - void shouldAddValidTrips() { - tripValidator.addTrip("trip1"); - assertDoesNotThrow(() -> tripValidator.addTrip("trip2")); - } - - @Test - void shouldNotAddDuplicateTrip() { - tripValidator.addTrip("trip1"); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> tripValidator.addTrip("trip1")); - assertEquals("Trip trip1 already exists.", exception.getMessage()); - } - } - - @Nested - class AddStopTime { - - @Test - void shouldAddValidStopTimes() { - tripValidator.addTrip("trip1"); - StopTime stopTime1 = new StopTime(100, 200); - StopTime stopTime2 = new StopTime(300, 400); - StopTime stopTime3 = new StopTime(500, 600); - - assertDoesNotThrow(() -> tripValidator.addStopTime("trip1", "stop1", stopTime1)); - assertDoesNotThrow(() -> tripValidator.addStopTime("trip1", "stop2", stopTime2)); - assertDoesNotThrow(() -> tripValidator.addStopTime("trip1", "stop3", stopTime3)); - } - - @Test - void shouldNotAddStopTimeToNonExistentTrip() { - StopTime stopTime = new StopTime(100, 200); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> tripValidator.addStopTime("trip1", "stop1", stopTime)); - assertEquals("Trip trip1 does not exist.", exception.getMessage()); - } - - @Test - void shouldNotAddStopTimeForNonExistentStop() { - tripValidator.addTrip("trip1"); - StopTime stopTime = new StopTime(100, 200); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> tripValidator.addStopTime("trip1", "nonexistentStop", stopTime)); - assertEquals("Stop nonexistentStop does not exist.", exception.getMessage()); - } - - @Test - void shouldNotAddDuplicateStopTimes() { - tripValidator.addTrip("trip1"); - StopTime stopTime = new StopTime(100, 200); - tripValidator.addStopTime("trip1", "stop1", stopTime); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> tripValidator.addStopTime("trip1", "stop1", stopTime)); - assertEquals("Stop time for stop stop1 already exists.", exception.getMessage()); - } - - @Test - void shouldNotAddStopTimesWithOverlapOnPreviousStop() { - tripValidator.addTrip("trip1"); - StopTime stopTime1 = new StopTime(100, 200); - StopTime stopTime2 = new StopTime(150, 250); // overlapping times - - tripValidator.addStopTime("trip1", "stop1", stopTime1); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> tripValidator.addStopTime("trip1", "stop2", stopTime2)); - assertEquals("Departure time at previous stop is greater than arrival time at current stop.", - exception.getMessage()); - } - - @Test - void shouldNotAddStopTimesWithOverlapOnNextStop() { - tripValidator.addTrip("trip1"); - StopTime stopTime1 = new StopTime(100, 200); - StopTime stopTime2 = new StopTime(250, 350); - StopTime stopTime3 = new StopTime(300, 400); - - tripValidator.addStopTime("trip1", "stop1", stopTime1); - tripValidator.addStopTime("trip1", "stop3", stopTime3); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> tripValidator.addStopTime("trip1", "stop2", stopTime2)); - assertEquals("Departure time at current stop is greater than arrival time at next stop.", - exception.getMessage()); - } - } - - @Nested - class Validate { - - @Test - void shouldValidateCompleteRoute() { - tripValidator.addTrip("trip1"); - StopTime stopTime1 = new StopTime(100, 200); - StopTime stopTime2 = new StopTime(300, 400); - StopTime stopTime3 = new StopTime(500, 600); - - tripValidator.addStopTime("trip1", "stop1", stopTime1); - tripValidator.addStopTime("trip1", "stop2", stopTime2); - tripValidator.addStopTime("trip1", "stop3", stopTime3); - - assertDoesNotThrow(() -> tripValidator.validate()); - } - - @Test - void shouldNotValidateWhenStopTimeIsMissing() { - tripValidator.addTrip("trip1"); - StopTime stopTime1 = new StopTime(100, 200); - StopTime stopTime2 = new StopTime(300, 400); - - tripValidator.addStopTime("trip1", "stop1", stopTime1); - tripValidator.addStopTime("trip1", "stop2", stopTime2); - - IllegalStateException exception = assertThrows(IllegalStateException.class, () -> tripValidator.validate()); - assertTrue(exception.getMessage().contains("Stop time at stop stop3 on trip trip1 not set.")); - } - } -} From d59277e086989882c16ab0c2d103adb3d724b4c9 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Wed, 22 May 2024 20:47:40 +0200 Subject: [PATCH 26/35] ENH: NAV-42 - Make raptor internal classes package-private - Some cosmetics: Remove sys.out.print call and correct log config file path in benchmark javadoc. --- src/main/java/ch/naviqore/raptor/model/Lookup.java | 2 +- src/main/java/ch/naviqore/raptor/model/Raptor.java | 8 ++++---- src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java | 2 -- src/main/java/ch/naviqore/raptor/model/Route.java | 2 +- src/main/java/ch/naviqore/raptor/model/RouteStop.java | 2 +- .../java/ch/naviqore/raptor/model/RouteTraversal.java | 2 +- src/main/java/ch/naviqore/raptor/model/Stop.java | 2 +- src/main/java/ch/naviqore/raptor/model/StopContext.java | 2 +- src/main/java/ch/naviqore/raptor/model/StopTime.java | 2 +- src/main/java/ch/naviqore/raptor/model/Transfer.java | 2 +- src/test/java/ch/naviqore/Benchmark.java | 2 +- src/test/java/ch/naviqore/raptor/model/RaptorTest.java | 1 - 12 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/java/ch/naviqore/raptor/model/Lookup.java b/src/main/java/ch/naviqore/raptor/model/Lookup.java index 0c4483a9..751b45a7 100644 --- a/src/main/java/ch/naviqore/raptor/model/Lookup.java +++ b/src/main/java/ch/naviqore/raptor/model/Lookup.java @@ -2,5 +2,5 @@ import java.util.Map; -public record Lookup(Map stops, Map routes) { +record Lookup(Map stops, Map routes) { } diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index 48ce9bc0..4b75f5e8 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -7,8 +7,6 @@ /** * Raptor algorithm implementation - * - * @author munterfi */ @Log4j2 public class Raptor { @@ -171,8 +169,10 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in } else { log.debug("Stop {} was not improved", stop.id()); Leg previous = earliestArrivalsLastRound[stopIdx]; - if( previous == null || previous.arrivalTime >= stopTime.arrival() ) { - log.debug("Stop {} has been improved in same round, earlier trip not possible within this round", stop.id()); + if (previous == null || previous.arrivalTime >= stopTime.arrival()) { + log.debug( + "Stop {} has been improved in same round, earlier trip not possible within this round", + stop.id()); continue; } else { log.debug("Checking for earlier trips at stop {}", stop.id()); diff --git a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java index 6df7f13a..7a59c2ad 100644 --- a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java +++ b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java @@ -9,8 +9,6 @@ /** * Builds the Raptor and its internal data structures - *

- * Note: The builder expects that stops, routes, route stops and stop times are added in the correct order. * * @author munterfi */ diff --git a/src/main/java/ch/naviqore/raptor/model/Route.java b/src/main/java/ch/naviqore/raptor/model/Route.java index a7d526cc..89998cd5 100644 --- a/src/main/java/ch/naviqore/raptor/model/Route.java +++ b/src/main/java/ch/naviqore/raptor/model/Route.java @@ -1,4 +1,4 @@ package ch.naviqore.raptor.model; -public record Route(String id, int firstRouteStopIdx, int numberOfStops, int firstStopTimeIdx, int numberOfTrips) { +record Route(String id, int firstRouteStopIdx, int numberOfStops, int firstStopTimeIdx, int numberOfTrips) { } diff --git a/src/main/java/ch/naviqore/raptor/model/RouteStop.java b/src/main/java/ch/naviqore/raptor/model/RouteStop.java index 1c7fc9fd..5f9c5b26 100644 --- a/src/main/java/ch/naviqore/raptor/model/RouteStop.java +++ b/src/main/java/ch/naviqore/raptor/model/RouteStop.java @@ -1,4 +1,4 @@ package ch.naviqore.raptor.model; -public record RouteStop(int stopIndex, int routeIndex) { +record RouteStop(int stopIndex, int routeIndex) { } \ No newline at end of file diff --git a/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java b/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java index 3b2c3935..f467f818 100644 --- a/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java +++ b/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java @@ -7,5 +7,5 @@ * @param routes routes * @param routeStops route stops */ -public record RouteTraversal(StopTime[] stopTimes, Route[] routes, RouteStop[] routeStops) { +record RouteTraversal(StopTime[] stopTimes, Route[] routes, RouteStop[] routeStops) { } diff --git a/src/main/java/ch/naviqore/raptor/model/Stop.java b/src/main/java/ch/naviqore/raptor/model/Stop.java index 52c9a63e..17a9b49d 100644 --- a/src/main/java/ch/naviqore/raptor/model/Stop.java +++ b/src/main/java/ch/naviqore/raptor/model/Stop.java @@ -1,4 +1,4 @@ package ch.naviqore.raptor.model; -public record Stop(String id, int stopRouteIdx, int numberOfRoutes, int transferIdx, int numberOfTransfers) { +record Stop(String id, int stopRouteIdx, int numberOfRoutes, int transferIdx, int numberOfTransfers) { } diff --git a/src/main/java/ch/naviqore/raptor/model/StopContext.java b/src/main/java/ch/naviqore/raptor/model/StopContext.java index db25e9e5..3900d187 100644 --- a/src/main/java/ch/naviqore/raptor/model/StopContext.java +++ b/src/main/java/ch/naviqore/raptor/model/StopContext.java @@ -1,4 +1,4 @@ package ch.naviqore.raptor.model; -public record StopContext(Transfer[] transfers, Stop[] stops, int[] stopRoutes) { +record StopContext(Transfer[] transfers, Stop[] stops, int[] stopRoutes) { } diff --git a/src/main/java/ch/naviqore/raptor/model/StopTime.java b/src/main/java/ch/naviqore/raptor/model/StopTime.java index 732505d8..08cf72a4 100644 --- a/src/main/java/ch/naviqore/raptor/model/StopTime.java +++ b/src/main/java/ch/naviqore/raptor/model/StopTime.java @@ -1,6 +1,6 @@ package ch.naviqore.raptor.model; -public record StopTime(int arrival, int departure) { +record StopTime(int arrival, int departure) { public StopTime { if (arrival > departure) { diff --git a/src/main/java/ch/naviqore/raptor/model/Transfer.java b/src/main/java/ch/naviqore/raptor/model/Transfer.java index 64294cf2..c8699d00 100644 --- a/src/main/java/ch/naviqore/raptor/model/Transfer.java +++ b/src/main/java/ch/naviqore/raptor/model/Transfer.java @@ -1,4 +1,4 @@ package ch.naviqore.raptor.model; -public record Transfer(int targetStopIdx, int duration) { +record Transfer(int targetStopIdx, int duration) { } diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 7c077599..70d3813e 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -29,7 +29,7 @@ * Measures the time it takes to route a number of requests using Raptor algorithm on large GTFS datasets. *

* Note: To run this benchmark, ensure that the log level is set to INFO in the - * {@code src/test/resources/log4j2.properties} file. + * {@code src/test/resources/log4j2-test.properties} file. * * @author munterfi */ diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java index 1c52ef02..dffbe0bd 100644 --- a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java @@ -78,7 +78,6 @@ void routeBetweenTwoStopsOnSameRoute(RaptorTestBuilder builder) { String targetStop = "B"; int departureTime = 8 * SECONDS_IN_HOUR; List connections = raptor.routeEarliestArrival(sourceStop, targetStop, departureTime); - System.out.println(connections); assertEquals(1, connections.size()); Connection connection = connections.getFirst(); assertEquals(sourceStop, connection.getFromStopId()); From 100a78b1528fff8b0f9b9002b199cf63fa52891b Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Wed, 22 May 2024 21:23:40 +0200 Subject: [PATCH 27/35] TEST: NAV-42 - Move GTFS integration test data to correct package structure --- .../gtfs/schedule/GtfsScheduleTestData.java | 2 +- .../{ => ch/naviqore}/gtfs/schedule/SOURCE.md | 0 .../naviqore}/gtfs/schedule/sample-feed-1.zip | Bin 3 files changed, 1 insertion(+), 1 deletion(-) rename src/test/resources/{ => ch/naviqore}/gtfs/schedule/SOURCE.md (100%) rename src/test/resources/{ => ch/naviqore}/gtfs/schedule/sample-feed-1.zip (100%) diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java index 5d26b32c..e365dfea 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java @@ -16,7 +16,7 @@ public final class GtfsScheduleTestData { public static final String SAMPLE_FEED = "sample-feed-1"; public static final String SAMPLE_FEED_ZIP = SAMPLE_FEED + ".zip"; - public static final String RESOURCE_PATH = "gtfs/schedule/" + SAMPLE_FEED_ZIP; + public static final String RESOURCE_PATH = "ch/naviqore/gtfs/schedule/" + SAMPLE_FEED_ZIP; public static File prepareZipDataset(@TempDir Path tempDir) throws IOException { try (InputStream is = GtfsScheduleReaderIT.class.getClassLoader().getResourceAsStream(RESOURCE_PATH)) { diff --git a/src/test/resources/gtfs/schedule/SOURCE.md b/src/test/resources/ch/naviqore/gtfs/schedule/SOURCE.md similarity index 100% rename from src/test/resources/gtfs/schedule/SOURCE.md rename to src/test/resources/ch/naviqore/gtfs/schedule/SOURCE.md diff --git a/src/test/resources/gtfs/schedule/sample-feed-1.zip b/src/test/resources/ch/naviqore/gtfs/schedule/sample-feed-1.zip similarity index 100% rename from src/test/resources/gtfs/schedule/sample-feed-1.zip rename to src/test/resources/ch/naviqore/gtfs/schedule/sample-feed-1.zip From 12114efe290b6543e9972fd92422e407abaf93e4 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Wed, 22 May 2024 21:24:56 +0200 Subject: [PATCH 28/35] DOC: NAV-42 - Informative javadoc of raptor builder --- .../java/ch/naviqore/raptor/model/RaptorBuilder.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java index 7a59c2ad..fd2dc260 100644 --- a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java +++ b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java @@ -8,7 +8,15 @@ import java.util.*; /** - * Builds the Raptor and its internal data structures + * 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: + *

    + *
  • All stops must have at least one route serving them.
  • + *
  • All stops of a route must be known before adding the route.
  • + *
  • All trips of a route must have the same stop sequence.
  • + *
  • Each stop time of a trip must have a departure time after the previous stop time's arrival time.
  • + *
  • All trips in the final route container must be sorted by departure time.
  • + *
* * @author munterfi */ From 000a21f124270be8bb6434f627460988bbad802f Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Thu, 23 May 2024 00:00:58 +0200 Subject: [PATCH 29/35] FIX: NAV-17 - Make sure that trip is not entered if no valid trips are found. --- src/main/java/ch/naviqore/raptor/model/Raptor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index e8ac1823..6bdcd4b7 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -196,6 +196,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in } else { // no active trip found log.debug("No active trip found on route {}", currentRoute.id()); + enteredTrip = false; break; } } From 20dfb65bef209247144387bc92860e3fcb7cf672 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Thu, 23 May 2024 00:01:38 +0200 Subject: [PATCH 30/35] FIX: NAV-17 - Make sure that only last round arrival times are used for looking for next trip departures. --- src/main/java/ch/naviqore/raptor/model/Raptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index 6bdcd4b7..529dfeb7 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -186,7 +186,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in enteredAtArrival = earliestArrivalsLastRound[stopIdx]; while (tripOffset < numberOfTrips) { StopTime currentStopTime = stopTimes[firstStopTimeIdx + tripOffset * numberOfStops + stopOffset]; - if (currentStopTime.departure() >= earliestArrivalTime + SAME_STOP_TRANSFER_TIME) { + if (currentStopTime.departure() >= enteredAtArrival.arrivalTime + SAME_STOP_TRANSFER_TIME) { log.debug("Found active trip ({}) on route {}", tripOffset, currentRoute.id()); tripEntryTime = currentStopTime.departure(); break; From 936037f18ae7bc99e551db2760a127e195b45884 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Thu, 23 May 2024 00:02:35 +0200 Subject: [PATCH 31/35] FIX: NAV-17 - Do not write null results to csv in benchmark. --- src/test/java/ch/naviqore/Benchmark.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 7c077599..8dd41fab 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -125,6 +125,9 @@ private static void writeResultsToCsv(RoutingResult[] results) throws IOExceptio writer.println(header); for (RoutingResult result : results) { + if( result == null ){ + continue; + } writer.printf("%s,%s,%d,%d,%d,%d,%d,%d%n", result.sourceStop, result.targetStop, result.requestedDepartureTime, result.connections.size(), result.departureTime, result.arrivalTime, result.transfers, result.time); From 285ddc2f2329270106b3c9228c17e822fe87dc9a Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Thu, 23 May 2024 11:51:14 +0200 Subject: [PATCH 32/35] DOC: TEST-42 - Enhance benchmark runs - Only request valid stop ids from raptor. - Write more information to the results. - Add converters to ServiceDayTime. --- .../gtfs/schedule/type/ServiceDayTime.java | 27 +++++ src/test/java/ch/naviqore/Benchmark.java | 113 +++++++++++++----- .../schedule/type/ServiceDayTimeTest.java | 110 +++++++++++++++++ 3 files changed, 223 insertions(+), 27 deletions(-) create mode 100644 src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java index dc1238ec..fce57518 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java @@ -3,6 +3,10 @@ import lombok.EqualsAndHashCode; import lombok.Getter; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + /** * Service day time *

@@ -14,13 +18,21 @@ @EqualsAndHashCode @Getter public final class ServiceDayTime implements Comparable { + + public static final int SECONDS_IN_DAY = 86400; private final int totalSeconds; public ServiceDayTime(int seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("Seconds cannot be negative"); + } this.totalSeconds = seconds; } public ServiceDayTime(int hours, int minutes, int seconds) { + if (hours < 0 || minutes < 0 || seconds < 0) { + throw new IllegalArgumentException("Hours, minutes, and seconds cannot be negative"); + } this.totalSeconds = seconds + 60 * minutes + 3600 * hours; } @@ -32,6 +44,20 @@ public static ServiceDayTime parse(String timeString) { return new ServiceDayTime(hours, minutes, seconds); } + public LocalTime toLocalTime() { + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int seconds = totalSeconds % 60; + return LocalTime.of(hours % 24, minutes, seconds); + } + + public LocalDateTime toLocalDateTime(LocalDate date) { + LocalTime localTime = this.toLocalTime(); + int hours = totalSeconds / 3600; + LocalDate adjustedDate = date.plusDays(hours / 24); + return LocalDateTime.of(adjustedDate, localTime); + } + @Override public int compareTo(ServiceDayTime o) { return Integer.compare(totalSeconds, o.totalSeconds); @@ -44,4 +70,5 @@ public String toString() { int seconds = totalSeconds % 60; return String.format("%02d:%02d:%02d", hours, minutes, seconds); } + } diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 7016e5ad..4ab9fc0e 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -3,6 +3,10 @@ import ch.naviqore.BenchmarkData.Dataset; import ch.naviqore.gtfs.schedule.GtfsScheduleReader; import ch.naviqore.gtfs.schedule.model.GtfsSchedule; +import ch.naviqore.gtfs.schedule.model.Stop; +import ch.naviqore.gtfs.schedule.model.StopTime; +import ch.naviqore.gtfs.schedule.model.Trip; +import ch.naviqore.gtfs.schedule.type.ServiceDayTime; import ch.naviqore.raptor.GtfsToRaptorConverter; import ch.naviqore.raptor.model.Connection; import ch.naviqore.raptor.model.Raptor; @@ -19,9 +23,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; +import java.util.*; /** * Benchmark for Raptor routing algorithm. @@ -37,19 +39,28 @@ @Log4j2 final class Benchmark { - private static final long SEED = 1234; - private static final int N = 1000; + // dataset private static final Dataset DATASET = Dataset.SWITZERLAND; private static final LocalDate SCHEDULE_DATE = LocalDate.of(2024, 4, 26); - private static final int SECONDS_IN_DAY = 86400; + + // sampling + /** + * Limit in seconds after midnight for the departure time. Only allow early departure times, otherwise many + * connections crossing the complete schedule (region) are not feasible. + */ + private static final int DEPARTURE_TIME_LIMIT = 8 * 60 * 60; + private static final long RANDOM_SEED = 1234; + private static final int SAMPLE_SIZE = 10000; + + // constants 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; public static void main(String[] args) throws IOException, InterruptedException { GtfsSchedule schedule = initializeSchedule(); Raptor raptor = initializeRaptor(schedule); - List stopIds = new ArrayList<>(schedule.getStops().keySet()); - RouteRequest[] requests = sampleRouteRequests(stopIds); + RouteRequest[] requests = sampleRouteRequests(schedule); RoutingResult[] results = processRequests(raptor, requests); writeResultsToCsv(results); } @@ -72,14 +83,24 @@ private static void manageResources() throws InterruptedException { Thread.sleep(MONITORING_INTERVAL_MS); } - private static RouteRequest[] sampleRouteRequests(List stopIds) { - Random random = new Random(SEED); - RouteRequest[] requests = new RouteRequest[N]; - for (int i = 0; i < N; i++) { + private static RouteRequest[] sampleRouteRequests(GtfsSchedule schedule) { + // extract valid stops for day + Set uniqueStopIds = new HashSet<>(); + for (Trip trip : schedule.getActiveTrips(SCHEDULE_DATE)) { + for (StopTime stopTime : trip.getStopTimes()) { + uniqueStopIds.add(stopTime.stop().getId()); + } + } + List stopIds = new ArrayList<>(uniqueStopIds); + + // sample + Random random = new Random(RANDOM_SEED); + RouteRequest[] requests = new RouteRequest[SAMPLE_SIZE]; + for (int i = 0; i < SAMPLE_SIZE; i++) { int sourceIndex = random.nextInt(stopIds.size()); int destinationIndex = getRandomDestinationIndex(stopIds.size(), sourceIndex, random); - requests[i] = new RouteRequest(stopIds.get(sourceIndex), stopIds.get(destinationIndex), - random.nextInt(SECONDS_IN_DAY)); + requests[i] = new RouteRequest(schedule.getStops().get(stopIds.get(sourceIndex)), + schedule.getStops().get(stopIds.get(destinationIndex)), random.nextInt(DEPARTURE_TIME_LIMIT)); } return requests; } @@ -95,12 +116,10 @@ private static RoutingResult[] processRequests(Raptor raptor, RouteRequest[] req for (int i = 0; i < requests.length; i++) { long startTime = System.nanoTime(); try { - List connections = raptor.routeEarliestArrival(requests[i].sourceStop(), - requests[i].targetStop(), requests[i].departureTime()); + List connections = raptor.routeEarliestArrival(requests[i].sourceStop().getId(), + requests[i].targetStop().getId(), requests[i].departureTime()); long endTime = System.nanoTime(); - responses[i] = new RoutingResult(requests[i].sourceStop(), requests[i].targetStop(), - requests[i].departureTime(), connections, 0, 0, 0, - (endTime - startTime) / NS_TO_MS_CONVERSION_FACTOR); + responses[i] = toResult(i, requests[i], connections, startTime, endTime); } catch (IllegalArgumentException e) { log.error("Could not process route request: {}", e.getMessage()); } @@ -109,8 +128,32 @@ private static RoutingResult[] processRequests(Raptor raptor, RouteRequest[] req return responses; } + private static RoutingResult toResult(int id, RouteRequest request, List connections, long startTime, + long endTime) { + Optional earliestDepartureTime = toLocalDatetime( + connections.stream().mapToInt(Connection::getDepartureTime).min().orElse(NOT_AVAILABLE)); + Optional earliestArrivalTime = toLocalDatetime( + connections.stream().mapToInt(Connection::getArrivalTime).min().orElse(NOT_AVAILABLE)); + int minDuration = connections.stream().mapToInt(Connection::getDuration).min().orElse(NOT_AVAILABLE); + int maxDuration = connections.stream().mapToInt(Connection::getDuration).max().orElse(NOT_AVAILABLE); + int minTransfers = connections.stream().mapToInt(Connection::getNumTransfers).min().orElse(NOT_AVAILABLE); + int maxTransfers = connections.stream().mapToInt(Connection::getNumTransfers).max().orElse(NOT_AVAILABLE); + long beelineDistance = Math.round( + request.sourceStop.getCoordinate().distanceTo(request.targetStop.getCoordinate())); + long processingTime = (endTime - startTime) / NS_TO_MS_CONVERSION_FACTOR; + return new RoutingResult(id, request.sourceStop().getId(), request.targetStop().getId(), + request.sourceStop().getName(), request.targetStop.getName(), + toLocalDatetime(request.departureTime).orElseThrow(), connections.size(), earliestDepartureTime, + earliestArrivalTime, minDuration, maxDuration, minTransfers, maxTransfers, beelineDistance, + processingTime); + } + private static void writeResultsToCsv(RoutingResult[] results) throws IOException { - String header = "source_stop,target_stop,requested_departure_time,connections,departure_time,arrival_time,transfers,processing_time_ms"; + String[] headers = {"id", "source_stop_id", "target_stop_id", "source_stop_name", "target_stop_name", + "requested_departure_time", "connections", "earliest_departure_time", "earliest_arrival_time", + "min_duration", "max_duration", "min_transfers", "max_transfers", "beeline_distance", + "processing_time_ms"}; + String header = String.join(",", headers); String folderPath = String.format("benchmark/output/%s", DATASET.name().toLowerCase()); String fileName = String.format("%s_raptor_results.csv", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd_HH_mm_ss"))); @@ -125,20 +168,36 @@ private static void writeResultsToCsv(RoutingResult[] results) throws IOExceptio writer.println(header); for (RoutingResult result : results) { - if( result == null ){ + if (result == null) { continue; } - writer.printf("%s,%s,%d,%d,%d,%d,%d,%d%n", result.sourceStop, result.targetStop, - result.requestedDepartureTime, result.connections.size(), result.departureTime, - result.arrivalTime, result.transfers, result.time); + writer.printf("%d,%s,%s,\"%s\",\"%s\",%s,%d,%s,%s,%d,%d,%d,%d,%d,%d%n", result.id, result.sourceStopId, + result.targetStopId, result.sourceStopName, result.targetStopName, + result.requestedDepartureTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), result.connections, + result.earliestDepartureTime.map(dt -> dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + .orElse("N/A"), + result.earliestArrivalTime.map(dt -> dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + .orElse("N/A"), result.minDuration, result.maxDuration, result.minTransfers, + result.maxTransfers, result.beelineDistance, result.processingTime); } } } - record RouteRequest(String sourceStop, String targetStop, int departureTime) { + private static Optional toLocalDatetime(int seconds) { + if (seconds == NOT_AVAILABLE) { + return Optional.empty(); + } + return Optional.of(new ServiceDayTime(seconds).toLocalDateTime(SCHEDULE_DATE)); } - record RoutingResult(String sourceStop, String targetStop, int requestedDepartureTime, List connections, - int departureTime, int arrivalTime, int transfers, long time) { + record RouteRequest(Stop sourceStop, Stop targetStop, int departureTime) { } + + record RoutingResult(int id, String sourceStopId, String targetStopId, String sourceStopName, String targetStopName, + LocalDateTime requestedDepartureTime, int connections, + Optional earliestDepartureTime, Optional earliestArrivalTime, + int minDuration, int maxDuration, int minTransfers, int maxTransfers, long beelineDistance, + long processingTime) { + } + } diff --git a/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java b/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java new file mode 100644 index 00000000..f35e5887 --- /dev/null +++ b/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java @@ -0,0 +1,110 @@ +package ch.naviqore.gtfs.schedule.type; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.*; + +class ServiceDayTimeTest { + + @Nested + class Constructor { + + @Test + void shouldCreateServiceDayTimeFromSeconds() { + ServiceDayTime sdt = new ServiceDayTime(3661); + assertEquals(3661, sdt.getTotalSeconds()); + } + + @Test + void shouldCreateServiceDayTimeFromHoursMinutesSeconds() { + ServiceDayTime sdt = new ServiceDayTime(1, 1, 1); + assertEquals(3661, sdt.getTotalSeconds()); + } + + @Test + void shouldThrowExceptionForNegativeSeconds() { + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(-1)); + } + + @Test + void shouldThrowExceptionForNegativeHoursMinutesSeconds() { + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(-1, 0, 0)); + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, -1, 0)); + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, 0, -1)); + } + } + + @Nested + class Parse { + + @Test + void shouldParseTimeStringCorrectly() { + ServiceDayTime sdt = ServiceDayTime.parse("01:01:01"); + assertEquals(3661, sdt.getTotalSeconds()); + } + } + + @Nested + class Comparison { + + @Test + void shouldCompareServiceDayTimesCorrectly() { + ServiceDayTime sdt1 = new ServiceDayTime(3600); + ServiceDayTime sdt2 = new ServiceDayTime(7200); + ServiceDayTime sdt3 = new ServiceDayTime(3600); + + assertTrue(sdt1.compareTo(sdt2) < 0); + assertTrue(sdt2.compareTo(sdt1) > 0); + assertEquals(0, sdt1.compareTo(sdt3)); + } + } + + @Nested + class Conversion { + + @Test + void shouldConvertToLocalTime() { + ServiceDayTime sdt = new ServiceDayTime(25, 30, 0); + LocalTime expectedTime = LocalTime.of(1, 30, 0); + assertEquals(expectedTime, sdt.toLocalTime()); + } + + @Test + void shouldConvertZeroSecondsToLocalTime() { + ServiceDayTime sdt = new ServiceDayTime(0); + LocalTime expectedTime = LocalTime.MIDNIGHT; + assertEquals(expectedTime, sdt.toLocalTime()); + } + + @Test + void shouldConvertToLocalDateTime() { + LocalDate baseDate = LocalDate.of(2024, 1, 1); + ServiceDayTime sdt = new ServiceDayTime(25, 30, 0); + LocalDateTime expectedDateTime = LocalDateTime.of(2024, 1, 2, 1, 30, 0); + assertEquals(expectedDateTime, sdt.toLocalDateTime(baseDate)); + } + + @Test + void shouldConvertZeroSecondsToLocalDateTime() { + LocalDate baseDate = LocalDate.of(2024, 1, 1); + ServiceDayTime sdt = new ServiceDayTime(0); + LocalDateTime expectedDateTime = LocalDateTime.of(baseDate, LocalTime.MIDNIGHT); + assertEquals(expectedDateTime, sdt.toLocalDateTime(baseDate)); + } + + @Test + void shouldConvertTotalSecondsInDayToLocalDateTime() { + LocalDate baseDate = LocalDate.of(2024, 1, 1); + ServiceDayTime sdt = new ServiceDayTime(ServiceDayTime.SECONDS_IN_DAY); + LocalDateTime expectedDateTime = LocalDateTime.of(2024, 1, 2, 0, 0, 0); + assertEquals(expectedDateTime, sdt.toLocalDateTime(baseDate)); + } + + } + +} From 46e22e3c6e19fe2a612a7e5fd0bebc9f25e351c4 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Thu, 23 May 2024 14:04:38 +0200 Subject: [PATCH 33/35] TEST: TEST-42 - Remove the now superfluous null check in benchmark --- src/test/java/ch/naviqore/Benchmark.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java index 4ab9fc0e..e24938d2 100644 --- a/src/test/java/ch/naviqore/Benchmark.java +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -168,9 +168,6 @@ private static void writeResultsToCsv(RoutingResult[] results) throws IOExceptio writer.println(header); for (RoutingResult result : results) { - if (result == null) { - continue; - } writer.printf("%d,%s,%s,\"%s\",\"%s\",%s,%d,%s,%s,%d,%d,%d,%d,%d,%d%n", result.id, result.sourceStopId, result.targetStopId, result.sourceStopName, result.targetStopName, result.requestedDepartureTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), result.connections, From ee9f55c29862362c340432328cac7874ebf5b577 Mon Sep 17 00:00:00 2001 From: Merlin Unterfinger Date: Thu, 23 May 2024 14:53:00 +0200 Subject: [PATCH 34/35] ENH: NAV-42 - Service day time input validation, route builder tests - Check for invalid minute (0,60) and seconds (0, 60) in service day time constructor. Update tests. - Rename non-existent trips in route builder test. --- .../naviqore/gtfs/schedule/type/ServiceDayTime.java | 12 +++++++++--- .../gtfs/schedule/type/ServiceDayTimeTest.java | 11 +++++++++++ .../ch/naviqore/raptor/model/RouteBuilderTest.java | 7 +++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java index fce57518..8ab4a277 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java @@ -24,14 +24,20 @@ public final class ServiceDayTime implements Comparable { public ServiceDayTime(int seconds) { if (seconds < 0) { - throw new IllegalArgumentException("Seconds cannot be negative"); + throw new IllegalArgumentException("Seconds cannot be negative."); } this.totalSeconds = seconds; } public ServiceDayTime(int hours, int minutes, int seconds) { - if (hours < 0 || minutes < 0 || seconds < 0) { - throw new IllegalArgumentException("Hours, minutes, and seconds cannot be negative"); + if (hours < 0) { + throw new IllegalArgumentException("Hours cannot be negative."); + } + if (minutes < 0 || minutes > 59) { + throw new IllegalArgumentException("Minutes must be between 0 and 59 inclusive"); + } + if (seconds < 0 || seconds > 59) { + throw new IllegalArgumentException("Seconds must be between 0 and 59 inclusive"); } this.totalSeconds = seconds + 60 * minutes + 3600 * hours; } diff --git a/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java b/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java index f35e5887..9414f069 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java @@ -37,6 +37,17 @@ void shouldThrowExceptionForNegativeHoursMinutesSeconds() { assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, -1, 0)); assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, 0, -1)); } + + @Test + void shouldThrowExceptionForInvalidMinutes() { + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, 60, 0)); + } + + @Test + void shouldThrowExceptionForInvalidSeconds() { + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, 0, 60)); + } + } @Nested diff --git a/src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java b/src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java index e4492640..d989c792 100644 --- a/src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java +++ b/src/test/java/ch/naviqore/raptor/model/RouteBuilderTest.java @@ -64,8 +64,8 @@ void shouldAddValidStopTimes() { void shouldNotAddStopTimeToNonExistentTrip() { StopTime stopTime = new StopTime(100, 200); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> builder.addStopTime("trip2", 0, STOP_1, stopTime)); - assertEquals("Trip trip2 does not exist.", exception.getMessage()); + () -> builder.addStopTime("nonexistentTrip", 0, STOP_1, stopTime)); + assertEquals("Trip nonexistentTrip does not exist.", exception.getMessage()); } @Test @@ -91,6 +91,9 @@ void shouldNotAddStopTimeForNonMatchingStop() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> builder.addStopTime(TRIP_1, 0, "nonexistentStop", stopTime)); assertEquals("Stop nonexistentStop does not match stop stop1 at position 0.", exception.getMessage()); + exception = assertThrows(IllegalArgumentException.class, + () -> builder.addStopTime(TRIP_1, 0, STOP_2, stopTime)); + assertEquals("Stop stop2 does not match stop stop1 at position 0.", exception.getMessage()); } @Test From a7931774bd3c39474ed92b1264952790c13a9abf Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Thu, 23 May 2024 20:48:12 +0200 Subject: [PATCH 35/35] REFACTOR: NAV-17 - Replace numbers in code with constants. --- .../gtfs/schedule/type/ServiceDayTime.java | 31 +++++++++++-------- .../java/ch/naviqore/raptor/model/Raptor.java | 7 +++-- .../schedule/type/ServiceDayTimeTest.java | 10 +++--- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java index 8ab4a277..e477bdef 100644 --- a/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java +++ b/src/main/java/ch/naviqore/gtfs/schedule/type/ServiceDayTime.java @@ -19,7 +19,12 @@ @Getter public final class ServiceDayTime implements Comparable { - public static final int SECONDS_IN_DAY = 86400; + public static final int HOURS_IN_DAY = 24; + public static final int MINUTES_IN_HOUR = 60; + public static final int SECONDS_IN_MINUTE = 60; + public static final int SECONDS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE; + public static final int SECONDS_IN_DAY = HOURS_IN_DAY * SECONDS_IN_HOUR; + private final int totalSeconds; public ServiceDayTime(int seconds) { @@ -33,13 +38,13 @@ public ServiceDayTime(int hours, int minutes, int seconds) { if (hours < 0) { throw new IllegalArgumentException("Hours cannot be negative."); } - if (minutes < 0 || minutes > 59) { + if (minutes < 0 || minutes >= MINUTES_IN_HOUR) { throw new IllegalArgumentException("Minutes must be between 0 and 59 inclusive"); } - if (seconds < 0 || seconds > 59) { + if (seconds < 0 || seconds >= SECONDS_IN_MINUTE) { throw new IllegalArgumentException("Seconds must be between 0 and 59 inclusive"); } - this.totalSeconds = seconds + 60 * minutes + 3600 * hours; + this.totalSeconds = seconds + SECONDS_IN_MINUTE * minutes + SECONDS_IN_HOUR * hours; } public static ServiceDayTime parse(String timeString) { @@ -51,16 +56,16 @@ public static ServiceDayTime parse(String timeString) { } public LocalTime toLocalTime() { - int hours = totalSeconds / 3600; - int minutes = (totalSeconds % 3600) / 60; - int seconds = totalSeconds % 60; - return LocalTime.of(hours % 24, minutes, seconds); + int hours = totalSeconds / SECONDS_IN_HOUR; + int minutes = (totalSeconds % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE; + int seconds = totalSeconds % SECONDS_IN_MINUTE; + return LocalTime.of(hours % HOURS_IN_DAY, minutes, seconds); } public LocalDateTime toLocalDateTime(LocalDate date) { LocalTime localTime = this.toLocalTime(); - int hours = totalSeconds / 3600; - LocalDate adjustedDate = date.plusDays(hours / 24); + int hours = totalSeconds / SECONDS_IN_HOUR; + LocalDate adjustedDate = date.plusDays(hours / HOURS_IN_DAY); return LocalDateTime.of(adjustedDate, localTime); } @@ -71,9 +76,9 @@ public int compareTo(ServiceDayTime o) { @Override public String toString() { - int hours = totalSeconds / 3600; - int minutes = (totalSeconds % 3600) / 60; - int seconds = totalSeconds % 60; + int hours = totalSeconds / SECONDS_IN_HOUR; + int minutes = (totalSeconds % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE; + int seconds = totalSeconds % SECONDS_IN_MINUTE; return String.format("%02d:%02d:%02d", hours, minutes, seconds); } diff --git a/src/main/java/ch/naviqore/raptor/model/Raptor.java b/src/main/java/ch/naviqore/raptor/model/Raptor.java index f3af51dc..eedecadd 100644 --- a/src/main/java/ch/naviqore/raptor/model/Raptor.java +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -11,6 +11,7 @@ @Log4j2 public class Raptor { + public final static int INFINITY = Integer.MAX_VALUE; public final static int NO_INDEX = -1; public final static int SAME_STOP_TRANSFER_TIME = 120; private final InputValidator validator = new InputValidator(); @@ -64,7 +65,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int departureTime) { private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, int departureTime) { // initialization final int[] earliestArrivals = new int[stops.length]; - Arrays.fill(earliestArrivals, Integer.MAX_VALUE); + Arrays.fill(earliestArrivals, INFINITY); // subtract same stop transfer time, as this will be added by default before scanning routes earliestArrivals[sourceStopIdx] = departureTime - SAME_STOP_TRANSFER_TIME; @@ -125,7 +126,7 @@ private List spawnFromSourceStop(int sourceStopIdx, int targetStopIdx, in // find first marked stop in route if (!enteredTrip) { - if (earliestArrivalTime == Integer.MAX_VALUE) { + if (earliestArrivalTime == INFINITY) { // when current arrival is infinity (Integer.MAX_VALUE), then the stop cannot be reached log.debug("Stop {} cannot be reached, continue", stop.id()); continue; @@ -320,7 +321,7 @@ private record Leg(int departureTime, int arrivalTime, ArrivalType type, int rou */ private class InputValidator { private static final int MIN_DEPARTURE_TIME = 0; - private static final int MAX_DEPARTURE_TIME = 48 * 60 * 60; + private static final int MAX_DEPARTURE_TIME = 48 * 60 * 60; // 48 hours private static void validateStopIds(String sourceStopId, String targetStopId) { if (sourceStopId.equals(targetStopId)) { diff --git a/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java b/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java index 9414f069..bca5bee5 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/type/ServiceDayTimeTest.java @@ -40,12 +40,12 @@ void shouldThrowExceptionForNegativeHoursMinutesSeconds() { @Test void shouldThrowExceptionForInvalidMinutes() { - assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, 60, 0)); + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, ServiceDayTime.MINUTES_IN_HOUR, 0)); } @Test void shouldThrowExceptionForInvalidSeconds() { - assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, 0, 60)); + assertThrows(IllegalArgumentException.class, () -> new ServiceDayTime(0, 0, ServiceDayTime.SECONDS_IN_MINUTE)); } } @@ -65,9 +65,9 @@ class Comparison { @Test void shouldCompareServiceDayTimesCorrectly() { - ServiceDayTime sdt1 = new ServiceDayTime(3600); - ServiceDayTime sdt2 = new ServiceDayTime(7200); - ServiceDayTime sdt3 = new ServiceDayTime(3600); + ServiceDayTime sdt1 = new ServiceDayTime(ServiceDayTime.SECONDS_IN_HOUR); + ServiceDayTime sdt2 = new ServiceDayTime(2 * ServiceDayTime.SECONDS_IN_HOUR); + ServiceDayTime sdt3 = new ServiceDayTime(ServiceDayTime.SECONDS_IN_HOUR); assertTrue(sdt1.compareTo(sdt2) < 0); assertTrue(sdt2.compareTo(sdt1) > 0);