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;