1use crate::errors::ProjectionError;
4use crate::models::{GnssPosition, ProjectedPosition};
5use geo::algorithm::HaversineDistance;
6use geo::{LineString, Point};
7
8#[cfg(test)]
9use geo::Coord;
10
11pub fn project_point_onto_linestring(
17 point: &Point<f64>,
18 linestring: &LineString<f64>,
19) -> Result<Point<f64>, ProjectionError> {
20 let coords = &linestring.0;
21 if coords.len() < 2 {
22 return Err(ProjectionError::InvalidGeometry(
23 "Linestring must have at least 2 points for projection".to_string(),
24 ));
25 }
26
27 let cos_lat = point.y().to_radians().cos();
28 let mut min_dist_sq = f64::INFINITY;
29 let mut best = coords[0];
30
31 for i in 0..coords.len() - 1 {
32 let p1 = &coords[i];
33 let p2 = &coords[i + 1];
34
35 let dx = (p2.x - p1.x) * cos_lat;
38 let dy = p2.y - p1.y;
39 let len_sq = dx * dx + dy * dy;
40
41 let t = if len_sq > 0.0 {
42 let px = (point.x() - p1.x) * cos_lat;
43 let py = point.y() - p1.y;
44 ((px * dx + py * dy) / len_sq).clamp(0.0, 1.0)
45 } else {
46 0.0
47 };
48
49 let proj_x = p1.x + t * (p2.x - p1.x);
51 let proj_y = p1.y + t * (p2.y - p1.y);
52
53 let ex = (point.x() - proj_x) * cos_lat;
54 let ey = point.y() - proj_y;
55 let dist_sq = ex * ex + ey * ey;
56
57 if dist_sq < min_dist_sq {
58 min_dist_sq = dist_sq;
59 best = geo::Coord {
60 x: proj_x,
61 y: proj_y,
62 };
63 }
64 }
65
66 Ok(Point::from(best))
67}
68
69pub fn calculate_measure_along_linestring(
75 linestring: &LineString<f64>,
76 point: &Point<f64>,
77) -> Result<f64, ProjectionError> {
78 let coords = &linestring.0;
79 if coords.is_empty() {
80 return Err(ProjectionError::InvalidGeometry(
81 "Cannot calculate measure on empty linestring".to_string(),
82 ));
83 }
84 if coords.len() < 2 {
85 return Err(ProjectionError::InvalidGeometry(
86 "Linestring must have at least 2 points".to_string(),
87 ));
88 }
89
90 let cos_lat = point.y().to_radians().cos();
93 let mut min_dist_sq = f64::INFINITY;
94 let mut best_seg: usize = 0;
95 let mut best_t: f64 = 0.0;
96
97 for i in 0..coords.len() - 1 {
98 let p1 = &coords[i];
99 let p2 = &coords[i + 1];
100
101 let dx = (p2.x - p1.x) * cos_lat;
102 let dy = p2.y - p1.y;
103 let len_sq = dx * dx + dy * dy;
104
105 let t = if len_sq > 0.0 {
106 let px = (point.x() - p1.x) * cos_lat;
107 let py = point.y() - p1.y;
108 ((px * dx + py * dy) / len_sq).clamp(0.0, 1.0)
109 } else {
110 0.0
111 };
112
113 let proj_x = p1.x + t * (p2.x - p1.x);
114 let proj_y = p1.y + t * (p2.y - p1.y);
115 let ex = (point.x() - proj_x) * cos_lat;
116 let ey = point.y() - proj_y;
117 let dist_sq = ex * ex + ey * ey;
118
119 if dist_sq < min_dist_sq {
120 min_dist_sq = dist_sq;
121 best_seg = i;
122 best_t = t;
123 }
124 }
125
126 let mut measure = 0.0;
128 for i in 0..best_seg {
129 let a = Point::new(coords[i].x, coords[i].y);
130 let b = Point::new(coords[i + 1].x, coords[i + 1].y);
131 measure += a.haversine_distance(&b);
132 }
133
134 let seg_start = Point::new(coords[best_seg].x, coords[best_seg].y);
136 let seg_end = Point::new(coords[best_seg + 1].x, coords[best_seg + 1].y);
137 measure += best_t * seg_start.haversine_distance(&seg_end);
138
139 Ok(measure)
140}
141
142pub fn project_gnss_position(
144 gnss: &GnssPosition,
145 netelement_id: String,
146 linestring: &LineString<f64>,
147 crs: String,
148) -> Result<ProjectedPosition, ProjectionError> {
149 let gnss_point = Point::new(gnss.longitude, gnss.latitude);
151
152 let projected = project_point_onto_linestring(&gnss_point, linestring)?;
154
155 let measure = calculate_measure_along_linestring(linestring, &projected)?;
157
158 let projection_distance = gnss_point.haversine_distance(&projected);
160
161 Ok(ProjectedPosition::new(
162 gnss.clone(),
163 projected,
164 netelement_id,
165 measure,
166 projection_distance,
167 crs,
168 ))
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use chrono::{FixedOffset, TimeZone};
175
176 #[test]
177 fn test_project_point_on_line() {
178 let linestring =
179 LineString::from(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 10.0, y: 0.0 }]);
180
181 let point = Point::new(5.0, 2.0);
182 let projected = project_point_onto_linestring(&point, &linestring);
183
184 assert!(projected.is_ok());
185 let result = projected.unwrap();
186 assert!((result.y()).abs() < 0.1);
188 }
189
190 #[test]
191 fn test_calculate_measure_empty_linestring() {
192 let linestring = LineString::<f64>::new(vec![]);
193 let point = Point::new(4.0, 50.0);
194 let result = calculate_measure_along_linestring(&linestring, &point);
195 assert!(result.is_err());
196 if let Err(ProjectionError::InvalidGeometry(msg)) = result {
197 assert!(msg.contains("empty"));
198 }
199 }
200
201 #[test]
202 fn test_calculate_measure_single_point_linestring() {
203 let linestring = LineString::from(vec![Coord { x: 4.0, y: 50.0 }]);
204 let point = Point::new(4.0, 50.0);
205 let result = calculate_measure_along_linestring(&linestring, &point);
206 assert!(result.is_err());
207 if let Err(ProjectionError::InvalidGeometry(msg)) = result {
208 assert!(msg.contains("at least 2 points"));
209 }
210 }
211
212 #[test]
213 fn test_calculate_measure_at_start() {
214 let linestring =
216 LineString::from(vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 5.0, y: 50.0 }]);
217 let point = Point::new(4.0, 50.0);
218 let result = calculate_measure_along_linestring(&linestring, &point);
219 assert!(result.is_ok());
220 let measure = result.unwrap();
221 assert!(
223 measure < 10.0,
224 "Measure at start should be near 0, got {}",
225 measure
226 );
227 }
228
229 #[test]
230 fn test_calculate_measure_at_end() {
231 let linestring =
233 LineString::from(vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 5.0, y: 50.0 }]);
234 let point = Point::new(5.0, 50.0);
235 let result = calculate_measure_along_linestring(&linestring, &point);
236 assert!(result.is_ok());
237 let measure = result.unwrap();
238
239 let start = Point::new(4.0, 50.0);
241 let end = Point::new(5.0, 50.0);
242 let expected_length = start.haversine_distance(&end);
243
244 assert!(
246 (measure - expected_length).abs() < 10.0,
247 "Measure at end should be near {}, got {}",
248 expected_length,
249 measure
250 );
251 }
252
253 #[test]
254 fn test_calculate_measure_at_middle() {
255 let linestring = LineString::from(vec![
257 Coord { x: 4.0, y: 50.0 },
258 Coord { x: 4.5, y: 50.0 },
259 Coord { x: 5.0, y: 50.0 },
260 ]);
261 let point = Point::new(4.5, 50.0);
262 let result = calculate_measure_along_linestring(&linestring, &point);
263 assert!(result.is_ok());
264 let measure = result.unwrap();
265
266 let start = Point::new(4.0, 50.0);
268 let middle = Point::new(4.5, 50.0);
269 let expected_measure = start.haversine_distance(&middle);
270
271 assert!(
273 (measure - expected_measure).abs() < 10.0,
274 "Measure at middle should be near {}, got {}",
275 expected_measure,
276 measure
277 );
278 }
279
280 #[test]
281 fn test_calculate_measure_point_off_line() {
282 let linestring =
284 LineString::from(vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 5.0, y: 50.0 }]);
285 let point = Point::new(4.5, 50.1);
287 let result = calculate_measure_along_linestring(&linestring, &point);
288 assert!(result.is_ok());
289 let measure = result.unwrap();
290
291 let start = Point::new(4.0, 50.0);
293 let projected_approx = Point::new(4.5, 50.0);
294 let expected_measure = start.haversine_distance(&projected_approx);
295
296 assert!(
298 (measure - expected_measure).abs() < 1000.0,
299 "Measure for point off line should be near {}, got {}",
300 expected_measure,
301 measure
302 );
303 }
304
305 #[test]
306 fn test_project_gnss_position() {
307 let linestring =
309 LineString::from(vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 5.0, y: 50.0 }]);
310
311 let timestamp = FixedOffset::east_opt(0)
313 .unwrap()
314 .with_ymd_and_hms(2024, 1, 1, 12, 0, 0)
315 .unwrap();
316 let gnss = GnssPosition::new(4.5, 50.01, timestamp, "EPSG:4326".to_string()).unwrap();
317
318 let result = project_gnss_position(
319 &gnss,
320 "NE123".to_string(),
321 &linestring,
322 "EPSG:4326".to_string(),
323 );
324
325 assert!(result.is_ok());
326 let projected = result.unwrap();
327
328 assert_eq!(projected.netelement_id, "NE123");
330 assert_eq!(projected.crs, "EPSG:4326");
331
332 assert!(
334 projected.projection_distance_meters > 0.0,
335 "Projection distance should be positive, got {}",
336 projected.projection_distance_meters
337 );
338
339 assert!(
341 projected.measure_meters >= 0.0,
342 "Measure should be non-negative, got {}",
343 projected.measure_meters
344 );
345
346 let start = Point::new(4.0, 50.0);
348 let end = Point::new(5.0, 50.0);
349 let total_length = start.haversine_distance(&end);
350 assert!(
351 projected.measure_meters <= total_length + 1.0,
352 "Measure should not exceed linestring length {}, got {}",
353 total_length,
354 projected.measure_meters
355 );
356 }
357}