gnss_qc/report/
mod.rs

1//! Generic analysis report
2use itertools::Itertools;
3use maud::{html, Markup, PreEscaped, Render, DOCTYPE};
4use std::collections::HashMap;
5use thiserror::Error;
6
7use crate::prelude::{ProductType, QcConfig, QcContext, QcReportType};
8
9// shared analysis, that may apply to several products
10mod shared;
11
12mod summary;
13use summary::QcSummary;
14
15mod rinex;
16use rinex::RINEXReport;
17
18#[cfg(feature = "navigation")]
19mod orbital;
20
21#[cfg(feature = "sp3")]
22mod sp3;
23
24#[cfg(feature = "sp3")]
25use sp3::SP3Report;
26
27// preprocessed navi
28// mod navi;
29// use navi::QcNavi;
30
31#[derive(Debug, Error)]
32pub enum Error {
33    #[error("non supported RINEX format")]
34    NonSupportedRINEX,
35    #[error("missing Clock RINEX header")]
36    MissingClockHeader,
37    #[error("missing Meteo RINEX header")]
38    MissingMeteoHeader,
39    #[error("missing IONEX header")]
40    MissingIonexHeader,
41}
42
43enum ProductReport {
44    /// RINEX products report
45    RINEX(RINEXReport),
46    #[cfg(feature = "sp3")]
47    /// SP3 product report
48    SP3(SP3Report),
49}
50
51impl ProductReport {
52    pub fn html_inline_menu_bar(&self) -> Markup {
53        match self {
54            #[cfg(feature = "sp3")]
55            Self::SP3(report) => report.html_inline_menu_bar(),
56            Self::RINEX(report) => report.html_inline_menu_bar(),
57        }
58    }
59}
60
61fn html_id(product: &ProductType) -> &str {
62    match product {
63        ProductType::IONEX => "ionex",
64        ProductType::DORIS => "doris",
65        ProductType::ANTEX => "antex",
66        ProductType::Observation => "obs",
67        ProductType::BroadcastNavigation => "brdc",
68        ProductType::HighPrecisionClock => "clk",
69        ProductType::MeteoObservation => "meteo",
70        #[cfg(feature = "sp3")]
71        ProductType::HighPrecisionOrbit => "sp3",
72    }
73}
74
75impl Render for ProductReport {
76    fn render(&self) -> Markup {
77        match self {
78            Self::RINEX(report) => match report {
79                RINEXReport::Obs(report) => {
80                    html! {
81                        div class="section" {
82                            (report.render())
83                        }
84                    }
85                }
86                RINEXReport::Doris(report) => {
87                    html! {
88                        div class="section" {
89                            (report.render())
90                        }
91                    }
92                }
93                RINEXReport::Ionex(report) => {
94                    html! {
95                        div class="section" {
96                            (report.render())
97                        }
98                    }
99                }
100                RINEXReport::Nav(report) => {
101                    html! {
102                        div class="section" {
103                            (report.render())
104                        }
105                    }
106                }
107                RINEXReport::Clk(report) => {
108                    html! {
109                        div class="section" {
110                            (report.render())
111                        }
112                    }
113                }
114                RINEXReport::Meteo(report) => {
115                    html! {
116                        div class="section" {
117                            (report.render())
118                        }
119                    }
120                }
121            },
122            #[cfg(feature = "sp3")]
123            Self::SP3(report) => {
124                html! {
125                    div class="section" {
126                        (report.render())
127                    }
128                }
129            }
130        }
131    }
132}
133
134/// [QcExtraPage] you can add to customize [QcReport]
135pub struct QcExtraPage {
136    /// tab for pagination
137    pub tab: Box<dyn Render>,
138    /// content
139    pub content: Box<dyn Render>,
140    /// HTML id
141    pub html_id: String,
142}
143
144/// [QcReport] is a generic structure to report complex analysis results
145pub struct QcReport {
146    /// Report Summary (always present)
147    summary: QcSummary,
148
149    /// In depth analysis per input product.
150    /// In summary mode, these do not exist (empty).
151    products: HashMap<ProductType, ProductReport>,
152
153    /// Custom chapters
154    custom_chapters: Vec<QcExtraPage>,
155}
156
157impl QcReport {
158    /// Builds a new GNSS report, ready to be rendered
159    pub fn new(context: &QcContext, cfg: QcConfig) -> Self {
160        let summary = QcSummary::new(&context, &cfg);
161        let summary_only = cfg.report == QcReportType::Summary;
162        Self {
163            custom_chapters: Vec::new(),
164            // navi: {
165            //    if summary.navi.nav_compatible && !summary_only {
166            //        Some(QcNavi::new(context))
167            //    } else {
168            //        None
169            //    }
170            //},
171            // Build the report, which comprises
172            //   1. one general (high level) context tab
173            //   2. one tab per product type (which can have sub tabs itself)
174            //   3. one complex tab for "shared" analysis
175            products: {
176                let mut items = HashMap::<ProductType, ProductReport>::new();
177                if !summary_only {
178                    // one tab per RINEX product
179                    for product in [
180                        ProductType::Observation,
181                        ProductType::DORIS,
182                        ProductType::MeteoObservation,
183                        ProductType::BroadcastNavigation,
184                        ProductType::HighPrecisionClock,
185                        ProductType::IONEX,
186                        ProductType::ANTEX,
187                    ] {
188                        if let Some(rinex) = context.rinex(product) {
189                            if let Ok(report) = RINEXReport::new(rinex) {
190                                items.insert(product, ProductReport::RINEX(report));
191                            }
192                        }
193                    }
194                    // one tab for SP3 when supported
195                    #[cfg(feature = "sp3")]
196                    if let Some(sp3) = context.sp3() {
197                        items.insert(
198                            ProductType::HighPrecisionOrbit,
199                            ProductReport::SP3(SP3Report::new(sp3)),
200                        );
201                    }
202                }
203                items
204            },
205            summary,
206        }
207    }
208    /// Add a custom chapter to the report
209    pub fn add_chapter(&mut self, chapter: QcExtraPage) {
210        self.custom_chapters.push(chapter);
211    }
212    /// Generates a menu bar to nagivate [Self]
213    #[cfg(not(feature = "sp3"))]
214    fn menu_bar(&self) -> Markup {
215        html! {
216            aside class="menu" {
217                p class="menu-label" {
218                    (format!("RINEX-QC v{}", env!("CARGO_PKG_VERSION")))
219                }
220                ul class="menu-list" {
221                    li {
222                        a id="menu:summary" {
223                            span class="icon" {
224                                i class="fa fa-home" {}
225                            }
226                            "Summary"
227                        }
228                    }
229                    @for product in self.products.keys().sorted() {
230                        @if let Some(report) = self.products.get(&product) {
231                            li {
232                                (report.html_inline_menu_bar())
233                            }
234                        }
235                    }
236                    @for chapter in self.custom_chapters.iter() {
237                        li {
238                            (chapter.tab.render())
239                        }
240                    }
241                    p class="menu-label" {
242                        a href="https://github.com/georust/rinex/wiki" style="margin-left:29px" {
243                            "Documentation"
244                        }
245                    }
246                    p class="menu-label" {
247                        a href="https://github.com/georust/rinex/issues" style="margin-left:29px" {
248                            "Bug Report"
249                        }
250                    }
251                    p class="menu-label" {
252                        a href="https://github.com/georust/rinex" {
253                            span class="icon" {
254                                i class="fa-brands fa-github" {}
255                            }
256                            "Sources"
257                        }
258                    }
259                } // menu-list
260            }//menu
261        }
262    }
263    /// Generates a menu bar to nagivate [Self]
264    #[cfg(feature = "sp3")]
265    fn menu_bar(&self) -> Markup {
266        html! {
267            aside class="menu" {
268                p class="menu-label" {
269                    (format!("RINEX-QC v{}", env!("CARGO_PKG_VERSION")))
270                }
271                ul class="menu-list" {
272                    li {
273                        a id="menu:summary" {
274                            span class="icon" {
275                                i class="fa fa-home" {}
276                            }
277                            "Summary"
278                        }
279                    }
280                    @for product in self.products.keys().sorted() {
281                        @if let Some(report) = self.products.get(&product) {
282                            li {
283                                (report.html_inline_menu_bar())
284                            }
285                        }
286                    }
287                    @for chapter in self.custom_chapters.iter() {
288                        li {
289                            (chapter.tab.render())
290                        }
291                    }
292                    p class="menu-label" {
293                        a href="https://github.com/georust/rinex/wiki" style="margin-left:29px" {
294                            "Documentation"
295                        }
296                    }
297                    p class="menu-label" {
298                        a href="https://github.com/georust/rinex/issues" style="margin-left:29px" {
299                            "Bug Report"
300                        }
301                    }
302                    p class="menu-label" {
303                        a href="https://github.com/georust/rinex" {
304                            span class="icon" {
305                                i class="fa-brands fa-github" {}
306                            }
307                            "Sources"
308                        }
309                    }
310                } // menu-list
311            }//menu
312        }
313    }
314}
315
316impl Render for QcReport {
317    fn render(&self) -> Markup {
318        html! {
319            (DOCTYPE)
320            html {
321                head {
322                    meta charset="utf-8";
323                    meta http-equip="X-UA-Compatible" content="IE-edge";
324                    meta name="viewport" content="width=device-width, initial-scale=1";
325                    link rel="icon" type="image/x-icon" href="https://raw.githubusercontent.com/georust/meta/master/logo/logo.png";
326                    script src="https://cdn.plot.ly/plotly-2.12.1.min.js" {};
327                    script defer="true" src="https://use.fontawesome.com/releases/v5.3.1/js/all.js" {};
328                    script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js" {};
329                    link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css";
330                    link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css";
331                    link rel="stylesheet" href="https://unpkg.com/balloon-css/balloon.min.css";
332                }//head
333                body {
334                        div id="title" {
335                            title {
336                                "RINEX QC"
337                            }
338                        }
339                        div id="body" {
340                            div class="columns is-fullheight" {
341                                div id="menubar" class="column is-3 is-sidebar-menu is-hidden-mobile" {
342                                    (self.menu_bar())
343                                } // id=menubar
344                                div class="hero is-fullheight" {
345                                    div id="summary" class="container is-main" style="display:block" {
346                                        div class="section" {
347                                            (self.summary.render())
348                                        }
349                                    }//id=summary
350                                    @for product in self.products.keys().sorted() {
351                                        @if let Some(report) = self.products.get(product) {
352                                            div id=(html_id(product)) class="container is-main" style="display:none" {
353                                                (report.render())
354                                            }
355                                        }
356                                    }
357                                    div id="extra-chapters" class="container" style="display:block" {
358                                        @for chapter in self.custom_chapters.iter() {
359                                            div id=(chapter.html_id) class="container is-main" style="display:none" {
360                                                (chapter.content.render())
361                                            }
362                                            div id=(&format!("end:{}", chapter.html_id)) style="display:none" {
363                                            }
364                                        }
365                                    }
366                                }//class=hero
367                            } // class=columns
368                        }
369                        script {
370                          (PreEscaped(
371"
372		var sidebar_menu = document.getElementById('menubar');
373		var main_pages = document.getElementsByClassName('is-main');
374		var sub_pages = document.getElementsByClassName('is-page');
375
376		sidebar_menu.onclick = function (evt) {
377			var clicked_id = evt.originalTarget.id;
378			var category = clicked_id.substring(5).split(':')[0];
379			var tab = clicked_id.substring(5).split(':')[1];
380			var is_tab = clicked_id.split(':').length == 3;
381			var menu_subtabs = document.getElementsByClassName('menu:subtab');
382			console.log('clicked id: ' + clicked_id + ' category: ' + category + ' tab: ' + is_tab);
383
384			if (is_tab == true) {
385				// show tabs for this category
386				var cat_tabs = 'menu:'+category;
387				for (var i = 0; i < menu_subtabs.length; i++) {
388					if (menu_subtabs[i].id.startsWith(cat_tabs)) {
389						menu_subtabs[i].style = 'display:block';
390					} else {
391						menu_subtabs[i].style = 'display:none';
392					}
393				}
394				// hide any other main page
395				for (var i = 0; i < main_pages.length; i++) {
396					if (main_pages[i].id != category) {
397						main_pages[i].style = 'display:none';
398					}
399				}
400				// show specialized content
401				var targetted_content = 'body:' + category + ':' + tab;
402				for (var i = 0; i < sub_pages.length; i++) {
403					if (sub_pages[i].id == targetted_content) {
404						sub_pages[i].style = 'display:block';
405					} else {
406						sub_pages[i].style = 'display:none';
407					}
408				}
409			} else {
410				// show tabs for this category
411				var cat_tabs = 'menu:'+category;
412				for (var i = 0; i < menu_subtabs.length; i++) {
413					if (menu_subtabs[i].id.startsWith(cat_tabs)) {
414						menu_subtabs[i].style = 'display:block';
415					} else {
416						menu_subtabs[i].style = 'display:none';
417					}
418				}
419				// hide any other main page
420				for (var i = 0; i < main_pages.length; i++) {
421					if (main_pages[i].id == category) {
422						main_pages[i].style = 'display:block';
423					} else {
424						main_pages[i].style = 'display:none';
425					}
426				}
427				// click on parent: show first specialized content
428				var done = false;
429				for (var i = 0; i < sub_pages.length; i++) {
430					if (done == false) {
431						if (sub_pages[i].id.includes('body:'+category)) {
432							sub_pages[i].style = 'display:block';
433							done = true;
434						} else {
435							sub_pages[i].style = 'display:none';
436						}
437					} else {
438						sub_pages[i].style = 'display:none';
439					}
440				}
441			}
442		}
443"
444                        ))} //JS
445                }//body
446            }
447        }
448    }
449}