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:
efiten
2026-04-14 10:50:42 +02:00
parent 1881c92d6e
commit e92a2333f2
2 changed files with 171 additions and 48 deletions
+150 -26
View File
@@ -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');
});
+21 -22
View File
@@ -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.
&nbsp;·&nbsp; <a href="/geofilter-docs.html">Documentation</a>
&nbsp;·&nbsp; <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() {