use std::num::ParseFloatError;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Once, RwLock};
use crate::utils::datadir;
use crate::utils::{download_file, download_if_not_exist};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("Invalid entry in EOP file")]
InvalidEntry,
#[error(
"Data directory is read-only. Try setting the environment variable SATKIT_DATA \
to a writeable directory and re-starting or explicitly set data directory"
)]
DataDirReadOnly,
#[error("EOP byte buffer is not valid UTF-8: {0}")]
Utf8(#[from] std::str::Utf8Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
ParseFloat(#[from] ParseFloatError),
#[error(transparent)]
Datadir(#[from] crate::utils::datadir::Error),
#[error(transparent)]
Download(#[from] crate::utils::download::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
#[allow(non_snake_case)]
struct EOPEntry {
mjd_utc: f64,
xp: f64,
yp: f64,
dut1: f64,
lod: f64,
dX: f64,
dY: f64,
}
fn parse_csv(text: &str) -> Result<Vec<EOPEntry>> {
text.lines()
.skip(1)
.map(|line| -> Result<EOPEntry> {
let lvals: Vec<&str> = line.split(",").collect();
if lvals.len() < 12 {
return Err(Error::InvalidEntry);
}
Ok(EOPEntry {
mjd_utc: lvals[1].parse()?,
xp: lvals[2].parse()?,
yp: lvals[3].parse()?,
dut1: lvals[4].parse()?,
lod: lvals[5].parse()?,
dX: lvals[8].parse()?,
dY: lvals[9].parse()?,
})
})
.collect()
}
fn load_eop_file_csv() -> Result<Vec<EOPEntry>> {
let path = datadir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("EOP-All.csv");
download_if_not_exist(&path, Some("http://celestrak.org/SpaceData/"))?;
parse_csv(&std::fs::read_to_string(&path)?)
}
static WARNING_SHOWN: AtomicBool = AtomicBool::new(false);
static EOP: RwLock<Option<Vec<EOPEntry>>> = RwLock::new(None);
static DEFAULT_LOAD_ONCE: Once = Once::new();
fn ensure_default_loaded() {
DEFAULT_LOAD_ONCE.call_once(|| {
if let Ok(records) = load_eop_file_csv() {
*EOP.write().unwrap() = Some(records);
}
});
}
pub fn init_from_bytes(bytes: &[u8]) -> Result<()> {
let records = parse_csv(std::str::from_utf8(bytes)?)?;
DEFAULT_LOAD_ONCE.call_once(|| {});
*EOP.write().unwrap() = Some(records);
Ok(())
}
pub fn init_from_path(path: &std::path::Path) -> Result<()> {
let records = parse_csv(&std::fs::read_to_string(path)?)?;
DEFAULT_LOAD_ONCE.call_once(|| {});
*EOP.write().unwrap() = Some(records);
Ok(())
}
pub fn disable_eop_time_warning() {
WARNING_SHOWN.store(true, Ordering::Relaxed);
}
pub fn update() -> Result<()> {
let d = datadir()?;
if d.metadata()?.permissions().readonly() {
return Err(Error::DataDirReadOnly);
}
let url = "http://celestrak.org/SpaceData/EOP-All.csv";
download_file(url, &d, true)?;
let records = load_eop_file_csv()?;
DEFAULT_LOAD_ONCE.call_once(|| {});
*EOP.write().unwrap() = Some(records);
Ok(())
}
pub fn eop_from_mjd_utc(mjd_utc: f64) -> Option<[f64; 6]> {
ensure_default_loaded();
let guard = EOP.read().unwrap();
let eop = guard.as_ref()?;
let idx = eop.partition_point(|x| x.mjd_utc <= mjd_utc);
if idx == 0 {
if !WARNING_SHOWN.swap(true, Ordering::Relaxed) {
eprintln!(
"Warning: EOP data not available for MJD UTC = {mjd_utc} (too early).\n\
Run `satkit::utils::update_datafiles()` to download the most recent data.\n\
To disable: `satkit::earth_orientation_params::disable_eop_time_warning()`"
);
}
return None;
}
if idx >= eop.len() {
let last = &eop[eop.len() - 1];
return Some([last.dut1, last.xp, last.yp, last.lod, last.dX, last.dY]);
}
let v0 = &eop[idx - 1];
let v1 = &eop[idx];
let g1 = (mjd_utc - v0.mjd_utc) / (v1.mjd_utc - v0.mjd_utc);
let g0 = 1.0 - g1;
Some([
g0.mul_add(v0.dut1, g1 * v1.dut1),
g0.mul_add(v0.xp, g1 * v1.xp),
g0.mul_add(v0.yp, g1 * v1.yp),
g0.mul_add(v0.lod, g1 * v1.lod),
g0.mul_add(v0.dX, g1 * v1.dX),
g0.mul_add(v0.dY, g1 * v1.dY),
])
}
#[inline]
pub fn get<T: crate::TimeLike>(tm: &T) -> Option<[f64; 6]> {
eop_from_mjd_utc(tm.as_mjd_with_scale(crate::TimeScale::UTC))
}
#[inline]
pub fn get_or_zero<T: crate::TimeLike>(tm: &T) -> [f64; 6] {
get(tm).unwrap_or([0.0; 6])
}
#[inline]
pub fn eop_from_mjd_utc_or_zero(mjd_utc: f64) -> [f64; 6] {
eop_from_mjd_utc(mjd_utc).unwrap_or([0.0; 6])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loaded() {
ensure_default_loaded();
let guard = EOP.read().unwrap();
let eop = guard
.as_ref()
.expect("default EOP load should succeed in tests");
assert!(eop[0].mjd_utc >= 0.0);
}
#[test]
fn test_time_bound() {
let tm = crate::Instant::from_rfc3339("2056-04-16T17:52:50.805408Z").unwrap();
let eop = eop_from_mjd_utc(tm.as_mjd_with_scale(crate::TimeScale::UTC));
assert!(eop.is_some());
let tm = crate::Instant::from_rfc3339("1950-04-16T17:52:50.805408Z").unwrap();
let eop = eop_from_mjd_utc(tm.as_mjd_with_scale(crate::TimeScale::UTC));
assert!(eop.is_none());
}
#[test]
fn checkval() {
let tm = crate::Instant::from_rfc3339("2006-04-16T17:52:50.805408Z").unwrap();
let v: Option<[f64; 6]> = eop_from_mjd_utc(tm.as_mjd_utc());
assert!(v.is_some());
let v = eop_from_mjd_utc(59464.00).unwrap();
const TRUTH: [f64; 4] = [-0.1145667, 0.241155, 0.317274, -0.0002255];
for it in v.iter().zip(TRUTH.iter()) {
let (a, b) = it;
assert!(((a - b) / b).abs() < 1.0e-3);
}
}
#[test]
fn checkinterp() {
let mjd0: f64 = 57909.00;
const TRUTH0: [f64; 4] = [0.3754421, 0.102693, 0.458455, 0.0011699];
const TRUTH1: [f64; 4] = [0.3743358, 0.104031, 0.458373, 0.0010383];
for x in 0..101 {
let dt: f64 = x as f64 / 100.0;
let vt = eop_from_mjd_utc(mjd0 + dt).unwrap();
let g0: f64 = 1.0 - dt;
let g1: f64 = dt;
for it in vt.iter().zip(TRUTH0.iter().zip(TRUTH1.iter())) {
let (v, (v0, v1)) = it;
let vtest: f64 = g0 * v0 + g1 * v1;
assert!(((v - vtest) / v).abs() < 1.0e-5);
}
}
}
}