Skip to content

Commit

Permalink
Add compile time EOP data fetch and refactor EOPData and `EOPManage…
Browse files Browse the repository at this point in the history
…r` (#4)
  • Loading branch information
ReeceHumphreys authored Feb 24, 2025
1 parent a1bf2d4 commit 8699264
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 97 deletions.
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ name = "KosmOSS"
version = "0.1.0"
edition = "2021"

[build-dependencies]
reqwest = { version = "0.11", features = ["blocking"] }

[dependencies]
nalgebra = "0.32.3" # For linear algebra and vectors
rand = "0.8.5" # For random number generation if needed
csv = "1.3"
approx = "0.5" # For float comparisons in tests
hifitime = "3.9.0" # Latest stable version
reqwest = { version = "0.11", features = ["blocking"] }
chrono = "0.4"
dirs = "5.0"
lazy_static = "1.4"
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["blocking"] }
serde = { version = "1.0", features = ["derive"] }
68 changes: 68 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use reqwest::blocking::Client;
use std::env;
use std::error::Error;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};

const CELESTRAK_URL: &str = "https://celestrak.org/SpaceData/EOP-All.csv";
const CACHE_FILE: &str = "eop_cache.csv";
const CACHE_EXPIRATION_HOURS: u64 = 6; // CelesTrak updates every 6 hours

fn main() {
// Get Cargo's OUT_DIR (temporary build directory)
let out_dir = env::var("OUT_DIR").expect("Cargo should set OUT_DIR");
let cache_path = PathBuf::from(out_dir).join(CACHE_FILE);

// Download and store the EOP data
match fetch_eop_data(&cache_path) {
Ok(_) => println!("EOP data fetched successfully!"),
Err(e) => panic!("Failed to fetch EOP data: {}", e),
}
}

fn fetch_eop_data(cache_path: &PathBuf) -> Result<(), Box<dyn Error>> {
// Check last modified time of cache
if let Ok(metadata) = fs::metadata(cache_path) {
if let Ok(modified) = metadata.modified() {
let now = SystemTime::now();
let age = now.duration_since(modified).unwrap_or(Duration::ZERO);

// Skip download if the cache is still fresh
if age < Duration::from_secs(CACHE_EXPIRATION_HOURS * 3600) {
eprintln!(
"Skipping download: Cached EOP data is still fresh ({} minutes old).",
age.as_secs() / 60
);
return Ok(());
}
}
}

eprintln!("Fetching new EOP data from: {}", CELESTRAK_URL);

let client = Client::new();
let response = client.get(CELESTRAK_URL).send()?;
let status = response.status();

if !status.is_success() {
let response_body = response
.text()
.unwrap_or_else(|_| "Failed to read response body".to_string());
return Err(format!(
"HTTP request failed: {} - Response: {}",
status, response_body
)
.into());
}

let bytes = response.bytes()?;
eprintln!("Downloaded {} bytes of EOP data.", bytes.len());

let mut file = File::create(cache_path)?;
file.write_all(&bytes)?;

eprintln!("EOP data successfully written to {:?}", cache_path);
Ok(())
}
78 changes: 53 additions & 25 deletions src/coordinates/coordinate_transformation.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use super::eop_errors::EOPErrors;
use crate::constants::*;
use crate::coordinates::eop_manager::EOPManager;
use hifitime::Epoch;
use lazy_static::lazy_static;
use nalgebra as na;
use std::error::Error;
use std::sync::Mutex;
use std::sync::{Mutex, OnceLock};

lazy_static! {
static ref EOP_MANAGER: Mutex<EOPManager> = Mutex::new(EOPManager::new());
static ref INIT_STATUS: OnceLock<bool> = OnceLock::new(); // Tracks if `initialize()` was called
}

