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 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;