use std::io::Read;
use std::sync::OnceLock;
pub const EMBED_MAX_DEGREE: usize = 70;
pub const EMBEDDED_FILES: [&str; 7] = [
"tab5.2a.txt",
"tab5.2b.txt",
"tab5.2d.txt",
"EGM96.gfc",
"ITU_GRACE16.gfc",
"JGM2.gfc",
"JGM3.gfc",
];
pub const SOURCES_JSON: &str = include_str!("../../data/embedded/SOURCES.json");
fn compressed(name: &str) -> Option<&'static [u8]> {
Some(match name {
"tab5.2a.txt" => include_bytes!("../../data/embedded/tab5.2a.txt.gz"),
"tab5.2b.txt" => include_bytes!("../../data/embedded/tab5.2b.txt.gz"),
"tab5.2d.txt" => include_bytes!("../../data/embedded/tab5.2d.txt.gz"),
"EGM96.gfc" => include_bytes!("../../data/embedded/EGM96.gfc.gz"),
"ITU_GRACE16.gfc" => include_bytes!("../../data/embedded/ITU_GRACE16.gfc.gz"),
"JGM2.gfc" => include_bytes!("../../data/embedded/JGM2.gfc.gz"),
"JGM3.gfc" => include_bytes!("../../data/embedded/JGM3.gfc.gz"),
_ => return None,
})
}
pub fn has(name: &str) -> bool {
compressed(name).is_some()
}
pub fn compressed_size() -> usize {
EMBEDDED_FILES
.iter()
.filter_map(|n| compressed(n))
.map(|b| b.len())
.sum()
}
pub fn get(name: &str) -> Option<Vec<u8>> {
let gz = compressed(name)?;
let mut out = Vec::with_capacity(gz.len() * 4);
flate2::read::GzDecoder::new(gz)
.read_to_end(&mut out)
.expect("embedded data blob is not valid gzip");
note_once(name);
Some(out)
}
fn note_once(name: &str) {
static NOTED: OnceLock<()> = OnceLock::new();
NOTED.get_or_init(|| {
if std::env::var_os("SATKIT_QUIET").is_none() {
eprintln!(
"satkit: using the compiled-in copy of {name} (no copy found in the data \
directory); frames and gravity models to degree {EMBED_MAX_DEGREE} work \
without any data files. Run satkit::utils::update_datafiles() to install \
the full files if you need them."
);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
fn sha256_hex(b: &[u8]) -> String {
super::super::manifest::sha256_hex(b)
}
#[test]
fn offline_embedded_blobs_match_sources_json() {
let sources: serde_json::Value = serde_json::from_str(SOURCES_JSON).unwrap();
assert_eq!(sources["embed_max_degree"], EMBED_MAX_DEGREE as u64);
for name in EMBEDDED_FILES {
let bytes = get(name).expect(name);
let rec = &sources["files"][name];
assert_eq!(
rec["embedded_size"].as_u64().unwrap() as usize,
bytes.len(),
"{name}"
);
assert_eq!(
rec["embedded_sha256"].as_str().unwrap(),
sha256_hex(&bytes),
"{name}"
);
}
assert!(get("nonexistent").is_none());
assert!(
compressed_size() < 1_500_000,
"embedded data budget exceeded"
);
}
#[test]
fn offline_embedded_gravity_files_parse() {
for name in ["EGM96.gfc", "ITU_GRACE16.gfc", "JGM2.gfc", "JGM3.gfc"] {
let bytes = get(name).unwrap();
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("end_of_head"), "{name}: header terminator");
let max_n = text
.lines()
.filter(|l| l.starts_with("gfc "))
.filter_map(|l| l.split_whitespace().nth(1)?.parse::<usize>().ok())
.max()
.unwrap();
assert_eq!(max_n, EMBED_MAX_DEGREE, "{name}");
let g = crate::earthgravity::Gravity::from_bytes(&bytes).expect(name);
assert!(g.max_degree >= 40, "{name}: parsed max degree");
}
}
#[test]
fn offline_embedded_iers_tables_parse() {
use crate::frametransform::ierstable::IERSTable;
for name in ["tab5.2a.txt", "tab5.2b.txt", "tab5.2d.txt"] {
let bytes = get(name).unwrap();
IERSTable::from_bytes(&bytes).expect(name);
}
}
#[test]
fn embedded_gravity_matches_full_file_to_degree_40() {
use crate::earthgravity::Gravity;
let mut compared = 0;
for name in ["EGM96.gfc", "ITU_GRACE16.gfc", "JGM2.gfc", "JGM3.gfc"] {
let Some(full_path) = crate::utils::find_data_file(name) else {
continue;
};
let full = Gravity::from_path(&full_path).unwrap();
let emb = Gravity::from_bytes(&get(name).unwrap()).unwrap();
for (x, y, z) in [
(6.778e6, 0.0, 0.0),
(2.0e6, 5.0e6, 4.0e6),
(-1.0e6, -3.0e6, 6.5e6),
(4.2164e7, 0.0, 1.0e5),
] {
let p = numeris::vector![x, y, z];
let a = full.accel(&p, 40, 40);
let b = emb.accel(&p, 40, 40);
assert_eq!(a.as_slice(), b.as_slice(), "{name} at {p:?}");
}
compared += 1;
}
eprintln!("compared {compared} gravity models against full files");
}
}