use std::sync::atomic::{AtomicU64, Ordering};
use geo::{
Area, BoundingRect, Centroid, Geometry, LineString, MultiLineString, MultiPolygon, Point,
Polygon, Rect, Simplify, Validation,
};
pub use super::level::{Crs, METERS_PER_DEGREE};
static FULL_RES_FALLBACKS: AtomicU64 = AtomicU64::new(0);
pub fn full_resolution_fallback_count() -> u64 {
FULL_RES_FALLBACKS.load(Ordering::Relaxed)
}
static VALIDATION_SKIPS: AtomicU64 = AtomicU64::new(0);
pub fn validation_skip_count() -> u64 {
VALIDATION_SKIPS.load(Ordering::Relaxed)
}
pub const DEFAULT_SIMPLIFY_FACTOR: f64 = 1.0;
const MIN_POLYGON_RING_POINTS: usize = 4;
const MIN_LINESTRING_POINTS: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CollapseMode {
#[default]
Drop,
Point,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SimplifyOptions {
pub factor: f64,
pub collapse: CollapseMode,
pub cascade: bool,
}
impl Default for SimplifyOptions {
fn default() -> Self {
Self {
factor: DEFAULT_SIMPLIFY_FACTOR,
collapse: CollapseMode::Drop,
cascade: true,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Simplified {
Keep(Geometry<f64>),
Dropped,
}
pub fn world_tolerance(gsd_meters: f64, crs: Crs, opts: &SimplifyOptions) -> f64 {
let meters = opts.factor * gsd_meters;
if meters <= 0.0 {
return 0.0;
}
crs.meters_to_units(meters)
}
pub fn simplify_for_level(
geom: &Geometry<f64>,
gsd_meters: f64,
crs: Crs,
opts: &SimplifyOptions,
) -> Simplified {
let tol = world_tolerance(gsd_meters, crs, opts);
if tol <= 0.0 {
return Simplified::Keep(geom.clone());
}
match geom {
Geometry::Point(_) | Geometry::MultiPoint(_) => Simplified::Keep(geom.clone()),
Geometry::LineString(ls) => match simplify_linestring_impl(ls, tol) {
Some(out) => Simplified::Keep(Geometry::LineString(out)),
None => Simplified::Dropped,
},
Geometry::MultiLineString(mls) => {
let kept: Vec<LineString<f64>> = mls
.0
.iter()
.filter_map(|ls| simplify_linestring_impl(ls, tol))
.collect();
if kept.is_empty() {
Simplified::Dropped
} else {
Simplified::Keep(Geometry::MultiLineString(MultiLineString::new(kept)))
}
}
Geometry::Polygon(poly) => simplify_polygon_impl(poly, tol, opts.collapse, opts.cascade),
Geometry::MultiPolygon(mp) => {
let part_mode = match opts.collapse {
CollapseMode::Square => CollapseMode::Square,
_ => CollapseMode::Drop,
};
let kept: Vec<Polygon<f64>> =
mp.0.iter()
.flat_map(
|p| match simplify_polygon_impl(p, tol, part_mode, opts.cascade) {
Simplified::Keep(Geometry::Polygon(poly)) => vec![poly],
Simplified::Keep(Geometry::MultiPolygon(parts)) => parts.0,
_ => Vec::new(),
},
)
.collect();
if !kept.is_empty() {
Simplified::Keep(Geometry::MultiPolygon(MultiPolygon::new(kept)))
} else {
match opts.collapse {
CollapseMode::Drop | CollapseMode::Square => Simplified::Dropped,
CollapseMode::Point => match mp.centroid() {
Some(pt) => Simplified::Keep(Geometry::Point(pt)),
None => Simplified::Dropped,
},
}
}
}
other => Simplified::Keep(other.clone()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Representation {
#[default]
Geometry,
Point,
Square,
}
impl Representation {
pub fn as_str(self) -> &'static str {
match self {
Representation::Geometry => "geom",
Representation::Point => "point",
Representation::Square => "square",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CascadeStep {
pub gsd_meters: f64,
pub repr: Representation,
}
impl CascadeStep {
pub fn geom(gsd_meters: f64) -> Self {
Self {
gsd_meters,
repr: Representation::Geometry,
}
}
pub fn point(gsd_meters: f64) -> Self {
Self {
gsd_meters,
repr: Representation::Point,
}
}
pub fn square(gsd_meters: f64) -> Self {
Self {
gsd_meters,
repr: Representation::Square,
}
}
}
fn polygonal_representative_point(geom: &Geometry<f64>) -> Option<Simplified> {
match geom {
Geometry::Polygon(poly) => Some(collapse_polygon(poly, CollapseMode::Point, 0.0)),
Geometry::MultiPolygon(mp) => {
let pt = mp
.centroid()
.or_else(|| mp.bounding_rect().map(|r| r.center().into()))
.or_else(|| {
mp.0.first()
.and_then(|p| p.exterior().0.first())
.map(|c| Point::new(c.x, c.y))
});
Some(match pt {
Some(p) => Simplified::Keep(Geometry::Point(p)),
None => Simplified::Dropped,
})
}
_ => None,
}
}
pub fn simplify_step(
geom: &Geometry<f64>,
gsd_meters: f64,
crs: Crs,
opts: &SimplifyOptions,
repr: Representation,
) -> Simplified {
match repr {
Representation::Geometry => simplify_for_level(geom, gsd_meters, crs, opts),
Representation::Point => {
if let Some(out) = polygonal_representative_point(geom) {
return out;
}
simplify_for_level(geom, gsd_meters, crs, opts)
}
Representation::Square => {
let opts = SimplifyOptions {
collapse: CollapseMode::Square,
..*opts
};
simplify_for_level(geom, gsd_meters, crs, &opts)
}
}
}
pub fn simplify_cascade(
geom: &Geometry<f64>,
steps_fine_to_coarse: &[CascadeStep],
crs: Crs,
opts: &SimplifyOptions,
) -> Simplified {
let mut current: Option<Geometry<f64>> = None;
let mut alive = true;
let mut result = Simplified::Keep(geom.clone());
for step in steps_fine_to_coarse {
let out = if !alive && step.repr == Representation::Geometry {
Simplified::Dropped
} else {
let input = if alive {
current.as_ref().unwrap_or(geom)
} else {
geom
};
simplify_step(input, step.gsd_meters, crs, opts, step.repr)
};
match &out {
Simplified::Keep(g) => {
current = Some(g.clone());
alive = true;
}
Simplified::Dropped => alive = false,
}
result = out;
}
result
}
#[inline]
fn rect_diag(r: Rect<f64>) -> f64 {
r.width().hypot(r.height())
}
#[inline]
fn linestring_diag(ls: &LineString<f64>) -> f64 {
ls.bounding_rect().map(rect_diag).unwrap_or(0.0)
}
#[inline]
fn polygon_diag(poly: &Polygon<f64>) -> f64 {
poly.bounding_rect().map(rect_diag).unwrap_or(0.0)
}
fn simplify_linestring_impl(ls: &LineString<f64>, tol: f64) -> Option<LineString<f64>> {
if ls.0.len() < MIN_LINESTRING_POINTS {
return None;
}
let diag = linestring_diag(ls);
if diag < tol {
return None;
}
let simplified = ls.simplify(tol);
if simplified.0.len() < MIN_LINESTRING_POINTS || linestring_diag(&simplified) <= 0.0 {
return None;
}
Some(simplified)
}
const INVALID_RETRY_HALVINGS: u32 = 3;
const MAX_VALIDATION_VERTS: usize = 2_048;
fn polygon_vertex_count(poly: &Polygon<f64>) -> usize {
poly.exterior().0.len() + poly.interiors().iter().map(|r| r.0.len()).sum::<usize>()
}
fn capped_is_valid(candidate: &Polygon<f64>) -> bool {
let verts = polygon_vertex_count(candidate);
if verts > MAX_VALIDATION_VERTS {
VALIDATION_SKIPS.fetch_add(1, Ordering::Relaxed);
log::trace!(
"overview simplify: skipping O(V²) validity check on {verts}-vertex \
candidate (cap {MAX_VALIDATION_VERTS}); assuming valid"
);
return true;
}
candidate.is_valid()
}
fn polygon_unchanged(candidate: &Polygon<f64>, original: &Polygon<f64>) -> bool {
candidate.exterior().0.len() == original.exterior().0.len()
&& candidate.interiors().len() == original.interiors().len()
&& candidate
.interiors()
.iter()
.zip(original.interiors())
.all(|(a, b)| a.0.len() == b.0.len())
}
fn simplify_polygon_impl(
poly: &Polygon<f64>,
tol: f64,
mode: CollapseMode,
repair: bool,
) -> Simplified {
if polygon_diag(poly) < tol {
return collapse_polygon(poly, mode, tol);
}
let min_area = tol * tol;
let attempts = if repair {
1
} else {
INVALID_RETRY_HALVINGS + 1
};
let mut eps = tol;
let mut invalid_candidate: Option<Polygon<f64>> = None;
for _ in 0..attempts {
let simplified = poly.simplify(eps);
let interiors: Vec<LineString<f64>> = simplified
.interiors()
.iter()
.filter(|ring| linestring_diag(ring) >= tol)
.cloned()
.collect();
let exterior = simplified.exterior().clone();
let candidate = Polygon::new(exterior, interiors);
if candidate.exterior().0.len() < MIN_POLYGON_RING_POINTS
|| candidate.unsigned_area() < min_area
{
return collapse_polygon(poly, mode, tol);
}
if polygon_unchanged(&candidate, poly) || capped_is_valid(&candidate) {
return Simplified::Keep(Geometry::Polygon(candidate));
}
invalid_candidate = Some(candidate);
eps *= 0.5;
}
if repair {
let candidate = invalid_candidate.expect("loop ran at least once");
if let Some(repaired) = repair_candidate(&candidate, tol, min_area) {
log::trace!(
"overview simplify: repaired self-intersecting RDP candidate \
instead of keeping full resolution"
);
return Simplified::Keep(repaired);
}
return collapse_polygon(poly, mode, tol);
}
FULL_RES_FALLBACKS.fetch_add(1, Ordering::Relaxed);
log::trace!(
"overview simplify: RDP candidate invalid after {} epsilon retries; \
keeping full-resolution geometry",
INVALID_RETRY_HALVINGS + 1
);
Simplified::Keep(Geometry::Polygon(poly.clone()))
}
fn repair_candidate(candidate: &Polygon<f64>, tol: f64, min_area: f64) -> Option<Geometry<f64>> {
let kept: Vec<Polygon<f64>> = match crate::ioverlay_clip::repair_polygon_ioverlay(candidate)? {
Geometry::Polygon(p) => vec![p],
Geometry::MultiPolygon(mp) => mp.0,
_ => return None,
}
.into_iter()
.filter(|p| polygon_diag(p) >= tol && p.unsigned_area() >= min_area)
.collect();
match kept.len() {
0 => None,
1 => Some(Geometry::Polygon(kept.into_iter().next().expect("len 1"))),
_ => Some(Geometry::MultiPolygon(MultiPolygon::new(kept))),
}
}
fn polygon_anchor(poly: &Polygon<f64>) -> Option<Point<f64>> {
poly.centroid()
.or_else(|| poly.bounding_rect().map(|r| r.center().into()))
.or_else(|| poly.exterior().0.first().map(|c| Point::new(c.x, c.y)))
}
fn dither_u01(x: f64, y: f64) -> f64 {
let mut z = x.to_bits() ^ y.to_bits().rotate_left(32);
z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
(z >> 11) as f64 / (1u64 << 53) as f64
}
fn placeholder_square(anchor: Point<f64>, tol: f64) -> Polygon<f64> {
let h = tol * 0.5;
let (cx, cy) = (anchor.x(), anchor.y());
Polygon::new(
LineString::new(vec![
geo::Coord {
x: cx - h,
y: cy - h,
},
geo::Coord {
x: cx + h,
y: cy - h,
},
geo::Coord {
x: cx + h,
y: cy + h,
},
geo::Coord {
x: cx - h,
y: cy + h,
},
geo::Coord {
x: cx - h,
y: cy - h,
},
]),
vec![],
)
}
fn squarify_polygon(poly: &Polygon<f64>, tol: f64) -> Simplified {
let Some(anchor) = polygon_anchor(poly) else {
return Simplified::Dropped;
};
let threshold = tol * tol;
let p = if threshold > 0.0 {
(poly.unsigned_area() / threshold).min(1.0)
} else {
0.0
};
if dither_u01(anchor.x(), anchor.y()) < p {
Simplified::Keep(Geometry::Polygon(placeholder_square(anchor, tol)))
} else {
Simplified::Dropped
}
}
pub(super) fn carrier_square(
geom: &Geometry<f64>,
gsd_meters: f64,
crs: Crs,
opts: &SimplifyOptions,
) -> Option<Geometry<f64>> {
let anchor = match geom {
Geometry::Polygon(p) => polygon_anchor(p),
Geometry::MultiPolygon(mp) => {
mp.0.iter()
.max_by(|a, b| a.unsigned_area().total_cmp(&b.unsigned_area()))
.and_then(polygon_anchor)
}
_ => None,
}?;
Some(Geometry::Polygon(placeholder_square(
anchor,
world_tolerance(gsd_meters, crs, opts),
)))
}
fn collapse_polygon(poly: &Polygon<f64>, mode: CollapseMode, tol: f64) -> Simplified {
match mode {
CollapseMode::Drop => Simplified::Dropped,
CollapseMode::Point => match polygon_anchor(poly) {
Some(p) => Simplified::Keep(Geometry::Point(p)),
None => Simplified::Dropped,
},
CollapseMode::Square => squarify_polygon(poly, tol),
}
}
#[cfg(test)]
mod tests {
use super::*;
use geo::{Coord, MultiPoint};
fn wiggly_line(n: usize, wobble: f64) -> LineString<f64> {
LineString::new(
(0..n)
.map(|i| Coord {
x: i as f64 * 100.0,
y: (i as f64 * 0.7).sin() * wobble,
})
.collect(),
)
}
fn line_len(g: &Simplified) -> usize {
match g {
Simplified::Keep(Geometry::LineString(ls)) => ls.0.len(),
other => panic!("expected Keep(LineString), got {other:?}"),
}
}
fn square(cx: f64, cy: f64, half: f64) -> Polygon<f64> {
Polygon::new(
LineString::new(vec![
Coord {
x: cx - half,
y: cy - half,
},
Coord {
x: cx + half,
y: cy - half,
},
Coord {
x: cx + half,
y: cy + half,
},
Coord {
x: cx - half,
y: cy + half,
},
Coord {
x: cx - half,
y: cy - half,
},
]),
vec![],
)
}
#[test]
fn test_tolerance_crs_conversion() {
let opts = SimplifyOptions::default();
assert_eq!(world_tolerance(1000.0, Crs::Epsg3857, &opts), 1000.0);
let deg = world_tolerance(1000.0, Crs::Epsg4326, &opts);
assert!((deg - 1000.0 / METERS_PER_DEGREE).abs() < 1e-12);
assert!(deg < 1.0 && deg > 0.0);
}
#[test]
fn test_tolerance_zero_is_canonical() {
let opts = SimplifyOptions::default();
assert_eq!(world_tolerance(0.0, Crs::Epsg3857, &opts), 0.0);
let zero_factor = SimplifyOptions {
factor: 0.0,
..opts
};
assert_eq!(world_tolerance(500.0, Crs::Epsg3857, &zero_factor), 0.0);
}
#[test]
fn test_coarser_gsd_fewer_vertices_monotone() {
let line = Geometry::LineString(wiggly_line(200, 50.0));
let opts = SimplifyOptions::default();
let gsds = [10.0, 50.0, 100.0, 500.0, 2000.0];
let counts: Vec<usize> = gsds
.iter()
.map(|g| line_len(&simplify_for_level(&line, *g, Crs::Epsg3857, &opts)))
.collect();
for w in counts.windows(2) {
assert!(
w[0] >= w[1],
"vertex count should not increase with coarser GSD: {counts:?}"
);
}
assert!(
counts.first() > counts.last(),
"coarsest GSD should reduce vertices vs finest: {counts:?}"
);
}
#[test]
fn test_4326_and_3857_scale_comparably() {
let n = 120;
let wobble_m = 40.0;
let line_m = Geometry::LineString(LineString::new(
(0..n)
.map(|i| Coord {
x: i as f64 * 200.0,
y: (i as f64 * 0.6).sin() * wobble_m,
})
.collect(),
));
let line_deg = Geometry::LineString(LineString::new(
(0..n)
.map(|i| Coord {
x: i as f64 * 200.0 / METERS_PER_DEGREE,
y: (i as f64 * 0.6).sin() * wobble_m / METERS_PER_DEGREE,
})
.collect(),
));
let opts = SimplifyOptions::default();
let gsd = 100.0;
let c_m = line_len(&simplify_for_level(&line_m, gsd, Crs::Epsg3857, &opts));
let c_deg = line_len(&simplify_for_level(&line_deg, gsd, Crs::Epsg4326, &opts));
assert_eq!(
c_m, c_deg,
"CRS-converted tolerance should simplify identical shapes equally: {c_m} vs {c_deg}"
);
}
#[test]
fn test_points_pass_through() {
let opts = SimplifyOptions::default();
let p = Geometry::Point(Point::new(3.0, 4.0));
assert_eq!(
simplify_for_level(&p, 5000.0, Crs::Epsg3857, &opts),
Simplified::Keep(p.clone())
);
let mp = Geometry::MultiPoint(MultiPoint::new(vec![
Point::new(1.0, 1.0),
Point::new(2.0, 2.0),
]));
assert_eq!(
simplify_for_level(&mp, 5000.0, Crs::Epsg3857, &opts),
Simplified::Keep(mp.clone())
);
}
#[test]
fn test_canonical_identity_bit_equal() {
let opts = SimplifyOptions::default();
let line = Geometry::LineString(wiggly_line(50, 30.0));
match simplify_for_level(&line, 0.0, Crs::Epsg3857, &opts) {
Simplified::Keep(g) => assert_eq!(g, line),
Simplified::Dropped => panic!("canonical level must not drop"),
}
let poly = Geometry::Polygon(square(0.0, 0.0, 1000.0));
match simplify_for_level(&poly, 0.0, Crs::Epsg3857, &opts) {
Simplified::Keep(g) => assert_eq!(g, poly),
Simplified::Dropped => panic!("canonical level must not drop"),
}
}
#[test]
fn test_line_dropped_below_visibility() {
let opts = SimplifyOptions::default();
let tiny = Geometry::LineString(LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 10.0, y: 0.0 },
]));
assert_eq!(
simplify_for_level(&tiny, 1000.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
assert!(matches!(
simplify_for_level(&tiny, 1.0, Crs::Epsg3857, &opts),
Simplified::Keep(_)
));
}
#[test]
fn test_single_point_line_dropped() {
let opts = SimplifyOptions::default();
let degen = Geometry::LineString(LineString::new(vec![Coord { x: 5.0, y: 5.0 }]));
assert_eq!(
simplify_for_level(°en, 1.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
let dup = Geometry::LineString(LineString::new(vec![
Coord { x: 5.0, y: 5.0 },
Coord { x: 5.0, y: 5.0 },
]));
assert_eq!(
simplify_for_level(&dup, 1.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
}
#[test]
fn test_empty_line_dropped() {
let opts = SimplifyOptions::default();
let empty = Geometry::LineString(LineString::new(vec![]));
assert_eq!(
simplify_for_level(&empty, 1.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
}
#[test]
fn test_polygon_ring_valid_after_simplify() {
let opts = SimplifyOptions::default();
let coords: Vec<Coord<f64>> = (0..=64)
.map(|i| {
let a = i as f64 * std::f64::consts::TAU / 64.0;
Coord {
x: a.cos() * 5000.0,
y: a.sin() * 5000.0,
}
})
.collect();
let poly = Geometry::Polygon(Polygon::new(LineString::new(coords), vec![]));
match simplify_for_level(&poly, 500.0, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::Polygon(p)) => {
assert!(p.exterior().0.len() >= MIN_POLYGON_RING_POINTS);
assert_eq!(
p.exterior().0.first(),
p.exterior().0.last(),
"exterior ring must stay closed"
);
assert!(p.is_valid(), "simplified polygon must be valid");
assert!(
p.exterior().0.len() < 65,
"polygon should actually be simplified"
);
}
other => panic!("expected Keep(Polygon), got {other:?}"),
}
}
#[test]
fn test_interior_ring_collapse_dropped() {
let opts = SimplifyOptions::default();
let exterior = LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 10000.0, y: 0.0 },
Coord {
x: 10000.0,
y: 10000.0,
},
Coord { x: 0.0, y: 10000.0 },
Coord { x: 0.0, y: 0.0 },
]);
let tiny_hole = LineString::new(vec![
Coord { x: 100.0, y: 100.0 },
Coord { x: 105.0, y: 100.0 },
Coord { x: 105.0, y: 105.0 },
Coord { x: 100.0, y: 105.0 },
Coord { x: 100.0, y: 100.0 },
]);
let poly = Geometry::Polygon(Polygon::new(exterior, vec![tiny_hole]));
match simplify_for_level(&poly, 1000.0, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::Polygon(p)) => {
assert_eq!(p.interiors().len(), 0, "collapsed hole must be dropped");
assert!(p.is_valid());
}
other => panic!("expected Keep(Polygon) with no interiors, got {other:?}"),
}
}
#[test]
fn test_polygon_collapse_default_drop_vs_optin_point() {
let poly = Geometry::Polygon(square(1000.0, 2000.0, 10.0));
let drop_opts = SimplifyOptions::default();
assert_eq!(
simplify_for_level(&poly, 5000.0, Crs::Epsg3857, &drop_opts),
Simplified::Dropped
);
let collapse_opts = SimplifyOptions {
collapse: CollapseMode::Point,
..Default::default()
};
match simplify_for_level(&poly, 5000.0, Crs::Epsg3857, &collapse_opts) {
Simplified::Keep(Geometry::Point(pt)) => {
assert!((pt.x() - 1000.0).abs() < 1.0);
assert!((pt.y() - 2000.0).abs() < 1.0);
}
other => panic!("expected Keep(Point), got {other:?}"),
}
}
#[test]
fn test_sliver_polygon_collapses() {
let opts = SimplifyOptions::default();
let sliver = Polygon::new(
LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 1000.0, y: 0.0 },
Coord { x: 2000.0, y: 0.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
);
assert_eq!(
simplify_for_level(&Geometry::Polygon(sliver), 100.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
}
fn notch_finger_polygon() -> Polygon<f64> {
let c = |x: f64, y: f64| Coord { x, y };
Polygon::new(
LineString::new(vec![
c(0.0, 0.0),
c(45.0, 0.0),
c(50.0, -3.0),
c(55.0, 0.0),
c(100.0, 0.0),
c(100.1, 30.0), c(100.0, 60.0),
c(99.9, 80.0), c(100.0, 100.0),
c(52.0, 100.0),
c(50.0, -1.0),
c(48.0, 100.0),
c(0.0, 100.0),
c(0.1, 50.0), c(0.0, 0.0),
]),
vec![],
)
}
#[test]
fn test_invalid_rdp_candidate_retries_to_simplified_valid() {
let poly = notch_finger_polygon();
let orig_len = poly.exterior().0.len();
assert!(poly.is_valid(), "fixture must start valid");
assert!(
!poly.simplify(4.0).is_valid(),
"fixture must self-intersect at the full tolerance (precondition)"
);
let opts = SimplifyOptions {
cascade: false,
..SimplifyOptions::default()
};
match simplify_for_level(&Geometry::Polygon(poly), 4.0, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::Polygon(p)) => {
assert!(
p.is_valid(),
"retry output must be valid, got {:?}",
p.exterior()
);
assert!(
p.exterior().0.len() < orig_len,
"retry output must be simplified, not the full-resolution \
fallback ({} !< {orig_len})",
p.exterior().0.len()
);
}
other => panic!("expected Keep(Polygon), got {other:?}"),
}
}
#[test]
fn test_invalid_rdp_candidate_repaired_when_cascade() {
let poly = notch_finger_polygon();
let orig_len = poly.exterior().0.len();
assert!(
!poly.simplify(4.0).is_valid(),
"fixture must self-intersect at the full tolerance (precondition)"
);
let opts = SimplifyOptions::default();
let fallbacks_before = full_resolution_fallback_count();
match simplify_for_level(&Geometry::Polygon(poly), 4.0, Crs::Epsg3857, &opts) {
Simplified::Keep(g) => {
assert!(g.is_valid(), "repaired output must be valid, got {g:?}");
let out_len: usize = match &g {
Geometry::Polygon(p) => p.exterior().0.len(),
Geometry::MultiPolygon(mp) => mp.0.iter().map(|p| p.exterior().0.len()).sum(),
other => panic!("expected (Multi)Polygon, got {other:?}"),
};
assert!(
out_len < orig_len,
"repaired output must be simplified, not the \
full-resolution fallback ({out_len} !< {orig_len})"
);
}
other => panic!("expected Keep, got {other:?}"),
}
assert_eq!(
full_resolution_fallback_count(),
fallbacks_before,
"repair path must never count a full-resolution fallback"
);
}
#[test]
fn test_cascade_default_on() {
assert!(SimplifyOptions::default().cascade);
}
fn geom_steps(gsds: &[f64]) -> Vec<CascadeStep> {
gsds.iter().map(|&g| CascadeStep::geom(g)).collect()
}
#[test]
fn test_cascade_empty_chain_is_identity() {
let g = Geometry::LineString(wiggly_line(50, 10.0));
assert_eq!(
simplify_cascade(&g, &[], Crs::Epsg3857, &SimplifyOptions::default()),
Simplified::Keep(g.clone())
);
}
#[test]
fn test_cascade_single_step_matches_direct() {
let g = Geometry::LineString(wiggly_line(200, 30.0));
let opts = SimplifyOptions::default();
assert_eq!(
simplify_cascade(&g, &geom_steps(&[100.0]), Crs::Epsg3857, &opts),
simplify_for_level(&g, 100.0, Crs::Epsg3857, &opts)
);
}
#[test]
fn test_cascade_vertices_subset_of_canonical() {
let canonical = wiggly_line(400, 60.0);
let canonical_set: Vec<Coord<f64>> = canonical.0.clone();
let g = Geometry::LineString(canonical);
let opts = SimplifyOptions::default();
match simplify_cascade(&g, &geom_steps(&[25.0, 50.0, 100.0]), Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::LineString(out)) => {
assert!(out.0.len() < canonical_set.len(), "chain must simplify");
for c in &out.0 {
assert!(
canonical_set.contains(c),
"cascaded vertex {c:?} not in canonical geometry"
);
}
}
other => panic!("expected Keep(LineString), got {other:?}"),
}
}
#[test]
fn test_cascade_drop_short_circuits() {
let tiny = Geometry::LineString(LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 10.0, y: 0.0 },
]));
let opts = SimplifyOptions::default();
assert_eq!(
simplify_cascade(&tiny, &geom_steps(&[1000.0, 5000.0]), Crs::Epsg3857, &opts),
Simplified::Dropped
);
}
#[test]
fn test_simplify_step_point_repr_polygon_to_centroid() {
let opts = SimplifyOptions::default();
let poly = Geometry::Polygon(square(1000.0, 2000.0, 5000.0));
match simplify_step(&poly, 100.0, Crs::Epsg3857, &opts, Representation::Point) {
Simplified::Keep(Geometry::Point(pt)) => {
assert!((pt.x() - 1000.0).abs() < 1e-9);
assert!((pt.y() - 2000.0).abs() < 1e-9);
}
other => panic!("expected Keep(Point), got {other:?}"),
}
assert_eq!(
simplify_step(&poly, 100.0, Crs::Epsg3857, &opts, Representation::Geometry),
simplify_for_level(&poly, 100.0, Crs::Epsg3857, &opts)
);
}
#[test]
fn test_simplify_step_point_repr_multipolygon_to_centroid() {
let opts = SimplifyOptions::default();
let mp = Geometry::MultiPolygon(MultiPolygon::new(vec![
square(0.0, 0.0, 1000.0),
square(4000.0, 0.0, 1000.0),
]));
match simplify_step(&mp, 100.0, Crs::Epsg3857, &opts, Representation::Point) {
Simplified::Keep(Geometry::Point(pt)) => {
assert!((pt.x() - 2000.0).abs() < 1e-9, "centroid x, got {}", pt.x());
assert!(pt.y().abs() < 1e-9);
}
other => panic!("expected Keep(Point), got {other:?}"),
}
}
#[test]
fn test_simplify_step_point_repr_sub_gate_polygon_kept_as_point() {
let opts = SimplifyOptions::default();
let tiny = Geometry::Polygon(square(50.0, 50.0, 10.0));
assert_eq!(
simplify_for_level(&tiny, 5000.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
assert!(matches!(
simplify_step(&tiny, 5000.0, Crs::Epsg3857, &opts, Representation::Point),
Simplified::Keep(Geometry::Point(_))
));
}
#[test]
fn test_simplify_step_point_repr_lines_and_points_unaffected() {
let opts = SimplifyOptions::default();
let line = Geometry::LineString(wiggly_line(200, 50.0));
assert_eq!(
simplify_step(&line, 100.0, Crs::Epsg3857, &opts, Representation::Point),
simplify_for_level(&line, 100.0, Crs::Epsg3857, &opts)
);
let p = Geometry::Point(Point::new(3.0, 4.0));
assert_eq!(
simplify_step(&p, 5000.0, Crs::Epsg3857, &opts, Representation::Point),
Simplified::Keep(p.clone())
);
}
#[test]
fn test_cascade_point_band_shares_one_point_across_coarser_steps() {
let opts = SimplifyOptions::default();
let poly = Geometry::Polygon(square(500.0, -300.0, 5000.0));
let steps = [
CascadeStep::geom(50.0),
CascadeStep::geom(100.0),
CascadeStep::point(200.0),
CascadeStep::point(400.0),
];
let at_boundary = simplify_cascade(&poly, &steps[..3], Crs::Epsg3857, &opts);
let at_coarser = simplify_cascade(&poly, &steps, Crs::Epsg3857, &opts);
match (&at_boundary, &at_coarser) {
(Simplified::Keep(Geometry::Point(a)), Simplified::Keep(Geometry::Point(b))) => {
assert_eq!(a, b, "band levels must share the boundary point");
}
other => panic!("expected two Keep(Point), got {other:?}"),
}
}
#[test]
fn test_cascade_point_step_revives_dropped_geometry() {
let opts = SimplifyOptions::default();
let poly = Geometry::Polygon(square(500.0, 700.0, 10.0));
let steps = [
CascadeStep::geom(5_000.0),
CascadeStep::point(10_000.0),
CascadeStep::point(20_000.0),
];
assert_eq!(
simplify_cascade(&poly, &steps[..1], Crs::Epsg3857, &opts),
Simplified::Dropped,
"precondition: geometry step drops the tiny polygon"
);
for chain in [&steps[..2], &steps[..3]] {
match simplify_cascade(&poly, chain, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::Point(pt)) => {
assert!((pt.x() - 500.0).abs() < 1e-9);
assert!((pt.y() - 700.0).abs() < 1e-9);
}
other => panic!("point step must revive from canonical, got {other:?}"),
}
}
}
#[test]
fn test_cascade_square_step_revives_dropped_geometry() {
let opts = SimplifyOptions::default();
let mut revived = 0;
for i in 0..300 {
let (cx, cy) = (i as f64 * 7_919.0, i as f64 * 3_571.0);
let poly = Geometry::Polygon(square(cx, cy, 2_000.0));
let steps = [CascadeStep::geom(5_000.0), CascadeStep::square(8_000.0)];
assert_eq!(
simplify_cascade(&poly, &steps[..1], Crs::Epsg3857, &opts),
Simplified::Dropped,
"precondition: geometry step drops it"
);
match simplify_cascade(&poly, &steps, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::Polygon(sq)) => {
let r = sq.bounding_rect().unwrap();
assert!((r.width() - 8_000.0).abs() < 1e-6, "side = step tol");
revived += 1;
}
Simplified::Dropped => {}
other => panic!("expected Keep(Polygon) or Dropped, got {other:?}"),
}
}
assert!(revived > 0, "some anchors must dither through");
assert!(revived < 300, "not all (p = 16e6/64e6 = 0.25 per anchor)");
}
#[test]
fn test_square_collapse_emits_gsd_square_at_anchor() {
let tol = 5000.0; let opts = SimplifyOptions {
collapse: CollapseMode::Square,
..Default::default()
};
let mut checked = 0;
for i in 0..200 {
let (cx, cy) = (1000.0 + i as f64 * 3137.0, -2000.0 + i as f64 * 911.0);
let poly = Geometry::Polygon(square(cx, cy, 1000.0));
match simplify_for_level(&poly, tol, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::Polygon(sq)) => {
let ring = &sq.exterior().0;
assert_eq!(ring.len(), 5, "closed 5-coordinate square ring");
let r = sq.bounding_rect().unwrap();
assert!((r.width() - tol).abs() < 1e-6, "side = tol");
assert!((r.height() - tol).abs() < 1e-6, "side = tol");
let c = r.center();
assert!((c.x - cx).abs() < 1e-6 && (c.y - cy).abs() < 1e-6);
assert!((sq.unsigned_area() - tol * tol).abs() < 1e-3);
checked += 1;
}
Simplified::Dropped => {}
other => panic!("expected Keep(Polygon) or Dropped, got {other:?}"),
}
}
assert!(checked > 0, "at least one anchor must survive the dither");
assert!(checked < 200, "not every anchor may survive (p ≈ 0.16)");
}
#[test]
fn test_square_collapse_deterministic() {
let opts = SimplifyOptions {
collapse: CollapseMode::Square,
..Default::default()
};
for i in 0..50 {
let poly = Geometry::Polygon(square(i as f64 * 731.0, i as f64 * 197.0, 500.0));
let a = simplify_for_level(&poly, 5000.0, Crs::Epsg3857, &opts);
let b = simplify_for_level(&poly, 5000.0, Crs::Epsg3857, &opts);
assert_eq!(a, b);
}
}
#[test]
fn test_square_collapse_preserves_aggregate_area_statistically() {
let tol = 5000.0;
let opts = SimplifyOptions {
collapse: CollapseMode::Square,
..Default::default()
};
let n = 4000;
let half = 1250.0; let mut true_area = 0.0;
let mut emitted_area = 0.0;
for i in 0..n {
let (cx, cy) = (i as f64 * 17_077.0, (i % 613) as f64 * 12_923.0);
let poly = square(cx, cy, half);
true_area += poly.unsigned_area();
if let Simplified::Keep(Geometry::Polygon(sq)) =
simplify_for_level(&Geometry::Polygon(poly), tol, Crs::Epsg3857, &opts)
{
emitted_area += sq.unsigned_area();
}
}
let ratio = emitted_area / true_area;
assert!(
(0.85..1.15).contains(&ratio),
"aggregate area must be preserved in expectation, ratio = {ratio}"
);
}
#[test]
fn test_square_collapse_leaves_visible_polygons_alone() {
let big = Geometry::Polygon(square(0.0, 0.0, 50_000.0));
let drop_opts = SimplifyOptions::default();
let square_opts = SimplifyOptions {
collapse: CollapseMode::Square,
..Default::default()
};
assert_eq!(
simplify_for_level(&big, 1000.0, Crs::Epsg3857, &square_opts),
simplify_for_level(&big, 1000.0, Crs::Epsg3857, &drop_opts)
);
}
#[test]
fn test_square_collapse_multipolygon_per_part() {
let tol = 5000.0;
let opts = SimplifyOptions {
collapse: CollapseMode::Square,
..Default::default()
};
let parts: Vec<Polygon<f64>> = (0..40)
.map(|i| square(i as f64 * 40_000.0, i as f64 * 23_000.0, 2000.0))
.collect();
let mp = Geometry::MultiPolygon(MultiPolygon::new(parts));
match simplify_for_level(&mp, tol, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::MultiPolygon(out)) => {
assert!(out.0.len() > 1, "several parts should dither through");
assert!(out.0.len() < 40, "some parts should dither out");
for p in &out.0 {
let r = p.bounding_rect().unwrap();
assert!((r.width() - tol).abs() < 1e-6);
}
}
other => panic!("expected Keep(MultiPolygon), got {other:?}"),
}
}
#[test]
fn test_square_step_and_cascade_consistency() {
let opts = SimplifyOptions::default();
let poly = Geometry::Polygon(square(731.0, -1911.0, 800.0));
let direct = simplify_step(&poly, 5000.0, Crs::Epsg3857, &opts, Representation::Square);
let steps = [CascadeStep {
gsd_meters: 5000.0,
repr: Representation::Square,
}];
assert_eq!(
direct,
simplify_cascade(&poly, &steps, Crs::Epsg3857, &opts)
);
let chain = [
CascadeStep {
gsd_meters: 5000.0,
repr: Representation::Square,
},
CascadeStep {
gsd_meters: 10_000.0,
repr: Representation::Square,
},
];
let coarser = simplify_cascade(&poly, &chain, Crs::Epsg3857, &opts);
if matches!(direct, Simplified::Dropped) {
assert_eq!(coarser, Simplified::Dropped, "drops are monotone");
}
}
#[test]
fn test_multipolygon_part_dropping() {
let opts = SimplifyOptions::default();
let big = square(0.0, 0.0, 5000.0);
let tiny = square(20000.0, 20000.0, 10.0);
let mp = Geometry::MultiPolygon(MultiPolygon::new(vec![big, tiny]));
match simplify_for_level(&mp, 1000.0, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::MultiPolygon(m)) => {
assert_eq!(m.0.len(), 1, "tiny part should be dropped");
assert!(m.0[0].is_valid());
}
other => panic!("expected Keep(MultiPolygon) with 1 part, got {other:?}"),
}
}
#[test]
fn test_multipolygon_all_parts_gone_dropped() {
let opts = SimplifyOptions::default();
let mp = Geometry::MultiPolygon(MultiPolygon::new(vec![
square(0.0, 0.0, 10.0),
square(100.0, 100.0, 8.0),
]));
assert_eq!(
simplify_for_level(&mp, 5000.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
}
#[test]
fn test_multilinestring_part_dropping() {
let opts = SimplifyOptions::default();
let long = LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 10000.0, y: 0.0 }]);
let short = LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 5.0, y: 0.0 }]);
let mls = Geometry::MultiLineString(MultiLineString::new(vec![long, short]));
match simplify_for_level(&mls, 1000.0, Crs::Epsg3857, &opts) {
Simplified::Keep(Geometry::MultiLineString(m)) => {
assert_eq!(m.0.len(), 1, "short part should be dropped");
}
other => panic!("expected Keep(MultiLineString) with 1 part, got {other:?}"),
}
}
#[test]
fn test_multilinestring_all_gone_dropped() {
let opts = SimplifyOptions::default();
let mls = Geometry::MultiLineString(MultiLineString::new(vec![
LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 5.0, y: 0.0 }]),
LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 3.0, y: 0.0 }]),
]));
assert_eq!(
simplify_for_level(&mls, 1000.0, Crs::Epsg3857, &opts),
Simplified::Dropped
);
}
fn padded_bowtie(periods: usize) -> Polygon<f64> {
let mut v = vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 10.0, y: 10.0 },
Coord { x: 10.0, y: 0.0 },
Coord { x: 0.0, y: 10.0 },
];
let h = 8.0 / periods as f64;
for i in 0..periods {
let y = 9.0 - i as f64 * h;
v.push(Coord { x: 0.0, y });
v.push(Coord { x: 0.05, y });
v.push(Coord {
x: 0.051,
y: y - h / 2.0,
});
v.push(Coord { x: 0.05, y: y - h });
v.push(Coord { x: 0.0, y: y - h });
}
v.push(v[0]);
Polygon::new(LineString::new(v), vec![])
}
#[test]
fn validation_skipped_above_vertex_cap_keeps_candidate() {
std::thread::Builder::new()
.stack_size(64 * 1024 * 1024)
.spawn(validation_skipped_above_vertex_cap_impl)
.unwrap()
.join()
.unwrap();
}
fn has_crossing_vertex(g: &Geometry<f64>) -> bool {
use geo::coords_iter::CoordsIter;
g.coords_iter()
.any(|c| (c.x - 5.0).abs() < 1e-6 && (c.y - 5.0).abs() < 1e-6)
}
fn validation_skipped_above_vertex_cap_impl() {
let poly = padded_bowtie(1_200);
assert!(poly.exterior().0.len() > MAX_VALIDATION_VERTS);
match simplify_polygon_impl(&poly, 0.01, CollapseMode::Drop, true) {
Simplified::Keep(g @ Geometry::Polygon(_)) => {
let Geometry::Polygon(ref out) = g else {
unreachable!()
};
assert!(
out.exterior().0.len() > MAX_VALIDATION_VERTS,
"RDP should keep the zigzag padding (got {} verts)",
out.exterior().0.len()
);
assert!(
out.exterior().0.len() < poly.exterior().0.len(),
"RDP should remove the sub-epsilon padding vertices"
);
assert!(
!has_crossing_vertex(&g),
"candidate must be kept verbatim, not repaired"
);
}
other => panic!("expected Keep(Polygon) above the cap, got {other:?}"),
}
}
#[test]
fn validation_exact_below_vertex_cap_still_repairs() {
let poly = padded_bowtie(40);
assert!(poly.exterior().0.len() <= MAX_VALIDATION_VERTS);
match simplify_polygon_impl(&poly, 0.01, CollapseMode::Drop, true) {
Simplified::Keep(g) => {
assert!(
has_crossing_vertex(&g),
"below the cap the bowtie must be repaired, got {g:?}"
);
}
other => panic!("expected Keep(repaired geometry), got {other:?}"),
}
}
}