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