use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use crate::space_weather::file_provider::FileSpaceWeatherProvider;
use crate::space_weather::provider::SpaceWeatherProvider;
use crate::space_weather::types::{SpaceWeatherExtrapolation, SpaceWeatherType};
use crate::time::{Epoch, TimeSystem};
use crate::utils::BraheError;
use crate::utils::atomic_write;
use crate::utils::cache::get_space_weather_cache_dir;
const DEFAULT_SW_URL: &str = "https://celestrak.org/SpaceData/sw19571001.txt";
const DEFAULT_SW_FILENAME: &str = "sw19571001.txt";
#[derive(Clone)]
pub struct CachingSpaceWeatherProvider {
inner: Arc<Mutex<FileSpaceWeatherProvider>>,
cache_path: PathBuf,
max_age: u64,
pub auto_refresh: bool,
extrapolate: SpaceWeatherExtrapolation,
file_loaded_at: Arc<Mutex<SystemTime>>,
}
impl CachingSpaceWeatherProvider {
pub fn new(
cache_dir: Option<PathBuf>,
max_age: u64,
auto_refresh: bool,
extrapolate: SpaceWeatherExtrapolation,
) -> Result<Self, BraheError> {
let cache_dir = match cache_dir {
Some(dir) => dir,
None => PathBuf::from(get_space_weather_cache_dir()?),
};
let cache_path = cache_dir.join(DEFAULT_SW_FILENAME);
let needs_download = Self::check_file_age(&cache_path, max_age)?;
if needs_download {
match download_space_weather(&cache_path) {
Ok(_) => {}
Err(e) => {
if !cache_path.exists() {
return Err(e);
}
}
}
}
let inner = if cache_path.exists() {
FileSpaceWeatherProvider::from_file(&cache_path, extrapolate)?
} else {
FileSpaceWeatherProvider::from_default_file()?
};
let file_loaded_at = Arc::new(Mutex::new(SystemTime::now()));
Ok(Self {
inner: Arc::new(Mutex::new(inner)),
cache_path,
max_age,
auto_refresh,
extrapolate,
file_loaded_at,
})
}
pub fn with_url(
url: &str,
cache_dir: Option<PathBuf>,
max_age: u64,
auto_refresh: bool,
extrapolate: SpaceWeatherExtrapolation,
) -> Result<Self, BraheError> {
let cache_dir = match cache_dir {
Some(dir) => dir,
None => PathBuf::from(get_space_weather_cache_dir()?),
};
let cache_path = cache_dir.join(DEFAULT_SW_FILENAME);
let needs_download = Self::check_file_age(&cache_path, max_age)?;
if needs_download {
download_from_url(url, &cache_path)?;
}
let inner = FileSpaceWeatherProvider::from_file(&cache_path, extrapolate)?;
let file_loaded_at = Arc::new(Mutex::new(SystemTime::now()));
Ok(Self {
inner: Arc::new(Mutex::new(inner)),
cache_path,
max_age,
auto_refresh,
extrapolate,
file_loaded_at,
})
}
fn check_file_age(filepath: &Path, max_age: 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 age = SystemTime::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)
}
pub fn refresh(&self) -> Result<(), BraheError> {
let needs_download = Self::check_file_age(&self.cache_path, self.max_age)?;
if needs_download {
download_space_weather(&self.cache_path)?;
let new_provider =
FileSpaceWeatherProvider::from_file(&self.cache_path, self.extrapolate)?;
*self.inner.lock().unwrap() = new_provider;
*self.file_loaded_at.lock().unwrap() = SystemTime::now();
}
Ok(())
}
pub fn cache_path(&self) -> &Path {
&self.cache_path
}
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 SpaceWeatherProvider for CachingSpaceWeatherProvider {
fn len(&self) -> usize {
self.inner.lock().unwrap().len()
}
fn sw_type(&self) -> SpaceWeatherType {
self.inner.lock().unwrap().sw_type()
}
fn is_initialized(&self) -> bool {
self.inner.lock().unwrap().is_initialized()
}
fn extrapolation(&self) -> SpaceWeatherExtrapolation {
self.inner.lock().unwrap().extrapolation()
}
fn mjd_min(&self) -> f64 {
self.inner.lock().unwrap().mjd_min()
}
fn mjd_max(&self) -> f64 {
self.inner.lock().unwrap().mjd_max()
}
fn mjd_last_observed(&self) -> f64 {
self.inner.lock().unwrap().mjd_last_observed()
}
fn mjd_last_daily_predicted(&self) -> f64 {
self.inner.lock().unwrap().mjd_last_daily_predicted()
}
fn mjd_last_monthly_predicted(&self) -> f64 {
self.inner.lock().unwrap().mjd_last_monthly_predicted()
}
fn get_kp(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_kp(mjd)
}
fn get_kp_all(&self, mjd: f64) -> Result<[f64; 8], BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_kp_all(mjd)
}
fn get_kp_daily(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_kp_daily(mjd)
}
fn get_ap(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_ap(mjd)
}
fn get_ap_all(&self, mjd: f64) -> Result<[f64; 8], BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_ap_all(mjd)
}
fn get_ap_daily(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_ap_daily(mjd)
}
fn get_f107_observed(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_f107_observed(mjd)
}
fn get_f107_adjusted(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_f107_adjusted(mjd)
}
fn get_f107_obs_avg81(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_f107_obs_avg81(mjd)
}
fn get_f107_adj_avg81(&self, mjd: f64) -> Result<f64, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_f107_adj_avg81(mjd)
}
fn get_sunspot_number(&self, mjd: f64) -> Result<u32, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_sunspot_number(mjd)
}
fn get_last_kp(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_last_kp(mjd, n)
}
fn get_last_ap(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_last_ap(mjd, n)
}
fn get_last_daily_kp(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_last_daily_kp(mjd, n)
}
fn get_last_daily_ap(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_last_daily_ap(mjd, n)
}
fn get_last_f107(&self, mjd: f64, n: usize) -> Result<Vec<f64>, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_last_f107(mjd, n)
}
fn get_last_kpap_epochs(&self, mjd: f64, n: usize) -> Result<Vec<Epoch>, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_last_kpap_epochs(mjd, n)
}
fn get_last_daily_epochs(&self, mjd: f64, n: usize) -> Result<Vec<Epoch>, BraheError> {
self.check_auto_refresh()?;
self.inner.lock().unwrap().get_last_daily_epochs(mjd, n)
}
}
fn download_space_weather(output_path: &Path) -> Result<(), BraheError> {
download_from_url(DEFAULT_SW_URL, output_path)
}
fn download_from_url(url: &str, output_path: &Path) -> Result<(), BraheError> {
let body = ureq::get(url)
.call()
.map_err(|e| BraheError::IoError(format!("Failed to download {}: {}", url, e)))?
.body_mut()
.read_to_string()
.map_err(|e| BraheError::IoError(format!("Failed to read response: {}", e)))?;
atomic_write(output_path, body.as_bytes())?;
Ok(())
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::utils::testing::get_test_space_weather_filepath;
use std::fs::File;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
fn setup_test_cache(cache_dir: &PathBuf) {
let test_file = get_test_space_weather_filepath();
let cache_path = cache_dir.join(DEFAULT_SW_FILENAME);
fs::create_dir_all(cache_dir).unwrap();
fs::copy(&test_file, &cache_path).unwrap();
}
#[test]
fn test_check_file_age_nonexistent() {
let dir = TempDir::new().unwrap();
let filepath = dir.path().join("nonexistent.txt");
assert!(CachingSpaceWeatherProvider::check_file_age(&filepath, 86400).unwrap());
}
#[test]
fn test_check_file_age_current() {
let dir = TempDir::new().unwrap();
let filepath = dir.path().join("current.txt");
File::create(&filepath).unwrap();
assert!(!CachingSpaceWeatherProvider::check_file_age(&filepath, 86400).unwrap());
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
fn test_check_file_age_stale() {
let dir = TempDir::new().unwrap();
let filepath = dir.path().join("stale.txt");
File::create(&filepath).unwrap();
thread::sleep(Duration::from_secs(2));
assert!(CachingSpaceWeatherProvider::check_file_age(&filepath, 1).unwrap());
}
#[test]
fn test_caching_provider_from_packaged() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
let result = CachingSpaceWeatherProvider::new(
Some(cache_dir),
0,
false,
SpaceWeatherExtrapolation::Hold,
);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_file_age_and_epoch() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400, false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let age = provider.file_age();
assert!(
age < 1.0,
"File age should be less than 1 second, got {}",
age
);
let epoch = provider.file_epoch();
let now_mjd = Epoch::now().mjd();
let epoch_mjd = epoch.mjd();
assert!(
(now_mjd - epoch_mjd).abs() < 1.0 / 86400.0, "File epoch MJD {} should be close to now {}",
epoch_mjd,
now_mjd
);
}
#[test]
fn test_refresh() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let original_len = provider.len();
provider.refresh().unwrap();
assert_eq!(provider.len(), original_len);
}
#[test]
fn test_provider_delegation() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
assert!(provider.is_initialized());
assert_eq!(provider.sw_type(), SpaceWeatherType::CssiSpaceWeather);
assert_eq!(provider.extrapolation(), SpaceWeatherExtrapolation::Hold);
assert!(provider.len() > 0);
let test_mjd = 60000.0;
let kp = provider.get_kp(test_mjd).unwrap();
assert!((0.0..=9.0).contains(&kp));
let ap = provider.get_ap_daily(test_mjd).unwrap();
assert!((0.0..=400.0).contains(&ap));
let f107 = provider.get_f107_observed(test_mjd).unwrap();
assert!(f107 > 0.0);
let isn = provider.get_sunspot_number(test_mjd).unwrap();
assert!(isn < 500);
}
#[test]
fn test_mjd_boundaries() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
assert_eq!(provider.mjd_min(), 36112.0);
assert!(provider.mjd_max() > 60000.0);
assert!(provider.mjd_last_observed() > 60000.0);
assert!(provider.mjd_last_daily_predicted() >= provider.mjd_last_observed());
assert!(provider.mjd_last_daily_predicted() > 58849.0);
assert!(provider.mjd_last_monthly_predicted() >= provider.mjd_last_daily_predicted());
assert!(provider.mjd_last_monthly_predicted() > 58849.0);
}
#[test]
fn test_get_f107_adj_avg81() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let f107_adj_avg = provider.get_f107_adj_avg81(mjd).unwrap();
assert!(f107_adj_avg > 0.0);
assert!(f107_adj_avg > 50.0 && f107_adj_avg < 400.0);
}
#[test]
fn test_get_last_methods() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let kp_values = provider.get_last_kp(mjd, 5).unwrap();
assert_eq!(kp_values.len(), 5);
for kp in &kp_values {
assert!((0.0..=9.0).contains(kp));
}
let ap_values = provider.get_last_ap(mjd, 5).unwrap();
assert_eq!(ap_values.len(), 5);
for ap in &ap_values {
assert!(*ap >= 0.0);
}
let daily_kp = provider.get_last_daily_kp(mjd, 3).unwrap();
assert_eq!(daily_kp.len(), 3);
for kp in &daily_kp {
assert!(*kp >= 0.0 && *kp <= 9.0);
}
let daily_ap = provider.get_last_daily_ap(mjd, 3).unwrap();
assert_eq!(daily_ap.len(), 3);
for ap in &daily_ap {
assert!(*ap >= 0.0);
}
let f107_values = provider.get_last_f107(mjd, 3).unwrap();
assert_eq!(f107_values.len(), 3);
for f107 in &f107_values {
assert!(*f107 > 0.0);
}
let epochs = provider.get_last_kpap_epochs(mjd, 5).unwrap();
assert_eq!(epochs.len(), 5);
for i in 0..epochs.len() - 1 {
assert!(epochs[i].mjd() < epochs[i + 1].mjd());
}
let daily_epochs = provider.get_last_daily_epochs(mjd, 3).unwrap();
assert_eq!(daily_epochs.len(), 3);
for i in 0..daily_epochs.len() - 1 {
assert!(daily_epochs[i].mjd() < daily_epochs[i + 1].mjd());
}
}
#[test]
fn test_with_url_from_cached_file() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::with_url(
"https://example.com/sw19571001.txt",
Some(cache_dir),
365 * 86400, false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
assert!(provider.is_initialized());
assert!(provider.len() > 0);
assert_eq!(provider.sw_type(), SpaceWeatherType::CssiSpaceWeather);
assert_eq!(provider.extrapolation(), SpaceWeatherExtrapolation::Hold);
let test_mjd = 60000.0;
let kp = provider.get_kp(test_mjd).unwrap();
assert!((0.0..=9.0).contains(&kp));
let ap = provider.get_ap_daily(test_mjd).unwrap();
assert!(ap >= 0.0);
let f107 = provider.get_f107_observed(test_mjd).unwrap();
assert!(f107 > 0.0);
}
#[test]
fn test_get_kp_all() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let kp_all = provider.get_kp_all(mjd).unwrap();
assert_eq!(kp_all.len(), 8);
for kp in kp_all.iter() {
assert!((0.0..=9.0).contains(kp));
}
}
#[test]
fn test_get_kp_daily() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let kp_daily = provider.get_kp_daily(mjd).unwrap();
assert!((0.0..=9.0).contains(&kp_daily));
}
#[test]
fn test_get_ap() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let ap = provider.get_ap(mjd).unwrap();
assert!(ap >= 0.0);
}
#[test]
fn test_get_ap_all() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let ap_all = provider.get_ap_all(mjd).unwrap();
assert_eq!(ap_all.len(), 8);
for ap in ap_all.iter() {
assert!(*ap >= 0.0);
}
}
#[test]
fn test_get_f107_adjusted() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let f107_adj = provider.get_f107_adjusted(mjd).unwrap();
assert!(f107_adj >= 0.0);
}
#[test]
fn test_get_f107_obs_avg81() {
let temp_dir = TempDir::new().unwrap();
let cache_dir = temp_dir.path().to_path_buf();
setup_test_cache(&cache_dir);
let provider = CachingSpaceWeatherProvider::new(
Some(cache_dir),
365 * 86400,
false,
SpaceWeatherExtrapolation::Hold,
)
.unwrap();
let mjd = 60000.0;
let f107_obs_avg = provider.get_f107_obs_avg81(mjd).unwrap();
assert!(f107_obs_avg > 0.0);
}
}