1use crate::error::{AlgorithmError, Result};
41use oxigdal_core::buffer::RasterBuffer;
42use oxigdal_core::types::RasterDataType;
43use std::cmp::Ordering;
44use std::collections::BinaryHeap;
45
46#[derive(Copy, Clone, PartialEq)]
51struct CostCell {
52 x: u64,
53 y: u64,
54 cost: f64,
55}
56
57impl Eq for CostCell {}
58
59impl PartialOrd for CostCell {
60 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
61 Some(self.cmp(other))
62 }
63}
64
65impl Ord for CostCell {
66 fn cmp(&self, other: &Self) -> Ordering {
67 other
68 .cost
69 .partial_cmp(&self.cost)
70 .unwrap_or(Ordering::Equal)
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Default)]
80pub enum FrictionModel {
81 #[default]
83 Isotropic,
84
85 ToblerHiking,
88
89 SymmetricSlope {
92 slope_weight: f64,
94 },
95
96 AsymmetricSlope {
100 uphill_weight: f64,
102 downhill_weight: f64,
104 },
105}
106
107#[derive(Debug, Clone, Copy)]
123pub struct Direction;
124
125impl Direction {
126 pub const N: f64 = 0.0;
128 pub const NE: f64 = 1.0;
130 pub const E: f64 = 2.0;
132 pub const SE: f64 = 3.0;
134 pub const S: f64 = 4.0;
136 pub const SW: f64 = 5.0;
138 pub const W: f64 = 6.0;
140 pub const NW: f64 = 7.0;
142 pub const SOURCE: f64 = -1.0;
144}
145
146#[derive(Debug)]
148pub struct CostDistanceResult {
149 pub cost_distance: RasterBuffer,
151 pub direction: Option<RasterBuffer>,
153 pub allocation: Option<RasterBuffer>,
155}
156
157const NEIGHBORS: [(i64, i64, f64); 8] = [
163 (0, -1, Direction::S), (1, -1, Direction::SW), (1, 0, Direction::W), (1, 1, Direction::NW), (0, 1, Direction::N), (-1, 1, Direction::NE), (-1, 0, Direction::E), (-1, -1, Direction::SE), ];
172
173fn direction_from_offset(dx: i64, dy: i64) -> f64 {
175 match (dx, dy) {
176 (0, -1) => Direction::N,
177 (1, -1) => Direction::NE,
178 (1, 0) => Direction::E,
179 (1, 1) => Direction::SE,
180 (0, 1) => Direction::S,
181 (-1, 1) => Direction::SW,
182 (-1, 0) => Direction::W,
183 (-1, -1) => Direction::NW,
184 _ => Direction::SOURCE,
185 }
186}
187
188pub fn euclidean_distance(sources: &RasterBuffer, cell_size: f64) -> Result<RasterBuffer> {
206 let width = sources.width();
207 let height = sources.height();
208 let mut distance = RasterBuffer::zeros(width, height, RasterDataType::Float64);
209
210 let mut source_cells = Vec::new();
212 for y in 0..height {
213 for x in 0..width {
214 let val = sources.get_pixel(x, y).map_err(AlgorithmError::Core)?;
215 if val > 0.0 {
216 source_cells.push((x, y));
217 }
218 }
219 }
220
221 for y in 0..height {
223 for x in 0..width {
224 distance
225 .set_pixel(x, y, f64::INFINITY)
226 .map_err(AlgorithmError::Core)?;
227 }
228 }
229
230 for y in 0..height {
232 for x in 0..width {
233 let mut min_dist = f64::INFINITY;
234
235 for &(sx, sy) in &source_cells {
236 let dx = (x as f64 - sx as f64) * cell_size;
237 let dy = (y as f64 - sy as f64) * cell_size;
238 let dist = (dx * dx + dy * dy).sqrt();
239 min_dist = min_dist.min(dist);
240 }
241
242 distance
243 .set_pixel(x, y, min_dist)
244 .map_err(AlgorithmError::Core)?;
245 }
246 }
247
248 Ok(distance)
249}
250
251pub fn cost_distance(
267 sources: &RasterBuffer,
268 cost_surface: &RasterBuffer,
269 cell_size: f64,
270) -> Result<RasterBuffer> {
271 let result = cost_distance_full(
272 sources,
273 cost_surface,
274 None,
275 cell_size,
276 FrictionModel::Isotropic,
277 )?;
278 Ok(result.cost_distance)
279}
280
281pub fn cost_distance_full(
295 sources: &RasterBuffer,
296 cost_surface: &RasterBuffer,
297 barriers: Option<&RasterBuffer>,
298 cell_size: f64,
299 friction_model: FrictionModel,
300) -> Result<CostDistanceResult> {
301 let width = sources.width();
302 let height = sources.height();
303
304 let mut cost_dist = RasterBuffer::zeros(width, height, RasterDataType::Float64);
305 let mut direction = RasterBuffer::zeros(width, height, RasterDataType::Float64);
306 let mut allocation = RasterBuffer::zeros(width, height, RasterDataType::Float64);
307 let mut visited = vec![false; (width * height) as usize];
308
309 for y in 0..height {
311 for x in 0..width {
312 cost_dist
313 .set_pixel(x, y, f64::INFINITY)
314 .map_err(AlgorithmError::Core)?;
315 direction
316 .set_pixel(x, y, Direction::SOURCE)
317 .map_err(AlgorithmError::Core)?;
318 allocation
319 .set_pixel(x, y, -1.0)
320 .map_err(AlgorithmError::Core)?;
321 }
322 }
323
324 let mut pq = BinaryHeap::new();
326
327 let mut source_index = 0.0_f64;
329 for y in 0..height {
330 for x in 0..width {
331 let val = sources.get_pixel(x, y).map_err(AlgorithmError::Core)?;
332 if val > 0.0 {
333 cost_dist
334 .set_pixel(x, y, 0.0)
335 .map_err(AlgorithmError::Core)?;
336 allocation
337 .set_pixel(x, y, source_index)
338 .map_err(AlgorithmError::Core)?;
339 pq.push(CostCell { x, y, cost: 0.0 });
340 source_index += 1.0;
341 }
342 }
343 }
344
345 while let Some(cell) = pq.pop() {
350 let idx = (cell.y * width + cell.x) as usize;
351 if visited[idx] {
352 continue;
353 }
354 visited[idx] = true;
355
356 let current_alloc = allocation
357 .get_pixel(cell.x, cell.y)
358 .map_err(AlgorithmError::Core)?;
359
360 for &(dx, dy, _dir_from) in &NEIGHBORS {
362 let nx = cell.x as i64 + dx;
363 let ny = cell.y as i64 + dy;
364
365 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
366 continue;
367 }
368
369 let nx_u = nx as u64;
370 let ny_u = ny as u64;
371 let n_idx = (ny_u * width + nx_u) as usize;
372
373 if visited[n_idx] {
374 continue;
375 }
376
377 if let Some(barrier_raster) = barriers {
379 let barrier_val = barrier_raster
380 .get_pixel(nx_u, ny_u)
381 .map_err(AlgorithmError::Core)?;
382 if barrier_val > 0.0 {
383 continue; }
385 }
386
387 let neighbor_cost = cost_surface
388 .get_pixel(nx_u, ny_u)
389 .map_err(AlgorithmError::Core)?;
390
391 if neighbor_cost < 0.0 || neighbor_cost.is_nan() || neighbor_cost.is_infinite() {
392 continue; }
394
395 let current_cost_val = cost_surface
396 .get_pixel(cell.x, cell.y)
397 .map_err(AlgorithmError::Core)?;
398
399 let base_dist = if dx.abs() + dy.abs() == 2 {
401 cell_size * core::f64::consts::SQRT_2
402 } else {
403 cell_size
404 };
405
406 let avg_cost = (current_cost_val + neighbor_cost) / 2.0;
408
409 let friction_factor = match friction_model {
411 FrictionModel::Isotropic => 1.0,
412 FrictionModel::ToblerHiking => {
413 let dz = neighbor_cost - current_cost_val;
415 let slope_tangent = dz / base_dist;
416 let speed = 6.0 * (-3.5 * (slope_tangent + 0.05).abs()).exp();
417 if speed > 0.001 { 1.0 / speed } else { 1000.0 }
418 }
419 FrictionModel::SymmetricSlope { slope_weight } => {
420 let dz = (neighbor_cost - current_cost_val).abs();
421 1.0 + dz * slope_weight / base_dist
422 }
423 FrictionModel::AsymmetricSlope {
424 uphill_weight,
425 downhill_weight,
426 } => {
427 let dz = neighbor_cost - current_cost_val;
428 if dz > 0.0 {
429 1.0 + dz * uphill_weight / base_dist
430 } else {
431 1.0 + dz.abs() * downhill_weight / base_dist
432 }
433 }
434 };
435
436 let travel_cost = avg_cost * base_dist * friction_factor;
437 let new_cost = cell.cost + travel_cost;
438
439 let current_best = cost_dist
440 .get_pixel(nx_u, ny_u)
441 .map_err(AlgorithmError::Core)?;
442
443 if new_cost < current_best {
444 cost_dist
445 .set_pixel(nx_u, ny_u, new_cost)
446 .map_err(AlgorithmError::Core)?;
447
448 let back_dir = direction_from_offset(-dx, -dy);
450 direction
451 .set_pixel(nx_u, ny_u, back_dir)
452 .map_err(AlgorithmError::Core)?;
453
454 allocation
455 .set_pixel(nx_u, ny_u, current_alloc)
456 .map_err(AlgorithmError::Core)?;
457
458 pq.push(CostCell {
459 x: nx_u,
460 y: ny_u,
461 cost: new_cost,
462 });
463 }
464 }
465 }
466
467 Ok(CostDistanceResult {
468 cost_distance: cost_dist,
469 direction: Some(direction),
470 allocation: Some(allocation),
471 })
472}
473
474pub fn cost_distance_anisotropic(
492 sources: &RasterBuffer,
493 cost_surface: &RasterBuffer,
494 dem: &RasterBuffer,
495 barriers: Option<&RasterBuffer>,
496 cell_size: f64,
497 friction_model: FrictionModel,
498) -> Result<CostDistanceResult> {
499 let width = sources.width();
500 let height = sources.height();
501
502 if dem.width() != width || dem.height() != height {
503 return Err(AlgorithmError::InvalidDimensions {
504 message: "DEM dimensions must match source raster",
505 actual: dem.width() as usize,
506 expected: width as usize,
507 });
508 }
509
510 let mut cost_dist = RasterBuffer::zeros(width, height, RasterDataType::Float64);
511 let mut direction = RasterBuffer::zeros(width, height, RasterDataType::Float64);
512 let mut allocation = RasterBuffer::zeros(width, height, RasterDataType::Float64);
513 let mut visited = vec![false; (width * height) as usize];
514
515 for y in 0..height {
517 for x in 0..width {
518 cost_dist
519 .set_pixel(x, y, f64::INFINITY)
520 .map_err(AlgorithmError::Core)?;
521 direction
522 .set_pixel(x, y, Direction::SOURCE)
523 .map_err(AlgorithmError::Core)?;
524 allocation
525 .set_pixel(x, y, -1.0)
526 .map_err(AlgorithmError::Core)?;
527 }
528 }
529
530 let mut pq = BinaryHeap::new();
531 let mut source_index = 0.0_f64;
532
533 for y in 0..height {
534 for x in 0..width {
535 let val = sources.get_pixel(x, y).map_err(AlgorithmError::Core)?;
536 if val > 0.0 {
537 cost_dist
538 .set_pixel(x, y, 0.0)
539 .map_err(AlgorithmError::Core)?;
540 allocation
541 .set_pixel(x, y, source_index)
542 .map_err(AlgorithmError::Core)?;
543 pq.push(CostCell { x, y, cost: 0.0 });
544 source_index += 1.0;
545 }
546 }
547 }
548
549 while let Some(cell) = pq.pop() {
550 let idx = (cell.y * width + cell.x) as usize;
551 if visited[idx] {
552 continue;
553 }
554 visited[idx] = true;
555
556 let current_alloc = allocation
557 .get_pixel(cell.x, cell.y)
558 .map_err(AlgorithmError::Core)?;
559
560 let current_elev = dem
561 .get_pixel(cell.x, cell.y)
562 .map_err(AlgorithmError::Core)?;
563
564 let current_friction = cost_surface
565 .get_pixel(cell.x, cell.y)
566 .map_err(AlgorithmError::Core)?;
567
568 for &(dx, dy, _) in &NEIGHBORS {
569 let nx = cell.x as i64 + dx;
570 let ny = cell.y as i64 + dy;
571
572 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
573 continue;
574 }
575
576 let nx_u = nx as u64;
577 let ny_u = ny as u64;
578 let n_idx = (ny_u * width + nx_u) as usize;
579
580 if visited[n_idx] {
581 continue;
582 }
583
584 if let Some(barrier_raster) = barriers {
585 let barrier_val = barrier_raster
586 .get_pixel(nx_u, ny_u)
587 .map_err(AlgorithmError::Core)?;
588 if barrier_val > 0.0 {
589 continue;
590 }
591 }
592
593 let neighbor_friction = cost_surface
594 .get_pixel(nx_u, ny_u)
595 .map_err(AlgorithmError::Core)?;
596
597 if neighbor_friction < 0.0
598 || neighbor_friction.is_nan()
599 || neighbor_friction.is_infinite()
600 {
601 continue;
602 }
603
604 let neighbor_elev = dem.get_pixel(nx_u, ny_u).map_err(AlgorithmError::Core)?;
605
606 let base_dist = if dx.abs() + dy.abs() == 2 {
607 cell_size * core::f64::consts::SQRT_2
608 } else {
609 cell_size
610 };
611
612 let avg_friction = (current_friction + neighbor_friction) / 2.0;
613 let dz = neighbor_elev - current_elev;
614
615 let friction_factor = match friction_model {
616 FrictionModel::Isotropic => 1.0,
617 FrictionModel::ToblerHiking => {
618 let slope_tangent = dz / base_dist;
619 let speed = 6.0 * (-3.5 * (slope_tangent + 0.05).abs()).exp();
620 if speed > 0.001 { 1.0 / speed } else { 1000.0 }
621 }
622 FrictionModel::SymmetricSlope { slope_weight } => {
623 1.0 + dz.abs() * slope_weight / base_dist
624 }
625 FrictionModel::AsymmetricSlope {
626 uphill_weight,
627 downhill_weight,
628 } => {
629 if dz > 0.0 {
630 1.0 + dz * uphill_weight / base_dist
631 } else {
632 1.0 + dz.abs() * downhill_weight / base_dist
633 }
634 }
635 };
636
637 let travel_cost = avg_friction * base_dist * friction_factor;
638 let new_cost = cell.cost + travel_cost;
639
640 let current_best = cost_dist
641 .get_pixel(nx_u, ny_u)
642 .map_err(AlgorithmError::Core)?;
643
644 if new_cost < current_best {
645 cost_dist
646 .set_pixel(nx_u, ny_u, new_cost)
647 .map_err(AlgorithmError::Core)?;
648
649 let back_dir = direction_from_offset(-dx, -dy);
650 direction
651 .set_pixel(nx_u, ny_u, back_dir)
652 .map_err(AlgorithmError::Core)?;
653
654 allocation
655 .set_pixel(nx_u, ny_u, current_alloc)
656 .map_err(AlgorithmError::Core)?;
657
658 pq.push(CostCell {
659 x: nx_u,
660 y: ny_u,
661 cost: new_cost,
662 });
663 }
664 }
665 }
666
667 Ok(CostDistanceResult {
668 cost_distance: cost_dist,
669 direction: Some(direction),
670 allocation: Some(allocation),
671 })
672}
673
674pub fn least_cost_path(
692 cost_distance_raster: &RasterBuffer,
693 dest_x: u64,
694 dest_y: u64,
695) -> Result<RasterBuffer> {
696 let width = cost_distance_raster.width();
697 let height = cost_distance_raster.height();
698 let mut path = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
699
700 if dest_x >= width || dest_y >= height {
701 return Err(AlgorithmError::InvalidParameter {
702 parameter: "destination",
703 message: format!(
704 "Destination ({}, {}) is outside raster bounds ({}, {})",
705 dest_x, dest_y, width, height
706 ),
707 });
708 }
709
710 let mut current_x = dest_x;
711 let mut current_y = dest_y;
712 let max_steps = (width * height) as usize; for _ in 0..max_steps {
715 path.set_pixel(current_x, current_y, 1.0)
716 .map_err(AlgorithmError::Core)?;
717
718 let current_cost = cost_distance_raster
719 .get_pixel(current_x, current_y)
720 .map_err(AlgorithmError::Core)?;
721
722 if current_cost <= 0.0 || current_cost.is_nan() {
723 break; }
725
726 let mut min_cost = current_cost;
728 let mut next_x = current_x;
729 let mut next_y = current_y;
730
731 for &(dx, dy, _) in &NEIGHBORS {
732 let nx = current_x as i64 + dx;
733 let ny = current_y as i64 + dy;
734
735 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
736 continue;
737 }
738
739 let nx_u = nx as u64;
740 let ny_u = ny as u64;
741
742 let neighbor_cost = cost_distance_raster
743 .get_pixel(nx_u, ny_u)
744 .map_err(AlgorithmError::Core)?;
745
746 if neighbor_cost < min_cost {
747 min_cost = neighbor_cost;
748 next_x = nx_u;
749 next_y = ny_u;
750 }
751 }
752
753 if next_x == current_x && next_y == current_y {
754 break; }
756
757 current_x = next_x;
758 current_y = next_y;
759 }
760
761 Ok(path)
762}
763
764pub fn least_cost_path_from_direction(
780 direction_raster: &RasterBuffer,
781 dest_x: u64,
782 dest_y: u64,
783) -> Result<RasterBuffer> {
784 let width = direction_raster.width();
785 let height = direction_raster.height();
786 let mut path = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
787
788 if dest_x >= width || dest_y >= height {
789 return Err(AlgorithmError::InvalidParameter {
790 parameter: "destination",
791 message: format!(
792 "Destination ({}, {}) is outside raster bounds ({}, {})",
793 dest_x, dest_y, width, height
794 ),
795 });
796 }
797
798 let mut cx = dest_x;
799 let mut cy = dest_y;
800 let max_steps = (width * height) as usize;
801
802 for _ in 0..max_steps {
803 path.set_pixel(cx, cy, 1.0).map_err(AlgorithmError::Core)?;
804
805 let dir = direction_raster
806 .get_pixel(cx, cy)
807 .map_err(AlgorithmError::Core)?;
808
809 if (dir - Direction::SOURCE).abs() < 0.5 {
810 break; }
812
813 let (dx, dy) = direction_to_offset(dir);
815 let nx = cx as i64 + dx;
816 let ny = cy as i64 + dy;
817
818 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
819 break;
820 }
821
822 cx = nx as u64;
823 cy = ny as u64;
824 }
825
826 Ok(path)
827}
828
829fn direction_to_offset(dir: f64) -> (i64, i64) {
831 let d = dir.round() as i64;
832 match d {
833 0 => (0, -1), 1 => (1, -1), 2 => (1, 0), 3 => (1, 1), 4 => (0, 1), 5 => (-1, 1), 6 => (-1, 0), 7 => (-1, -1), _ => (0, 0), }
843}
844
845#[derive(Copy, Clone, PartialEq)]
851struct AStarCell {
852 x: u64,
853 y: u64,
854 g_cost: f64, f_cost: f64, }
857
858impl Eq for AStarCell {}
859
860impl PartialOrd for AStarCell {
861 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
862 Some(self.cmp(other))
863 }
864}
865
866impl Ord for AStarCell {
867 fn cmp(&self, other: &Self) -> Ordering {
868 other
869 .f_cost
870 .partial_cmp(&self.f_cost)
871 .unwrap_or(Ordering::Equal)
872 }
873}
874
875pub fn astar_path(
892 cost_surface: &RasterBuffer,
893 barriers: Option<&RasterBuffer>,
894 start_x: u64,
895 start_y: u64,
896 end_x: u64,
897 end_y: u64,
898 cell_size: f64,
899) -> Result<(RasterBuffer, f64)> {
900 let width = cost_surface.width();
901 let height = cost_surface.height();
902
903 if start_x >= width || start_y >= height || end_x >= width || end_y >= height {
904 return Err(AlgorithmError::InvalidParameter {
905 parameter: "coordinates",
906 message: "Start or end coordinates are outside raster bounds".to_string(),
907 });
908 }
909
910 let mut g_costs = vec![f64::INFINITY; (width * height) as usize];
911 let mut came_from = vec![(u64::MAX, u64::MAX); (width * height) as usize];
912 let mut closed = vec![false; (width * height) as usize];
913
914 let start_idx = (start_y * width + start_x) as usize;
915 g_costs[start_idx] = 0.0;
916
917 let mut open = BinaryHeap::new();
918 let h = heuristic(start_x, start_y, end_x, end_y, cell_size);
919 open.push(AStarCell {
920 x: start_x,
921 y: start_y,
922 g_cost: 0.0,
923 f_cost: h,
924 });
925
926 while let Some(current) = open.pop() {
927 if current.x == end_x && current.y == end_y {
928 let path = reconstruct_path(&came_from, width, height, end_x, end_y)?;
930 return Ok((path, current.g_cost));
931 }
932
933 let idx = (current.y * width + current.x) as usize;
934 if closed[idx] {
935 continue;
936 }
937 closed[idx] = true;
938
939 for &(dx, dy, _) in &NEIGHBORS {
940 let nx = current.x as i64 + dx;
941 let ny = current.y as i64 + dy;
942
943 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
944 continue;
945 }
946
947 let nx_u = nx as u64;
948 let ny_u = ny as u64;
949 let n_idx = (ny_u * width + nx_u) as usize;
950
951 if closed[n_idx] {
952 continue;
953 }
954
955 if let Some(barrier_raster) = barriers {
956 let barrier_val = barrier_raster
957 .get_pixel(nx_u, ny_u)
958 .map_err(AlgorithmError::Core)?;
959 if barrier_val > 0.0 {
960 continue;
961 }
962 }
963
964 let neighbor_cost = cost_surface
965 .get_pixel(nx_u, ny_u)
966 .map_err(AlgorithmError::Core)?;
967
968 if neighbor_cost < 0.0 || neighbor_cost.is_nan() || neighbor_cost.is_infinite() {
969 continue;
970 }
971
972 let current_cost_val = cost_surface
973 .get_pixel(current.x, current.y)
974 .map_err(AlgorithmError::Core)?;
975
976 let base_dist = if dx.abs() + dy.abs() == 2 {
977 cell_size * core::f64::consts::SQRT_2
978 } else {
979 cell_size
980 };
981
982 let avg_cost = (current_cost_val + neighbor_cost) / 2.0;
983 let travel_cost = avg_cost * base_dist;
984 let tentative_g = current.g_cost + travel_cost;
985
986 if tentative_g < g_costs[n_idx] {
987 g_costs[n_idx] = tentative_g;
988 came_from[n_idx] = (current.x, current.y);
989
990 let h = heuristic(nx_u, ny_u, end_x, end_y, cell_size);
991 open.push(AStarCell {
992 x: nx_u,
993 y: ny_u,
994 g_cost: tentative_g,
995 f_cost: tentative_g + h,
996 });
997 }
998 }
999 }
1000
1001 Err(AlgorithmError::PathNotFound(format!(
1002 "No path exists from ({}, {}) to ({}, {})",
1003 start_x, start_y, end_x, end_y
1004 )))
1005}
1006
1007fn heuristic(x1: u64, y1: u64, x2: u64, y2: u64, cell_size: f64) -> f64 {
1009 let dx = (x2 as f64 - x1 as f64) * cell_size;
1010 let dy = (y2 as f64 - y1 as f64) * cell_size;
1011 (dx * dx + dy * dy).sqrt()
1012}
1013
1014fn reconstruct_path(
1016 came_from: &[(u64, u64)],
1017 width: u64,
1018 height: u64,
1019 end_x: u64,
1020 end_y: u64,
1021) -> Result<RasterBuffer> {
1022 let mut path = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
1023
1024 let mut cx = end_x;
1025 let mut cy = end_y;
1026 let max_steps = (width * height) as usize;
1027
1028 for _ in 0..max_steps {
1029 path.set_pixel(cx, cy, 1.0).map_err(AlgorithmError::Core)?;
1030
1031 let idx = (cy * width + cx) as usize;
1032 let (px, py) = came_from[idx];
1033
1034 if px == u64::MAX {
1035 break; }
1037
1038 cx = px;
1039 cy = py;
1040 }
1041
1042 Ok(path)
1043}
1044
1045pub fn compute_corridor(
1065 cost_from_a: &RasterBuffer,
1066 cost_from_b: &RasterBuffer,
1067) -> Result<RasterBuffer> {
1068 let width = cost_from_a.width();
1069 let height = cost_from_a.height();
1070
1071 if cost_from_b.width() != width || cost_from_b.height() != height {
1072 return Err(AlgorithmError::InvalidDimensions {
1073 message: "Cost distance rasters must have the same dimensions",
1074 actual: cost_from_b.width() as usize,
1075 expected: width as usize,
1076 });
1077 }
1078
1079 let mut corridor = RasterBuffer::zeros(width, height, RasterDataType::Float64);
1080
1081 for y in 0..height {
1082 for x in 0..width {
1083 let ca = cost_from_a.get_pixel(x, y).map_err(AlgorithmError::Core)?;
1084 let cb = cost_from_b.get_pixel(x, y).map_err(AlgorithmError::Core)?;
1085
1086 let combined = if ca.is_infinite() || cb.is_infinite() {
1087 f64::INFINITY
1088 } else {
1089 ca + cb
1090 };
1091
1092 corridor
1093 .set_pixel(x, y, combined)
1094 .map_err(AlgorithmError::Core)?;
1095 }
1096 }
1097
1098 Ok(corridor)
1099}
1100
1101pub fn compute_corridor_normalized(
1116 cost_from_a: &RasterBuffer,
1117 cost_from_b: &RasterBuffer,
1118 threshold: f64,
1119) -> Result<RasterBuffer> {
1120 let corridor = compute_corridor(cost_from_a, cost_from_b)?;
1121 let width = corridor.width();
1122 let height = corridor.height();
1123
1124 let mut min_val = f64::INFINITY;
1126 for y in 0..height {
1127 for x in 0..width {
1128 let v = corridor.get_pixel(x, y).map_err(AlgorithmError::Core)?;
1129 if v < min_val {
1130 min_val = v;
1131 }
1132 }
1133 }
1134
1135 let mut normalized = RasterBuffer::zeros(width, height, RasterDataType::Float64);
1136
1137 for y in 0..height {
1138 for x in 0..width {
1139 let v = corridor.get_pixel(x, y).map_err(AlgorithmError::Core)?;
1140 let relative = v - min_val;
1141 let final_val = if relative > threshold {
1142 f64::INFINITY
1143 } else {
1144 relative
1145 };
1146 normalized
1147 .set_pixel(x, y, final_val)
1148 .map_err(AlgorithmError::Core)?;
1149 }
1150 }
1151
1152 Ok(normalized)
1153}
1154
1155#[cfg(test)]
1160mod tests {
1161 use super::*;
1162
1163 fn create_uniform_cost(size: u64, cost_value: f64) -> RasterBuffer {
1164 let mut buf = RasterBuffer::zeros(size, size, RasterDataType::Float32);
1165 for y in 0..size {
1166 for x in 0..size {
1167 let _ = buf.set_pixel(x, y, cost_value);
1168 }
1169 }
1170 buf
1171 }
1172
1173 fn create_sources_single(size: u64, sx: u64, sy: u64) -> RasterBuffer {
1174 let mut buf = RasterBuffer::zeros(size, size, RasterDataType::Float32);
1175 let _ = buf.set_pixel(sx, sy, 1.0);
1176 buf
1177 }
1178
1179 fn create_barrier_wall(size: u64, wall_col: u64) -> RasterBuffer {
1180 let mut buf = RasterBuffer::zeros(size, size, RasterDataType::Float32);
1181 for y in 0..size {
1182 let _ = buf.set_pixel(wall_col, y, 1.0);
1183 }
1184 buf
1185 }
1186
1187 #[test]
1190 fn test_euclidean_distance() {
1191 let sources = create_sources_single(10, 5, 5);
1192 let distance = euclidean_distance(&sources, 1.0).expect("euclidean");
1193
1194 let at_source = distance.get_pixel(5, 5).expect("pixel");
1196 assert!(at_source < 1e-6);
1197
1198 let adjacent = distance.get_pixel(5, 6).expect("pixel");
1200 assert!((adjacent - 1.0).abs() < 0.01);
1201
1202 let d1 = distance.get_pixel(5, 6).expect("pixel");
1204 let d2 = distance.get_pixel(5, 7).expect("pixel");
1205 assert!(d2 > d1);
1206 }
1207
1208 #[test]
1211 fn test_cost_distance_uniform() {
1212 let sources = create_sources_single(10, 0, 0);
1213 let cost = create_uniform_cost(10, 1.0);
1214 let result = cost_distance(&sources, &cost, 1.0).expect("cost_dist");
1215
1216 let at_source = result.get_pixel(0, 0).expect("pixel");
1218 assert!(at_source < 1e-6);
1219
1220 let near = result.get_pixel(1, 0).expect("near");
1222 let far = result.get_pixel(5, 5).expect("far");
1223 assert!(far > near);
1224 }
1225
1226 #[test]
1229 fn test_cost_distance_with_barrier() {
1230 let sources = create_sources_single(10, 0, 5);
1231 let cost = create_uniform_cost(10, 1.0);
1232 let barrier = create_barrier_wall(10, 5);
1233
1234 let result_no_barrier =
1235 cost_distance_full(&sources, &cost, None, 1.0, FrictionModel::Isotropic)
1236 .expect("no_barrier");
1237
1238 let result_with_barrier = cost_distance_full(
1239 &sources,
1240 &cost,
1241 Some(&barrier),
1242 1.0,
1243 FrictionModel::Isotropic,
1244 )
1245 .expect("with_barrier");
1246
1247 let cost_no_barrier = result_no_barrier
1249 .cost_distance
1250 .get_pixel(9, 5)
1251 .expect("pixel");
1252 let cost_with_barrier = result_with_barrier
1253 .cost_distance
1254 .get_pixel(9, 5)
1255 .expect("pixel");
1256
1257 assert!(
1259 cost_with_barrier.is_infinite(),
1260 "Cells behind full wall barrier should be unreachable, got {cost_with_barrier}"
1261 );
1262 assert!(cost_no_barrier.is_finite());
1263 }
1264
1265 #[test]
1268 fn test_direction_raster() {
1269 let sources = create_sources_single(10, 5, 5);
1270 let cost = create_uniform_cost(10, 1.0);
1271
1272 let result =
1273 cost_distance_full(&sources, &cost, None, 1.0, FrictionModel::Isotropic).expect("full");
1274
1275 assert!(result.direction.is_some());
1276 let dir = result.direction.expect("dir");
1277
1278 let source_dir = dir.get_pixel(5, 5).expect("pixel");
1280 assert!(
1281 (source_dir - Direction::SOURCE).abs() < 0.5,
1282 "Source direction should be SOURCE (-1), got {source_dir}"
1283 );
1284
1285 let east_dir = dir.get_pixel(6, 5).expect("pixel");
1287 assert!(
1288 (east_dir - Direction::W).abs() < 0.5,
1289 "East neighbor should point West, got {east_dir}"
1290 );
1291 }
1292
1293 #[test]
1296 fn test_allocation_raster() {
1297 let mut sources = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1298 let _ = sources.set_pixel(2, 5, 1.0);
1299 let _ = sources.set_pixel(7, 5, 1.0);
1300
1301 let cost = create_uniform_cost(10, 1.0);
1302
1303 let result = cost_distance_full(&sources, &cost, None, 1.0, FrictionModel::Isotropic)
1304 .expect("allocation");
1305
1306 assert!(result.allocation.is_some());
1307 let alloc = result.allocation.expect("alloc");
1308
1309 let near_0 = alloc.get_pixel(1, 5).expect("pixel");
1311 assert!(
1312 (near_0 - 0.0).abs() < 0.5,
1313 "Cell near source 0 should be allocated to 0, got {near_0}"
1314 );
1315
1316 let near_1 = alloc.get_pixel(8, 5).expect("pixel");
1318 assert!(
1319 (near_1 - 1.0).abs() < 0.5,
1320 "Cell near source 1 should be allocated to 1, got {near_1}"
1321 );
1322 }
1323
1324 #[test]
1327 fn test_least_cost_path() {
1328 let sources = create_sources_single(10, 0, 0);
1329 let cost = create_uniform_cost(10, 1.0);
1330 let cost_dist = cost_distance(&sources, &cost, 1.0).expect("cost_dist");
1331
1332 let path = least_cost_path(&cost_dist, 5, 5).expect("path");
1333
1334 let at_dest = path.get_pixel(5, 5).expect("pixel");
1336 assert!(at_dest > 0.0);
1337
1338 let at_source = path.get_pixel(0, 0).expect("pixel");
1340 assert!(at_source > 0.0);
1341 }
1342
1343 #[test]
1344 fn test_least_cost_path_from_direction() {
1345 let sources = create_sources_single(10, 0, 0);
1346 let cost = create_uniform_cost(10, 1.0);
1347
1348 let result =
1349 cost_distance_full(&sources, &cost, None, 1.0, FrictionModel::Isotropic).expect("full");
1350
1351 let dir_raster = result.direction.expect("dir");
1352 let path = least_cost_path_from_direction(&dir_raster, 5, 5).expect("path");
1353
1354 let at_dest = path.get_pixel(5, 5).expect("pixel");
1355 assert!(at_dest > 0.0);
1356 }
1357
1358 #[test]
1361 fn test_astar_path() {
1362 let cost = create_uniform_cost(10, 1.0);
1363 let (path, total_cost) = astar_path(&cost, None, 0, 0, 9, 9, 1.0).expect("astar");
1364
1365 let at_start = path.get_pixel(0, 0).expect("pixel");
1366 let at_end = path.get_pixel(9, 9).expect("pixel");
1367 assert!(at_start > 0.0);
1368 assert!(at_end > 0.0);
1369
1370 assert!(total_cost > 10.0 && total_cost < 20.0);
1372 }
1373
1374 #[test]
1375 fn test_astar_path_with_barrier() {
1376 let cost = create_uniform_cost(10, 1.0);
1377 let mut barrier = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1379 for y in 1..10 {
1380 let _ = barrier.set_pixel(5, y, 1.0);
1381 }
1382
1383 let result = astar_path(&cost, Some(&barrier), 0, 5, 9, 5, 1.0);
1384 assert!(result.is_ok());
1385 let (_, total_cost) = result.expect("astar_barrier");
1386 assert!(total_cost > 9.0, "Path around barrier should be longer");
1388 }
1389
1390 #[test]
1391 fn test_astar_no_path() {
1392 let cost = create_uniform_cost(10, 1.0);
1393 let barrier = create_barrier_wall(10, 5); let result = astar_path(&cost, Some(&barrier), 0, 5, 9, 5, 1.0);
1396 assert!(result.is_err());
1397 }
1398
1399 #[test]
1402 fn test_corridor() {
1403 let source_a = create_sources_single(10, 0, 5);
1404 let source_b = create_sources_single(10, 9, 5);
1405 let cost = create_uniform_cost(10, 1.0);
1406
1407 let cost_a = cost_distance(&source_a, &cost, 1.0).expect("cost_a");
1408 let cost_b = cost_distance(&source_b, &cost, 1.0).expect("cost_b");
1409
1410 let corridor = compute_corridor(&cost_a, &cost_b).expect("corridor");
1411
1412 let center = corridor.get_pixel(5, 5).expect("center");
1414 let off_center = corridor.get_pixel(5, 0).expect("off_center");
1415 assert!(
1416 center < off_center,
1417 "Corridor center should have lower cost than off-center"
1418 );
1419 }
1420
1421 #[test]
1422 fn test_corridor_normalized() {
1423 let source_a = create_sources_single(10, 0, 5);
1424 let source_b = create_sources_single(10, 9, 5);
1425 let cost = create_uniform_cost(10, 1.0);
1426
1427 let cost_a = cost_distance(&source_a, &cost, 1.0).expect("cost_a");
1428 let cost_b = cost_distance(&source_b, &cost, 1.0).expect("cost_b");
1429
1430 let normalized = compute_corridor_normalized(&cost_a, &cost_b, 5.0).expect("normalized");
1431
1432 let mut found_zero = false;
1434 for y in 0..10 {
1435 for x in 0..10 {
1436 let v = normalized.get_pixel(x, y).expect("pixel");
1437 if v.abs() < 0.01 {
1438 found_zero = true;
1439 }
1440 }
1441 }
1442 assert!(found_zero, "Normalized corridor should have a zero minimum");
1443 }
1444
1445 #[test]
1448 fn test_anisotropic_tobler() {
1449 let sources = create_sources_single(10, 5, 5);
1450 let cost = create_uniform_cost(10, 1.0);
1451 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1453 for y in 0..10 {
1454 for x in 0..10 {
1455 let _ = dem.set_pixel(x, y, y as f64 * 10.0);
1456 }
1457 }
1458
1459 let result = cost_distance_anisotropic(
1460 &sources,
1461 &cost,
1462 &dem,
1463 None,
1464 1.0,
1465 FrictionModel::ToblerHiking,
1466 );
1467 assert!(result.is_ok());
1468 }
1469
1470 #[test]
1471 fn test_anisotropic_symmetric_slope() {
1472 let sources = create_sources_single(10, 5, 5);
1473 let cost = create_uniform_cost(10, 1.0);
1474 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1475 for y in 0..10 {
1476 for x in 0..10 {
1477 let _ = dem.set_pixel(x, y, y as f64 * 5.0);
1478 }
1479 }
1480
1481 let result = cost_distance_anisotropic(
1482 &sources,
1483 &cost,
1484 &dem,
1485 None,
1486 1.0,
1487 FrictionModel::SymmetricSlope { slope_weight: 1.0 },
1488 );
1489 assert!(result.is_ok());
1490 }
1491
1492 #[test]
1493 fn test_anisotropic_asymmetric_slope() {
1494 let sources = create_sources_single(10, 5, 5);
1495 let cost = create_uniform_cost(10, 1.0);
1496 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
1497 for y in 0..10 {
1498 for x in 0..10 {
1499 let _ = dem.set_pixel(x, y, y as f64 * 5.0);
1500 }
1501 }
1502
1503 let result = cost_distance_anisotropic(
1504 &sources,
1505 &cost,
1506 &dem,
1507 None,
1508 1.0,
1509 FrictionModel::AsymmetricSlope {
1510 uphill_weight: 2.0,
1511 downhill_weight: 0.5,
1512 },
1513 );
1514 assert!(result.is_ok());
1515
1516 let cd = result.expect("aniso");
1517
1518 let uphill = cd.cost_distance.get_pixel(5, 0).expect("uphill"); let downhill = cd.cost_distance.get_pixel(5, 9).expect("downhill"); assert!(uphill.is_finite());
1523 assert!(downhill.is_finite());
1524 }
1525
1526 #[test]
1529 fn test_friction_model_default() {
1530 let model = FrictionModel::default();
1531 assert_eq!(model, FrictionModel::Isotropic);
1532 }
1533
1534 #[test]
1537 fn test_least_cost_path_invalid_destination() {
1538 let cost_dist = RasterBuffer::zeros(10, 10, RasterDataType::Float64);
1539 let result = least_cost_path(&cost_dist, 100, 100);
1540 assert!(result.is_err());
1541 }
1542
1543 #[test]
1544 fn test_direction_from_offset_all() {
1545 assert!((direction_from_offset(0, -1) - Direction::N).abs() < 0.5);
1546 assert!((direction_from_offset(1, -1) - Direction::NE).abs() < 0.5);
1547 assert!((direction_from_offset(1, 0) - Direction::E).abs() < 0.5);
1548 assert!((direction_from_offset(1, 1) - Direction::SE).abs() < 0.5);
1549 assert!((direction_from_offset(0, 1) - Direction::S).abs() < 0.5);
1550 assert!((direction_from_offset(-1, 1) - Direction::SW).abs() < 0.5);
1551 assert!((direction_from_offset(-1, 0) - Direction::W).abs() < 0.5);
1552 assert!((direction_from_offset(-1, -1) - Direction::NW).abs() < 0.5);
1553 }
1554
1555 #[test]
1556 fn test_direction_to_offset_all() {
1557 assert_eq!(direction_to_offset(Direction::N), (0, -1));
1558 assert_eq!(direction_to_offset(Direction::NE), (1, -1));
1559 assert_eq!(direction_to_offset(Direction::E), (1, 0));
1560 assert_eq!(direction_to_offset(Direction::SE), (1, 1));
1561 assert_eq!(direction_to_offset(Direction::S), (0, 1));
1562 assert_eq!(direction_to_offset(Direction::SW), (-1, 1));
1563 assert_eq!(direction_to_offset(Direction::W), (-1, 0));
1564 assert_eq!(direction_to_offset(Direction::NW), (-1, -1));
1565 }
1566}