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}