gnss_qc/report/rinex/
clock.rs1use 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
14struct ConstellPage {
16 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}