mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 22:31:37 +00:00
feat: geofilter map modal + light tile theme (#669)
Clicking the small inline map in the customizer GeoFilter tab now opens a full-screen modal (92vw × 86vh) with Undo/Clear/Done/Cancel controls. The inline map becomes a read-only preview. Both maps and the standalone geofilter-builder.html now use CartoDB Positron (light) instead of dark. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+150
-26
@@ -786,6 +786,8 @@
|
||||
|
||||
// GeoFilter tab state
|
||||
var _gfMap = null;
|
||||
var _gfModalMap = null;
|
||||
var _gfWriteEnabled = false;
|
||||
var _gfPoints = [];
|
||||
var _gfMarkers = [];
|
||||
var _gfPolygon = null;
|
||||
@@ -1172,15 +1174,15 @@
|
||||
return '<div class="cust-panel' + (_activeTab === 'geofilter' ? ' active' : '') + '" data-panel="geofilter">' +
|
||||
'<p class="cust-section-title">Geographic Filter</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:12px">Shows the active geographic filter. Nodes outside this area are excluded at ingest time and in API responses.</p>' +
|
||||
'<div id="cv2-gf-map" style="height:220px;border-radius:6px;border:1px solid var(--border);margin-bottom:8px;background:var(--surface-1)"></div>' +
|
||||
'<div style="position:relative;margin-bottom:8px">' +
|
||||
'<div id="cv2-gf-map" style="height:200px;border-radius:6px;border:1px solid var(--border);background:var(--surface-1);cursor:pointer"></div>' +
|
||||
'<div style="position:absolute;top:7px;right:7px;background:rgba(255,255,255,0.88);border-radius:4px;padding:3px 8px;font-size:11px;color:#444;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,0.15)">🔍 click to expand</div>' +
|
||||
'</div>' +
|
||||
'<div id="cv2-gf-status" style="font-size:12px;color:var(--text-muted);margin-bottom:10px">Loading current filter…</div>' +
|
||||
// Edit controls — hidden until server confirms write access (writeEnabled=true)
|
||||
'<div id="cv2-gf-edit" style="display:none">' +
|
||||
'<p style="font-size:11px;color:var(--text-muted);margin-bottom:8px">Click the map to add polygon points. Need at least 3 points.</p>' +
|
||||
'<div style="display:flex;gap:8px;margin-bottom:10px;align-items:center">' +
|
||||
'<button id="cv2-gf-undo" style="padding:5px 10px;background:var(--surface-1);color:var(--text-muted);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:12px">↩ Undo</button>' +
|
||||
'<button id="cv2-gf-clear-pts" style="padding:5px 10px;background:var(--surface-1);color:var(--text-muted);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:12px">✕ Clear</button>' +
|
||||
'<label style="font-size:12px;color:var(--text-muted);margin-left:auto">Buffer km:</label>' +
|
||||
'<label style="font-size:12px;color:var(--text-muted)">Buffer km:</label>' +
|
||||
'<input type="number" id="cv2-gf-buffer" value="20" min="0" max="500" style="width:64px;padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--input-bg);color:var(--text);font-size:12px">' +
|
||||
'</div>' +
|
||||
'<div class="cust-field"><label>Server API Key</label>' +
|
||||
@@ -1195,6 +1197,142 @@
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function _gfOpenModal(container) {
|
||||
var existing = document.getElementById('cv2-gf-modal-overlay');
|
||||
if (existing) existing.remove();
|
||||
if (_gfModalMap) { _gfModalMap.remove(); _gfModalMap = null; }
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'cv2-gf-modal-overlay';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.65);z-index:99999;display:flex;align-items:center;justify-content:center;';
|
||||
|
||||
var dialog = document.createElement('div');
|
||||
dialog.style.cssText = 'width:92vw;height:86vh;background:#fff;border-radius:10px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,0.4);';
|
||||
|
||||
var toolbarEl = document.createElement('div');
|
||||
toolbarEl.style.cssText = 'padding:10px 14px;display:flex;gap:8px;align-items:center;border-bottom:1px solid #e0e0e0;background:#f5f5f5;flex-shrink:0;';
|
||||
var title = document.createElement('span');
|
||||
title.style.cssText = 'font-weight:600;color:#333;font-size:14px;';
|
||||
title.textContent = _gfWriteEnabled ? 'Edit GeoFilter — click map to add points' : 'GeoFilter — read only';
|
||||
toolbarEl.appendChild(title);
|
||||
|
||||
if (_gfWriteEnabled) {
|
||||
var undoBtn = document.createElement('button');
|
||||
undoBtn.id = 'cv2-gfm-undo';
|
||||
undoBtn.textContent = '↩ Undo';
|
||||
undoBtn.style.cssText = 'padding:5px 10px;background:#eee;color:#555;border:1px solid #ccc;border-radius:6px;cursor:pointer;font-size:12px;';
|
||||
var clearBtn = document.createElement('button');
|
||||
clearBtn.id = 'cv2-gfm-clear';
|
||||
clearBtn.textContent = '✕ Clear';
|
||||
clearBtn.style.cssText = 'padding:5px 10px;background:#fee;color:#c44;border:1px solid #fcc;border-radius:6px;cursor:pointer;font-size:12px;';
|
||||
var countEl = document.createElement('span');
|
||||
countEl.id = 'cv2-gfm-count';
|
||||
countEl.style.cssText = 'font-size:12px;color:#888;';
|
||||
var spacer = document.createElement('span');
|
||||
spacer.style.cssText = 'flex:1;';
|
||||
var doneBtn = document.createElement('button');
|
||||
doneBtn.id = 'cv2-gfm-done';
|
||||
doneBtn.textContent = 'Done';
|
||||
doneBtn.style.cssText = 'padding:7px 18px;background:#4a9eff;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;';
|
||||
toolbarEl.appendChild(undoBtn);
|
||||
toolbarEl.appendChild(clearBtn);
|
||||
toolbarEl.appendChild(countEl);
|
||||
toolbarEl.appendChild(spacer);
|
||||
toolbarEl.appendChild(doneBtn);
|
||||
} else {
|
||||
var spacer2 = document.createElement('span');
|
||||
spacer2.style.cssText = 'flex:1;';
|
||||
toolbarEl.appendChild(spacer2);
|
||||
}
|
||||
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.id = 'cv2-gfm-close';
|
||||
closeBtn.textContent = _gfWriteEnabled ? 'Cancel' : 'Close';
|
||||
closeBtn.style.cssText = 'padding:7px 14px;background:#eee;color:#555;border:1px solid #ccc;border-radius:6px;cursor:pointer;font-size:13px;';
|
||||
toolbarEl.appendChild(closeBtn);
|
||||
|
||||
var mapDiv = document.createElement('div');
|
||||
mapDiv.id = 'cv2-gf-modal-map';
|
||||
mapDiv.style.cssText = 'flex:1;';
|
||||
|
||||
dialog.appendChild(toolbarEl);
|
||||
dialog.appendChild(mapDiv);
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
var modalPoints = _gfPoints.map(function (p) { return [p[0], p[1]]; });
|
||||
var modalMarkers = [];
|
||||
var modalPolygon = null;
|
||||
var modalClosingLine = null;
|
||||
|
||||
_gfModalMap = L.map(mapDiv, { zoomControl: true });
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap © CartoDB', maxZoom: 19
|
||||
}).addTo(_gfModalMap);
|
||||
|
||||
function renderModal() {
|
||||
if (modalPolygon) { _gfModalMap.removeLayer(modalPolygon); modalPolygon = null; }
|
||||
if (modalClosingLine) { _gfModalMap.removeLayer(modalClosingLine); modalClosingLine = null; }
|
||||
modalMarkers.forEach(function (m) { _gfModalMap.removeLayer(m); });
|
||||
modalMarkers = [];
|
||||
modalPoints.forEach(function (pt, i) {
|
||||
var m = L.circleMarker(pt, { radius: 6, color: '#4a9eff', weight: 2, fillColor: '#4a9eff', fillOpacity: 0.9 })
|
||||
.addTo(_gfModalMap)
|
||||
.bindTooltip(String(i + 1), { permanent: true, direction: 'top', offset: [0, -8] });
|
||||
modalMarkers.push(m);
|
||||
});
|
||||
if (modalPoints.length >= 3) {
|
||||
modalPolygon = L.polygon(modalPoints, { color: '#4a9eff', weight: 2, fillColor: '#4a9eff', fillOpacity: 0.12 }).addTo(_gfModalMap);
|
||||
} else if (modalPoints.length === 2) {
|
||||
modalClosingLine = L.polyline(modalPoints, { color: '#4a9eff', weight: 2, dashArray: '5,5' }).addTo(_gfModalMap);
|
||||
}
|
||||
var ce = document.getElementById('cv2-gfm-count');
|
||||
if (ce) ce.textContent = modalPoints.length + ' point' + (modalPoints.length !== 1 ? 's' : '');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (_gfModalMap) { _gfModalMap.remove(); _gfModalMap = null; }
|
||||
overlay.remove();
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
_gfModalMap.invalidateSize();
|
||||
renderModal();
|
||||
if (modalPoints.length >= 3) {
|
||||
_gfModalMap.fitBounds(L.latLngBounds(modalPoints), { padding: [40, 40] });
|
||||
} else {
|
||||
_gfModalMap.setView([50.5, 4.4], 5);
|
||||
}
|
||||
}, 80);
|
||||
|
||||
if (_gfWriteEnabled) {
|
||||
_gfModalMap.on('click', function (e) {
|
||||
modalPoints.push([parseFloat(e.latlng.lat.toFixed(6)), parseFloat(e.latlng.lng.toFixed(6))]);
|
||||
renderModal();
|
||||
});
|
||||
document.getElementById('cv2-gfm-undo').addEventListener('click', function () {
|
||||
if (!modalPoints.length) return;
|
||||
modalPoints.pop();
|
||||
renderModal();
|
||||
});
|
||||
document.getElementById('cv2-gfm-clear').addEventListener('click', function () {
|
||||
modalPoints = [];
|
||||
renderModal();
|
||||
});
|
||||
document.getElementById('cv2-gfm-done').addEventListener('click', function () {
|
||||
_gfPoints = modalPoints;
|
||||
_gfRender();
|
||||
var prune = container.querySelector('#cv2-gf-prune-section');
|
||||
if (prune) prune.style.display = _gfPoints.length >= 3 ? '' : 'none';
|
||||
_gfStatus(container, _gfPoints.length + ' point' + (_gfPoints.length !== 1 ? 's' : '') + '.');
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); });
|
||||
}
|
||||
|
||||
function _gfRender() {
|
||||
if (!_gfMap) return;
|
||||
if (_gfPolygon) { _gfMap.removeLayer(_gfPolygon); _gfPolygon = null; }
|
||||
@@ -1266,8 +1404,8 @@
|
||||
var mapEl = container.querySelector('#cv2-gf-map');
|
||||
if (!mapEl || typeof L === 'undefined') return;
|
||||
|
||||
_gfMap = L.map(mapEl, { zoomControl: true });
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
_gfMap = L.map(mapEl, { zoomControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, touchZoom: false });
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap © CartoDB', maxZoom: 19
|
||||
}).addTo(_gfMap);
|
||||
|
||||
@@ -1275,6 +1413,7 @@
|
||||
api('/config/geo-filter', { ttl: 0 }).then(function (gf) {
|
||||
// Show edit controls only on servers that have a write-capable API key configured
|
||||
if (gf && gf.writeEnabled) {
|
||||
_gfWriteEnabled = true;
|
||||
var editEl = container.querySelector('#cv2-gf-edit');
|
||||
if (editEl) editEl.style.display = '';
|
||||
}
|
||||
@@ -1287,7 +1426,7 @@
|
||||
_gfStatus(container, gf.polygon.length + ' points · bufferKm=' + (gf.bufferKm || 0));
|
||||
} else {
|
||||
_gfPoints = [];
|
||||
_gfStatus(container, gf && gf.writeEnabled ? 'No geo filter. Click the map to draw a polygon.' : 'No geo filter configured.');
|
||||
_gfStatus(container, gf && gf.writeEnabled ? 'No geo filter. Click the map to open the editor.' : 'No geo filter configured.');
|
||||
_gfMap.setView([50.5, 4.4], 5);
|
||||
}
|
||||
_gfLoaded = true;
|
||||
@@ -1311,23 +1450,8 @@
|
||||
setTimeout(function () { if (_gfMap) _gfMap.invalidateSize(); }, 100);
|
||||
}
|
||||
|
||||
_gfMap.on('click', function (e) {
|
||||
_gfPoints.push([parseFloat(e.latlng.lat.toFixed(6)), parseFloat(e.latlng.lng.toFixed(6))]);
|
||||
_gfRender();
|
||||
_gfStatus(container, _gfPoints.length + ' point' + (_gfPoints.length !== 1 ? 's' : '') + '.');
|
||||
});
|
||||
_gfMap.on('click', function () { _gfOpenModal(container); });
|
||||
|
||||
container.querySelector('#cv2-gf-undo').addEventListener('click', function () {
|
||||
if (!_gfPoints.length) return;
|
||||
_gfPoints.pop();
|
||||
_gfRender();
|
||||
_gfStatus(container, _gfPoints.length + ' point' + (_gfPoints.length !== 1 ? 's' : '') + '.');
|
||||
});
|
||||
container.querySelector('#cv2-gf-clear-pts').addEventListener('click', function () {
|
||||
_gfPoints = [];
|
||||
_gfRender();
|
||||
_gfStatus(container, 'Cleared. Click the map to draw a polygon.');
|
||||
});
|
||||
container.querySelector('#cv2-gf-save').addEventListener('click', function () { _gfSave(container); });
|
||||
container.querySelector('#cv2-gf-remove').addEventListener('click', function () { _gfRemove(container); });
|
||||
}
|
||||
@@ -1435,7 +1559,7 @@
|
||||
// Tab switching
|
||||
container.querySelectorAll('.cust-tab').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
if (_gfMap) { _gfMap.remove(); _gfMap = null; _gfMarkers = []; _gfPolygon = null; _gfClosingLine = null; }
|
||||
if (_gfMap) { _gfMap.remove(); _gfMap = null; _gfMarkers = []; _gfPolygon = null; _gfClosingLine = null; } if (_gfModalMap) { _gfModalMap.remove(); _gfModalMap = null; } var _ov = document.getElementById('cv2-gf-modal-overlay'); if (_ov) _ov.remove();
|
||||
_activeTab = btn.dataset.tab;
|
||||
_renderPanel(container);
|
||||
});
|
||||
@@ -1709,7 +1833,7 @@
|
||||
document.body.appendChild(_panelEl);
|
||||
|
||||
_panelEl.querySelector('.cust-close').addEventListener('click', function () {
|
||||
if (_gfMap) { _gfMap.remove(); _gfMap = null; _gfMarkers = []; _gfPolygon = null; _gfClosingLine = null; }
|
||||
if (_gfMap) { _gfMap.remove(); _gfMap = null; _gfMarkers = []; _gfPolygon = null; _gfClosingLine = null; } if (_gfModalMap) { _gfModalMap.remove(); _gfModalMap = null; } var _ov = document.getElementById('cv2-gf-modal-overlay'); if (_ov) _ov.remove();
|
||||
_panelEl.classList.add('hidden');
|
||||
});
|
||||
|
||||
|
||||
@@ -8,32 +8,32 @@
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
|
||||
header { padding: 12px 16px; background: #0f0f23; border-bottom: 1px solid #333; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
||||
header h1 { font-size: 1rem; font-weight: 600; color: #4a9eff; white-space: nowrap; }
|
||||
body { font-family: system-ui, sans-serif; background: #f5f5f5; color: #222; height: 100vh; display: flex; flex-direction: column; }
|
||||
header { padding: 12px 16px; background: #fff; border-bottom: 1px solid #ddd; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
||||
header h1 { font-size: 1rem; font-weight: 600; color: #1a6abf; white-space: nowrap; }
|
||||
.controls { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
button { padding: 6px 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 500; }
|
||||
#btnUndo { background: #333; color: #ccc; }
|
||||
#btnClear { background: #5a2020; color: #ffaaaa; }
|
||||
#btnUndo:hover { background: #444; }
|
||||
#btnClear:hover { background: #7a2020; }
|
||||
#btnUndo { background: #eee; color: #555; border: 1px solid #ccc; }
|
||||
#btnClear { background: #fee; color: #c44; border: 1px solid #fcc; }
|
||||
#btnUndo:hover { background: #e0e0e0; }
|
||||
#btnClear:hover { background: #fdd; }
|
||||
.hint { font-size: 0.8rem; color: #888; margin-left: auto; }
|
||||
#map { flex: 1; }
|
||||
#output-panel { background: #0f0f23; border-top: 1px solid #333; padding: 12px 16px; display: flex; gap: 12px; align-items: flex-start; }
|
||||
#output-panel { background: #fff; border-top: 1px solid #ddd; padding: 12px 16px; display: flex; gap: 12px; align-items: flex-start; }
|
||||
#output-panel label { font-size: 0.75rem; color: #888; white-space: nowrap; padding-top: 6px; }
|
||||
#output { flex: 1; background: #111; border: 1px solid #333; border-radius: 6px; padding: 10px 12px; font-family: monospace; font-size: 0.78rem; color: #7ec8e3; white-space: pre; overflow-x: auto; min-height: 54px; max-height: 140px; overflow-y: auto; cursor: text; }
|
||||
#output.empty { color: #555; font-style: italic; }
|
||||
#btnCopy { padding: 6px 14px; background: #1a4a7a; color: #7ec8e3; border-radius: 6px; border: none; cursor: pointer; font-size: 0.85rem; white-space: nowrap; align-self: flex-end; }
|
||||
#btnCopy:hover { background: #2a6aaa; }
|
||||
#btnCopy.copied { background: #1a6a3a; color: #7effa0; }
|
||||
#output { flex: 1; background: #f0f4f8; border: 1px solid #ccd; border-radius: 6px; padding: 10px 12px; font-family: monospace; font-size: 0.78rem; color: #1a6abf; white-space: pre; overflow-x: auto; min-height: 54px; max-height: 140px; overflow-y: auto; cursor: text; }
|
||||
#output.empty { color: #aaa; font-style: italic; }
|
||||
#btnCopy { padding: 6px 14px; background: #1a6abf; color: #fff; border-radius: 6px; border: none; cursor: pointer; font-size: 0.85rem; white-space: nowrap; align-self: flex-end; }
|
||||
#btnCopy:hover { background: #1558a0; }
|
||||
#btnCopy.copied { background: #1a7a3a; color: #fff; }
|
||||
#counter { font-size: 0.8rem; color: #888; padding-top: 6px; white-space: nowrap; }
|
||||
.bufferRow { display: flex; align-items: center; gap: 8px; }
|
||||
.bufferRow label { font-size: 0.85rem; color: #aaa; }
|
||||
.bufferRow input { width: 60px; padding: 5px 8px; background: #222; border: 1px solid #444; border-radius: 6px; color: #eee; font-size: 0.85rem; }
|
||||
#help-bar { background: #0f0f23; padding: 6px 16px; font-size: 0.75rem; color: #666; border-top: 1px solid #222; }
|
||||
#help-bar a { color: #4a9eff; text-decoration: none; }
|
||||
.bufferRow label { font-size: 0.85rem; color: #555; }
|
||||
.bufferRow input { width: 60px; padding: 5px 8px; background: #fff; border: 1px solid #ccc; border-radius: 6px; color: #222; font-size: 0.85rem; }
|
||||
#help-bar { background: #fff; padding: 6px 16px; font-size: 0.75rem; color: #888; border-top: 1px solid #e0e0e0; }
|
||||
#help-bar a { color: #1a6abf; text-decoration: none; }
|
||||
#help-bar a:hover { text-decoration: underline; }
|
||||
#back-link { font-size: 0.8rem; color: #4a9eff; text-decoration: none; white-space: nowrap; }
|
||||
#back-link { font-size: 0.8rem; color: #1a6abf; text-decoration: none; white-space: nowrap; }
|
||||
#back-link:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -70,13 +70,13 @@
|
||||
<div id="help-bar">
|
||||
Copy the JSON above → paste as a top-level key in <code>config.json</code> → restart the server.
|
||||
Nodes with no GPS fix always pass through. Remove the <code>geo_filter</code> block to disable filtering.
|
||||
· <a href="/geofilter-docs.html">Documentation</a>
|
||||
· <a href="https://github.com/Kpa-clawbot/CoreScope/blob/master/docs/user-guide/geofilter.md" target="_blank">Documentation ↗</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const map = L.map('map').setView([50.5, 4.4], 8);
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© OpenStreetMap © CartoDB',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
@@ -87,8 +87,7 @@ let polygon = null;
|
||||
let closingLine = null;
|
||||
|
||||
function latLonPair(latlng) {
|
||||
const w = latlng.wrap();
|
||||
return [parseFloat(w.lat.toFixed(6)), parseFloat(w.lng.toFixed(6))];
|
||||
return [parseFloat(latlng.lat.toFixed(6)), parseFloat(latlng.lng.toFixed(6))];
|
||||
}
|
||||
|
||||
function render() {
|
||||
|
||||
Reference in New Issue
Block a user