2424use pyo3:: exceptions:: { PyIOError , PyRuntimeError , PyValueError } ;
2525use pyo3:: prelude:: * ;
2626use 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 ) ) ]
262262fn 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