use crate::{
Angle, Error,
fmt::{FormatOptions, Formatter, formatter_impl},
inner,
parse::{self, Parsed, Value},
};
use core::{
fmt::{Debug, Display, Write},
str::FromStr,
};
use ordered_float::OrderedFloat;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct Latitude(OrderedFloat<f64>);
pub const NORTH_POLE: Latitude = Latitude(OrderedFloat(LATITUDE_LIMIT));
pub const ARCTIC_CIRCLE: Latitude = Latitude(OrderedFloat(66.5));
pub const TROPIC_OF_CANCER: Latitude = Latitude(OrderedFloat(23.5));
pub const EQUATOR: Latitude = Latitude(inner::ZERO);
pub const TROPIC_OF_CAPRICORN: Latitude = Latitude(OrderedFloat(-23.5));
pub const ANTARCTIC_CIRCLE: Latitude = Latitude(OrderedFloat(-66.5));
pub const SOUTH_POLE: Latitude = Latitude(OrderedFloat(-LATITUDE_LIMIT));
#[macro_export]
macro_rules! lat {
(N $degrees:expr, $minutes:expr, $seconds:expr) => {
lat!($degrees.abs(), $minutes, $seconds).unwrap()
};
(S $degrees:expr, $minutes:expr, $seconds:expr) => {
lat!(-$degrees.abs(), $minutes, $seconds).unwrap()
};
($degrees:expr, $minutes:expr, $seconds:expr) => {
Latitude::new($degrees, $minutes, $seconds).unwrap()
};
(N $degrees:expr, $minutes:expr) => {
lat!($degrees.abs(), $minutes).unwrap()
};
(S $degrees:expr, $minutes:expr) => {
lat!(-$degrees.abs(), $minutes).unwrap()
};
($degrees:expr, $minutes:expr) => {
lat!($degrees, $minutes, 0.0).unwrap()
};
(N $degrees:expr) => {
lat!($degrees.abs()).unwrap()
};
(S $degrees:expr) => {
lat!(-$degrees.abs()).unwrap()
};
($degrees:expr) => {
lat!($degrees, 0, 0.0).unwrap()
};
}
const LATITUDE_LIMIT: f64 = 90.0;
impl Default for Latitude {
fn default() -> Self {
EQUATOR
}
}
impl TryFrom<f64> for Latitude {
type Error = Error;
fn try_from(value: f64) -> Result<Self, Self::Error> {
Self::try_from(OrderedFloat(value))
}
}
impl TryFrom<OrderedFloat<f64>> for Latitude {
type Error = Error;
fn try_from(value: OrderedFloat<f64>) -> Result<Self, Self::Error> {
if value.is_infinite() || value.is_nan() {
Err(Error::InvalidNumericValue(value.into()))
} else if value.0 < -LATITUDE_LIMIT || value.0 > LATITUDE_LIMIT {
Err(Error::InvalidAngle(value.into_inner(), LATITUDE_LIMIT))
} else {
Ok(Self(value))
}
}
}
impl From<Latitude> for OrderedFloat<f64> {
fn from(value: Latitude) -> Self {
value.0
}
}
impl From<Latitude> for f64 {
fn from(value: Latitude) -> Self {
value.0.into()
}
}
impl FromStr for Latitude {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match parse::parse_str(s)? {
Parsed::Angle(Value::Unknown(decimal)) => Self::try_from(decimal),
Parsed::Angle(Value::Latitude(lat)) => Ok(lat),
_ => Err(Error::InvalidAngle(0.0, 0.0)),
}
}
}
impl Display for Latitude {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
let mut buf = String::new();
self.format(&mut buf, &FormatOptions::dms_signed())?;
f.write_str(&buf)
} else {
Display::fmt(&(self.0), f)
}
}
}
impl Formatter for Latitude {
fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
let fmt = (*fmt).with_labels(('N', 'S'));
formatter_impl(self.0, f, &fmt)
}
}
impl Angle for Latitude {
const MIN: Self = Self(OrderedFloat(-LATITUDE_LIMIT));
const MAX: Self = Self(OrderedFloat(LATITUDE_LIMIT));
fn new(degrees: i32, minutes: u32, seconds: f32) -> Result<Self, Error> {
if degrees < Self::MIN.as_float().0 as i32 || degrees > Self::MAX.as_float().0 as i32 {
return Err(Error::InvalidLatitudeDegrees(degrees));
}
let float = inner::from_degrees_minutes_seconds(degrees, minutes, seconds)?;
Self::try_from(float).map_err(|_| Error::InvalidLatitudeDegrees(degrees))
}
fn as_float(&self) -> OrderedFloat<f64> {
self.0
}
}
impl Latitude {
#[must_use]
pub fn is_on_equator(&self) -> bool {
self.is_zero()
}
#[must_use]
pub fn is_northern(&self) -> bool {
self.is_nonzero_positive()
}
#[must_use]
pub fn is_southern(&self) -> bool {
self.is_nonzero_negative()
}
#[must_use]
pub fn is_arctic(&self) -> bool {
*self >= ARCTIC_CIRCLE
}
#[must_use]
pub fn is_antarctic(&self) -> bool {
*self <= ANTARCTIC_CIRCLE
}
#[must_use]
pub fn is_tropic_of_cancer(&self) -> bool {
*self >= TROPIC_OF_CANCER
}
#[must_use]
pub fn is_tropic_of_capricorn(&self) -> bool {
*self <= TROPIC_OF_CAPRICORN
}
#[must_use]
pub fn is_tropical(&self) -> bool {
self.is_tropic_of_cancer() || self.is_tropic_of_capricorn()
}
#[must_use]
pub fn is_polar(&self) -> bool {
self.is_arctic() || self.is_antarctic()
}
}