use crate::centers;
use crate::frames;
use qtty::*;
use std::marker::PhantomData;
#[cfg(feature = "serde")]
#[path = "position_serde.rs"]
mod position_serde;
#[derive(Debug, Clone, Copy)]
pub struct Position<C: centers::ReferenceCenter, F: frames::ReferenceFrame, U: LengthUnit> {
pub polar: Degrees,
pub azimuth: Degrees,
pub distance: Quantity<U>,
center_params: C::Params,
_frame: PhantomData<F>,
}
impl<C, F, U> Position<C, F, U>
where
C: centers::ReferenceCenter,
F: frames::ReferenceFrame,
U: LengthUnit,
{
pub const fn new_raw_with_params(
center_params: C::Params,
polar: Degrees,
azimuth: Degrees,
distance: Quantity<U>,
) -> Self {
Self {
polar,
azimuth,
distance,
center_params,
_frame: PhantomData,
}
}
pub fn center_params(&self) -> &C::Params {
&self.center_params
}
pub fn angular_separation(&self, other: Self) -> Degrees {
super::angular_separation_impl(self.polar, self.azimuth, other.polar, other.azimuth)
}
#[must_use]
pub fn direction(&self) -> super::direction::Direction<F> {
super::direction::Direction::new_raw(self.polar, self.azimuth)
}
#[must_use]
pub fn to_cartesian(&self) -> crate::cartesian::Position<C, F, U>
where
F: frames::ReferenceFrame,
{
let polar_rad = self.polar.to::<Radian>();
let azimuth_rad = self.azimuth.to::<Radian>();
let x = self.distance * azimuth_rad.cos() * polar_rad.cos();
let y = self.distance * azimuth_rad.sin() * polar_rad.cos();
let z = self.distance * polar_rad.sin();
crate::cartesian::Position::<C, F, U>::new_with_params(self.center_params.clone(), x, y, z)
}
#[must_use]
pub fn from_cartesian(cart: &crate::cartesian::Position<C, F, U>) -> Self
where
F: frames::ReferenceFrame,
{
let x = cart.x().value();
let y = cart.y().value();
let z = cart.z().value();
let r = cart.distance().value();
let (polar, azimuth) = if r.abs() < f64::EPSILON {
(Degrees::new(0.0), Degrees::new(0.0))
} else {
super::xyz_to_polar_azimuth(x / r, y / r, z / r)
};
Self::new_raw_with_params(
cart.center_params().clone(),
polar,
azimuth,
cart.distance(),
)
}
}
impl<C, F, U> Position<C, F, U>
where
C: centers::ReferenceCenter<Params = ()>,
F: frames::ReferenceFrame,
U: LengthUnit,
{
pub const fn new_raw(polar: Degrees, azimuth: Degrees, distance: Quantity<U>) -> Self {
Self::new_raw_with_params((), polar, azimuth, distance)
}
pub const CENTER: Self = Self::new_raw(
Degrees::new(0.0),
Degrees::new(0.0),
Quantity::<U>::new(0.0),
);
}
impl<C, F, U> Position<C, F, U>
where
C: centers::ReferenceCenter,
F: frames::ReferenceFrame,
U: LengthUnit,
{
#[must_use]
pub fn distance_to(&self, other: &Self) -> Quantity<U>
where
U: std::cmp::PartialEq + std::fmt::Debug,
F: frames::ReferenceFrame,
{
self.to_cartesian().distance_to(&other.to_cartesian())
}
}
impl<C, F, U> std::fmt::Display for Position<C, F, U>
where
C: centers::ReferenceCenter,
F: frames::ReferenceFrame,
U: LengthUnit,
Quantity<U>: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (polar_name, azimuth_name, distance_name) =
F::spherical_names().unwrap_or(("\u{03b8}", "\u{03c6}", "r"));
write!(
f,
"Center: {}, Frame: {}, {}: ",
C::center_name(),
F::frame_name(),
polar_name
)?;
std::fmt::Display::fmt(&self.polar, f)?;
write!(f, ", {}: ", azimuth_name)?;
std::fmt::Display::fmt(&self.azimuth, f)?;
write!(f, ", {}: ", distance_name)?;
std::fmt::Display::fmt(&self.distance, f)
}
}
impl<C, F, U> std::fmt::LowerExp for Position<C, F, U>
where
C: centers::ReferenceCenter,
F: frames::ReferenceFrame,
U: LengthUnit,
Quantity<U>: std::fmt::LowerExp,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (polar_name, azimuth_name, distance_name) =
F::spherical_names().unwrap_or(("\u{03b8}", "\u{03c6}", "r"));
write!(
f,
"Center: {}, Frame: {}, {}: ",
C::center_name(),
F::frame_name(),
polar_name
)?;
std::fmt::LowerExp::fmt(&self.polar, f)?;
write!(f, ", {}: ", azimuth_name)?;
std::fmt::LowerExp::fmt(&self.azimuth, f)?;
write!(f, ", {}: ", distance_name)?;
std::fmt::LowerExp::fmt(&self.distance, f)
}
}
impl<C, F, U> std::fmt::UpperExp for Position<C, F, U>
where
C: centers::ReferenceCenter,
F: frames::ReferenceFrame,
U: LengthUnit,
Quantity<U>: std::fmt::UpperExp,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (polar_name, azimuth_name, distance_name) =
F::spherical_names().unwrap_or(("\u{03b8}", "\u{03c6}", "r"));
write!(
f,
"Center: {}, Frame: {}, {}: ",
C::center_name(),
F::frame_name(),
polar_name
)?;
std::fmt::UpperExp::fmt(&self.polar, f)?;
write!(f, ", {}: ", azimuth_name)?;
std::fmt::UpperExp::fmt(&self.azimuth, f)?;
write!(f, ", {}: ", distance_name)?;
std::fmt::UpperExp::fmt(&self.distance, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
use std::f64::consts::SQRT_2;
#[derive(Debug, Copy, Clone, ReferenceFrame)]
struct TestFrame;
#[derive(Debug, Copy, Clone, ReferenceCenter)]
struct TestCenter;
#[derive(Clone, Debug, Default, PartialEq)]
struct TestParams {
id: i32,
}
#[derive(Debug, Copy, Clone, ReferenceCenter)]
#[center(params = TestParams)]
struct ParamCenter;
const EPS: f64 = 1e-6;
#[test]
fn test_spherical_coord_creation() {
let coord = Position::<TestCenter, TestFrame, Meter>::new_raw(
Degrees::new(90.0),
Degrees::new(45.0),
1.0 * M,
);
assert_eq!(coord.polar.value(), 90.0);
assert_eq!(coord.azimuth.value(), 45.0);
assert_eq!(coord.distance.value(), 1.0);
}
#[test]
fn test_spherical_coord_to_string() {
let coord = Position::<TestCenter, TestFrame, Meter>::new_raw(
Degrees::new(60.0),
Degrees::new(30.0),
1000.0 * M,
);
let coord_string = coord.to_string();
assert!(coord_string.contains("θ: 60"));
assert!(coord_string.contains("φ: 30"));
assert!(coord_string.contains("r: 1000"));
}
#[test]
fn test_spherical_coord_format_specifiers() {
let coord =
Position::<TestCenter, TestFrame, Meter>::new_raw(60.0 * DEG, 30.0 * DEG, 1000.0 * M);
let text_prec = format!("{:.2}", coord);
let expected_r_prec = format!("{:.2}", coord.distance);
assert!(text_prec.contains(&format!("r: {expected_r_prec}")));
let text_exp = format!("{:.3e}", coord);
let expected_theta_exp = format!("{:.3e}", coord.polar);
assert!(text_exp.contains(&format!("θ: {expected_theta_exp}")));
}
#[test]
fn test_spherical_coord_zero_values() {
let coord =
Position::<TestCenter, TestFrame, Meter>::new_raw(0.0 * DEG, 0.0 * DEG, 0.0 * M);
assert_eq!(coord.polar.value(), 0.0);
assert_eq!(coord.azimuth.value(), 0.0);
assert_eq!(coord.distance.value(), 0.0);
}
#[test]
fn test_spherical_coord_precision() {
let coord = Position::<TestCenter, TestFrame, Meter>::new_raw(
45.123456 * DEG,
90.654321 * DEG,
1234.56789 * M,
);
assert!((coord.polar.value() - 45.123456).abs() < 1e-6);
assert!((coord.azimuth.value() - 90.654321).abs() < 1e-6);
assert!((coord.distance - 1234.56789 * M).abs() < 1e-6 * M);
}
#[test]
fn direction_returns_unit_vector() {
let pos =
Position::<TestCenter, TestFrame, Meter>::new_raw(10.0 * DEG, 20.0 * DEG, 2.5 * M);
let dir = pos.direction();
let cart = dir.to_cartesian();
let magnitude = (cart.x().powi(2) + cart.y().powi(2) + cart.z().powi(2)).sqrt();
assert!(
(magnitude - 1.0).abs() < EPS,
"magnitude should be 1.0, got {}",
magnitude
);
assert!((dir.polar - 10.0 * DEG).abs() < EPS * DEG);
assert!((dir.azimuth - 20.0 * DEG).abs() < EPS * DEG);
}
#[test]
fn center_constant_is_origin() {
let c = Position::<TestCenter, TestFrame, Meter>::CENTER;
assert_eq!(c.polar.value(), 0.0);
assert_eq!(c.azimuth.value(), 0.0);
assert_eq!(c.distance.value(), 0.0);
}
#[test]
fn from_degrees_matches_new_raw() {
let a = Position::<TestCenter, TestFrame, Meter>::new_raw(45.0 * DEG, 30.0 * DEG, 3.0 * M);
let b = Position::<TestCenter, TestFrame, Meter>::new_raw(45.0 * DEG, 30.0 * DEG, 3.0 * M);
assert_eq!(a.polar, b.polar);
assert_eq!(a.azimuth, b.azimuth);
assert_eq!(a.distance, b.distance);
}
#[test]
fn distance_identity_zero_and_orthogonal() {
let a = Position::<TestCenter, TestFrame, Meter>::new_raw(0.0 * DEG, 0.0 * DEG, 1.0 * M);
let d0 = a.distance_to(&a);
assert!(d0.abs().value() < EPS);
let b = Position::<TestCenter, TestFrame, Meter>::new_raw(0.0 * DEG, 90.0 * DEG, 1.0 * M);
let d = a.distance_to(&b);
assert!((d.value() - SQRT_2).abs() < EPS);
}
#[test]
fn position_with_params_and_center_params() {
let params = TestParams { id: 9 };
let pos = Position::<ParamCenter, TestFrame, Meter>::new_raw_with_params(
params.clone(),
5.0 * DEG,
10.0 * DEG,
2.0 * M,
);
assert_eq!(pos.center_params(), ¶ms);
assert!((pos.distance.value() - 2.0).abs() < EPS);
}
#[test]
fn angular_separation_quarter_turn() {
let a = Position::<TestCenter, TestFrame, Meter>::new_raw(0.0 * DEG, 0.0 * DEG, 1.0 * M);
let b = Position::<TestCenter, TestFrame, Meter>::new_raw(0.0 * DEG, 90.0 * DEG, 1.0 * M);
let sep = a.angular_separation(b);
assert!((sep.value() - 90.0).abs() < EPS);
}
#[test]
fn cartesian_roundtrip_and_zero_radius() {
type CartPos = crate::cartesian::Position<TestCenter, TestFrame, Meter>;
let cart = CartPos::new(1.0, -1.0, 0.0);
let sph = Position::from_cartesian(&cart);
assert!((sph.polar.value()).abs() < EPS);
assert!((sph.azimuth.value() - 315.0).abs() < EPS);
let back = sph.to_cartesian();
assert!((back.x().value() - cart.x().value()).abs() < EPS);
assert!((back.y().value() - cart.y().value()).abs() < EPS);
let origin = CartPos::new(0.0, 0.0, 0.0);
let sph_origin = Position::from_cartesian(&origin);
assert!((sph_origin.polar.value()).abs() < EPS);
assert!((sph_origin.azimuth.value()).abs() < EPS);
assert!(sph_origin.distance.value().abs() < EPS);
}
}