use glam::DVec3;
use std::fmt;
pub(crate) const MAX_MERCATOR_LAT: f64 = 85.051_129;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GeoCoord {
pub lat: f64,
pub lon: f64,
pub alt: f64,
}
impl GeoCoord {
#[inline]
pub fn new(lat: f64, lon: f64, alt: f64) -> Self {
const EPS: f64 = 1e-10;
debug_assert!(
(-90.0 - EPS..=90.0 + EPS).contains(&lat),
"latitude {lat} out of range [-90, 90]"
);
debug_assert!(
(-180.0 - EPS..=180.0 + EPS).contains(&lon),
"longitude {lon} out of range [-180, 180]"
);
Self { lat, lon, alt }
}
#[inline]
pub fn from_lat_lon(lat: f64, lon: f64) -> Self {
Self::new(lat, lon, 0.0)
}
#[inline]
pub fn new_checked(lat: f64, lon: f64, alt: f64) -> Option<Self> {
if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
return None;
}
Some(Self { lat, lon, alt })
}
#[inline]
pub fn is_web_mercator_valid(&self) -> bool {
self.lat.abs() <= MAX_MERCATOR_LAT && self.lon.abs() <= 180.0
}
#[inline]
pub fn clamped_mercator(&self) -> Self {
let lat = self.lat.clamp(-MAX_MERCATOR_LAT, MAX_MERCATOR_LAT);
let mut lon = self.lon % 360.0;
if lon > 180.0 {
lon -= 360.0;
}
if lon < -180.0 {
lon += 360.0;
}
Self {
lat,
lon,
alt: self.alt,
}
}
}
impl Default for GeoCoord {
fn default() -> Self {
Self {
lat: 0.0,
lon: 0.0,
alt: 0.0,
}
}
}
impl fmt::Display for GeoCoord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ns = if self.lat >= 0.0 { 'N' } else { 'S' };
let ew = if self.lon >= 0.0 { 'E' } else { 'W' };
write!(
f,
"{:.6} {} {:.6} {} {:.1}m",
self.lat.abs(),
ns,
self.lon.abs(),
ew,
self.alt
)
}
}
impl From<(f64, f64)> for GeoCoord {
#[inline]
fn from((lat, lon): (f64, f64)) -> Self {
Self::from_lat_lon(lat, lon)
}
}
impl From<(f64, f64, f64)> for GeoCoord {
#[inline]
fn from((lat, lon, alt): (f64, f64, f64)) -> Self {
Self::new(lat, lon, alt)
}
}
impl From<[f64; 2]> for GeoCoord {
#[inline]
fn from(arr: [f64; 2]) -> Self {
Self::from_lat_lon(arr[0], arr[1])
}
}
impl From<[f64; 3]> for GeoCoord {
#[inline]
fn from(arr: [f64; 3]) -> Self {
Self::new(arr[0], arr[1], arr[2])
}
}
impl From<GeoCoord> for (f64, f64, f64) {
#[inline]
fn from(c: GeoCoord) -> Self {
(c.lat, c.lon, c.alt)
}
}
impl From<GeoCoord> for [f64; 3] {
#[inline]
fn from(c: GeoCoord) -> Self {
[c.lat, c.lon, c.alt]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WorldCoord {
pub position: DVec3,
}
impl WorldCoord {
#[inline]
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self {
position: DVec3::new(x, y, z),
}
}
}
impl Default for WorldCoord {
fn default() -> Self {
Self {
position: DVec3::ZERO,
}
}
}
impl fmt::Display for WorldCoord {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"({:.2}, {:.2}, {:.2})m",
self.position.x, self.position.y, self.position.z
)
}
}
impl From<DVec3> for WorldCoord {
#[inline]
fn from(v: DVec3) -> Self {
Self { position: v }
}
}
impl From<WorldCoord> for DVec3 {
#[inline]
fn from(c: WorldCoord) -> Self {
c.position
}
}
impl From<[f64; 3]> for WorldCoord {
#[inline]
fn from(arr: [f64; 3]) -> Self {
Self::new(arr[0], arr[1], arr[2])
}
}
impl From<WorldCoord> for [f64; 3] {
#[inline]
fn from(c: WorldCoord) -> Self {
[c.position.x, c.position.y, c.position.z]
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for WorldCoord {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("WorldCoord", 3)?;
s.serialize_field("x", &self.position.x)?;
s.serialize_field("y", &self.position.y)?;
s.serialize_field("z", &self.position.z)?;
s.end()
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for WorldCoord {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
struct Helper {
x: f64,
y: f64,
z: f64,
}
let h = Helper::deserialize(deserializer)?;
Ok(Self::new(h.x, h.y, h.z))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_geo_coord() {
let c = GeoCoord::default();
assert_eq!(c.lat, 0.0);
assert_eq!(c.lon, 0.0);
assert_eq!(c.alt, 0.0);
}
#[test]
fn geo_coord_checked_valid() {
assert!(GeoCoord::new_checked(45.0, 90.0, 0.0).is_some());
}
#[test]
fn geo_coord_checked_invalid_lat() {
assert!(GeoCoord::new_checked(91.0, 0.0, 0.0).is_none());
}
#[test]
fn geo_coord_checked_invalid_lon() {
assert!(GeoCoord::new_checked(0.0, 181.0, 0.0).is_none());
}
#[test]
fn geo_coord_checked_boundary_values() {
assert!(GeoCoord::new_checked(90.0, 180.0, 0.0).is_some());
assert!(GeoCoord::new_checked(-90.0, -180.0, 0.0).is_some());
}
#[test]
fn geo_coord_display_north_east() {
let c = GeoCoord::new(51.1, 17.0, 100.0);
let s = format!("{c}");
assert!(s.contains('N'));
assert!(s.contains('E'));
assert!(s.contains("100.0m"));
}
#[test]
fn geo_coord_display_south_west() {
let c = GeoCoord::new(-33.9, -70.6, 0.0);
let s = format!("{c}");
assert!(s.contains('S'));
assert!(s.contains('W'));
}
#[test]
fn from_tuple_2() {
let c: GeoCoord = (51.1, 17.0).into();
assert_eq!(c.lat, 51.1);
assert_eq!(c.lon, 17.0);
assert_eq!(c.alt, 0.0);
}
#[test]
fn from_tuple_3() {
let c: GeoCoord = (51.1, 17.0, 500.0).into();
assert_eq!(c.lat, 51.1);
assert_eq!(c.lon, 17.0);
assert_eq!(c.alt, 500.0);
}
#[test]
fn from_array_2() {
let c: GeoCoord = [51.1, 17.0].into();
assert_eq!(c.lat, 51.1);
assert_eq!(c.lon, 17.0);
assert_eq!(c.alt, 0.0);
}
#[test]
fn from_array_3() {
let c: GeoCoord = [51.1, 17.0, 100.0].into();
assert_eq!(c.lat, 51.1);
assert_eq!(c.alt, 100.0);
}
#[test]
fn into_tuple() {
let c = GeoCoord::new(51.1, 17.0, 100.0);
let (lat, lon, alt): (f64, f64, f64) = c.into();
assert_eq!(lat, 51.1);
assert_eq!(lon, 17.0);
assert_eq!(alt, 100.0);
}
#[test]
fn into_array() {
let c = GeoCoord::new(51.1, 17.0, 100.0);
let arr: [f64; 3] = c.into();
assert_eq!(arr, [51.1, 17.0, 100.0]);
}
#[test]
fn is_web_mercator_valid() {
assert!(GeoCoord::from_lat_lon(51.0, 17.0).is_web_mercator_valid());
assert!(!GeoCoord::from_lat_lon(86.0, 17.0).is_web_mercator_valid());
}
#[test]
fn clamped_mercator_positive_overflow() {
let c = GeoCoord {
lat: 89.0,
lon: 200.0,
alt: 42.0,
};
let m = c.clamped_mercator();
assert!(m.lat <= MAX_MERCATOR_LAT);
assert!(m.lon >= -180.0 && m.lon <= 180.0);
assert!((m.lon - (-160.0)).abs() < 1e-10);
assert_eq!(m.alt, 42.0);
}
#[test]
fn clamped_mercator_negative_overflow() {
let c = GeoCoord {
lat: -89.0,
lon: -200.0,
alt: 0.0,
};
let m = c.clamped_mercator();
assert!(m.lat >= -MAX_MERCATOR_LAT);
assert!((m.lon - 160.0).abs() < 1e-10);
}
#[test]
fn clamped_mercator_already_valid() {
let c = GeoCoord::from_lat_lon(51.0, 17.0);
let m = c.clamped_mercator();
assert!((m.lat - 51.0).abs() < 1e-10);
assert!((m.lon - 17.0).abs() < 1e-10);
}
#[test]
fn default_world_coord() {
let c = WorldCoord::default();
assert_eq!(c.position, DVec3::ZERO);
}
#[test]
fn world_coord_display() {
let c = WorldCoord::new(1.0, 2.0, 3.0);
let s = format!("{c}");
assert!(s.contains("1.00"));
assert!(s.contains("2.00"));
assert!(s.contains("3.00"));
assert!(s.ends_with(")m"));
}
#[test]
fn world_coord_from_dvec3() {
let v = DVec3::new(1.0, 2.0, 3.0);
let c: WorldCoord = v.into();
assert_eq!(c.position, v);
let back: DVec3 = c.into();
assert_eq!(back, v);
}
#[test]
fn world_coord_from_array() {
let c: WorldCoord = [10.0, 20.0, 30.0].into();
assert_eq!(c.position.x, 10.0);
assert_eq!(c.position.y, 20.0);
assert_eq!(c.position.z, 30.0);
}
#[test]
fn world_coord_into_array() {
let c = WorldCoord::new(10.0, 20.0, 30.0);
let arr: [f64; 3] = c.into();
assert_eq!(arr, [10.0, 20.0, 30.0]);
}
}