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 LasHeader {
582 pub file_creation_year: u16,
584 pub file_creation_doy: u16,
586 pub version_major: u8,
588 pub version_minor: u8,
590 pub point_count: u64,
592 pub x_scale: f64,
594 pub y_scale: f64,
596 pub z_scale: f64,
598 pub x_offset: f64,
600 pub y_offset: f64,
602 pub z_offset: f64,
604 pub point_format: u8,
606 pub point_record_len: u16,
608 pub point_data_offset: u32,
610}
611
612#[derive(Debug, Clone)]
614pub struct LasPoint {
615 pub x: f64,
617 pub y: f64,
619 pub z: f64,
621 pub intensity: u16,
623 pub return_flags: u8,
625 pub classification: u8,
627 pub scan_angle: i8,
629 pub user_data: u8,
631 pub point_source_id: u16,
633}
634
635#[derive(Debug, Clone)]
637pub struct LasPointCloud {
638 pub header: LasHeader,
640 pub points: Vec<LasPoint>,
642}
643
644fn las_read_u8(data: &[u8], off: &mut usize) -> Result<u8, String> {
647 if *off >= data.len() {
648 return Err(format!("LAS: unexpected EOF at offset {}", *off));
649 }
650 let v = data[*off];
651 *off += 1;
652 Ok(v)
653}
654
655fn las_read_u16_le(data: &[u8], off: &mut usize) -> Result<u16, String> {
656 if *off + 2 > data.len() {
657 return Err(format!(
658 "LAS: unexpected EOF reading u16 at offset {}",
659 *off
660 ));
661 }
662 let v = u16::from_le_bytes([data[*off], data[*off + 1]]);
663 *off += 2;
664 Ok(v)
665}
666
667fn las_read_u32_le(data: &[u8], off: &mut usize) -> Result<u32, String> {
668 if *off + 4 > data.len() {
669 return Err(format!(
670 "LAS: unexpected EOF reading u32 at offset {}",
671 *off
672 ));
673 }
674 let v = u32::from_le_bytes(
675 data[*off..*off + 4]
676 .try_into()
677 .map_err(|e| format!("{e}"))?,
678 );
679 *off += 4;
680 Ok(v)
681}
682
683fn las_read_u64_le(data: &[u8], off: &mut usize) -> Result<u64, String> {
684 if *off + 8 > data.len() {
685 return Err(format!(
686 "LAS: unexpected EOF reading u64 at offset {}",
687 *off
688 ));
689 }
690 let v = u64::from_le_bytes(
691 data[*off..*off + 8]
692 .try_into()
693 .map_err(|e| format!("{e}"))?,
694 );
695 *off += 8;
696 Ok(v)
697}
698
699fn las_read_f64_le(data: &[u8], off: &mut usize) -> Result<f64, String> {
700 if *off + 8 > data.len() {
701 return Err(format!(
702 "LAS: unexpected EOF reading f64 at offset {}",
703 *off
704 ));
705 }
706 let v = f64::from_le_bytes(
707 data[*off..*off + 8]
708 .try_into()
709 .map_err(|e| format!("{e}"))?,
710 );
711 *off += 8;
712 Ok(v)
713}
714
715fn las_read_i8(data: &[u8], off: &mut usize) -> Result<i8, String> {
716 las_read_u8(data, off).map(|v| v as i8)
717}
718
719pub fn read_las_binary(data: &[u8]) -> Result<LasPointCloud, String> {
730 if data.len() < 375 {
732 return Err(format!(
733 "LAS: data too short ({} bytes, need at least 375)",
734 data.len()
735 ));
736 }
737
738 if &data[0..4] != b"LASF" {
740 return Err(format!(
741 "LAS: bad signature {:?}, expected b\"LASF\"",
742 &data[0..4]
743 ));
744 }
745
746 let mut off = 4usize;
747
748 let _file_source_id = las_read_u16_le(data, &mut off)?;
750 let _global_encoding = las_read_u16_le(data, &mut off)?;
751 off += 16;
753 let version_major = las_read_u8(data, &mut off)?;
755 let version_minor = las_read_u8(data, &mut off)?;
756 off += 32;
758 off += 32;
760 let file_creation_doy = las_read_u16_le(data, &mut off)?;
762 let file_creation_year = las_read_u16_le(data, &mut off)?;
763 let _header_size = las_read_u16_le(data, &mut off)?;
765 let point_data_offset = las_read_u32_le(data, &mut off)?;
767 let _num_vlrs = las_read_u32_le(data, &mut off)?;
769 let point_format = las_read_u8(data, &mut off)?;
771 let point_record_len = las_read_u16_le(data, &mut off)?;
773 let legacy_point_count = las_read_u32_le(data, &mut off)?;
775 off += 20;
777 let x_scale = las_read_f64_le(data, &mut off)?;
779 let y_scale = las_read_f64_le(data, &mut off)?;
780 let z_scale = las_read_f64_le(data, &mut off)?;
781 let x_offset = las_read_f64_le(data, &mut off)?;
783 let y_offset = las_read_f64_le(data, &mut off)?;
784 let z_offset = las_read_f64_le(data, &mut off)?;
785 off += 48;
787 off += 20;
790
791 let point_count_extended = las_read_u64_le(data, &mut off)?;
793 let _ = off; let point_count = if point_count_extended > 0 {
798 point_count_extended
799 } else {
800 legacy_point_count as u64
801 };
802
803 let header = LasHeader {
804 file_creation_year,
805 file_creation_doy,
806 version_major,
807 version_minor,
808 point_count,
809 x_scale,
810 y_scale,
811 z_scale,
812 x_offset,
813 y_offset,
814 z_offset,
815 point_format,
816 point_record_len,
817 point_data_offset,
818 };
819
820 let mut points: Vec<LasPoint> = Vec::with_capacity(point_count as usize);
822 let rec_len = point_record_len.max(20) as usize;
823 let mut p_off = point_data_offset as usize;
824
825 for _ in 0..point_count {
826 if p_off + rec_len > data.len() {
827 break;
828 }
829 let mut o = p_off;
830 let raw_x = i32::from_le_bytes(
831 data[o..o + 4]
832 .try_into()
833 .map_err(|e| format!("LAS point x: {e}"))?,
834 );
835 o += 4;
836 let raw_y = i32::from_le_bytes(
837 data[o..o + 4]
838 .try_into()
839 .map_err(|e| format!("LAS point y: {e}"))?,
840 );
841 o += 4;
842 let raw_z = i32::from_le_bytes(
843 data[o..o + 4]
844 .try_into()
845 .map_err(|e| format!("LAS point z: {e}"))?,
846 );
847 o += 4;
848 let intensity = las_read_u16_le(data, &mut o)?;
849 let return_flags = las_read_u8(data, &mut o)?;
850 let classification = las_read_u8(data, &mut o)?;
851 let scan_angle = las_read_i8(data, &mut o)?;
852 let user_data = las_read_u8(data, &mut o)?;
853 let point_source_id = las_read_u16_le(data, &mut o)?;
854
855 points.push(LasPoint {
856 x: raw_x as f64 * x_scale + x_offset,
857 y: raw_y as f64 * y_scale + y_offset,
858 z: raw_z as f64 * z_scale + z_offset,
859 intensity,
860 return_flags,
861 classification,
862 scan_angle,
863 user_data,
864 point_source_id,
865 });
866 p_off += rec_len;
867 }
868
869 Ok(LasPointCloud { header, points })
870}
871
872pub fn write_las_binary_minimal(points: &[[f64; 3]], scale: f64) -> Vec<u8> {
878 let n = points.len() as u64;
879 let rec_len: u16 = 20; let header_size: u16 = 375;
881 let point_data_offset: u32 = header_size as u32;
882 let mut buf = Vec::with_capacity(header_size as usize + n as usize * rec_len as usize);
883
884 buf.extend_from_slice(b"LASF");
886 buf.extend_from_slice(&0u16.to_le_bytes());
888 buf.extend_from_slice(&0u16.to_le_bytes());
889 buf.extend_from_slice(&[0u8; 16]);
891 buf.push(1u8);
893 buf.push(4u8);
894 buf.extend_from_slice(&[0u8; 32]);
896 let mut sw = [b' '; 32];
898 let tag = b"oxiphysics";
899 sw[..tag.len()].copy_from_slice(tag);
900 buf.extend_from_slice(&sw);
901 buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&2026u16.to_le_bytes()); buf.extend_from_slice(&header_size.to_le_bytes());
906 buf.extend_from_slice(&point_data_offset.to_le_bytes());
908 buf.extend_from_slice(&0u32.to_le_bytes());
910 buf.push(0u8); buf.extend_from_slice(&rec_len.to_le_bytes());
914 let legacy_n = n.min(u32::MAX as u64) as u32;
916 buf.extend_from_slice(&legacy_n.to_le_bytes());
917 buf.extend_from_slice(&[0u8; 20]);
919 buf.extend_from_slice(&scale.to_le_bytes());
921 buf.extend_from_slice(&scale.to_le_bytes());
922 buf.extend_from_slice(&scale.to_le_bytes());
923 buf.extend_from_slice(&0.0f64.to_le_bytes());
925 buf.extend_from_slice(&0.0f64.to_le_bytes());
926 buf.extend_from_slice(&0.0f64.to_le_bytes());
927 let (mut max_x, mut min_x) = (f64::NEG_INFINITY, f64::INFINITY);
929 let (mut max_y, mut min_y) = (f64::NEG_INFINITY, f64::INFINITY);
930 let (mut max_z, mut min_z) = (f64::NEG_INFINITY, f64::INFINITY);
931 for &[x, y, z] in points {
932 if x > max_x {
933 max_x = x;
934 }
935 if x < min_x {
936 min_x = x;
937 }
938 if y > max_y {
939 max_y = y;
940 }
941 if y < min_y {
942 min_y = y;
943 }
944 if z > max_z {
945 max_z = z;
946 }
947 if z < min_z {
948 min_z = z;
949 }
950 }
951 if points.is_empty() {
952 max_x = 0.0;
953 min_x = 0.0;
954 max_y = 0.0;
955 min_y = 0.0;
956 max_z = 0.0;
957 min_z = 0.0;
958 }
959 buf.extend_from_slice(&max_x.to_le_bytes());
960 buf.extend_from_slice(&min_x.to_le_bytes());
961 buf.extend_from_slice(&max_y.to_le_bytes());
962 buf.extend_from_slice(&min_y.to_le_bytes());
963 buf.extend_from_slice(&max_z.to_le_bytes());
964 buf.extend_from_slice(&min_z.to_le_bytes());
965 buf.extend_from_slice(&0u64.to_le_bytes());
967 buf.extend_from_slice(&0u64.to_le_bytes());
969 buf.extend_from_slice(&0u32.to_le_bytes());
971 buf.extend_from_slice(&n.to_le_bytes());
973 buf.extend_from_slice(&[0u8; 120]);
975
976 assert_eq!(buf.len(), header_size as usize, "header length mismatch");
978
979 for &[x, y, z] in points {
981 let raw_x = ((x) / scale).round() as i32;
982 let raw_y = ((y) / scale).round() as i32;
983 let raw_z = ((z) / scale).round() as i32;
984 buf.extend_from_slice(&raw_x.to_le_bytes());
985 buf.extend_from_slice(&raw_y.to_le_bytes());
986 buf.extend_from_slice(&raw_z.to_le_bytes());
987 buf.extend_from_slice(&0u16.to_le_bytes()); buf.push(0u8); buf.push(0u8); buf.push(0u8); buf.push(0u8); buf.extend_from_slice(&0u16.to_le_bytes()); }
994
995 buf
996}
997
998#[derive(Debug, Clone)]
1004pub struct SatelliteImage {
1005 pub bands: Vec<Vec<f64>>,
1007 pub band_names: Vec<String>,
1009}
1010
1011impl SatelliteImage {
1012 pub fn new(bands: Vec<Vec<f64>>, band_names: Vec<String>) -> Self {
1014 Self { bands, band_names }
1015 }
1016
1017 pub fn band_index(&self, name: &str) -> Option<usize> {
1019 self.band_names.iter().position(|n| n == name)
1020 }
1021
1022 pub fn ndvi(&self) -> Vec<f64> {
1027 let nir_idx = self.band_index("nir").unwrap_or(0);
1028 let red_idx = self.band_index("red").unwrap_or(1);
1029 if self.bands.len() <= nir_idx.max(red_idx) {
1030 return vec![];
1031 }
1032 let nir = &self.bands[nir_idx];
1033 let red = &self.bands[red_idx];
1034 nir.iter()
1035 .zip(red.iter())
1036 .map(|(&n, &r)| {
1037 let denom = n + r;
1038 if denom.abs() < 1e-10 {
1039 0.0
1040 } else {
1041 (n - r) / denom
1042 }
1043 })
1044 .collect()
1045 }
1046
1047 pub fn ndwi(&self) -> Vec<f64> {
1052 let green_idx = self.band_index("green").unwrap_or(0);
1053 let nir_idx = self.band_index("nir").unwrap_or(1);
1054 if self.bands.len() <= green_idx.max(nir_idx) {
1055 return vec![];
1056 }
1057 let green = &self.bands[green_idx];
1058 let nir = &self.bands[nir_idx];
1059 green
1060 .iter()
1061 .zip(nir.iter())
1062 .map(|(&g, &n)| {
1063 let denom = g + n;
1064 if denom.abs() < 1e-10 {
1065 0.0
1066 } else {
1067 (g - n) / denom
1068 }
1069 })
1070 .collect()
1071 }
1072
1073 pub fn false_color_composite(&self, r: usize, g: usize, b: usize) -> Vec<[f64; 3]> {
1076 if self.bands.len() <= r.max(g).max(b) {
1077 return vec![];
1078 }
1079 let normalize = |band: &Vec<f64>| -> Vec<f64> {
1080 let mn = band.iter().cloned().fold(f64::INFINITY, f64::min);
1081 let mx = band.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1082 let range = mx - mn;
1083 if range < 1e-10 {
1084 vec![0.0; band.len()]
1085 } else {
1086 band.iter().map(|&v| (v - mn) / range).collect()
1087 }
1088 };
1089 let rb = normalize(&self.bands[r]);
1090 let gb = normalize(&self.bands[g]);
1091 let bb = normalize(&self.bands[b]);
1092 rb.iter()
1093 .zip(gb.iter())
1094 .zip(bb.iter())
1095 .map(|((&rv, &gv), &bv)| [rv, gv, bv])
1096 .collect()
1097 }
1098}
1099
1100#[cfg(test)]
1105mod tests {
1106 use super::*;
1107
1108 fn sample_raster() -> RasterData {
1109 let data: Vec<Vec<f64>> = (0..5)
1111 .map(|r| (0..5).map(|c| (r * 5 + c) as f64 * 10.0).collect())
1112 .collect();
1113 RasterData::new(data, -9999.0)
1114 }
1115
1116 fn sample_dem() -> Dem {
1117 Dem::new(sample_raster(), 30.0)
1118 }
1119
1120 #[test]
1123 fn test_geotiff_pixel_to_world_origin() {
1124 let t = [100.0, 0.5, 0.0, 50.0, 0.0, -0.5];
1125 let reader = GeoTiffReader::new(10, 10, 1, t, vec![vec![1.0; 100]]);
1126 let [lon, lat] = reader.pixel_to_world(0, 0);
1127 assert!((lon - 100.0).abs() < 1e-9);
1128 assert!((lat - 50.0).abs() < 1e-9);
1129 }
1130
1131 #[test]
1132 fn test_geotiff_pixel_to_world_offset() {
1133 let t = [0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
1134 let reader = GeoTiffReader::new(4, 4, 1, t, vec![vec![0.0; 16]]);
1135 let [lon, lat] = reader.pixel_to_world(2, 3);
1136 assert!((lon - 2.0).abs() < 1e-9);
1137 assert!((lat - 3.0).abs() < 1e-9);
1138 }
1139
1140 #[test]
1141 fn test_geotiff_world_to_pixel_roundtrip() {
1142 let t = [10.0, 0.1, 0.0, 20.0, 0.0, -0.1];
1143 let data = vec![vec![5.0; 100]];
1144 let reader = GeoTiffReader::new(10, 10, 1, t, data);
1145 let (px, py) = reader.world_to_pixel(10.5, 19.5);
1146 assert_eq!(px, 5);
1147 assert_eq!(py, 5);
1148 }
1149
1150 #[test]
1151 fn test_geotiff_elevation_at() {
1152 let t = [0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
1153 let band: Vec<f64> = (0..16).map(|i| i as f64).collect();
1154 let reader = GeoTiffReader::new(4, 4, 1, t, vec![band]);
1155 let elev = reader.elevation_at(0.0, 0.0);
1157 assert!((elev - 0.0).abs() < 1e-9);
1158 }
1159
1160 #[test]
1161 fn test_geotiff_bounding_box_size() {
1162 let t = [10.0, 1.0, 0.0, 50.0, 0.0, -1.0];
1163 let reader = GeoTiffReader::new(5, 5, 1, t, vec![vec![0.0; 25]]);
1164 let bb = reader.bounding_box();
1165 assert!(bb[2] > bb[0]);
1166 }
1167
1168 #[test]
1169 fn test_geotiff_read_band_empty_when_out_of_range() {
1170 let reader = GeoTiffReader::new(2, 2, 1, [0.0; 6], vec![vec![1.0; 4]]);
1171 assert!(reader.read_band(5).is_empty());
1172 }
1173
1174 #[test]
1177 fn test_raster_statistics_basic() {
1178 let r = sample_raster();
1179 let (mn, mx, mean, _std) = r.statistics();
1180 assert!((mn - 0.0).abs() < 1e-9);
1181 assert!((mx - 240.0).abs() < 1e-9);
1182 assert!(mean > 0.0);
1183 }
1184
1185 #[test]
1186 fn test_raster_statistics_all_nodata() {
1187 let r = RasterData::new(vec![vec![-9999.0; 3]; 3], -9999.0);
1188 let (mn, mx, mean, std) = r.statistics();
1189 assert_eq!((mn, mx, mean, std), (0.0, 0.0, 0.0, 0.0));
1190 }
1191
1192 #[test]
1193 fn test_raster_clip_to_bbox_dimensions() {
1194 let r = sample_raster();
1195 let clipped = r.clip_to_bbox([1.0, 1.0, 4.0, 4.0]);
1196 assert_eq!(clipped.width, 3);
1197 assert_eq!(clipped.height, 3);
1198 }
1199
1200 #[test]
1201 fn test_raster_resample_down() {
1202 let r = sample_raster();
1203 let small = r.resample(0.5);
1204 assert!(small.width <= r.width);
1205 assert!(small.height <= r.height);
1206 }
1207
1208 #[test]
1209 fn test_raster_resample_up() {
1210 let r = sample_raster();
1211 let big = r.resample(2.0);
1212 assert!(big.width >= r.width);
1213 assert!(big.height >= r.height);
1214 }
1215
1216 #[test]
1217 fn test_raster_slope_border_zero() {
1218 let r = sample_raster();
1219 let s = r.slope();
1220 assert!((s[0][0] - 0.0).abs() < 1e-9);
1222 }
1223
1224 #[test]
1225 fn test_raster_slope_interior_positive() {
1226 let r = sample_raster();
1227 let s = r.slope();
1228 assert!(s[2][2] >= 0.0);
1229 }
1230
1231 #[test]
1232 fn test_raster_aspect_range() {
1233 let r = sample_raster();
1234 let a = r.aspect();
1235 for row in &a {
1236 for &v in row {
1237 assert!(v >= 0.0 && v < 360.0 + 1e-9, "aspect out of range: {v}");
1238 }
1239 }
1240 }
1241
1242 #[test]
1243 fn test_raster_hillshade_range() {
1244 let r = sample_raster();
1245 let hs = r.hillshade(315.0, 45.0);
1246 for row in &hs {
1247 for &v in row {
1248 assert!(v >= 0.0 && v <= 1.0 + 1e-9, "hillshade out of [0,1]: {v}");
1249 }
1250 }
1251 }
1252
1253 #[test]
1256 fn test_dem_flow_direction_shape() {
1257 let d = sample_dem();
1258 let fd = d.flow_direction();
1259 assert_eq!(fd.len(), 5);
1260 assert_eq!(fd[0].len(), 5);
1261 }
1262
1263 #[test]
1264 fn test_dem_flow_accumulation_shape() {
1265 let d = sample_dem();
1266 let fa = d.flow_accumulation();
1267 assert_eq!(fa.len(), 5);
1268 assert_eq!(fa[0].len(), 5);
1269 }
1270
1271 #[test]
1272 fn test_dem_flow_accumulation_positive() {
1273 let d = sample_dem();
1274 let fa = d.flow_accumulation();
1275 for row in &fa {
1276 for &v in row {
1277 assert!(v >= 1.0, "flow accumulation must be ≥ 1");
1278 }
1279 }
1280 }
1281
1282 #[test]
1283 fn test_dem_watershed_contains_outlet() {
1284 let d = sample_dem();
1285 let ws = d.watershed_delineation((4, 4));
1286 assert!(ws.contains(&(4, 4)));
1287 }
1288
1289 #[test]
1290 fn test_dem_stream_network_threshold_high() {
1291 let d = sample_dem();
1292 let segs = d.extract_stream_network(1e12);
1294 assert!(segs.is_empty());
1295 }
1296
1297 #[test]
1298 fn test_dem_stream_network_threshold_low() {
1299 let d = sample_dem();
1300 let segs = d.extract_stream_network(0.0);
1302 assert!(!segs.is_empty());
1303 }
1304
1305 #[test]
1308 fn test_point_cloud_write_read_roundtrip() {
1309 let pts = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
1310 let s = PointCloudIO::write_xyz(&pts);
1311 let back = PointCloudIO::read_xyz(&s);
1312 assert_eq!(back.len(), 2);
1313 assert!((back[0][0] - 1.0).abs() < 1e-9);
1314 assert!((back[1][2] - 6.0).abs() < 1e-9);
1315 }
1316
1317 #[test]
1318 fn test_point_cloud_bounding_box_empty() {
1319 let bb = PointCloudIO::bounding_box(&[]);
1320 assert_eq!(bb, [0.0; 6]);
1321 }
1322
1323 #[test]
1324 fn test_point_cloud_bounding_box_single() {
1325 let bb = PointCloudIO::bounding_box(&[[3.0, 1.0, -2.0]]);
1326 assert!((bb[0] - 3.0).abs() < 1e-9);
1327 assert!((bb[3] - 3.0).abs() < 1e-9);
1328 }
1329
1330 #[test]
1331 fn test_point_cloud_bounding_box_multiple() {
1332 let pts = vec![[0.0, 0.0, 0.0], [1.0, 2.0, 3.0], [-1.0, 5.0, -2.0]];
1333 let bb = PointCloudIO::bounding_box(&pts);
1334 assert!((bb[0] - (-1.0)).abs() < 1e-9); assert!((bb[4] - 5.0).abs() < 1e-9); }
1337
1338 #[test]
1339 fn test_point_cloud_voxelize_reduces_count() {
1340 let pts: Vec<[f64; 3]> = (0..100)
1341 .map(|i| [(i % 10) as f64 * 0.1, (i / 10) as f64 * 0.1, 0.0])
1342 .collect();
1343 let vox = PointCloudIO::voxelize(&pts, 0.5);
1344 assert!(vox.len() < pts.len());
1345 }
1346
1347 #[test]
1348 fn test_point_cloud_las_stub_comments_skipped() {
1349 let content = "# header\n1 2 3\n# comment\n4 5 6\n";
1350 let pts = PointCloudIO::read_las_stub(content);
1351 assert_eq!(pts.len(), 2);
1352 }
1353
1354 fn make_image() -> SatelliteImage {
1357 let red: Vec<f64> = (0..4).map(|i| i as f64 * 0.1).collect();
1358 let nir: Vec<f64> = (0..4).map(|i| i as f64 * 0.2 + 0.1).collect();
1359 let green: Vec<f64> = (0..4).map(|i| i as f64 * 0.15).collect();
1360 SatelliteImage::new(
1361 vec![red, nir, green],
1362 vec!["red".into(), "nir".into(), "green".into()],
1363 )
1364 }
1365
1366 #[test]
1367 fn test_satellite_ndvi_length() {
1368 let img = make_image();
1369 assert_eq!(img.ndvi().len(), 4);
1370 }
1371
1372 #[test]
1373 fn test_satellite_ndvi_range() {
1374 let img = make_image();
1375 for v in img.ndvi() {
1376 assert!(
1377 v >= -1.0 - 1e-9 && v <= 1.0 + 1e-9,
1378 "NDVI out of range: {v}"
1379 );
1380 }
1381 }
1382
1383 #[test]
1384 fn test_satellite_ndwi_length() {
1385 let img = make_image();
1386 assert_eq!(img.ndwi().len(), 4);
1387 }
1388
1389 #[test]
1390 fn test_satellite_false_color_length() {
1391 let img = make_image();
1392 let fc = img.false_color_composite(0, 2, 1);
1393 assert_eq!(fc.len(), 4);
1394 }
1395
1396 #[test]
1397 fn test_satellite_false_color_range() {
1398 let img = make_image();
1399 for px in img.false_color_composite(0, 2, 1) {
1400 for &ch in &px {
1401 assert!(
1402 ch >= 0.0 - 1e-9 && ch <= 1.0 + 1e-9,
1403 "channel out of [0,1]: {ch}"
1404 );
1405 }
1406 }
1407 }
1408
1409 #[test]
1410 fn test_satellite_band_index_found() {
1411 let img = make_image();
1412 assert_eq!(img.band_index("nir"), Some(1));
1413 }
1414
1415 #[test]
1416 fn test_satellite_band_index_not_found() {
1417 let img = make_image();
1418 assert_eq!(img.band_index("swir"), None);
1419 }
1420
1421 #[test]
1424 fn test_read_las_bad_signature() {
1425 let data = b"NOTL\x00\x00\x00\x00";
1426 assert!(read_las_binary(data).is_err());
1427 }
1428
1429 #[test]
1430 fn test_read_las_too_short() {
1431 let data = vec![0u8; 100];
1432 assert!(read_las_binary(&data).is_err());
1433 }
1434
1435 #[test]
1436 fn test_las_roundtrip_empty() {
1437 let data = write_las_binary_minimal(&[], 0.001);
1438 let cloud = read_las_binary(&data).expect("should parse empty LAS");
1439 assert_eq!(cloud.points.len(), 0);
1440 assert_eq!(cloud.header.version_major, 1);
1441 assert_eq!(cloud.header.version_minor, 4);
1442 }
1443
1444 #[test]
1445 fn test_las_roundtrip_single_point() {
1446 let pts = vec![[1.234, 5.678, -0.5]];
1447 let data = write_las_binary_minimal(&pts, 0.001);
1448 let cloud = read_las_binary(&data).expect("should parse single-point LAS");
1449 assert_eq!(cloud.points.len(), 1);
1450 assert!((cloud.points[0].x - 1.234).abs() < 0.002, "x mismatch");
1451 assert!((cloud.points[0].y - 5.678).abs() < 0.002, "y mismatch");
1452 assert!((cloud.points[0].z - (-0.5)).abs() < 0.002, "z mismatch");
1453 }
1454
1455 #[test]
1456 fn test_las_roundtrip_multiple_points() {
1457 let pts: Vec<[f64; 3]> = (0..10)
1458 .map(|i| [i as f64 * 0.5, i as f64 * 0.25, i as f64 * 0.1])
1459 .collect();
1460 let data = write_las_binary_minimal(&pts, 0.001);
1461 let cloud = read_las_binary(&data).expect("should parse multi-point LAS");
1462 assert_eq!(cloud.points.len(), 10);
1463 for (i, pt) in cloud.points.iter().enumerate() {
1464 let expected_x = i as f64 * 0.5;
1465 assert!(
1466 (pt.x - expected_x).abs() < 0.002,
1467 "x mismatch at point {i}: expected {expected_x}, got {}",
1468 pt.x
1469 );
1470 }
1471 }
1472}