Skip to main content

tp_lib_core/models/
detection.rs

1//! Detection input types (T007).
2//!
3//! Types modelling absolute train-position detections (punctual or linear)
4//! supplied by the user as anchors for path calculation.
5//!
6//! See `specs/004-train-detections/data-model.md`.
7
8use std::collections::BTreeMap;
9use std::ops::RangeInclusive;
10
11use chrono::{DateTime, FixedOffset};
12use serde::{Deserialize, Serialize};
13
14/// Topological reference to a position on a netelement.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct TopologicalLocation {
17    /// Identifier of the netelement.
18    pub netelement_id: String,
19    /// Position along the netelement, 0.0 = start, 1.0 = end.
20    pub intrinsic: f64,
21}
22
23/// Geographic (lat/lon + CRS) reference for a punctual detection.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct GeographicLocation {
26    pub latitude: f64,
27    pub longitude: f64,
28    /// Authoritative CRS of `(latitude, longitude)`, e.g. `"EPSG:4326"`.
29    pub crs: String,
30}
31
32/// A punctual detection: train was at a precise (timestamp, position).
33///
34/// Either `location` (topological) or `coordinates` (geographic) MUST be
35/// supplied — never both. The combination is validated at load time.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct PunctualDetection {
38    pub timestamp: DateTime<FixedOffset>,
39    /// Topological position, mutually exclusive with `coordinates`.
40    pub location: Option<TopologicalLocation>,
41    /// Geographic position, mutually exclusive with `location`.
42    pub coordinates: Option<GeographicLocation>,
43    /// Optional intrinsic to associate with `coordinates` once resolved
44    /// (currently unused at load time; reserved for future enhancements).
45    pub intrinsic: Option<f64>,
46    /// Optional caller-supplied stable identifier (free-form).
47    pub id: Option<String>,
48    /// Free-form source label (e.g. `"axle-counter-A12"`).
49    pub source: Option<String>,
50    /// Provenance: origin file path.
51    pub source_file: String,
52    /// Provenance: origin row index (CSV) or feature index (GeoJSON).
53    pub source_row: usize,
54    /// Unknown / extra columns or properties, captured verbatim.
55    pub metadata: BTreeMap<String, String>,
56}
57
58/// A linear detection: train was somewhere on `netelement_id` between `t_from` and `t_to`.
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60pub struct LinearDetection {
61    pub t_from: DateTime<FixedOffset>,
62    pub t_to: DateTime<FixedOffset>,
63    pub netelement_id: String,
64    pub start_intrinsic: f64,
65    pub end_intrinsic: f64,
66    pub id: Option<String>,
67    pub source: Option<String>,
68    pub source_file: String,
69    pub source_row: usize,
70    pub metadata: BTreeMap<String, String>,
71}
72
73/// A detection of either kind, as parsed from input.
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75#[serde(tag = "kind", rename_all = "lowercase")]
76pub enum Detection {
77    Punctual(PunctualDetection),
78    Linear(LinearDetection),
79}
80
81impl Detection {
82    pub fn source_file(&self) -> &str {
83        match self {
84            Detection::Punctual(p) => &p.source_file,
85            Detection::Linear(l) => &l.source_file,
86        }
87    }
88
89    pub fn source_row(&self) -> usize {
90        match self {
91            Detection::Punctual(p) => p.source_row,
92            Detection::Linear(l) => l.source_row,
93        }
94    }
95}
96
97/// A detection successfully resolved into a Viterbi anchor.
98///
99/// Linear anchors carry a `gnss_range` indexed into the GNSS observation array
100/// (covering every index whose timestamp falls within `[t_from, t_to]`).
101#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
102pub enum ResolvedAnchor {
103    Punctual {
104        netelement_id: String,
105        intrinsic: f64,
106        gnss_index: usize,
107    },
108    Linear {
109        netelement_id: String,
110        start_intrinsic: f64,
111        end_intrinsic: f64,
112        gnss_range: RangeInclusive<usize>,
113    },
114}
115
116impl ResolvedAnchor {
117    /// First GNSS index this anchor affects (used for ordering).
118    pub fn first_index(&self) -> usize {
119        match self {
120            ResolvedAnchor::Punctual { gnss_index, .. } => *gnss_index,
121            ResolvedAnchor::Linear { gnss_range, .. } => *gnss_range.start(),
122        }
123    }
124
125    /// Netelement this anchor pins.
126    pub fn netelement_id(&self) -> &str {
127        match self {
128            ResolvedAnchor::Punctual { netelement_id, .. }
129            | ResolvedAnchor::Linear { netelement_id, .. } => netelement_id,
130        }
131    }
132}