use core::f64::consts::PI;
use std::io;
use std::path::Path;
use memmap2::Mmap;
use super::format_1476::CatalogStar;
use crate::error::{ArcsecError, Result};
const RECORD_LEN: usize = 12;
const HEADER_LEN: usize = 4;
pub fn read_001_file(
file_path: &Path,
telescope_ra: f64,
telescope_dec: f64,
field_diameter: f64,
cos_telescope_dec: f64,
max_stars: usize,
) -> Result<Vec<CatalogStar>> {
let file = std::fs::File::open(file_path).map_err(ArcsecError::CatalogIo)?;
let mmap = unsafe { Mmap::map(&file).map_err(ArcsecError::CatalogIo)? };
if mmap.len() < HEADER_LEN {
return Ok(vec![]);
}
let declared = u32::from_le_bytes([mmap[0], mmap[1], mmap[2], mmap[3]]) as usize;
let available = (mmap.len() - HEADER_LEN) / RECORD_LEN;
if declared != available {
return Err(ArcsecError::CatalogIo(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: header declares {declared} stars but the file holds {available}",
file_path.display()
),
)));
}
let half_diam = field_diameter * 0.5;
let body = &mmap[HEADER_LEN..];
let mut stars = Vec::new();
for i in 0..available {
let p = i * RECORD_LEN;
let mag = f32::from_le_bytes([body[p], body[p + 1], body[p + 2], body[p + 3]]);
let ra = f32::from_le_bytes([body[p + 4], body[p + 5], body[p + 6], body[p + 7]]) as f64;
let dec = f32::from_le_bytes([body[p + 8], body[p + 9], body[p + 10], body[p + 11]]) as f64;
let mut delta_ra = (ra - telescope_ra).abs();
if delta_ra > PI {
delta_ra = 2.0 * PI - delta_ra;
}
if delta_ra * cos_telescope_dec < half_diam && (dec - telescope_dec).abs() < half_diam {
stars.push(CatalogStar {
ra,
dec,
mag: mag as f64 / 10.0,
});
if stars.len() >= max_stars {
break;
}
}
}
Ok(stars)
}
#[cfg(test)]
mod tests {
use super::*;
fn synth(stars: &[(f64, f64, f64)]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&(stars.len() as u32).to_le_bytes());
for &(mag, ra, dec) in stars {
buf.extend_from_slice(&((mag * 10.0) as f32).to_le_bytes());
buf.extend_from_slice(&(ra as f32).to_le_bytes());
buf.extend_from_slice(&(dec as f32).to_le_bytes());
}
buf
}
fn write_temp(bytes: &[u8], name: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(name);
std::fs::write(&p, bytes).unwrap();
p
}
#[test]
fn reads_stars_inside_the_field_and_rejects_the_rest() {
let bytes = synth(&[
(-1.5, 1.0, 0.0), (2.0, 1.001, 0.0), (3.0, 2.5, 0.0), (4.0, 1.0, 1.0), ]);
let p = write_temp(&bytes, "arcsec_test_001_basic.001");
let got = read_001_file(&p, 1.0, 0.0, 0.05, 1.0, 100).unwrap();
assert_eq!(got.len(), 2);
assert!(
(got[0].mag - -1.5).abs() < 1e-6,
"mag decoded as {}",
got[0].mag
);
assert!((got[0].ra - 1.0).abs() < 1e-6);
std::fs::remove_file(&p).ok();
}
#[test]
fn respects_max_stars() {
let bytes = synth(&[(1.0, 1.0, 0.0), (2.0, 1.0, 0.0), (3.0, 1.0, 0.0)]);
let p = write_temp(&bytes, "arcsec_test_001_cap.001");
let got = read_001_file(&p, 1.0, 0.0, 0.05, 1.0, 2).unwrap();
assert_eq!(got.len(), 2);
std::fs::remove_file(&p).ok();
}
#[test]
fn ra_wrap_is_handled() {
let bytes = synth(&[(1.0, 2.0 * PI - 0.001, 0.0)]);
let p = write_temp(&bytes, "arcsec_test_001_wrap.001");
let got = read_001_file(&p, 0.0, 0.0, 0.05, 1.0, 10).unwrap();
assert_eq!(got.len(), 1, "star across the RA=0 seam was rejected");
std::fs::remove_file(&p).ok();
}
#[test]
fn truncated_file_is_an_error_not_garbage() {
let mut bytes = synth(&[(1.0, 1.0, 0.0), (2.0, 1.0, 0.0)]);
bytes.truncate(bytes.len() - 5);
let p = write_temp(&bytes, "arcsec_test_001_trunc.001");
assert!(read_001_file(&p, 1.0, 0.0, 0.05, 1.0, 10).is_err());
std::fs::remove_file(&p).ok();
}
}