gnss_qc/report/rinex/
ionex.rs1use crate::report::Error;
2use maud::{html, Markup, Render};
3use rinex::ionex::{MappingFunction, RefSystem as Reference};
4use rinex::prelude::{Duration, Epoch, Rinex};
5
6use crate::plot::{MapboxStyle, Plot, Visible};
7
8use plotly::{
9 layout::update_menu::{Button, ButtonBuilder},
10 DensityMapbox,
11};
12
13pub struct IonexReport {
14 nb_of_maps: usize,
15 map_dimension: u8,
16 epoch_first_map: Epoch,
17 epoch_last_map: Epoch,
18 sampling_interval: Option<Duration>,
19 reference: Reference,
20 description: Option<String>,
21 mapping: Option<MappingFunction>,
22 world_map: Plot,
23}
24
25impl IonexReport {
26 pub fn new(rnx: &Rinex) -> Result<Self, Error> {
27 let nb_of_maps = rnx.epoch_iter().count();
28 let header = rnx.header.ionex.as_ref().ok_or(Error::MissingIonexHeader)?;
29 Ok(Self {
30 nb_of_maps,
31 map_dimension: header.map_dimension,
32 epoch_last_map: header.epoch_of_last_map,
33 epoch_first_map: header.epoch_of_first_map,
34 mapping: header.mapping.clone(),
35 reference: header.reference.clone(),
36 description: header.description.clone(),
37 sampling_interval: rnx.sampling_interval(),
38 world_map: {
39 let mut plot = Plot::world_map(
40 "ionex_tec",
41 "Ionosphere TEC maps",
42 MapboxStyle::OpenStreetMap,
43 (32.5, -40.0),
44 0,
45 true,
46 );
47
48 let mut buttons = Vec::<Button>::new();
50
51 for (epoch_index, epoch) in rnx.epoch_iter().enumerate() {
52 let label = epoch.to_string();
53
54 let lat = rnx
55 .ionex_tecu_latlong_ddeg_alt_km_iter()
56 .filter_map(
57 |(t, _, lat, _, _)| {
58 if t == epoch {
59 Some(lat)
60 } else {
61 None
62 }
63 },
64 )
65 .collect::<Vec<_>>();
66
67 let long = rnx
68 .ionex_tecu_latlong_ddeg_alt_km_iter()
69 .filter_map(
70 |(t, _, long, _, _)| {
71 if t == epoch {
72 Some(long)
73 } else {
74 None
75 }
76 },
77 )
78 .collect::<Vec<_>>();
79
80 let tecu = rnx
81 .ionex_tecu_latlong_ddeg_alt_km_iter()
82 .filter_map(
83 |(t, tecu, _, _, _)| {
84 if t == epoch {
85 Some(tecu)
86 } else {
87 None
88 }
89 },
90 )
91 .collect::<Vec<_>>();
92
93 let trace = Plot::density_mapbox(
94 lat.clone(),
95 long.clone(),
96 tecu,
97 &label,
98 0.6,
99 3,
100 epoch_index == 0,
101 );
102
103 plot.add_trace(trace);
104
105 buttons.push(
106 ButtonBuilder::new()
107 .name("Epoch")
108 .label(&label)
109 .push_restyle(DensityMapbox::<f64, f64, f64>::modify_visible(
110 (0..nb_of_maps)
111 .map(|i| {
112 if epoch_index == i {
113 Visible::True
114 } else {
115 Visible::False
116 }
117 })
118 .collect(),
119 ))
120 .build(),
121 );
122 }
123
124 plot.add_custom_controls(buttons);
125 plot
126 },
127 })
128 }
129 pub fn html_inline_menu_bar(&self) -> Markup {
130 html! {
131 a id="menu:ionex" {
132 span class="icon" {
133 i class="fa-solid fa-earth-americas" {}
134 }
135 "Ionosphere Maps (IONEX)"
136 }
137 }
138 }
139}
140
141impl Render for IonexReport {
142 fn render(&self) -> Markup {
143 html! {
144 table class="table is-bordered" {
145 tr {
146 @if self.map_dimension == 2 {
147 th class="is-info"{
148 button aria-label="Isosurface TEC maps" data-balloon-pos="right" {
149 "2D IONEX"
150 }
151 }
152 } @else {
153 th class="is-info" {
154 button aria-label="Isofurface TEC maps by altitude layers" data-balloon-pos="right" {
155 "3D IONEX"
156 }
157 }
158 }
159 }
160 tr {
161 th class="is-info" {
162 "Number of Maps"
163 }
164 td {
165 (self.nb_of_maps)
166 }
167 }
168 tr {
169 th class="is-info" {
170 "Sampling Period"
171 }
172 @ if let Some(sampling_interval) = &self.sampling_interval {
173 td {
174 (sampling_interval.to_string())
175 }
176 } @ else {
177 td class="is-warning" {
178 "Unknown"
179 }
180 }
181 }
182 tr {
183 th class="is-info" {
184 "Epoch of first map"
185 }
186 td {
187 (self.epoch_first_map.to_string())
188 }
189 }
190 tr {
191 th class="is-info" {
192 "Epoch of Last map"
193 }
194 td {
195 (self.epoch_last_map.to_string())
196 }
197 }
198 tr {
199 th class="is-info" {
200 "Reference"
201 }
202 td {
203 (self.reference.to_string())
204 }
205 }
206 @if let Some(desc) = &self.description {
207 tr {
208 th class="is-info" {
209 "Description"
210 }
211 td {
212 (desc)
213 }
214 }
215 }
216 @if let Some(mapf) = &self.mapping {
217 tr {
218 th class="is-info" {
219 button aria-label="Mapping function used in TEC map evaluation" data-balloon-pos="right" {
220 "Mapping function"
221 }
222 }
223 td {
224 (mapf.to_string())
225 }
226 }
227 }
228 tr {
229 th class="is-info" {
230 "TEC Map"
231 }
232 }
233 tr {
234 td {
235 (self.world_map.render())
236 }
237 }
238 }
239 }
240 }
241}