#[derive(Clone)]
pub struct EOPData {
Expand All @@ -16,6 +21,52 @@ pub struct EOPData {
pub ddeps: f64, // Nutation correction to obliquity (arcsec)
}

impl Default for EOPData {
fn default() -> Self {
EOPData {
x_pole: 0.161556,
y_pole: 0.247219,
ut1_utc: -0.0890529,
lod: 0.0017,
ddpsi: -0.052,
ddeps: -0.003,
}
}
}

impl TryFrom<Epoch> for EOPData {
type Error = EOPErrors;

/// Try to get EOP data for a given epoch
/// This will fetch the EOP data from the cache file if available otherwise it will fail
fn try_from(epoch: Epoch) -> Result<Self, Self::Error> {
let mut manager = EOP_MANAGER.lock().unwrap();

// Ensure `initialize()` is only called once
if INIT_STATUS.get().is_none() {
manager.initialize()?;
INIT_STATUS.set(true).unwrap();
}

// Fetch EOP data
manager.get_eop_data(epoch, false)
}
}

impl EOPData {
/// Interpolate EOP data between two epochs
pub fn interpolate(eop1: &EOPData, eop2: &EOPData, fraction: f64) -> EOPData {
EOPData {
x_pole: eop1.x_pole + (eop2.x_pole - eop1.x_pole) * fraction,
y_pole: eop1.y_pole + (eop2.y_pole - eop1.y_pole) * fraction,
ut1_utc: eop1.ut1_utc + (eop2.ut1_utc - eop1.ut1_utc) * fraction,
lod: eop1.lod + (eop2.lod - eop1.lod) * fraction,
ddpsi: eop1.ddpsi + (eop2.ddpsi - eop1.ddpsi) * fraction,
ddeps: eop1.ddeps + (eop2.ddeps - eop1.ddeps) * fraction,
}
}
}

/// Convert ITRS Cartesian to Geodetic coordinates (WGS84)
pub fn itrs_to_geodetic(pos: &na::Vector3<f64>) -> (f64, f64, f64) {
let x = pos[0];
Expand Down Expand Up @@ -67,29 +118,6 @@ pub fn itrs_to_geodetic(pos: &na::Vector3<f64>) -> (f64, f64, f64) {
(longitude.to_degrees(), latitude.to_degrees(), altitude)
}

impl EOPData {
/// Interpolate EOP data between two epochs
pub fn interpolate(eop1: &EOPData, eop2: &EOPData, fraction: f64) -> EOPData {
EOPData {
x_pole: eop1.x_pole + (eop2.x_pole - eop1.x_pole) * fraction,
y_pole: eop1.y_pole + (eop2.y_pole - eop1.y_pole) * fraction,
ut1_utc: eop1.ut1_utc + (eop2.ut1_utc - eop1.ut1_utc) * fraction,
lod: eop1.lod + (eop2.lod - eop1.lod) * fraction,
ddpsi: eop1.ddpsi + (eop2.ddpsi - eop1.ddpsi) * fraction,
ddeps: eop1.ddeps + (eop2.ddeps - eop1.ddeps) * fraction,
}
}

pub fn from_epoch(epoch: Epoch) -> Result<Self, Box<dyn Error>> {
lazy_static! {
static ref EOP_MANAGER: Mutex<EOPManager> = Mutex::new(EOPManager::new());
}

let mut manager = EOP_MANAGER.lock().unwrap();
manager.get_eop_data(epoch)
}
}

/// Convert GCRS to ITRS using IAU 2000/2006 CIO-based transformation
pub fn gcrs_to_itrs(position: &na::Vector3<f64>, epoch: &Epoch, eop: &EOPData) -> na::Vector3<f64> {
// Convert arcseconds to radians
Expand Down
56 changes: 56 additions & 0 deletions src/coordinates/eop_errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use reqwest;
use std::{error::Error, fmt, io, num::ParseFloatError};

#[derive(Debug)]
pub enum EOPErrors {
IoError(std::io::Error),
ReqwestError(reqwest::Error),
CsvError(csv::Error),
ParseFloatError(ParseFloatError),
InvalidEpoch(hifitime::errors::Errors),
MissingEOPData,
DataInterpolationError,
HttpForbidden,
}

impl fmt::Display for EOPErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EOPErrors::IoError(e) => write!(f, "I/O error: {}", e),
EOPErrors::ReqwestError(e) => write!(f, "Request error: {}", e),
EOPErrors::CsvError(e) => write!(f, "CSV parsing error: {}", e),
EOPErrors::ParseFloatError(e) => write!(f, "Float parsing error: {}", e),
EOPErrors::InvalidEpoch(e) => write!(f, "Invalid epoch {}", e),
EOPErrors::MissingEOPData => write!(f, "EOP data is missing"),
EOPErrors::DataInterpolationError => write!(f, "Failed to interpolate EOP data"),
EOPErrors::HttpForbidden => write!(f, "HTTP 403 Forbidden"),
}
}
}

impl Error for EOPErrors {}

// Implement `From<T>` conversions for automatic error mapping
impl From<io::Error> for EOPErrors {
fn from(err: io::Error) -> Self {
EOPErrors::IoError(err)
}
}

impl From<reqwest::Error> for EOPErrors {
fn from(err: reqwest::Error) -> Self {
EOPErrors::ReqwestError(err)
}
}

impl From<csv::Error> for EOPErrors {
fn from(err: csv::Error) -> Self {
EOPErrors::CsvError(err)
}
}

impl From<ParseFloatError> for EOPErrors {
fn from(err: ParseFloatError) -> Self {
EOPErrors::ParseFloatError(err)
}
}
Loading

0 comments on commit 8699264

Please sign in to comment.