use crate::constants::{AU_KM, DAY_S};
use crate::jplephem::errors::{JplephemError, Result};
use crate::jplephem::kernel::SpiceKernel;
use crate::jplephem::spk::{jd_to_seconds, SUPPORTED_DATA_TYPES};
use crate::positions::Position;
use crate::time::Time;
impl Position {
pub fn from_spk_target(kernel: &mut SpiceKernel, target_id: i32, t: &Time) -> Result<Position> {
let chain = match kernel.get(&target_id.to_string()) {
Ok(vf) => vf.chain,
Err(err) => return Err(unsupported_data_type(kernel, target_id).unwrap_or(err)),
};
let (position_km, velocity_km_s) =
kernel.compute_chain_pub(&chain, jd_to_seconds(t.tdb()))?;
Ok(Position::barycentric(
position_km / AU_KM,
velocity_km_s * DAY_S / AU_KM,
target_id,
))
}
}
fn unsupported_data_type(kernel: &SpiceKernel, target_id: i32) -> Option<JplephemError> {
let daf = &kernel.spk().daf;
let summaries = daf.summaries().ok()?;
for (_, values) in summaries {
if values.len() < (daf.nd + daf.ni) as usize {
continue;
}
let target = values[2] as i32;
let data_type = values[5] as i32;
if target == target_id && !SUPPORTED_DATA_TYPES.contains(&data_type) {
return Some(JplephemError::UnsupportedDataType(data_type));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jplephem_ext::SpiceKernelExt;
use crate::time::Timescale;
fn de421_kernel() -> SpiceKernel {
SpiceKernel::open("test_data/de421.bsp").expect("Failed to open DE421")
}
fn synthetic_spk(target: i32, center: i32, data_type: i32) -> Vec<u8> {
const RECORD_SIZE: usize = 1024;
let start_i = 3 * RECORD_SIZE / 8 + 1;
let end_i = start_i + 7;
let mut file = vec![0u8; 3 * RECORD_SIZE + 64];
file[0..8].copy_from_slice(b"DAF/SPK ");
file[8..12].copy_from_slice(&2u32.to_le_bytes()); file[12..16].copy_from_slice(&6u32.to_le_bytes()); file[16..76].copy_from_slice(format!("{:<60}", "SYNTHETIC").as_bytes());
file[76..80].copy_from_slice(&2u32.to_le_bytes()); file[80..84].copy_from_slice(&2u32.to_le_bytes()); file[84..88].copy_from_slice(&((end_i + 1) as u32).to_le_bytes());
let summary = RECORD_SIZE;
let mut put = |at: usize, value: f64| {
file[at..at + 8].copy_from_slice(&value.to_le_bytes());
};
put(summary, 0.0); put(summary + 8, 0.0); put(summary + 16, 1.0); put(summary + 24, -1.0e9); put(summary + 32, 1.0e9);
let ints = [target, center, 1, data_type, start_i as i32, end_i as i32];
for (i, value) in ints.iter().enumerate() {
let at = summary + 40 + (i / 2) * 8 + (i % 2) * 4;
file[at..at + 4].copy_from_slice(&value.to_le_bytes());
}
file[2 * RECORD_SIZE..2 * RECORD_SIZE + 40]
.copy_from_slice(format!("{:<40}", "SYNTHETIC SPK").as_bytes());
file
}
#[test]
fn test_from_spk_target_earth_matches_named_lookup() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let by_name = kernel.at("earth", &t).unwrap();
let by_id = Position::from_spk_target(&mut kernel, 399, &t).unwrap();
assert_eq!(by_id.target, 399);
assert_eq!(by_id.center, 0);
assert_eq!(by_id.kind, crate::positions::PositionKind::Barycentric);
assert!((by_id.position - by_name.position).norm() < 1e-15);
assert!((by_id.velocity - by_name.velocity).norm() < 1e-15);
}
#[test]
fn test_from_spk_target_moon_matches_named_lookup() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let by_name = kernel.at("moon", &t).unwrap();
let by_id = Position::from_spk_target(&mut kernel, 301, &t).unwrap();
assert!((by_id.position - by_name.position).norm() < 1e-15);
assert!((by_id.velocity - by_name.velocity).norm() < 1e-15);
}
#[test]
fn test_from_spk_target_missing_body() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let err = Position::from_spk_target(&mut kernel, -74, &t).unwrap_err();
assert!(
matches!(err, JplephemError::Other(ref m) if m.contains("No path from SSB")),
"unexpected error: {err}"
);
}
#[test]
fn test_from_spk_target_unsupported_data_type() {
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let bytes = synthetic_spk(-74, 4, 1);
let mut kernel = SpiceKernel::from_bytes(&bytes).unwrap();
let err = Position::from_spk_target(&mut kernel, -74, &t).unwrap_err();
assert!(
matches!(err, JplephemError::UnsupportedDataType(1)),
"unexpected error: {err}"
);
let bytes = synthetic_spk(-74, 4, 13);
let mut kernel = SpiceKernel::from_bytes(&bytes).unwrap();
let err = Position::from_spk_target(&mut kernel, -74, &t).unwrap_err();
assert!(
matches!(err, JplephemError::UnsupportedDataType(13)),
"unexpected error: {err}"
);
}
#[test]
fn test_from_spk_target_observes_and_apparent() {
let mut kernel = de421_kernel();
let ts = Timescale::default();
let t = ts.tdb_jd(2451545.0);
let mars = Position::from_spk_target(&mut kernel, 499, &t).unwrap();
let earth = mars
.observe("earth", &mut kernel, &t)
.unwrap()
.apparent(&mut kernel, &t)
.unwrap();
assert_eq!(earth.target, 399);
assert!(earth.observer_barycentric.is_some());
assert!(earth.distance() > 0.3 && earth.distance() < 2.7);
}
}