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.
This commit is contained in:
dborup
2026-07-22 18:44:59 +02:00
parent af2c105337
commit 4d46dba4b2
2 changed files with 141 additions and 4 deletions
+71 -3
View File
@@ -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 @@
<span id="favDesc" class="sr-only">Show only favorited and claimed nodes</span>
<label><input type="checkbox" id="liveMultibyteToggle" aria-describedby="multibyteDesc"> Multibyte only</label>
<span id="multibyteDesc" class="sr-only">Show only multibyte (≥2-byte path-hash) packets; hide unreliable single-byte traffic</span>
<label id="liveGeoFilterLabel" style="display:none"><input type="checkbox" id="liveGeoFilterToggle"> Mesh live area</label>
<label id="liveGeoFilterLabel" style="display:none"><input type="checkbox" id="liveGeoFilterToggle"> Geo filter boundary</label>
<label id="liveSelectedAreaLabel" style="display:none"><input type="checkbox" id="liveSelectedAreaToggle"> Selected area outline</label>
</div>
<div class="live-toggles">
<div class="live-node-filter-wrap" style="position:relative">
@@ -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
+70 -1
View File
@@ -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 @@
<label for="mcHeatmap"><input type="checkbox" id="mcHeatmap"> Heat map</label>
<label for="mcHashLabels"><input type="checkbox" id="mcHashLabels"> Hash prefix labels</label>
<label for="mcMultiByte"><input type="checkbox" id="mcMultiByte"> Multi-byte support</label>
<label id="mcGeoFilterLabel" for="mcGeoFilter" style="display:none"><input type="checkbox" id="mcGeoFilter"> Mesh live area</label>
<label id="mcGeoFilterLabel" for="mcGeoFilter" style="display:none"><input type="checkbox" id="mcGeoFilter"> Geo filter boundary</label>
<label id="mcSelectedAreaLabel" for="mcSelectedArea" style="display:none"><input type="checkbox" id="mcSelectedArea"> Selected area outline</label>
</fieldset>
<div id="mapAreaFilter"></div>
<fieldset class="mc-section">
@@ -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'; })) {