1use nalgebra::Vector3;
2use std::cmp::Ordering;
3use std::f64::consts::PI;
4
5const KMH_TO_MPS: f64 = 1000.0 / 3600.0;
7
8pub fn wind_vector(speed_mps: f64, direction_rad: f64, vertical_mps: f64) -> Vector3<f64> {
13 Vector3::new(
14 -speed_mps * direction_rad.cos(),
15 vertical_mps,
16 -speed_mps * direction_rad.sin(),
17 )
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Default)]
27pub struct WindSegment {
28 pub speed_kmh: f64,
29 pub angle_deg: f64,
30 pub until_m: f64,
31 pub vertical_mps: f64,
32}
33
34impl WindSegment {
35 pub fn new(speed_kmh: f64, angle_deg: f64, until_m: f64) -> Self {
38 Self {
39 speed_kmh,
40 angle_deg,
41 until_m,
42 vertical_mps: 0.0,
43 }
44 }
45}
46
47pub(crate) fn sort_wind_segments_by_distance(segments: &mut [WindSegment]) {
52 segments.sort_by(|a, b| match (a.until_m.is_nan(), b.until_m.is_nan()) {
53 (true, true) => Ordering::Equal,
54 (true, false) => Ordering::Greater,
55 (false, true) => Ordering::Less,
56 (false, false) => {
57 a.until_m
58 .partial_cmp(&b.until_m)
59 .expect("non-NaN distances are ordered")
60 }
61 });
62}
63
64#[derive(Debug, Clone)]
66pub struct WindSock {
67 winds: Vec<WindSegment>,
69 wind_vecs: Vec<Vector3<f64>>,
72 current: usize,
74 next_range: f64,
76 current_vec: Vector3<f64>,
78}
79
80impl WindSock {
81 pub fn new(mut segments: Vec<WindSegment>) -> Self {
86 sort_wind_segments_by_distance(&mut segments);
88
89 let wind_vecs: Vec<Vector3<f64>> = segments.iter().map(Self::calc_vec).collect();
91
92 let (current, next_range, current_vec) = if segments.is_empty() {
93 (0, f64::INFINITY, Vector3::zeros())
94 } else {
95 (0, segments[0].until_m, wind_vecs[0])
96 };
97
98 WindSock {
99 winds: segments,
100 wind_vecs,
101 current,
102 next_range,
103 current_vec,
104 }
105 }
106
107 fn calc_vec(seg: &WindSegment) -> Vector3<f64> {
109 let speed_mps = seg.speed_kmh * KMH_TO_MPS;
111 let angle_rad = seg.angle_deg * PI / 180.0;
112
113 wind_vector(speed_mps, angle_rad, seg.vertical_mps)
122 }
123
124 pub fn vector_for_range(&mut self, range_m: f64) -> Vector3<f64> {
129 if range_m.is_nan() {
131 return Vector3::zeros();
132 }
133
134 while range_m >= self.next_range && self.current < self.winds.len() {
137 self.current += 1;
138 if self.current >= self.winds.len() {
139 self.current_vec = Vector3::zeros();
140 self.next_range = f64::INFINITY;
141 } else {
142 self.current_vec = self.wind_vecs[self.current];
143 self.next_range = self.winds[self.current].until_m;
144 }
145 }
146
147 self.current_vec
148 }
149
150 pub fn vector_for_range_stateless(&self, range_m: f64) -> Vector3<f64> {
155 if range_m.is_nan() {
157 return Vector3::zeros();
158 }
159
160 for (i, segment) in self.winds.iter().enumerate() {
162 if range_m < segment.until_m {
163 return self.wind_vecs[i];
164 }
165 }
166
167 Vector3::zeros()
169 }
170}
171
172pub fn parse_wind_segment_str(s: &str, imperial: bool) -> Result<WindSegment, String> {
186 let parts: Vec<&str> = s.split(':').collect();
187 if parts.len() != 3 && parts.len() != 4 {
188 return Err(format!(
189 "invalid wind segment '{s}': expected SPEED:ANGLE:UNTIL_DISTANCE[:VERTICAL] \
190 (three or four colon-separated numbers; the optional 4th field VERTICAL is always \
191 m/s, positive = updraft, regardless of --units)"
192 ));
193 }
194 let num = |i: usize, name: &str| -> Result<f64, String> {
195 parts[i].trim().parse::<f64>().map_err(|_| {
196 format!("invalid wind segment '{s}': {name} '{}' is not a number", parts[i])
197 })
198 };
199 let speed = num(0, "speed")?;
200 let angle = num(1, "angle")?;
201 let until = num(2, "until-distance")?;
202 let vertical = if parts.len() == 4 {
203 num(3, "vertical")?
204 } else {
205 0.0
206 };
207 if !speed.is_finite() || !angle.is_finite() || !until.is_finite() || !vertical.is_finite() {
208 return Err(format!(
209 "invalid wind segment '{s}': speed, angle, until-distance, and vertical (m/s, \
210 positive = updraft) must be finite numbers"
211 ));
212 }
213 if speed < 0.0 {
214 return Err(format!("invalid wind segment '{s}': speed must be >= 0"));
215 }
216 if until <= 0.0 {
217 return Err(format!("invalid wind segment '{s}': until-distance must be > 0"));
218 }
219 let (speed_kmh, until_m) = if imperial {
220 (speed * 1.609344, until * 0.9144) } else {
222 (speed * 3.6, until) };
224 let mut segment = WindSegment::new(speed_kmh, angle, until_m);
225 segment.vertical_mps = vertical;
226 Ok(segment)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn segment_sort_is_stable_and_places_nan_endpoints_last() {
235 let mut segments = vec![
236 WindSegment::new(10.0, 0.0, f64::NAN),
237 WindSegment::new(20.0, 0.0, 100.0),
238 WindSegment::new(30.0, 0.0, 100.0),
239 WindSegment::new(40.0, 0.0, f64::INFINITY),
240 WindSegment::new(50.0, 0.0, f64::NEG_INFINITY),
241 WindSegment::new(60.0, 0.0, f64::NAN),
242 ];
243
244 sort_wind_segments_by_distance(&mut segments);
245
246 assert_eq!(segments[0].speed_kmh, 50.0); assert_eq!(segments[1].speed_kmh, 20.0); assert_eq!(segments[2].speed_kmh, 30.0);
249 assert_eq!(segments[3].speed_kmh, 40.0); assert_eq!(segments[4].speed_kmh, 10.0); assert_eq!(segments[5].speed_kmh, 60.0);
252 assert!(segments[4].until_m.is_nan() && segments[5].until_m.is_nan());
253 }
254
255 #[test]
256 fn test_wind_sock_empty() {
257 let sock = WindSock::new(vec![]);
258 assert_eq!(sock.vector_for_range_stateless(50.0), Vector3::zeros());
259 }
260
261 #[test]
262 fn test_wind_sock_single_segment() {
263 let sock = WindSock::new(vec![WindSegment::new(16.0934, 90.0, 100.0)]);
265
266 let vec_50 = sock.vector_for_range_stateless(50.0);
268 println!("vec_50 = [{}, {}, {}]", vec_50[0], vec_50[1], vec_50[2]);
269 assert!(vec_50.norm() > 0.0);
270 assert!(
272 vec_50[2] < 0.0,
273 "Z (lateral) should be negative for 90° wind, got {}",
274 vec_50[2]
275 );
276 assert_eq!(vec_50[1], 0.0); assert!(
278 vec_50[0].abs() < 0.01,
279 "X (downrange) should be nearly zero for 90° wind, got {}",
280 vec_50[0]
281 );
282
283 let vec_150 = sock.vector_for_range_stateless(150.0);
285 assert_eq!(vec_150, Vector3::zeros());
286 }
287
288 #[test]
289 fn test_wind_sock_multiple_segments() {
290 let sock = WindSock::new(vec![
292 WindSegment::new(16.0934, 90.0, 50.0), WindSegment::new(24.1401, 45.0, 100.0), WindSegment::new(8.0467, 180.0, 200.0), ]);
296
297 let vec_25 = sock.vector_for_range_stateless(25.0);
299 println!("vec_25 = [{}, {}, {}]", vec_25[0], vec_25[1], vec_25[2]);
300 assert!(vec_25.norm() > 0.0);
301 assert!(vec_25[2] < 0.0, "90° wind should have negative Z (lateral)"); let vec_75 = sock.vector_for_range_stateless(75.0);
304 println!("vec_75 = [{}, {}, {}]", vec_75[0], vec_75[1], vec_75[2]);
305 assert!(vec_75.norm() > vec_25.norm()); assert!(vec_75[0] < 0.0); assert!(vec_75[2] < 0.0); let vec_150 = sock.vector_for_range_stateless(150.0);
310 println!("vec_150 = [{}, {}, {}]", vec_150[0], vec_150[1], vec_150[2]);
311 assert!(vec_150.norm() < vec_75.norm()); assert!(
313 vec_150[2].abs() < 0.01,
314 "180° wind should have near-zero Z (lateral), got {}",
315 vec_150[2]
316 ); assert!(
318 vec_150[0] > 0.0,
319 "180° wind should have positive X (tailwind, downrange), got {}",
320 vec_150[0]
321 ); let vec_250 = sock.vector_for_range_stateless(250.0);
324 assert_eq!(vec_250, Vector3::zeros()); }
326
327 #[test]
328 fn test_wind_conversion() {
329 let sock = WindSock::new(vec![WindSegment::new(16.0934, 0.0, 100.0)]);
331 let vec = sock.vector_for_range_stateless(50.0);
332
333 let expected_speed = 16.0934 * KMH_TO_MPS;
334 assert!((vec.norm() - expected_speed).abs() < 0.01);
335 }
336
337 #[test]
338 fn test_wind_sock_boundary_is_upper_exclusive() {
339 let sock = WindSock::new(vec![
342 WindSegment::new(16.0934, 90.0, 100.0),
343 WindSegment::new(32.1868, 270.0, 200.0),
344 ]);
345 assert!(sock.vector_for_range_stateless(99.999)[2] < 0.0);
347 assert!(sock.vector_for_range_stateless(100.0)[2] > 0.0);
349 assert_eq!(sock.vector_for_range_stateless(200.0), Vector3::zeros());
351 }
352
353 #[test]
354 fn calc_vec_passes_through_segment_vertical_unscaled() {
355 let seg = WindSegment {
358 speed_kmh: 16.0934,
359 angle_deg: 90.0,
360 until_m: 100.0,
361 vertical_mps: 3.0,
362 };
363 let vec = WindSock::calc_vec(&seg);
364 assert_eq!(vec[1], 3.0);
365 }
366
367 #[test]
368 fn calc_vec_zero_vertical_segment_keeps_zero_y() {
369 let seg = WindSegment::new(16.0934, 90.0, 100.0);
372 assert_eq!(seg.vertical_mps, 0.0);
373 let vec = WindSock::calc_vec(&seg);
374 assert_eq!(vec[1], 0.0);
375 }
376
377 #[test]
378 fn test_parse_wind_segment_str_units() {
379 let seg = parse_wind_segment_str("10:90:100", true).unwrap();
381 assert!((seg.speed_kmh - 16.09344).abs() < 1e-4);
382 assert_eq!(seg.angle_deg, 90.0);
383 assert!((seg.until_m - 91.44).abs() < 1e-4);
384
385 let seg = parse_wind_segment_str("5:270:200", false).unwrap();
387 assert!((seg.speed_kmh - 18.0).abs() < 1e-9);
388 assert_eq!(seg.angle_deg, 270.0);
389 assert!((seg.until_m - 200.0).abs() < 1e-9);
390
391 assert!(parse_wind_segment_str("10:90", true).is_err()); assert!(parse_wind_segment_str("10:bad:100", true).is_err()); assert!(parse_wind_segment_str("10:90:0", true).is_err()); assert!(parse_wind_segment_str("-3:90:100", true).is_err()); assert!(parse_wind_segment_str("10:nan:5000", true).is_err());
398 assert!(parse_wind_segment_str("10:90:nan", true).is_err());
399 assert!(parse_wind_segment_str("inf:90:100", true).is_err());
400 }
401
402 #[test]
403 fn test_parse_wind_segment_str_vertical_field() {
404 let seg = parse_wind_segment_str("10:90:100", true).unwrap();
406 assert_eq!(seg.vertical_mps, 0.0);
407 let seg = parse_wind_segment_str("5:270:200", false).unwrap();
408 assert_eq!(seg.vertical_mps, 0.0);
409
410 let seg = parse_wind_segment_str("10:90:100:5", true).unwrap();
412 assert_eq!(seg.vertical_mps, 5.0);
413 assert!((seg.speed_kmh - 16.09344).abs() < 1e-4);
415 assert!((seg.until_m - 91.44).abs() < 1e-4);
416
417 let seg_metric = parse_wind_segment_str("5:270:200:5", false).unwrap();
420 assert_eq!(seg_metric.vertical_mps, 5.0);
421
422 let seg_neg = parse_wind_segment_str("10:90:100:-3.5", true).unwrap();
424 assert_eq!(seg_neg.vertical_mps, -3.5);
425
426 assert!(parse_wind_segment_str("10:90:100:bad", true).is_err());
428 assert!(parse_wind_segment_str("10:90:100:nan", true).is_err());
429 assert!(parse_wind_segment_str("10:90:100:inf", true).is_err());
430
431 assert!(parse_wind_segment_str("10:90:100:5:1", true).is_err());
433 }
434}