rinex/antex/
record.rs

1use std::{collections::HashMap, str::FromStr};
2
3use crate::{
4    antex::{Antenna, AntennaSpecific, Calibration, CalibrationMethod, RxAntenna, SvAntenna},
5    linspace::Linspace,
6    prelude::{Carrier, Epoch, ParsingError, COSPAR, SV},
7};
8
9#[cfg(feature = "serde")]
10use serde::Serialize;
11
12/// Returns true if this line matches
13/// the beginning of a `epoch` for ATX file (special files),
14/// this is not really an epoch but rather a group of dataset
15/// for this given antenna, there is no sampling data attached to it.
16pub(crate) fn is_new_epoch(content: &str) -> bool {
17    content.contains("START OF ANTENNA")
18}
19
20/// Phase pattern description.
21/// We currently do not support azimuth dependent phase patterns.
22#[derive(Debug, Clone, PartialEq, PartialOrd)]
23#[cfg_attr(feature = "serde", derive(Serialize))]
24pub enum AntennaPhasePattern {
25    /// Azimuth Independent Phase pattern
26    AzimuthIndependentPattern(Vec<f64>),
27}
28
29impl Default for AntennaPhasePattern {
30    fn default() -> Self {
31        Self::AzimuthIndependentPattern(Vec::<f64>::new())
32    }
33}
34
35#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
36#[cfg_attr(feature = "serde", derive(Serialize))]
37pub struct FrequencyDependentData {
38    /// Eccentricities of the mean APC as NEU coordinates in millimeters.
39    /// The offset position is either relative to
40    /// Antenna Reference point (ARP), if this is an [`RxAntenna`],
41    /// or the Spacecraft Mass Center, if this is an [`SvAntenna`].
42    pub apc_eccentricity: (f64, f64, f64),
43    /// Antenna Phase Pattern.
44    /// We currently do not support Azimuth Dependent phase patterns.
45    pub phase_pattern: AntennaPhasePattern,
46}
47
48/// ANTEX RINEX record content.
49/// Data is a list of Antenna containing several [Frequency] items.
50/// We do not parse RMS frequencies at the moment, but it will
51/// easily be unlocked in near future.
52/*TODO
53/// Record browsing example:
54/// ```
55/// // grab ATX RINEX
56///
57/// // grab record
58/// let record = record.as_antex()
59///    .unwrap();
60/// // browse antennas
61/// for (antenna, frequencies) in record.iter() {
62///   let calibration = antenna.calibration;
63///   // several calibration methods exist
64///   if calibration.method == CalibrationMethod::Chamber {
65///     // calibration is certified
66///     // from `calibration.valid_from` (chrono::NaiveDateTime)
67///     // until `calibration.valid_until` (chrono::NaiveDateTime)
68///   }
69///   // calibration process informations
70///   assert_eq!(calibration.agency, "Some agency");
71///   assert_eq!(calibration.date, "DateTime description");
72///   // antenna information
73///   assert_eq!(antenna.sn, "Serial Number");
74///   assert_eq!(antenna.dazi, 1.0);
75///   for frequency in frequencies.iter() {
76///     for pattern in frequency.patterns {
77///         assert_eq!(pattern.is_azimuth_dependent(), true);
78///         let Some((azimuth, phase_pattern)) = pattern.azimuth_dependent() {
79///             for raw_phase in phase_patter.iter() {
80///                 // raw phase pattern data
81///             }
82///         }
83///     }
84///   }
85/// }
86/// ```
87*/
88pub type Record = Vec<(Antenna, HashMap<Carrier, FrequencyDependentData>)>;
89
90fn parse_datetime(content: &str) -> Result<Epoch, ParsingError> {
91    let mut parser = content.split('-');
92
93    let year = parser.next().ok_or(ParsingError::DatetimeFormat)?;
94
95    let year = year
96        .parse::<i32>()
97        .map_err(|_| ParsingError::DatetimeParsing)?;
98
99    let month = parser.next().ok_or(ParsingError::DatetimeFormat)?;
100
101    let month = match month {
102        "JAN" | "Jan" => 1,
103        "FEB" | "Feb" => 2,
104        "MAR" | "Mar" => 3,
105        "APR" | "Apr" => 4,
106        "MAY" | "May" => 5,
107        "JUN" | "Jun" => 6,
108        "JUL" | "Jul" => 7,
109        "AUG" | "Aug" => 8,
110        "SEP" | "Sep" => 9,
111        "OCT" | "Oct" => 10,
112        "NOV" | "Nov" => 11,
113        "DEC" | "Dec" => 12,
114        _ => {
115            return Err(ParsingError::DatetimeParsing);
116        },
117    };
118
119    let day = parser.next().ok_or(ParsingError::DatetimeFormat)?;
120    let day = day
121        .parse::<u8>()
122        .map_err(|_| ParsingError::DatetimeParsing)?;
123
124    Ok(Epoch::from_gregorian_utc_at_midnight(
125        2000 + year,
126        month,
127        day,
128    ))
129}
130
131/*
132 * Parses the calibration validity FROM/UNTIL field
133 */
134fn parse_validity_epoch(content: &str) -> Result<Epoch, ParsingError> {
135    let mut items = content.split_ascii_whitespace();
136
137    let year = items.next().ok_or(ParsingError::DatetimeFormat)?;
138
139    let year = year
140        .parse::<i32>()
141        .map_err(|_| ParsingError::DatetimeParsing)?;
142
143    let month = items.next().ok_or(ParsingError::DatetimeFormat)?;
144
145    let month = month
146        .parse::<u8>()
147        .map_err(|_| ParsingError::DatetimeParsing)?;
148
149    let day = items.next().ok_or(ParsingError::DatetimeFormat)?;
150    let day = day
151        .parse::<u8>()
152        .map_err(|_| ParsingError::DatetimeParsing)?;
153
154    let hh = items.next().ok_or(ParsingError::DatetimeFormat)?;
155    let hh = hh
156        .parse::<u8>()
157        .map_err(|_| ParsingError::DatetimeParsing)?;
158
159    let mm = items.next().ok_or(ParsingError::DatetimeFormat)?;
160    let mm = mm.parse::<u8>().map_err(|_| ParsingError::DatetimeFormat)?;
161
162    let ss = items.next().ok_or(ParsingError::DatetimeParsing)?;
163
164    let secs: u8;
165    let mut nanos = 0_u32;
166
167    if let Some(dot) = ss.find('.') {
168        secs = ss[..dot]
169            .trim()
170            .parse::<u8>()
171            .map_err(|_| ParsingError::DatetimeParsing)?;
172
173        nanos = ss[dot + 1..]
174            .trim()
175            .parse::<u32>()
176            .map_err(|_| ParsingError::DatetimeParsing)?;
177    } else {
178        secs = ss
179            .parse::<u8>()
180            .map_err(|_| ParsingError::DatetimeParsing)?;
181    }
182
183    Ok(Epoch::from_gregorian_utc(
184        year, month, day, hh, mm, secs, nanos,
185    ))
186}
187
188/// Parses entire Antenna block
189/// and all inner frequency entries
190pub(crate) fn parse_antenna(
191    content: &str,
192) -> Result<(Antenna, HashMap<Carrier, FrequencyDependentData>), ParsingError> {
193    let lines = content.lines();
194    let mut antenna = Antenna::default();
195    let mut inner = HashMap::<Carrier, FrequencyDependentData>::new();
196    let mut frequency = Carrier::default();
197    let mut freq_data = FrequencyDependentData::default();
198    let mut valid_from = Epoch::default();
199
200    for line in lines {
201        let (content, marker) = line.split_at(60);
202        if marker.contains("TYPE / SERIAL NO") {
203            let (ant_igs, rem) = content.split_at(16); // IGS V.1.4 does not follow the specs ?
204            let (block1, rem) = rem.split_at(20 + 4);
205            let (block2, rem) = rem.split_at(10);
206            let (block3, _rem) = rem.split_at(10);
207
208            let (block1, block2, block3) = (block1.trim(), block2.trim(), block3.trim());
209            /*
210             * SV/RX antenna determination
211             */
212            let specificities = match block2.is_empty() && block3.is_empty() {
213                false => AntennaSpecific::SvAntenna(SvAntenna {
214                    igs_type: ant_igs.trim().to_string(),
215                    sv: SV::from_str(block1)?,
216                    cospar: COSPAR::from_str(block3)?,
217                }),
218                true => AntennaSpecific::RxAntenna(RxAntenna {
219                    igs_type: ant_igs.trim().to_string(),
220                    serial_number: {
221                        if !block1.is_empty() && !block1.eq("NONE") {
222                            Some(block1.to_string())
223                        } else {
224                            None
225                        }
226                    },
227                }),
228            };
229            antenna = antenna.with_specificities(specificities);
230        } else if marker.contains("METH / BY / # / DATE") {
231            let (method, rem) = content.split_at(20);
232            let (agency, rem) = rem.split_at(20);
233            let (number, rem) = rem.split_at(10); // N#
234            let (date, _) = rem.split_at(10);
235
236            let cal = Calibration {
237                method: CalibrationMethod::from_str(method.trim()).unwrap(),
238                number: number
239                    .trim()
240                    .parse::<u16>()
241                    .map_err(|_| ParsingError::AntexAntennaCalibrationNumber)?,
242                agency: agency.trim().to_string(),
243                date: parse_datetime(date.trim())?,
244                validity_period: None,
245            };
246
247            antenna.calibration = cal.clone();
248        } else if marker.contains("VALID FROM") {
249            valid_from = parse_validity_epoch(content.trim())?;
250        } else if marker.contains("VALID UNTIL") {
251            let valid_until = parse_validity_epoch(content.trim())?;
252
253            antenna = antenna.with_validity_period(valid_from, valid_until);
254        } else if marker.contains("SINEX CODE") {
255            let sinex = content.split_at(20).0;
256
257            antenna.sinex_code = sinex.trim().to_string();
258        } else if marker.contains("DAZI") {
259            //let dazi = content.split_at(20).0.trim();
260            //if let Ok(dazi) = f64::from_str(dazi) {
261            //    antenna = antenna.with_dazi(dazi)
262            //}
263        } else if marker.contains("# OF FREQUENCIES") {
264            /*
265             * we actually do not care about this field
266             * it is easy to determine it from the current infrastructure
267             */
268        } else if marker.contains("START OF FREQUENCY") {
269            let svnn = content.split_at(10).0;
270            let sv = SV::from_str(svnn.trim())?;
271            frequency = Carrier::from_sv(sv)?;
272        } else if marker.contains("NORTH / EAST / UP") {
273            let (north, rem) = content.split_at(10);
274            let (east, rem) = rem.split_at(10);
275            let (up, _) = rem.split_at(10);
276
277            let north = north
278                .trim()
279                .parse::<f64>()
280                .map_err(|_| ParsingError::AntexAPCCoordinates)?;
281
282            let east = east
283                .trim()
284                .parse::<f64>()
285                .map_err(|_| ParsingError::AntexAPCCoordinates)?;
286
287            let up = up
288                .trim()
289                .parse::<f64>()
290                .map_err(|_| ParsingError::AntexAPCCoordinates)?;
291
292            freq_data.apc_eccentricity = (north, east, up);
293        } else if marker.contains("ZEN1 / ZEN2 / DZEN") {
294            let (start, rem) = content.split_at(8);
295            let (end, rem) = rem.split_at(6);
296            let (spacing, _) = rem.split_at(6);
297
298            let start = start
299                .trim()
300                .parse::<f64>()
301                .map_err(|_| ParsingError::AntexZenithGrid)?;
302
303            let end = end
304                .trim()
305                .parse::<f64>()
306                .map_err(|_| ParsingError::AntexZenithGrid)?;
307
308            let spacing = spacing
309                .trim()
310                .parse::<f64>()
311                .map_err(|_| ParsingError::AntexZenithGrid)?;
312
313            antenna.zenith_grid = Linspace {
314                start,
315                end,
316                spacing,
317            };
318        } else if marker.contains("END OF FREQUENCY") {
319            inner.insert(frequency, freq_data.clone());
320        } else if marker.contains("END OF ANTENNA") {
321            break; // end of this block, considered as an `epoch`
322                   // if we make a parallel with other types of RINEX
323        } else {
324            // inside phase pattern
325        }
326        //    } else if marker.contains("SINEX CODE") {
327        //        let sinex = content.split_at(10).0;
328        //        antenna = antenna.with_sinex_code(sinex.trim())
329        //        if line.contains("NOAZI") {
330        //            frequency = frequency.add_pattern(Pattern::NonAzimuthDependent(values.clone()))
331        //        } else {
332        //            let angle = f64::from_str(content.trim()).unwrap();
333        //            frequency =
334        //                frequency.add_pattern(Pattern::AzimuthDependent((angle, values.clone())))
335        //        }
336        //    }
337    }
338
339    Ok((antenna, inner))
340}
341
342#[cfg(test)]
343mod test {
344    use super::*;
345    #[test]
346    fn test_new_epoch() {
347        let content = "                                                           START OF ANTENNA";
348        assert!(is_new_epoch(content));
349        let content =
350            "TROSAR25.R4      LEIT727259                                 TYPE / SERIAL NO";
351        assert!(!is_new_epoch(content));
352        let content =
353            "    26                                                      # OF FREQUENCIES";
354        assert!(!is_new_epoch(content));
355        let content =
356            "   G01                                                      START OF FREQUENCY";
357        assert!(!is_new_epoch(content));
358    }
359}