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::{MultiPolygonTag, PolygonTag, RingTag, SameAs};
use geometry_trait::{
Geometry, MultiPolygon as MultiPolygonTrait, 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,
DuplicatePoints,
NotClosed,
SelfIntersection,
InvalidCoordinate,
InteriorRingOutside,
CoordinateOutOfRange,
Spikes,
WrongOrientation,
NestedInteriorRings,
DisconnectedInterior,
IntersectingInteriors,
WrongTopologicalDimension,
WrongCornerOrder,
CollinearPointsOnFace,
NonCoplanarPointsOnFace,
FewPointsOnFace,
InconsistentOrientation,
InvalidIntersection,
DisconnectedSurface,
}
impl ValidityFailure {
#[must_use]
pub const fn message(self) -> &'static str {
match self {
Self::FewPoints => "Geometry has too few points",
Self::WrongTopologicalDimension => "Geometry has wrong topological dimension",
Self::Spikes => "Geometry has spikes",
Self::DuplicatePoints => "Geometry has duplicate (consecutive) points",
Self::NotClosed => "Geometry is defined as closed but is open",
Self::SelfIntersection => "Geometry has invalid self-intersections",
Self::WrongOrientation => "Geometry has wrong orientation",
Self::InteriorRingOutside => {
"Geometry has interior rings defined outside the outer boundary"
}
Self::NestedInteriorRings => "Geometry has nested interior rings",
Self::DisconnectedInterior => "Geometry has disconnected interior",
Self::IntersectingInteriors => "Multi-polygon has intersecting interiors",
Self::WrongCornerOrder => "Box has corners in wrong order",
Self::InvalidCoordinate => "Geometry has point(s) with invalid coordinate(s)",
Self::CoordinateOutOfRange => {
"Geometry has coordinate(s) outside the supported arithmetic range"
}
Self::CollinearPointsOnFace => "Geometry has collinear points on a face",
Self::NonCoplanarPointsOnFace => "Geometry has non-coplanar points on a face",
Self::FewPointsOnFace => "Geometry has too few points on a face",
Self::InconsistentOrientation => "Geometry has inconsistent surface orientation",
Self::InvalidIntersection => "Geometry has invalid face intersections",
Self::DisconnectedSurface => "Geometry has a disconnected surface",
}
}
}
impl core::fmt::Display for ValidityFailure {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(self.message())
}
}
#[cfg(feature = "std")]
impl std::error::Error for ValidityFailure {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidityOptions {
allow_duplicates: bool,
allow_spikes_for_linear: bool,
}
impl ValidityOptions {
pub const STRICT: Self = Self::new(false, false);
pub const BOOST_DEFAULT: Self = Self::new(true, true);
#[must_use]
pub const fn new(allow_duplicates: bool, allow_spikes_for_linear: bool) -> Self {
Self {
allow_duplicates,
allow_spikes_for_linear,
}
}
#[must_use]
pub const fn allows_duplicates(self) -> bool {
self.allow_duplicates
}
#[must_use]
pub const fn allows_spikes_for_linear(self) -> bool {
self.allow_spikes_for_linear
}
}
impl Default for ValidityOptions {
fn default() -> Self {
Self::STRICT
}
}
#[doc(hidden)]
pub trait ValidityStrategy<G> {
fn apply(&self, geometry: &G, options: ValidityOptions) -> Result<(), ValidityFailure>;
}
#[doc(hidden)]
pub trait ValidityStrategyForKind {
type S: Default;
}
#[doc(hidden)]
#[derive(Debug, Default, Clone, Copy)]
pub struct RingValidity;
#[doc(hidden)]
#[derive(Debug, Default, Clone, Copy)]
pub struct PolygonValidity;
#[doc(hidden)]
#[derive(Debug, Default, Clone, Copy)]
pub struct MultiPolygonValidity;
impl ValidityStrategyForKind for RingTag {
type S = RingValidity;
}
impl ValidityStrategyForKind for PolygonTag {
type S = PolygonValidity;
}
impl ValidityStrategyForKind for MultiPolygonTag {
type S = MultiPolygonValidity;
}
#[inline]
#[must_use = "validity failures must be handled"]
pub fn is_valid<G>(geometry: &G) -> Result<(), ValidityFailure>
where
G: Geometry,
G::Kind: ValidityStrategyForKind,
<G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
{
is_valid_with(geometry, ValidityOptions::STRICT)
}
#[inline]
#[must_use = "validity failures must be handled"]
pub fn is_valid_with<G>(geometry: &G, options: ValidityOptions) -> Result<(), ValidityFailure>
where
G: Geometry,
G::Kind: ValidityStrategyForKind,
<G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
{
<<G::Kind as ValidityStrategyForKind>::S as Default>::default().apply(geometry, options)
}
#[inline]
#[must_use]
pub fn validity_reason<G>(geometry: &G) -> &'static str
where
G: Geometry,
G::Kind: ValidityStrategyForKind,
<G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
{
validity_reason_with(geometry, ValidityOptions::STRICT)
}
#[inline]
#[must_use]
pub fn validity_reason_with<G>(geometry: &G, options: ValidityOptions) -> &'static str
where
G: Geometry,
G::Kind: ValidityStrategyForKind,
<G::Kind as ValidityStrategyForKind>::S: ValidityStrategy<G>,
{
match is_valid_with(geometry, options) {
Ok(()) => "Geometry is valid",
Err(failure) => failure.message(),
}
}
impl<G, P> ValidityStrategy<G> for RingValidity
where
G: RingTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
fn apply(&self, ring: &G, options: ValidityOptions) -> Result<(), ValidityFailure> {
is_valid_ring_with(ring, options)
}
}
impl<G, P> ValidityStrategy<G> for PolygonValidity
where
G: PolygonTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
fn apply(&self, polygon: &G, options: ValidityOptions) -> Result<(), ValidityFailure> {
is_valid_polygon_with(polygon, options)
}
}
impl<G, P> ValidityStrategy<G> for MultiPolygonValidity
where
G: MultiPolygonTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
fn apply(&self, multi_polygon: &G, options: ValidityOptions) -> Result<(), ValidityFailure> {
let polygons: Vec<_> = multi_polygon.polygons().collect();
for polygon in &polygons {
is_valid_polygon_with(*polygon, options)?;
}
for first in 0..polygons.len() {
for second in (first + 1)..polygons.len() {
let matrix = crate::relate::relate(polygons[first], polygons[second])
.map_err(|_| ValidityFailure::SelfIntersection)?;
if matrix.interior_interior() == crate::relate::Dimension::Area
|| matrix.boundary_boundary() == crate::relate::Dimension::Curve
{
return Err(ValidityFailure::IntersectingInteriors);
}
}
}
Ok(())
}
}
#[inline]
#[must_use = "validity failures must be handled"]
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>,
{
is_valid_ring_with(ring, ValidityOptions::STRICT)
}
#[inline]
#[must_use = "validity failures must be handled"]
pub fn is_valid_ring_with<R, P>(ring: &R, options: ValidityOptions) -> 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, options)
}
fn validate_ring<R, P>(
ring: &R,
is_interior: bool,
options: ValidityOptions,
) -> Result<(), ValidityFailure>
where
R: RingTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
let mut 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);
}
}
if options.allows_duplicates() {
let mut deduplicated = Vec::with_capacity(pts.len());
for point in pts {
if !deduplicated
.last()
.is_some_and(|previous| same_point(previous, &point))
{
deduplicated.push(point);
}
}
pts = deduplicated;
}
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 !options.allows_duplicates() && pts.windows(2).any(|pair| same_point(&pair[0], &pair[1])) {
return Err(ValidityFailure::DuplicatePoints);
}
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(())
}
#[inline]
#[must_use = "validity failures must be handled"]
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>,
{
is_valid_polygon_with(polygon, ValidityOptions::STRICT)
}
#[inline]
#[must_use = "validity failures must be handled"]
pub fn is_valid_polygon_with<G, P>(
polygon: &G,
options: ValidityOptions,
) -> 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, options)?;
let inners: Vec<_> = polygon.interiors().collect();
for inner in &inners {
validate_ring(*inner, true, options)?;
let rep = inner
.points()
.next()
.expect("a validated ring contains at least four points");
if !WithinRing.covered_by(rep, polygon.exterior()) {
return Err(ValidityFailure::InteriorRingOutside);
}
let interaction = ring_pair_interaction(polygon.exterior(), *inner);
if interaction.proper_crossing {
return Err(ValidityFailure::SelfIntersection);
}
if interaction.overlap || interaction.contacts.len() > 1 {
return Err(ValidityFailure::DisconnectedInterior);
}
}
for first in 0..inners.len() {
for second in (first + 1)..inners.len() {
let interaction = ring_pair_interaction(inners[first], inners[second]);
if interaction.proper_crossing || interaction.overlap {
return Err(ValidityFailure::SelfIntersection);
}
if interaction.contacts.len() > 1 {
return Err(ValidityFailure::DisconnectedInterior);
}
let nested = interaction.contacts.is_empty()
&& (ring_first_point_within(inners[first], inners[second])
|| ring_first_point_within(inners[second], inners[first]));
(!nested)
.then_some(())
.ok_or(ValidityFailure::NestedInteriorRings)?;
}
}
Ok(())
}
#[derive(Default)]
struct RingPairInteraction<P> {
proper_crossing: bool,
overlap: bool,
contacts: Vec<P>,
}
fn ring_pair_interaction<R1, R2, P>(first: &R1, second: &R2) -> RingPairInteraction<P>
where
R1: RingTrait<Point = P>,
R2: RingTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
{
let first_points: Vec<P> = first.points().copied().collect();
let second_points: Vec<P> = second.points().copied().collect();
let mut interaction = RingPairInteraction::default();
for first_pair in first_points.windows(2) {
let first_segment = Segment::new(first_pair[0], first_pair[1]);
for second_pair in second_points.windows(2) {
let second_segment = Segment::new(second_pair[0], second_pair[1]);
match segment_intersection(&first_segment, &second_segment) {
SegmentIntersection::Disjoint | SegmentIntersection::OutOfRange => {}
SegmentIntersection::Collinear { .. } => interaction.overlap = true,
SegmentIntersection::Single(point) => {
let endpoint_contact = first_pair.iter().any(|value| same_point(value, &point))
|| second_pair.iter().any(|value| same_point(value, &point));
if !endpoint_contact {
interaction.proper_crossing = true;
}
if !interaction
.contacts
.iter()
.any(|value| same_point(value, &point))
{
interaction.contacts.push(point);
}
}
}
}
}
interaction
}
fn ring_first_point_within<R1, R2, P>(inner: &R1, outer: &R2) -> bool
where
R1: RingTrait<Point = P>,
R2: RingTrait<Point = P>,
P: Point,
P::Scalar: CoordinateScalar,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
inner
.points()
.next()
.is_some_and(|point| WithinRing.within(point, outer))
}
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)
);
}
}