1use nalgebra::Vector3;
2use std::f64::consts::PI;
3
4const KMH_TO_MPS: f64 = 1000.0 / 3600.0;
6
7pub type WindSegment = (f64, f64, f64);
10
11#[derive(Debug, Clone)]
13pub struct WindSock {
14 winds: Vec<WindSegment>,
16 wind_vecs: Vec<Vector3<f64>>,
19 current: usize,
21 next_range: f64,
23 current_vec: Vector3<f64>,
25}
26
27impl WindSock {
28 pub fn new(mut segments: Vec<WindSegment>) -> Self {
33 segments.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Greater));
35
36 let wind_vecs: Vec<Vector3<f64>> = segments.iter().map(Self::calc_vec).collect();
38
39 let (current, next_range, current_vec) = if segments.is_empty() {
40 (0, f64::INFINITY, Vector3::zeros())
41 } else {
42 (0, segments[0].2, wind_vecs[0])
43 };
44
45 WindSock {
46 winds: segments,
47 wind_vecs,
48 current,
49 next_range,
50 current_vec,
51 }
52 }
53
54 fn calc_vec(seg: &WindSegment) -> Vector3<f64> {
56 let (speed_kmh, angle_deg, _) = *seg;
57
58 let speed_mps = speed_kmh * KMH_TO_MPS;
60 let angle_rad = angle_deg * PI / 180.0;
61
62 Vector3::new(
70 -speed_mps * angle_rad.cos(), 0.0, -speed_mps * angle_rad.sin(), )
74 }
75
76 pub fn vector_for_range(&mut self, range_m: f64) -> Vector3<f64> {
81 if range_m.is_nan() {
83 return Vector3::zeros();
84 }
85
86 while range_m >= self.next_range && self.current < self.winds.len() {
89 self.current += 1;
90 if self.current >= self.winds.len() {
91 self.current_vec = Vector3::zeros();
92 self.next_range = f64::INFINITY;
93 } else {
94 self.current_vec = self.wind_vecs[self.current];
95 self.next_range = self.winds[self.current].2;
96 }
97 }
98
99 self.current_vec
100 }
101
102 pub fn vector_for_range_stateless(&self, range_m: f64) -> Vector3<f64> {
107 if range_m.is_nan() {
109 return Vector3::zeros();
110 }
111
112 for (i, segment) in self.winds.iter().enumerate() {
114 if range_m < segment.2 {
115 return self.wind_vecs[i];
116 }
117 }
118
119 Vector3::zeros()
121 }
122}
123
124pub fn parse_wind_segment_str(s: &str, imperial: bool) -> Result<WindSegment, String> {
132 let parts: Vec<&str> = s.split(':').collect();
133 if parts.len() != 3 {
134 return Err(format!(
135 "invalid wind segment '{s}': expected SPEED:ANGLE:UNTIL_DISTANCE (three colon-separated numbers)"
136 ));
137 }
138 let num = |i: usize, name: &str| -> Result<f64, String> {
139 parts[i].trim().parse::<f64>().map_err(|_| {
140 format!("invalid wind segment '{s}': {name} '{}' is not a number", parts[i])
141 })
142 };
143 let speed = num(0, "speed")?;
144 let angle = num(1, "angle")?;
145 let until = num(2, "until-distance")?;
146 if !speed.is_finite() || !angle.is_finite() || !until.is_finite() {
147 return Err(format!(
148 "invalid wind segment '{s}': speed, angle, and until-distance must be finite numbers"
149 ));
150 }
151 if speed < 0.0 {
152 return Err(format!("invalid wind segment '{s}': speed must be >= 0"));
153 }
154 if until <= 0.0 {
155 return Err(format!("invalid wind segment '{s}': until-distance must be > 0"));
156 }
157 let (speed_kmh, until_m) = if imperial {
158 (speed * 1.609344, until * 0.9144) } else {
160 (speed * 3.6, until) };
162 Ok((speed_kmh, angle, until_m))
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_wind_sock_empty() {
171 let sock = WindSock::new(vec![]);
172 assert_eq!(sock.vector_for_range_stateless(50.0), Vector3::zeros());
173 }
174
175 #[test]
176 fn test_wind_sock_single_segment() {
177 let sock = WindSock::new(vec![(16.0934, 90.0, 100.0)]);
179
180 let vec_50 = sock.vector_for_range_stateless(50.0);
182 println!("vec_50 = [{}, {}, {}]", vec_50[0], vec_50[1], vec_50[2]);
183 assert!(vec_50.norm() > 0.0);
184 assert!(
186 vec_50[2] < 0.0,
187 "Z (lateral) should be negative for 90° wind, got {}",
188 vec_50[2]
189 );
190 assert_eq!(vec_50[1], 0.0); assert!(
192 vec_50[0].abs() < 0.01,
193 "X (downrange) should be nearly zero for 90° wind, got {}",
194 vec_50[0]
195 );
196
197 let vec_150 = sock.vector_for_range_stateless(150.0);
199 assert_eq!(vec_150, Vector3::zeros());
200 }
201
202 #[test]
203 fn test_wind_sock_multiple_segments() {
204 let sock = WindSock::new(vec![
206 (16.0934, 90.0, 50.0), (24.1401, 45.0, 100.0), (8.0467, 180.0, 200.0), ]);
210
211 let vec_25 = sock.vector_for_range_stateless(25.0);
213 println!("vec_25 = [{}, {}, {}]", vec_25[0], vec_25[1], vec_25[2]);
214 assert!(vec_25.norm() > 0.0);
215 assert!(vec_25[2] < 0.0, "90° wind should have negative Z (lateral)"); let vec_75 = sock.vector_for_range_stateless(75.0);
218 println!("vec_75 = [{}, {}, {}]", vec_75[0], vec_75[1], vec_75[2]);
219 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);
224 println!("vec_150 = [{}, {}, {}]", vec_150[0], vec_150[1], vec_150[2]);
225 assert!(vec_150.norm() < vec_75.norm()); assert!(
227 vec_150[2].abs() < 0.01,
228 "180° wind should have near-zero Z (lateral), got {}",
229 vec_150[2]
230 ); assert!(
232 vec_150[0] > 0.0,
233 "180° wind should have positive X (tailwind, downrange), got {}",
234 vec_150[0]
235 ); let vec_250 = sock.vector_for_range_stateless(250.0);
238 assert_eq!(vec_250, Vector3::zeros()); }
240
241 #[test]
242 fn test_wind_conversion() {
243 let sock = WindSock::new(vec![(16.0934, 0.0, 100.0)]);
245 let vec = sock.vector_for_range_stateless(50.0);
246
247 let expected_speed = 16.0934 * KMH_TO_MPS;
248 assert!((vec.norm() - expected_speed).abs() < 0.01);
249 }
250
251 #[test]
252 fn test_wind_sock_boundary_is_upper_exclusive() {
253 let sock = WindSock::new(vec![(16.0934, 90.0, 100.0), (32.1868, 270.0, 200.0)]);
256 assert!(sock.vector_for_range_stateless(99.999)[2] < 0.0);
258 assert!(sock.vector_for_range_stateless(100.0)[2] > 0.0);
260 assert_eq!(sock.vector_for_range_stateless(200.0), Vector3::zeros());
262 }
263
264 #[test]
265 fn test_parse_wind_segment_str_units() {
266 let (kmh, ang, until) = parse_wind_segment_str("10:90:100", true).unwrap();
268 assert!((kmh - 16.09344).abs() < 1e-4);
269 assert_eq!(ang, 90.0);
270 assert!((until - 91.44).abs() < 1e-4);
271
272 let (kmh, ang, until) = parse_wind_segment_str("5:270:200", false).unwrap();
274 assert!((kmh - 18.0).abs() < 1e-9);
275 assert_eq!(ang, 270.0);
276 assert!((until - 200.0).abs() < 1e-9);
277
278 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());
285 assert!(parse_wind_segment_str("10:90:nan", true).is_err());
286 assert!(parse_wind_segment_str("inf:90:100", true).is_err());
287 }
288}