From 4d46dba4b2df36e25820a76abb9d5fda743770ad Mon Sep 17 00:00:00 2001 From: dborup Date: Wed, 22 Jul 2026 18:44:59 +0200 Subject: [PATCH] feat: split "Mesh live area" into a geo-filter toggle and a selected-area outline The old single "Mesh live area" checkbox actually showed the homeArea-linked geo_filter boundary, which was confusing paired with the already-existing "Area: X" dropdown (AreaFilter) that filters which nodes show on the map -- picking an area there drew nothing. Now two independent checkboxes on both Map and Live: - "Geo filter boundary" -- unchanged behavior, the geo_filter/homeArea polygon (with buffer zone if configured). - "Selected area outline" -- new, draws whichever area is currently picked via the Area dropdown (falls back to a box when the area has no polygon), checked by default whenever an area is selected. --- public/live.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++++-- public/map.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 4 deletions(-) diff --git a/public/live.js b/public/live.js index 027ed381..977b6a91 100644 --- a/public/live.js +++ b/public/live.js @@ -17,7 +17,7 @@ function cssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } function statusGreen() { return cssVar('--status-green') || '#22c55e'; } - let map, ws, nodesLayer, pathsLayer, animLayer, heatLayer, geoFilterLayer, clickablePathsLayer; + let map, ws, nodesLayer, pathsLayer, animLayer, heatLayer, geoFilterLayer, selectedAreaLayer, clickablePathsLayer; // New animation canvas let animCanvas, animCtx; let _dprMedia = null; @@ -1151,7 +1151,8 @@ Show only favorited and claimed nodes Show only multibyte (≥2-byte path-hash) packets; hide unreliable single-byte traffic - + +
@@ -1856,6 +1857,73 @@ } catch (e) { /* no geo filter configured */ } })(); + // Selected area outline — distinct from the geo filter boundary above: + // this draws whichever area is currently picked via the "Area: X ▾" + // dropdown (AreaFilter, shared with Nodes/Map/Analytics), not + // necessarily the homeArea-linked geo_filter. Falls back to a plain + // box when the area has no polygon. Only one area can be selected at + // a time (AreaFilter is single-select), so at most one outline shows. + (function () { + var areaColor = cssVar('--geo-filter-color') || '#3b82f6'; + var areaPolygonsCache = null; + async function fetchAreaPolygons() { + if (areaPolygonsCache) return areaPolygonsCache; + try { + areaPolygonsCache = await api('/config/areas/polygons', { ttl: 3600 }); + } catch (e) { + areaPolygonsCache = []; + } + return areaPolygonsCache; + } + function latLngsFor(entry) { + if (entry.polygon && entry.polygon.length >= 3) { + return entry.polygon.map(function (p) { return [p[0], p[1]]; }); + } + if (entry.latMin != null && entry.latMax != null && entry.lonMin != null && entry.lonMax != null) { + return [ + [entry.latMin, entry.lonMin], [entry.latMin, entry.lonMax], + [entry.latMax, entry.lonMax], [entry.latMax, entry.lonMin], + ]; + } + return null; + } + async function refresh() { + var label = document.getElementById('liveSelectedAreaLabel'); + var checkbox = document.getElementById('liveSelectedAreaToggle'); + if (selectedAreaLayer) { map.removeLayer(selectedAreaLayer); selectedAreaLayer = null; } + var key = AreaFilter.getSelected(); + if (!key) { + if (label) label.style.display = 'none'; + return; + } + var areas = await fetchAreaPolygons(); + var entry = areas.find(function (a) { return a.key === key; }); + var latlngs = entry && latLngsFor(entry); + if (!latlngs) { + if (label) label.style.display = 'none'; + return; + } + selectedAreaLayer = L.polygon(latlngs, { + color: areaColor, weight: 2, opacity: 0.8, dashArray: '2 4', + fillColor: areaColor, fillOpacity: 0.06 + }); + if (label) label.style.display = ''; + var saved = localStorage.getItem('meshcore-map-selected-area-outline') !== 'false'; + if (checkbox) checkbox.checked = saved; + if (saved) selectedAreaLayer.addTo(map); + } + var checkbox = document.getElementById('liveSelectedAreaToggle'); + if (checkbox) { + checkbox.addEventListener('change', function (e) { + localStorage.setItem('meshcore-map-selected-area-outline', e.target.checked); + if (!selectedAreaLayer) return; + if (e.target.checked) { selectedAreaLayer.addTo(map); } else { map.removeLayer(selectedAreaLayer); } + }); + } + AreaFilter.onChange(refresh); + refresh(); + })(); + const matrixToggle = document.getElementById('liveMatrixToggle'); matrixToggle.checked = matrixMode; matrixToggle.addEventListener('change', (e) => { @@ -4546,7 +4614,7 @@ } _navCleanup = null; } - nodesLayer = pathsLayer = animLayer = heatLayer = geoFilterLayer = clickablePathsLayer = null; + nodesLayer = pathsLayer = animLayer = heatLayer = geoFilterLayer = selectedAreaLayer = clickablePathsLayer = null; clickablePaths = []; stopMatrixRain(); // #1572 — clear body.live-fullscreen on route exit. The class hides diff --git a/public/map.js b/public/map.js index 0ff3f2b6..c1859a45 100644 --- a/public/map.js +++ b/public/map.js @@ -15,6 +15,7 @@ let wsHandler = null; let heatLayer = null; let geoFilterLayer = null; + let selectedAreaLayer = null; let affinityLayer = null; let affinityData = null; let userHasMoved = false; @@ -204,7 +205,8 @@ - + +
@@ -588,6 +590,73 @@ } catch (e) { /* no geo filter configured */ } })(); + // Selected area outline — distinct from the geo filter boundary above: + // this draws whichever area is currently picked via the "Area: X ▾" + // dropdown (AreaFilter, shared with Nodes/Live/Analytics), not + // necessarily the homeArea-linked geo_filter. Falls back to a plain + // box when the area has no polygon. Only one area can be selected at + // a time (AreaFilter is single-select), so at most one outline shows. + (function () { + var areaColor = getComputedStyle(document.documentElement).getPropertyValue('--geo-filter-color').trim() || '#3b82f6'; + var areaPolygonsCache = null; + async function fetchAreaPolygons() { + if (areaPolygonsCache) return areaPolygonsCache; + try { + areaPolygonsCache = await api('/config/areas/polygons', { ttl: 3600 }); + } catch (e) { + areaPolygonsCache = []; + } + return areaPolygonsCache; + } + function latLngsFor(entry) { + if (entry.polygon && entry.polygon.length >= 3) { + return entry.polygon.map(function (p) { return [p[0], p[1]]; }); + } + if (entry.latMin != null && entry.latMax != null && entry.lonMin != null && entry.lonMax != null) { + return [ + [entry.latMin, entry.lonMin], [entry.latMin, entry.lonMax], + [entry.latMax, entry.lonMax], [entry.latMax, entry.lonMin], + ]; + } + return null; + } + async function refresh() { + var label = document.getElementById('mcSelectedAreaLabel'); + var checkbox = document.getElementById('mcSelectedArea'); + if (selectedAreaLayer) { map.removeLayer(selectedAreaLayer); selectedAreaLayer = null; } + var key = AreaFilter.getSelected(); + if (!key) { + if (label) label.style.display = 'none'; + return; + } + var areas = await fetchAreaPolygons(); + var entry = areas.find(function (a) { return a.key === key; }); + var latlngs = entry && latLngsFor(entry); + if (!latlngs) { + if (label) label.style.display = 'none'; + return; + } + selectedAreaLayer = L.polygon(latlngs, { + color: areaColor, weight: 2, opacity: 0.8, dashArray: '2 4', + fillColor: areaColor, fillOpacity: 0.06 + }); + if (label) label.style.display = ''; + var saved = localStorage.getItem('meshcore-map-selected-area-outline') !== 'false'; + if (checkbox) checkbox.checked = saved; + if (saved) selectedAreaLayer.addTo(map); + } + var checkbox = document.getElementById('mcSelectedArea'); + if (checkbox) { + checkbox.addEventListener('change', function (e) { + localStorage.setItem('meshcore-map-selected-area-outline', e.target.checked); + if (!selectedAreaLayer) return; + if (e.target.checked) { selectedAreaLayer.addTo(map); } else { map.removeLayer(selectedAreaLayer); } + }); + } + AreaFilter.onChange(refresh); + refresh(); + })(); + // WS for live advert updates wsHandler = debouncedOnWS(function (msgs) { if (msgs.some(function (m) { return m.type === 'packet' && m.data?.decoded?.header?.payloadTypeName === 'ADVERT'; })) {