352 lines
9.3 KiB
Elixir
352 lines
9.3 KiB
Elixir
defmodule ToweropsWeb.CoverageLive.Form do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Phoenix.HTML.Form, as: HTMLForm
|
|
alias Towerops.Coverages
|
|
alias Towerops.Coverages.Antenna
|
|
alias Towerops.Coverages.Coverage
|
|
alias Towerops.Devices
|
|
alias Towerops.Sites
|
|
|
|
# Defaults expressed in the user's preferred units (imperial / GHz).
|
|
@default_tx_power_dbm 18.0
|
|
@default_height_ft 100.0
|
|
@default_radius_mi 4.0
|
|
@default_frequency_ghz 5.8
|
|
@default_azimuth_deg 0.0
|
|
@default_downtilt_deg 0.0
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
sites = Sites.list_organization_sites(organization.id)
|
|
antennas = Antenna.list()
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:organization, organization)
|
|
|> assign(:sites, sites)
|
|
|> assign(:antenna_options, antenna_options(antennas))
|
|
|> assign(:devices, [])}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
|
end
|
|
|
|
defp apply_action(socket, :new, _params) do
|
|
coverage = %Coverage{
|
|
organization_id: socket.assigns.organization.id,
|
|
tx_power_dbm: @default_tx_power_dbm,
|
|
azimuth_deg: @default_azimuth_deg,
|
|
downtilt_deg: @default_downtilt_deg,
|
|
foliage_tuning: 0,
|
|
receiver_height_m: 3.0,
|
|
rx_threshold_dbm: -90.0,
|
|
cable_loss_db: 0.0,
|
|
sm_gain_dbi: 0.0,
|
|
height_above_rooftop_m: 0.0,
|
|
height_agl_ft: @default_height_ft,
|
|
radius_mi: @default_radius_mi,
|
|
frequency_ghz: @default_frequency_ghz
|
|
}
|
|
|
|
socket
|
|
|> assign(:page_title, t("New Coverage"))
|
|
|> assign(:coverage, coverage)
|
|
|> assign_form(Coverages.change_coverage(coverage))
|
|
end
|
|
|
|
defp apply_action(socket, :edit, %{"id" => id}) do
|
|
coverage =
|
|
socket.assigns.organization.id
|
|
|> Coverages.get_coverage!(id)
|
|
|> populate_imperial_virtuals()
|
|
|
|
socket
|
|
|> assign(:page_title, t("Edit Coverage"))
|
|
|> assign(:coverage, coverage)
|
|
|> assign(:devices, devices_for_site(socket.assigns.organization.id, coverage.site_id))
|
|
|> assign_form(Coverages.change_coverage(coverage))
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"coverage" => attrs}, socket) do
|
|
changeset =
|
|
socket.assigns.coverage
|
|
|> Coverages.change_coverage(attrs)
|
|
|> Map.put(:action, :validate)
|
|
|
|
new_site_id = Map.get(attrs, "site_id")
|
|
socket = maybe_refresh_devices(socket, new_site_id)
|
|
|
|
{:noreply, assign_form(socket, changeset)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("save", %{"coverage" => attrs}, socket) do
|
|
save(socket, socket.assigns.live_action, attrs)
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("location_picked", %{"lat" => lat, "lon" => lon}, socket) do
|
|
attrs =
|
|
socket
|
|
|> current_form_attrs()
|
|
|> Map.put("latitude_override", to_string(lat))
|
|
|> Map.put("longitude_override", to_string(lon))
|
|
|
|
changeset =
|
|
socket.assigns.coverage
|
|
|> Coverages.change_coverage(attrs)
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign_form(socket, changeset)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("use_site_location", _params, socket) do
|
|
attrs =
|
|
socket
|
|
|> current_form_attrs()
|
|
|> Map.put("latitude_override", "")
|
|
|> Map.put("longitude_override", "")
|
|
|
|
changeset =
|
|
socket.assigns.coverage
|
|
|> Coverages.change_coverage(attrs)
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign_form(socket, changeset)}
|
|
end
|
|
|
|
defp current_form_attrs(socket) do
|
|
case socket.assigns[:form] do
|
|
nil -> %{}
|
|
form -> form.params || %{}
|
|
end
|
|
end
|
|
|
|
defp save(socket, :new, attrs) do
|
|
with {:ok, coverage} <- Coverages.create_coverage(socket.assigns.organization.id, attrs),
|
|
{:ok, queued} <- Coverages.queue_compute(coverage) do
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t("Coverage queued for compute"))
|
|
|> push_navigate(to: ~p"/coverage/#{queued.id}")}
|
|
else
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
{:noreply, assign_form(socket, changeset)}
|
|
|
|
{:error, _other} ->
|
|
{:noreply, put_flash(socket, :error, t("Failed to queue coverage compute"))}
|
|
end
|
|
end
|
|
|
|
defp save(socket, :edit, attrs) do
|
|
case Coverages.update_coverage(socket.assigns.coverage, attrs) do
|
|
{:ok, coverage} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t("Coverage updated"))
|
|
|> push_navigate(to: ~p"/coverage/#{coverage.id}")}
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
{:noreply, assign_form(socket, changeset)}
|
|
end
|
|
end
|
|
|
|
defp assign_form(socket, changeset) do
|
|
form = to_form(changeset)
|
|
eirp = compute_eirp_from_form(form)
|
|
|
|
socket
|
|
|> assign(:form, form)
|
|
|> assign(:computed_eirp, eirp)
|
|
end
|
|
|
|
# EIRP = TX power + antenna gain. Cable loss is intentionally not part
|
|
# of the user-facing formula (kept zero by default in the schema).
|
|
defp compute_eirp_from_form(form) do
|
|
tx_power = form_number(form, :tx_power_dbm, 0.0)
|
|
|
|
gain =
|
|
case form[:antenna_slug].value do
|
|
slug when is_binary(slug) and slug != "" ->
|
|
case Antenna.get(slug) do
|
|
%Antenna{gain_dbi: g} -> g
|
|
_ -> 0.0
|
|
end
|
|
|
|
_ ->
|
|
0.0
|
|
end
|
|
|
|
tx_power + gain
|
|
end
|
|
|
|
defp form_number(form, field, default) do
|
|
case form[field].value do
|
|
nil ->
|
|
default
|
|
|
|
"" ->
|
|
default
|
|
|
|
n when is_number(n) ->
|
|
n * 1.0
|
|
|
|
s when is_binary(s) ->
|
|
case Float.parse(s) do
|
|
{n, _} -> n
|
|
:error -> default
|
|
end
|
|
end
|
|
end
|
|
|
|
defp antenna_options(antennas) do
|
|
antennas
|
|
|> Enum.group_by(& &1.manufacturer)
|
|
|> Enum.sort_by(fn {mfr, _} -> mfr || "" end)
|
|
|> Enum.map(fn {mfr, ants} ->
|
|
{mfr || t("Other"),
|
|
Enum.map(ants, fn ant ->
|
|
{"#{ant.model} (#{ant.gain_dbi} dBi)", ant.slug}
|
|
end)}
|
|
end)
|
|
end
|
|
|
|
# Reload the devices dropdown when the user picks a different site.
|
|
# Clearing site_id clears the dropdown.
|
|
defp maybe_refresh_devices(socket, site_id) do
|
|
current_site_id =
|
|
case socket.assigns[:form] do
|
|
nil -> nil
|
|
form -> form[:site_id].value
|
|
end
|
|
|
|
if to_string(site_id) == to_string(current_site_id) do
|
|
socket
|
|
else
|
|
assign(socket, :devices, devices_for_site(socket.assigns.organization.id, site_id))
|
|
end
|
|
end
|
|
|
|
defp devices_for_site(_org_id, site_id) when site_id in [nil, ""], do: []
|
|
|
|
defp devices_for_site(org_id, site_id) do
|
|
site_id
|
|
|> Devices.list_site_devices()
|
|
|> Enum.filter(&(&1.organization_id == org_id))
|
|
end
|
|
|
|
@doc """
|
|
Latitude to display on the location-picker marker:
|
|
override (if set) → selected site's lat → nil.
|
|
"""
|
|
def radio_marker_lat(form, sites) do
|
|
case form_field_value(form, :latitude_override) do
|
|
lat when is_number(lat) ->
|
|
lat
|
|
|
|
lat when is_binary(lat) and lat != "" ->
|
|
case Float.parse(lat) do
|
|
{n, _} -> n
|
|
:error -> selected_site_lat(form, sites)
|
|
end
|
|
|
|
_ ->
|
|
selected_site_lat(form, sites)
|
|
end
|
|
end
|
|
|
|
@doc "Longitude to display on the location-picker marker."
|
|
def radio_marker_lon(form, sites) do
|
|
case form_field_value(form, :longitude_override) do
|
|
lon when is_number(lon) ->
|
|
lon
|
|
|
|
lon when is_binary(lon) and lon != "" ->
|
|
case Float.parse(lon) do
|
|
{n, _} -> n
|
|
:error -> selected_site_lon(form, sites)
|
|
end
|
|
|
|
_ ->
|
|
selected_site_lon(form, sites)
|
|
end
|
|
end
|
|
|
|
@doc "True if the form's lat/lon override is set (non-empty)."
|
|
def radio_overridden?(form) do
|
|
has_override?(form_field_value(form, :latitude_override)) and
|
|
has_override?(form_field_value(form, :longitude_override))
|
|
end
|
|
|
|
defp has_override?(nil), do: false
|
|
defp has_override?(""), do: false
|
|
defp has_override?(_), do: true
|
|
|
|
defp selected_site_lat(form, sites) do
|
|
case selected_site(form, sites) do
|
|
%{latitude: lat} when is_number(lat) -> lat
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp selected_site_lon(form, sites) do
|
|
case selected_site(form, sites) do
|
|
%{longitude: lon} when is_number(lon) -> lon
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp selected_site(form, sites) do
|
|
case form_field_value(form, :site_id) do
|
|
id when is_binary(id) and id != "" -> Enum.find(sites, &(&1.id == id))
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp form_field_value(form, field) do
|
|
v = HTMLForm.input_value(form, field)
|
|
v
|
|
end
|
|
|
|
@doc false
|
|
def device_label(device) do
|
|
case device do
|
|
%{name: name, ip_address: ip} when is_binary(name) and name != "" ->
|
|
"#{name} (#{ip})"
|
|
|
|
%{ip_address: ip} ->
|
|
"#{ip}"
|
|
|
|
_ ->
|
|
""
|
|
end
|
|
end
|
|
|
|
defp populate_imperial_virtuals(%Coverage{} = c) do
|
|
%{
|
|
c
|
|
| height_agl_ft: m_to_ft(c.height_agl_m),
|
|
height_above_rooftop_ft: m_to_ft(c.height_above_rooftop_m),
|
|
receiver_height_ft: m_to_ft(c.receiver_height_m),
|
|
tx_clearance_ft: m_to_ft(c.tx_clearance_m),
|
|
radius_mi: m_to_mi(c.radius_m),
|
|
frequency_ghz: mhz_to_ghz(c.frequency_mhz)
|
|
}
|
|
end
|
|
|
|
defp m_to_ft(nil), do: nil
|
|
defp m_to_ft(m) when is_number(m), do: Float.round(m / 0.3048, 1)
|
|
|
|
defp m_to_mi(nil), do: nil
|
|
defp m_to_mi(m) when is_number(m), do: Float.round(m / 1609.344, 2)
|
|
|
|
defp mhz_to_ghz(nil), do: nil
|
|
defp mhz_to_ghz(mhz) when is_number(mhz), do: Float.round(mhz / 1000.0, 3)
|
|
end
|