Skip to main content

nodedb_types/
geometry.rs

1//! GeoJSON-compatible geometry types.
2//!
3//! Supports Point, LineString, Polygon, MultiPoint, MultiLineString,
4//! MultiPolygon, and GeometryCollection. Stored as GeoJSON for JSON
5//! compatibility. Includes distance (Haversine), area, bearing, and
6//! centroid calculations.
7
8use serde::{Deserialize, Serialize};
9
10/// A 2D coordinate (longitude, latitude) following GeoJSON convention.
11/// Note: GeoJSON uses [lng, lat] order, NOT [lat, lng].
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Coord {
14    pub lng: f64,
15    pub lat: f64,
16}
17
18impl Coord {
19    pub fn new(lng: f64, lat: f64) -> Self {
20        Self { lng, lat }
21    }
22}
23
24/// GeoJSON-compatible geometry types.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(tag = "type")]
27pub enum Geometry {
28    Point {
29        coordinates: [f64; 2],
30    },
31    LineString {
32        coordinates: Vec<[f64; 2]>,
33    },
34    Polygon {
35        coordinates: Vec<Vec<[f64; 2]>>,
36    },
37    MultiPoint {
38        coordinates: Vec<[f64; 2]>,
39    },
40    MultiLineString {
41        coordinates: Vec<Vec<[f64; 2]>>,
42    },
43    MultiPolygon {
44        coordinates: Vec<Vec<Vec<[f64; 2]>>>,
45    },
46    GeometryCollection {
47        geometries: Vec<Geometry>,
48    },
49}
50
51impl Geometry {
52    /// Create a Point from (longitude, latitude).
53    pub fn point(lng: f64, lat: f64) -> Self {
54        Geometry::Point {
55            coordinates: [lng, lat],
56        }
57    }
58
59    /// Create a LineString from a series of [lng, lat] pairs.
60    pub fn line_string(coords: Vec<[f64; 2]>) -> Self {
61        Geometry::LineString {
62            coordinates: coords,
63        }
64    }
65
66    /// Create a Polygon from exterior ring (and optional holes).
67    ///
68    /// The first ring is the exterior, subsequent rings are holes.
69    /// Each ring must be a closed loop (first point == last point).
70    pub fn polygon(rings: Vec<Vec<[f64; 2]>>) -> Self {
71        Geometry::Polygon { coordinates: rings }
72    }
73
74    /// Get the type name of this geometry.
75    pub fn geometry_type(&self) -> &'static str {
76        match self {
77            Geometry::Point { .. } => "Point",
78            Geometry::LineString { .. } => "LineString",
79            Geometry::Polygon { .. } => "Polygon",
80            Geometry::MultiPoint { .. } => "MultiPoint",
81            Geometry::MultiLineString { .. } => "MultiLineString",
82            Geometry::MultiPolygon { .. } => "MultiPolygon",
83            Geometry::GeometryCollection { .. } => "GeometryCollection",
84        }
85    }
86
87    /// Compute the centroid of the geometry.
88    pub fn centroid(&self) -> Option<[f64; 2]> {
89        match self {
90            Geometry::Point { coordinates } => Some(*coordinates),
91            Geometry::LineString { coordinates } => {
92                if coordinates.is_empty() {
93                    return None;
94                }
95                let n = coordinates.len() as f64;
96                let sum_lng: f64 = coordinates.iter().map(|c| c[0]).sum();
97                let sum_lat: f64 = coordinates.iter().map(|c| c[1]).sum();
98                Some([sum_lng / n, sum_lat / n])
99            }
100            Geometry::Polygon { coordinates } => {
101                // Centroid of exterior ring.
102                coordinates.first().and_then(|ring| {
103                    if ring.is_empty() {
104                        return None;
105                    }
106                    let n = ring.len() as f64;
107                    let sum_lng: f64 = ring.iter().map(|c| c[0]).sum();
108                    let sum_lat: f64 = ring.iter().map(|c| c[1]).sum();
109                    Some([sum_lng / n, sum_lat / n])
110                })
111            }
112            Geometry::MultiPoint { coordinates } => {
113                if coordinates.is_empty() {
114                    return None;
115                }
116                let n = coordinates.len() as f64;
117                let sum_lng: f64 = coordinates.iter().map(|c| c[0]).sum();
118                let sum_lat: f64 = coordinates.iter().map(|c| c[1]).sum();
119                Some([sum_lng / n, sum_lat / n])
120            }
121            _ => None,
122        }
123    }
124}
125
126// ── Geo math functions ──
127
128const EARTH_RADIUS_M: f64 = 6_371_000.0;
129
130/// Haversine distance between two points in meters.
131///
132/// Input: (lng1, lat1) and (lng2, lat2) in degrees.
133pub fn haversine_distance(lng1: f64, lat1: f64, lng2: f64, lat2: f64) -> f64 {
134    let lat1_r = lat1.to_radians();
135    let lat2_r = lat2.to_radians();
136    let dlat = (lat2 - lat1).to_radians();
137    let dlng = (lng2 - lng1).to_radians();
138
139    let a = (dlat / 2.0).sin().powi(2) + lat1_r.cos() * lat2_r.cos() * (dlng / 2.0).sin().powi(2);
140    let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
141    EARTH_RADIUS_M * c
142}
143
144/// Haversine bearing from point A to point B in degrees (0-360).
145pub fn haversine_bearing(lng1: f64, lat1: f64, lng2: f64, lat2: f64) -> f64 {
146    let lat1_r = lat1.to_radians();
147    let lat2_r = lat2.to_radians();
148    let dlng = (lng2 - lng1).to_radians();
149
150    let y = dlng.sin() * lat2_r.cos();
151    let x = lat1_r.cos() * lat2_r.sin() - lat1_r.sin() * lat2_r.cos() * dlng.cos();
152    let bearing = y.atan2(x).to_degrees();
153    (bearing + 360.0) % 360.0
154}
155
156/// Approximate area of a polygon on Earth's surface in square meters.
157///
158/// Uses the Shoelace formula on projected coordinates (simple equirectangular).
159/// Accurate for small polygons; for large polygons use spherical excess.
160pub fn polygon_area(ring: &[[f64; 2]]) -> f64 {
161    if ring.len() < 3 {
162        return 0.0;
163    }
164    let mut sum = 0.0;
165    let n = ring.len();
166    for i in 0..n {
167        let j = (i + 1) % n;
168        // Convert to approximate meters from center.
169        let lat_avg = ((ring[i][1] + ring[j][1]) / 2.0).to_radians();
170        let x1 = ring[i][0].to_radians() * EARTH_RADIUS_M * lat_avg.cos();
171        let y1 = ring[i][1].to_radians() * EARTH_RADIUS_M;
172        let x2 = ring[j][0].to_radians() * EARTH_RADIUS_M * lat_avg.cos();
173        let y2 = ring[j][1].to_radians() * EARTH_RADIUS_M;
174        sum += x1 * y2 - x2 * y1;
175    }
176    (sum / 2.0).abs()
177}
178
179/// Check if a point is inside a polygon (ray casting algorithm).
180pub fn point_in_polygon(lng: f64, lat: f64, ring: &[[f64; 2]]) -> bool {
181    let mut inside = false;
182    let n = ring.len();
183    let mut j = n.wrapping_sub(1);
184    for i in 0..n {
185        let yi = ring[i][1];
186        let yj = ring[j][1];
187        if ((yi > lat) != (yj > lat))
188            && (lng < (ring[j][0] - ring[i][0]) * (lat - yi) / (yj - yi) + ring[i][0])
189        {
190            inside = !inside;
191        }
192        j = i;
193    }
194    inside
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn point_creation() {
203        let p = Geometry::point(-73.9857, 40.7484);
204        assert_eq!(p.geometry_type(), "Point");
205        if let Geometry::Point { coordinates } = &p {
206            assert!((coordinates[0] - (-73.9857)).abs() < 1e-6);
207            assert!((coordinates[1] - 40.7484).abs() < 1e-6);
208        }
209    }
210
211    #[test]
212    fn haversine_nyc_to_london() {
213        // NYC: -74.006, 40.7128 → London: -0.1278, 51.5074
214        let d = haversine_distance(-74.006, 40.7128, -0.1278, 51.5074);
215        // ~5,570 km
216        assert!((d - 5_570_000.0).abs() < 50_000.0, "got {d}m");
217    }
218
219    #[test]
220    fn haversine_same_point() {
221        let d = haversine_distance(0.0, 0.0, 0.0, 0.0);
222        assert!(d.abs() < 1e-6);
223    }
224
225    #[test]
226    fn bearing_north() {
227        let b = haversine_bearing(0.0, 0.0, 0.0, 1.0);
228        assert!((b - 0.0).abs() < 1.0, "expected ~0, got {b}");
229    }
230
231    #[test]
232    fn bearing_east() {
233        let b = haversine_bearing(0.0, 0.0, 1.0, 0.0);
234        assert!((b - 90.0).abs() < 1.0, "expected ~90, got {b}");
235    }
236
237    #[test]
238    fn polygon_area_simple() {
239        // ~1 degree square near equator ≈ 111km × 111km ≈ 12,321 km²
240        let ring = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
241        let area = polygon_area(&ring);
242        let area_km2 = area / 1e6;
243        assert!(
244            (area_km2 - 12321.0).abs() < 500.0,
245            "expected ~12321 km², got {area_km2}"
246        );
247    }
248
249    #[test]
250    fn point_in_polygon_inside() {
251        let ring = vec![
252            [0.0, 0.0],
253            [10.0, 0.0],
254            [10.0, 10.0],
255            [0.0, 10.0],
256            [0.0, 0.0],
257        ];
258        assert!(point_in_polygon(5.0, 5.0, &ring));
259        assert!(!point_in_polygon(15.0, 5.0, &ring));
260    }
261
262    #[test]
263    fn centroid_point() {
264        let p = Geometry::point(10.0, 20.0);
265        assert_eq!(p.centroid(), Some([10.0, 20.0]));
266    }
267
268    #[test]
269    fn centroid_linestring() {
270        let ls = Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]]);
271        let c = ls.centroid().unwrap();
272        assert!((c[0] - 6.6667).abs() < 0.01);
273        assert!((c[1] - 3.3333).abs() < 0.01);
274    }
275
276    #[test]
277    fn geojson_serialize() {
278        let p = Geometry::point(1.0, 2.0);
279        let json = serde_json::to_string(&p).unwrap();
280        assert!(json.contains("\"type\":\"Point\""));
281        assert!(json.contains("\"coordinates\":[1.0,2.0]"));
282    }
283
284    #[test]
285    fn geojson_roundtrip() {
286        let original = Geometry::polygon(vec![vec![
287            [0.0, 0.0],
288            [1.0, 0.0],
289            [1.0, 1.0],
290            [0.0, 1.0],
291            [0.0, 0.0],
292        ]]);
293        let json = serde_json::to_string(&original).unwrap();
294        let parsed: Geometry = serde_json::from_str(&json).unwrap();
295        assert_eq!(original, parsed);
296    }
297}