1use crate::antenna;
8use crate::constants::MM_PER_M;
9use crate::parse::{fortran_f64, raw_field};
10use crate::validate;
11use std::collections::BTreeMap;
12use std::fmt;
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct Antex {
17 pub antennas: BTreeMap<String, Antenna>,
18 antenna_intervals: BTreeMap<String, Vec<Antenna>>,
19}
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct Antenna {
24 pub id: String,
25 pub kind: AntennaKind,
26 pub antenna_type: String,
27 pub serial: String,
28 pub dazi_deg: f64,
29 pub zenith_start_deg: f64,
30 pub zenith_end_deg: f64,
31 pub zenith_step_deg: f64,
32 pub sinex_code: Option<String>,
33 pub valid_from: Option<AntexDateTime>,
34 pub valid_until: Option<AntexDateTime>,
35 pub frequencies: BTreeMap<String, Frequency>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum AntennaKind {
41 Receiver,
42 Satellite,
43}
44
45#[derive(Debug, Clone, PartialEq)]
47pub struct Frequency {
48 pub frequency: String,
49 pub pco_m: [f64; 3],
50 pub pcv_samples: Vec<PcvSample>,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub struct PcvSample {
56 pub grid: PcvGrid,
57 pub azimuth_deg: Option<f64>,
58 pub zenith_deg: f64,
59 pub value_m: f64,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum PcvGrid {
65 NoAzimuth,
66 Azimuth,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
71pub struct AntexDateTime {
72 pub year: i32,
73 pub month: u8,
74 pub day: u8,
75 pub hour: u8,
76 pub minute: u8,
77 pub second: u8,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum AntexError {
83 InvalidDateTime,
84 InvalidInput {
85 field: &'static str,
86 reason: &'static str,
87 },
88 UnknownFrequency {
89 antenna_id: String,
90 frequency: String,
91 },
92 MissingPco {
93 antenna_id: String,
94 frequency: String,
95 },
96 EmptyPcvGrid {
97 antenna_id: String,
98 frequency: String,
99 },
100}
101
102impl fmt::Display for AntexError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::InvalidDateTime => write!(f, "invalid ANTEX datetime"),
106 Self::InvalidInput { field, reason } => {
107 write!(f, "invalid ANTEX input {field}: {reason}")
108 }
109 Self::UnknownFrequency {
110 antenna_id,
111 frequency,
112 } => write!(f, "unknown frequency {frequency:?} for {antenna_id:?}"),
113 Self::MissingPco {
114 antenna_id,
115 frequency,
116 } => write!(
117 f,
118 "missing or malformed PCO for frequency {frequency:?} on {antenna_id:?}"
119 ),
120 Self::EmptyPcvGrid {
121 antenna_id,
122 frequency,
123 } => write!(
124 f,
125 "empty PCV grid for frequency {frequency:?} on {antenna_id:?}"
126 ),
127 }
128 }
129}
130
131impl std::error::Error for AntexError {}
132
133#[derive(Debug, Clone)]
134struct ParseState {
135 antennas: BTreeMap<String, Antenna>,
136 antenna_intervals: BTreeMap<String, Vec<Antenna>>,
137 current_antenna: Option<Antenna>,
138 current_frequency: Option<FrequencyState>,
139}
140
141#[derive(Debug, Clone)]
142struct FrequencyState {
143 frequency: String,
144 phase: FrequencyPhase,
145 pco_m: Option<[f64; 3]>,
146 samples: Vec<PcvSample>,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150enum FrequencyPhase {
151 Pco,
152 Pcv,
153}
154
155impl Antex {
156 pub fn parse(text: &str) -> Result<Self, AntexError> {
158 let mut state = ParseState {
159 antennas: BTreeMap::new(),
160 antenna_intervals: BTreeMap::new(),
161 current_antenna: None,
162 current_frequency: None,
163 };
164
165 for line in text.lines() {
166 step(line, &mut state)?;
167 }
168 finalize_antenna(&mut state)?;
169
170 Ok(Self {
171 antennas: state.antennas,
172 antenna_intervals: state.antenna_intervals,
173 })
174 }
175
176 pub fn antenna(&self, id: &str) -> Option<&Antenna> {
178 self.antennas.get(id.trim())
179 }
180
181 pub fn antenna_intervals(&self, id: &str) -> impl Iterator<Item = &Antenna> {
183 self.antenna_intervals.get(id.trim()).into_iter().flatten()
184 }
185
186 pub fn antenna_at(&self, id: &str, epoch: AntexDateTime) -> Option<&Antenna> {
188 self.antenna_intervals(id)
189 .find(|antenna| antenna.valid_at(epoch))
190 }
191
192 pub fn satellite_antenna(&self, prn: &str, epoch: AntexDateTime) -> Option<&Antenna> {
194 let prn = prn.trim();
195 self.antenna_intervals.values().flatten().find(|antenna| {
196 antenna.kind == AntennaKind::Satellite
197 && antenna.serial.trim() == prn
198 && antenna.valid_at(epoch)
199 })
200 }
201}
202
203impl Antenna {
204 pub fn valid_at(&self, epoch: AntexDateTime) -> bool {
206 self.valid_from.is_none_or(|from| epoch >= from)
207 && self.valid_until.is_none_or(|until| epoch <= until)
208 }
209
210 pub fn pco(&self, frequency: &str) -> Result<[f64; 3], AntexError> {
212 self.frequencies
213 .get(frequency.trim())
214 .map(|f| f.pco_m)
215 .ok_or_else(|| AntexError::UnknownFrequency {
216 antenna_id: self.id.clone(),
217 frequency: frequency.to_string(),
218 })
219 }
220
221 pub fn pcv(
223 &self,
224 frequency: &str,
225 zenith_deg: f64,
226 azimuth_deg: Option<f64>,
227 ) -> Result<f64, AntexError> {
228 validate_pcv_zenith(zenith_deg, self.zenith_start_deg, self.zenith_end_deg)?;
229
230 let frequency =
231 self.frequencies
232 .get(frequency.trim())
233 .ok_or_else(|| AntexError::UnknownFrequency {
234 antenna_id: self.id.clone(),
235 frequency: frequency.to_string(),
236 })?;
237
238 frequency.pcv(self.id.as_str(), zenith_deg, azimuth_deg)
239 }
240}
241
242impl Frequency {
243 fn pcv(
244 &self,
245 antenna_id: &str,
246 zenith_deg: f64,
247 azimuth_deg: Option<f64>,
248 ) -> Result<f64, AntexError> {
249 let noazi: Vec<(f64, f64)> = self
250 .pcv_samples
251 .iter()
252 .filter(|sample| sample.grid == PcvGrid::NoAzimuth)
253 .map(|sample| (sample.zenith_deg, sample.value_m))
254 .collect();
255
256 let has_azimuth = self
257 .pcv_samples
258 .iter()
259 .any(|sample| sample.grid == PcvGrid::Azimuth);
260
261 if azimuth_deg.is_none() || !has_azimuth {
262 return interpolate(antenna_id, &self.frequency, &noazi, zenith_deg);
263 }
264
265 let mut azimuth_samples: BTreeMap<OrderedF64, Vec<(f64, f64)>> = BTreeMap::new();
266 for sample in self
267 .pcv_samples
268 .iter()
269 .filter(|sample| sample.grid == PcvGrid::Azimuth)
270 {
271 if let Some(azimuth) = sample.azimuth_deg {
272 azimuth_samples
273 .entry(OrderedF64(azimuth))
274 .or_default()
275 .push((sample.zenith_deg, sample.value_m));
276 }
277 }
278
279 if azimuth_samples.is_empty() {
280 interpolate(antenna_id, &self.frequency, &noazi, zenith_deg)
281 } else {
282 interpolate_azimuth(
283 antenna_id,
284 &self.frequency,
285 &azimuth_samples,
286 azimuth_deg.expect("checked Some"),
287 zenith_deg,
288 )
289 }
290 }
291}
292
293fn validate_pcv_zenith(
294 zenith_deg: f64,
295 zenith_start_deg: f64,
296 zenith_end_deg: f64,
297) -> Result<(), AntexError> {
298 validate::finite(zenith_deg, "zenith_deg").map_err(map_antex_field_error)?;
299 if zenith_deg < zenith_start_deg || zenith_deg > zenith_end_deg {
300 return Err(invalid_input("zenith_deg", "out of range"));
301 }
302 Ok(())
303}
304
305fn map_antex_field_error(error: validate::FieldError) -> AntexError {
306 invalid_input(error.field(), error.reason())
307}
308
309fn invalid_input(field: &'static str, reason: &'static str) -> AntexError {
310 AntexError::InvalidInput { field, reason }
311}
312
313impl AntexDateTime {
314 pub fn new(
315 year: i32,
316 month: u8,
317 day: u8,
318 hour: u8,
319 minute: u8,
320 second: u8,
321 ) -> Result<Self, AntexError> {
322 let civil = validate::civil_datetime_with_second_policy(
323 i64::from(year),
324 i64::from(month),
325 i64::from(day),
326 i64::from(hour),
327 i64::from(minute),
328 f64::from(second),
329 validate::CivilSecondPolicy::UtcLike,
330 )
331 .map_err(|_| AntexError::InvalidDateTime)?;
332 Ok(Self::from_valid_civil(civil))
333 }
334
335 fn from_valid_civil(civil: validate::ValidCivil) -> Self {
336 Self {
337 year: civil.year as i32,
338 month: civil.month as u8,
339 day: civil.day as u8,
340 hour: civil.hour as u8,
341 minute: civil.minute as u8,
342 second: civil.second.trunc() as u8,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Copy, PartialEq)]
348struct OrderedF64(f64);
349
350impl Eq for OrderedF64 {}
351
352impl PartialOrd for OrderedF64 {
353 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
354 Some(self.cmp(other))
355 }
356}
357
358impl Ord for OrderedF64 {
359 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
360 self.0.total_cmp(&other.0)
361 }
362}
363
364fn step(line: &str, state: &mut ParseState) -> Result<(), AntexError> {
365 match tag(line) {
366 "START OF ANTENNA" => {
367 finalize_antenna(state)?;
368 state.current_antenna = None;
369 state.current_frequency = None;
370 }
371 "END OF ANTENNA" => finalize_antenna(state)?,
372 "TYPE / SERIAL NO" => parse_type_serial(line, state),
373 "DAZI" => parse_dazi(line, state),
374 "ZEN1 / ZEN2 / DZEN" => parse_zenith_grid(line, state),
375 "SINEX CODE" => parse_sinex_code(line, state),
376 "VALID FROM" => parse_valid(line, state, ValidField::From)?,
377 "VALID UNTIL" => parse_valid(line, state, ValidField::Until)?,
378 "START OF FREQUENCY" => begin_frequency(line, state),
379 "END OF FREQUENCY" => finalize_frequency(state)?,
380 "NORTH / EAST / UP" => parse_pco(line, state),
381 _ => parse_pcv_row(line, state),
382 }
383 Ok(())
384}
385
386fn parse_type_serial(line: &str, state: &mut ParseState) {
387 state.current_antenna = Some(decode_antenna_header(line));
388 state.current_frequency = None;
389}
390
391fn parse_dazi(line: &str, state: &mut ParseState) {
392 let Some(current) = state.current_antenna.as_mut() else {
393 return;
394 };
395 if let Some(dazi) = parse_floats_from_prefix(line).first() {
396 current.dazi_deg = *dazi;
397 }
398}
399
400fn parse_zenith_grid(line: &str, state: &mut ParseState) {
401 let Some(current) = state.current_antenna.as_mut() else {
402 return;
403 };
404 let values = parse_floats_from_prefix(line);
405 if values.len() >= 3 {
406 current.zenith_start_deg = values[0];
407 current.zenith_end_deg = values[1];
408 current.zenith_step_deg = values[2];
409 }
410}
411
412fn parse_sinex_code(line: &str, state: &mut ParseState) {
413 let Some(current) = state.current_antenna.as_mut() else {
414 return;
415 };
416 let code = raw_field(line, 0, 60).trim();
417 if !code.is_empty() {
418 current.sinex_code = Some(code.to_string());
419 }
420}
421
422#[derive(Debug, Clone, Copy)]
423enum ValidField {
424 From,
425 Until,
426}
427
428fn parse_valid(line: &str, state: &mut ParseState, field: ValidField) -> Result<(), AntexError> {
429 let Some(current) = state.current_antenna.as_mut() else {
430 return Ok(());
431 };
432 let values = parse_floats_from_prefix(line);
433 if values.len() >= 6 {
434 let year = datetime_i32(values[0])?;
435 let month = datetime_u8(values[1])?;
436 let day = datetime_u8(values[2])?;
437 let hour = datetime_u8(values[3])?;
438 let minute = datetime_u8(values[4])?;
439 let civil = validate::civil_datetime_with_second_policy(
440 i64::from(year),
441 i64::from(month),
442 i64::from(day),
443 i64::from(hour),
444 i64::from(minute),
445 values[5],
446 validate::CivilSecondPolicy::UtcLike,
447 )
448 .map_err(|_| AntexError::InvalidDateTime)?;
449 let dt = AntexDateTime::from_valid_civil(civil);
450 match field {
451 ValidField::From => current.valid_from = Some(dt),
452 ValidField::Until => current.valid_until = Some(dt),
453 }
454 }
455 Ok(())
456}
457
458fn datetime_i32(value: f64) -> Result<i32, AntexError> {
459 if !value.is_finite()
460 || value.fract() != 0.0
461 || value < i32::MIN as f64
462 || value > i32::MAX as f64
463 {
464 return Err(AntexError::InvalidDateTime);
465 }
466 Ok(value as i32)
467}
468
469fn datetime_u8(value: f64) -> Result<u8, AntexError> {
470 if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value > u8::MAX as f64 {
471 return Err(AntexError::InvalidDateTime);
472 }
473 Ok(value as u8)
474}
475
476fn decode_antenna_header(line: &str) -> Antenna {
477 let id = raw_field(line, 0, 60).trim().to_string();
478 let antenna_type = raw_field(line, 0, 20).trim().to_string();
479 let serial = raw_field(line, 20, 40).trim().to_string();
480 let kind = if is_satellite_serial(&serial) {
481 AntennaKind::Satellite
482 } else {
483 AntennaKind::Receiver
484 };
485
486 Antenna {
487 id,
488 kind,
489 antenna_type,
490 serial,
491 dazi_deg: 0.0,
492 zenith_start_deg: 0.0,
493 zenith_end_deg: 0.0,
494 zenith_step_deg: 0.0,
495 sinex_code: None,
496 valid_from: None,
497 valid_until: None,
498 frequencies: BTreeMap::new(),
499 }
500}
501
502fn is_satellite_serial(serial: &str) -> bool {
503 let bytes = serial.as_bytes();
504 bytes.len() == 3
505 && bytes[0].is_ascii_uppercase()
506 && bytes[1].is_ascii_digit()
507 && bytes[2].is_ascii_digit()
508}
509
510fn begin_frequency(line: &str, state: &mut ParseState) {
511 if state.current_antenna.is_none() {
512 return;
513 }
514 state.current_frequency = Some(FrequencyState {
515 frequency: raw_field(line, 0, 20).trim().to_string(),
516 phase: FrequencyPhase::Pco,
517 pco_m: None,
518 samples: Vec::new(),
519 });
520}
521
522fn parse_pco(line: &str, state: &mut ParseState) {
523 let Some(current_frequency) = state.current_frequency.as_mut() else {
524 return;
525 };
526 if current_frequency.phase != FrequencyPhase::Pco {
527 return;
528 }
529
530 let values = parse_floats_from_prefix(line);
531 if values.len() >= 3 && values[..3].iter().all(|value| value.is_finite()) {
532 current_frequency.pco_m = Some([
533 values[0] / MM_PER_M,
534 values[1] / MM_PER_M,
535 values[2] / MM_PER_M,
536 ]);
537 current_frequency.phase = FrequencyPhase::Pcv;
538 }
539}
540
541fn parse_pcv_row(line: &str, state: &mut ParseState) {
542 if state
543 .current_frequency
544 .as_ref()
545 .is_none_or(|frequency| frequency.phase != FrequencyPhase::Pcv)
546 {
547 return;
548 }
549
550 let tokens = parse_tokens(line);
551 let Some((first, values)) = tokens.split_first() else {
552 return;
553 };
554
555 if *first == "NOAZI" {
556 add_pcv_values(None, values, state);
557 } else if let Some(azimuth) = parse_float(first) {
558 add_pcv_values(Some(azimuth), values, state);
559 }
560}
561
562fn add_pcv_values(azimuth_deg: Option<f64>, values: &[&str], state: &mut ParseState) {
563 let Some(current_antenna) = state.current_antenna.as_ref() else {
564 return;
565 };
566 let Some(current_frequency) = state.current_frequency.as_mut() else {
567 return;
568 };
569
570 let grid_start = current_antenna.zenith_start_deg;
571 let grid_step = current_antenna.zenith_step_deg;
572 for (index, value_text) in values.iter().enumerate() {
573 let Some(value) = parse_float(value_text) else {
574 continue;
575 };
576 let zenith_deg = if grid_step == 0.0 {
577 grid_start
578 } else {
579 grid_start + grid_step * index as f64
580 };
581 current_frequency.samples.push(PcvSample {
582 grid: if azimuth_deg.is_some() {
583 PcvGrid::Azimuth
584 } else {
585 PcvGrid::NoAzimuth
586 },
587 azimuth_deg,
588 zenith_deg,
589 value_m: value / MM_PER_M,
590 });
591 }
592}
593
594fn finalize_frequency(state: &mut ParseState) -> Result<(), AntexError> {
595 let Some(current_frequency) = state.current_frequency.take() else {
596 return Ok(());
597 };
598 let Some(current_antenna) = state.current_antenna.as_mut() else {
599 return Ok(());
600 };
601
602 let pco_m = current_frequency
603 .pco_m
604 .ok_or_else(|| AntexError::MissingPco {
605 antenna_id: current_antenna.id.clone(),
606 frequency: current_frequency.frequency.clone(),
607 })?;
608
609 let frequency = Frequency {
610 frequency: current_frequency.frequency,
611 pco_m,
612 pcv_samples: current_frequency.samples,
613 };
614 current_antenna
615 .frequencies
616 .insert(frequency.frequency.clone(), frequency);
617 Ok(())
618}
619
620fn finalize_antenna(state: &mut ParseState) -> Result<(), AntexError> {
621 finalize_frequency(state)?;
622 let Some(current_antenna) = state.current_antenna.take() else {
623 return Ok(());
624 };
625 state
626 .antenna_intervals
627 .entry(current_antenna.id.clone())
628 .or_default()
629 .push(current_antenna.clone());
630 state
631 .antennas
632 .insert(current_antenna.id.clone(), current_antenna);
633 Ok(())
634}
635
636fn interpolate_azimuth(
637 antenna_id: &str,
638 frequency: &str,
639 azimuth_samples: &BTreeMap<OrderedF64, Vec<(f64, f64)>>,
640 azimuth_deg: f64,
641 zenith_deg: f64,
642) -> Result<f64, AntexError> {
643 let azimuth = antenna::normalize_azimuth(azimuth_deg);
644 let azimuths: Vec<f64> = azimuth_samples.keys().map(|az| az.0).collect();
645 let (low_deg, high_deg) = antenna::azimuth_bracket(&azimuths, azimuth);
646
647 let low_samples = &azimuth_samples[&OrderedF64(low_deg)];
648 let high_samples = &azimuth_samples[&OrderedF64(high_deg)];
649
650 let low_value = interpolate(antenna_id, frequency, low_samples, zenith_deg)?;
651 let high_value = interpolate(antenna_id, frequency, high_samples, zenith_deg)?;
652
653 Ok(antenna::blend_azimuth(
654 low_deg, high_deg, azimuth, low_value, high_value,
655 ))
656}
657
658fn interpolate(
659 antenna_id: &str,
660 frequency: &str,
661 samples: &[(f64, f64)],
662 zenith_deg: f64,
663) -> Result<f64, AntexError> {
664 if samples.is_empty() {
665 return Err(AntexError::EmptyPcvGrid {
666 antenna_id: antenna_id.to_string(),
667 frequency: frequency.to_string(),
668 });
669 }
670
671 let mut sorted = samples.to_vec();
672 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
673
674 Ok(antenna::interpolate_zenith_sorted(&sorted, zenith_deg)
675 .expect("non-empty grid yields a value"))
676}
677
678fn tag(line: &str) -> &str {
679 raw_field(line, 60, 80).trim()
680}
681
682fn parse_tokens(line: &str) -> Vec<&str> {
683 line.split_whitespace().collect()
684}
685
686fn parse_floats_from_prefix(line: &str) -> Vec<f64> {
687 let mut values = Vec::new();
688 for token in parse_tokens(line) {
689 let Some(value) = parse_float(token) else {
690 break;
691 };
692 values.push(value);
693 }
694 values
695}
696
697fn parse_float(token: &str) -> Option<f64> {
698 fortran_f64(token, 0, token.len())
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 fn test_antenna() -> Antenna {
706 Antenna {
707 id: "TESTANT TESTSER".to_string(),
708 kind: AntennaKind::Receiver,
709 antenna_type: "TESTANT".to_string(),
710 serial: "TESTSER".to_string(),
711 dazi_deg: 0.0,
712 zenith_start_deg: 0.0,
713 zenith_end_deg: 10.0,
714 zenith_step_deg: 10.0,
715 sinex_code: None,
716 valid_from: None,
717 valid_until: None,
718 frequencies: BTreeMap::from([(
719 "G01".to_string(),
720 Frequency {
721 frequency: "G01".to_string(),
722 pco_m: [0.0, 0.0, 0.0],
723 pcv_samples: vec![
724 PcvSample {
725 grid: PcvGrid::NoAzimuth,
726 azimuth_deg: None,
727 zenith_deg: 0.0,
728 value_m: 1.0,
729 },
730 PcvSample {
731 grid: PcvGrid::NoAzimuth,
732 azimuth_deg: None,
733 zenith_deg: 10.0,
734 value_m: 3.0,
735 },
736 ],
737 },
738 )]),
739 }
740 }
741
742 #[test]
743 fn pcv_rejects_nonfinite_zenith() {
744 let antenna = test_antenna();
745 for zenith_deg in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
746 assert_eq!(
747 antenna.pcv("G01", zenith_deg, None),
748 Err(AntexError::InvalidInput {
749 field: "zenith_deg",
750 reason: "not finite"
751 })
752 );
753 }
754 }
755
756 #[test]
757 fn pcv_rejects_out_of_range_zenith() {
758 let antenna = test_antenna();
759 assert_eq!(
760 antenna.pcv("G01", 11.0, None),
761 Err(AntexError::InvalidInput {
762 field: "zenith_deg",
763 reason: "out of range"
764 })
765 );
766 }
767
768 #[test]
769 fn pcv_accepts_valid_zenith_unchanged() {
770 let antenna = test_antenna();
771 let got = antenna.pcv("G01", 5.0, None).expect("valid PCV");
772 assert_eq!(got.to_bits(), 2.0_f64.to_bits());
773 }
774}