gnss_qc/report/rinex/
clock.rs

1use itertools::Itertools;
2use maud::{html, Markup, Render};
3
4use qc_traits::{Filter, FilterItem, MaskOperand, Preprocessing};
5
6use rinex::prelude::{clock::ClockProfileType, Constellation, Rinex, TimeScale, DOMES, SV};
7use std::collections::HashMap;
8
9use crate::report::shared::SamplingReport;
10use crate::report::Error;
11
12use crate::plot::{MarkerSymbol, Mode, Plot, Visible};
13
14/// [ClockPage] per [Constellation]
15struct ConstellPage {
16    /// satellites
17    satellites: Vec<SV>,
18    offset_plot: Plot,
19    drift_plot: Plot,
20}
21
22impl ConstellPage {
23    fn new(rinex: &Rinex) -> Self {
24        let satellites = rinex.sv_iter().collect::<Vec<_>>();
25
26        Self {
27            offset_plot: {
28                let mut plot =
29                    Plot::timedomain_plot("clock_offset", "Clock Offset", "Offset [s]", true);
30                for (sv_index, sv) in satellites.iter().enumerate() {
31                    let label = sv.to_string();
32                    let plot_x = rinex
33                        .precise_sv_clock()
34                        .filter_map(|(t, svnn, _, _)| if *sv == svnn { Some(t) } else { None })
35                        .collect::<Vec<_>>();
36                    let plot_y = rinex
37                        .precise_sv_clock()
38                        .filter_map(
39                            |(_, svnn, _, prof)| {
40                                if *sv == svnn {
41                                    Some(prof.bias)
42                                } else {
43                                    None
44                                }
45                            },
46                        )
47                        .collect::<Vec<_>>();
48                    let trace = Plot::timedomain_chart(
49                        &label,
50                        Mode::Markers,
51                        MarkerSymbol::Cross,
52                        &plot_x,
53                        plot_y,
54                        true,
55                    )
56                    .visible({
57                        if sv_index == 0 {
58                            Visible::True
59                        } else {
60                            Visible::LegendOnly
61                        }
62                    });
63                    plot.add_trace(trace);
64                }
65                plot
66            },
67            drift_plot: {
68                let mut plot =
69                    Plot::timedomain_plot("clock_drift", "Clock Drift", "Drift [s/s]", true);
70                for sv in &satellites {
71                    let label = sv.to_string();
72                    let plot_x = rinex
73                        .precise_sv_clock()
74                        .filter_map(|(t, svnn, _, prof)| {
75                            let _ = prof.drift?;
76                            if *sv == svnn {
77                                Some(t)
78                            } else {
79                                None
80                            }
81                        })
82                        .collect::<Vec<_>>();
83                    let plot_y = rinex
84                        .precise_sv_clock()
85                        .filter_map(|(_, svnn, _, prof)| {
86                            let drift = prof.drift?;
87                            if *sv == svnn {
88                                Some(drift)
89                            } else {
90                                None
91                            }
92                        })
93                        .collect::<Vec<_>>();
94                    let trace = Plot::timedomain_chart(
95                        &label,
96                        Mode::Markers,
97                        MarkerSymbol::Cross,
98                        &plot_x,
99                        plot_y,
100                        true,
101                    );
102                    plot.add_trace(trace);
103                }
104                plot
105            },
106            satellites,
107        }
108    }
109}
110
111impl Render for ConstellPage {
112    fn render(&self) -> Markup {
113        html! {
114            div class="table-container" {
115                table class="table is-bordered" {
116                    tr {
117                        th class="is-info" {
118                            button aria-label="Embedded Clocks described in this file." data-balloon-pos="right" {
119                                "Satellites"
120                            }
121                        }
122                        td {
123                            (self.satellites.iter().join(", "))
124                        }
125                    }
126                    tr {
127                        th class="is-info" {
128                            button aria-label="Offset to Constellation" data-balloon-pos="right" {
129                                "Clock offset"
130                            }
131                        }
132                        td {
133                            (self.offset_plot.render())
134                        }
135                    }
136                    tr {
137                        th class="is-info" {
138                            button aria-label="Drift with respect to Constellation" data-balloon-pos="right" {
139                                "Clock drift"
140                            }
141                        }
142                        td {
143                            (self.drift_plot.render())
144                        }
145                    }
146                }
147            }
148        }
149    }
150}
151
152pub struct ClkReport {
153    site: Option<String>,
154    domes: Option<DOMES>,
155    clk_name: Option<String>,
156    sampling: SamplingReport,
157    ref_clock: Option<String>,
158    codes: Vec<ClockProfileType>,
159    igs_clock_name: Option<String>,
160    timescale: Option<TimeScale>,
161    constellations: HashMap<Constellation, ConstellPage>,
162}
163
164impl ClkReport {
165    pub fn html_inline_menu_bar(&self) -> Markup {
166        html! {
167            a id="menu:clk" {
168                span class="icon" {
169                    i class="fa-solid fa-clock" {}
170                }
171                "High Precision Clock (RINEX)"
172            }
173            ul class="menu-list" style="display:none" {
174                @for constell in self.constellations.keys().sorted() {
175                    li {
176                        a id=(&format!("menu:clk:{}", constell)) class="menu:subtab" style="margin-left:29px" {
177                            span class="icon" {
178                                i class="fa-solid fa-satellite" {};
179                            }
180                            (constell.to_string())
181                        }
182                    }
183                }
184            }
185        }
186    }
187    pub fn new(rnx: &Rinex) -> Result<Self, Error> {
188        let clk_header = rnx.header.clock.as_ref().ok_or(Error::MissingClockHeader)?;
189        Ok(Self {
190            site: clk_header.site.clone(),
191            domes: clk_header.domes.clone(),
192            codes: clk_header.codes.clone(),
193            igs_clock_name: clk_header.igs.clone(),
194            clk_name: clk_header.full_name.clone(),
195            ref_clock: clk_header.ref_clock.clone(),
196            timescale: clk_header.timescale.clone(),
197            sampling: SamplingReport::from_rinex(rnx),
198            constellations: {
199                let mut pages = HashMap::<Constellation, ConstellPage>::new();
200                for constellation in rnx.constellations_iter() {
201                    let filter = Filter::mask(
202                        MaskOperand::Equals,
203                        FilterItem::ConstellationItem(vec![constellation]),
204                    );
205                    let focused = rnx.filter(&filter);
206                    pages.insert(constellation, ConstellPage::new(&focused));
207                }
208                pages
209            },
210        })
211    }
212}
213
214impl Render for ClkReport {
215    fn render(&self) -> Markup {
216        html! {
217            div class="table-container" {
218                table class="table is-bordered" {
219                    tbody {
220                        @if let Some(clk_name) = &self.clk_name {
221                            tr {
222                                th class="is-info" {
223                                    "Agency"
224                                }
225                                td {
226                                    (clk_name)
227                                }
228                            }
229                        }
230                        @if let Some(site) = &self.site {
231                            tr {
232                                th class="is-info" {
233                                    "Clock Site"
234                                }
235                                td {
236                                    (site)
237                                }
238                            }
239                        }
240                        @if let Some(domes) = &self.domes {
241                            tr {
242                                th class="is-info" {
243                                    "DOMES #ID"
244                                }
245                                td {
246                                    (domes.to_string())
247                                }
248                            }
249                        }
250                        tr {
251                            th class="is-info" {
252                                "Clock Profiles"
253                            }
254                            td {
255                                (self.codes.iter().join(", "))
256                            }
257                        }
258                        @if let Some(ref_clk) = &self.ref_clock {
259                            tr {
260                                th class="is-info" {
261                                    "Reference Clock"
262                                }
263                                td {
264                                    (ref_clk)
265                                }
266                            }
267                        }
268                        @if let Some(igs_name) = &self.igs_clock_name {
269                            tr {
270                                th class="is-info" {
271                                    "IGS Clock Name"
272                                }
273                                td {
274                                    (igs_name)
275                                }
276                            }
277                        }
278                        @if let Some(timescale) = self.timescale {
279                            tr {
280                                th class="is-info" {
281                                    button aria-label="Timescale in which Clock states are expressed.
282        In PPP context, this should be identical to your Observation RINEX for optimal precision." data-balloon-pos="right" {
283                                        "Timescale"
284                                    }
285                                }
286                                td {
287                                    (timescale.to_string())
288                                }
289                            }
290                        }
291                    }
292                }
293            }
294            div class="table-container" {
295                (self.sampling.render())
296            }
297            @for constell in self.constellations.keys().sorted() {
298                @if let Some(page) = self.constellations.get(&constell) {
299                    div class="table-container" {
300                        table class="table is-bordered" {
301                            tr {
302                                th class="is-info" {
303                                    (constell.to_string())
304                                }
305                                td {
306                                    (page.render())
307                                }
308                            }
309                        }
310                    }
311                }
312            }
313        }
314    }
315}