defmodule MicrowavepropWeb.BeaconLive.Show do @moduledoc false use MicrowavepropWeb, :live_view alias Microwaveprop.Beacons alias Microwaveprop.Beacons.Beacon alias Microwaveprop.Beacons.RangeEstimate alias Microwaveprop.Radio.BandResolver alias Microwaveprop.Radio.Maidenhead @impl true def render(assigns) do ~H""" <.header> {@beacon.callsign} <:subtitle> {Beacon.format_freq(@beacon.frequency_mhz)} MHz · {@beacon.grid} Pending approval <:actions> <.button :if={admin?(@current_scope) and not @beacon.approved} variant="primary" phx-click="approve" > <.icon name="hero-check" /> Approve <.button navigate={~p"/beacons"}> <.icon name="hero-arrow-left" /> <.button navigate={path_calc_link(@beacon)}> <.icon name="hero-map" /> Plot path to beacon <.button :if={admin?(@current_scope)} variant="primary" navigate={~p"/beacons/#{@beacon}/edit?return_to=show"} > <.icon name="hero-pencil-square" /> Edit
@beacon.id} phx-hook="BeaconMap" phx-update="ignore" class="h-96 w-full rounded-lg border border-base-300 mb-2" data-lat={@beacon.lat} data-lon={@beacon.lon} data-label={"#{@beacon.callsign} · #{Beacon.format_freq(@beacon.frequency_mhz)} MHz"} data-on-the-air={to_string(@beacon.on_the_air)} data-grid-step={@estimate.grid_step} data-cells={Jason.encode!(@estimate.cells)} data-show-coverage={to_string(@show_coverage)} >
Band {@estimate.band_label} · EIRP {@estimate.eirp_dbm} dBm · Atm loss {@estimate.atm_per_km} dB/km Beacon grid score {@estimate.center_score} <%= if @estimate.valid_time do %> at {Calendar.strftime(@estimate.valid_time, "%Y-%m-%d %H:%M UTC")} <% else %> (no HRRR data — defaulting cells to 50) <% end %> · {length(@estimate.cells)} cells rendered
<%= for tier <- @estimate.tiers do %>
{tier.label} (≥ {tier.min_dbm} dBm)
<% end %>
<.stat_field label="Frequency"> {Beacon.format_freq(@beacon.frequency_mhz)} MHz <.stat_field label="Callsign">{@beacon.callsign} <.stat_field label="Keying">{Beacon.keying_label(@beacon.keying)} <.stat_field label="On the air"> {if @beacon.on_the_air, do: "Yes", else: "No"} <.stat_field label="Grid">{@beacon.grid} <.stat_field label="Latitude">{format_coord(@beacon.lat)} <.stat_field label="Longitude">{format_coord(@beacon.lon)} <.stat_field label="EIRP">{Beacon.format_mw(@beacon.power_mw)} mW <.stat_field label="Height AGL">{@beacon.height_ft} ft <.stat_field label="Bearing">{bearing_label(@beacon.bearing)} <.stat_field :if={@beacon.beamwidth_deg} label="Beamwidth"> {Beacon.format_mw(@beacon.beamwidth_deg)}°
Notes
{@beacon.notes}
Submitted by {@beacon.user.callsign}
""" end @impl true def mount(%{"id" => id}, _session, socket) do if connected?(socket), do: Beacons.subscribe_beacons() beacon = Beacons.get_beacon!(id) {:ok, socket |> assign(:page_title, "Beacon") |> assign(:beacon, beacon) |> assign(:show_coverage, false) |> assign(:estimate, RangeEstimate.estimate(beacon))} end @impl true def handle_event("toggle_coverage", _params, socket) do show = not socket.assigns.show_coverage {:noreply, socket |> assign(:show_coverage, show) |> push_event("toggle_coverage", %{show: show})} end def handle_event("approve", _params, socket) do if admin?(socket.assigns.current_scope) do {:ok, approved} = Beacons.approve_beacon(socket.assigns.beacon) {:noreply, socket |> assign(:beacon, approved) |> put_flash(:info, "Beacon approved.")} else {:noreply, put_flash(socket, :error, "Admins only.")} end end @impl true def handle_info({:updated, %Beacon{id: id} = beacon}, %{assigns: %{beacon: %{id: id}}} = socket) do {:noreply, socket |> assign(:beacon, beacon) |> assign(:estimate, RangeEstimate.estimate(beacon))} end def handle_info({:deleted, %Beacon{id: id}}, %{assigns: %{beacon: %{id: id}}} = socket) do {:noreply, socket |> put_flash(:error, "This beacon was deleted.") |> push_navigate(to: ~p"/beacons")} end def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do {:noreply, socket} end defp admin?(%{user: %{is_admin: true}}), do: true defp admin?(_), do: false defp bearing_label(nil), do: "Omni" defp bearing_label("omni"), do: "Omni" defp bearing_label(value), do: "#{value}°" defp format_coord(value) when is_float(value), do: :erlang.float_to_binary(value, decimals: 6) defp format_coord(value), do: to_string(value) defp path_calc_link(%Beacon{} = beacon) do base = %{ "destination" => precise_grid(beacon), "dst_height_ft" => to_string(beacon.height_ft) } params = maybe_put(base, "band", BandResolver.resolve_as_string(beacon.frequency_mhz)) "/path?" <> URI.encode_query(params) end defp precise_grid(%Beacon{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do Maidenhead.from_latlon(lat, lon, 8) end defp precise_grid(%Beacon{grid: grid}), do: grid defp maybe_put(map, _key, nil), do: map defp maybe_put(map, key, value), do: Map.put(map, key, value) attr :label, :string, required: true slot :inner_block, required: true defp stat_field(assigns) do ~H"""
{@label}
{render_slot(@inner_block)}
""" end end