diff --git a/lib/microwaveprop/beacons/beacon.ex b/lib/microwaveprop/beacons/beacon.ex index 6b4b0f96..de6db988 100644 --- a/lib/microwaveprop/beacons/beacon.ex +++ b/lib/microwaveprop/beacons/beacon.ex @@ -2,7 +2,7 @@ defmodule Microwaveprop.Beacons.Beacon do @moduledoc """ A beacon transmitter. Frequency in MHz, coordinates as lat/lon, grid as Maidenhead (auto-computed from lat/lon when not supplied), - power in milliwatts, height above ground in meters. + power in milliwatts, height above ground in feet. """ use Ecto.Schema @@ -20,26 +20,27 @@ defmodule Microwaveprop.Beacons.Beacon do field :lat, :float field :lon, :float field :power_mw, :float - field :height_m, :float + field :height_ft, :float field :user_id, :binary_id timestamps(type: :utc_datetime) end - @required_fields [:frequency_mhz, :callsign, :lat, :lon, :power_mw, :height_m] + @required_fields [:frequency_mhz, :callsign, :lat, :lon, :power_mw, :height_ft] def changeset(beacon, attrs) do beacon - |> cast(attrs, [:frequency_mhz, :callsign, :grid, :lat, :lon, :power_mw, :height_m]) + |> cast(attrs, [:frequency_mhz, :callsign, :grid, :lat, :lon, :power_mw, :height_ft]) |> update_change(:callsign, fn cs -> cs && String.upcase(String.trim(cs)) end) + |> maybe_fill_latlon() + |> maybe_fill_grid() |> validate_required(@required_fields) |> validate_number(:frequency_mhz, greater_than: 0) |> validate_number(:power_mw, greater_than_or_equal_to: 0) - |> validate_number(:height_m, greater_than_or_equal_to: 0) + |> validate_number(:height_ft, greater_than_or_equal_to: 0) |> validate_number(:lat, greater_than_or_equal_to: -90, less_than_or_equal_to: 90) |> validate_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180) |> validate_length(:callsign, min: 3, max: 10) - |> maybe_fill_grid() |> validate_grid_format() end @@ -56,6 +57,27 @@ defmodule Microwaveprop.Beacons.Beacon do end end + # If lat/lon are blank but grid is a valid Maidenhead, derive them. + defp maybe_fill_latlon(changeset) do + grid = get_field(changeset, :grid) + lat = get_field(changeset, :lat) + lon = get_field(changeset, :lon) + + if is_binary(grid) and grid != "" and (lat in [nil, ""] or lon in [nil, ""]) do + case Maidenhead.to_latlon(grid) do + {:ok, {new_lat, new_lon}} -> + changeset + |> put_change(:lat, new_lat) + |> put_change(:lon, new_lon) + + :error -> + changeset + end + else + changeset + end + end + defp validate_grid_format(changeset) do case get_field(changeset, :grid) do nil -> diff --git a/lib/microwaveprop_web/live/beacon_live/form.ex b/lib/microwaveprop_web/live/beacon_live/form.ex index ebe63a67..46d72c65 100644 --- a/lib/microwaveprop_web/live/beacon_live/form.ex +++ b/lib/microwaveprop_web/live/beacon_live/form.ex @@ -11,7 +11,10 @@ defmodule MicrowavepropWeb.BeaconLive.Form do <.header> {@page_title} - <:subtitle>Leave the grid blank to auto-fill it from lat/lon. + <:subtitle> + Enter either a grid or lat/lon — whichever you leave blank is filled in + from the other. + <.form for={@form} id="beacon-form" phx-change="validate" phx-submit="save"> @@ -23,9 +26,9 @@ defmodule MicrowavepropWeb.BeaconLive.Form do required /> <.input field={@form[:callsign]} type="text" label="Call" required /> - <.input field={@form[:grid]} type="text" label="Grid (auto if blank)" /> - <.input field={@form[:lat]} type="number" label="Latitude" step="any" required /> - <.input field={@form[:lon]} type="number" label="Longitude" step="any" required /> + <.input field={@form[:grid]} type="text" label="Grid" /> + <.input field={@form[:lat]} type="number" label="Latitude" step="any" /> + <.input field={@form[:lon]} type="number" label="Longitude" step="any" /> <.input field={@form[:power_mw]} type="number" @@ -34,9 +37,9 @@ defmodule MicrowavepropWeb.BeaconLive.Form do required /> <.input - field={@form[:height_m]} + field={@form[:height_ft]} type="number" - label="Height above ground (m)" + label="Height above ground (ft)" step="any" required /> diff --git a/lib/microwaveprop_web/live/beacon_live/index.ex b/lib/microwaveprop_web/live/beacon_live/index.ex index 34060ca0..3470ffe2 100644 --- a/lib/microwaveprop_web/live/beacon_live/index.ex +++ b/lib/microwaveprop_web/live/beacon_live/index.ex @@ -29,7 +29,7 @@ defmodule MicrowavepropWeb.BeaconLive.Index do <:col :let={{_id, beacon}} label="Lat">{beacon.lat} <:col :let={{_id, beacon}} label="Lon">{beacon.lon} <:col :let={{_id, beacon}} label="Power (mW)">{beacon.power_mw} - <:col :let={{_id, beacon}} label="Height AGL (m)">{beacon.height_m} + <:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft} <:action :let={{_id, beacon}}>
<.link navigate={~p"/beacons/#{beacon}"}>Show diff --git a/lib/microwaveprop_web/live/beacon_live/show.ex b/lib/microwaveprop_web/live/beacon_live/show.ex index 48c6d8de..98ebe16e 100644 --- a/lib/microwaveprop_web/live/beacon_live/show.ex +++ b/lib/microwaveprop_web/live/beacon_live/show.ex @@ -33,7 +33,7 @@ defmodule MicrowavepropWeb.BeaconLive.Show do <:item title="Latitude">{@beacon.lat} <:item title="Longitude">{@beacon.lon} <:item title="Power (mW)">{@beacon.power_mw} - <:item title="Height above ground (m)">{@beacon.height_m} + <:item title="Height above ground (ft)">{@beacon.height_ft} """ diff --git a/priv/repo/migrations/20260408174719_rename_beacon_height_to_ft.exs b/priv/repo/migrations/20260408174719_rename_beacon_height_to_ft.exs new file mode 100644 index 00000000..e651a054 --- /dev/null +++ b/priv/repo/migrations/20260408174719_rename_beacon_height_to_ft.exs @@ -0,0 +1,16 @@ +defmodule Microwaveprop.Repo.Migrations.RenameBeaconHeightToFt do + use Ecto.Migration + + # 1 meter = 3.28083989501 feet + @m_to_ft 3.28083989501 + + def up do + rename table(:beacons), :height_m, to: :height_ft + execute "UPDATE beacons SET height_ft = height_ft * #{@m_to_ft}" + end + + def down do + execute "UPDATE beacons SET height_ft = height_ft / #{@m_to_ft}" + rename table(:beacons), :height_ft, to: :height_m + end +end diff --git a/test/microwaveprop/beacons_test.exs b/test/microwaveprop/beacons_test.exs index e548e0d8..0b56976a 100644 --- a/test/microwaveprop/beacons_test.exs +++ b/test/microwaveprop/beacons_test.exs @@ -14,7 +14,7 @@ defmodule Microwaveprop.BeaconsTest do lat: nil, lon: nil, power_mw: nil, - height_m: nil + height_ft: nil } describe "list_beacons/0" do @@ -46,7 +46,7 @@ defmodule Microwaveprop.BeaconsTest do assert beacon.lat == attrs.lat assert beacon.lon == attrs.lon assert beacon.power_mw == attrs.power_mw - assert beacon.height_m == attrs.height_m + assert beacon.height_ft == attrs.height_ft assert beacon.user_id == user.id end @@ -57,6 +57,19 @@ defmodule Microwaveprop.BeaconsTest do assert String.starts_with?(beacon.grid, "EM12") end + test "derives lat/lon from grid when lat/lon are omitted" do + user = user_fixture() + + attrs = + valid_beacon_attrs(grid: "EM12kp") + |> Map.drop([:lat, :lon]) + + assert {:ok, beacon} = Beacons.create_beacon(user, attrs) + assert beacon.grid == "EM12kp" + assert_in_delta beacon.lat, 32.6458, 0.01 + assert_in_delta beacon.lon, -97.125, 0.01 + end + test "keeps an explicitly provided valid grid" do user = user_fixture() {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(grid: "EM13")) diff --git a/test/support/fixtures/beacons_fixtures.ex b/test/support/fixtures/beacons_fixtures.ex index a1606815..2a2c56cd 100644 --- a/test/support/fixtures/beacons_fixtures.ex +++ b/test/support/fixtures/beacons_fixtures.ex @@ -12,7 +12,7 @@ defmodule Microwaveprop.BeaconsFixtures do lat: 32.897, lon: -97.038, power_mw: 10_000.0, - height_m: 30.0 + height_ft: 100.0 }) end