use crate::error::{AlgorithmError, Result};
use oxigdal_core::buffer::RasterBuffer;
use oxigdal_core::types::RasterDataType;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::collections::VecDeque;
const DX: [i64; 8] = [1, 1, 0, -1, -1, -1, 0, 1];
const DY: [i64; 8] = [0, 1, 1, 1, 0, -1, -1, -1];
#[inline]
fn in_bounds(x: i64, y: i64, w: u64, h: u64) -> bool {
x >= 0 && y >= 0 && (x as u64) < w && (y as u64) < h
}
fn get_neighbors(x: u64, y: u64, w: u64, h: u64) -> Vec<(u64, u64)> {
let mut out = Vec::with_capacity(8);
for i in 0..8 {
let nx = x as i64 + DX[i];
let ny = y as i64 + DY[i];
if in_bounds(nx, ny, w, h) {
out.push((nx as u64, ny as u64));
}
}
out
}
#[derive(Copy, Clone, PartialEq)]
struct PqCell {
x: u64,
y: u64,
elevation: f64,
}
impl Eq for PqCell {}
impl PartialOrd for PqCell {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PqCell {
fn cmp(&self, other: &Self) -> Ordering {
match other.elevation.partial_cmp(&self.elevation) {
Some(Ordering::Equal) | None => {}
Some(ord) => return ord,
}
match self.y.cmp(&other.y) {
Ordering::Equal => self.x.cmp(&other.x),
ord => ord,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillMethod {
WangLiu,
PlanchonDarboux,
}
#[derive(Debug, Clone)]
pub struct FillSinksConfig {
pub method: FillMethod,
pub epsilon: f64,
pub max_fill_depth: f64,
pub breach_first: bool,
pub max_breach_length: usize,
}
impl Default for FillSinksConfig {
fn default() -> Self {
Self {
method: FillMethod::WangLiu,
epsilon: 1e-5,
max_fill_depth: f64::INFINITY,
breach_first: false,
max_breach_length: 50,
}
}
}
pub fn fill_sinks(dem: &RasterBuffer, epsilon: f64) -> Result<RasterBuffer> {
let cfg = FillSinksConfig {
epsilon,
..Default::default()
};
fill_sinks_cfg(dem, &cfg)
}
pub fn fill_sinks_cfg(dem: &RasterBuffer, cfg: &FillSinksConfig) -> Result<RasterBuffer> {
let mut working = dem.clone();
if cfg.breach_first {
working = breach_depressions(&working, cfg.max_breach_length)?;
}
match cfg.method {
FillMethod::WangLiu => wang_liu_fill(&working, cfg.epsilon, cfg.max_fill_depth),
FillMethod::PlanchonDarboux => {
planchon_darboux_fill(&working, cfg.epsilon, cfg.max_fill_depth)
}
}
}
pub fn identify_sinks(dem: &RasterBuffer) -> Result<RasterBuffer> {
let w = dem.width();
let h = dem.height();
let mut sinks = RasterBuffer::zeros(w, h, RasterDataType::UInt8);
for y in 1..(h.saturating_sub(1)) {
for x in 1..(w.saturating_sub(1)) {
let center = dem.get_pixel(x, y).map_err(AlgorithmError::Core)?;
let neighbors = get_neighbors(x, y, w, h);
let is_sink = neighbors
.iter()
.all(|&(nx, ny)| dem.get_pixel(nx, ny).map(|n| center <= n).unwrap_or(false));
if is_sink {
sinks.set_pixel(x, y, 1.0).map_err(AlgorithmError::Core)?;
}
}
}
Ok(sinks)
}
pub fn compute_fill_depth(dem: &RasterBuffer, epsilon: f64) -> Result<RasterBuffer> {
let filled = fill_sinks(dem, epsilon)?;
let w = dem.width();
let h = dem.height();
let mut depth = RasterBuffer::zeros(w, h, RasterDataType::Float64);
for y in 0..h {
for x in 0..w {
let orig = dem.get_pixel(x, y).map_err(AlgorithmError::Core)?;
let fill = filled.get_pixel(x, y).map_err(AlgorithmError::Core)?;
let d = (fill - orig).max(0.0);
depth.set_pixel(x, y, d).map_err(AlgorithmError::Core)?;
}
}
Ok(depth)
}
fn wang_liu_fill(dem: &RasterBuffer, epsilon: f64, max_depth: f64) -> Result<RasterBuffer> {
let w = dem.width();
let h = dem.height();
let mut filled = dem.clone();
let n = (w * h) as usize;
let mut visited = vec![false; n];
let mut pq = BinaryHeap::new();
for y in 0..h {
for x in 0..w {
if x == 0 || x == w - 1 || y == 0 || y == h - 1 {
let elev = dem.get_pixel(x, y).map_err(AlgorithmError::Core)?;
pq.push(PqCell {
x,
y,
elevation: elev,
});
visited[(y * w + x) as usize] = true;
}
}
}
while let Some(cell) = pq.pop() {
for i in 0..8 {
let nx = cell.x as i64 + DX[i];
let ny = cell.y as i64 + DY[i];
if !in_bounds(nx, ny, w, h) {
continue;
}
let nxu = nx as u64;
let nyu = ny as u64;
let nidx = (nyu * w + nxu) as usize;
if visited[nidx] {
continue;
}
visited[nidx] = true;
let original = dem.get_pixel(nxu, nyu).map_err(AlgorithmError::Core)?;
let neighbour_elev = filled.get_pixel(nxu, nyu).map_err(AlgorithmError::Core)?;
let min_drain = cell.elevation + epsilon;
if neighbour_elev < min_drain {
let fill_amount = min_drain - original;
let new_elev = if fill_amount <= max_depth {
min_drain
} else {
neighbour_elev
};
filled
.set_pixel(nxu, nyu, new_elev)
.map_err(AlgorithmError::Core)?;
pq.push(PqCell {
x: nxu,
y: nyu,
elevation: new_elev,
});
} else {
pq.push(PqCell {
x: nxu,
y: nyu,
elevation: neighbour_elev,
});
}
}
}
Ok(filled)
}
fn planchon_darboux_fill(dem: &RasterBuffer, epsilon: f64, max_depth: f64) -> Result<RasterBuffer> {
let w = dem.width();
let h = dem.height();
let n = (w * h) as usize;
let mut orig = vec![0.0_f64; n];
let mut filled = vec![0.0_f64; n];
let big = f64::MAX / 2.0;
for y in 0..h {
for x in 0..w {
let idx = (y * w + x) as usize;
let elev = dem.get_pixel(x, y).map_err(AlgorithmError::Core)?;
orig[idx] = elev;
if x == 0 || x == w - 1 || y == 0 || y == h - 1 {
filled[idx] = elev; } else {
filled[idx] = big; }
}
}
let mut changed = true;
while changed {
changed = false;
for y in 1..(h - 1) {
for x in 1..(w - 1) {
let idx = (y * w + x) as usize;
if (filled[idx] - orig[idx]).abs() < f64::EPSILON {
continue; }
for i in 0..8 {
let nx = x as i64 + DX[i];
let ny = y as i64 + DY[i];
if !in_bounds(nx, ny, w, h) {
continue;
}
let nidx = (ny as u64 * w + nx as u64) as usize;
if orig[idx] >= filled[nidx] + epsilon {
if filled[idx] > orig[idx] {
filled[idx] = orig[idx];
changed = true;
}
}
else {
let candidate = filled[nidx] + epsilon;
if candidate < filled[idx] {
let depth = candidate - orig[idx];
if depth <= max_depth {
filled[idx] = candidate;
changed = true;
}
}
}
}
}
}
}
let mut result = dem.clone();
for y in 0..h {
for x in 0..w {
let idx = (y * w + x) as usize;
result
.set_pixel(x, y, filled[idx])
.map_err(AlgorithmError::Core)?;
}
}
Ok(result)
}
pub fn breach_depressions(dem: &RasterBuffer, max_path_length: usize) -> Result<RasterBuffer> {
let w = dem.width();
let h = dem.height();
let mut result = dem.clone();
let sink_mask = identify_sinks(dem)?;
for y in 1..(h.saturating_sub(1)) {
for x in 1..(w.saturating_sub(1)) {
let is_sink = sink_mask.get_pixel(x, y).map_err(AlgorithmError::Core)?;
if is_sink < 0.5 {
continue;
}
if let Some(path) = find_breach_path(&result, x, y, max_path_length)? {
apply_breach_path(&mut result, &path)?;
}
}
}
Ok(result)
}
fn find_breach_path(
dem: &RasterBuffer,
sx: u64,
sy: u64,
max_len: usize,
) -> Result<Option<Vec<(u64, u64)>>> {
let w = dem.width();
let h = dem.height();
let n = (w * h) as usize;
let sink_elev = dem.get_pixel(sx, sy).map_err(AlgorithmError::Core)?;
let mut visited = vec![false; n];
let mut parent: Vec<Option<usize>> = vec![None; n];
let mut dist = vec![0usize; n];
let start = (sy * w + sx) as usize;
visited[start] = true;
let mut queue = VecDeque::new();
queue.push_back((sx, sy));
while let Some((cx, cy)) = queue.pop_front() {
let cidx = (cy * w + cx) as usize;
if dist[cidx] >= max_len {
continue;
}
for i in 0..8 {
let nx = cx as i64 + DX[i];
let ny = cy as i64 + DY[i];
if !in_bounds(nx, ny, w, h) {
continue;
}
let nxu = nx as u64;
let nyu = ny as u64;
let nidx = (nyu * w + nxu) as usize;
if visited[nidx] {
continue;
}
visited[nidx] = true;
parent[nidx] = Some(cidx);
dist[nidx] = dist[cidx] + 1;
let ne = dem.get_pixel(nxu, nyu).map_err(AlgorithmError::Core)?;
if ne < sink_elev || nxu == 0 || nxu == w - 1 || nyu == 0 || nyu == h - 1 {
let mut path = Vec::new();
let mut cur = nidx;
path.push((nxu, nyu));
while let Some(p) = parent[cur] {
let px = (p as u64) % w;
let py = (p as u64) / w;
path.push((px, py));
if cur == start {
break;
}
cur = p;
}
path.reverse();
return Ok(Some(path));
}
queue.push_back((nxu, nyu));
}
}
Ok(None) }
fn apply_breach_path(dem: &mut RasterBuffer, path: &[(u64, u64)]) -> Result<()> {
if path.len() < 2 {
return Ok(());
}
let start_elev = dem
.get_pixel(path[0].0, path[0].1)
.map_err(AlgorithmError::Core)?;
let end_elev = dem
.get_pixel(path[path.len() - 1].0, path[path.len() - 1].1)
.map_err(AlgorithmError::Core)?;
let steps = path.len() as f64;
for (i, &(px, py)) in path.iter().enumerate() {
let frac = i as f64 / (steps - 1.0);
let target = start_elev + frac * (end_elev - start_elev);
let current = dem.get_pixel(px, py).map_err(AlgorithmError::Core)?;
if current > target {
dem.set_pixel(px, py, target)
.map_err(AlgorithmError::Core)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_flat_with_pit(size: u64, pit_depth: f64) -> RasterBuffer {
let mut dem = RasterBuffer::zeros(size, size, RasterDataType::Float32);
for y in 0..size {
for x in 0..size {
let _ = dem.set_pixel(x, y, 10.0);
}
}
let mid = size / 2;
let _ = dem.set_pixel(mid, mid, 10.0 - pit_depth);
dem
}
fn make_bowl(size: u64) -> RasterBuffer {
let mut dem = RasterBuffer::zeros(size, size, RasterDataType::Float32);
let center = size as f64 / 2.0;
for y in 0..size {
for x in 0..size {
let dx = x as f64 - center;
let dy = y as f64 - center;
let dist = (dx * dx + dy * dy).sqrt();
let _ = dem.set_pixel(x, y, dist);
}
}
dem
}
#[test]
fn test_wang_liu_fills_pit() {
let dem = make_flat_with_pit(7, 5.0);
let filled = fill_sinks(&dem, 0.001);
assert!(filled.is_ok());
let filled = filled.expect("should succeed");
let orig = dem.get_pixel(3, 3).expect("should succeed");
let after = filled.get_pixel(3, 3).expect("should succeed");
assert!(
after > orig,
"Pit should be filled: orig={orig}, after={after}"
);
}
#[test]
fn test_wang_liu_preserves_slope() {
let mut dem = RasterBuffer::zeros(7, 7, RasterDataType::Float32);
for y in 0..7u64 {
for x in 0..7u64 {
let _ = dem.set_pixel(x, y, x as f64);
}
}
let filled = fill_sinks(&dem, 0.001).expect("should succeed");
for y in 1..6u64 {
for x in 1..6u64 {
let orig = dem.get_pixel(x, y).expect("should succeed");
let f = filled.get_pixel(x, y).expect("should succeed");
assert!(
(f - orig).abs() < 0.1,
"Cell ({x},{y}): orig={orig}, filled={f}"
);
}
}
}
#[test]
fn test_wang_liu_max_depth() {
let dem = make_flat_with_pit(7, 20.0);
let cfg = FillSinksConfig {
method: FillMethod::WangLiu,
epsilon: 0.001,
max_fill_depth: 5.0,
..Default::default()
};
let filled = fill_sinks_cfg(&dem, &cfg).expect("should succeed");
let after = filled.get_pixel(3, 3).expect("should succeed");
let orig = dem.get_pixel(3, 3).expect("should succeed");
let depth = after - orig;
assert!(
depth <= 5.1,
"Fill depth {depth} should not exceed max of 5.0 + epsilon"
);
}
#[test]
fn test_planchon_darboux_fills_pit() {
let dem = make_flat_with_pit(7, 5.0);
let cfg = FillSinksConfig {
method: FillMethod::PlanchonDarboux,
epsilon: 0.001,
..Default::default()
};
let filled = fill_sinks_cfg(&dem, &cfg);
assert!(filled.is_ok());
let filled = filled.expect("should succeed");
let orig = dem.get_pixel(3, 3).expect("should succeed");
let after = filled.get_pixel(3, 3).expect("should succeed");
assert!(after > orig, "Planchon-Darboux should fill pit");
}
#[test]
fn test_planchon_darboux_bowl() {
let dem = make_bowl(9);
let cfg = FillSinksConfig {
method: FillMethod::PlanchonDarboux,
epsilon: 0.001,
..Default::default()
};
let filled = fill_sinks_cfg(&dem, &cfg);
assert!(filled.is_ok());
}
#[test]
fn test_identify_sinks() {
let dem = make_flat_with_pit(7, 5.0);
let sinks = identify_sinks(&dem).expect("should succeed");
let center = sinks.get_pixel(3, 3).expect("should succeed");
assert!(center > 0.0, "Center pit should be identified as a sink");
}
#[test]
fn test_no_sinks_on_slope() {
let mut dem = RasterBuffer::zeros(7, 7, RasterDataType::Float32);
for y in 0..7u64 {
for x in 0..7u64 {
let _ = dem.set_pixel(x, y, (6 - x) as f64);
}
}
let sinks = identify_sinks(&dem).expect("should succeed");
for y in 1..6u64 {
for x in 1..6u64 {
let v = sinks.get_pixel(x, y).expect("should succeed");
assert!(v < 0.5, "Cell ({x},{y}) should not be a sink on a slope");
}
}
}
#[test]
fn test_breach_simple_pit() {
let dem = make_flat_with_pit(9, 3.0);
let breached = breach_depressions(&dem, 20);
assert!(breached.is_ok());
}
#[test]
fn test_breach_max_path_limit() {
let dem = make_flat_with_pit(9, 3.0);
let breached = breach_depressions(&dem, 1);
assert!(breached.is_ok());
}
#[test]
fn test_compute_fill_depth() {
let dem = make_flat_with_pit(7, 5.0);
let depth = compute_fill_depth(&dem, 0.001);
assert!(depth.is_ok());
let depth = depth.expect("should succeed");
let center_depth = depth.get_pixel(3, 3).expect("should succeed");
assert!(
center_depth > 0.0,
"Pit cell should have positive fill depth"
);
let boundary_depth = depth.get_pixel(0, 0).expect("should succeed");
assert!(
boundary_depth < 0.01,
"Boundary cell should have ~zero fill depth"
);
}
#[test]
fn test_breach_then_fill() {
let dem = make_flat_with_pit(9, 5.0);
let cfg = FillSinksConfig {
method: FillMethod::WangLiu,
epsilon: 0.001,
breach_first: true,
max_breach_length: 20,
..Default::default()
};
let filled = fill_sinks_cfg(&dem, &cfg);
assert!(filled.is_ok());
}
}