use std::path::{Path, PathBuf};
use atfits_rs::copy_header_only;
use fitsio::FitsFile;
use ndarray::Array2;
use thiserror::Error;
use crate::beam::Beam;
use crate::smooth::BrightnessUnit;
#[derive(Debug, Error)]
pub enum FitsError {
#[error("FITS I/O error: {0}")]
Fitsio(#[from] fitsio::errors::Error),
#[error("missing keyword: {0}")]
MissingKeyword(String),
#[error("unsupported NAXIS={0} (expected 2 or 4)")]
UnsupportedNaxis(i64),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
impl From<atfits_rs::AtfitsError> for FitsError {
fn from(e: atfits_rs::AtfitsError) -> Self {
use atfits_rs::AtfitsError as A;
match e {
A::Fits(e) => FitsError::Fitsio(e),
A::Io(e) => FitsError::Io(e),
A::MissingKeyword(s) | A::TargetAxisMissing(s) => FitsError::MissingKeyword(s),
A::UnsupportedNaxis(n) => FitsError::UnsupportedNaxis(n),
A::Other(s) => FitsError::Io(std::io::Error::other(s)),
}
}
}
pub struct FitsImageData {
pub path: PathBuf,
pub image: Array2<f32>,
pub is_4d: bool,
pub dx_deg: f64,
pub dy_deg: f64,
pub beam: Beam,
pub unit: BrightnessUnit,
pub header_cards: Vec<(String, String)>,
}
pub fn read_fits(path: &Path) -> Result<FitsImageData, FitsError> {
let path_str = path.to_string_lossy().into_owned();
let mut fptr = FitsFile::open(&path_str)?;
let hdu = fptr.primary_hdu()?;
let naxis: i64 = hdu.read_key(&mut fptr, "NAXIS")?;
let naxis1: i64 = hdu.read_key(&mut fptr, "NAXIS1")?; let naxis2: i64 = hdu.read_key(&mut fptr, "NAXIS2")?;
if naxis != 2 && naxis != 4 {
return Err(FitsError::UnsupportedNaxis(naxis));
}
let nx = naxis1 as usize;
let ny = naxis2 as usize;
let data: Vec<f32> = hdu.read_image(&mut fptr)?;
let image = Array2::from_shape_vec((ny, nx), data)
.map_err(|e| FitsError::Io(std::io::Error::other(e.to_string())))?;
let cdelt1: f64 = hdu.read_key(&mut fptr, "CDELT1")?;
let cdelt2: f64 = hdu.read_key(&mut fptr, "CDELT2")?;
let dx_deg = cdelt1.abs();
let dy_deg = cdelt2.abs();
let bmaj: f64 = hdu
.read_key(&mut fptr, "BMAJ")
.map_err(|_| FitsError::MissingKeyword("BMAJ".into()))?;
let bmin: f64 = hdu.read_key(&mut fptr, "BMIN").unwrap_or(bmaj);
let bpa: f64 = hdu.read_key(&mut fptr, "BPA").unwrap_or(0.0);
let beam = Beam::new(bmaj, bmin, bpa)
.map_err(|e| FitsError::Io(std::io::Error::other(e.to_string())))?;
let unit = match hdu.read_key::<String>(&mut fptr, "BUNIT") {
Ok(s) => BrightnessUnit::from_bunit(&s),
Err(_) => {
tracing::warn!(
"No BUNIT keyword in {}; assuming Jy/beam (flux scaling applied).",
path.display()
);
BrightnessUnit::default()
}
};
Ok(FitsImageData {
path: path.to_path_buf(),
image,
is_4d: naxis == 4,
dx_deg,
dy_deg,
beam,
unit,
header_cards: vec![],
})
}
pub fn write_fits(
image: &Array2<f32>,
out_path: &Path,
template_path: &Path,
new_beam: &Beam,
_was_4d: bool,
) -> Result<(), FitsError> {
copy_header_only(template_path, out_path)?;
let out_str = out_path.to_string_lossy().into_owned();
let mut fptr = FitsFile::edit(&out_str)?;
let hdu = fptr.primary_hdu()?;
let flat: Vec<f32> = image.iter().cloned().collect();
hdu.write_image(&mut fptr, &flat)?;
hdu.write_key(&mut fptr, "BMAJ", new_beam.major_deg)?;
hdu.write_key(&mut fptr, "BMIN", new_beam.minor_deg)?;
hdu.write_key(&mut fptr, "BPA", new_beam.pa_deg)?;
Ok(())
}
pub use atfits_rs::output_path;