use geocart::{Cartesian, Geographic, Latitude, Longitude};
use num_traits::{Euclid, Float, FloatConst, Signed};
use crate::{IsClose, Tolerance, Vertex};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Inclination<T>(T);
impl<T> From<T> for Inclination<T>
where
T: Float + FloatConst,
{
fn from(value: T) -> Self {
Self(if (T::zero()..=T::PI()).contains(&value) {
value
} else {
value.cos().acos()
})
}
}
impl<T> From<Latitude<T>> for Inclination<T>
where
T: Signed + FloatConst,
{
fn from(latitude: Latitude<T>) -> Self {
Self(T::FRAC_PI_2() - latitude.into_inner())
}
}
impl<T> From<Inclination<T>> for Latitude<T>
where
T: Signed + Float + FloatConst,
{
fn from(angle: Inclination<T>) -> Self {
(-angle.into_inner() + T::FRAC_PI_2()).into()
}
}
impl<T> IsClose for Inclination<T>
where
T: IsClose<Tolerance = Tolerance<T>>,
{
type Tolerance = Tolerance<T>;
fn is_close(&self, rhs: &Self, tolerance: &Self::Tolerance) -> bool {
self.0.is_close(&rhs.0, tolerance)
}
}
impl<T> Inclination<T> {
pub fn into_inner(self) -> T {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Azimuth<T>(T);
impl<T> From<T> for Azimuth<T>
where
T: Float + FloatConst,
{
fn from(value: T) -> Self {
if (T::zero()..T::TAU()).contains(&value) {
return Self(value);
}
let mut modulus = value % T::TAU();
if value.is_sign_negative() {
modulus = (modulus + T::TAU()) % T::TAU();
}
Self(modulus)
}
}
impl<T> From<Longitude<T>> for Azimuth<T>
where
T: Float + FloatConst,
{
fn from(longitude: Longitude<T>) -> Self {
longitude.into_inner().into()
}
}
impl<T> From<Azimuth<T>> for Longitude<T>
where
T: Signed + Float + FloatConst + Euclid,
{
fn from(angle: Azimuth<T>) -> Self {
angle.into_inner().into()
}
}
impl<T> IsClose for Azimuth<T>
where
T: IsClose<Tolerance = Tolerance<T>>,
{
type Tolerance = Tolerance<T>;
fn is_close(&self, rhs: &Self, tolerance: &Self::Tolerance) -> bool {
self.0.is_close(&rhs.0, tolerance)
}
}
impl<T> Azimuth<T> {
pub fn into_inner(self) -> T {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Point<T> {
pub inclination: Inclination<T>,
pub azimuth: Azimuth<T>,
}
impl<T> From<Point<T>> for Geographic<T>
where
T: Signed + Float + FloatConst + Euclid,
{
fn from(point: Point<T>) -> Self {
Self {
latitude: point.inclination.into(),
longitude: point.azimuth.into(),
altitude: T::one().into(),
}
}
}
impl<T> From<Point<T>> for Cartesian<T>
where
T: PartialOrd + Signed + Float + FloatConst + Euclid,
{
fn from(point: Point<T>) -> Self {
Geographic::from(point).into()
}
}
impl<T, U> From<[U; 2]> for Point<T>
where
T: PartialOrd + Signed + Float + FloatConst + Euclid,
U: Into<T>,
{
fn from([inclination, azimuth]: [U; 2]) -> Self {
Self {
inclination: inclination.into().into(),
azimuth: azimuth.into().into(),
}
}
}
impl<T> From<Geographic<T>> for Point<T>
where
T: Signed + Float + FloatConst + Euclid,
{
fn from(point: Geographic<T>) -> Self {
Self {
inclination: point.latitude.into(),
azimuth: point.longitude.into(),
}
}
}
impl<T> From<Cartesian<T>> for Point<T>
where
T: PartialOrd + Signed + Float + FloatConst + Euclid,
{
fn from(point: Cartesian<T>) -> Self {
Geographic::from(point).into()
}
}
impl<T> Vertex for Point<T>
where
T: Signed + Float + FloatConst + Euclid,
{
type Scalar = T;
fn distance(&self, rhs: &Self) -> T {
Geographic::from(*self).distance(&(*rhs).into())
}
}
impl<T> IsClose for Point<T>
where
T: IsClose<Tolerance = Tolerance<T>>,
{
type Tolerance = Tolerance<T>;
fn is_close(&self, rhs: &Self, tolerance: &Self::Tolerance) -> bool {
self.inclination.is_close(&rhs.inclination, tolerance)
&& self
.azimuth
.is_close(&rhs.azimuth, tolerance)
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::{FRAC_PI_2, PI, TAU};
use crate::{
spherical::{Azimuth, Inclination},
IsClose, Tolerance,
};
#[test]
fn inclination_must_not_exceed_boundaries() {
struct Test {
name: &'static str,
input: f64,
output: f64,
}
vec![
Test {
name: "angle within range must not change",
input: PI,
output: PI,
},
Test {
name: "2π radians must equals zero",
input: TAU,
output: 0.,
},
Test {
name: "negative angle must change",
input: -FRAC_PI_2,
output: FRAC_PI_2,
},
Test {
name: "overflowing angle must change",
input: 3. * FRAC_PI_2,
output: FRAC_PI_2,
},
]
.into_iter()
.for_each(|test| {
let inclination = Inclination::from(test.input).into_inner();
let tolerance = Tolerance {
relative: 1e-9.into(),
..Default::default()
};
assert!(
inclination.is_close(&test.output, &tolerance),
"{}: got inclination = {}, want {}",
test.name,
inclination,
test.output
);
});
}
#[test]
fn azimuth_must_not_exceed_boundaries() {
struct Test {
name: &'static str,
input: f64,
output: f64,
}
vec![
Test {
name: "angle within range must not change",
input: PI,
output: PI,
},
Test {
name: "2π radians must equals zero",
input: TAU,
output: 0.,
},
Test {
name: "negative angle must change",
input: -FRAC_PI_2,
output: TAU - FRAC_PI_2,
},
Test {
name: "overflowing angle must change",
input: TAU + FRAC_PI_2,
output: FRAC_PI_2,
},
]
.into_iter()
.for_each(|test| {
let azimuth = Azimuth::from(test.input).into_inner();
assert_eq!(
azimuth, test.output,
"{}: got azimuth = {}, want {}",
test.name, azimuth, test.output
);
});
}
}