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}