Add Leaflet.heat crowd density heatmap to GSM Spy dashboard

Adds a toggleable heatmap layer that visualizes crowd density data from
the existing /gsm_spy/crowd_density endpoint as a gradient overlay on the
map, with auto-refresh every 30s during active monitoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-08 20:55:06 +00:00
parent 7409e77364
commit d92b9c220a
4 changed files with 120 additions and 1 deletions
+92
View File
@@ -18,6 +18,12 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
{% endif %}
<!-- Leaflet.heat plugin - Conditional CDN/Local loading -->
{% if offline_settings.assets_source == 'local' %}
<script src="{{ url_for('static', filename='vendor/leaflet-heat/leaflet-heat.js') }}"></script>
{% else %}
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
{% endif %}
<!-- Core CSS variables -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
@@ -1450,6 +1456,9 @@
<span id="trackingStatus">STANDBY</span>
</div>
<div class="strip-time" id="utcTime">--:--:-- UTC</div>
<button class="strip-btn" id="heatmapToggle" onclick="toggleHeatmap()">
Heatmap: OFF
</button>
<button class="strip-btn" onclick="openAnalyticsModal()">
Analytics Overview
</button>
@@ -1805,6 +1814,11 @@
let isScanning = false;
let currentRegion = 'americas';
// Heatmap state
let heatmapLayer = null;
let heatmapVisible = false;
let heatmapLastRefresh = 0;
// Statistics
let stats = {
totalTowers: 0,
@@ -2460,6 +2474,11 @@
stats.totalDevices = devicesCount;
}
updateStatsDisplay();
// Auto-refresh heatmap every 30s while visible
if (heatmapVisible && (Date.now() - heatmapLastRefresh > 30000)) {
updateHeatmap();
}
}
function hideMonitorStatus() {
@@ -3135,6 +3154,79 @@
}
}
// ============================================
// HEATMAP
// ============================================
async function toggleHeatmap() {
if (heatmapVisible) {
removeHeatmap();
} else {
await updateHeatmap();
heatmapVisible = true;
const btn = document.getElementById('heatmapToggle');
btn.textContent = 'Heatmap: ON';
btn.style.borderColor = 'var(--accent-cyan)';
btn.style.color = 'var(--accent-cyan)';
}
}
async function updateHeatmap() {
try {
const response = await fetch('/gsm_spy/crowd_density?hours=1');
const data = await response.json();
if (!data || data.length === 0) return;
const points = [];
let maxIntensity = 1;
data.forEach(item => {
// Match CID to tower coordinates
const cid = item.cid;
for (const key in towers) {
const t = towers[key];
if (t.cid === cid && t.lat && t.lon) {
const intensity = item.unique_devices || 1;
if (intensity > maxIntensity) maxIntensity = intensity;
points.push([t.lat, t.lon, intensity]);
break;
}
}
});
if (points.length === 0) return;
// Remove existing layer before adding new one
if (heatmapLayer && gsmMap.hasLayer(heatmapLayer)) {
gsmMap.removeLayer(heatmapLayer);
}
heatmapLayer = L.heatLayer(points, {
radius: 35,
blur: 25,
maxZoom: 17,
max: maxIntensity,
gradient: {0.2: '#304ffe', 0.4: '#00bcd4', 0.6: '#76ff03', 0.8: '#ffea00', 1.0: '#ff1744'}
}).addTo(gsmMap);
heatmapLastRefresh = Date.now();
} catch (error) {
console.error('[GSM SPY] Heatmap update error:', error);
}
}
function removeHeatmap() {
if (heatmapLayer && gsmMap.hasLayer(heatmapLayer)) {
gsmMap.removeLayer(heatmapLayer);
}
heatmapLayer = null;
heatmapVisible = false;
const btn = document.getElementById('heatmapToggle');
btn.textContent = 'Heatmap: OFF';
btn.style.borderColor = '';
btn.style.color = '';
}
</script>
<!-- Settings Manager -->