1use crate::errors::ProjectionError;
4use geo::Point;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct GnssNetElementLink {
34 pub gnss_index: usize,
36
37 pub netelement_id: String,
39
40 pub projected_point: Point<f64>,
42
43 pub distance_meters: f64,
45
46 pub intrinsic_coordinate: f64,
49
50 pub heading_difference: Option<f64>,
53
54 pub probability: f64,
57}
58
59impl GnssNetElementLink {
60 pub fn new(
62 gnss_index: usize,
63 netelement_id: String,
64 projected_point: Point<f64>,
65 distance_meters: f64,
66 intrinsic_coordinate: f64,
67 heading_difference: Option<f64>,
68 probability: f64,
69 ) -> Result<Self, ProjectionError> {
70 let link = Self {
71 gnss_index,
72 netelement_id,
73 projected_point,
74 distance_meters,
75 intrinsic_coordinate,
76 heading_difference,
77 probability,
78 };
79
80 link.validate()?;
81 Ok(link)
82 }
83
84 fn validate(&self) -> Result<(), ProjectionError> {
86 if self.netelement_id.is_empty() {
88 return Err(ProjectionError::InvalidGeometry(
89 "GnssNetElementLink netelement_id must not be empty".to_string(),
90 ));
91 }
92
93 if self.distance_meters < 0.0 {
95 return Err(ProjectionError::InvalidGeometry(format!(
96 "distance_meters must be non-negative, got {}",
97 self.distance_meters
98 )));
99 }
100
101 if !(0.0..=1.0).contains(&self.intrinsic_coordinate) {
103 return Err(ProjectionError::InvalidGeometry(format!(
104 "intrinsic_coordinate must be in [0, 1], got {}",
105 self.intrinsic_coordinate
106 )));
107 }
108
109 if let Some(heading_diff) = self.heading_difference {
111 if !(0.0..=180.0).contains(&heading_diff) {
112 return Err(ProjectionError::InvalidGeometry(format!(
113 "heading_difference must be in [0, 180], got {}",
114 heading_diff
115 )));
116 }
117 }
118
119 if !(0.0..=1.0).contains(&self.probability) {
121 return Err(ProjectionError::InvalidGeometry(format!(
122 "Probability must be in [0, 1], got {}",
123 self.probability
124 )));
125 }
126
127 Ok(())
128 }
129
130 pub fn is_high_probability(&self, threshold: f64) -> bool {
132 self.probability >= threshold
133 }
134
135 pub fn is_within_distance(&self, max_distance_meters: f64) -> bool {
137 self.distance_meters <= max_distance_meters
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_gnss_link_valid() {
147 let link = GnssNetElementLink::new(
148 5,
149 "NE_A".to_string(),
150 Point::new(4.3517, 50.8503),
151 3.2,
152 0.45,
153 Some(5.3),
154 0.89,
155 );
156
157 assert!(link.is_ok());
158 }
159
160 #[test]
161 fn test_gnss_link_invalid_probability() {
162 let link = GnssNetElementLink::new(
163 5,
164 "NE_A".to_string(),
165 Point::new(4.3517, 50.8503),
166 3.2,
167 0.45,
168 Some(5.3),
169 1.5, );
171
172 assert!(link.is_err());
173 }
174
175 #[test]
176 fn test_gnss_link_is_high_probability() {
177 let link = GnssNetElementLink::new(
178 5,
179 "NE_A".to_string(),
180 Point::new(4.3517, 50.8503),
181 3.2,
182 0.45,
183 Some(5.3),
184 0.89,
185 )
186 .unwrap();
187
188 assert!(link.is_high_probability(0.8));
189 assert!(link.is_high_probability(0.89));
190 assert!(!link.is_high_probability(0.9));
191 }
192
193 #[test]
194 fn test_gnss_link_is_within_distance() {
195 let link = GnssNetElementLink::new(
196 5,
197 "NE_A".to_string(),
198 Point::new(4.3517, 50.8503),
199 3.2,
200 0.45,
201 Some(5.3),
202 0.89,
203 )
204 .unwrap();
205
206 assert!(link.is_within_distance(5.0));
207 assert!(link.is_within_distance(3.2));
208 assert!(!link.is_within_distance(3.0));
209 }
210
211 #[test]
212 fn test_gnss_link_negative_distance() {
213 let link = GnssNetElementLink::new(
214 5,
215 "NE_A".to_string(),
216 Point::new(4.3517, 50.8503),
217 -1.0, 0.45,
219 Some(5.3),
220 0.89,
221 );
222
223 assert!(link.is_err());
224 }
225
226 #[test]
227 fn test_gnss_link_invalid_intrinsic() {
228 let link = GnssNetElementLink::new(
229 5,
230 "NE_A".to_string(),
231 Point::new(4.3517, 50.8503),
232 3.2,
233 1.5, Some(5.3),
235 0.89,
236 );
237
238 assert!(link.is_err());
239 }
240
241 #[test]
242 fn test_gnss_link_invalid_heading_difference() {
243 let link = GnssNetElementLink::new(
244 5,
245 "NE_A".to_string(),
246 Point::new(4.3517, 50.8503),
247 3.2,
248 0.45,
249 Some(200.0), 0.89,
251 );
252
253 assert!(link.is_err());
254 }
255
256 #[test]
257 fn test_gnss_link_no_heading_difference() {
258 let link = GnssNetElementLink::new(
259 5,
260 "NE_A".to_string(),
261 Point::new(4.3517, 50.8503),
262 3.2,
263 0.45,
264 None, 0.89,
266 );
267
268 assert!(link.is_ok());
269 }
270
271 #[test]
272 fn test_gnss_link_empty_netelement_id() {
273 let link = GnssNetElementLink::new(
274 5,
275 "".to_string(), Point::new(4.3517, 50.8503),
277 3.2,
278 0.45,
279 Some(5.3),
280 0.89,
281 );
282
283 assert!(link.is_err());
284 }
285}