use crate::error::{Error, Result};
use crate::header::Header;
use mapproj::conic::cod::Cod;
use mapproj::conic::coe::Coe;
use mapproj::conic::coo::Coo;
use mapproj::conic::cop::Cop;
use mapproj::cylindrical::car::Car;
use mapproj::cylindrical::cea::Cea;
use mapproj::cylindrical::cyp::Cyp;
use mapproj::cylindrical::mer::Mer;
use mapproj::hybrid::hpx::Hpx;
use mapproj::img2celestial::Img2Celestial;
use mapproj::img2proj::WcsImgXY2ProjXY;
use mapproj::pseudocyl::ait::Ait;
use mapproj::pseudocyl::mol::Mol;
use mapproj::pseudocyl::par::Par;
use mapproj::pseudocyl::sfl::Sfl;
use mapproj::zenithal::air::Air;
use mapproj::zenithal::arc::Arc;
use mapproj::zenithal::azp::Azp;
use mapproj::zenithal::feye::Feye;
use mapproj::zenithal::ncp::Ncp;
use mapproj::zenithal::sin::Sin;
use mapproj::zenithal::stg::Stg;
use mapproj::zenithal::szp::Szp;
use mapproj::zenithal::tan::Tan;
use mapproj::zenithal::zea::Zea;
use mapproj::{CanonicalProjection, CenteredProjection, ImgXY, LonLat, Projection};
#[derive(Debug, Clone, Copy)]
struct LinearTransform {
crpix1: f64,
crpix2: f64,
cd11: f64,
cd12: f64,
cd21: f64,
cd22: f64,
}
#[derive(Debug, Clone)]
pub struct Wcs {
code: ProjCode,
crval1: f64,
crval2: f64,
lin: LinearTransform,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProjCode {
Tan,
Sin,
Arc,
Zea,
Stg,
Azp,
Szp,
Air,
Ncp,
Feye,
Mer,
Car,
Cea,
Cyp,
Sfl,
Par,
Mol,
Ait,
Cod,
Coe,
Coo,
Cop,
Hpx,
}
impl ProjCode {
fn from_str(code: &str) -> Option<Self> {
Some(match code {
"TAN" => ProjCode::Tan,
"SIN" => ProjCode::Sin,
"ARC" => ProjCode::Arc,
"ZEA" => ProjCode::Zea,
"STG" => ProjCode::Stg,
"AZP" => ProjCode::Azp,
"SZP" => ProjCode::Szp,
"AIR" => ProjCode::Air,
"NCP" => ProjCode::Ncp,
"FEYE" => ProjCode::Feye,
"MER" => ProjCode::Mer,
"CAR" => ProjCode::Car,
"CEA" => ProjCode::Cea,
"CYP" => ProjCode::Cyp,
"SFL" => ProjCode::Sfl,
"PAR" => ProjCode::Par,
"MOL" => ProjCode::Mol,
"AIT" => ProjCode::Ait,
"COD" => ProjCode::Cod,
"COE" => ProjCode::Coe,
"COO" => ProjCode::Coo,
"COP" => ProjCode::Cop,
"HPX" => ProjCode::Hpx,
_ => return None,
})
}
}
fn build_img2cel<P: CanonicalProjection + Default>(
lin: &LinearTransform,
crval1_rad: f64,
crval2_rad: f64,
) -> Img2Celestial<P, WcsImgXY2ProjXY> {
let img2proj = WcsImgXY2ProjXY::from_cd(
lin.crpix1, lin.crpix2, lin.cd11, lin.cd12, lin.cd21, lin.cd22,
);
Img2Celestial::new(img2proj, build_proj::<P>(crval1_rad, crval2_rad))
}
fn build_proj<P: CanonicalProjection + Default>(
crval1_rad: f64,
crval2_rad: f64,
) -> CenteredProjection<P> {
let mut proj = CenteredProjection::new(P::default());
proj.set_proj_center_from_lonlat(&LonLat::new(crval1_rad, crval2_rad));
proj
}
macro_rules! dispatch_proj {
($code:expr, $op:ident, $($args:expr),* $(,)?) => {
match $code {
ProjCode::Tan => $op::<Tan>($($args),*),
ProjCode::Sin => $op::<Sin>($($args),*),
ProjCode::Arc => $op::<Arc>($($args),*),
ProjCode::Zea => $op::<Zea>($($args),*),
ProjCode::Stg => $op::<Stg>($($args),*),
ProjCode::Azp => $op::<Azp>($($args),*),
ProjCode::Szp => $op::<Szp>($($args),*),
ProjCode::Air => $op::<Air>($($args),*),
ProjCode::Ncp => $op::<Ncp>($($args),*),
ProjCode::Feye => $op::<Feye>($($args),*),
ProjCode::Mer => $op::<Mer>($($args),*),
ProjCode::Car => $op::<Car>($($args),*),
ProjCode::Cea => $op::<Cea>($($args),*),
ProjCode::Cyp => $op::<Cyp>($($args),*),
ProjCode::Sfl => $op::<Sfl>($($args),*),
ProjCode::Par => $op::<Par>($($args),*),
ProjCode::Mol => $op::<Mol>($($args),*),
ProjCode::Ait => $op::<Ait>($($args),*),
ProjCode::Cod => $op::<Cod>($($args),*),
ProjCode::Coe => $op::<Coe>($($args),*),
ProjCode::Coo => $op::<Coo>($($args),*),
ProjCode::Cop => $op::<Cop>($($args),*),
ProjCode::Hpx => $op::<Hpx>($($args),*),
}
};
}
fn pixel_to_world_for<P: CanonicalProjection + Default>(
lin: &LinearTransform,
crval1_rad: f64,
crval2_rad: f64,
x: f64,
y: f64,
) -> Option<LonLat> {
let i2c = build_img2cel::<P>(lin, crval1_rad, crval2_rad);
i2c.img2lonlat(&ImgXY::new(x, y))
}
fn world_to_pixel_for<P: CanonicalProjection + Default>(
lin: &LinearTransform,
crval1_rad: f64,
crval2_rad: f64,
lon_rad: f64,
lat_rad: f64,
) -> Option<(f64, f64)> {
let proj = build_proj::<P>(crval1_rad, crval2_rad);
let pxy = proj.proj_lonlat(&LonLat::new(lon_rad, lat_rad))?;
let f = std::f64::consts::PI / 180.0;
let (a, b, c, d) = (lin.cd11 * f, lin.cd12 * f, lin.cd21 * f, lin.cd22 * f);
let det = a * d - b * c;
if det == 0.0 {
return None;
}
let (px, py) = (pxy.x(), pxy.y());
let dx = (d * px - b * py) / det;
let dy = (-c * px + a * py) / det;
Some((dx + lin.crpix1, dy + lin.crpix2))
}
fn ctype_proj_code(ctype: &str) -> &str {
let t = ctype.trim();
match t.rfind('-') {
Some(i) => &t[i + 1..],
None => t,
}
}
impl Wcs {
pub fn from_header(header: &Header) -> Result<Self> {
let ctype1 = header
.get_string("CTYPE1")
.ok_or_else(|| Error::Wcs("missing CTYPE1".into()))?
.to_string();
let ctype2 = header
.get_string("CTYPE2")
.ok_or_else(|| Error::Wcs("missing CTYPE2".into()))?
.to_string();
if ctype1.trim_end().ends_with("-SIP") || ctype2.trim_end().ends_with("-SIP") {
return Err(Error::UnsupportedWcs(
"SIP distortion (CTYPE -SIP) is not supported".into(),
));
}
let code1 = ctype_proj_code(&ctype1);
let code2 = ctype_proj_code(&ctype2);
if code1 != code2 {
return Err(Error::Wcs(format!(
"CTYPE1/CTYPE2 projection codes differ: '{code1}' vs '{code2}'"
)));
}
let code = ProjCode::from_str(code1).ok_or_else(|| {
Error::UnsupportedWcs(format!("unsupported projection code '{code1}'"))
})?;
for key in ["CUNIT1", "CUNIT2"] {
if let Some(u) = header.get_string(key) {
let u = u.trim();
if !u.is_empty() && !u.eq_ignore_ascii_case("deg") && !u.eq_ignore_ascii_case("degree") {
return Err(Error::UnsupportedWcs(format!(
"non-degree {key} = '{u}' is not supported"
)));
}
}
}
let crval1 = header
.get_float("CRVAL1")
.ok_or_else(|| Error::Wcs("missing CRVAL1".into()))?;
let crval2 = header
.get_float("CRVAL2")
.ok_or_else(|| Error::Wcs("missing CRVAL2".into()))?;
let crpix1 = header
.get_float("CRPIX1")
.ok_or_else(|| Error::Wcs("missing CRPIX1".into()))?;
let crpix2 = header
.get_float("CRPIX2")
.ok_or_else(|| Error::Wcs("missing CRPIX2".into()))?;
let lin = parse_linear(header, crpix1, crpix2)?;
Ok(Wcs {
code,
crval1,
crval2,
lin,
})
}
pub fn pixel_to_world(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let c1 = self.crval1.to_radians();
let c2 = self.crval2.to_radians();
let lonlat = dispatch_proj!(self.code, pixel_to_world_for, &self.lin, c1, c2, x, y)?;
let lon = lonlat.lon().to_degrees().rem_euclid(360.0);
let lat = lonlat.lat().to_degrees();
Some((lon, lat))
}
pub fn world_to_pixel(&self, lon: f64, lat: f64) -> Option<(f64, f64)> {
let c1 = self.crval1.to_radians();
let c2 = self.crval2.to_radians();
let lon_rad = lon.to_radians();
let lat_rad = lat.to_radians();
dispatch_proj!(
self.code,
world_to_pixel_for,
&self.lin,
c1,
c2,
lon_rad,
lat_rad
)
}
}
fn parse_linear(header: &Header, crpix1: f64, crpix2: f64) -> Result<LinearTransform> {
let has_cd = header.find("CD1_1").is_some()
|| header.find("CD1_2").is_some()
|| header.find("CD2_1").is_some()
|| header.find("CD2_2").is_some();
if has_cd {
let cd11 = header.get_float("CD1_1").unwrap_or(0.0);
let cd12 = header.get_float("CD1_2").unwrap_or(0.0);
let cd21 = header.get_float("CD2_1").unwrap_or(0.0);
let cd22 = header.get_float("CD2_2").unwrap_or(0.0);
return Ok(LinearTransform {
crpix1,
crpix2,
cd11,
cd12,
cd21,
cd22,
});
}
let cdelt1 = header
.get_float("CDELT1")
.ok_or_else(|| Error::Wcs("missing CDELT1 (and no CD matrix)".into()))?;
let cdelt2 = header
.get_float("CDELT2")
.ok_or_else(|| Error::Wcs("missing CDELT2 (and no CD matrix)".into()))?;
let pc11 = header.get_float("PC1_1").unwrap_or(1.0);
let pc12 = header.get_float("PC1_2").unwrap_or(0.0);
let pc21 = header.get_float("PC2_1").unwrap_or(0.0);
let pc22 = header.get_float("PC2_2").unwrap_or(1.0);
Ok(LinearTransform {
crpix1,
crpix2,
cd11: cdelt1 * pc11,
cd12: cdelt1 * pc12,
cd21: cdelt2 * pc21,
cd22: cdelt2 * pc22,
})
}
impl Header {
pub fn wcs(&self) -> Result<Wcs> {
Wcs::from_header(self)
}
}
impl crate::Hdu {
pub fn wcs(&self) -> Result<Wcs> {
self.header.wcs()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keyword::HeaderValue;
fn tan_header() -> Header {
let mut h = Header::new();
h.set("CTYPE1", HeaderValue::String("RA---TAN".into()), None);
h.set("CTYPE2", HeaderValue::String("DEC--TAN".into()), None);
h.set("CRVAL1", HeaderValue::Float(150.0), None);
h.set("CRVAL2", HeaderValue::Float(30.0), None);
h.set("CRPIX1", HeaderValue::Float(50.5), None);
h.set("CRPIX2", HeaderValue::Float(50.5), None);
h.set("CD1_1", HeaderValue::Float(-1.0 / 3600.0), None);
h.set("CD1_2", HeaderValue::Float(0.0), None);
h.set("CD2_1", HeaderValue::Float(0.0), None);
h.set("CD2_2", HeaderValue::Float(1.0 / 3600.0), None);
h
}
#[test]
fn reference_pixel_maps_to_crval() {
let wcs = tan_header().wcs().unwrap();
let (lon, lat) = wcs.pixel_to_world(50.5, 50.5).unwrap();
assert!((lon - 150.0).abs() < 1e-9, "lon = {lon}");
assert!((lat - 30.0).abs() < 1e-9, "lat = {lat}");
}
#[test]
fn world_to_pixel_near_crval_is_reference() {
let wcs = tan_header().wcs().unwrap();
let (x, y) = wcs.world_to_pixel(150.0, 30.0 + 1.0 / 3600.0).unwrap();
assert!((x - 50.5).abs() < 1e-6, "x = {x}");
assert!((y - 51.5).abs() < 1e-6, "y = {y}");
}
#[test]
fn round_trip_pixel_world_pixel() {
let wcs = tan_header().wcs().unwrap();
for &(x, y) in &[(1.0, 1.0), (100.0, 100.0), (1.0, 100.0), (75.25, 12.5)] {
let (lon, lat) = wcs.pixel_to_world(x, y).unwrap();
let (x2, y2) = wcs.world_to_pixel(lon, lat).unwrap();
assert!((x - x2).abs() < 1e-7, "x {x} != {x2}");
assert!((y - y2).abs() < 1e-7, "y {y} != {y2}");
}
}
#[test]
fn pc_cdelt_equivalent_to_cd() {
let mut h = Header::new();
h.set("CTYPE1", HeaderValue::String("RA---TAN".into()), None);
h.set("CTYPE2", HeaderValue::String("DEC--TAN".into()), None);
h.set("CRVAL1", HeaderValue::Float(150.0), None);
h.set("CRVAL2", HeaderValue::Float(30.0), None);
h.set("CRPIX1", HeaderValue::Float(50.5), None);
h.set("CRPIX2", HeaderValue::Float(50.5), None);
h.set("CDELT1", HeaderValue::Float(-1.0 / 3600.0), None);
h.set("CDELT2", HeaderValue::Float(1.0 / 3600.0), None);
let wcs = h.wcs().unwrap();
let wcs_cd = tan_header().wcs().unwrap();
for &(x, y) in &[(10.0, 20.0), (90.0, 80.0)] {
let a = wcs.pixel_to_world(x, y).unwrap();
let b = wcs_cd.pixel_to_world(x, y).unwrap();
assert!((a.0 - b.0).abs() < 1e-12 && (a.1 - b.1).abs() < 1e-12);
}
}
#[test]
fn unsupported_projection_rejected() {
let mut h = tan_header();
h.set("CTYPE1", HeaderValue::String("RA---XYZ".into()), None);
h.set("CTYPE2", HeaderValue::String("DEC--XYZ".into()), None);
assert!(matches!(h.wcs(), Err(Error::UnsupportedWcs(_))));
}
#[test]
fn sip_rejected() {
let mut h = tan_header();
h.set("CTYPE1", HeaderValue::String("RA---TAN-SIP".into()), None);
h.set("CTYPE2", HeaderValue::String("DEC--TAN-SIP".into()), None);
assert!(matches!(h.wcs(), Err(Error::UnsupportedWcs(_))));
}
#[test]
fn missing_keyword_errors() {
let mut h = tan_header();
h.keywords.retain(|k| k.name != "CRVAL1");
assert!(matches!(h.wcs(), Err(Error::Wcs(_))));
}
}