1use crate::aes::Aesthetic;
7use crate::coord::Coord;
8use crate::data::{DataFrame, Value};
9use crate::position::identity::PositionIdentity;
10use crate::position::Position;
11use crate::render::backend::{DrawBackend, LineStyle, Linetype, PointStyle, RectStyle};
12use crate::render::RenderError;
13use crate::scale::ScaleSet;
14use crate::stat::identity::StatIdentity;
15use crate::stat::Stat;
16use crate::theme::Theme;
17
18use super::{Geom, GeomParams};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
22pub enum WindUnit {
23 #[default]
24 Knots,
25 MetersPerSecond,
26}
27
28pub struct GeomWindbarb {
31 pub speed: String,
33 pub direction: String,
35 pub unit: WindUnit,
36 pub length: f64,
38 pub color: (u8, u8, u8),
39 pub line_width: f64,
40}
41
42impl GeomWindbarb {
43 pub fn new(speed: impl Into<String>, direction: impl Into<String>) -> Self {
44 GeomWindbarb {
45 speed: speed.into(),
46 direction: direction.into(),
47 unit: WindUnit::Knots,
48 length: 28.0,
49 color: (40, 40, 40),
50 line_width: 1.3,
51 }
52 }
53
54 pub fn meters_per_second(mut self) -> Self {
56 self.unit = WindUnit::MetersPerSecond;
57 self
58 }
59}
60
61pub(crate) fn barb_parts(knots: f64) -> (usize, usize, usize) {
63 let s = (knots.max(0.0) / 5.0).round() as usize * 5;
64 (s / 50, (s % 50) / 10, (s % 10) / 5)
65}
66
67type Px = (f64, f64);
69
70pub(crate) fn barb_glyph(
73 at: (f64, f64),
74 dir: f64,
75 knots: f64,
76 len: f64,
77) -> (Vec<[Px; 2]>, Vec<[Px; 3]>) {
78 let rad = dir.to_radians();
79 let u = (rad.sin(), -rad.cos());
81 let side = (-u.1, u.0);
83 let tip = (at.0 + u.0 * len, at.1 + u.1 * len);
84 let barb = 0.42 * len;
85 let gap = 0.13 * len;
86 let along = |d: f64| (tip.0 - u.0 * d, tip.1 - u.1 * d);
87 let out = |p: (f64, f64), l: f64| {
89 (
90 p.0 + side.0 * l + u.0 * l * 0.35,
91 p.1 + side.1 * l + u.1 * l * 0.35,
92 )
93 };
94 let (pennants, full, half) = barb_parts(knots);
95 let mut lines = vec![[at, tip]];
96 let mut tris = Vec::new();
97 let mut d = 0.0;
98 for _ in 0..pennants {
99 let (a, b) = (along(d), along(d + gap));
100 tris.push([a, out(a, barb), b]);
101 d += gap * 1.15;
102 }
103 for _ in 0..full {
104 let p = along(d);
105 lines.push([p, out(p, barb)]);
106 d += gap;
107 }
108 for _ in 0..half {
109 if pennants == 0 && full == 0 {
111 d += gap;
112 }
113 let p = along(d);
114 lines.push([p, out(p, barb / 2.0)]);
115 }
116 (lines, tris)
117}
118
119impl Geom for GeomWindbarb {
120 fn draw(
121 &self,
122 data: &DataFrame,
123 coord: &dyn Coord,
124 scales: &ScaleSet,
125 _theme: &Theme,
126 backend: &mut dyn DrawBackend,
127 ) -> Result<(), RenderError> {
128 let x_col = data
129 .column("x")
130 .ok_or(RenderError::MissingAesthetic("x".into()))?;
131 let y_col = data.column("y");
132 let speed = data
133 .column(&self.speed)
134 .ok_or_else(|| RenderError::MissingAesthetic(self.speed.clone()))?;
135 let dir = data
136 .column(&self.direction)
137 .ok_or_else(|| RenderError::MissingAesthetic(self.direction.clone()))?;
138 let color_col = data.column("color");
139 let area = backend.plot_area();
140 let mx = |v: &Value| scales.get(&Aesthetic::X).map(|s| s.map(v)).unwrap_or(0.5);
141 let my = |v: &Value| scales.get(&Aesthetic::Y).map(|s| s.map(v)).unwrap_or(0.5);
142 let factor = match self.unit {
143 WindUnit::Knots => 1.0,
144 WindUnit::MetersPerSecond => 1.943_844,
145 };
146 for i in 0..data.nrows() {
147 let (Some(s), Some(d)) = (speed[i].as_f64(), dir[i].as_f64()) else {
148 continue;
149 };
150 let ny = y_col.map(|c| my(&c[i])).unwrap_or(0.5);
151 let at = coord.transform((mx(&x_col[i]), ny), &area);
152 if !(at.0.is_finite() && at.1.is_finite() && s.is_finite() && d.is_finite()) {
153 continue;
154 }
155 let color = color_col
156 .and_then(|c| scales.map_color(&Aesthetic::Color, &c[i]))
157 .unwrap_or(self.color);
158 let knots = s * factor;
159 super::set_mark(
160 backend,
161 Some(format!(
162 "{}: {} from {}°",
163 super::tip_value(&x_col[i]),
164 super::tip_value(&speed[i]),
165 super::tip_value(&dir[i])
166 )),
167 Some(super::tip_value(&x_col[i])),
168 super::series_key(data, i),
169 super::raw_value(&speed[i]),
170 );
171 super::set_row_extras(backend, data, i);
172 if knots < 2.5 {
173 backend.draw_circle(
174 at,
175 4.0,
176 &PointStyle {
177 color,
178 alpha: 1.0,
179 filled: false,
180 ..Default::default()
181 },
182 )?;
183 continue;
184 }
185 let (lines, tris) = barb_glyph(at, d, knots, self.length);
186 let style = LineStyle {
187 color,
188 alpha: 1.0,
189 width: self.line_width,
190 linetype: Linetype::Solid,
191 ..Default::default()
192 };
193 for l in &lines {
194 backend.draw_line(l, &style)?;
195 }
196 for t in &tris {
197 backend.draw_polygon(
198 t,
199 &RectStyle {
200 fill: Some(color),
201 stroke: Some(color),
202 stroke_width: 0.5,
203 alpha: 1.0,
204 clip: true,
205 corner_radius: 0.0,
206 },
207 )?;
208 }
209 }
210 super::clear_mark(backend);
211 super::clear_row_extras(backend);
212 Ok(())
213 }
214
215 fn required_aes(&self) -> Vec<Aesthetic> {
216 vec![Aesthetic::X]
217 }
218 fn default_stat(&self) -> Box<dyn Stat> {
219 Box::new(StatIdentity)
220 }
221 fn default_position(&self) -> Box<dyn Position> {
222 Box::new(PositionIdentity)
223 }
224 fn default_params(&self) -> GeomParams {
225 GeomParams::default()
226 }
227 fn name(&self) -> &str {
228 "windbarb"
229 }
230 fn consumes_tooltips(&self) -> bool {
231 true
232 }
233 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
234 if let (true, Some(ink)) = (self.color == (40, 40, 40), el.ink) {
235 self.color = ink;
236 }
237 }
238 fn setup_data(&self, data: &mut DataFrame, _warnings: &mut Vec<String>) {
240 if !data.has_column("y") {
241 let n = data.nrows();
242 for (c, v) in [("ymin", 0.0), ("ymax", 1.0)] {
243 if !data.has_column(c) {
244 data.add_column(c.into(), vec![Value::Float(v); n]);
245 }
246 }
247 }
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn barb_counts_follow_the_wmo_convention() {
257 assert_eq!(barb_parts(0.0), (0, 0, 0));
258 assert_eq!(barb_parts(5.0), (0, 0, 1));
259 assert_eq!(barb_parts(15.0), (0, 1, 1));
260 assert_eq!(barb_parts(65.0), (1, 1, 1));
261 assert_eq!(barb_parts(102.0), (2, 0, 0), "rounded to 100");
262 assert_eq!(barb_parts(147.0), (2, 4, 1));
263 assert_eq!(barb_parts(148.0), (3, 0, 0), "rounded to 150");
264 }
265
266 #[test]
267 fn shaft_points_into_the_wind() {
268 let (lines, _) = barb_glyph((0.0, 0.0), 0.0, 10.0, 30.0);
270 let [a, b] = lines[0];
271 assert!((b.0 - a.0).abs() < 1e-9 && (b.1 - a.1 + 30.0).abs() < 1e-9);
272 let (lines, tris) = barb_glyph((0.0, 0.0), 90.0, 50.0, 30.0);
274 assert!((lines[0][1].0 - 30.0).abs() < 1e-9);
275 assert_eq!(tris.len(), 1, "one pennant");
276 }
277}