Skip to main content

nodedb_spatial/predicates/
distance.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ST_Distance and ST_DWithin — minimum distance between geometries.
4//!
5//! Point-to-point uses haversine (great-circle distance in meters).
6//! All other combinations use planar geometry to find the nearest
7//! segment pair, then convert the coordinate-space result to meters
8//! using equirectangular approximation.
9//!
10//! ST_DWithin uses bbox expansion as a fast pre-filter: expand query
11//! geometry's bbox by the distance threshold, check intersection first.
12
13use nodedb_types::geometry::{Geometry, haversine_distance, point_in_polygon};
14use nodedb_types::geometry_bbox;
15
16use super::edge::{point_on_ring_boundary, ring_edges, segment_to_segment_dist_sq};
17
18/// ST_Distance(a, b) — minimum distance in meters between two geometries.
19///
20/// Point-to-point: haversine (exact great-circle).
21/// All others: find minimum coordinate-space distance, convert to meters.
22pub fn st_distance(a: &Geometry, b: &Geometry) -> f64 {
23    match (a, b) {
24        // Point–Point: haversine (exact).
25        (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) => {
26            haversine_distance(ca[0], ca[1], cb[0], cb[1])
27        }
28
29        // Point–LineString or reverse.
30        (Geometry::Point { coordinates: pt }, Geometry::LineString { coordinates: line })
31        | (Geometry::LineString { coordinates: line }, Geometry::Point { coordinates: pt }) => {
32            point_to_linestring_distance(*pt, line)
33        }
34
35        // Point–Polygon or reverse.
36        (Geometry::Point { coordinates: pt }, Geometry::Polygon { coordinates: rings })
37        | (Geometry::Polygon { coordinates: rings }, Geometry::Point { coordinates: pt }) => {
38            point_to_polygon_distance(*pt, rings)
39        }
40
41        // LineString–LineString.
42        (Geometry::LineString { coordinates: la }, Geometry::LineString { coordinates: lb }) => {
43            linestring_to_linestring_distance(la, lb)
44        }
45
46        // LineString–Polygon or reverse.
47        (Geometry::LineString { coordinates: line }, Geometry::Polygon { coordinates: rings })
48        | (Geometry::Polygon { coordinates: rings }, Geometry::LineString { coordinates: line }) => {
49            linestring_to_polygon_distance(line, rings)
50        }
51
52        // Polygon–Polygon.
53        (Geometry::Polygon { coordinates: ra }, Geometry::Polygon { coordinates: rb }) => {
54            polygon_to_polygon_distance(ra, rb)
55        }
56
57        // Multi* types: minimum distance among all component pairs.
58        (Geometry::MultiPoint { coordinates }, other)
59        | (other, Geometry::MultiPoint { coordinates }) => coordinates
60            .iter()
61            .map(|pt| st_distance(&Geometry::Point { coordinates: *pt }, other))
62            .fold(f64::INFINITY, f64::min),
63
64        (Geometry::MultiLineString { coordinates }, other)
65        | (other, Geometry::MultiLineString { coordinates }) => coordinates
66            .iter()
67            .map(|ls| {
68                st_distance(
69                    &Geometry::LineString {
70                        coordinates: ls.clone(),
71                    },
72                    other,
73                )
74            })
75            .fold(f64::INFINITY, f64::min),
76
77        (Geometry::MultiPolygon { coordinates }, other)
78        | (other, Geometry::MultiPolygon { coordinates }) => coordinates
79            .iter()
80            .map(|poly| {
81                st_distance(
82                    &Geometry::Polygon {
83                        coordinates: poly.clone(),
84                    },
85                    other,
86                )
87            })
88            .fold(f64::INFINITY, f64::min),
89
90        (Geometry::GeometryCollection { geometries }, other)
91        | (other, Geometry::GeometryCollection { geometries }) => geometries
92            .iter()
93            .map(|g| st_distance(g, other))
94            .fold(f64::INFINITY, f64::min),
95
96        // Unknown future geometry types — treat as infinite distance.
97        (&_, &_) => f64::INFINITY,
98    }
99}
100
101/// ST_DWithin(a, b, distance_meters) — are A and B within the given distance?
102///
103/// Optimized: uses bbox expansion pre-filter to avoid expensive exact
104/// distance computation when geometries are clearly far apart.
105pub fn st_dwithin(a: &Geometry, b: &Geometry, distance_meters: f64) -> bool {
106    // Fast path for points: just haversine.
107    if let (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) = (a, b) {
108        return haversine_distance(ca[0], ca[1], cb[0], cb[1]) <= distance_meters;
109    }
110
111    // Bbox pre-filter: expand A's bbox by distance, check if B's bbox intersects.
112    let a_bb = geometry_bbox(a).expand_meters(distance_meters);
113    let b_bb = geometry_bbox(b);
114    if !a_bb.intersects(&b_bb) {
115        return false;
116    }
117
118    // Exact check.
119    st_distance(a, b) <= distance_meters
120}
121
122// ── Distance helpers ──
123
124/// Point to linestring: minimum haversine distance to any segment.
125fn point_to_linestring_distance(pt: [f64; 2], line: &[[f64; 2]]) -> f64 {
126    if line.is_empty() {
127        return f64::INFINITY;
128    }
129    if line.len() == 1 {
130        return haversine_distance(pt[0], pt[1], line[0][0], line[0][1]);
131    }
132
133    let mut min_dist = f64::INFINITY;
134    for i in 0..line.len() - 1 {
135        let d = point_to_segment_meters(pt, line[i], line[i + 1]);
136        min_dist = min_dist.min(d);
137    }
138    min_dist
139}
140
141/// Point to polygon: 0 if inside, otherwise min distance to any edge.
142fn point_to_polygon_distance(pt: [f64; 2], rings: &[Vec<[f64; 2]>]) -> f64 {
143    let Some(exterior) = rings.first() else {
144        return f64::INFINITY;
145    };
146
147    // Inside exterior (and not in a hole) → distance 0.
148    if point_in_polygon(pt[0], pt[1], exterior) || point_on_ring_boundary(pt, exterior) {
149        let in_hole = rings[1..]
150            .iter()
151            .any(|hole| point_in_polygon(pt[0], pt[1], hole) && !point_on_ring_boundary(pt, hole));
152        if !in_hole {
153            return 0.0;
154        }
155    }
156
157    // Distance to nearest edge of all rings.
158    let mut min_dist = f64::INFINITY;
159    for ring in rings {
160        for i in 0..ring.len().saturating_sub(1) {
161            let d = point_to_segment_meters(pt, ring[i], ring[i + 1]);
162            min_dist = min_dist.min(d);
163        }
164    }
165    min_dist
166}
167
168fn linestring_to_linestring_distance(la: &[[f64; 2]], lb: &[[f64; 2]]) -> f64 {
169    let mut min_dist = f64::INFINITY;
170    for i in 0..la.len().saturating_sub(1) {
171        for j in 0..lb.len().saturating_sub(1) {
172            let d_sq = segment_to_segment_dist_sq(la[i], la[i + 1], lb[j], lb[j + 1]);
173            if d_sq < 1e-20 {
174                return 0.0;
175            }
176            let d = coord_dist_to_meters(d_sq.sqrt(), la[i], lb[j]);
177            min_dist = min_dist.min(d);
178        }
179    }
180    min_dist
181}
182
183fn linestring_to_polygon_distance(line: &[[f64; 2]], rings: &[Vec<[f64; 2]>]) -> f64 {
184    // If any line point is inside the polygon, distance is 0.
185    let Some(exterior) = rings.first() else {
186        return f64::INFINITY;
187    };
188    for pt in line {
189        if point_in_polygon(pt[0], pt[1], exterior) || point_on_ring_boundary(*pt, exterior) {
190            return 0.0;
191        }
192    }
193
194    // Minimum distance between line segments and ring edges.
195    let mut min_dist = f64::INFINITY;
196    for ring in rings {
197        let edges = ring_edges(ring);
198        for i in 0..line.len().saturating_sub(1) {
199            for &(re_a, re_b) in &edges {
200                let d_sq = segment_to_segment_dist_sq(line[i], line[i + 1], re_a, re_b);
201                if d_sq < 1e-20 {
202                    return 0.0;
203                }
204                let d = coord_dist_to_meters(d_sq.sqrt(), line[i], re_a);
205                min_dist = min_dist.min(d);
206            }
207        }
208    }
209    min_dist
210}
211
212fn polygon_to_polygon_distance(ra: &[Vec<[f64; 2]>], rb: &[Vec<[f64; 2]>]) -> f64 {
213    let Some(ext_a) = ra.first() else {
214        return f64::INFINITY;
215    };
216    let Some(ext_b) = rb.first() else {
217        return f64::INFINITY;
218    };
219
220    // If any vertex of B is inside A (or vice versa), distance is 0.
221    for pt in ext_b {
222        if point_in_polygon(pt[0], pt[1], ext_a) || point_on_ring_boundary(*pt, ext_a) {
223            return 0.0;
224        }
225    }
226    for pt in ext_a {
227        if point_in_polygon(pt[0], pt[1], ext_b) || point_on_ring_boundary(*pt, ext_b) {
228            return 0.0;
229        }
230    }
231
232    // Min edge-to-edge distance.
233    let mut min_dist = f64::INFINITY;
234    let a_edges = ring_edges(ext_a);
235    let b_edges = ring_edges(ext_b);
236    for &(a1, a2) in &a_edges {
237        for &(b1, b2) in &b_edges {
238            let d_sq = segment_to_segment_dist_sq(a1, a2, b1, b2);
239            if d_sq < 1e-20 {
240                return 0.0;
241            }
242            let d = coord_dist_to_meters(d_sq.sqrt(), a1, b1);
243            min_dist = min_dist.min(d);
244        }
245    }
246    min_dist
247}
248
249/// Point to segment distance in meters.
250///
251/// Projects onto segment in coordinate space, then uses haversine for the
252/// final distance to the nearest point on the segment.
253fn point_to_segment_meters(pt: [f64; 2], seg_a: [f64; 2], seg_b: [f64; 2]) -> f64 {
254    let dx = seg_b[0] - seg_a[0];
255    let dy = seg_b[1] - seg_a[1];
256    let len_sq = dx * dx + dy * dy;
257
258    let nearest = if len_sq < 1e-20 {
259        seg_a
260    } else {
261        let t = ((pt[0] - seg_a[0]) * dx + (pt[1] - seg_a[1]) * dy) / len_sq;
262        let t = t.clamp(0.0, 1.0);
263        [seg_a[0] + t * dx, seg_a[1] + t * dy]
264    };
265
266    haversine_distance(pt[0], pt[1], nearest[0], nearest[1])
267}
268
269/// Convert coordinate-space distance to approximate meters.
270///
271/// Uses equirectangular approximation at the midpoint latitude.
272fn coord_dist_to_meters(coord_dist: f64, a: [f64; 2], b: [f64; 2]) -> f64 {
273    let mid_lat = ((a[1] + b[1]) / 2.0).to_radians();
274    let meters_per_deg_lat = 110_540.0;
275    let meters_per_deg_lng = 111_320.0 * mid_lat.cos();
276    // Approximate: treat coord_dist as an isotropic distance in degrees,
277    // scale by average meters per degree.
278    let avg_scale = (meters_per_deg_lat + meters_per_deg_lng) / 2.0;
279    coord_dist * avg_scale
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    fn square() -> Geometry {
287        Geometry::polygon(vec![vec![
288            [0.0, 0.0],
289            [1.0, 0.0],
290            [1.0, 1.0],
291            [0.0, 1.0],
292            [0.0, 0.0],
293        ]])
294    }
295
296    #[test]
297    fn point_to_point_haversine() {
298        let d = st_distance(&Geometry::point(0.0, 0.0), &Geometry::point(0.0, 1.0));
299        // ~111 km for 1 degree latitude.
300        assert!((d - 111_195.0).abs() < 500.0, "got {d}");
301    }
302
303    #[test]
304    fn point_to_point_same() {
305        let d = st_distance(&Geometry::point(5.0, 5.0), &Geometry::point(5.0, 5.0));
306        assert!(d < 0.01);
307    }
308
309    #[test]
310    fn point_inside_polygon_zero_distance() {
311        let d = st_distance(&Geometry::point(0.5, 0.5), &square());
312        assert!(d < 0.01);
313    }
314
315    #[test]
316    fn point_on_edge_zero_distance() {
317        let d = st_distance(&Geometry::point(0.5, 0.0), &square());
318        assert!(d < 0.01);
319    }
320
321    #[test]
322    fn point_to_polygon_outside() {
323        let d = st_distance(&Geometry::point(2.0, 0.5), &square());
324        // 1 degree longitude at equator ≈ 111 km.
325        assert!(d > 100_000.0, "got {d}");
326    }
327
328    #[test]
329    fn point_to_linestring() {
330        let line = Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0]]);
331        // Point 1 degree north of the line.
332        let d = st_distance(&Geometry::point(5.0, 1.0), &line);
333        assert!((d - 111_195.0).abs() < 500.0, "got {d}");
334    }
335
336    #[test]
337    fn linestring_to_linestring_crossing() {
338        let a = Geometry::line_string(vec![[0.0, 0.0], [10.0, 10.0]]);
339        let b = Geometry::line_string(vec![[0.0, 10.0], [10.0, 0.0]]);
340        let d = st_distance(&a, &b);
341        assert!(d < 0.01);
342    }
343
344    #[test]
345    fn dwithin_points_close() {
346        let a = Geometry::point(0.0, 0.0);
347        let b = Geometry::point(0.001, 0.0);
348        // ~111 meters apart.
349        assert!(st_dwithin(&a, &b, 200.0));
350        assert!(!st_dwithin(&a, &b, 50.0));
351    }
352
353    #[test]
354    fn dwithin_point_polygon() {
355        let pt = Geometry::point(2.0, 0.5);
356        // Point is ~1 degree east of polygon.
357        assert!(st_dwithin(&pt, &square(), 200_000.0)); // 200 km
358        assert!(!st_dwithin(&pt, &square(), 50_000.0)); // 50 km
359    }
360
361    #[test]
362    fn dwithin_bbox_prefilter_rejects() {
363        // Points very far apart — bbox pre-filter should reject cheaply.
364        let a = Geometry::point(0.0, 0.0);
365        let b = Geometry::point(90.0, 45.0);
366        assert!(!st_dwithin(&a, &b, 1000.0));
367    }
368
369    #[test]
370    fn polygon_to_polygon_overlapping() {
371        let other = Geometry::polygon(vec![vec![
372            [0.5, 0.5],
373            [1.5, 0.5],
374            [1.5, 1.5],
375            [0.5, 1.5],
376            [0.5, 0.5],
377        ]]);
378        let d = st_distance(&square(), &other);
379        assert!(d < 0.01);
380    }
381
382    #[test]
383    fn polygon_to_polygon_separated() {
384        let far = Geometry::polygon(vec![vec![
385            [5.0, 5.0],
386            [6.0, 5.0],
387            [6.0, 6.0],
388            [5.0, 6.0],
389            [5.0, 5.0],
390        ]]);
391        let d = st_distance(&square(), &far);
392        assert!(d > 100_000.0, "got {d}");
393    }
394}