
Round-based public-transit optimized router offering ultra-fast forward/arrive-by searches, multi-period schedules, address/coordinate endpoints with integrated walking, pedestrian-router support, and route filtering.
RAPTOR (Round-Based Public Transit Optimized Router) implementation in Kotlin Multiplatform (Android + iOS).
Add to your build.gradle.kts:
dependencies {
implementation("eu.dotshell:raptor-kmp:2.1.0")
}// Place your stops.bin and routes.bin files in assets folder
val raptor = RaptorLibrary(
stopsBytes = assets.open("stops.bin").readBytes(),
routesBytes = assets.open("routes.bin").readBytes()
)
// Search for stops
val originStops = raptor.searchStopsByName("Perrache")
val destStops = raptor.searchStopsByName("Cuire")
// Get optimized paths
val departureTime = 8 * 3600 // 08:00:00 in seconds
val journeys = raptor.getOptimizedPaths(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
departureTime = departureTime
)
// Display results
for (journey in journeys) {
raptor.displayJourney(journey)
}If you have multiple sets of transit data for different time periods (e.g., winter/summer schedules), you can load them all at once:
// Load multiple periods
val raptor = RaptorLibrary(listOf(
PeriodData(
periodId = "winter",
stopsBytes = assets.open("stops_winter.bin").readBytes(),
routesBytes = assets.open("routes_winter.bin").readBytes()
),
PeriodData(
periodId = "summer",
stopsBytes = assets.open("stops_summer.bin").readBytes(),
routesBytes = assets.open("routes_summer.bin").readBytes()
)
))
// Check available periods
val periods = raptor.getAvailablePeriods() // Returns: ["winter", "summer"]
// Switch to a specific period
raptor.setPeriod("summer")
// All subsequent queries will use the summer schedule
val journeys = raptor.getOptimizedPaths(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
departureTime = departureTime
)
// Get current active period
val currentPeriod = raptor.getCurrentPeriod() // Returns: "summer"You can also search for routes that arrive before a specific time (useful for "I need to be there by 9am" scenarios):
// Find the best routes to arrive by 09:00
val arrivalTime = 9 * 3600 // 09:00:00 in seconds
val journeys = raptor.getOptimizedPathsArriveBy(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
arrivalTime = arrivalTime,
searchWindowMinutes = 120 // Search departures up to 2 hours before arrival time
)
// The returned journeys will arrive at or before 09:00
// with the latest possible departure time
for (journey in journeys) {
raptor.displayJourney(journey)
}Journeys can start or end at arbitrary WGS84 coordinates (e.g. a geocoded address or POI) instead of stops. The router walks to/from nearby stops, and walking competes inside the optimization itself: a journey ending with a longer walk can beat one waiting for a later connection. A pure-walk journey is proposed when both points are within walking range.
// From home (geocoded address) to a point of interest, departing at 08:00
val journeys = raptor.getOptimizedPaths(
origin = Location.Point(lat = 45.7578, lon = 4.8320),
destination = Location.Point(lat = 45.7605, lon = 4.8590),
departureTime = 8 * 3600
)
// Mixed endpoints work too (stop ids on one side, coordinates on the other),
// and the arrive-by variant accepts the same Location endpoints:
val arriveBy = raptor.getOptimizedPathsArriveBy(
origin = Location.StopIds(originStops.map { it.id }),
destination = Location.Point(lat = 45.7605, lon = 4.8590),
arrivalTime = 9 * 3600
)
// Walking model is configurable per query
val custom = raptor.getOptimizedPaths(
origin = Location.Point(45.7578, 4.8320),
destination = Location.Point(45.7605, 4.8590),
departureTime = 8 * 3600,
walking = WalkingParams(
speedMetersPerSecond = 4.8 / 3.6, // average walking speed
detourFactor = 1.3, // street-network detour vs straight line
maxAccessEgressDistanceMeters = 500.0, // stop search radius around each point
maxDirectWalkDistanceMeters = 1000.0 // max distance for a pure-walk journey
)
)Walk legs are regular JourneyLegs with isTransfer = true, a legType of WALK_ACCESS,
WALK_EGRESS or WALK_DIRECT, and the coordinates of both ends (fromLat/fromLon/
toLat/toLon). A coordinate endpoint uses stop index -1 — resolve names only for
indices >= 0. Location.StopIds on both sides behaves exactly like the classic id-based
methods.
When the app has access to a real pedestrian router (e.g. OSRM foot), it can supply exact walk times instead of the great-circle estimate:
val journeys = raptor.getOptimizedPaths(
origin = Location.ResolvedPoint(
lat = 45.7578, lon = 4.8320,
stops = listOf(StopWalk(stopId = 1234, walkSeconds = 240), StopWalk(1237, 310))
),
destination = Location.Point(45.7605, 4.8590),
departureTime = 8 * 3600,
directWalkSecondsOverride = null // or the router's origin->destination walk duration
)You can restrict which lines are eligible during routing using route names or ids. This is useful to keep a journey within the same fare system or to exclude specific lines.
// Allow only specific lines by name
val journeys = raptor.getOptimizedPaths(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
departureTime = departureTime,
allowedRouteNames = setOf("JD2", "JD3", "RX")
)
// Exclude specific lines by id
val journeysArriveBy = raptor.getOptimizedPathsArriveBy(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
arrivalTime = arrivalTime,
blockedRouteIds = setOf(12, 27)
)
// Works with searchAndDisplayRoute too
raptor.searchAndDisplayRoute(
originName = "Perrache",
destinationName = "Cuire",
departureTime = departureTime,
allowedRouteNames = setOf("JD2", "JD3", "RX")
)Measured with JMH (2 separate JVM forks, 5 warmup + 10 measurement iterations of 1 s each) on an Intel Core i7-11700F, 32 GB DDR4 2666 MHz, Windows 11, JDK 17. Times are average per query. Origins and destinations are resolved by stop name (multi-stop sets); forward departs at 08:00, arrive-by targets 09:00 with the default 120 min search window.
| Route | Forward | Arrive-By |
|---|---|---|
| Perrache → Vaulx-en-Velin La Soie | 0.19 ms | 0.36 ms |
| Bellecour → Part-Dieu | 0.18 ms | 0.28 ms |
| Gare de Vaise → Oullins Centre | 0.38 ms | 0.69 ms |
| Perrache → Cuire | 0.38 ms | 0.56 ms |
| Laurent Bonnevay → Gorge de Loup | 0.37 ms | 0.75 ms |
| Part-Dieu → Bellecour | 0.17 ms | 0.19 ms |
Aggregate over 1 000 random O-D pairs (JMH, same config): forward 0.35 ms, arrive-by 0.40 ms per query — arrive-by now costs barely more than a forward search thanks to the single backward RAPTOR pass introduced in v1.7.0.
| Route | Forward | Arrive-By |
|---|---|---|
| Vieux-Port → La Rose | 0.12 ms | 0.25 ms |
| Castellane → Bougainville | 0.08 ms | 0.11 ms |
| La Timone → Joliette | 0.10 ms | 0.19 ms |
| La Rose → Castellane | 0.10 ms | 0.22 ms |
| Noailles → Sainte-Marguerite Dromel | 0.06 ms | 0.13 ms |
| Bougainville → La Fourragère | 0.15 ms | 0.25 ms |
| Route | Forward | Arrive-By |
|---|---|---|
| Gare de Lyon → Gare du Nord | 1.54 ms | 2.76 ms |
| Gare Saint-Lazare → Montparnasse Bienvenue | 2.08 ms | 4.63 ms |
| Charles de Gaulle - Étoile → Nation | 0.71 ms | 0.87 ms |
| République → Bastille | 0.89 ms | 0.92 ms |
| Gare du Nord → Gare Montparnasse | 5.71 ms | 8.09 ms |
| Bastille → Gare Saint-Lazare | 2.07 ms | 3.55 ms |
| Glacière → Bonne Nouvelle | 6.16 ms | 7.22 ms |
Since v1.7.0, arrive-by runs a single backward RAPTOR pass instead of a departure-time binary search: on these Paris queries it is 4–10× faster than v1.1.0, and costs barely more than a forward search (~1.5× on average, versus ~7× before).
RAPTOR (Round-Based Public Transit Optimized Router) implementation in Kotlin Multiplatform (Android + iOS).
Add to your build.gradle.kts:
dependencies {
implementation("eu.dotshell:raptor-kmp:2.1.0")
}// Place your stops.bin and routes.bin files in assets folder
val raptor = RaptorLibrary(
stopsBytes = assets.open("stops.bin").readBytes(),
routesBytes = assets.open("routes.bin").readBytes()
)
// Search for stops
val originStops = raptor.searchStopsByName("Perrache")
val destStops = raptor.searchStopsByName("Cuire")
// Get optimized paths
val departureTime = 8 * 3600 // 08:00:00 in seconds
val journeys = raptor.getOptimizedPaths(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
departureTime = departureTime
)
// Display results
for (journey in journeys) {
raptor.displayJourney(journey)
}If you have multiple sets of transit data for different time periods (e.g., winter/summer schedules), you can load them all at once:
// Load multiple periods
val raptor = RaptorLibrary(listOf(
PeriodData(
periodId = "winter",
stopsBytes = assets.open("stops_winter.bin").readBytes(),
routesBytes = assets.open("routes_winter.bin").readBytes()
),
PeriodData(
periodId = "summer",
stopsBytes = assets.open("stops_summer.bin").readBytes(),
routesBytes = assets.open("routes_summer.bin").readBytes()
)
))
// Check available periods
val periods = raptor.getAvailablePeriods() // Returns: ["winter", "summer"]
// Switch to a specific period
raptor.setPeriod("summer")
// All subsequent queries will use the summer schedule
val journeys = raptor.getOptimizedPaths(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
departureTime = departureTime
)
// Get current active period
val currentPeriod = raptor.getCurrentPeriod() // Returns: "summer"You can also search for routes that arrive before a specific time (useful for "I need to be there by 9am" scenarios):
// Find the best routes to arrive by 09:00
val arrivalTime = 9 * 3600 // 09:00:00 in seconds
val journeys = raptor.getOptimizedPathsArriveBy(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
arrivalTime = arrivalTime,
searchWindowMinutes = 120 // Search departures up to 2 hours before arrival time
)
// The returned journeys will arrive at or before 09:00
// with the latest possible departure time
for (journey in journeys) {
raptor.displayJourney(journey)
}Journeys can start or end at arbitrary WGS84 coordinates (e.g. a geocoded address or POI) instead of stops. The router walks to/from nearby stops, and walking competes inside the optimization itself: a journey ending with a longer walk can beat one waiting for a later connection. A pure-walk journey is proposed when both points are within walking range.
// From home (geocoded address) to a point of interest, departing at 08:00
val journeys = raptor.getOptimizedPaths(
origin = Location.Point(lat = 45.7578, lon = 4.8320),
destination = Location.Point(lat = 45.7605, lon = 4.8590),
departureTime = 8 * 3600
)
// Mixed endpoints work too (stop ids on one side, coordinates on the other),
// and the arrive-by variant accepts the same Location endpoints:
val arriveBy = raptor.getOptimizedPathsArriveBy(
origin = Location.StopIds(originStops.map { it.id }),
destination = Location.Point(lat = 45.7605, lon = 4.8590),
arrivalTime = 9 * 3600
)
// Walking model is configurable per query
val custom = raptor.getOptimizedPaths(
origin = Location.Point(45.7578, 4.8320),
destination = Location.Point(45.7605, 4.8590),
departureTime = 8 * 3600,
walking = WalkingParams(
speedMetersPerSecond = 4.8 / 3.6, // average walking speed
detourFactor = 1.3, // street-network detour vs straight line
maxAccessEgressDistanceMeters = 500.0, // stop search radius around each point
maxDirectWalkDistanceMeters = 1000.0 // max distance for a pure-walk journey
)
)Walk legs are regular JourneyLegs with isTransfer = true, a legType of WALK_ACCESS,
WALK_EGRESS or WALK_DIRECT, and the coordinates of both ends (fromLat/fromLon/
toLat/toLon). A coordinate endpoint uses stop index -1 — resolve names only for
indices >= 0. Location.StopIds on both sides behaves exactly like the classic id-based
methods.
When the app has access to a real pedestrian router (e.g. OSRM foot), it can supply exact walk times instead of the great-circle estimate:
val journeys = raptor.getOptimizedPaths(
origin = Location.ResolvedPoint(
lat = 45.7578, lon = 4.8320,
stops = listOf(StopWalk(stopId = 1234, walkSeconds = 240), StopWalk(1237, 310))
),
destination = Location.Point(45.7605, 4.8590),
departureTime = 8 * 3600,
directWalkSecondsOverride = null // or the router's origin->destination walk duration
)You can restrict which lines are eligible during routing using route names or ids. This is useful to keep a journey within the same fare system or to exclude specific lines.
// Allow only specific lines by name
val journeys = raptor.getOptimizedPaths(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
departureTime = departureTime,
allowedRouteNames = setOf("JD2", "JD3", "RX")
)
// Exclude specific lines by id
val journeysArriveBy = raptor.getOptimizedPathsArriveBy(
originStopIds = originStops.map { it.id },
destinationStopIds = destStops.map { it.id },
arrivalTime = arrivalTime,
blockedRouteIds = setOf(12, 27)
)
// Works with searchAndDisplayRoute too
raptor.searchAndDisplayRoute(
originName = "Perrache",
destinationName = "Cuire",
departureTime = departureTime,
allowedRouteNames = setOf("JD2", "JD3", "RX")
)Measured with JMH (2 separate JVM forks, 5 warmup + 10 measurement iterations of 1 s each) on an Intel Core i7-11700F, 32 GB DDR4 2666 MHz, Windows 11, JDK 17. Times are average per query. Origins and destinations are resolved by stop name (multi-stop sets); forward departs at 08:00, arrive-by targets 09:00 with the default 120 min search window.
| Route | Forward | Arrive-By |
|---|---|---|
| Perrache → Vaulx-en-Velin La Soie | 0.19 ms | 0.36 ms |
| Bellecour → Part-Dieu | 0.18 ms | 0.28 ms |
| Gare de Vaise → Oullins Centre | 0.38 ms | 0.69 ms |
| Perrache → Cuire | 0.38 ms | 0.56 ms |
| Laurent Bonnevay → Gorge de Loup | 0.37 ms | 0.75 ms |
| Part-Dieu → Bellecour | 0.17 ms | 0.19 ms |
Aggregate over 1 000 random O-D pairs (JMH, same config): forward 0.35 ms, arrive-by 0.40 ms per query — arrive-by now costs barely more than a forward search thanks to the single backward RAPTOR pass introduced in v1.7.0.
| Route | Forward | Arrive-By |
|---|---|---|
| Vieux-Port → La Rose | 0.12 ms | 0.25 ms |
| Castellane → Bougainville | 0.08 ms | 0.11 ms |
| La Timone → Joliette | 0.10 ms | 0.19 ms |
| La Rose → Castellane | 0.10 ms | 0.22 ms |
| Noailles → Sainte-Marguerite Dromel | 0.06 ms | 0.13 ms |
| Bougainville → La Fourragère | 0.15 ms | 0.25 ms |
| Route | Forward | Arrive-By |
|---|---|---|
| Gare de Lyon → Gare du Nord | 1.54 ms | 2.76 ms |
| Gare Saint-Lazare → Montparnasse Bienvenue | 2.08 ms | 4.63 ms |
| Charles de Gaulle - Étoile → Nation | 0.71 ms | 0.87 ms |
| République → Bastille | 0.89 ms | 0.92 ms |
| Gare du Nord → Gare Montparnasse | 5.71 ms | 8.09 ms |
| Bastille → Gare Saint-Lazare | 2.07 ms | 3.55 ms |
| Glacière → Bonne Nouvelle | 6.16 ms | 7.22 ms |
Since v1.7.0, arrive-by runs a single backward RAPTOR pass instead of a departure-time binary search: on these Paris queries it is 4–10× faster than v1.1.0, and costs barely more than a forward search (~1.5× on average, versus ~7× before).