Skip to main content

nodedb_spatial/
operations.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Geometry construction and transformation operations.
4//!
5//! ST_Buffer, ST_Envelope, ST_Union — produce new geometries from existing ones.
6
7use nodedb_types::geometry::Geometry;
8use nodedb_types::{BoundingBox, geometry_bbox};
9
10/// ST_Buffer(geom, distance_meters) → Polygon.
11///
12/// Expands geometry by distance using equirectangular approximation.
13/// - Point → 32-sided circle polygon
14/// - LineString → buffered corridor (parallel offset + end caps)
15/// - Polygon → expanded polygon (vertex offset outward)
16///
17/// Meter-to-degree conversion: `Δlng = meters / (111320 * cos(lat))`,
18/// `Δlat = meters / 110540`. Accurate to <0.5% for ±70° latitude.
19pub fn st_buffer(geom: &Geometry, distance_meters: f64, segments: usize) -> Geometry {
20    let segments = segments.max(4);
21
22    match geom {
23        Geometry::Point { coordinates } => {
24            buffer_point(coordinates[0], coordinates[1], distance_meters, segments)
25        }
26        Geometry::LineString { coordinates } => {
27            buffer_linestring(coordinates, distance_meters, segments)
28        }
29        Geometry::Polygon { coordinates } => buffer_polygon(coordinates, distance_meters, segments),
30        // Multi* types: buffer each component and collect.
31        Geometry::MultiPoint { coordinates } => {
32            let polys: Vec<Vec<Vec<[f64; 2]>>> = coordinates
33                .iter()
34                .map(|pt| {
35                    if let Geometry::Polygon { coordinates: rings } =
36                        buffer_point(pt[0], pt[1], distance_meters, segments)
37                    {
38                        rings
39                    } else {
40                        vec![]
41                    }
42                })
43                .collect();
44            Geometry::MultiPolygon { coordinates: polys }
45        }
46        _ => {
47            // For unsupported types, fall back to buffering the bbox.
48            let bb = geometry_bbox(geom).expand_meters(distance_meters);
49            bbox_to_polygon(&bb)
50        }
51    }
52}
53
54/// ST_Envelope(geom) → Polygon — bounding box as a polygon.
55pub fn st_envelope(geom: &Geometry) -> Geometry {
56    let bb = geometry_bbox(geom);
57    bbox_to_polygon(&bb)
58}
59
60/// ST_Union(a, b) → Geometry — merge two geometries.
61///
62/// For this version, collects into appropriate Multi* type or
63/// GeometryCollection. True polygon clipping (Weiler-Atherton) is
64/// deferred — this handles the common cases correctly.
65pub fn st_union(a: &Geometry, b: &Geometry) -> Geometry {
66    match (a, b) {
67        // Point + Point → MultiPoint
68        (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) => {
69            Geometry::MultiPoint {
70                coordinates: vec![*ca, *cb],
71            }
72        }
73        // LineString + LineString → MultiLineString
74        (Geometry::LineString { coordinates: la }, Geometry::LineString { coordinates: lb }) => {
75            Geometry::MultiLineString {
76                coordinates: vec![la.clone(), lb.clone()],
77            }
78        }
79        // Polygon + Polygon → MultiPolygon
80        (Geometry::Polygon { coordinates: ra }, Geometry::Polygon { coordinates: rb }) => {
81            Geometry::MultiPolygon {
82                coordinates: vec![ra.clone(), rb.clone()],
83            }
84        }
85        // Same-type Multi* + element → extend
86        (Geometry::MultiPoint { coordinates: pts }, Geometry::Point { coordinates: pt }) => {
87            let mut coords = pts.clone();
88            coords.push(*pt);
89            Geometry::MultiPoint {
90                coordinates: coords,
91            }
92        }
93        (Geometry::Point { coordinates: pt }, Geometry::MultiPoint { coordinates: pts }) => {
94            let mut coords = vec![*pt];
95            coords.extend_from_slice(pts);
96            Geometry::MultiPoint {
97                coordinates: coords,
98            }
99        }
100        // Everything else → GeometryCollection
101        _ => Geometry::GeometryCollection {
102            geometries: vec![a.clone(), b.clone()],
103        },
104    }
105}
106
107// ── Buffer helpers ──
108
109/// Buffer a point into an approximate circle polygon.
110fn buffer_point(lng: f64, lat: f64, meters: f64, segments: usize) -> Geometry {
111    let dlat = meters / 110_540.0;
112    let dlng = meters / (111_320.0 * lat.to_radians().cos().max(0.001));
113
114    // no-governor: cold buffer geometry; segments is a fixed constant (default 32), geometry build path
115    let mut ring = Vec::with_capacity(segments + 1);
116    for i in 0..segments {
117        let angle = 2.0 * std::f64::consts::PI * (i as f64) / (segments as f64);
118        ring.push([lng + dlng * angle.cos(), lat + dlat * angle.sin()]);
119    }
120    // Close the ring.
121    ring.push(ring[0]);
122
123    Geometry::Polygon {
124        coordinates: vec![ring],
125    }
126}
127
128/// Buffer a linestring by creating a corridor (parallel offset on both sides + end caps).
129fn buffer_linestring(line: &[[f64; 2]], meters: f64, segments: usize) -> Geometry {
130    if line.is_empty() {
131        return Geometry::Polygon {
132            coordinates: vec![vec![]],
133        };
134    }
135    if line.len() == 1 {
136        return buffer_point(line[0][0], line[0][1], meters, segments);
137    }
138
139    // Build the offset polygon: left side forward, right side backward, with
140    // semicircular end caps.
141    let mut left_side = Vec::new();
142    let mut right_side = Vec::new();
143
144    for i in 0..line.len() - 1 {
145        let (nl, nr) = offset_segment(line[i], line[i + 1], meters);
146        left_side.push(nl.0);
147        left_side.push(nl.1);
148        right_side.push(nr.0);
149        right_side.push(nr.1);
150    }
151
152    // Build polygon: left side forward → end cap → right side backward → start cap.
153    let mut ring = Vec::new();
154    ring.extend_from_slice(&left_side);
155
156    // End cap (semicircle around last point).
157    let last = line[line.len() - 1];
158    let cap_segments = segments / 2;
159    let end_dir = direction(line[line.len() - 2], last);
160    for i in 0..=cap_segments {
161        let angle = end_dir - std::f64::consts::FRAC_PI_2
162            + std::f64::consts::PI * (i as f64) / (cap_segments as f64);
163        let dlat = meters / 110_540.0;
164        let dlng = meters / (111_320.0 * last[1].to_radians().cos().max(0.001));
165        ring.push([last[0] + dlng * angle.cos(), last[1] + dlat * angle.sin()]);
166    }
167
168    // Right side backward.
169    for pt in right_side.iter().rev() {
170        ring.push(*pt);
171    }
172
173    // Start cap (semicircle around first point).
174    let first = line[0];
175    let start_dir = direction(first, line[1]);
176    for i in 0..=cap_segments {
177        let angle = start_dir
178            + std::f64::consts::FRAC_PI_2
179            + std::f64::consts::PI * (i as f64) / (cap_segments as f64);
180        let dlat = meters / 110_540.0;
181        let dlng = meters / (111_320.0 * first[1].to_radians().cos().max(0.001));
182        ring.push([first[0] + dlng * angle.cos(), first[1] + dlat * angle.sin()]);
183    }
184
185    // Close.
186    if let Some(&first_pt) = ring.first() {
187        ring.push(first_pt);
188    }
189
190    Geometry::Polygon {
191        coordinates: vec![ring],
192    }
193}
194
195/// Buffer a polygon by offsetting each vertex outward.
196fn buffer_polygon(rings: &[Vec<[f64; 2]>], meters: f64, _segments: usize) -> Geometry {
197    // no-governor: cold polygon buffer; ring count bounded by input geometry, geometry transform path
198    let mut new_rings = Vec::with_capacity(rings.len());
199
200    for (ring_idx, ring) in rings.iter().enumerate() {
201        if ring.len() < 3 {
202            new_rings.push(ring.clone());
203            continue;
204        }
205        let is_exterior = ring_idx == 0;
206        // For exterior: expand outward. For holes: shrink inward.
207        let sign = if is_exterior { 1.0 } else { -1.0 };
208        let offset_m = meters * sign;
209
210        // no-governor: cold ring offset; ring.len() = vertex count, geometry transform path
211        let mut new_ring = Vec::with_capacity(ring.len());
212        let n = if ring.first() == ring.last() {
213            ring.len() - 1
214        } else {
215            ring.len()
216        };
217
218        for i in 0..n {
219            let prev = ring[(i + n - 1) % n];
220            let curr = ring[i];
221            let next = ring[(i + 1) % n];
222
223            // Bisector direction (average of the two edge normals).
224            let n1 = edge_outward_normal(prev, curr);
225            let n2 = edge_outward_normal(curr, next);
226            let bisect = [(n1[0] + n2[0]) / 2.0, (n1[1] + n2[1]) / 2.0];
227            let len = (bisect[0] * bisect[0] + bisect[1] * bisect[1])
228                .sqrt()
229                .max(1e-12);
230            let unit = [bisect[0] / len, bisect[1] / len];
231
232            let dlat = offset_m / 110_540.0;
233            let dlng = offset_m / (111_320.0 * curr[1].to_radians().cos().max(0.001));
234
235            new_ring.push([curr[0] + unit[0] * dlng, curr[1] + unit[1] * dlat]);
236        }
237
238        // Close.
239        if let Some(&first_pt) = new_ring.first() {
240            new_ring.push(first_pt);
241        }
242        new_rings.push(new_ring);
243    }
244
245    Geometry::Polygon {
246        coordinates: new_rings,
247    }
248}
249
250/// Compute offset segments (left and right parallel lines at `meters` distance).
251/// A pair of offset segments: (left_segment, right_segment).
252type OffsetPair = (([f64; 2], [f64; 2]), ([f64; 2], [f64; 2]));
253
254fn offset_segment(a: [f64; 2], b: [f64; 2], meters: f64) -> OffsetPair {
255    let normal = edge_outward_normal(a, b);
256    let dlat = meters / 110_540.0;
257    let mid_lat = ((a[1] + b[1]) / 2.0).to_radians().cos().max(0.001);
258    let dlng = meters / (111_320.0 * mid_lat);
259
260    let left = (
261        [a[0] + normal[0] * dlng, a[1] + normal[1] * dlat],
262        [b[0] + normal[0] * dlng, b[1] + normal[1] * dlat],
263    );
264    let right = (
265        [a[0] - normal[0] * dlng, a[1] - normal[1] * dlat],
266        [b[0] - normal[0] * dlng, b[1] - normal[1] * dlat],
267    );
268    (left, right)
269}
270
271/// Outward-pointing unit normal for an edge (a→b).
272///
273/// For CCW rings (standard exterior), the outward direction is the right
274/// normal (dy, -dx). This is correct because CCW traversal has the
275/// interior on the left.
276fn edge_outward_normal(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
277    let dx = b[0] - a[0];
278    let dy = b[1] - a[1];
279    let len = (dx * dx + dy * dy).sqrt().max(1e-12);
280    // Right normal: (dy, -dx) normalized — outward for CCW rings.
281    [dy / len, -dx / len]
282}
283
284/// Direction angle (radians) from a to b.
285fn direction(a: [f64; 2], b: [f64; 2]) -> f64 {
286    (b[1] - a[1]).atan2(b[0] - a[0])
287}
288
289/// Convert a BoundingBox to a Polygon.
290fn bbox_to_polygon(bb: &BoundingBox) -> Geometry {
291    Geometry::Polygon {
292        coordinates: vec![vec![
293            [bb.min_lng, bb.min_lat],
294            [bb.max_lng, bb.min_lat],
295            [bb.max_lng, bb.max_lat],
296            [bb.min_lng, bb.max_lat],
297            [bb.min_lng, bb.min_lat],
298        ]],
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn buffer_point_circle() {
308        let circle = st_buffer(&Geometry::point(0.0, 0.0), 1000.0, 32);
309        if let Geometry::Polygon { coordinates } = &circle {
310            assert_eq!(coordinates.len(), 1);
311            // 32 segments + 1 closing point.
312            assert_eq!(coordinates[0].len(), 33);
313            // All points should be roughly 1000m from center.
314            for pt in &coordinates[0][..32] {
315                let d = nodedb_types::geometry::haversine_distance(0.0, 0.0, pt[0], pt[1]);
316                assert!((d - 1000.0).abs() < 50.0, "d={d}");
317            }
318        } else {
319            panic!("expected Polygon");
320        }
321    }
322
323    #[test]
324    fn buffer_point_4_segments() {
325        let circle = st_buffer(&Geometry::point(10.0, 50.0), 500.0, 4);
326        if let Geometry::Polygon { coordinates } = &circle {
327            assert_eq!(coordinates[0].len(), 5); // 4 + close
328        } else {
329            panic!("expected Polygon");
330        }
331    }
332
333    #[test]
334    fn envelope_polygon() {
335        let poly = Geometry::polygon(vec![vec![[1.0, 2.0], [5.0, 2.0], [3.0, 8.0], [1.0, 2.0]]]);
336        let env = st_envelope(&poly);
337        if let Geometry::Polygon { coordinates } = &env {
338            let ring = &coordinates[0];
339            assert_eq!(ring[0], [1.0, 2.0]); // min_lng, min_lat
340            assert_eq!(ring[2], [5.0, 8.0]); // max_lng, max_lat
341        } else {
342            panic!("expected Polygon");
343        }
344    }
345
346    #[test]
347    fn envelope_point() {
348        let env = st_envelope(&Geometry::point(5.0, 10.0));
349        if let Geometry::Polygon { coordinates } = &env {
350            // Zero-area bbox → degenerate polygon.
351            assert_eq!(coordinates[0][0], [5.0, 10.0]);
352            assert_eq!(coordinates[0][2], [5.0, 10.0]);
353        } else {
354            panic!("expected Polygon");
355        }
356    }
357
358    #[test]
359    fn union_points() {
360        let a = Geometry::point(1.0, 2.0);
361        let b = Geometry::point(3.0, 4.0);
362        let u = st_union(&a, &b);
363        if let Geometry::MultiPoint { coordinates } = &u {
364            assert_eq!(coordinates.len(), 2);
365        } else {
366            panic!("expected MultiPoint, got {:?}", u.geometry_type());
367        }
368    }
369
370    #[test]
371    fn union_polygons() {
372        let a = Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]]);
373        let b = Geometry::polygon(vec![vec![[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [2.0, 2.0]]]);
374        let u = st_union(&a, &b);
375        if let Geometry::MultiPolygon { coordinates } = &u {
376            assert_eq!(coordinates.len(), 2);
377        } else {
378            panic!("expected MultiPolygon");
379        }
380    }
381
382    #[test]
383    fn union_mixed_types() {
384        let a = Geometry::point(1.0, 2.0);
385        let b = Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]]);
386        let u = st_union(&a, &b);
387        if let Geometry::GeometryCollection { geometries } = &u {
388            assert_eq!(geometries.len(), 2);
389        } else {
390            panic!("expected GeometryCollection");
391        }
392    }
393
394    #[test]
395    fn buffer_linestring_corridor() {
396        let line = Geometry::line_string(vec![[0.0, 0.0], [0.1, 0.0]]);
397        let buf = st_buffer(&line, 1000.0, 16);
398        if let Geometry::Polygon { coordinates } = &buf {
399            assert!(!coordinates[0].is_empty());
400            // Should have more points than a simple rectangle due to end caps.
401            assert!(coordinates[0].len() > 10);
402        } else {
403            panic!("expected Polygon");
404        }
405    }
406
407    #[test]
408    fn buffer_polygon_expands() {
409        let poly = Geometry::polygon(vec![vec![
410            [0.0, 0.0],
411            [1.0, 0.0],
412            [1.0, 1.0],
413            [0.0, 1.0],
414            [0.0, 0.0],
415        ]]);
416        let buf = st_buffer(&poly, 1000.0, 32);
417        if let Geometry::Polygon { .. } = &buf {
418            let orig_bb = geometry_bbox(&poly);
419            let buf_bb = geometry_bbox(&buf);
420            // Buffered should be larger.
421            assert!(buf_bb.min_lng < orig_bb.min_lng);
422            assert!(buf_bb.max_lng > orig_bb.max_lng);
423            assert!(buf_bb.min_lat < orig_bb.min_lat);
424            assert!(buf_bb.max_lat > orig_bb.max_lat);
425        } else {
426            panic!("expected Polygon");
427        }
428    }
429}