use geometry_coords::CoordinateScalar;
use geometry_model::{Linestring, MultiPolygon, Polygon, Ring};
use geometry_trait::Point as PointTrait;
pub fn remove_spikes<G: RemoveSpikes>(g: &mut G) {
g.remove_spikes();
}
#[doc(hidden)]
pub trait RemoveSpikes {
fn remove_spikes(&mut self);
}
fn is_spike_2d<P: PointTrait>(a: &P, b: &P, c: &P) -> bool {
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 cross = ux * vy - uy * vx;
let dot = ux * vx + uy * vy;
let zero = <P::Scalar as CoordinateScalar>::ZERO;
cross == zero && dot < zero
}
fn walk_spikes<P: PointTrait>(pts: &mut alloc::vec::Vec<P>) {
let mut changed = true;
while changed && pts.len() >= 3 {
changed = false;
let mut i = 1;
while i + 1 < pts.len() {
if is_spike_2d(&pts[i - 1], &pts[i], &pts[i + 1]) {
pts.remove(i);
changed = true;
if i > 1 {
i -= 1;
}
} else {
i += 1;
}
}
}
}
impl<P: PointTrait> RemoveSpikes for Linestring<P> {
fn remove_spikes(&mut self) {
walk_spikes(&mut self.0);
}
}
fn walk_ring_spikes<P: PointTrait + Copy>(pts: &mut alloc::vec::Vec<P>, closed: bool) {
walk_spikes(pts);
let had_closing = closed && pts.len() >= 2 && same_point(&pts[0], &pts[pts.len() - 1]);
if had_closing {
pts.pop();
}
let mut found = true;
while found {
found = false;
while pts.len() >= 3 && is_spike_2d(&pts[pts.len() - 2], &pts[pts.len() - 1], &pts[0]) {
pts.pop();
found = true;
}
while pts.len() >= 3 && is_spike_2d(&pts[pts.len() - 1], &pts[0], &pts[1]) {
pts.remove(0);
found = true;
}
}
if had_closing && !pts.is_empty() {
let first = pts[0];
pts.push(first);
}
}
fn same_point<P: PointTrait>(a: &P, b: &P) -> bool {
a.get::<0>() == b.get::<0>() && a.get::<1>() == b.get::<1>()
}
impl<P: PointTrait + Copy, const CW: bool, const CL: bool> RemoveSpikes for Ring<P, CW, CL> {
fn remove_spikes(&mut self) {
walk_ring_spikes(&mut self.0, CL);
}
}
impl<P: PointTrait + Copy, const CW: bool, const CL: bool> RemoveSpikes for Polygon<P, CW, CL> {
fn remove_spikes(&mut self) {
walk_ring_spikes(&mut self.outer.0, CL);
for inner in &mut self.inners {
walk_ring_spikes(&mut inner.0, CL);
}
}
}
impl<Pg: RemoveSpikes + geometry_trait::Polygon> RemoveSpikes for MultiPolygon<Pg> {
fn remove_spikes(&mut self) {
for p in &mut self.0 {
p.remove_spikes();
}
}
}
#[cfg(test)]
mod tests {
use super::remove_spikes;
use geometry_cs::Cartesian;
use geometry_model::{Point2D, linestring};
use geometry_trait::Linestring as _;
type P = Point2D<f64, Cartesian>;
#[test]
fn out_and_back_spur_is_removed() {
let mut ls: geometry_model::Linestring<P> =
linestring![(0.0, 0.0), (1.0, 0.0), (3.0, 0.0), (2.0, 0.0)];
remove_spikes(&mut ls);
let xs: Vec<f64> = ls.points().map(geometry_trait::Point::get::<0>).collect();
assert_eq!(xs, vec![0.0, 1.0, 2.0]);
}
#[test]
fn spike_free_linestring_is_unchanged() {
let mut ls: geometry_model::Linestring<P> = linestring![(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)];
remove_spikes(&mut ls);
assert_eq!(ls.points().count(), 3);
}
#[test]
fn cascading_spikes_all_collapse() {
let mut ls: geometry_model::Linestring<P> =
linestring![(0.0, 0.0), (2.0, 0.0), (5.0, 0.0), (3.0, 0.0), (1.0, 0.0)];
remove_spikes(&mut ls);
let xs: Vec<f64> = ls.points().map(geometry_trait::Point::get::<0>).collect();
assert_eq!(xs, vec![0.0, 1.0]);
}
#[test]
fn polygon_removes_spikes_in_outer_and_holes() {
use geometry_model::{Polygon, Ring};
use geometry_trait::{Point as _, Polygon as _, Ring as _};
let outer = Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(4.0, 0.0),
P::new(5.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::from_vec(vec![
P::new(1.0, 1.0),
P::new(2.0, 1.0),
P::new(3.0, 1.0), P::new(2.0, 1.0),
P::new(2.0, 2.0),
P::new(1.0, 1.0),
]);
let mut pg: Polygon<P> = Polygon::with_inners(outer, vec![hole]);
remove_spikes(&mut pg);
let ext: Vec<(f64, f64)> = pg
.exterior()
.points()
.map(|p| (p.get::<0>(), p.get::<1>()))
.collect();
assert!(!ext.contains(&(5.0, 0.0)), "outer spike survived: {ext:?}");
let hole_pts: Vec<(f64, f64)> = pg
.interiors()
.next()
.unwrap()
.points()
.map(|p| (p.get::<0>(), p.get::<1>()))
.collect();
assert!(!hole_pts.contains(&(3.0, 1.0)), "hole spike survived");
}
#[test]
fn multipolygon_removes_spikes_from_each_member() {
use geometry_model::{MultiPolygon, Polygon, Ring};
use geometry_trait::{Point as _, Polygon as _, Ring as _};
let spiky = || {
Polygon::<P>::new(Ring::from_vec(vec![
P::new(0.0, 0.0),
P::new(4.0, 0.0),
P::new(5.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 mut mpg: MultiPolygon<Polygon<P>> = MultiPolygon(vec![spiky(), spiky()]);
remove_spikes(&mut mpg);
for pg in &mpg.0 {
let pts: Vec<(f64, f64)> = pg
.exterior()
.points()
.map(|p| (p.get::<0>(), p.get::<1>()))
.collect();
assert!(!pts.contains(&(5.0, 0.0)), "member spike survived");
}
}
#[test]
fn ring_seam_spike_is_removed() {
use geometry_model::Ring;
use geometry_trait::{Point as _, Ring as _};
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(1.0, 0.0),
P::new(0.0, 0.0),
]);
remove_spikes(&mut r);
let pts: Vec<(f64, f64)> = r.points().map(|p| (p.get::<0>(), p.get::<1>())).collect();
assert!(
!pts.contains(&(0.0, 0.0)),
"seam spike vertex (0,0) must be gone: {pts:?}"
);
assert!(r.points().count() >= 4);
assert_eq!(pts.first(), pts.last(), "ring must remain closed");
}
}