use crate::error::{AlgorithmError, Result};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ContourPoint {
pub x: f64,
pub y: f64,
}
impl ContourPoint {
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone)]
pub struct ContourLine {
pub level: f64,
pub points: Vec<ContourPoint>,
pub is_closed: bool,
}
#[derive(Debug, Clone)]
pub struct ContourConfig {
pub interval: f64,
pub base: f64,
pub nodata: Option<f64>,
}
impl ContourConfig {
pub fn new(interval: f64) -> Result<Self> {
if interval <= 0.0 || !interval.is_finite() {
return Err(AlgorithmError::InvalidParameter {
parameter: "interval",
message: format!("interval must be positive, got {interval}"),
});
}
Ok(Self {
interval,
base: 0.0,
nodata: None,
})
}
#[must_use]
pub fn with_base(mut self, base: f64) -> Self {
self.base = base;
self
}
#[must_use]
pub fn with_nodata(mut self, nodata: f64) -> Self {
self.nodata = Some(nodata);
self
}
fn compute_levels(&self, min_val: f64, max_val: f64) -> Vec<f64> {
if min_val > max_val || !min_val.is_finite() || !max_val.is_finite() {
return vec![];
}
let start = ((min_val - self.base) / self.interval).ceil();
let end = ((max_val - self.base) / self.interval).floor();
let start_i = start as i64;
let end_i = end as i64;
let mut levels = Vec::new();
for i in start_i..=end_i {
let level = self.base + (i as f64) * self.interval;
if level > min_val && level < max_val {
levels.push(level);
}
}
levels
}
}
#[derive(Debug, Clone, Copy)]
struct Segment {
p0: ContourPoint,
p1: ContourPoint,
}
#[inline]
fn lerp_frac(v0: f64, v1: f64, level: f64) -> f64 {
let denom = v1 - v0;
if denom.abs() < 1e-15 {
0.5
} else {
(level - v0) / denom
}
}
fn cell_segments(
col: usize,
row: usize,
tl: f64,
tr: f64,
bl: f64,
br: f64,
level: f64,
) -> Vec<Segment> {
let case = ((tl >= level) as u8) << 3
| ((tr >= level) as u8) << 2
| ((bl >= level) as u8) << 1
| (br >= level) as u8;
let top = || {
let t = lerp_frac(tl, tr, level);
ContourPoint::new(col as f64 + t, row as f64)
};
let bottom = || {
let t = lerp_frac(bl, br, level);
ContourPoint::new(col as f64 + t, row as f64 + 1.0)
};
let left = || {
let t = lerp_frac(tl, bl, level);
ContourPoint::new(col as f64, row as f64 + t)
};
let right = || {
let t = lerp_frac(tr, br, level);
ContourPoint::new(col as f64 + 1.0, row as f64 + t)
};
match case {
0 | 15 => vec![], 1 => vec![Segment {
p0: bottom(),
p1: right(),
}],
2 => vec![Segment {
p0: left(),
p1: bottom(),
}],
3 => vec![Segment {
p0: left(),
p1: right(),
}],
4 => vec![Segment {
p0: top(),
p1: right(),
}],
5 => {
let center = (tl + tr + bl + br) * 0.25;
if center >= level {
vec![
Segment {
p0: top(),
p1: right(),
},
Segment {
p0: left(),
p1: bottom(),
},
]
} else {
vec![
Segment {
p0: top(),
p1: left(),
},
Segment {
p0: bottom(),
p1: right(),
},
]
}
}
6 => vec![Segment {
p0: top(),
p1: bottom(),
}],
7 => vec![Segment {
p0: top(),
p1: left(),
}],
8 => vec![Segment {
p0: top(),
p1: left(),
}],
9 => vec![Segment {
p0: top(),
p1: bottom(),
}],
10 => {
let center = (tl + tr + bl + br) * 0.25;
if center >= level {
vec![
Segment {
p0: top(),
p1: left(),
},
Segment {
p0: bottom(),
p1: right(),
},
]
} else {
vec![
Segment {
p0: top(),
p1: right(),
},
Segment {
p0: left(),
p1: bottom(),
},
]
}
}
11 => vec![Segment {
p0: top(),
p1: right(),
}],
12 => vec![Segment {
p0: left(),
p1: right(),
}],
13 => vec![Segment {
p0: left(),
p1: bottom(),
}],
14 => vec![Segment {
p0: bottom(),
p1: right(),
}],
_ => vec![], }
}
#[inline]
fn is_nodata(val: f64, nodata: Option<f64>) -> bool {
if !val.is_finite() {
return true;
}
if let Some(nd) = nodata {
(val - nd).abs() < 1e-10
} else {
false
}
}
fn chain_segments(segments: &[Segment]) -> Vec<(Vec<ContourPoint>, bool)> {
if segments.is_empty() {
return vec![];
}
let eps = 1e-9;
let points_eq = |a: &ContourPoint, b: &ContourPoint| -> bool {
(a.x - b.x).abs() < eps && (a.y - b.y).abs() < eps
};
let mut chains: Vec<Vec<ContourPoint>> = Vec::new();
for seg in segments {
let mut attached = false;
for chain in chains.iter_mut() {
let first = chain[0];
let last = chain[chain.len() - 1];
if points_eq(&last, &seg.p0) {
chain.push(seg.p1);
attached = true;
break;
} else if points_eq(&last, &seg.p1) {
chain.push(seg.p0);
attached = true;
break;
} else if points_eq(&first, &seg.p1) {
chain.insert(0, seg.p0);
attached = true;
break;
} else if points_eq(&first, &seg.p0) {
chain.insert(0, seg.p1);
attached = true;
break;
}
}
if !attached {
chains.push(vec![seg.p0, seg.p1]);
}
}
let mut merged = true;
while merged {
merged = false;
let mut i = 0;
while i < chains.len() {
let mut j = i + 1;
while j < chains.len() {
let i_first = chains[i][0];
let i_last = chains[i][chains[i].len() - 1];
let j_first = chains[j][0];
let j_last = chains[j][chains[j].len() - 1];
if points_eq(&i_last, &j_first) {
let mut taken = chains.remove(j);
taken.remove(0); chains[i].append(&mut taken);
merged = true;
continue; } else if points_eq(&i_first, &j_last) {
let mut taken = chains.remove(j);
taken.pop(); taken.append(&mut chains[i]);
chains[i] = taken;
merged = true;
continue;
} else if points_eq(&i_last, &j_last) {
let mut taken = chains.remove(j);
taken.reverse();
taken.remove(0);
chains[i].append(&mut taken);
merged = true;
continue;
} else if points_eq(&i_first, &j_first) {
let mut taken = chains.remove(j);
taken.reverse();
taken.pop();
taken.append(&mut chains[i]);
chains[i] = taken;
merged = true;
continue;
}
j += 1;
}
i += 1;
}
}
chains
.into_iter()
.map(|pts: Vec<ContourPoint>| {
let closed = pts.len() >= 3 && points_eq(&pts[0], &pts[pts.len() - 1]);
(pts, closed)
})
.collect()
}
pub fn generate_contours(
data: &[f64],
width: usize,
height: usize,
config: &ContourConfig,
) -> Result<Vec<ContourLine>> {
if width == 0 || height == 0 {
return Ok(vec![]);
}
if data.len() != width * height {
return Err(AlgorithmError::InvalidDimensions {
message: "data length must equal width * height",
actual: data.len(),
expected: width * height,
});
}
if width < 2 || height < 2 {
return Ok(vec![]);
}
let mut min_val = f64::INFINITY;
let mut max_val = f64::NEG_INFINITY;
for &v in data {
if is_nodata(v, config.nodata) {
continue;
}
if v < min_val {
min_val = v;
}
if v > max_val {
max_val = v;
}
}
if !min_val.is_finite() || !max_val.is_finite() {
return Ok(vec![]);
}
let levels = config.compute_levels(min_val, max_val);
if levels.is_empty() {
return Ok(vec![]);
}
let mut result: BTreeMap<i64, Vec<Segment>> = BTreeMap::new();
for &level in &levels {
let key = (level * 1e10) as i64;
let segments = result.entry(key).or_default();
for row in 0..height - 1 {
for col in 0..width - 1 {
let tl = data[row * width + col];
let tr = data[row * width + col + 1];
let bl = data[(row + 1) * width + col];
let br = data[(row + 1) * width + col + 1];
if is_nodata(tl, config.nodata)
|| is_nodata(tr, config.nodata)
|| is_nodata(bl, config.nodata)
|| is_nodata(br, config.nodata)
{
continue;
}
let cell_segs = cell_segments(col, row, tl, tr, bl, br, level);
segments.extend_from_slice(&cell_segs);
}
}
}
let mut contour_lines: Vec<ContourLine> = Vec::new();
for level in levels.iter() {
let key = (*level * 1e10) as i64;
if let Some(segments) = result.get(&key) {
let chains: Vec<(Vec<ContourPoint>, bool)> = chain_segments(segments);
for (points, is_closed) in chains {
if points.len() >= 2 {
contour_lines.push(ContourLine {
level: *level,
points,
is_closed,
});
}
}
}
}
Ok(contour_lines)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contour_config_valid() {
let config = ContourConfig::new(10.0);
assert!(config.is_ok());
let config = config.expect("should be ok");
assert!((config.interval - 10.0).abs() < f64::EPSILON);
assert!((config.base - 0.0).abs() < f64::EPSILON);
assert!(config.nodata.is_none());
}
#[test]
fn test_contour_config_zero_interval_error() {
let result = ContourConfig::new(0.0);
assert!(result.is_err());
}
#[test]
fn test_contour_config_negative_interval_error() {
let result = ContourConfig::new(-5.0);
assert!(result.is_err());
}
#[test]
fn test_compute_levels() {
let config = ContourConfig::new(10.0).expect("valid config");
let levels = config.compute_levels(5.0, 35.0);
assert_eq!(levels, vec![10.0, 20.0, 30.0]);
}
#[test]
fn test_contour_flat_grid() {
let data = vec![100.0; 16];
let config = ContourConfig::new(10.0).expect("valid config");
let contours = generate_contours(&data, 4, 4, &config).expect("should succeed");
assert!(contours.is_empty());
}
#[test]
fn test_contour_simple_slope() {
let data = vec![
0.0, 10.0, 20.0, 30.0, 0.0, 10.0, 20.0, 30.0, 0.0, 10.0, 20.0, 30.0, 0.0, 10.0, 20.0,
30.0,
];
let config = ContourConfig::new(15.0).expect("valid config");
let contours = generate_contours(&data, 4, 4, &config).expect("should succeed");
assert!(!contours.is_empty());
let levels: Vec<f64> = contours.iter().map(|c| c.level).collect();
assert!(levels.contains(&15.0));
for c in &contours {
assert!(c.points.len() >= 2);
}
}
#[test]
fn test_contour_single_level() {
let data = vec![0.0, 5.0, 10.0, 0.0, 5.0, 10.0, 0.0, 5.0, 10.0];
let config = ContourConfig::new(3.0).expect("valid config");
let contours = generate_contours(&data, 3, 3, &config).expect("should succeed");
let levels: Vec<f64> = contours.iter().map(|c| c.level).collect();
assert!(levels.contains(&3.0));
assert!(levels.contains(&6.0));
assert!(levels.contains(&9.0));
}
#[test]
fn test_contour_nodata_handling() {
let nodata = -9999.0;
let data = vec![0.0, 5.0, 10.0, 0.0, nodata, 10.0, 0.0, 5.0, 10.0];
let config = ContourConfig::new(4.0)
.expect("valid config")
.with_nodata(nodata);
let contours = generate_contours(&data, 3, 3, &config).expect("should succeed");
assert!(contours.is_empty());
}
#[test]
fn test_contour_closed_loop() {
let data = vec![
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.0, 5.0, 5.0, 0.0, 0.0, 5.0, 20.0, 5.0, 0.0, 0.0, 5.0,
5.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
];
let config = ContourConfig::new(10.0).expect("valid config");
let contours = generate_contours(&data, 5, 5, &config).expect("should succeed");
assert!(!contours.is_empty());
let at_10: Vec<&ContourLine> = contours
.iter()
.filter(|c| (c.level - 10.0).abs() < 1e-10)
.collect();
assert!(!at_10.is_empty());
let has_closed = at_10.iter().any(|c| c.is_closed);
assert!(has_closed, "expected a closed contour around the hill");
}
#[test]
fn test_contour_saddle_point() {
let data = vec![10.0, 0.0, 10.0, 0.0, 10.0, 0.0, 10.0, 0.0, 10.0];
let config = ContourConfig::new(4.0).expect("valid config");
let contours = generate_contours(&data, 3, 3, &config).expect("should succeed");
assert!(!contours.is_empty());
for c in &contours {
assert!(c.points.len() >= 2);
for p in &c.points {
assert!(p.x.is_finite() && p.y.is_finite());
}
}
}
#[test]
fn test_contour_empty_grid() {
let config = ContourConfig::new(10.0).expect("valid config");
let contours = generate_contours(&[], 0, 0, &config).expect("should succeed");
assert!(contours.is_empty());
let contours = generate_contours(&[], 0, 5, &config).expect("should succeed");
assert!(contours.is_empty());
let contours = generate_contours(&[], 5, 0, &config).expect("should succeed");
assert!(contours.is_empty());
}
#[test]
fn test_contour_wrong_data_size() {
let data = vec![1.0, 2.0, 3.0]; let config = ContourConfig::new(10.0).expect("valid config");
let result = generate_contours(&data, 2, 2, &config);
assert!(result.is_err());
}
#[test]
fn test_compute_levels_with_base() {
let config = ContourConfig::new(10.0)
.expect("valid config")
.with_base(5.0);
let levels = config.compute_levels(0.0, 30.0);
assert!(levels.contains(&15.0));
assert!(levels.contains(&25.0));
}
#[test]
fn test_contour_config_nan_interval() {
let result = ContourConfig::new(f64::NAN);
assert!(result.is_err());
}
#[test]
fn test_contour_config_inf_interval() {
let result = ContourConfig::new(f64::INFINITY);
assert!(result.is_err());
}
}