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