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