1use crate::antenna;
8use crate::constants::MM_PER_M;
9use crate::format::columns::{fortran_f64, raw_field};
10use crate::format::{Diagnostics, RecordRef, Skip, SkipReason};
11use crate::validate::{self, FieldError};
12use std::collections::BTreeMap;
13use std::fmt;
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct Antex {
18 pub antennas: BTreeMap<String, Antenna>,
19 antenna_intervals: BTreeMap<String, Vec<Antenna>>,
20 skipped_records: usize,
28}
29
30#[derive(Debug, Clone, PartialEq)]
32pub struct Antenna {
33 pub id: String,
34 pub kind: AntennaKind,
35 pub antenna_type: String,
36 pub serial: String,
37 pub dazi_deg: f64,
38 pub zenith_start_deg: f64,
39 pub zenith_end_deg: f64,
40 pub zenith_step_deg: f64,
41 pub sinex_code: Option<String>,
42 pub valid_from: Option<AntexDateTime>,
43 pub valid_until: Option<AntexDateTime>,
44 pub frequencies: BTreeMap<String, Frequency>,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum AntennaKind {
50 Receiver,
51 Satellite,
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct Frequency {
57 pub frequency: String,
58 pub pco_m: [f64; 3],
59 pub pcv_samples: Vec<PcvSample>,
60}
61
62#[derive(Debug, Clone, PartialEq)]
64pub struct PcvSample {
65 pub grid: PcvGrid,
66 pub azimuth_deg: Option<f64>,
67 pub zenith_deg: f64,
68 pub value_m: f64,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum PcvGrid {
74 NoAzimuth,
75 Azimuth,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
80pub struct AntexDateTime {
81 pub year: i32,
82 pub month: u8,
83 pub day: u8,
84 pub hour: u8,
85 pub minute: u8,
86 pub second: u8,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum AntexError {
92 InvalidDateTime,
93 InvalidInput {
94 field: &'static str,
95 reason: &'static str,
96 },
97 UnknownFrequency {
98 antenna_id: String,
99 frequency: String,
100 },
101 MissingPco {
102 antenna_id: String,
103 frequency: String,
104 },
105 EmptyPcvGrid {
106 antenna_id: String,
107 frequency: String,
108 },
109}
110
111impl fmt::Display for AntexError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 Self::InvalidDateTime => write!(f, "invalid ANTEX datetime"),
115 Self::InvalidInput { field, reason } => {
116 write!(f, "invalid ANTEX input {field}: {reason}")
117 }
118 Self::UnknownFrequency {
119 antenna_id,
120 frequency,
121 } => write!(f, "unknown frequency {frequency:?} for {antenna_id:?}"),
122 Self::MissingPco {
123 antenna_id,
124 frequency,
125 } => write!(
126 f,
127 "missing or malformed PCO for frequency {frequency:?} on {antenna_id:?}"
128 ),
129 Self::EmptyPcvGrid {
130 antenna_id,
131 frequency,
132 } => write!(
133 f,
134 "empty PCV grid for frequency {frequency:?} on {antenna_id:?}"
135 ),
136 }
137 }
138}
139
140impl std::error::Error for AntexError {}
141
142#[derive(Debug, Clone)]
143struct ParseState {
144 antennas: BTreeMap<String, Antenna>,
145 antenna_intervals: BTreeMap<String, Vec<Antenna>>,
146 current_antenna: Option<Antenna>,
147 current_frequency: Option<FrequencyState>,
148 line: usize,
150 diagnostics: Diagnostics,
153}
154
155#[derive(Debug, Clone)]
156struct FrequencyState {
157 frequency: String,
158 phase: FrequencyPhase,
159 pco_m: Option<[f64; 3]>,
160 samples: Vec<PcvSample>,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164enum FrequencyPhase {
165 Pco,
166 Pcv,
167}
168
169impl Antex {
170 pub fn parse(text: &str) -> Result<Self, AntexError> {
172 let mut state = ParseState {
173 antennas: BTreeMap::new(),
174 antenna_intervals: BTreeMap::new(),
175 current_antenna: None,
176 current_frequency: None,
177 line: 0,
178 diagnostics: Diagnostics::new(),
179 };
180
181 for (index, line) in text.lines().enumerate() {
182 state.line = index + 1;
183 step(line, &mut state)?;
184 }
185 finalize_antenna(&mut state)?;
186
187 let skipped_records = state.diagnostics.skips.len();
188 Ok(Self {
189 antennas: state.antennas,
190 antenna_intervals: state.antenna_intervals,
191 skipped_records,
192 })
193 }
194
195 pub fn skipped_records(&self) -> usize {
197 self.skipped_records
198 }
199
200 pub fn antenna(&self, id: &str) -> Option<&Antenna> {
202 self.antennas.get(id.trim())
203 }
204
205 pub fn antenna_intervals(&self, id: &str) -> impl Iterator<Item = &Antenna> {
207 self.antenna_intervals.get(id.trim()).into_iter().flatten()
208 }
209
210 pub fn antenna_at(&self, id: &str, epoch: AntexDateTime) -> Option<&Antenna> {
212 self.antenna_intervals(id)
213 .find(|antenna| antenna.valid_at(epoch))
214 }
215
216 pub fn satellite_antenna(&self, prn: &str, epoch: AntexDateTime) -> Option<&Antenna> {
218 let prn = prn.trim();
219 self.antenna_intervals.values().flatten().find(|antenna| {
220 antenna.kind == AntennaKind::Satellite
221 && antenna.serial.trim() == prn
222 && antenna.valid_at(epoch)
223 })
224 }
225}
226
227impl Antenna {
228 pub fn valid_at(&self, epoch: AntexDateTime) -> bool {
230 self.valid_from.is_none_or(|from| epoch >= from)
231 && self.valid_until.is_none_or(|until| epoch <= until)
232 }
233
234 pub fn pco(&self, frequency: &str) -> Result<[f64; 3], AntexError> {
236 self.frequencies
237 .get(frequency.trim())
238 .map(|f| f.pco_m)
239 .ok_or_else(|| AntexError::UnknownFrequency {
240 antenna_id: self.id.clone(),
241 frequency: frequency.to_string(),
242 })
243 }
244
245 pub fn pcv(
247 &self,
248 frequency: &str,
249 zenith_deg: f64,
250 azimuth_deg: Option<f64>,
251 ) -> Result<f64, AntexError> {
252 validate_pcv_zenith(zenith_deg, self.zenith_start_deg, self.zenith_end_deg)?;
253
254 let frequency =
255 self.frequencies
256 .get(frequency.trim())
257 .ok_or_else(|| AntexError::UnknownFrequency {
258 antenna_id: self.id.clone(),
259 frequency: frequency.to_string(),
260 })?;
261
262 frequency.pcv(self.id.as_str(), zenith_deg, azimuth_deg)
263 }
264}
265
266impl Frequency {
267 fn pcv(
268 &self,
269 antenna_id: &str,
270 zenith_deg: f64,
271 azimuth_deg: Option<f64>,
272 ) -> Result<f64, AntexError> {
273 let noazi: Vec<(f64, f64)> = self
274 .pcv_samples
275 .iter()
276 .filter(|sample| sample.grid == PcvGrid::NoAzimuth)
277 .map(|sample| (sample.zenith_deg, sample.value_m))
278 .collect();
279
280 let has_azimuth = self
281 .pcv_samples
282 .iter()
283 .any(|sample| sample.grid == PcvGrid::Azimuth);
284
285 if azimuth_deg.is_none() || !has_azimuth {
286 return interpolate(antenna_id, &self.frequency, &noazi, zenith_deg);
287 }
288
289 let mut azimuth_samples: BTreeMap<OrderedF64, Vec<(f64, f64)>> = BTreeMap::new();
290 for sample in self
291 .pcv_samples
292 .iter()
293 .filter(|sample| sample.grid == PcvGrid::Azimuth)
294 {
295 if let Some(azimuth) = sample.azimuth_deg {
296 azimuth_samples
297 .entry(OrderedF64(azimuth))
298 .or_default()
299 .push((sample.zenith_deg, sample.value_m));
300 }
301 }
302
303 if azimuth_samples.is_empty() {
304 interpolate(antenna_id, &self.frequency, &noazi, zenith_deg)
305 } else {
306 interpolate_azimuth(
307 antenna_id,
308 &self.frequency,
309 &azimuth_samples,
310 azimuth_deg.expect("checked Some"),
311 zenith_deg,
312 )
313 }
314 }
315}
316
317fn validate_pcv_zenith(
318 zenith_deg: f64,
319 zenith_start_deg: f64,
320 zenith_end_deg: f64,
321) -> Result<(), AntexError> {
322 validate::finite(zenith_deg, "zenith_deg").map_err(map_antex_field_error)?;
323 if zenith_deg < zenith_start_deg || zenith_deg > zenith_end_deg {
324 return Err(invalid_input("zenith_deg", "out of range"));
325 }
326 Ok(())
327}
328
329fn map_antex_field_error(error: validate::FieldError) -> AntexError {
330 invalid_input(error.field(), error.reason())
331}
332
333fn invalid_input(field: &'static str, reason: &'static str) -> AntexError {
334 AntexError::InvalidInput { field, reason }
335}
336
337impl AntexDateTime {
338 pub fn new(
339 year: i32,
340 month: u8,
341 day: u8,
342 hour: u8,
343 minute: u8,
344 second: u8,
345 ) -> Result<Self, AntexError> {
346 let civil = validate::civil_datetime_with_second_policy(
347 i64::from(year),
348 i64::from(month),
349 i64::from(day),
350 i64::from(hour),
351 i64::from(minute),
352 f64::from(second),
353 validate::CivilSecondPolicy::UtcLike,
354 )
355 .map_err(|_| AntexError::InvalidDateTime)?;
356 Ok(Self::from_valid_civil(civil))
357 }
358
359 fn from_valid_civil(civil: validate::ValidCivil) -> Self {
360 Self {
361 year: civil.year as i32,
362 month: civil.month as u8,
363 day: civil.day as u8,
364 hour: civil.hour as u8,
365 minute: civil.minute as u8,
366 second: civil.second.trunc() as u8,
367 }
368 }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq)]
372struct OrderedF64(f64);
373
374impl Eq for OrderedF64 {}
375
376impl PartialOrd for OrderedF64 {
377 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
378 Some(self.cmp(other))
379 }
380}
381
382impl Ord for OrderedF64 {
383 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
384 self.0.total_cmp(&other.0)
385 }
386}
387
388const LABEL_COLUMN: usize = 60;
402
403impl Antex {
404 pub fn encode(&self) -> String {
417 let mut out = String::new();
418 out.push_str(&labeled(" 1.4 M", "ANTEX VERSION / SYST"));
419 out.push_str(&labeled("", "END OF HEADER"));
420 for blocks in self.antenna_intervals.values() {
421 for antenna in blocks {
422 encode_antenna(antenna, &mut out);
423 }
424 }
425 out
426 }
427}
428
429fn encode_antenna(antenna: &Antenna, out: &mut String) {
430 out.push_str(&labeled("", "START OF ANTENNA"));
431 out.push_str(&labeled(&antenna.id, "TYPE / SERIAL NO"));
432 out.push_str(&labeled(&fmt_num(antenna.dazi_deg), "DAZI"));
433 out.push_str(&labeled(
434 &format!(
435 "{} {} {}",
436 fmt_num(antenna.zenith_start_deg),
437 fmt_num(antenna.zenith_end_deg),
438 fmt_num(antenna.zenith_step_deg),
439 ),
440 "ZEN1 / ZEN2 / DZEN",
441 ));
442 if let Some(code) = &antenna.sinex_code {
443 out.push_str(&labeled(code, "SINEX CODE"));
444 }
445 if let Some(from) = antenna.valid_from {
446 out.push_str(&labeled(&fmt_datetime(from), "VALID FROM"));
447 }
448 if let Some(until) = antenna.valid_until {
449 out.push_str(&labeled(&fmt_datetime(until), "VALID UNTIL"));
450 }
451 for frequency in antenna.frequencies.values() {
452 encode_frequency(frequency, out);
453 }
454 out.push_str(&labeled("", "END OF ANTENNA"));
455}
456
457fn encode_frequency(frequency: &Frequency, out: &mut String) {
458 out.push_str(&labeled(&frequency.frequency, "START OF FREQUENCY"));
459 out.push_str(&labeled(
460 &format!(
461 "{} {} {}",
462 fmt_num(frequency.pco_m[0] * MM_PER_M),
463 fmt_num(frequency.pco_m[1] * MM_PER_M),
464 fmt_num(frequency.pco_m[2] * MM_PER_M),
465 ),
466 "NORTH / EAST / UP",
467 ));
468
469 let noazi: Vec<&PcvSample> = frequency
473 .pcv_samples
474 .iter()
475 .filter(|sample| sample.grid == PcvGrid::NoAzimuth)
476 .collect();
477 if !noazi.is_empty() {
478 out.push_str(&pcv_row("NOAZI", &noazi));
479 }
480
481 let mut azimuth_rows: Vec<(f64, Vec<&PcvSample>)> = Vec::new();
485 for sample in &frequency.pcv_samples {
486 if sample.grid != PcvGrid::Azimuth {
487 continue;
488 }
489 let Some(azimuth) = sample.azimuth_deg else {
490 continue;
491 };
492 match azimuth_rows
493 .iter_mut()
494 .find(|(az, _)| az.to_bits() == azimuth.to_bits())
495 {
496 Some((_, row)) => row.push(sample),
497 None => azimuth_rows.push((azimuth, vec![sample])),
498 }
499 }
500 for (azimuth, row) in &azimuth_rows {
501 out.push_str(&pcv_row(&fmt_num(*azimuth), row));
502 }
503
504 out.push_str(&labeled("", "END OF FREQUENCY"));
505}
506
507fn pcv_row(head: &str, samples: &[&PcvSample]) -> String {
511 let mut line = String::from(head);
512 for sample in samples {
513 line.push(' ');
514 line.push_str(&fmt_num(sample.value_m * MM_PER_M));
515 }
516 line.push('\n');
517 line
518}
519
520fn labeled(body: &str, label: &str) -> String {
523 format!("{body:<LABEL_COLUMN$}{label}\n")
524}
525
526fn fmt_datetime(dt: AntexDateTime) -> String {
529 format!(
530 "{} {} {} {} {} {}",
531 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
532 )
533}
534
535fn fmt_num(value: f64) -> String {
537 format!("{value}")
538}
539
540fn step(line: &str, state: &mut ParseState) -> Result<(), AntexError> {
541 match tag(line) {
542 "START OF ANTENNA" => {
543 finalize_antenna(state)?;
544 state.current_antenna = None;
545 state.current_frequency = None;
546 }
547 "END OF ANTENNA" => finalize_antenna(state)?,
548 "TYPE / SERIAL NO" => parse_type_serial(line, state),
549 "DAZI" => parse_dazi(line, state),
550 "ZEN1 / ZEN2 / DZEN" => parse_zenith_grid(line, state),
551 "SINEX CODE" => parse_sinex_code(line, state),
552 "VALID FROM" => parse_valid(line, state, ValidField::From)?,
553 "VALID UNTIL" => parse_valid(line, state, ValidField::Until)?,
554 "START OF FREQUENCY" => begin_frequency(line, state),
555 "END OF FREQUENCY" => finalize_frequency(state)?,
556 "NORTH / EAST / UP" => parse_pco(line, state),
557 _ => parse_pcv_row(line, state),
558 }
559 Ok(())
560}
561
562fn parse_type_serial(line: &str, state: &mut ParseState) {
563 state.current_antenna = Some(decode_antenna_header(line));
564 state.current_frequency = None;
565}
566
567fn parse_dazi(line: &str, state: &mut ParseState) {
568 let Some(current) = state.current_antenna.as_mut() else {
569 return;
570 };
571 if let Some(dazi) = parse_floats_from_prefix(line).first() {
572 current.dazi_deg = *dazi;
573 }
574}
575
576fn parse_zenith_grid(line: &str, state: &mut ParseState) {
577 let Some(current) = state.current_antenna.as_mut() else {
578 return;
579 };
580 let values = parse_floats_from_prefix(line);
581 if values.len() >= 3 {
582 current.zenith_start_deg = values[0];
583 current.zenith_end_deg = values[1];
584 current.zenith_step_deg = values[2];
585 }
586}
587
588fn parse_sinex_code(line: &str, state: &mut ParseState) {
589 let Some(current) = state.current_antenna.as_mut() else {
590 return;
591 };
592 let code = raw_field(line, 0, 60).trim();
593 if !code.is_empty() {
594 current.sinex_code = Some(code.to_string());
595 }
596}
597
598#[derive(Debug, Clone, Copy)]
599enum ValidField {
600 From,
601 Until,
602}
603
604fn parse_valid(line: &str, state: &mut ParseState, field: ValidField) -> Result<(), AntexError> {
605 let Some(current) = state.current_antenna.as_mut() else {
606 return Ok(());
607 };
608 let values = parse_floats_from_prefix(line);
609 if values.len() >= 6 {
610 let year = datetime_i32(values[0])?;
611 let month = datetime_u8(values[1])?;
612 let day = datetime_u8(values[2])?;
613 let hour = datetime_u8(values[3])?;
614 let minute = datetime_u8(values[4])?;
615 let civil = validate::civil_datetime_with_second_policy(
616 i64::from(year),
617 i64::from(month),
618 i64::from(day),
619 i64::from(hour),
620 i64::from(minute),
621 values[5],
622 validate::CivilSecondPolicy::UtcLike,
623 )
624 .map_err(|_| AntexError::InvalidDateTime)?;
625 let dt = AntexDateTime::from_valid_civil(civil);
626 match field {
627 ValidField::From => current.valid_from = Some(dt),
628 ValidField::Until => current.valid_until = Some(dt),
629 }
630 }
631 Ok(())
632}
633
634fn datetime_i32(value: f64) -> Result<i32, AntexError> {
635 if !value.is_finite()
636 || value.fract() != 0.0
637 || value < i32::MIN as f64
638 || value > i32::MAX as f64
639 {
640 return Err(AntexError::InvalidDateTime);
641 }
642 Ok(value as i32)
643}
644
645fn datetime_u8(value: f64) -> Result<u8, AntexError> {
646 if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value > u8::MAX as f64 {
647 return Err(AntexError::InvalidDateTime);
648 }
649 Ok(value as u8)
650}
651
652fn decode_antenna_header(line: &str) -> Antenna {
653 let id = raw_field(line, 0, 60).trim().to_string();
654 let antenna_type = raw_field(line, 0, 20).trim().to_string();
655 let serial = raw_field(line, 20, 40).trim().to_string();
656 let kind = if is_satellite_serial(&serial) {
657 AntennaKind::Satellite
658 } else {
659 AntennaKind::Receiver
660 };
661
662 Antenna {
663 id,
664 kind,
665 antenna_type,
666 serial,
667 dazi_deg: 0.0,
668 zenith_start_deg: 0.0,
669 zenith_end_deg: 0.0,
670 zenith_step_deg: 0.0,
671 sinex_code: None,
672 valid_from: None,
673 valid_until: None,
674 frequencies: BTreeMap::new(),
675 }
676}
677
678fn is_satellite_serial(serial: &str) -> bool {
679 let bytes = serial.as_bytes();
680 bytes.len() == 3
681 && bytes[0].is_ascii_uppercase()
682 && bytes[1].is_ascii_digit()
683 && bytes[2].is_ascii_digit()
684}
685
686fn begin_frequency(line: &str, state: &mut ParseState) {
687 if state.current_antenna.is_none() {
688 return;
689 }
690 state.current_frequency = Some(FrequencyState {
691 frequency: raw_field(line, 0, 20).trim().to_string(),
692 phase: FrequencyPhase::Pco,
693 pco_m: None,
694 samples: Vec::new(),
695 });
696}
697
698fn parse_pco(line: &str, state: &mut ParseState) {
699 let Some(current_frequency) = state.current_frequency.as_mut() else {
700 return;
701 };
702 if current_frequency.phase != FrequencyPhase::Pco {
703 return;
704 }
705
706 let values = parse_floats_from_prefix(line);
707 if values.len() >= 3 && values[..3].iter().all(|value| value.is_finite()) {
708 current_frequency.pco_m = Some([
709 values[0] / MM_PER_M,
710 values[1] / MM_PER_M,
711 values[2] / MM_PER_M,
712 ]);
713 current_frequency.phase = FrequencyPhase::Pcv;
714 }
715}
716
717fn parse_pcv_row(line: &str, state: &mut ParseState) {
718 if state
719 .current_frequency
720 .as_ref()
721 .is_none_or(|frequency| frequency.phase != FrequencyPhase::Pcv)
722 {
723 return;
724 }
725
726 let tokens = parse_tokens(line);
727 let Some((first, values)) = tokens.split_first() else {
728 return;
729 };
730
731 if *first == "NOAZI" {
732 add_pcv_values(None, values, state);
733 } else if let Some(azimuth) = parse_float(first) {
734 add_pcv_values(Some(azimuth), values, state);
735 } else {
736 state.diagnostics.push_skip(Skip {
741 at: RecordRef::at_line(state.line),
742 reason: SkipReason::MalformedField(FieldError::FloatParse {
743 field: "antex pcv row head",
744 value: (*first).to_string(),
745 }),
746 });
747 }
748}
749
750fn add_pcv_values(azimuth_deg: Option<f64>, values: &[&str], state: &mut ParseState) {
751 let Some(current_antenna) = state.current_antenna.as_ref() else {
752 return;
753 };
754 let Some(current_frequency) = state.current_frequency.as_mut() else {
755 return;
756 };
757
758 let grid_start = current_antenna.zenith_start_deg;
759 let grid_step = current_antenna.zenith_step_deg;
760 let line = state.line;
761 for (index, value_text) in values.iter().enumerate() {
762 let Some(value) = parse_float(value_text) else {
763 state.diagnostics.push_skip(Skip {
767 at: RecordRef::at_line(line),
768 reason: SkipReason::MalformedField(FieldError::FloatParse {
769 field: "antex pcv value",
770 value: (*value_text).to_string(),
771 }),
772 });
773 continue;
774 };
775 let zenith_deg = if grid_step == 0.0 {
776 grid_start
777 } else {
778 grid_start + grid_step * index as f64
779 };
780 current_frequency.samples.push(PcvSample {
781 grid: if azimuth_deg.is_some() {
782 PcvGrid::Azimuth
783 } else {
784 PcvGrid::NoAzimuth
785 },
786 azimuth_deg,
787 zenith_deg,
788 value_m: value / MM_PER_M,
789 });
790 }
791}
792
793fn finalize_frequency(state: &mut ParseState) -> Result<(), AntexError> {
794 let Some(current_frequency) = state.current_frequency.take() else {
795 return Ok(());
796 };
797 let Some(current_antenna) = state.current_antenna.as_mut() else {
798 return Ok(());
799 };
800
801 let pco_m = current_frequency
802 .pco_m
803 .ok_or_else(|| AntexError::MissingPco {
804 antenna_id: current_antenna.id.clone(),
805 frequency: current_frequency.frequency.clone(),
806 })?;
807
808 let frequency = Frequency {
809 frequency: current_frequency.frequency,
810 pco_m,
811 pcv_samples: current_frequency.samples,
812 };
813 current_antenna
814 .frequencies
815 .insert(frequency.frequency.clone(), frequency);
816 Ok(())
817}
818
819fn finalize_antenna(state: &mut ParseState) -> Result<(), AntexError> {
820 finalize_frequency(state)?;
821 let Some(current_antenna) = state.current_antenna.take() else {
822 return Ok(());
823 };
824 state
825 .antenna_intervals
826 .entry(current_antenna.id.clone())
827 .or_default()
828 .push(current_antenna.clone());
829 state
830 .antennas
831 .insert(current_antenna.id.clone(), current_antenna);
832 Ok(())
833}
834
835fn interpolate_azimuth(
836 antenna_id: &str,
837 frequency: &str,
838 azimuth_samples: &BTreeMap<OrderedF64, Vec<(f64, f64)>>,
839 azimuth_deg: f64,
840 zenith_deg: f64,
841) -> Result<f64, AntexError> {
842 let azimuth = antenna::normalize_azimuth(azimuth_deg);
843 let azimuths: Vec<f64> = azimuth_samples.keys().map(|az| az.0).collect();
844 let (low_deg, high_deg) = antenna::azimuth_bracket(&azimuths, azimuth);
845
846 let low_samples = &azimuth_samples[&OrderedF64(low_deg)];
847 let high_samples = &azimuth_samples[&OrderedF64(high_deg)];
848
849 let low_value = interpolate(antenna_id, frequency, low_samples, zenith_deg)?;
850 let high_value = interpolate(antenna_id, frequency, high_samples, zenith_deg)?;
851
852 Ok(antenna::blend_azimuth(
853 low_deg, high_deg, azimuth, low_value, high_value,
854 ))
855}
856
857fn interpolate(
858 antenna_id: &str,
859 frequency: &str,
860 samples: &[(f64, f64)],
861 zenith_deg: f64,
862) -> Result<f64, AntexError> {
863 if samples.is_empty() {
864 return Err(AntexError::EmptyPcvGrid {
865 antenna_id: antenna_id.to_string(),
866 frequency: frequency.to_string(),
867 });
868 }
869
870 let mut sorted = samples.to_vec();
871 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
872
873 Ok(antenna::interpolate_zenith_sorted(&sorted, zenith_deg)
874 .expect("non-empty grid yields a value"))
875}
876
877fn tag(line: &str) -> &str {
878 raw_field(line, 60, 80).trim()
879}
880
881fn parse_tokens(line: &str) -> Vec<&str> {
882 line.split_whitespace().collect()
883}
884
885fn parse_floats_from_prefix(line: &str) -> Vec<f64> {
886 let mut values = Vec::new();
887 for token in parse_tokens(line) {
888 let Some(value) = parse_float(token) else {
889 break;
890 };
891 values.push(value);
892 }
893 values
894}
895
896fn parse_float(token: &str) -> Option<f64> {
897 fortran_f64(token, 0, token.len(), "antex numeric field")
898}
899
900#[cfg(test)]
901mod tests {
902 use super::*;
903
904 fn test_antenna() -> Antenna {
905 Antenna {
906 id: "TESTANT TESTSER".to_string(),
907 kind: AntennaKind::Receiver,
908 antenna_type: "TESTANT".to_string(),
909 serial: "TESTSER".to_string(),
910 dazi_deg: 0.0,
911 zenith_start_deg: 0.0,
912 zenith_end_deg: 10.0,
913 zenith_step_deg: 10.0,
914 sinex_code: None,
915 valid_from: None,
916 valid_until: None,
917 frequencies: BTreeMap::from([(
918 "G01".to_string(),
919 Frequency {
920 frequency: "G01".to_string(),
921 pco_m: [0.0, 0.0, 0.0],
922 pcv_samples: vec![
923 PcvSample {
924 grid: PcvGrid::NoAzimuth,
925 azimuth_deg: None,
926 zenith_deg: 0.0,
927 value_m: 1.0,
928 },
929 PcvSample {
930 grid: PcvGrid::NoAzimuth,
931 azimuth_deg: None,
932 zenith_deg: 10.0,
933 value_m: 3.0,
934 },
935 ],
936 },
937 )]),
938 }
939 }
940
941 #[test]
942 fn pcv_rejects_nonfinite_zenith() {
943 let antenna = test_antenna();
944 for zenith_deg in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
945 assert_eq!(
946 antenna.pcv("G01", zenith_deg, None),
947 Err(AntexError::InvalidInput {
948 field: "zenith_deg",
949 reason: "not finite"
950 })
951 );
952 }
953 }
954
955 #[test]
956 fn pcv_rejects_out_of_range_zenith() {
957 let antenna = test_antenna();
958 assert_eq!(
959 antenna.pcv("G01", 11.0, None),
960 Err(AntexError::InvalidInput {
961 field: "zenith_deg",
962 reason: "out of range"
963 })
964 );
965 }
966
967 #[test]
968 fn pcv_accepts_valid_zenith_unchanged() {
969 let antenna = test_antenna();
970 let got = antenna.pcv("G01", 5.0, None).expect("valid PCV");
971 assert_eq!(got.to_bits(), 2.0_f64.to_bits());
972 }
973
974 fn line(prefix: &str, tag: &str) -> String {
975 format!("{prefix:<60}{tag}")
976 }
977
978 fn synthetic_block() -> String {
979 [
980 line("", "START OF ANTENNA"),
981 line("TESTANT TESTSER", "TYPE / SERIAL NO"),
982 line("0", "DAZI"),
983 line("0.0 10.0 5.0", "ZEN1 / ZEN2 / DZEN"),
984 line("IGS_TEST", "SINEX CODE"),
985 line("2020 1 1 0 0 0", "VALID FROM"),
986 line("2021 12 31 23 59 59", "VALID UNTIL"),
987 line("G01", "START OF FREQUENCY"),
988 line("1.5 2.0 3.0", "NORTH / EAST / UP"),
989 "NOAZI 1.0 2.0 3.0".to_string(),
990 "0.0 1.0 2.0 3.0".to_string(),
991 "90.0 4.0 5.0 6.0".to_string(),
992 line("", "END OF FREQUENCY"),
993 line("", "END OF ANTENNA"),
994 ]
995 .join("\n")
996 }
997
998 #[test]
999 fn encode_round_trips_synthetic_block() {
1000 let antex = Antex::parse(&synthetic_block()).expect("parse synthetic block");
1001 assert_eq!(antex.skipped_records(), 0);
1002 let encoded = antex.encode();
1003 let reparsed = Antex::parse(&encoded).expect("re-parse encoded block");
1004 assert_eq!(antex, reparsed);
1005 assert_eq!(encoded, reparsed.encode());
1006 }
1007
1008 #[test]
1009 fn malformed_pcv_value_is_skipped_not_silent() {
1010 let text = [
1015 line("", "START OF ANTENNA"),
1016 line("TESTANT TESTSER", "TYPE / SERIAL NO"),
1017 line("0.0 10.0 5.0", "ZEN1 / ZEN2 / DZEN"),
1018 line("G01", "START OF FREQUENCY"),
1019 line("0.0 0.0 0.0", "NORTH / EAST / UP"),
1020 "NOAZI 1.0 BAD 3.0".to_string(),
1021 line("", "END OF FREQUENCY"),
1022 line("", "END OF ANTENNA"),
1023 ]
1024 .join("\n");
1025
1026 let antex = Antex::parse(&text).expect("forgiving parse");
1027 assert_eq!(antex.skipped_records(), 1);
1028 let antenna = antex
1029 .antenna("TESTANT TESTSER")
1030 .expect("antenna");
1031 let frequency = &antenna.frequencies["G01"];
1032 assert_eq!(frequency.pcv_samples.len(), 2);
1034 }
1035}