Shows the active geographic filter. Nodes outside this area are excluded at ingest time and in API responses.
' +
// Edit controls — hidden until server confirms write access (writeEnabled=true)
'
' +
- '
Click the map to add polygon points. Need at least 3 points.
' +
'
' +
- '↩ Undo ' +
- '✕ Clear ' +
- 'Buffer km: ' +
+ 'Buffer km: ' +
' ' +
'
' +
'
Server API Key ' +
@@ -1195,6 +1197,142 @@
'
';
}
+ 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');
});
diff --git a/public/geofilter-builder.html b/public/geofilter-builder.html
index 1aa18ac7..a67dd8e0 100644
--- a/public/geofilter-builder.html
+++ b/public/geofilter-builder.html
@@ -8,32 +8,32 @@
@@ -70,13 +70,13 @@
Copy the JSON above → paste as a top-level key in
config.json → restart the server.
Nodes with no GPS fix always pass through. Remove the
geo_filter block to disable filtering.
- ·
Documentation
+ ·
Documentation ↗