use crate::centers;
use crate::frames;
use qtty::angular::Degrees;
use qtty::length::LengthUnit;
use qtty::units::Radian;
use qtty::Quantity;
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_unchecked_with_params(
center_params: C::Params,
polar: Degrees,
azimuth: Degrees,
distance: Quantity<U>,
) -> Self {
Self {
polar,
azimuth,
distance,
center_params,
_frame: PhantomData,
}
}
pub fn new_with_params(
center_params: C::Params,
polar: Degrees,
azimuth: Degrees,
distance: Quantity<U>,
) -> Self {
let (polar, azimuth) = if distance < Quantity::new(0.0) {
super::canonicalize_polar_azimuth(-polar, azimuth + Degrees::new(180.0))
} else {
super::canonicalize_polar_azimuth(polar, azimuth)
};
Self::new_unchecked_with_params(center_params, polar, azimuth, distance.abs())
}
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_unchecked(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_unchecked_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_unchecked(polar: Degrees, azimuth: Degrees, distance: Quantity<U>) -> Self {
Self::new_unchecked_with_params((), polar, azimuth, distance)
}
pub const CENTER: Self = Self::new_unchecked(
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
C: centers::ReferenceCenter<Params = ()>,
F: frames::ReferenceFrame,
{
self.to_cartesian().distance_to(&other.to_cartesian())
}
}
impl_quantity_fmt_triplet! {
impl[C, F, U] for Position<C, F, U>
where {
C: centers::ReferenceCenter,
F: frames::ReferenceFrame,
U: LengthUnit,
},
fmt_each: { Quantity<U>, },
|this, f, FmtOne| {
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
)?;
FmtOne::fmt(&this.polar, f)?;
write!(f, ", {}: ", azimuth_name)?;
FmtOne::fmt(&this.azimuth, f)?;
write!(f, ", {}: ", distance_name)?;
FmtOne::fmt(&this.distance, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
use qtty::units::Meter;
use qtty::{DEG, M};
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_unchecked(
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_unchecked(
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_unchecked(
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_unchecked(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_unchecked(
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 new_with_params_canonicalizes_pole_crossing_without_changing_point() {
let canonical = Position::<TestCenter, TestFrame, Meter>::new_with_params(
(),
100.0 * DEG,
30.0 * DEG,
1.0 * M,
);
let raw = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
100.0 * DEG,
30.0 * DEG,
1.0 * M,
);
assert!((canonical.polar.value() - 80.0).abs() < EPS);
assert!((canonical.azimuth.value() - 210.0).abs() < EPS);
let a = canonical.to_cartesian();
let b = raw.to_cartesian();
assert!((a.x() - b.x()).abs() < EPS * M);
assert!((a.y() - b.y()).abs() < EPS * M);
assert!((a.z() - b.z()).abs() < EPS * M);
}
#[test]
fn new_with_params_maps_negative_distance_to_antipodal_direction() {
let pos = Position::<TestCenter, TestFrame, Meter>::new_with_params(
(),
10.0 * DEG,
20.0 * DEG,
-2.0 * M,
);
assert!((pos.polar.value() + 10.0).abs() < EPS);
assert!((pos.azimuth.value() - 200.0).abs() < EPS);
assert!((pos.distance.value() - 2.0).abs() < EPS);
}
#[test]
fn direction_returns_unit_vector() {
let pos = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
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_unchecked(
45.0 * DEG,
30.0 * DEG,
3.0 * M,
);
let b = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
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_unchecked(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_unchecked(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_unchecked_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_unchecked(0.0 * DEG, 0.0 * DEG, 1.0 * M);
let b =
Position::<TestCenter, TestFrame, Meter>::new_unchecked(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);
}
}