defmodule Microwaveprop.RoverPlanning.Station do @moduledoc """ A stationary endpoint for a rover mission. The user provides one of: callsign, Maidenhead grid, or `lat, lon`; the changeset normalizes the input into all three when possible (callsigns geocode via `MicrowavepropWeb.LocationResolver`). """ use Ecto.Schema import Ecto.Changeset alias Microwaveprop.RoverPlanning.Mission alias MicrowavepropWeb.LocationResolver @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "rover_mission_stations" do field :callsign, :string field :grid, :string field :input, :string field :lat, :float field :lon, :float field :position, :integer, default: 0 belongs_to :mission, Mission 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, :input, :lat, :lon, :position]) |> validate_required([:input]) |> resolve_input() |> validate_required([:lat, :lon]) |> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0) |> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0) end # When `input` looks like a callsign or grid, geocode it and populate # the matching field. If the user TYPED a new input (changeset has a # `:input` change), wipe any previously-resolved callsign / grid / # lat / lon first — otherwise an early prefix that resolved # successfully (e.g. "AA5" matched a callsign) would lock those # fields and the user's later keystrokes would never re-resolve. defp resolve_input(changeset) do cond do not changeset.valid? -> changeset input_changed?(changeset) -> changeset |> clear_resolved() |> resolve_from_input() get_field(changeset, :lat) && get_field(changeset, :lon) -> changeset true -> resolve_from_input(changeset) end end defp input_changed?(changeset), do: not is_nil(get_change(changeset, :input)) defp clear_resolved(changeset) do changeset |> put_change(:callsign, nil) |> put_change(:grid, nil) |> put_change(:lat, nil) |> put_change(:lon, nil) end defp resolve_from_input(changeset) do case get_field(changeset, :input) do input when is_binary(input) and input != "" -> apply_resolution(changeset, LocationResolver.resolve(String.trim(input))) _ -> changeset end end defp apply_resolution(changeset, {:ok, %{kind: :callsign} = res}) do changeset |> put_change(:callsign, res.label) |> put_change(:grid, Map.get(res, :grid)) |> put_change(:lat, res.lat) |> put_change(:lon, res.lon) end defp apply_resolution(changeset, {:ok, %{kind: :grid} = res}) do changeset |> put_change(:grid, res.label) |> put_change(:lat, res.lat) |> put_change(:lon, res.lon) end defp apply_resolution(changeset, {:ok, %{kind: :coordinates} = res}) do changeset |> put_change(:lat, res.lat) |> put_change(:lon, res.lon) end defp apply_resolution(changeset, {:error, msg}), do: add_error(changeset, :input, msg) defp apply_resolution(changeset, :empty), do: add_error(changeset, :input, "is required") end