- Create MaidenheadChangesetHelpers: consolidate grid validation, callsign normalization, lat/lon validation, grid/latlon derivation across 6 schemas - Create ContextHelpers: shared fetch_owned with admin bypass, safe_enqueue for Oban workers, UUID casting to replace CastError rescues - Extend LiveHelpers: add current_user/1 (removes 7 duplicate definitions), subscribe/2 (replaces 13 inline PubSub sites), assign_url_params/2 - Extract Propagation.ScoreStore (528 lines): separate file I/O and cache management from scoring logic, 13 defdelegate passthroughs - Split SubmitLive (942->475 lines): extract CSV/ADIF upload rendering into 3 function component modules (csv_upload, adif_upload, preview) - Update 16 LiveViews to use shared helpers
85 lines
2.8 KiB
Elixir
85 lines
2.8 KiB
Elixir
defmodule Microwaveprop.Rover.FixedStation do
|
|
@moduledoc """
|
|
A fixed station owned by a user. Used as a known endpoint for the rover
|
|
planner — the rover predicts SNR margins from each candidate cell back
|
|
to the selected fixed stations.
|
|
|
|
Either an explicit `lat`/`lon` pair or a Maidenhead `grid` is required;
|
|
when only `grid` is provided the changeset derives the centre lat/lon
|
|
via `Microwaveprop.Radio.Maidenhead.to_latlon/1`. Elevation is *not*
|
|
filled in by the changeset — `Microwaveprop.Workers.StationElevationWorker`
|
|
enriches it after insert.
|
|
"""
|
|
use Ecto.Schema
|
|
|
|
import Ecto.Changeset
|
|
|
|
alias Microwaveprop.Radio.MaidenheadChangesetHelpers, as: MCH
|
|
|
|
@callsign_regex ~r/^[A-Z0-9\/]{3,12}$/
|
|
|
|
@primary_key {:id, :binary_id, autogenerate: true}
|
|
@foreign_key_type :binary_id
|
|
schema "fixed_stations" do
|
|
field :callsign, :string
|
|
field :grid, :string
|
|
field :lat, :float
|
|
field :lon, :float
|
|
field :elevation_m, :integer
|
|
field :selected, :boolean, default: true
|
|
field :position, :integer, default: 0
|
|
|
|
belongs_to :user, Microwaveprop.Accounts.User
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@type t :: %__MODULE__{}
|
|
|
|
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
|
def changeset(station, attrs) do
|
|
station
|
|
|> cast(attrs, [:callsign, :grid, :lat, :lon, :elevation_m, :selected, :position])
|
|
|> MCH.normalize_callsign(:callsign)
|
|
|> normalize_grid()
|
|
|> validate_required([:callsign])
|
|
|> validate_format(:callsign, @callsign_regex)
|
|
|> MCH.validate_grid(:grid, blank_error: false, normalizer: &normalize_grid_string/1)
|
|
|> MCH.derive_latlon(:grid, :lat, :lon)
|
|
|> validate_required([:lat, :lon])
|
|
|> MCH.validate_latlon(:lat, :lon)
|
|
|> foreign_key_constraint(:user_id)
|
|
|> unique_constraint(:callsign, name: :fixed_stations_user_id_callsign_index)
|
|
end
|
|
|
|
defp normalize_grid(changeset) do
|
|
case get_change(changeset, :grid) do
|
|
nil -> changeset
|
|
"" -> put_change(changeset, :grid, nil)
|
|
grid when is_binary(grid) -> put_change(changeset, :grid, normalize_grid_string(grid))
|
|
end
|
|
end
|
|
|
|
# Field + Square uppercase, subsquare lowercase, extended square uppercase.
|
|
# Maidenhead convention: A-R / 0-9 / a-x / 0-9 / A-X / ...
|
|
defp normalize_grid_string(grid) do
|
|
grid
|
|
|> String.trim()
|
|
|> String.graphemes()
|
|
|> Enum.with_index()
|
|
|> Enum.map_join(fn {ch, idx} ->
|
|
pair = div(idx, 2)
|
|
|
|
letter_pair_index = div(pair, 2)
|
|
|
|
cond do
|
|
# Pair 0 (Field, letters) + Pair 1 (Square, digits) +
|
|
# Pair 2 (Subsquare, letters) + Pair 3 (Extended, digits) ...
|
|
# Letter pairs alternate uppercase / lowercase / uppercase ...
|
|
rem(pair, 2) == 1 -> ch
|
|
rem(letter_pair_index, 2) == 0 -> String.upcase(ch)
|
|
true -> String.downcase(ch)
|
|
end
|
|
end)
|
|
end
|
|
end
|