geo/algorithm/
is_convex.rs

1use crate::kernels::*;
2use crate::{Coord, LineString};
3
4/// Predicates to test the convexity of a [ `LineString` ].
5/// A closed `LineString` is said to be _convex_ if it
6/// encloses a [convex set]. It is said to be _strictly
7/// convex_ if in addition, no three consecutive vertices
8/// are collinear. It is _collinear_ if all the vertices lie
9/// on the same line.
10///
11/// # Remarks
12///
13/// - Collinearity does not require that the `LineString`
14/// be closed, but the rest of the predicates do.
15///
16/// - This definition is closely related to the notion
17/// of [convexity of polygons][convex set]. In particular, a
18/// [`Polygon`](crate::Polygon) is convex, if and only if its `exterior` is
19/// convex, and `interiors` is empty.
20///
21/// - The [`ConvexHull`] algorithm always returns a strictly
22/// convex `LineString` unless the input is empty or
23/// collinear. The [`graham_hull`] algorithm provides an
24/// option to include collinear points, producing a
25/// (possibly non-strict) convex `LineString`.
26///
27/// # Edge Cases
28///
29/// - the convexity, and collinearity of an empty
30/// `LineString` is _unspecified_ and must not be relied
31/// upon.
32///
33/// - A closed `LineString` with at most three coordinates
34/// (including the possibly repeated first coordinate) is
35/// both convex and collinear. However, the strict convexity
36/// is _unspecified_ and must not be relied upon.
37///
38/// [convex combination]: //en.wikipedia.org/wiki/Convex_combination
39/// [convex set]: //en.wikipedia.org/wiki/Convex_set
40/// [`ConvexHull`]: crate::ConvexHull
41/// [`graham_hull`]: crate::convex_hull::graham_hull
42pub trait IsConvex {
43    /// Test and get the orientation if the shape is convex.
44    /// Tests for strict convexity if `allow_collinear`, and
45    /// only accepts a specific orientation if provided.
46    ///
47    /// The return value is `None` if either:
48    ///
49    /// 1. the shape is not convex
50    ///
51    /// 1. the shape is not strictly convex, and
52    ///    `allow_collinear` is false
53    ///
54    /// 1. an orientation is specified, and some three
55    ///    consecutive vertices where neither collinear, nor
56    ///    in the specified orientation.
57    ///
58    /// In all other cases, the return value is the
59    /// orientation of the shape, or `Orientation::Collinear`
60    /// if all the vertices are on the same line.
61    ///
62    /// **Note.** This predicate is not equivalent to
63    /// `is_collinear` as this requires that the input is
64    /// closed.
65    fn convex_orientation(
66        &self,
67        allow_collinear: bool,
68        specific_orientation: Option<Orientation>,
69    ) -> Option<Orientation>;
70
71    /// Test if the shape is convex.
72    fn is_convex(&self) -> bool {
73        self.convex_orientation(true, None).is_some()
74    }
75
76    /// Test if the shape is convex, and oriented
77    /// counter-clockwise.
78    fn is_ccw_convex(&self) -> bool {
79        self.convex_orientation(true, Some(Orientation::CounterClockwise))
80            .is_some()
81    }
82
83    /// Test if the shape is convex, and oriented clockwise.
84    fn is_cw_convex(&self) -> bool {
85        self.convex_orientation(true, Some(Orientation::Clockwise))
86            .is_some()
87    }
88
89    /// Test if the shape is strictly convex.
90    fn is_strictly_convex(&self) -> bool {
91        self.convex_orientation(false, None).is_some()
92    }
93
94    /// Test if the shape is strictly convex, and oriented
95    /// counter-clockwise.
96    fn is_strictly_ccw_convex(&self) -> bool {
97        self.convex_orientation(false, Some(Orientation::CounterClockwise))
98            == Some(Orientation::CounterClockwise)
99    }
100
101    /// Test if the shape is strictly convex, and oriented
102    /// clockwise.
103    fn is_strictly_cw_convex(&self) -> bool {
104        self.convex_orientation(false, Some(Orientation::Clockwise)) == Some(Orientation::Clockwise)
105    }
106
107    /// Test if the shape lies on a line.
108    fn is_collinear(&self) -> bool;
109}
110
111impl<T: HasKernel> IsConvex for LineString<T> {
112    fn convex_orientation(
113        &self,
114        allow_collinear: bool,
115        specific_orientation: Option<Orientation>,
116    ) -> Option<Orientation> {
117        if !self.is_closed() || self.0.is_empty() {
118            None
119        } else {
120            is_convex_shaped(&self.0[1..], allow_collinear, specific_orientation)
121        }
122    }
123
124    fn is_collinear(&self) -> bool {
125        self.0.is_empty()
126            || is_convex_shaped(&self.0[1..], true, Some(Orientation::Collinear)).is_some()
127    }
128}
129
130/// A utility that tests convexity of a sequence of
131/// coordinates. It verifies that for all `0 <= i < n`, the
132/// vertices at positions `i`, `i+1`, `i+2` (mod `n`) have
133/// the same orientation, optionally accepting collinear
134/// triplets, and expecting a specific orientation. The
135/// output is `None` or the only non-collinear orientation,
136/// unless everything is collinear.
137fn is_convex_shaped<T>(
138    coords: &[Coord<T>],
139    allow_collinear: bool,
140    specific_orientation: Option<Orientation>,
141) -> Option<Orientation>
142where
143    T: HasKernel,
144{
145    let n = coords.len();
146
147    let orientation_at = |i: usize| {
148        let coord = coords[i];
149        let next = coords[(i + 1) % n];
150        let nnext = coords[(i + 2) % n];
151        (i, T::Ker::orient2d(coord, next, nnext))
152    };
153
154    let find_first_non_collinear = (0..n).map(orientation_at).find_map(|(i, orientation)| {
155        match orientation {
156            Orientation::Collinear => {
157                // If collinear accepted, we skip, otherwise
158                // stop.
159                if allow_collinear {
160                    None
161                } else {
162                    Some((i, orientation))
163                }
164            }
165            _ => Some((i, orientation)),
166        }
167    });
168
169    let (i, first_non_collinear) = if let Some((i, orientation)) = find_first_non_collinear {
170        match orientation {
171            Orientation::Collinear => {
172                // Only happens if !allow_collinear
173                assert!(!allow_collinear);
174                return None;
175            }
176            _ => (i, orientation),
177        }
178    } else {
179        // Empty or everything collinear, and allowed.
180        return Some(Orientation::Collinear);
181    };
182
183    // If a specific orientation is expected, accept only that.
184    if let Some(req_orientation) = specific_orientation {
185        if req_orientation != first_non_collinear {
186            return None;
187        }
188    }
189
190    // Now we have a fixed orientation expected at the rest
191    // of the coords. Loop to check everything matches it.
192    if ((i + 1)..n)
193        .map(orientation_at)
194        .all(|(_, orientation)| match orientation {
195            Orientation::Collinear => allow_collinear,
196            orientation => orientation == first_non_collinear,
197        })
198    {
199        Some(first_non_collinear)
200    } else {
201        None
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use geo_types::line_string;
209
210    #[test]
211    fn test_corner_cases() {
212        // This is just tested to ensure there is no panic
213        // due to out-of-index access
214        let empty: LineString = line_string!();
215        assert!(empty.is_collinear());
216        assert!(!empty.is_convex());
217        assert!(!empty.is_strictly_ccw_convex());
218
219        let one = line_string![(x: 0., y: 0.)];
220        assert!(one.is_collinear());
221        assert!(one.is_convex());
222        assert!(one.is_cw_convex());
223        assert!(one.is_ccw_convex());
224        assert!(one.is_strictly_convex());
225        assert!(!one.is_strictly_ccw_convex());
226        assert!(!one.is_strictly_cw_convex());
227
228        let one_rep = line_string![(x: 0, y: 0), (x: 0, y: 0)];
229        assert!(one_rep.is_collinear());
230        assert!(one_rep.is_convex());
231        assert!(one_rep.is_cw_convex());
232        assert!(one_rep.is_ccw_convex());
233        assert!(!one_rep.is_strictly_convex());
234        assert!(!one_rep.is_strictly_ccw_convex());
235        assert!(!one_rep.is_strictly_cw_convex());
236
237        let mut two = line_string![(x: 0, y: 0), (x: 1, y: 1)];
238        assert!(two.is_collinear());
239        assert!(!two.is_convex());
240
241        two.close();
242        assert!(two.is_cw_convex());
243        assert!(two.is_ccw_convex());
244        assert!(!two.is_strictly_convex());
245        assert!(!two.is_strictly_ccw_convex());
246        assert!(!two.is_strictly_cw_convex());
247    }
248}