/** * Measles Vaccination Coverage vs. Disease Outbreaks (2000-2023) * 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 measles data (Point geometries with realistic metrics) const measlesData = generateVaccineData('measles'); // Initialize map with validated configuration const map = new mapboxgl.Map({ container: 'map', ...MAPBOX_CONFIG.getMapOptions({ style: 'mapbox://styles/mapbox/dark-v11', center: [15, 25], zoom: 1.5, pitch: 0 }) }); // Map load event map.on('load', () => { const factory = new LayerFactory(map); // Apply dark medical atmosphere factory.applyGlobeAtmosphere({ theme: 'dark', customConfig: { color: 'rgb(10, 10, 15)', 'high-color': 'rgb(25, 35, 60)', 'horizon-blend': 0.02, 'space-color': 'rgb(5, 5, 10)', 'star-intensity': 0.6 } }); // Add data source map.addSource('measles-data', { type: 'geojson', data: measlesData }); // Create coverage layer (first dose coverage) const coverageLayer = factory.createCircleLayer({ id: 'coverage-layer', source: 'measles-data', sizeProperty: 'population', colorProperty: 'coverage_dose1', colorScale: 'coverageReverse', // Green (high) to Red (low) sizeRange: [4, 25], opacityRange: [0.7, 0.85] }); map.addLayer(coverageLayer); // Create outbreak circles layer (sized by cases_2023) const outbreaksLayer = factory.createCircleLayer({ id: 'outbreaks-layer', source: 'measles-data', sizeProperty: 'cases_2023', colorProperty: 'deaths_2023', sizeRange: [6, 32], opacityRange: [0.6, 0.8] }); // Custom styling for outbreak layer (red circles) outbreaksLayer.paint['circle-color'] = 'rgba(239, 83, 80, 0.7)'; outbreaksLayer.paint['circle-stroke-color'] = '#ef5350'; outbreaksLayer.paint['circle-stroke-width'] = 2; // Only show outbreaks where cases_2023 > 100 (use coalesce to handle nulls) outbreaksLayer.filter = ['>', ['coalesce', ['get', 'cases_2023'], 0], 100]; map.addLayer(outbreaksLayer); // Create pulse effect for major outbreaks (>1000 cases) const pulseLayer = factory.createPulseLayer('measles-data', { id: 'outbreaks-pulse', sizeMultiplier: 1.8, color: 'rgba(239, 83, 80, 0.3)', filter: ['>', ['coalesce', ['get', 'cases_2023'], 0], 1000] }); map.addLayer(pulseLayer); // Setup hover effects for coverage layer map.on('mouseenter', 'coverage-layer', (e) => { map.getCanvas().style.cursor = 'pointer'; if (e.features.length > 0) { const feature = e.features[0]; const props = feature.properties; const popupContent = ` ${props.coverage_dose1 < 75 ? ` ` : ''} ${props.coverage_dose1 >= 95 ? ` ` : ''} `; new mapboxgl.Popup({ offset: 15 }) .setLngLat(feature.geometry.coordinates) .setHTML(popupContent) .addTo(map); } }); map.on('mouseleave', 'coverage-layer', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); // Setup hover for outbreak layer map.on('mouseenter', 'outbreaks-layer', (e) => { map.getCanvas().style.cursor = 'pointer'; if (e.features.length > 0) { const feature = e.features[0]; const props = feature.properties; const popupContent = ` `; new mapboxgl.Popup({ offset: 15 }) .setLngLat(feature.geometry.coordinates) .setHTML(popupContent) .addTo(map); } }); map.on('mouseleave', 'outbreaks-layer', () => { map.getCanvas().style.cursor = ''; popup.remove(); }); // Add custom legend for coverage const legendDiv = document.createElement('div'); legendDiv.className = 'legend'; legendDiv.style.cssText = ` position: absolute; bottom: 20px; right: 20px; background: rgba(10, 14, 26, 0.95); padding: 20px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); z-index: 1000; min-width: 250px; `; legendDiv.innerHTML = `
Legend
Vaccination Coverage (1st Dose)
95%+ 80% 60% <60%
Outbreak Size (2023)
100
1,000
10,000+
`; map.getContainer().parentElement.appendChild(legendDiv); // Setup layer toggles document.getElementById('toggle-coverage').addEventListener('change', (e) => { const visibility = e.target.checked ? 'visible' : 'none'; map.setLayoutProperty('coverage-layer', 'visibility', visibility); }); document.getElementById('toggle-outbreaks').addEventListener('change', (e) => { const visibility = e.target.checked ? 'visible' : 'none'; map.setLayoutProperty('outbreaks-layer', 'visibility', visibility); map.setLayoutProperty('outbreaks-pulse', 'visibility', visibility); }); document.getElementById('toggle-borders').addEventListener('change', (e) => { const visibility = e.target.checked ? 'visible' : 'none'; // This would toggle borders if we had a borders layer // For now, we can adjust stroke on coverage layer if (e.target.checked) { map.setPaintProperty('coverage-layer', 'circle-stroke-width', map.getPaintProperty('coverage-layer', 'circle-stroke-width')); } else { map.setPaintProperty('coverage-layer', 'circle-stroke-width', 0); } }); // Globe auto-rotation let userInteracting = false; let spinEnabled = false; const spinGlobe = () => { if (spinEnabled && !userInteracting && map.isStyleLoaded()) { map.easeTo({ center: [map.getCenter().lng + 0.15, 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; }); }); // Handle window resize window.addEventListener('resize', () => { map.resize(); });