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
|
this.bandInfo = band_info
|
||||||
|
|
||||||
// Viewshed depends on freq + score-derived range, so recompute it when
|
// Viewshed depends on freq + score-derived range, so recompute it when
|
||||||
// the band changes if a point is still selected. update_scores already
|
// the band changes or antenna height moves if a point is still selected.
|
||||||
// handles the heatmap, reach polygon, and detail panel refresh.
|
// 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") {
|
if (this.clickedLatLng && this.detailPanel && this.detailPanel.style.display !== "none") {
|
||||||
this.viewshedLoading = true
|
this.viewshedLoading = true
|
||||||
this.pushEvent("compute_viewshed", {
|
this.pushEvent("compute_viewshed", {
|
||||||
lat: this.clickedLatLng[0],
|
lat: this.clickedLatLng[0],
|
||||||
lon: this.clickedLatLng[1]
|
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)
|
|> assign(selected_band: band, valid_times: valid_times, selected_time: selected_time)
|
||||||
|> LiveStash.stash_assigns([:selected_band, :selected_time])
|
|> LiveStash.stash_assigns([:selected_band, :selected_time])
|
||||||
|> push_event("update_scores", %{scores: scores})
|
|> 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()
|
|> push_timeline()
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -199,7 +201,14 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
_ -> 8
|
_ -> 8
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
def handle_event("toggle_grid", _params, socket) do
|
def handle_event("toggle_grid", _params, socket) do
|
||||||
|
|
@ -274,7 +283,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
_ -> 50
|
_ -> 50
|
||||||
end
|
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 =
|
socket =
|
||||||
start_async(socket, :viewshed, fn ->
|
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)
|
Enum.min_by(times, fn t -> abs(DateTime.diff(t, now)) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp score_range_km(score, config) do
|
defp score_range_km(score, config, ant_height_ft) do
|
||||||
cond do
|
factor = height_factor(ant_height_ft)
|
||||||
score >= 80 -> config.exceptional_range_km
|
|
||||||
score >= 65 -> config.extended_range_km
|
base =
|
||||||
score >= 50 -> config.typical_range_km
|
cond do
|
||||||
score >= 33 -> round(config.typical_range_km * 0.6)
|
score >= 80 -> config.exceptional_range_km
|
||||||
true -> round(config.typical_range_km * 0.3)
|
score >= 65 -> config.extended_range_km
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
defp band_info(band_mhz) do
|
defp band_info(band_mhz, ant_height_ft) do
|
||||||
config = BandConfig.get(band_mhz)
|
config = BandConfig.get(band_mhz)
|
||||||
|
factor = height_factor(ant_height_ft)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
band_label: config.label,
|
band_label: config.label,
|
||||||
typical_range_km: config.typical_range_km,
|
typical_range_km: round(config.typical_range_km * factor),
|
||||||
extended_range_km: config.extended_range_km,
|
extended_range_km: round(config.extended_range_km * factor),
|
||||||
exceptional_range_km: config.exceptional_range_km,
|
exceptional_range_km: round(config.exceptional_range_km * factor),
|
||||||
humidity_effect: to_string(config.humidity_effect),
|
humidity_effect: to_string(config.humidity_effect),
|
||||||
freq_mhz: config.freq_mhz
|
freq_mhz: config.freq_mhz,
|
||||||
|
antenna_height_ft: ant_height_ft
|
||||||
}
|
}
|
||||||
end
|
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
|
defp selected_label(bands, selected_band) do
|
||||||
case Enum.find(bands, &(&1.freq_mhz == selected_band)) do
|
case Enum.find(bands, &(&1.freq_mhz == selected_band)) do
|
||||||
nil -> "Select Band"
|
nil -> "Select Band"
|
||||||
|
|
@ -555,7 +582,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
phx-hook="PropagationMap"
|
phx-hook="PropagationMap"
|
||||||
phx-update="ignore"
|
phx-update="ignore"
|
||||||
data-scores={@initial_scores_json}
|
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={
|
data-valid-times={
|
||||||
Jason.encode!(
|
Jason.encode!(
|
||||||
Enum.map(@valid_times, fn t ->
|
Enum.map(@valid_times, fn t ->
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,20 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
||||||
html = render_change(lv, "set_antenna_height", %{"height_ft" => "20"})
|
html = render_change(lv, "set_antenna_height", %{"height_ft" => "20"})
|
||||||
assert html =~ ~s(value="20")
|
assert html =~ ~s(value="20")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "compute_viewshed" do
|
describe "compute_viewshed" do
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue