use std::cmp::Ordering;
use std::collections::VecDeque;
use crate::kernels::*;
use crate::{Coord, GeoNum, LineString, Orientation};
pub trait IsConvex {
fn convex_orientation(
&self,
allow_collinear: bool,
specific_orientation: Option<Orientation>,
) -> Option<Orientation>;
fn is_convex(&self) -> bool {
self.convex_orientation(true, None).is_some()
}
fn is_ccw_convex(&self) -> bool {
self.convex_orientation(true, Some(Orientation::CounterClockwise))
.is_some()
}
fn is_cw_convex(&self) -> bool {
self.convex_orientation(true, Some(Orientation::Clockwise))
.is_some()
}
fn is_strictly_convex(&self) -> bool {
self.convex_orientation(false, None).is_some()
}
fn is_strictly_ccw_convex(&self) -> bool {
self.convex_orientation(false, Some(Orientation::CounterClockwise))
== Some(Orientation::CounterClockwise)
}
fn is_strictly_cw_convex(&self) -> bool {
self.convex_orientation(false, Some(Orientation::Clockwise)) == Some(Orientation::Clockwise)
}
fn is_collinear(&self) -> bool;
}
impl<T: GeoNum> IsConvex for LineString<T> {
fn convex_orientation(
&self,
allow_collinear: bool,
specific_orientation: Option<Orientation>,
) -> Option<Orientation> {
if !self.is_closed() || self.0.is_empty() {
None
} else {
if is_convex_sign_flips(&self.0) {
is_convex_shaped(&self.0[1..], allow_collinear, specific_orientation)
} else {
None
}
}
}
fn is_collinear(&self) -> bool {
self.0.is_empty()
|| is_convex_shaped(&self.0[1..], true, Some(Orientation::Collinear)).is_some()
}
fn is_convex(&self) -> bool {
is_convex_sign_flips(&self.0)
}
}
fn is_convex_sign_flips<T: GeoNum>(coords: &[Coord<T>]) -> bool {
if coords.first() != coords.last() {
return false;
}
let n = coords.len();
if n <= 1 {
return true; }
if n == 2 {
return true; }
let mut w_orientation: Option<Orientation> = None;
let mut x_sign = 0i8; let mut x_first_sign = 0i8; let mut x_flips = 0;
let mut y_sign = 0i8;
let mut y_first_sign = 0i8;
let mut y_flips = 0;
let vertices = &coords[..n - 1];
let vertex_count = vertices.len();
let mut prev = vertices[vertex_count - 1];
let mut curr = vertices[0];
for next in vertices.iter().cycle().skip(1).take(vertex_count) {
let ax = next.x - curr.x; let ay = next.y - curr.y;
match ax.partial_cmp(&T::zero()) {
Some(Ordering::Greater) => {
match x_sign.cmp(&0) {
Ordering::Equal => x_first_sign = 1,
Ordering::Less => x_flips += 1,
_ => {}
}
x_sign = 1;
}
Some(Ordering::Less) => {
match x_sign.cmp(&0) {
Ordering::Equal => x_first_sign = -1,
Ordering::Greater => x_flips += 1,
_ => {}
}
x_sign = -1;
}
_ => {
}
}
if x_flips > 2 {
return false;
}
match ay.partial_cmp(&T::zero()) {
Some(Ordering::Greater) => {
match y_sign.cmp(&0) {
Ordering::Equal => y_first_sign = 1,
Ordering::Less => y_flips += 1,
_ => {}
}
y_sign = 1;
}
Some(Ordering::Less) => {
match y_sign.cmp(&0) {
Ordering::Equal => y_first_sign = -1,
Ordering::Greater => y_flips += 1,
_ => {}
}
y_sign = -1;
}
_ => {
}
}
if y_flips > 2 {
return false;
}
let current_orientation = T::Ker::orient2d(prev, curr, *next);
if current_orientation != Orientation::Collinear {
if let Some(established_orientation) = w_orientation {
if current_orientation != established_orientation {
return false;
}
} else {
w_orientation = Some(current_orientation); }
}
prev = curr;
curr = *next;
}
if x_sign != 0 && x_first_sign != 0 && x_sign != x_first_sign {
x_flips += 1;
}
if y_sign != 0 && y_first_sign != 0 && y_sign != y_first_sign {
y_flips += 1;
}
if vertex_count == 3 {
w_orientation.is_some() || (x_flips <= 2 && y_flips <= 2)
} else {
x_flips == 2 && y_flips == 2
}
}
fn is_convex_shaped<T>(
coords: &[Coord<T>],
allow_collinear: bool,
specific_orientation: Option<Orientation>,
) -> Option<Orientation>
where
T: GeoNum,
{
if coords.is_empty() {
return Some(Orientation::Collinear);
}
let c0 = coords[0];
let mut coords: VecDeque<Coord<T>> = coords
.windows(2)
.filter_map(|w| if w[0] == w[1] { None } else { Some(w[1]) })
.collect();
coords.push_front(c0);
let n = coords.len();
let orientation_at = |i: usize| {
let coord = coords[i];
let next = coords[(i + 1) % n];
let nnext = coords[(i + 2) % n];
(i, T::Ker::orient2d(coord, next, nnext))
};
let find_first_non_collinear = (0..n).map(orientation_at).find_map(|(i, orientation)| {
match orientation {
Orientation::Collinear => {
if allow_collinear {
None
} else {
Some((i, orientation))
}
}
_ => Some((i, orientation)),
}
});
let (i, first_non_collinear) = if let Some((i, orientation)) = find_first_non_collinear {
match orientation {
Orientation::Collinear => {
assert!(!allow_collinear);
return None;
}
_ => (i, orientation),
}
} else {
return Some(Orientation::Collinear);
};
if let Some(req_orientation) = specific_orientation
&& req_orientation != first_non_collinear
{
return None;
}
if ((i + 1)..n)
.map(orientation_at)
.all(|(_, orientation)| match orientation {
Orientation::Collinear => allow_collinear,
orientation => orientation == first_non_collinear,
})
{
Some(first_non_collinear)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algorithm::Convert;
use crate::wkt;
use geo_types::line_string;
#[test]
fn test_corner_cases() {
let empty: LineString = line_string!();
assert!(empty.is_collinear());
assert!(empty.is_convex());
assert!(!empty.is_strictly_ccw_convex());
let one = line_string![(x: 0., y: 0.)];
assert!(one.is_collinear());
assert!(one.is_convex());
assert!(one.is_cw_convex());
assert!(one.is_ccw_convex());
assert!(one.is_strictly_convex());
assert!(!one.is_strictly_ccw_convex());
assert!(!one.is_strictly_cw_convex());
let one_rep = line_string![(x: 0, y: 0), (x: 0, y: 0)];
assert!(one_rep.is_collinear());
assert!(one_rep.is_convex());
assert!(one_rep.is_cw_convex());
assert!(one_rep.is_ccw_convex());
assert!(!one_rep.is_strictly_convex());
assert!(!one_rep.is_strictly_ccw_convex());
assert!(!one_rep.is_strictly_cw_convex());
let mut two = line_string![(x: 0, y: 0), (x: 1, y: 1)];
assert!(two.is_collinear());
assert!(!two.is_convex());
two.close();
assert!(two.is_cw_convex());
assert!(two.is_ccw_convex());
assert!(!two.is_strictly_convex());
assert!(!two.is_strictly_ccw_convex());
assert!(!two.is_strictly_cw_convex());
}
#[test]
fn test_duplicate_pt() {
let ls_unclosed: LineString<f64> = wkt! (LINESTRING (0 0, 1 1, 2 0)).convert();
let ls_cw: LineString<f64> = wkt! (LINESTRING (0 0, 2 0, 1 1, 0 0)).convert();
let ls_1: LineString<f64> = wkt! (LINESTRING (0 0, 1 1, 2 0, 0 0)).convert();
let ls_2: LineString<f64> = wkt! (LINESTRING (0 0, 1 1, 2 0, 2 0, 0 0)).convert();
let ls_3: LineString<f64> = wkt! (LINESTRING (0 0, 1 1, 2 0, 2 0, 2 0, 0 0)).convert();
assert!(!ls_unclosed.is_convex());
assert!(ls_cw.is_convex());
assert!(ls_cw.is_ccw_convex());
assert!(ls_1.is_convex());
assert!(ls_1.is_cw_convex());
assert!(ls_2.is_convex());
assert!(ls_2.is_cw_convex());
assert!(ls_3.is_convex());
assert!(ls_3.is_cw_convex());
}
#[test]
fn test_single_point() {
let ls: LineString<f64> = wkt! (LINESTRING (0 0)).convert();
assert!(ls.is_strictly_convex());
}
#[test]
fn test_star_polygon_not_convex() {
let ls: LineString<f64> =
wkt! (LINESTRING (0 0, 10 0, 7 -1, 4 5, 6 5, 3 -1, 0 0)).convert();
assert!(!ls.is_convex());
}
}