use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use crate::eop::download::{download_c04_eop_file, download_standard_eop_file};
use crate::eop::eop_provider::EarthOrientationProvider;
use crate::eop::eop_types::{EOPExtrapolation, EOPType};
use crate::eop::file_provider::FileEOPProvider;
use crate::time::{Epoch, TimeSystem};
use crate::utils::BraheError;
#[derive(Clone)]
pub struct CachingEOPProvider {
filepath: PathBuf,
eop_type: EOPType,
max_age_seconds: u64,
pub auto_refresh: bool,
interpolate: bool,
extrapolate: EOPExtrapolation,
provider: Arc<Mutex<FileEOPProvider>>,
file_loaded_at: Arc<Mutex<SystemTime>>,
}
impl CachingEOPProvider {
pub fn new(
filepath: Option<&Path>,
eop_type: EOPType,
max_age_seconds: u64,
auto_refresh: bool,
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
let filepath = if let Some(path) = filepath {
path.to_path_buf()
} else {
let cache_dir = crate::utils::cache::get_eop_cache_dir()?;
let filename = match eop_type {
EOPType::StandardBulletinA => "finals.all.iau2000.txt",
EOPType::C04 => "EOP_20_C04_one_file_1962-now.txt",
_ => {
return Err(BraheError::EOPError(format!(
"Unsupported EOP type for caching: {:?}. Only C04 and StandardBulletinA are supported.",
eop_type
)));
}
};
PathBuf::from(cache_dir).join(filename)
};
let needs_download = Self::check_file_age(&filepath, max_age_seconds)?;
if needs_download {
Self::download_file(&filepath, eop_type)?;
}
let provider = FileEOPProvider::from_file(&filepath, interpolate, extrapolate)?;
let file_loaded_at = Arc::new(Mutex::new(SystemTime::now()));
Ok(Self {
filepath,
eop_type,
max_age_seconds,
auto_refresh,
interpolate,
extrapolate,
provider: Arc::new(Mutex::new(provider)),
file_loaded_at,
})
}
fn check_file_age(filepath: &Path, max_age_seconds: u64) -> Result<bool, BraheError> {
if !filepath.exists() {
return Ok(true);
}
let metadata = fs::metadata(filepath).map_err(|e| {
BraheError::IoError(format!(
"Failed to get metadata for {}: {}",
filepath.display(),
e
))
})?;
let modified = metadata.modified().map_err(|e| {
BraheError::IoError(format!(
"Failed to get modification time for {}: {}",
filepath.display(),
e
))
})?;
let now = SystemTime::now();
let age = now
.duration_since(modified)
.map_err(|e| {
BraheError::IoError(format!(
"Failed to calculate file age for {}: {}",
filepath.display(),
e
))
})?
.as_secs();
Ok(age > max_age_seconds)
}
fn download_file(filepath: &Path, eop_type: EOPType) -> Result<(), BraheError> {
let filepath_str = filepath
.to_str()
.ok_or_else(|| BraheError::IoError("Invalid file path".to_string()))?;
match eop_type {
EOPType::C04 => download_c04_eop_file(filepath_str),
EOPType::StandardBulletinA => download_standard_eop_file(filepath_str),
_ => Err(BraheError::EOPError(format!(
"Unsupported EOP type for download: {:?}",
eop_type
))),
}
}
pub fn refresh(&self) -> Result<(), BraheError> {
let needs_download = Self::check_file_age(&self.filepath, self.max_age_seconds)?;
if needs_download {
Self::download_file(&self.filepath, self.eop_type)?;
let new_provider =
FileEOPProvider::from_file(&self.filepath, self.interpolate, self.extrapolate)?;
*self.provider.lock().unwrap() = new_provider;
*self.file_loaded_at.lock().unwrap() = SystemTime::now();
}
Ok(())
}
pub fn file_epoch(&self) -> Epoch {
let system_time = *self.file_loaded_at.lock().unwrap();
let duration_since_unix_epoch = system_time
.duration_since(SystemTime::UNIX_EPOCH)
.expect("System time is before UNIX epoch");
let seconds_since_unix = duration_since_unix_epoch.as_secs_f64();
const UNIX_EPOCH_MJD: f64 = 40587.0;
let mjd = UNIX_EPOCH_MJD + seconds_since_unix / 86400.0;
Epoch::from_mjd(mjd, TimeSystem::UTC)
}
pub fn file_age(&self) -> f64 {
let system_time = *self.file_loaded_at.lock().unwrap();
let now = SystemTime::now();
let duration = now
.duration_since(system_time)
.expect("System time went backwards");
duration.as_secs_f64()
}
fn check_auto_refresh(&self) -> Result<(), BraheError> {
if self.auto_refresh {
self.refresh()?;
}
Ok(())
}
}
impl EarthOrientationProvider for CachingEOPProvider {
fn is_initialized(&self) -> bool {
self.provider.lock().unwrap().is_initialized()
}
fn len(&self) -> usize {
self.provider.lock().unwrap().len()
}
fn eop_type(&self) -> EOPType {
self.provider.lock().unwrap().eop_type()
}
fn extrapolation(&self) -> EOPExtrapolation {
self.provider.lock().unwrap().extrapolation()
}
fn interpolation(&self) -> bool {
self.provider.lock().unwrap().interpolation()
}
fn mjd_min(&self) -> f64 {
self.provider.lock().unwrap().mjd_min()
}
fn mjd_max(&self) -> f64 {
self.provider.lock().unwrap().mjd_max()
}
fn mjd_last_lod(&self) -> f64 {
self.provider.lock().unwrap().mjd_last_lod()
}
fn mjd_last_dxdy(&self) -> f64 {
self.provider.lock().unwrap().mjd_last_dxdy()
}
fn get_ut1_utc(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.provider.lock().unwrap().get_ut1_utc(mjd)
}
fn get_pm(&self, mjd: f64) -> Result<(f64, f64), BraheError> {
self.check_auto_refresh()?;
self.provider.lock().unwrap().get_pm(mjd)
}
fn get_dxdy(&self, mjd: f64) -> Result<(f64, f64), BraheError> {
self.check_auto_refresh()?;
self.provider.lock().unwrap().get_dxdy(mjd)
}
fn get_lod(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.provider.lock().unwrap().get_lod(mjd)
}
fn get_eop(&self, mjd: f64) -> Result<(f64, f64, f64, f64, f64, f64), BraheError> {
self.check_auto_refresh()?;
self.provider.lock().unwrap().get_eop(mjd)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use std::env;
use std::fs::File;
use std::thread;
use std::time::Duration;
use tempfile::tempdir;
#[test]
fn test_check_file_age_nonexistent() {
let dir = tempdir().unwrap();
let filepath = dir.path().join("nonexistent.txt");
assert!(CachingEOPProvider::check_file_age(&filepath, 86400).unwrap());
}
#[test]
fn test_check_file_age_current() {
let dir = tempdir().unwrap();
let filepath = dir.path().join("current.txt");
File::create(&filepath).unwrap();
assert!(!CachingEOPProvider::check_file_age(&filepath, 86400).unwrap());
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
fn test_check_file_age_stale() {
let dir = tempdir().unwrap();
let filepath = dir.path().join("stale.txt");
File::create(&filepath).unwrap();
thread::sleep(Duration::from_secs(2));
assert!(CachingEOPProvider::check_file_age(&filepath, 1).unwrap());
}
#[test]
fn test_new_with_existing_file() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_eop.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
365 * 86400, false, true,
EOPExtrapolation::Hold,
)
.unwrap();
assert!(provider.is_initialized());
assert_eq!(provider.eop_type(), EOPType::StandardBulletinA);
assert!(provider.len() > 0);
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
fn test_new_creates_missing_file() {
let dir = tempdir().unwrap();
let filepath = dir.path().join("downloaded_eop.txt");
let provider = CachingEOPProvider::new(
Some(&filepath),
EOPType::StandardBulletinA,
7 * 86400,
true,
true,
EOPExtrapolation::Hold,
)
.unwrap();
assert!(filepath.exists());
assert!(provider.is_initialized());
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
fn test_new_with_default_path() {
let provider = CachingEOPProvider::new(
None,
EOPType::StandardBulletinA,
7 * 86400,
false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
assert!(provider.is_initialized());
assert_eq!(provider.eop_type(), EOPType::StandardBulletinA);
assert!(provider.len() > 0);
let cache_dir = crate::utils::cache::get_eop_cache_dir().unwrap();
let expected_path = PathBuf::from(cache_dir).join("finals.all.iau2000.txt");
assert!(expected_path.exists());
}
#[test]
fn test_refresh() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_eop_refresh.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
365 * 86400,
false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
let original_len = provider.len();
provider.refresh().unwrap();
assert_eq!(provider.len(), original_len);
}
#[test]
fn test_eop_provider_delegation() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_eop_delegation.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
100 * 365 * 86400, false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
assert!(provider.is_initialized());
assert_eq!(provider.eop_type(), EOPType::StandardBulletinA);
assert_eq!(provider.extrapolation(), EOPExtrapolation::Hold);
assert!(provider.interpolation());
assert_eq!(provider.mjd_min(), 41684.0);
assert!(provider.mjd_max() >= 60672.0);
let ut1_utc = provider.get_ut1_utc(59569.0).unwrap();
assert_eq!(ut1_utc, -0.1079939);
let (pm_x, pm_y) = provider.get_pm(59569.0).unwrap();
assert!(pm_x > 0.0);
assert!(pm_y > 0.0);
let (dx, dy) = provider.get_dxdy(59569.0).unwrap();
assert!(dx != 0.0 || dy != 0.0);
let lod = provider.get_lod(59569.0).unwrap();
assert!(lod != 0.0);
}
#[test]
fn test_new_with_c04_type() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("EOP_20_C04_one_file_1962-now.txt");
let dest_path = dir.path().join("test_eop_c04.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::C04,
365 * 86400,
false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
assert!(provider.is_initialized());
assert_eq!(provider.eop_type(), EOPType::C04);
assert!(provider.len() > 0);
}
#[test]
fn test_new_with_unknown_type_error() {
let dir = tempdir().unwrap();
let dest_path = dir.path().join("test_eop_unknown.txt");
let result = CachingEOPProvider::new(
Some(&dest_path),
EOPType::Unknown,
365 * 86400,
false,
true,
EOPExtrapolation::Hold,
);
assert!(result.is_err());
}
#[test]
fn test_file_epoch() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_eop_epoch.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
365 * 86400,
false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
let epoch = provider.file_epoch();
assert_eq!(epoch.time_system, TimeSystem::UTC);
let now = Epoch::now();
let diff_seconds = (now.mjd() - epoch.mjd()) * 86400.0;
assert!(diff_seconds.abs() < 365.0 * 86400.0);
}
#[test]
fn test_file_age() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_eop_age.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
365 * 86400,
false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
let age = provider.file_age();
assert!(age < 10.0);
thread::sleep(Duration::from_secs(1));
let age2 = provider.file_age();
assert!(age2 >= 1.0);
assert!(age2 > age);
}
#[test]
fn test_mjd_last_lod_delegation() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_eop_last_lod.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
100 * 365 * 86400, false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
let mjd_last_lod = provider.mjd_last_lod();
assert_eq!(mjd_last_lod, 60298.0);
}
#[test]
fn test_mjd_last_dxdy_delegation() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_eop_last_dxdy.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
100 * 365 * 86400, false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
let mjd_last_dxdy = provider.mjd_last_dxdy();
assert_eq!(mjd_last_dxdy, 60373.0);
}
#[test]
fn test_get_eop_method() {
let dir = tempdir().unwrap();
let src_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap())
.join("test_assets")
.join("finals.all.iau2000.txt");
let dest_path = dir.path().join("test_get_eop.txt");
fs::copy(&src_path, &dest_path).unwrap();
let provider = CachingEOPProvider::new(
Some(&dest_path),
EOPType::StandardBulletinA,
365 * 86400,
false,
true,
EOPExtrapolation::Hold,
)
.unwrap();
let eop_data = provider.get_eop(59569.0).unwrap();
assert_eq!(eop_data.2, -0.1079939); assert!(eop_data.0 > 0.0); assert!(eop_data.1 > 0.0); }
}