nodedb_spatial/predicates/
intersection.rs1use nodedb_types::geometry::Geometry;
17
18pub fn st_intersection(a: &Geometry, b: &Geometry) -> Geometry {
23 match (a, b) {
24 (
26 Geometry::Polygon {
27 coordinates: rings_a,
28 },
29 Geometry::Polygon {
30 coordinates: rings_b,
31 },
32 ) => {
33 let Some(ext_a) = rings_a.first() else {
34 return empty_geometry();
35 };
36 let Some(ext_b) = rings_b.first() else {
37 return empty_geometry();
38 };
39 let clipped = sutherland_hodgman(ext_a, ext_b);
40 if clipped.len() < 3 {
41 return empty_geometry();
42 }
43 let mut ring = clipped;
45 if ring.first() != ring.last()
46 && let Some(&first) = ring.first()
47 {
48 ring.push(first);
49 }
50 Geometry::Polygon {
51 coordinates: vec![ring],
52 }
53 }
54
55 (Geometry::Point { coordinates: pt }, Geometry::Polygon { coordinates: rings })
57 | (Geometry::Polygon { coordinates: rings }, Geometry::Point { coordinates: pt }) => {
58 if let Some(ext) = rings.first()
59 && (nodedb_types::geometry::point_in_polygon(pt[0], pt[1], ext)
60 || crate::predicates::edge::point_on_ring_boundary(*pt, ext))
61 {
62 return Geometry::Point { coordinates: *pt };
63 }
64 empty_geometry()
65 }
66
67 (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) => {
69 if (ca[0] - cb[0]).abs() < 1e-12 && (ca[1] - cb[1]).abs() < 1e-12 {
70 Geometry::Point { coordinates: *ca }
71 } else {
72 empty_geometry()
73 }
74 }
75
76 (Geometry::LineString { coordinates: line }, Geometry::Polygon { coordinates: rings })
78 | (Geometry::Polygon { coordinates: rings }, Geometry::LineString { coordinates: line }) => {
79 clip_linestring_to_polygon(line, rings)
80 }
81
82 _ => {
85 if crate::predicates::st_intersects(a, b) {
86 Geometry::GeometryCollection {
89 geometries: vec![a.clone(), b.clone()],
90 }
91 } else {
92 empty_geometry()
93 }
94 }
95 }
96}
97
98fn empty_geometry() -> Geometry {
100 Geometry::GeometryCollection {
101 geometries: Vec::new(),
102 }
103}
104
105fn sutherland_hodgman(subject: &[[f64; 2]], clip: &[[f64; 2]]) -> Vec<[f64; 2]> {
115 if subject.is_empty() || clip.is_empty() {
116 return Vec::new();
117 }
118
119 let mut output = strip_closing(subject);
120 let clip_edges = strip_closing(clip);
121
122 let n = clip_edges.len();
123 for i in 0..n {
124 if output.is_empty() {
125 return Vec::new();
126 }
127
128 let edge_start = clip_edges[i];
129 let edge_end = clip_edges[(i + 1) % n];
130
131 let input = output;
132 output = Vec::with_capacity(input.len());
133
134 let m = input.len();
135 for j in 0..m {
136 let current = input[j];
137 let previous = input[(j + m - 1) % m];
138
139 let curr_inside = is_inside(current, edge_start, edge_end);
140 let prev_inside = is_inside(previous, edge_start, edge_end);
141
142 if curr_inside {
143 if !prev_inside {
144 if let Some(pt) = line_intersection(previous, current, edge_start, edge_end) {
146 output.push(pt);
147 }
148 }
149 output.push(current);
150 } else if prev_inside {
151 if let Some(pt) = line_intersection(previous, current, edge_start, edge_end) {
153 output.push(pt);
154 }
155 }
156 }
157 }
158
159 output
160}
161
162fn is_inside(point: [f64; 2], edge_start: [f64; 2], edge_end: [f64; 2]) -> bool {
164 let cross = (edge_end[0] - edge_start[0]) * (point[1] - edge_start[1])
167 - (edge_end[1] - edge_start[1]) * (point[0] - edge_start[0]);
168 cross >= 0.0
169}
170
171fn line_intersection(a1: [f64; 2], a2: [f64; 2], b1: [f64; 2], b2: [f64; 2]) -> Option<[f64; 2]> {
173 let dx_a = a2[0] - a1[0];
174 let dy_a = a2[1] - a1[1];
175 let dx_b = b2[0] - b1[0];
176 let dy_b = b2[1] - b1[1];
177
178 let denom = dx_a * dy_b - dy_a * dx_b;
179 if denom.abs() < 1e-15 {
180 return None; }
182
183 let t = ((b1[0] - a1[0]) * dy_b - (b1[1] - a1[1]) * dx_b) / denom;
184 Some([a1[0] + t * dx_a, a1[1] + t * dy_a])
185}
186
187fn strip_closing(ring: &[[f64; 2]]) -> Vec<[f64; 2]> {
189 if ring.len() >= 2 && ring.first() == ring.last() {
190 ring[..ring.len() - 1].to_vec()
191 } else {
192 ring.to_vec()
193 }
194}
195
196fn clip_linestring_to_polygon(line: &[[f64; 2]], rings: &[Vec<[f64; 2]>]) -> Geometry {
198 let Some(exterior) = rings.first() else {
199 return empty_geometry();
200 };
201
202 let mut inside_points: Vec<[f64; 2]> = Vec::new();
204 for pt in line {
205 if nodedb_types::geometry::point_in_polygon(pt[0], pt[1], exterior)
206 || crate::predicates::edge::point_on_ring_boundary(*pt, exterior)
207 {
208 inside_points.push(*pt);
209 }
210 }
211
212 if inside_points.len() < 2 {
213 return empty_geometry();
214 }
215
216 Geometry::LineString {
217 coordinates: inside_points,
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn square(min: f64, max: f64) -> Geometry {
226 Geometry::polygon(vec![vec![
227 [min, min],
228 [max, min],
229 [max, max],
230 [min, max],
231 [min, min],
232 ]])
233 }
234
235 #[test]
236 fn overlapping_squares() {
237 let a = square(0.0, 10.0);
238 let b = square(5.0, 15.0);
239 let result = st_intersection(&a, &b);
240 if let Geometry::Polygon { coordinates } = &result {
241 assert!(!coordinates[0].is_empty());
242 let xs: Vec<f64> = coordinates[0].iter().map(|c| c[0]).collect();
244 let ys: Vec<f64> = coordinates[0].iter().map(|c| c[1]).collect();
245 let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min);
246 let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
247 let min_y = ys.iter().cloned().fold(f64::INFINITY, f64::min);
248 let max_y = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
249 assert!((min_x - 5.0).abs() < 0.01, "min_x={min_x}");
250 assert!((max_x - 10.0).abs() < 0.01, "max_x={max_x}");
251 assert!((min_y - 5.0).abs() < 0.01, "min_y={min_y}");
252 assert!((max_y - 10.0).abs() < 0.01, "max_y={max_y}");
253 } else {
254 panic!("expected Polygon, got {:?}", result.geometry_type());
255 }
256 }
257
258 #[test]
259 fn contained_square() {
260 let a = square(0.0, 10.0);
261 let b = square(2.0, 8.0);
262 let result = st_intersection(&a, &b);
263 if let Geometry::Polygon { coordinates } = &result {
264 let xs: Vec<f64> = coordinates[0].iter().map(|c| c[0]).collect();
266 let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min);
267 let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
268 assert!((min_x - 2.0).abs() < 0.01);
269 assert!((max_x - 8.0).abs() < 0.01);
270 } else {
271 panic!("expected Polygon");
272 }
273 }
274
275 #[test]
276 fn disjoint_returns_empty() {
277 let a = square(0.0, 5.0);
278 let b = square(10.0, 15.0);
279 let result = st_intersection(&a, &b);
280 if let Geometry::GeometryCollection { geometries } = &result {
281 assert!(geometries.is_empty());
282 } else {
283 panic!("expected empty GeometryCollection");
284 }
285 }
286
287 #[test]
288 fn point_inside_polygon() {
289 let poly = square(0.0, 10.0);
290 let pt = Geometry::point(5.0, 5.0);
291 let result = st_intersection(&poly, &pt);
292 assert_eq!(result.geometry_type(), "Point");
293 }
294
295 #[test]
296 fn point_outside_polygon() {
297 let poly = square(0.0, 10.0);
298 let pt = Geometry::point(20.0, 20.0);
299 let result = st_intersection(&poly, &pt);
300 if let Geometry::GeometryCollection { geometries } = &result {
301 assert!(geometries.is_empty());
302 }
303 }
304
305 #[test]
306 fn identical_points() {
307 let a = Geometry::point(5.0, 5.0);
308 let b = Geometry::point(5.0, 5.0);
309 let result = st_intersection(&a, &b);
310 assert_eq!(result.geometry_type(), "Point");
311 }
312
313 #[test]
314 fn different_points() {
315 let a = Geometry::point(0.0, 0.0);
316 let b = Geometry::point(1.0, 1.0);
317 let result = st_intersection(&a, &b);
318 if let Geometry::GeometryCollection { geometries } = &result {
319 assert!(geometries.is_empty());
320 }
321 }
322
323 #[test]
324 fn linestring_clipped_to_polygon() {
325 let poly = square(0.0, 10.0);
326 let line = Geometry::line_string(vec![[2.0, 5.0], [5.0, 5.0], [8.0, 5.0]]);
328 let result = st_intersection(&poly, &line);
329 assert_eq!(result.geometry_type(), "LineString");
330 if let Geometry::LineString { coordinates } = &result {
331 assert_eq!(coordinates.len(), 3);
332 }
333 }
334}