feat: Implement heatmap visualization for device density
- Add Leaflet.heat plugin (leaflet-heat.js) - Create renderHeatmap() function with confidence-based intensity - Toggle between markers and heatmap view - Support frequency filtering in heatmap mode - Custom gradient: blue -> cyan -> lime -> yellow -> red - Configurable radius (25px), blur (15px), and maxZoom (17) - Console logging for debugging heatmap rendering - Intensity based on device match confidence (0.5 default)
This commit is contained in:
+77
-4
@@ -3,7 +3,9 @@
|
|||||||
let map;
|
let map;
|
||||||
let markerLayer;
|
let markerLayer;
|
||||||
let markerClusterGroup;
|
let markerClusterGroup;
|
||||||
|
let heatmapLayer;
|
||||||
let capturesData = [];
|
let capturesData = [];
|
||||||
|
let heatmapEnabled = false;
|
||||||
|
|
||||||
// Frequency color mapping
|
// Frequency color mapping
|
||||||
const FREQUENCY_COLORS = {
|
const FREQUENCY_COLORS = {
|
||||||
@@ -64,14 +66,33 @@ function setupMapControls() {
|
|||||||
|
|
||||||
// Frequency filter
|
// Frequency filter
|
||||||
document.getElementById('frequency-filter').addEventListener('change', () => {
|
document.getElementById('frequency-filter').addEventListener('change', () => {
|
||||||
|
if (heatmapEnabled) {
|
||||||
|
renderHeatmap();
|
||||||
|
} else {
|
||||||
renderMarkers();
|
renderMarkers();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Heatmap toggle (placeholder)
|
// Heatmap toggle
|
||||||
document.getElementById('heatmap-toggle').addEventListener('change', (e) => {
|
document.getElementById('heatmap-toggle').addEventListener('change', (e) => {
|
||||||
if (e.target.checked) {
|
heatmapEnabled = e.target.checked;
|
||||||
alert('Heatmap view coming soon!');
|
if (heatmapEnabled) {
|
||||||
e.target.checked = false;
|
// Hide markers, show heatmap
|
||||||
|
map.removeLayer(markerClusterGroup);
|
||||||
|
map.removeLayer(markerLayer);
|
||||||
|
renderHeatmap();
|
||||||
|
} else {
|
||||||
|
// Hide heatmap, show markers
|
||||||
|
if (heatmapLayer) {
|
||||||
|
map.removeLayer(heatmapLayer);
|
||||||
|
}
|
||||||
|
// Re-add appropriate marker layer based on cluster toggle
|
||||||
|
const clusterEnabled = document.getElementById('cluster-toggle').checked;
|
||||||
|
if (clusterEnabled) {
|
||||||
|
map.addLayer(markerClusterGroup);
|
||||||
|
} else {
|
||||||
|
map.addLayer(markerLayer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -199,5 +220,57 @@ function updateStats() {
|
|||||||
document.getElementById('unique-devices').textContent = uniqueDevices;
|
document.getElementById('unique-devices').textContent = uniqueDevices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderHeatmap() {
|
||||||
|
// Remove existing heatmap layer if it exists
|
||||||
|
if (heatmapLayer) {
|
||||||
|
map.removeLayer(heatmapLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get frequency filter
|
||||||
|
const frequencyFilter = document.getElementById('frequency-filter').value;
|
||||||
|
|
||||||
|
// Filter captures
|
||||||
|
let filtered = capturesData;
|
||||||
|
|
||||||
|
if (frequencyFilter) {
|
||||||
|
const targetFreq = parseInt(frequencyFilter) * 1e6;
|
||||||
|
filtered = capturesData.filter(c => {
|
||||||
|
const freq = c.frequency / 1e6;
|
||||||
|
return Math.abs(freq - parseInt(frequencyFilter)) < 50;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert captures to heatmap data format
|
||||||
|
// Format: [[lat, lng, intensity], ...]
|
||||||
|
const heatmapData = filtered.map(capture => {
|
||||||
|
// Use match confidence as intensity (default to 0.5 if no match)
|
||||||
|
const intensity = capture.match_confidence || 0.5;
|
||||||
|
return [capture.latitude, capture.longitude, intensity];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Rendering heatmap with ${heatmapData.length} points`);
|
||||||
|
|
||||||
|
// Create heatmap layer
|
||||||
|
heatmapLayer = L.heatLayer(heatmapData, {
|
||||||
|
radius: 25, // Radius of each point
|
||||||
|
blur: 15, // Amount of blur
|
||||||
|
maxZoom: 17, // Zoom level where max intensity is reached
|
||||||
|
max: 1.0, // Maximum point intensity
|
||||||
|
gradient: { // Custom color gradient
|
||||||
|
0.0: 'blue',
|
||||||
|
0.3: 'cyan',
|
||||||
|
0.5: 'lime',
|
||||||
|
0.7: 'yellow',
|
||||||
|
1.0: 'red'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to map
|
||||||
|
map.addLayer(heatmapLayer);
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
document.getElementById('total-captures').textContent = filtered.length;
|
||||||
|
}
|
||||||
|
|
||||||
// Export for external use
|
// Export for external use
|
||||||
window.reloadMapData = loadCaptures;
|
window.reloadMapData = loadCaptures;
|
||||||
|
|||||||
@@ -280,6 +280,7 @@
|
|||||||
<!-- Leaflet JS -->
|
<!-- Leaflet JS -->
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||||||
|
|
||||||
<!-- Chart.js -->
|
<!-- Chart.js -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user