jagua_rs/geometry/primitives/
simple_polygon.rs

1use std::borrow::Borrow;
2
3use itertools::Itertools;
4use ordered_float::{NotNan, OrderedFloat};
5
6use crate::geometry::Transformation;
7use crate::geometry::convex_hull::convex_hull_from_points;
8use crate::geometry::fail_fast::{SPSurrogate, SPSurrogateConfig, compute_pole};
9use crate::geometry::geo_enums::GeoPosition;
10use crate::geometry::geo_traits::{
11    CollidesWith, DistanceTo, SeparationDistance, Transformable, TransformableFrom,
12};
13use crate::geometry::primitives::Circle;
14use crate::geometry::primitives::Edge;
15use crate::geometry::primitives::Point;
16use crate::geometry::primitives::Rect;
17use crate::util::FPA;
18use anyhow::{Result, bail};
19
20/// A Simple Polygon is a polygon that does not intersect itself and contains no holes.
21/// It is a closed shape with a finite number of vertices and edges.
22/// [read more](https://en.wikipedia.org/wiki/Simple_polygon)
23#[derive(Clone, Debug)]
24pub struct SPolygon {
25    /// Set of points that form the polygon
26    pub vertices: Vec<Point>,
27    /// Bounding box
28    pub bbox: Rect,
29    /// Area of its interior
30    pub area: f32,
31    /// Maximum distance between any two points in the polygon
32    pub diameter: f32,
33    /// [Pole of inaccessibility](https://en.wikipedia.org/wiki/Pole_of_inaccessibility) represented as a circle
34    pub poi: Circle,
35    /// Optional surrogate representation of the polygon (subset of the original)
36    pub surrogate: Option<SPSurrogate>,
37}
38
39impl SPolygon {
40    /// Create a new simple polygon from a set of points, expensive operations are performed here! Use [`Self::clone()`] or [`Self::transform()`] to avoid recomputation.
41    pub fn new(mut points: Vec<Point>) -> Result<Self> {
42        if points.len() < 3 {
43            bail!("Simple polygon must have at least 3 points: {points:?}");
44        }
45        if points.iter().unique().count() != points.len() {
46            bail!("Simple polygon should not contain duplicate points: {points:?}");
47        }
48        if let Some((e1_idx, e2_idx)) = SPolygon::find_self_intersection(&points) {
49            bail!("Simple polygon contains intersecting edges {e1_idx} and {e2_idx}: {points:?}");
50        }
51
52        let area = match SPolygon::calculate_area(&points) {
53            0.0 => bail!("Simple polygon has no area: {points:?}"),
54            area if area < 0.0 => {
55                //edges should always be ordered counterclockwise (positive area)
56                points.reverse();
57                -area
58            }
59            area => area,
60        };
61
62        let diameter = SPolygon::calculate_diameter(points.clone());
63        let bbox = SPolygon::generate_bounding_box(&points);
64        let poi = SPolygon::calculate_poi(&points, diameter)?;
65
66        Ok(SPolygon {
67            vertices: points,
68            bbox,
69            area,
70            diameter,
71            poi,
72            surrogate: None,
73        })
74    }
75
76    pub fn generate_surrogate(&mut self, config: SPSurrogateConfig) -> Result<()> {
77        //regenerate the surrogate if it is not present or if the config has changed
78        match &self.surrogate {
79            Some(surrogate) if surrogate.config == config => {}
80            _ => self.surrogate = Some(SPSurrogate::new(self, config)?),
81        }
82        Ok(())
83    }
84
85    #[must_use]
86    pub fn vertex(&self, i: usize) -> Point {
87        self.vertices[i]
88    }
89
90    #[must_use]
91    pub fn edge(&self, i: usize) -> Edge {
92        assert!(i < self.n_vertices(), "index out of bounds");
93        let j = if i == self.n_vertices() - 1 { 0 } else { i + 1 };
94        Edge {
95            start: self.vertices[i],
96            end: self.vertices[j],
97        }
98    }
99
100    pub fn edge_iter(&self) -> impl Iterator<Item = Edge> + '_ {
101        (0..self.n_vertices()).map(move |i| self.edge(i))
102    }
103
104    #[must_use]
105    pub fn n_vertices(&self) -> usize {
106        self.vertices.len()
107    }
108
109    #[must_use]
110    pub fn surrogate(&self) -> &SPSurrogate {
111        self.surrogate.as_ref().expect("surrogate not generated")
112    }
113
114    #[must_use]
115    pub fn calculate_diameter(points: Vec<Point>) -> f32 {
116        //The two points furthest apart must be part of the convex hull
117        let ch = convex_hull_from_points(points);
118
119        //go through all pairs of points and find the pair with the largest distance
120        let sq_diam = ch
121            .iter()
122            .array_combinations::<2>()
123            .map(|[p1, p2]| p1.sq_distance_to(p2))
124            .max_by_key(|sq_d| NotNan::new(*sq_d).unwrap())
125            .expect("convex hull is empty");
126
127        sq_diam.sqrt()
128    }
129
130    #[must_use]
131    pub fn generate_bounding_box(points: &[Point]) -> Rect {
132        let (mut x_min, mut y_min) = (f32::MAX, f32::MAX);
133        let (mut x_max, mut y_max) = (f32::MIN, f32::MIN);
134
135        for point in points {
136            x_min = x_min.min(point.0);
137            y_min = y_min.min(point.1);
138            x_max = x_max.max(point.0);
139            y_max = y_max.max(point.1);
140        }
141        Rect::try_new(x_min, y_min, x_max, y_max).unwrap()
142    }
143
144    //https://en.wikipedia.org/wiki/Shoelace_formula
145    //counterclockwise = positive area, clockwise = negative area
146    #[must_use]
147    pub fn calculate_area(points: &[Point]) -> f32 {
148        let mut sigma: f32 = 0.0;
149        for i in 0..points.len() {
150            //next point
151            let j = (i + 1) % points.len();
152
153            let (x_i, y_i) = points[i].into();
154            let (x_j, y_j) = points[j].into();
155
156            sigma += (y_i + y_j) * (x_i - x_j);
157        }
158
159        0.5 * sigma
160    }
161
162    pub fn calculate_poi(points: &[Point], diameter: f32) -> Result<Circle> {
163        //need to make a dummy simple polygon, because the pole generation algorithm
164        //relies on many of the methods provided by the simple polygon struct
165        let dummy_sp = {
166            let bbox = SPolygon::generate_bounding_box(points);
167            let area = SPolygon::calculate_area(points);
168            let dummy_poi = Circle::try_new(Point(f32::MAX, f32::MAX), f32::MAX).unwrap();
169
170            SPolygon {
171                vertices: points.to_vec(),
172                bbox,
173                area,
174                diameter,
175                poi: dummy_poi,
176                surrogate: None,
177            }
178        };
179
180        compute_pole(&dummy_sp, &[])
181    }
182
183    #[must_use]
184    pub fn centroid(&self) -> Point {
185        //based on: https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
186
187        let area = self.area;
188        let mut c_x = 0.0;
189        let mut c_y = 0.0;
190
191        for i in 0..self.n_vertices() {
192            let j = if i == self.n_vertices() - 1 { 0 } else { i + 1 };
193            let Point(x_i, y_i) = self.vertex(i);
194            let Point(x_j, y_j) = self.vertex(j);
195            c_x += (x_i + x_j) * (x_i * y_j - x_j * y_i);
196            c_y += (y_i + y_j) * (x_i * y_j - x_j * y_i);
197        }
198
199        c_x /= 6.0 * area;
200        c_y /= 6.0 * area;
201
202        (c_x, c_y).into()
203    }
204
205    fn find_self_intersection(points: &[Point]) -> Option<(usize, usize)> {
206        let edge = |i| Edge {
207            start: points[i],
208            end: points[(i + 1) % points.len()],
209        };
210        let are_neighboring_edges = |i, j| i + 1 == j || (i == 0 && j == points.len() - 1);
211
212        (0..points.len()).combinations(2).find_map(|pair| {
213            let (i, j) = (pair[0], pair[1]);
214            (!are_neighboring_edges(i, j) && edge(i).collides_with(&edge(j))).then_some((i, j))
215        })
216    }
217}
218
219impl Transformable for SPolygon {
220    fn transform(&mut self, t: &Transformation) -> &mut Self {
221        //destructuring pattern to ensure that the code is updated when the struct changes
222        let SPolygon {
223            vertices: points,
224            bbox,
225            area: _,
226            diameter: _,
227            poi,
228            surrogate,
229        } = self;
230
231        //transform all points of the simple poly
232        for p in points.iter_mut() {
233            p.transform(t);
234        }
235
236        poi.transform(t);
237
238        //transform the surrogate
239        if let Some(surrogate) = surrogate.as_mut() {
240            surrogate.transform(t);
241        }
242
243        //regenerate bounding box
244        *bbox = SPolygon::generate_bounding_box(points);
245
246        self
247    }
248}
249
250impl TransformableFrom for SPolygon {
251    fn transform_from(&mut self, reference: &Self, t: &Transformation) -> &mut Self {
252        //destructuring pattern to ensure that the code is updated when the struct changes
253        let SPolygon {
254            vertices: points,
255            bbox,
256            area: _,
257            diameter: _,
258            poi,
259            surrogate,
260        } = self;
261
262        for (p, ref_p) in points.iter_mut().zip(&reference.vertices) {
263            p.transform_from(ref_p, t);
264        }
265
266        poi.transform_from(&reference.poi, t);
267
268        //transform the surrogate
269        if let Some(surrogate) = surrogate.as_mut() {
270            surrogate.transform_from(reference.surrogate(), t);
271        }
272        //regenerate bounding box
273        *bbox = SPolygon::generate_bounding_box(points);
274
275        self
276    }
277}
278
279impl CollidesWith<Point> for SPolygon {
280    fn collides_with(&self, point: &Point) -> bool {
281        //based on the ray casting algorithm: https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
282        if self.bbox.collides_with(point) {
283            //horizontal ray shot to the right.
284            //Starting from the point to another point that is certainly outside the shape
285            let point_outside = Point(self.bbox.x_max + self.bbox.width(), point.1);
286            let ray = Edge {
287                start: *point,
288                end: point_outside,
289            };
290
291            let mut n_intersections = 0;
292            for edge in self.edge_iter() {
293                //Check if the ray does not go through (or almost through) a vertex
294                //This can result in funky behaviour, which could incorrect results
295                //Therefore we handle this case
296                let (s_x, s_y) = (FPA(edge.start.0), FPA(edge.start.1));
297                let (e_x, e_y) = (FPA(edge.end.0), FPA(edge.end.1));
298                let (p_x, p_y) = (FPA(point.0), FPA(point.1));
299
300                if (s_y == p_y && s_x > p_x) || (e_y == p_y && e_x > p_x) {
301                    //in this case, the ray passes through (or dangerously close to) a vertex
302                    //We handle this case by only counting an intersection if the edge is below the ray
303                    if s_y < p_y || e_y < p_y {
304                        n_intersections += 1;
305                    }
306                } else if ray.collides_with(&edge) {
307                    n_intersections += 1;
308                }
309            }
310            n_intersections % 2 == 1
311        } else {
312            false
313        }
314    }
315}
316
317impl DistanceTo<Point> for SPolygon {
318    fn distance_to(&self, point: &Point) -> f32 {
319        self.sq_distance_to(point).sqrt()
320    }
321    fn sq_distance_to(&self, point: &Point) -> f32 {
322        if self.collides_with(point) {
323            0.0
324        } else {
325            self.edge_iter()
326                .map(|edge| edge.sq_distance_to(point))
327                .min_by(|a, b| a.partial_cmp(b).unwrap())
328                .unwrap()
329        }
330    }
331}
332
333impl SeparationDistance<Point> for SPolygon {
334    fn separation_distance(&self, point: &Point) -> (GeoPosition, f32) {
335        let (position, sq_distance) = self.sq_separation_distance(point);
336        (position, sq_distance.sqrt())
337    }
338
339    fn sq_separation_distance(&self, point: &Point) -> (GeoPosition, f32) {
340        let distance_to_closest_edge = self
341            .edge_iter()
342            .map(|edge| edge.sq_distance_to(point))
343            .min_by_key(|sq_d| OrderedFloat(*sq_d))
344            .unwrap();
345
346        if self.collides_with(point) {
347            (GeoPosition::Interior, distance_to_closest_edge)
348        } else {
349            (GeoPosition::Exterior, distance_to_closest_edge)
350        }
351    }
352}
353
354impl<T> From<T> for SPolygon
355where
356    T: Borrow<Rect>,
357{
358    fn from(r: T) -> Self {
359        let r = r.borrow();
360        SPolygon::new(vec![
361            (r.x_min, r.y_min).into(),
362            (r.x_max, r.y_min).into(),
363            (r.x_max, r.y_max).into(),
364            (r.x_min, r.y_max).into(),
365        ])
366        .unwrap()
367    }
368}