use serde::{Deserialize, Serialize};
#[derive(Debug)]
#[non_exhaustive]
pub enum GeoJsonError {
Serialization(String),
Deserialization(String),
InvalidGeometry(String),
InvalidCoordinates(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum DistanceMetric {
#[default]
Haversine,
Geodesic,
Rhumb,
Euclidean,
}
impl std::fmt::Display for GeoJsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Serialization(msg) => write!(f, "GeoJSON serialization error: {}", msg),
Self::Deserialization(msg) => write!(f, "GeoJSON deserialization error: {}", msg),
Self::InvalidGeometry(msg) => write!(f, "Invalid GeoJSON geometry: {}", msg),
Self::InvalidCoordinates(msg) => write!(f, "Invalid GeoJSON coordinates: {}", msg),
}
}
}
impl std::error::Error for GeoJsonError {}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Point {
inner: geo::Point<f64>,
}
impl Point {
#[inline]
pub fn new(x: f64, y: f64) -> Self {
Self {
inner: geo::Point::new(x, y),
}
}
#[inline]
pub fn x(&self) -> f64 {
self.inner.x()
}
#[inline]
pub fn y(&self) -> f64 {
self.inner.y()
}
#[inline]
pub fn lon(&self) -> f64 {
self.x()
}
#[inline]
pub fn lat(&self) -> f64 {
self.y()
}
#[inline]
pub fn inner(&self) -> &geo::Point<f64> {
&self.inner
}
#[inline]
pub fn into_inner(self) -> geo::Point<f64> {
self.inner
}
#[inline]
pub fn haversine_distance(&self, other: &Point) -> f64 {
use geo::Distance;
geo::Haversine.distance(self.inner, other.inner)
}
#[inline]
pub fn geodesic_distance(&self, other: &Point) -> f64 {
use geo::Distance;
geo::Geodesic.distance(self.inner, other.inner)
}
#[inline]
pub fn euclidean_distance(&self, other: &Point) -> f64 {
use geo::Distance;
geo::Euclidean.distance(self.inner, other.inner)
}
#[cfg(feature = "geojson")]
pub fn to_geojson(&self) -> Result<String, GeoJsonError> {
use geojson::{Geometry, Value};
let geom = Geometry::new(Value::Point(vec![self.x(), self.y()]));
serde_json::to_string(&geom)
.map_err(|e| GeoJsonError::Serialization(format!("Failed to serialize point: {}", e)))
}
#[cfg(feature = "geojson")]
pub fn from_geojson(geojson: &str) -> Result<Self, GeoJsonError> {
use geojson::{Geometry, Value};
let geom: Geometry = serde_json::from_str(geojson).map_err(|e| {
GeoJsonError::Deserialization(format!("Failed to parse GeoJSON: {}", e))
})?;
match geom.value {
Value::Point(coords) => {
if coords.len() < 2 {
return Err(GeoJsonError::InvalidCoordinates(
"Point must have at least 2 coordinates".to_string(),
));
}
Ok(Point::new(coords[0], coords[1]))
}
_ => Err(GeoJsonError::InvalidGeometry(
"GeoJSON geometry is not a Point".to_string(),
)),
}
}
}
impl From<geo::Point<f64>> for Point {
fn from(point: geo::Point<f64>) -> Self {
Self { inner: point }
}
}
impl From<Point> for geo::Point<f64> {
fn from(point: Point) -> Self {
point.inner
}
}
impl From<(f64, f64)> for Point {
fn from((x, y): (f64, f64)) -> Self {
Self::new(x, y)
}
}
impl From<Point> for (f64, f64) {
fn from(point: Point) -> Self {
(point.x(), point.y())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Polygon {
inner: geo::Polygon<f64>,
}
impl Polygon {
pub fn new(exterior: geo::LineString<f64>, interiors: Vec<geo::LineString<f64>>) -> Self {
Self {
inner: geo::Polygon::new(exterior, interiors),
}
}
pub fn from_coords(exterior: &[(f64, f64)], interiors: Vec<Vec<(f64, f64)>>) -> Self {
let exterior_coords: Vec<geo::Coord> =
exterior.iter().map(|&(x, y)| geo::Coord { x, y }).collect();
let exterior_line = geo::LineString::from(exterior_coords);
let interior_lines: Vec<geo::LineString<f64>> = interiors
.into_iter()
.map(|interior| {
let coords: Vec<geo::Coord> = interior
.into_iter()
.map(|(x, y)| geo::Coord { x, y })
.collect();
geo::LineString::from(coords)
})
.collect();
Self::new(exterior_line, interior_lines)
}
#[inline]
pub fn exterior(&self) -> &geo::LineString<f64> {
self.inner.exterior()
}
#[inline]
pub fn interiors(&self) -> &[geo::LineString<f64>] {
self.inner.interiors()
}
#[inline]
pub fn inner(&self) -> &geo::Polygon<f64> {
&self.inner
}
#[inline]
pub fn into_inner(self) -> geo::Polygon<f64> {
self.inner
}
#[inline]
pub fn contains(&self, point: &Point) -> bool {
use geo::Contains;
self.inner.contains(&point.inner)
}
#[cfg(feature = "geojson")]
pub fn to_geojson(&self) -> Result<String, GeoJsonError> {
use geojson::{Geometry, Value};
let mut rings = Vec::new();
let exterior: Vec<Vec<f64>> = self
.exterior()
.coords()
.map(|coord| vec![coord.x, coord.y])
.collect();
rings.push(exterior);
for interior in self.interiors() {
let ring: Vec<Vec<f64>> = interior
.coords()
.map(|coord| vec![coord.x, coord.y])
.collect();
rings.push(ring);
}
let geom = Geometry::new(Value::Polygon(rings));
serde_json::to_string(&geom)
.map_err(|e| GeoJsonError::Serialization(format!("Failed to serialize polygon: {}", e)))
}
#[cfg(feature = "geojson")]
pub fn from_geojson(geojson: &str) -> Result<Self, GeoJsonError> {
use geojson::{Geometry, Value};
let geom: Geometry = serde_json::from_str(geojson).map_err(|e| {
GeoJsonError::Deserialization(format!("Failed to parse GeoJSON: {}", e))
})?;
match geom.value {
Value::Polygon(rings) => {
if rings.is_empty() {
return Err(GeoJsonError::InvalidCoordinates(
"Polygon must have at least one ring".to_string(),
));
}
let exterior: Result<Vec<geo::Coord>, GeoJsonError> = rings[0]
.iter()
.map(|coords| {
if coords.len() < 2 {
return Err(GeoJsonError::InvalidCoordinates(
"Coordinate must have at least 2 values".to_string(),
));
}
Ok(geo::Coord {
x: coords[0],
y: coords[1],
})
})
.collect();
let exterior_coords = exterior?;
let exterior_line = geo::LineString::from(exterior_coords);
let mut interiors = Vec::new();
for ring in rings.iter().skip(1) {
let interior: Result<Vec<geo::Coord>, GeoJsonError> = ring
.iter()
.map(|coords| {
if coords.len() < 2 {
return Err(GeoJsonError::InvalidCoordinates(
"Coordinate must have at least 2 values".to_string(),
));
}
Ok(geo::Coord {
x: coords[0],
y: coords[1],
})
})
.collect();
let interior_coords = interior?;
interiors.push(geo::LineString::from(interior_coords));
}
Ok(Polygon::new(exterior_line, interiors))
}
_ => Err(GeoJsonError::InvalidGeometry(
"GeoJSON geometry is not a Polygon".to_string(),
)),
}
}
}
impl From<geo::Polygon<f64>> for Polygon {
fn from(polygon: geo::Polygon<f64>) -> Self {
Self { inner: polygon }
}
}
impl From<Polygon> for geo::Polygon<f64> {
fn from(polygon: Polygon) -> Self {
polygon.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point_creation() {
let point = Point::new(-74.0060, 40.7128);
assert_eq!(point.x(), -74.0060);
assert_eq!(point.y(), 40.7128);
assert_eq!(point.lon(), -74.0060);
assert_eq!(point.lat(), 40.7128);
}
#[test]
fn test_point_from_tuple() {
let point: Point = (-74.0060, 40.7128).into();
assert_eq!(point.x(), -74.0060);
assert_eq!(point.y(), 40.7128);
}
#[test]
fn test_point_to_tuple() {
let point = Point::new(-74.0060, 40.7128);
let (x, y): (f64, f64) = point.into();
assert_eq!(x, -74.0060);
assert_eq!(y, 40.7128);
}
#[test]
fn test_point_haversine_distance() {
let nyc = Point::new(-74.0060, 40.7128);
let la = Point::new(-118.2437, 34.0522);
let distance = nyc.haversine_distance(&la);
assert!(distance > 3_900_000.0 && distance < 4_000_000.0);
}
#[test]
fn test_point_euclidean_distance() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(3.0, 4.0);
let distance = p1.euclidean_distance(&p2);
assert_eq!(distance, 5.0);
}
#[test]
fn test_polygon_creation() {
use geo::polygon;
let poly = polygon![
(x: -80.0, y: 35.0),
(x: -70.0, y: 35.0),
(x: -70.0, y: 45.0),
(x: -80.0, y: 45.0),
(x: -80.0, y: 35.0),
];
let polygon = Polygon::from(poly);
assert_eq!(polygon.exterior().coords().count(), 5);
assert_eq!(polygon.interiors().len(), 0);
}
#[test]
fn test_polygon_contains() {
use geo::polygon;
let poly = polygon![
(x: -80.0, y: 35.0),
(x: -70.0, y: 35.0),
(x: -70.0, y: 45.0),
(x: -80.0, y: 45.0),
(x: -80.0, y: 35.0),
];
let polygon = Polygon::from(poly);
let inside = Point::new(-75.0, 40.0);
let outside = Point::new(-85.0, 40.0);
assert!(polygon.contains(&inside));
assert!(!polygon.contains(&outside));
}
#[cfg(feature = "geojson")]
#[test]
fn test_point_geojson_roundtrip() {
let original = Point::new(-74.0060, 40.7128);
let json = original.to_geojson().unwrap();
let parsed = Point::from_geojson(&json).unwrap();
assert!((original.x() - parsed.x()).abs() < 1e-10);
assert!((original.y() - parsed.y()).abs() < 1e-10);
}
#[cfg(feature = "geojson")]
#[test]
fn test_polygon_geojson_roundtrip() {
use geo::polygon;
let poly = polygon![
(x: -80.0, y: 35.0),
(x: -70.0, y: 35.0),
(x: -70.0, y: 45.0),
(x: -80.0, y: 45.0),
(x: -80.0, y: 35.0),
];
let original = Polygon::from(poly);
let json = original.to_geojson().unwrap();
let parsed = Polygon::from_geojson(&json).unwrap();
assert_eq!(
original.exterior().coords().count(),
parsed.exterior().coords().count()
);
}
}