use std::collections::HashMap;
use super::text::{TextKernel, TextValue};
use super::SpiceError;
#[derive(Debug, Clone)]
pub struct BodyOrientation {
pub pole_ra: Vec<f64>,
pub pole_dec: Vec<f64>,
pub pm: Vec<f64>,
pub radii: Option<[f64; 3]>,
}
impl BodyOrientation {
pub fn rotation_to_j2000(&self, tdb_s: f64) -> [[f64; 3]; 3] {
let t = tdb_s / (36_525.0 * 86_400.0);
let d = tdb_s / 86_400.0;
let ra_deg = self.pole_ra[0] + self.pole_ra.get(1).copied().unwrap_or(0.0) * t;
let dec_deg = self.pole_dec[0] + self.pole_dec.get(1).copied().unwrap_or(0.0) * t;
let w_deg = self.pm[0] + self.pm.get(1).copied().unwrap_or(0.0) * d;
let ra = ra_deg.to_radians();
let dec = dec_deg.to_radians();
let w = w_deg.to_radians();
mat3_mul(
mat3_mul(
r3(std::f64::consts::FRAC_PI_2 + ra),
r1(std::f64::consts::FRAC_PI_2 - dec),
),
r3(-w),
)
}
}
#[derive(Debug, Default, Clone)]
pub struct PckKernel {
pub bodies: HashMap<i32, BodyOrientation>,
}
impl PckKernel {
pub fn from_text(src: &str) -> Result<Self, SpiceError> {
let kernel = TextKernel::parse(src)?;
let mut bodies: HashMap<i32, BodyOrientation> = HashMap::new();
for (key, value) in &kernel.data {
let Some(rest) = key.strip_prefix("BODY") else {
continue;
};
let Some((id_text, suffix)) = rest.split_once('_') else {
continue;
};
let Ok(id) = id_text.parse::<i32>() else {
continue;
};
let entry = bodies.entry(id).or_insert_with(|| BodyOrientation {
pole_ra: Vec::new(),
pole_dec: Vec::new(),
pm: Vec::new(),
radii: None,
});
match suffix {
"POLE_RA" => entry.pole_ra = value_as_f64_array(value, key)?,
"POLE_DEC" => entry.pole_dec = value_as_f64_array(value, key)?,
"PM" => entry.pm = value_as_f64_array(value, key)?,
"RADII" => entry.radii = Some(parse_radii(value, key)?),
_ => {}
}
}
Ok(Self { bodies })
}
pub fn body(&self, id: i32) -> Option<&BodyOrientation> {
self.bodies.get(&id)
}
}
fn value_as_f64_array(value: &TextValue, key: &str) -> Result<Vec<f64>, SpiceError> {
match value {
TextValue::Integer(number) => Ok(vec![*number as f64]),
TextValue::Float(number) => Ok(vec![*number]),
TextValue::Array(values) => values
.iter()
.map(|entry| match entry {
TextValue::Integer(number) => Ok(*number as f64),
TextValue::Float(number) => Ok(*number),
other => Err(SpiceError::FormatParse(format!(
"{key} must contain only numeric entries, got {other:?}"
))),
})
.collect(),
other => Err(SpiceError::FormatParse(format!(
"{key} must be numeric, got {other:?}"
))),
}
}
fn parse_radii(value: &TextValue, key: &str) -> Result<[f64; 3], SpiceError> {
let values = value_as_f64_array(value, key)?;
match values.as_slice() {
[equatorial, polar] => Ok([*equatorial, *equatorial, *polar]),
[x, y, z] => Ok([*x, *y, *z]),
_ => Err(SpiceError::FormatParse(format!(
"{key} must contain 2 or 3 radii values"
))),
}
}
fn r1(angle: f64) -> [[f64; 3]; 3] {
let (sin, cos) = angle.sin_cos();
[[1.0, 0.0, 0.0], [0.0, cos, sin], [0.0, -sin, cos]]
}
fn r3(angle: f64) -> [[f64; 3]; 3] {
let (sin, cos) = angle.sin_cos();
[[cos, sin, 0.0], [-sin, cos, 0.0], [0.0, 0.0, 1.0]]
}
fn mat3_mul(left: [[f64; 3]; 3], right: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut out = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
out[i][j] += left[i][k] * right[k][j];
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::PckKernel;
#[test]
fn parse_minimal_pck_with_earth_data() {
let src = "\\begindata\nBODY399_POLE_RA = ( 0.0 0.0 )\nBODY399_POLE_DEC = ( 90.0 0.0 )\nBODY399_PM = ( 190.0 360.9856235 )\nBODY399_RADII = ( 6378.137 6378.137 6356.752 )\n";
let kernel = PckKernel::from_text(src).unwrap();
assert!(kernel.body(399).is_some());
}
#[test]
fn rotation_is_orthonormal_at_j2000() {
let src = "\\begindata\nBODY399_POLE_RA = ( 0.0 0.0 )\nBODY399_POLE_DEC = ( 90.0 0.0 )\nBODY399_PM = ( 0.0 1.0 )\n";
let kernel = PckKernel::from_text(src).unwrap();
let matrix = kernel.body(399).unwrap().rotation_to_j2000(0.0);
let det = matrix[0][0] * (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1])
- matrix[0][1] * (matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0])
+ matrix[0][2] * (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]);
let row0_norm = (matrix[0][0].powi(2) + matrix[0][1].powi(2) + matrix[0][2].powi(2)).sqrt();
assert!((row0_norm - 1.0).abs() < 1.0e-12);
assert!((det - 1.0).abs() < 1.0e-12);
}
#[test]
fn body_lookup_returns_correct_entry() {
let src = "\\begindata\nBODY301_POLE_RA = ( 269.9949 0.0031 )\nBODY301_POLE_DEC = ( 66.5392 0.0130 )\nBODY301_PM = ( 38.3213 13.17635815 )\n";
let kernel = PckKernel::from_text(src).unwrap();
assert_eq!(kernel.body(301).unwrap().pole_ra[0], 269.9949);
}
}