gnss_qc/report/rinex/
meteo.rs

1use 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    //CompassArrow,
15    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                    // if k.observable == Observable::WindDirection {
116                    //     let hover_text = t.to_string();
117                    //     let mut rho = 1.0;
118                    //     for (rhs_ob, rhs_value) in observations.iter() {
119                    //         if *rhs_ob == Observable::WindSpeed {
120                    //             rho = *rhs_value;
121                    //         }
122                    //     }
123                    //     let trace = CompassArrow::new(
124                    //         Mode::LinesMarkers,
125                    //         rho,
126                    //         *value,
127                    //         hover_text,
128                    //         visible,
129                    //         0.25,
130                    //         25.0,
131                    //     );
132                    //     compass_plot.add_trace(trace.scatter);
133                    // }
134                }
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
212/// Meteo RINEX analysis
213pub 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            }//table
327        }
328    }
329}