tp-lib-core 0.0.6

Core library for GNSS track axis projection with spatial indexing
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
//! TP-Core: Train Positioning Library - Core Engine
//!
//! This library provides geospatial projection of GNSS positions onto railway track netelements.
//!
//! # Overview
//!
//! TP-Core enables projection of GNSS (GPS) coordinates onto railway track centerlines (netelements),
//! calculating precise measures along the track and assigning positions to specific track segments.
//!
//! # Quick Start
//!
//! ```rust,no_run
//! use tp_lib_core::{parse_gnss_csv, parse_network_geojson, RailwayNetwork, project_gnss, ProjectionConfig};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Load railway network from GeoJSON
//! let (netelements, _netrelations) = parse_network_geojson("network.geojson")?;
//! let network = RailwayNetwork::new(netelements)?;
//!
//! // Load GNSS positions from CSV
//! let positions = parse_gnss_csv("gnss.csv", "EPSG:4326", "latitude", "longitude", "timestamp")?;
//!
//! // Project onto network with default configuration
//! let config = ProjectionConfig::default();
//! let projected = project_gnss(&positions, &network, &config)?;
//!
//! // Use projected results
//! for pos in projected {
//!     println!("Position at measure {} on netelement {}", pos.measure_meters, pos.netelement_id);
//! }
//! # Ok(())
//! # }
//! ```
//!
//! # Features
//!
//! - **Spatial Indexing**: R-tree based spatial indexing for efficient nearest-netelement search
//! - **CRS Support**: Explicit coordinate reference system handling with optional transformations
//! - **Timezone Awareness**: RFC3339 timestamps with explicit timezone offsets
//! - **Multiple Formats**: CSV and GeoJSON input/output support

pub mod crs;
pub mod detections;
pub mod errors;
pub mod io;
pub mod models;
pub mod path;
pub mod projection;
pub mod temporal;
pub mod workflow;

// Re-export main types for convenience
pub use detections::{
    prepare_detections, prepare_detections_from_loaded, DetectionError, PreparedDetections,
};
pub use errors::ProjectionError;
pub use io::{
    build_netelements_query, build_netrelations_query, parse_gnss_csv, parse_gnss_csv_str,
    parse_gnss_geojson, parse_gnss_geojson_str, parse_netrelations_geojson, parse_network_geojson,
    parse_network_geojson_str, parse_trainpath_csv, parse_trainpath_geojson, write_csv,
    write_geojson, write_network_geojson, write_trainpath_csv, write_trainpath_geojson,
    SparqlClient, UreqSparqlClient,
};
pub use models::{
    AssociatedNetElement,
    // Feature 004: detections
    Detection,
    DetectionKind,
    DetectionRecord,
    DetectionStatus,
    DiscardReason,
    GeographicLocation,
    GnssNetElementLink,
    GnssPosition,
    LinearDetection,
    NetRelation,
    Netelement,
    PathDiagnosticInfo,
    PathMetadata,
    PathOrigin,
    ProjectedPosition,
    PunctualDetection,
    ResolvedAnchor,
    // Feature 006: RINF retrieval
    RetrievalArea,
    RetrievalOutcome,
    RetrievalStatus,
    RetrievedTopology,
    RinfNavigability,
    RinfNetelementRow,
    RinfNetrelationRow,
    SegmentDiagnostic,
    TimestampOrRange,
    TopologySource,
    TopologyValidationReport,
    TopologyValidationStatus,
    TrainPath,
    WorkflowKind,
    DEFAULT_RETRIEVAL_BUFFER_METERS,
    DEFAULT_RINF_ENDPOINT,
};
pub use path::{
    calculate_mean_spacing,
    calculate_train_path,
    export_all_debug_info,
    project_onto_path,
    select_resampled_subset,
    CandidateInfo,
    CandidatePath,
    // Debug info types (US7)
    DebugInfo,
    PathCalculationMode,
    PathConfig,
    PathConfigBuilder,
    PathDecision,
    PathResult,
    PositionCandidates,
    TransitionProbabilityEntry,
};
pub use workflow::{build_retrieval_area, resolve_topology, validate_topology, RetrievalConfig};

/// Result type alias using ProjectionError
pub type Result<T> = std::result::Result<T, ProjectionError>;

use geo::Point;
use projection::geom::project_gnss_position;
use projection::spatial::{find_nearest_netelement, NetworkIndex};

