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",
|
page_title: "Rover Locations",
|
||||||
form: nil,
|
form: nil,
|
||||||
editing_id: nil,
|
editing_id: nil,
|
||||||
|
grid_input: "",
|
||||||
status_filter: nil
|
status_filter: nil
|
||||||
)
|
)
|
||||||
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [nil]})}
|
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [nil]})}
|
||||||
|
|
@ -75,7 +76,7 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
|
|
||||||
def handle_event("new", _params, socket) do
|
def handle_event("new", _params, socket) do
|
||||||
if user = current_user(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
|
else
|
||||||
{:noreply, put_flash(socket, :error, "Sign in to add a location.")}
|
{:noreply, put_flash(socket, :error, "Sign in to add a location.")}
|
||||||
end
|
end
|
||||||
|
|
@ -87,7 +88,8 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
case Repo.get(Location, id) do
|
case Repo.get(Location, id) do
|
||||||
%Location{user_id: ^user_id} = loc ->
|
%Location{user_id: ^user_id} = loc ->
|
||||||
cs = Location.changeset(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.")}
|
{:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
|
||||||
|
|
@ -99,13 +101,16 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("cancel", _, socket) do
|
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
|
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{}
|
base = if socket.assigns.editing_id, do: location_for_edit(socket), else: %Location{}
|
||||||
cs = base |> Location.changeset(params) |> Map.put(:action, :validate)
|
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
|
end
|
||||||
|
|
||||||
def handle_event("save", %{"location" => params}, socket) do
|
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(value) when value in @valid_status_filters, do: value
|
||||||
defp parse_status(_), do: nil
|
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(%Phoenix.LiveView.Socket{assigns: assigns}), do: scope_user(assigns)
|
||||||
defp current_user(assigns) when is_map(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"}
|
{if @editing_id, do: "Edit location", else: "New location"}
|
||||||
</h2>
|
</h2>
|
||||||
<.form for={@form} phx-change="validate" phx-submit="save" id="location-form">
|
<.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
|
<.input
|
||||||
field={@form[:lat]}
|
field={@form[:lat]}
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -340,12 +398,6 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
label="Longitude"
|
label="Longitude"
|
||||||
placeholder="-97.5"
|
placeholder="-97.5"
|
||||||
/>
|
/>
|
||||||
<.input
|
|
||||||
field={@form[:status]}
|
|
||||||
type="select"
|
|
||||||
label="Status"
|
|
||||||
options={[{"Ideal", :ideal}, {"Off Limits", :off_limits}]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<.input
|
<.input
|
||||||
field={@form[:notes]}
|
field={@form[:notes]}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,48 @@ defmodule MicrowavepropWeb.RoverLocationsLiveTest do
|
||||||
assert html =~ "test note"
|
assert html =~ "test note"
|
||||||
end
|
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
|
test "edit/delete buttons appear only on the owner's row", %{conn: conn, user: user} do
|
||||||
other = Microwaveprop.AccountsFixtures.user_fixture()
|
other = Microwaveprop.AccountsFixtures.user_fixture()
|
||||||
{:ok, mine} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :ideal})
|
{:ok, mine} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :ideal})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue