use super::reference_surface::ReferenceSurface;
use crate::bodies::solar_system::{Earth, Mars, Mercury, Moon, Pluto, Venus};
use crate::coordinates::centers::{
Geocentric, Marscentric, Mercurycentric, Plutocentric, ReferenceCenter, Selenocentric,
Venuscentric,
};
use crate::coordinates::frames::{
MarsFixed, MercuryFixed, MoonIauMeanEarth, PlutoFixed, ReferenceFrame, VenusFixed, ECEF,
};
use crate::qtty::{Degrees, Meters};
use affn::AffineCenter;
use core::fmt::Debug;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LatitudeConvention {
Planetocentric,
Planetographic,
}
impl LatitudeConvention {
pub const fn as_str(self) -> &'static str {
match self {
Self::Planetocentric => "planetocentric",
Self::Planetographic => "planetographic",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PlanetographicLongitudeSense {
EastPositive,
WestPositive,
}
impl PlanetographicLongitudeSense {
pub const fn as_str(self) -> &'static str {
match self {
Self::EastPositive => "east-positive",
Self::WestPositive => "west-positive",
}
}
}
#[inline]
pub fn east_positive_from_planetographic(
lon_planetographic: Degrees,
sense: PlanetographicLongitudeSense,
) -> Degrees {
match sense {
PlanetographicLongitudeSense::EastPositive => lon_planetographic,
PlanetographicLongitudeSense::WestPositive => Degrees::new(-lon_planetographic.value()),
}
}
#[inline]
pub fn planetographic_from_east_positive(
lon_east_positive: Degrees,
sense: PlanetographicLongitudeSense,
) -> Degrees {
east_positive_from_planetographic(lon_east_positive, sense)
}
pub trait BodySurfaceModel: Copy + Clone + Debug + Send + Sync + 'static {
type Center: ReferenceCenter<Params = ()> + AffineCenter;
type FixedFrame: ReferenceFrame;
fn body_name() -> &'static str;
fn default_surface() -> ReferenceSurface;
fn default_latitude_convention() -> LatitudeConvention;
fn planetographic_longitude_sense() -> PlanetographicLongitudeSense;
#[inline]
fn east_positive_longitude(lon_planetographic: Degrees) -> Degrees {
east_positive_from_planetographic(
lon_planetographic,
Self::planetographic_longitude_sense(),
)
}
#[inline]
fn planetographic_longitude(lon_east_positive: Degrees) -> Degrees {
planetographic_from_east_positive(lon_east_positive, Self::planetographic_longitude_sense())
}
}
pub const EARTH_WGS84_SURFACE: ReferenceSurface = ReferenceSurface::Ellipsoid {
a: Meters::new(6_378_137.0),
f: 1.0 / 298.257_223_563,
};
pub const MOON_IAU_SPHERE: ReferenceSurface = ReferenceSurface::Sphere {
radius: Meters::new(1_737_400.0),
};
pub const MARS_IAU_ELLIPSOID: ReferenceSurface = ReferenceSurface::Ellipsoid {
a: Meters::new(3_396_190.0),
f: (3_396_190.0 - 3_376_200.0) / 3_396_190.0,
};
pub const MERCURY_IAU_SPHERE: ReferenceSurface = ReferenceSurface::Sphere {
radius: Meters::new(2_439_700.0),
};
pub const VENUS_IAU_SPHERE: ReferenceSurface = ReferenceSurface::Sphere {
radius: Meters::new(6_051_800.0),
};
pub const PLUTO_IAU_SPHERE: ReferenceSurface = ReferenceSurface::Sphere {
radius: Meters::new(1_188_300.0),
};
impl BodySurfaceModel for Earth {
type Center = Geocentric;
type FixedFrame = ECEF;
fn body_name() -> &'static str {
"Earth"
}
fn default_surface() -> ReferenceSurface {
EARTH_WGS84_SURFACE
}
fn default_latitude_convention() -> LatitudeConvention {
LatitudeConvention::Planetographic
}
fn planetographic_longitude_sense() -> PlanetographicLongitudeSense {
PlanetographicLongitudeSense::EastPositive
}
}
impl BodySurfaceModel for Moon {
type Center = Selenocentric;
type FixedFrame = MoonIauMeanEarth;
fn body_name() -> &'static str {
"Moon"
}
fn default_surface() -> ReferenceSurface {
MOON_IAU_SPHERE
}
fn default_latitude_convention() -> LatitudeConvention {
LatitudeConvention::Planetocentric
}
fn planetographic_longitude_sense() -> PlanetographicLongitudeSense {
PlanetographicLongitudeSense::EastPositive
}
}
impl BodySurfaceModel for Mars {
type Center = Marscentric;
type FixedFrame = MarsFixed;
fn body_name() -> &'static str {
"Mars"
}
fn default_surface() -> ReferenceSurface {
MARS_IAU_ELLIPSOID
}
fn default_latitude_convention() -> LatitudeConvention {
LatitudeConvention::Planetographic
}
fn planetographic_longitude_sense() -> PlanetographicLongitudeSense {
PlanetographicLongitudeSense::WestPositive
}
}
impl BodySurfaceModel for Mercury {
type Center = Mercurycentric;
type FixedFrame = MercuryFixed;
fn body_name() -> &'static str {
"Mercury"
}
fn default_surface() -> ReferenceSurface {
MERCURY_IAU_SPHERE
}
fn default_latitude_convention() -> LatitudeConvention {
LatitudeConvention::Planetocentric
}
fn planetographic_longitude_sense() -> PlanetographicLongitudeSense {
PlanetographicLongitudeSense::WestPositive
}
}
impl BodySurfaceModel for Venus {
type Center = Venuscentric;
type FixedFrame = VenusFixed;
fn body_name() -> &'static str {
"Venus"
}
fn default_surface() -> ReferenceSurface {
VENUS_IAU_SPHERE
}
fn default_latitude_convention() -> LatitudeConvention {
LatitudeConvention::Planetocentric
}
fn planetographic_longitude_sense() -> PlanetographicLongitudeSense {
PlanetographicLongitudeSense::EastPositive
}
}
impl BodySurfaceModel for Pluto {
type Center = Plutocentric;
type FixedFrame = PlutoFixed;
fn body_name() -> &'static str {
"Pluto"
}
fn default_surface() -> ReferenceSurface {
PLUTO_IAU_SPHERE
}
fn default_latitude_convention() -> LatitudeConvention {
LatitudeConvention::Planetocentric
}
fn planetographic_longitude_sense() -> PlanetographicLongitudeSense {
PlanetographicLongitudeSense::WestPositive
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coordinates::surface::SurfaceSite;
use crate::qtty::{DEG, M};
#[test]
fn default_surfaces_admit_origin_sites() {
assert!(SurfaceSite::<Earth>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).is_ok());
assert!(SurfaceSite::<Moon>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).is_ok());
assert!(SurfaceSite::<Mars>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).is_ok());
assert!(SurfaceSite::<Mercury>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).is_ok());
assert!(SurfaceSite::<Venus>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).is_ok());
assert!(SurfaceSite::<Pluto>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).is_ok());
}
#[test]
fn mars_flattening_matches_iau() {
let s = Mars::default_surface();
let f = s.flattening();
assert!((f - 0.005_886_007_9).abs() < 1e-9, "f={f}");
}
#[test]
fn longitude_senses_match_naif_cartographic_rules() {
assert_eq!(
Earth::planetographic_longitude_sense(),
PlanetographicLongitudeSense::EastPositive
);
assert_eq!(
Moon::planetographic_longitude_sense(),
PlanetographicLongitudeSense::EastPositive
);
assert_eq!(
Venus::planetographic_longitude_sense(),
PlanetographicLongitudeSense::EastPositive
);
assert_eq!(
Mars::planetographic_longitude_sense(),
PlanetographicLongitudeSense::WestPositive
);
assert_eq!(
Mercury::planetographic_longitude_sense(),
PlanetographicLongitudeSense::WestPositive
);
assert_eq!(
Pluto::planetographic_longitude_sense(),
PlanetographicLongitudeSense::WestPositive
);
}
#[test]
fn mars_spice_west_longitude_converts_to_east_positive_without_hidden_flips() {
let lon_west = 137.4 * DEG;
let lon_east = Mars::east_positive_longitude(lon_west);
assert!((lon_east.value() + 137.4).abs() < 1e-12);
assert!((Mars::planetographic_longitude(lon_east).value() - 137.4).abs() < 1e-12);
let site = SurfaceSite::<Mars>::new(lon_east, (-4.6) * DEG, 0.0 * M).unwrap();
let p = site.body_fixed_cartesian().unwrap();
assert_near(p[0].value() / 1000.0, -2491.967067275659, 1e-9);
assert_near(p[1].value() / 1000.0, -2291.481184471932, 1e-9);
assert_near(p[2].value() / 1000.0, -269.1840291055111, 1e-9);
let wrong = SurfaceSite::<Mars>::new(lon_west, (-4.6) * DEG, 0.0 * M).unwrap();
let w = wrong.body_fixed_cartesian().unwrap();
assert!((w[1].value() - p[1].value()).abs() > 1e6);
}
#[test]
fn earth_and_venus_planetographic_longitude_is_identity() {
let lon = 28.75 * DEG;
assert_eq!(Earth::east_positive_longitude(lon), lon);
assert_eq!(Venus::east_positive_longitude(lon), lon);
assert_eq!(Moon::east_positive_longitude(lon), lon);
}
#[test]
fn default_surface_and_latitude_conventions_for_rocky_bodies() {
assert_eq!(Earth::body_name(), "Earth");
assert_eq!(Moon::body_name(), "Moon");
assert_eq!(Mars::body_name(), "Mars");
assert_eq!(Mercury::body_name(), "Mercury");
assert_eq!(Venus::body_name(), "Venus");
assert_eq!(Pluto::body_name(), "Pluto");
assert_eq!(
Earth::default_latitude_convention(),
LatitudeConvention::Planetographic
);
assert_eq!(
Mars::default_latitude_convention(),
LatitudeConvention::Planetographic
);
for latitude in [
Moon::default_latitude_convention(),
Mercury::default_latitude_convention(),
Venus::default_latitude_convention(),
Pluto::default_latitude_convention(),
] {
assert_eq!(latitude, LatitudeConvention::Planetocentric);
}
let mercury = SurfaceSite::<Mercury>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).unwrap();
let venus = SurfaceSite::<Venus>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).unwrap();
let pluto = SurfaceSite::<Pluto>::new(0.0 * DEG, 0.0 * DEG, 0.0 * M).unwrap();
assert_eq!(
mercury.body_fixed_cartesian().unwrap()[0].value(),
2_439_700.0
);
assert_eq!(
venus.body_fixed_cartesian().unwrap()[0].value(),
6_051_800.0
);
assert_eq!(
pluto.body_fixed_cartesian().unwrap()[0].value(),
1_188_300.0
);
}
fn assert_near(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() < tol,
"got {actual}, expected {expected} (tol {tol})"
);
}
#[test]
fn unsupported_surface_error_remains_available_for_diagnostics() {
use super::super::error::SurfaceError;
let err = SurfaceError::UnsupportedSurface {
body: "Jupiter",
reason: "no default 1-bar reference surface",
};
assert!(alloc::string::ToString::to_string(&err).contains("Jupiter"));
}
}