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