use nalgebra::Vector3;
use crate::AngleFormat;
use crate::constants::WGS84_A;
use crate::constants::WGS84_F;
use crate::coordinates::rotation_enz_to_ellipsoid;
use crate::time::Epoch;
const WGS84_A_KM: f64 = WGS84_A / 1000.0;
const WGS84_E2: f64 = WGS84_F * (2.0 - WGS84_F);
pub(crate) fn epoch_to_decimal_year(epoch: &Epoch) -> f64 {
let year = epoch.year();
let doy = epoch.day_of_year();
let is_leap = year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400));
let days_in_year = if is_leap { 366.0 } else { 365.0 };
year as f64 + (doy - 1.0) / days_in_year
}
pub(crate) fn geodetic_to_geocentric_mag(
lat_deg: f64,
lon_deg: f64,
alt_km: f64,
) -> (f64, f64, f64) {
let a = WGS84_A_KM;
let b = a * (1.0 - WGS84_E2).sqrt();
let lat_rad = lat_deg.to_radians();
let sin_lat = lat_rad.sin();
let cos_lat = lat_rad.cos();
let sin_lat_2 = sin_lat * sin_lat;
let cos_lat_2 = cos_lat * cos_lat;
let tmp = alt_km * (a * a * cos_lat_2 + b * b * sin_lat_2).sqrt();
let beta = ((tmp + b * b) / (tmp + a * a) * lat_rad.tan()).atan();
let theta = std::f64::consts::FRAC_PI_2 - beta;
let b_over_a = b / a;
let r = (alt_km * alt_km
+ 2.0 * tmp
+ a * a * (1.0 - (1.0 - b_over_a.powi(4)) * sin_lat_2)
/ (1.0 - (1.0 - b_over_a.powi(2)) * sin_lat_2))
.sqrt();
let phi = lon_deg.to_radians();
(r, theta, phi)
}
pub(crate) fn field_geocentric_to_geodetic_enz(
b_r: f64,
b_theta: f64,
b_phi: f64,
theta_rad: f64,
lat_geod_rad: f64,
) -> Vector3<f64> {
let psi = lat_geod_rad.sin() * theta_rad.sin() - lat_geod_rad.cos() * theta_rad.cos();
let b_north = -psi.cos() * b_theta - psi.sin() * b_r;
let b_zenith = -psi.sin() * b_theta + psi.cos() * b_r;
let b_east = b_phi;
Vector3::new(b_east, b_north, b_zenith)
}
pub(crate) fn field_geocentric_to_geocentric_enz(
b_r: f64,
b_theta: f64,
b_phi: f64,
) -> Vector3<f64> {
Vector3::new(b_phi, -b_theta, b_r)
}
pub(crate) fn field_geodetic_enz_to_ecef(
b_enz: Vector3<f64>,
lon_deg: f64,
lat_deg: f64,
) -> Vector3<f64> {
let geod = Vector3::new(lon_deg, lat_deg, 0.0);
let rot = rotation_enz_to_ellipsoid(geod, AngleFormat::Degrees);
rot * b_enz
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_epoch_to_decimal_year() {
use crate::time::TimeSystem;
let epoch = Epoch::from_datetime(2025, 1, 1, 0, 0, 0.0, 0.0, TimeSystem::UTC);
assert_abs_diff_eq!(epoch_to_decimal_year(&epoch), 2025.0, epsilon = 1e-6);
let epoch_mid = Epoch::from_datetime(2025, 7, 2, 12, 0, 0.0, 0.0, TimeSystem::UTC);
assert_abs_diff_eq!(epoch_to_decimal_year(&epoch_mid), 2025.5, epsilon = 1e-3);
}
#[test]
fn test_geodetic_to_geocentric_equator() {
let (r, theta, phi) = geodetic_to_geocentric_mag(0.0, 0.0, 0.0);
assert_abs_diff_eq!(theta, std::f64::consts::FRAC_PI_2, epsilon = 1e-10);
assert_abs_diff_eq!(phi, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(r, WGS84_A_KM, epsilon = 0.1);
}
#[test]
fn test_geodetic_to_geocentric_pole() {
let (r, theta, _phi) = geodetic_to_geocentric_mag(90.0, 0.0, 0.0);
assert!(theta < 0.01); let b = WGS84_A_KM * (1.0 - WGS84_E2).sqrt();
assert_abs_diff_eq!(r, b, epsilon = 0.1);
}
#[test]
fn test_field_geodetic_enz_at_equator() {
let b_r = 100.0;
let b_theta = 50.0;
let b_phi = 30.0;
let theta_rad = std::f64::consts::FRAC_PI_2; let lat_geod_rad = 0.0;
let b_enz = field_geocentric_to_geodetic_enz(b_r, b_theta, b_phi, theta_rad, lat_geod_rad);
assert_abs_diff_eq!(b_enz[0], b_phi, epsilon = 1e-10); assert_abs_diff_eq!(b_enz[1], -b_theta, epsilon = 1e-10); assert_abs_diff_eq!(b_enz[2], b_r, epsilon = 1e-10); }
#[test]
fn test_field_geocentric_enz() {
let b_r = 100.0;
let b_theta = 50.0;
let b_phi = 30.0;
let b_enz = field_geocentric_to_geocentric_enz(b_r, b_theta, b_phi);
assert_abs_diff_eq!(b_enz[0], 30.0, epsilon = 1e-10); assert_abs_diff_eq!(b_enz[1], -50.0, epsilon = 1e-10); assert_abs_diff_eq!(b_enz[2], 100.0, epsilon = 1e-10); }
}