diff --git a/docs/adamaps-index-preview.html b/docs/adamaps-index-preview.html new file mode 100644 index 0000000..495e946 --- /dev/null +++ b/docs/adamaps-index-preview.html @@ -0,0 +1,545 @@ + + + + + + AdaMaps — Verified Sign Map + + + + + + + +
+ + + + + + +// 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(); + + +