oxigdal_terrain/hydrology/
flow_direction.rs1use crate::error::{Result, TerrainError};
7use num_traits::Float;
8use scirs2_core::prelude::*;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FlowAlgorithm {
13 D8,
15 DInfinity,
17}
18
19pub(crate) const D8_DIRS: [(isize, isize, u8); 8] = [
22 (0, 1, 1), (1, 1, 2), (1, 0, 4), (1, -1, 8), (0, -1, 16), (-1, -1, 32), (-1, 0, 64), (-1, 1, 128), ];
31
32pub fn flow_direction_d8<T>(
37 dem: &Array2<T>,
38 cell_size: f64,
39 nodata: Option<T>,
40) -> Result<Array2<u8>>
41where
42 T: Float + Into<f64> + Copy,
43{
44 validate_inputs(dem, cell_size)?;
45
46 let (height, width) = dem.dim();
47 let mut flow_dir = Array2::zeros((height, width));
48
49 let diag_size = cell_size * 2.0_f64.sqrt();
51
52 for y in 0..height {
53 for x in 0..width {
54 let center = dem[[y, x]];
55
56 if let Some(nd) = nodata {
57 if is_nodata(center, nd) {
58 flow_dir[[y, x]] = 0;
59 continue;
60 }
61 }
62
63 let center_val = center.into();
64 let mut max_slope = f64::NEG_INFINITY;
65 let mut max_dir = 0_u8;
66
67 for (dy, dx, dir_code) in &D8_DIRS {
69 let ny = y as isize + dy;
70 let nx = x as isize + dx;
71
72 if ny >= 0 && ny < height as isize && nx >= 0 && nx < width as isize {
73 let neighbor = dem[[ny as usize, nx as usize]];
74
75 if let Some(nd) = nodata {
76 if is_nodata(neighbor, nd) {
77 continue;
78 }
79 }
80
81 let neighbor_val = neighbor.into();
82 let elevation_diff = center_val - neighbor_val;
83
84 let distance = if dy.abs() == 1 && dx.abs() == 1 {
86 diag_size
87 } else {
88 cell_size
89 };
90
91 let slope = elevation_diff / distance;
92
93 if slope > max_slope {
94 max_slope = slope;
95 max_dir = *dir_code;
96 }
97 }
98 }
99
100 flow_dir[[y, x]] = max_dir;
102 }
103 }
104
105 Ok(flow_dir)
106}
107
108pub fn flow_direction_dinf<T>(
113 dem: &Array2<T>,
114 cell_size: f64,
115 nodata: Option<T>,
116) -> Result<Array2<f64>>
117where
118 T: Float + Into<f64> + Copy,
119{
120 validate_inputs(dem, cell_size)?;
121
122 let (height, width) = dem.dim();
123 let mut flow_dir = Array2::zeros((height, width));
124
125 for y in 1..height - 1 {
126 for x in 1..width - 1 {
127 let center = dem[[y, x]];
128
129 if let Some(nd) = nodata {
130 if is_nodata(center, nd) {
131 flow_dir[[y, x]] = -1.0;
132 continue;
133 }
134 }
135
136 let z = center.into();
137
138 let e0 = dem[[y, x + 1]].into();
140 let e1 = dem[[y - 1, x + 1]].into();
141 let e2 = dem[[y - 1, x]].into();
142 let e3 = dem[[y - 1, x - 1]].into();
143 let e4 = dem[[y, x - 1]].into();
144 let e5 = dem[[y + 1, x - 1]].into();
145 let e6 = dem[[y + 1, x]].into();
146 let e7 = dem[[y + 1, x + 1]].into();
147
148 let facets = [
150 (e0, e1, 0.0), (e2, e1, std::f64::consts::FRAC_PI_4), (e2, e3, std::f64::consts::FRAC_PI_2), (e4, e3, 3.0 * std::f64::consts::FRAC_PI_4), (e4, e5, std::f64::consts::PI), (e6, e5, 5.0 * std::f64::consts::FRAC_PI_4), (e6, e7, 3.0 * std::f64::consts::FRAC_PI_2), (e0, e7, 7.0 * std::f64::consts::FRAC_PI_4), ];
159
160 let mut max_slope = f64::NEG_INFINITY;
161 let mut flow_angle = 0.0;
162
163 for (e1_val, e2_val, base_angle) in &facets {
164 let s1 = (z - e1_val) / cell_size;
165 let s2 = (z - e2_val) / (cell_size * 2.0_f64.sqrt());
166
167 let r = (s1 * s1 + s2 * s2).sqrt();
168 let angle = s2.atan2(s1);
169
170 if r > max_slope {
171 max_slope = r;
172 flow_angle = base_angle + angle;
173 }
174 }
175
176 if flow_angle < 0.0 {
178 flow_angle += 2.0 * std::f64::consts::PI;
179 }
180 if flow_angle >= 2.0 * std::f64::consts::PI {
181 flow_angle -= 2.0 * std::f64::consts::PI;
182 }
183
184 flow_dir[[y, x]] = flow_angle;
185 }
186 }
187
188 Ok(flow_dir)
189}
190
191pub fn flow_direction<T>(
193 dem: &Array2<T>,
194 cell_size: f64,
195 algorithm: FlowAlgorithm,
196 nodata: Option<T>,
197) -> Result<Array2<f64>>
198where
199 T: Float + Into<f64> + Copy,
200{
201 match algorithm {
202 FlowAlgorithm::D8 => {
203 let d8 = flow_direction_d8(dem, cell_size, nodata)?;
204 let (height, width) = d8.dim();
205 let mut result = Array2::zeros((height, width));
206 for y in 0..height {
207 for x in 0..width {
208 result[[y, x]] = d8[[y, x]] as f64;
209 }
210 }
211 Ok(result)
212 }
213 FlowAlgorithm::DInfinity => flow_direction_dinf(dem, cell_size, nodata),
214 }
215}
216
217fn validate_inputs<T>(dem: &Array2<T>, cell_size: f64) -> Result<()> {
220 let (height, width) = dem.dim();
221
222 if height < 3 || width < 3 {
223 return Err(TerrainError::InvalidDimensions { width, height });
224 }
225
226 if cell_size <= 0.0 {
227 return Err(TerrainError::InvalidCellSize { size: cell_size });
228 }
229
230 Ok(())
231}
232
233fn is_nodata<T: Float>(value: T, nodata: T) -> bool {
234 if value.is_nan() && nodata.is_nan() {
235 true
236 } else {
237 (value - nodata).abs() < T::epsilon()
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_d8_simple_slope() {
247 let mut dem = Array2::zeros((5, 5));
249 for y in 0..5 {
250 for x in 0..5 {
251 dem[[y, x]] = 100.0 - (x as f64) * 10.0; }
253 }
254
255 let flow_dir = flow_direction_d8(&dem, 10.0, None).expect("flow direction failed");
256
257 for y in 1..4 {
259 for x in 1..3 {
260 assert_eq!(flow_dir[[y, x]], 1, "should flow east");
261 }
262 }
263 }
264
265 #[test]
266 fn test_d8_directions() {
267 let mut dem = Array2::from_elem((5, 5), 100.0);
269 dem[[2, 2]] = 50.0; let flow_dir = flow_direction_d8(&dem, 10.0, None).expect("flow direction failed");
272
273 assert!(flow_dir[[2, 1]] > 0); assert!(flow_dir[[2, 3]] > 0); assert!(flow_dir[[1, 2]] > 0); assert!(flow_dir[[3, 2]] > 0); }
279
280 #[test]
281 fn test_dinf_continuous() {
282 let mut dem = Array2::zeros((5, 5));
283 for y in 0..5 {
284 for x in 0..5 {
285 dem[[y, x]] = 100.0 - (x as f64) * 10.0;
286 }
287 }
288
289 let flow_dir = flow_direction_dinf(&dem, 10.0, None).expect("D-Infinity failed");
290
291 for y in 1..4 {
293 for x in 1..4 {
294 assert!(flow_dir[[y, x]] >= 0.0 && flow_dir[[y, x]] < 2.0 * std::f64::consts::PI);
295 }
296 }
297 }
298}