Files
leetcrypt 34b08c4ad6 fix: show real capture date in map popup (timestamp field + invalid-date guard)
map.js read capture.captured_at, but the backend stores the field as
`timestamp`, so every marker popup rendered "Invalid Date". Fall back to
timestamp and guard against unparseable values. Regenerated README popup
screenshot to reflect the fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-18 18:16:49 -07:00

279 lines
8.3 KiB
JavaScript

// GigLez - Map Visualization
let map;
let markerLayer;
let markerClusterGroup;
let heatmapLayer;
let capturesData = [];
let heatmapEnabled = false;
// Frequency color mapping
const FREQUENCY_COLORS = {
315: '#10b981', // Green
433: '#3b82f6', // Blue
868: '#f59e0b', // Orange
915: '#ef4444', // Red
default: '#6b7280' // Gray
};
// Initialize map
document.addEventListener('DOMContentLoaded', () => {
initMap();
loadCaptures();
setupMapControls();
});
function initMap() {
// Create map centered on US
map = L.map('map').setView([39.8283, -98.5795], 4);
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}).addTo(map);
// Create marker cluster group
markerClusterGroup = L.markerClusterGroup({
maxClusterRadius: 50,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
zoomToBoundsOnClick: true
});
// Create regular marker layer
markerLayer = L.layerGroup();
// Add cluster group by default
map.addLayer(markerClusterGroup);
// Export map to window for access from other modules
window.map = map;
}
function setupMapControls() {
// Cluster toggle
document.getElementById('cluster-toggle').addEventListener('change', (e) => {
if (e.target.checked) {
map.removeLayer(markerLayer);
map.addLayer(markerClusterGroup);
} else {
map.removeLayer(markerClusterGroup);
map.addLayer(markerLayer);
}
renderMarkers();
});
// Frequency filter
document.getElementById('frequency-filter').addEventListener('change', () => {
if (heatmapEnabled) {
renderHeatmap();
} else {
renderMarkers();
}
});
// Heatmap toggle
document.getElementById('heatmap-toggle').addEventListener('change', (e) => {
heatmapEnabled = e.target.checked;
if (heatmapEnabled) {
// 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);
}
}
});
}
async function loadCaptures() {
try {
const response = await fetch('/api/v1/query/captures?limit=1000');
if (!response.ok) {
console.error('Failed to load captures');
return;
}
const data = await response.json();
capturesData = data.captures || [];
renderMarkers();
updateStats();
} catch (error) {
console.error('Error loading captures:', error);
}
}
function renderMarkers() {
// Clear existing markers
markerClusterGroup.clearLayers();
markerLayer.clearLayers();
// 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;
});
}
// Create markers
filtered.forEach(capture => {
const marker = createMarker(capture);
// Add to both layers (only one will be visible)
markerClusterGroup.addLayer(marker);
markerLayer.addLayer(marker);
});
// Update count
document.getElementById('total-captures').textContent = filtered.length;
}
function createMarker(capture) {
// Determine color based on frequency
const freqMHz = Math.round(capture.frequency / 1e6);
let color = FREQUENCY_COLORS.default;
for (const [freq, col] of Object.entries(FREQUENCY_COLORS)) {
if (freq === 'default') continue;
if (Math.abs(freqMHz - parseInt(freq)) < 50) {
color = col;
break;
}
}
// Create custom icon
const icon = L.divIcon({
className: 'custom-marker',
html: `<div style="background-color: ${color}; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white; box-shadow: 0 0 4px rgba(0,0,0,0.3);"></div>`,
iconSize: [12, 12],
iconAnchor: [6, 6]
});
// Create marker
const marker = L.marker([capture.latitude, capture.longitude], { icon });
// Create popup
const popupContent = createPopupContent(capture);
marker.bindPopup(popupContent);
return marker;
}
function createPopupContent(capture) {
const freqMHz = (capture.frequency / 1e6).toFixed(2);
const ts = capture.timestamp || capture.captured_at;
const parsed = ts ? new Date(ts) : null;
const date = parsed && !isNaN(parsed) ? parsed.toLocaleDateString() : 'Unknown';
let deviceInfo = 'Unknown Device';
if (capture.device_name) {
deviceInfo = `<strong>${capture.device_name}</strong>`;
if (capture.match_confidence) {
deviceInfo += ` (${(capture.match_confidence * 100).toFixed(0)}% confidence)`;
}
}
return `
<div class="marker-popup">
<h4>${deviceInfo}</h4>
<p><strong>Frequency:</strong> ${freqMHz} MHz</p>
<p><strong>Protocol:</strong> ${capture.protocol || 'RAW'}</p>
<p><strong>Captured:</strong> ${date}</p>
<p><strong>Location:</strong> ${capture.latitude.toFixed(6)}, ${capture.longitude.toFixed(6)}</p>
${capture.accuracy ? `<p><strong>Accuracy:</strong> ±${capture.accuracy.toFixed(1)}m</p>` : ''}
<button onclick="viewCaptureDetails(${capture.id})" class="btn btn-primary" style="margin-top: 0.5rem;">
View Details
</button>
</div>
`;
}
function updateStats() {
// Total captures
document.getElementById('total-captures').textContent = capturesData.length;
// Unique devices
const uniqueDevices = new Set(
capturesData
.filter(c => c.device_name)
.map(c => c.device_name)
).size;
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
window.reloadMapData = loadCaptures;