1use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError};
25use pyo3::prelude::*;
26use 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,
30};
31
32fn convert_error(error: ProjectionError) -> PyErr {
38 match error {
39 ProjectionError::InvalidCrs(msg) => PyValueError::new_err(format!("Invalid CRS: {}", msg)),
40 ProjectionError::TransformFailed(msg) => {
41 PyRuntimeError::new_err(format!("Coordinate transformation failed: {}", msg))
42 }
43 ProjectionError::InvalidCoordinate(msg) => {
44 PyValueError::new_err(format!("Invalid coordinate: {}", msg))
45 }
46 ProjectionError::MissingTimezone(msg) => {
47 PyValueError::new_err(format!("Missing timezone: {}", msg))
48 }
49 ProjectionError::InvalidTimestamp(msg) => {
50 PyValueError::new_err(format!("Invalid timestamp: {}", msg))
51 }
52 ProjectionError::EmptyNetwork => PyValueError::new_err("Railway network is empty"),
53 ProjectionError::InvalidGeometry(msg) => {
54 PyValueError::new_err(format!("Invalid geometry: {}", msg))
55 }
56 ProjectionError::CsvError(err) => PyIOError::new_err(format!("CSV error: {}", err)),
57 ProjectionError::GeoJsonError(msg) => PyIOError::new_err(format!("GeoJSON error: {}", msg)),
58 ProjectionError::IoError(err) => PyIOError::new_err(format!("IO error: {}", err)),
59 }
60}
61
62#[pyclass]
68#[derive(Clone)]
69pub struct ProjectionConfig {
70 #[pyo3(get, set)]
72 pub projection_distance_warning_threshold: f64,
73
74 #[pyo3(get, set)]
76 pub transform_crs: bool,
77
78 #[pyo3(get, set)]
80 pub suppress_warnings: bool,
81}
82
83#[pymethods]
84impl ProjectionConfig {
85 #[new]
86 #[pyo3(signature = (projection_distance_warning_threshold=50.0, transform_crs=true, suppress_warnings=false))]
87 fn new(
88 projection_distance_warning_threshold: f64,
89 transform_crs: bool,
90 suppress_warnings: bool,
91 ) -> Self {
92 Self {
93 projection_distance_warning_threshold,
94 transform_crs,
95 suppress_warnings,
96 }
97 }
98
99 fn __repr__(&self) -> String {
100 format!(
101 "ProjectionConfig(projection_distance_warning_threshold={}, transform_crs={}, suppress_warnings={})",
102 self.projection_distance_warning_threshold,
103 self.transform_crs,
104 self.suppress_warnings
105 )
106 }
107}
108
109impl From<ProjectionConfig> for CoreProjectionConfig {
110 fn from(py_config: ProjectionConfig) -> Self {
111 CoreProjectionConfig {
112 projection_distance_warning_threshold: py_config.projection_distance_warning_threshold,
113 transform_crs: py_config.transform_crs,
114 suppress_warnings: py_config.suppress_warnings,
115 }
116 }
117}
118
119#[pyclass]
121#[derive(Clone)]
122pub struct ProjectedPosition {
123 #[pyo3(get)]
125 pub original_latitude: f64,
126
127 #[pyo3(get)]
129 pub original_longitude: f64,
130
131 #[pyo3(get)]
133 pub timestamp: String,
134
135 #[pyo3(get)]
137 pub projected_x: f64,
138
139 #[pyo3(get)]
141 pub projected_y: f64,
142
143 #[pyo3(get)]
145 pub netelement_id: String,
146
147 #[pyo3(get)]
149 pub measure_meters: f64,
150
151 #[pyo3(get)]
153 pub projection_distance_meters: f64,
154
155 #[pyo3(get)]
157 pub crs: String,
158}
159
160#[pymethods]
161impl ProjectedPosition {
162 fn __repr__(&self) -> String {
163 format!(
164 "ProjectedPosition(netelement_id='{}', measure={}m, distance={}m)",
165 self.netelement_id, self.measure_meters, self.projection_distance_meters
166 )
167 }
168
169 fn to_dict(&self) -> PyResult<std::collections::HashMap<String, String>> {
170 let mut dict = std::collections::HashMap::new();
171 dict.insert(
172 "original_latitude".to_string(),
173 self.original_latitude.to_string(),
174 );
175 dict.insert(
176 "original_longitude".to_string(),
177 self.original_longitude.to_string(),
178 );
179 dict.insert("timestamp".to_string(), self.timestamp.clone());
180 dict.insert("projected_x".to_string(), self.projected_x.to_string());
181 dict.insert("projected_y".to_string(), self.projected_y.to_string());
182 dict.insert("netelement_id".to_string(), self.netelement_id.clone());
183 dict.insert(
184 "measure_meters".to_string(),
185 self.measure_meters.to_string(),
186 );
187 dict.insert(
188 "projection_distance_meters".to_string(),
189 self.projection_distance_meters.to_string(),
190 );
191 dict.insert("crs".to_string(), self.crs.clone());
192 Ok(dict)
193 }
194}
195
196impl From<&CoreProjectedPosition> for ProjectedPosition {
197 fn from(core_result: &CoreProjectedPosition) -> Self {
198 ProjectedPosition {
199 original_latitude: core_result.original.latitude,
200 original_longitude: core_result.original.longitude,
201 timestamp: core_result.original.timestamp.to_rfc3339(),
202 projected_x: core_result.projected_coords.x(),
203 projected_y: core_result.projected_coords.y(),
204 netelement_id: core_result.netelement_id.clone(),
205 measure_meters: core_result.measure_meters,
206 projection_distance_meters: core_result.projection_distance_meters,
207 crs: core_result.crs.clone(),
208 }
209 }
210}
211
212#[pyfunction]
255#[pyo3(signature = (gnss_file, gnss_crs, network_file, _network_crs, _target_crs, config=None))]
256fn project_gnss(
257 gnss_file: &str,
258 gnss_crs: &str,
259 network_file: &str,
260 _network_crs: &str, _target_crs: &str, config: Option<ProjectionConfig>,
263) -> PyResult<Vec<ProjectedPosition>> {
264 let core_config: CoreProjectionConfig = config
266 .unwrap_or_else(|| ProjectionConfig::new(50.0, true, false))
267 .into();
268
269 let gnss_positions = parse_gnss_csv(gnss_file, gnss_crs, "latitude", "longitude", "timestamp")
272 .map_err(convert_error)?;
273
274 let netelements = parse_network_geojson(network_file).map_err(convert_error)?;
276
277 let network = RailwayNetwork::new(netelements).map_err(convert_error)?;
279
280 let results =
282 core_project_gnss(&gnss_positions, &network, &core_config).map_err(convert_error)?;
283
284 Ok(results.iter().map(ProjectedPosition::from).collect())
286}
287
288#[pymodule]
294fn tp_lib(m: &Bound<'_, PyModule>) -> PyResult<()> {
295 m.add_function(wrap_pyfunction!(project_gnss, m)?)?;
296 m.add_class::<ProjectionConfig>()?;
297 m.add_class::<ProjectedPosition>()?;
298 Ok(())
299}