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