177 lines
4.8 KiB
JavaScript
177 lines
4.8 KiB
JavaScript
/**
|
|
* Heatmap visualization module
|
|
* Renders user activity heatmap using Leaflet.heat
|
|
*/
|
|
|
|
let heatmapMap = null;
|
|
let heatLayer = null;
|
|
|
|
/**
|
|
* Initialize the heatmap on page load
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
// Check authentication
|
|
if (!FitPubAuth.isAuthenticated()) {
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
|
|
await loadHeatmap();
|
|
});
|
|
|
|
/**
|
|
* Load and render the heatmap
|
|
*/
|
|
async function loadHeatmap() {
|
|
const loadingIndicator = document.getElementById('loadingIndicator');
|
|
const errorAlert = document.getElementById('errorAlert');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
const emptyState = document.getElementById('emptyState');
|
|
const heatmapContainer = document.getElementById('heatmapContainer');
|
|
const statsCard = document.getElementById('statsCard');
|
|
const legend = document.getElementById('legend');
|
|
|
|
// Show loading
|
|
loadingIndicator.style.display = 'block';
|
|
errorAlert.classList.add('d-none');
|
|
emptyState.classList.add('d-none');
|
|
heatmapContainer.style.display = 'none';
|
|
statsCard.style.display = 'none';
|
|
legend.style.display = 'none';
|
|
|
|
try {
|
|
// Fetch heatmap data
|
|
const response = await FitPubAuth.authenticatedFetch('/api/heatmap/me');
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load heatmap data');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Hide loading
|
|
loadingIndicator.style.display = 'none';
|
|
|
|
// Check if user has any data
|
|
if (!data.features || data.features.length === 0) {
|
|
emptyState.classList.remove('d-none');
|
|
return;
|
|
}
|
|
|
|
// Show map and stats
|
|
heatmapContainer.style.display = 'block';
|
|
statsCard.style.display = 'block';
|
|
legend.style.display = 'block';
|
|
|
|
// Update stats
|
|
document.getElementById('cellCount').textContent = data.features.length.toLocaleString();
|
|
document.getElementById('maxIntensity').textContent = data.maxIntensity.toLocaleString();
|
|
|
|
// Initialize map
|
|
initializeMap();
|
|
|
|
// Render heatmap
|
|
renderHeatmap(data);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading heatmap:', error);
|
|
loadingIndicator.style.display = 'none';
|
|
errorAlert.classList.remove('d-none');
|
|
errorMessage.textContent = 'Failed to load heatmap. Please try again later.';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the Leaflet map
|
|
*/
|
|
function initializeMap() {
|
|
if (heatmapMap) {
|
|
return; // Already initialized
|
|
}
|
|
|
|
// Create map centered on world
|
|
heatmapMap = L.map('heatmapContainer').setView([20, 0], 2);
|
|
|
|
// Add OpenStreetMap tile layer
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
maxZoom: 18
|
|
}).addTo(heatmapMap);
|
|
}
|
|
|
|
/**
|
|
* Render heatmap layer from GeoJSON data
|
|
*/
|
|
function renderHeatmap(data) {
|
|
// Convert GeoJSON features to Leaflet.heat format: [lat, lon, intensity]
|
|
const heatData = data.features.map(feature => {
|
|
const lon = feature.geometry.coordinates[0];
|
|
const lat = feature.geometry.coordinates[1];
|
|
const intensity = feature.properties.intensity;
|
|
|
|
// Normalize intensity to 0-1 range
|
|
const normalizedIntensity = Math.min(intensity / data.maxIntensity, 1.0);
|
|
|
|
return [lat, lon, normalizedIntensity];
|
|
});
|
|
|
|
// Remove existing heat layer if present
|
|
if (heatLayer) {
|
|
heatmapMap.removeLayer(heatLayer);
|
|
}
|
|
|
|
// Create heat layer
|
|
heatLayer = L.heatLayer(heatData, {
|
|
radius: 25,
|
|
blur: 15,
|
|
maxZoom: 17,
|
|
max: 1.0,
|
|
gradient: {
|
|
0.0: 'blue',
|
|
0.4: 'cyan',
|
|
0.6: 'lime',
|
|
0.7: 'yellow',
|
|
0.9: 'orange',
|
|
1.0: 'red'
|
|
}
|
|
}).addTo(heatmapMap);
|
|
|
|
// Fit map bounds to heatmap data
|
|
fitMapToBounds(data.features);
|
|
}
|
|
|
|
/**
|
|
* Fit map to show all heatmap data
|
|
*/
|
|
function fitMapToBounds(features) {
|
|
if (features.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Calculate bounds
|
|
let minLat = Infinity;
|
|
let maxLat = -Infinity;
|
|
let minLon = Infinity;
|
|
let maxLon = -Infinity;
|
|
|
|
features.forEach(feature => {
|
|
const lon = feature.geometry.coordinates[0];
|
|
const lat = feature.geometry.coordinates[1];
|
|
|
|
minLat = Math.min(minLat, lat);
|
|
maxLat = Math.max(maxLat, lat);
|
|
minLon = Math.min(minLon, lon);
|
|
maxLon = Math.max(maxLon, lon);
|
|
});
|
|
|
|
// Add padding
|
|
const latPadding = (maxLat - minLat) * 0.1;
|
|
const lonPadding = (maxLon - minLon) * 0.1;
|
|
|
|
const bounds = [
|
|
[minLat - latPadding, minLon - lonPadding],
|
|
[maxLat + latPadding, maxLon + lonPadding]
|
|
];
|
|
|
|
heatmapMap.fitBounds(bounds);
|
|
}
|