diff --git a/.gitignore b/.gitignore index c2ed7152..02610048 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,4 @@ build/ .DS_Store # Benchmark test files -benchmark/input/*.zip \ No newline at end of file +benchmark/ \ No newline at end of file diff --git a/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java new file mode 100644 index 00000000..3ecbdc13 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java @@ -0,0 +1,53 @@ +package ch.naviqore.raptor; + +import ch.naviqore.gtfs.schedule.model.*; +import ch.naviqore.raptor.model.Raptor; +import ch.naviqore.raptor.model.RaptorBuilder; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Maps GTFS schedule to Raptor + * + * @author munterfi + */ +@RequiredArgsConstructor +@Log4j2 +public class GtfsToRaptorConverter { + + private final Set stops = new HashSet<>(); + private final Set routes = new HashSet<>(); + private final RaptorBuilder builder; + + public Raptor convert(GtfsSchedule schedule, LocalDate date) { + List activeTrips = schedule.getActiveTrips(date); + log.info("Converting {} active trips from GTFS schedule to Raptor model", activeTrips.size()); + for (Trip trip : activeTrips) { + Route route = trip.getRoute(); + if (!routes.contains(route)) { + routes.add(route); + builder.addRoute(route.getId()); + // TODO: Add test for consistency of route stops. Since in GTFS are defined per trip, but Raptor + // builder expects them to be the same for all trips of a route. + for (StopTime stopTime : trip.getStopTimes()) { + if (!stops.contains(stopTime.stop())) { + stops.add(stopTime.stop()); + builder.addStop(stopTime.stop().getId()); + } + builder.addRouteStop(stopTime.stop().getId(), route.getId()); + } + } + for (StopTime stopTime : trip.getStopTimes()) { + builder.addStopTime(stopTime.stop().getId(), route.getId(), stopTime.arrival().getTotalSeconds(), + stopTime.departure().getTotalSeconds()); + } + } + + return builder.build(); + } +} diff --git a/src/main/java/ch/naviqore/raptor/model/Lookup.java b/src/main/java/ch/naviqore/raptor/model/Lookup.java new file mode 100644 index 00000000..0c4483a9 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/Lookup.java @@ -0,0 +1,6 @@ +package ch.naviqore.raptor.model; + +import java.util.Map; + +public 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 new file mode 100644 index 00000000..a293b343 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/Raptor.java @@ -0,0 +1,144 @@ +package ch.naviqore.raptor.model; + +import lombok.extern.log4j.Log4j2; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Raptor algorithm implementation + * + * @author munterfi + */ +@Log4j2 +public class Raptor { + + public final static int NO_INDEX = -1; + // lookup + private final Map stopsToIdx; + private final Map routesToIdx; + // stop context + private final Transfer[] transfers; + private final Stop[] stops; + private final int[] stopRoutes; + // route traversal + private final StopTime[] stopTimes; + private final Route[] routes; + private final RouteStop[] routeStops; + + Raptor(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal) { + this.stopsToIdx = lookup.stops(); + this.routesToIdx = lookup.routes(); + this.transfers = stopContext.transfers(); + this.stops = stopContext.stops(); + this.stopRoutes = stopContext.stopRoutes(); + this.stopTimes = routeTraversal.stopTimes(); + this.routes = routeTraversal.routes(); + this.routeStops = routeTraversal.routeStops(); + } + + 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); + + 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; + } + 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; + } + 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); + } + currentRouteStopIdx++; + } + } + } + + // 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); + } + } + } + + // prepare for next round + marked = nextMarked; + round++; + } + + // print results for debugging + for (int i = 0; i < earliestArrival.length; i++) { + if (earliestArrival[i] == Integer.MAX_VALUE) { + earliestArrival[i] = -1; + } + } + for (Stop stop : stops) { + System.out.println(stop.id() + ": " + earliestArrival[stopsToIdx.get(stop.id())]); + } + + log.debug("Earliest arrival at {}: {}", targetStop, earliestArrival[stopsToIdx.get(targetStop)]); + } + +} diff --git a/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java new file mode 100644 index 00000000..86c3f40f --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java @@ -0,0 +1,181 @@ +package ch.naviqore.raptor.model; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; + +import java.util.*; + +/** + * 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 + */ +@NoArgsConstructor(access = AccessLevel.PACKAGE) +@Log4j2 +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> transfers = new HashMap<>(); + + private int stopSize = 0; + private int routeSize = 0; + private int routeStopSize = 0; + private int stopTimeSize = 0; + private 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; + } + + 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"); + } + routeStops.computeIfAbsent(routeId, k -> new ArrayList<>()).add(stopId); + stopRoutes.computeIfAbsent(stopId, k -> new HashSet<>()).add(routeId); + routeStopSize++; + 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"); + } + if (!routes.containsKey(routeId)) { + throw new IllegalArgumentException("Route " + routeId + " does not exist"); + } + stopTimes.computeIfAbsent(routeId, k -> new ArrayList<>()).add(new StopTime(arrival, departure)); + 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); + return new Raptor(lookup, stopContext, routeTraversal); + } + + private Lookup buildLookup() { + log.info("Building lookup with {} stops and {} routes", stopSize, routeSize); + return new Lookup(new HashMap<>(stops), new HashMap<>(routes)); + } + + private StopContext buildStopContext() { + log.info("Building stop context with {} stops and {} transfers", stopSize, transferSize); + Stop[] stopArr = new Stop[stopSize]; + int[] stopRouteArr = new int[stopRoutes.values().stream().mapToInt(Set::size).sum()]; + Transfer[] transferArr = new Transfer[transferSize]; + + int transferCnt = 0; + int stopRouteCnt = 0; + for (Map.Entry entry : stops.entrySet()) { + String stopId = entry.getKey(); + int stopIdx = entry.getValue(); + + List currentTransfers = transfers.get(stopId); + int currentTransferCnt = 0; + if (currentTransfers != null) { + for (Transfer transfer : currentTransfers) { + transferArr[transferCnt++] = transfer; + currentTransferCnt++; + } + } + + Set currentStopRoutes = stopRoutes.get(stopId); + if (currentStopRoutes == null) { + throw new IllegalStateException("Stop " + stopId + " has no routes"); + } + for (String routeId : currentStopRoutes) { + stopRouteArr[stopRouteCnt++] = 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.info("Building route traversal with {} routes, {} route stops, {} stop times", routeSize, routeStopSize, + stopTimeSize); + Route[] routeArr = new Route[routeSize]; + RouteStop[] routeStopArr = new RouteStop[routeStopSize]; + StopTime[] stopTimeArr = new StopTime[stopTimeSize]; + + 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); + } + + List currentStopTimes = stopTimes.get(routeId); + if (currentStopTimes == null) { + throw new IllegalStateException("Route " + routeId + " has no stop times"); + } + for (StopTime stopTime : currentStopTimes) { + stopTimeArr[stopTimeCnt++] = stopTime; + } + + routeArr[routeIdx] = new Route(routeId, routeStopCnt - currentRouteStops.size(), currentRouteStops.size(), + stopTimeCnt - currentStopTimes.size(), currentStopTimes.size()); + } + return new RouteTraversal(stopTimeArr, routeArr, routeStopArr); + } +} diff --git a/src/main/java/ch/naviqore/raptor/model/Route.java b/src/main/java/ch/naviqore/raptor/model/Route.java index 578936ea..a7d526cc 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(int firstRouteStopIdx, int numberOfTrips, int numberOfStops) { +public 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 174cc355..1c7fc9fd 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() { -} +public 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 78471484..adfcb9ca 100644 --- a/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java +++ b/src/main/java/ch/naviqore/raptor/model/RouteTraversal.java @@ -1,10 +1,12 @@ package ch.naviqore.raptor.model; -import lombok.RequiredArgsConstructor; +/** + * Memory optimized itinerant data structure for efficient route traversal + * + * @param stopTimes stop times + * @param routes routes + * @param routeStops route stops + */ +public record RouteTraversal(StopTime[] stopTimes, Route[] routes, RouteStop[] routeStops) { -@RequiredArgsConstructor -public class RouteTraversal { - private final StopTime[] stopTimes; - private final Route[] routes; - // private final Stop[] routeStops; } diff --git a/src/main/java/ch/naviqore/raptor/model/RouteTraversalBuilder.java b/src/main/java/ch/naviqore/raptor/model/RouteTraversalBuilder.java deleted file mode 100644 index a9442964..00000000 --- a/src/main/java/ch/naviqore/raptor/model/RouteTraversalBuilder.java +++ /dev/null @@ -1,38 +0,0 @@ -package ch.naviqore.raptor.model; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -import java.util.HashMap; -import java.util.Map; - -@NoArgsConstructor(access = AccessLevel.PACKAGE) -public class RouteTraversalBuilder { - - private final Map routes = new HashMap<>(); - private final Map stops = new HashMap<>(); - - public static RouteTraversalBuilder builder() { - return new RouteTraversalBuilder(); - } - - public RouteTraversalBuilder addRoute(String id) { - if (routes.containsKey(id)) { - throw new IllegalArgumentException("Route " + id + " already exists"); - } - routes.put(id, routes.size()); - return this; - } - - public RouteTraversalBuilder addStopTime() { - return this; - } - - public RouteTraversalBuilder addRouteStop(String id, String routeId) { - if (routes.containsKey(id)) { - throw new IllegalArgumentException("Route " + id + " already exists"); - } - return this; - } - -} diff --git a/src/main/java/ch/naviqore/raptor/model/Stop.java b/src/main/java/ch/naviqore/raptor/model/Stop.java new file mode 100644 index 00000000..52c9a63e --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/Stop.java @@ -0,0 +1,4 @@ +package ch.naviqore.raptor.model; + +public 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 new file mode 100644 index 00000000..db25e9e5 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/StopContext.java @@ -0,0 +1,4 @@ +package ch.naviqore.raptor.model; + +public record StopContext(Transfer[] transfers, Stop[] stops, int[] stopRoutes) { +} diff --git a/src/main/java/ch/naviqore/raptor/model/Transfer.java b/src/main/java/ch/naviqore/raptor/model/Transfer.java new file mode 100644 index 00000000..64294cf2 --- /dev/null +++ b/src/main/java/ch/naviqore/raptor/model/Transfer.java @@ -0,0 +1,4 @@ +package ch.naviqore.raptor.model; + +public record Transfer(int targetStopIdx, int duration) { +} diff --git a/src/test/java/ch/naviqore/Benchmark.java b/src/test/java/ch/naviqore/Benchmark.java new file mode 100644 index 00000000..585e3ed9 --- /dev/null +++ b/src/test/java/ch/naviqore/Benchmark.java @@ -0,0 +1,130 @@ +package ch.naviqore; + +import ch.naviqore.BenchmarkData.Dataset; +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 lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +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; + +/** + * Benchmark for Raptor routing algorithm. + *

