use crate::catalog;
use astronomy_engine_bindings::{
Astronomy_DefineStar, Astronomy_Equator, Astronomy_Horizon, Astronomy_MakeTime,
Astronomy_MoonPhase, astro_aberration_t_ABERRATION, astro_aberration_t_NO_ABERRATION,
astro_body_t_BODY_MOON, astro_body_t_BODY_STAR1, astro_body_t_BODY_SUN,
astro_equator_date_t_EQUATOR_OF_DATE, astro_equatorial_t, astro_horizon_t, astro_observer_t,
astro_refraction_t_REFRACTION_NONE, astro_refraction_t_REFRACTION_NORMAL,
astro_status_t_ASTRO_SUCCESS, astro_time_t,
};
use chrono::{DateTime, Datelike, Timelike, Utc};
use std::ops::{Add, Mul, Sub};
#[derive(Debug)]
pub enum StarHorizonError {
DefineStar(u32),
EquatorOfDate(u32),
}
pub fn star_stereo(
star: &catalog::Star,
time: &mut astro_time_t,
observer: &astro_observer_t,
aberration: bool,
refraction: bool,
) -> Result<PolarCoordinates, StarHorizonError> {
unsafe {
let star_status = Astronomy_DefineStar(astro_body_t_BODY_STAR1, star.ra, star.dec, 1000.0);
if star_status != astro_status_t_ASTRO_SUCCESS {
return Err(StarHorizonError::DefineStar(star_status));
}
let eq_date = Astronomy_Equator(
astro_body_t_BODY_STAR1,
time as *mut _,
*observer,
astro_equator_date_t_EQUATOR_OF_DATE,
if aberration {
astro_aberration_t_ABERRATION
} else {
astro_aberration_t_NO_ABERRATION
},
);
if eq_date.status != astro_status_t_ASTRO_SUCCESS {
return Err(StarHorizonError::EquatorOfDate(eq_date.status));
}
let hor = Astronomy_Horizon(
time as *mut _,
*observer,
eq_date.ra,
eq_date.dec,
if refraction {
astro_refraction_t_REFRACTION_NORMAL
} else {
astro_refraction_t_REFRACTION_NONE
},
);
let stereo = hor_to_stereo(&hor);
Ok(stereo)
}
}
#[derive(Debug, Clone)]
pub struct CartesianCoordinates {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Add for CartesianCoordinates {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
z: self.z + other.z,
}
}
}
impl Add<&Self> for CartesianCoordinates {
type Output = Self;
fn add(self, other: &Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
z: self.z + other.z,
}
}
}
impl Sub for CartesianCoordinates {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
x: self.x - other.x,
y: self.y - other.y,
z: self.z - other.z,
}
}
}
impl From<&PolarCoordinates> for CartesianCoordinates {
fn from(polar_coord: &PolarCoordinates) -> Self {
CartesianCoordinates {
x: polar_coord.rad * polar_coord.phi.to_radians().cos(),
y: polar_coord.rad * polar_coord.phi.to_radians().sin(),
z: 0.0,
}
}
}
impl From<PolarCoordinates> for CartesianCoordinates {
fn from(polar_coord: PolarCoordinates) -> Self {
CartesianCoordinates {
x: polar_coord.rad * polar_coord.phi.to_radians().cos(),
y: polar_coord.rad * polar_coord.phi.to_radians().sin(),
z: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct PolarCoordinates {
pub rad: f64,
pub phi: f64,
}
impl Mul<f64> for PolarCoordinates {
type Output = Self;
fn mul(self, other: f64) -> Self {
if other < 0.0 {
Self {
rad: -self.rad * other,
phi: self.phi + 180.0,
}
} else {
Self {
rad: self.rad * other,
phi: self.phi,
}
}
}
}
impl PolarCoordinates {
pub fn mut_canvas_orient(&mut self) {
self.phi -= 90.0;
}
pub fn canvas_orient(self) -> Self {
Self {
rad: self.rad,
phi: self.phi - 90.0,
}
}
pub fn mut_rot(&mut self, phi: f64) {
self.phi += phi;
}
pub fn rot(self, phi: f64) -> Self {
let mut out = self.clone();
out.mut_rot(phi);
out
}
}
pub fn hor_to_stereo(hz: &astro_horizon_t) -> PolarCoordinates {
let radius = 2f64 * (45f64 - hz.altitude / 2f64).to_radians().tan();
PolarCoordinates {
rad: radius,
phi: hz.azimuth,
}
}
pub fn astro_time_from_datetime(datetime: DateTime<Utc>) -> astro_time_t {
unsafe {
Astronomy_MakeTime(
datetime.year(),
datetime.month() as i32,
datetime.day() as i32,
datetime.hour() as i32,
datetime.minute() as i32,
datetime.second() as f64,
)
}
}
#[derive(Debug)]
pub struct SunMoonProjection {
pub sun_hor: astro_horizon_t,
pub moon_hor: astro_horizon_t,
pub moon_cycle_degrees: f64,
}
impl SunMoonProjection {
pub fn from_time_observer(time: &mut astro_time_t, observer: &astro_observer_t) -> Self {
let sun_hor: astro_horizon_t;
unsafe {
let sun_eq: astro_equatorial_t = Astronomy_Equator(
astro_body_t_BODY_SUN,
time as *mut _,
*observer,
astro_equator_date_t_EQUATOR_OF_DATE,
astro_aberration_t_ABERRATION,
);
sun_hor = Astronomy_Horizon(
time as *mut _,
*observer,
sun_eq.ra,
sun_eq.dec,
astro_refraction_t_REFRACTION_NORMAL,
);
}
let moon_hor: astro_horizon_t;
unsafe {
let moon_eq: astro_equatorial_t = Astronomy_Equator(
astro_body_t_BODY_MOON,
time as *mut _,
*observer,
astro_equator_date_t_EQUATOR_OF_DATE,
astro_aberration_t_ABERRATION,
);
moon_hor = Astronomy_Horizon(
time as *mut _,
*observer,
moon_eq.ra,
moon_eq.dec,
astro_refraction_t_REFRACTION_NORMAL,
);
}
let moon_cycle_degrees: f64;
unsafe {
let phase = Astronomy_MoonPhase(*time);
if phase.status != astro_status_t_ASTRO_SUCCESS {
panic!("Error {} trying to calculate Moon phase.\n", phase.status);
}
moon_cycle_degrees = phase.angle;
}
Self {
sun_hor,
moon_hor,
moon_cycle_degrees,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use astronomy_engine_bindings::astro_horizon_t;
use chrono::TimeZone;
fn make_hz(altitude: f64, azimuth: f64) -> astro_horizon_t {
astro_horizon_t {
altitude,
azimuth,
ra: 0.0,
dec: 0.0,
}
}
#[test]
fn hor_to_stereo_zenith() {
let hz = make_hz(90.0, 0.0);
let p = hor_to_stereo(&hz);
assert!(
p.rad.abs() < 1e-10,
"zenith rad should be ~0, got {}",
p.rad
);
}
#[test]
fn hor_to_stereo_horizon() {
let hz = make_hz(0.0, 0.0);
let p = hor_to_stereo(&hz);
assert!(
(p.rad - 2.0).abs() < 1e-10,
"horizon rad should be 2.0, got {}",
p.rad
);
}
#[test]
fn hor_to_stereo_below_horizon() {
let hz = make_hz(-10.0, 0.0);
let p = hor_to_stereo(&hz);
assert!(
p.rad > 2.0,
"below-horizon rad should be >2.0, got {}",
p.rad
);
}
#[test]
fn hor_to_stereo_azimuth_passthrough() {
let hz = make_hz(45.0, 137.5);
let p = hor_to_stereo(&hz);
assert_eq!(p.phi, 137.5);
}
#[test]
fn canvas_orient_shifts_phi() {
let p = PolarCoordinates {
rad: 1.0,
phi: 100.0,
};
let oriented = p.canvas_orient();
assert_eq!(oriented.phi, 10.0);
}
#[test]
fn mut_canvas_orient_shifts_phi() {
let mut p = PolarCoordinates {
rad: 1.0,
phi: 100.0,
};
p.mut_canvas_orient();
assert_eq!(p.phi, 10.0);
}
#[test]
fn rot_adds_angle() {
let p = PolarCoordinates {
rad: 1.0,
phi: 10.0,
};
let rotated = p.rot(45.0);
assert_eq!(rotated.phi, 55.0);
}
#[test]
fn polar_mul_positive() {
let p = PolarCoordinates {
rad: 2.0,
phi: 30.0,
};
let scaled = p * 3.0;
assert_eq!(scaled.rad, 6.0);
assert_eq!(scaled.phi, 30.0);
}
#[test]
fn polar_mul_negative() {
let p = PolarCoordinates {
rad: 2.0,
phi: 30.0,
};
let scaled = p * -1.0;
assert_eq!(scaled.rad, 2.0);
assert_eq!(scaled.phi, 210.0);
}
#[test]
fn cartesian_from_polar_zero() {
let p = PolarCoordinates { rad: 0.0, phi: 0.0 };
let c = CartesianCoordinates::from(p);
assert!(c.x.abs() < 1e-10);
assert!(c.y.abs() < 1e-10);
}
#[test]
fn cartesian_from_polar_north() {
let p = PolarCoordinates { rad: 1.0, phi: 0.0 };
let c = CartesianCoordinates::from(p);
assert!((c.x - 1.0).abs() < 1e-10, "x={}", c.x);
assert!(c.y.abs() < 1e-10, "y={}", c.y);
}
#[test]
fn cartesian_from_polar_east() {
let p = PolarCoordinates {
rad: 1.0,
phi: 90.0,
};
let c = CartesianCoordinates::from(p);
assert!(c.x.abs() < 1e-10, "x={}", c.x);
assert!((c.y - 1.0).abs() < 1e-10, "y={}", c.y);
}
#[test]
fn cartesian_add() {
let a = CartesianCoordinates {
x: 1.0,
y: 2.0,
z: 0.0,
};
let b = CartesianCoordinates {
x: 3.0,
y: 4.0,
z: 0.0,
};
let sum = a + b;
assert_eq!(sum.x, 4.0);
assert_eq!(sum.y, 6.0);
}
#[test]
fn cartesian_sub() {
let a = CartesianCoordinates {
x: 5.0,
y: 7.0,
z: 0.0,
};
let b = CartesianCoordinates {
x: 2.0,
y: 3.0,
z: 0.0,
};
let diff = a - b;
assert_eq!(diff.x, 3.0);
assert_eq!(diff.y, 4.0);
}
#[test]
fn astro_time_from_datetime_does_not_panic() {
let dt = Utc.with_ymd_and_hms(2024, 6, 21, 12, 0, 0).unwrap();
let _ = astro_time_from_datetime(dt);
}
#[test]
fn star_stereo_known_star() {
use astronomy_engine_bindings::astro_observer_t;
let polaris = crate::catalog::Star {
id: 99999,
ra: 2.530_111,
dec: 89.264_108,
mag: 1.98,
};
let observer = astro_observer_t {
latitude: 40.71,
longitude: -74.01,
height: 0.0,
};
let dt = Utc.with_ymd_and_hms(2024, 6, 21, 12, 0, 0).unwrap();
let mut time = astro_time_from_datetime(dt);
let result = star_stereo(&polaris, &mut time, &observer, false, false);
let polar = result.expect("Polaris projection should succeed");
assert!(
polar.rad < 2.0,
"Polaris rad={} should be <2.0 (above horizon)",
polar.rad
);
assert!(
polar.phi >= 0.0 && polar.phi < 360.0,
"phi={} out of range",
polar.phi
);
}
}