Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
78593c1
Use platform-aware stop levels for OSR first and last mile matching
kylerchin Oct 31, 2026
7201ee2
Use platform-aware stop levels for OSR first and last mile matching
kylerchin Oct 31, 2026
431f77b
Use platform-aware stop levels for OSR first and last mile matching
kylerchin Oct 31, 2026
55f31bb
Pin OSR FFI bridge with named-platform level lookup
kylerchin Oct 31, 2026
f351777
Test platform-name request backward compatibility
kylerchin Oct 31, 2026
b94ad3b
Match OSR access destinations using MOTIS GTFS identity, routes and p…
kylerchin Oct 31, 2026
1b324c8
Send GTFS identifiers to OSR platform matching for first and last mile
kylerchin Oct 31, 2026
09a4b70
Use matched OSM platform levels consistently in Gentian transfer prec…
kylerchin Oct 31, 2026
83311a9
Integrate updated native platform matching into Gentian and Edelweiss
kylerchin Oct 31, 2026
3b5cb05
Integrate updated native platform matching into Gentian and Edelweiss
kylerchin Oct 31, 2026
74acb44
Cover full GTFS stop-matching payload and legacy compatibility in tests
kylerchin Oct 31, 2026
202834d
Propagate public GTFS route identifiers into MOTIS-style OSR station …
kylerchin Oct 31, 2026
f7337f0
Load GTFS platform codes and route names as validated timetable sidecar
kylerchin Oct 31, 2026
a2b20f3
Compile GTFS platform codes and transit route labels for identical Ge…
kylerchin Oct 31, 2026
43065ec
Apply full GTFS platform and route metadata to Gentian's OSR matching
kylerchin Oct 31, 2026
357222e
Use compiled GTFS platform code and route hints for Edelweiss access …
kylerchin Oct 31, 2026
8cb0bfa
Fix OSR stop payload iterator syntax
kylerchin Oct 31, 2026
d3e6bec
Match MOTIS candidate stop filtering and allow mode-appropriate first…
kylerchin Oct 31, 2026
a479585
Use MOTIS-style traffic stop enumeration and speed-budget access radius
kylerchin Oct 31, 2026
fb9908f
Pin merged OSR FFI GCC 14 platform matcher fix (#17)
kylerchin Oct 31, 2026
08105ef
Fix OSR worker test request and bound larger GTFS metadata payloads
kylerchin Oct 31, 2026
f39def9
Fix Gentian platform stop slice initialization before validation
kylerchin Oct 31, 2026
a230dac
Invalidate old OSR graph extracts when enabling platform metadata
kylerchin Oct 31, 2026
97719c3
Repair invalid Clippy workflow YAML indentation
kylerchin Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/rust-clippy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ jobs:
components: clippy
override: true
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
cache-workspace-crates: true
with:
cache-on-failure: true
cache-workspace-crates: true
- run: cargo install clippy-sarif sarif-fmt
- run: set -o pipefail; cargo clippy --all-targets --all-features --message-format=json |
clippy-sarif | tee results.sarif | sarif-fmt
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ non_ascii_idents = "deny"
lto = "thin"

[dependencies]
osr-ffi = { git = "https://github.com/catenarytransit/osr-ffi", rev = "f6cabba43267fbf87111a9e9b3061c9d40e14e2f" }
osr-ffi = { git = "https://github.com/catenarytransit/osr-ffi", rev = "fe06b16f6ed1f66dc2f5a0038b38d0097b328161" }
cxx = "1"
good_lp = { version = "1.0", features = ["coin_cbc"] }
mvt = "0.10"
Expand Down
31 changes: 30 additions & 1 deletion src/edelweiss/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ struct OsrStop {
stop: u32,
latitude: f64,
longitude: f64,
name: String,
gtfs_id: String,
platform_code: String,
route_names: Vec<String>,
}
#[derive(Serialize)]
struct OsrRequest<'a> {
Expand Down Expand Up @@ -198,8 +202,19 @@ fn osr_access(
pedestrian_speed: f32,
cycling_speed: f32,
) -> Result<Vec<WalkingEndpoint>> {
let nearest = router.nearest_stops(transit_region, point, 4096)?;
// MOTIS enumerates traffic-bearing stops in a mode-appropriate search
// radius before performing one-to-many native street routing.
let speed = f64::from(match mode {
StreetMode::Walk => pedestrian_speed,
StreetMode::Bike => cycling_speed,
});
let search_radius = (speed * f64::from(max_seconds))
.max(router.settings.access_radius_meters);
let nearest = router.nearest_stops(
transit_region, point, 4096, search_radius,
)?;
let table = router.timetable_for(transit_region)?;
let platform_metadata = router.platform_metadata_for(transit_region)?;
let stops = nearest
.into_iter()
.map(|(id, _)| {
Expand All @@ -208,6 +223,16 @@ fn osr_access(
stop: id.0,
latitude: f64::from(c.lat),
longitude: f64::from(c.lon),
name: table.location_name().get(id.idx())
.and_then(|r| table.get_string(*r))
.unwrap_or("")
.to_owned(),
gtfs_id: table.location_id().get(id.idx())
.and_then(|r| table.get_string(*r))
.unwrap_or("")
.to_owned(),
platform_code: platform_metadata[id.idx()].platform_code.clone(),
route_names: platform_metadata[id.idx()].route_names.clone(),
}
})
.collect();
Expand Down Expand Up @@ -267,6 +292,10 @@ fn direct_osr_seconds(
stop: 0,
latitude: to.lat,
longitude: to.lon,
name: String::new(),
gtfs_id: String::new(),
platform_code: String::new(),
route_names: Vec::new(),
}],
};
let mut stream = TcpStream::connect(addr)?;
Expand Down
57 changes: 56 additions & 1 deletion src/gentian/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,54 @@ fn add_footpaths(tt: &mut TimetableOwned, mut transfers: Vec<Vec<(u32, u32)>>) -
Ok(())
}