/// Configuration for GNSS projection operations
///
/// # Fields
///
/// * `projection_distance_warning_threshold` - Distance in meters above which warnings are emitted
/// * `suppress_warnings` - If true, suppresses console warnings during projection
///
/// # Examples
///
/// ```
/// use tp_lib_core::ProjectionConfig;
///
/// // Use default configuration (50m warning threshold)
/// let config = ProjectionConfig::default();
///
/// // Custom configuration with higher threshold
/// let config = ProjectionConfig {
///     projection_distance_warning_threshold: 100.0,
///     suppress_warnings: false,
/// };
/// ```
#[derive(Debug, Clone)]
pub struct ProjectionConfig {
    /// Threshold distance in meters for emitting warnings about large projection distances
    pub projection_distance_warning_threshold: f64,
    /// Whether to suppress console warnings (useful for benchmarking)
    pub suppress_warnings: bool,
}

impl Default for ProjectionConfig {
    fn default() -> Self {
        Self {
            projection_distance_warning_threshold: 50.0,
            suppress_warnings: false,
        }
    }
}

/// Railway network with spatial indexing for efficient projection
///
/// The `RailwayNetwork` wraps netelements with an R-tree spatial index for O(log n)
/// nearest-neighbor searches, enabling efficient projection of large GNSS datasets.
///
/// # Examples
///
/// ```rust,no_run
/// use tp_lib_core::{parse_network_geojson, RailwayNetwork};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Load netelements from GeoJSON
/// let (netelements, _netrelations) = parse_network_geojson("network.geojson")?;
///
/// // Build spatial index
/// let network = RailwayNetwork::new(netelements)?;
///
/// // Query netelements
/// println!("Network has {} netelements", network.netelements().len());
/// # Ok(())
/// # }
/// ```
pub struct RailwayNetwork {
    index: NetworkIndex,
}

impl Clone for RailwayNetwork {
    fn clone(&self) -> Self {
        Self {
            index: self.index.clone(),
        }
    }
}

impl RailwayNetwork {
    /// Create a new railway network from netelements
    ///
    /// Builds an R-tree spatial index for efficient nearest-neighbor queries.
    ///
    /// # Arguments
    ///
    /// * `netelements` - Vector of railway track segments with LineString geometries
    ///
    /// # Returns
    ///
    /// * `Ok(RailwayNetwork)` - Successfully indexed network
    /// * `Err(ProjectionError)` - If netelements are empty or geometries are invalid
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use tp_lib_core::{Netelement, RailwayNetwork};
    /// use geo::LineString;
    ///
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let netelements = vec![
    ///     Netelement {
    ///         id: "NE001".to_string(),
    ///         geometry: LineString::from(vec![(4.35, 50.85), (4.36, 50.86)]),
    ///         crs: "EPSG:4326".to_string(),
    ///     },
    /// ];
    ///
    /// let network = RailwayNetwork::new(netelements)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn new(netelements: Vec<Netelement>) -> Result<Self> {
        let index = NetworkIndex::new(netelements)?;
        Ok(Self { index })
    }

    /// Find the nearest netelement to a given point
    ///
    /// Uses R-tree spatial index for efficient O(log n) lookup.
    ///
    /// # Arguments
    ///
    /// * `point` - Geographic point in (longitude, latitude) coordinates
    ///
    /// # Returns
    ///
    /// Index of the nearest netelement in the network
    pub fn find_nearest(&self, point: &Point<f64>) -> Result<usize> {
        find_nearest_netelement(point, &self.index)
    }

    /// Get netelement by index
    ///
    /// # Arguments
    ///
    /// * `index` - Zero-based index of the netelement
    ///
    /// # Returns
    ///
    /// * `Some(&Netelement)` - If index is valid
    /// * `None` - If index is out of bounds
    pub fn get_by_index(&self, index: usize) -> Option<&Netelement> {
        self.index.netelements().get(index)
    }

    /// Get all netelements
    ///
    /// Returns a slice containing all netelements in the network.
    pub fn netelements(&self) -> &[Netelement] {
        self.index.netelements()
    }

    /// Get the number of netelements in the network
    ///
    /// Returns the total count of railway track segments indexed in this network.
    pub fn netelement_count(&self) -> usize {
        self.index.netelements().len()
    }

    /// Get the number of netelements in the network
    ///
    /// Alias for `netelement_count()` for convenience.
    pub fn len(&self) -> usize {
        self.netelement_count()
    }

    /// Check if the network has no netelements
    pub fn is_empty(&self) -> bool {
        self.netelement_count() == 0
    }

    /// Iterate over all netelements
    pub fn iter(&self) -> impl Iterator<Item = &Netelement> {
        self.index.netelements().iter()
    }
}

