Skip to main content

spatio_types/
geo.rs

1//! Wrapped geometric types from the `geo` crate with spatio-specific functionality.
2//!
3//! This module provides wrapper types around `geo` crate primitives with additional
4//! methods for GeoJSON serialization, distance calculations, and other spatial operations.
5
6use serde::{Deserialize, Serialize};
7
8/// Error type for GeoJSON conversions.
9#[derive(Debug)]
10#[non_exhaustive]
11pub enum GeoJsonError {
12    /// Serialization failed
13    Serialization(String),
14    /// Deserialization failed
15    Deserialization(String),
16    /// Invalid geometry type
17    InvalidGeometry(String),
18    /// Invalid coordinates
19    InvalidCoordinates(String),
20}
21
22/// Distance metric for spatial calculations.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
24pub enum DistanceMetric {
25    /// Great-circle distance on a sphere, in **meters**. Fast; good default.
26    #[default]
27    Haversine,
28    /// Geodesic distance on the WGS-84 ellipsoid, in **meters**. Most accurate.
29    Geodesic,
30    /// Rhumb-line (constant-bearing) distance, in **meters**.
31    Rhumb,
32    /// Planar Euclidean distance in **coordinate degrees**, not meters — only
33    /// meaningful for projected/local coordinates, not lon/lat.
34    Euclidean,
35}
36
37impl std::fmt::Display for GeoJsonError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Serialization(msg) => write!(f, "GeoJSON serialization error: {}", msg),
41            Self::Deserialization(msg) => write!(f, "GeoJSON deserialization error: {}", msg),
42            Self::InvalidGeometry(msg) => write!(f, "Invalid GeoJSON geometry: {}", msg),
43            Self::InvalidCoordinates(msg) => write!(f, "Invalid GeoJSON coordinates: {}", msg),
44        }
45    }
46}
47
48impl std::error::Error for GeoJsonError {}
49
50/// A geographic point with longitude/latitude coordinates.
51///
52/// This wraps `geo::Point` and provides additional functionality for
53/// GeoJSON conversion, distance calculations, and other operations.
54///
55/// # Examples
56///
57/// ```
58/// use spatio_types::geo::Point;
59///
60/// let nyc = Point::new(-74.0060, 40.7128);
61/// assert_eq!(nyc.x(), -74.0060);
62/// assert_eq!(nyc.y(), 40.7128);
63/// ```
64#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
65pub struct Point {
66    inner: geo::Point<f64>,
67}
68
69impl Point {
70    /// Create a new point from x (longitude) and y (latitude) coordinates.
71    ///
72    /// # Arguments
73    ///
74    /// * `x` - Longitude in degrees (typically -180 to 180)
75    /// * `y` - Latitude in degrees (typically -90 to 90)
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use spatio_types::geo::Point;
81    ///
82    /// let point = Point::new(-74.0060, 40.7128);
83    /// ```
84    #[inline]
85    pub fn new(x: f64, y: f64) -> Self {
86        Self {
87            inner: geo::Point::new(x, y),
88        }
89    }
90
91    /// Get the x coordinate (longitude).
92    #[inline]
93    pub fn x(&self) -> f64 {
94        self.inner.x()
95    }
96
97    /// Get the y coordinate (latitude).
98    #[inline]
99    pub fn y(&self) -> f64 {
100        self.inner.y()
101    }
102
103    /// Get the longitude (alias for x).
104    #[inline]
105    pub fn lon(&self) -> f64 {
106        self.x()
107    }
108
109    /// Get the latitude (alias for y).
110    #[inline]
111    pub fn lat(&self) -> f64 {
112        self.y()
113    }
114
115    /// Access the inner `geo::Point`.
116    #[inline]
117    pub fn inner(&self) -> &geo::Point<f64> {
118        &self.inner
119    }
120
121    /// Convert into the inner `geo::Point`.
122    #[inline]
123    pub fn into_inner(self) -> geo::Point<f64> {
124        self.inner
125    }
126
127    /// Calculate haversine distance to another point in meters.
128    ///
129    /// Uses the haversine formula which accounts for Earth's curvature.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use spatio_types::geo::Point;
135    ///
136    /// let nyc = Point::new(-74.0060, 40.7128);
137    /// let la = Point::new(-118.2437, 34.0522);
138    /// let distance = nyc.haversine_distance(&la);
139    /// assert!(distance > 3_900_000.0); // ~3,944 km
140    /// ```
141    #[inline]
142    pub fn haversine_distance(&self, other: &Point) -> f64 {
143        use geo::Distance;
144        geo::Haversine.distance(self.inner, other.inner)
145    }
146
147    /// Calculate geodesic distance to another point in meters.
148    ///
149    /// Uses the `geo` crate's geodesic algorithm (Karney), which models the
150    /// Earth as an ellipsoid and is more accurate than haversine but slightly
151    /// slower.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use spatio_types::geo::Point;
157    ///
158    /// let p1 = Point::new(-74.0060, 40.7128);
159    /// let p2 = Point::new(-74.0070, 40.7138);
160    /// let distance = p1.geodesic_distance(&p2);
161    /// ```
162    #[inline]
163    pub fn geodesic_distance(&self, other: &Point) -> f64 {
164        use geo::Distance;
165        geo::Geodesic.distance(self.inner, other.inner)
166    }
167
168    /// Calculate euclidean distance to another point.
169    ///
170    /// This calculates straight-line distance in the coordinate space,
171    /// which is only accurate for small distances.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use spatio_types::geo::Point;
177    ///
178    /// let p1 = Point::new(0.0, 0.0);
179    /// let p2 = Point::new(3.0, 4.0);
180    /// let distance = p1.euclidean_distance(&p2);
181    /// assert_eq!(distance, 5.0); // 3-4-5 triangle
182    /// ```
183    #[inline]
184    pub fn euclidean_distance(&self, other: &Point) -> f64 {
185        use geo::Distance;
186        geo::Euclidean.distance(self.inner, other.inner)
187    }
188
189    /// Convert to GeoJSON string representation.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// # #[cfg(feature = "geojson")]
195    /// # {
196    /// use spatio_types::geo::Point;
197    ///
198    /// let point = Point::new(-74.0060, 40.7128);
199    /// let json = point.to_geojson().unwrap();
200    /// assert!(json.contains("Point"));
201    /// # }
202    /// ```
203    #[cfg(feature = "geojson")]
204    pub fn to_geojson(&self) -> Result<String, GeoJsonError> {
205        use geojson::{Geometry, Value};
206
207        let geom = Geometry::new(Value::Point(vec![self.x(), self.y()]));
208        serde_json::to_string(&geom)
209            .map_err(|e| GeoJsonError::Serialization(format!("Failed to serialize point: {}", e)))
210    }
211
212    /// Parse from GeoJSON string.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// # #[cfg(feature = "geojson")]
218    /// # {
219    /// use spatio_types::geo::Point;
220    ///
221    /// let json = r#"{"type":"Point","coordinates":[-74.006,40.7128]}"#;
222    /// let point = Point::from_geojson(json).unwrap();
223    /// assert_eq!(point.x(), -74.006);
224    /// # }
225    /// ```
226    #[cfg(feature = "geojson")]
227    pub fn from_geojson(geojson: &str) -> Result<Self, GeoJsonError> {
228        use geojson::{Geometry, Value};
229
230        let geom: Geometry = serde_json::from_str(geojson).map_err(|e| {
231            GeoJsonError::Deserialization(format!("Failed to parse GeoJSON: {}", e))
232        })?;
233
234        match geom.value {
235            Value::Point(coords) => {
236                if coords.len() < 2 {
237                    return Err(GeoJsonError::InvalidCoordinates(
238                        "Point must have at least 2 coordinates".to_string(),
239                    ));
240                }
241                Ok(Point::new(coords[0], coords[1]))
242            }
243            _ => Err(GeoJsonError::InvalidGeometry(
244                "GeoJSON geometry is not a Point".to_string(),
245            )),
246        }
247    }
248}
249
250impl From<geo::Point<f64>> for Point {
251    fn from(point: geo::Point<f64>) -> Self {
252        Self { inner: point }
253    }
254}
255
256impl From<Point> for geo::Point<f64> {
257    fn from(point: Point) -> Self {
258        point.inner
259    }
260}
261
262impl From<(f64, f64)> for Point {
263    fn from((x, y): (f64, f64)) -> Self {
264        Self::new(x, y)
265    }
266}
267
268impl From<Point> for (f64, f64) {
269    fn from(point: Point) -> Self {
270        (point.x(), point.y())
271    }
272}
273
274/// A polygon with exterior and optional interior rings.
275///
276/// This wraps `geo::Polygon` and provides additional functionality for
277/// GeoJSON conversion and spatial operations.
278///
279/// # Examples
280///
281/// ```
282/// use spatio_types::geo::Polygon;
283/// use geo::polygon;
284///
285/// let poly = polygon![
286///     (x: -80.0, y: 35.0),
287///     (x: -70.0, y: 35.0),
288///     (x: -70.0, y: 45.0),
289///     (x: -80.0, y: 45.0),
290///     (x: -80.0, y: 35.0),
291/// ];
292/// let wrapped = Polygon::from(poly);
293/// ```
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub struct Polygon {
296    inner: geo::Polygon<f64>,
297}
298
299impl Polygon {
300    /// Create a new polygon from an exterior ring and optional interior rings (holes).
301    ///
302    /// # Arguments
303    ///
304    /// * `exterior` - The outer boundary of the polygon
305    /// * `interiors` - Optional holes within the polygon
306    pub fn new(exterior: geo::LineString<f64>, interiors: Vec<geo::LineString<f64>>) -> Self {
307        Self {
308            inner: geo::Polygon::new(exterior, interiors),
309        }
310    }
311
312    /// Create a new polygon from coordinate arrays without requiring `geo::LineString`.
313    ///
314    /// This is a convenience method that allows creating polygons from raw coordinates
315    /// without needing to import types from the `geo` crate.
316    ///
317    /// # Arguments
318    ///
319    /// * `exterior` - Coordinates for the outer boundary [(lon, lat), ...]
320    /// * `interiors` - Optional holes within the polygon, each as [(lon, lat), ...]
321    ///
322    /// # Examples
323    ///
324    /// ```
325    /// use spatio_types::geo::Polygon;
326    ///
327    /// // Create a simple rectangle
328    /// let polygon = Polygon::from_coords(
329    ///     &[
330    ///         (-80.0, 35.0),
331    ///         (-70.0, 35.0),
332    ///         (-70.0, 45.0),
333    ///         (-80.0, 45.0),
334    ///         (-80.0, 35.0),  // Close the ring
335    ///     ],
336    ///     vec![],
337    /// );
338    ///
339    /// // Create a polygon with a hole
340    /// let polygon_with_hole = Polygon::from_coords(
341    ///     &[
342    ///         (-80.0, 35.0),
343    ///         (-70.0, 35.0),
344    ///         (-70.0, 45.0),
345    ///         (-80.0, 45.0),
346    ///         (-80.0, 35.0),
347    ///     ],
348    ///     vec![
349    ///         vec![
350    ///             (-75.0, 38.0),
351    ///             (-74.0, 38.0),
352    ///             (-74.0, 40.0),
353    ///             (-75.0, 40.0),
354    ///             (-75.0, 38.0),
355    ///         ]
356    ///     ],
357    /// );
358    /// ```
359    pub fn from_coords(exterior: &[(f64, f64)], interiors: Vec<Vec<(f64, f64)>>) -> Self {
360        let exterior_coords: Vec<geo::Coord> =
361            exterior.iter().map(|&(x, y)| geo::Coord { x, y }).collect();
362        let exterior_line = geo::LineString::from(exterior_coords);
363
364        let interior_lines: Vec<geo::LineString<f64>> = interiors
365            .into_iter()
366            .map(|interior| {
367                let coords: Vec<geo::Coord> = interior
368                    .into_iter()
369                    .map(|(x, y)| geo::Coord { x, y })
370                    .collect();
371                geo::LineString::from(coords)
372            })
373            .collect();
374
375        Self::new(exterior_line, interior_lines)
376    }
377
378    /// Get a reference to the exterior ring.
379    #[inline]
380    pub fn exterior(&self) -> &geo::LineString<f64> {
381        self.inner.exterior()
382    }
383
384    /// Get references to the interior rings (holes).
385    #[inline]
386    pub fn interiors(&self) -> &[geo::LineString<f64>] {
387        self.inner.interiors()
388    }
389
390    /// Access the inner `geo::Polygon`.
391    #[inline]
392    pub fn inner(&self) -> &geo::Polygon<f64> {
393        &self.inner
394    }
395
396    /// Convert into the inner `geo::Polygon`.
397    #[inline]
398    pub fn into_inner(self) -> geo::Polygon<f64> {
399        self.inner
400    }
401
402    /// Check if a point is contained within this polygon.
403    ///
404    /// # Examples
405    ///
406    /// ```
407    /// use spatio_types::geo::{Point, Polygon};
408    /// use geo::polygon;
409    ///
410    /// let poly = polygon![
411    ///     (x: -80.0, y: 35.0),
412    ///     (x: -70.0, y: 35.0),
413    ///     (x: -70.0, y: 45.0),
414    ///     (x: -80.0, y: 45.0),
415    ///     (x: -80.0, y: 35.0),
416    /// ];
417    /// let polygon = Polygon::from(poly);
418    /// let point = Point::new(-75.0, 40.0);
419    /// assert!(polygon.contains(&point));
420    /// ```
421    #[inline]
422    pub fn contains(&self, point: &Point) -> bool {
423        use geo::Contains;
424        self.inner.contains(&point.inner)
425    }
426
427    /// Convert to GeoJSON string representation.
428    ///
429    /// # Examples
430    ///
431    /// ```
432    /// # #[cfg(feature = "geojson")]
433    /// # {
434    /// use spatio_types::geo::Polygon;
435    /// use geo::polygon;
436    ///
437    /// let poly = polygon![
438    ///     (x: -80.0, y: 35.0),
439    ///     (x: -70.0, y: 35.0),
440    ///     (x: -70.0, y: 45.0),
441    ///     (x: -80.0, y: 45.0),
442    ///     (x: -80.0, y: 35.0),
443    /// ];
444    /// let polygon = Polygon::from(poly);
445    /// let json = polygon.to_geojson().unwrap();
446    /// assert!(json.contains("Polygon"));
447    /// # }
448    /// ```
449    #[cfg(feature = "geojson")]
450    pub fn to_geojson(&self) -> Result<String, GeoJsonError> {
451        use geojson::{Geometry, Value};
452
453        let mut rings = Vec::new();
454
455        let exterior: Vec<Vec<f64>> = self
456            .exterior()
457            .coords()
458            .map(|coord| vec![coord.x, coord.y])
459            .collect();
460        rings.push(exterior);
461
462        for interior in self.interiors() {
463            let ring: Vec<Vec<f64>> = interior
464                .coords()
465                .map(|coord| vec![coord.x, coord.y])
466                .collect();
467            rings.push(ring);
468        }
469
470        let geom = Geometry::new(Value::Polygon(rings));
471
472        serde_json::to_string(&geom)
473            .map_err(|e| GeoJsonError::Serialization(format!("Failed to serialize polygon: {}", e)))
474    }
475
476    /// Parse from GeoJSON string.
477    ///
478    /// # Examples
479    ///
480    /// ```
481    /// # #[cfg(feature = "geojson")]
482    /// # {
483    /// use spatio_types::geo::Polygon;
484    ///
485    /// let json = r#"{"type":"Polygon","coordinates":[[[-80.0,35.0],[-70.0,35.0],[-70.0,45.0],[-80.0,45.0],[-80.0,35.0]]]}"#;
486    /// let polygon = Polygon::from_geojson(json).unwrap();
487    /// assert_eq!(polygon.exterior().coords().count(), 5);
488    /// # }
489    /// ```
490    #[cfg(feature = "geojson")]
491    pub fn from_geojson(geojson: &str) -> Result<Self, GeoJsonError> {
492        use geojson::{Geometry, Value};
493
494        let geom: Geometry = serde_json::from_str(geojson).map_err(|e| {
495            GeoJsonError::Deserialization(format!("Failed to parse GeoJSON: {}", e))
496        })?;
497
498        match geom.value {
499            Value::Polygon(rings) => {
500                if rings.is_empty() {
501                    return Err(GeoJsonError::InvalidCoordinates(
502                        "Polygon must have at least one ring".to_string(),
503                    ));
504                }
505
506                let exterior: Result<Vec<geo::Coord>, GeoJsonError> = rings[0]
507                    .iter()
508                    .map(|coords| {
509                        if coords.len() < 2 {
510                            return Err(GeoJsonError::InvalidCoordinates(
511                                "Coordinate must have at least 2 values".to_string(),
512                            ));
513                        }
514                        Ok(geo::Coord {
515                            x: coords[0],
516                            y: coords[1],
517                        })
518                    })
519                    .collect();
520
521                let exterior_coords = exterior?;
522                let exterior_line = geo::LineString::from(exterior_coords);
523
524                let mut interiors = Vec::new();
525                for ring in rings.iter().skip(1) {
526                    let interior: Result<Vec<geo::Coord>, GeoJsonError> = ring
527                        .iter()
528                        .map(|coords| {
529                            if coords.len() < 2 {
530                                return Err(GeoJsonError::InvalidCoordinates(
531                                    "Coordinate must have at least 2 values".to_string(),
532                                ));
533                            }
534                            Ok(geo::Coord {
535                                x: coords[0],
536                                y: coords[1],
537                            })
538                        })
539                        .collect();
540                    let interior_coords = interior?;
541                    interiors.push(geo::LineString::from(interior_coords));
542                }
543
544                Ok(Polygon::new(exterior_line, interiors))
545            }
546            _ => Err(GeoJsonError::InvalidGeometry(
547                "GeoJSON geometry is not a Polygon".to_string(),
548            )),
549        }
550    }
551}
552
553impl From<geo::Polygon<f64>> for Polygon {
554    fn from(polygon: geo::Polygon<f64>) -> Self {
555        Self { inner: polygon }
556    }
557}
558
559impl From<Polygon> for geo::Polygon<f64> {
560    fn from(polygon: Polygon) -> Self {
561        polygon.inner
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568
569    #[test]
570    fn test_point_creation() {
571        let point = Point::new(-74.0060, 40.7128);
572        assert_eq!(point.x(), -74.0060);
573        assert_eq!(point.y(), 40.7128);
574        assert_eq!(point.lon(), -74.0060);
575        assert_eq!(point.lat(), 40.7128);
576    }
577
578    #[test]
579    fn test_point_from_tuple() {
580        let point: Point = (-74.0060, 40.7128).into();
581        assert_eq!(point.x(), -74.0060);
582        assert_eq!(point.y(), 40.7128);
583    }
584
585    #[test]
586    fn test_point_to_tuple() {
587        let point = Point::new(-74.0060, 40.7128);
588        let (x, y): (f64, f64) = point.into();
589        assert_eq!(x, -74.0060);
590        assert_eq!(y, 40.7128);
591    }
592
593    #[test]
594    fn test_point_haversine_distance() {
595        let nyc = Point::new(-74.0060, 40.7128);
596        let la = Point::new(-118.2437, 34.0522);
597        let distance = nyc.haversine_distance(&la);
598        // Distance NYC to LA is approximately 3,944 km
599        assert!(distance > 3_900_000.0 && distance < 4_000_000.0);
600    }
601
602    #[test]
603    fn test_point_euclidean_distance() {
604        let p1 = Point::new(0.0, 0.0);
605        let p2 = Point::new(3.0, 4.0);
606        let distance = p1.euclidean_distance(&p2);
607        assert_eq!(distance, 5.0);
608    }
609
610    #[test]
611    fn test_polygon_creation() {
612        use geo::polygon;
613
614        let poly = polygon![
615            (x: -80.0, y: 35.0),
616            (x: -70.0, y: 35.0),
617            (x: -70.0, y: 45.0),
618            (x: -80.0, y: 45.0),
619            (x: -80.0, y: 35.0),
620        ];
621        let polygon = Polygon::from(poly);
622        assert_eq!(polygon.exterior().coords().count(), 5);
623        assert_eq!(polygon.interiors().len(), 0);
624    }
625
626    #[test]
627    fn test_polygon_contains() {
628        use geo::polygon;
629
630        let poly = polygon![
631            (x: -80.0, y: 35.0),
632            (x: -70.0, y: 35.0),
633            (x: -70.0, y: 45.0),
634            (x: -80.0, y: 45.0),
635            (x: -80.0, y: 35.0),
636        ];
637        let polygon = Polygon::from(poly);
638
639        let inside = Point::new(-75.0, 40.0);
640        let outside = Point::new(-85.0, 40.0);
641
642        assert!(polygon.contains(&inside));
643        assert!(!polygon.contains(&outside));
644    }
645
646    #[cfg(feature = "geojson")]
647    #[test]
648    fn test_point_geojson_roundtrip() {
649        let original = Point::new(-74.0060, 40.7128);
650        let json = original.to_geojson().unwrap();
651        let parsed = Point::from_geojson(&json).unwrap();
652
653        assert!((original.x() - parsed.x()).abs() < 1e-10);
654        assert!((original.y() - parsed.y()).abs() < 1e-10);
655    }
656
657    #[cfg(feature = "geojson")]
658    #[test]
659    fn test_polygon_geojson_roundtrip() {
660        use geo::polygon;
661
662        let poly = polygon![
663            (x: -80.0, y: 35.0),
664            (x: -70.0, y: 35.0),
665            (x: -70.0, y: 45.0),
666            (x: -80.0, y: 45.0),
667            (x: -80.0, y: 35.0),
668        ];
669        let original = Polygon::from(poly);
670        let json = original.to_geojson().unwrap();
671        let parsed = Polygon::from_geojson(&json).unwrap();
672
673        assert_eq!(
674            original.exterior().coords().count(),
675            parsed.exterior().coords().count()
676        );
677    }
678}