Skip to content

Commit 3ceed4d

Browse files
committed
fix(tp-py): implement CRS validation and target_crs coordinate transform
- Validate gnss_crs, network_crs, target_crs upfront using CrsTransformer so invalid CRS strings raise ValueError immediately - Transform projected coordinates from native CRS to target_crs using CrsTransformer, and set result.crs to the target_crs - Add geo as direct dependency in tp-py/Cargo.toml
1 parent 6bbbff5 commit 3ceed4d

File tree

2 files changed

+29
-10
lines changed

2 files changed

+29
-10
lines changed

tp-py/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ crate-type = ["cdylib", "rlib"]
1717
[dependencies]
1818
tp-lib-core = { version = "0.0.1", path = "../tp-core" }
1919
pyo3.workspace = true
20+
geo.workspace = true

tp-py/src/lib.rs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError};
2525
use pyo3::prelude::*;
2626
use tp_lib_core::{
27-
parse_gnss_csv, parse_network_geojson, project_gnss as core_project_gnss,
28-
ProjectedPosition as CoreProjectedPosition, ProjectionConfig as CoreProjectionConfig,
29-
ProjectionError, RailwayNetwork,
27+
crs::transform::CrsTransformer, parse_gnss_csv, parse_network_geojson,
28+
project_gnss as core_project_gnss, ProjectedPosition as CoreProjectedPosition,
29+
ProjectionConfig as CoreProjectionConfig, ProjectionError, RailwayNetwork,
3030
};
3131

3232
// ============================================================================
@@ -258,22 +258,26 @@ impl From<&CoreProjectedPosition> for ProjectedPosition {
258258
/// print(f"{pos.netelement_id}: {pos.measure_meters}m")
259259
/// ```
260260
#[pyfunction]
261-
#[pyo3(signature = (gnss_file, gnss_crs, network_file, _network_crs, _target_crs, config=None))]
261+
#[pyo3(signature = (gnss_file, gnss_crs, network_file, network_crs, target_crs, config=None))]
262262
fn project_gnss(
263263
gnss_file: &str,
264264
gnss_crs: &str,
265265
network_file: &str,
266-
_network_crs: &str, // Reserved for future use when CRS per file is supported
267-
_target_crs: &str, // Reserved for future use when explicit target CRS is supported
266+
network_crs: &str,
267+
target_crs: &str,
268268
config: Option<ProjectionConfig>,
269269
) -> PyResult<Vec<ProjectedPosition>> {
270+
// Validate all CRS strings upfront so callers get a clear ValueError for bad CRS
271+
CrsTransformer::new(gnss_crs.to_string(), gnss_crs.to_string()).map_err(convert_error)?;
272+
CrsTransformer::new(network_crs.to_string(), network_crs.to_string()).map_err(convert_error)?;
273+
CrsTransformer::new(target_crs.to_string(), target_crs.to_string()).map_err(convert_error)?;
274+
270275
// Convert Python config to Rust config
271276
let core_config: CoreProjectionConfig = config
272277
.unwrap_or_else(|| ProjectionConfig::new(1000.0, 50.0, false))
273278
.into();
274279

275280
// Parse GNSS positions from CSV
276-
// Note: parse_gnss_csv signature is (path, crs, lat_col, lon_col, time_col)
277281
let gnss_positions = parse_gnss_csv(gnss_file, gnss_crs, "latitude", "longitude", "timestamp")
278282
.map_err(convert_error)?;
279283

@@ -285,11 +289,25 @@ fn project_gnss(
285289
let network = RailwayNetwork::new(netelements).map_err(convert_error)?;
286290

287291
// Project positions
288-
let results =
292+
let core_results =
289293
core_project_gnss(&gnss_positions, &network, &core_config).map_err(convert_error)?;
290294

291-
// Convert to Python objects
292-
Ok(results.iter().map(ProjectedPosition::from).collect())
295+
// Convert to Python objects, transforming coordinates to target_crs
296+
let mut py_results = Vec::with_capacity(core_results.len());
297+
for core_result in &core_results {
298+
let mut pos = ProjectedPosition::from(core_result);
299+
if pos.crs != target_crs {
300+
let transformer = CrsTransformer::new(pos.crs.clone(), target_crs.to_string())
301+
.map_err(convert_error)?;
302+
let point = geo::Point::new(pos.projected_x, pos.projected_y);
303+
let transformed = transformer.transform(point).map_err(convert_error)?;
304+
pos.projected_x = transformed.x();
305+
pos.projected_y = transformed.y();
306+
pos.crs = target_crs.to_string();
307+
}
308+
py_results.push(pos);
309+
}
310+
Ok(py_results)
293311
}
294312

295313
// ============================================================================

0 commit comments

Comments
 (0)