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