Skip to main content

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;