use ndarray::Array2;
use crate::kernel::{get_clamped, grid_map};
const OFFSETS: [(isize, isize); 8] = [
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, -1),
(1, 0),
(1, 1),
];
pub fn tri(dem: &Array2<f64>) -> Array2<f64> {
let (h, w) = dem.dim();
if h < 3 || w < 3 {
return Array2::zeros((h, w));
}
grid_map(h, w, |row, col| {
let center = dem[[row, col]];
let sum_sq: f64 = OFFSETS
.iter()
.map(|&(dy, dx)| {
let n = get_clamped(dem, row, col, dy, dx);
(center - n) * (center - n)
})
.sum();
sum_sq.sqrt()
})
}
pub fn tpi(dem: &Array2<f64>) -> Array2<f64> {
let (h, w) = dem.dim();
if h < 3 || w < 3 {
return Array2::zeros((h, w));
}
grid_map(h, w, |row, col| {
let center = dem[[row, col]];
let mean: f64 = OFFSETS
.iter()
.map(|&(dy, dx)| get_clamped(dem, row, col, dy, dx))
.sum::<f64>()
/ 8.0;
center - mean
})
}
pub fn roughness(dem: &Array2<f64>) -> Array2<f64> {
let (h, w) = dem.dim();
if h < 3 || w < 3 {
return Array2::zeros((h, w));
}
grid_map(h, w, |row, col| {
let center = dem[[row, col]];
if center.is_nan() {
return f64::NAN;
}
let mut min_val = center;
let mut max_val = center;
for &(dy, dx) in &OFFSETS {
let n = get_clamped(dem, row, col, dy, dx);
if n.is_nan() {
return f64::NAN;
}
if n < min_val {
min_val = n;
}
if n > max_val {
max_val = n;
}
}
max_val - min_val
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_dem_zero_roughness() {
let dem = Array2::from_elem((10, 10), 100.0);
for grid in [tri(&dem), tpi(&dem), roughness(&dem)] {
for &v in grid.iter() {
assert!(v.abs() < 1e-10, "flat DEM should have 0 roughness, got {v}");
}
}
}
#[test]
fn tri_positive_for_varied_terrain() {
let dem =
Array2::from_shape_fn((10, 10), |(r, c)| (r as f64 * 3.0 + c as f64).sin() * 50.0);
let t = tri(&dem);
assert!(t[[5, 5]] > 0.0, "varied terrain should have positive TRI");
}
#[test]
fn tpi_ridge_positive() {
let mut dem = Array2::from_elem((5, 5), 10.0);
dem[[2, 2]] = 100.0;
let t = tpi(&dem);
assert!(t[[2, 2]] > 0.0, "ridge should have positive TPI");
}
#[test]
fn tpi_valley_negative() {
let mut dem = Array2::from_elem((5, 5), 100.0);
dem[[2, 2]] = 10.0;
let t = tpi(&dem);
assert!(t[[2, 2]] < 0.0, "valley should have negative TPI");
}
#[test]
fn roughness_matches_range() {
let mut dem = Array2::from_elem((5, 5), 50.0);
dem[[2, 1]] = 10.0;
dem[[2, 3]] = 90.0;
let r = roughness(&dem);
assert!(
(r[[2, 2]] - 80.0).abs() < 1e-6,
"roughness should be 90-10=80, got {}",
r[[2, 2]]
);
}
#[test]
fn small_grid_returns_zeros() {
let dem = Array2::from_elem((2, 2), 50.0);
for grid in [tri(&dem), tpi(&dem), roughness(&dem)] {
for &v in grid.iter() {
assert!(v.abs() < 1e-10);
}
}
}
#[test]
fn nan_neighbor_propagates_on_normal_grid() {
let mut dem = Array2::from_elem((5, 5), 50.0);
dem[[2, 3]] = f64::NAN;
assert!(tri(&dem)[[2, 2]].is_nan());
assert!(tpi(&dem)[[2, 2]].is_nan());
assert!(roughness(&dem)[[2, 2]].is_nan());
}
}