use std::fmt;
use serde::{Deserialize, Serialize};
use crate::geo::GeoError;
use crate::geo::ewkb;
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct GeoPoint {
pub lat: f64,
pub lon: f64,
}
impl GeoPoint {
pub const MIN_LAT: f64 = -90.0;
pub const MAX_LAT: f64 = 90.0;
pub const MIN_LON: f64 = -180.0;
pub const MAX_LON: f64 = 180.0;
pub fn new(lat: f64, lon: f64) -> Result<Self, GeoError> {
if !lat.is_finite() || !(Self::MIN_LAT..=Self::MAX_LAT).contains(&lat) {
return Err(GeoError::InvalidLatitude(lat));
}
if !lon.is_finite() || !(Self::MIN_LON..=Self::MAX_LON).contains(&lon) {
return Err(GeoError::InvalidLongitude(lon));
}
Ok(Self { lat, lon })
}
pub fn to_ewkb_bytes(self) -> Vec<u8> {
let mut buf = Vec::with_capacity(ewkb::EWKB_LEN);
ewkb::encode_point_into(&self, &mut buf);
buf
}
pub fn from_ewkb_bytes(bytes: &[u8]) -> Result<Self, GeoError> {
let (lon, lat) = ewkb::decode_point(bytes)?;
Self::new(lat, lon)
}
pub fn distance_to(self, other: Self) -> f64 {
const R: f64 = 6_371_000.0;
let d_lat = (other.lat - self.lat).to_radians();
let d_lon = (other.lon - self.lon).to_radians();
let lat1 = self.lat.to_radians();
let lat2 = other.lat.to_radians();
let a = (d_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (d_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
R * c
}
}
impl fmt::Display for GeoPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "POINT({} {})", self.lon, self.lat)
}
}
impl<'de> Deserialize<'de> for GeoPoint {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Raw {
lat: f64,
lon: f64,
}
let raw = Raw::deserialize(deserializer)?;
GeoPoint::new(raw.lat, raw.lon).map_err(serde::de::Error::custom)
}
}
crate::geo::impl_geography_codec!(GeoPoint, ewkb::encode_point_into);
#[cfg(all(test, feature = "spatial"))]
mod tests {
use super::*;
use crate::geo::GeoError;
#[test]
fn geopoint_new_validates_latitude() {
assert!(GeoPoint::new(90.0, 0.0).is_ok());
assert!(GeoPoint::new(-90.0, 0.0).is_ok());
assert!(GeoPoint::new(0.0, 0.0).is_ok());
assert!(matches!(
GeoPoint::new(91.0, 0.0),
Err(GeoError::InvalidLatitude(_))
));
assert!(matches!(
GeoPoint::new(-91.0, 0.0),
Err(GeoError::InvalidLatitude(_))
));
assert!(matches!(
GeoPoint::new(f64::NAN, 0.0),
Err(GeoError::InvalidLatitude(_))
));
assert!(matches!(
GeoPoint::new(f64::INFINITY, 0.0),
Err(GeoError::InvalidLatitude(_))
));
assert!(matches!(
GeoPoint::new(f64::NEG_INFINITY, 0.0),
Err(GeoError::InvalidLatitude(_))
));
}
#[test]
fn geopoint_new_validates_longitude() {
assert!(GeoPoint::new(0.0, 180.0).is_ok());
assert!(GeoPoint::new(0.0, -180.0).is_ok());
assert!(GeoPoint::new(0.0, 0.0).is_ok());
assert!(matches!(
GeoPoint::new(0.0, 181.0),
Err(GeoError::InvalidLongitude(_))
));
assert!(matches!(
GeoPoint::new(0.0, -181.0),
Err(GeoError::InvalidLongitude(_))
));
assert!(matches!(
GeoPoint::new(0.0, f64::NAN),
Err(GeoError::InvalidLongitude(_))
));
assert!(matches!(
GeoPoint::new(0.0, f64::INFINITY),
Err(GeoError::InvalidLongitude(_))
));
assert!(matches!(
GeoPoint::new(0.0, f64::NEG_INFINITY),
Err(GeoError::InvalidLongitude(_))
));
}
#[test]
fn ewkb_round_trip() {
let p = GeoPoint::new(37.7749, -122.4194).unwrap();
let bytes = p.to_ewkb_bytes();
assert_eq!(bytes.len(), 25, "EWKB buffer must be 25 bytes");
let decoded = GeoPoint::from_ewkb_bytes(&bytes).unwrap();
assert!((decoded.lat - p.lat).abs() < 1e-9);
assert!((decoded.lon - p.lon).abs() < 1e-9);
}
#[test]
fn ewkb_rejects_malformed() {
assert!(GeoPoint::from_ewkb_bytes(&[]).is_err());
let short = vec![0u8; 24];
assert!(GeoPoint::from_ewkb_bytes(&short).is_err());
let long = vec![0u8; 26];
assert!(GeoPoint::from_ewkb_bytes(&long).is_err());
let mut bad_endian = GeoPoint::new(0.0, 0.0).unwrap().to_ewkb_bytes();
bad_endian[0] = 0x02;
assert!(GeoPoint::from_ewkb_bytes(&bad_endian).is_err());
let mut bad_srid = GeoPoint::new(0.0, 0.0).unwrap().to_ewkb_bytes();
let srid_4269 = 4269u32.to_le_bytes();
bad_srid[5] = srid_4269[0];
bad_srid[6] = srid_4269[1];
bad_srid[7] = srid_4269[2];
bad_srid[8] = srid_4269[3];
assert!(
matches!(
GeoPoint::from_ewkb_bytes(&bad_srid),
Err(GeoError::UnexpectedSrid(4269))
),
"expected UnexpectedSrid(4269)"
);
}
#[test]
fn haversine_sanity() {
let sfo = GeoPoint::new(37.6189, -122.3750).unwrap();
let jfk = GeoPoint::new(40.6413, -73.7781).unwrap();
let dist_km = sfo.distance_to(jfk) / 1000.0;
assert!(
(dist_km - 4151.0).abs() < 50.0,
"SFO→JFK Haversine = {dist_km:.1} km, expected ~4151 km"
);
}
#[test]
fn wkt_format() {
let p = GeoPoint::new(37.7749, -122.4194).unwrap();
assert_eq!(format!("{p}"), "POINT(-122.4194 37.7749)");
}
#[test]
fn serde_round_trip() {
let original = GeoPoint::new(37.7749, -122.4194).unwrap();
let json = serde_json::to_string(&original).unwrap();
let decoded: GeoPoint = serde_json::from_str(&json).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn to_sql_writes_ewkb_bytes() {
use bytes::BytesMut;
use postgres_types::{ToSql, Type};
let p = GeoPoint::new(51.5074, -0.1278).unwrap();
let expected = p.to_ewkb_bytes();
assert_eq!(expected.len(), 25);
let mut out = BytesMut::new();
p.to_sql(&Type::BYTEA, &mut out).unwrap();
assert_eq!(out.as_ref(), expected.as_slice());
}
}