use crate::{Coordinate, CoordinateF64, Tolerance};
fn shoelace_doubled(points: &[Coordinate]) -> i128 {
let mut acc: i128 = 0;
for i in 0..points.len() {
let a = points[i];
let b = points[(i + 1) % points.len()];
acc += (a.x as i128) * (b.y as i128) - (b.x as i128) * (a.y as i128);
}
acc
}
#[must_use]
pub fn polygon_area(points: &[Coordinate]) -> f64 {
if points.len() < 3 {
return 0.0;
}
(shoelace_doubled(points).unsigned_abs() as f64) / 2.0
}
#[must_use]
pub fn polygon_perimeter(points: &[Coordinate]) -> f64 {
if points.len() < 2 {
return 0.0;
}
(0..points.len())
.map(|i| {
let a = points[i];
let b = points[(i + 1) % points.len()];
(a.x as f64 - b.x as f64).hypot(a.y as f64 - b.y as f64)
})
.sum()
}
#[must_use]
pub fn polygon_centroid(points: &[Coordinate]) -> Option<CoordinateF64> {
if points.len() < 3 {
return None;
}
let doubled = shoelace_doubled(points);
if doubled == 0 {
return None;
}
let (mut cx, mut cy): (i128, i128) = (0, 0);
for i in 0..points.len() {
let a = points[i];
let b = points[(i + 1) % points.len()];
let cross = (a.x as i128) * (b.y as i128) - (b.x as i128) * (a.y as i128);
cx += (a.x as i128 + b.x as i128) * cross;
cy += (a.y as i128 + b.y as i128) * cross;
}
let denom = 3.0 * doubled as f64;
Some(CoordinateF64::new(cx as f64 / denom, cy as f64 / denom))
}
fn cross(o: Coordinate, a: Coordinate, b: Coordinate) -> i128 {
let (ox, oy) = (o.x as i128, o.y as i128);
(a.x as i128 - ox) * (b.y as i128 - oy) - (a.y as i128 - oy) * (b.x as i128 - ox)
}
#[must_use]
pub fn convex_hull(points: &[Coordinate]) -> Vec<Coordinate> {
let mut sorted: Vec<Coordinate> = points.to_vec();
sorted.sort_by_key(|p| (p.x, p.y));
sorted.dedup();
if sorted.len() < 3 {
return sorted;
}
let mut hull: Vec<Coordinate> = Vec::with_capacity(sorted.len() + 1);
for pass in 0..2 {
let base = hull.len();
let iter: Box<dyn Iterator<Item = &Coordinate>> = if pass == 0 {
Box::new(sorted.iter())
} else {
Box::new(sorted.iter().rev())
};
for &p in iter {
while hull.len() >= base + 2
&& cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0
{
hull.pop();
}
hull.push(p);
}
hull.pop(); }
hull
}
#[must_use]
pub fn approximate_polygon(points: &[Coordinate], tolerance: Tolerance) -> Vec<Coordinate> {
if points.len() < 3 {
return points.to_vec();
}
let first = points[0];
let dist2 = |p: Coordinate| {
(p.x as f64 - first.x as f64).powi(2) + (p.y as f64 - first.y as f64).powi(2)
};
let farthest = points
.iter()
.enumerate()
.max_by(|(_, p), (_, q)| dist2(**p).total_cmp(&dist2(**q)))
.map(|(i, _)| i)
.expect("non-empty by the length check above");
let mut out = Vec::new();
simplify_open(&points[..=farthest], tolerance.get(), &mut out);
let mut second: Vec<Coordinate> = points[farthest..].to_vec();
second.push(points[0]);
simplify_open(&second, tolerance.get(), &mut out);
out
}
fn simplify_open(points: &[Coordinate], epsilon: f64, out: &mut Vec<Coordinate>) {
if points.len() < 3 {
if let Some((_, rest)) = points.split_last() {
out.extend_from_slice(rest);
}
return;
}
let (a, b) = (points[0], points[points.len() - 1]);
let (ax, ay) = (a.x as f64, a.y as f64);
let (bx, by) = (b.x as f64, b.y as f64);
let chord = (bx - ax).hypot(by - ay);
let mut far = (0usize, -1.0f64);
for (i, p) in points.iter().enumerate().take(points.len() - 1).skip(1) {
let (px, py) = (p.x as f64, p.y as f64);
let d = if chord == 0.0 {
(px - ax).hypot(py - ay)
} else {
((bx - ax) * (ay - py) - (ax - px) * (by - ay)).abs() / chord
};
if d > far.1 {
far = (i, d);
}
}
if far.1 > epsilon {
simplify_open(&points[..=far.0], epsilon, out);
simplify_open(&points[far.0..], epsilon, out);
} else {
out.push(a);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tolerance;
fn c(x: usize, y: usize) -> Coordinate {
Coordinate::new(x, y)
}
#[test]
fn area_of_triangle() {
let tri = [c(0, 0), c(4, 0), c(0, 4)];
assert_eq!(polygon_area(&tri), 8.0);
}
#[test]
fn area_degenerate_is_zero() {
assert_eq!(polygon_area(&[]), 0.0);
assert_eq!(polygon_area(&[c(1, 1)]), 0.0);
assert_eq!(polygon_area(&[c(1, 1), c(5, 5)]), 0.0);
assert_eq!(polygon_area(&[c(0, 0), c(2, 0), c(4, 0)]), 0.0);
}
#[test]
fn perimeter_counts_the_closing_edge() {
let tri = [c(0, 0), c(3, 0), c(3, 4)];
assert_eq!(polygon_perimeter(&tri), 12.0);
assert_eq!(polygon_perimeter(&[c(0, 0), c(3, 4)]), 10.0);
assert_eq!(polygon_perimeter(&[c(7, 7)]), 0.0);
}
#[test]
fn centroid_of_square_and_of_degenerate() {
let sq = [c(1, 1), c(5, 1), c(5, 5), c(1, 5)];
let ctr = polygon_centroid(&sq).unwrap();
assert_eq!((ctr.x, ctr.y), (3.0, 3.0));
assert!(polygon_centroid(&[c(0, 0), c(2, 0), c(4, 0)]).is_none());
assert!(polygon_centroid(&[c(0, 0)]).is_none());
}
#[test]
fn centroid_is_area_weighted_not_vertex_mean() {
let l = [c(0, 0), c(4, 0), c(4, 2), c(2, 2), c(2, 6), c(0, 6)];
let ctr = polygon_centroid(&l).unwrap();
assert!((ctr.x - 1.5).abs() < 1e-12, "cx = {}", ctr.x);
assert!((ctr.y - 2.5).abs() < 1e-12, "cy = {}", ctr.y);
}
#[test]
fn hull_orientation_and_strict_corners() {
let pts = [c(0, 0), c(2, 0), c(4, 0), c(4, 4), c(0, 4), c(2, 2)];
let hull = convex_hull(&pts);
assert_eq!(hull, vec![c(0, 0), c(4, 0), c(4, 4), c(0, 4)]);
assert!(super::shoelace_doubled(&hull) > 0);
}
#[test]
fn hull_degenerate_inputs() {
assert!(convex_hull(&[]).is_empty());
assert_eq!(convex_hull(&[c(3, 3)]), vec![c(3, 3)]);
assert_eq!(convex_hull(&[c(3, 3), c(3, 3)]), vec![c(3, 3)]);
assert_eq!(
convex_hull(&[c(0, 0), c(1, 1), c(2, 2), c(3, 3)]),
vec![c(0, 0), c(3, 3)]
);
}
#[test]
fn hull_is_invariant_to_input_order() {
let mut pts = vec![c(0, 0), c(4, 0), c(4, 4), c(0, 4), c(2, 2), c(3, 1)];
let expected = convex_hull(&pts);
pts.reverse();
assert_eq!(convex_hull(&pts), expected);
pts.swap(0, 3);
assert_eq!(convex_hull(&pts), expected);
}
#[test]
fn approximate_keeps_corners_drops_collinear() {
let border: Vec<Coordinate> = [
(0, 0),
(1, 0),
(2, 0),
(3, 0),
(3, 1),
(3, 2),
(3, 3),
(2, 3),
(1, 3),
(0, 3),
(0, 2),
(0, 1),
]
.map(Coordinate::from)
.to_vec();
let simplified = approximate_polygon(&border, tolerance!(0.0));
assert_eq!(simplified.len(), 4);
for corner in [c(0, 0), c(3, 0), c(3, 3), c(0, 3)] {
assert!(simplified.contains(&corner), "missing {corner:?}");
}
assert_eq!(polygon_area(&simplified), polygon_area(&border));
assert_eq!(polygon_perimeter(&simplified), polygon_perimeter(&border));
}
#[test]
fn approximate_with_tolerance_removes_small_bumps() {
let outline: Vec<Coordinate> = [
(0, 0),
(3, 0),
(4, 1), (5, 0),
(9, 0),
(9, 5),
(0, 5),
]
.map(Coordinate::from)
.to_vec();
let coarse = approximate_polygon(&outline, tolerance!(1.5));
assert!(
!coarse.contains(&c(4, 1)),
"bump survived ε=1.5: {coarse:?}"
);
let fine = approximate_polygon(&outline, tolerance!(0.5));
assert!(fine.contains(&c(4, 1)), "bump lost at ε=0.5: {fine:?}");
}
#[test]
fn approximate_degenerate_inputs_pass_through() {
assert_eq!(approximate_polygon(&[], tolerance!(1.0)), vec![]);
assert_eq!(
approximate_polygon(&[c(1, 2)], tolerance!(1.0)),
vec![c(1, 2)]
);
assert_eq!(
approximate_polygon(&[c(1, 2), c(3, 4)], tolerance!(1.0)),
vec![c(1, 2), c(3, 4)]
);
}
}