gnss_qc/report/rinex/
meteo.rs1use itertools::Itertools;
2use maud::{html, Markup, Render};
3use qc_traits::{Filter, FilterItem, MaskOperand, Preprocessing};
4
5use rinex::{
6 meteo::Sensor,
7 prelude::{Observable, Rinex},
8};
9use std::collections::HashMap;
10
11use crate::report::{shared::SamplingReport, Error};
12
13use crate::plot::{
14 MarkerSymbol,
16 Mode,
17 Plot,
18};
19
20fn obs2physics(ob: &Observable) -> String {
21 match ob {
22 Observable::Pressure => "Pressure".to_string(),
23 Observable::Temperature => "Temperature".to_string(),
24 Observable::HumidityRate => "Moisture".to_string(),
25 Observable::ZenithWetDelay => "Wet Delay".to_string(),
26 Observable::ZenithDryDelay => "Dry Delay".to_string(),
27 Observable::ZenithTotalDelay => "Wet+Dry Delay".to_string(),
28 Observable::WindDirection => "Wind Direction".to_string(),
29 Observable::WindSpeed => "Wind Speed".to_string(),
30 Observable::RainIncrement => "Rain Increment".to_string(),
31 Observable::HailIndicator => "Hail".to_string(),
32 _ => "Not applicable".to_string(),
33 }
34}
35
36fn obs2unit(ob: &Observable) -> String {
37 match ob {
38 Observable::Pressure => "hPa".to_string(),
39 Observable::Temperature => "°C".to_string(),
40 Observable::HumidityRate | Observable::RainIncrement => "%".to_string(),
41 Observable::ZenithWetDelay | Observable::ZenithDryDelay | Observable::ZenithTotalDelay => {
42 "s".to_string()
43 }
44 Observable::WindDirection => "°".to_string(),
45 Observable::WindSpeed => "m/s".to_string(),
46 Observable::HailIndicator => "boolean".to_string(),
47 _ => "not applicable".to_string(),
48 }
49}
50
51struct WindDirectionReport {
52 compass_plot: Plot,
53}
54
55impl Render for WindDirectionReport {
56 fn render(&self) -> Markup {
57 html! {
58 div class="table-container" {
59 table class="table is-bordered" {
60 tbody {
61 tr {
62 td {
63 (self.compass_plot.render())
64 }
65 }
66 }
67 }
68 }
69 }
70 }
71}
72
73struct SinglePlotReport {
74 plot: Plot,
75}
76
77impl Render for SinglePlotReport {
78 fn render(&self) -> Markup {
79 html! {
80 (self.plot.render())
81 }
82 }
83}
84
85enum ObservableDependent {
86 SinglePlot(SinglePlotReport),
87 WindDirection(WindDirectionReport),
88}
89
90impl Render for ObservableDependent {
91 fn render(&self) -> Markup {
92 match self {
93 Self::SinglePlot(plot) => plot.render(),
94 Self::WindDirection(report) => report.render(),
95 }
96 }
97}
98
99struct MeteoPage {
100 inner: ObservableDependent,
101 sampling: SamplingReport,
102}
103
104impl MeteoPage {
105 fn new(observable: &Observable, rnx: &Rinex) -> Self {
106 let title = format!("{} Observations", observable);
107 let y_label = format!("{} [{}]", observable, obs2unit(observable));
108 let html_id = observable.to_string();
109 if *observable == Observable::WindDirection {
110 let mut compass_plot =
111 Plot::timedomain_plot(&html_id, "Wind Direction", "Angle [°]", true);
112 for (index, (k, observations)) in rnx.meteo_observations_iter().enumerate() {
113 let visible = index == 0;
114 if &k.observable == observable {
115 }
135 }
136 let report = WindDirectionReport { compass_plot };
137 Self {
138 sampling: SamplingReport::from_rinex(rnx),
139 inner: ObservableDependent::WindDirection(report),
140 }
141 } else {
142 let mut plot = Plot::timedomain_plot(&html_id, &title, &y_label, true);
143 let data_x = rnx
144 .meteo_observations_iter()
145 .flat_map(|(k, _)| {
146 if &k.observable == observable {
147 Some(k.epoch)
148 } else {
149 None
150 }
151 })
152 .collect::<Vec<_>>();
153
154 let data_y = rnx
155 .meteo_observations_iter()
156 .flat_map(|(k, observation)| {
157 if &k.observable == observable {
158 Some(*observation)
159 } else {
160 None
161 }
162 })
163 .collect::<Vec<_>>();
164
165 let trace = Plot::timedomain_chart(
166 &observable.to_string(),
167 Mode::LinesMarkers,
168 MarkerSymbol::TriangleUp,
169 &data_x,
170 data_y,
171 true,
172 );
173 plot.add_trace(trace);
174 let report = SinglePlotReport { plot };
175 Self {
176 sampling: SamplingReport::from_rinex(rnx),
177 inner: ObservableDependent::SinglePlot(report),
178 }
179 }
180 }
181}
182
183impl Render for MeteoPage {
184 fn render(&self) -> Markup {
185 html! {
186 div class="table-container" {
187 table class="table is-bordered" {
188 tbody {
189 tr {
190 th class="is-info" {
191 "Sampling"
192 }
193 td {
194 (self.sampling.render())
195 }
196 }
197 tr {
198 th class="is-info" {
199 "Observations"
200 }
201 td {
202 (self.inner.render())
203 }
204 }
205 }
206 }
207 }
208 }
209 }
210}
211
212pub struct MeteoReport {
214 sensors: Vec<Sensor>,
215 agency: Option<String>,
216 sampling: SamplingReport,
217 pages: HashMap<String, MeteoPage>,
218}
219
220impl MeteoReport {
221 pub fn html_inline_menu_bar(&self) -> Markup {
222 html! {
223 a id="menu:meteo" {
224 span class="icon" {
225 i class="fa-solid fa-cloud-sun-rain" {}
226 }
227 "Meteo Observations"
228 }
229 }
230 }
231 pub fn new(rnx: &Rinex) -> Result<Self, Error> {
232 let header = rnx.header.meteo.as_ref().ok_or(Error::MissingMeteoHeader)?;
233 Ok(Self {
234 agency: None,
235 sensors: header.sensors.clone(),
236 sampling: SamplingReport::from_rinex(&rnx),
237 pages: {
238 let mut pages = HashMap::<String, MeteoPage>::new();
239 for observable in rnx.observables_iter() {
240 let filter = if *observable == Observable::WindDirection {
241 Filter::mask(
242 MaskOperand::Equals,
243 FilterItem::ComplexItem(vec![observable.to_string(), "WS".to_string()]),
244 )
245 } else {
246 Filter::mask(
247 MaskOperand::Equals,
248 FilterItem::ComplexItem(vec![observable.to_string()]),
249 )
250 };
251 let focused = rnx.filter(&filter);
252 pages.insert(
253 obs2physics(observable),
254 MeteoPage::new(observable, &focused),
255 );
256 }
257 pages
258 },
259 })
260 }
261}
262
263impl Render for MeteoReport {
264 fn render(&self) -> Markup {
265 html! {
266 div class="table-container" {
267 table class="table is-bordered" {
268 tbody {
269 tr {
270 th class="is-info" {
271 "Agency"
272 }
273 @if let Some(agency) = &self.agency {
274 td {
275 (agency)
276 }
277 } @else {
278 td {
279 "Unknown"
280 }
281 }
282 }
283 @for sensor in self.sensors.iter() {
284 tr {
285 th {
286 (&format!("{} sensor", obs2physics(&sensor.observable)))
287 }
288 td {
289 (sensor.render())
290 }
291 }
292 }
293 tr {
294 th class="is-info" {
295 "Sampling"
296 }
297 td {
298 (self.sampling.render())
299 }
300 }
301 tr {
302 th class="is-info" {
303 "Observations"
304 }
305 td {
306 div class="table-container" {
307 table class="table is-bordered" {
308 @for key in self.pages.keys().sorted() {
309 @if let Some(page) = self.pages.get(key) {
310 tr {
311 th class="is-info" {
312 (format!("{} observations", key))
313 }
314 td {
315 (page.render())
316 }
317 }
318 }
319 }
320 }
321 }
322 }
323 }
324 }
325 }
326 }}
328 }
329}