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 max_speed_mps(&self) -> f64 {
127 self.winds
128 .iter()
129 .map(|seg| seg.speed_kmh.abs() * KMH_TO_MPS + seg.vertical_mps.abs())
130 .fold(0.0, f64::max)
131 }
132
133 pub fn vector_for_range(&mut self, range_m: f64) -> Vector3<f64> {
138 if range_m.is_nan() {
140 return Vector3::zeros();
141 }
142
143 while range_m >= self.next_range && self.current < self.winds.len() {
146 self.current += 1;
147 if self.current >= self.winds.len() {
148 self.current_vec = Vector3::zeros();
149 self.next_range = f64::INFINITY;
150 } else {
151 self.current_vec = self.wind_vecs[self.current];
152 self.next_range = self.winds[self.current].until_m;
153 }
154 }
155
156 self.current_vec
157 }
158
159 pub fn vector_for_range_stateless(&self, range_m: f64) -> Vector3<f64> {
164 if range_m.is_nan() {
166 return Vector3::zeros();
167 }
168
169 for (i, segment) in self.winds.iter().enumerate() {
171 if range_m < segment.until_m {
172 return self.wind_vecs[i];
173 }
174 }
175
176 Vector3::zeros()
178 }
179}
180
181pub fn parse_wind_segment_str(s: &str, imperial: bool) -> Result<WindSegment, String> {
195 let parts: Vec<&str> = s.split(':').collect();
196 if parts.len() != 3 && parts.len() != 4 {
197 return Err(format!(
198 "invalid wind segment '{s}': expected SPEED:ANGLE:UNTIL_DISTANCE[:VERTICAL] \
199 (three or four colon-separated numbers; the optional 4th field VERTICAL is always \
200 m/s, positive = updraft, regardless of --units)"
201 ));
202 }
203 let num = |i: usize, name: &str| -> Result<f64, String> {
204 parts[i].trim().parse::<f64>().map_err(|_| {
205 format!("invalid wind segment '{s}': {name} '{}' is not a number", parts[i])
206 })
207 };
208 let speed = num(0, "speed")?;
209 let angle = num(1, "angle")?;
210 let until = num(2, "until-distance")?;
211 let vertical = if parts.len() == 4 {
212 num(3, "vertical")?
213 } else {
214 0.0
215 };
216 if !speed.is_finite() || !angle.is_finite() || !until.is_finite() || !vertical.is_finite() {
217 return Err(format!(
218 "invalid wind segment '{s}': speed, angle, until-distance, and vertical (m/s, \
219 positive = updraft) must be finite numbers"
220 ));
221 }
222 if speed < 0.0 {
223 return Err(format!("invalid wind segment '{s}': speed must be >= 0"));
224 }
225 if until <= 0.0 {
226 return Err(format!("invalid wind segment '{s}': until-distance must be > 0"));
227 }
228 let (speed_kmh, until_m) = if imperial {
229 (speed * 1.609344, until * 0.9144) } else {
231 (speed * 3.6, until) };
233 let mut segment = WindSegment::new(speed_kmh, angle, until_m);
234 segment.vertical_mps = vertical;
235 Ok(segment)
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn segment_sort_is_stable_and_places_nan_endpoints_last() {
244 let mut segments = vec![
245 WindSegment::new(10.0, 0.0, f64::NAN),
246 WindSegment::new(20.0, 0.0, 100.0),
247 WindSegment::new(30.0, 0.0, 100.0),
248 WindSegment::new(40.0, 0.0, f64::INFINITY),
249 WindSegment::new(50.0, 0.0, f64::NEG_INFINITY),
250 WindSegment::new(60.0, 0.0, f64::NAN),
251 ];
252
253 sort_wind_segments_by_distance(&mut segments);
254
255 assert_eq!(segments[0].speed_kmh, 50.0); assert_eq!(segments[1].speed_kmh, 20.0); assert_eq!(segments[2].speed_kmh, 30.0);
258 assert_eq!(segments[3].speed_kmh, 40.0); assert_eq!(segments[4].speed_kmh, 10.0); assert_eq!(segments[5].speed_kmh, 60.0);
261 assert!(segments[4].until_m.is_nan() && segments[5].until_m.is_nan());
262 }
263
264 #[test]
265 fn test_wind_sock_empty() {
266 let sock = WindSock::new(vec![]);
267 assert_eq!(sock.vector_for_range_stateless(50.0), Vector3::zeros());
268 }
269
270 #[test]
271 fn test_wind_sock_single_segment() {
272 let sock = WindSock::new(vec![WindSegment::new(16.0934, 90.0, 100.0)]);
274
275 let vec_50 = sock.vector_for_range_stateless(50.0);
277 println!("vec_50 = [{}, {}, {}]", vec_50[0], vec_50[1], vec_50[2]);
278 assert!(vec_50.norm() > 0.0);
279 assert!(
281 vec_50[2] < 0.0,
282 "Z (lateral) should be negative for 90° wind, got {}",
283 vec_50[2]
284 );
285 assert_eq!(vec_50[1], 0.0); assert!(
287 vec_50[0].abs() < 0.01,
288 "X (downrange) should be nearly zero for 90° wind, got {}",
289 vec_50[0]
290 );
291
292 let vec_150 = sock.vector_for_range_stateless(150.0);
294 assert_eq!(vec_150, Vector3::zeros());
295 }
296
297 #[test]
298 fn test_wind_sock_multiple_segments() {
299 let sock = WindSock::new(vec![
301 WindSegment::new(16.0934, 90.0, 50.0), WindSegment::new(24.1401, 45.0, 100.0), WindSegment::new(8.0467, 180.0, 200.0), ]);
305
306 let vec_25 = sock.vector_for_range_stateless(25.0);
308 println!("vec_25 = [{}, {}, {}]", vec_25[0], vec_25[1], vec_25[2]);
309 assert!(vec_25.norm() > 0.0);
310 assert!(vec_25[2] < 0.0, "90° wind should have negative Z (lateral)"); let vec_75 = sock.vector_for_range_stateless(75.0);
313 println!("vec_75 = [{}, {}, {}]", vec_75[0], vec_75[1], vec_75[2]);
314 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);
319 println!("vec_150 = [{}, {}, {}]", vec_150[0], vec_150[1], vec_150[2]);
320 assert!(vec_150.norm() < vec_75.norm()); assert!(
322 vec_150[2].abs() < 0.01,
323 "180° wind should have near-zero Z (lateral), got {}",
324 vec_150[2]
325 ); assert!(
327 vec_150[0] > 0.0,
328 "180° wind should have positive X (tailwind, downrange), got {}",
329 vec_150[0]
330 ); let vec_250 = sock.vector_for_range_stateless(250.0);
333 assert_eq!(vec_250, Vector3::zeros()); }
335
336 #[test]
337 fn test_wind_conversion() {
338 let sock = WindSock::new(vec![WindSegment::new(16.0934, 0.0, 100.0)]);
340 let vec = sock.vector_for_range_stateless(50.0);
341
342 let expected_speed = 16.0934 * KMH_TO_MPS;
343 assert!((vec.norm() - expected_speed).abs() < 0.01);
344 }
345
346 #[test]
347 fn test_wind_sock_boundary_is_upper_exclusive() {
348 let sock = WindSock::new(vec![
351 WindSegment::new(16.0934, 90.0, 100.0),
352 WindSegment::new(32.1868, 270.0, 200.0),
353 ]);
354 assert!(sock.vector_for_range_stateless(99.999)[2] < 0.0);
356 assert!(sock.vector_for_range_stateless(100.0)[2] > 0.0);
358 assert_eq!(sock.vector_for_range_stateless(200.0), Vector3::zeros());
360 }
361
362 #[test]
363 fn calc_vec_passes_through_segment_vertical_unscaled() {
364 let seg = WindSegment {
367 speed_kmh: 16.0934,
368 angle_deg: 90.0,
369 until_m: 100.0,
370 vertical_mps: 3.0,
371 };
372 let vec = WindSock::calc_vec(&seg);
373 assert_eq!(vec[1], 3.0);
374 }
375
376 #[test]
377 fn calc_vec_zero_vertical_segment_keeps_zero_y() {
378 let seg = WindSegment::new(16.0934, 90.0, 100.0);
381 assert_eq!(seg.vertical_mps, 0.0);
382 let vec = WindSock::calc_vec(&seg);
383 assert_eq!(vec[1], 0.0);
384 }
385
386 #[test]
387 fn test_parse_wind_segment_str_units() {
388 let seg = parse_wind_segment_str("10:90:100", true).unwrap();
390 assert!((seg.speed_kmh - 16.09344).abs() < 1e-4);
391 assert_eq!(seg.angle_deg, 90.0);
392 assert!((seg.until_m - 91.44).abs() < 1e-4);
393
394 let seg = parse_wind_segment_str("5:270:200", false).unwrap();
396 assert!((seg.speed_kmh - 18.0).abs() < 1e-9);
397 assert_eq!(seg.angle_deg, 270.0);
398 assert!((seg.until_m - 200.0).abs() < 1e-9);
399
400 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());
407 assert!(parse_wind_segment_str("10:90:nan", true).is_err());
408 assert!(parse_wind_segment_str("inf:90:100", true).is_err());
409 }
410
411 #[test]
412 fn test_parse_wind_segment_str_vertical_field() {
413 let seg = parse_wind_segment_str("10:90:100", true).unwrap();
415 assert_eq!(seg.vertical_mps, 0.0);
416 let seg = parse_wind_segment_str("5:270:200", false).unwrap();
417 assert_eq!(seg.vertical_mps, 0.0);
418
419 let seg = parse_wind_segment_str("10:90:100:5", true).unwrap();
421 assert_eq!(seg.vertical_mps, 5.0);
422 assert!((seg.speed_kmh - 16.09344).abs() < 1e-4);
424 assert!((seg.until_m - 91.44).abs() < 1e-4);
425
426 let seg_metric = parse_wind_segment_str("5:270:200:5", false).unwrap();
429 assert_eq!(seg_metric.vertical_mps, 5.0);
430
431 let seg_neg = parse_wind_segment_str("10:90:100:-3.5", true).unwrap();
433 assert_eq!(seg_neg.vertical_mps, -3.5);
434
435 assert!(parse_wind_segment_str("10:90:100:bad", true).is_err());
437 assert!(parse_wind_segment_str("10:90:100:nan", true).is_err());
438 assert!(parse_wind_segment_str("10:90:100:inf", true).is_err());
439
440 assert!(parse_wind_segment_str("10:90:100:5:1", true).is_err());
442 }
443}