Skip to main content

interior_point/
centroid_first_interior_point.rs

1//! A centroid-first variant of `interior_point`. For an areal geometry it
2//! returns the centroid whenever the centroid lies strictly inside that
3//! geometry, and otherwise returns exactly what `interior_point` returns.
4//! Dimensions 0 and 1 are handed straight to `interior_point`.
5//!
6//! The centroid is preferred because it is a stable, purely arithmetic function
7//! of the input: two implementations that agree on the arithmetic agree on the
8//! point. The scanline fallback is not — it depends on the scan line chosen and
9//! on the widest interval found along it — so it is worth reaching for only when
10//! the centroid is not inside the geometry, which for a convex shape is never.
11//!
12//! Acceptance is INTERIOR alone. A centroid that lands exactly on the boundary
13//! is rejected, which keeps this function from ever returning a point the
14//! geometry only touches.
15//!
16//! @jts-adapter InteriorPoint — an original surface with no JTS counterpart:
17//!   JTS has no centroid-first entry point, and nothing here is ported. Every
18//!   member it calls is.
19
20use geo_types::{Coord, Geometry};
21
22use crate::algorithm::centroid::get_centroid;
23use crate::algorithm::interior_point::{dimension_non_empty, interior_point};
24use crate::algorithm::locate::simple_point_in_area_locator::locate;
25use crate::geom::location::INTERIOR;
26use crate::geometry_adapter::is_geometry_empty;
27
28/// Computes a representative point of a geometry, preferring its centroid.
29///
30/// Returns the centroid when it lies inside `geom`, otherwise whatever
31/// `interior_point` returns, or `None` if the input is empty. The signature is
32/// `interior_point`'s, so a caller swaps one call for the other. Which of the
33/// two branches produced the point is not reported: a caller that needs to know
34/// can compare the result against a centroid it computes itself.
35///
36/// Dimensions other than 2 delegate without computing a centroid at all. The
37/// check could not pass there — the locator answers EXTERIOR for every point
38/// against a puntal or lineal geometry, including that geometry's own vertices —
39/// so computing a centroid only to discard it would be pure waste.
40///
41/// The predicate calls the locator directly rather than going through
42/// `verify_interior_point`: at dimension 2 that function is this same locator
43/// call plus a mapping onto its outcome enum, and it would also accept a point
44/// on the boundary, which this function does not.
45pub fn centroid_first_interior_point(geom: &Geometry<f64>) -> Option<Coord<f64>> {
46    if is_geometry_empty(geom) {
47        return None;
48    }
49
50    let dim = dimension_non_empty(geom);
51    if dim != 2 {
52        return interior_point(geom);
53    }
54
55    if let Some(centroid) = get_centroid(geom)
56        && locate(centroid, geom) == INTERIOR
57    {
58        return Some(centroid);
59    }
60    interior_point(geom)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::centroid_first_interior_point;
66    use crate::algorithm::centroid::get_centroid;
67    use crate::algorithm::interior_point::interior_point;
68    use crate::algorithm::locate::simple_point_in_area_locator::locate;
69    use crate::geom::location::{BOUNDARY, EXTERIOR, INTERIOR};
70    use geo_types::{
71        Coord, Geometry, GeometryCollection, LineString, MultiPoint, MultiPolygon, Point, Polygon,
72    };
73
74    /// A polygon from its rings, shell first.
75    fn polygon(rings: Vec<Vec<(f64, f64)>>) -> Geometry<f64> {
76        let mut rings = rings.into_iter();
77        let shell = LineString::from(rings.next().expect("a polygon needs a shell"));
78        Geometry::Polygon(Polygon::new(shell, rings.map(LineString::from).collect()))
79    }
80
81    fn coord(x: f64, y: f64) -> Coord<f64> {
82        Coord { x, y }
83    }
84
85    fn triangle() -> Geometry<f64> {
86        polygon(vec![vec![(0.0, 0.0), (10.0, 0.0), (0.0, 10.0), (0.0, 0.0)]])
87    }
88
89    fn square() -> Geometry<f64> {
90        polygon(vec![vec![
91            (0.0, 0.0),
92            (10.0, 0.0),
93            (10.0, 10.0),
94            (0.0, 10.0),
95            (0.0, 0.0),
96        ]])
97    }
98
99    /// A square with a central hole. Its centroid falls in the hole.
100    fn donut() -> Geometry<f64> {
101        polygon(vec![
102            vec![
103                (0.0, 0.0),
104                (10.0, 0.0),
105                (10.0, 10.0),
106                (0.0, 10.0),
107                (0.0, 0.0),
108            ],
109            vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0), (3.0, 3.0)],
110        ])
111    }
112
113    /// A C, notched from the right between y = 2 and y = 8. Its centroid falls
114    /// in the notch, which is outside the polygon.
115    fn c_shape() -> Geometry<f64> {
116        polygon(vec![vec![
117            (0.0, 0.0),
118            (10.0, 0.0),
119            (10.0, 2.0),
120            (3.0, 2.0),
121            (3.0, 8.0),
122            (10.0, 8.0),
123            (10.0, 10.0),
124            (0.0, 10.0),
125            (0.0, 0.0),
126        ]])
127    }
128
129    /// An L, notched from the upper right. Its centroid falls in the notch.
130    fn l_shape() -> Geometry<f64> {
131        polygon(vec![vec![
132            (0.0, 0.0),
133            (10.0, 0.0),
134            (10.0, 3.0),
135            (3.0, 3.0),
136            (3.0, 10.0),
137            (0.0, 10.0),
138            (0.0, 0.0),
139        ]])
140    }
141
142    /// A ring whose hole is its shell, so the polygon encloses no area at all.
143    fn shell_identical_to_hole() -> Geometry<f64> {
144        let ring = vec![(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)];
145        polygon(vec![ring.clone(), ring])
146    }
147
148    /// A ring whose vertices are collinear: dimension 2, zero area.
149    fn collinear_ring() -> Geometry<f64> {
150        polygon(vec![vec![(0.0, 0.0), (5.0, 0.0), (10.0, 0.0), (0.0, 0.0)]])
151    }
152
153    fn line() -> Geometry<f64> {
154        Geometry::LineString(LineString::from(vec![(0.0, 0.0), (10.0, 10.0)]))
155    }
156
157    #[test]
158    fn returns_the_centroid_when_it_lies_inside_an_areal_geometry() {
159        let geom = triangle();
160        let centroid = get_centroid(&geom).expect("a triangle has a centroid");
161        assert_eq!(locate(centroid, &geom), INTERIOR);
162        assert_eq!(centroid, coord(3.333333333333333, 3.333333333333333));
163        assert_eq!(centroid_first_interior_point(&geom), Some(centroid));
164        // And the point it did not return, so the two branches are visibly
165        // different on this input.
166        assert_eq!(interior_point(&geom), Some(coord(2.5, 5.0)));
167    }
168
169    /// The one shape where both branches agree. It is here so that a reader
170    /// does not mistake agreement for the centroid branch never firing.
171    #[test]
172    fn returns_the_centroid_for_a_square_where_both_branches_agree() {
173        let geom = square();
174        let centroid = get_centroid(&geom).expect("a square has a centroid");
175        assert_eq!(centroid, coord(5.0, 5.0));
176        assert_eq!(centroid_first_interior_point(&geom), Some(centroid));
177        assert_eq!(interior_point(&geom), Some(centroid));
178    }
179
180    #[test]
181    fn returns_the_centroid_for_a_collection_whose_areal_part_accepts_it() {
182        let geom = Geometry::GeometryCollection(GeometryCollection(vec![
183            polygon(vec![vec![
184                (0.0, 0.0),
185                (4.0, 0.0),
186                (4.0, 4.0),
187                (0.0, 4.0),
188                (0.0, 0.0),
189            ]]),
190            Geometry::LineString(LineString::from(vec![(0.0, 50.0), (10.0, 60.0)])),
191        ]));
192        let centroid = get_centroid(&geom).expect("the collection has a centroid");
193        assert_eq!(centroid, coord(2.0, 2.0));
194        assert_eq!(locate(centroid, &geom), INTERIOR);
195        assert_eq!(centroid_first_interior_point(&geom), Some(centroid));
196    }
197
198    #[test]
199    fn falls_back_when_the_centroid_lands_in_a_hole() {
200        let geom = donut();
201        assert_eq!(locate(get_centroid(&geom).unwrap(), &geom), EXTERIOR);
202        assert_eq!(centroid_first_interior_point(&geom), interior_point(&geom));
203        assert_eq!(centroid_first_interior_point(&geom), Some(coord(1.5, 5.0)));
204    }
205
206    #[test]
207    fn falls_back_when_the_centroid_lands_in_a_notch() {
208        for geom in [c_shape(), l_shape()] {
209            let centroid = get_centroid(&geom).expect("both shapes have a centroid");
210            assert_eq!(locate(centroid, &geom), EXTERIOR);
211            assert_eq!(centroid_first_interior_point(&geom), interior_point(&geom));
212            assert_ne!(centroid_first_interior_point(&geom), Some(centroid));
213        }
214    }
215
216    #[test]
217    fn falls_back_for_a_multi_polygon_whose_centroid_is_between_its_parts() {
218        let geom = Geometry::MultiPolygon(MultiPolygon(vec![
219            Polygon::new(
220                LineString::from(vec![
221                    (0.0, 0.0),
222                    (10.0, 0.0),
223                    (10.0, 10.0),
224                    (0.0, 10.0),
225                    (0.0, 0.0),
226                ]),
227                vec![],
228            ),
229            Polygon::new(
230                LineString::from(vec![
231                    (20.0, 0.0),
232                    (30.0, 0.0),
233                    (30.0, 10.0),
234                    (20.0, 10.0),
235                    (20.0, 0.0),
236                ]),
237                vec![],
238            ),
239        ]));
240        assert_eq!(get_centroid(&geom), Some(coord(15.0, 5.0)));
241        assert_eq!(locate(coord(15.0, 5.0), &geom), EXTERIOR);
242        assert_eq!(centroid_first_interior_point(&geom), Some(coord(5.0, 5.0)));
243    }
244
245    #[test]
246    fn falls_back_when_the_shell_is_its_own_hole() {
247        let geom = shell_identical_to_hole();
248        assert_eq!(get_centroid(&geom), Some(coord(2.0, 2.0)));
249        assert_eq!(locate(coord(2.0, 2.0), &geom), EXTERIOR);
250        assert_eq!(centroid_first_interior_point(&geom), Some(coord(0.0, 0.0)));
251    }
252
253    /// Acceptance is INTERIOR alone. This centroid sits exactly on the
254    /// degenerate ring, which the locator answers BOUNDARY for, and a boundary
255    /// point is not accepted.
256    #[test]
257    fn rejects_a_centroid_that_lands_on_the_boundary() {
258        let geom = collinear_ring();
259        let centroid = get_centroid(&geom).expect("a collinear ring has a centroid");
260        assert_eq!(centroid, coord(5.0, 0.0));
261        assert_eq!(locate(centroid, &geom), BOUNDARY);
262        assert_eq!(centroid_first_interior_point(&geom), Some(coord(0.0, 0.0)));
263        assert_eq!(centroid_first_interior_point(&geom), interior_point(&geom));
264    }
265
266    /// At dimension 2 with no area anywhere, the centroid falls through to the
267    /// lineal branch and is pulled toward the line, far from the polygon.
268    #[test]
269    fn falls_back_for_a_collection_whose_centroid_is_dragged_off_by_a_line() {
270        let geom = Geometry::GeometryCollection(GeometryCollection(vec![
271            collinear_ring(),
272            Geometry::LineString(LineString::from(vec![(0.0, 50.0), (10.0, 60.0)])),
273        ]));
274        assert_eq!(get_centroid(&geom), Some(coord(5.0, 22.781745930520227)));
275        assert_eq!(centroid_first_interior_point(&geom), Some(coord(0.0, 0.0)));
276    }
277
278    #[test]
279    fn delegates_every_dimension_below_two() {
280        let point = Geometry::Point(Point::new(5.0, 5.0));
281        let points = Geometry::MultiPoint(MultiPoint(vec![
282            Point::new(0.0, 0.0),
283            Point::new(10.0, 10.0),
284        ]));
285        let mixed = Geometry::GeometryCollection(GeometryCollection(vec![
286            Geometry::Point(Point::new(5.0, 5.0)),
287            line(),
288        ]));
289        for geom in [point, line(), points, mixed] {
290            assert_eq!(centroid_first_interior_point(&geom), interior_point(&geom));
291        }
292    }
293
294    /// The locator answers EXTERIOR for a lineal geometry's own vertices, so
295    /// the dimension branch is not merely an optimisation of a check that would
296    /// otherwise pass — the check could never pass there.
297    #[test]
298    fn a_lineal_centroid_would_have_been_rejected_anyway() {
299        let geom = line();
300        let centroid = get_centroid(&geom).expect("a line has a centroid");
301        assert_eq!(centroid, coord(5.0, 5.0));
302        assert_eq!(locate(centroid, &geom), EXTERIOR);
303        assert_eq!(centroid_first_interior_point(&geom), Some(coord(0.0, 0.0)));
304    }
305
306    #[test]
307    fn answers_none_for_every_empty_shape() {
308        let empty_polygon = Geometry::Polygon(Polygon::new(LineString(vec![]), vec![]));
309        let empty_multi_polygon = Geometry::MultiPolygon(MultiPolygon(vec![]));
310        let multi_polygon_of_one_empty =
311            Geometry::MultiPolygon(MultiPolygon(vec![Polygon::new(LineString(vec![]), vec![])]));
312        let hole_without_a_shell = Geometry::Polygon(Polygon::new(
313            LineString(vec![]),
314            vec![LineString::from(vec![
315                (0.0, 0.0),
316                (4.0, 0.0),
317                (4.0, 4.0),
318                (0.0, 0.0),
319            ])],
320        ));
321        for geom in [
322            empty_polygon,
323            empty_multi_polygon,
324            multi_polygon_of_one_empty,
325            hole_without_a_shell,
326        ] {
327            assert_eq!(centroid_first_interior_point(&geom), None);
328            assert_eq!(interior_point(&geom), None);
329        }
330    }
331
332    /// The result is one of exactly two points on every input, which is the
333    /// whole contract restated as a property.
334    #[test]
335    fn always_returns_either_the_centroid_or_the_algorithms_own_point() {
336        for geom in [
337            triangle(),
338            square(),
339            donut(),
340            c_shape(),
341            l_shape(),
342            shell_identical_to_hole(),
343            collinear_ring(),
344            line(),
345        ] {
346            let result = centroid_first_interior_point(&geom);
347            assert!(result == get_centroid(&geom) || result == interior_point(&geom));
348        }
349    }
350}