Beacon height in feet, auto-fill lat/lon from grid
- Rename beacons.height_m to height_ft; migration converts existing values (m * 3.28084). Form/list/show labels updated. - Beacon changeset now fills lat/lon from the grid when they're blank (previously only the reverse worked). Lat/lon and grid inputs are no longer marked required in the form — enter one side and the other populates on blur via phx-change.
This commit is contained in:
parent
d94b681d14
commit
374f9b106a
7 changed files with 71 additions and 17 deletions
|
|
@ -2,7 +2,7 @@ defmodule Microwaveprop.Beacons.Beacon do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A beacon transmitter. Frequency in MHz, coordinates as lat/lon,
|
A beacon transmitter. Frequency in MHz, coordinates as lat/lon,
|
||||||
grid as Maidenhead (auto-computed from lat/lon when not supplied),
|
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
|
use Ecto.Schema
|
||||||
|
|
@ -20,26 +20,27 @@ defmodule Microwaveprop.Beacons.Beacon do
|
||||||
field :lat, :float
|
field :lat, :float
|
||||||
field :lon, :float
|
field :lon, :float
|
||||||
field :power_mw, :float
|
field :power_mw, :float
|
||||||
field :height_m, :float
|
field :height_ft, :float
|
||||||
field :user_id, :binary_id
|
field :user_id, :binary_id
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
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
|
def changeset(beacon, attrs) do
|
||||||
beacon
|
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)
|
|> update_change(:callsign, fn cs -> cs && String.upcase(String.trim(cs)) end)
|
||||||
|
|> maybe_fill_latlon()
|
||||||
|
|> maybe_fill_grid()
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|> validate_number(:frequency_mhz, greater_than: 0)
|
|> validate_number(:frequency_mhz, greater_than: 0)
|
||||||
|> validate_number(:power_mw, greater_than_or_equal_to: 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(: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_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180)
|
||||||
|> validate_length(:callsign, min: 3, max: 10)
|
|> validate_length(:callsign, min: 3, max: 10)
|
||||||
|> maybe_fill_grid()
|
|
||||||
|> validate_grid_format()
|
|> validate_grid_format()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -56,6 +57,27 @@ defmodule Microwaveprop.Beacons.Beacon do
|
||||||
end
|
end
|
||||||
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
|
defp validate_grid_format(changeset) do
|
||||||
case get_field(changeset, :grid) do
|
case get_field(changeset, :grid) do
|
||||||
nil ->
|
nil ->
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ defmodule MicrowavepropWeb.BeaconLive.Form do
|
||||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||||
<.header>
|
<.header>
|
||||||
{@page_title}
|
{@page_title}
|
||||||
<:subtitle>Leave the grid blank to auto-fill it from lat/lon.</:subtitle>
|
<:subtitle>
|
||||||
|
Enter either a grid or lat/lon — whichever you leave blank is filled in
|
||||||
|
from the other.
|
||||||
|
</:subtitle>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
||||||
<.form for={@form} id="beacon-form" phx-change="validate" phx-submit="save">
|
<.form for={@form} id="beacon-form" phx-change="validate" phx-submit="save">
|
||||||
|
|
@ -23,9 +26,9 @@ defmodule MicrowavepropWeb.BeaconLive.Form do
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<.input field={@form[:callsign]} type="text" label="Call" required />
|
<.input field={@form[:callsign]} type="text" label="Call" required />
|
||||||
<.input field={@form[:grid]} type="text" label="Grid (auto if blank)" />
|
<.input field={@form[:grid]} type="text" label="Grid" />
|
||||||
<.input field={@form[:lat]} type="number" label="Latitude" step="any" required />
|
<.input field={@form[:lat]} type="number" label="Latitude" step="any" />
|
||||||
<.input field={@form[:lon]} type="number" label="Longitude" step="any" required />
|
<.input field={@form[:lon]} type="number" label="Longitude" step="any" />
|
||||||
<.input
|
<.input
|
||||||
field={@form[:power_mw]}
|
field={@form[:power_mw]}
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -34,9 +37,9 @@ defmodule MicrowavepropWeb.BeaconLive.Form do
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<.input
|
<.input
|
||||||
field={@form[:height_m]}
|
field={@form[:height_ft]}
|
||||||
type="number"
|
type="number"
|
||||||
label="Height above ground (m)"
|
label="Height above ground (ft)"
|
||||||
step="any"
|
step="any"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
<:col :let={{_id, beacon}} label="Lat">{beacon.lat}</:col>
|
<:col :let={{_id, beacon}} label="Lat">{beacon.lat}</:col>
|
||||||
<:col :let={{_id, beacon}} label="Lon">{beacon.lon}</:col>
|
<:col :let={{_id, beacon}} label="Lon">{beacon.lon}</:col>
|
||||||
<:col :let={{_id, beacon}} label="Power (mW)">{beacon.power_mw}</:col>
|
<:col :let={{_id, beacon}} label="Power (mW)">{beacon.power_mw}</:col>
|
||||||
<:col :let={{_id, beacon}} label="Height AGL (m)">{beacon.height_m}</:col>
|
<:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}</:col>
|
||||||
<:action :let={{_id, beacon}}>
|
<:action :let={{_id, beacon}}>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
<.link navigate={~p"/beacons/#{beacon}"}>Show</.link>
|
<.link navigate={~p"/beacons/#{beacon}"}>Show</.link>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
||||||
<:item title="Latitude">{@beacon.lat}</:item>
|
<:item title="Latitude">{@beacon.lat}</:item>
|
||||||
<:item title="Longitude">{@beacon.lon}</:item>
|
<:item title="Longitude">{@beacon.lon}</:item>
|
||||||
<:item title="Power (mW)">{@beacon.power_mw}</:item>
|
<:item title="Power (mW)">{@beacon.power_mw}</:item>
|
||||||
<:item title="Height above ground (m)">{@beacon.height_m}</:item>
|
<:item title="Height above ground (ft)">{@beacon.height_ft}</:item>
|
||||||
</.list>
|
</.list>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule Microwaveprop.BeaconsTest do
|
||||||
lat: nil,
|
lat: nil,
|
||||||
lon: nil,
|
lon: nil,
|
||||||
power_mw: nil,
|
power_mw: nil,
|
||||||
height_m: nil
|
height_ft: nil
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "list_beacons/0" do
|
describe "list_beacons/0" do
|
||||||
|
|
@ -46,7 +46,7 @@ defmodule Microwaveprop.BeaconsTest do
|
||||||
assert beacon.lat == attrs.lat
|
assert beacon.lat == attrs.lat
|
||||||
assert beacon.lon == attrs.lon
|
assert beacon.lon == attrs.lon
|
||||||
assert beacon.power_mw == attrs.power_mw
|
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
|
assert beacon.user_id == user.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -57,6 +57,19 @@ defmodule Microwaveprop.BeaconsTest do
|
||||||
assert String.starts_with?(beacon.grid, "EM12")
|
assert String.starts_with?(beacon.grid, "EM12")
|
||||||
end
|
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
|
test "keeps an explicitly provided valid grid" do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(grid: "EM13"))
|
{:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs(grid: "EM13"))
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Microwaveprop.BeaconsFixtures do
|
||||||
lat: 32.897,
|
lat: 32.897,
|
||||||
lon: -97.038,
|
lon: -97.038,
|
||||||
power_mw: 10_000.0,
|
power_mw: 10_000.0,
|
||||||
height_m: 30.0
|
height_ft: 100.0
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue