hydro_analysis/
lib.rs

1//! # Hydro-analysis
2//!
3//! `hydro-analysis` provides functions for Hydrology DEM manipulation.  There are
4//! a couple generic functions for reading/writing raster files of any common
5//! primative type (which surprizingly I couldn't find anywhere else, unless you
6//! use GDAL which I am trying to avoid).  Also there are a couple functions based
7//! on [whitebox](https://github.com/jblindsay/whitebox-tools).  Whitebox is a
8//! command line tool, this provides functionality via functions so can be called
9//! from your code.
10//!
11//! ## Example of reading and writing rasters                                                         
12//!                                                                                                   
13//! ```                                                                                               
14//! let ifn = PathBuf::from("input.tif");                                                             
15//! let (d8, nd, crs, geo, gdir, proj) = rasterfile_to_array::<u8>(&ifn)?;                            
16//! /* do something with d8, or make a new array2 */                                                  
17//! let ofn = PathBuf::from("output.tif");                                                            
18//! if let Err(e) = array_to_rasterfile::<u8>(&d8, nd, &geo, &gdir, &proj, &ofn) {                    
19//!     eprintln!("Error occured while writing {}: {:?}", ofn.display(), e);                          
20//! }                                                                                                 
21//! ```                                                                                               
22//!                                                                                                   
23//! ## Example of filling and d8                                                                      
24//!
25//! ```
26//! use ndarray::Array2;
27//! use hydro_analysis::{fill_depressions, d8_pointer};
28//!
29//! let mut dem = Array2::from_shape_vec(
30//!     (3, 3),
31//!     vec![
32//!         10.0, 12.0, 10.0,
33//!         12.0, 9.0,  12.0,
34//!         10.0, 12.0, 10.0,
35//!     ],
36//! ).expect("Failed to create DEM");
37//!
38//! fill_depressions(&mut dem, -3.0, 8.0, 8.0, true);
39//! let (d8, d8_nd) = d8_pointer(&dem, -1.0, 8.0, 8.0);
40//! ```
41
42use rayon::prelude::*;
43use std::collections::{BinaryHeap, VecDeque};
44use std::cmp::Ordering;
45use std::cmp::Ordering::Equal;
46use ndarray::Array2;
47
48use std::{fs::File, f64, path::PathBuf};
49use thiserror::Error;
50use bytemuck::cast_slice;
51
52use tiff::decoder::DecodingResult;
53use tiff::encoder::compression::Deflate;
54use tiff::encoder::colortype::{Gray8,Gray16,Gray32,Gray64,Gray32Float,Gray64Float,GrayI8,GrayI16,GrayI32,GrayI64};
55use tiff::tags::Tag;
56use tiff::TiffFormatError;
57
58
59#[derive(Debug, Error)]
60pub enum RasterError {
61    #[error("TIFF error: {0}")]
62    Tiff(#[from] tiff::TiffError),
63
64    #[error("I/O error: {0}")]
65    Io(#[from] std::io::Error),
66
67    #[error("NDarray: {0}")]
68    Shape(#[from] ndarray::ShapeError),
69
70    #[error("Failed to parse nodata value")]
71    ParseIntError(#[from] std::num::ParseIntError),
72
73    #[error("Failed to parse nodata value")]
74    ParseFloatError(#[from] std::num::ParseFloatError),
75
76    #[error("Unsupported type: {0}")]
77    UnsupportedType(String)
78}
79
80/// Reads a single-band grayscale GeoTIFF raster file returning ndarry2 and metadata
81///
82/// # Type Parameters
83/// - `T`: The pixel value type.  u8, u16, i16, f64 etc
84///
85/// # Parameters
86/// - `fname`: Path to the input `.tif` GeoTIFF file.
87///
88/// # Returns
89/// A `Result` with a tuple containing:
90///     - `Array2<T>`: The raster data in a 2D array.
91///     - `T`: nodata
92///     - `u16`: CRS (e.g. 2193)
93///     - `[f64; 6]`: The affine GeoTransform in the format:
94///         `[origin_x, pixel_size_x, rotation_x, origin_y, rotation_y, pixel_size_y]`.
95///     - `Vec<u64>`: raw GeoKeyDirectoryTag values (needed for writing to file)
96///     - `String`: PROJ string (needed for writing to file)
97///
98/// # Errors
99///     - Returns `RasterError` variants if reading fails, the type conversion for data or metadata
100///       fails, or required tags are missing from the TIFF file.
101///
102/// # Example
103/// ```
104/// let path = PathBuf::from("input.tif");
105/// let (d8, nd, crs, geo, gdir, proj) = rasterfile_to_array::<u8>(&path)?;
106/// ```
107pub fn rasterfile_to_array<T>(fname: &PathBuf) -> Result<
108    (
109        Array2<T>,
110        T,          // nodata
111        u16,        // crs
112        [f64; 6],   // geo transform [start_x, psize_x, rotation, starty, rotation, psize_y]
113        Vec<u64>,   // geo dir, it has the crs in it
114        String      // the projection string
115    ),
116    RasterError
117>
118    where T: std::str::FromStr + num::FromPrimitive,
119          <T as std::str::FromStr>::Err: std::fmt::Debug,
120          RasterError: std::convert::From<<T as std::str::FromStr>::Err>
121{
122    // Open the file
123    let file = File::open(fname)?;
124
125    // Create a TIFF decoder
126    let mut decoder = tiff::decoder::Decoder::new(file)?;
127    decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
128
129    // Read the image dimensions
130    let (width, height) = decoder.dimensions()?;
131
132    fn estr<T>(etype: &'static str) -> RasterError {
133        RasterError::Tiff(TiffFormatError::Format(format!("Raster is {}, I was expecting {}", etype, std::any::type_name::<T>()).into()).into())
134    }
135    let data: Vec<T> = match decoder.read_image()? {
136        DecodingResult::I8(buf)  => buf.into_iter().map(|v| <T>::from_i8(v).ok_or(estr::<T>("I8"))).collect::<Result<_, _>>(),
137        DecodingResult::I16(buf) => buf.into_iter().map(|v| <T>::from_i16(v).ok_or(estr::<T>("I16"))).collect::<Result<_, _>>(),
138        DecodingResult::I32(buf) => buf.into_iter().map(|v| <T>::from_i32(v).ok_or(estr::<T>("I32"))).collect::<Result<_, _>>(),
139        DecodingResult::I64(buf) => buf.into_iter().map(|v| <T>::from_i64(v).ok_or(estr::<T>("I64"))).collect::<Result<_, _>>(),
140        DecodingResult::U8(buf)  => buf.into_iter().map(|v| <T>::from_u8(v).ok_or(estr::<T>("U8"))).collect::<Result<_, _>>(),
141        DecodingResult::U16(buf) => buf.into_iter().map(|v| <T>::from_u16(v).ok_or(estr::<T>("U16"))).collect::<Result<_, _>>(),
142        DecodingResult::U32(buf) => buf.into_iter().map(|v| <T>::from_u32(v).ok_or(estr::<T>("U32"))).collect::<Result<_, _>>(),
143        DecodingResult::U64(buf) => buf.into_iter().map(|v| <T>::from_u64(v).ok_or(estr::<T>("U64"))).collect::<Result<_, _>>(),
144        DecodingResult::F32(buf) => buf.into_iter().map(|v| <T>::from_f32(v).ok_or(estr::<T>("F32"))).collect::<Result<_, _>>(),
145        DecodingResult::F64(buf) => buf.into_iter().map(|v| <T>::from_f64(v).ok_or(estr::<T>("F64"))).collect::<Result<_, _>>(),
146    }?;
147
148    // Convert the flat vector into an ndarray::Array2
149    let array: Array2<T> = Array2::from_shape_vec((height as usize, width as usize), data)?;
150
151    // nodata value
152    let nodata: T = decoder.get_tag_ascii_string(Tag::GdalNodata)?.trim().parse::<T>()?;
153
154    // pixel scale [pixel scale x, pixel scale y, ...]
155    let pscale: Vec<f64> = decoder.get_tag_f64_vec(Tag::ModelPixelScaleTag)?.into_iter().collect();
156
157    // tie point [0 0 0 startx starty 0]
158    let tie: Vec<f64>  = decoder.get_tag_f64_vec(Tag::ModelTiepointTag)?.into_iter().collect();
159
160    // transform, the zeros are the rotations [start x, x pixel size, 0, start y, 0, y pixel size]
161    let geotrans: [f64; 6] = [tie[3], pscale[0], 0.0, tie[4], 0.0, -pscale[1]];
162
163    let projection: String = decoder.get_tag_ascii_string(Tag::GeoAsciiParamsTag)?;
164    let geokeydir: Vec<u64> = decoder .get_tag_u64_vec(Tag::GeoKeyDirectoryTag)?;
165
166    // try and get the CRS out of the geokeydir, it is the bit after 3072
167    let crs = geokeydir.windows(4).find(|w| w[0] == 3072).map(|w| w[3])
168        .ok_or(RasterError::Tiff(tiff::TiffFormatError::InvalidTagValueType(Tag::GeoKeyDirectoryTag).into()))? as u16;
169
170    Ok((array, nodata, crs, geotrans, geokeydir, projection))
171}
172
173/// Writes a 2D array of values to a GeoTIFF raster with geo metadata.
174///
175/// # Type Parameters
176/// - `T`: The element type of the array, which must implement `bytemuck::Pod`
177/// (for safe byte casting) and `ToString` (for writing NoData values to
178/// metadata).
179///
180/// # Parameters
181/// 	- `data`: A 2D array (`ndarray::Array2<T>`) containing raster pixel values.
182/// 	- `nd`: NoData value
183/// 	- `geotrans`: A 6-element array defining the affine geotransform:
184/// 	    `[origin_x, pixel_size_x, rotation_x, origin_y, rotation_y, pixel_size_y]`.
185/// 	- `geokeydir`: &[u64] the GeoKeyDirectoryTag (best got from reading a raster)
186/// 	- `proj`: PROJ string (best got from reading a raster)
187/// 	- `outfile`: The path to the output `.tif` file.
188///
189/// # Returns
190/// Ok() or a `RasterError`
191///
192/// # Errors
193/// - Returns `RasterError::UnsupportedType` if `T` can't be mapped to a TIFF format.
194/// - Propagates I/O and TIFF writing errors
195///
196/// # Example
197/// ```
198/// let path = PathBuf::from("input.tif");
199/// let (d8, nd, crs, geo, gdir, proj) = rasterfile_to_array::<u8>(&path)?;
200/// /* do something with d8, or make a new array2 */
201/// let out = PathBuf::from("output.tif");
202/// if let Err(e) = array_to_rasterfile::<u8>(&d8, nd, &geo, &gdir, &proj, &out) {
203///		eprintln!("Error occured while writing {}: {:?}", out.display(), e);
204/// }
205/// ```
206pub fn array_to_rasterfile<T>(
207    data: &Array2<T>,
208    nd: T,                      // nodata
209    geotrans: &[f64; 6],        // geo transform [start_x, psize_x, rotation, starty, rotation, psize_y]
210    geokeydir: &[u64],          // geo dir, it has the crs in it
211    proj: &str,                 // the projection string
212    outfile: &PathBuf
213) -> Result<(), RasterError>
214    where T: bytemuck::Pod + ToString
215{
216    let (nrows, ncols) = (data.nrows(), data.ncols());
217
218    let fh = File::create(outfile)?;
219    let mut encoder = tiff::encoder::TiffEncoder::new(fh)?;
220
221    // Because image doesn't have traits I couldn't figure out how to do this with generics
222    // This macro takes the tiff colortype
223    macro_rules! writit {
224        ($pix:ty) => {{
225            let mut image = encoder.new_image_with_compression::<$pix, Deflate>(ncols as u32, nrows as u32, Deflate::default())?;
226            image.encoder().write_tag(Tag::GdalNodata, &nd.to_string()[..])?;
227            image.encoder().write_tag(Tag::ModelPixelScaleTag, &[geotrans[1], geotrans[5], 0.0][..])?;
228            image.encoder().write_tag(Tag::ModelTiepointTag, &[0.0, 0.0, 0.0, geotrans[0], geotrans[3], 0.0][..])?;
229            image.encoder().write_tag(Tag::GeoKeyDirectoryTag, geokeydir)?;
230            image.encoder().write_tag(Tag::GeoAsciiParamsTag, &proj)?;
231            image.write_data(cast_slice(data.as_slice().unwrap()))?;
232        }};
233    }
234
235    match std::any::TypeId::of::<T>() {
236        id if id == std::any::TypeId::of::<u8>()  => writit!(Gray8),
237        id if id == std::any::TypeId::of::<u16>() => writit!(Gray16),
238        id if id == std::any::TypeId::of::<u32>() => writit!(Gray32),
239        id if id == std::any::TypeId::of::<u64>() => writit!(Gray64),
240        id if id == std::any::TypeId::of::<f32>() => writit!(Gray32Float),
241        id if id == std::any::TypeId::of::<f64>() => writit!(Gray64Float),
242        id if id == std::any::TypeId::of::<i8>()  => writit!(GrayI8),
243        id if id == std::any::TypeId::of::<i16>() => writit!(GrayI16),
244        id if id == std::any::TypeId::of::<i32>() => writit!(GrayI32),
245        id if id == std::any::TypeId::of::<i64>() => writit!(GrayI64),
246        _ => return Err(RasterError::UnsupportedType(format!("Cannot handle type {}", std::any::type_name::<T>())))
247    };
248
249    Ok(())
250}
251
252
253#[derive(PartialEq, Debug)]
254struct GridCell {
255    row: usize,
256    column: usize,
257    priority: f64,
258}
259
260impl Eq for GridCell {}
261
262impl PartialOrd for GridCell {
263    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
264        other.priority.partial_cmp(&self.priority)
265    }
266}
267
268impl Ord for GridCell {
269    fn cmp(&self, other: &Self) -> Ordering {
270        self.partial_cmp(other).unwrap()
271    }
272}
273
274#[derive(PartialEq, Debug)]
275struct GridCell2 {
276    row: usize,
277    column: usize,
278    z: f64,
279    priority: f64,
280}
281
282impl Eq for GridCell2 {}
283
284impl PartialOrd for GridCell2 {
285    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
286        other.priority.partial_cmp(&self.priority)
287    }
288}
289
290impl Ord for GridCell2 {
291    fn cmp(&self, other: &Self) -> Ordering {
292        self.partial_cmp(other).unwrap()
293    }
294}
295
296
297/// Fills depressions (sinks) in a digital elevation model (DEM).
298///
299/// More-or-less the contents of
300/// [whitebox fill_depressions](https://github.com/jblindsay/whitebox-tools/blob/master/whitebox-tools-app/src/tools/hydro_analysis/fill_depressions.rs)
301///
302/// This function modifies the input `dem` to ensure that all depressions (local minima that do not
303/// drain) are removed, making the surface hydrologically correct. It also considers no-data values
304/// and can optionally fix flat areas.
305///
306/// # Parameters
307///
308/// - `dem`: A mutable reference to a 2D array (`Array2<f64>`) representing the elevation data.
309/// - `nodata`: The value representing no-data cells in the DEM.
310/// - `resx`: The horizontal resolution (grid spacing in the x-direction).
311/// - `resy`: The vertical resolution (grid spacing in the y-direction).
312/// - `fix_flats`: A boolean flag to determine whether flat areas should be slightly sloped.
313///
314/// # Example
315///
316/// ```
317/// use ndarray::Array2;
318/// use hydro_analysis::fill_depressions;
319///
320/// let mut dem = Array2::from_shape_vec(
321///     (3, 3),
322///     vec![
323///         10.0, 12.0, 10.0,
324///         12.0, 9.0,  12.0,
325///         10.0, 12.0, 10.0,
326///     ],
327/// ).expect("Failed to create DEM");
328///
329/// fill_depressions(&mut dem, -3.0, 8.0, 8.0, true);
330/// ```
331pub fn fill_depressions(
332    dem: &mut Array2<f64>, nodata: f64, resx: f64, resy: f64, fix_flats: bool
333)
334{
335    let (rows, columns) = (dem.nrows(), dem.ncols());
336    let small_num = {
337        let diagres = (resx * resx + resy * resy).sqrt();
338        let elev_digits = (dem.iter().cloned().fold(f64::NEG_INFINITY, f64::max) as i64).to_string().len();
339        let elev_multiplier = 10.0_f64.powi((9 - elev_digits) as i32);
340        1.0_f64 / elev_multiplier as f64 * diagres.ceil()
341    };
342
343    let dx = [1, 1, 1, 0, -1, -1, -1, 0];
344    let dy = [-1, 0, 1, 1, 1, 0, -1, -1];
345
346    // Find pit cells. This step is parallelizable.
347	let mut pits: Vec<_> = (1..rows - 1)
348		.into_par_iter()
349		.flat_map(|row| {
350			let mut local_pits = Vec::new();
351			for col in 1..columns - 1 {
352				let z = dem[[row, col]];
353				if z == nodata {
354					continue;
355				}
356				let mut apit = true;
357            	// is anything lower than me?
358				for n in 0..8 {
359					let zn = dem[[(row as isize + dy[n]) as usize, (col as isize + dx[n]) as usize]];
360					if zn < z || zn == nodata {
361						apit = false;
362						break;
363					}
364				}
365				// no, so I am a pit
366				if apit {
367					local_pits.push((row, col, z));
368				}
369			}
370			local_pits
371		}).collect();
372
373    // Now we need to perform an in-place depression filling
374    let mut minheap = BinaryHeap::new();
375    let mut minheap2 = BinaryHeap::new();
376    let mut visited = Array2::<u8>::zeros((rows, columns));
377    let mut flats = Array2::<u8>::zeros((rows, columns));
378    let mut possible_outlets = vec![];
379    let mut queue = VecDeque::new();
380
381    // go through pits from highest to lowest
382    pits.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(Equal));
383    while let Some(cell) = pits.pop() {
384        let row: usize = cell.0;
385        let col: usize = cell.1;
386
387        // if it's already in a solved site, don't do it a second time.
388        if flats[[row, col]] != 1 {
389            // First there is a priority region-growing operation to find the outlets.
390            minheap.clear();
391            minheap.push(GridCell {
392                row: row,
393                column: col,
394                priority: dem[[row, col]],
395            });
396            visited[[row, col]] = 1;
397            let mut outlet_found = false;
398            let mut outlet_z = f64::INFINITY;
399            if !queue.is_empty() {
400                queue.clear();
401            }
402            while let Some(cell2) = minheap.pop() {
403                let z = cell2.priority;
404                if outlet_found && z > outlet_z {
405                    break;
406                }
407                if !outlet_found {
408                    for n in 0..8 {
409                        let cn: usize = (cell2.column as isize + dx[n]) as usize;
410                        let rn: usize = (cell2.row as isize + dy[n]) as usize;
411                        if rn < rows && cn < columns && visited[[rn, cn]] == 0 {
412                            let zn = dem[[rn, cn]];
413                            if !outlet_found {
414                                if zn >= z && zn != nodata {
415                                    minheap.push(GridCell {
416                                        row: rn,
417                                        column: cn,
418                                        priority: zn,
419                                    });
420                                    visited[[rn, cn]] = 1;
421                                } else if zn != nodata {
422                                    // zn < z
423                                    // 'cell' has a lower neighbour that hasn't already passed through minheap.
424                                    // Therefore, 'cell' is a pour point cell.
425                                    outlet_found = true;
426                                    outlet_z = z;
427                                    queue.push_back((cell2.row, cell2.column));
428                                    possible_outlets.push((cell2.row, cell2.column));
429                                }
430                            } else if zn == outlet_z {
431                                // We've found the outlet but are still looking for additional depression cells.
432                                minheap.push(GridCell {
433                                    row: rn,
434                                    column: cn,
435                                    priority: zn,
436                                });
437                                visited[[rn, cn]] = 1;
438                            }
439                        }
440                    }
441                } else {
442                    // We've found the outlet but are still looking for additional depression cells and potential outlets.
443                    if z == outlet_z {
444                        let mut anoutlet = false;
445                        for n in 0..8 {
446                            let cn: usize = (cell2.column as isize + dx[n]) as usize;
447                            let rn: usize = (cell2.row as isize + dy[n]) as usize;
448                            if rn < rows && cn < columns && visited[[rn, cn]] == 0 {
449                                let zn = dem[[rn, cn]];
450                                if zn < z {
451                                    anoutlet = true;
452                                } else if zn == outlet_z {
453                                    minheap.push(GridCell {
454                                        row: rn,
455                                        column: cn,
456                                        priority: zn,
457                                    });
458                                    visited[[rn, cn]] = 1;
459                                }
460                            }
461                        }
462                        if anoutlet {
463                            queue.push_back((cell2.row, cell2.column));
464                            possible_outlets.push((cell2.row, cell2.column));
465                        } else {
466                            visited[[cell2.row, cell2.column]] = 1;
467                        }
468                    }
469                }
470            }
471
472            if outlet_found {
473                // Now that we have the outlets, raise the interior of the depression.
474                // Start from the outlets.
475                while let Some(cell2) = queue.pop_front() {
476                    for n in 0..8 {
477                        let rn: usize = (cell2.0 as isize + dy[n]) as usize;
478                        let cn: usize = (cell2.1 as isize + dx[n]) as usize;
479                        if rn < rows && cn < columns && visited[[rn, cn]] == 1 {
480                            visited[[rn, cn]] = 0;
481                            queue.push_back((rn, cn));
482                            let z = dem[[rn, cn]];
483                            if z < outlet_z {
484                                dem[[rn, cn]] = outlet_z;
485                                flats[[rn, cn]] = 1;
486                            } else if z == outlet_z {
487                                flats[[rn, cn]] = 1;
488                            }
489                        }
490                    }
491                }
492            } else {
493                queue.push_back((row, col)); // start at the pit cell and clean up visited
494                while let Some(cell2) = queue.pop_front() {
495                    for n in 0..8 {
496                        let rn: usize = (cell2.0 as isize + dy[n]) as usize;
497                        let cn: usize = (cell2.1 as isize + dx[n]) as usize;
498                        if visited[[rn, cn]] == 1 {
499                            visited[[rn, cn]] = 0;
500                            queue.push_back((rn, cn));
501                        }
502                    }
503                }
504            }
505        }
506
507    }
508
509    drop(visited);
510
511    //let (mut col, mut row): (isize, isize);
512    //let (mut rn, mut cn): (isize, isize);
513    //let (mut z, mut zn): (f64, f64);
514    //let mut flag: bool;
515
516    if small_num > 0.0 && fix_flats {
517        // Some of the potential outlets really will have lower cells.
518        minheap.clear();
519        while let Some(cell) = possible_outlets.pop() {
520            let z = dem[[cell.0, cell.1]];
521            let mut anoutlet = false;
522            for n in 0..8 {
523                let rn: usize = (cell.0 as isize + dy[n]) as usize;
524                let cn: usize = (cell.1 as isize + dx[n]) as usize;
525                if rn >= rows || cn >= columns {
526                    break;
527                }
528                let zn = dem[[rn, cn]];
529                if zn < z && zn != nodata {
530                    anoutlet = true;
531                    break;
532                }
533            }
534            if anoutlet {
535                minheap.push(GridCell {
536                    row: cell.0,
537                    column: cell.1,
538                    priority: z,
539                });
540            }
541        }
542
543        let mut outlets = vec![];
544        while let Some(cell) = minheap.pop() {
545            if flats[[cell.row, cell.column]] != 3 {
546                let z = dem[[cell.row, cell.column]];
547                flats[[cell.row, cell.column]] = 3;
548                if !outlets.is_empty() {
549                    outlets.clear();
550                }
551                outlets.push(cell);
552                // Are there any other outlet cells at the same elevation (likely for the same feature)
553                let mut flag = true;
554                while flag {
555                    match minheap.peek() {
556                        Some(cell2) => {
557                            if cell2.priority == z {
558                                flats[[cell2.row, cell2.column]] = 3;
559                                outlets
560                                    .push(minheap.pop().expect("Error during pop operation."));
561                            } else {
562                                flag = false;
563                            }
564                        }
565                        None => {
566                            flag = false;
567                        }
568                    }
569                }
570                if !minheap2.is_empty() {
571                    minheap2.clear();
572                }
573                for cell2 in &outlets {
574                    let z = dem[[cell2.row, cell2.column]];
575                    for n in 0..8 {
576                        let cn: usize = (cell2.column as isize + dx[n]) as usize;
577                        let rn: usize = (cell2.row as isize + dy[n]) as usize;
578                        if rn < rows && cn < columns && flats[[rn, cn]] != 3 {
579                            let zn = dem[[rn, cn]];
580                            if zn == z && zn != nodata {
581                                minheap2.push(GridCell2 {
582                                    row: rn,
583                                    column: cn,
584                                    z: z,
585                                    priority: dem[[rn, cn]],
586                                });
587                                dem[[rn, cn]] = z + small_num;
588                                flats[[rn, cn]] = 3;
589                            }
590                        }
591                    }
592                }
593                // Now fix the flats
594                while let Some(cell2) = minheap2.pop() {
595                    let z = dem[[cell2.row, cell2.column]];
596                    for n in 0..8 {
597                        let cn: usize = (cell2.column as isize + dx[n]) as usize;
598                        let rn: usize = (cell2.row as isize + dy[n]) as usize;
599                        if rn < rows && cn < columns && flats[[rn, cn]] != 3 {
600                            let zn = dem[[rn, cn]];
601                            if zn < z + small_num && zn >= cell2.z && zn != nodata {
602                                minheap2.push(GridCell2 {
603                                    row: rn,
604                                    column: cn,
605                                    z: cell2.z,
606                                    priority: dem[[rn, cn]],
607                                });
608                                dem[[rn, cn]] = z + small_num;
609                                flats[[rn, cn]] = 3;
610                            }
611                        }
612                    }
613                }
614            }
615
616        }
617    }
618}
619
620/// Calculates the D8 flow direction from a digital elevation model (DEM).
621///
622/// More-or-less the contents of
623/// [whitebox d8_pointer](https://github.com/jblindsay/whitebox-tools/blob/master/whitebox-tools-app/src/tools/hydro_analysis/d8_pointer.rs)
624///
625/// This function computes the D8 flow direction for each cell in the provided DEM:
626///
627/// | .  |  .  |  . |
628/// |:--:|:---:|:--:|
629/// | 64 | 128 | 1  |
630/// | 32 |  0  | 2  |
631/// | 16 |  8  | 4  |
632///
633/// Grid cells that have no lower neighbours are assigned a flow direction of zero. In a DEM that
634/// has been pre-processed to remove all depressions and flat areas, this condition will only occur
635/// along the edges of the grid.
636///
637/// Grid cells possessing the NoData value in the input DEM are assigned the NoData value in the
638/// output image.
639///
640/// # Parameters
641/// - `dem`: A 2D array representing the digital elevation model (DEM)
642/// - `nodata`: The nodata in the DEM
643/// - `resx`: The resolution of the DEM in the x-direction in meters
644/// - `resy`: The resolution of the DEM in the y-direction in meters
645///
646/// # Returns
647/// - A tuple containing:
648///     - An `Array2<u8>` representing the D8 flow directions for each cell.
649///     - A `u8` nodata value (255)
650///
651/// # Example
652/// ```rust
653/// let dem = Array2::from_shape_vec(
654///     (3, 3),
655///     vec![
656///         10.0, 12.0, 10.0,
657///         12.0, 13.0, 12.0,
658///         10.0, 12.0, 10.0,
659///     ],
660/// ).expect("Failed to create DEM");
661/// let nodata = -9999.0;
662/// let resx = 8.0;
663/// let resy = 8.0;
664/// let (d8, nd) = d8_pointer(&dem, nodata, resx, resy);
665/// ```
666pub fn d8_pointer(dem: &Array2<f64>, nodata: f64, resx: f64, resy: f64) -> (Array2<u8>, u8)
667{
668    let (nrows, ncols) = (dem.nrows(), dem.ncols());
669    let out_nodata: u8 = 255;
670    let mut d8: Array2<u8> = Array2::from_elem((nrows, ncols), out_nodata);
671
672    let diag = (resx * resx + resy * resy).sqrt();
673    let grid_lengths = [diag, resx, diag, resy, diag, resx, diag, resy];
674
675    let dx = [1, 1, 1, 0, -1, -1, -1, 0];
676    let dy = [-1, 0, 1, 1, 1, 0, -1, -1];
677
678    d8.axis_iter_mut(ndarray::Axis(0))
679        .into_par_iter()
680        .enumerate()
681        .for_each(|(row, mut d8_row)| {
682            for col in 0..ncols {
683                let z = dem[[row, col]];
684                if z == nodata {
685                    continue;
686                }
687
688                let mut dir = 0;
689                let mut max_slope = f64::MIN;
690                for i in 0..8 {
691                    let rn: isize = row as isize + dy[i];
692                    let cn: isize = col as isize + dx[i];
693                    if rn < 0 || rn >= nrows as isize || cn < 0 || cn >= ncols as isize {
694                        continue;
695                    }
696                    let z_n = dem[[rn as usize, cn as usize]];
697                    if z_n != nodata {
698                        let slope = (z - z_n) / grid_lengths[i];
699                        if slope > max_slope && slope > 0.0 {
700                            max_slope = slope;
701                            dir = i;
702                        }
703                    }
704                }
705
706                if max_slope >= 0.0 {
707                    d8_row[col] = 1 << dir;
708                } else {
709                    d8_row[col] = 0u8;
710                }
711            }
712        });
713
714    return (d8, out_nodata);
715}
716
717