// AQI color gradient: green→yellow→orange→red→purple const AQI_GRADIENT = { 0.0: '#00e400', // Good (0-50) 0.17: '#ffff00', // Moderate (51-100) 0.33: '#ff7e00', // Unhealthy for sensitive (101-150) 0.50: '#ff0000', // Unhealthy (151-200) 0.67: '#8f3f97', // Very unhealthy (201-300) 1.0: '#7e0023', // Hazardous (301+) }; const PM25_GRADIENT = { 0.0: '#00e5ff', 0.25: '#00ff88', 0.50: '#ffff00', 0.75: '#ff7e00', 1.0: '#ff0000', }; function aqiLabel(aqi) { if (aqi == null) return '—'; if (aqi <= 50) return `${aqi} Good`; if (aqi <= 100) return `${aqi} Moderate`; if (aqi <= 150) return `${aqi} Unhealthy (Sensitive)`; if (aqi <= 200) return `${aqi} Unhealthy`; if (aqi <= 300) return `${aqi} Very Unhealthy`; return `${aqi} Hazardous`; } function aqiColor(aqi) { if (aqi == null) return '#888'; if (aqi <= 50) return '#00e400'; if (aqi <= 100) return '#ffff00'; if (aqi <= 150) return '#ff7e00'; if (aqi <= 200) return '#ff0000'; if (aqi <= 300) return '#8f3f97'; return '#7e0023'; } let airHeatAqi = null; let airHeatPm25 = null; let airMode = null; // null | 'aqi' | 'pm25' async function fetchAirOverlay(metric) { try { const res = await fetch( `https://api.adamaps.org/api/air/heatmap?metric=${metric}&hours=24`, { cache: 'no-store' } ); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); // [[lat, lon, intensity], ...] } catch (e) { console.warn('Air overlay fetch failed:', e); return []; } } async function toggleAirOverlay(metric) { // Clear both layers first if (airHeatAqi) { map.removeLayer(airHeatAqi); airHeatAqi = null; } if (airHeatPm25) { map.removeLayer(airHeatPm25); airHeatPm25 = null; } // Toggle off if same metric clicked again if (airMode === metric) { airMode = null; btnAqi.classList.remove('active'); btnPm25.classList.remove('active'); statusEl.textContent = 'live · ' + new Date().toLocaleTimeString(); return; } airMode = metric; btnAqi.classList.toggle('active', metric === 'aqi'); btnPm25.classList.toggle('active', metric === 'pm25'); statusEl.textContent = `loading ${metric.toUpperCase()} overlay...`; const points = await fetchAirOverlay(metric); if (!points.length) { statusEl.textContent = 'no air quality data yet'; airMode = null; btnAqi.classList.remove('active'); btnPm25.classList.remove('active'); return; } const gradient = metric === 'aqi' ? AQI_GRADIENT : PM25_GRADIENT; const layer = L.heatLayer(points, { radius: 25, blur: 20, maxZoom: 17, gradient, minOpacity: 0.3, }); if (metric === 'aqi') airHeatAqi = layer; else airHeatPm25 = layer; layer.addTo(map); statusEl.textContent = `${metric.toUpperCase()} overlay · ${points.length.toLocaleString()} pts`; } // ── Legend ──────────────────────────────────────────────────────────────────── function buildAirLegend() { const legend = L.control({ position: 'bottomright' }); legend.onAdd = () => { const div = L.DomUtil.create('div', 'air-legend'); div.innerHTML = `
AQI / PM2.5
Good (0-50)
Moderate (51-100)
Unhealthy* (101-150)
Unhealthy (151-200)
Very Unhealthy (201-300)
Hazardous (301+)
`; return div; }; return legend; } // Call this after map init — wires up buttons and legend function initAirOverlay() { buildAirLegend().addTo(map); btnAqi.addEventListener('click', () => toggleAirOverlay('aqi')); btnPm25.addEventListener('click', () => toggleAirOverlay('pm25')); } // Init air overlay buttons + legend after map is ready initAirOverlay();