use alloc::vec::Vec;
use geometry_coords::CoordinateScalar;
use geometry_cs::{CartesianFamily, CoordinateSystem};
use geometry_model::{Linestring, MultiPoint, Polygon, Ring};
use geometry_tag::SameAs;
use geometry_trait::{Point, PointMut, Polygon as PolygonTrait, Ring as RingTrait};
pub trait ConvexHullStrategy<G> {
type OutputPoint: Point;
type Output;
fn convex_hull(&self, g: &G) -> Self::Output;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct MonotoneChain;
#[doc(hidden)]
pub trait CollectPoints {
type Point: Point + Copy;
fn collect_points(&self, out: &mut Vec<Self::Point>);
}
impl<P: Point + Copy> CollectPoints for MultiPoint<P> {
type Point = P;
fn collect_points(&self, out: &mut Vec<P>) {
out.extend(self.0.iter().copied());
}
}
impl<P: Point + Copy> CollectPoints for Linestring<P> {
type Point = P;
fn collect_points(&self, out: &mut Vec<P>) {
out.extend(self.0.iter().copied());
}
}
impl<P: Point + Copy, const CW: bool, const CL: bool> CollectPoints for Ring<P, CW, CL> {
type Point = P;
fn collect_points(&self, out: &mut Vec<P>) {
out.extend(self.0.iter().copied());
}
}
impl<P: Point + Copy, const CW: bool, const CL: bool> CollectPoints for Polygon<P, CW, CL> {
type Point = P;
fn collect_points(&self, out: &mut Vec<P>) {
out.extend(self.exterior().points().copied());
}
}
fn cross_2d<P: Point>(a: &P, b: &P, c: &P) -> P::Scalar {
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>();
ux * vy - uy * vx
}
fn monotone_chain<P>(mut pts: Vec<P>) -> Vec<P>
where
P: Point + Copy,
{
pts.sort_by(|a, b| {
a.get::<0>()
.partial_cmp(&b.get::<0>())
.unwrap_or(core::cmp::Ordering::Equal)
.then_with(|| {
a.get::<1>()
.partial_cmp(&b.get::<1>())
.unwrap_or(core::cmp::Ordering::Equal)
})
});
let mut h: Vec<P> = Vec::with_capacity(pts.len() * 2);
for &p in &pts {
while h.len() >= 2 && cross_2d(&h[h.len() - 2], &h[h.len() - 1], &p) <= P::Scalar::ZERO {
h.pop();
}
h.push(p);
}
let lower_len = h.len() + 1;
for &p in pts.iter().rev().skip(1) {
while h.len() >= lower_len
&& cross_2d(&h[h.len() - 2], &h[h.len() - 1], &p) <= P::Scalar::ZERO
{
h.pop();
}
h.push(p);
}
h.pop();
h
}
impl<G, P> ConvexHullStrategy<G> for MonotoneChain
where
G: CollectPoints<Point = P>,
P: Point + PointMut + Default + Copy,
<P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
{
type OutputPoint = P;
type Output = Ring<P, true, true>;
fn convex_hull(&self, g: &G) -> Self::Output {
let mut pts = Vec::new();
g.collect_points(&mut pts);
if pts.len() < 3 {
return Ring::from_vec(pts);
}
let mut hull = monotone_chain(pts);
hull.reverse();
let first = hull[0];
hull.push(first);
Ring::from_vec(hull)
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
reason = "Hull corner coordinates are exact literals."
)]
mod tests {
use super::{ConvexHullStrategy, MonotoneChain};
use geometry_cs::Cartesian;
use geometry_model::{MultiPoint, Point2D};
use geometry_trait::{Point as _, Ring as _};
type Pt = Point2D<f64, Cartesian>;
fn multi_point(points: &[(f64, f64)]) -> MultiPoint<Pt> {
let mut mp = MultiPoint::new();
for &(x, y) in points {
mp.push(Pt::new(x, y));
}
mp
}
#[test]
fn square_plus_interior_point_keeps_four_corners() {
let mp = multi_point(&[(0., 0.), (4., 0.), (4., 4.), (0., 4.), (2., 2.)]);
let hull = MonotoneChain.convex_hull(&mp);
assert_eq!(hull.points().count(), 5);
assert_eq!(
hull.0.first().unwrap().get::<0>(),
hull.0.last().unwrap().get::<0>()
);
assert_eq!(
hull.0.first().unwrap().get::<1>(),
hull.0.last().unwrap().get::<1>()
);
let has_interior = hull
.0
.iter()
.any(|p| p.get::<0>() == 2.0 && p.get::<1>() == 2.0);
assert!(!has_interior);
}
#[test]
fn hull_of_two_points_is_the_input() {
let mp = multi_point(&[(0., 0.), (1., 1.)]);
let hull = MonotoneChain.convex_hull(&mp);
assert_eq!(hull.points().count(), 2);
}
}