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    /// Get a reference to the exterior ring.
296    #[inline]
297    pub fn exterior(&self) -> &geo::LineString<f64> {
298        self.inner.exterior()
299    }
300
301    /// Get references to the interior rings (holes).
302    #[inline]
303    pub fn interiors(&self) -> &[geo::LineString<f64>] {
304        self.inner.interiors()
305    }
306
307    /// Access the inner `geo::Polygon`.
308    #[inline]
309    pub fn inner(&self) -> &geo::Polygon<f64> {
310        &self.inner
311    }
312
313    /// Convert into the inner `geo::Polygon`.
314    #[inline]
315    pub fn into_inner(self) -> geo::Polygon<f64> {
316        self.inner
317    }
318
319    /// Check if a point is contained within this polygon.
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use spatio_types::geo::{Point, Polygon};
325    /// use geo::polygon;
326    ///
327    /// let poly = polygon![
328    ///     (x: -80.0, y: 35.0),
329    ///     (x: -70.0, y: 35.0),
330    ///     (x: -70.0, y: 45.0),
331    ///     (x: -80.0, y: 45.0),
332    ///     (x: -80.0, y: 35.0),
333    /// ];
334    /// let polygon = Polygon::from(poly);
335    /// let point = Point::new(-75.0, 40.0);
336    /// assert!(polygon.contains(&point));
337    /// ```
338    #[inline]
339    pub fn contains(&self, point: &Point) -> bool {
340        use geo::Contains;
341        self.inner.contains(&point.inner)
342    }
343
344    /// Convert to GeoJSON string representation.
345    ///
346    /// # Examples
347    ///
348    /// ```
349    /// # #[cfg(feature = "geojson")]
350    /// # {
351    /// use spatio_types::geo::Polygon;
352    /// use geo::polygon;
353    ///
354    /// let poly = polygon![
355    ///     (x: -80.0, y: 35.0),
356    ///     (x: -70.0, y: 35.0),
357    ///     (x: -70.0, y: 45.0),
358    ///     (x: -80.0, y: 45.0),
359    ///     (x: -80.0, y: 35.0),
360    /// ];
361    /// let polygon = Polygon::from(poly);
362    /// let json = polygon.to_geojson().unwrap();
363    /// assert!(json.contains("Polygon"));
364    /// # }
365    /// ```
366    #[cfg(feature = "geojson")]
367    pub fn to_geojson(&self) -> Result<String, GeoJsonError> {
368        use geojson::{Geometry, Value};
369
370        let mut rings = Vec::new();
371
372        let exterior: Vec<Vec<f64>> = self
373            .exterior()
374            .coords()
375            .map(|coord| vec![coord.x, coord.y])
376            .collect();
377        rings.push(exterior);
378
379        for interior in self.interiors() {
380            let ring: Vec<Vec<f64>> = interior
381                .coords()
382                .map(|coord| vec![coord.x, coord.y])
383                .collect();
384            rings.push(ring);
385        }
386
387        let geom = Geometry::new(Value::Polygon(rings));
388
389        serde_json::to_string(&geom)
390            .map_err(|e| GeoJsonError::Serialization(format!("Failed to serialize polygon: {}", e)))
391    }
392
393    /// Parse from GeoJSON string.
394    ///
395    /// # Examples
396    ///
397    /// ```
398    /// # #[cfg(feature = "geojson")]
399    /// # {
400    /// use spatio_types::geo::Polygon;
401    ///
402    /// 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]]]}"#;
403    /// let polygon = Polygon::from_geojson(json).unwrap();
404    /// assert_eq!(polygon.exterior().coords().count(), 5);
405    /// # }
406    /// ```
407    #[cfg(feature = "geojson")]
408    pub fn from_geojson(geojson: &str) -> Result<Self, GeoJsonError> {
409        use geojson::{Geometry, Value};
410
411        let geom: Geometry = serde_json::from_str(geojson).map_err(|e| {
412            GeoJsonError::Deserialization(format!("Failed to parse GeoJSON: {}", e))
413        })?;
414
415        match geom.value {
416            Value::Polygon(rings) => {
417                if rings.is_empty() {
418                    return Err(GeoJsonError::InvalidCoordinates(
419                        "Polygon must have at least one ring".to_string(),
420                    ));
421                }
422
423                let exterior: Result<Vec<geo::Coord>, GeoJsonError> = rings[0]
424                    .iter()
425                    .map(|coords| {
426                        if coords.len() < 2 {
427                            return Err(GeoJsonError::InvalidCoordinates(
428                                "Coordinate must have at least 2 values".to_string(),
429                            ));
430                        }
431                        Ok(geo::Coord {
432                            x: coords[0],
433                            y: coords[1],
434                        })
435                    })
436                    .collect();
437
438                let exterior_coords = exterior?;
439                let exterior_line = geo::LineString::from(exterior_coords);
440
441                let mut interiors = Vec::new();
442                for ring in rings.iter().skip(1) {
443                    let interior: Result<Vec<geo::Coord>, GeoJsonError> = ring
444                        .iter()
445                        .map(|coords| {
446                            if coords.len() < 2 {
447                                return Err(GeoJsonError::InvalidCoordinates(
448                                    "Coordinate must have at least 2 values".to_string(),
449                                ));
450                            }
451                            Ok(geo::Coord {
452                                x: coords[0],
453                                y: coords[1],
454                            })
455                        })
456                        .collect();
457                    let interior_coords = interior?;
458                    interiors.push(geo::LineString::from(interior_coords));
459                }
460
461                Ok(Polygon::new(exterior_line, interiors))
462            }
463            _ => Err(GeoJsonError::InvalidGeometry(
464                "GeoJSON geometry is not a Polygon".to_string(),
465            )),
466        }
467    }
468}
469
470impl From<geo::Polygon<f64>> for Polygon {
471    fn from(polygon: geo::Polygon<f64>) -> Self {
472        Self { inner: polygon }
473    }
474}
475
476impl From<Polygon> for geo::Polygon<f64> {
477    fn from(polygon: Polygon) -> Self {
478        polygon.inner
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    #[test]
487    fn test_point_creation() {
488        let point = Point::new(-74.0060, 40.7128);
489        assert_eq!(point.x(), -74.0060);
490        assert_eq!(point.y(), 40.7128);
491        assert_eq!(point.lon(), -74.0060);
492        assert_eq!(point.lat(), 40.7128);
493    }
494
495    #[test]
496    fn test_point_from_tuple() {
497        let point: Point = (-74.0060, 40.7128).into();
498        assert_eq!(point.x(), -74.0060);
499        assert_eq!(point.y(), 40.7128);
500    }
501
502    #[test]
503    fn test_point_to_tuple() {
504        let point = Point::new(-74.0060, 40.7128);
505        let (x, y): (f64, f64) = point.into();
506        assert_eq!(x, -74.0060);
507        assert_eq!(y, 40.7128);
508    }
509
510    #[test]
511    fn test_point_haversine_distance() {
512        let nyc = Point::new(-74.0060, 40.7128);
513        let la = Point::new(-118.2437, 34.0522);
514        let distance = nyc.haversine_distance(&la);
515        // Distance NYC to LA is approximately 3,944 km
516        assert!(distance > 3_900_000.0 && distance < 4_000_000.0);
517    }
518
519    #[test]
520    fn test_point_euclidean_distance() {
521        let p1 = Point::new(0.0, 0.0);
522        let p2 = Point::new(3.0, 4.0);
523        let distance = p1.euclidean_distance(&p2);
524        assert_eq!(distance, 5.0);
525    }
526
527    #[test]
528    fn test_polygon_creation() {
529        use geo::polygon;
530
531        let poly = polygon![
532            (x: -80.0, y: 35.0),
533            (x: -70.0, y: 35.0),
534            (x: -70.0, y: 45.0),
535            (x: -80.0, y: 45.0),
536            (x: -80.0, y: 35.0),
537        ];
538        let polygon = Polygon::from(poly);
539        assert_eq!(polygon.exterior().coords().count(), 5);
540        assert_eq!(polygon.interiors().len(), 0);
541    }
542
543    #[test]
544    fn test_polygon_contains() {
545        use geo::polygon;
546
547        let poly = polygon![
548            (x: -80.0, y: 35.0),
549            (x: -70.0, y: 35.0),
550            (x: -70.0, y: 45.0),
551            (x: -80.0, y: 45.0),
552            (x: -80.0, y: 35.0),
553        ];
554        let polygon = Polygon::from(poly);
555
556        let inside = Point::new(-75.0, 40.0);
557        let outside = Point::new(-85.0, 40.0);
558
559        assert!(polygon.contains(&inside));
560        assert!(!polygon.contains(&outside));
561    }
562
563    #[cfg(feature = "geojson")]
564    #[test]
565    fn test_point_geojson_roundtrip() {
566        let original = Point::new(-74.0060, 40.7128);
567        let json = original.to_geojson().unwrap();
568        let parsed = Point::from_geojson(&json).unwrap();
569
570        assert!((original.x() - parsed.x()).abs() < 1e-10);
571        assert!((original.y() - parsed.y()).abs() < 1e-10);
572    }
573
574    #[cfg(feature = "geojson")]
575    #[test]
576    fn test_polygon_geojson_roundtrip() {
577        use geo::polygon;
578
579        let poly = polygon![
580            (x: -80.0, y: 35.0),
581            (x: -70.0, y: 35.0),
582            (x: -70.0, y: 45.0),
583            (x: -80.0, y: 45.0),
584            (x: -80.0, y: 35.0),
585        ];
586        let original = Polygon::from(poly);
587        let json = original.to_geojson().unwrap();
588        let parsed = Polygon::from_geojson(&json).unwrap();
589
590        assert_eq!(
591            original.exterior().coords().count(),
592            parsed.exterior().coords().count()
593        );
594    }
595}