use crate::error::{Result, AstroError, validate_ra, validate_dec};
pub struct TangentPlane {
pub ra0: f64,
pub dec0: f64,
pub scale: f64,
pub rotation: f64,
pub crpix1: f64,
pub crpix2: f64,
}
impl TangentPlane {
pub fn new(ra0: f64, dec0: f64, scale: f64) -> Result<Self> {
validate_ra(ra0)?;
validate_dec(dec0)?;
if scale <= 0.0 {
return Err(AstroError::OutOfRange {
parameter: "scale",
value: scale,
min: f64::MIN_POSITIVE,
max: f64::MAX,
});
}
Ok(Self {
ra0,
dec0,
scale,
rotation: 0.0,
crpix1: 0.0,
crpix2: 0.0,
})
}
pub fn with_reference_pixel(mut self, x: f64, y: f64) -> Self {
self.crpix1 = x;
self.crpix2 = y;
self
}
pub fn with_rotation(mut self, rotation: f64) -> Self {
self.rotation = rotation;
self
}
pub fn ra_dec_to_pixel(&self, ra: f64, dec: f64) -> Result<(f64, f64)> {
validate_ra(ra)?;
validate_dec(dec)?;
let ra_rad = ra.to_radians();
let dec_rad = dec.to_radians();
let ra0_rad = self.ra0.to_radians();
let dec0_rad = self.dec0.to_radians();
let result = erfars::gnomonic::Tpxes(ra_rad, dec_rad, ra0_rad, dec0_rad);
let (xi, eta) = match result {
Ok((xi, eta)) => (xi, eta),
Err(_) => {
return Err(AstroError::ProjectionError {
reason: "Point is on opposite side of sky from projection center".to_string(),
});
}
};
let xi_deg = xi.to_degrees();
let eta_deg = eta.to_degrees();
let cos_rot = self.rotation.to_radians().cos();
let sin_rot = self.rotation.to_radians().sin();
let xi_rot = xi_deg * cos_rot + eta_deg * sin_rot;
let eta_rot = -xi_deg * sin_rot + eta_deg * cos_rot;
let x = self.crpix1 - xi_rot * 3600.0 / self.scale;
let y = self.crpix2 + eta_rot * 3600.0 / self.scale;
Ok((x, y))
}
pub fn pixel_to_ra_dec(&self, x: f64, y: f64) -> Result<(f64, f64)> {
let dx = x - self.crpix1;
let dy = y - self.crpix2;
let xi_deg = -dx * self.scale / 3600.0;
let eta_deg = dy * self.scale / 3600.0;
let cos_rot = self.rotation.to_radians().cos();
let sin_rot = self.rotation.to_radians().sin();
let xi_unrot = xi_deg * cos_rot - eta_deg * sin_rot;
let eta_unrot = xi_deg * sin_rot + eta_deg * cos_rot;
let xi = xi_unrot.to_radians();
let eta = eta_unrot.to_radians();
let ra0_rad = self.ra0.to_radians();
let dec0_rad = self.dec0.to_radians();
let (ra_rad, dec_rad) = erfars::gnomonic::Tpsts(xi, eta, ra0_rad, dec0_rad);
let mut ra = ra_rad.to_degrees();
let dec = dec_rad.to_degrees();
while ra < 0.0 {
ra += 360.0;
}
while ra >= 360.0 {
ra -= 360.0;
}
Ok((ra, dec))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tangent_plane_projection() {
let tp = TangentPlane::new(180.0, 45.0, 1.0).unwrap()
.with_reference_pixel(512.0, 512.0);
let (x, y) = tp.ra_dec_to_pixel(180.0, 45.0).unwrap();
assert!((x - 512.0).abs() < 1e-10);
assert!((y - 512.0).abs() < 1e-10);
}
#[test]
fn test_projection_round_trip() {
let tp = TangentPlane::new(83.8, -5.4, 2.0).unwrap()
.with_reference_pixel(1024.0, 1024.0)
.with_rotation(15.0);
let ra_orig = 84.0;
let dec_orig = -5.5;
let (x, y) = tp.ra_dec_to_pixel(ra_orig, dec_orig).unwrap();
let (ra_back, dec_back) = tp.pixel_to_ra_dec(x, y).unwrap();
assert!((ra_orig - ra_back).abs() < 1e-10);
assert!((dec_orig - dec_back).abs() < 1e-10);
}
#[test]
fn test_opposite_side_of_sky() {
let tp = TangentPlane::new(0.0, 0.0, 1.0).unwrap();
let result = tp.ra_dec_to_pixel(180.0, 0.0);
assert!(result.is_err());
assert!(matches!(result, Err(AstroError::ProjectionError { .. })));
}
#[test]
fn test_pixel_to_radec_at_reference() {
let tp = TangentPlane::new(100.0, 30.0, 1.0).unwrap()
.with_reference_pixel(512.0, 512.0);
let (ra, dec) = tp.pixel_to_ra_dec(512.0, 512.0).unwrap();
assert!((ra - 100.0).abs() < 1e-10);
assert!((dec - 30.0).abs() < 1e-10);
}
#[test]
fn test_ra_normalization() {
let tp = TangentPlane::new(1.0, 0.0, 1.0).unwrap()
.with_reference_pixel(512.0, 512.0);
let (ra1, _) = tp.pixel_to_ra_dec(1000.0, 512.0).unwrap();
assert!((0.0..360.0).contains(&ra1));
let tp2 = TangentPlane::new(359.0, 0.0, 1.0).unwrap()
.with_reference_pixel(512.0, 512.0);
let (ra2, _) = tp2.pixel_to_ra_dec(100.0, 512.0).unwrap();
assert!((0.0..360.0).contains(&ra2));
}
#[test]
fn test_projection_ra_while_loops() {
let tp = TangentPlane::new(0.0, 0.0, 1.0).unwrap()
.with_reference_pixel(512.0, 512.0);
let (ra1, _) = tp.pixel_to_ra_dec(2000.0, 512.0).unwrap();
assert!((0.0..360.0).contains(&ra1));
let tp2 = TangentPlane::new(359.9, 0.0, 1.0).unwrap()
.with_reference_pixel(512.0, 512.0);
let (ra2, _) = tp2.pixel_to_ra_dec(100.0, 512.0).unwrap();
assert!((0.0..360.0).contains(&ra2));
}
}