Click anywhere on the propagation map to see a detailed popup with: - Overall score and tier label with color - Estimated range for the selected band (CW mode) - All 9 scoring factors with visual bar charts, individual scores, and weight percentages - Grid point coordinates and data timestamp Factors are displayed in weight order so users can immediately see which atmospheric conditions are driving the prediction.
167 lines
4.8 KiB
Elixir
167 lines
4.8 KiB
Elixir
defmodule MicrowavepropWeb.MapLive do
|
|
@moduledoc false
|
|
use MicrowavepropWeb, :live_view
|
|
|
|
alias Microwaveprop.Propagation
|
|
alias Microwaveprop.Propagation.BandConfig
|
|
|
|
@default_band 10_000
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
bands = BandConfig.all_bands()
|
|
valid_time = Propagation.latest_valid_time()
|
|
|
|
if connected?(socket) do
|
|
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")
|
|
end
|
|
|
|
{:ok,
|
|
assign(socket,
|
|
page_title: "Propagation Map",
|
|
bands: bands,
|
|
selected_band: @default_band,
|
|
scores: [],
|
|
valid_time: valid_time,
|
|
bounds: nil
|
|
)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("select_band", %{"value" => band}, socket) do
|
|
band = if is_binary(band), do: String.to_integer(band), else: band
|
|
scores = Propagation.latest_scores(band, socket.assigns.bounds)
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:selected_band, band)
|
|
|> assign(:scores, scores)
|
|
|> push_event("update_scores", %{scores: scores})
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("point_detail", %{"lat" => lat, "lon" => lon}, socket) do
|
|
band = socket.assigns.selected_band
|
|
|
|
case Propagation.point_detail(band, lat, lon) do
|
|
nil ->
|
|
{:noreply, push_event(socket, "point_detail", %{detail: nil})}
|
|
|
|
detail ->
|
|
band_config = BandConfig.get(band)
|
|
|
|
payload = %{
|
|
lat: detail.lat,
|
|
lon: detail.lon,
|
|
score: detail.score,
|
|
factors: detail.factors,
|
|
valid_time: DateTime.to_iso8601(detail.valid_time),
|
|
band_label: band_config.label,
|
|
typical_range_km: band_config.typical_range_km,
|
|
extended_range_km: band_config.extended_range_km,
|
|
exceptional_range_km: band_config.exceptional_range_km
|
|
}
|
|
|
|
{:noreply, push_event(socket, "point_detail", %{detail: payload})}
|
|
end
|
|
end
|
|
|
|
def handle_event("map_bounds", bounds, socket) do
|
|
scores = Propagation.latest_scores(socket.assigns.selected_band, bounds)
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:bounds, bounds)
|
|
|> assign(:scores, scores)
|
|
|> push_event("update_scores", %{scores: scores})
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:propagation_updated, valid_time}, socket) do
|
|
scores = Propagation.latest_scores(socket.assigns.selected_band, socket.assigns.bounds)
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:scores, scores)
|
|
|> assign(:valid_time, valid_time)
|
|
|> push_event("update_scores", %{scores: scores})
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
defp time_ago(dt) do
|
|
seconds = DateTime.diff(DateTime.utc_now(), dt)
|
|
|
|
cond do
|
|
seconds < 60 -> "just now"
|
|
seconds < 3600 -> "#{div(seconds, 60)}m ago"
|
|
seconds < 86_400 -> "#{div(seconds, 3600)}h ago"
|
|
true -> "#{div(seconds, 86_400)}d ago"
|
|
end
|
|
end
|
|
|
|
defp selected_label(bands, selected_band) do
|
|
case Enum.find(bands, &(&1.freq_mhz == selected_band)) do
|
|
nil -> "Select Band"
|
|
band -> band.label
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<div class="relative w-screen h-screen">
|
|
<%!-- Full-page map --%>
|
|
<div
|
|
id="propagation-map"
|
|
phx-hook="PropagationMap"
|
|
phx-update="ignore"
|
|
data-scores="[]"
|
|
class="absolute inset-0 z-0"
|
|
>
|
|
</div>
|
|
|
|
<%!-- Top-left controls overlay --%>
|
|
<div class="absolute top-3 left-14 z-[1000] flex items-center gap-2">
|
|
<.link navigate="/" class="btn btn-sm bg-base-100/90 shadow border-base-300">
|
|
<.icon name="hero-home" class="size-4" />
|
|
</.link>
|
|
|
|
<div class="dropdown">
|
|
<div tabindex="0" role="button" class="btn btn-sm bg-base-100/90 shadow border-base-300">
|
|
<.icon name="hero-signal" class="size-4" />
|
|
{selected_label(@bands, @selected_band)}
|
|
<.icon name="hero-chevron-down" class="size-3" />
|
|
</div>
|
|
<ul
|
|
tabindex="0"
|
|
class="dropdown-content menu bg-base-100 rounded-box shadow-lg w-44 mt-1 p-1"
|
|
>
|
|
<li :for={band <- @bands}>
|
|
<button
|
|
phx-click={
|
|
JS.dispatch("show-loading", to: "#propagation-map")
|
|
|> JS.push("select_band", value: %{value: band.freq_mhz})
|
|
|> JS.dispatch("click", to: "#propagation-map")
|
|
}
|
|
class={[if(@selected_band == band.freq_mhz, do: "active")]}
|
|
>
|
|
{band.label}
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div :if={@valid_time} class="badge badge-ghost bg-base-100/90 shadow text-xs">
|
|
Latest data: {Calendar.strftime(@valid_time, "%b %d, %H:%M UTC")} ({time_ago(@valid_time)})
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Layouts.flash_group flash={@flash} />
|
|
"""
|
|
end
|
|
end
|