1use crate::error::{AlgorithmError, Result};
41use oxigdal_core::buffer::RasterBuffer;
42use oxigdal_core::types::RasterDataType;
43
44#[cfg(feature = "parallel")]
45use rayon::prelude::*;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum ViewshedAlgorithm {
54 #[default]
56 R1LineOfSight,
57
58 R2ReferencePlane,
60
61 R3SweepLine,
63}
64
65#[derive(Debug, Clone, Copy)]
67pub struct CurvatureCorrection {
68 pub earth_radius: f64,
70
71 pub refraction_coefficient: f64,
74}
75
76impl Default for CurvatureCorrection {
77 fn default() -> Self {
78 Self {
79 earth_radius: 6_371_000.0,
80 refraction_coefficient: 0.13,
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct ViewshedConfig {
88 pub observer_x: u64,
90 pub observer_y: u64,
92 pub observer_height: f64,
94 pub target_height: f64,
96 pub max_distance: Option<f64>,
98 pub cell_size: f64,
100 pub algorithm: ViewshedAlgorithm,
102 pub curvature_correction: Option<CurvatureCorrection>,
104}
105
106#[derive(Debug, Clone, Copy)]
108pub struct ObserverPoint {
109 pub x: u64,
111 pub y: u64,
113 pub height: f64,
115}
116
117#[derive(Debug)]
119pub struct ViewshedResult {
120 pub visibility: RasterBuffer,
122 pub elevation_angle: Option<RasterBuffer>,
125}
126
127pub fn compute_viewshed(
149 dem: &RasterBuffer,
150 observer_x: u64,
151 observer_y: u64,
152 observer_height: f64,
153 target_height: f64,
154 max_distance: Option<f64>,
155 cell_size: f64,
156) -> Result<RasterBuffer> {
157 let config = ViewshedConfig {
158 observer_x,
159 observer_y,
160 observer_height,
161 target_height,
162 max_distance,
163 cell_size,
164 algorithm: ViewshedAlgorithm::R1LineOfSight,
165 curvature_correction: None,
166 };
167
168 let result = compute_viewshed_advanced(dem, &config)?;
169 Ok(result.visibility)
170}
171
172pub fn compute_viewshed_advanced(
183 dem: &RasterBuffer,
184 config: &ViewshedConfig,
185) -> Result<ViewshedResult> {
186 if config.observer_x >= dem.width() || config.observer_y >= dem.height() {
188 return Err(AlgorithmError::InvalidParameter {
189 parameter: "observer position",
190 message: format!(
191 "Observer ({}, {}) is outside DEM bounds ({}, {})",
192 config.observer_x,
193 config.observer_y,
194 dem.width(),
195 dem.height()
196 ),
197 });
198 }
199
200 match config.algorithm {
201 ViewshedAlgorithm::R1LineOfSight => viewshed_r1(dem, config),
202 ViewshedAlgorithm::R2ReferencePlane => viewshed_r2(dem, config),
203 ViewshedAlgorithm::R3SweepLine => viewshed_r3(dem, config),
204 }
205}
206
207pub fn compute_cumulative_viewshed(
223 dem: &RasterBuffer,
224 observers: &[(u64, u64, f64)],
225 target_height: f64,
226 max_distance: Option<f64>,
227 cell_size: f64,
228) -> Result<RasterBuffer> {
229 let observer_points: Vec<ObserverPoint> = observers
230 .iter()
231 .map(|&(x, y, height)| ObserverPoint { x, y, height })
232 .collect();
233
234 compute_cumulative_viewshed_advanced(
235 dem,
236 &observer_points,
237 target_height,
238 max_distance,
239 cell_size,
240 None,
241 )
242}
243
244pub fn compute_cumulative_viewshed_advanced(
259 dem: &RasterBuffer,
260 observers: &[ObserverPoint],
261 target_height: f64,
262 max_distance: Option<f64>,
263 cell_size: f64,
264 curvature: Option<CurvatureCorrection>,
265) -> Result<RasterBuffer> {
266 let width = dem.width();
267 let height = dem.height();
268 let mut cumulative = RasterBuffer::zeros(width, height, RasterDataType::Float64);
269
270 #[cfg(feature = "parallel")]
271 {
272 let viewsheds: Result<Vec<RasterBuffer>> = observers
273 .par_iter()
274 .map(|obs| {
275 let config = ViewshedConfig {
276 observer_x: obs.x,
277 observer_y: obs.y,
278 observer_height: obs.height,
279 target_height,
280 max_distance,
281 cell_size,
282 algorithm: ViewshedAlgorithm::R2ReferencePlane,
283 curvature_correction: curvature,
284 };
285 let result = compute_viewshed_advanced(dem, &config)?;
286 Ok(result.visibility)
287 })
288 .collect();
289
290 for viewshed in viewsheds? {
291 for y in 0..height {
292 for x in 0..width {
293 let visible = viewshed.get_pixel(x, y).map_err(AlgorithmError::Core)?;
294 if visible > 0.0 {
295 let current = cumulative.get_pixel(x, y).map_err(AlgorithmError::Core)?;
296 cumulative
297 .set_pixel(x, y, current + 1.0)
298 .map_err(AlgorithmError::Core)?;
299 }
300 }
301 }
302 }
303 }
304
305 #[cfg(not(feature = "parallel"))]
306 {
307 for obs in observers {
308 let config = ViewshedConfig {
309 observer_x: obs.x,
310 observer_y: obs.y,
311 observer_height: obs.height,
312 target_height,
313 max_distance,
314 cell_size,
315 algorithm: ViewshedAlgorithm::R2ReferencePlane,
316 curvature_correction: curvature,
317 };
318 let result = compute_viewshed_advanced(dem, &config)?;
319
320 for y in 0..height {
321 for x in 0..width {
322 let visible = result
323 .visibility
324 .get_pixel(x, y)
325 .map_err(AlgorithmError::Core)?;
326 if visible > 0.0 {
327 let current = cumulative.get_pixel(x, y).map_err(AlgorithmError::Core)?;
328 cumulative
329 .set_pixel(x, y, current + 1.0)
330 .map_err(AlgorithmError::Core)?;
331 }
332 }
333 }
334 }
335 }
336
337 Ok(cumulative)
338}
339
340pub fn compute_los_profile(
358 dem: &RasterBuffer,
359 from_x: u64,
360 from_y: u64,
361 from_height: f64,
362 to_x: u64,
363 to_y: u64,
364 cell_size: f64,
365 curvature: Option<CurvatureCorrection>,
366) -> Result<Vec<(f64, f64, f64)>> {
367 let observer_elev = dem
368 .get_pixel(from_x, from_y)
369 .map_err(AlgorithmError::Core)?
370 + from_height;
371
372 let target_elev = dem.get_pixel(to_x, to_y).map_err(AlgorithmError::Core)?;
373
374 let dx = (to_x as f64 - from_x as f64) * cell_size;
375 let dy = (to_y as f64 - from_y as f64) * cell_size;
376 let total_distance = (dx * dx + dy * dy).sqrt();
377
378 if total_distance < 1e-10 {
379 return Ok(vec![(0.0, observer_elev, observer_elev)]);
380 }
381
382 let num_samples = (total_distance / (cell_size * 0.5)).ceil() as usize;
383 let num_samples = num_samples.max(2);
384
385 let mut profile = Vec::with_capacity(num_samples);
386
387 for i in 0..num_samples {
388 let t = i as f64 / (num_samples - 1) as f64;
389 let px = from_x as f64 + (to_x as f64 - from_x as f64) * t;
390 let py = from_y as f64 + (to_y as f64 - from_y as f64) * t;
391
392 let ix = (px.round() as u64).min(dem.width() - 1);
393 let iy = (py.round() as u64).min(dem.height() - 1);
394
395 let dist = t * total_distance;
396 let mut terrain_elev = dem.get_pixel(ix, iy).map_err(AlgorithmError::Core)?;
397
398 if let Some(ref curv) = curvature {
400 terrain_elev -= earth_curvature_offset(dist, curv);
401 }
402
403 let los_elev = observer_elev + (target_elev - observer_elev) * t;
405
406 profile.push((dist, terrain_elev, los_elev));
407 }
408
409 Ok(profile)
410}
411
412pub fn compute_fresnel_clearance(
433 dem: &RasterBuffer,
434 from_x: u64,
435 from_y: u64,
436 from_height: f64,
437 to_x: u64,
438 to_y: u64,
439 to_height: f64,
440 cell_size: f64,
441 frequency_ghz: f64,
442 curvature: Option<CurvatureCorrection>,
443) -> Result<f64> {
444 let obs_elev = dem
445 .get_pixel(from_x, from_y)
446 .map_err(AlgorithmError::Core)?
447 + from_height;
448 let tgt_elev = dem.get_pixel(to_x, to_y).map_err(AlgorithmError::Core)? + to_height;
449
450 let dx = (to_x as f64 - from_x as f64) * cell_size;
451 let dy = (to_y as f64 - from_y as f64) * cell_size;
452 let total_distance = (dx * dx + dy * dy).sqrt();
453
454 if total_distance < 1e-10 {
455 return Ok(f64::INFINITY);
456 }
457
458 let wavelength = 0.3 / frequency_ghz;
460
461 let num_samples = (total_distance / (cell_size * 0.5)).ceil() as usize;
462 let num_samples = num_samples.max(2);
463
464 let mut min_clearance_ratio = f64::INFINITY;
465
466 for i in 1..(num_samples - 1) {
467 let t = i as f64 / (num_samples - 1) as f64;
468 let px = from_x as f64 + (to_x as f64 - from_x as f64) * t;
469 let py = from_y as f64 + (to_y as f64 - from_y as f64) * t;
470
471 let ix = (px.round() as u64).min(dem.width() - 1);
472 let iy = (py.round() as u64).min(dem.height() - 1);
473
474 let dist_from = t * total_distance;
475 let dist_to = (1.0 - t) * total_distance;
476
477 let mut terrain_elev = dem.get_pixel(ix, iy).map_err(AlgorithmError::Core)?;
478
479 if let Some(ref curv) = curvature {
480 terrain_elev -= earth_curvature_offset(dist_from, curv);
481 }
482
483 let los_elev = obs_elev + (tgt_elev - obs_elev) * t;
485
486 let fresnel_radius = (wavelength * dist_from * dist_to / total_distance).sqrt();
488
489 let clearance = los_elev - terrain_elev;
491 let ratio = if fresnel_radius > 1e-10 {
492 clearance / fresnel_radius
493 } else {
494 if clearance >= 0.0 {
495 f64::INFINITY
496 } else {
497 f64::NEG_INFINITY
498 }
499 };
500
501 if ratio < min_clearance_ratio {
502 min_clearance_ratio = ratio;
503 }
504 }
505
506 Ok(min_clearance_ratio)
507}
508
509fn viewshed_r1(dem: &RasterBuffer, config: &ViewshedConfig) -> Result<ViewshedResult> {
515 let width = dem.width();
516 let height = dem.height();
517 let mut visibility = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
518 let mut elev_angle = RasterBuffer::zeros(width, height, RasterDataType::Float64);
519
520 let observer_elev = dem
521 .get_pixel(config.observer_x, config.observer_y)
522 .map_err(AlgorithmError::Core)?
523 + config.observer_height;
524
525 for y in 0..height {
526 for x in 0..width {
527 if x == config.observer_x && y == config.observer_y {
528 visibility
529 .set_pixel(x, y, 1.0)
530 .map_err(AlgorithmError::Core)?;
531 continue;
532 }
533
534 let dx = (x as f64 - config.observer_x as f64) * config.cell_size;
535 let dy = (y as f64 - config.observer_y as f64) * config.cell_size;
536 let distance = (dx * dx + dy * dy).sqrt();
537
538 if let Some(max_dist) = config.max_distance {
539 if distance > max_dist {
540 continue;
541 }
542 }
543
544 let target_elev =
545 dem.get_pixel(x, y).map_err(AlgorithmError::Core)? + config.target_height;
546
547 let (is_vis, angle) = check_line_of_sight(
548 dem,
549 config.observer_x,
550 config.observer_y,
551 observer_elev,
552 x,
553 y,
554 target_elev,
555 config.cell_size,
556 &config.curvature_correction,
557 )?;
558
559 if is_vis {
560 visibility
561 .set_pixel(x, y, 1.0)
562 .map_err(AlgorithmError::Core)?;
563 elev_angle
564 .set_pixel(x, y, angle)
565 .map_err(AlgorithmError::Core)?;
566 }
567 }
568 }
569
570 Ok(ViewshedResult {
571 visibility,
572 elevation_angle: Some(elev_angle),
573 })
574}
575
576fn check_line_of_sight(
578 dem: &RasterBuffer,
579 x0: u64,
580 y0: u64,
581 elev0: f64,
582 x1: u64,
583 y1: u64,
584 elev1: f64,
585 cell_size: f64,
586 curvature: &Option<CurvatureCorrection>,
587) -> Result<(bool, f64)> {
588 let dx = (x1 as f64 - x0 as f64) * cell_size;
589 let dy = (y1 as f64 - y0 as f64) * cell_size;
590 let total_distance = (dx * dx + dy * dy).sqrt();
591
592 if total_distance < 1e-10 {
593 return Ok((true, 0.0));
594 }
595
596 let target_angle = (elev1 - elev0).atan2(total_distance);
598
599 let steps = ((total_distance / cell_size) * 2.0).max(2.0) as usize;
600 let mut max_angle = f64::NEG_INFINITY;
601
602 for i in 1..steps {
603 let t = i as f64 / steps as f64;
604 let px = x0 as f64 + (x1 as f64 - x0 as f64) * t;
605 let py = y0 as f64 + (y1 as f64 - y0 as f64) * t;
606
607 let ix = px.round() as u64;
608 let iy = py.round() as u64;
609
610 if ix >= dem.width() || iy >= dem.height() {
611 continue;
612 }
613
614 let mut terrain_elev = dem.get_pixel(ix, iy).map_err(AlgorithmError::Core)?;
615 let curr_dist = t * total_distance;
616
617 if let Some(curv) = curvature {
619 terrain_elev -= earth_curvature_offset(curr_dist, curv);
620 }
621
622 let angle = (terrain_elev - elev0).atan2(curr_dist);
623
624 if angle > max_angle {
625 max_angle = angle;
626 }
627
628 if angle > target_angle + 1e-9 {
629 return Ok((false, 0.0));
630 }
631 }
632
633 Ok((true, target_angle))
634}
635
636fn viewshed_r2(dem: &RasterBuffer, config: &ViewshedConfig) -> Result<ViewshedResult> {
646 let width = dem.width();
647 let height = dem.height();
648 let mut visibility = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
649
650 let observer_elev = dem
651 .get_pixel(config.observer_x, config.observer_y)
652 .map_err(AlgorithmError::Core)?
653 + config.observer_height;
654
655 visibility
657 .set_pixel(config.observer_x, config.observer_y, 1.0)
658 .map_err(AlgorithmError::Core)?;
659
660 let max_cells = if let Some(max_dist) = config.max_distance {
662 (max_dist / config.cell_size).ceil() as i64
663 } else {
664 (width.max(height)) as i64
665 };
666
667 let num_rays = (2.0 * core::f64::consts::PI * max_cells as f64).ceil() as usize;
669 let num_rays = num_rays.max(360);
670
671 for ray_idx in 0..num_rays {
672 let angle = 2.0 * core::f64::consts::PI * ray_idx as f64 / num_rays as f64;
673 let cos_a = angle.cos();
674 let sin_a = angle.sin();
675
676 let mut max_elev_angle = f64::NEG_INFINITY;
677
678 for step in 1..=max_cells {
680 let fx = config.observer_x as f64 + step as f64 * cos_a;
681 let fy = config.observer_y as f64 + step as f64 * sin_a;
682
683 let ix = fx.round() as i64;
684 let iy = fy.round() as i64;
685
686 if ix < 0 || ix >= width as i64 || iy < 0 || iy >= height as i64 {
687 break;
688 }
689
690 let ix_u = ix as u64;
691 let iy_u = iy as u64;
692
693 let dist = step as f64 * config.cell_size;
694
695 if let Some(max_dist) = config.max_distance {
696 if dist > max_dist {
697 break;
698 }
699 }
700
701 let mut terrain_elev =
702 dem.get_pixel(ix_u, iy_u).map_err(AlgorithmError::Core)? + config.target_height;
703
704 if let Some(ref curv) = config.curvature_correction {
706 terrain_elev -= earth_curvature_offset(dist, curv);
707 }
708
709 let elev_angle = (terrain_elev - observer_elev).atan2(dist);
710
711 if elev_angle >= max_elev_angle {
712 visibility
713 .set_pixel(ix_u, iy_u, 1.0)
714 .map_err(AlgorithmError::Core)?;
715 max_elev_angle = elev_angle;
716 }
717 }
718 }
719
720 Ok(ViewshedResult {
721 visibility,
722 elevation_angle: None,
723 })
724}
725
726fn viewshed_r3(dem: &RasterBuffer, config: &ViewshedConfig) -> Result<ViewshedResult> {
736 let width = dem.width();
737 let height = dem.height();
738 let mut visibility = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
739
740 let observer_elev = dem
741 .get_pixel(config.observer_x, config.observer_y)
742 .map_err(AlgorithmError::Core)?
743 + config.observer_height;
744
745 visibility
746 .set_pixel(config.observer_x, config.observer_y, 1.0)
747 .map_err(AlgorithmError::Core)?;
748
749 let mut cells: Vec<(u64, u64, f64, f64)> = Vec::new(); let max_dist_sq = config.max_distance.map(|d| d * d);
753
754 for y in 0..height {
755 for x in 0..width {
756 if x == config.observer_x && y == config.observer_y {
757 continue;
758 }
759
760 let dx = (x as f64 - config.observer_x as f64) * config.cell_size;
761 let dy = (y as f64 - config.observer_y as f64) * config.cell_size;
762 let dist_sq = dx * dx + dy * dy;
763
764 if let Some(max_sq) = max_dist_sq {
765 if dist_sq > max_sq {
766 continue;
767 }
768 }
769
770 let angle = dy.atan2(dx);
771 let distance = dist_sq.sqrt();
772 cells.push((x, y, angle, distance));
773 }
774 }
775
776 cells.sort_by(|a, b| {
778 a.2.partial_cmp(&b.2)
779 .unwrap_or(core::cmp::Ordering::Equal)
780 .then_with(|| a.3.partial_cmp(&b.3).unwrap_or(core::cmp::Ordering::Equal))
781 });
782
783 let num_sectors = 3600usize; let mut sector_max_angle = vec![f64::NEG_INFINITY; num_sectors];
787
788 for (x, y, angle, distance) in &cells {
789 let sector_idx = (((angle + core::f64::consts::PI) / (2.0 * core::f64::consts::PI)
790 * num_sectors as f64)
791 .floor() as usize)
792 .min(num_sectors - 1);
793
794 let mut terrain_elev =
795 dem.get_pixel(*x, *y).map_err(AlgorithmError::Core)? + config.target_height;
796
797 if let Some(ref curv) = config.curvature_correction {
798 terrain_elev -= earth_curvature_offset(*distance, curv);
799 }
800
801 let elev_angle = (terrain_elev - observer_elev).atan2(*distance);
802
803 if elev_angle >= sector_max_angle[sector_idx] {
804 visibility
805 .set_pixel(*x, *y, 1.0)
806 .map_err(AlgorithmError::Core)?;
807 sector_max_angle[sector_idx] = elev_angle;
808 }
809 }
810
811 Ok(ViewshedResult {
812 visibility,
813 elevation_angle: None,
814 })
815}
816
817fn earth_curvature_offset(distance: f64, correction: &CurvatureCorrection) -> f64 {
827 let effective_radius = correction.earth_radius / (1.0 - correction.refraction_coefficient);
828 (distance * distance) / (2.0 * effective_radius)
829}
830
831#[cfg(test)]
836mod tests {
837 use super::*;
838
839 fn create_flat_dem(size: u64) -> RasterBuffer {
840 RasterBuffer::zeros(size, size, RasterDataType::Float32)
841 }
842
843 fn create_hill_dem() -> RasterBuffer {
844 let mut dem = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
845 for y in 0..20 {
847 for x in 0..20 {
848 let dx = x as f64 - 10.0;
849 let dy = y as f64 - 10.0;
850 let dist = (dx * dx + dy * dy).sqrt();
851 let elev = (5.0 - dist).max(0.0);
852 let _ = dem.set_pixel(x, y, elev);
853 }
854 }
855 dem
856 }
857
858 fn create_wall_dem() -> RasterBuffer {
859 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
860 for x in 0..10 {
862 let _ = dem.set_pixel(x, 6, 50.0);
863 }
864 dem
865 }
866
867 #[test]
870 fn test_viewshed_flat_terrain_r1() {
871 let dem = create_flat_dem(10);
872 let result = compute_viewshed(&dem, 5, 5, 10.0, 0.0, None, 1.0);
873 assert!(result.is_ok());
874 let viewshed = result.expect("viewshed");
875
876 for y in 0..10 {
878 for x in 0..10 {
879 let val = viewshed.get_pixel(x, y).expect("pixel");
880 assert!(
881 val > 0.0,
882 "Cell ({x},{y}) should be visible on flat terrain"
883 );
884 }
885 }
886 }
887
888 #[test]
889 fn test_viewshed_flat_terrain_r2() {
890 let dem = create_flat_dem(10);
891 let config = ViewshedConfig {
892 observer_x: 5,
893 observer_y: 5,
894 observer_height: 10.0,
895 target_height: 0.0,
896 max_distance: None,
897 cell_size: 1.0,
898 algorithm: ViewshedAlgorithm::R2ReferencePlane,
899 curvature_correction: None,
900 };
901 let result = compute_viewshed_advanced(&dem, &config);
902 assert!(result.is_ok());
903 }
904
905 #[test]
906 fn test_viewshed_flat_terrain_r3() {
907 let dem = create_flat_dem(10);
908 let config = ViewshedConfig {
909 observer_x: 5,
910 observer_y: 5,
911 observer_height: 10.0,
912 target_height: 0.0,
913 max_distance: None,
914 cell_size: 1.0,
915 algorithm: ViewshedAlgorithm::R3SweepLine,
916 curvature_correction: None,
917 };
918 let result = compute_viewshed_advanced(&dem, &config);
919 assert!(result.is_ok());
920 }
921
922 #[test]
925 fn test_viewshed_with_wall() {
926 let dem = create_wall_dem();
927 let viewshed = compute_viewshed(&dem, 5, 5, 1.0, 0.0, None, 1.0).expect("viewshed");
928
929 let obs = viewshed.get_pixel(5, 5).expect("obs");
931 assert!(obs > 0.0);
932
933 let behind = viewshed.get_pixel(5, 8).expect("behind");
935 assert!(
936 behind < 0.5,
937 "Cell behind wall should not be visible, got {behind}"
938 );
939 }
940
941 #[test]
944 fn test_viewshed_max_distance() {
945 let dem = create_flat_dem(20);
946 let viewshed = compute_viewshed(&dem, 10, 10, 10.0, 0.0, Some(5.0), 1.0).expect("viewshed");
947
948 let near = viewshed.get_pixel(10, 12).expect("near");
950 assert!(near > 0.0);
951
952 let far = viewshed.get_pixel(0, 0).expect("far");
954 assert!(
955 far < 0.5,
956 "Cell beyond max distance should not be visible, got {far}"
957 );
958 }
959
960 #[test]
963 fn test_earth_curvature_offset() {
964 let correction = CurvatureCorrection::default();
965
966 let offset_1km = earth_curvature_offset(1000.0, &correction);
968 assert!(offset_1km > 0.0 && offset_1km < 0.1);
969
970 let offset_10km = earth_curvature_offset(10_000.0, &correction);
972 assert!(offset_10km > offset_1km);
973
974 assert!(offset_10km > 5.0 && offset_10km < 10.0);
979 }
980
981 #[test]
982 fn test_viewshed_with_curvature() {
983 let dem = create_flat_dem(10);
984 let config = ViewshedConfig {
985 observer_x: 5,
986 observer_y: 5,
987 observer_height: 10.0,
988 target_height: 0.0,
989 max_distance: None,
990 cell_size: 1.0,
991 algorithm: ViewshedAlgorithm::R1LineOfSight,
992 curvature_correction: Some(CurvatureCorrection::default()),
993 };
994 let result = compute_viewshed_advanced(&dem, &config);
995 assert!(result.is_ok());
996 }
997
998 #[test]
1001 fn test_cumulative_viewshed() {
1002 let dem = create_flat_dem(10);
1003 let observers = vec![(2, 2, 10.0), (7, 7, 10.0)];
1004
1005 let cumulative =
1006 compute_cumulative_viewshed(&dem, &observers, 0.0, None, 1.0).expect("cumulative");
1007
1008 let center = cumulative.get_pixel(5, 5).expect("center");
1009 assert!(
1010 center >= 2.0,
1011 "Center should be visible from both observers"
1012 );
1013 }
1014
1015 #[test]
1016 fn test_cumulative_viewshed_advanced() {
1017 let dem = create_flat_dem(10);
1018 let observers = vec![
1019 ObserverPoint {
1020 x: 2,
1021 y: 2,
1022 height: 10.0,
1023 },
1024 ObserverPoint {
1025 x: 7,
1026 y: 7,
1027 height: 10.0,
1028 },
1029 ];
1030
1031 let result = compute_cumulative_viewshed_advanced(
1032 &dem,
1033 &observers,
1034 0.0,
1035 None,
1036 1.0,
1037 Some(CurvatureCorrection::default()),
1038 );
1039 assert!(result.is_ok());
1040 }
1041
1042 #[test]
1045 fn test_los_profile() {
1046 let dem = create_flat_dem(10);
1047 let profile = compute_los_profile(&dem, 0, 0, 10.0, 9, 9, 1.0, None);
1048 assert!(profile.is_ok());
1049 let prof = profile.expect("profile");
1050 assert!(prof.len() >= 2);
1051
1052 assert!(prof[0].0.abs() < 1e-6);
1054 }
1055
1056 #[test]
1057 fn test_los_profile_with_curvature() {
1058 let dem = create_flat_dem(10);
1059 let profile = compute_los_profile(
1060 &dem,
1061 0,
1062 0,
1063 10.0,
1064 9,
1065 9,
1066 1.0,
1067 Some(CurvatureCorrection::default()),
1068 );
1069 assert!(profile.is_ok());
1070 }
1071
1072 #[test]
1075 fn test_fresnel_clearance_flat() {
1076 let dem = create_flat_dem(10);
1077 let clearance = compute_fresnel_clearance(&dem, 0, 0, 10.0, 9, 9, 10.0, 1.0, 2.4, None);
1078 assert!(clearance.is_ok());
1079 let c = clearance.expect("clearance");
1080 assert!(c > 0.0, "Fresnel clearance should be positive, got {c}");
1082 }
1083
1084 #[test]
1085 fn test_fresnel_clearance_with_wall() {
1086 let dem = create_wall_dem();
1087 let clearance = compute_fresnel_clearance(&dem, 5, 3, 2.0, 5, 9, 2.0, 1.0, 2.4, None);
1088 assert!(clearance.is_ok());
1089 let c = clearance.expect("clearance");
1090 assert!(
1092 c < 0.0,
1093 "Fresnel clearance should be negative with wall, got {c}"
1094 );
1095 }
1096
1097 #[test]
1100 fn test_viewshed_invalid_observer() {
1101 let dem = create_flat_dem(10);
1102 let config = ViewshedConfig {
1103 observer_x: 100,
1104 observer_y: 100,
1105 observer_height: 10.0,
1106 target_height: 0.0,
1107 max_distance: None,
1108 cell_size: 1.0,
1109 algorithm: ViewshedAlgorithm::R1LineOfSight,
1110 curvature_correction: None,
1111 };
1112 let result = compute_viewshed_advanced(&dem, &config);
1113 assert!(result.is_err());
1114 }
1115
1116 #[test]
1119 fn test_viewshed_elevation_angle() {
1120 let dem = create_flat_dem(10);
1121 let config = ViewshedConfig {
1122 observer_x: 5,
1123 observer_y: 5,
1124 observer_height: 10.0,
1125 target_height: 0.0,
1126 max_distance: None,
1127 cell_size: 1.0,
1128 algorithm: ViewshedAlgorithm::R1LineOfSight,
1129 curvature_correction: None,
1130 };
1131 let result = compute_viewshed_advanced(&dem, &config).expect("result");
1132 assert!(result.elevation_angle.is_some());
1133
1134 let elev_angle = result.elevation_angle.expect("elev_angle");
1135 let angle = elev_angle.get_pixel(5, 8).expect("angle");
1136 assert!(
1139 angle < 0.0,
1140 "Elevation angle should be negative (looking down), got {angle}"
1141 );
1142 }
1143}