use anyhow::{Result, bail};
use geo_types::{Coord, Geometry};
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum PointReductionStrategy {
None,
DropRate,
#[default]
MinDistance,
}
impl PointReductionStrategy {
pub fn parse(name: &str) -> Result<Self> {
match name {
"none" => Ok(Self::None),
"drop_rate" => Ok(Self::DropRate),
"min_distance" => Ok(Self::MinDistance),
other => bail!("unknown point_reduction strategy '{other}'; expected one of: none / drop_rate / min_distance"),
}
}
#[must_use]
pub fn variants() -> &'static [&'static str] {
&["none", "drop_rate", "min_distance"]
}
}
impl TryFrom<&str> for PointReductionStrategy {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self> {
Self::parse(value)
}
}
#[must_use]
pub fn apply_drop_rate(features: Vec<(usize, Geometry<f64>)>, keep_ratio: f64) -> Vec<(usize, Geometry<f64>)> {
if keep_ratio >= 1.0 {
return features;
}
if keep_ratio <= 0.0 {
return features.into_iter().filter(|(_, g)| !is_point(g)).collect();
}
features
.into_iter()
.filter(|(idx, g)| !is_point(g) || keep_for_index(*idx, keep_ratio))
.collect()
}
#[must_use]
pub fn apply_min_distance(features: Vec<(usize, Geometry<f64>)>, threshold: f64) -> Vec<(usize, Geometry<f64>)> {
if threshold <= 0.0 {
return features;
}
let cell_size = threshold;
let threshold_sq = threshold * threshold;
let mut grid: HashMap<(i64, i64), Vec<Coord<f64>>> = HashMap::new();
let mut kept = Vec::with_capacity(features.len());
for (idx, geometry) in features {
let Geometry::Point(point) = &geometry else {
kept.push((idx, geometry));
continue;
};
let coord = point.0;
if !coord.x.is_finite() || !coord.y.is_finite() {
continue;
}
#[allow(clippy::cast_possible_truncation)]
let cx = (coord.x / cell_size).floor() as i64;
#[allow(clippy::cast_possible_truncation)]
let cy = (coord.y / cell_size).floor() as i64;
let mut too_close = false;
'outer: for dx in -1..=1_i64 {
for dy in -1..=1_i64 {
if let Some(cell_pts) = grid.get(&(cx + dx, cy + dy)) {
for p in cell_pts {
let dsq = (p.x - coord.x).powi(2) + (p.y - coord.y).powi(2);
if dsq < threshold_sq {
too_close = true;
break 'outer;
}
}
}
}
}
if !too_close {
grid.entry((cx, cy)).or_default().push(coord);
kept.push((idx, geometry));
}
}
kept
}
fn is_point(g: &Geometry<f64>) -> bool {
matches!(g, Geometry::Point(_))
}
fn keep_for_index(index: usize, keep_ratio: f64) -> bool {
let h = splitmix64(index as u64);
#[allow(clippy::cast_precision_loss)]
let u = (h >> 11) as f64 / (1u64 << 53) as f64;
u < keep_ratio
}
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
x ^ (x >> 31)
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss)] mod tests {
use super::*;
use geo_types::{LineString, Point};
fn point_geom(lon: f64, lat: f64) -> Geometry<f64> {
Geometry::Point(Point::new(lon, lat))
}
#[test]
fn parse_strategy_names() -> Result<()> {
assert_eq!(PointReductionStrategy::parse("none")?, PointReductionStrategy::None);
assert_eq!(
PointReductionStrategy::parse("drop_rate")?,
PointReductionStrategy::DropRate
);
assert_eq!(
PointReductionStrategy::parse("min_distance")?,
PointReductionStrategy::MinDistance
);
assert!(PointReductionStrategy::parse("unknown").is_err());
Ok(())
}
#[test]
fn variants_match_try_from() {
for &v in PointReductionStrategy::variants() {
assert!(PointReductionStrategy::try_from(v).is_ok(), "{v} should parse");
}
}
#[test]
fn drop_rate_keeps_all_at_ratio_1() {
let features: Vec<(usize, Geometry<f64>)> = (0..100).map(|i| (i, point_geom(i as f64, 0.0))).collect();
let kept = apply_drop_rate(features, 1.0);
assert_eq!(kept.len(), 100);
}
#[test]
fn drop_rate_drops_all_at_ratio_0() {
let features: Vec<(usize, Geometry<f64>)> = (0..100).map(|i| (i, point_geom(i as f64, 0.0))).collect();
let kept = apply_drop_rate(features, 0.0);
assert_eq!(kept.len(), 0);
}
#[test]
fn drop_rate_approximate_keep_ratio() {
let features: Vec<(usize, Geometry<f64>)> = (0..10_000).map(|i| (i, point_geom(i as f64, 0.0))).collect();
let kept = apply_drop_rate(features, 0.5);
let ratio = kept.len() as f64 / 10_000.0;
assert!((ratio - 0.5).abs() < 0.05, "expected ~0.5, got {ratio}");
}
#[test]
fn drop_rate_is_deterministic() {
let make = || -> Vec<(usize, Geometry<f64>)> { (0..1000).map(|i| (i, point_geom(i as f64, 0.0))).collect() };
let kept1 = apply_drop_rate(make(), 0.3);
let kept2 = apply_drop_rate(make(), 0.3);
let indices1: Vec<usize> = kept1.iter().map(|(i, _)| *i).collect();
let indices2: Vec<usize> = kept2.iter().map(|(i, _)| *i).collect();
assert_eq!(indices1, indices2);
}
#[test]
fn drop_rate_passes_non_points_unchanged() {
let line: Geometry<f64> = Geometry::LineString(LineString::from(vec![[0.0, 0.0], [1.0, 1.0]]));
let features: Vec<(usize, Geometry<f64>)> = vec![(0, line.clone())];
let kept = apply_drop_rate(features, 0.0);
assert_eq!(kept.len(), 1);
}
#[test]
fn min_distance_drops_points_inside_threshold() {
let features = vec![
(0, point_geom(0.0, 0.0)),
(1, point_geom(5.0, 0.0)),
(2, point_geom(10.0, 0.0)),
];
let kept = apply_min_distance(features, 7.0);
assert_eq!(kept.len(), 2);
assert_eq!(kept[0].0, 0);
assert_eq!(kept[1].0, 2);
}
#[test]
fn min_distance_first_seen_wins() {
let features = vec![
(0, point_geom(0.0, 0.0)),
(1, point_geom(0.5, 0.0)), (2, point_geom(0.7, 0.0)), ];
let kept = apply_min_distance(features, 1.0);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].0, 0); }
#[test]
fn min_distance_threshold_zero_keeps_everything() {
let features = vec![
(0, point_geom(0.0, 0.0)),
(1, point_geom(0.0, 0.0)), ];
let kept = apply_min_distance(features, 0.0);
assert_eq!(kept.len(), 2);
}
#[test]
fn min_distance_passes_non_points_unchanged() {
let line: Geometry<f64> = Geometry::LineString(LineString::from(vec![[0.0, 0.0], [1.0, 1.0]]));
let features: Vec<(usize, Geometry<f64>)> = vec![(0, line.clone())];
let kept = apply_min_distance(features, 100.0);
assert_eq!(kept.len(), 1);
}
#[test]
fn min_distance_grid_neighbor_check() {
let features = vec![
(0, point_geom(9.99, 0.0)), (1, point_geom(10.01, 0.0)), ];
let kept = apply_min_distance(features, 10.0);
assert_eq!(kept.len(), 1);
}
}