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}