feat(rover): two-way grid <-> lat/lon sync on location form
Adds a Maidenhead grid input to the rover-location form. Typing a grid fills in lat/lon (using square center); editing lat/lon recomputes the grid at 6-char precision. The phx-change `_target` decides direction so edits never clobber what the user just typed.
This commit is contained in:
parent
7eafa4c71a
commit
4a82571cda
2 changed files with 106 additions and 12 deletions
|
|
@ -56,6 +56,7 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
|||
page_title: "Rover Locations",
|
||||
form: nil,
|
||||
editing_id: nil,
|
||||
grid_input: "",
|
||||
status_filter: nil
|
||||
)
|
||||
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [nil]})}
|
||||
|
|
@ -75,7 +76,7 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
|||
|
||||
def handle_event("new", _params, socket) do
|
||||
if user = current_user(socket) do
|
||||
{:noreply, assign(socket, form: blank_form(user), editing_id: nil)}
|
||||
{:noreply, assign(socket, form: blank_form(user), editing_id: nil, grid_input: "")}
|
||||
else
|
||||
{:noreply, put_flash(socket, :error, "Sign in to add a location.")}
|
||||
end
|
||||
|
|
@ -87,7 +88,8 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
|||
case Repo.get(Location, id) do
|
||||
%Location{user_id: ^user_id} = loc ->
|
||||
cs = Location.changeset(loc, %{})
|
||||
{:noreply, assign(socket, form: to_form(cs), editing_id: id)}
|
||||
grid = Maidenhead.from_latlon(loc.lat, loc.lon, 6)
|
||||
{:noreply, assign(socket, form: to_form(cs), editing_id: id, grid_input: grid)}
|
||||
|
||||
_ ->
|
||||
{:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
|
||||
|
|
@ -99,13 +101,16 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
|||
end
|
||||
|
||||
def handle_event("cancel", _, socket) do
|
||||
{:noreply, assign(socket, form: nil, editing_id: nil)}
|
||||
{:noreply, assign(socket, form: nil, editing_id: nil, grid_input: "")}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"location" => params}, socket) do
|
||||
def handle_event("validate", %{"location" => params} = event_params, socket) do
|
||||
target = event_params["_target"]
|
||||
{params, grid_input} = sync_grid_and_latlon(params, target, socket.assigns.grid_input)
|
||||
|
||||
base = if socket.assigns.editing_id, do: location_for_edit(socket), else: %Location{}
|
||||
cs = base |> Location.changeset(params) |> Map.put(:action, :validate)
|
||||
{:noreply, assign(socket, form: to_form(cs))}
|
||||
{:noreply, assign(socket, form: to_form(cs), grid_input: grid_input)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"location" => params}, socket) do
|
||||
|
|
@ -201,6 +206,44 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
|||
defp parse_status(value) when value in @valid_status_filters, do: value
|
||||
defp parse_status(_), do: nil
|
||||
|
||||
# Two-way grid <-> lat/lon sync. `_target` from the phx-change event
|
||||
# tells us which field the user actually edited; we mirror the change
|
||||
# into the other side. Unparseable input leaves the other side alone.
|
||||
defp sync_grid_and_latlon(params, ["location", "grid"], _old_grid) do
|
||||
raw = params |> Map.get("grid", "") |> to_string() |> String.trim()
|
||||
|
||||
case Maidenhead.to_latlon(raw) do
|
||||
{:ok, {lat, lon}} ->
|
||||
params =
|
||||
params
|
||||
|> Map.put("lat", format_coord(lat))
|
||||
|> Map.put("lon", format_coord(lon))
|
||||
|
||||
{params, raw}
|
||||
|
||||
:error ->
|
||||
{params, raw}
|
||||
end
|
||||
end
|
||||
|
||||
defp sync_grid_and_latlon(params, ["location", field], old_grid) when field in ["lat", "lon"] do
|
||||
with {lat, ""} <- params |> Map.get("lat", "") |> to_string() |> String.trim() |> Float.parse(),
|
||||
{lon, ""} <- params |> Map.get("lon", "") |> to_string() |> String.trim() |> Float.parse(),
|
||||
lat when lat >= -90.0 and lat <= 90.0 <- lat,
|
||||
lon when lon >= -180.0 and lon <= 180.0 <- lon do
|
||||
grid = Maidenhead.from_latlon(lat, lon, 6)
|
||||
{Map.put(params, "grid", grid), grid}
|
||||
else
|
||||
_ -> {params, old_grid}
|
||||
end
|
||||
end
|
||||
|
||||
defp sync_grid_and_latlon(params, _target, old_grid), do: {params, old_grid}
|
||||
|
||||
defp format_coord(value) do
|
||||
value |> Float.round(6) |> Float.to_string()
|
||||
end
|
||||
|
||||
defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}), do: scope_user(assigns)
|
||||
defp current_user(assigns) when is_map(assigns), do: scope_user(assigns)
|
||||
|
||||
|
|
@ -325,7 +368,22 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
|||
{if @editing_id, do: "Edit location", else: "New location"}
|
||||
</h2>
|
||||
<.form for={@form} phx-change="validate" phx-submit="save" id="location-form">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<.input
|
||||
name="location[grid]"
|
||||
value={@grid_input}
|
||||
type="text"
|
||||
label="Grid (Maidenhead)"
|
||||
placeholder="EM12kp"
|
||||
/>
|
||||
<.input
|
||||
field={@form[:status]}
|
||||
type="select"
|
||||
label="Status"
|
||||
options={[{"Ideal", :ideal}, {"Off Limits", :off_limits}]}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<.input
|
||||
field={@form[:lat]}
|
||||
type="number"
|
||||
|
|
@ -340,12 +398,6 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
|||
label="Longitude"
|
||||
placeholder="-97.5"
|
||||
/>
|
||||
<.input
|
||||
field={@form[:status]}
|
||||
type="select"
|
||||
label="Status"
|
||||
options={[{"Ideal", :ideal}, {"Off Limits", :off_limits}]}
|
||||
/>
|
||||
</div>
|
||||
<.input
|
||||
field={@form[:notes]}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,48 @@ defmodule MicrowavepropWeb.RoverLocationsLiveTest do
|
|||
assert html =~ "test note"
|
||||
end
|
||||
|
||||
test "typing a grid populates lat/lon", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/rover-locations")
|
||||
lv |> element("button", "Add location") |> render_click()
|
||||
|
||||
html =
|
||||
lv
|
||||
|> form("#location-form",
|
||||
location: %{
|
||||
grid: "EM12kp",
|
||||
lat: "",
|
||||
lon: "",
|
||||
status: "ideal",
|
||||
notes: ""
|
||||
}
|
||||
)
|
||||
|> render_change(%{"_target" => ["location", "grid"]})
|
||||
|
||||
# EM12kp center: lat 32.645833, lon -97.125
|
||||
assert html =~ ~s(value="32.645833")
|
||||
assert html =~ ~s(value="-97.125")
|
||||
end
|
||||
|
||||
test "typing lat/lon populates the grid field", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/rover-locations")
|
||||
lv |> element("button", "Add location") |> render_click()
|
||||
|
||||
html =
|
||||
lv
|
||||
|> form("#location-form",
|
||||
location: %{
|
||||
grid: "",
|
||||
lat: "32.5",
|
||||
lon: "-97.5",
|
||||
status: "ideal",
|
||||
notes: ""
|
||||
}
|
||||
)
|
||||
|> render_change(%{"_target" => ["location", "lat"]})
|
||||
|
||||
assert html =~ ~s(value="EM12gm")
|
||||
end
|
||||
|
||||
test "edit/delete buttons appear only on the owner's row", %{conn: conn, user: user} do
|
||||
other = Microwaveprop.AccountsFixtures.user_fixture()
|
||||
{:ok, mine} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :ideal})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue