Skip to main content

nodedb_spatial/predicates/
contains.rs

1//! ST_Contains — geometry A fully contains geometry B.
2//!
3//! DE-9IM pattern: T*****FF* — B's interior intersects A's interior, and
4//! B does not intersect A's exterior. A point on A's boundary is NOT
5//! contained (strict OGC semantics). Use ST_Covers for boundary-inclusive.
6//!
7//! Implementation: bbox pre-filter → exact geometry test.
8
9use nodedb_types::geometry::{Geometry, point_in_polygon};
10use nodedb_types::geometry_bbox;
11
12use super::edge::{point_on_ring_boundary, ring_edges};
13
14/// ST_Contains(a, b) — does geometry A fully contain geometry B?
15///
16/// Returns false if any part of B lies outside A, or if B only touches
17/// A's boundary without entering its interior.
18pub fn st_contains(a: &Geometry, b: &Geometry) -> bool {
19    // Fast path: bbox pre-filter.
20    let a_bb = geometry_bbox(a);
21    let b_bb = geometry_bbox(b);
22    if !a_bb.contains_bbox(&b_bb) {
23        return false;
24    }
25
26    match (a, b) {
27        // Point contains Point — only if identical.
28        (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) => {
29            (ca[0] - cb[0]).abs() < 1e-12 && (ca[1] - cb[1]).abs() < 1e-12
30        }
31
32        // Polygon contains Point — ray casting, point on edge = false (DE-9IM).
33        (Geometry::Polygon { coordinates: rings }, Geometry::Point { coordinates: pt }) => {
34            polygon_contains_point(rings, *pt)
35        }
36
37        // Polygon contains LineString — all points strictly inside, no edge crossings
38        // with exterior.
39        (Geometry::Polygon { coordinates: rings }, Geometry::LineString { coordinates: line }) => {
40            polygon_contains_linestring(rings, line)
41        }
42
43        // Polygon contains Polygon — all vertices of B inside A, no edge crossings.
44        (
45            Geometry::Polygon {
46                coordinates: rings_a,
47            },
48            Geometry::Polygon {
49                coordinates: rings_b,
50            },
51        ) => polygon_contains_polygon(rings_a, rings_b),
52
53        // Polygon contains Multi* — all components contained.
54        (Geometry::Polygon { .. }, Geometry::MultiPoint { coordinates }) => coordinates
55            .iter()
56            .all(|pt| st_contains(a, &Geometry::Point { coordinates: *pt })),
57        (Geometry::Polygon { .. }, Geometry::MultiLineString { coordinates }) => {
58            coordinates.iter().all(|ls| {
59                st_contains(
60                    a,
61                    &Geometry::LineString {
62                        coordinates: ls.clone(),
63                    },
64                )
65            })
66        }
67        (Geometry::Polygon { .. }, Geometry::MultiPolygon { coordinates }) => {
68            coordinates.iter().all(|poly| {
69                st_contains(
70                    a,
71                    &Geometry::Polygon {
72                        coordinates: poly.clone(),
73                    },
74                )
75            })
76        }
77
78        // MultiPolygon contains anything — at least one component polygon contains all of B.
79        (Geometry::MultiPolygon { coordinates: polys }, _) => polys.iter().any(|poly| {
80            st_contains(
81                &Geometry::Polygon {
82                    coordinates: poly.clone(),
83                },
84                b,
85            )
86        }),
87
88        // LineString contains Point — point must be strictly on the line interior
89        // (not just an endpoint for DE-9IM T*****FF*, but for practical purposes
90        // we check if point lies on any segment).
91        (Geometry::LineString { coordinates: line }, Geometry::Point { coordinates: pt }) => {
92            super::edge::point_on_ring_boundary(*pt, line)
93        }
94
95        // GeometryCollection contains B — any member contains B.
96        (Geometry::GeometryCollection { geometries }, _) => {
97            geometries.iter().any(|g| st_contains(g, b))
98        }
99
100        // Anything else: not supported or always false.
101        _ => false,
102    }
103}
104
105/// Polygon (with holes) contains a point — strict DE-9IM (boundary = false).
106fn polygon_contains_point(rings: &[Vec<[f64; 2]>], pt: [f64; 2]) -> bool {
107    let Some(exterior) = rings.first() else {
108        return false;
109    };
110
111    // Point on exterior boundary → NOT contained (DE-9IM).
112    if point_on_ring_boundary(pt, exterior) {
113        return false;
114    }
115
116    // Point must be inside exterior ring.
117    if !point_in_polygon(pt[0], pt[1], exterior) {
118        return false;
119    }
120
121    // Point must not be inside any hole.
122    for hole in &rings[1..] {
123        if point_in_polygon(pt[0], pt[1], hole) {
124            return false;
125        }
126        // Point on hole boundary is also outside (it's on A's boundary).
127        if point_on_ring_boundary(pt, hole) {
128            return false;
129        }
130    }
131
132    true
133}
134
135/// Polygon contains a linestring — all vertices inside, no edge crossings with exterior.
136fn polygon_contains_linestring(rings: &[Vec<[f64; 2]>], line: &[[f64; 2]]) -> bool {
137    if line.is_empty() {
138        return true;
139    }
140
141    let Some(exterior) = rings.first() else {
142        return false;
143    };
144
145    // All line vertices must be strictly inside the polygon.
146    for pt in line {
147        if !polygon_contains_point(rings, *pt) {
148            // Allow points on the boundary if the line passes through interior.
149            // But for strict DE-9IM, if the entire line is on the boundary, it's
150            // NOT contained. We check: if point is on boundary, at least one other
151            // point must be in the interior.
152            if !point_on_ring_boundary(*pt, exterior) {
153                return false;
154            }
155        }
156    }
157
158    // No edge of the linestring may cross any edge of the polygon exterior.
159    let poly_edges = ring_edges(exterior);
160    for i in 0..line.len() - 1 {
161        for &(pe_a, pe_b) in &poly_edges {
162            if edges_properly_cross(line[i], line[i + 1], pe_a, pe_b) {
163                return false;
164            }
165        }
166    }
167
168    // No edge of the linestring may cross any hole.
169    for hole in &rings[1..] {
170        let hole_edges = ring_edges(hole);
171        for i in 0..line.len() - 1 {
172            for &(he_a, he_b) in &hole_edges {
173                if edges_properly_cross(line[i], line[i + 1], he_a, he_b) {
174                    return false;
175                }
176            }
177        }
178        // Line vertices must not be inside holes.
179        for pt in line {
180            if point_in_polygon(pt[0], pt[1], hole) {
181                return false;
182            }
183        }
184    }
185
186    // At least one point must be strictly interior (not just on boundary).
187    line.iter().any(|pt| polygon_contains_point(rings, *pt))
188}
189
190/// Polygon A contains Polygon B.
191fn polygon_contains_polygon(rings_a: &[Vec<[f64; 2]>], rings_b: &[Vec<[f64; 2]>]) -> bool {
192    let Some(ext_b) = rings_b.first() else {
193        return true;
194    };
195
196    // All vertices of B's exterior must be inside A (or on A's boundary,
197    // but at least one must be strictly inside).
198    let Some(ext_a) = rings_a.first() else {
199        return false;
200    };
201
202    for pt in ext_b {
203        if !point_in_polygon(pt[0], pt[1], ext_a) && !point_on_ring_boundary(*pt, ext_a) {
204            return false;
205        }
206    }
207
208    // B's exterior must not be inside any hole of A.
209    for hole in &rings_a[1..] {
210        for pt in ext_b {
211            if point_in_polygon(pt[0], pt[1], hole) {
212                return false;
213            }
214        }
215    }
216
217    // No proper edge crossings between A's exterior and B's exterior.
218    let a_edges = ring_edges(ext_a);
219    let b_edges = ring_edges(ext_b);
220    for &(a1, a2) in &a_edges {
221        for &(b1, b2) in &b_edges {
222            if edges_properly_cross(a1, a2, b1, b2) {
223                return false;
224            }
225        }
226    }
227
228    // At least one vertex of B must be strictly inside A.
229    ext_b.iter().any(|pt| polygon_contains_point(rings_a, *pt))
230}
231
232/// Whether two segments properly cross (not just touch at endpoints).
233fn edges_properly_cross(a1: [f64; 2], a2: [f64; 2], b1: [f64; 2], b2: [f64; 2]) -> bool {
234    use super::edge::Orientation;
235    use super::edge::orientation;
236
237    let o1 = orientation(a1, a2, b1);
238    let o2 = orientation(a1, a2, b2);
239    let o3 = orientation(b1, b2, a1);
240    let o4 = orientation(b1, b2, a2);
241
242    // Proper crossing requires different orientations on both sides.
243    if o1 != o2
244        && o3 != o4
245        && o1 != Orientation::Collinear
246        && o2 != Orientation::Collinear
247        && o3 != Orientation::Collinear
248        && o4 != Orientation::Collinear
249    {
250        return true;
251    }
252    false
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn square_poly() -> Geometry {
260        Geometry::polygon(vec![vec![
261            [0.0, 0.0],
262            [10.0, 0.0],
263            [10.0, 10.0],
264            [0.0, 10.0],
265            [0.0, 0.0],
266        ]])
267    }
268
269    fn poly_with_hole() -> Geometry {
270        Geometry::polygon(vec![
271            vec![
272                [0.0, 0.0],
273                [20.0, 0.0],
274                [20.0, 20.0],
275                [0.0, 20.0],
276                [0.0, 0.0],
277            ],
278            vec![
279                [5.0, 5.0],
280                [15.0, 5.0],
281                [15.0, 15.0],
282                [5.0, 15.0],
283                [5.0, 5.0],
284            ],
285        ])
286    }
287
288    #[test]
289    fn point_inside_polygon() {
290        assert!(st_contains(&square_poly(), &Geometry::point(5.0, 5.0)));
291    }
292
293    #[test]
294    fn point_outside_polygon() {
295        assert!(!st_contains(&square_poly(), &Geometry::point(15.0, 5.0)));
296    }
297
298    #[test]
299    fn point_on_edge_not_contained() {
300        // DE-9IM: point on boundary is NOT contained.
301        assert!(!st_contains(&square_poly(), &Geometry::point(5.0, 0.0)));
302    }
303
304    #[test]
305    fn point_on_vertex_not_contained() {
306        assert!(!st_contains(&square_poly(), &Geometry::point(0.0, 0.0)));
307    }
308
309    #[test]
310    fn point_in_hole_not_contained() {
311        // Point in the hole of a polygon with hole.
312        assert!(!st_contains(
313            &poly_with_hole(),
314            &Geometry::point(10.0, 10.0)
315        ));
316    }
317
318    #[test]
319    fn point_between_exterior_and_hole() {
320        // Point between exterior and hole (in the ring area).
321        assert!(st_contains(&poly_with_hole(), &Geometry::point(2.0, 2.0)));
322    }
323
324    #[test]
325    fn linestring_inside_polygon() {
326        let line = Geometry::line_string(vec![[2.0, 2.0], [5.0, 5.0], [8.0, 3.0]]);
327        assert!(st_contains(&square_poly(), &line));
328    }
329
330    #[test]
331    fn linestring_crossing_boundary() {
332        let line = Geometry::line_string(vec![[5.0, 5.0], [15.0, 5.0]]);
333        assert!(!st_contains(&square_poly(), &line));
334    }
335
336    #[test]
337    fn polygon_contains_smaller_polygon() {
338        let inner = Geometry::polygon(vec![vec![
339            [2.0, 2.0],
340            [8.0, 2.0],
341            [8.0, 8.0],
342            [2.0, 8.0],
343            [2.0, 2.0],
344        ]]);
345        assert!(st_contains(&square_poly(), &inner));
346    }
347
348    #[test]
349    fn polygon_does_not_contain_overlapping() {
350        let overlapping = Geometry::polygon(vec![vec![
351            [5.0, 5.0],
352            [15.0, 5.0],
353            [15.0, 15.0],
354            [5.0, 15.0],
355            [5.0, 5.0],
356        ]]);
357        assert!(!st_contains(&square_poly(), &overlapping));
358    }
359
360    #[test]
361    fn point_contains_same_point() {
362        let p = Geometry::point(5.0, 5.0);
363        assert!(st_contains(&p, &p));
364    }
365
366    #[test]
367    fn multipoint_contained() {
368        let mp = Geometry::MultiPoint {
369            coordinates: vec![[2.0, 2.0], [5.0, 5.0], [8.0, 8.0]],
370        };
371        assert!(st_contains(&square_poly(), &mp));
372    }
373
374    #[test]
375    fn multipoint_not_all_contained() {
376        let mp = Geometry::MultiPoint {
377            coordinates: vec![[2.0, 2.0], [15.0, 15.0]],
378        };
379        assert!(!st_contains(&square_poly(), &mp));
380    }
381}