use super::{SpiceContext, SpiceContextError};
use crate::bodies::solar_system::Moon;
use crate::coordinates::surface::{LatitudeConvention, ReferenceSurface, SurfaceSite};
use crate::coordinates::transform::HorizontalObservation;
use crate::formats::spice::{FrameClass, SpiceError};
use crate::qtty::{Degrees, Kilometer, Meters, Quantity};
use tempoch::{Time, TDB};
#[derive(Debug, Clone, Copy)]
pub enum LunarTarget {
Sun,
Earth,
}
pub struct LunarObserver<'a> {
context: &'a SpiceContext,
frame: i32,
site: SurfaceSite<Moon>,
}
pub struct LunarObservation<'a> {
pub horizontal: HorizontalObservation<Moon, Kilometer>,
pub epoch: Time<TDB>,
pub frame_id: i32,
pub target: LunarTarget,
pub kernels: Vec<&'a str>,
pub target_fixed: [Quantity<Kilometer>; 3],
pub relative_fixed: [Quantity<Kilometer>; 3],
}
impl LunarObservation<'_> {
pub fn above_horizon(&self) -> bool {
self.horizontal.alt().value() > 0.0
}
}
impl<'a> LunarObserver<'a> {
pub fn new(
context: &'a SpiceContext,
frame: &str,
site: SurfaceSite<Moon>,
) -> Result<Self, SpiceContextError> {
let id = context.frame_registry().frame_by_name(frame)?.id;
let mut current = id;
for _ in 0..64 {
let spec = context.frame_registry().frame_by_id(current)?;
if spec.center_id != 301 {
return Err(invalid("lunar observer frame must be Moon-centred"));
}
if let Some((_, relative)) = &spec.tk_spec {
current = context.frame_registry().frame_by_name(relative)?.id;
} else if spec.class == FrameClass::Pck {
if !context
.kernel_set()
.binary_pcks()
.any(|(_, b)| b.contains_class(spec.class_id))
{
return Err(invalid(
"lunar observer requires binary PCK data for selected frame",
));
}
if context.kernel_set().binary_pcks().any(|(_, b)| {
b.contains_class(spec.class_id) && !b.has_j2000_base(spec.class_id)
}) {
return Err(invalid(
"lunar observer supports J2000-based binary PCK only",
));
}
return Ok(Self {
context,
frame: id,
site,
});
} else {
return Err(invalid("lunar frame chain must end in binary PCK"));
}
}
Err(invalid("cyclic or excessively deep lunar frame graph"))
}
pub fn radial(
context: &'a SpiceContext,
frame: &str,
lon: Degrees,
lat: Degrees,
radius: Meters,
) -> Result<Self, SpiceContextError> {
let site = SurfaceSite::with_surface(
lon,
lat,
Meters::new(0.0),
ReferenceSurface::sphere(radius)?,
LatitudeConvention::Planetocentric,
)?;
Self::new(context, frame, site)
}
pub fn site(&self) -> &SurfaceSite<Moon> {
&self.site
}
pub fn observe(
&self,
target: LunarTarget,
epoch: Time<TDB>,
) -> Result<LunarObservation<'a>, SpiceContextError> {
let et = epoch.to_j2000s().raw().value();
let matrix = self.context.rotation_naif(1, self.frame, et)?;
let state = self.context.state_naif(
match target {
LunarTarget::Sun => 10,
LunarTarget::Earth => 399,
},
301,
et,
)?;
let target_fixed = apply(matrix, [state[0], state[1], state[2]]);
let observer = self
.site
.body_fixed_cartesian()?
.map(|q| q.to::<Kilometer>().value());
let relative = core::array::from_fn(|i| target_fixed[i] - observer[i]);
let horizontal = HorizontalObservation::from_fixed_relative(
&self.site,
relative.map(Quantity::<Kilometer>::new),
)?;
Ok(LunarObservation {
horizontal,
epoch,
frame_id: self.frame,
target,
kernels: self.context.kernel_set().provenance(),
target_fixed: target_fixed.map(Quantity::new),
relative_fixed: relative.map(Quantity::new),
})
}
}
fn invalid(message: &str) -> SpiceContextError {
SpiceError::Corrupted {
message: message.into(),
}
.into()
}
fn apply(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
m.map(|r| r[0] * v[0] + r[1] * v[1] + r[2] * v[2])
}