-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from naviqore/feature/NAV-15-raptor-data-structure
Feature/nav 15 raptor data structure
- Loading branch information
Showing
20 changed files
with
684 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,4 +38,4 @@ build/ | |
.DS_Store | ||
|
||
# Benchmark test files | ||
benchmark/input/*.zip | ||
benchmark/ |
53 changes: 53 additions & 0 deletions
53
src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Stop> stops = new HashSet<>(); | ||
private final Set<Route> routes = new HashSet<>(); | ||
private final RaptorBuilder builder; | ||
|
||
public Raptor convert(GtfsSchedule schedule, LocalDate date) { | ||
List<Trip> 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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package ch.naviqore.raptor.model; | ||
|
||
import java.util.Map; | ||
|
||
public record Lookup(Map<String, Integer> stops, Map<String, Integer> routes) { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Integer> stopsToIdx; | ||
private final Map<String, Integer> 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<Integer> 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<Integer> 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)]); | ||
} | ||
|
||
} |
181 changes: 181 additions & 0 deletions
181
src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
* <p> | ||
* 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<String, Integer> stops = new HashMap<>(); | ||
private final Map<String, Integer> routes = new HashMap<>(); | ||
private final Map<String, List<String>> routeStops = new HashMap<>(); | ||
private final Map<String, List<StopTime>> stopTimes = new HashMap<>(); | ||
private final Map<String, Set<String>> stopRoutes = new HashMap<>(); | ||
private final Map<String, List<Transfer>> 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<String, Integer> entry : stops.entrySet()) { | ||
String stopId = entry.getKey(); | ||
int stopIdx = entry.getValue(); | ||
|
||
List<Transfer> currentTransfers = transfers.get(stopId); | ||
int currentTransferCnt = 0; | ||
if (currentTransfers != null) { | ||
for (Transfer transfer : currentTransfers) { | ||
transferArr[transferCnt++] = transfer; | ||
currentTransferCnt++; | ||
} | ||
} | ||
|
||
Set<String> 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<String, Integer> entry : routes.entrySet()) { | ||
String routeId = entry.getKey(); | ||
int routeIdx = entry.getValue(); | ||
|
||
List<String> 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<StopTime> 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); | ||
} | ||
} |
Oops, something went wrong.