From 9bfd345e8d099eb8709f89cb8e7a875943de5afc Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 25 Apr 2026 17:04:45 -0500 Subject: [PATCH] feat(accounts): derive 10-char home_grid from explicit lat/lon --- lib/microwaveprop/accounts/user.ex | 32 ++++++++++++++++++++--- test/microwaveprop/accounts/user_test.exs | 21 +++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/lib/microwaveprop/accounts/user.ex b/lib/microwaveprop/accounts/user.ex index 6404c57e..ec60685a 100644 --- a/lib/microwaveprop/accounts/user.ex +++ b/lib/microwaveprop/accounts/user.ex @@ -220,9 +220,10 @@ defmodule Microwaveprop.Accounts.User do Accepts either a Maidenhead grid or an explicit lat/lon. When only the grid is given, lat/lon are derived from its centre; when only lat/lon - are given, the grid is left untouched. An empty `home_grid` paired - with explicit nil lat/lon clears all home fields (used by the "clear - home" affordance on /rover). + are given, a 10-character grid is computed from the coordinate pair so + callers always end up with both fields populated. An empty `home_grid` + paired with explicit nil lat/lon clears all home fields (used by the + "clear home" affordance on /rover). """ @spec change_home_qth(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() def change_home_qth(user, attrs) do @@ -231,6 +232,7 @@ defmodule Microwaveprop.Accounts.User do |> normalize_home_grid() |> validate_home_grid_format() |> derive_home_latlon_from_grid() + |> derive_home_grid_from_latlon() |> validate_home_latlon_range() |> require_home_qth_pair() end @@ -294,6 +296,30 @@ defmodule Microwaveprop.Accounts.User do end end + # If lat/lon were provided without a grid, encode a 10-char Maidenhead + # grid from the coordinates so the UI always has a human-readable label + # to render and downstream callers can rely on `home_grid` being set + # whenever the QTH is set. Skipped on invalid changesets (lat/lon range + # check has not run yet) and when both fields aren't numeric. + defp derive_home_grid_from_latlon(changeset) do + lat = get_field(changeset, :home_lat) + lon = get_field(changeset, :home_lon) + + cond do + not changeset.valid? -> + changeset + + not (is_number(lat) and is_number(lon)) -> + changeset + + get_field(changeset, :home_grid) not in [nil, ""] -> + changeset + + true -> + put_change(changeset, :home_grid, Maidenhead.from_latlon(lat * 1.0, lon * 1.0, 10)) + end + end + defp validate_home_latlon_range(changeset) do changeset |> validate_number(:home_lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0) diff --git a/test/microwaveprop/accounts/user_test.exs b/test/microwaveprop/accounts/user_test.exs index 90a0d83a..625600f9 100644 --- a/test/microwaveprop/accounts/user_test.exs +++ b/test/microwaveprop/accounts/user_test.exs @@ -461,6 +461,27 @@ defmodule Microwaveprop.Accounts.UserTest do assert Ecto.Changeset.get_change(changeset, :home_lon) == -96.7 end + test "derives a 10-char home_grid when lat/lon are given without one" do + changeset = User.change_home_qth(%User{}, %{"home_lat" => 33.10, "home_lon" => -96.625}) + assert changeset.valid? + grid = Ecto.Changeset.get_change(changeset, :home_grid) + assert is_binary(grid) + assert String.length(grid) == 10 + assert String.starts_with?(grid, "EM13qc") + end + + test "preserves an explicit grid when both grid and lat/lon are provided" do + changeset = + User.change_home_qth(%User{}, %{ + "home_grid" => "EM13qc", + "home_lat" => 33.10, + "home_lon" => -96.625 + }) + + assert changeset.valid? + assert Ecto.Changeset.get_change(changeset, :home_grid) == "EM13qc" + end + test "rejects invalid grids" do changeset = User.change_home_qth(%User{}, %{"home_grid" => "ZZ99zz"}) refute changeset.valid?