Add URL updating and restoration for map state (zoom/pan)

Features implemented:
- Parse URL parameters on mount (lat, lng, z)
- Update URL when map moves or zooms
- Restore map state from URL parameters
- Handle URL changes via handle_params
- Validate and clamp coordinate values

URL format: /?lat=40.7128&lng=-74.0060&z=12

Benefits:
- Shareable map links with exact position
- Browser back/forward navigation support
- Bookmarkable map locations
- Deep linking to specific map views

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2025-07-09 09:23:54 -05:00
parent c2d3418972
commit 052e131400
No known key found for this signature in database
3 changed files with 121 additions and 3 deletions

View file

@ -290,13 +290,19 @@ let MapAPRSMap = {
if (self.boundsTimer) clearTimeout(self.boundsTimer);
self.boundsTimer = setTimeout(() => {
self.sendBoundsToServer();
// Save map state
// Save map state and update URL
const center = self.map.getCenter();
const zoom = self.map.getZoom();
localStorage.setItem(
"aprs_map_state",
JSON.stringify({ lat: center.lat, lng: center.lng, zoom }),
);
// Send map state update to server for URL updating
self.pushEvent("update_map_state", {
center: { lat: center.lat, lng: center.lng },
zoom: zoom
});
}, 300);
});
@ -315,13 +321,19 @@ let MapAPRSMap = {
self.sendBoundsToServer();
self.lastZoom = currentZoom;
// Save map state
// Save map state and update URL
const center = self.map.getCenter();
const zoom = self.map.getZoom();
localStorage.setItem(
"aprs_map_state",
JSON.stringify({ lat: center.lat, lng: center.lng, zoom }),
);
// Send map state update to server for URL updating
self.pushEvent("update_map_state", {
center: { lat: center.lat, lng: center.lng },
zoom: zoom
});
}, 300);
});

View file

@ -15,8 +15,54 @@ defmodule AprsmeWeb.MapLive.Index do
@default_center %{lat: 39.8283, lng: -98.5795}
@default_zoom 5
# Parse map state from URL parameters
@spec parse_map_params(map()) :: {map(), integer()}
defp parse_map_params(params) do
# Parse latitude (lat parameter)
lat =
case Map.get(params, "lat") do
nil ->
@default_center.lat
lat_str ->
case Float.parse(lat_str) do
{lat_val, _} when lat_val >= -90 and lat_val <= 90 -> lat_val
_ -> @default_center.lat
end
end
# Parse longitude (lng parameter)
lng =
case Map.get(params, "lng") do
nil ->
@default_center.lng
lng_str ->
case Float.parse(lng_str) do
{lng_val, _} when lng_val >= -180 and lng_val <= 180 -> lng_val
_ -> @default_center.lng
end
end
# Parse zoom level (z parameter)
zoom =
case Map.get(params, "z") do
nil ->
@default_zoom
zoom_str ->
case Integer.parse(zoom_str) do
{zoom_val, _} when zoom_val >= 1 and zoom_val <= 20 -> zoom_val
_ -> @default_zoom
end
end
map_center = %{lat: lat, lng: lng}
{map_center, zoom}
end
@impl true
def mount(_params, _session, socket) do
def mount(params, _session, socket) do
if connected?(socket) do
# Subscribe to packet updates
Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets")
@ -31,7 +77,11 @@ defmodule AprsmeWeb.MapLive.Index do
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
# Parse map state from URL parameters
{map_center, map_zoom} = parse_map_params(params)
socket = assign_defaults(socket, one_hour_ago)
socket = assign(socket, map_center: map_center, map_zoom: map_zoom)
socket = assign(socket, packet_buffer: [], buffer_timer: nil)
socket = assign(socket, all_packets: %{}, station_popup_open: false)
@ -266,6 +316,61 @@ defmodule AprsmeWeb.MapLive.Index do
{:noreply, socket}
end
@impl true
def handle_event("update_map_state", %{"center" => center, "zoom" => zoom}, socket) do
# Parse center coordinates
lat =
case center do
%{"lat" => lat_val} -> lat_val
_ -> socket.assigns.map_center.lat
end
lng =
case center do
%{"lng" => lng_val} -> lng_val
_ -> socket.assigns.map_center.lng
end
# Validate and clamp values
lat = max(-90.0, min(90.0, lat))
lng = max(-180.0, min(180.0, lng))
zoom = max(1, min(20, zoom))
map_center = %{lat: lat, lng: lng}
# Update socket state
socket = assign(socket, map_center: map_center, map_zoom: zoom)
# Update URL without page reload
new_path = "/?lat=#{lat}&lng=#{lng}&z=#{zoom}"
socket = push_patch(socket, to: new_path, replace: true)
{:noreply, socket}
end
@impl true
def handle_params(params, _url, socket) do
# Parse new map state from URL parameters
{map_center, map_zoom} = parse_map_params(params)
# Update socket state
socket = assign(socket, map_center: map_center, map_zoom: map_zoom)
# If map is ready, update the client-side map
socket =
if socket.assigns.map_ready do
push_event(socket, "zoom_to_location", %{
lat: map_center.lat,
lng: map_center.lng,
zoom: map_zoom
})
else
socket
end
{:noreply, socket}
end
@impl true
def handle_info({:process_bounds_update, map_bounds}, socket), do: handle_info_process_bounds_update(map_bounds, socket)

View file

@ -1,4 +1,5 @@
%{
"aprs": {:git, "https://github.com/aprsme/aprs.git", "39cbe6e277a2a98296140ec0ef5073a798c258d0", [branch: "main"]},
"bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},