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
59struct FrequencyPage {
61 total_spp_epochs: usize,
63 total_cpp_epochs: usize,
65 total_ppp_epochs: usize,
67 sampling: SamplingReport,
69 raw_plots: HashMap<Physics, Plot>,
71 combination_plots: HashMap<Combination, Plot>,
73 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 }
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 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
234struct ConstellationPage {
236 satellites: Vec<SV>,
238 sampling: SamplingReport,
240 spp_compatible: bool,
242 cpp_compatible: bool,
244 ppp_compatible: bool,
246 frequencies: HashMap<String, FrequencyPage>,
248 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; let mut cpp_compatible = false; let mut ppp_compatible = false; 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
395pub 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 let focused = rinex.filter(&filter);
472 constellations.insert(
473 constellation.to_string(),
474 ConstellationPage::new(constellation, &focused),
475 );
476 }
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 }
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 }
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 }}
540 }
541}