gnss_qc/report/rinex/
obs.rs

1use itertools::Itertools;
2use maud::{html, Markup, Render};
3use qc_traits::{Filter, FilterItem, MaskOperand, Preprocessing};
4use std::collections::HashMap;
5
6use rinex::{
7    carrier::Carrier,
8    hardware::{Antenna, Receiver},
9    prelude::{Constellation, Epoch, Observable, Rinex, SV},
10};
11
12use crate::report::shared::SamplingReport;
13
14use crate::plot::{MarkerSymbol, Mode, Plot};
15
16#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17enum Physics {
18    SSI,
19    Doppler,
20    Phase,
21    PseudoRange,
22}
23
24impl Physics {
25    pub fn from_observable(observable: &Observable) -> Self {
26        if observable.is_phase_range_observable() {
27            Self::Phase
28        } else if observable.is_doppler_observable() {
29            Self::Doppler
30        } else if observable.is_ssi_observable() {
31            Self::SSI
32        } else {
33            Self::PseudoRange
34        }
35    }
36    pub fn plot_title(&self) -> String {
37        match self {
38            Self::SSI => "SSI".to_string(),
39            Self::Phase => "Phase".to_string(),
40            Self::Doppler => "Doppler".to_string(),
41            Self::PseudoRange => "Pseudo Range".to_string(),
42        }
43    }
44    pub fn y_label(&self) -> String {
45        match self {
46            Self::SSI => "Power [dB]".to_string(),
47            Self::Phase => "Carrier Cycles".to_string(),
48            Self::Doppler => "Doppler Shifts".to_string(),
49            Self::PseudoRange => "Pseudo Range [m]".to_string(),
50        }
51    }
52}
53
54struct Combination {
55    lhs: Observable,
56    rhs: Observable,
57}
58
59/// Frequency dependent pagination
60struct FrequencyPage {
61    /// Total SPP compatible epochs
62    total_spp_epochs: usize,
63    /// Total CPP compatible epochs
64    total_cpp_epochs: usize,
65    /// Total PPP compatible epochs
66    total_ppp_epochs: usize,
67    /// Sampling
68    sampling: SamplingReport,
69    /// One plot per physics
70    raw_plots: HashMap<Physics, Plot>,
71    /// One plot per combination,
72    combination_plots: HashMap<Combination, Plot>,
73    /// Code Multipath
74    multipath_plot: Plot,
75}
76
77impl FrequencyPage {
78    pub fn new(rinex: &Rinex) -> Self {
79        let mut total_spp_epochs = 0;
80        let mut total_cpp_epochs = 0;
81        let mut total_ppp_epochs = 0;
82        let sampling = SamplingReport::from_rinex(rinex);
83
84        for (k, v) in rinex.observations_iter() {
85            let mut nb_pr = 0;
86            let mut nb_ph = 0;
87
88            for signal in v.signals.iter() {
89                // if nb_pr > 0 {
90                //     total_spp_epochs += 1;
91                // }
92                // if nb_pr > 1 {
93                //     total_cpp_epochs += 1;
94                //     if nb_ph > 1 {
95                //         total_ppp_epochs += 1;
96                //     }
97                // }
98            }
99        }
100
101        Self {
102            sampling,
103            total_cpp_epochs,
104            total_spp_epochs,
105            total_ppp_epochs,
106            combination_plots: HashMap::new(),
107            multipath_plot: Plot::timedomain_plot("code_mp", "Code Multipath", "BiasĀ [m]", true),
108            raw_plots: {
109                let mut plots = HashMap::<Physics, Plot>::new();
110                let svnn = rinex.sv_iter().collect::<Vec<_>>();
111                let observables = rinex.observables_iter().collect::<Vec<_>>();
112
113                // draw carrier phase plot for all SV; per signal
114
115                for obs in observables {
116                    let physics = Physics::from_observable(obs);
117                    let title = physics.plot_title();
118                    let y_label = physics.y_label();
119                    let mut plot = Plot::timedomain_plot(&title, &title, &y_label, true);
120
121                    for sv in &svnn {
122                        let obs_x_ok = rinex
123                            .signal_observations_iter()
124                            .flat_map(|(k, v)| {
125                                if v.sv == *sv && v.observable == *obs {
126                                    Some(k.epoch)
127                                } else {
128                                    None
129                                }
130                            })
131                            .collect::<Vec<_>>();
132
133                        let obs_y_ok = rinex
134                            .signal_observations_iter()
135                            .flat_map(|(k, v)| {
136                                if v.sv == *sv && v.observable == *obs {
137                                    Some(v.value)
138                                } else {
139                                    None
140                                }
141                            })
142                            .collect::<Vec<_>>();
143
144                        let trace = Plot::timedomain_chart(
145                            &format!("{}({})", sv, obs),
146                            Mode::Markers,
147                            MarkerSymbol::Cross,
148                            &obs_x_ok,
149                            obs_y_ok,
150                            true,
151                        );
152                        plot.add_trace(trace);
153                    }
154                    plots.insert(physics, plot);
155                }
156                plots
157            },
158        }
159    }
160}
161
162impl Render for FrequencyPage {
163    fn render(&self) -> Markup {
164        html! {
165            table class="table is-bordered" {
166                tr {
167                    th class="is-info" {
168                        "Sampling"
169                    }
170                    td {
171                        (self.sampling.render())
172                    }
173                }
174                tr {
175                    th class="is-info" {
176                        button aria-label="Epochs analysis" data-balloon-pos="right" {
177                            "Epochs"
178                        }
179                    }
180                    td {
181                        table class="table is-bordered" {
182                            tr {
183                                th class="is-info" {
184                                    button aria-label="Total SPP compatible Epochs" data-balloon-pos="right" {
185                                        "SPP Compatible"
186                                    }
187                                }
188                                td {
189                                    (format!("{}/{} ({}%)", self.total_spp_epochs, self.sampling.total, self.total_spp_epochs * 100 / self.sampling.total))
190                                }
191                            }
192                            tr {
193                                th class="is-info" {
194                                    button aria-label="Total CPP compatible Epochs" data-balloon-pos="right" {
195                                        "CPP Compatible"
196                                    }
197                                }
198                                td {
199                                    (format!("{}/{} ({}%)", self.total_cpp_epochs, self.sampling.total, self.total_cpp_epochs * 100 / self.sampling.total))
200                                }
201                            }
202                            tr {
203                                th class="is-info" {
204                                    button aria-label="Total PPP compatible Epochs" data-balloon-pos="right" {
205                                        "PPP Compatible"
206                                    }
207                                }
208                                td {
209                                    (format!("{}/{} ({}%)", self.total_ppp_epochs, self.sampling.total, self.total_ppp_epochs * 100 / self.sampling.total))
210                                }
211                            }
212                        }
213                    }
214                }
215                tr {
216                    @for physics in self.raw_plots.keys().sorted() {
217                        @if let Some(plot) = self.raw_plots.get(physics) {
218                            tr {
219                                th class="is-info" {
220                                    (format!("{} observations", physics.plot_title()))
221                                }
222                                td {
223                                    (plot.render())
224                                }
225                            }
226                        }
227                    }
228                }
229            }
230        }
231    }
232}
233
234/// Constellation dependent pagination
235struct ConstellationPage {
236    /// Satellites
237    satellites: Vec<SV>,
238    /// sampling
239    sampling: SamplingReport,
240    /// True if Standard Positioning compatible
241    spp_compatible: bool,
242    /// True if Code Dual Frequency Positioning compatible
243    cpp_compatible: bool,
244    /// True if PPP compatible
245    ppp_compatible: bool,
246    /// Signal dependent pagination
247    frequencies: HashMap<String, FrequencyPage>,
248    /// SV per epoch
249    sv_epoch: HashMap<Epoch, Vec<SV>>,
250}
251
252impl ConstellationPage {
253    pub fn new(constellation: Constellation, rinex: &Rinex) -> Self {
254        let mut spp_compatible = false; // TODO
255        let mut cpp_compatible = false; // TODO
256        let mut ppp_compatible = false; // TODO
257        let satellites = rinex.sv_iter().collect::<Vec<_>>();
258        let sampling = SamplingReport::from_rinex(rinex);
259        let mut frequencies = HashMap::<String, FrequencyPage>::new();
260        for carrier in rinex.carrier_iter().sorted() {
261            let mut observables = Vec::<Observable>::new();
262            for observable in rinex.observables_iter() {
263                if let Ok(signal) = Carrier::from_observable(constellation, observable) {
264                    if signal == carrier {
265                        observables.push(observable.clone());
266                    }
267                }
268            }
269            if observables.len() > 0 {
270                let filter =
271                    Filter::equals(&observables.iter().map(|ob| ob.to_string()).join(", "))
272                        .unwrap();
273                let focused = rinex.filter(&filter);
274                FrequencyPage::new(&focused);
275                frequencies.insert(format!("{:?}", carrier), FrequencyPage::new(&focused));
276            }
277        }
278        Self {
279            satellites,
280            sampling,
281            frequencies,
282            spp_compatible,
283            cpp_compatible,
284            ppp_compatible,
285            sv_epoch: Default::default(),
286        }
287    }
288}
289
290impl Render for ConstellationPage {
291    fn render(&self) -> Markup {
292        html! {
293            div class="table-container" {
294                table class="table is-bordered" {
295                    tbody {
296                        tr {
297                            th {
298                                button aria-label="Pseudo Range single frequency navigation" data-balloon-pos="right" {
299                                    "SPP Compatible"
300                                }
301                            }
302                            td {
303                                @if self.spp_compatible {
304                                    span class="icon" style="color:green" {
305                                        i class="fa-solid fa-circle-check" {}
306                                    }
307                                } @else {
308                                    span class="icon" style="color:red" {
309                                        i class="fa-solid fa-circle-xmark" {}
310                                    }
311                                }
312                            }
313                        }
314                        tr {
315                            th {
316                                button aria-label="Pseudo Range dual frequency navigation" data-balloon-pos="right" {
317                                    "CPP compatible"
318                                }
319                            }
320                            td {
321                                @if self.cpp_compatible {
322                                    span class="icon" style="color:green" {
323                                        i class="fa-solid fa-circle-check" {}
324                                    }
325                                } @else {
326                                    span class="icon" style="color:red" {
327                                        i class="fa-solid fa-circle-xmark" {}
328                                    }
329                                }
330                            }
331                        }
332                        tr {
333                            th {
334                                button aria-label="Dual frequency Pseudo + Phase Range navigation" data-balloon-pos="right" {
335                                    "PPP compatible"
336                                }
337                            }
338                            td {
339                                @if self.ppp_compatible {
340                                    span class="icon" style="color:green" {
341                                        i class="fa-solid fa-circle-check" {};
342                                    }
343                                } @else {
344                                    span class="icon" style="color:red" {
345                                        i class="fa-solid fa-circle-xmark" {};
346                                    }
347                                }
348                            }
349                        }
350                        tr {
351                            th class="is-info" {
352                                button aria-label="Observed Satellites" data-balloon-pos="right" {
353                                    "Satellites"
354                                }
355                            }
356                            td {
357                                (self.satellites.iter().sorted().join(", "))
358                            }
359                        }
360                        tr {
361                            th class="is-info" {
362                                "Sampling"
363                            }
364                            td {
365                                (self.sampling.render())
366                            }
367                        }
368                        tr {
369                            th class="is-info" {
370                                "Signals"
371                            }
372                            td {
373                                (self.frequencies.keys().sorted().join(", "))
374                            }
375                        }
376                        @for signal in self.frequencies.keys().sorted() {
377                            @if let Some(page) = self.frequencies.get(signal) {
378                                tr {
379                                    th class="is-info" {
380                                        (signal.to_string())
381                                    }
382                                    td id=(format!("page:obs:{}", signal)) class="page:obs:{}" style="display:block" {
383                                        (page.render())
384                                    }
385                                }
386                            }
387                        }
388                    }
389                }
390            }
391        }
392    }
393}
394
395/// RINEX Observation Report
396pub struct Report {
397    antenna: Option<Antenna>,
398    receiver: Option<Receiver>,
399    clock_plot: Plot,
400    sampling: SamplingReport,
401    constellations: HashMap<String, ConstellationPage>,
402}
403
404impl Report {
405    pub fn html_inline_menu_bar(&self) -> Markup {
406        html! {
407            a id="menu:obs" {
408                span class="icon" {
409                    i class="fa-solid fa-tower-cell" {}
410                }
411                "Observations"
412            }
413            ul class="menu-list" style="display:block" {
414                @for constell in self.constellations.keys().sorted() {
415                    li {
416                        a id=(&format!("menu:obs:{}", constell)) class="menu:subtab" style="margin-left:29px" {
417                            span class="icon" {
418                                i class="fa-solid fa-satellite" {};
419                            }
420                            (constell.to_string())
421                        }
422                    }
423                }
424            }
425        }
426    }
427    pub fn new(rinex: &Rinex) -> Self {
428        Self {
429            sampling: SamplingReport::from_rinex(rinex),
430            receiver: if let Some(rcvr) = &rinex.header.rcvr {
431                Some(rcvr.clone())
432            } else {
433                None
434            },
435            antenna: if let Some(ant) = &rinex.header.rcvr_antenna {
436                Some(ant.clone())
437            } else {
438                None
439            },
440            clock_plot: {
441                let mut plot = Plot::timedomain_plot("rx_clock", "Clock offset", "Second", true);
442                plot
443            },
444            constellations: {
445                let mut constellations: HashMap<String, ConstellationPage> =
446                    HashMap::<String, ConstellationPage>::new();
447                for constellation in rinex.constellations_iter() {
448                    let filter = Filter::mask(
449                        MaskOperand::Equals,
450                        FilterItem::ConstellationItem(vec![constellation]),
451                    );
452                    //if constellation == Constellation::BeiDou {
453                    //    // MEO mask
454                    //    let meo1 = Filter::greater_than("C05").unwrap();
455                    //    let meo2 = Filter::lower_than("C58").unwrap();
456                    //    let meo = rinex.filter(&meo1).filter(&meo2);
457
458                    //    constellations.insert(
459                    //        "BeiDou (MEO)".to_string(),
460                    //        ConstellationPage::new(constellation, &meo),
461                    //    );
462
463                    //    // GEO mask
464                    //    let geo = rinex.filter(&!meo1).filter(&!meo2);
465
466                    //    constellations.insert(
467                    //        "BeiDou (GEO)".to_string(),
468                    //        ConstellationPage::new(constellation, &geo),
469                    //    );
470                    //} else {
471                    let focused = rinex.filter(&filter);
472                    constellations.insert(
473                        constellation.to_string(),
474                        ConstellationPage::new(constellation, &focused),
475                    );
476                    //}
477                }
478                constellations
479            },
480        }
481    }
482}
483
484impl Render for Report {
485    fn render(&self) -> Markup {
486        html! {
487            div class="table-container" {
488                @if let Some(rx) = &self.receiver {
489                    table class="table is-bordered" {
490                        tr {
491                            th class="is-info" {
492                                "Receiver"
493                            }
494                            // TODO
495                            // td {
496                            //     (rx.render())
497                            // }
498                        }
499                    }
500                }
501                @if let Some(ant) = &self.antenna {
502                    table class="table is-bordered" {
503                        tr {
504                            th class="is-info" {
505                                "Antenna"
506                            }
507                            // TODO
508                            // td {
509                            //      (ant.render())
510                            // }
511                        }
512                    }
513                }
514                table class="table is-bordered" {
515                    tr {
516                        th class="is-info" {
517                            "Sampling"
518                        }
519                        td {
520                            (self.sampling.render())
521                        }
522                    }
523                }
524                @for constell in self.constellations.keys().sorted() {
525                    @if let Some(page) = self.constellations.get(constell) {
526                        table class="table is-bordered is-page" id=(format!("body:obs:{}", constell)) style="display:none" {
527                            tr {
528                                th class="is-info" {
529                                    (constell.to_string())
530                                }
531                                td {
532                                    (page.render())
533                                }
534                            }
535                        }
536                    }
537                }
538            }//table-container
539        }
540    }
541}