From 957a6bd431a52bfc4381c8d7f26cab35076b0b6c Mon Sep 17 00:00:00 2001 From: Younes Khoudli Date: Fri, 3 Apr 2026 22:17:28 +0200 Subject: [PATCH 1/2] Add callbacks instead of printing to stdout when a data issue is found This is useful for osrd-data, where such issues are usually reported through the errdb mechanism, allowing more precise statistics. Signed-off-by: Younes Khoudli --- python/liblrs_python.pyi | 5 +++- python/src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++++-- src/builder.rs | 7 +++-- src/geometry_from_osm.rs | 1 + src/lib.rs | 40 +++++++++++++++++++++++++ src/lrs.rs | 2 +- src/osm_helpers.rs | 27 ++++++++++++----- 7 files changed, 130 insertions(+), 15 deletions(-) diff --git a/python/liblrs_python.pyi b/python/liblrs_python.pyi index 4def51d..b5b93a8 100644 --- a/python/liblrs_python.pyi +++ b/python/liblrs_python.pyi @@ -128,7 +128,7 @@ class Builder: r""" List all the traversals by their id and index """ - def read_from_osm(self, input_osm_file: builtins.str | os.PathLike | pathlib.Path, lrm_tag: builtins.str, required: typing.Sequence[tuple[builtins.str, builtins.str]], to_reject: typing.Sequence[tuple[builtins.str, builtins.str]]) -> None: + def read_from_osm(self, input_osm_file: builtins.str | os.PathLike | pathlib.Path, lrm_tag: builtins.str, required: typing.Sequence[tuple[builtins.str, builtins.str]], to_reject: typing.Sequence[tuple[builtins.str, builtins.str]], reporter: DataIssueReporter) -> None: r""" Read the topology from an OpenStreetMap source @@ -179,6 +179,9 @@ class Builder: If both points are so far from the curve that they are projected to a end, we consider the offset to the curve """ +class DataIssueReporter: + def report_ignoring_traversal_edges(self, traversal_ref: builtins.str, ignored_count: builtins.int, total_count: builtins.int, first_node: builtins.int, last_node: builtins.int) -> None: ... + @typing.final class LrmProjection: r""" diff --git a/python/src/lib.rs b/python/src/lib.rs index f333b26..35beee1 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -27,6 +27,7 @@ fn liblrs_python(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } @@ -576,15 +577,22 @@ impl Builder { /// Read the topology from an OpenStreetMap source /// /// It reads the nodes, segments and traversals. - pub fn read_from_osm( + pub fn read_from_osm<'py>( &mut self, input_osm_file: PathBuf, lrm_tag: String, required: Vec<(String, String)>, to_reject: Vec<(String, String)>, + reporter: Bound<'py, DataIssueReporter>, ) { - self.inner - .read_from_osm(&input_osm_file, &lrm_tag, required, to_reject) + let mut reporter = PythonDataIssueReporter(reporter); + self.inner.read_from_osm( + &input_osm_file, + &lrm_tag, + required, + to_reject, + Some(&mut reporter), + ) } /// Save the lrs to a file @@ -656,3 +664,52 @@ impl Builder { } define_stub_info_gatherer!(stub_info); + +#[gen_stub_pyclass] +#[pyclass(subclass)] +struct DataIssueReporter {} + +#[pymethods] +impl DataIssueReporter { + #[new] + fn new() -> Self { + Self {} + } +} + +#[allow(unused_variables)] +#[gen_stub_pymethods] +impl liblrs::DataIssueReporter for DataIssueReporter { + fn report_ignoring_traversal_edges( + &mut self, + traversal_ref: &str, + ignored_count: usize, + total_count: usize, + first_node: i64, + last_node: i64, + ) { + } +} + +struct PythonDataIssueReporter<'a>(Bound<'a, DataIssueReporter>); +impl liblrs::DataIssueReporter for PythonDataIssueReporter<'_> { + fn report_ignoring_traversal_edges( + &mut self, + traversal_ref: &str, + ignored_count: usize, + total_count: usize, + first_node: i64, + last_node: i64, + ) { + let _ = self.0.call_method1( + "report_ignoring_traversal_edges", + ( + traversal_ref, + ignored_count, + total_count, + first_node, + last_node, + ), + ); + } +} diff --git a/src/builder.rs b/src/builder.rs index 038145e..69f531c 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -13,7 +13,7 @@ use crate::lrs::Properties; use crate::lrs_ext::ExtLrs; use crate::lrs_generated::{self, *}; use crate::osm_helpers::sort_edges; -use crate::properties; +use crate::{DataIssueReporter, properties}; /// The linear position of an [`Anchor`] doesn’t always match the measured distance. /// For example if a road was transformed into a bypass, resulting in a longer road, @@ -436,7 +436,10 @@ impl<'fbb> Builder<'fbb> { lrm_tag: &str, required: Vec<(String, String)>, to_reject: Vec<(String, String)>, + reporter: Option<&mut dyn DataIssueReporter>, ) { + let mut default_reporter = (); + let reporter = reporter.unwrap_or(&mut default_reporter); let mut reader = osm4routing::Reader::new().merge_ways().read_tag(lrm_tag); for (key, value) in required.iter() { @@ -479,7 +482,7 @@ impl<'fbb> Builder<'fbb> { // Sort the traversals for (srv_ref, edges) in traversals.into_iter() { - let segments: Vec<_> = sort_edges(edges, &srv_ref) + let segments: Vec<_> = sort_edges(edges, &srv_ref, reporter) .into_iter() .map(|(edge, reversed)| SegmentOfTraversal { segment_index: edges_map[&edge.id], diff --git a/src/geometry_from_osm.rs b/src/geometry_from_osm.rs index bed17c5..1edb594 100644 --- a/src/geometry_from_osm.rs +++ b/src/geometry_from_osm.rs @@ -44,6 +44,7 @@ fn main() { &cli_args.lrm_tag, required, to_reject, + Some(&mut liblrs::LoggingDataIssueReporter), ); builder.save( diff --git a/src/lib.rs b/src/lib.rs index 9a6f900..56e2c2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,46 @@ pub mod lrs_ext; #[deny(missing_docs)] pub mod builder; +pub trait DataIssueReporter { + fn report_ignoring_traversal_edges( + &mut self, + traversal_ref: &str, + ignored_count: usize, + total_count: usize, + first_node: i64, + last_node: i64, + ); +} + +pub struct LoggingDataIssueReporter; + +impl DataIssueReporter for LoggingDataIssueReporter { + fn report_ignoring_traversal_edges( + &mut self, + traversal_ref: &str, + ignored_count: usize, + total_count: usize, + first_node: i64, + last_node: i64, + ) { + println!( + "[WARN] on traversal {traversal_ref}, ignoring {ignored_count} edges out of {total_count}. Sorted from {first_node} to {last_node}" + ); + } +} + +impl DataIssueReporter for () { + fn report_ignoring_traversal_edges( + &mut self, + _traversal_ref: &str, + _ignored_count: usize, + _total_count: usize, + _first_node: i64, + _last_node: i64, + ) { + } +} + #[test] fn read_and_write_lrs() { use builder::*; diff --git a/src/lrs.rs b/src/lrs.rs index b27f73a..c1626c4 100644 --- a/src/lrs.rs +++ b/src/lrs.rs @@ -711,7 +711,7 @@ mod tests { use approx::assert_relative_eq; use geo::line_string; - use crate::{curves::PlanarLineStringCurve, properties}; + use crate::curves::PlanarLineStringCurve; use super::*; diff --git a/src/osm_helpers.rs b/src/osm_helpers.rs index a1b5230..2b50f8f 100644 --- a/src/osm_helpers.rs +++ b/src/osm_helpers.rs @@ -3,6 +3,8 @@ use osm4routing::Edge; +use crate::DataIssueReporter; + /// When sorting the edges, each candidate is tested to see if they match and if they need to be reversed. #[derive(PartialEq, Eq, Debug)] enum Candidate { @@ -79,7 +81,11 @@ fn sort_iteration( /// The traversals are identified by a tag that is used on many ways. /// We try to build the longest continous chain of ways, but the is no guarantee to succeed. /// The ways might not share nodes or they might represent a tree. -pub fn sort_edges(edges: Vec, traversal_ref: &str) -> Vec<(Edge, bool)> { +pub fn sort_edges( + edges: Vec, + traversal_ref: &str, + reporter: &mut dyn DataIssueReporter, +) -> Vec<(Edge, bool)> { let (to_insert, sorted) = sort_iteration(edges, vec![]); // Print some stats about edges that could not be matched @@ -98,10 +104,7 @@ pub fn sort_edges(edges: Vec, traversal_ref: &str) -> Vec<(Edge, bool)> { } else { last_edge.0.target }; - println!( - "[WARN] on traversal {traversal_ref}, ignoring {ignored} edges out of {total}. Sorted from {} to {}", - first.0, last.0 - ); + reporter.report_ignoring_traversal_edges(traversal_ref, ignored, total, first.0, last.0); } sorted @@ -170,7 +173,7 @@ pub mod tests { fn sort_edges_simple() { let e = edge(0, 1); - let sorted = sort_edges(vec![e.clone()], ""); + let sorted = sort_edges(vec![e.clone()], "", &mut crate::LoggingDataIssueReporter); assert_eq!(sorted[0].0, e); assert!(!sorted[0].1); } @@ -180,7 +183,11 @@ pub mod tests { let e1 = edge(0, 1); let e2 = edge(1, 2); - let sorted = sort_edges(vec![e1.clone(), e2.clone()], ""); + let sorted = sort_edges( + vec![e1.clone(), e2.clone()], + "", + &mut crate::LoggingDataIssueReporter, + ); assert_eq!(sorted[0].0, e1); assert_eq!(sorted[1].0, e2); assert!(!sorted[0].1); @@ -192,7 +199,11 @@ pub mod tests { let e1 = edge(1, 0); let e2 = edge(1, 2); - let sorted = sort_edges(vec![e1.clone(), e2.clone()], ""); + let sorted = sort_edges( + vec![e1.clone(), e2.clone()], + "", + &mut crate::LoggingDataIssueReporter, + ); assert_eq!(sorted[0].0, e1); assert_eq!(sorted[1].0, e2); assert!(sorted[0].1); From ad1dd85e15b02c993d77678e3f411f776d925bb3 Mon Sep 17 00:00:00 2001 From: Younes Khoudli Date: Fri, 3 Apr 2026 22:17:28 +0200 Subject: [PATCH 2/2] Bump version to 0.5.0 Signed-off-by: Younes Khoudli --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- python/Cargo.toml | 2 +- python/pyproject.toml | 2 +- python/uv.lock | 2 +- wasm/Cargo.toml | 2 +- wasm/package-lock.json | 4 ++-- wasm/package.json | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97b7a63..631aefc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,7 +703,7 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "liblrs" -version = "0.4.3" +version = "0.5.0" dependencies = [ "approx", "clap", @@ -717,7 +717,7 @@ dependencies = [ [[package]] name = "liblrs-wasm" -version = "0.4.3" +version = "0.5.0" dependencies = [ "console_error_panic_hook", "geo-types", @@ -729,7 +729,7 @@ dependencies = [ [[package]] name = "liblrs_python" -version = "0.4.3" +version = "0.5.0" dependencies = [ "geo-types", "liblrs", diff --git a/Cargo.toml b/Cargo.toml index a758d4a..b4048a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "liblrs" -version = "0.4.3" +version = "0.5.0" edition = "2024" description = "Library to manipulate linear referencing systems" license = "MIT" diff --git a/python/Cargo.toml b/python/Cargo.toml index 6ad59e5..962d3c8 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "liblrs_python" description = "Python bindings for liblrs: a library to work with linear referencing systems" -version = "0.4.3" +version = "0.5.0" edition = "2024" license = "MIT" repository = "https://github.com/OpenRailAssociation/liblrs/" diff --git a/python/pyproject.toml b/python/pyproject.toml index 1d8a9f3..4979a49 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "liblrs_python" -version = "0.4.3" +version = "0.5.0" requires-python = ">=3.12" dependencies = ["pip>=25.2"] diff --git a/python/uv.lock b/python/uv.lock index 8e93a70..3c21f30 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -22,7 +22,7 @@ wheels = [ [[package]] name = "liblrs-python" -version = "0.4.3" +version = "0.5.0" source = { editable = "." } dependencies = [ { name = "pip" }, diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 335bddd..159672e 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "liblrs-wasm" -version = "0.4.3" +version = "0.5.0" edition = "2024" [lib] diff --git a/wasm/package-lock.json b/wasm/package-lock.json index 7d4ec2c..f0482c8 100644 --- a/wasm/package-lock.json +++ b/wasm/package-lock.json @@ -1,12 +1,12 @@ { "name": "liblrs", - "version": "0.4.3", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "liblrs", - "version": "0.4.3", + "version": "0.5.0", "dependencies": { "@turf/bbox": "7.3.1", "@turf/helpers": "7.3.1", diff --git a/wasm/package.json b/wasm/package.json index 908e007..c7b57ff 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -21,7 +21,7 @@ "text-encoding": "0.7.0" }, "name": "liblrs", - "version": "0.4.3", + "version": "0.5.0", "repository": { "type": "git", "url": "git+https://github.com/OpenRailAssociation/liblrs.git"