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 feet. """ use Ecto.Schema import Ecto.Changeset alias Microwaveprop.Radio.Maidenhead @keyings ~w(on_off fsk) def keyings, do: @keyings @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "beacons" do field :frequency_mhz, :float field :callsign, :string field :grid, :string field :lat, :float field :lon, :float field :power_mw, :float field :height_ft, :float field :on_the_air, :boolean, default: true field :approved, :boolean, default: false field :keying, :string, default: "on_off" field :notes, :string field :user_id, :binary_id timestamps(type: :utc_datetime) end @required_fields [:frequency_mhz, :callsign, :lat, :lon, :power_mw, :height_ft, :keying] def changeset(beacon, attrs) do beacon |> cast(attrs, [ :frequency_mhz, :callsign, :grid, :lat, :lon, :power_mw, :height_ft, :on_the_air, :keying, :notes ]) |> update_change(:callsign, fn cs -> cs && String.upcase(String.trim(cs)) end) |> maybe_fill_latlon() |> maybe_fill_grid() |> validate_required(@required_fields) |> validate_inclusion(:keying, @keyings) |> validate_number(:frequency_mhz, greater_than: 0) |> validate_number(:power_mw, 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) |> validate_grid_format() end # If grid is blank but lat/lon are valid, derive it. defp maybe_fill_grid(changeset) do grid = get_field(changeset, :grid) lat = get_field(changeset, :lat) lon = get_field(changeset, :lon) if grid in [nil, ""] and is_number(lat) and is_number(lon) do put_change(changeset, :grid, Maidenhead.from_latlon(lat, lon, 6)) else changeset 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 -> add_error(changeset, :grid, "can't be blank") grid -> if Maidenhead.valid?(grid) do update_change(changeset, :grid, &normalize_grid/1) else add_error(changeset, :grid, "is not a valid Maidenhead grid") end end end # Field uppercase, square digits, subsquare lowercase (standard form) defp normalize_grid(nil), do: nil defp normalize_grid(grid) do grid = String.trim(grid) len = String.length(grid) if len >= 6 do grid |> String.slice(0, 4) |> String.upcase() |> Kernel.<>(String.downcase(String.slice(grid, 4, len - 4))) else String.upcase(grid) end end end