Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 4m35s
- MonitorLive.Show: safe nil-guard on current_scope for anonymous access - Admin.MonitorLive.Index: add phx-update=stream to enable stream ops - ImportLive: require owner/admin authorization, not_found redirect - MapLive: store timer refs in assigns, cancel before reschedule - 10 schemas: add missing foreign_key_constraint on belongs_to - Soundings: preload :station to eliminate N+1 in path analysis - PathAnalysis: defensive preload of :station on soundings - GridTaskEnqueuer: wrap reclaim_stale_running in Repo.transaction() - HrdpsClient: replace String.to_atom with compile-time atom literals - Contacts: fix extract_latlon false return for lon=0.0 - Tests: remove duplicate Mox.defmock, unblock swallowed task exits, bump refute_receive timeouts from 50ms to 200ms
111 lines
3.4 KiB
Elixir
111 lines
3.4 KiB
Elixir
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)
|
|
|> foreign_key_constraint(:mission_id)
|
|
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
|