use nalgebra::Vector3;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Cartesian3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Cartesian3 {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Cartesian3 { x, y, z }
}
pub fn from_spherical(ra: f64, dec: f64, distance: f64) -> Self {
let cos_dec = dec.cos();
Cartesian3 {
x: distance * cos_dec * ra.cos(),
y: distance * cos_dec * ra.sin(),
z: distance * dec.sin(),
}
}
pub fn to_spherical(&self) -> (f64, f64, f64) {
let distance = self.magnitude();
if distance == 0.0 {
return (0.0, 0.0, 0.0);
}
let dec = (self.z / distance).asin();
let ra = if self.x == 0.0 && self.y == 0.0 {
0.0 } else {
let mut ra = self.y.atan2(self.x);
if ra < 0.0 {
ra += 2.0 * PI;
}
ra
};
(ra, dec, distance)
}
pub fn magnitude(&self) -> f64 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
pub fn normalize(&self) -> Option<Cartesian3> {
let mag = self.magnitude();
if mag == 0.0 {
None
} else {
Some(Cartesian3 {
x: self.x / mag,
y: self.y / mag,
z: self.z / mag,
})
}
}
pub fn dot(&self, other: &Cartesian3) -> f64 {
self.x * other.x + self.y * other.y + self.z * other.z
}
pub fn cross(&self, other: &Cartesian3) -> Cartesian3 {
Cartesian3 {
x: self.y * other.z - self.z * other.y,
y: self.z * other.x - self.x * other.z,
z: self.x * other.y - self.y * other.x,
}
}
pub fn angular_distance(&self, other: &Cartesian3) -> f64 {
let dot_product = self.dot(other);
let mag_product = self.magnitude() * other.magnitude();
if mag_product == 0.0 {
return 0.0;
}
let cos_angle = dot_product / mag_product;
if cos_angle >= 1.0 {
0.0
} else if cos_angle <= -1.0 {
PI
} else {
cos_angle.acos()
}
}
pub fn to_vector3(&self) -> Vector3<f64> {
Vector3::new(self.x, self.y, self.z)
}
pub fn from_vector3(vec: Vector3<f64>) -> Self {
Cartesian3 {
x: vec.x,
y: vec.y,
z: vec.z,
}
}
}
impl From<[f64; 3]> for Cartesian3 {
fn from(arr: [f64; 3]) -> Self {
Cartesian3::new(arr[0], arr[1], arr[2])
}
}
impl From<Cartesian3> for [f64; 3] {
fn from(cart: Cartesian3) -> Self {
[cart.x, cart.y, cart.z]
}
}
impl std::ops::Add for Cartesian3 {
type Output = Cartesian3;
fn add(self, other: Cartesian3) -> Cartesian3 {
Cartesian3 {
x: self.x + other.x,
y: self.y + other.y,
z: self.z + other.z,
}
}
}
impl std::ops::Sub for Cartesian3 {
type Output = Cartesian3;
fn sub(self, other: Cartesian3) -> Cartesian3 {
Cartesian3 {
x: self.x - other.x,
y: self.y - other.y,
z: self.z - other.z,
}
}
}
impl std::ops::Mul<f64> for Cartesian3 {
type Output = Cartesian3;
fn mul(self, scalar: f64) -> Cartesian3 {
Cartesian3 {
x: self.x * scalar,
y: self.y * scalar,
z: self.z * scalar,
}
}
}
impl std::ops::Div<f64> for Cartesian3 {
type Output = Cartesian3;
fn div(self, scalar: f64) -> Cartesian3 {
Cartesian3 {
x: self.x / scalar,
y: self.y / scalar,
z: self.z / scalar,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_cartesian_creation() {
let coord = Cartesian3::new(1.0, 2.0, 3.0);
assert_eq!(coord.x, 1.0);
assert_eq!(coord.y, 2.0);
assert_eq!(coord.z, 3.0);
}
#[test]
fn test_magnitude_calculation() {
let coord = Cartesian3::new(3.0, 4.0, 0.0);
assert_eq!(coord.magnitude(), 5.0);
let unit_vector = Cartesian3::new(1.0, 0.0, 0.0);
assert_eq!(unit_vector.magnitude(), 1.0);
let zero_vector = Cartesian3::new(0.0, 0.0, 0.0);
assert_eq!(zero_vector.magnitude(), 0.0);
}
#[test]
fn test_normalize() {
let coord = Cartesian3::new(3.0, 4.0, 0.0);
let normalized = coord.normalize().unwrap();
assert!((normalized.magnitude() - 1.0).abs() < 1e-15);
assert!((normalized.x - 0.6).abs() < 1e-15);
assert!((normalized.y - 0.8).abs() < 1e-15);
assert_eq!(normalized.z, 0.0);
let zero = Cartesian3::new(0.0, 0.0, 0.0);
assert!(zero.normalize().is_none());
}
#[test]
fn test_dot_product() {
let x_axis = Cartesian3::new(1.0, 0.0, 0.0);
let y_axis = Cartesian3::new(0.0, 1.0, 0.0);
let z_axis = Cartesian3::new(0.0, 0.0, 1.0);
assert_eq!(x_axis.dot(&y_axis), 0.0);
assert_eq!(x_axis.dot(&z_axis), 0.0);
assert_eq!(y_axis.dot(&z_axis), 0.0);
let same_direction = Cartesian3::new(2.0, 0.0, 0.0);
assert_eq!(x_axis.dot(&same_direction), 2.0);
let opposite = Cartesian3::new(-1.0, 0.0, 0.0);
assert_eq!(x_axis.dot(&opposite), -1.0);
}
#[test]
fn test_cross_product() {
let x_axis = Cartesian3::new(1.0, 0.0, 0.0);
let y_axis = Cartesian3::new(0.0, 1.0, 0.0);
let z_axis = Cartesian3::new(0.0, 0.0, 1.0);
let cross_xy = x_axis.cross(&y_axis);
assert!((cross_xy.x - 0.0).abs() < 1e-15);
assert!((cross_xy.y - 0.0).abs() < 1e-15);
assert!((cross_xy.z - 1.0).abs() < 1e-15);
let cross_yz = y_axis.cross(&z_axis);
assert!((cross_yz.x - 1.0).abs() < 1e-15);
assert!((cross_yz.y - 0.0).abs() < 1e-15);
assert!((cross_yz.z - 0.0).abs() < 1e-15);
let cross_zx = z_axis.cross(&x_axis);
assert!((cross_zx.x - 0.0).abs() < 1e-15);
assert!((cross_zx.y - 1.0).abs() < 1e-15);
assert!((cross_zx.z - 0.0).abs() < 1e-15);
}
#[test]
fn test_spherical_conversions() {
let vernal_equinox = Cartesian3::from_spherical(0.0, 0.0, 1.0);
assert!((vernal_equinox.x - 1.0).abs() < 1e-15);
assert!((vernal_equinox.y - 0.0).abs() < 1e-15);
assert!((vernal_equinox.z - 0.0).abs() < 1e-15);
let (ra, dec, dist) = vernal_equinox.to_spherical();
assert!((ra - 0.0).abs() < 1e-15);
assert!((dec - 0.0).abs() < 1e-15);
assert!((dist - 1.0).abs() < 1e-15);
let north_pole = Cartesian3::from_spherical(0.0, PI / 2.0, 1.0);
assert!((north_pole.x - 0.0).abs() < 1e-15);
assert!((north_pole.y - 0.0).abs() < 1e-15);
assert!((north_pole.z - 1.0).abs() < 1e-15);
let (_, dec, dist) = north_pole.to_spherical();
assert!((dec - PI / 2.0).abs() < 1e-15);
assert!((dist - 1.0).abs() < 1e-15);
let ra_90 = Cartesian3::from_spherical(PI / 2.0, 0.0, 1.0);
assert!((ra_90.x - 0.0).abs() < 1e-15);
assert!((ra_90.y - 1.0).abs() < 1e-15);
assert!((ra_90.z - 0.0).abs() < 1e-15);
let (ra, dec, dist) = ra_90.to_spherical();
assert!((ra - PI / 2.0).abs() < 1e-15);
assert!((dec - 0.0).abs() < 1e-15);
assert!((dist - 1.0).abs() < 1e-15);
}
#[test]
fn test_angular_distance() {
let x_axis = Cartesian3::new(1.0, 0.0, 0.0);
let y_axis = Cartesian3::new(0.0, 1.0, 0.0);
let z_axis = Cartesian3::new(0.0, 0.0, 1.0);
let angle_xy = x_axis.angular_distance(&y_axis);
assert!((angle_xy - PI / 2.0).abs() < 1e-15);
let angle_xz = x_axis.angular_distance(&z_axis);
assert!((angle_xz - PI / 2.0).abs() < 1e-15);
let opposite_x = Cartesian3::new(-1.0, 0.0, 0.0);
let angle_opposite = x_axis.angular_distance(&opposite_x);
assert!((angle_opposite - PI).abs() < 1e-15);
let same_direction = Cartesian3::new(2.0, 0.0, 0.0);
let angle_same = x_axis.angular_distance(&same_direction);
assert!((angle_same - 0.0).abs() < 1e-15);
}
#[test]
fn test_arithmetic_operations() {
let a = Cartesian3::new(1.0, 2.0, 3.0);
let b = Cartesian3::new(4.0, 5.0, 6.0);
let sum = a + b;
assert_eq!(sum.x, 5.0);
assert_eq!(sum.y, 7.0);
assert_eq!(sum.z, 9.0);
let diff = b - a;
assert_eq!(diff.x, 3.0);
assert_eq!(diff.y, 3.0);
assert_eq!(diff.z, 3.0);
let scaled = a * 2.0;
assert_eq!(scaled.x, 2.0);
assert_eq!(scaled.y, 4.0);
assert_eq!(scaled.z, 6.0);
let divided = a / 2.0;
assert_eq!(divided.x, 0.5);
assert_eq!(divided.y, 1.0);
assert_eq!(divided.z, 1.5);
}
#[test]
fn test_vector3_conversions() {
let coord = Cartesian3::new(1.0, 2.0, 3.0);
let vec = coord.to_vector3();
assert_eq!(vec.x, 1.0);
assert_eq!(vec.y, 2.0);
assert_eq!(vec.z, 3.0);
let coord_back = Cartesian3::from_vector3(vec);
assert_eq!(coord, coord_back);
}
#[test]
fn test_round_trip_spherical_conversion() {
let test_cases = vec![
(0.0, 0.0), (PI / 2.0, 0.0), (PI, 0.0), (3.0 * PI / 2.0, 0.0), (0.0, PI / 4.0), (0.0, -PI / 4.0), (PI / 3.0, PI / 6.0), ];
for (ra, dec) in test_cases {
let original_coord = Cartesian3::from_spherical(ra, dec, 1.0);
let (converted_ra, converted_dec, converted_dist) = original_coord.to_spherical();
let round_trip_coord =
Cartesian3::from_spherical(converted_ra, converted_dec, converted_dist);
assert!(
(original_coord.x - round_trip_coord.x).abs() < 1e-14,
"X mismatch for RA={}, Dec={}",
ra,
dec
);
assert!(
(original_coord.y - round_trip_coord.y).abs() < 1e-14,
"Y mismatch for RA={}, Dec={}",
ra,
dec
);
assert!(
(original_coord.z - round_trip_coord.z).abs() < 1e-14,
"Z mismatch for RA={}, Dec={}",
ra,
dec
);
}
}
#[test]
fn test_special_cases() {
let zero = Cartesian3::new(0.0, 0.0, 0.0);
let (ra, dec, dist) = zero.to_spherical();
assert_eq!(ra, 0.0);
assert_eq!(dec, 0.0);
assert_eq!(dist, 0.0);
let tiny = Cartesian3::new(1e-15, 1e-15, 1e-15);
assert!(tiny.magnitude() > 0.0);
let normalized = tiny.normalize().unwrap();
assert!((normalized.magnitude() - 1.0).abs() < 1e-14);
}
#[test]
fn test_precision_preservation() {
let precise_coord =
Cartesian3::new(0.123456789012345, 0.987654321098765, 0.555666777888999);
assert_eq!(precise_coord.x, 0.123456789012345);
assert_eq!(precise_coord.y, 0.987654321098765);
assert_eq!(precise_coord.z, 0.555666777888999);
let doubled = precise_coord * 2.0;
assert_eq!(doubled.x, 0.123456789012345 * 2.0);
assert_eq!(doubled.y, 0.987654321098765 * 2.0);
assert_eq!(doubled.z, 0.555666777888999 * 2.0);
}
#[test]
fn test_from_array() {
let cart: Cartesian3 = [1.0, 2.0, 3.0].into();
assert_eq!(cart.x, 1.0);
assert_eq!(cart.y, 2.0);
assert_eq!(cart.z, 3.0);
}
#[test]
fn test_into_array() {
let cart = Cartesian3::new(4.0, 5.0, 6.0);
let arr: [f64; 3] = cart.into();
assert_eq!(arr, [4.0, 5.0, 6.0]);
}
#[test]
fn test_array_roundtrip() {
let original = [1.5, -2.7, 3.14];
let cart: Cartesian3 = original.into();
let back: [f64; 3] = cart.into();
assert_eq!(original, back);
}
}