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_lib_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, _netrelations) = 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 path;
47pub mod projection;
48pub mod temporal;
49
50// Re-export main types for convenience
51pub use errors::ProjectionError;
52pub use io::{
53 parse_gnss_csv, parse_gnss_geojson, parse_netrelations_geojson, parse_network_geojson,
54 parse_trainpath_csv, parse_trainpath_geojson, write_csv, write_geojson, write_trainpath_csv,
55 write_trainpath_geojson,
56};
57pub use models::{
58 AssociatedNetElement, GnssNetElementLink, GnssPosition, NetRelation, Netelement,
59 PathDiagnosticInfo, PathMetadata, PathOrigin, ProjectedPosition, SegmentDiagnostic, TrainPath,
60};
61pub use path::{
62 calculate_mean_spacing,
63 calculate_train_path,
64 export_all_debug_info,
65 project_onto_path,
66 select_resampled_subset,
67 CandidateInfo,
68 CandidatePath,
69 // Debug info types (US7)
70 DebugInfo,
71 PathCalculationMode,
72 PathConfig,
73 PathConfigBuilder,
74 PathDecision,
75 PathResult,
76 PositionCandidates,
77 TransitionProbabilityEntry,
78};
79
80/// Result type alias using ProjectionError
81pub type Result<T> = std::result::Result<T, ProjectionError>;
82
83use geo::Point;
84use projection::geom::project_gnss_position;
85use projection::spatial::{find_nearest_netelement, NetworkIndex};
86
87/// Configuration for GNSS projection operations
88///
89/// # Fields
90///
91/// * `projection_distance_warning_threshold` - Distance in meters above which warnings are emitted
92/// * `suppress_warnings` - If true, suppresses console warnings during projection
93///
94/// # Examples
95///
96/// ```
97/// use tp_lib_core::ProjectionConfig;
98///
99/// // Use default configuration (50m warning threshold)
100/// let config = ProjectionConfig::default();
101///
102/// // Custom configuration with higher threshold
103/// let config = ProjectionConfig {
104/// projection_distance_warning_threshold: 100.0,
105/// suppress_warnings: false,
106/// };
107/// ```
108#[derive(Debug, Clone)]
109pub struct ProjectionConfig {
110 /// Threshold distance in meters for emitting warnings about large projection distances
111 pub projection_distance_warning_threshold: f64,
112 /// Whether to suppress console warnings (useful for benchmarking)
113 pub suppress_warnings: bool,
114}
115
116impl Default for ProjectionConfig {
117 fn default() -> Self {
118 Self {
119 projection_distance_warning_threshold: 50.0,
120 suppress_warnings: false,
121 }
122 }
123}
124
125/// Railway network with spatial indexing for efficient projection
126///
127/// The `RailwayNetwork` wraps netelements with an R-tree spatial index for O(log n)
128/// nearest-neighbor searches, enabling efficient projection of large GNSS datasets.
129///
130/// # Examples
131///
132/// ```rust,no_run
133/// use tp_lib_core::{parse_network_geojson, RailwayNetwork};
134///
135/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
136/// // Load netelements from GeoJSON
137/// let (netelements, _netrelations) = parse_network_geojson("network.geojson")?;
138///
139/// // Build spatial index
140/// let network = RailwayNetwork::new(netelements)?;
141///
142/// // Query netelements
143/// println!("Network has {} netelements", network.netelements().len());
144/// # Ok(())
145/// # }
146/// ```
147pub struct RailwayNetwork {
148 index: NetworkIndex,
149}
150
151impl Clone for RailwayNetwork {
152 fn clone(&self) -> Self {
153 Self {
154 index: self.index.clone(),
155 }
156 }
157}
158
159impl RailwayNetwork {
160 /// Create a new railway network from netelements
161 ///
162 /// Builds an R-tree spatial index for efficient nearest-neighbor queries.
163 ///
164 /// # Arguments
165 ///
166 /// * `netelements` - Vector of railway track segments with LineString geometries
167 ///
168 /// # Returns
169 ///
170 /// * `Ok(RailwayNetwork)` - Successfully indexed network
171 /// * `Err(ProjectionError)` - If netelements are empty or geometries are invalid
172 ///
173 /// # Examples
174 ///
175 /// ```rust,no_run
176 /// use tp_lib_core::{Netelement, RailwayNetwork};
177 /// use geo::LineString;
178 ///
179 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
180 /// let netelements = vec![
181 /// Netelement {
182 /// id: "NE001".to_string(),
183 /// geometry: LineString::from(vec![(4.35, 50.85), (4.36, 50.86)]),
184 /// crs: "EPSG:4326".to_string(),
185 /// },
186 /// ];
187 ///
188 /// let network = RailwayNetwork::new(netelements)?;
189 /// # Ok(())
190 /// # }
191 /// ```
192 pub fn new(netelements: Vec<Netelement>) -> Result<Self> {
193 let index = NetworkIndex::new(netelements)?;
194 Ok(Self { index })
195 }
196
197 /// Find the nearest netelement to a given point
198 ///
199 /// Uses R-tree spatial index for efficient O(log n) lookup.
200 ///
201 /// # Arguments
202 ///
203 /// * `point` - Geographic point in (longitude, latitude) coordinates
204 ///
205 /// # Returns
206 ///
207 /// Index of the nearest netelement in the network
208 pub fn find_nearest(&self, point: &Point<f64>) -> Result<usize> {
209 find_nearest_netelement(point, &self.index)
210 }
211
212 /// Get netelement by index
213 ///
214 /// # Arguments
215 ///
216 /// * `index` - Zero-based index of the netelement
217 ///
218 /// # Returns
219 ///
220 /// * `Some(&Netelement)` - If index is valid
221 /// * `None` - If index is out of bounds
222 pub fn get_by_index(&self, index: usize) -> Option<&Netelement> {
223 self.index.netelements().get(index)
224 }
225
226 /// Get all netelements
227 ///
228 /// Returns a slice containing all netelements in the network.
229 pub fn netelements(&self) -> &[Netelement] {
230 self.index.netelements()
231 }
232
233 /// Get the number of netelements in the network
234 ///
235 /// Returns the total count of railway track segments indexed in this network.
236 pub fn netelement_count(&self) -> usize {
237 self.index.netelements().len()
238 }
239
240 /// Get the number of netelements in the network
241 ///
242 /// Alias for `netelement_count()` for convenience.
243 pub fn len(&self) -> usize {
244 self.netelement_count()
245 }
246
247 /// Check if the network has no netelements
248 pub fn is_empty(&self) -> bool {
249 self.netelement_count() == 0
250 }
251
252 /// Iterate over all netelements
253 pub fn iter(&self) -> impl Iterator<Item = &Netelement> {
254 self.index.netelements().iter()
255 }
256}
257
258/// Project GNSS positions onto railway network
259///
260/// Projects each GNSS position onto the nearest railway netelement, calculating
261/// the measure (distance along track) and perpendicular projection distance.
262///
263/// # Algorithm
264///
265/// 1. Find nearest netelement using R-tree spatial index
266/// 2. Project GNSS point onto netelement LineString geometry
267/// 3. Calculate measure from start of netelement
268/// 4. Calculate perpendicular distance from point to line
269/// 5. Emit warning if projection distance exceeds threshold
270///
271/// # Arguments
272///
273/// * `positions` - Slice of GNSS positions with coordinates and timestamps
274/// * `network` - Railway network with spatial index
275/// * `config` - Projection configuration (warning threshold, CRS settings)
276///
277/// # Returns
278///
279/// * `Ok(Vec<ProjectedPosition>)` - Successfully projected positions
280/// * `Err(ProjectionError)` - If projection fails (invalid geometry, CRS mismatch, etc.)
281///
282/// # Examples
283///
284/// ```rust,no_run
285/// use tp_lib_core::{parse_gnss_csv, parse_network_geojson, RailwayNetwork};
286/// use tp_lib_core::{project_gnss, ProjectionConfig};
287///
288/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
289/// // Load data
290/// let (netelements, _netrelations) = parse_network_geojson("network.geojson")?;
291/// let network = RailwayNetwork::new(netelements)?;
292/// let positions = parse_gnss_csv("gnss.csv", "EPSG:4326", "latitude", "longitude", "timestamp")?;
293///
294/// // Project with custom warning threshold
295/// let config = ProjectionConfig {
296/// projection_distance_warning_threshold: 100.0,
297/// suppress_warnings: false,
298/// };
299/// let projected = project_gnss(&positions, &network, &config)?;
300///
301/// // Check projection quality
302/// for pos in &projected {
303/// if pos.projection_distance_meters > 50.0 {
304/// println!("Warning: large projection distance for {}", pos.netelement_id);
305/// }
306/// }
307/// # Ok(())
308/// # }
309/// ```
310///
311/// # Performance
312///
313/// - O(n log m) where n = GNSS positions, m = netelements
314/// - Spatial indexing enables efficient nearest-neighbor search
315/// - Target: <10 seconds for 1000 positions × 50 netelements
316#[tracing::instrument(skip(positions, network), fields(position_count = positions.len(), netelement_count = network.netelement_count()))]
317pub fn project_gnss(
318 positions: &[GnssPosition],
319 network: &RailwayNetwork,
320 config: &ProjectionConfig,
321) -> Result<Vec<ProjectedPosition>> {
322 tracing::info!(
323 "Starting projection of {} GNSS positions onto {} netelements",
324 positions.len(),
325 network.netelement_count()
326 );
327
328 let mut results = Vec::with_capacity(positions.len());
329
330 for (idx, gnss) in positions.iter().enumerate() {
331 // Create point from GNSS position
332 let gnss_point = Point::new(gnss.longitude, gnss.latitude);
333
334 tracing::debug!(
335 position_idx = idx,
336 latitude = gnss.latitude,
337 longitude = gnss.longitude,
338 timestamp = %gnss.timestamp,
339 crs = %gnss.crs,
340 "Processing GNSS position"
341 );
342
343 // Find nearest netelement
344 let netelement_idx = network.find_nearest(&gnss_point)?;
345 let netelement = network.get_by_index(netelement_idx).ok_or_else(|| {
346 ProjectionError::InvalidGeometry(format!(
347 "Netelement index {} out of bounds",
348 netelement_idx
349 ))
350 })?;
351
352 tracing::debug!(
353 position_idx = idx,
354 netelement_id = %netelement.id,
355 netelement_idx = netelement_idx,
356 "Assigned to nearest netelement"
357 );
358
359 // Project onto netelement
360 let projected = project_gnss_position(
361 gnss,
362 netelement.id.clone(),
363 &netelement.geometry,
364 netelement.crs.clone(),
365 )?;
366
367 tracing::debug!(
368 position_idx = idx,
369 netelement_id = %netelement.id,
370 measure_meters = projected.measure_meters,
371 projection_distance_meters = projected.projection_distance_meters,
372 "Projection completed"
373 );
374
375 // Emit warning if projection distance exceeds threshold
376 if !config.suppress_warnings
377 && projected.projection_distance_meters > config.projection_distance_warning_threshold
378 {
379 tracing::warn!(
380 position_idx = idx,
381 projection_distance_meters = projected.projection_distance_meters,
382 threshold = config.projection_distance_warning_threshold,
383 timestamp = %gnss.timestamp,
384 netelement_id = %netelement.id,
385 "Large projection distance exceeds threshold"
386 );
387
388 eprintln!(
389 "WARNING: Large projection distance ({:.2}m > {:.2}m threshold) for position at {:?}",
390 projected.projection_distance_meters,
391 config.projection_distance_warning_threshold,
392 gnss.timestamp
393 );
394 }
395
396 results.push(projected);
397 }
398
399 tracing::info!(
400 projected_count = results.len(),
401 "Projection completed successfully"
402 );
403
404 Ok(results)
405}
406
407#[cfg(test)]
408mod tests;