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