use super::text::{TextKernel, TextValue};
use super::SpiceError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameClass {
Inertial = 1,
Pck = 2,
Ck = 3,
TwoVector = 4,
Dynamic = 5,
}
#[derive(Debug, Clone)]
pub enum TkSpec {
Matrix([[f64; 3]; 3]),
}
#[derive(Debug, Clone)]
pub struct FrameSpec {
pub id: i32,
pub name: String,
pub class: FrameClass,
pub class_id: i32,
pub center_id: i32,
pub tk_spec: Option<(TkSpec, String)>,
}
#[derive(Debug, Clone, Default)]
pub struct FrameKernel {
pub frames: Vec<FrameSpec>,
}
impl FrameKernel {
pub fn from_text(src: &str) -> Result<Self, SpiceError> {
let kernel = TextKernel::parse(src)?;
let mut ids = Vec::new();
for key in kernel.data.keys() {
if let Some(id) = key
.strip_prefix("FRAME_")
.and_then(|rest| rest.strip_suffix("_NAME"))
.and_then(|rest| rest.parse::<i32>().ok())
{
ids.push(id);
}
}
ids.sort_unstable();
ids.dedup();
let mut frames = Vec::with_capacity(ids.len());
for id in ids {
let name = text_value(
kernel.get(&format!("FRAME_{id}_NAME")).ok_or_else(|| {
SpiceError::FormatParse(format!("FK missing FRAME_{id}_NAME"))
})?,
&format!("FRAME_{id}_NAME"),
)?
.to_ascii_uppercase();
let class = parse_frame_class(int_value(
kernel.get(&format!("FRAME_{id}_CLASS")).ok_or_else(|| {
SpiceError::FormatParse(format!("FK missing FRAME_{id}_CLASS"))
})?,
&format!("FRAME_{id}_CLASS"),
)?)?;
let class_id = int_value(
kernel.get(&format!("FRAME_{id}_CLASS_ID")).ok_or_else(|| {
SpiceError::FormatParse(format!("FK missing FRAME_{id}_CLASS_ID"))
})?,
&format!("FRAME_{id}_CLASS_ID"),
)?;
let center_id = int_value(
kernel.get(&format!("FRAME_{id}_CENTER")).ok_or_else(|| {
SpiceError::FormatParse(format!("FK missing FRAME_{id}_CENTER"))
})?,
&format!("FRAME_{id}_CENTER"),
)?;
let tk_spec = if class == FrameClass::TwoVector {
let spec_key = format!("TKFRAME_{id}_SPEC");
let spec = kernel
.get(&spec_key)
.map(|value| text_value(value, &spec_key))
.transpose()?;
match spec {
Some("MATRIX") => {
let matrix_key = format!("TKFRAME_{id}_MATRIX");
let relative_key = format!("TKFRAME_{id}_RELATIVE");
let values = kernel.get_f64_array(&matrix_key).ok_or_else(|| {
SpiceError::FormatParse(format!("FK missing or invalid {matrix_key}"))
})?;
if values.len() != 9 {
return Err(SpiceError::FormatParse(format!(
"{matrix_key} must contain 9 values"
)));
}
let relative = text_value(
kernel.get(&relative_key).ok_or_else(|| {
SpiceError::FormatParse(format!("FK missing {relative_key}"))
})?,
&relative_key,
)?
.to_ascii_uppercase();
Some((
TkSpec::Matrix([
[values[0], values[1], values[2]],
[values[3], values[4], values[5]],
[values[6], values[7], values[8]],
]),
relative,
))
}
Some(other) => {
return Err(SpiceError::FormatParse(format!(
"unsupported TKFRAME spec '{other}' for frame {id}"
)));
}
None => None,
}
} else {
None
};
frames.push(FrameSpec {
id,
name,
class,
class_id,
center_id,
tk_spec,
});
}
Ok(Self { frames })
}
pub fn frame_by_id(&self, id: i32) -> Option<&FrameSpec> {
self.frames.iter().find(|frame| frame.id == id)
}
pub fn frame_by_name(&self, name: &str) -> Option<&FrameSpec> {
let upper = name.to_ascii_uppercase();
self.frames.iter().find(|frame| frame.name == upper)
}
}
fn parse_frame_class(value: i32) -> Result<FrameClass, SpiceError> {
match value {
1 => Ok(FrameClass::Inertial),
2 => Ok(FrameClass::Pck),
3 => Ok(FrameClass::Ck),
4 => Ok(FrameClass::TwoVector),
5 => Ok(FrameClass::Dynamic),
other => Err(SpiceError::FormatParse(format!(
"unknown frame class {other}"
))),
}
}
fn int_value(value: &TextValue, key: &str) -> Result<i32, SpiceError> {
match value {
TextValue::Integer(number) => i32::try_from(*number).map_err(|_| {
SpiceError::FormatParse(format!("{key} integer {number} does not fit in i32"))
}),
TextValue::Float(number) => Ok(*number as i32),
other => Err(SpiceError::FormatParse(format!(
"{key} must be numeric, got {other:?}"
))),
}
}
fn text_value<'a>(value: &'a TextValue, key: &str) -> Result<&'a str, SpiceError> {
match value {
TextValue::Text(text) => Ok(text.as_str()),
other => Err(SpiceError::FormatParse(format!(
"{key} must be text, got {other:?}"
))),
}
}
#[cfg(test)]
mod tests {
use super::{FrameClass, FrameKernel, TkSpec};
#[test]
fn parse_minimal_inertial_frame() {
let src = "\\begindata\nFRAME_1234_NAME = 'TEST'\nFRAME_1234_CLASS = 1\nFRAME_1234_CLASS_ID = 1234\nFRAME_1234_CENTER = 0\n";
let kernel = FrameKernel::from_text(src).unwrap();
let frame = kernel.frame_by_id(1234).unwrap();
assert_eq!(frame.name, "TEST");
assert_eq!(frame.class, FrameClass::Inertial);
}
#[test]
fn parse_tk_matrix_frame() {
let src = "\\begindata\nFRAME_2000_NAME = 'TEST_TK'\nFRAME_2000_CLASS = 4\nFRAME_2000_CLASS_ID = 2000\nFRAME_2000_CENTER = 399\nTKFRAME_2000_SPEC = 'MATRIX'\nTKFRAME_2000_RELATIVE = 'J2000'\nTKFRAME_2000_MATRIX = ( 1 0 0 0 1 0 0 0 1 )\n";
let kernel = FrameKernel::from_text(src).unwrap();
let frame = kernel.frame_by_name("test_tk").unwrap();
match frame.tk_spec.as_ref().unwrap().0 {
TkSpec::Matrix(matrix) => assert_eq!(matrix[0][0], 1.0),
}
}
#[test]
fn frame_lookup_by_id_and_name() {
let src = "\\begindata\nFRAME_1234_NAME = 'TEST'\nFRAME_1234_CLASS = 1\nFRAME_1234_CLASS_ID = 1234\nFRAME_1234_CENTER = 0\n";
let kernel = FrameKernel::from_text(src).unwrap();
assert_eq!(kernel.frame_by_id(1234).unwrap().name, "TEST");
assert_eq!(kernel.frame_by_name("test").unwrap().id, 1234);
}
}