1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
23#![allow(clippy::too_many_arguments)]
24
25use std::f64::consts::PI;
26
27#[derive(Debug, Clone)]
37pub struct Dem {
38 pub heights: Vec<f64>,
40 pub rows: usize,
42 pub cols: usize,
44 pub cell_size: f64,
46 pub nodata: f64,
48}
49
50impl Dem {
51 pub fn new(heights: Vec<f64>, rows: usize, cols: usize, cell_size: f64) -> Self {
56 assert_eq!(heights.len(), rows * cols, "heights length mismatch");
57 Self {
58 heights,
59 rows,
60 cols,
61 cell_size,
62 nodata: -9999.0,
63 }
64 }
65
66 pub fn get(&self, row: usize, col: usize) -> Option<f64> {
68 if row < self.rows && col < self.cols {
69 Some(self.heights[row * self.cols + col])
70 } else {
71 None
72 }
73 }
74
75 pub fn get_clamped(&self, row: i64, col: i64) -> f64 {
77 let r = row.clamp(0, self.rows as i64 - 1) as usize;
78 let c = col.clamp(0, self.cols as i64 - 1) as usize;
79 self.heights[r * self.cols + c]
80 }
81
82 pub fn is_valid(&self, row: usize, col: usize) -> bool {
84 if let Some(h) = self.get(row, col) {
85 (h - self.nodata).abs() > 1.0
86 } else {
87 false
88 }
89 }
90
91 pub fn window_3x3(&self, row: usize, col: usize) -> [[f64; 3]; 3] {
95 let mut w = [[0.0f64; 3]; 3];
96 for dr in 0i64..3 {
97 for dc in 0i64..3 {
98 let r = row as i64 + dr - 1;
99 let c = col as i64 + dc - 1;
100 w[dr as usize][dc as usize] = self.get_clamped(r, c);
101 }
102 }
103 w
104 }
105}
106
107pub fn compute_slope(dem: &Dem) -> Vec<f64> {
116 let mut slope = vec![0.0f64; dem.rows * dem.cols];
117 let cs = dem.cell_size;
118 for r in 0..dem.rows {
119 for c in 0..dem.cols {
120 let w = dem.window_3x3(r, c);
121 let dzdx = ((w[0][2] + 2.0 * w[1][2] + w[2][2]) - (w[0][0] + 2.0 * w[1][0] + w[2][0]))
123 / (8.0 * cs);
124 let dzdy = ((w[2][0] + 2.0 * w[2][1] + w[2][2]) - (w[0][0] + 2.0 * w[0][1] + w[0][2]))
126 / (8.0 * cs);
127 slope[r * dem.cols + c] = (dzdx * dzdx + dzdy * dzdy).sqrt().atan();
128 }
129 }
130 slope
131}
132
133pub fn compute_aspect(dem: &Dem) -> Vec<f64> {
138 let mut aspect = vec![-1.0f64; dem.rows * dem.cols];
139 let cs = dem.cell_size;
140 for r in 0..dem.rows {
141 for c in 0..dem.cols {
142 let w = dem.window_3x3(r, c);
143 let dzdx = ((w[0][2] + 2.0 * w[1][2] + w[2][2]) - (w[0][0] + 2.0 * w[1][0] + w[2][0]))
144 / (8.0 * cs);
145 let dzdy = ((w[2][0] + 2.0 * w[2][1] + w[2][2]) - (w[0][0] + 2.0 * w[0][1] + w[0][2]))
146 / (8.0 * cs);
147 if dzdx.abs() > 1e-15 || dzdy.abs() > 1e-15 {
148 let a = 180.0 - dzdy.atan2(dzdx).to_degrees() + 90.0;
150 let a = if a < 0.0 {
151 a + 360.0
152 } else if a >= 360.0 {
153 a - 360.0
154 } else {
155 a
156 };
157 aspect[r * dem.cols + c] = a.to_radians();
158 }
159 }
160 }
161 aspect
162}
163
164#[derive(Debug, Clone, Copy, PartialEq)]
170pub enum CurvatureKind {
171 Profile,
173 Planform,
175 Tangential,
177}
178
179pub fn compute_curvature(dem: &Dem, kind: CurvatureKind) -> Vec<f64> {
183 let mut curv = vec![0.0f64; dem.rows * dem.cols];
184 let l = dem.cell_size;
185 for r in 0..dem.rows {
186 for c in 0..dem.cols {
187 let w = dem.window_3x3(r, c);
188 let z5 = w[1][1];
193 let _ = z5; let d = ((w[0][1] + w[2][1]) / 2.0 - w[1][1]) / (l * l);
196 let e = ((w[1][0] + w[1][2]) / 2.0 - w[1][1]) / (l * l);
197 let f = (-w[0][0] + w[0][2] + w[2][0] - w[2][2]) / (4.0 * l * l);
198 let g = (-w[0][1] + w[2][1]) / (2.0 * l);
199 let h = (w[1][2] - w[1][0]) / (2.0 * l);
200
201 let p2 = g * g + h * h;
202
203 curv[r * dem.cols + c] = match kind {
204 CurvatureKind::Profile => {
205 if p2 > 1e-15 {
206 -2.0 * (d * g * g + e * h * h + f * g * h) / (p2 * (1.0 + p2).sqrt())
207 } else {
208 0.0
209 }
210 }
211 CurvatureKind::Planform => {
212 if p2 > 1e-15 {
213 -2.0 * (d * h * h + e * g * g - f * g * h) / p2.powf(1.5)
214 } else {
215 0.0
216 }
217 }
218 CurvatureKind::Tangential => {
219 if p2 > 1e-15 {
220 -2.0 * (d * h * h + e * g * g - f * g * h) / (p2 * (1.0 + p2).sqrt())
221 } else {
222 0.0
223 }
224 }
225 };
226 }
227 }
228 curv
229}
230
231pub type D8Code = u8;
240
241pub const D8_OFFSETS: [(i64, i64, D8Code); 8] = [
245 (0, 1, 1),
246 (1, 1, 2),
247 (1, 0, 4),
248 (1, -1, 8),
249 (0, -1, 16),
250 (-1, -1, 32),
251 (-1, 0, 64),
252 (-1, 1, 128),
253];
254
255fn d8_distance(dr: i64, dc: i64) -> f64 {
259 if dr != 0 && dc != 0 {
260 2.0f64.sqrt()
261 } else {
262 1.0
263 }
264}
265
266pub fn flow_direction_d8(dem: &Dem) -> Vec<D8Code> {
271 let mut fdir = vec![0u8; dem.rows * dem.cols];
272 for r in 0..dem.rows {
273 for c in 0..dem.cols {
274 let z = dem.heights[r * dem.cols + c];
275 let mut max_drop = 0.0f64;
276 let mut best_code = 0u8;
277 for &(dr, dc, code) in &D8_OFFSETS {
278 let nr = r as i64 + dr;
279 let nc = c as i64 + dc;
280 if nr < 0 || nr >= dem.rows as i64 || nc < 0 || nc >= dem.cols as i64 {
281 continue;
282 }
283 let nz = dem.heights[nr as usize * dem.cols + nc as usize];
284 let dist = d8_distance(dr, dc) * dem.cell_size;
285 let drop = (z - nz) / dist;
286 if drop > max_drop {
287 max_drop = drop;
288 best_code = code;
289 }
290 }
291 fdir[r * dem.cols + c] = best_code;
292 }
293 }
294 fdir
295}
296
297pub fn flow_direction_dinf(dem: &Dem) -> Vec<f64> {
303 const FACETS: [(i64, i64, i64, i64); 8] = [
307 (0, 1, -1, 1),
308 (-1, 1, -1, 0),
309 (-1, 0, -1, -1),
310 (-1, -1, 0, -1),
311 (0, -1, 1, -1),
312 (1, -1, 1, 0),
313 (1, 0, 1, 1),
314 (1, 1, 0, 1),
315 ];
316 let mut fdir = vec![0.0f64; dem.rows * dem.cols];
317 for r in 0..dem.rows {
318 for c in 0..dem.cols {
319 let z0 = dem.heights[r * dem.cols + c];
320 let l = dem.cell_size;
321 let mut max_slope = f64::NEG_INFINITY;
322 let mut best_angle = 0.0f64;
323 for (fi, &(dr1, dc1, dr2, dc2)) in FACETS.iter().enumerate() {
324 let facet_angle_base = fi as f64 * PI / 4.0;
325 let r1 = r as i64 + dr1;
326 let c1 = c as i64 + dc1;
327 let r2 = r as i64 + dr2;
328 let c2 = c as i64 + dc2;
329 if r1 < 0 || r1 >= dem.rows as i64 || c1 < 0 || c1 >= dem.cols as i64 {
330 continue;
331 }
332 if r2 < 0 || r2 >= dem.rows as i64 || c2 < 0 || c2 >= dem.cols as i64 {
333 continue;
334 }
335 let z1 = dem.heights[r1 as usize * dem.cols + c1 as usize];
336 let z2 = dem.heights[r2 as usize * dem.cols + c2 as usize];
337 let e1 = (z0 - z1) / l;
338 let e2 = (z1 - z2) / l;
339 let alpha = if e2.abs() < 1e-15 {
341 0.0
342 } else {
343 (e1 / e2).atan().clamp(0.0, PI / 4.0)
344 };
345 let s = (e1 * e1 + e2 * e2 - 2.0 * alpha.cos() * e1 * e2).sqrt();
346 if s > max_slope {
347 max_slope = s;
348 best_angle = facet_angle_base + alpha;
349 }
350 }
351 fdir[r * dem.cols + c] = best_angle;
352 }
353 }
354 fdir
355}
356
357pub fn flow_accumulation_d8(dem: &Dem, fdir: &[D8Code]) -> Vec<u32> {
365 let n = dem.rows * dem.cols;
366 let mut accum = vec![1u32; n];
367 let mut order: Vec<usize> = (0..n).collect();
369 order.sort_by(|&a, &b| {
370 dem.heights[b]
371 .partial_cmp(&dem.heights[a])
372 .unwrap_or(std::cmp::Ordering::Equal)
373 });
374 for idx in order {
375 let r = idx / dem.cols;
376 let c = idx % dem.cols;
377 let code = fdir[idx];
378 if code == 0 {
379 continue;
380 }
381 for &(dr, dc, mask) in &D8_OFFSETS {
383 if code == mask {
384 let nr = r as i64 + dr;
385 let nc = c as i64 + dc;
386 if nr >= 0 && nr < dem.rows as i64 && nc >= 0 && nc < dem.cols as i64 {
387 let nidx = nr as usize * dem.cols + nc as usize;
388 accum[nidx] += accum[idx];
389 }
390 break;
391 }
392 }
393 }
394 accum
395}
396
397pub fn delineate_watershed(
405 dem: &Dem,
406 fdir: &[D8Code],
407 outlet_row: usize,
408 outlet_col: usize,
409) -> Vec<bool> {
410 let n = dem.rows * dem.cols;
411 let mut mask = vec![false; n];
412 let mut upstream: Vec<Vec<usize>> = vec![vec![]; n];
414 for r in 0..dem.rows {
415 for c in 0..dem.cols {
416 let idx = r * dem.cols + c;
417 let code = fdir[idx];
418 if code == 0 {
419 continue;
420 }
421 for &(dr, dc, mask_code) in &D8_OFFSETS {
422 if code == mask_code {
423 let nr = r as i64 + dr;
424 let nc = c as i64 + dc;
425 if nr >= 0 && nr < dem.rows as i64 && nc >= 0 && nc < dem.cols as i64 {
426 let nidx = nr as usize * dem.cols + nc as usize;
427 upstream[nidx].push(idx);
428 }
429 break;
430 }
431 }
432 }
433 }
434 let outlet = outlet_row * dem.cols + outlet_col;
436 let mut queue = std::collections::VecDeque::new();
437 queue.push_back(outlet);
438 mask[outlet] = true;
439 while let Some(idx) = queue.pop_front() {
440 for &up in &upstream[idx] {
441 if !mask[up] {
442 mask[up] = true;
443 queue.push_back(up);
444 }
445 }
446 }
447 mask
448}
449
450pub fn extract_streams(accum: &[u32], threshold: u32) -> Vec<bool> {
459 accum.iter().map(|&a| a >= threshold).collect()
460}
461
462pub fn strahler_order(dem: &Dem, fdir: &[D8Code], streams: &[bool]) -> Vec<u8> {
467 let n = dem.rows * dem.cols;
468 let mut order = vec![0u8; n];
469 let mut upstream: Vec<Vec<usize>> = vec![vec![]; n];
471 for r in 0..dem.rows {
472 for c in 0..dem.cols {
473 let idx = r * dem.cols + c;
474 if !streams[idx] {
475 continue;
476 }
477 let code = fdir[idx];
478 if code == 0 {
479 continue;
480 }
481 for &(dr, dc, mask_code) in &D8_OFFSETS {
482 if code == mask_code {
483 let nr = r as i64 + dr;
484 let nc = c as i64 + dc;
485 if nr >= 0 && nr < dem.rows as i64 && nc >= 0 && nc < dem.cols as i64 {
486 let nidx = nr as usize * dem.cols + nc as usize;
487 if streams[nidx] {
488 upstream[nidx].push(idx);
489 }
490 }
491 break;
492 }
493 }
494 }
495 }
496 let mut topo_order: Vec<usize> = (0..n).filter(|&i| streams[i]).collect();
498 topo_order.sort_by(|&a, &b| {
499 dem.heights[b]
500 .partial_cmp(&dem.heights[a])
501 .unwrap_or(std::cmp::Ordering::Equal)
502 });
503 for idx in topo_order {
504 let ups: Vec<u8> = upstream[idx].iter().map(|&u| order[u]).collect();
505 if ups.is_empty() {
506 order[idx] = 1;
507 } else {
508 let max_ord = *ups.iter().max().expect("iterator should not be empty");
509 let count_max = ups.iter().filter(|&&o| o == max_ord).count();
510 order[idx] = if count_max >= 2 { max_ord + 1 } else { max_ord };
511 }
512 }
513 order
514}
515
516pub fn terrain_roughness_index(dem: &Dem) -> Vec<f64> {
525 let mut tri = vec![0.0f64; dem.rows * dem.cols];
526 for r in 0..dem.rows {
527 for c in 0..dem.cols {
528 let z = dem.heights[r * dem.cols + c];
529 let w = dem.window_3x3(r, c);
530 let mut sum_sq = 0.0f64;
531 for dr in 0..3usize {
532 for dc in 0..3usize {
533 if dr == 1 && dc == 1 {
534 continue;
535 }
536 let diff = w[dr][dc] - z;
537 sum_sq += diff * diff;
538 }
539 }
540 tri[r * dem.cols + c] = sum_sq.sqrt();
541 }
542 }
543 tri
544}
545
546pub fn terrain_position_index(dem: &Dem, radius: usize) -> Vec<f64> {
555 let mut tpi = vec![0.0f64; dem.rows * dem.cols];
556 for r in 0..dem.rows {
557 for c in 0..dem.cols {
558 let z = dem.heights[r * dem.cols + c];
559 let mut sum = 0.0f64;
560 let mut count = 0usize;
561 for dr in -(radius as i64)..=(radius as i64) {
562 for dc in -(radius as i64)..=(radius as i64) {
563 if dr == 0 && dc == 0 {
564 continue;
565 }
566 let dist_sq = (dr * dr + dc * dc) as f64;
567 if dist_sq > (radius as f64 * radius as f64) {
568 continue;
569 }
570 let nr = r as i64 + dr;
571 let nc = c as i64 + dc;
572 if nr >= 0 && nr < dem.rows as i64 && nc >= 0 && nc < dem.cols as i64 {
573 sum += dem.heights[nr as usize * dem.cols + nc as usize];
574 count += 1;
575 }
576 }
577 }
578 tpi[r * dem.cols + c] = if count > 0 {
579 z - sum / count as f64
580 } else {
581 0.0
582 };
583 }
584 }
585 tpi
586}
587
588#[derive(Debug, Clone, Copy, PartialEq)]
590pub enum LandformClass {
591 Ridge,
593 UpperSlope,
595 MiddleSlope,
597 Flat,
599 LowerSlope,
601 Valley,
603}
604
605pub fn classify_landform(tpi_small: f64, tpi_large: f64, slope_rad: f64) -> LandformClass {
607 let std_small = 1.0; let std_large = 1.0;
609 let ts = tpi_small / std_small;
610 let tl = tpi_large / std_large;
611 let slope_deg = slope_rad.to_degrees();
612 if tl > 1.0 {
613 LandformClass::Ridge
614 } else if tl < -1.0 {
615 LandformClass::Valley
616 } else if ts > 1.0 {
617 LandformClass::UpperSlope
618 } else if ts < -1.0 {
619 LandformClass::LowerSlope
620 } else if slope_deg < 5.0 {
621 LandformClass::Flat
622 } else {
623 LandformClass::MiddleSlope
624 }
625}
626
627pub fn topographic_wetness_index(dem: &Dem, accum: &[u32], slope: &[f64]) -> Vec<f64> {
636 let n = dem.rows * dem.cols;
637 let mut twi = vec![0.0f64; n];
638 let cs = dem.cell_size;
639 for i in 0..n {
640 let a = (accum[i] as f64) * cs; let beta = slope[i].max(1e-6_f64.atan()); twi[i] = (a / beta.tan()).ln();
643 }
644 twi
645}
646
647#[derive(Debug, Clone, Copy)]
653pub struct Observer {
654 pub row: usize,
656 pub col: usize,
658 pub height_above_terrain: f64,
660 pub max_range: f64,
662}
663
664pub fn compute_viewshed(dem: &Dem, obs: &Observer) -> Vec<bool> {
670 let n = dem.rows * dem.cols;
671 let mut visible = vec![false; n];
672 let obs_z = dem.heights[obs.row * dem.cols + obs.col] + obs.height_above_terrain;
673
674 for r in 0..dem.rows {
675 for c in 0..dem.cols {
676 let dr = r as f64 - obs.row as f64;
677 let dc = c as f64 - obs.col as f64;
678 let dist = (dr * dr + dc * dc).sqrt() * dem.cell_size;
679 if dist > obs.max_range {
680 continue;
681 }
682 if r == obs.row && c == obs.col {
683 visible[r * dem.cols + c] = true;
684 continue;
685 }
686 let mut blocked = false;
688 let steps = dr.abs().max(dc.abs()) as usize;
689 if steps == 0 {
690 visible[r * dem.cols + c] = true;
691 continue;
692 }
693 for step in 1..steps {
694 let t = step as f64 / steps as f64;
695 let ir = (obs.row as f64 + t * dr).round() as i64;
696 let ic = (obs.col as f64 + t * dc).round() as i64;
697 if ir < 0 || ir >= dem.rows as i64 || ic < 0 || ic >= dem.cols as i64 {
698 continue;
699 }
700 let inter_z = dem.heights[ir as usize * dem.cols + ic as usize];
701 let inter_dist = (t * dist).max(1e-9);
702 let target_z = dem.heights[r * dem.cols + c];
703 let los_z = obs_z + (target_z - obs_z) * (inter_dist / dist.max(1e-9));
704 if inter_z > los_z {
705 blocked = true;
706 break;
707 }
708 }
709 visible[r * dem.cols + c] = !blocked;
710 }
711 }
712 visible
713}
714
715pub fn cumulative_viewshed(dem: &Dem, observers: &[Observer]) -> Vec<u32> {
717 let n = dem.rows * dem.cols;
718 let mut count = vec![0u32; n];
719 for obs in observers {
720 let vs = compute_viewshed(dem, obs);
721 for i in 0..n {
722 if vs[i] {
723 count[i] += 1;
724 }
725 }
726 }
727 count
728}
729
730#[derive(Debug, Clone, Copy)]
736pub struct SlopeStabilityParams {
737 pub cohesion: f64,
739 pub friction_angle: f64,
741 pub soil_density: f64,
743 pub water_density: f64,
745 pub depth: f64,
747 pub saturation: f64,
749}
750
751impl Default for SlopeStabilityParams {
752 fn default() -> Self {
753 Self {
754 cohesion: 5000.0,
755 friction_angle: 30.0_f64.to_radians(),
756 soil_density: 1800.0,
757 water_density: 1000.0,
758 depth: 1.0,
759 saturation: 0.5,
760 }
761 }
762}
763
764pub fn slope_stability_index(dem: &Dem, slope: &[f64], params: &SlopeStabilityParams) -> Vec<f64> {
771 let g = 9.81f64;
772 let n = dem.rows * dem.cols;
773 let mut fs = vec![0.0f64; n];
774 for i in 0..n {
775 let beta = slope[i];
776 let sin_b = beta.sin();
777 let cos_b = beta.cos();
778 let h = params.depth;
779 let m = params.saturation;
780 let rho_s = params.soil_density;
781 let rho_w = params.water_density;
782 let c = params.cohesion;
783 let phi = params.friction_angle;
784 let denom = rho_s * g * h * sin_b * cos_b;
785 if denom.abs() < 1e-9 {
786 fs[i] = f64::MAX;
787 } else {
788 let numer = c + (rho_s - m * rho_w) * g * h * cos_b * cos_b * phi.tan();
789 fs[i] = numer / denom;
790 }
791 }
792 fs
793}
794
795#[derive(Debug, Clone, Copy)]
801pub struct SolarPosition {
802 pub zenith: f64,
804 pub azimuth: f64,
806}
807
808impl SolarPosition {
809 pub fn from_degrees(elevation_deg: f64, azimuth_deg: f64) -> Self {
811 Self {
812 zenith: (90.0 - elevation_deg).to_radians(),
813 azimuth: azimuth_deg.to_radians(),
814 }
815 }
816
817 pub fn direct_normal_irradiance(&self, i0: f64, tau: f64) -> f64 {
822 let cos_z = self.zenith.cos().max(0.0);
823 if cos_z < 1e-6 {
824 return 0.0;
825 }
826 let air_mass = 1.0 / cos_z;
827 i0 * (-tau * air_mass).exp()
828 }
829}
830
831pub fn cos_incidence(solar: &SolarPosition, slope_rad: f64, aspect_rad: f64) -> f64 {
836 let cos_z = solar.zenith.cos();
837 let sin_z = solar.zenith.sin();
838 let cos_b = slope_rad.cos();
839 let sin_b = slope_rad.sin();
840 let delta_azimuth = solar.azimuth - aspect_rad;
841 let ci = cos_z * cos_b + sin_z * sin_b * delta_azimuth.cos();
842 ci.max(0.0)
843}
844
845pub fn terrain_solar_irradiance(
850 dem: &Dem,
851 slope: &[f64],
852 aspect: &[f64],
853 solar: &SolarPosition,
854 dni: f64,
855 diffuse_horizontal: f64,
856) -> Vec<f64> {
857 let n = dem.rows * dem.cols;
858 let mut irr = vec![0.0f64; n];
859 let cos_z = solar.zenith.cos().max(0.0);
860 for i in 0..n {
861 let ci = cos_incidence(solar, slope[i], aspect[i].max(0.0));
862 let direct = if cos_z > 1e-6 { dni * ci } else { 0.0 };
864 let diffuse = diffuse_horizontal * (1.0 + slope[i].cos()) / 2.0;
866 irr[i] = direct + diffuse;
867 }
868 irr
869}
870
871pub fn raster_stats(data: &[f64]) -> (f64, f64, f64, f64) {
879 if data.is_empty() {
880 return (0.0, 0.0, 0.0, 0.0);
881 }
882 let mut min = f64::MAX;
883 let mut max = f64::MIN;
884 let mut sum = 0.0f64;
885 for &v in data {
886 if v < min {
887 min = v;
888 }
889 if v > max {
890 max = v;
891 }
892 sum += v;
893 }
894 let mean = sum / data.len() as f64;
895 let var = data.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / data.len() as f64;
896 (min, max, mean, var.sqrt())
897}
898
899pub fn normalise_raster(data: &[f64]) -> Vec<f64> {
901 let (min, max, _, _) = raster_stats(data);
902 let range = max - min;
903 if range.abs() < 1e-15 {
904 return vec![0.0; data.len()];
905 }
906 data.iter().map(|&v| (v - min) / range).collect()
907}
908
909pub fn resample_dem(dem: &Dem, factor: usize) -> Dem {
913 let new_rows = dem.rows.div_ceil(factor);
914 let new_cols = dem.cols.div_ceil(factor);
915 let mut heights = vec![0.0f64; new_rows * new_cols];
916 for nr in 0..new_rows {
917 for nc in 0..new_cols {
918 let r0 = (nr * factor) as i64;
919 let c0 = (nc * factor) as i64;
920 heights[nr * new_cols + nc] = dem.get_clamped(r0, c0);
921 }
922 }
923 Dem::new(heights, new_rows, new_cols, dem.cell_size * factor as f64)
924}
925
926pub fn fill_sinks(dem: &Dem, max_iterations: usize) -> Dem {
931 let mut filled = dem.clone();
932 for _iter in 0..max_iterations {
933 let mut changed = false;
934 for r in 0..filled.rows {
935 for c in 0..filled.cols {
936 let idx = r * filled.cols + c;
937 let z = filled.heights[idx];
938 let w = filled.window_3x3(r, c);
939 let min_nb = {
940 let mut m = f64::MAX;
941 for dr in 0..3usize {
942 for dc in 0..3usize {
943 if dr == 1 && dc == 1 {
944 continue;
945 }
946 if w[dr][dc] < m {
947 m = w[dr][dc];
948 }
949 }
950 }
951 m
952 };
953 if z < min_nb {
954 filled.heights[idx] = min_nb;
955 changed = true;
956 }
957 }
958 }
959 if !changed {
960 break;
961 }
962 }
963 filled
964}
965
966#[derive(Debug, Clone)]
972pub struct Contour {
973 pub elevation: f64,
975 pub vertices: Vec<(f64, f64)>,
977}
978
979pub fn extract_contours(dem: &Dem, levels: &[f64]) -> Vec<Contour> {
983 let mut contours = Vec::new();
984 for &level in levels {
985 let vertices = marching_squares_level(dem, level);
986 if !vertices.is_empty() {
987 contours.push(Contour {
988 elevation: level,
989 vertices,
990 });
991 }
992 }
993 contours
994}
995
996fn marching_squares_level(dem: &Dem, level: f64) -> Vec<(f64, f64)> {
997 let mut pts = Vec::new();
998 for r in 0..dem.rows.saturating_sub(1) {
999 for c in 0..dem.cols.saturating_sub(1) {
1000 let z00 = dem.heights[r * dem.cols + c];
1001 let z10 = dem.heights[(r + 1) * dem.cols + c];
1002 let z01 = dem.heights[r * dem.cols + (c + 1)];
1003 let z11 = dem.heights[(r + 1) * dem.cols + (c + 1)];
1004 let interp = |za: f64, zb: f64| -> f64 {
1006 if (zb - za).abs() < 1e-15 {
1007 0.5
1008 } else {
1009 (level - za) / (zb - za)
1010 }
1011 };
1012 let mut code = 0u8;
1013 if z00 >= level {
1014 code |= 1;
1015 }
1016 if z01 >= level {
1017 code |= 2;
1018 }
1019 if z10 >= level {
1020 code |= 4;
1021 }
1022 if z11 >= level {
1023 code |= 8;
1024 }
1025 if code == 0 || code == 15 {
1026 continue;
1027 }
1028 if (code & 3) == 1 || (code & 3) == 2 {
1030 let t = interp(z00, z01);
1031 pts.push((c as f64 + t, r as f64));
1032 }
1033 if (code & 5) == 1 || (code & 5) == 4 {
1035 let t = interp(z00, z10);
1036 pts.push((c as f64, r as f64 + t));
1037 }
1038 }
1039 }
1040 pts
1041}
1042
1043pub fn hypsometric_curve(dem: &Dem, bins: usize) -> Vec<(f64, f64)> {
1052 let (min, max, _, _) = raster_stats(&dem.heights);
1053 let range = max - min;
1054 if range < 1e-9 {
1055 return vec![(0.0, 1.0), (1.0, 0.0)];
1056 }
1057 let total = dem.heights.len() as f64;
1058 let mut curve = Vec::with_capacity(bins + 1);
1059 for b in 0..=bins {
1060 let frac = b as f64 / bins as f64;
1061 let threshold = min + frac * range;
1062 let above = dem.heights.iter().filter(|&&h| h >= threshold).count() as f64;
1063 curve.push((frac, above / total));
1064 }
1065 curve
1066}
1067
1068pub fn hypsometric_integral(curve: &[(f64, f64)]) -> f64 {
1072 if curve.len() < 2 {
1073 return 0.0;
1074 }
1075 let mut area = 0.0f64;
1076 for w in curve.windows(2) {
1077 let dx = w[1].0 - w[0].0;
1078 let avg_y = (w[0].1 + w[1].1) / 2.0;
1079 area += dx * avg_y;
1080 }
1081 area
1082}
1083
1084#[derive(Debug, Clone, Copy, PartialEq)]
1090pub enum RidgeValley {
1091 Ridge,
1093 Valley,
1095 Slope,
1097}
1098
1099pub fn ridge_valley_lines(dem: &Dem) -> Vec<RidgeValley> {
1103 let curv = compute_curvature(dem, CurvatureKind::Profile);
1104 curv.iter()
1105 .map(|&c| {
1106 if c > 0.01 {
1107 RidgeValley::Valley
1108 } else if c < -0.01 {
1109 RidgeValley::Ridge
1110 } else {
1111 RidgeValley::Slope
1112 }
1113 })
1114 .collect()
1115}
1116
1117#[cfg(test)]
1122mod tests {
1123 use super::*;
1124
1125 fn cone_dem(rows: usize, cols: usize, cell_size: f64) -> Dem {
1127 let cx = (cols as f64 - 1.0) / 2.0;
1128 let cy = (rows as f64 - 1.0) / 2.0;
1129 let heights: Vec<f64> = (0..rows)
1130 .flat_map(|r| {
1131 (0..cols).map(move |c| {
1132 let dx = c as f64 - cx;
1133 let dy = r as f64 - cy;
1134 100.0 - (dx * dx + dy * dy).sqrt() * cell_size
1135 })
1136 })
1137 .collect();
1138 Dem::new(heights, rows, cols, cell_size)
1139 }
1140
1141 fn flat_dem(rows: usize, cols: usize, elevation: f64) -> Dem {
1143 Dem::new(vec![elevation; rows * cols], rows, cols, 10.0)
1144 }
1145
1146 fn tilted_dem(rows: usize, cols: usize, cell_size: f64, slope_gradient: f64) -> Dem {
1148 let heights: Vec<f64> = (0..rows)
1149 .flat_map(|_r| (0..cols).map(move |c| c as f64 * cell_size * slope_gradient))
1150 .collect();
1151 Dem::new(heights, rows, cols, cell_size)
1152 }
1153
1154 #[test]
1155 fn test_dem_construction() {
1156 let dem = flat_dem(5, 5, 100.0);
1157 assert_eq!(dem.rows, 5);
1158 assert_eq!(dem.cols, 5);
1159 assert_eq!(dem.heights.len(), 25);
1160 }
1161
1162 #[test]
1163 fn test_dem_get_valid() {
1164 let dem = flat_dem(4, 4, 50.0);
1165 assert_eq!(dem.get(2, 3), Some(50.0));
1166 assert_eq!(dem.get(4, 0), None);
1167 }
1168
1169 #[test]
1170 fn test_dem_get_clamped_edge() {
1171 let dem = flat_dem(3, 3, 42.0);
1172 assert_eq!(dem.get_clamped(-1, 0), 42.0);
1173 assert_eq!(dem.get_clamped(0, 10), 42.0);
1174 }
1175
1176 #[test]
1177 fn test_flat_dem_slope_zero() {
1178 let dem = flat_dem(5, 5, 100.0);
1179 let slope = compute_slope(&dem);
1180 for &s in &slope {
1181 assert!(s.abs() < 1e-10, "flat DEM slope should be zero, got {}", s);
1182 }
1183 }
1184
1185 #[test]
1186 fn test_tilted_dem_slope_nonzero() {
1187 let dem = tilted_dem(5, 5, 10.0, 0.5);
1188 let slope = compute_slope(&dem);
1189 let interior_slope = slope[2 * 5 + 2];
1191 assert!(
1192 interior_slope > 0.01,
1193 "tilted DEM should have positive slope, got {}",
1194 interior_slope
1195 );
1196 }
1197
1198 #[test]
1199 fn test_aspect_range() {
1200 let dem = cone_dem(7, 7, 10.0);
1201 let aspect = compute_aspect(&dem);
1202 for &a in &aspect {
1203 if a >= 0.0 {
1204 assert!(a <= 2.0 * PI + 1e-9, "aspect out of range: {}", a);
1205 }
1206 }
1207 }
1208
1209 #[test]
1210 fn test_profile_curvature_flat_near_zero() {
1211 let dem = flat_dem(5, 5, 200.0);
1212 let curv = compute_curvature(&dem, CurvatureKind::Profile);
1213 for &c in &curv {
1214 assert!(c.abs() < 1e-10, "flat curvature should be 0, got {}", c);
1215 }
1216 }
1217
1218 #[test]
1219 fn test_planform_curvature_flat_near_zero() {
1220 let dem = flat_dem(5, 5, 200.0);
1221 let curv = compute_curvature(&dem, CurvatureKind::Planform);
1222 for &c in &curv {
1223 assert!(
1224 c.abs() < 1e-10,
1225 "flat planform curvature should be 0, got {}",
1226 c
1227 );
1228 }
1229 }
1230
1231 #[test]
1232 fn test_tangential_curvature_flat_near_zero() {
1233 let dem = flat_dem(5, 5, 200.0);
1234 let curv = compute_curvature(&dem, CurvatureKind::Tangential);
1235 for &c in &curv {
1236 assert!(
1237 c.abs() < 1e-10,
1238 "flat tangential curvature should be 0, got {}",
1239 c
1240 );
1241 }
1242 }
1243
1244 #[test]
1245 fn test_flow_direction_d8_tilted() {
1246 let dem = tilted_dem(5, 5, 10.0, 1.0);
1247 let fdir = flow_direction_d8(&dem);
1248 let mid = 2 * 5 + 2;
1250 assert!(fdir[mid] != 0, "interior cell should have a flow direction");
1251 }
1252
1253 #[test]
1254 fn test_flow_accumulation_monotone() {
1255 let dem = tilted_dem(6, 6, 10.0, 1.0);
1256 let fdir = flow_direction_d8(&dem);
1257 let accum = flow_accumulation_d8(&dem, &fdir);
1258 for &a in &accum {
1260 assert!(a >= 1, "accumulation must be >= 1");
1261 }
1262 }
1263
1264 #[test]
1265 fn test_watershed_includes_outlet() {
1266 let dem = cone_dem(9, 9, 5.0);
1267 let fdir = flow_direction_d8(&dem);
1268 let mask = delineate_watershed(&dem, &fdir, 8, 4);
1269 assert!(mask[8 * 9 + 4], "outlet must be included in watershed");
1270 }
1271
1272 #[test]
1273 fn test_stream_extraction_threshold() {
1274 let accum = vec![1u32, 2, 5, 10, 20];
1275 let streams = extract_streams(&accum, 5);
1276 assert!(!streams[0]);
1277 assert!(!streams[1]);
1278 assert!(streams[2]);
1279 assert!(streams[3]);
1280 assert!(streams[4]);
1281 }
1282
1283 #[test]
1284 fn test_tri_flat_zero() {
1285 let dem = flat_dem(5, 5, 50.0);
1286 let tri = terrain_roughness_index(&dem);
1287 for &t in &tri {
1288 assert!(t.abs() < 1e-10, "TRI of flat DEM should be 0, got {}", t);
1289 }
1290 }
1291
1292 #[test]
1293 fn test_tri_non_negative() {
1294 let dem = cone_dem(7, 7, 10.0);
1295 let tri = terrain_roughness_index(&dem);
1296 for &t in &tri {
1297 assert!(t >= 0.0, "TRI must be non-negative");
1298 }
1299 }
1300
1301 #[test]
1302 fn test_tpi_flat_near_zero() {
1303 let dem = flat_dem(7, 7, 100.0);
1304 let tpi = terrain_position_index(&dem, 2);
1305 for &v in &tpi {
1306 assert!(v.abs() < 1e-9, "TPI on flat DEM should be 0, got {}", v);
1307 }
1308 }
1309
1310 #[test]
1311 fn test_twi_positive() {
1312 let dem = tilted_dem(6, 6, 10.0, 0.3);
1313 let fdir = flow_direction_d8(&dem);
1314 let accum = flow_accumulation_d8(&dem, &fdir);
1315 let slope = compute_slope(&dem);
1316 let twi = topographic_wetness_index(&dem, &accum, &slope);
1317 for &v in &twi {
1318 assert!(v.is_finite(), "TWI must be finite");
1319 }
1320 }
1321
1322 #[test]
1323 fn test_viewshed_observer_visible_to_itself() {
1324 let dem = flat_dem(5, 5, 0.0);
1325 let obs = Observer {
1326 row: 2,
1327 col: 2,
1328 height_above_terrain: 1.8,
1329 max_range: 1000.0,
1330 };
1331 let vs = compute_viewshed(&dem, &obs);
1332 assert!(vs[2 * 5 + 2], "observer cell must always be visible");
1333 }
1334
1335 #[test]
1336 fn test_viewshed_flat_all_visible() {
1337 let dem = flat_dem(5, 5, 0.0);
1338 let obs = Observer {
1339 row: 2,
1340 col: 2,
1341 height_above_terrain: 1.8,
1342 max_range: 1000.0,
1343 };
1344 let vs = compute_viewshed(&dem, &obs);
1345 for (i, &v) in vs.iter().enumerate() {
1346 assert!(v, "flat terrain: cell {} should be visible", i);
1347 }
1348 }
1349
1350 #[test]
1351 fn test_slope_stability_flat_infinite_fs() {
1352 let dem = flat_dem(3, 3, 0.0);
1353 let slope = compute_slope(&dem);
1354 let params = SlopeStabilityParams::default();
1355 let fs = slope_stability_index(&dem, &slope, ¶ms);
1356 for &f in &fs {
1358 assert!(f >= 1.0, "FS on flat terrain should be large, got {}", f);
1359 }
1360 }
1361
1362 #[test]
1363 fn test_slope_stability_steep_low_fs() {
1364 let dem = tilted_dem(3, 3, 1.0, 10.0);
1366 let slope = compute_slope(&dem);
1367 let params = SlopeStabilityParams {
1368 cohesion: 0.0,
1369 friction_angle: 30.0_f64.to_radians(),
1370 soil_density: 1800.0,
1371 water_density: 1000.0,
1372 depth: 1.0,
1373 saturation: 1.0,
1374 };
1375 let fs = slope_stability_index(&dem, &slope, ¶ms);
1376 let has_unstable = fs.iter().any(|&f| f < 2.0 && f > 0.0);
1378 assert!(has_unstable, "steep saturated slope should show low FS");
1379 }
1380
1381 #[test]
1382 fn test_cos_incidence_overhead_sun() {
1383 let solar = SolarPosition {
1385 zenith: 0.0,
1386 azimuth: 0.0,
1387 };
1388 let ci = cos_incidence(&solar, 0.0, 0.0);
1389 assert!(
1390 (ci - 1.0).abs() < 1e-9,
1391 "overhead sun on flat terrain: cos_incidence = {}",
1392 ci
1393 );
1394 }
1395
1396 #[test]
1397 fn test_cos_incidence_below_horizon() {
1398 let solar = SolarPosition {
1400 zenith: PI * 0.6,
1401 azimuth: 0.0,
1402 };
1403 let ci = cos_incidence(&solar, 0.0, 0.0);
1404 assert!(ci >= 0.0, "cos_incidence must be non-negative");
1405 }
1406
1407 #[test]
1408 fn test_solar_irradiance_no_sun() {
1409 let dem = flat_dem(3, 3, 0.0);
1410 let slope = compute_slope(&dem);
1411 let aspect = compute_aspect(&dem);
1412 let solar = SolarPosition {
1413 zenith: PI / 2.0 + 0.1,
1414 azimuth: 0.0,
1415 }; let irr = terrain_solar_irradiance(&dem, &slope, &aspect, &solar, 800.0, 100.0);
1417 for &v in &irr {
1418 assert!(v >= 0.0, "irradiance must be non-negative");
1420 }
1421 }
1422
1423 #[test]
1424 fn test_raster_stats_uniform() {
1425 let data = vec![5.0f64; 100];
1426 let (min, max, mean, std) = raster_stats(&data);
1427 assert_eq!(min, 5.0);
1428 assert_eq!(max, 5.0);
1429 assert_eq!(mean, 5.0);
1430 assert!(std.abs() < 1e-12);
1431 }
1432
1433 #[test]
1434 fn test_normalise_raster_range() {
1435 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1436 let norm = normalise_raster(&data);
1437 assert!((norm[0]).abs() < 1e-9);
1438 assert!((norm[4] - 1.0).abs() < 1e-9);
1439 }
1440
1441 #[test]
1442 fn test_resample_dem_size() {
1443 let dem = flat_dem(8, 8, 100.0);
1444 let resampled = resample_dem(&dem, 2);
1445 assert_eq!(resampled.rows, 4);
1446 assert_eq!(resampled.cols, 4);
1447 }
1448
1449 #[test]
1450 fn test_fill_sinks_no_lower_than_input() {
1451 let dem = cone_dem(5, 5, 10.0);
1452 let filled = fill_sinks(&dem, 50);
1453 for (i, (&orig, &fill)) in dem.heights.iter().zip(filled.heights.iter()).enumerate() {
1454 assert!(
1455 fill >= orig - 1e-9,
1456 "filled cell {} should not be lower than original",
1457 i
1458 );
1459 }
1460 }
1461
1462 #[test]
1463 fn test_hypsometric_curve_bounds() {
1464 let dem = cone_dem(9, 9, 5.0);
1465 let curve = hypsometric_curve(&dem, 20);
1466 for &(h, a) in &curve {
1467 assert!((0.0..=1.0).contains(&h), "h out of [0,1]: {}", h);
1468 assert!(
1469 (0.0..=1.0).contains(&a),
1470 "area fraction out of [0,1]: {}",
1471 a
1472 );
1473 }
1474 }
1475
1476 #[test]
1477 fn test_hypsometric_integral_range() {
1478 let dem = cone_dem(9, 9, 5.0);
1479 let curve = hypsometric_curve(&dem, 50);
1480 let hi = hypsometric_integral(&curve);
1481 assert!(
1482 (0.0..=1.0).contains(&hi),
1483 "hypsometric integral out of [0,1]: {}",
1484 hi
1485 );
1486 }
1487
1488 #[test]
1489 fn test_classify_landform_ridge() {
1490 let lf = classify_landform(2.0, 2.0, 0.1);
1491 assert_eq!(lf, LandformClass::Ridge);
1492 }
1493
1494 #[test]
1495 fn test_classify_landform_valley() {
1496 let lf = classify_landform(-2.0, -2.0, 0.1);
1497 assert_eq!(lf, LandformClass::Valley);
1498 }
1499
1500 #[test]
1501 fn test_classify_landform_flat() {
1502 let lf = classify_landform(0.1, 0.1, 0.01);
1503 assert_eq!(lf, LandformClass::Flat);
1504 }
1505
1506 #[test]
1507 fn test_strahler_order_source_is_one() {
1508 let dem = tilted_dem(5, 10, 10.0, 1.0);
1509 let fdir = flow_direction_d8(&dem);
1510 let accum = flow_accumulation_d8(&dem, &fdir);
1511 let streams = extract_streams(&accum, 2);
1512 let ord = strahler_order(&dem, &fdir, &streams);
1513 for (i, (&s, &o)) in streams.iter().zip(ord.iter()).enumerate() {
1514 if s {
1515 assert!(o >= 1, "stream cell {} should have order >= 1", i);
1516 }
1517 }
1518 }
1519
1520 #[test]
1521 fn test_extract_contours_levels() {
1522 let dem = cone_dem(9, 9, 5.0);
1523 let levels = vec![80.0, 85.0, 90.0];
1524 let contours = extract_contours(&dem, &levels);
1525 assert!(!contours.is_empty(), "should find at least one contour");
1526 for c in &contours {
1527 assert!(
1528 !c.vertices.is_empty(),
1529 "contour at {} should have vertices",
1530 c.elevation
1531 );
1532 }
1533 }
1534
1535 #[test]
1536 fn test_ridge_valley_lines_length() {
1537 let dem = cone_dem(7, 7, 5.0);
1538 let rv = ridge_valley_lines(&dem);
1539 assert_eq!(rv.len(), dem.rows * dem.cols);
1540 }
1541
1542 #[test]
1543 fn test_dinf_flow_direction_range() {
1544 let dem = cone_dem(7, 7, 5.0);
1545 let fdir = flow_direction_dinf(&dem);
1546 for &a in &fdir {
1547 assert!(
1548 (0.0..=2.0 * PI + 0.01).contains(&a),
1549 "D-inf angle out of range: {}",
1550 a
1551 );
1552 }
1553 }
1554
1555 #[test]
1556 fn test_cumulative_viewshed_non_negative() {
1557 let dem = flat_dem(5, 5, 0.0);
1558 let observers = vec![
1559 Observer {
1560 row: 0,
1561 col: 0,
1562 height_above_terrain: 2.0,
1563 max_range: 500.0,
1564 },
1565 Observer {
1566 row: 4,
1567 col: 4,
1568 height_above_terrain: 2.0,
1569 max_range: 500.0,
1570 },
1571 ];
1572 let cv = cumulative_viewshed(&dem, &observers);
1573 for &v in &cv {
1574 assert!(v <= 2, "cumulative count cannot exceed number of observers");
1575 }
1576 }
1577
1578 #[test]
1579 fn test_fill_sinks_flat_unchanged() {
1580 let dem = flat_dem(5, 5, 100.0);
1581 let filled = fill_sinks(&dem, 10);
1582 for (&orig, &fill) in dem.heights.iter().zip(filled.heights.iter()) {
1583 assert!(
1584 (orig - fill).abs() < 1e-9,
1585 "flat DEM should be unchanged by fill_sinks"
1586 );
1587 }
1588 }
1589}