From f63e679e340a65eb0ee8df82bf27a1490c4947de Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 26 Apr 2026 13:55:05 -0500 Subject: [PATCH] feat(rover-locations): inline satellite map per location Click a row's info area to expand a Leaflet mini-map centered on that pin, defaulting to ESRI World_Imagery satellite (zoom up to 21) with OSM/Topo toggles in a top-right layer control. --- assets/js/app.ts | 3 +- assets/js/location_map_hook.ts | 64 +++++++++++++++++++ .../live/rover_locations_live.ex | 24 ++++++- 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 assets/js/location_map_hook.ts diff --git a/assets/js/app.ts b/assets/js/app.ts index a4c4ee68..a571f94a 100644 --- a/assets/js/app.ts +++ b/assets/js/app.ts @@ -20,6 +20,7 @@ import {ElevationProfile} from "./elevation_profile_hook" import {WeatherMap} from "./weather_map_hook" import {LocateMe, CopyLink} from "./locate_me_hook" import {RoverMap} from "./rover_map_hook" +import {LocationMap} from "./location_map_hook" import {RoverSlider} from "./rover_slider_hook" import {HorizontalWheelScroll} from "./horizontal_wheel_scroll_hook" import {BeaconMap} from "./beacon_map_hook" @@ -313,7 +314,7 @@ const csrfToken = document.querySelector("meta[name='csrf-token']")!.getAttribut const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: initLiveStash({_csrf_token: csrfToken}), - hooks: {...colocatedHooks, PropagationMap, PathForecast, UtcClock, ContactMap, ContactsMap, ElevationProfile, WeatherMap, LocateMe, CopyLink, RoverMap, RoverSlider, HorizontalWheelScroll, BeaconMap, BeaconsListMap, CommaNumber, ImportConfetti, EmeGlobe, Skewt}, + hooks: {...colocatedHooks, PropagationMap, PathForecast, UtcClock, ContactMap, ContactsMap, ElevationProfile, WeatherMap, LocateMe, CopyLink, RoverMap, LocationMap, RoverSlider, HorizontalWheelScroll, BeaconMap, BeaconsListMap, CommaNumber, ImportConfetti, EmeGlobe, Skewt}, }) // live_table rows are dead on their own — wire up whole-row clicks to diff --git a/assets/js/location_map_hook.ts b/assets/js/location_map_hook.ts new file mode 100644 index 00000000..1cf6bb0a --- /dev/null +++ b/assets/js/location_map_hook.ts @@ -0,0 +1,64 @@ +import type { ViewHook } from "phoenix_live_view" + +interface LocationMapHook extends ViewHook { + map: L.Map + marker: L.Marker + visibilityHandler: (() => void) | null +} + +export const LocationMap: Partial = { + mounted(this: LocationMapHook) { + const lat = parseFloat(this.el.dataset.lat || "0") + const lon = parseFloat(this.el.dataset.lon || "0") + + const osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19 + }) + const satellite = L.tileLayer( + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + { maxZoom: 21, maxNativeZoom: 19 } + ) + const topo = L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", { + maxZoom: 17 + }) + + const map = L.map(this.el, { + center: [lat, lon], + zoom: 16, + zoomControl: true, + attributionControl: false + }) + this.map = map + + satellite.addTo(map) + + L.control.layers( + { "Satellite": satellite, "OSM": osm, "Topo": topo }, + {}, + { position: "topright", collapsed: false } + ).addTo(map) + + this.marker = L.marker([lat, lon]).addTo(map) + this.marker.bindPopup(`${lat.toFixed(6)}, ${lon.toFixed(6)}`) + + // LiveView reconnect / tab visibility — Leaflet needs invalidateSize. + this.visibilityHandler = () => { + if (document.visibilityState === "visible") this.map.invalidateSize() + } + document.addEventListener("visibilitychange", this.visibilityHandler) + + setTimeout(() => map.invalidateSize(), 50) + }, + + reconnected(this: LocationMapHook) { + this.map?.invalidateSize() + }, + + destroyed(this: LocationMapHook) { + if (this.visibilityHandler) { + document.removeEventListener("visibilitychange", this.visibilityHandler) + this.visibilityHandler = null + } + this.map?.remove() + } +} diff --git a/lib/microwaveprop_web/live/rover_locations_live.ex b/lib/microwaveprop_web/live/rover_locations_live.ex index e748c843..6b57dea3 100644 --- a/lib/microwaveprop_web/live/rover_locations_live.ex +++ b/lib/microwaveprop_web/live/rover_locations_live.ex @@ -18,11 +18,17 @@ defmodule MicrowavepropWeb.RoverLocationsLive do page_title: "Rover Locations", locations: Rover.list_locations(), form: nil, - editing_id: nil + editing_id: nil, + expanded_id: nil )} end @impl true + def handle_event("toggle_map", %{"id" => id}, socket) do + new_id = if socket.assigns.expanded_id == id, do: nil, else: id + {:noreply, assign(socket, expanded_id: new_id)} + end + def handle_event("new", _params, socket) do if user = current_user(socket) do form = blank_form(user) @@ -237,7 +243,11 @@ defmodule MicrowavepropWeb.RoverLocationsLive do class="card bg-base-100 border border-base-300 p-4" >
-
+
{status_label(loc.status)} @@ -274,6 +284,16 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
+
+