tp_lib_core/lib.rs
1//! TP-Core: Train Positioning Library - Core Engine
2//!
3//! This library provides geospatial projection of GNSS positions onto railway track netelements.
4//!
5//! # Overview
6//!
7//! TP-Core enables projection of GNSS (GPS) coordinates onto railway track centerlines (netelements),
8//! calculating precise measures along the track and assigning positions to specific track segments.
9//!
10//! # Quick Start
11//!
12//! ```rust,no_run
13//! use tp_core::{parse_gnss_csv, parse_network_geojson, RailwayNetwork, project_gnss, ProjectionConfig};
14//!
15//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! // Load railway network from GeoJSON
17//! let netelements = parse_network_geojson("network.geojson")?;
18//! let network = RailwayNetwork::new(netelements)?;
19//!
20//! // Load GNSS positions from CSV
21//! let positions = parse_gnss_csv("gnss.csv", "EPSG:4326", "latitude", "longitude", "timestamp")?;
22//!
23//! // Project onto network with default configuration
24//! let config = ProjectionConfig::default();
25//! let projected = project_gnss(&positions, &network, &config)?;
26//!
27//! // Use projected results
28//! for pos in projected {
29//! println!("Position at measure {} on netelement {}", pos.measure_meters, pos.netelement_id);
30//! }
31//! # Ok(())
32//! # }
33//! ```
34//!
35//! # Features
36//!
37//! - **Spatial Indexing**: R-tree based spatial indexing for efficient nearest-netelement search
38//! - **CRS Support**: Explicit coordinate reference system handling with optional transformations
39//! - **Timezone Awareness**: RFC3339 timestamps with explicit timezone offsets
40//! - **Multiple Formats**: CSV and GeoJSON input/output support
41
42pub mod crs;
43pub mod errors;
44pub mod io;
45pub mod models;
46pub mod projection;
47pub mod temporal;
48
49// Re-export main types for convenience
50pub use errors::ProjectionError;
51pub use io::{parse_gnss_csv, parse_gnss_geojson, parse_network_geojson, write_csv, write_geojson};
52pub use models::{GnssPosition, Netelement, ProjectedPosition};
53
54/// Result type alias using ProjectionError
55pub type Result<T> = std::result::Result<T, ProjectionError>;
56
57use geo::Point;
58use projection::geom::project_gnss_position;
59use projection::spatial::{find_nearest_netelement, NetworkIndex};
60
61/// Configuration for GNSS projection operations
62///
63/// # Fields
64///
65/// * `projection_distance_warning_threshold` - Distance in meters above which warnings are emitted
66/// * `transform_crs` - Enable CRS transformation
67///
68/// # Examples
69///
70/// ```
71/// use tp_core::ProjectionConfig;
72///
73/// // Use default configuration (50m warning threshold)
74/// let config = ProjectionConfig::default();
75///
76/// // Custom configuration with higher threshold
77/// let config = ProjectionConfig {
78/// projection_distance_warning_threshold: 100.0,
79/// transform_crs: false,
80/// suppress_warnings: false,
81/// };
82/// ```
83#[derive(Debug, Clone)]
84pub struct ProjectionConfig {
85 /// Threshold distance in meters for emitting warnings about large projection distances
86 pub projection_distance_warning_threshold: f64,
87 /// Whether to enable CRS transformation (requires proj feature)
88 pub transform_crs: bool,
89 /// Whether to suppress console warnings (useful for benchmarking)
90 pub suppress_warnings: bool,
91}
92
93impl Default for ProjectionConfig {
94 fn default() -> Self {
95 Self {
96 projection_distance_warning_threshold: 50.0,
97 transform_crs: true,
98 suppress_warnings: false,
99 }
100 }
101}
102
103/// Railway network with spatial indexing for efficient projection
104///
105/// The `RailwayNetwork` wraps netelements with an R-tree spatial index for O(log n)
106/// nearest-neighbor searches, enabling efficient projection of large GNSS datasets.
107///
108/// # Examples
109///
110/// ```rust,no_run
111/// use tp_core::{parse_network_geojson, RailwayNetwork};
112///
113/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
114/// // Load netelements from GeoJSON
115/// let netelements = parse_network_geojson("network.geojson")?;
116///
117/// // Build spatial index
118/// let network = RailwayNetwork::new(netelements)?;
119///
120/// // Query netelements
121/// println!("Network has {} netelements", network.netelements().len());
122/// # Ok(())
123/// # }
124/// ```
125pub struct RailwayNetwork {
126 index: NetworkIndex,
127}
128
129impl RailwayNetwork {
130 /// Create a new railway network from netelements
131 ///
132 /// Builds an R-tree spatial index for efficient nearest-neighbor queries.
133 ///
134 /// # Arguments
135 ///
136 /// * `netelements` - Vector of railway track segments with LineString geometries
137 ///
138 /// # Returns
139 ///
140 /// * `Ok(RailwayNetwork)` - Successfully indexed network
141 /// * `Err(ProjectionError)` - If netelements are empty or geometries are invalid
142 ///
143 /// # Examples
144 ///
145 /// ```rust,no_run
146 /// use tp_core::{Netelement, RailwayNetwork};
147 /// use geo::LineString;
148 ///
149 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
150 /// let netelements = vec![
151 /// Netelement {
152 /// id: "NE001".to_string(),
153 /// geometry: LineString::from(vec![(4.35, 50.85), (4.36, 50.86)]),
154 /// crs: "EPSG:4326".to_string(),
155 /// },
156 /// ];
157 ///
158 /// let network = RailwayNetwork::new(netelements)?;
159 /// # Ok(())
160 /// # }
161 /// ```
162 pub fn new(netelements: Vec<Netelement>) -> Result<Self> {
163 let index = NetworkIndex::new(netelements)?;
164 Ok(Self { index })
165 }
166
167 /// Find the nearest netelement to a given point
168 ///
169 /// Uses R-tree spatial index for efficient O(log n) lookup.
170 ///
171 /// # Arguments
172 ///
173 /// * `point` - Geographic point in (longitude, latitude) coordinates
174 ///
175 /// # Returns
176 ///
177 /// Index of the nearest netelement in the network
178 pub fn find_nearest(&self, point: &Point<f64>) -> Result<usize> {
179 find_nearest_netelement(point, &self.index)
180 }
181
182 /// Get netelement by index
183 ///
184 /// # Arguments
185 ///
186 /// * `index` - Zero-based index of the netelement
187 ///
188 /// # Returns
189 ///
190 /// * `Some(&Netelement)` - If index is valid
191 /// * `None` - If index is out of bounds
192 pub fn get_by_index(&self, index: usize) -> Option<&Netelement> {
193 self.index.netelements().get(index)
194 }
195
196 /// Get all netelements
197 ///
198 /// Returns a slice containing all netelements in the network.
199 pub fn netelements(&self) -> &[Netelement] {
200 self.index.netelements()
201 }
202
203 /// Get the number of netelements in the network
204 ///
205 /// Returns the total count of railway track segments indexed in this network.
206 pub fn netelement_count(&self) -> usize {
207 self.index.netelements().len()
208 }
209}
210
211/// Project GNSS positions onto railway network
212///
213/// Projects each GNSS position onto the nearest railway netelement, calculating
214/// the measure (distance along track) and perpendicular projection distance.
215///
216/// # Algorithm
217///
218/// 1. Find nearest netelement using R-tree spatial index
219/// 2. Project GNSS point onto netelement LineString geometry
220/// 3. Calculate measure from start of netelement
221/// 4. Calculate perpendicular distance from point to line
222/// 5. Emit warning if projection distance exceeds threshold
223///
224/// # Arguments
225///
226/// * `positions` - Slice of GNSS positions with coordinates and timestamps
227/// * `network` - Railway network with spatial index
228/// * `config` - Projection configuration (warning threshold, CRS settings)
229///
230/// # Returns
231///
232/// * `Ok(Vec<ProjectedPosition>)` - Successfully projected positions
233/// * `Err(ProjectionError)` - If projection fails (invalid geometry, CRS mismatch, etc.)
234///
235/// # Examples
236///
237/// ```rust,no_run
238/// use tp_core::{parse_gnss_csv, parse_network_geojson, RailwayNetwork};
239/// use tp_core::{project_gnss, ProjectionConfig};
240///
241/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
242/// // Load data
243/// let netelements = parse_network_geojson("network.geojson")?;
244/// let network = RailwayNetwork::new(netelements)?;
245/// let positions = parse_gnss_csv("gnss.csv", "EPSG:4326", "latitude", "longitude", "timestamp")?;
246///
247/// // Project with custom warning threshold
248/// let config = ProjectionConfig {
249/// projection_distance_warning_threshold: 100.0,
250/// transform_crs: true,
251/// suppress_warnings: false,
252/// };
253/// let projected = project_gnss(&positions, &network, &config)?;
254///
255/// // Check projection quality
256/// for pos in &projected {
257/// if pos.projection_distance_meters > 50.0 {
258/// println!("Warning: large projection distance for {}", pos.netelement_id);
259/// }
260/// }
261/// # Ok(())
262/// # }
263/// ```
264///
265/// # Performance
266///
267/// - O(n log m) where n = GNSS positions, m = netelements
268/// - Spatial indexing enables efficient nearest-neighbor search
269/// - Target: <10 seconds for 1000 positions × 50 netelements
270#[tracing::instrument(skip(positions, network), fields(position_count = positions.len(), netelement_count = network.netelement_count()))]
271pub fn project_gnss(
272 positions: &[GnssPosition],
273 network: &RailwayNetwork,
274 config: &ProjectionConfig,
275) -> Result<Vec<ProjectedPosition>> {
276 tracing::info!(
277 "Starting projection of {} GNSS positions onto {} netelements",
278 positions.len(),
279 network.netelement_count()
280 );
281
282 let mut results = Vec::with_capacity(positions.len());
283
284 for (idx, gnss) in positions.iter().enumerate() {
285 // Create point from GNSS position
286 let gnss_point = Point::new(gnss.longitude, gnss.latitude);
287
288 tracing::debug!(
289 position_idx = idx,
290 latitude = gnss.latitude,
291 longitude = gnss.longitude,
292 timestamp = %gnss.timestamp,
293 crs = %gnss.crs,
294 "Processing GNSS position"
295 );
296
297 // Find nearest netelement
298 let netelement_idx = network.find_nearest(&gnss_point)?;
299 let netelement = network.get_by_index(netelement_idx).ok_or_else(|| {
300 ProjectionError::InvalidGeometry(format!(
301 "Netelement index {} out of bounds",
302 netelement_idx
303 ))
304 })?;
305
306 tracing::debug!(
307 position_idx = idx,
308 netelement_id = %netelement.id,
309 netelement_idx = netelement_idx,
310 "Assigned to nearest netelement"
311 );
312
313 // Project onto netelement
314 let projected = project_gnss_position(
315 gnss,
316 netelement.id.clone(),
317 &netelement.geometry,
318 netelement.crs.clone(),
319 )?;
320
321 tracing::debug!(
322 position_idx = idx,
323 netelement_id = %netelement.id,
324 measure_meters = projected.measure_meters,
325 projection_distance_meters = projected.projection_distance_meters,
326 "Projection completed"
327 );
328
329 // Emit warning if projection distance exceeds threshold
330 if !config.suppress_warnings
331 && projected.projection_distance_meters > config.projection_distance_warning_threshold
332 {
333 tracing::warn!(
334 position_idx = idx,
335 projection_distance_meters = projected.projection_distance_meters,
336 threshold = config.projection_distance_warning_threshold,
337 timestamp = %gnss.timestamp,
338 netelement_id = %netelement.id,
339 "Large projection distance exceeds threshold"
340 );
341
342 eprintln!(
343 "WARNING: Large projection distance ({:.2}m > {:.2}m threshold) for position at {:?}",
344 projected.projection_distance_meters,
345 config.projection_distance_warning_threshold,
346 gnss.timestamp
347 );
348 }
349
350 results.push(projected);
351 }
352
353 tracing::info!(
354 projected_count = results.len(),
355 "Projection completed successfully"
356 );
357
358 Ok(results)
359}