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}