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}