use super::text::{TextKernel, TextValue};
use super::SpiceError;
#[derive(Debug, Clone)]
pub enum FovShape {
Circle,
Rectangle,
Ellipse,
Polygon,
Other(String),
}
#[derive(Debug, Clone)]
pub struct InstrumentKernel {
pub inst_id: i32,
pub fov_shape: Option<FovShape>,
pub fov_frame: Option<String>,
pub boresight: Option<[f64; 3]>,
pub fov_ref_angle: Option<f64>,
pub fov_cross_angle: Option<f64>,
}
#[derive(Debug, Default, Clone)]
pub struct IkKernel {
pub instruments: Vec<InstrumentKernel>,
}
impl IkKernel {
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() {
let Some(rest) = key.strip_prefix("INS") else {
continue;
};
let Some((id_text, _suffix)) = rest.split_once('_') else {
continue;
};
let Ok(id) = id_text.parse::<i32>() else {
continue;
};
ids.push(id);
}
ids.sort_unstable();
ids.dedup();
let mut instruments = Vec::with_capacity(ids.len());
for inst_id in ids {
let shape = kernel
.get(&format!("INS{inst_id}_FOV_SHAPE"))
.map(|value| parse_shape(value, inst_id))
.transpose()?;
let frame = kernel
.get(&format!("INS{inst_id}_FOV_FRAME"))
.map(|value| {
text_value(value, &format!("INS{inst_id}_FOV_FRAME"))
.map(|text| text.to_ascii_uppercase())
})
.transpose()?;
let boresight = kernel
.get_f64_array(&format!("INS{inst_id}_BORESIGHT"))
.map(|values| {
if values.len() == 3 {
Ok([values[0], values[1], values[2]])
} else {
Err(SpiceError::FormatParse(format!(
"INS{inst_id}_BORESIGHT must contain 3 values"
)))
}
})
.transpose()?;
let fov_ref_angle = kernel
.get(&format!("INS{inst_id}_FOV_REF_ANGLE"))
.map(|value| scalar_value(value, &format!("INS{inst_id}_FOV_REF_ANGLE")))
.transpose()?;
let fov_cross_angle = kernel
.get(&format!("INS{inst_id}_FOV_CROSS_ANGLE"))
.map(|value| scalar_value(value, &format!("INS{inst_id}_FOV_CROSS_ANGLE")))
.transpose()?;
instruments.push(InstrumentKernel {
inst_id,
fov_shape: shape,
fov_frame: frame,
boresight,
fov_ref_angle,
fov_cross_angle,
});
}
Ok(Self { instruments })
}
pub fn instrument(&self, id: i32) -> Option<&InstrumentKernel> {
self.instruments
.iter()
.find(|instrument| instrument.inst_id == id)
}
}
fn parse_shape(value: &TextValue, inst_id: i32) -> Result<FovShape, SpiceError> {
let text = text_value(value, &format!("INS{inst_id}_FOV_SHAPE"))?.to_ascii_uppercase();
Ok(match text.as_str() {
"CIRCLE" => FovShape::Circle,
"RECTANGLE" => FovShape::Rectangle,
"ELLIPSE" => FovShape::Ellipse,
"POLYGON" => FovShape::Polygon,
other => FovShape::Other(other.to_string()),
})
}
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:?}"
))),
}
}
fn scalar_value(value: &TextValue, key: &str) -> Result<f64, SpiceError> {
match value {
TextValue::Integer(number) => Ok(*number as f64),
TextValue::Float(number) => Ok(*number),
other => Err(SpiceError::FormatParse(format!(
"{key} must be numeric, got {other:?}"
))),
}
}
#[cfg(test)]
mod tests {
use super::{FovShape, IkKernel};
#[test]
fn parse_minimal_ik_kernel() {
let src = "\\begindata\nINS-82000_FOV_SHAPE = 'RECTANGLE'\nINS-82000_FOV_FRAME = 'CASSINI_ISS_NAC'\nINS-82000_BORESIGHT = ( 0 0 1 )\nINS-82000_FOV_REF_ANGLE = 0.35\nINS-82000_FOV_CROSS_ANGLE = 0.35\n";
let kernel = IkKernel::from_text(src).unwrap();
let instrument = kernel.instrument(-82_000).unwrap();
assert!(matches!(instrument.fov_shape, Some(FovShape::Rectangle)));
assert_eq!(instrument.fov_frame.as_deref(), Some("CASSINI_ISS_NAC"));
assert_eq!(instrument.boresight, Some([0.0, 0.0, 1.0]));
}
}