use geometry_coords::CoordinateScalar;
use geometry_cs::CoordinateSystem;
use geometry_model::{
Box as ModelBox, DynGeometry, DynGeometryCollection, Linestring, MultiLinestring, MultiPoint,
MultiPolygon, Point as ModelPoint, Polygon, Ring, Segment,
};
use geometry_strategy::{AreaStrategy, ShoelaceArea};
use geometry_trait::{
Closure, Linestring as LinestringTrait, Point as PointTrait, Ring as RingTrait,
};
pub fn correct<G: Correct>(g: &mut G) {
g.correct();
}
#[inline]
pub fn correct_closure<G: CorrectClosure>(g: &mut G) {
g.correct_closure();
}
#[doc(hidden)]
pub trait Correct {
fn correct(&mut self);
}
#[doc(hidden)]
pub trait CorrectClosure {
fn correct_closure(&mut self);
}
fn fix_closure<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>)
where
P: PointTrait + Copy,
{
if r.0.len() <= 2 {
return;
}
let first = r.0[0];
let last = *r.0.last().unwrap();
let should_be_closed = matches!(r.closure(), Closure::Closed);
let already_closed = coords_equal(&first, &last);
match (should_be_closed, already_closed) {
(true, false) => r.0.push(first), (false, true) => {
r.0.pop(); }
_ => {}
}
}
fn coords_equal<P: PointTrait>(a: &P, b: &P) -> bool {
geometry_trait::fold_dims(true, a, |acc, _p, d| {
acc && match d {
0 => a.get::<0>() == b.get::<0>(),
1 => a.get::<1>() == b.get::<1>(),
2 => a.get::<2>() == b.get::<2>(),
3 => a.get::<3>() == b.get::<3>(),
_ => unreachable!("fold_dims caps at MAX_DIM"),
}
})
}
fn fix_orientation<P, const CW: bool, const CL: bool>(r: &mut Ring<P, CW, CL>, want_positive: bool)
where
P: PointTrait,
ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
{
let a = ShoelaceArea.area(&*r);
let zero = <P::Scalar as CoordinateScalar>::ZERO;
let is_positive = a > zero;
let is_negative = a < zero;
if (want_positive && is_negative) || (!want_positive && is_positive) {
r.0.reverse();
}
}
impl<P, const CW: bool, const CL: bool> Correct for Ring<P, CW, CL>
where
P: PointTrait + Copy,
P::Cs: CoordinateSystem,
ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
{
fn correct(&mut self) {
fix_closure(self);
fix_orientation(self, true);
}
}
impl<P, const CW: bool, const CL: bool> CorrectClosure for Ring<P, CW, CL>
where
P: PointTrait + Copy,
{
fn correct_closure(&mut self) {
fix_closure(self);
}
}
impl<P, const CW: bool, const CL: bool> Correct for Polygon<P, CW, CL>
where
P: PointTrait + Copy,
P::Cs: CoordinateSystem,
ShoelaceArea: AreaStrategy<Ring<P, CW, CL>, Out = P::Scalar>,
{
fn correct(&mut self) {
fix_closure(&mut self.outer);
fix_orientation(&mut self.outer, true);
for inner in &mut self.inners {
fix_closure(inner);
fix_orientation(inner, false);
}
}
}
impl<P, const CW: bool, const CL: bool> CorrectClosure for Polygon<P, CW, CL>
where
P: PointTrait + Copy,
{
fn correct_closure(&mut self) {
fix_closure(&mut self.outer);
for inner in &mut self.inners {
fix_closure(inner);
}
}
}
impl<Pg: Correct + geometry_trait::Polygon> Correct for MultiPolygon<Pg> {
fn correct(&mut self) {
for p in &mut self.0 {
p.correct();
}
}
}
impl<Pg> CorrectClosure for MultiPolygon<Pg>
where
Pg: CorrectClosure + geometry_trait::Polygon,
{
fn correct_closure(&mut self) {
for polygon in &mut self.0 {
polygon.correct_closure();
}
}
}
impl<T, const D: usize, Cs> CorrectClosure for ModelPoint<T, D, Cs>
where
T: CoordinateScalar,
Cs: CoordinateSystem,
{
fn correct_closure(&mut self) {}
}
impl<P: PointTrait> CorrectClosure for Linestring<P> {
fn correct_closure(&mut self) {}
}
impl<P: PointTrait> CorrectClosure for Segment<P> {
fn correct_closure(&mut self) {}
}
impl<P: PointTrait> CorrectClosure for ModelBox<P> {
fn correct_closure(&mut self) {}
}
impl<P: PointTrait> CorrectClosure for MultiPoint<P> {
fn correct_closure(&mut self) {}
}
impl<L: LinestringTrait> CorrectClosure for MultiLinestring<L> {
fn correct_closure(&mut self) {}
}
impl<T, Cs> CorrectClosure for DynGeometry<T, Cs>
where
T: CoordinateScalar,
Cs: CoordinateSystem + Copy,
{
fn correct_closure(&mut self) {
match self {
DynGeometry::Point(_)
| DynGeometry::LineString(_)
| DynGeometry::MultiPoint(_)
| DynGeometry::MultiLineString(_) => {}
DynGeometry::Polygon(polygon) => polygon.correct_closure(),
DynGeometry::MultiPolygon(multi_polygon) => multi_polygon.correct_closure(),
DynGeometry::GeometryCollection(geometries) => {
for geometry in geometries {
geometry.correct_closure();
}
}
}
}
}
impl<T, Cs> CorrectClosure for DynGeometryCollection<T, Cs>
where
T: CoordinateScalar,
Cs: CoordinateSystem + Copy,
{
fn correct_closure(&mut self) {
for geometry in &mut self.0 {
geometry.correct_closure();
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp, reason = "Areas are exact integer literals.")]
use super::correct;
use crate::area::ring_area;
use geometry_cs::Cartesian;
use geometry_model::{Point2D, Ring};
type P = Point2D<f64, Cartesian>;
#[test]
fn ccw_exterior_of_cw_ring_is_reversed() {
let mut 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!(ring_area(&r) < 0.0, "precondition: CCW ring is negative");
correct(&mut r);
assert_eq!(ring_area(&r), 4.0);
}
#[test]
fn already_correct_ring_is_unchanged() {
let mut r: Ring<P> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(0.0, 2.0),
P::new(2.0, 2.0),
P::new(2.0, 0.0),
P::new(0.0, 0.0),
]);
assert_eq!(ring_area(&r), 4.0);
correct(&mut r);
assert_eq!(ring_area(&r), 4.0);
}
#[test]
fn two_point_ring_is_left_untouched() {
use geometry_trait::Ring as _;
let mut r: Ring<P> = Ring::from_vec(vec![P::new(0.0, 0.0), P::new(1.0, 1.0)]);
correct(&mut r);
assert_eq!(r.points().count(), 2, "2-point ring must stay 2 points");
}
#[test]
fn open_declared_ring_drops_redundant_closing_vertex() {
use geometry_trait::Ring as _;
let mut r: Ring<P, true, false> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(0.0, 2.0),
P::new(2.0, 2.0),
P::new(2.0, 0.0),
P::new(0.0, 0.0),
]);
correct(&mut r);
assert_eq!(r.points().count(), 4);
assert_eq!(ring_area(&r), 4.0);
}
#[test]
fn multipolygon_corrects_each_member() {
use geometry_model::{MultiPolygon, Polygon};
let ccw_square = || {
Polygon::<P>::new(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),
]))
};
let mut mpg: MultiPolygon<Polygon<P>> = MultiPolygon(vec![ccw_square(), ccw_square()]);
assert!(ring_area(&mpg.0[0].outer) < 0.0);
correct(&mut mpg);
assert_eq!(ring_area(&mpg.0[0].outer), 4.0);
assert_eq!(ring_area(&mpg.0[1].outer), 4.0);
}
#[test]
fn coords_equal_compares_the_third_ordinate() {
use geometry_model::Point3D;
use geometry_trait::Ring as _;
type P3 = Point3D<f64, Cartesian>;
let mut r: Ring<P3, true, false> = Ring::from_vec(vec![
P3::new(0.0, 0.0, 5.0),
P3::new(1.0, 0.0, 5.0),
P3::new(1.0, 1.0, 5.0),
P3::new(0.0, 0.0, 5.0),
]);
let before = r.points().count();
super::fix_closure(&mut r);
assert_eq!(before, 4);
assert_eq!(r.points().count(), 3, "closing vertex dropped");
let mut open: Ring<P3, true, false> = Ring::from_vec(vec![
P3::new(0.0, 0.0, 5.0),
P3::new(1.0, 0.0, 5.0),
P3::new(1.0, 1.0, 5.0),
P3::new(0.0, 0.0, 9.0), ]);
super::fix_closure(&mut open);
assert_eq!(open.points().count(), 4, "z differs → not closed → kept");
}
#[test]
fn ccw_ring_correctly_wound_is_a_noop() {
let mut r: Ring<P, false> = Ring::from_vec(vec![
P::new(1.0, 0.0),
P::new(0.0, 1.0),
P::new(-1.0, 0.0),
P::new(0.0, -1.0),
P::new(1.0, 0.0),
]);
assert_eq!(ring_area(&r), 2.0, "precondition: correctly wound");
correct(&mut r);
assert_eq!(ring_area(&r), 2.0, "correct() must be a no-op");
}
#[test]
fn ccw_ring_wrongly_wound_is_reversed() {
let mut r: Ring<P, false> = Ring::from_vec(vec![
P::new(1.0, 0.0),
P::new(0.0, -1.0),
P::new(-1.0, 0.0),
P::new(0.0, 1.0),
P::new(1.0, 0.0),
]);
assert_eq!(ring_area(&r), -2.0, "precondition: wrongly wound");
correct(&mut r);
assert_eq!(ring_area(&r), 2.0);
}
#[test]
fn ccw_polygon_with_hole_correctly_wound_is_a_noop() {
use geometry_model::Polygon;
let outer: Ring<P, false> = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(4.0, 0.0),
P::new(4.0, 4.0),
P::new(0.0, 4.0),
P::new(0.0, 0.0),
]);
let hole: Ring<P, false> = Ring::from_vec(vec![
P::new(1.0, 1.0),
P::new(1.0, 2.0),
P::new(2.0, 2.0),
P::new(2.0, 1.0),
P::new(1.0, 1.0),
]);
let mut pg: Polygon<P, false> = Polygon::new(outer);
pg.inners.push(hole);
assert_eq!(ring_area(&pg.outer), 16.0, "precondition: outer CCW-stored");
assert_eq!(
ring_area(&pg.inners[0]),
-1.0,
"precondition: hole CW-stored"
);
correct(&mut pg);
assert_eq!(ring_area(&pg.outer), 16.0, "outer must be untouched");
assert_eq!(ring_area(&pg.inners[0]), -1.0, "hole must be untouched");
}
#[test]
fn ccw_polygon_wrongly_wound_is_fixed() {
use geometry_model::Polygon;
let outer: Ring<P, false> = 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(0.0, 0.0),
]);
let hole: Ring<P, false> = Ring::from_vec(vec![
P::new(1.0, 1.0),
P::new(2.0, 1.0),
P::new(2.0, 2.0),
P::new(1.0, 2.0),
P::new(1.0, 1.0),
]);
let mut pg: Polygon<P, false> = Polygon::new(outer);
pg.inners.push(hole);
assert_eq!(ring_area(&pg.outer), -16.0, "precondition: outer wrong");
assert_eq!(ring_area(&pg.inners[0]), 1.0, "precondition: hole wrong");
correct(&mut pg);
assert_eq!(ring_area(&pg.outer), 16.0);
assert_eq!(ring_area(&pg.inners[0]), -1.0);
}
}