Skip to main content

nodedb_spatial/predicates/
contains.rs

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