use crate::error::{AlgorithmError, Result};
use core::f64::consts::PI;
use oxigdal_core::buffer::RasterBuffer;
use oxigdal_core::types::RasterDataType;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SlopeAlgorithm {
#[default]
Horn,
ZevenbergenThorne,
EvansYoung,
MaximumDownhill,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SlopeUnits {
#[default]
Degrees,
Radians,
Percent,
Tangent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EdgeHandling {
#[default]
Skip,
Reflect,
Extrapolate,
Replicate,
}
#[derive(Debug, Clone)]
pub struct SlopeAspectConfig {
pub algorithm: SlopeAlgorithm,
pub slope_units: SlopeUnits,
pub edge_handling: EdgeHandling,
pub z_factor: f64,
}
impl Default for SlopeAspectConfig {
fn default() -> Self {
Self {
algorithm: SlopeAlgorithm::default(),
slope_units: SlopeUnits::default(),
edge_handling: EdgeHandling::default(),
z_factor: 1.0,
}
}
}
#[derive(Debug)]
pub struct SlopeAspectOutput {
pub slope: RasterBuffer,
pub aspect: RasterBuffer,
}
pub fn compute_slope_aspect(
dem: &RasterBuffer,
pixel_size: f64,
z_factor: f64,
) -> Result<SlopeAspectOutput> {
compute_slope_aspect_advanced(
dem,
pixel_size,
&SlopeAspectConfig {
z_factor,
..Default::default()
},
)
}
pub fn compute_slope_aspect_advanced(
dem: &RasterBuffer,
pixel_size: f64,
config: &SlopeAspectConfig,
) -> Result<SlopeAspectOutput> {
let width = dem.width();
let height = dem.height();
if width < 3 || height < 3 {
return Err(AlgorithmError::InsufficientData {
operation: "slope/aspect",
message: "DEM must be at least 3x3".to_string(),
});
}
let mut slope_buf = RasterBuffer::zeros(width, height, dem.data_type());
let mut aspect_buf = RasterBuffer::zeros(width, height, dem.data_type());
let (y_start, y_end, x_start, x_end) = match config.edge_handling {
EdgeHandling::Skip => (1, height - 1, 1, width - 1),
_ => (0, height, 0, width),
};
#[cfg(feature = "parallel")]
{
let results: Result<Vec<_>> = (y_start..y_end)
.into_par_iter()
.map(|y| {
let mut row = Vec::new();
for x in x_start..x_end {
let (s, a) = compute_pixel_slope_aspect(dem, x, y, pixel_size, config)?;
row.push((x, s, a));
}
Ok((y, row))
})
.collect();
for (y, row) in results? {
for (x, s, a) in row {
slope_buf.set_pixel(x, y, s).map_err(AlgorithmError::Core)?;
aspect_buf
.set_pixel(x, y, a)
.map_err(AlgorithmError::Core)?;
}
}
}
#[cfg(not(feature = "parallel"))]
{
for y in y_start..y_end {
for x in x_start..x_end {
let (s, a) = compute_pixel_slope_aspect(dem, x, y, pixel_size, config)?;
slope_buf.set_pixel(x, y, s).map_err(AlgorithmError::Core)?;
aspect_buf
.set_pixel(x, y, a)
.map_err(AlgorithmError::Core)?;
}
}
}
Ok(SlopeAspectOutput {
slope: slope_buf,
aspect: aspect_buf,
})
}
pub fn slope(dem: &RasterBuffer, pixel_size: f64, z_factor: f64) -> Result<RasterBuffer> {
let output = compute_slope_aspect(dem, pixel_size, z_factor)?;
Ok(output.slope)
}
pub fn aspect(dem: &RasterBuffer, pixel_size: f64, z_factor: f64) -> Result<RasterBuffer> {
let output = compute_slope_aspect(dem, pixel_size, z_factor)?;
Ok(output.aspect)
}
pub fn slope_advanced(
dem: &RasterBuffer,
pixel_size: f64,
config: &SlopeAspectConfig,
) -> Result<RasterBuffer> {
let output = compute_slope_aspect_advanced(dem, pixel_size, config)?;
Ok(output.slope)
}
pub fn aspect_advanced(
dem: &RasterBuffer,
pixel_size: f64,
config: &SlopeAspectConfig,
) -> Result<RasterBuffer> {
let output = compute_slope_aspect_advanced(dem, pixel_size, config)?;
Ok(output.aspect)
}
fn compute_pixel_slope_aspect(
dem: &RasterBuffer,
x: u64,
y: u64,
pixel_size: f64,
config: &SlopeAspectConfig,
) -> Result<(f64, f64)> {
let z = get_neighborhood(dem, x, y, config.edge_handling)?;
let (dz_dx, dz_dy) = match config.algorithm {
SlopeAlgorithm::Horn => compute_gradients_horn(&z, pixel_size, config.z_factor),
SlopeAlgorithm::ZevenbergenThorne => {
compute_gradients_zevenbergen_thorne(&z, pixel_size, config.z_factor)
}
SlopeAlgorithm::EvansYoung => {
compute_gradients_evans_young(&z, pixel_size, config.z_factor)
}
SlopeAlgorithm::MaximumDownhill => {
return compute_max_downhill_slope(&z, pixel_size, config);
}
};
let slope_tan = (dz_dx * dz_dx + dz_dy * dz_dy).sqrt();
let slope_value = convert_slope(slope_tan, config.slope_units);
let aspect_value = if dz_dx.abs() < 1e-15 && dz_dy.abs() < 1e-15 {
-1.0 } else {
let aspect_rad = dz_dy.atan2(-dz_dx);
let mut aspect_deg = aspect_rad * 180.0 / PI;
if aspect_deg < 0.0 {
aspect_deg += 360.0;
}
aspect_deg
};
Ok((slope_value, aspect_value))
}
fn compute_gradients_horn(z: &[[f64; 3]; 3], pixel_size: f64, z_factor: f64) -> (f64, f64) {
let scale = z_factor / (8.0 * pixel_size);
let dz_dx = ((z[0][2] + 2.0 * z[1][2] + z[2][2]) - (z[0][0] + 2.0 * z[1][0] + z[2][0])) * scale;
let dz_dy = ((z[2][0] + 2.0 * z[2][1] + z[2][2]) - (z[0][0] + 2.0 * z[0][1] + z[0][2])) * scale;
(dz_dx, dz_dy)
}
fn compute_gradients_zevenbergen_thorne(
z: &[[f64; 3]; 3],
pixel_size: f64,
z_factor: f64,
) -> (f64, f64) {
let scale = z_factor / (2.0 * pixel_size);
let dz_dx = (z[1][2] - z[1][0]) * scale;
let dz_dy = (z[2][1] - z[0][1]) * scale;
(dz_dx, dz_dy)
}
fn compute_gradients_evans_young(z: &[[f64; 3]; 3], pixel_size: f64, z_factor: f64) -> (f64, f64) {
let scale = z_factor / (6.0 * pixel_size);
let dz_dx = (z[0][2] + z[1][2] + z[2][2] - z[0][0] - z[1][0] - z[2][0]) * scale;
let dz_dy = (z[2][0] + z[2][1] + z[2][2] - z[0][0] - z[0][1] - z[0][2]) * scale;
(dz_dx, dz_dy)
}
fn compute_max_downhill_slope(
z: &[[f64; 3]; 3],
pixel_size: f64,
config: &SlopeAspectConfig,
) -> Result<(f64, f64)> {
let center = z[1][1];
let neighbors: [(usize, usize, f64, f64); 8] = [
(0, 1, 0.0, pixel_size), (0, 2, 45.0, pixel_size * core::f64::consts::SQRT_2), (1, 2, 90.0, pixel_size), (2, 2, 135.0, pixel_size * core::f64::consts::SQRT_2), (2, 1, 180.0, pixel_size), (2, 0, 225.0, pixel_size * core::f64::consts::SQRT_2), (1, 0, 270.0, pixel_size), (0, 0, 315.0, pixel_size * core::f64::consts::SQRT_2), ];
let mut max_slope_tan = 0.0_f64;
let mut max_aspect = -1.0_f64;
for &(row, col, aspect_deg, dist) in &neighbors {
let neighbor_elev = z[row][col];
let drop = (center - neighbor_elev) * config.z_factor;
let slope_tan = drop / dist;
if slope_tan > max_slope_tan {
max_slope_tan = slope_tan;
max_aspect = aspect_deg;
}
}
let slope_value = convert_slope(max_slope_tan, config.slope_units);
Ok((slope_value, max_aspect))
}
fn convert_slope(slope_tangent: f64, units: SlopeUnits) -> f64 {
match units {
SlopeUnits::Degrees => slope_tangent.atan() * 180.0 / PI,
SlopeUnits::Radians => slope_tangent.atan(),
SlopeUnits::Percent => slope_tangent * 100.0,
SlopeUnits::Tangent => slope_tangent,
}
}
pub fn convert_slope_degrees(slope_degrees: f64, target_units: SlopeUnits) -> f64 {
match target_units {
SlopeUnits::Degrees => slope_degrees,
SlopeUnits::Radians => slope_degrees * PI / 180.0,
SlopeUnits::Percent => (slope_degrees * PI / 180.0).tan() * 100.0,
SlopeUnits::Tangent => (slope_degrees * PI / 180.0).tan(),
}
}
fn get_neighborhood(
dem: &RasterBuffer,
x: u64,
y: u64,
edge_handling: EdgeHandling,
) -> Result<[[f64; 3]; 3]> {
let width = dem.width();
let height = dem.height();
let mut z = [[0.0f64; 3]; 3];
for dy in 0..3i64 {
for dx in 0..3i64 {
let src_x = x as i64 + dx - 1;
let src_y = y as i64 + dy - 1;
let (px, py) = resolve_coords(src_x, src_y, width, height, edge_handling);
z[dy as usize][dx as usize] = dem.get_pixel(px, py).map_err(AlgorithmError::Core)?;
}
}
Ok(z)
}
fn resolve_coords(
x: i64,
y: i64,
width: u64,
height: u64,
edge_handling: EdgeHandling,
) -> (u64, u64) {
let w = width as i64;
let h = height as i64;
match edge_handling {
EdgeHandling::Skip => {
(x.clamp(0, w - 1) as u64, y.clamp(0, h - 1) as u64)
}
EdgeHandling::Reflect => {
let rx = if x < 0 {
(-x).min(w - 1)
} else if x >= w {
(2 * w - x - 2).max(0)
} else {
x
};
let ry = if y < 0 {
(-y).min(h - 1)
} else if y >= h {
(2 * h - y - 2).max(0)
} else {
y
};
(rx as u64, ry as u64)
}
EdgeHandling::Extrapolate => {
(x.clamp(0, w - 1) as u64, y.clamp(0, h - 1) as u64)
}
EdgeHandling::Replicate => (x.clamp(0, w - 1) as u64, y.clamp(0, h - 1) as u64),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_slope_dem() -> RasterBuffer {
let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
let _ = dem.set_pixel(x, y, (x + y) as f64);
}
}
dem
}
fn create_flat_dem() -> RasterBuffer {
let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
let _ = dem.set_pixel(x, y, 100.0);
}
}
dem
}
fn create_north_facing_dem() -> RasterBuffer {
let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
for y in 0..10 {
for x in 0..10 {
let _ = dem.set_pixel(x, y, y as f64 * 10.0);
}
}
dem
}
#[test]
fn test_slope_aspect_horn() {
let dem = create_slope_dem();
let result = compute_slope_aspect(&dem, 1.0, 1.0);
assert!(result.is_ok());
let output = result.expect("slope/aspect");
let s = output.slope.get_pixel(5, 5).expect("slope pixel");
assert!(s > 0.0, "Slope should be positive on inclined surface");
}
#[test]
fn test_slope_flat() {
let dem = create_flat_dem();
let result = compute_slope_aspect(&dem, 1.0, 1.0);
assert!(result.is_ok());
let output = result.expect("slope/aspect");
let s = output.slope.get_pixel(5, 5).expect("slope pixel");
assert!(
s.abs() < 1e-6,
"Slope should be zero on flat terrain, got {s}"
);
}
#[test]
fn test_aspect_flat() {
let dem = create_flat_dem();
let result = compute_slope_aspect_advanced(
&dem,
1.0,
&SlopeAspectConfig {
z_factor: 1.0,
..Default::default()
},
);
assert!(result.is_ok());
let output = result.expect("slope/aspect");
let a = output.aspect.get_pixel(5, 5).expect("aspect pixel");
assert!(a < 0.0, "Flat aspect should be -1, got {a}");
}
#[test]
fn test_zevenbergen_thorne() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
algorithm: SlopeAlgorithm::ZevenbergenThorne,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
assert!(result.is_ok());
let output = result.expect("zt");
let s = output.slope.get_pixel(5, 5).expect("slope");
assert!(s > 0.0);
}
#[test]
fn test_evans_young() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
algorithm: SlopeAlgorithm::EvansYoung,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
assert!(result.is_ok());
let output = result.expect("ey");
let s = output.slope.get_pixel(5, 5).expect("slope");
assert!(s > 0.0);
}
#[test]
fn test_maximum_downhill() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
algorithm: SlopeAlgorithm::MaximumDownhill,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
assert!(result.is_ok());
let output = result.expect("mds");
let s = output.slope.get_pixel(5, 5).expect("slope");
assert!(s > 0.0);
}
#[test]
fn test_algorithms_consistent_direction() {
let dem = create_north_facing_dem();
let horn = compute_slope_aspect_advanced(
&dem,
1.0,
&SlopeAspectConfig {
algorithm: SlopeAlgorithm::Horn,
..Default::default()
},
)
.expect("horn");
let zt = compute_slope_aspect_advanced(
&dem,
1.0,
&SlopeAspectConfig {
algorithm: SlopeAlgorithm::ZevenbergenThorne,
..Default::default()
},
)
.expect("zt");
let horn_aspect = horn.aspect.get_pixel(5, 5).expect("aspect");
let zt_aspect = zt.aspect.get_pixel(5, 5).expect("aspect");
let diff = (horn_aspect - zt_aspect).abs();
assert!(
!(45.0..=315.0).contains(&diff),
"Horn and ZT aspects should be similar: {horn_aspect} vs {zt_aspect}"
);
}
#[test]
fn test_slope_units_degrees() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
slope_units: SlopeUnits::Degrees,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("degrees");
let s = result.slope.get_pixel(5, 5).expect("slope");
assert!(s > 0.0 && s < 90.0, "Degrees should be in (0,90), got {s}");
}
#[test]
fn test_slope_units_radians() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
slope_units: SlopeUnits::Radians,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("radians");
let s = result.slope.get_pixel(5, 5).expect("slope");
assert!(
s > 0.0 && s < PI / 2.0,
"Radians should be in (0, pi/2), got {s}"
);
}
#[test]
fn test_slope_units_percent() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
slope_units: SlopeUnits::Percent,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("percent");
let s = result.slope.get_pixel(5, 5).expect("slope");
assert!(s > 0.0, "Percent slope should be positive");
}
#[test]
fn test_slope_units_tangent() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
slope_units: SlopeUnits::Tangent,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("tangent");
let s = result.slope.get_pixel(5, 5).expect("slope");
assert!(s > 0.0, "Tangent slope should be positive");
}
#[test]
fn test_convert_slope_degrees() {
let deg = 45.0;
let rad = convert_slope_degrees(deg, SlopeUnits::Radians);
assert!((rad - PI / 4.0).abs() < 1e-10);
let pct = convert_slope_degrees(deg, SlopeUnits::Percent);
assert!((pct - 100.0).abs() < 0.1);
let same = convert_slope_degrees(deg, SlopeUnits::Degrees);
assert!((same - deg).abs() < 1e-10);
}
#[test]
fn test_edge_handling_skip() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
edge_handling: EdgeHandling::Skip,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
assert!(result.is_ok());
let output = result.expect("skip");
let edge = output.slope.get_pixel(0, 0).expect("edge pixel");
assert!(
edge.abs() < 1e-10,
"Skip edge pixel should be 0, got {edge}"
);
}
#[test]
fn test_edge_handling_reflect() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
edge_handling: EdgeHandling::Reflect,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
assert!(result.is_ok());
let output = result.expect("reflect");
let corner = output.slope.get_pixel(0, 0).expect("corner");
assert!(corner >= 0.0);
}
#[test]
fn test_edge_handling_replicate() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
edge_handling: EdgeHandling::Replicate,
..Default::default()
};
let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
assert!(result.is_ok());
}
#[test]
fn test_slope_convenience() {
let dem = create_slope_dem();
let result = slope(&dem, 1.0, 1.0);
assert!(result.is_ok());
}
#[test]
fn test_aspect_convenience() {
let dem = create_slope_dem();
let result = aspect(&dem, 1.0, 1.0);
assert!(result.is_ok());
}
#[test]
fn test_slope_advanced_convenience() {
let dem = create_slope_dem();
let config = SlopeAspectConfig {
algorithm: SlopeAlgorithm::ZevenbergenThorne,
slope_units: SlopeUnits::Percent,
..Default::default()
};
let result = slope_advanced(&dem, 1.0, &config);
assert!(result.is_ok());
}
#[test]
fn test_too_small_dem() {
let dem = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
let result = compute_slope_aspect(&dem, 1.0, 1.0);
assert!(result.is_err());
}
#[test]
fn test_z_factor_scaling() {
let dem = create_slope_dem();
let result1 = compute_slope_aspect(&dem, 1.0, 1.0).expect("z=1");
let result2 = compute_slope_aspect(&dem, 1.0, 2.0).expect("z=2");
let s1 = result1.slope.get_pixel(5, 5).expect("s1");
let s2 = result2.slope.get_pixel(5, 5).expect("s2");
assert!(
s2 > s1,
"Higher z-factor should produce steeper slope: {s1} vs {s2}"
);
}
}