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}