/// Project GNSS positions onto railway network
///
/// Projects each GNSS position onto the nearest railway netelement, calculating
/// the measure (distance along track) and perpendicular projection distance.
///
/// # Algorithm
///
/// 1. Find nearest netelement using R-tree spatial index
/// 2. Project GNSS point onto netelement LineString geometry
/// 3. Calculate measure from start of netelement
/// 4. Calculate perpendicular distance from point to line
/// 5. Emit warning if projection distance exceeds threshold
///
/// # Arguments
///
/// * `positions` - Slice of GNSS positions with coordinates and timestamps
/// * `network` - Railway network with spatial index
/// * `config` - Projection configuration (warning threshold, CRS settings)
///
/// # Returns
///
/// * `Ok(Vec<ProjectedPosition>)` - Successfully projected positions
/// * `Err(ProjectionError)` - If projection fails (invalid geometry, CRS mismatch, etc.)
///
/// # Examples
///
/// ```rust,no_run
/// use tp_lib_core::{parse_gnss_csv, parse_network_geojson, RailwayNetwork};
/// use tp_lib_core::{project_gnss, ProjectionConfig};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Load data
/// let (netelements, _netrelations) = parse_network_geojson("network.geojson")?;
/// let network = RailwayNetwork::new(netelements)?;
/// let positions = parse_gnss_csv("gnss.csv", "EPSG:4326", "latitude", "longitude", "timestamp")?;
///
/// // Project with custom warning threshold
/// let config = ProjectionConfig {
///     projection_distance_warning_threshold: 100.0,
///     suppress_warnings: false,
/// };
/// let projected = project_gnss(&positions, &network, &config)?;
///
/// // Check projection quality
/// for pos in &projected {
///     if pos.projection_distance_meters > 50.0 {
///         println!("Warning: large projection distance for {}", pos.netelement_id);
///     }
/// }
/// # Ok(())
/// # }
/// ```
///
/// # Performance
///
/// - O(n log m) where n = GNSS positions, m = netelements
/// - Spatial indexing enables efficient nearest-neighbor search
/// - Target: <10 seconds for 1000 positions × 50 netelements
#[tracing::instrument(skip(positions, network), fields(position_count = positions.len(), netelement_count = network.netelement_count()))]
pub fn project_gnss(
    positions: &[GnssPosition],
    network: &RailwayNetwork,
    config: &ProjectionConfig,
) -> Result<Vec<ProjectedPosition>> {
    tracing::info!(
        "Starting projection of {} GNSS positions onto {} netelements",
        positions.len(),
        network.netelement_count()
    );

    let mut results = Vec::with_capacity(positions.len());

    for (idx, gnss) in positions.iter().enumerate() {
        // Create point from GNSS position
        let gnss_point = Point::new(gnss.longitude, gnss.latitude);

        tracing::debug!(
            position_idx = idx,
            latitude = gnss.latitude,
            longitude = gnss.longitude,
            timestamp = %gnss.timestamp,
            crs = %gnss.crs,
            "Processing GNSS position"
        );

        // Find nearest netelement
        let netelement_idx = network.find_nearest(&gnss_point)?;
        let netelement = network.get_by_index(netelement_idx).ok_or_else(|| {
            ProjectionError::InvalidGeometry(format!(
                "Netelement index {} out of bounds",
                netelement_idx
            ))
        })?;

        tracing::debug!(
            position_idx = idx,
            netelement_id = %netelement.id,
            netelement_idx = netelement_idx,
            "Assigned to nearest netelement"
        );

        // Project onto netelement
        let projected = project_gnss_position(
            gnss,
            netelement.id.clone(),
            &netelement.geometry,
            netelement.crs.clone(),
        )?;

        tracing::debug!(
            position_idx = idx,
            netelement_id = %netelement.id,
            measure_meters = projected.measure_meters,
            projection_distance_meters = projected.projection_distance_meters,
            "Projection completed"
        );

        // Emit warning if projection distance exceeds threshold
        if !config.suppress_warnings
            && projected.projection_distance_meters > config.projection_distance_warning_threshold
        {
            tracing::warn!(
                position_idx = idx,
                projection_distance_meters = projected.projection_distance_meters,
                threshold = config.projection_distance_warning_threshold,
                timestamp = %gnss.timestamp,
                netelement_id = %netelement.id,
                "Large projection distance exceeds threshold"
            );

            eprintln!(
                "WARNING: Large projection distance ({:.2}m > {:.2}m threshold) for position at {:?}",
                projected.projection_distance_meters,
                config.projection_distance_warning_threshold,
                gnss.timestamp
            );
        }

        results.push(projected);
    }

    tracing::info!(
        projected_count = results.len(),
        "Projection completed successfully"
    );

    Ok(results)
}

#[cfg(test)]
mod tests;