Skip to main content

nodedb_spatial/predicates/
distance.rs

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