/**
* 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();
});