use alloc::vec::Vec;
use geometry_coords::CoordinateScalar;
use geometry_cs::{CartesianFamily, CoordinateSystem};
use geometry_model::Segment;
use geometry_strategy::{AreaStrategy, ShoelaceArea, WithinRing, WithinStrategy};
use geometry_tag::SameAs;
use geometry_trait::{Point, PointMut, Polygon as PolygonTrait, Ring as RingTrait};
use crate::predicate::range_guard::coordinate_in_range;
use crate::predicate::segment_intersection::{SegmentIntersection, segment_intersection};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidityFailure {
FewPoints,
NotClosed,
SelfIntersection,
InvalidCoordinate,
InteriorRingOutside,
CoordinateOutOfRange,
Spikes,
WrongOrientation,
}
pub fn is_valid_ring<R, P>(ring: &R) -> Result<(), ValidityFailure>
where
R: RingTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
validate_ring(ring, false)
}
fn validate_ring<R, P>(ring: &R, is_interior: bool) -> Result<(), ValidityFailure>
where
R: RingTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
let pts: Vec<P> = ring.points().copied().collect();
for p in &pts {
let x: f64 = p.get::<0>().into();
let y: f64 = p.get::<1>().into();
if !x.is_finite() || !y.is_finite() {
return Err(ValidityFailure::InvalidCoordinate);
}
}
for p in &pts {
if !coordinate_in_range(p) {
return Err(ValidityFailure::CoordinateOutOfRange);
}
}
if pts.len() < 4 {
return Err(ValidityFailure::FewPoints);
}
if !same_point(&pts[0], &pts[pts.len() - 1]) {
return Err(ValidityFailure::NotClosed);
}
if has_spike(&pts) {
return Err(ValidityFailure::Spikes);
}
if has_self_intersection(&pts) {
return Err(ValidityFailure::SelfIntersection);
}
let area = ShoelaceArea.area(ring);
let zero = <P::Scalar as CoordinateScalar>::ZERO;
let properly_oriented = if is_interior {
area < zero
} else {
area > zero
};
if !properly_oriented {
return Err(ValidityFailure::WrongOrientation);
}
Ok(())
}
pub fn is_valid_polygon<G, P>(polygon: &G) -> Result<(), ValidityFailure>
where
G: PolygonTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
validate_ring(polygon.exterior(), false)?;
for inner in polygon.interiors() {
validate_ring(inner, true)?;
if let Some(rep) = inner.points().next() {
if !WithinRing.covered_by(rep, polygon.exterior()) {
return Err(ValidityFailure::InteriorRingOutside);
}
}
}
Ok(())
}
fn has_self_intersection<P>(pts: &[P]) -> bool
where
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
{
let n = pts.len();
let edges = n - 1;
for i in 0..edges {
let a = Segment::new(pts[i], pts[i + 1]);
for j in (i + 1)..edges {
if j == i + 1 {
continue;
}
if i == 0 && j == edges - 1 {
continue;
}
let b = Segment::new(pts[j], pts[j + 1]);
match segment_intersection::<Segment<P>, P>(&a, &b) {
SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange => {}
_ => return true,
}
}
}
false
}
fn same_point<P: Point>(a: &P, b: &P) -> bool
where
P::Scalar: PartialEq,
{
a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
}
fn is_spike_triple<P: Point>(a: &P, b: &P, c: &P) -> bool
where
P::Scalar: CoordinateScalar,
{
let ux = b.get::<0>() - a.get::<0>();
let uy = b.get::<1>() - a.get::<1>();
let vx = c.get::<0>() - b.get::<0>();
let vy = c.get::<1>() - b.get::<1>();
let zero = <P::Scalar as CoordinateScalar>::ZERO;
ux * vy - uy * vx == zero && ux * vx + uy * vy < zero
}
fn has_spike<P: Point + Copy>(pts: &[P]) -> bool
where
P::Scalar: CoordinateScalar,
{
let cycle = &pts[..pts.len() - 1];
let n = cycle.len(); (0..n).any(|i| is_spike_triple(&cycle[(i + n - 1) % n], &cycle[i], &cycle[(i + 1) % n]))
}
#[cfg(test)]
mod tests {
use super::{ValidityFailure, is_valid_polygon, is_valid_ring};
use geometry_cs::Cartesian;
use geometry_model::{Point2D, Polygon, Ring, polygon};
type P = Point2D<f64, Cartesian>;
#[test]
fn valid_square_ring() {
let r: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(0.0, 1.0),
P::new(1.0, 1.0),
P::new(1.0, 0.0),
P::new(0.0, 0.0),
]);
assert!(is_valid_ring(&r).is_ok());
}
#[test]
fn too_few_points() {
let r: Ring<P> = Ring::from_vec(vec![P::new(0.0, 0.0), P::new(1.0, 0.0), P::new(0.0, 0.0)]);
assert_eq!(is_valid_ring(&r), Err(ValidityFailure::FewPoints));
}
#[test]
fn out_of_range_self_intersection_is_not_reported_valid() {
let s = 2.0e14;
let huge_bowtie: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(s, s),
P::new(s, 0.0),
P::new(0.0, s),
P::new(0.0, 0.0),
]);
assert_eq!(
is_valid_ring(&huge_bowtie),
Err(ValidityFailure::CoordinateOutOfRange)
);
let small_bowtie: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(2.0, 2.0),
P::new(2.0, 0.0),
P::new(0.0, 2.0),
P::new(0.0, 0.0),
]);
assert_eq!(
is_valid_ring(&small_bowtie),
Err(ValidityFailure::SelfIntersection)
);
}
#[test]
fn not_closed() {
let r: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(1.0, 0.0),
P::new(1.0, 1.0),
P::new(0.0, 1.0),
]);
assert_eq!(is_valid_ring(&r), Err(ValidityFailure::NotClosed));
}
#[test]
fn self_intersecting_bowtie() {
let r: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(2.0, 2.0),
P::new(2.0, 0.0),
P::new(0.0, 2.0),
P::new(0.0, 0.0),
]);
assert_eq!(is_valid_ring(&r), Err(ValidityFailure::SelfIntersection));
}
#[test]
fn invalid_coordinate() {
let r: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(f64::NAN, 0.0),
P::new(1.0, 1.0),
P::new(0.0, 0.0),
]);
assert_eq!(is_valid_ring(&r), Err(ValidityFailure::InvalidCoordinate));
}
#[test]
fn valid_polygon() {
let pg: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)]];
assert!(is_valid_polygon(&pg).is_ok());
}
#[test]
fn valid_polygon_with_hole() {
let pg: Polygon<P> = polygon![
[
(0.0, 0.0),
(0.0, 10.0),
(10.0, 10.0),
(10.0, 0.0),
(0.0, 0.0)
],
[(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 4.0), (2.0, 2.0)]
];
assert!(is_valid_polygon(&pg).is_ok());
}
#[test]
fn wrongly_oriented_ring_is_rejected() {
let r: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(2.0, 0.0),
P::new(2.0, 2.0),
P::new(0.0, 2.0),
P::new(0.0, 0.0),
]);
assert_eq!(is_valid_ring(&r), Err(ValidityFailure::WrongOrientation));
}
#[test]
fn ccw_declared_ring_correctly_wound_is_ok() {
let r: Ring<P, false> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(2.0, 0.0),
P::new(2.0, 2.0),
P::new(0.0, 2.0),
P::new(0.0, 0.0),
]);
assert!(is_valid_ring(&r).is_ok());
}
#[test]
fn all_collinear_ring_is_spikes() {
let flat: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(4.0, 0.0),
P::new(2.0, 0.0),
P::new(0.0, 0.0),
]);
assert_eq!(is_valid_ring(&flat), Err(ValidityFailure::Spikes));
}
#[test]
fn square_with_spike_is_spikes() {
let r: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(0.0, 4.0),
P::new(4.0, 4.0),
P::new(4.0, 0.0),
P::new(2.0, 0.0),
P::new(2.0, -2.0),
P::new(2.0, 0.0),
P::new(0.0, 0.0),
]);
assert_eq!(is_valid_ring(&r), Err(ValidityFailure::Spikes));
}
#[test]
fn hole_outside_exterior_is_rejected() {
let pg: Polygon<P> = polygon![
[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
[
(10.0, 10.0),
(12.0, 10.0),
(12.0, 12.0),
(10.0, 12.0),
(10.0, 10.0)
]
];
assert_eq!(
is_valid_polygon(&pg),
Err(ValidityFailure::InteriorRingOutside)
);
}
#[test]
fn hole_touching_exterior_boundary_is_ok() {
let pg: Polygon<P> = polygon![
[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
[(0.0, 2.0), (1.0, 1.0), (1.0, 3.0), (0.0, 2.0)]
];
assert!(is_valid_polygon(&pg).is_ok());
}
#[test]
fn wrongly_oriented_hole_is_rejected() {
let pg: Polygon<P> = polygon![
[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0), (0.0, 0.0)],
[(1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0)]
];
assert_eq!(
is_valid_polygon(&pg),
Err(ValidityFailure::WrongOrientation)
);
}
}