From a774e9835056c5ed5e42887e07d475426c480bf6 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 7 Apr 2026 07:42:23 -0500 Subject: [PATCH] Smooth viewshed boundary to remove SRTM elevation spikes Apply 5-point circular moving average to reach_km values before rendering the boundary polygon. Eliminates sharp spikes caused by isolated terrain anomalies in SRTM data. --- lib/microwaveprop/terrain/viewshed.ex | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/microwaveprop/terrain/viewshed.ex b/lib/microwaveprop/terrain/viewshed.ex index 08362868..e1a8dfd2 100644 --- a/lib/microwaveprop/terrain/viewshed.ex +++ b/lib/microwaveprop/terrain/viewshed.ex @@ -129,6 +129,7 @@ defmodule Microwaveprop.Terrain.Viewshed do {:exit, _} -> nil end) |> Enum.reject(&is_nil/1) + |> smooth_boundary(lat, lon) %{origin: %{lat: lat, lon: lon}, boundary: boundary} end @@ -180,6 +181,34 @@ defmodule Microwaveprop.Terrain.Viewshed do Application.get_env(:microwaveprop, :srtm_tiles_dir, Path.expand("~/srtm/tiles")) end + # Smooth reach_km values with a circular moving average to remove spikes + # from SRTM elevation artifacts, then recompute boundary lat/lon. + defp smooth_boundary(points, _origin_lat, _origin_lon) when length(points) < 5, do: points + + defp smooth_boundary(points, origin_lat, origin_lon) do + reaches = Enum.map(points, & &1.reach_km) + n = length(reaches) + arr = :array.from_list(reaches) + half = 2 + + smoothed_reaches = + Enum.map(0..(n - 1), fn i -> + window = + for offset <- -half..half do + :array.get(rem(i + offset + n, n), arr) + end + + Enum.sum(window) / length(window) + end) + + points + |> Enum.zip(smoothed_reaches) + |> Enum.map(fn {pt, reach} -> + {lat, lon} = destination_point(origin_lat, origin_lon, pt.bearing, reach) + %{pt | reach_km: reach, lat: lat, lon: lon} + end) + end + defp find_first_obstructed_index(interior) do Enum.find_index(interior, & &1.obstructed) end