use std::collections::HashMap;
use super::assign::{AssignConfig, AssignFeature, FeatureKind, Priority};
use super::level::Crs;
pub const POINT_COUNT_COLUMN: &str = "point_count";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccumulateOp {
Sum,
Max,
Min,
Mean,
}
impl AccumulateOp {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"sum" => Some(Self::Sum),
"max" => Some(Self::Max),
"min" => Some(Self::Min),
"mean" => Some(Self::Mean),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Sum => "sum",
Self::Max => "max",
Self::Min => "min",
Self::Mean => "mean",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AccumulateSpec {
pub column: String,
pub op: AccumulateOp,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClusterEntry {
pub point_count: i64,
pub aggregates: Vec<Option<f64>>,
}
pub type ClusterTables = Vec<HashMap<usize, ClusterEntry>>;
#[derive(Debug, Clone, Copy)]
struct AggState {
sum: f64,
min: f64,
max: f64,
count: u64,
}
impl AggState {
fn new() -> Self {
Self {
sum: 0.0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
count: 0,
}
}
fn add(&mut self, v: f64) {
self.sum += v;
self.min = self.min.min(v);
self.max = self.max.max(v);
self.count += 1;
}
fn finalize(&self, op: AccumulateOp) -> Option<f64> {
if self.count == 0 {
return None;
}
Some(match op {
AccumulateOp::Sum => self.sum,
AccumulateOp::Max => self.max,
AccumulateOp::Min => self.min,
AccumulateOp::Mean => self.sum / self.count as f64,
})
}
}
pub fn build_cluster_tables(
features: &[AssignFeature],
min_levels: &[u8],
level_gsds: &[f64],
config: &AssignConfig,
crs: Crs,
values: &[Vec<Option<f64>>],
ops: &[AccumulateOp],
) -> ClusterTables {
debug_assert_eq!(features.len(), min_levels.len());
debug_assert_eq!(values.len(), ops.len());
let num_levels = level_gsds.len();
let mut tables: ClusterTables = vec![HashMap::new(); num_levels];
if num_levels == 0 || features.is_empty() {
return tables;
}
let finest = num_levels - 1;
let point_pos: Vec<usize> = features
.iter()
.enumerate()
.filter(|(_, f)| f.kind == FeatureKind::Point)
.map(|(p, _)| p)
.collect();
if point_pos.is_empty() {
return tables;
}
let prio: Vec<Priority> = features
.iter()
.map(|f| Priority::new(f, config.sort_direction))
.collect();
if config.point_thinning == 0.0 {
log::warn!(
"--cluster with point thinning off: every point survives at every level, so every cluster is a singleton and every point_count is 1. The point_count column and the clustering provenance are still written. Set --point-thinning above 0 to actually cluster."
);
}
for level in 0..finest {
let cell_size = crs.meters_to_units(level_gsds[level]) * config.point_thinning;
if cell_size <= 0.0 || cell_size.is_nan() {
continue;
}
let cell = |pos: usize| -> (i64, i64) {
let (cx, cy) = features[pos].center();
(
(cx / cell_size).floor() as i64,
(cy / cell_size).floor() as i64,
)
};
let mut present: HashMap<(i64, i64), usize> = HashMap::new();
for &pos in &point_pos {
if min_levels[pos] as usize > level {
continue;
}
let key = cell(pos);
present
.entry(key)
.and_modify(|best| {
if prio[pos].beats(&prio[*best]) {
*best = pos;
}
})
.or_insert(pos);
}
if present.is_empty() {
continue;
}
let mut rep: Vec<usize> = Vec::with_capacity(point_pos.len());
let mut orphan_cells: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
for &pos in &point_pos {
if min_levels[pos] as usize <= level {
rep.push(pos); continue;
}
let key = cell(pos);
match present.get(&key) {
Some(&w) => rep.push(w),
None => {
orphan_cells.entry(key).or_default().push(pos);
rep.push(usize::MAX); }
}
}
if !orphan_cells.is_empty() {
let mut orphan_keys: Vec<(i64, i64)> = orphan_cells.keys().copied().collect();
orphan_keys.sort_unstable();
let mut resolved: HashMap<(i64, i64), usize> = HashMap::new();
for key in orphan_keys {
let w = nearest_present(key, &present, features, &prio, cell_size);
resolved.insert(key, w);
}
for (i, &pos) in point_pos.iter().enumerate() {
if rep[i] == usize::MAX {
rep[i] = resolved[&cell(pos)];
}
}
}
let mut acc: HashMap<usize, (i64, Vec<AggState>)> = HashMap::new();
for (i, &pos) in point_pos.iter().enumerate() {
let w = rep[i];
let entry = acc
.entry(w)
.or_insert_with(|| (0, vec![AggState::new(); ops.len()]));
entry.0 += 1;
for (s, vals) in values.iter().enumerate() {
if let Some(v) = vals[pos] {
entry.1[s].add(v);
}
}
}
let table = &mut tables[level];
for (w, (count, states)) in acc {
if count <= 1 {
continue;
}
table.insert(
features[w].index,
ClusterEntry {
point_count: count,
aggregates: states
.iter()
.zip(ops)
.map(|(st, &op)| st.finalize(op))
.collect(),
},
);
}
}
tables
}
pub fn verify_sum_invariant(
features: &[AssignFeature],
min_levels: &[u8],
tables: &ClusterTables,
) -> Result<(), String> {
debug_assert_eq!(features.len(), min_levels.len());
let total: i64 = features
.iter()
.filter(|f| f.kind == FeatureKind::Point)
.count() as i64;
if total == 0 {
return Ok(()); }
for (level, table) in tables.iter().enumerate() {
let mut sum = 0i64;
let mut point_rows = 0usize;
for (f, &ml) in features.iter().zip(min_levels) {
if f.kind != FeatureKind::Point || ml as usize > level {
continue;
}
point_rows += 1;
sum += table.get(&f.index).map_or(1, |e| e.point_count);
}
if point_rows == 0 {
return Err(format!(
"clustered level {level} has no surviving point row to absorb \
{total} source points (spec §12.1: a clustered level cannot \
thin points to zero while the source contains points)"
));
}
if sum != total {
return Err(format!(
"clustered level {level}: sum(point_count) over point rows = \
{sum}, expected the source point count {total} (spec §12.1 \
sum invariant)"
));
}
}
Ok(())
}
fn nearest_present(
cell_key: (i64, i64),
present: &HashMap<(i64, i64), usize>,
features: &[AssignFeature],
prio: &[Priority],
cell_size: f64,
) -> usize {
let center = (
(cell_key.0 as f64 + 0.5) * cell_size,
(cell_key.1 as f64 + 0.5) * cell_size,
);
let dist_sq = |pos: usize| -> f64 {
let (x, y) = features[pos].center();
let dx = x - center.0;
let dy = y - center.1;
dx * dx + dy * dy
};
let better = |a: usize, b: usize| -> bool {
let (da, db) = (dist_sq(a), dist_sq(b));
if da != db {
da < db
} else {
prio[a].beats(&prio[b])
}
};
let max_r = present
.keys()
.map(|&(x, y)| (x - cell_key.0).abs().max((y - cell_key.1).abs()))
.max()
.expect("present is non-empty");
let mut best: Option<usize> = None;
for r in 1..=max_r {
for (dx, dy) in ring_offsets(r) {
if let Some(&w) = present.get(&(cell_key.0 + dx, cell_key.1 + dy)) {
if best.is_none_or(|b| better(w, b)) {
best = Some(w);
}
}
}
if let Some(b) = best {
let rr = r + 1;
if rr <= max_r {
for (dx, dy) in ring_offsets(rr) {
if let Some(&w) = present.get(&(cell_key.0 + dx, cell_key.1 + dy)) {
if better(w, b) {
best = Some(w);
}
}
}
}
return best.expect("best set");
}
}
let mut all: Vec<usize> = present.values().copied().collect();
all.sort_unstable();
all.into_iter()
.reduce(|a, b| if better(b, a) { b } else { a })
.expect("present is non-empty")
}
fn ring_offsets(r: i64) -> Vec<(i64, i64)> {
debug_assert!(r >= 1);
let mut out = Vec::with_capacity((8 * r) as usize);
for dx in -r..=r {
out.push((dx, -r));
out.push((dx, r));
}
for dy in (-r + 1)..r {
out.push((-r, dy));
out.push((r, dy));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::overview::assign::{assign_levels, SortDirection};
fn gsd(z: u32) -> f64 {
40_075_016.69 / 1024.0 / 2f64.powi(z as i32)
}
fn point(index: usize, x: f64, y: f64) -> AssignFeature {
AssignFeature {
index,
bbox: [x, y, x, y],
kind: FeatureKind::Point,
sort_key: None,
entry_level: None,
}
}
fn level_count_sum(
features: &[AssignFeature],
min_levels: &[u8],
table: &HashMap<usize, ClusterEntry>,
level: u8,
) -> i64 {
features
.iter()
.zip(min_levels)
.filter(|(f, &ml)| f.kind == FeatureKind::Point && ml <= level)
.map(|(f, _)| table.get(&f.index).map_or(1, |e| e.point_count))
.sum()
}
#[test]
fn accumulate_op_parse_and_names() {
assert_eq!(AccumulateOp::parse("sum"), Some(AccumulateOp::Sum));
assert_eq!(AccumulateOp::parse("MAX"), Some(AccumulateOp::Max));
assert_eq!(AccumulateOp::parse("Min"), Some(AccumulateOp::Min));
assert_eq!(AccumulateOp::parse("mean"), Some(AccumulateOp::Mean));
assert_eq!(AccumulateOp::parse("median"), None);
assert_eq!(AccumulateOp::Mean.as_str(), "mean");
}
#[test]
fn ten_points_two_cells_counts_six_and_four() {
let mut feats: Vec<AssignFeature> = Vec::new();
for i in 0..6 {
feats.push(point(i, i as f64 * 200.0, 0.0));
}
for j in 0..4 {
feats.push(point(6 + j, 1.0e7 + j as f64 * 200.0, 0.0));
}
let gsds = [gsd(2), gsd(10)];
let cfg = AssignConfig::default();
let assignment = assign_levels(&feats, &gsds, &cfg, Crs::Epsg3857);
let min_levels: Vec<u8> = assignment.assignments.iter().map(|a| a.min_level).collect();
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
assert_eq!(tables.len(), 2);
let present0: Vec<usize> = (0..feats.len()).filter(|&i| min_levels[i] == 0).collect();
assert_eq!(present0.len(), 2, "one winner per clump at level 0");
let mut counts: Vec<i64> = present0
.iter()
.map(|&i| tables[0].get(&feats[i].index).map_or(1, |e| e.point_count))
.collect();
counts.sort_unstable();
assert_eq!(counts, vec![4, 6]);
assert_eq!(level_count_sum(&feats, &min_levels, &tables[0], 0), 10);
assert!(tables[1].is_empty(), "canonical level has no clusters");
assert_eq!(level_count_sum(&feats, &min_levels, &tables[1], 1), 10);
}
#[test]
fn aggregation_ops_including_nulls() {
let feats: Vec<AssignFeature> = (0..4).map(|i| point(i, i as f64 * 100.0, 0.0)).collect();
let gsds = [gsd(2), gsd(10)];
let cfg = AssignConfig::default();
let assignment = assign_levels(&feats, &gsds, &cfg, Crs::Epsg3857);
let min_levels: Vec<u8> = assignment.assignments.iter().map(|a| a.min_level).collect();
let vals = vec![vec![Some(10.0), Some(30.0), None, Some(20.0)]; 4];
let ops = [
AccumulateOp::Sum,
AccumulateOp::Max,
AccumulateOp::Min,
AccumulateOp::Mean,
];
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &vals, &ops);
assert_eq!(tables[0].len(), 1, "one cluster at level 0");
let entry = tables[0].values().next().unwrap();
assert_eq!(entry.point_count, 4);
assert_eq!(
entry.aggregates,
vec![Some(60.0), Some(30.0), Some(10.0), Some(20.0)]
);
}
#[test]
fn all_null_values_yield_none_aggregate() {
let feats: Vec<AssignFeature> = (0..3).map(|i| point(i, i as f64 * 100.0, 0.0)).collect();
let gsds = [gsd(2), gsd(10)];
let cfg = AssignConfig::default();
let assignment = assign_levels(&feats, &gsds, &cfg, Crs::Epsg3857);
let min_levels: Vec<u8> = assignment.assignments.iter().map(|a| a.min_level).collect();
let vals = vec![vec![None, None, None]];
let tables = build_cluster_tables(
&feats,
&min_levels,
&gsds,
&cfg,
Crs::Epsg3857,
&vals,
&[AccumulateOp::Sum],
);
let entry = tables[0].values().next().unwrap();
assert_eq!(entry.point_count, 3);
assert_eq!(entry.aggregates, vec![None]);
}
#[test]
fn mean_is_exact_across_levels_not_mean_of_means() {
let mut feats = vec![
point(0, 0.0, 0.0),
point(1, 100.0, 0.0),
point(2, 200.0, 0.0),
point(3, 5000.0, 0.0), ];
feats[0].sort_key = Some(1.0);
let gsds = [gsd(2), gsd(6), gsd(12)];
let cfg = AssignConfig::default();
let assignment = assign_levels(&feats, &gsds, &cfg, Crs::Epsg3857);
let min_levels: Vec<u8> = assignment.assignments.iter().map(|a| a.min_level).collect();
let vals = vec![vec![Some(0.0), Some(0.0), Some(0.0), Some(8.0)]];
let tables = build_cluster_tables(
&feats,
&min_levels,
&gsds,
&cfg,
Crs::Epsg3857,
&vals,
&[AccumulateOp::Mean],
);
let e0 = tables[0].get(&0).expect("feature 0 wins level 0");
assert_eq!(e0.point_count, 4);
assert_eq!(e0.aggregates, vec![Some(2.0)]);
let sum1 = level_count_sum(&feats, &min_levels, &tables[1], 1);
assert_eq!(sum1, 4, "level-1 counts partition the source set");
let e1 = tables[1]
.values()
.find(|e| e.point_count == 3)
.expect("3-point cluster at level 1");
assert_eq!(e1.aggregates, vec![Some(0.0)]);
}
#[test]
fn per_level_absorption_partitions_at_every_level() {
let mut feats = Vec::new();
for c in 0..3 {
for i in 0..4 {
feats.push(point(c * 4 + i, c as f64 * 5000.0 + i as f64 * 50.0, 0.0));
}
}
let gsds = [gsd(2), gsd(6), gsd(12)];
let cfg = AssignConfig::default();
let assignment = assign_levels(&feats, &gsds, &cfg, Crs::Epsg3857);
let min_levels: Vec<u8> = assignment.assignments.iter().map(|a| a.min_level).collect();
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
let l0_winners: Vec<usize> = (0..feats.len()).filter(|&i| min_levels[i] == 0).collect();
assert_eq!(l0_winners.len(), 1);
assert_eq!(tables[0][&l0_winners[0]].point_count, 12);
assert_eq!(level_count_sum(&feats, &min_levels, &tables[1], 1), 12);
let present1: Vec<usize> = (0..feats.len()).filter(|&i| min_levels[i] <= 1).collect();
assert_eq!(present1.len(), 3, "one winner per clump at level 1");
for &w in &present1 {
assert_eq!(
tables[1].get(&w).map_or(1, |e| e.point_count),
4,
"each level-1 winner holds its own clump"
);
}
assert!(tables[2].is_empty());
assert_eq!(level_count_sum(&feats, &min_levels, &tables[2], 2), 12);
}
#[test]
fn orphan_cell_attaches_to_nearest_present_winner() {
let cell = 4.0 * gsd(2); let feats = vec![
point(0, 0.5 * cell, 0.0),
point(1, 1.5 * cell, 0.0), point(2, 4.5 * cell, 0.0),
];
let min_levels = vec![0u8, 1, 0];
let gsds = [gsd(2), gsd(10)];
let cfg = AssignConfig::default();
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
assert_eq!(tables[0].get(&0).map(|e| e.point_count), Some(2));
assert!(!tables[0].contains_key(&2), "far winner stays a singleton");
assert_eq!(level_count_sum(&feats, &min_levels, &tables[0], 0), 3);
}
#[test]
fn non_point_features_are_ignored() {
let mut feats = vec![
point(0, 0.0, 0.0),
point(1, 100.0, 0.0),
AssignFeature {
index: 2,
bbox: [0.0, 0.0, 50_000.0, 50_000.0],
kind: FeatureKind::Polygon,
sort_key: None,
entry_level: None,
},
AssignFeature {
index: 3,
bbox: [0.0, 0.0, 60_000.0, 60_000.0],
kind: FeatureKind::Line,
sort_key: None,
entry_level: None,
},
];
feats[0].sort_key = Some(1.0);
let gsds = [gsd(2), gsd(10)];
let cfg = AssignConfig::default();
let assignment = assign_levels(&feats, &gsds, &cfg, Crs::Epsg3857);
let min_levels: Vec<u8> = assignment.assignments.iter().map(|a| a.min_level).collect();
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
assert_eq!(tables[0].len(), 1);
assert_eq!(tables[0].get(&0).map(|e| e.point_count), Some(2));
assert!(!tables[0].contains_key(&2));
assert!(!tables[0].contains_key(&3));
}
#[test]
fn representative_matches_cell_winner_priority() {
let mut feats: Vec<AssignFeature> =
(0..5).map(|i| point(i, i as f64 * 10.0, 0.0)).collect();
feats[3].sort_key = Some(99.0); let gsds = [gsd(2), gsd(10)];
let cfg = AssignConfig {
sort_direction: SortDirection::Desc,
..Default::default()
};
let assignment = assign_levels(&feats, &gsds, &cfg, Crs::Epsg3857);
let min_levels: Vec<u8> = assignment.assignments.iter().map(|a| a.min_level).collect();
assert_eq!(min_levels[3], 0, "sort-key holder wins the coarse cell");
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
assert_eq!(tables[0].get(&3).map(|e| e.point_count), Some(5));
}
#[test]
fn empty_inputs_and_no_points_are_noops() {
let gsds = [gsd(2), gsd(6)];
let cfg = AssignConfig::default();
let t = build_cluster_tables(&[], &[], &gsds, &cfg, Crs::Epsg3857, &[], &[]);
assert!(t.iter().all(|m| m.is_empty()));
let poly = AssignFeature {
index: 0,
bbox: [0.0, 0.0, 50_000.0, 50_000.0],
kind: FeatureKind::Polygon,
sort_key: None,
entry_level: None,
};
let t = build_cluster_tables(&[poly], &[0], &gsds, &cfg, Crs::Epsg3857, &[], &[]);
assert!(t.iter().all(|m| m.is_empty()));
}
#[test]
fn verify_sum_invariant_pass_orphan_and_zero_survivor() {
let cell = 4.0 * gsd(2);
let feats = vec![
point(0, 0.5 * cell, 0.0),
point(1, 1.5 * cell, 0.0), point(2, 4.5 * cell, 0.0),
];
let gsds = [gsd(2), gsd(10)];
let cfg = AssignConfig::default();
let min_levels = vec![0u8, 1, 0];
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
verify_sum_invariant(&feats, &min_levels, &tables).unwrap();
let all_deferred = vec![1u8, 1, 1];
let tables =
build_cluster_tables(&feats, &all_deferred, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
let err = verify_sum_invariant(&feats, &all_deferred, &tables).unwrap_err();
assert!(
err.contains("no surviving point row"),
"unexpected message: {err}"
);
let min_levels = vec![0u8, 1, 0];
let mut tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
tables[0].clear(); let err = verify_sum_invariant(&feats, &min_levels, &tables).unwrap_err();
assert!(err.contains("sum invariant"), "unexpected message: {err}");
}
#[test]
fn verify_sum_invariant_no_points_is_ok() {
let poly = AssignFeature {
index: 0,
bbox: [0.0, 0.0, 50_000.0, 50_000.0],
kind: FeatureKind::Polygon,
sort_key: None,
entry_level: None,
};
let gsds = [gsd(2), gsd(6)];
let cfg = AssignConfig::default();
let feats = vec![poly];
let min_levels = vec![0u8];
let tables =
build_cluster_tables(&feats, &min_levels, &gsds, &cfg, Crs::Epsg3857, &[], &[]);
verify_sum_invariant(&feats, &min_levels, &tables).unwrap();
}
#[test]
fn ring_offsets_cover_ring_exactly() {
for r in 1..=3i64 {
let ring = ring_offsets(r);
assert_eq!(ring.len() as i64, 8 * r);
assert!(ring.iter().all(|&(x, y)| x.abs().max(y.abs()) == r));
let mut sorted = ring.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), ring.len(), "no duplicate cells");
}
}
}