feat(map): scale propagation ranges with antenna height
Antenna height now drives typical/extended/exceptional range estimates and viewshed max range via a sqrt(h_m/10) factor clamped to [0.6, 2.0]. Baseline 33 ft matches the existing defaults. Changing the height input now pushes update_band_info and refreshes the clicked point's detail panel plus viewshed so the range-estimate line and coverage shape track the user's actual station height.
This commit is contained in:
parent
8bded0c6e8
commit
e827cacc48
3 changed files with 67 additions and 19 deletions
|
|
@ -739,14 +739,21 @@ export const PropagationMap: Record<string, unknown> & {
|
|||
this.bandInfo = band_info
|
||||
|
||||
// Viewshed depends on freq + score-derived range, so recompute it when
|
||||
// the band changes if a point is still selected. update_scores already
|
||||
// handles the heatmap, reach polygon, and detail panel refresh.
|
||||
// the band changes or antenna height moves if a point is still selected.
|
||||
// Also re-request point_detail so the popup's range-estimate line picks
|
||||
// up the updated typical/extended/exceptional ranges. update_scores
|
||||
// (on band change) already refreshes the polygon/heatmap; for
|
||||
// antenna-height changes the polygon is height-independent today.
|
||||
if (this.clickedLatLng && this.detailPanel && this.detailPanel.style.display !== "none") {
|
||||
this.viewshedLoading = true
|
||||
this.pushEvent("compute_viewshed", {
|
||||
lat: this.clickedLatLng[0],
|
||||
lon: this.clickedLatLng[1]
|
||||
})
|
||||
this.pushEvent("point_detail", {
|
||||
lat: this.clickedLatLng[0],
|
||||
lon: this.clickedLatLng[1]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,9 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
|> assign(selected_band: band, valid_times: valid_times, selected_time: selected_time)
|
||||
|> LiveStash.stash_assigns([:selected_band, :selected_time])
|
||||
|> push_event("update_scores", %{scores: scores})
|
||||
|> push_event("update_band_info", %{band_info: band_info(band)})
|
||||
|> push_event("update_band_info", %{
|
||||
band_info: band_info(band, socket.assigns.antenna_height_ft)
|
||||
})
|
||||
|> push_timeline()
|
||||
|
||||
{:noreply, socket}
|
||||
|
|
@ -199,7 +201,14 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
_ -> 8
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :antenna_height_ft, height)}
|
||||
socket =
|
||||
socket
|
||||
|> assign(:antenna_height_ft, height)
|
||||
|> push_event("update_band_info", %{
|
||||
band_info: band_info(socket.assigns.selected_band, height)
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("toggle_grid", _params, socket) do
|
||||
|
|
@ -274,7 +283,7 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
_ -> 50
|
||||
end
|
||||
|
||||
max_range_km = score_range_km(score, band_config)
|
||||
max_range_km = score_range_km(score, band_config, socket.assigns.antenna_height_ft)
|
||||
|
||||
socket =
|
||||
start_async(socket, :viewshed, fn ->
|
||||
|
|
@ -433,29 +442,47 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
Enum.min_by(times, fn t -> abs(DateTime.diff(t, now)) end)
|
||||
end
|
||||
|
||||
defp score_range_km(score, config) do
|
||||
cond do
|
||||
score >= 80 -> config.exceptional_range_km
|
||||
score >= 65 -> config.extended_range_km
|
||||
score >= 50 -> config.typical_range_km
|
||||
score >= 33 -> round(config.typical_range_km * 0.6)
|
||||
true -> round(config.typical_range_km * 0.3)
|
||||
end
|
||||
defp score_range_km(score, config, ant_height_ft) do
|
||||
factor = height_factor(ant_height_ft)
|
||||
|
||||
base =
|
||||
cond do
|
||||
score >= 80 -> config.exceptional_range_km
|
||||
score >= 65 -> config.extended_range_km
|
||||
score >= 50 -> config.typical_range_km
|
||||
score >= 33 -> round(config.typical_range_km * 0.6)
|
||||
true -> round(config.typical_range_km * 0.3)
|
||||
end
|
||||
|
||||
round(base * factor)
|
||||
end
|
||||
|
||||
defp band_info(band_mhz) do
|
||||
defp band_info(band_mhz, ant_height_ft) do
|
||||
config = BandConfig.get(band_mhz)
|
||||
factor = height_factor(ant_height_ft)
|
||||
|
||||
%{
|
||||
band_label: config.label,
|
||||
typical_range_km: config.typical_range_km,
|
||||
extended_range_km: config.extended_range_km,
|
||||
exceptional_range_km: config.exceptional_range_km,
|
||||
typical_range_km: round(config.typical_range_km * factor),
|
||||
extended_range_km: round(config.extended_range_km * factor),
|
||||
exceptional_range_km: round(config.exceptional_range_km * factor),
|
||||
humidity_effect: to_string(config.humidity_effect),
|
||||
freq_mhz: config.freq_mhz
|
||||
freq_mhz: config.freq_mhz,
|
||||
antenna_height_ft: ant_height_ft
|
||||
}
|
||||
end
|
||||
|
||||
# Scales effective range with antenna height. Baseline 33 ft (~10 m) = 1.0;
|
||||
# scales as sqrt(h_m / 10) because radio horizon scales as sqrt(height).
|
||||
# Clamped to [0.6, 2.0] to avoid silly extremes at 0 ft and very tall towers.
|
||||
defp height_factor(ant_height_ft) when is_number(ant_height_ft) do
|
||||
h_m = max(ant_height_ft, 3) * 0.3048
|
||||
factor = :math.sqrt(h_m / 10.0)
|
||||
factor |> max(0.6) |> min(2.0)
|
||||
end
|
||||
|
||||
defp height_factor(_), do: 1.0
|
||||
|
||||
defp selected_label(bands, selected_band) do
|
||||
case Enum.find(bands, &(&1.freq_mhz == selected_band)) do
|
||||
nil -> "Select Band"
|
||||
|
|
@ -555,7 +582,7 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
phx-hook="PropagationMap"
|
||||
phx-update="ignore"
|
||||
data-scores={@initial_scores_json}
|
||||
data-band-info={Jason.encode!(band_info(@selected_band))}
|
||||
data-band-info={Jason.encode!(band_info(@selected_band, @antenna_height_ft))}
|
||||
data-valid-times={
|
||||
Jason.encode!(
|
||||
Enum.map(@valid_times, fn t ->
|
||||
|
|
|
|||
|
|
@ -99,6 +99,20 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
|||
html = render_change(lv, "set_antenna_height", %{"height_ft" => "20"})
|
||||
assert html =~ ~s(value="20")
|
||||
end
|
||||
|
||||
test "pushes update_band_info with height-scaled ranges", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||
|
||||
baseline = render_change(lv, "set_antenna_height", %{"height_ft" => "33"})
|
||||
assert_push_event(lv, "update_band_info", %{band_info: %{typical_range_km: baseline_range}})
|
||||
assert baseline =~ ~s(value="33")
|
||||
|
||||
tall = render_change(lv, "set_antenna_height", %{"height_ft" => "100"})
|
||||
assert_push_event(lv, "update_band_info", %{band_info: %{typical_range_km: tall_range}})
|
||||
assert tall =~ ~s(value="100")
|
||||
|
||||
assert tall_range > baseline_range
|
||||
end
|
||||
end
|
||||
|
||||
describe "compute_viewshed" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue