1use nodedb_mem::ScopedMemory;
17use nodedb_types::geometry::Geometry;
18use nodedb_types::{BoundingBox, geometry_bbox};
19
20use crate::predicates;
21use crate::rtree::{RTree, RTreeEntry};
22
23pub struct SpatialJoinResult {
25 pub pairs: Vec<(u64, u64)>,
27 pub probes: usize,
29 pub exact_evals: usize,
31}
32
33pub fn spatial_join(
41 indexed_side: &RTree,
42 probe_entries: &[(u64, BoundingBox)],
43 get_indexed_geom: &dyn Fn(u64) -> Option<Geometry>,
44 get_probe_geom: &dyn Fn(u64) -> Option<Geometry>,
45 predicate: SpatialJoinPredicate,
46) -> SpatialJoinResult {
47 let mut pairs = Vec::new();
48 let mut probes = 0;
49 let mut exact_evals = 0;
50
51 for &(probe_id, ref probe_bbox) in probe_entries {
52 let candidates = indexed_side.search(probe_bbox);
54 probes += 1;
55
56 for candidate in &candidates {
57 let Some(indexed_geom) = get_indexed_geom(candidate.id) else {
59 continue;
60 };
61 let Some(probe_geom) = get_probe_geom(probe_id) else {
62 continue;
63 };
64 exact_evals += 1;
65
66 let matches = match predicate {
67 SpatialJoinPredicate::Intersects => {
68 predicates::st_intersects(&probe_geom, &indexed_geom)
69 }
70 SpatialJoinPredicate::Contains => {
71 predicates::st_contains(&probe_geom, &indexed_geom)
72 }
73 SpatialJoinPredicate::Within => predicates::st_within(&probe_geom, &indexed_geom),
74 SpatialJoinPredicate::DWithin(dist) => {
75 predicates::st_dwithin(&probe_geom, &indexed_geom, dist)
76 }
77 };
78
79 if matches {
80 pairs.push((probe_id, candidate.id));
81 }
82 }
83 }
84
85 SpatialJoinResult {
86 pairs,
87 probes,
88 exact_evals,
89 }
90}
91
92pub fn build_join_index(entries: &[(u64, Geometry)], memory: ScopedMemory) -> RTree {
94 let rtree_entries: Vec<RTreeEntry> = entries
95 .iter()
96 .map(|(id, geom)| RTreeEntry {
97 id: *id,
98 bbox: geometry_bbox(geom),
99 })
100 .collect();
101 RTree::bulk_load(rtree_entries, memory)
102}
103
104#[derive(Debug, Clone, Copy)]
106#[non_exhaustive]
107pub enum SpatialJoinPredicate {
108 Intersects,
109 Contains,
110 Within,
111 DWithin(f64),
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::test_support::test_memory;
118
119 #[test]
120 fn join_overlapping_squares() {
121 let left: Vec<(u64, Geometry)> = (0..5)
123 .map(|i| {
124 let base = (i * 5) as f64;
125 (
126 i as u64,
127 Geometry::polygon(vec![vec![
128 [base, base],
129 [base + 4.0, base],
130 [base + 4.0, base + 4.0],
131 [base, base + 4.0],
132 [base, base],
133 ]]),
134 )
135 })
136 .collect();
137
138 let right: Vec<(u64, Geometry)> = (0..5)
140 .map(|i| {
141 let base = (i * 5 + 2) as f64;
142 (
143 100 + i as u64,
144 Geometry::polygon(vec![vec![
145 [base, base],
146 [base + 4.0, base],
147 [base + 4.0, base + 4.0],
148 [base, base + 4.0],
149 [base, base],
150 ]]),
151 )
152 })
153 .collect();
154
155 let index = build_join_index(&left, test_memory());
157
158 let probe_entries: Vec<(u64, BoundingBox)> = right
160 .iter()
161 .map(|(id, geom)| (*id, geometry_bbox(geom)))
162 .collect();
163
164 let left_map: std::collections::HashMap<u64, Geometry> = left.into_iter().collect();
165 let right_map: std::collections::HashMap<u64, Geometry> = right.into_iter().collect();
166
167 let result = spatial_join(
168 &index,
169 &probe_entries,
170 &|id| left_map.get(&id).cloned(),
171 &|id| right_map.get(&id).cloned(),
172 SpatialJoinPredicate::Intersects,
173 );
174
175 assert!(!result.pairs.is_empty(), "expected some join matches");
177 assert!(result.probes == 5); }
179
180 #[test]
181 fn join_no_overlap() {
182 let left = vec![(
183 1u64,
184 Geometry::polygon(vec![vec![
185 [0.0, 0.0],
186 [1.0, 0.0],
187 [1.0, 1.0],
188 [0.0, 1.0],
189 [0.0, 0.0],
190 ]]),
191 )];
192 let right = vec![(
193 100u64,
194 Geometry::polygon(vec![vec![
195 [50.0, 50.0],
196 [51.0, 50.0],
197 [51.0, 51.0],
198 [50.0, 51.0],
199 [50.0, 50.0],
200 ]]),
201 )];
202
203 let index = build_join_index(&left, test_memory());
204 let probes: Vec<(u64, BoundingBox)> = right
205 .iter()
206 .map(|(id, g)| (*id, geometry_bbox(g)))
207 .collect();
208
209 let left_map: std::collections::HashMap<u64, Geometry> = left.into_iter().collect();
210 let right_map: std::collections::HashMap<u64, Geometry> = right.into_iter().collect();
211
212 let result = spatial_join(
213 &index,
214 &probes,
215 &|id| left_map.get(&id).cloned(),
216 &|id| right_map.get(&id).cloned(),
217 SpatialJoinPredicate::Intersects,
218 );
219
220 assert!(result.pairs.is_empty());
221 }
222
223 #[test]
224 fn join_with_dwithin() {
225 let left = vec![(1u64, Geometry::point(0.0, 0.0))];
226 let right = vec![(100u64, Geometry::point(0.001, 0.0))]; let index = build_join_index(&left, test_memory());
229 let probes: Vec<(u64, BoundingBox)> = right
230 .iter()
231 .map(|(id, g)| {
232 (*id, geometry_bbox(g).expand_meters(500.0))
234 })
235 .collect();
236
237 let left_map: std::collections::HashMap<u64, Geometry> = left.into_iter().collect();
238 let right_map: std::collections::HashMap<u64, Geometry> = right.into_iter().collect();
239
240 let result = spatial_join(
241 &index,
242 &probes,
243 &|id| left_map.get(&id).cloned(),
244 &|id| right_map.get(&id).cloned(),
245 SpatialJoinPredicate::DWithin(500.0), );
247
248 assert_eq!(result.pairs.len(), 1);
249 }
250}