fitpub/src/main/resources/static/js/heatmap.js
2026-01-01 23:48:05 +01:00

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: '&copy; <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);
}