1#![allow(clippy::needless_range_loop, clippy::type_complexity)]
2#![allow(clippy::manual_range_contains)]
3#![allow(dead_code)]
13
14#[derive(Debug, Clone)]
25pub struct GeoTiffReader {
26 pub width: usize,
28 pub height: usize,
30 pub bands: usize,
32 pub transform: [f64; 6],
34 data: Vec<Vec<f64>>,
36}
37
38impl GeoTiffReader {
39 pub fn new(
41 width: usize,
42 height: usize,
43 bands: usize,
44 transform: [f64; 6],
45 data: Vec<Vec<f64>>,
46 ) -> Self {
47 Self {
48 width,
49 height,
50 bands,
51 transform,
52 data,
53 }
54 }
55
56 pub fn pixel_to_world(&self, px: usize, py: usize) -> [f64; 2] {
59 let t = &self.transform;
60 let lon = t[0] + px as f64 * t[1] + py as f64 * t[2];
61 let lat = t[3] + px as f64 * t[4] + py as f64 * t[5];
62 [lon, lat]
63 }
64
65 pub fn world_to_pixel(&self, lon: f64, lat: f64) -> (usize, usize) {
68 let t = &self.transform;
69 let det = t[1] * t[5] - t[2] * t[4];
70 let det = if det.abs() < 1e-15 { 1e-15 } else { det };
71 let dx = lon - t[0];
72 let dy = lat - t[3];
73 let px = (t[5] * dx - t[2] * dy) / det;
74 let py = (t[1] * dy - t[4] * dx) / det;
75 let px = px
76 .round()
77 .max(0.0)
78 .min((self.width.saturating_sub(1)) as f64) as usize;
79 let py = py
80 .round()
81 .max(0.0)
82 .min((self.height.saturating_sub(1)) as f64) as usize;
83 (px, py)
84 }
85
86 pub fn read_band(&self, band: usize) -> Vec<f64> {
90 if band < self.data.len() {
91 self.data[band].clone()
92 } else {
93 vec![]
94 }
95 }
96
97 pub fn elevation_at(&self, lon: f64, lat: f64) -> f64 {
99 if self.data.is_empty() || self.data[0].is_empty() {
100 return 0.0;
101 }
102 let (px, py) = self.world_to_pixel(lon, lat);
103 let idx = py * self.width + px;
104 *self.data[0].get(idx).unwrap_or(&0.0)
105 }
106
107 pub fn bounding_box(&self) -> [f64; 4] {
109 let tl = self.pixel_to_world(0, 0);
110 let br = self.pixel_to_world(self.width, self.height);
111 [
112 tl[0].min(br[0]),
113 tl[1].min(br[1]),
114 tl[0].max(br[0]),
115 tl[1].max(br[1]),
116 ]
117 }
118}
119
120#[derive(Debug, Clone)]
126pub struct RasterData {
127 pub data: Vec<Vec<f64>>,
129 pub width: usize,
131 pub height: usize,
133 pub nodata: f64,
135}
136
137impl RasterData {
138 pub fn new(data: Vec<Vec<f64>>, nodata: f64) -> Self {
140 let height = data.len();
141 let width = if height > 0 { data[0].len() } else { 0 };
142 Self {
143 data,
144 width,
145 height,
146 nodata,
147 }
148 }
149
150 pub fn statistics(&self) -> (f64, f64, f64, f64) {
152 let valid: Vec<f64> = self
153 .data
154 .iter()
155 .flat_map(|row| row.iter())
156 .copied()
157 .filter(|&v| (v - self.nodata).abs() > 1e-10)
158 .collect();
159 if valid.is_empty() {
160 return (0.0, 0.0, 0.0, 0.0);
161 }
162 let min = valid.iter().cloned().fold(f64::INFINITY, f64::min);
163 let max = valid.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
164 let mean = valid.iter().sum::<f64>() / valid.len() as f64;
165 let variance =
166 valid.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / valid.len() as f64;
167 (min, max, mean, variance.sqrt())
168 }
169
170 pub fn clip_to_bbox(&self, bbox: [f64; 4]) -> Self {
174 let col_start = (bbox[0] as usize).min(self.width);
175 let row_start = (bbox[1] as usize).min(self.height);
176 let col_end = (bbox[2] as usize).min(self.width);
177 let row_end = (bbox[3] as usize).min(self.height);
178 let clipped: Vec<Vec<f64>> = self.data[row_start..row_end]
179 .iter()
180 .map(|row| row[col_start..col_end].to_vec())
181 .collect();
182 RasterData::new(clipped, self.nodata)
183 }
184
185 pub fn resample(&self, factor: f64) -> Self {
187 let factor = factor.max(0.01);
188 let new_h = ((self.height as f64) * factor).round() as usize;
189 let new_w = ((self.width as f64) * factor).round() as usize;
190 let mut out = vec![vec![self.nodata; new_w]; new_h];
191 for r in 0..new_h {
192 for c in 0..new_w {
193 let src_r = ((r as f64) / factor) as usize;
194 let src_c = ((c as f64) / factor) as usize;
195 let src_r = src_r.min(self.height.saturating_sub(1));
196 let src_c = src_c.min(self.width.saturating_sub(1));
197 out[r][c] = self.data[src_r][src_c];
198 }
199 }
200 RasterData::new(out, self.nodata)
201 }
202
203 pub fn hillshade(&self, azimuth: f64, elevation: f64) -> Vec<Vec<f64>> {
205 let az_rad = azimuth.to_radians();
206 let el_rad = elevation.to_radians();
207 let sun = [
208 -el_rad.cos() * az_rad.sin(),
209 -el_rad.cos() * az_rad.cos(),
210 el_rad.sin(),
211 ];
212 let slope_grid = self.slope();
213 let aspect_grid = self.aspect();
214 let mut hs = vec![vec![0.0_f64; self.width]; self.height];
215 for r in 0..self.height {
216 for c in 0..self.width {
217 let s = slope_grid[r][c].to_radians();
218 let a = aspect_grid[r][c].to_radians();
219 let nx = s.sin() * a.sin();
220 let ny = s.sin() * a.cos();
221 let nz = s.cos();
222 let dot = (nx * sun[0] + ny * sun[1] + nz * sun[2]).max(0.0);
223 hs[r][c] = dot;
224 }
225 }
226 hs
227 }
228
229 pub fn slope(&self) -> Vec<Vec<f64>> {
231 let mut out = vec![vec![0.0_f64; self.width]; self.height];
232 for r in 1..self.height.saturating_sub(1) {
233 for c in 1..self.width.saturating_sub(1) {
234 let dzdx =
235 (self.data[r - 1][c + 1] + 2.0 * self.data[r][c + 1] + self.data[r + 1][c + 1]
236 - self.data[r - 1][c - 1]
237 - 2.0 * self.data[r][c - 1]
238 - self.data[r + 1][c - 1])
239 / 8.0;
240 let dzdy =
241 (self.data[r + 1][c - 1] + 2.0 * self.data[r + 1][c] + self.data[r + 1][c + 1]
242 - self.data[r - 1][c - 1]
243 - 2.0 * self.data[r - 1][c]
244 - self.data[r - 1][c + 1])
245 / 8.0;
246 out[r][c] = (dzdx * dzdx + dzdy * dzdy).sqrt().atan().to_degrees();
247 }
248 }
249 out
250 }
251
252 pub fn aspect(&self) -> Vec<Vec<f64>> {
254 let mut out = vec![vec![0.0_f64; self.width]; self.height];
255 for r in 1..self.height.saturating_sub(1) {
256 for c in 1..self.width.saturating_sub(1) {
257 let dzdx =
258 (self.data[r - 1][c + 1] + 2.0 * self.data[r][c + 1] + self.data[r + 1][c + 1]
259 - self.data[r - 1][c - 1]
260 - 2.0 * self.data[r][c - 1]
261 - self.data[r + 1][c - 1])
262 / 8.0;
263 let dzdy =
264 (self.data[r + 1][c - 1] + 2.0 * self.data[r + 1][c] + self.data[r + 1][c + 1]
265 - self.data[r - 1][c - 1]
266 - 2.0 * self.data[r - 1][c]
267 - self.data[r - 1][c + 1])
268 / 8.0;
269 let aspect = (-dzdy).atan2(dzdx).to_degrees();
270 out[r][c] = if aspect < 0.0 { aspect + 360.0 } else { aspect };
271 }
272 }
273 out
274 }
275}
276
277#[derive(Debug, Clone)]
283pub struct Dem {
284 pub raster: RasterData,
286 pub cell_size: f64,
288}
289
290impl Dem {
291 pub fn new(raster: RasterData, cell_size: f64) -> Self {
293 Self { raster, cell_size }
294 }
295
296 pub fn flow_direction(&self) -> Vec<Vec<u8>> {
300 let h = self.raster.height;
301 let w = self.raster.width;
302 let dirs: [(i32, i32, u8); 8] = [
304 (0, 1, 1), (1, 1, 2), (1, 0, 4), (1, -1, 8), (0, -1, 16), (-1, -1, 32), (-1, 0, 64), (-1, 1, 128), ];
313 let mut out = vec![vec![0u8; w]; h];
314 for r in 0..h {
315 for c in 0..w {
316 let z = self.raster.data[r][c];
317 let mut max_drop = 0.0_f64;
318 let mut best_dir = 1u8;
319 for &(dr, dc, code) in &dirs {
320 let nr = r as i32 + dr;
321 let nc = c as i32 + dc;
322 if nr < 0 || nc < 0 || nr >= h as i32 || nc >= w as i32 {
323 continue;
324 }
325 let nz = self.raster.data[nr as usize][nc as usize];
326 let dist = if dr == 0 || dc == 0 {
327 self.cell_size
328 } else {
329 self.cell_size * std::f64::consts::SQRT_2
330 };
331 let drop = (z - nz) / dist;
332 if drop > max_drop {
333 max_drop = drop;
334 best_dir = code;
335 }
336 }
337 out[r][c] = best_dir;
338 }
339 }
340 out
341 }
342
343 pub fn flow_accumulation(&self) -> Vec<Vec<f64>> {
345 let h = self.raster.height;
346 let w = self.raster.width;
347 let fd = self.flow_direction();
348 let mut accum = vec![vec![1.0_f64; w]; h];
349 for _ in 0..(h * w) {
351 let snap = accum.clone();
352 for r in 0..h {
353 for c in 0..w {
354 let dir = fd[r][c];
355 let (dr, dc): (i32, i32) = match dir {
356 1 => (0, 1),
357 2 => (1, 1),
358 4 => (1, 0),
359 8 => (1, -1),
360 16 => (0, -1),
361 32 => (-1, -1),
362 64 => (-1, 0),
363 128 => (-1, 1),
364 _ => continue,
365 };
366 let nr = r as i32 + dr;
367 let nc = c as i32 + dc;
368 if nr >= 0 && nc >= 0 && nr < h as i32 && nc < w as i32 {
369 accum[nr as usize][nc as usize] += snap[r][c];
370 }
371 }
372 }
373 }
374 accum
375 }
376
377 pub fn watershed_delineation(&self, outlet: (usize, usize)) -> Vec<(usize, usize)> {
381 let h = self.raster.height;
382 let w = self.raster.width;
383 let fd = self.flow_direction();
384 let dir_to_offset = |d: u8| -> (i32, i32) {
387 match d {
388 1 => (0, 1),
389 2 => (1, 1),
390 4 => (1, 0),
391 8 => (1, -1),
392 16 => (0, -1),
393 32 => (-1, -1),
394 64 => (-1, 0),
395 128 => (-1, 1),
396 _ => (0, 0),
397 }
398 };
399
400 let mut watershed = Vec::new();
401 let mut visited = vec![vec![false; w]; h];
402 let mut stack = vec![outlet];
403 visited[outlet.0][outlet.1] = true;
404
405 while let Some((r, c)) = stack.pop() {
406 watershed.push((r, c));
407 for nr in 0..h {
409 for nc in 0..w {
410 if visited[nr][nc] {
411 continue;
412 }
413 let (dr, dc) = dir_to_offset(fd[nr][nc]);
414 let tr = nr as i32 + dr;
415 let tc = nc as i32 + dc;
416 if tr == r as i32 && tc == c as i32 {
417 visited[nr][nc] = true;
418 stack.push((nr, nc));
419 }
420 }
421 }
422 }
423 watershed
424 }
425
426 pub fn extract_stream_network(&self, threshold: f64) -> Vec<Vec<(usize, usize)>> {
431 let accum = self.flow_accumulation();
432 let h = self.raster.height;
433 let w = self.raster.width;
434 let fd = self.flow_direction();
435 let is_stream: Vec<Vec<bool>> = accum
436 .iter()
437 .map(|row| row.iter().map(|&v| v >= threshold).collect())
438 .collect();
439
440 let dir_to_offset = |d: u8| -> (i32, i32) {
441 match d {
442 1 => (0, 1),
443 2 => (1, 1),
444 4 => (1, 0),
445 8 => (1, -1),
446 16 => (0, -1),
447 32 => (-1, -1),
448 64 => (-1, 0),
449 128 => (-1, 1),
450 _ => (0, 0),
451 }
452 };
453
454 let mut visited = vec![vec![false; w]; h];
455 let mut segments: Vec<Vec<(usize, usize)>> = Vec::new();
456
457 for r in 0..h {
458 for c in 0..w {
459 if !is_stream[r][c] || visited[r][c] {
460 continue;
461 }
462 let mut seg = Vec::new();
464 let mut cr = r;
465 let mut cc = c;
466 loop {
467 if visited[cr][cc] || !is_stream[cr][cc] {
468 break;
469 }
470 visited[cr][cc] = true;
471 seg.push((cr, cc));
472 let (dr, dc) = dir_to_offset(fd[cr][cc]);
473 let nr = cr as i32 + dr;
474 let nc = cc as i32 + dc;
475 if nr < 0 || nc < 0 || nr >= h as i32 || nc >= w as i32 {
476 break;
477 }
478 cr = nr as usize;
479 cc = nc as usize;
480 }
481 if !seg.is_empty() {
482 segments.push(seg);
483 }
484 }
485 }
486 segments
487 }
488}
489
490pub struct PointCloudIO;
496
497impl PointCloudIO {
498 pub fn read_las_stub(content: &str) -> Vec<[f64; 3]> {
502 content
503 .lines()
504 .filter(|l| !l.trim_start().starts_with('#') && !l.trim().is_empty())
505 .filter_map(|l| {
506 let mut parts = l.split_whitespace();
507 let x = parts.next()?.parse::<f64>().ok()?;
508 let y = parts.next()?.parse::<f64>().ok()?;
509 let z = parts.next()?.parse::<f64>().ok()?;
510 Some([x, y, z])
511 })
512 .collect()
513 }
514
515 pub fn write_xyz(points: &[[f64; 3]]) -> String {
517 points
518 .iter()
519 .map(|p| format!("{} {} {}\n", p[0], p[1], p[2]))
520 .collect()
521 }
522
523 pub fn read_xyz(content: &str) -> Vec<[f64; 3]> {
525 Self::read_las_stub(content)
526 }
527
528 pub fn bounding_box(points: &[[f64; 3]]) -> [f64; 6] {
532 if points.is_empty() {
533 return [0.0; 6];
534 }
535 let mut mn = points[0];
536 let mut mx = points[0];
537 for p in points.iter().skip(1) {
538 for i in 0..3 {
539 mn[i] = mn[i].min(p[i]);
540 mx[i] = mx[i].max(p[i]);
541 }
542 }
543 [mn[0], mn[1], mn[2], mx[0], mx[1], mx[2]]
544 }
545
546 pub fn voxelize(points: &[[f64; 3]], resolution: f64) -> Vec<[f64; 3]> {
549 if points.is_empty() || resolution <= 0.0 {
550 return vec![];
551 }
552 let bb = Self::bounding_box(points);
553 use std::collections::HashMap;
554 let mut voxels: HashMap<(i64, i64, i64), (f64, f64, f64, u64)> = HashMap::new();
555 for p in points {
556 let ix = ((p[0] - bb[0]) / resolution).floor() as i64;
557 let iy = ((p[1] - bb[1]) / resolution).floor() as i64;
558 let iz = ((p[2] - bb[2]) / resolution).floor() as i64;
559 let e = voxels.entry((ix, iy, iz)).or_insert((0.0, 0.0, 0.0, 0));
560 e.0 += p[0];
561 e.1 += p[1];
562 e.2 += p[2];
563 e.3 += 1;
564 }
565 voxels
566 .values()
567 .map(|&(sx, sy, sz, n)| {
568 let n = n as f64;
569 [sx / n, sy / n, sz / n]
570 })
571 .collect()
572 }
573}
574
575#[derive(Debug, Clone)]
581pub struct SatelliteImage {
582 pub bands: Vec<Vec<f64>>,
584 pub band_names: Vec<String>,
586}
587
588impl SatelliteImage {
589 pub fn new(bands: Vec<Vec<f64>>, band_names: Vec<String>) -> Self {
591 Self { bands, band_names }
592 }
593
594 pub fn band_index(&self, name: &str) -> Option<usize> {
596 self.band_names.iter().position(|n| n == name)
597 }
598
599 pub fn ndvi(&self) -> Vec<f64> {
604 let nir_idx = self.band_index("nir").unwrap_or(0);
605 let red_idx = self.band_index("red").unwrap_or(1);
606 if self.bands.len() <= nir_idx.max(red_idx) {
607 return vec![];
608 }
609 let nir = &self.bands[nir_idx];
610 let red = &self.bands[red_idx];
611 nir.iter()
612 .zip(red.iter())
613 .map(|(&n, &r)| {
614 let denom = n + r;
615 if denom.abs() < 1e-10 {
616 0.0
617 } else {
618 (n - r) / denom
619 }
620 })
621 .collect()
622 }
623
624 pub fn ndwi(&self) -> Vec<f64> {
629 let green_idx = self.band_index("green").unwrap_or(0);
630 let nir_idx = self.band_index("nir").unwrap_or(1);
631 if self.bands.len() <= green_idx.max(nir_idx) {
632 return vec![];
633 }
634 let green = &self.bands[green_idx];
635 let nir = &self.bands[nir_idx];
636 green
637 .iter()
638 .zip(nir.iter())
639 .map(|(&g, &n)| {
640 let denom = g + n;
641 if denom.abs() < 1e-10 {
642 0.0
643 } else {
644 (g - n) / denom
645 }
646 })
647 .collect()
648 }
649
650 pub fn false_color_composite(&self, r: usize, g: usize, b: usize) -> Vec<[f64; 3]> {
653 if self.bands.len() <= r.max(g).max(b) {
654 return vec![];
655 }
656 let normalize = |band: &Vec<f64>| -> Vec<f64> {
657 let mn = band.iter().cloned().fold(f64::INFINITY, f64::min);
658 let mx = band.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
659 let range = mx - mn;
660 if range < 1e-10 {
661 vec![0.0; band.len()]
662 } else {
663 band.iter().map(|&v| (v - mn) / range).collect()
664 }
665 };
666 let rb = normalize(&self.bands[r]);
667 let gb = normalize(&self.bands[g]);
668 let bb = normalize(&self.bands[b]);
669 rb.iter()
670 .zip(gb.iter())
671 .zip(bb.iter())
672 .map(|((&rv, &gv), &bv)| [rv, gv, bv])
673 .collect()
674 }
675}
676
677#[cfg(test)]
682mod tests {
683 use super::*;
684
685 fn sample_raster() -> RasterData {
686 let data: Vec<Vec<f64>> = (0..5)
688 .map(|r| (0..5).map(|c| (r * 5 + c) as f64 * 10.0).collect())
689 .collect();
690 RasterData::new(data, -9999.0)
691 }
692
693 fn sample_dem() -> Dem {
694 Dem::new(sample_raster(), 30.0)
695 }
696
697 #[test]
700 fn test_geotiff_pixel_to_world_origin() {
701 let t = [100.0, 0.5, 0.0, 50.0, 0.0, -0.5];
702 let reader = GeoTiffReader::new(10, 10, 1, t, vec![vec![1.0; 100]]);
703 let [lon, lat] = reader.pixel_to_world(0, 0);
704 assert!((lon - 100.0).abs() < 1e-9);
705 assert!((lat - 50.0).abs() < 1e-9);
706 }
707
708 #[test]
709 fn test_geotiff_pixel_to_world_offset() {
710 let t = [0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
711 let reader = GeoTiffReader::new(4, 4, 1, t, vec![vec![0.0; 16]]);
712 let [lon, lat] = reader.pixel_to_world(2, 3);
713 assert!((lon - 2.0).abs() < 1e-9);
714 assert!((lat - 3.0).abs() < 1e-9);
715 }
716
717 #[test]
718 fn test_geotiff_world_to_pixel_roundtrip() {
719 let t = [10.0, 0.1, 0.0, 20.0, 0.0, -0.1];
720 let data = vec![vec![5.0; 100]];
721 let reader = GeoTiffReader::new(10, 10, 1, t, data);
722 let (px, py) = reader.world_to_pixel(10.5, 19.5);
723 assert_eq!(px, 5);
724 assert_eq!(py, 5);
725 }
726
727 #[test]
728 fn test_geotiff_elevation_at() {
729 let t = [0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
730 let band: Vec<f64> = (0..16).map(|i| i as f64).collect();
731 let reader = GeoTiffReader::new(4, 4, 1, t, vec![band]);
732 let elev = reader.elevation_at(0.0, 0.0);
734 assert!((elev - 0.0).abs() < 1e-9);
735 }
736
737 #[test]
738 fn test_geotiff_bounding_box_size() {
739 let t = [10.0, 1.0, 0.0, 50.0, 0.0, -1.0];
740 let reader = GeoTiffReader::new(5, 5, 1, t, vec![vec![0.0; 25]]);
741 let bb = reader.bounding_box();
742 assert!(bb[2] > bb[0]);
743 }
744
745 #[test]
746 fn test_geotiff_read_band_empty_when_out_of_range() {
747 let reader = GeoTiffReader::new(2, 2, 1, [0.0; 6], vec![vec![1.0; 4]]);
748 assert!(reader.read_band(5).is_empty());
749 }
750
751 #[test]
754 fn test_raster_statistics_basic() {
755 let r = sample_raster();
756 let (mn, mx, mean, _std) = r.statistics();
757 assert!((mn - 0.0).abs() < 1e-9);
758 assert!((mx - 240.0).abs() < 1e-9);
759 assert!(mean > 0.0);
760 }
761
762 #[test]
763 fn test_raster_statistics_all_nodata() {
764 let r = RasterData::new(vec![vec![-9999.0; 3]; 3], -9999.0);
765 let (mn, mx, mean, std) = r.statistics();
766 assert_eq!((mn, mx, mean, std), (0.0, 0.0, 0.0, 0.0));
767 }
768
769 #[test]
770 fn test_raster_clip_to_bbox_dimensions() {
771 let r = sample_raster();
772 let clipped = r.clip_to_bbox([1.0, 1.0, 4.0, 4.0]);
773 assert_eq!(clipped.width, 3);
774 assert_eq!(clipped.height, 3);
775 }
776
777 #[test]
778 fn test_raster_resample_down() {
779 let r = sample_raster();
780 let small = r.resample(0.5);
781 assert!(small.width <= r.width);
782 assert!(small.height <= r.height);
783 }
784
785 #[test]
786 fn test_raster_resample_up() {
787 let r = sample_raster();
788 let big = r.resample(2.0);
789 assert!(big.width >= r.width);
790 assert!(big.height >= r.height);
791 }
792
793 #[test]
794 fn test_raster_slope_border_zero() {
795 let r = sample_raster();
796 let s = r.slope();
797 assert!((s[0][0] - 0.0).abs() < 1e-9);
799 }
800
801 #[test]
802 fn test_raster_slope_interior_positive() {
803 let r = sample_raster();
804 let s = r.slope();
805 assert!(s[2][2] >= 0.0);
806 }
807
808 #[test]
809 fn test_raster_aspect_range() {
810 let r = sample_raster();
811 let a = r.aspect();
812 for row in &a {
813 for &v in row {
814 assert!(v >= 0.0 && v < 360.0 + 1e-9, "aspect out of range: {v}");
815 }
816 }
817 }
818
819 #[test]
820 fn test_raster_hillshade_range() {
821 let r = sample_raster();
822 let hs = r.hillshade(315.0, 45.0);
823 for row in &hs {
824 for &v in row {
825 assert!(v >= 0.0 && v <= 1.0 + 1e-9, "hillshade out of [0,1]: {v}");
826 }
827 }
828 }
829
830 #[test]
833 fn test_dem_flow_direction_shape() {
834 let d = sample_dem();
835 let fd = d.flow_direction();
836 assert_eq!(fd.len(), 5);
837 assert_eq!(fd[0].len(), 5);
838 }
839
840 #[test]
841 fn test_dem_flow_accumulation_shape() {
842 let d = sample_dem();
843 let fa = d.flow_accumulation();
844 assert_eq!(fa.len(), 5);
845 assert_eq!(fa[0].len(), 5);
846 }
847
848 #[test]
849 fn test_dem_flow_accumulation_positive() {
850 let d = sample_dem();
851 let fa = d.flow_accumulation();
852 for row in &fa {
853 for &v in row {
854 assert!(v >= 1.0, "flow accumulation must be ≥ 1");
855 }
856 }
857 }
858
859 #[test]
860 fn test_dem_watershed_contains_outlet() {
861 let d = sample_dem();
862 let ws = d.watershed_delineation((4, 4));
863 assert!(ws.contains(&(4, 4)));
864 }
865
866 #[test]
867 fn test_dem_stream_network_threshold_high() {
868 let d = sample_dem();
869 let segs = d.extract_stream_network(1e12);
871 assert!(segs.is_empty());
872 }
873
874 #[test]
875 fn test_dem_stream_network_threshold_low() {
876 let d = sample_dem();
877 let segs = d.extract_stream_network(0.0);
879 assert!(!segs.is_empty());
880 }
881
882 #[test]
885 fn test_point_cloud_write_read_roundtrip() {
886 let pts = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
887 let s = PointCloudIO::write_xyz(&pts);
888 let back = PointCloudIO::read_xyz(&s);
889 assert_eq!(back.len(), 2);
890 assert!((back[0][0] - 1.0).abs() < 1e-9);
891 assert!((back[1][2] - 6.0).abs() < 1e-9);
892 }
893
894 #[test]
895 fn test_point_cloud_bounding_box_empty() {
896 let bb = PointCloudIO::bounding_box(&[]);
897 assert_eq!(bb, [0.0; 6]);
898 }
899
900 #[test]
901 fn test_point_cloud_bounding_box_single() {
902 let bb = PointCloudIO::bounding_box(&[[3.0, 1.0, -2.0]]);
903 assert!((bb[0] - 3.0).abs() < 1e-9);
904 assert!((bb[3] - 3.0).abs() < 1e-9);
905 }
906
907 #[test]
908 fn test_point_cloud_bounding_box_multiple() {
909 let pts = vec![[0.0, 0.0, 0.0], [1.0, 2.0, 3.0], [-1.0, 5.0, -2.0]];
910 let bb = PointCloudIO::bounding_box(&pts);
911 assert!((bb[0] - (-1.0)).abs() < 1e-9); assert!((bb[4] - 5.0).abs() < 1e-9); }
914
915 #[test]
916 fn test_point_cloud_voxelize_reduces_count() {
917 let pts: Vec<[f64; 3]> = (0..100)
918 .map(|i| [(i % 10) as f64 * 0.1, (i / 10) as f64 * 0.1, 0.0])
919 .collect();
920 let vox = PointCloudIO::voxelize(&pts, 0.5);
921 assert!(vox.len() < pts.len());
922 }
923
924 #[test]
925 fn test_point_cloud_las_stub_comments_skipped() {
926 let content = "# header\n1 2 3\n# comment\n4 5 6\n";
927 let pts = PointCloudIO::read_las_stub(content);
928 assert_eq!(pts.len(), 2);
929 }
930
931 fn make_image() -> SatelliteImage {
934 let red: Vec<f64> = (0..4).map(|i| i as f64 * 0.1).collect();
935 let nir: Vec<f64> = (0..4).map(|i| i as f64 * 0.2 + 0.1).collect();
936 let green: Vec<f64> = (0..4).map(|i| i as f64 * 0.15).collect();
937 SatelliteImage::new(
938 vec![red, nir, green],
939 vec!["red".into(), "nir".into(), "green".into()],
940 )
941 }
942
943 #[test]
944 fn test_satellite_ndvi_length() {
945 let img = make_image();
946 assert_eq!(img.ndvi().len(), 4);
947 }
948
949 #[test]
950 fn test_satellite_ndvi_range() {
951 let img = make_image();
952 for v in img.ndvi() {
953 assert!(
954 v >= -1.0 - 1e-9 && v <= 1.0 + 1e-9,
955 "NDVI out of range: {v}"
956 );
957 }
958 }
959
960 #[test]
961 fn test_satellite_ndwi_length() {
962 let img = make_image();
963 assert_eq!(img.ndwi().len(), 4);
964 }
965
966 #[test]
967 fn test_satellite_false_color_length() {
968 let img = make_image();
969 let fc = img.false_color_composite(0, 2, 1);
970 assert_eq!(fc.len(), 4);
971 }
972
973 #[test]
974 fn test_satellite_false_color_range() {
975 let img = make_image();
976 for px in img.false_color_composite(0, 2, 1) {
977 for &ch in &px {
978 assert!(
979 ch >= 0.0 - 1e-9 && ch <= 1.0 + 1e-9,
980 "channel out of [0,1]: {ch}"
981 );
982 }
983 }
984 }
985
986 #[test]
987 fn test_satellite_band_index_found() {
988 let img = make_image();
989 assert_eq!(img.band_index("nir"), Some(1));
990 }
991
992 #[test]
993 fn test_satellite_band_index_not_found() {
994 let img = make_image();
995 assert_eq!(img.band_index("swir"), None);
996 }
997}