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
64pub(crate) fn validate_wind_segments(segments: &[WindSegment]) -> Result<(), String> {
65 for (index, segment) in segments.iter().enumerate() {
66 let field = |name: &str| format!("wind.segments[{index}].{name}");
67
68 if !segment.speed_kmh.is_finite() || segment.speed_kmh < 0.0 {
69 return Err(format!(
70 "{} must be finite and non-negative",
71 field("speed_kmh")
72 ));
73 }
74 if !segment.angle_deg.is_finite() {
75 return Err(format!("{} must be finite", field("angle_deg")));
76 }
77 if !segment.until_m.is_finite() || segment.until_m <= 0.0 {
78 return Err(format!(
79 "{} must be finite and greater than zero",
80 field("until_m")
81 ));
82 }
83 if !segment.vertical_mps.is_finite() {
84 return Err(format!("{} must be finite", field("vertical_mps")));
85 }
86 }
87
88 Ok(())
89}
90
91#[derive(Debug, Clone)]
93pub struct WindSock {
94 winds: Vec<WindSegment>,
96 wind_vecs: Vec<Vector3<f64>>,
99 current: usize,
101 next_range: f64,
103 current_vec: Vector3<f64>,
105 validation_error: Option<String>,
107}
108
109impl WindSock {
110 pub fn new(mut segments: Vec<WindSegment>) -> Self {
115 let validation_error = validate_wind_segments(&segments).err();
119
120 sort_wind_segments_by_distance(&mut segments);
122
123 let wind_vecs: Vec<Vector3<f64>> = segments.iter().map(Self::calc_vec).collect();
125
126 let (current, next_range, current_vec) = if segments.is_empty() {
127 (0, f64::INFINITY, Vector3::zeros())
128 } else {
129 (0, segments[0].until_m, wind_vecs[0])
130 };
131
132 WindSock {
133 winds: segments,
134 wind_vecs,
135 current,
136 next_range,
137 current_vec,
138 validation_error,
139 }
140 }
141
142 pub(crate) fn validate_segments(&self) -> Result<(), String> {
144 self.validation_error.clone().map_or(Ok(()), Err)
145 }
146
147 fn calc_vec(seg: &WindSegment) -> Vector3<f64> {
149 let speed_mps = seg.speed_kmh * KMH_TO_MPS;
151 let angle_rad = if seg.angle_deg.abs() <= f64::MAX / PI {
154 seg.angle_deg * PI / 180.0
155 } else {
156 seg.angle_deg / 180.0 * PI
157 };
158
159 wind_vector(speed_mps, angle_rad, seg.vertical_mps)
168 }
169
170 pub fn max_speed_mps(&self) -> f64 {
173 self.winds
174 .iter()
175 .map(|seg| seg.speed_kmh.abs() * KMH_TO_MPS + seg.vertical_mps.abs())
176 .fold(0.0, f64::max)
177 }
178
179 pub(crate) fn muzzle_crosswind_from_right_mps(&self) -> Option<f64> {
183 (!self.winds.is_empty()).then(|| -self.vector_for_range_stateless(0.0)[2])
184 }
185
186 pub fn vector_for_range(&mut self, range_m: f64) -> Vector3<f64> {
191 if range_m.is_nan() {
193 return Vector3::zeros();
194 }
195
196 while range_m >= self.next_range && self.current < self.winds.len() {
199 self.current += 1;
200 if self.current >= self.winds.len() {
201 self.current_vec = Vector3::zeros();
202 self.next_range = f64::INFINITY;
203 } else {
204 self.current_vec = self.wind_vecs[self.current];
205 self.next_range = self.winds[self.current].until_m;
206 }
207 }
208
209 self.current_vec
210 }
211
212 pub fn vector_for_range_stateless(&self, range_m: f64) -> Vector3<f64> {
217 if range_m.is_nan() {
219 return Vector3::zeros();
220 }
221
222 for (i, segment) in self.winds.iter().enumerate() {
224 if range_m < segment.until_m {
225 return self.wind_vecs[i];
226 }
227 }
228
229 Vector3::zeros()
231 }
232}
233
234pub fn parse_wind_segment_str(s: &str, imperial: bool) -> Result<WindSegment, String> {
248 let parts: Vec<&str> = s.split(':').collect();
249 if parts.len() != 3 && parts.len() != 4 {
250 return Err(format!(
251 "invalid wind segment '{s}': expected SPEED:ANGLE:UNTIL_DISTANCE[:VERTICAL] \
252 (three or four colon-separated numbers; the optional 4th field VERTICAL is always \
253 m/s, positive = updraft, regardless of --units)"
254 ));
255 }
256 let num = |i: usize, name: &str| -> Result<f64, String> {
257 parts[i].trim().parse::<f64>().map_err(|_| {
258 format!("invalid wind segment '{s}': {name} '{}' is not a number", parts[i])
259 })
260 };
261 let speed = num(0, "speed")?;
262 let angle = num(1, "angle")?;
263 let until = num(2, "until-distance")?;
264 let vertical = if parts.len() == 4 {
265 num(3, "vertical")?
266 } else {
267 0.0
268 };
269 if !speed.is_finite() || !angle.is_finite() || !until.is_finite() || !vertical.is_finite() {
270 return Err(format!(
271 "invalid wind segment '{s}': speed, angle, until-distance, and vertical (m/s, \
272 positive = updraft) must be finite numbers"
273 ));
274 }
275 if speed < 0.0 {
276 return Err(format!("invalid wind segment '{s}': speed must be >= 0"));
277 }
278 if until <= 0.0 {
279 return Err(format!("invalid wind segment '{s}': until-distance must be > 0"));
280 }
281 let (speed_kmh, until_m) = if imperial {
282 (speed * 1.609344, until * 0.9144) } else {
284 (speed * 3.6, until) };
286 let mut segment = WindSegment::new(speed_kmh, angle, until_m);
287 segment.vertical_mps = vertical;
288 Ok(segment)
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn segment_sort_is_stable_and_places_nan_endpoints_last() {
297 let mut segments = vec![
298 WindSegment::new(10.0, 0.0, f64::NAN),
299 WindSegment::new(20.0, 0.0, 100.0),
300 WindSegment::new(30.0, 0.0, 100.0),
301 WindSegment::new(40.0, 0.0, f64::INFINITY),
302 WindSegment::new(50.0, 0.0, f64::NEG_INFINITY),
303 WindSegment::new(60.0, 0.0, f64::NAN),
304 ];
305
306 sort_wind_segments_by_distance(&mut segments);
307
308 assert_eq!(segments[0].speed_kmh, 50.0); assert_eq!(segments[1].speed_kmh, 20.0); assert_eq!(segments[2].speed_kmh, 30.0);
311 assert_eq!(segments[3].speed_kmh, 40.0); assert_eq!(segments[4].speed_kmh, 10.0); assert_eq!(segments[5].speed_kmh, 60.0);
314 assert!(segments[4].until_m.is_nan() && segments[5].until_m.is_nan());
315 }
316
317 #[test]
318 fn test_wind_sock_empty() {
319 let sock = WindSock::new(vec![]);
320 assert_eq!(sock.vector_for_range_stateless(50.0), Vector3::zeros());
321 assert_eq!(sock.muzzle_crosswind_from_right_mps(), None);
322 }
323
324 #[test]
325 fn muzzle_crosswind_distinguishes_an_explicit_zero_segment() {
326 let sock = WindSock::new(vec![WindSegment::new(0.0, 90.0, 100.0)]);
327 assert_eq!(sock.muzzle_crosswind_from_right_mps(), Some(0.0));
328 }
329
330 #[test]
331 fn muzzle_crosswind_uses_the_sorted_muzzle_segment_and_wind_from_sign() {
332 let from_right = WindSock::new(vec![
333 WindSegment::new(32.18688, 270.0, 5000.0),
334 WindSegment::new(16.09344, 90.0, 100.0),
335 ]);
336 let right_mps = from_right.muzzle_crosswind_from_right_mps().unwrap();
337 assert!((right_mps - 4.4704).abs() < 1e-12);
338
339 let from_left = WindSock::new(vec![WindSegment::new(16.09344, 270.0, 100.0)]);
340 let left_mps = from_left.muzzle_crosswind_from_right_mps().unwrap();
341 assert!((left_mps + 4.4704).abs() < 1e-12);
342 }
343
344 #[test]
345 fn test_wind_sock_single_segment() {
346 let sock = WindSock::new(vec![WindSegment::new(16.0934, 90.0, 100.0)]);
348
349 let vec_50 = sock.vector_for_range_stateless(50.0);
351 println!("vec_50 = [{}, {}, {}]", vec_50[0], vec_50[1], vec_50[2]);
352 assert!(vec_50.norm() > 0.0);
353 assert!(
355 vec_50[2] < 0.0,
356 "Z (lateral) should be negative for 90° wind, got {}",
357 vec_50[2]
358 );
359 assert_eq!(vec_50[1], 0.0); assert!(
361 vec_50[0].abs() < 0.01,
362 "X (downrange) should be nearly zero for 90° wind, got {}",
363 vec_50[0]
364 );
365
366 let vec_150 = sock.vector_for_range_stateless(150.0);
368 assert_eq!(vec_150, Vector3::zeros());
369 }
370
371 #[test]
372 fn test_wind_sock_multiple_segments() {
373 let sock = WindSock::new(vec![
375 WindSegment::new(16.0934, 90.0, 50.0), WindSegment::new(24.1401, 45.0, 100.0), WindSegment::new(8.0467, 180.0, 200.0), ]);
379
380 let vec_25 = sock.vector_for_range_stateless(25.0);
382 println!("vec_25 = [{}, {}, {}]", vec_25[0], vec_25[1], vec_25[2]);
383 assert!(vec_25.norm() > 0.0);
384 assert!(vec_25[2] < 0.0, "90° wind should have negative Z (lateral)"); let vec_75 = sock.vector_for_range_stateless(75.0);
387 println!("vec_75 = [{}, {}, {}]", vec_75[0], vec_75[1], vec_75[2]);
388 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);
393 println!("vec_150 = [{}, {}, {}]", vec_150[0], vec_150[1], vec_150[2]);
394 assert!(vec_150.norm() < vec_75.norm()); assert!(
396 vec_150[2].abs() < 0.01,
397 "180° wind should have near-zero Z (lateral), got {}",
398 vec_150[2]
399 ); assert!(
401 vec_150[0] > 0.0,
402 "180° wind should have positive X (tailwind, downrange), got {}",
403 vec_150[0]
404 ); let vec_250 = sock.vector_for_range_stateless(250.0);
407 assert_eq!(vec_250, Vector3::zeros()); }
409
410 #[test]
411 fn test_wind_conversion() {
412 let sock = WindSock::new(vec![WindSegment::new(16.0934, 0.0, 100.0)]);
414 let vec = sock.vector_for_range_stateless(50.0);
415
416 let expected_speed = 16.0934 * KMH_TO_MPS;
417 assert!((vec.norm() - expected_speed).abs() < 0.01);
418 }
419
420 #[test]
421 fn test_wind_sock_boundary_is_upper_exclusive() {
422 let sock = WindSock::new(vec![
425 WindSegment::new(16.0934, 90.0, 100.0),
426 WindSegment::new(32.1868, 270.0, 200.0),
427 ]);
428 assert!(sock.vector_for_range_stateless(99.999)[2] < 0.0);
430 assert!(sock.vector_for_range_stateless(100.0)[2] > 0.0);
432 assert_eq!(sock.vector_for_range_stateless(200.0), Vector3::zeros());
434 }
435
436 #[test]
437 fn calc_vec_passes_through_segment_vertical_unscaled() {
438 let seg = WindSegment {
441 speed_kmh: 16.0934,
442 angle_deg: 90.0,
443 until_m: 100.0,
444 vertical_mps: 3.0,
445 };
446 let vec = WindSock::calc_vec(&seg);
447 assert_eq!(vec[1], 3.0);
448 }
449
450 #[test]
451 fn calc_vec_zero_vertical_segment_keeps_zero_y() {
452 let seg = WindSegment::new(16.0934, 90.0, 100.0);
455 assert_eq!(seg.vertical_mps, 0.0);
456 let vec = WindSock::calc_vec(&seg);
457 assert_eq!(vec[1], 0.0);
458 }
459
460 #[test]
461 fn test_parse_wind_segment_str_units() {
462 let seg = parse_wind_segment_str("10:90:100", true).unwrap();
464 assert!((seg.speed_kmh - 16.09344).abs() < 1e-4);
465 assert_eq!(seg.angle_deg, 90.0);
466 assert!((seg.until_m - 91.44).abs() < 1e-4);
467
468 let seg = parse_wind_segment_str("5:270:200", false).unwrap();
470 assert!((seg.speed_kmh - 18.0).abs() < 1e-9);
471 assert_eq!(seg.angle_deg, 270.0);
472 assert!((seg.until_m - 200.0).abs() < 1e-9);
473
474 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());
481 assert!(parse_wind_segment_str("10:90:nan", true).is_err());
482 assert!(parse_wind_segment_str("inf:90:100", true).is_err());
483 }
484
485 #[test]
486 fn test_parse_wind_segment_str_vertical_field() {
487 let seg = parse_wind_segment_str("10:90:100", true).unwrap();
489 assert_eq!(seg.vertical_mps, 0.0);
490 let seg = parse_wind_segment_str("5:270:200", false).unwrap();
491 assert_eq!(seg.vertical_mps, 0.0);
492
493 let seg = parse_wind_segment_str("10:90:100:5", true).unwrap();
495 assert_eq!(seg.vertical_mps, 5.0);
496 assert!((seg.speed_kmh - 16.09344).abs() < 1e-4);
498 assert!((seg.until_m - 91.44).abs() < 1e-4);
499
500 let seg_metric = parse_wind_segment_str("5:270:200:5", false).unwrap();
503 assert_eq!(seg_metric.vertical_mps, 5.0);
504
505 let seg_neg = parse_wind_segment_str("10:90:100:-3.5", true).unwrap();
507 assert_eq!(seg_neg.vertical_mps, -3.5);
508
509 assert!(parse_wind_segment_str("10:90:100:bad", true).is_err());
511 assert!(parse_wind_segment_str("10:90:100:nan", true).is_err());
512 assert!(parse_wind_segment_str("10:90:100:inf", true).is_err());
513
514 assert!(parse_wind_segment_str("10:90:100:5:1", true).is_err());
516 }
517}