fn load_platform_metadata(
client: &mut Client,
sources: &[ChateauSource],
tt: &TimetableOwned,
) -> Result<Vec<catenary::routing_common::route_manager::PlatformStopMeta>> {
use catenary::routing_common::route_manager::PlatformStopMeta;
// GTFS identifiers are source-scoped: never index stops by bare GTFS ID.
let mut index = HashMap::with_capacity(tt.coordinates.len());
for i in 0..tt.coordinates.len() {
let source = tt.location_source[i].0;
let id = tt.string(tt.location_id[i]).context("invalid GTFS stop id")?;
index.insert((source, id.to_owned()), i);
}
let mut out = vec![PlatformStopMeta::default(); tt.coordinates.len()];
for (source_idx, source) in sources.iter().enumerate() {
let mut route_labels = HashMap::<String, String>::new();
query_source(client, "
SELECT route_id, COALESCE(NULLIF(short_name, ''), route_id)
FROM gtfs.routes
WHERE onestop_feed_id = $1 AND attempt_id = $2
", source, |r| {
route_labels.insert(r.get(0), r.get(1));
Ok(())
})?;
query_source(client, "
SELECT gtfs_id, COALESCE(platform_code, ''), routes
FROM gtfs.stops
WHERE onestop_feed_id = $1 AND attempt_id = $2
AND point IS NOT NULL
", source, |r| {
let id: String = r.get(0);
let code: String = r.get(1);
let routes: Vec<Option<String>> = r.get(2);
if let Some(&i) = index.get(&(u16::try_from(source_idx)?, id)) {
out[i] = PlatformStopMeta {
platform_code: code,
route_names: routes.into_iter().flatten()
.map(|route| route_labels.get(&route).cloned().unwrap_or(route))
.collect(),
};
}
Ok(())
})?;
}
Ok(out)
}

