tp_lib_core/models/
netelement.rs

1//! Netelement (railway track segment) data model
2
3use crate::errors::ProjectionError;
4use geo::LineString;
5use serde::{Deserialize, Serialize};
6
7#[cfg(test)]
8use geo::Coord;
9
10/// Represents a railway track segment (netelement)
11///
12/// A `Netelement` is a portion of railway track represented as a LineString geometry.
13/// The geometry defines the track centerline, and GNSS positions are projected onto
14/// the closest point on this centerline.
15///
16/// # Validation
17///
18/// - ID must be non-empty
19/// - Geometry must have at least 2 points
20/// - LineString coordinates must be valid
21///
22/// # Examples
23///
24/// ```
25/// use tp_core::Netelement;
26/// use geo::LineString;
27///
28/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
29/// let geometry = LineString::from(vec![
30///     (4.35, 50.85),  // (lon, lat) coordinates
31///     (4.36, 50.86),
32/// ]);
33///
34/// let netelement = Netelement::new(
35///     "NE001".to_string(),
36///     geometry,
37///     "EPSG:4326".to_string(),
38/// )?;
39///
40/// assert_eq!(netelement.id, "NE001");
41/// assert_eq!(netelement.geometry.coords().count(), 2);
42/// # Ok(())
43/// # }
44/// ```
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Netelement {
47    /// Unique identifier for the netelement
48    pub id: String,
49
50    /// LineString geometry representing the track centerline
51    pub geometry: LineString<f64>,
52
53    /// Coordinate Reference System (e.g., "EPSG:4326" for WGS84)
54    pub crs: String,
55}
56
57impl Netelement {
58    /// Create a new netelement with validation
59    pub fn new(
60        id: String,
61        geometry: LineString<f64>,
62        crs: String,
63    ) -> Result<Self, ProjectionError> {
64        let netelement = Self { id, geometry, crs };
65
66        netelement.validate()?;
67        Ok(netelement)
68    }
69
70    /// Validate netelement ID is non-empty
71    pub fn validate_id(&self) -> Result<(), ProjectionError> {
72        if self.id.is_empty() {
73            return Err(ProjectionError::InvalidGeometry(
74                "Netelement ID must not be empty".to_string(),
75            ));
76        }
77        Ok(())
78    }
79
80    /// Validate geometry has at least 2 points
81    pub fn validate_geometry(&self) -> Result<(), ProjectionError> {
82        let count = self.geometry.coords().count();
83        if count < 2 {
84            return Err(ProjectionError::InvalidGeometry(format!(
85                "LineString must have at least 2 points, got {}",
86                count
87            )));
88        }
89        Ok(())
90    }
91
92    /// Validate all fields
93    fn validate(&self) -> Result<(), ProjectionError> {
94        self.validate_id()?;
95        self.validate_geometry()?;
96        Ok(())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_valid_netelement() {
106        let coords = vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 4.0, y: 51.0 }];
107        let linestring = LineString::from(coords);
108
109        let netelement = Netelement::new("NE001".to_string(), linestring, "EPSG:4326".to_string());
110
111        assert!(netelement.is_ok());
112    }
113
114    #[test]
115    fn test_empty_id() {
116        let coords = vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 4.0, y: 51.0 }];
117        let linestring = LineString::from(coords);
118
119        let netelement = Netelement::new(
120            "".to_string(), // Invalid
121            linestring,
122            "EPSG:4326".to_string(),
123        );
124
125        assert!(netelement.is_err());
126    }
127
128    #[test]
129    fn test_invalid_geometry() {
130        let coords = vec![
131            Coord { x: 4.0, y: 50.0 }, // Only 1 point
132        ];
133        let linestring = LineString::from(coords);
134
135        let netelement = Netelement::new("NE001".to_string(), linestring, "EPSG:4326".to_string());
136
137        assert!(netelement.is_err());
138    }
139}