use super::{EPSILON_RAD, Vec3d};
use crate::{
CellIndex, EARTH_RADIUS_KM, Resolution,
error::InvalidLatLng,
math::{Coord2d, asin, atan2, cos, mul_add, sin, sqrt},
};
use core::fmt;
use float_eq::float_eq;
#[derive(Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LatLng {
lat: f64,
lng: f64,
}
impl LatLng {
pub const fn new(lat: f64, lng: f64) -> Result<Self, InvalidLatLng> {
Self::from_radians(lat.to_radians(), lng.to_radians())
}
pub const fn from_radians(
lat: f64,
lng: f64,
) -> Result<Self, InvalidLatLng> {
if !lat.is_finite() {
return Err(InvalidLatLng::new(lat, "infinite latitude"));
}
if !lng.is_finite() {
return Err(InvalidLatLng::new(lng, "infinite longitude"));
}
Ok(Self { lat, lng })
}
#[cfg(feature = "geo")]
pub fn from_coord<T>(value: &T) -> Result<Self, InvalidLatLng>
where
T: geo_traits::CoordTrait<T = f64>,
{
Self::new(value.y(), value.x())
}
#[must_use]
pub const fn lat(self) -> f64 {
self.lat.to_degrees()
}
#[must_use]
pub const fn lng(self) -> f64 {
self.lng.to_degrees()
}
#[must_use]
pub const fn lat_radians(self) -> f64 {
self.lat
}
#[must_use]
pub const fn lng_radians(self) -> f64 {
self.lng
}
#[must_use]
pub fn distance_rads(self, other: Self) -> f64 {
let sin_lat = sin((other.lat - self.lat) * 0.5);
let sin_lng = sin((other.lng - self.lng) * 0.5);
let a = mul_add(
sin_lat,
sin_lat,
cos(self.lat) * cos(other.lat) * sin_lng * sin_lng,
);
2. * atan2(sqrt(a), sqrt(1. - a))
}
#[must_use]
pub fn distance_km(self, other: Self) -> f64 {
self.distance_rads(other) * EARTH_RADIUS_KM
}
#[must_use]
pub fn distance_m(self, other: Self) -> f64 {
self.distance_km(other) * 1000.
}
#[must_use]
pub fn to_cell(self, resolution: Resolution) -> CellIndex {
Vec3d::from(self).to_cell(resolution)
}
#[cfg(any(test, feature = "typed_floats"))]
pub(crate) const fn new_unchecked(lat: f64, lng: f64) -> Self {
debug_assert!(lat.is_finite() && lng.is_finite());
Self { lat, lng }
}
}
impl PartialEq for LatLng {
fn eq(&self, other: &Self) -> bool {
float_eq!(self.lat, other.lat, abs <= EPSILON_RAD)
&& float_eq!(self.lng, other.lng, abs <= EPSILON_RAD)
}
}
impl Eq for LatLng {}
impl From<Vec3d> for LatLng {
#[inline]
fn from(value: Vec3d) -> Self {
Self {
lat: asin(value.z),
lng: atan2(value.y, value.x),
}
}
}
impl fmt::Display for LatLng {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:.10}, {:.10})", self.lat(), self.lng())
}
}
impl fmt::Debug for LatLng {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LatLng")
.field("lat_rad", &self.lat)
.field("lat_deg", &self.lat())
.field("lng_rad", &self.lng)
.field("lng_deg", &self.lng())
.finish()
}
}
impl Coord2d for LatLng {
fn xy(self) -> (f64, f64) {
(self.lng, self.lat)
}
}
#[cfg(feature = "geo")]
impl From<LatLng> for geo::Coord {
fn from(value: LatLng) -> Self {
Self {
x: value.lng(),
y: value.lat(),
}
}
}
#[cfg(feature = "typed_floats")]
mod typed_floats {
type TFCoord = typed_floats::NonNaNFinite<f64>;
type TFLatlng = (TFCoord, TFCoord);
impl From<TFLatlng> for crate::LatLng {
fn from(latlng: TFLatlng) -> Self {
Self::new_unchecked(latlng.0.into(), latlng.1.into())
}
}
}
#[cfg(feature = "geo")]
impl TryFrom<geo::Coord> for LatLng {
type Error = InvalidLatLng;
fn try_from(value: geo::Coord) -> Result<Self, Self::Error> {
Self::new(value.y, value.x)
}
}
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for LatLng {
fn arbitrary(
data: &mut arbitrary::Unstructured<'a>,
) -> arbitrary::Result<Self> {
let lat = f64::arbitrary(data)?;
let lng = f64::arbitrary(data)?;
Self::from_radians(lat, lng)
.map_err(|_| arbitrary::Error::IncorrectFormat)
}
}
#[cfg(feature = "geo")]
impl geo_traits::CoordTrait for LatLng {
type T = f64;
#[expect(clippy::panic, reason = "panic by design")]
fn nth_or_panic(&self, n: usize) -> Self::T {
match n {
0 => self.x(),
1 => self.y(),
_ => panic!("LatLng only supports 2 dimensions"),
}
}
fn dim(&self) -> geo_traits::Dimensions {
geo_traits::Dimensions::Xy
}
fn x(&self) -> Self::T {
self.lng()
}
fn y(&self) -> Self::T {
self.lat()
}
}