+ * Measures the time it takes to route a number of requests using Raptor algorithm on large GTFS datasets. + * + * @author munterfi + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +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 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 { + GtfsSchedule schedule = initializeSchedule(); + Raptor raptor = initializeRaptor(schedule); + List stopIds = new ArrayList<>(schedule.getStops().keySet()); + RouteRequest[] requests = sampleRouteRequests(stopIds); + RoutingResult[] results = processRequests(raptor, requests); + writeResultsToCsv(results); + + } + + private static GtfsSchedule initializeSchedule() throws IOException, InterruptedException { + String path = BenchmarkData.get(DATASET); + GtfsSchedule schedule = new GtfsScheduleReader().read(path); + manageResources(); + return schedule; + } + + private static Raptor initializeRaptor(GtfsSchedule schedule) throws InterruptedException { + Raptor raptor = new GtfsToRaptorConverter(Raptor.builder()).convert(schedule, DATE); + manageResources(); + return raptor; + } + + private static void manageResources() throws InterruptedException { + System.gc(); + Thread.sleep(MONITORING_INTERVAL_MS); + } + + private static RouteRequest[] sampleRouteRequests(List stopIds) { + Random random = new Random(); + RouteRequest[] requests = new RouteRequest[Benchmark.N]; + for (int i = 0; i < Benchmark.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), + random.nextInt(MAX_SECONDS_IN_DAY)); + } + return requests; + } + + private static int getRandomDestinationIndex(int size, int exclude, Random random) { + int index = random.nextInt(size - 1); + if (index >= exclude) index++; + return index; + } + + private static RoutingResult[] processRequests(Raptor raptor, RouteRequest[] requests) { + 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()); + 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); + } + 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 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"))); + Path directoryPath = Paths.get(folderPath); + if (!Files.exists(directoryPath)) { + Files.createDirectories(directoryPath); + } + Path filePath = directoryPath.resolve(fileName); + + try (PrintWriter writer = new PrintWriter( + Files.newBufferedWriter(filePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE))) { + 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()); + } + } + } + + record RouteRequest(String sourceStop, String targetStop, int departureTime) { + } + + record RoutingResult(String sourceStop, String targetStop, int requestedDepartureTime, int departureTime, + int arrivalTime, int transfers, long time) { + } +} diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmarkData.java b/src/test/java/ch/naviqore/BenchmarkData.java similarity index 96% rename from src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmarkData.java rename to src/test/java/ch/naviqore/BenchmarkData.java index f214b016..acaada2e 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmarkData.java +++ b/src/test/java/ch/naviqore/BenchmarkData.java @@ -1,4 +1,4 @@ -package ch.naviqore.gtfs.schedule; +package ch.naviqore; import lombok.AccessLevel; import lombok.Getter; @@ -21,7 +21,7 @@ */ @NoArgsConstructor(access = AccessLevel.PRIVATE) @Log4j2 -final class GtfsScheduleBenchmarkData { +final class BenchmarkData { private static final Path DATA_DIRECTORY = Path.of("benchmark/input"); private static final HttpClient httpClient = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.ALWAYS) @@ -66,7 +66,7 @@ public static String get(Dataset dataset) throws IOException, InterruptedExcepti } /** - * GTFS Schedule Datasets + * GTFS schedule datasets */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Getter diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmark.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmark.java deleted file mode 100644 index 932cc355..00000000 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleBenchmark.java +++ /dev/null @@ -1,27 +0,0 @@ -package ch.naviqore.gtfs.schedule; - -import ch.naviqore.gtfs.schedule.GtfsScheduleBenchmarkData.Dataset; -import ch.naviqore.gtfs.schedule.model.GtfsSchedule; -import ch.naviqore.gtfs.schedule.model.Trip; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -import java.io.IOException; -import java.time.LocalDate; -import java.util.List; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -final class GtfsScheduleBenchmark { - - private static final Dataset DATASET = Dataset.SWITZERLAND; - - public static void main(String[] args) throws IOException, InterruptedException { - String path = GtfsScheduleBenchmarkData.get(DATASET); - GtfsSchedule schedule = new GtfsScheduleReader().read(path); - List activeTrips = schedule.getActiveTrips(LocalDate.now()); - // clean heap from reading artifacts - System.gc(); - // monitor effect - Thread.sleep(30000); - } -} diff --git a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java index 23e5819e..5d26b32c 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/GtfsScheduleTestData.java @@ -12,7 +12,7 @@ * * @author munterfi */ -final class GtfsScheduleTestData { +public final class GtfsScheduleTestData { public static final String SAMPLE_FEED = "sample-feed-1"; public static final String SAMPLE_FEED_ZIP = SAMPLE_FEED + ".zip"; diff --git a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java index 87056923..3791dcf0 100644 --- a/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java +++ b/src/test/java/ch/naviqore/gtfs/schedule/model/GtfsScheduleTest.java @@ -38,7 +38,7 @@ void setUp() { .addRoute("route2", "agency1", "102", "Cross Town", RouteType.BUS) .addRoute("route3", "agency1", "103", "Circulator", RouteType.BUS) .addCalendar("weekdays", EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), START_DATE, END_DATE) - .addCalendar("weekends", EnumSet.of(DayOfWeek.SATURDAY), LocalDate.now(), LocalDate.now().plusMonths(1)) + .addCalendar("weekends", EnumSet.of(DayOfWeek.SATURDAY), START_DATE, END_DATE) .addTrip("trip1", "route1", "weekdays") .addTrip("trip2", "route2", "weekdays") .addTrip("trip3", "route3", "weekends") diff --git a/src/test/java/ch/naviqore/raptor/GtfsToRaptorConverterIT.java b/src/test/java/ch/naviqore/raptor/GtfsToRaptorConverterIT.java new file mode 100644 index 00000000..a3e803bd --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/GtfsToRaptorConverterIT.java @@ -0,0 +1,32 @@ +package ch.naviqore.raptor; + +import ch.naviqore.gtfs.schedule.GtfsScheduleReader; +import ch.naviqore.gtfs.schedule.GtfsScheduleTestData; +import ch.naviqore.gtfs.schedule.model.GtfsSchedule; +import ch.naviqore.raptor.model.Raptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.time.LocalDate; + +class GtfsToRaptorConverterIT { + + private static final LocalDate DATE = LocalDate.of(2009, 4, 26); + private GtfsSchedule schedule; + + @BeforeEach + void setUp(@TempDir Path tempDir) throws IOException { + File zipFile = GtfsScheduleTestData.prepareZipDataset(tempDir); + schedule = new GtfsScheduleReader().read(zipFile.getAbsolutePath()); + } + + @Test + void shouldConvertGtfsScheduleToRaptor() { + GtfsToRaptorConverter mapper = new GtfsToRaptorConverter(Raptor.builder()); + Raptor raptor = mapper.convert(schedule, DATE); + } +} \ No newline at end of file diff --git a/src/test/java/ch/naviqore/raptor/model/RaptorTest.java b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java new file mode 100644 index 00000000..ee4c989d --- /dev/null +++ b/src/test/java/ch/naviqore/raptor/model/RaptorTest.java @@ -0,0 +1,108 @@ +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.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 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 + */ +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; + + @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) { + 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; + } + } + }); + TRANSFERS.forEach(transfer -> { + builder.addTransfer(transfer.sourceStop, transfer.targetStop, transfer.duration); + builder.addTransfer(transfer.targetStop, transfer.sourceStop, transfer.duration); + }); + raptor = builder.build(); + } + + record Route(String id, List stops, int headway, int offset) { + } + + record Transfer(String sourceStop, String targetStop, int duration) { + } + + @Nested + class EarliestArrival { + @Test + void testRoutingBetweenIntersectingRoutes() { + raptor.routeEarliestArrival("A", "Q", 8 * 60 * 60); + + // TODO: assertThat... + } + } + +} diff --git a/src/test/resources/log4j2-test.properties b/src/test/resources/log4j2-test.properties index a2809ff8..435c93e9 100644 --- a/src/test/resources/log4j2-test.properties +++ b/src/test/resources/log4j2-test.properties @@ -1,4 +1,4 @@ -rootLogger.level=INFO +rootLogger.level=DEBUG rootLogger.appenderRef.stdout.ref=STDOUT appender.stdout.type=Console appender.stdout.name=STDOUT