use crate::astro::frames::transforms::geodetic_from_ecef_proj;
use crate::constants::{AU_KM, KM_TO_M, WGS84_A_KM, WGS84_E2};
use crate::estimation::recipe::FrameRecipe;
use crate::frame::{geocentric_neu_basis, geocentric_up, Wgs84Geodetic};
const PI: f64 = std::f64::consts::PI;
const TAU: f64 = std::f64::consts::TAU;
#[inline]
pub(crate) fn local_up(frame: FrameRecipe, position_ecef_m: [f64; 3]) -> [f64; 3] {
match frame {
FrameRecipe::GeocentricUpRtkReference | FrameRecipe::GeodeticNeuCrossProduct => {
geocentric_up(position_ecef_m)
}
_ => unreachable!("geocentric local up is selected only by the RTK/PPP geocentric recipes"),
}
}
#[inline]
pub(crate) fn local_neu_basis(
frame: FrameRecipe,
position_ecef_m: [f64; 3],
) -> ([f64; 3], [f64; 3], [f64; 3]) {
match frame {
FrameRecipe::GeocentricUpRtkReference | FrameRecipe::GeodeticNeuCrossProduct => {
geocentric_neu_basis(position_ecef_m)
}
_ => unreachable!(
"geocentric local NEU basis is selected only by the RTK/PPP geocentric recipes"
),
}
}
pub(crate) fn geodetic_from_ecef(frame: FrameRecipe, ecef_m: [f64; 3]) -> Wgs84Geodetic {
match frame {
FrameRecipe::SppSkyfieldAuThreeIter => skyfield_au_geodetic(ecef_m),
FrameRecipe::CanonicalWgs84 => canonical_wgs84_geodetic(ecef_m),
_ => unreachable!(
"the geodetic receiver frame is selected only by the SPP Skyfield and canonical recipes"
),
}
}
fn skyfield_au_geodetic(ecef_m: [f64; 3]) -> Wgs84Geodetic {
let x = ecef_m[0] / KM_TO_M;
let y = ecef_m[1] / KM_TO_M;
let z = ecef_m[2] / KM_TO_M;
let x_au = x / AU_KM;
let y_au = y / AU_KM;
let z_au = z / AU_KM;
let a_au = WGS84_A_KM / AU_KM;
let r_xy = (x_au * x_au + y_au * y_au).sqrt();
let lon_raw = y_au.atan2(x_au);
let mut lon_shifted = (lon_raw - PI) % TAU;
if lon_shifted < 0.0 {
lon_shifted += TAU;
}
let lon = lon_shifted - PI;
let mut lat = z_au.atan2(r_xy);
let mut a_c = 0.0;
let mut hyp = 0.0;
for _ in 0..3 {
let sin_lat = lat.sin();
let e2_sin_lat = WGS84_E2 * sin_lat;
a_c = a_au / (1.0 - e2_sin_lat * sin_lat).sqrt();
hyp = z_au + a_c * e2_sin_lat;
lat = hyp.atan2(r_xy);
}
let height_au = (hyp * hyp + r_xy * r_xy).sqrt() - a_c;
let height_m = height_au * AU_KM * KM_TO_M;
Wgs84Geodetic {
lat_rad: lat,
lon_rad: lon,
height_m,
}
}
fn canonical_wgs84_geodetic(ecef_m: [f64; 3]) -> Wgs84Geodetic {
let [lon_deg, lat_deg, alt_m] =
geodetic_from_ecef_proj(ecef_m[0], ecef_m[1], ecef_m[2]).expect("valid ECEF coordinates");
Wgs84Geodetic {
lat_rad: lat_deg.to_radians(),
lon_rad: lon_deg.to_radians(),
height_m: alt_m,
}
}
pub(crate) struct AzEl {
pub geodetic: Wgs84Geodetic,
pub az_rad: f64,
pub el_rad: f64,
}
pub(crate) fn az_el_from_ecef(
frame: FrameRecipe,
rx_ecef_m: [f64; 3],
sat_ecef_m: [f64; 3],
) -> AzEl {
let geo = match frame {
FrameRecipe::SppSkyfieldAuThreeIter => skyfield_au_geodetic(rx_ecef_m),
FrameRecipe::CanonicalWgs84 => canonical_wgs84_geodetic(rx_ecef_m),
_ => unreachable!(
"the geodetic ENU azimuth/elevation is selected only by the SPP Skyfield and canonical recipes"
),
};
geodetic_enu_az_el(geo, rx_ecef_m, sat_ecef_m)
}
fn geodetic_enu_az_el(geo: Wgs84Geodetic, rx_ecef_m: [f64; 3], sat_ecef_m: [f64; 3]) -> AzEl {
let dx = sat_ecef_m[0] - rx_ecef_m[0];
let dy = sat_ecef_m[1] - rx_ecef_m[1];
let dz = sat_ecef_m[2] - rx_ecef_m[2];
let sin_lat = geo.lat_rad.sin();
let cos_lat = geo.lat_rad.cos();
let sin_lon = geo.lon_rad.sin();
let cos_lon = geo.lon_rad.cos();
let e = -sin_lon * dx + cos_lon * dy;
let n = -sin_lat * cos_lon * dx - sin_lat * sin_lon * dy + cos_lat * dz;
let u = cos_lat * cos_lon * dx + cos_lat * sin_lon * dy + sin_lat * dz;
let rng = (e * e + n * n + u * u).sqrt();
let el = (u / rng).asin();
let mut az = e.atan2(n);
if az < 0.0 {
az += TAU;
}
AzEl {
geodetic: geo,
az_rad: az,
el_rad: el,
}
}
#[cfg(test)]
mod tests {
use super::*;
const POSITION: [f64; 3] = [4_027_894.0, 307_045.0, 4_919_474.0];
fn bits3(v: [f64; 3]) -> [u64; 3] {
[v[0].to_bits(), v[1].to_bits(), v[2].to_bits()]
}
#[test]
fn geocentric_recipes_match_frame_helper_bits() {
for frame in [
FrameRecipe::GeocentricUpRtkReference,
FrameRecipe::GeodeticNeuCrossProduct,
] {
assert_eq!(
bits3(local_up(frame, POSITION)),
bits3(geocentric_up(POSITION))
);
let (n, e, u) = local_neu_basis(frame, POSITION);
let (rn, re, ru) = geocentric_neu_basis(POSITION);
assert_eq!(bits3(n), bits3(rn));
assert_eq!(bits3(e), bits3(re));
assert_eq!(bits3(u), bits3(ru));
}
}
#[test]
fn spp_geodetic_recipe_matches_skyfield_au_solve_bits() {
let g = geodetic_from_ecef(FrameRecipe::SppSkyfieldAuThreeIter, POSITION);
let want = skyfield_au_geodetic(POSITION);
assert_eq!(g.lat_rad.to_bits(), want.lat_rad.to_bits());
assert_eq!(g.lon_rad.to_bits(), want.lon_rad.to_bits());
assert_eq!(g.height_m.to_bits(), want.height_m.to_bits());
}
#[test]
fn spp_az_el_recipe_matches_geodetic_enu_bits() {
let sat = [15_600_000.0, -20_400_000.0, 9_800_000.0];
let got = az_el_from_ecef(FrameRecipe::SppSkyfieldAuThreeIter, POSITION, sat);
let want = geodetic_enu_az_el(skyfield_au_geodetic(POSITION), POSITION, sat);
assert_eq!(got.az_rad.to_bits(), want.az_rad.to_bits());
assert_eq!(got.el_rad.to_bits(), want.el_rad.to_bits());
assert_eq!(
got.geodetic.lat_rad.to_bits(),
want.geodetic.lat_rad.to_bits()
);
}
#[test]
fn canonical_geodetic_recipe_matches_proj_solve_bits() {
let g = geodetic_from_ecef(FrameRecipe::CanonicalWgs84, POSITION);
let want = canonical_wgs84_geodetic(POSITION);
assert_eq!(g.lat_rad.to_bits(), want.lat_rad.to_bits());
assert_eq!(g.lon_rad.to_bits(), want.lon_rad.to_bits());
assert_eq!(g.height_m.to_bits(), want.height_m.to_bits());
}
#[test]
fn canonical_az_el_recipe_matches_geodetic_enu_bits() {
let sat = [15_600_000.0, -20_400_000.0, 9_800_000.0];
let got = az_el_from_ecef(FrameRecipe::CanonicalWgs84, POSITION, sat);
let want = geodetic_enu_az_el(canonical_wgs84_geodetic(POSITION), POSITION, sat);
assert_eq!(got.az_rad.to_bits(), want.az_rad.to_bits());
assert_eq!(got.el_rad.to_bits(), want.el_rad.to_bits());
assert_eq!(
got.geodetic.lat_rad.to_bits(),
want.geodetic.lat_rad.to_bits()
);
}
#[test]
fn canonical_and_skyfield_geodetic_agree_to_sub_milliarcsecond() {
let skyfield = skyfield_au_geodetic(POSITION);
let canonical = canonical_wgs84_geodetic(POSITION);
let mas_rad = std::f64::consts::PI / 180.0 / 3_600.0 / 1.0e3;
assert!((skyfield.lat_rad - canonical.lat_rad).abs() < mas_rad);
assert!((skyfield.lon_rad - canonical.lon_rad).abs() < mas_rad);
assert!((skyfield.height_m - canonical.height_m).abs() < 1.0e-6);
}
}