use std::path::Path;
use fitsio::FitsFile;
use crate::error::{AtfitsError, Result};
use crate::keys::{key_exists, key_f64, key_i64, key_string};
use crate::path::utf8;
#[derive(Debug, Clone)]
pub struct HeaderGeom {
pub naxis: usize,
pub dims: Vec<usize>,
pub bitpix: i64,
}
impl HeaderGeom {
pub fn read(path: &Path) -> Result<Self> {
let mut fptr = FitsFile::open(utf8(path)?)?;
fptr.primary_hdu()?;
Self::from_file(&mut fptr)
}
pub fn from_file(fptr: &mut FitsFile) -> Result<Self> {
let naxis = required_i64(fptr, "NAXIS")?;
let bitpix = required_i64(fptr, "BITPIX")?;
let dims = (1..=naxis)
.map(|i| required_i64(fptr, &format!("NAXIS{i}")).map(|n| n as usize))
.collect::<Result<_>>()?;
Ok(Self {
naxis: naxis as usize,
dims,
bitpix,
})
}
pub fn is_2d(&self) -> bool {
self.naxis == 2
}
pub fn plane_len(&self) -> usize {
self.dims.iter().take(2).product()
}
}
fn required_i64(fptr: &mut FitsFile, key: &str) -> Result<i64> {
key_i64(fptr, key)?.ok_or_else(|| AtfitsError::MissingKeyword(key.to_string()))
}
#[derive(Debug, Clone)]
pub struct TargetAxis {
pub fits_idx: usize,
pub array_idx: usize,
pub ctype: String,
pub crpix: f64,
pub crval: f64,
pub cdelt: f64,
pub cunit: Option<String>,
}
impl TargetAxis {
pub fn world(&self, pix: f64) -> f64 {
self.crval + (pix - self.crpix) * self.cdelt
}
pub fn unit_scale(&self) -> Option<f64> {
let unit = self.cunit.as_deref();
if self.ctype.contains("FREQ") {
frequency_unit_scale(unit)
} else if self.ctype.contains("TIME") {
time_unit_scale(unit)
} else {
None
}
}
}
pub fn find_target_axis(path: &Path, name: &str) -> Result<TargetAxis> {
let mut fptr = FitsFile::open(utf8(path)?)?;
fptr.primary_hdu()?;
find_target_axis_in(&mut fptr, name)
}
pub fn find_target_axis_in(fptr: &mut FitsFile, name: &str) -> Result<TargetAxis> {
let naxis = required_i64(fptr, "NAXIS")? as usize;
for axis in 1..=naxis {
let Some(ctype) = key_string(fptr, &format!("CTYPE{axis}"))? else {
continue;
};
if !ctype.contains(name) {
continue;
}
let cdelt = match key_f64(fptr, &format!("CD{axis}_{axis}"))? {
Some(cd) => cd,
None => {
key_f64(fptr, &format!("CDELT{axis}"))?.unwrap_or(1.0)
* key_f64(fptr, &format!("PC{axis}_{axis}"))?.unwrap_or(1.0)
}
};
return Ok(TargetAxis {
fits_idx: axis,
array_idx: naxis - axis,
crpix: key_f64(fptr, &format!("CRPIX{axis}"))?.unwrap_or(0.0),
crval: key_f64(fptr, &format!("CRVAL{axis}"))?.unwrap_or(0.0),
cdelt,
cunit: key_string(fptr, &format!("CUNIT{axis}"))?,
ctype,
});
}
Err(AtfitsError::TargetAxisMissing(format!(
"No {name} axis found in WCS of {}",
fptr.file_path().display()
)))
}
pub fn spectral_unit_scale(unit: Option<&str>) -> Option<f64> {
frequency_unit_scale(unit).or_else(|| time_unit_scale(unit))
}
pub fn frequency_unit_scale(unit: Option<&str>) -> Option<f64> {
unit_scale(
unit,
&[
("hz", 1.0),
("khz", 1e3),
("mhz", 1e6),
("ghz", 1e9),
("thz", 1e12),
],
)
}
pub fn time_unit_scale(unit: Option<&str>) -> Option<f64> {
unit_scale(
unit,
&[
("s", 1.0),
("ms", 1e-3),
("us", 1e-6),
("min", 60.0),
("h", 3600.0),
("d", 86400.0),
],
)
}
fn unit_scale(unit: Option<&str>, table: &[(&str, f64)]) -> Option<f64> {
let unit = unit.unwrap_or("").trim().to_ascii_lowercase();
if unit.is_empty() {
return Some(1.0);
}
table.iter().find(|(u, _)| *u == unit).map(|&(_, f)| f)
}
pub fn pixel_scale_deg(fptr: &mut FitsFile) -> Result<(f64, f64)> {
let mut has_cd = false;
for key in ["CD1_1", "CD1_2", "CD2_1", "CD2_2"] {
has_cd |= key_exists(fptr, key)?;
}
let mut m = [[0.0; 2]; 2];
for i in 1..=2 {
let row_scale = if has_cd {
1.0
} else {
key_f64(fptr, &format!("CDELT{i}"))?
.ok_or_else(|| AtfitsError::MissingKeyword(format!("CDELT{i} or CD{i}_{i}")))?
};
let unit = key_string(fptr, &format!("CUNIT{i}"))?;
let deg = angle_unit_deg(unit.as_deref())
.ok_or_else(|| AtfitsError::Other(format!("unknown angle unit CUNIT{i}={unit:?}")))?;
for j in 1..=2 {
let v = if has_cd {
key_f64(fptr, &format!("CD{i}_{j}"))?.unwrap_or(0.0)
} else {
let diag = if i == j { 1.0 } else { 0.0 };
key_f64(fptr, &format!("PC{i}_{j}"))?.unwrap_or(diag)
};
m[i - 1][j - 1] = v * row_scale * deg;
}
}
Ok((m[0][0].hypot(m[1][0]), m[0][1].hypot(m[1][1])))
}
fn angle_unit_deg(unit: Option<&str>) -> Option<f64> {
unit_scale(
unit,
&[
("deg", 1.0),
("arcmin", 1.0 / 60.0),
("arcsec", 1.0 / 3600.0),
("rad", 180.0 / std::f64::consts::PI),
],
)
}