1use crate::error::{AlgorithmError, Result};
42use core::f64::consts::PI;
43use oxigdal_core::buffer::RasterBuffer;
44use oxigdal_core::types::RasterDataType;
45
46#[cfg(feature = "parallel")]
47use rayon::prelude::*;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum SlopeAlgorithm {
56 #[default]
59 Horn,
60
61 ZevenbergenThorne,
64
65 EvansYoung,
68
69 MaximumDownhill,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum SlopeUnits {
81 #[default]
83 Degrees,
84
85 Radians,
87
88 Percent,
90
91 Tangent,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101pub enum EdgeHandling {
102 #[default]
104 Skip,
105
106 Reflect,
108
109 Extrapolate,
111
112 Replicate,
114}
115
116#[derive(Debug, Clone)]
122pub struct SlopeAspectConfig {
123 pub algorithm: SlopeAlgorithm,
125 pub slope_units: SlopeUnits,
127 pub edge_handling: EdgeHandling,
129 pub z_factor: f64,
131}
132
133impl Default for SlopeAspectConfig {
134 fn default() -> Self {
135 Self {
136 algorithm: SlopeAlgorithm::default(),
137 slope_units: SlopeUnits::default(),
138 edge_handling: EdgeHandling::default(),
139 z_factor: 1.0,
140 }
141 }
142}
143
144#[derive(Debug)]
146pub struct SlopeAspectOutput {
147 pub slope: RasterBuffer,
149 pub aspect: RasterBuffer,
151}
152
153pub fn compute_slope_aspect(
169 dem: &RasterBuffer,
170 pixel_size: f64,
171 z_factor: f64,
172) -> Result<SlopeAspectOutput> {
173 compute_slope_aspect_advanced(
174 dem,
175 pixel_size,
176 &SlopeAspectConfig {
177 z_factor,
178 ..Default::default()
179 },
180 )
181}
182
183pub fn compute_slope_aspect_advanced(
195 dem: &RasterBuffer,
196 pixel_size: f64,
197 config: &SlopeAspectConfig,
198) -> Result<SlopeAspectOutput> {
199 let width = dem.width();
200 let height = dem.height();
201
202 if width < 3 || height < 3 {
203 return Err(AlgorithmError::InsufficientData {
204 operation: "slope/aspect",
205 message: "DEM must be at least 3x3".to_string(),
206 });
207 }
208
209 let mut slope_buf = RasterBuffer::zeros(width, height, dem.data_type());
210 let mut aspect_buf = RasterBuffer::zeros(width, height, dem.data_type());
211
212 let (y_start, y_end, x_start, x_end) = match config.edge_handling {
214 EdgeHandling::Skip => (1, height - 1, 1, width - 1),
215 _ => (0, height, 0, width),
216 };
217
218 #[cfg(feature = "parallel")]
219 {
220 let results: Result<Vec<_>> = (y_start..y_end)
221 .into_par_iter()
222 .map(|y| {
223 let mut row = Vec::new();
224 for x in x_start..x_end {
225 let (s, a) = compute_pixel_slope_aspect(dem, x, y, pixel_size, config)?;
226 row.push((x, s, a));
227 }
228 Ok((y, row))
229 })
230 .collect();
231
232 for (y, row) in results? {
233 for (x, s, a) in row {
234 slope_buf.set_pixel(x, y, s).map_err(AlgorithmError::Core)?;
235 aspect_buf
236 .set_pixel(x, y, a)
237 .map_err(AlgorithmError::Core)?;
238 }
239 }
240 }
241
242 #[cfg(not(feature = "parallel"))]
243 {
244 for y in y_start..y_end {
245 for x in x_start..x_end {
246 let (s, a) = compute_pixel_slope_aspect(dem, x, y, pixel_size, config)?;
247 slope_buf.set_pixel(x, y, s).map_err(AlgorithmError::Core)?;
248 aspect_buf
249 .set_pixel(x, y, a)
250 .map_err(AlgorithmError::Core)?;
251 }
252 }
253 }
254
255 Ok(SlopeAspectOutput {
256 slope: slope_buf,
257 aspect: aspect_buf,
258 })
259}
260
261pub fn slope(dem: &RasterBuffer, pixel_size: f64, z_factor: f64) -> Result<RasterBuffer> {
267 let output = compute_slope_aspect(dem, pixel_size, z_factor)?;
268 Ok(output.slope)
269}
270
271pub fn aspect(dem: &RasterBuffer, pixel_size: f64, z_factor: f64) -> Result<RasterBuffer> {
277 let output = compute_slope_aspect(dem, pixel_size, z_factor)?;
278 Ok(output.aspect)
279}
280
281pub fn slope_advanced(
287 dem: &RasterBuffer,
288 pixel_size: f64,
289 config: &SlopeAspectConfig,
290) -> Result<RasterBuffer> {
291 let output = compute_slope_aspect_advanced(dem, pixel_size, config)?;
292 Ok(output.slope)
293}
294
295pub fn aspect_advanced(
301 dem: &RasterBuffer,
302 pixel_size: f64,
303 config: &SlopeAspectConfig,
304) -> Result<RasterBuffer> {
305 let output = compute_slope_aspect_advanced(dem, pixel_size, config)?;
306 Ok(output.aspect)
307}
308
309fn compute_pixel_slope_aspect(
315 dem: &RasterBuffer,
316 x: u64,
317 y: u64,
318 pixel_size: f64,
319 config: &SlopeAspectConfig,
320) -> Result<(f64, f64)> {
321 let z = get_neighborhood(dem, x, y, config.edge_handling)?;
322
323 let (dz_dx, dz_dy) = match config.algorithm {
324 SlopeAlgorithm::Horn => compute_gradients_horn(&z, pixel_size, config.z_factor),
325 SlopeAlgorithm::ZevenbergenThorne => {
326 compute_gradients_zevenbergen_thorne(&z, pixel_size, config.z_factor)
327 }
328 SlopeAlgorithm::EvansYoung => {
329 compute_gradients_evans_young(&z, pixel_size, config.z_factor)
330 }
331 SlopeAlgorithm::MaximumDownhill => {
332 return compute_max_downhill_slope(&z, pixel_size, config);
333 }
334 };
335
336 let slope_tan = (dz_dx * dz_dx + dz_dy * dz_dy).sqrt();
338 let slope_value = convert_slope(slope_tan, config.slope_units);
339
340 let aspect_value = if dz_dx.abs() < 1e-15 && dz_dy.abs() < 1e-15 {
342 -1.0 } else {
344 let aspect_rad = dz_dy.atan2(-dz_dx);
345 let mut aspect_deg = aspect_rad * 180.0 / PI;
346 if aspect_deg < 0.0 {
347 aspect_deg += 360.0;
348 }
349 aspect_deg
350 };
351
352 Ok((slope_value, aspect_value))
353}
354
355fn compute_gradients_horn(z: &[[f64; 3]; 3], pixel_size: f64, z_factor: f64) -> (f64, f64) {
364 let scale = z_factor / (8.0 * pixel_size);
365
366 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;
367
368 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;
369
370 (dz_dx, dz_dy)
371}
372
373fn compute_gradients_zevenbergen_thorne(
378 z: &[[f64; 3]; 3],
379 pixel_size: f64,
380 z_factor: f64,
381) -> (f64, f64) {
382 let scale = z_factor / (2.0 * pixel_size);
383
384 let dz_dx = (z[1][2] - z[1][0]) * scale;
385 let dz_dy = (z[2][1] - z[0][1]) * scale;
386
387 (dz_dx, dz_dy)
388}
389
390fn compute_gradients_evans_young(z: &[[f64; 3]; 3], pixel_size: f64, z_factor: f64) -> (f64, f64) {
399 let scale = z_factor / (6.0 * pixel_size);
400
401 let dz_dx = (z[0][2] + z[1][2] + z[2][2] - z[0][0] - z[1][0] - z[2][0]) * scale;
402 let dz_dy = (z[2][0] + z[2][1] + z[2][2] - z[0][0] - z[0][1] - z[0][2]) * scale;
403
404 (dz_dx, dz_dy)
405}
406
407fn compute_max_downhill_slope(
412 z: &[[f64; 3]; 3],
413 pixel_size: f64,
414 config: &SlopeAspectConfig,
415) -> Result<(f64, f64)> {
416 let center = z[1][1];
417
418 let neighbors: [(usize, usize, f64, f64); 8] = [
420 (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), ];
429
430 let mut max_slope_tan = 0.0_f64;
431 let mut max_aspect = -1.0_f64;
432
433 for &(row, col, aspect_deg, dist) in &neighbors {
434 let neighbor_elev = z[row][col];
435 let drop = (center - neighbor_elev) * config.z_factor;
436 let slope_tan = drop / dist;
437
438 if slope_tan > max_slope_tan {
439 max_slope_tan = slope_tan;
440 max_aspect = aspect_deg;
441 }
442 }
443
444 let slope_value = convert_slope(max_slope_tan, config.slope_units);
445
446 Ok((slope_value, max_aspect))
447}
448
449fn convert_slope(slope_tangent: f64, units: SlopeUnits) -> f64 {
455 match units {
456 SlopeUnits::Degrees => slope_tangent.atan() * 180.0 / PI,
457 SlopeUnits::Radians => slope_tangent.atan(),
458 SlopeUnits::Percent => slope_tangent * 100.0,
459 SlopeUnits::Tangent => slope_tangent,
460 }
461}
462
463pub fn convert_slope_degrees(slope_degrees: f64, target_units: SlopeUnits) -> f64 {
465 match target_units {
466 SlopeUnits::Degrees => slope_degrees,
467 SlopeUnits::Radians => slope_degrees * PI / 180.0,
468 SlopeUnits::Percent => (slope_degrees * PI / 180.0).tan() * 100.0,
469 SlopeUnits::Tangent => (slope_degrees * PI / 180.0).tan(),
470 }
471}
472
473fn get_neighborhood(
479 dem: &RasterBuffer,
480 x: u64,
481 y: u64,
482 edge_handling: EdgeHandling,
483) -> Result<[[f64; 3]; 3]> {
484 let width = dem.width();
485 let height = dem.height();
486 let mut z = [[0.0f64; 3]; 3];
487
488 for dy in 0..3i64 {
489 for dx in 0..3i64 {
490 let src_x = x as i64 + dx - 1;
491 let src_y = y as i64 + dy - 1;
492
493 let (px, py) = resolve_coords(src_x, src_y, width, height, edge_handling);
494
495 z[dy as usize][dx as usize] = dem.get_pixel(px, py).map_err(AlgorithmError::Core)?;
496 }
497 }
498
499 Ok(z)
500}
501
502fn resolve_coords(
504 x: i64,
505 y: i64,
506 width: u64,
507 height: u64,
508 edge_handling: EdgeHandling,
509) -> (u64, u64) {
510 let w = width as i64;
511 let h = height as i64;
512
513 match edge_handling {
514 EdgeHandling::Skip => {
515 (x.clamp(0, w - 1) as u64, y.clamp(0, h - 1) as u64)
517 }
518 EdgeHandling::Reflect => {
519 let rx = if x < 0 {
520 (-x).min(w - 1)
521 } else if x >= w {
522 (2 * w - x - 2).max(0)
523 } else {
524 x
525 };
526 let ry = if y < 0 {
527 (-y).min(h - 1)
528 } else if y >= h {
529 (2 * h - y - 2).max(0)
530 } else {
531 y
532 };
533 (rx as u64, ry as u64)
534 }
535 EdgeHandling::Extrapolate => {
536 (x.clamp(0, w - 1) as u64, y.clamp(0, h - 1) as u64)
539 }
540 EdgeHandling::Replicate => (x.clamp(0, w - 1) as u64, y.clamp(0, h - 1) as u64),
541 }
542}
543
544#[cfg(test)]
549mod tests {
550 use super::*;
551
552 fn create_slope_dem() -> RasterBuffer {
553 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
554 for y in 0..10 {
555 for x in 0..10 {
556 let _ = dem.set_pixel(x, y, (x + y) as f64);
557 }
558 }
559 dem
560 }
561
562 fn create_flat_dem() -> RasterBuffer {
563 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
564 for y in 0..10 {
565 for x in 0..10 {
566 let _ = dem.set_pixel(x, y, 100.0);
567 }
568 }
569 dem
570 }
571
572 fn create_north_facing_dem() -> RasterBuffer {
573 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
575 for y in 0..10 {
576 for x in 0..10 {
577 let _ = dem.set_pixel(x, y, y as f64 * 10.0);
578 }
579 }
580 dem
581 }
582
583 #[test]
586 fn test_slope_aspect_horn() {
587 let dem = create_slope_dem();
588 let result = compute_slope_aspect(&dem, 1.0, 1.0);
589 assert!(result.is_ok());
590 let output = result.expect("slope/aspect");
591 let s = output.slope.get_pixel(5, 5).expect("slope pixel");
592 assert!(s > 0.0, "Slope should be positive on inclined surface");
593 }
594
595 #[test]
596 fn test_slope_flat() {
597 let dem = create_flat_dem();
598 let result = compute_slope_aspect(&dem, 1.0, 1.0);
599 assert!(result.is_ok());
600 let output = result.expect("slope/aspect");
601 let s = output.slope.get_pixel(5, 5).expect("slope pixel");
602 assert!(
603 s.abs() < 1e-6,
604 "Slope should be zero on flat terrain, got {s}"
605 );
606 }
607
608 #[test]
609 fn test_aspect_flat() {
610 let dem = create_flat_dem();
611 let result = compute_slope_aspect_advanced(
612 &dem,
613 1.0,
614 &SlopeAspectConfig {
615 z_factor: 1.0,
616 ..Default::default()
617 },
618 );
619 assert!(result.is_ok());
620 let output = result.expect("slope/aspect");
621 let a = output.aspect.get_pixel(5, 5).expect("aspect pixel");
622 assert!(a < 0.0, "Flat aspect should be -1, got {a}");
624 }
625
626 #[test]
629 fn test_zevenbergen_thorne() {
630 let dem = create_slope_dem();
631 let config = SlopeAspectConfig {
632 algorithm: SlopeAlgorithm::ZevenbergenThorne,
633 ..Default::default()
634 };
635 let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
636 assert!(result.is_ok());
637 let output = result.expect("zt");
638 let s = output.slope.get_pixel(5, 5).expect("slope");
639 assert!(s > 0.0);
640 }
641
642 #[test]
643 fn test_evans_young() {
644 let dem = create_slope_dem();
645 let config = SlopeAspectConfig {
646 algorithm: SlopeAlgorithm::EvansYoung,
647 ..Default::default()
648 };
649 let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
650 assert!(result.is_ok());
651 let output = result.expect("ey");
652 let s = output.slope.get_pixel(5, 5).expect("slope");
653 assert!(s > 0.0);
654 }
655
656 #[test]
657 fn test_maximum_downhill() {
658 let dem = create_slope_dem();
659 let config = SlopeAspectConfig {
660 algorithm: SlopeAlgorithm::MaximumDownhill,
661 ..Default::default()
662 };
663 let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
664 assert!(result.is_ok());
665 let output = result.expect("mds");
666 let s = output.slope.get_pixel(5, 5).expect("slope");
667 assert!(s > 0.0);
668 }
669
670 #[test]
673 fn test_algorithms_consistent_direction() {
674 let dem = create_north_facing_dem();
675
676 let horn = compute_slope_aspect_advanced(
677 &dem,
678 1.0,
679 &SlopeAspectConfig {
680 algorithm: SlopeAlgorithm::Horn,
681 ..Default::default()
682 },
683 )
684 .expect("horn");
685
686 let zt = compute_slope_aspect_advanced(
687 &dem,
688 1.0,
689 &SlopeAspectConfig {
690 algorithm: SlopeAlgorithm::ZevenbergenThorne,
691 ..Default::default()
692 },
693 )
694 .expect("zt");
695
696 let horn_aspect = horn.aspect.get_pixel(5, 5).expect("aspect");
697 let zt_aspect = zt.aspect.get_pixel(5, 5).expect("aspect");
698
699 let diff = (horn_aspect - zt_aspect).abs();
701 assert!(
702 !(45.0..=315.0).contains(&diff),
703 "Horn and ZT aspects should be similar: {horn_aspect} vs {zt_aspect}"
704 );
705 }
706
707 #[test]
710 fn test_slope_units_degrees() {
711 let dem = create_slope_dem();
712 let config = SlopeAspectConfig {
713 slope_units: SlopeUnits::Degrees,
714 ..Default::default()
715 };
716 let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("degrees");
717 let s = result.slope.get_pixel(5, 5).expect("slope");
718 assert!(s > 0.0 && s < 90.0, "Degrees should be in (0,90), got {s}");
719 }
720
721 #[test]
722 fn test_slope_units_radians() {
723 let dem = create_slope_dem();
724 let config = SlopeAspectConfig {
725 slope_units: SlopeUnits::Radians,
726 ..Default::default()
727 };
728 let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("radians");
729 let s = result.slope.get_pixel(5, 5).expect("slope");
730 assert!(
731 s > 0.0 && s < PI / 2.0,
732 "Radians should be in (0, pi/2), got {s}"
733 );
734 }
735
736 #[test]
737 fn test_slope_units_percent() {
738 let dem = create_slope_dem();
739 let config = SlopeAspectConfig {
740 slope_units: SlopeUnits::Percent,
741 ..Default::default()
742 };
743 let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("percent");
744 let s = result.slope.get_pixel(5, 5).expect("slope");
745 assert!(s > 0.0, "Percent slope should be positive");
746 }
747
748 #[test]
749 fn test_slope_units_tangent() {
750 let dem = create_slope_dem();
751 let config = SlopeAspectConfig {
752 slope_units: SlopeUnits::Tangent,
753 ..Default::default()
754 };
755 let result = compute_slope_aspect_advanced(&dem, 1.0, &config).expect("tangent");
756 let s = result.slope.get_pixel(5, 5).expect("slope");
757 assert!(s > 0.0, "Tangent slope should be positive");
758 }
759
760 #[test]
761 fn test_convert_slope_degrees() {
762 let deg = 45.0;
763 let rad = convert_slope_degrees(deg, SlopeUnits::Radians);
764 assert!((rad - PI / 4.0).abs() < 1e-10);
765
766 let pct = convert_slope_degrees(deg, SlopeUnits::Percent);
767 assert!((pct - 100.0).abs() < 0.1);
768
769 let same = convert_slope_degrees(deg, SlopeUnits::Degrees);
770 assert!((same - deg).abs() < 1e-10);
771 }
772
773 #[test]
776 fn test_edge_handling_skip() {
777 let dem = create_slope_dem();
778 let config = SlopeAspectConfig {
779 edge_handling: EdgeHandling::Skip,
780 ..Default::default()
781 };
782 let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
783 assert!(result.is_ok());
784 let output = result.expect("skip");
786 let edge = output.slope.get_pixel(0, 0).expect("edge pixel");
787 assert!(
788 edge.abs() < 1e-10,
789 "Skip edge pixel should be 0, got {edge}"
790 );
791 }
792
793 #[test]
794 fn test_edge_handling_reflect() {
795 let dem = create_slope_dem();
796 let config = SlopeAspectConfig {
797 edge_handling: EdgeHandling::Reflect,
798 ..Default::default()
799 };
800 let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
801 assert!(result.is_ok());
802 let output = result.expect("reflect");
804 let corner = output.slope.get_pixel(0, 0).expect("corner");
805 assert!(corner >= 0.0);
807 }
808
809 #[test]
810 fn test_edge_handling_replicate() {
811 let dem = create_slope_dem();
812 let config = SlopeAspectConfig {
813 edge_handling: EdgeHandling::Replicate,
814 ..Default::default()
815 };
816 let result = compute_slope_aspect_advanced(&dem, 1.0, &config);
817 assert!(result.is_ok());
818 }
819
820 #[test]
823 fn test_slope_convenience() {
824 let dem = create_slope_dem();
825 let result = slope(&dem, 1.0, 1.0);
826 assert!(result.is_ok());
827 }
828
829 #[test]
830 fn test_aspect_convenience() {
831 let dem = create_slope_dem();
832 let result = aspect(&dem, 1.0, 1.0);
833 assert!(result.is_ok());
834 }
835
836 #[test]
837 fn test_slope_advanced_convenience() {
838 let dem = create_slope_dem();
839 let config = SlopeAspectConfig {
840 algorithm: SlopeAlgorithm::ZevenbergenThorne,
841 slope_units: SlopeUnits::Percent,
842 ..Default::default()
843 };
844 let result = slope_advanced(&dem, 1.0, &config);
845 assert!(result.is_ok());
846 }
847
848 #[test]
851 fn test_too_small_dem() {
852 let dem = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
853 let result = compute_slope_aspect(&dem, 1.0, 1.0);
854 assert!(result.is_err());
855 }
856
857 #[test]
860 fn test_z_factor_scaling() {
861 let dem = create_slope_dem();
862 let result1 = compute_slope_aspect(&dem, 1.0, 1.0).expect("z=1");
863 let result2 = compute_slope_aspect(&dem, 1.0, 2.0).expect("z=2");
864
865 let s1 = result1.slope.get_pixel(5, 5).expect("s1");
866 let s2 = result2.slope.get_pixel(5, 5).expect("s2");
867
868 assert!(
869 s2 > s1,
870 "Higher z-factor should produce steeper slope: {s1} vs {s2}"
871 );
872 }
873}