use std::ops::{Add, AddAssign, Sub, SubAssign};
use crate::mesh::{MeshCell, MeshNode, MeshUnit};
use crate::Correction;
#[inline(always)]
fn normalize_latitude(t: &f64) -> f64 {
if t.is_nan() || (-90.0..=90.0).contains(t) {
*t
} else {
match t % 360.0 {
s if !(-270.0..=270.0).contains(&s) => s - f64::copysign(360.0, s),
s if !(-90.0..=90.0).contains(&s) => f64::copysign(180.0, s) - s,
s => s,
}
}
}
#[inline(always)]
fn normalize_longitude(t: &f64) -> f64 {
if t.is_nan() || (-180.0..=180.0).contains(t) {
*t
} else {
match t % 360.0 {
s if !(-180.0..180.0).contains(&s) => s - f64::copysign(360.0, s),
s => s,
}
}
}
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Point {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
}
impl From<(f64, f64)> for Point {
#[inline]
fn from(rhs: (f64, f64)) -> Self {
Self::new_unchecked(rhs.0, rhs.1, 0.0)
}
}
impl From<(f64, f64, f64)> for Point {
#[inline]
fn from(rhs: (f64, f64, f64)) -> Self {
Self::new_unchecked(rhs.0, rhs.1, rhs.2)
}
}
impl From<MeshNode> for Point {
#[inline]
fn from(value: MeshNode) -> Self {
Self::from_node(&value)
}
}
macro_rules! impl_ops {
($t:ident, $m:ident, $s:ident, $rhs:ident) => {
impl $t<$rhs> for $s {
type Output = $s;
#[inline]
fn $m(self, rhs: $rhs) -> Self::Output {
Self::Output::new_unchecked(
$t::$m(self.latitude, rhs.latitude),
$t::$m(self.longitude, rhs.longitude),
$t::$m(self.altitude, rhs.altitude),
)
.normalize()
}
}
impl $t<&$rhs> for $s {
type Output = $s;
#[inline]
fn $m(self, rhs: &$rhs) -> Self::Output {
Self::Output::new_unchecked(
$t::$m(self.latitude, rhs.latitude),
$t::$m(self.longitude, rhs.longitude),
$t::$m(self.altitude, rhs.altitude),
)
.normalize()
}
}
impl $t<$rhs> for &$s {
type Output = $s;
#[inline]
fn $m(self, rhs: $rhs) -> Self::Output {
Self::Output::new_unchecked(
$t::$m(self.latitude, rhs.latitude),
$t::$m(self.longitude, rhs.longitude),
$t::$m(self.altitude, rhs.altitude),
)
.normalize()
}
}
impl $t<&$rhs> for &$s {
type Output = $s;
#[inline]
fn $m(self, rhs: &$rhs) -> Self::Output {
Self::Output::new_unchecked(
$t::$m(self.latitude, rhs.latitude),
$t::$m(self.longitude, rhs.longitude),
$t::$m(self.altitude, rhs.altitude),
)
.normalize()
}
}
};
}
impl_ops!(Add, add, Point, Correction);
impl_ops!(Sub, sub, Point, Correction);
macro_rules! impl_assign_ops {
($t:ident, $m:ident, $s:ident, $rhs:ident) => {
impl $t<$rhs> for $s {
#[inline]
fn $m(&mut self, rhs: $rhs) {
$t::$m(&mut self.latitude, rhs.latitude);
$t::$m(&mut self.longitude, rhs.longitude);
$t::$m(&mut self.altitude, rhs.altitude);
*self = self.normalize();
}
}
impl $t<&$rhs> for $s {
#[inline]
fn $m(&mut self, rhs: &$rhs) {
$t::$m(&mut self.latitude, rhs.latitude);
$t::$m(&mut self.longitude, rhs.longitude);
$t::$m(&mut self.altitude, rhs.altitude);
*self = self.normalize();
}
}
};
}
impl_assign_ops!(AddAssign, add_assign, Point, Correction);
impl_assign_ops!(SubAssign, sub_assign, Point, Correction);
impl Point {
#[inline]
#[must_use]
pub fn new(latitude: f64, longitude: f64, altitude: f64) -> Option<Self> {
if !(-90.0..=90.0).contains(&latitude)
|| !(-180.0..=180.0).contains(&longitude)
|| latitude.is_nan()
|| longitude.is_nan()
|| altitude.is_nan()
{
return None;
};
Some(Self::new_unchecked(latitude, longitude, altitude))
}
#[inline]
#[must_use]
pub const fn new_unchecked(latitude: f64, longitude: f64, altitude: f64) -> Self {
Self {
latitude,
longitude,
altitude,
}
}
#[inline]
#[must_use]
pub fn normalize(&self) -> Self {
Self {
latitude: normalize_latitude(&self.latitude),
longitude: normalize_longitude(&self.longitude),
altitude: self.altitude,
}
}
#[inline]
#[must_use]
pub fn try_from_meshcode(meshcode: &u32) -> Option<Self> {
MeshNode::try_from_meshcode(meshcode).map(|node| Self::from_node(&node))
}
#[inline]
#[must_use]
pub fn from_node(node: &MeshNode) -> Self {
let latitude = node.latitude.to_latitude();
let longitude = node.longitude.to_longitude();
Self::new_unchecked(latitude, longitude, 0.)
}
#[inline]
#[must_use]
pub fn try_to_meshcode(&self, mesh_unit: &MeshUnit) -> Option<u32> {
self.try_to_node(mesh_unit).map(|node| node.to_meshcode())
}
#[inline]
#[must_use]
pub fn try_to_node(&self, mesh_unit: &MeshUnit) -> Option<MeshNode> {
MeshNode::try_from_point(self, mesh_unit)
}
#[inline]
#[must_use]
pub fn try_to_cell(&self, mesh_unit: MeshUnit) -> Option<MeshCell> {
MeshCell::try_from_point(self, mesh_unit)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_normalize() {
for (e, v) in [
(0., 0.),
(-0., -0.),
(20.0, 20.0),
(-20.0, -20.0),
(0.0, 360.0),
(-90.0, 270.0),
(0.0, 180.0),
(90.0, 90.0),
(-0.0, -360.0),
(90.0, -270.0),
(-0.0, -180.0),
(-90.0, -90.0),
(20.0, 380.),
(-70.0, 290.),
(-20.0, 200.),
(70.0, 110.),
(-20.0, -380.),
(70.0, -290.),
(20.0, -200.),
(-70.0, -110.),
] {
assert_eq!(
Point::new_unchecked(v, 0.0, 0.0).normalize(),
Point::new_unchecked(e, 0.0, 0.0)
);
}
for (e, v) in [
(0.0, 0.0),
(-0.0, -0.0),
(20.0, 20.0),
(-20.0, -20.0),
(0.0, 360.0),
(-90.0, 270.0),
(180.0, 180.0),
(90.0, 90.0),
(-0.0, -360.0),
(90.0, -270.0),
(-180.0, -180.0),
(-90.0, -90.0),
(20.0, 380.),
(-70.0, 290.),
(-160.0, 200.),
(110.0, 110.),
(-20.0, -380.),
(70.0, -290.),
(160.0, -200.),
(-110.0, -110.),
] {
assert_eq!(
Point::new_unchecked(0.0, v, 0.0).normalize(),
Point::new_unchecked(0.0, e, 0.0)
);
}
let actual = Point::new_unchecked(f64::NAN, f64::NAN, f64::NAN).normalize();
assert!(actual.latitude.is_nan());
assert!(actual.longitude.is_nan());
assert!(actual.altitude.is_nan());
}
}