gnss_qc/report/shared/sampling.rs
1use maud::{html, Markup, Render};
2use rinex::prelude::{Duration, Epoch, Rinex};
3
4#[cfg(feature = "sp3")]
5use sp3::SP3;
6
7/// [SamplingReport] applies to all time domain products
8pub struct SamplingReport {
9 /// Total epochs
10 pub total: usize,
11 /// First [`Epoch`] identified in time
12 pub first_epoch: Epoch,
13 /// Last [`Epoch`] identified in time
14 pub last_epoch: Epoch,
15 /// Time span of this RINEX context
16 pub duration: Duration,
17 /// File [`Header`] sample rate
18 pub sampling_interval: Option<Duration>,
19 /// Dominant sample rate
20 pub dominant_sample_rate: Option<f64>,
21 /// Unusual data gaps
22 pub gaps: Vec<(Epoch, Duration)>,
23 /// longest gap detected
24 pub longest_gap: Option<(Epoch, Duration)>,
25 /// shortest gap detected
26 pub shortest_gap: Option<(Epoch, Duration)>,
27}
28
29impl SamplingReport {
30 pub fn from_rinex(rinex: &Rinex) -> Self {
31 let gaps = rinex.data_gaps(None).collect::<Vec<_>>();
32 Self {
33 total: rinex.epoch_iter().count(),
34 first_epoch: rinex
35 .first_epoch()
36 .expect("failed to determine first RINEX epoch, badly formed?"),
37 last_epoch: rinex
38 .last_epoch()
39 .expect("failed to determine last RINEX epoch, badly formed?"),
40 duration: rinex
41 .duration()
42 .expect("failed to determine RINEX time frame, badly formed?"),
43 sampling_interval: rinex.sampling_interval(),
44 dominant_sample_rate: rinex.dominant_sampling_rate_hz(),
45 shortest_gap: gaps
46 .iter()
47 .min_by(|(_, dur_a), (_, dur_b)| dur_a.partial_cmp(dur_b).unwrap())
48 .copied(),
49 longest_gap: gaps
50 .iter()
51 .max_by(|(_, dur_a), (_, dur_b)| dur_a.partial_cmp(dur_b).unwrap())
52 .copied(),
53 gaps,
54 }
55 }
56 #[cfg(feature = "sp3")]
57 pub fn from_sp3(sp3: &SP3) -> Self {
58 let t_start = sp3
59 .first_epoch()
60 .expect("undetermined first epoch: SP3 format error");
61
62 let t_end = sp3
63 .last_epoch()
64 .expect("undetermined last epoch: SP3 format error");
65 Self {
66 total: sp3.epochs_iter().count(),
67 gaps: Vec::new(), // TODO
68 longest_gap: None, //TODO
69 shortest_gap: None, //TODO
70 last_epoch: t_end,
71 first_epoch: t_start,
72 duration: t_end - t_start,
73 sampling_interval: Some(sp3.header.sampling_period),
74 dominant_sample_rate: Some(1.0 / sp3.header.sampling_period.to_seconds()),
75 }
76 }
77}
78
79impl Render for SamplingReport {
80 fn render(&self) -> Markup {
81 html! {
82 div class="table-container" {
83 table class="table is-bordered" {
84 tbody {
85 tr {
86 th class="is-info" {
87 "Time Frame"
88 }
89 }
90 tr {
91 th {
92 "Start"
93 }
94 td {
95 (self.first_epoch.to_string())
96 }
97 th {
98 "End"
99 }
100 td {
101 (self.last_epoch.to_string())
102 }
103 }
104 tr {
105 th {
106 "Duration"
107 }
108 td {
109 (self.duration.to_string())
110 }
111 }
112 @if let Some(sampling_interval) = self.sampling_interval {
113 tr {
114 th class="is-info" {
115 "Sampling Period"
116 }
117 }
118 tr {
119 td {
120 (sampling_interval.to_string())
121 }
122 }
123 }
124 @if let Some(sample_rate) = self.dominant_sample_rate {
125 tr {
126 th class="is-info" {
127 "Dominant Sampling Rate"
128 }
129 }
130 tr {
131 td {
132 (format!("{:.3} Hz", sample_rate))
133 }
134 }
135 }
136 @if self.gaps.len() == 0 {
137 tr {
138 th class="is-primary" {
139 "Data gaps"
140 }
141 td {
142 "No gaps detected"
143 }
144 }
145 } @else {
146 tr {
147 th class="is-warning" {
148 "Data gaps"
149 }
150 td {
151 (format!("{} Data gaps", self.gaps.len()))
152 }
153 }
154 @if let Some((t_start, dur)) = self.shortest_gap {
155 tr {
156 th {
157 "Shortest"
158 }
159 td {
160 (t_start.to_string())
161 }
162 td {
163 (dur.to_string())
164 }
165 }
166 }
167 @if let Some((t_start, dur)) = self.longest_gap {
168 tr {
169 th {
170 "Longest"
171 }
172 td {
173 (t_start.to_string())
174 }
175 td {
176 (dur.to_string())
177 }
178 }
179 }
180 }
181 }
182 }
183 }
184 }
185 }
186}