/** * Smallpox Eradication Campaign Visualization (1950-1980) * Using shared architecture for reliability and best practices */ import { MAPBOX_CONFIG } from '../../shared/mapbox-config.js'; import { generateVaccineData } from '../../shared/data-generator.js'; import { LayerFactory, COLOR_SCALES } from '../../shared/layer-factory.js'; // Generate smallpox data (Point geometries with realistic metrics) const smallpoxData = generateVaccineData('smallpox'); // Initialize map with validated configuration const map = new mapboxgl.Map({ container: 'map', ...MAPBOX_CONFIG.getMapOptions({ style: 'mapbox://styles/mapbox/dark-v11', center: [20, 20], zoom: 1.5, pitch: 0 }) }); // Timeline state let currentYear = 1950; let isPlaying = false; let animationInterval = null; // Color schemes for smallpox eradication timeline const COLORS = { endemic: '#dc2626', // Red - still endemic campaign: '#f59e0b', // Orange - active campaign eradicated: '#10b981', // Green - eradicated victory: '#8b5cf6' // Purple - celebration }; // Map load event map.on('load', () => { const factory = new LayerFactory(map); // Apply standard atmosphere factory.applyGlobeAtmosphere({ theme: 'default' }); // Add data source map.addSource('smallpox-data', { type: 'geojson', data: smallpoxData }); // Create main layer showing eradication progress // Use endemic status by decade const layer = factory.createCircleLayer({ id: 'smallpox-circles', source: 'smallpox-data', sizeProperty: 'population', colorProperty: 'endemic_1950', // Will update dynamically sizeRange: [4, 25], opacityRange: [0.75, 0.9] }); // Custom color expression for eradication timeline layer.paint['circle-color'] = [ 'case', ['get', 'endemic_1950'], // Will update dynamically COLORS.endemic, COLORS.eradicated ]; map.addLayer(layer); // Setup hover effects map.on('mouseenter', 'smallpox-circles', (e) => { map.getCanvas().style.cursor = 'pointer'; if (e.features.length > 0) { const feature = e.features[0]; const props = feature.properties; const decade = Math.floor(currentYear / 10) * 10; const isEndemic = props[`endemic_${decade}`]; const popupContent = ` ${!isEndemic && props.eradication_year ? ` ` : ''} ${isEndemic ? ` ` : ''} `; new mapboxgl.Popup({ offset: 15 }) .setLngLat(feature.geometry.coordinates) .setHTML(popupContent) .addTo(map); } }); map.on('mouseleave', 'smallpox-circles', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); // Initialize visualization updateVisualization(); // Globe auto-rotation (slower for dramatic effect) let userInteracting = false; const spinGlobe = () => { if (!userInteracting && map.isStyleLoaded()) { map.easeTo({ center: [map.getCenter().lng + 0.03, map.getCenter().lat], duration: 100, easing: (n) => n }); } requestAnimationFrame(spinGlobe); }; // spinGlobe(); // Auto-rotation disabled map.on('mousedown', () => { userInteracting = true; }); map.on('mouseup', () => { userInteracting = false; }); map.on('dragend', () => { userInteracting = false; }); map.on('pitchend', () => { userInteracting = false; }); map.on('rotateend', () => { userInteracting = false; }); }); // Update visualization for current year function updateVisualization() { if (!map.isStyleLoaded()) return; // Round to nearest decade const decade = Math.floor(currentYear / 10) * 10; // Update year display document.getElementById('current-year').textContent = currentYear; const slider = document.getElementById('timeline-slider'); if (slider) slider.value = currentYear; // Update timeline progress bar const progress = ((currentYear - 1950) / 30) * 100; const progressBar = document.getElementById('timeline-progress'); if (progressBar) progressBar.style.width = `${progress}%`; // Update map layer colors based on endemic status for current decade const colorExpression = [ 'case', ['get', `endemic_${decade}`], COLORS.endemic, COLORS.eradicated ]; map.setPaintProperty('smallpox-circles', 'circle-color', colorExpression); // Update statistics updateStatistics(decade); // Show victory overlay at 1980 if (currentYear >= 1980) { const victoryOverlay = document.getElementById('victory-overlay'); if (victoryOverlay) { victoryOverlay.style.display = 'flex'; } stopAnimation(); } } // Calculate and update statistics function updateStatistics(decade) { const features = smallpoxData.features; let endemicCount = 0; let eradicatedCount = 0; let totalVaccination = 0; features.forEach(feature => { const props = feature.properties; if (props[`endemic_${decade}`]) { endemicCount++; } else if (props.eradication_year && props.eradication_year <= currentYear) { eradicatedCount++; } if (props.vaccination_intensity) { totalVaccination += props.vaccination_intensity; } }); // Estimate cases (smallpox cases declined from ~2M in 1950 to 0 in 1980) const casesEstimate = Math.max(0, 2000000 * (1 - (currentYear - 1950) / 30)); const vaccinationProgress = Math.min(100, (eradicatedCount / features.length) * 100); // Update UI const endemicEl = document.getElementById('endemic-count'); const casesEl = document.getElementById('cases-estimate'); const vaccinationEl = document.getElementById('vaccination-progress'); if (endemicEl) endemicEl.textContent = endemicCount; if (casesEl) casesEl.textContent = Math.round(casesEstimate).toLocaleString(); if (vaccinationEl) vaccinationEl.textContent = `${vaccinationProgress.toFixed(0)}%`; } // Play/pause animation function playAnimation() { if (isPlaying) { stopAnimation(); return; } // Hide play prompt const prompt = document.getElementById('play-prompt'); if (prompt) prompt.style.display = 'none'; isPlaying = true; const playBtn = document.getElementById('play-button'); if (playBtn) playBtn.innerHTML = ' Pause'; animationInterval = setInterval(() => { if (currentYear >= 1980) { stopAnimation(); return; } currentYear++; updateVisualization(); }, 500); // Advance by 1 year every 500ms } function stopAnimation() { isPlaying = false; const playBtn = document.getElementById('play-button'); if (playBtn) playBtn.innerHTML = ' Play'; if (animationInterval) { clearInterval(animationInterval); animationInterval = null; } } function resetAnimation() { stopAnimation(); currentYear = 1950; // Hide victory overlay const victoryOverlay = document.getElementById('victory-overlay'); if (victoryOverlay) victoryOverlay.style.display = 'none'; // Show play prompt const prompt = document.getElementById('play-prompt'); if (prompt) prompt.style.display = 'flex'; updateVisualization(); } // Timeline controls const slider = document.getElementById('timeline-slider'); if (slider) { slider.addEventListener('input', (e) => { stopAnimation(); currentYear = parseInt(e.target.value); updateVisualization(); }); } const playBtn = document.getElementById('play-button'); if (playBtn) { playBtn.addEventListener('click', playAnimation); } const resetBtn = document.getElementById('reset-button'); if (resetBtn) { resetBtn.addEventListener('click', resetAnimation); } const closeVictory = document.getElementById('close-victory'); if (closeVictory) { closeVictory.addEventListener('click', () => { const victoryOverlay = document.getElementById('victory-overlay'); if (victoryOverlay) victoryOverlay.style.display = 'none'; }); } // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.code === 'Space') { e.preventDefault(); playAnimation(); } else if (e.code === 'KeyR') { resetAnimation(); } else if (e.code === 'ArrowRight') { e.preventDefault(); currentYear = Math.min(1980, currentYear + 1); updateVisualization(); } else if (e.code === 'ArrowLeft') { e.preventDefault(); currentYear = Math.max(1950, currentYear - 1); updateVisualization(); } }); // Handle window resize window.addEventListener('resize', () => { map.resize(); });