pub fn build(
client: &mut Client,
sources: &[ChateauSource],
Expand Down Expand Up @@ -1064,8 +1112,11 @@ pub fn build(
);
}
assemble_routes(&mut tt, schedules)?;
let platform_metadata = load_platform_metadata(client, sources, &tt)?;
let mut transfers =
super::footpaths::compute(osr_dir, &tt.coordinates, max_seconds, candidate_meters)?;
super::footpaths::compute(
osr_dir, &tt, &platform_metadata, max_seconds, candidate_meters,
)?;
// Nigiri also connects nearby platforms under one station, where the
// street graph often omits indoor passages. Never replace a routed OSR edge.
super::footpaths::add_station_links(&tt, &mut transfers)?;
Expand All @@ -1075,6 +1126,10 @@ pub fn build(
add_footpaths(&mut tt, transfers)?;
transfer_rules.finish(&mut tt);
let bytes = catenary::routing_common::timetable_mmap::publish(&tt, output)?;
let sidecar = output.with_extension("platforms.json");
let staging = sidecar.with_extension("platforms.json.tmp");
std::fs::write(&staging, serde_json::to_vec(&platform_metadata)?)?;
std::fs::rename(&staging, &sidecar)?;
println!(
"Wrote {} bytes; {} stops, {} RAPTOR routes, {} trips, {} footpaths",
bytes,
Expand Down
19 changes: 17 additions & 2 deletions src/gentian/footpaths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ fn meters(a: Coordinate, b: Coordinate) -> f64 {
/// Each index is the exact LocationIdx produced by Gentian's timetable writer.
pub fn compute(
osr_dir: &Path,
stops: &[Coordinate],
tt: &catenary::routing_common::timetable::TimetableOwned,
metadata: &[catenary::routing_common::route_manager::PlatformStopMeta],
max_seconds: u32,
candidate_meters: f64,
) -> Result<Vec<Vec<(u32, u32)>>> {
Expand All @@ -34,10 +35,12 @@ pub fn compute(
candidate_meters.is_finite() && candidate_meters > 0.0,
"candidate radius must be positive and finite"
);
let stops = &tt.coordinates;
ensure!(
u32::try_from(stops.len()).is_ok(),
"too many locations for packed indexes"
);
ensure!(metadata.len() == stops.len(), "GTFS platform metadata count mismatch");
const CELL: f64 = 0.02;
let mut tiles: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
for (idx, stop) in stops.iter().enumerate() {
Expand Down Expand Up @@ -85,7 +88,19 @@ pub fn compute(
..match_origin.clone()
};
for (i, stop) in stops.iter().enumerate() {
let coordinate = ffi::Coordinate::outdoor(f64::from(stop.lat), f64::from(stop.lon));
let mut coordinate = ffi::Coordinate::outdoor(f64::from(stop.lat), f64::from(stop.lon));
let stop_id = tt.location_id.get(i).and_then(|v| tt.string(*v)).unwrap_or("");
let name = tt.location_name.get(i).and_then(|v| tt.string(*v)).unwrap_or("");
let hint = &metadata[i];
let routes = hint.route_names.join("\u{1f}");
let matched = router.match_platform(
&coordinate, stop_id, name, &hint.platform_code, &routes, 25.0,
)?;
if matched.found {
coordinate = ffi::Coordinate::indoor(
f64::from(stop.lat), f64::from(stop.lon), matched.level,
);
}
// Profile-independent candidates can be reused for both match roles.
let raw = router.get_raw_match(&coordinate, 250.0)?;
let origin_idx =
Expand Down
68 changes: 64 additions & 4 deletions src/gentian/osr_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ struct Stop {
stop: u32,
latitude: f64,
longitude: f64,
#[serde(default)]
name: String,
#[serde(default)]
gtfs_id: String,
#[serde(default)]
platform_code: String,
#[serde(default)]
route_names: Vec<String>,
}
#[derive(Serialize)]
struct Access {
Expand Down Expand Up @@ -173,7 +181,22 @@ fn run_request(
x.latitude.is_finite() && x.longitude.is_finite(),
"invalid target"
);
targets.push(ffi::Coordinate::outdoor(x.latitude, x.longitude));
let mut point = ffi::Coordinate::outdoor(x.latitude, x.longitude);
// MOTIS derives GTFS stop levels from matched OSM platforms.
// Using kNoLevel for an elevated or underground station can shadow
// the real pedestrian platform with a road/way on another level.
// Only set an explicit level for a conservatively matched platform.
if !x.name.is_empty() || !x.gtfs_id.is_empty() || !x.platform_code.is_empty() {
let route_names = x.route_names.join("\u{1f}");
let matched = router.match_platform(
&point, &x.gtfs_id, &x.name, &x.platform_code,
&route_names, 25.0,
)?;
if matched.found {
point = ffi::Coordinate::indoor(x.latitude, x.longitude, matched.level);
}
}
targets.push(point);
}
// Use low-level native OSR matching. Access queries start at the user
// (forward); egress queries search backward from the destination to
Expand Down Expand Up @@ -267,12 +290,18 @@ fn handle(stream: TcpStream, graphs: &BTreeMap<String, cxx::SharedPtr<ffi::Route
let reader = BufReader::new(stream.try_clone()?);
// Enforce the wire budget while reading. Checking after read_line could
// allocate an unbounded String per concurrent client.
let mut limited = reader.take(524_289);
// A 4096-stop route search now includes GTFS names, platform codes and
// public route labels. A valid request can exceed the former 512 KiB
// ceiling; keep a strict bound without rejecting dense urban searches.
const MAX_OSR_REQUEST_BYTES: u64 = 4 * 1024 * 1024;
let mut limited = reader.take(MAX_OSR_REQUEST_BYTES + 1);
let mut input = Vec::new();
let received = limited.read_until(b'\n', &mut input)?;
ensure!(
received > 0 && received <= 524_288 && input.last() == Some(&b'\n'),
"OSR request exceeds 512KiB or is not newline terminated"
received > 0
&& received <= MAX_OSR_REQUEST_BYTES as usize
&& input.last() == Some(&b'\n'),
"OSR request exceeds 4 MiB or is not newline terminated"
);
let response = match serde_json::from_slice::<Request>(&input) {
Ok(req) => match run_request(graphs, req) {
Expand Down Expand Up @@ -465,6 +494,9 @@ mod tests {
longitude: 0.0,
max_seconds: 120,
direction: 0,
mode: StreetMode::Walk,
pedestrian_speed: None,
cycling_speed: None,
stops: Vec::new(),
include_geometry: false,
};
Expand All @@ -474,6 +506,34 @@ mod tests {
assert!(error.to_string().contains("OSR region is not loaded"));
}

#[test]
fn legacy_stop_requests_without_platform_name_remain_supported() -> Result<()> {
let legacy: Request = serde_json::from_value(serde_json::json!({
"region": "test",
"latitude": 34.0,
"longitude": -118.0,
"max_seconds": 900,
"stops": [{"stop": 1, "latitude": 34.01, "longitude": -118.01}]
}))?;
assert_eq!(legacy.stops[0].name, "");
let modern: Request = serde_json::from_value(serde_json::json!({
"region": "test",
"latitude": 34.0,
"longitude": -118.0,
"max_seconds": 900,
"stops": [{"stop": 1, "latitude": 34.01, "longitude": -118.01,
"name": "Expo / Sepulveda",
"gtfs_id": "expo-sepulveda-east",
"platform_code": "1",
"route_names": ["E"]}]
}))?;
assert_eq!(modern.stops[0].name, "Expo / Sepulveda");
assert_eq!(modern.stops[0].gtfs_id, "expo-sepulveda-east");
assert_eq!(modern.stops[0].platform_code, "1");
assert_eq!(modern.stops[0].route_names, vec!["E"]);
Ok(())
}

#[test]
fn cycling_uses_time_optimized_bicycle_profile_at_requested_si_speed() -> Result<()> {
// The real Edelweiss payload uses metres per second: 23.5 km/h.
Expand Down
Loading
Loading