Replace generic range circles with actual LOS coverage polygon computed from SRTM elevation data. Casts 180 rays (every 2 degrees) from the clicked point, runs Fresnel/diffraction analysis on each, and renders the reachable area as a Leaflet polygon. - Viewshed module with haversine forward, terrain sweep, async compute - Antenna height control (default 8 ft) in map panel - LiveView start_async/handle_async for non-blocking computation - Remove signal icon from band selector
110 lines
3.5 KiB
Elixir
110 lines
3.5 KiB
Elixir
defmodule Microwaveprop.Terrain.ViewshedTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Terrain.Viewshed
|
|
|
|
describe "destination_point/4" do
|
|
test "north bearing increases latitude, holds longitude" do
|
|
{lat, lon} = Viewshed.destination_point(32.0, -97.0, 0, 100.0)
|
|
assert_in_delta lat, 32.899, 0.01
|
|
assert_in_delta lon, -97.0, 0.01
|
|
end
|
|
|
|
test "east bearing increases longitude, holds latitude" do
|
|
{lat, lon} = Viewshed.destination_point(32.0, -97.0, 90, 100.0)
|
|
assert_in_delta lat, 32.0, 0.01
|
|
assert lon > -97.0
|
|
end
|
|
|
|
test "south bearing decreases latitude" do
|
|
{lat, lon} = Viewshed.destination_point(32.0, -97.0, 180, 50.0)
|
|
assert lat < 32.0
|
|
assert_in_delta lon, -97.0, 0.01
|
|
end
|
|
|
|
test "zero distance returns origin" do
|
|
{lat, lon} = Viewshed.destination_point(32.0, -97.0, 45, 0.0)
|
|
assert_in_delta lat, 32.0, 0.001
|
|
assert_in_delta lon, -97.0, 0.001
|
|
end
|
|
end
|
|
|
|
describe "find_reach_km/2" do
|
|
test "returns max range when no points are obstructed" do
|
|
points = [
|
|
%{obstructed: false, dist_km: 0.0},
|
|
%{obstructed: false, dist_km: 10.0},
|
|
%{obstructed: false, dist_km: 20.0},
|
|
%{obstructed: false, dist_km: 30.0},
|
|
%{obstructed: false, dist_km: 50.0}
|
|
]
|
|
|
|
assert Viewshed.find_reach_km(points, 50.0) == 50.0
|
|
end
|
|
|
|
test "returns distance of point before first obstruction" do
|
|
points = [
|
|
%{obstructed: false, dist_km: 0.0},
|
|
%{obstructed: false, dist_km: 10.0},
|
|
%{obstructed: false, dist_km: 20.0},
|
|
%{obstructed: true, dist_km: 30.0},
|
|
%{obstructed: false, dist_km: 40.0},
|
|
%{obstructed: false, dist_km: 50.0}
|
|
]
|
|
|
|
assert Viewshed.find_reach_km(points, 50.0) == 20.0
|
|
end
|
|
|
|
test "returns first interior point distance when obstruction is at second point" do
|
|
points = [
|
|
%{obstructed: false, dist_km: 0.0},
|
|
%{obstructed: true, dist_km: 5.0},
|
|
%{obstructed: false, dist_km: 10.0}
|
|
]
|
|
|
|
# First point is endpoint (excluded), second is first interior and is obstructed
|
|
# No clear interior point before it, return minimum
|
|
assert Viewshed.find_reach_km(points, 10.0) == 0.0
|
|
end
|
|
|
|
test "ignores endpoint obstruction flags" do
|
|
# Endpoints (first/last) are never counted as obstructed by TerrainAnalysis
|
|
# but just to be safe, find_reach_km skips them
|
|
points = [
|
|
%{obstructed: true, dist_km: 0.0},
|
|
%{obstructed: false, dist_km: 25.0},
|
|
%{obstructed: true, dist_km: 50.0}
|
|
]
|
|
|
|
assert Viewshed.find_reach_km(points, 50.0) == 50.0
|
|
end
|
|
end
|
|
|
|
describe "analyse_ray/5" do
|
|
test "returns full range for flat terrain with antenna heights" do
|
|
profile =
|
|
for i <- 0..10 do
|
|
f = i / 10
|
|
%{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: 200.0, dist_km: f * 10.0}
|
|
end
|
|
|
|
result = Viewshed.analyse_ray(profile, 10.0, 10.0, 2.4, 2.4)
|
|
assert result.reach_km == 10.0
|
|
end
|
|
|
|
test "detects obstruction and returns reduced reach" do
|
|
# Use 10km total so earth bulge is negligible (~0.7m) and
|
|
# the 500m peak at index 5 is the only obstruction.
|
|
profile =
|
|
for i <- 0..10 do
|
|
f = i / 10
|
|
elev = if i == 5, do: 500.0, else: 200.0
|
|
%{lat: 32.9 + f * 0.009, lon: -97.0, d: f, elev: elev, dist_km: f * 10.0}
|
|
end
|
|
|
|
result = Viewshed.analyse_ray(profile, 10.0, 10.0, 2.4, 2.4)
|
|
assert result.reach_km < 10.0
|
|
assert result.reach_km > 0.0
|
|
end
|
|
end
|
|
end
|