use core::fmt::Write as _;
use super::grid::Ionex;
use super::j2000_seconds_from_instant;
use crate::astro::time::civil::civil_from_j2000_seconds;
use crate::astro::time::model::Instant;
const VALUES_PER_LINE: usize = 16;
const LABEL_COLUMN: usize = 60;
impl Ionex {
pub fn to_ionex_string(&self) -> String {
let mut out = String::new();
self.write_header(&mut out);
let scale = 10f64.powi(self.exponent());
for (index, epoch) in self.map_epochs().iter().enumerate() {
let map_number = index + 1;
write_labeled(&mut out, &format!("{map_number:6}"), "START OF TEC MAP");
write_epoch(&mut out, *epoch);
self.write_map(&mut out, &self.tec_maps()[index], scale);
write_labeled(&mut out, &format!("{map_number:6}"), "END OF TEC MAP");
}
for (index, grid) in self.rms_maps().iter().enumerate() {
let map_number = index + 1;
write_labeled(&mut out, &format!("{map_number:6}"), "START OF RMS MAP");
if let Some(epoch) = self.map_epochs().get(index) {
write_epoch(&mut out, *epoch);
}
self.write_map(&mut out, grid, scale);
write_labeled(&mut out, &format!("{map_number:6}"), "END OF RMS MAP");
}
out
}
fn write_header(&self, out: &mut String) {
let lat1 = self.lat_nodes_deg().first().copied().unwrap_or(0.0);
let lat2 = self.lat_nodes_deg().last().copied().unwrap_or(0.0);
let lon1 = self.lon_nodes_deg().first().copied().unwrap_or(0.0);
let lon2 = self.lon_nodes_deg().last().copied().unwrap_or(0.0);
write_labeled(out, " 1.0 I", "IONEX VERSION / TYPE");
write_labeled(
out,
&format!("{lat1:8.1}{lat2:8.1}{:8.1}", self.dlat_deg()),
"LAT1 / LAT2 / DLAT",
);
write_labeled(
out,
&format!("{lon1:8.1}{lon2:8.1}{:8.1}", self.dlon_deg()),
"LON1 / LON2 / DLON",
);
let height = self.shell_height_km();
write_labeled(
out,
&format!("{height:8.1}{height:8.1}{:8.1}", 0.0),
"HGT1 / HGT2 / DHGT",
);
write_labeled(
out,
&format!("{:8.1}", self.base_radius_km()),
"BASE RADIUS",
);
write_labeled(out, &format!("{:6}", self.exponent()), "EXPONENT");
write_labeled(out, "", "END OF HEADER");
}
fn write_map(&self, out: &mut String, grid: &[Vec<f64>], scale: f64) {
let lon1 = self.lon_nodes_deg().first().copied().unwrap_or(0.0);
let lon2 = self.lon_nodes_deg().last().copied().unwrap_or(0.0);
let height = self.shell_height_km();
for (lat_index, band) in grid.iter().enumerate() {
let lat = self.lat_nodes_deg().get(lat_index).copied().unwrap_or(0.0);
write_labeled(
out,
&format!(
"{lat:8.1}{lon1:8.1}{lon2:8.1}{:8.1}{height:8.1}",
self.dlon_deg()
),
"LAT/LON1/LON2/DLON/H",
);
for chunk in band.chunks(VALUES_PER_LINE) {
for value in chunk {
let scaled = (value / scale).round() as i64;
let _ = write!(out, " {scaled:4}");
}
out.push('\n');
}
}
}
}
fn write_labeled(out: &mut String, data: &str, label: &str) {
let _ = writeln!(out, "{data:<LABEL_COLUMN$}{label}");
}
fn write_epoch(out: &mut String, epoch: Instant) {
let seconds =
j2000_seconds_from_instant(epoch).expect("IONEX map epoch is convertible to J2000 seconds");
let (year, month, day, hour, minute, second) = civil_from_j2000_seconds(seconds);
write_labeled(
out,
&format!("{year:6}{month:6}{day:6}{hour:6}{minute:6}{second:6}"),
"EPOCH OF CURRENT MAP",
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ionex::ionex_epoch_from_j2000_seconds;
#[test]
fn civil_from_j2000_seconds_inverts_the_parser_epoch() {
assert_eq!(
civil_from_j2000_seconds(646_315_200),
(2020, 6, 25, 0, 0, 0)
);
}
#[test]
fn civil_from_j2000_seconds_round_trips_through_the_instant_epoch() {
for seconds in [
646_315_200_i64,
646_315_200 + 7_323, 646_315_200 + 86_400, -43_200, -43_200 - 1, ] {
let epoch = ionex_epoch_from_j2000_seconds(seconds);
let recovered = j2000_seconds_from_instant(epoch).expect("J2000 seconds");
assert_eq!(recovered, seconds, "instant epoch round-trips its seconds");
let (year, month, day, hour, minute, second) = civil_from_j2000_seconds(seconds);
assert!((1..=12).contains(&month), "month in range: {month}");
assert!((1..=31).contains(&day), "day in range: {day}");
assert!((0..24).contains(&hour), "hour in range: {hour}");
assert!((0..60).contains(&minute), "minute in range: {minute}");
assert!((0..60).contains(&second), "second in range: {second}");
assert!(year >= 1999, "year plausible: {year}");
}
}
}