1use crate::error::{AlgorithmError, Result};
41use oxigdal_core::buffer::RasterBuffer;
42use oxigdal_core::types::RasterDataType;
43
44#[cfg(feature = "parallel")]
45use rayon::prelude::*;
46
47const EARTH_RADIUS_M: f64 = 6_371_000.0;
53
54const REFRACTION_COEFF: f64 = 0.13;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum ViewshedAlgorithm {
72 #[default]
74 R1LineOfSight,
75
76 R2ReferencePlane,
78
79 R3SweepLine,
81}
82
83#[derive(Debug, Clone, Copy)]
85pub struct CurvatureCorrection {
86 pub earth_radius: f64,
88
89 pub refraction_coefficient: f64,
94}
95
96impl Default for CurvatureCorrection {
97 fn default() -> Self {
98 Self {
99 earth_radius: EARTH_RADIUS_M,
100 refraction_coefficient: REFRACTION_COEFF,
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct ViewshedConfig {
108 pub observer_x: u64,
110 pub observer_y: u64,
112 pub observer_height: f64,
114 pub target_height: f64,
116 pub max_distance: Option<f64>,
118 pub cell_size: f64,
120 pub algorithm: ViewshedAlgorithm,
122 pub curvature_correction: Option<CurvatureCorrection>,
124}
125
126#[derive(Debug, Clone, Copy)]
128pub struct ObserverPoint {
129 pub x: u64,
131 pub y: u64,
133 pub height: f64,
135}
136
137#[derive(Debug)]
139pub struct ViewshedResult {
140 pub visibility: RasterBuffer,
142 pub elevation_angle: Option<RasterBuffer>,
145}
146
147pub fn compute_viewshed(
169 dem: &RasterBuffer,
170 observer_x: u64,
171 observer_y: u64,
172 observer_height: f64,
173 target_height: f64,
174 max_distance: Option<f64>,
175 cell_size: f64,
176) -> Result<RasterBuffer> {
177 let config = ViewshedConfig {
178 observer_x,
179 observer_y,
180 observer_height,
181 target_height,
182 max_distance,
183 cell_size,
184 algorithm: ViewshedAlgorithm::R1LineOfSight,
185 curvature_correction: None,
186 };
187
188 let result = compute_viewshed_advanced(dem, &config)?;
189 Ok(result.visibility)
190}
191
192pub fn compute_viewshed_advanced(
203 dem: &RasterBuffer,
204 config: &ViewshedConfig,
205) -> Result<ViewshedResult> {
206 if config.observer_x >= dem.width() || config.observer_y >= dem.height() {
208 return Err(AlgorithmError::InvalidParameter {
209 parameter: "observer position",
210 message: format!(
211 "Observer ({}, {}) is outside DEM bounds ({}, {})",
212 config.observer_x,
213 config.observer_y,
214 dem.width(),
215 dem.height()
216 ),
217 });
218 }
219
220 match config.algorithm {
221 ViewshedAlgorithm::R1LineOfSight => viewshed_r1(dem, config),
222 ViewshedAlgorithm::R2ReferencePlane => viewshed_r2(dem, config),
223 ViewshedAlgorithm::R3SweepLine => viewshed_r3(dem, config),
224 }
225}
226
227pub fn compute_cumulative_viewshed(
243 dem: &RasterBuffer,
244 observers: &[(u64, u64, f64)],
245 target_height: f64,
246 max_distance: Option<f64>,
247 cell_size: f64,
248) -> Result<RasterBuffer> {
249 let observer_points: Vec<ObserverPoint> = observers
250 .iter()
251 .map(|&(x, y, height)| ObserverPoint { x, y, height })
252 .collect();
253
254 compute_cumulative_viewshed_advanced(
255 dem,
256 &observer_points,
257 target_height,
258 max_distance,
259 cell_size,
260 None,
261 )
262}
263
264pub fn compute_cumulative_viewshed_advanced(
279 dem: &RasterBuffer,
280 observers: &[ObserverPoint],
281 target_height: f64,
282 max_distance: Option<f64>,
283 cell_size: f64,
284 curvature: Option<CurvatureCorrection>,
285) -> Result<RasterBuffer> {
286 let width = dem.width();
287 let height = dem.height();
288 let mut cumulative = RasterBuffer::zeros(width, height, RasterDataType::Float64);
289
290 #[cfg(feature = "parallel")]
291 {
292 let viewsheds: Result<Vec<RasterBuffer>> = observers
293 .par_iter()
294 .map(|obs| {
295 let config = ViewshedConfig {
296 observer_x: obs.x,
297 observer_y: obs.y,
298 observer_height: obs.height,
299 target_height,
300 max_distance,
301 cell_size,
302 algorithm: ViewshedAlgorithm::R2ReferencePlane,
303 curvature_correction: curvature,
304 };
305 let result = compute_viewshed_advanced(dem, &config)?;
306 Ok(result.visibility)
307 })
308 .collect();
309
310 for viewshed in viewsheds? {
311 for y in 0..height {
312 for x in 0..width {
313 let visible = viewshed.get_pixel(x, y).map_err(AlgorithmError::Core)?;
314 if visible > 0.0 {
315 let current = cumulative.get_pixel(x, y).map_err(AlgorithmError::Core)?;
316 cumulative
317 .set_pixel(x, y, current + 1.0)
318 .map_err(AlgorithmError::Core)?;
319 }
320 }
321 }
322 }
323 }
324
325 #[cfg(not(feature = "parallel"))]
326 {
327 for obs in observers {
328 let config = ViewshedConfig {
329 observer_x: obs.x,
330 observer_y: obs.y,
331 observer_height: obs.height,
332 target_height,
333 max_distance,
334 cell_size,
335 algorithm: ViewshedAlgorithm::R2ReferencePlane,
336 curvature_correction: curvature,
337 };
338 let result = compute_viewshed_advanced(dem, &config)?;
339
340 for y in 0..height {
341 for x in 0..width {
342 let visible = result
343 .visibility
344 .get_pixel(x, y)
345 .map_err(AlgorithmError::Core)?;
346 if visible > 0.0 {
347 let current = cumulative.get_pixel(x, y).map_err(AlgorithmError::Core)?;
348 cumulative
349 .set_pixel(x, y, current + 1.0)
350 .map_err(AlgorithmError::Core)?;
351 }
352 }
353 }
354 }
355 }
356
357 Ok(cumulative)
358}
359
360pub fn compute_los_profile(
378 dem: &RasterBuffer,
379 from_x: u64,
380 from_y: u64,
381 from_height: f64,
382 to_x: u64,
383 to_y: u64,
384 cell_size: f64,
385 curvature: Option<CurvatureCorrection>,
386) -> Result<Vec<(f64, f64, f64)>> {
387 let observer_elev = dem
388 .get_pixel(from_x, from_y)
389 .map_err(AlgorithmError::Core)?
390 + from_height;
391
392 let target_elev = dem.get_pixel(to_x, to_y).map_err(AlgorithmError::Core)?;
393
394 let dx = (to_x as f64 - from_x as f64) * cell_size;
395 let dy = (to_y as f64 - from_y as f64) * cell_size;
396 let total_distance = (dx * dx + dy * dy).sqrt();
397
398 if total_distance < 1e-10 {
399 return Ok(vec![(0.0, observer_elev, observer_elev)]);
400 }
401
402 let num_samples = (total_distance / (cell_size * 0.5)).ceil() as usize;
403 let num_samples = num_samples.max(2);
404
405 let mut profile = Vec::with_capacity(num_samples);
406
407 for i in 0..num_samples {
408 let t = i as f64 / (num_samples - 1) as f64;
409 let px = from_x as f64 + (to_x as f64 - from_x as f64) * t;
410 let py = from_y as f64 + (to_y as f64 - from_y as f64) * t;
411
412 let ix = (px.round() as u64).min(dem.width() - 1);
413 let iy = (py.round() as u64).min(dem.height() - 1);
414
415 let dist = t * total_distance;
416 let mut terrain_elev = dem.get_pixel(ix, iy).map_err(AlgorithmError::Core)?;
417
418 if let Some(ref curv) = curvature {
420 terrain_elev -= earth_curvature_offset(dist, curv);
421 }
422
423 let los_elev = observer_elev + (target_elev - observer_elev) * t;
425
426 profile.push((dist, terrain_elev, los_elev));
427 }
428
429 Ok(profile)
430}
431
432pub fn compute_fresnel_clearance(
453 dem: &RasterBuffer,
454 from_x: u64,
455 from_y: u64,
456 from_height: f64,
457 to_x: u64,
458 to_y: u64,
459 to_height: f64,
460 cell_size: f64,
461 frequency_ghz: f64,
462 curvature: Option<CurvatureCorrection>,
463) -> Result<f64> {
464 let obs_elev = dem
465 .get_pixel(from_x, from_y)
466 .map_err(AlgorithmError::Core)?
467 + from_height;
468 let tgt_elev = dem.get_pixel(to_x, to_y).map_err(AlgorithmError::Core)? + to_height;
469
470 let dx = (to_x as f64 - from_x as f64) * cell_size;
471 let dy = (to_y as f64 - from_y as f64) * cell_size;
472 let total_distance = (dx * dx + dy * dy).sqrt();
473
474 if total_distance < 1e-10 {
475 return Ok(f64::INFINITY);
476 }
477
478 let wavelength = 0.3 / frequency_ghz;
480
481 let num_samples = (total_distance / (cell_size * 0.5)).ceil() as usize;
482 let num_samples = num_samples.max(2);
483
484 let mut min_clearance_ratio = f64::INFINITY;
485
486 for i in 1..(num_samples - 1) {
487 let t = i as f64 / (num_samples - 1) as f64;
488 let px = from_x as f64 + (to_x as f64 - from_x as f64) * t;
489 let py = from_y as f64 + (to_y as f64 - from_y as f64) * t;
490
491 let ix = (px.round() as u64).min(dem.width() - 1);
492 let iy = (py.round() as u64).min(dem.height() - 1);
493
494 let dist_from = t * total_distance;
495 let dist_to = (1.0 - t) * total_distance;
496
497 let mut terrain_elev = dem.get_pixel(ix, iy).map_err(AlgorithmError::Core)?;
498
499 if let Some(ref curv) = curvature {
500 terrain_elev -= earth_curvature_offset(dist_from, curv);
501 }
502
503 let los_elev = obs_elev + (tgt_elev - obs_elev) * t;
505
506 let fresnel_radius = (wavelength * dist_from * dist_to / total_distance).sqrt();
508
509 let clearance = los_elev - terrain_elev;
511 let ratio = if fresnel_radius > 1e-10 {
512 clearance / fresnel_radius
513 } else {
514 if clearance >= 0.0 {
515 f64::INFINITY
516 } else {
517 f64::NEG_INFINITY
518 }
519 };
520
521 if ratio < min_clearance_ratio {
522 min_clearance_ratio = ratio;
523 }
524 }
525
526 Ok(min_clearance_ratio)
527}
528
529fn viewshed_r1(dem: &RasterBuffer, config: &ViewshedConfig) -> Result<ViewshedResult> {
535 let width = dem.width();
536 let height = dem.height();
537 let mut visibility = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
538 let mut elev_angle = RasterBuffer::zeros(width, height, RasterDataType::Float64);
539
540 let observer_elev = dem
541 .get_pixel(config.observer_x, config.observer_y)
542 .map_err(AlgorithmError::Core)?
543 + config.observer_height;
544
545 for y in 0..height {
546 for x in 0..width {
547 if x == config.observer_x && y == config.observer_y {
548 visibility
549 .set_pixel(x, y, 1.0)
550 .map_err(AlgorithmError::Core)?;
551 continue;
552 }
553
554 let dx = (x as f64 - config.observer_x as f64) * config.cell_size;
555 let dy = (y as f64 - config.observer_y as f64) * config.cell_size;
556 let distance = (dx * dx + dy * dy).sqrt();
557
558 if let Some(max_dist) = config.max_distance {
559 if distance > max_dist {
560 continue;
561 }
562 }
563
564 let target_elev =
565 dem.get_pixel(x, y).map_err(AlgorithmError::Core)? + config.target_height;
566
567 let (is_vis, angle) = check_line_of_sight(
568 dem,
569 config.observer_x,
570 config.observer_y,
571 observer_elev,
572 x,
573 y,
574 target_elev,
575 config.cell_size,
576 &config.curvature_correction,
577 )?;
578
579 if is_vis {
580 visibility
581 .set_pixel(x, y, 1.0)
582 .map_err(AlgorithmError::Core)?;
583 elev_angle
584 .set_pixel(x, y, angle)
585 .map_err(AlgorithmError::Core)?;
586 }
587 }
588 }
589
590 Ok(ViewshedResult {
591 visibility,
592 elevation_angle: Some(elev_angle),
593 })
594}
595
596fn check_line_of_sight(
598 dem: &RasterBuffer,
599 x0: u64,
600 y0: u64,
601 elev0: f64,
602 x1: u64,
603 y1: u64,
604 elev1: f64,
605 cell_size: f64,
606 curvature: &Option<CurvatureCorrection>,
607) -> Result<(bool, f64)> {
608 let dx = (x1 as f64 - x0 as f64) * cell_size;
609 let dy = (y1 as f64 - y0 as f64) * cell_size;
610 let total_distance = (dx * dx + dy * dy).sqrt();
611
612 if total_distance < 1e-10 {
613 return Ok((true, 0.0));
614 }
615
616 let target_angle = (elev1 - elev0).atan2(total_distance);
618
619 let steps = ((total_distance / cell_size) * 2.0).max(2.0) as usize;
620 let mut max_angle = f64::NEG_INFINITY;
621
622 for i in 1..steps {
623 let t = i as f64 / steps as f64;
624 let px = x0 as f64 + (x1 as f64 - x0 as f64) * t;
625 let py = y0 as f64 + (y1 as f64 - y0 as f64) * t;
626
627 let ix = px.round() as u64;
628 let iy = py.round() as u64;
629
630 if ix >= dem.width() || iy >= dem.height() {
631 continue;
632 }
633
634 let mut terrain_elev = dem.get_pixel(ix, iy).map_err(AlgorithmError::Core)?;
635 let curr_dist = t * total_distance;
636
637 if let Some(curv) = curvature {
639 terrain_elev -= earth_curvature_offset(curr_dist, curv);
640 }
641
642 let angle = (terrain_elev - elev0).atan2(curr_dist);
643
644 if angle > max_angle {
645 max_angle = angle;
646 }
647
648 if angle > target_angle + 1e-9 {
649 return Ok((false, 0.0));
650 }
651 }
652
653 Ok((true, target_angle))
654}
655
656fn viewshed_r2(dem: &RasterBuffer, config: &ViewshedConfig) -> Result<ViewshedResult> {
666 let width = dem.width();
667 let height = dem.height();
668 let mut visibility = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
669
670 let observer_elev = dem
671 .get_pixel(config.observer_x, config.observer_y)
672 .map_err(AlgorithmError::Core)?
673 + config.observer_height;
674
675 visibility
677 .set_pixel(config.observer_x, config.observer_y, 1.0)
678 .map_err(AlgorithmError::Core)?;
679
680 let max_cells = if let Some(max_dist) = config.max_distance {
682 (max_dist / config.cell_size).ceil() as i64
683 } else {
684 (width.max(height)) as i64
685 };
686
687 let num_rays = (2.0 * core::f64::consts::PI * max_cells as f64).ceil() as usize;
689 let num_rays = num_rays.max(360);
690
691 for ray_idx in 0..num_rays {
692 let angle = 2.0 * core::f64::consts::PI * ray_idx as f64 / num_rays as f64;
693 let cos_a = angle.cos();
694 let sin_a = angle.sin();
695
696 let mut max_elev_angle = f64::NEG_INFINITY;
697
698 for step in 1..=max_cells {
700 let fx = config.observer_x as f64 + step as f64 * cos_a;
701 let fy = config.observer_y as f64 + step as f64 * sin_a;
702
703 let ix = fx.round() as i64;
704 let iy = fy.round() as i64;
705
706 if ix < 0 || ix >= width as i64 || iy < 0 || iy >= height as i64 {
707 break;
708 }
709
710 let ix_u = ix as u64;
711 let iy_u = iy as u64;
712
713 let dist = step as f64 * config.cell_size;
714
715 if let Some(max_dist) = config.max_distance {
716 if dist > max_dist {
717 break;
718 }
719 }
720
721 let mut terrain_elev =
722 dem.get_pixel(ix_u, iy_u).map_err(AlgorithmError::Core)? + config.target_height;
723
724 if let Some(ref curv) = config.curvature_correction {
726 terrain_elev -= earth_curvature_offset(dist, curv);
727 }
728
729 let elev_angle = (terrain_elev - observer_elev).atan2(dist);
730
731 if elev_angle >= max_elev_angle {
732 visibility
733 .set_pixel(ix_u, iy_u, 1.0)
734 .map_err(AlgorithmError::Core)?;
735 max_elev_angle = elev_angle;
736 }
737 }
738 }
739
740 Ok(ViewshedResult {
741 visibility,
742 elevation_angle: None,
743 })
744}
745
746fn viewshed_r3(dem: &RasterBuffer, config: &ViewshedConfig) -> Result<ViewshedResult> {
756 let width = dem.width();
757 let height = dem.height();
758 let mut visibility = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
759
760 let observer_elev = dem
761 .get_pixel(config.observer_x, config.observer_y)
762 .map_err(AlgorithmError::Core)?
763 + config.observer_height;
764
765 visibility
766 .set_pixel(config.observer_x, config.observer_y, 1.0)
767 .map_err(AlgorithmError::Core)?;
768
769 let mut cells: Vec<(u64, u64, f64, f64)> = Vec::new(); let max_dist_sq = config.max_distance.map(|d| d * d);
773
774 for y in 0..height {
775 for x in 0..width {
776 if x == config.observer_x && y == config.observer_y {
777 continue;
778 }
779
780 let dx = (x as f64 - config.observer_x as f64) * config.cell_size;
781 let dy = (y as f64 - config.observer_y as f64) * config.cell_size;
782 let dist_sq = dx * dx + dy * dy;
783
784 if let Some(max_sq) = max_dist_sq {
785 if dist_sq > max_sq {
786 continue;
787 }
788 }
789
790 let angle = dy.atan2(dx);
791 let distance = dist_sq.sqrt();
792 cells.push((x, y, angle, distance));
793 }
794 }
795
796 cells.sort_by(|a, b| {
798 a.2.partial_cmp(&b.2)
799 .unwrap_or(core::cmp::Ordering::Equal)
800 .then_with(|| a.3.partial_cmp(&b.3).unwrap_or(core::cmp::Ordering::Equal))
801 });
802
803 let num_sectors = 3600usize; let mut sector_max_angle = vec![f64::NEG_INFINITY; num_sectors];
807
808 for (x, y, angle, distance) in &cells {
809 let sector_idx = (((angle + core::f64::consts::PI) / (2.0 * core::f64::consts::PI)
810 * num_sectors as f64)
811 .floor() as usize)
812 .min(num_sectors - 1);
813
814 let mut terrain_elev =
815 dem.get_pixel(*x, *y).map_err(AlgorithmError::Core)? + config.target_height;
816
817 if let Some(ref curv) = config.curvature_correction {
818 terrain_elev -= earth_curvature_offset(*distance, curv);
819 }
820
821 let elev_angle = (terrain_elev - observer_elev).atan2(*distance);
822
823 if elev_angle >= sector_max_angle[sector_idx] {
824 visibility
825 .set_pixel(*x, *y, 1.0)
826 .map_err(AlgorithmError::Core)?;
827 sector_max_angle[sector_idx] = elev_angle;
828 }
829 }
830
831 Ok(ViewshedResult {
832 visibility,
833 elevation_angle: None,
834 })
835}
836
837fn earth_curvature_offset(distance: f64, correction: &CurvatureCorrection) -> f64 {
852 let effective_radius = correction.earth_radius / (1.0 - correction.refraction_coefficient);
853 (distance * distance) / (2.0 * effective_radius)
854}
855
856#[cfg(test)]
861mod tests {
862 use super::*;
863
864 fn create_flat_dem(size: u64) -> RasterBuffer {
865 RasterBuffer::zeros(size, size, RasterDataType::Float32)
866 }
867
868 fn create_hill_dem() -> RasterBuffer {
869 let mut dem = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
870 for y in 0..20 {
872 for x in 0..20 {
873 let dx = x as f64 - 10.0;
874 let dy = y as f64 - 10.0;
875 let dist = (dx * dx + dy * dy).sqrt();
876 let elev = (5.0 - dist).max(0.0);
877 let _ = dem.set_pixel(x, y, elev);
878 }
879 }
880 dem
881 }
882
883 fn create_wall_dem() -> RasterBuffer {
884 let mut dem = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
885 for x in 0..10 {
887 let _ = dem.set_pixel(x, 6, 50.0);
888 }
889 dem
890 }
891
892 #[test]
895 fn test_viewshed_flat_terrain_r1() {
896 let dem = create_flat_dem(10);
897 let result = compute_viewshed(&dem, 5, 5, 10.0, 0.0, None, 1.0);
898 assert!(result.is_ok());
899 let viewshed = result.expect("viewshed");
900
901 for y in 0..10 {
903 for x in 0..10 {
904 let val = viewshed.get_pixel(x, y).expect("pixel");
905 assert!(
906 val > 0.0,
907 "Cell ({x},{y}) should be visible on flat terrain"
908 );
909 }
910 }
911 }
912
913 #[test]
914 fn test_viewshed_flat_terrain_r2() {
915 let dem = create_flat_dem(10);
916 let config = ViewshedConfig {
917 observer_x: 5,
918 observer_y: 5,
919 observer_height: 10.0,
920 target_height: 0.0,
921 max_distance: None,
922 cell_size: 1.0,
923 algorithm: ViewshedAlgorithm::R2ReferencePlane,
924 curvature_correction: None,
925 };
926 let result = compute_viewshed_advanced(&dem, &config);
927 assert!(result.is_ok());
928 }
929
930 #[test]
931 fn test_viewshed_flat_terrain_r3() {
932 let dem = create_flat_dem(10);
933 let config = ViewshedConfig {
934 observer_x: 5,
935 observer_y: 5,
936 observer_height: 10.0,
937 target_height: 0.0,
938 max_distance: None,
939 cell_size: 1.0,
940 algorithm: ViewshedAlgorithm::R3SweepLine,
941 curvature_correction: None,
942 };
943 let result = compute_viewshed_advanced(&dem, &config);
944 assert!(result.is_ok());
945 }
946
947 #[test]
950 fn test_viewshed_with_wall() {
951 let dem = create_wall_dem();
952 let viewshed = compute_viewshed(&dem, 5, 5, 1.0, 0.0, None, 1.0).expect("viewshed");
953
954 let obs = viewshed.get_pixel(5, 5).expect("obs");
956 assert!(obs > 0.0);
957
958 let behind = viewshed.get_pixel(5, 8).expect("behind");
960 assert!(
961 behind < 0.5,
962 "Cell behind wall should not be visible, got {behind}"
963 );
964 }
965
966 #[test]
969 fn test_viewshed_max_distance() {
970 let dem = create_flat_dem(20);
971 let viewshed = compute_viewshed(&dem, 10, 10, 10.0, 0.0, Some(5.0), 1.0).expect("viewshed");
972
973 let near = viewshed.get_pixel(10, 12).expect("near");
975 assert!(near > 0.0);
976
977 let far = viewshed.get_pixel(0, 0).expect("far");
979 assert!(
980 far < 0.5,
981 "Cell beyond max distance should not be visible, got {far}"
982 );
983 }
984
985 #[test]
988 fn test_earth_curvature_offset() {
989 let correction = CurvatureCorrection::default();
990
991 let offset_1km = earth_curvature_offset(1000.0, &correction);
993 assert!(offset_1km > 0.0 && offset_1km < 0.1);
994
995 let offset_10km = earth_curvature_offset(10_000.0, &correction);
997 assert!(offset_10km > offset_1km);
998
999 assert!(offset_10km > 5.0 && offset_10km < 10.0);
1004 }
1005
1006 #[test]
1007 fn test_viewshed_with_curvature() {
1008 let dem = create_flat_dem(10);
1009 let config = ViewshedConfig {
1010 observer_x: 5,
1011 observer_y: 5,
1012 observer_height: 10.0,
1013 target_height: 0.0,
1014 max_distance: None,
1015 cell_size: 1.0,
1016 algorithm: ViewshedAlgorithm::R1LineOfSight,
1017 curvature_correction: Some(CurvatureCorrection::default()),
1018 };
1019 let result = compute_viewshed_advanced(&dem, &config);
1020 assert!(result.is_ok());
1021 }
1022
1023 #[test]
1026 fn test_cumulative_viewshed() {
1027 let dem = create_flat_dem(10);
1028 let observers = vec![(2, 2, 10.0), (7, 7, 10.0)];
1029
1030 let cumulative =
1031 compute_cumulative_viewshed(&dem, &observers, 0.0, None, 1.0).expect("cumulative");
1032
1033 let center = cumulative.get_pixel(5, 5).expect("center");
1034 assert!(
1035 center >= 2.0,
1036 "Center should be visible from both observers"
1037 );
1038 }
1039
1040 #[test]
1041 fn test_cumulative_viewshed_advanced() {
1042 let dem = create_flat_dem(10);
1043 let observers = vec![
1044 ObserverPoint {
1045 x: 2,
1046 y: 2,
1047 height: 10.0,
1048 },
1049 ObserverPoint {
1050 x: 7,
1051 y: 7,
1052 height: 10.0,
1053 },
1054 ];
1055
1056 let result = compute_cumulative_viewshed_advanced(
1057 &dem,
1058 &observers,
1059 0.0,
1060 None,
1061 1.0,
1062 Some(CurvatureCorrection::default()),
1063 );
1064 assert!(result.is_ok());
1065 }
1066
1067 #[test]
1070 fn test_los_profile() {
1071 let dem = create_flat_dem(10);
1072 let profile = compute_los_profile(&dem, 0, 0, 10.0, 9, 9, 1.0, None);
1073 assert!(profile.is_ok());
1074 let prof = profile.expect("profile");
1075 assert!(prof.len() >= 2);
1076
1077 assert!(prof[0].0.abs() < 1e-6);
1079 }
1080
1081 #[test]
1082 fn test_los_profile_with_curvature() {
1083 let dem = create_flat_dem(10);
1084 let profile = compute_los_profile(
1085 &dem,
1086 0,
1087 0,
1088 10.0,
1089 9,
1090 9,
1091 1.0,
1092 Some(CurvatureCorrection::default()),
1093 );
1094 assert!(profile.is_ok());
1095 }
1096
1097 #[test]
1100 fn test_fresnel_clearance_flat() {
1101 let dem = create_flat_dem(10);
1102 let clearance = compute_fresnel_clearance(&dem, 0, 0, 10.0, 9, 9, 10.0, 1.0, 2.4, None);
1103 assert!(clearance.is_ok());
1104 let c = clearance.expect("clearance");
1105 assert!(c > 0.0, "Fresnel clearance should be positive, got {c}");
1107 }
1108
1109 #[test]
1110 fn test_fresnel_clearance_with_wall() {
1111 let dem = create_wall_dem();
1112 let clearance = compute_fresnel_clearance(&dem, 5, 3, 2.0, 5, 9, 2.0, 1.0, 2.4, None);
1113 assert!(clearance.is_ok());
1114 let c = clearance.expect("clearance");
1115 assert!(
1117 c < 0.0,
1118 "Fresnel clearance should be negative with wall, got {c}"
1119 );
1120 }
1121
1122 #[test]
1125 fn test_viewshed_invalid_observer() {
1126 let dem = create_flat_dem(10);
1127 let config = ViewshedConfig {
1128 observer_x: 100,
1129 observer_y: 100,
1130 observer_height: 10.0,
1131 target_height: 0.0,
1132 max_distance: None,
1133 cell_size: 1.0,
1134 algorithm: ViewshedAlgorithm::R1LineOfSight,
1135 curvature_correction: None,
1136 };
1137 let result = compute_viewshed_advanced(&dem, &config);
1138 assert!(result.is_err());
1139 }
1140
1141 #[test]
1144 fn test_viewshed_elevation_angle() {
1145 let dem = create_flat_dem(10);
1146 let config = ViewshedConfig {
1147 observer_x: 5,
1148 observer_y: 5,
1149 observer_height: 10.0,
1150 target_height: 0.0,
1151 max_distance: None,
1152 cell_size: 1.0,
1153 algorithm: ViewshedAlgorithm::R1LineOfSight,
1154 curvature_correction: None,
1155 };
1156 let result = compute_viewshed_advanced(&dem, &config).expect("result");
1157 assert!(result.elevation_angle.is_some());
1158
1159 let elev_angle = result.elevation_angle.expect("elev_angle");
1160 let angle = elev_angle.get_pixel(5, 8).expect("angle");
1161 assert!(
1164 angle < 0.0,
1165 "Elevation angle should be negative (looking down), got {angle}"
1166 );
1167 }
1168}