1use 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
9mod 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#[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(RINEXReport),
46 #[cfg(feature = "sp3")]
47 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
134pub struct QcExtraPage {
136 pub tab: Box<dyn Render>,
138 pub content: Box<dyn Render>,
140 pub html_id: String,
142}
143
144pub struct QcReport {
146 summary: QcSummary,
148
149 products: HashMap<ProductType, ProductReport>,
152
153 custom_chapters: Vec<QcExtraPage>,
155}
156
157impl QcReport {
158 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 products: {
176 let mut items = HashMap::<ProductType, ProductReport>::new();
177 if !summary_only {
178 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 #[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 pub fn add_chapter(&mut self, chapter: QcExtraPage) {
210 self.custom_chapters.push(chapter);
211 }
212 #[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 } }}
262 }
263 #[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 } }}
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 }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 } 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 }@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 }} }
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 ))} }}
447 }
448 }
449}