Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjZTY0ZTQ1Yi0zYmYxLTQ5MjItODdkOS05ZDY0ZGRjYjQwM2QiLCJpZCI6MjA5ODgwLCJpYXQiOjE3MTM4MTY3OTB9.A-3Jt_G0K81s-A-XLpT2bn5aY2H3s-n2p-2jYf-i-g'; function initializeGlobe() { try { const viewer = new Cesium.Viewer('cesiumContainer', { imageryProvider: new Cesium.OpenStreetMapImageryProvider({ url : 'https://a.tile.openstreetmap.org/' }), animation: false, baseLayerPicker: false, fullscreenButton: false, geocoder: false, homeButton: false, infoBox: false, // Disable default infobox sceneModePicker: false, selectionIndicator: false, timeline: false, navigationHelpButton: false, scene3DOnly: true }); viewer.scene.globe.depthTestAgainstTerrain = false; // --- Layer Toggles --- const layers = { weather: new Cesium.UrlTemplateImageryProvider({ url: `api/weather.php?layer=clouds_new&z={z}&x={x}&y={y}` }), wildfires: new Cesium.GeoJsonDataSource('wildfires'), spc: new Cesium.CustomDataSource('spcOutlook'), weatherAlerts: new Cesium.CustomDataSource('weatherAlerts'), hurricanes: new Cesium.CustomDataSource('hurricanes') }; const weatherLayer = viewer.imageryLayers.addImageryProvider(layers.weather); weatherLayer.alpha = 0.6; viewer.dataSources.add(layers.wildfires); viewer.dataSources.add(layers.spc); viewer.dataSources.add(layers.weatherAlerts); viewer.dataSources.add(layers.hurricanes); document.getElementById('weatherLayerCheckbox').addEventListener('change', e => { weatherLayer.show = e.target.checked; }); document.getElementById('wildfireLayerCheckbox').addEventListener('change', e => { layers.wildfires.show = e.target.checked; }); document.getElementById('spcLayerCheckbox').addEventListener('change', e => { layers.spc.show = e.target.checked; }); document.getElementById('weatherAlertsLayerCheckbox').addEventListener('change', e => { layers.weatherAlerts.show = e.target.checked; }); document.getElementById('hurricaneLayerCheckbox').addEventListener('change', e => { layers.hurricanes.show = e.target.checked; }); // --- Wind Layer --- const windLayer = new WindLayer(viewer, { particleHeight: 10000 }); document.getElementById('windLayerCheckbox').addEventListener('change', e => { windLayer.setVisible(e.target.checked); }); document.getElementById('windAltitudeSlider').addEventListener('input', e => { const altitude = parseInt(e.target.value, 10); document.getElementById('windAltitudeLabel').textContent = `${altitude} m`; windLayer.setOptions({ particleHeight: altitude }); }); document.getElementById('playPauseWind').addEventListener('click', e => { const isPlaying = windLayer.isPlaying(); isPlaying ? windLayer.pause() : windLayer.play(); e.target.textContent = isPlaying ? 'Play' : 'Pause'; }); document.getElementById('particleDensitySlider').addEventListener('input', e => { windLayer.setParticleDensity(parseFloat(e.target.value)); }); // --- Data Loading --- const loadDataSources = () => { // Wildfires fetch('api/wildfires.php').then(res => res.json()).then(data => { layers.wildfires.load(data, { stroke: Cesium.Color.RED, fill: Cesium.Color.RED.withAlpha(0.5), strokeWidth: 3 }).then(() => { layers.wildfires.entities.values.forEach(entity => { entity.point = new Cesium.PointGraphics({ color: Cesium.Color.FIREBRICK, pixelSize: 8 }); }); }); }); // Hurricanes fetch('api/hurricanes.php').then(res => res.json()).then(data => { processHurricaneData(data, layers.hurricanes); }); // Weather Alerts fetch('api/weather_alerts.php').then(res => res.json()).then(data => { processWeatherAlerts(data, layers.weatherAlerts); }); // SPC Outlook fetch('api/spc.php').then(res => res.json()).then(data => { processSpcOutlook(data, layers.spc); }); // Other data sources like SPC/Alerts can be loaded here too if needed on init }; function processSpcOutlook(spcData, dataSource) { dataSource.entities.removeAll(); if (!spcData || !Array.isArray(spcData)) return; spcData.forEach(feature => { const riskLevel = feature.name; const color = getSpcColor(riskLevel); dataSource.entities.add({ name: `SPC Outlook: ${riskLevel}`, polygon: { hierarchy: new Cesium.PolygonHierarchy( Cesium.Cartesian3.fromDegreesArray(feature.coordinates) ), material: color.withAlpha(0.5), outline: true, outlineColor: color }, description: `

SPC Convective Outlook

Risk Level: ${riskLevel}

` }); }); } function getSpcColor(riskLevel) { switch (riskLevel) { case 'TSTM': return Cesium.Color.fromCssColorString('#c1e2c1'); // Light Green case 'MRGL': return Cesium.Color.fromCssColorString('#a4d0a4'); // Green case 'SLGT': return Cesium.Color.fromCssColorString('#fdfd96'); // Yellow case 'ENH': return Cesium.Color.fromCssColorString('#ffb347'); // Orange case 'MDT': return Cesium.Color.fromCssColorString('#ff6961'); // Red case 'HIGH': return Cesium.Color.fromCssColorString('#ff00ff'); // Magenta default: return Cesium.Color.GRAY; } } function processWeatherAlerts(alertsData, dataSource) { dataSource.entities.removeAll(); const alertsListContainer = document.getElementById('alerts-list-container'); alertsListContainer.innerHTML = ''; // Clear previous list const alertColors = { "warning": Cesium.Color.RED, "watch": Cesium.Color.ORANGE, "advisory": Cesium.Color.YELLOW, "statement": Cesium.Color.SKYBLUE, "default": Cesium.Color.LIGHTGRAY }; if (!alertsData || !Array.isArray(alertsData) || alertsData.length === 0) { alertsListContainer.innerHTML = '

No active alerts in the covered area.

'; return; } const ul = document.createElement('ul'); ul.className = 'alerts-list'; alertsData.forEach(alert => { // Add to list in modal const li = document.createElement('li'); li.innerHTML = `

${alert.event}

Sender: ${alert.sender_name}

${alert.description || 'No description provided.'}

`; ul.appendChild(li); // Draw on map if (alert.polygons && Array.isArray(alert.polygons)) { const eventName = (alert.event || 'alert').toLowerCase(); let color = alertColors.default; for (const key in alertColors) { if (eventName.includes(key)) { color = alertColors[key]; break; } } alert.polygons.forEach(polygonCoords => { const coordinates = polygonCoords.flat(); dataSource.entities.add({ name: alert.event || 'Weather Alert', polygon: { hierarchy: new Cesium.PolygonHierarchy( Cesium.Cartesian3.fromDegreesArray(coordinates) ), material: color.withAlpha(0.4), outline: true, outlineColor: color.withAlpha(0.8) }, description: `

${alert.event}

Sender: ${alert.sender_name}

${alert.description || 'No description provided.'}

` }); }); } }); alertsListContainer.appendChild(ul); } function processHurricaneData(geoJson, dataSource) { dataSource.entities.removeAll(); const features = geoJson.features; // Separate features by type const cones = features.filter(f => f.geometry.type === 'Polygon'); const tracks = features.filter(f => f.geometry.type === 'LineString'); const points = features.filter(f => f.geometry.type === 'Point'); // Process cones first (bottom layer) cones.forEach(feature => { dataSource.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy( Cesium.Cartesian3.fromDegreesArray(feature.geometry.coordinates[0].flat()) ), material: Cesium.Color.WHITE.withAlpha(0.15), outline: true, outlineColor: Cesium.Color.WHITE.withAlpha(0.3) } }); }); // Process tracks tracks.forEach(feature => { dataSource.entities.add({ polyline: { positions: Cesium.Cartesian3.fromDegreesArray(feature.geometry.coordinates.flat()), width: 2, material: new Cesium.PolylineDashMaterialProperty({ color: Cesium.Color.WHITE }) } }); }); // Process points (top layer) points.forEach(feature => { const props = feature.properties; const windSpeed = parseInt(props.WINDSPEED, 10); dataSource.entities.add({ position: Cesium.Cartesian3.fromDegrees(feature.geometry.coordinates[0], feature.geometry.coordinates[1]), point: { pixelSize: 10, color: Cesium.Color.fromCssColorString(getStormColor(windSpeed)), outlineColor: Cesium.Color.WHITE, outlineWidth: 2 }, label: { text: `${props.STORMNAME} (${windSpeed} mph)`, font: '12pt Inter, sans-serif', style: Cesium.LabelStyle.FILL_AND_OUTLINE, outlineWidth: 2, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, -12) } }); }); } function getStormColor(windspeed) { if (windspeed >= 157) return '#c83226'; // Cat 5 if (windspeed >= 130) return '#ff6900'; // Cat 4 if (windspeed >= 111) return '#ff9c00'; // Cat 3 if (windspeed >= 96) return '#ffcf00'; // Cat 2 if (windspeed >= 74) return '#ffff00'; // Cat 1 return '#00c8ff'; // Tropical Storm } loadDataSources(); // --- Modal & Click Handling --- const crossSectionModal = document.getElementById("crossSectionModal"); const crossSectionCloseButton = crossSectionModal.querySelector(".close-button"); const alertsModal = document.getElementById("alertsModal"); const alertsCloseButton = alertsModal.querySelector(".close-button"); const alertsNavButton = document.getElementById("show-alerts-btn"); let crossSectionChart; crossSectionCloseButton.onclick = () => { crossSectionModal.style.display = "none"; }; alertsCloseButton.onclick = () => { alertsModal.style.display = "none"; }; alertsNavButton.addEventListener('click', (e) => { e.preventDefault(); alertsModal.style.display = "block"; }); window.onclick = (event) => { if (event.target == crossSectionModal) crossSectionModal.style.display = "none"; if (event.target == alertsModal) alertsModal.style.display = "none"; }; const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction(function (click) { const cartesian = viewer.scene.pickPosition(click.position); if (!Cesium.defined(cartesian)) { return; // Didn't pick a point on the globe } const cartographic = Cesium.Cartographic.fromCartesian(cartesian); const longitude = Cesium.Math.toDegrees(cartographic.longitude); const latitude = Cesium.Math.toDegrees(cartographic.latitude); // --- Now, handle the risk assessment modal --- crossSectionModal.style.display = "block"; // Reset UI document.getElementById('riskScoreValue').textContent = 'Calculating...'; document.getElementById('wildfireRisk').textContent = 'Calculating...'; document.getElementById('hurricaneRisk').textContent = 'Calculating...'; document.getElementById('historicalHurricaneRisk').textContent = 'Calculating...'; document.getElementById("loadingIndicator").style.display = 'block'; if (crossSectionChart) { crossSectionChart.destroy(); } // Fetch Risk Score fetch(`api/risk_score.php?lat=${latitude}&lon=${longitude}`) .then(response => response.json()) .then(data => { if (data.error) throw new Error(data.error); document.getElementById('riskScoreValue').textContent = data.risk_score ? `${data.risk_score} / 100` : 'N/A'; document.getElementById('wildfireRisk').textContent = data.factors?.wildfire?.details || 'N/A'; document.getElementById('hurricaneRisk').textContent = data.factors?.live_hurricane?.details || 'N/A'; document.getElementById('historicalHurricaneRisk').textContent = data.factors?.historical_hurricane?.details || 'N/A'; }) .catch(error => { console.error("Error fetching risk score:", error); document.getElementById('riskScoreValue').textContent = 'Error'; }); // Fetch Cross-Section Data fetch(`/api/cross_section.php?lat=${latitude}&lon=${longitude}`) .then(response => response.json()) .then(data => { document.getElementById("loadingIndicator").style.display = 'none'; if (data.error) throw new Error(data.error); const { pressure_level, temperature, relative_humidity, wind_speed } = data.hourly; crossSectionChart = new Chart(document.getElementById("crossSectionChart"), { type: "line", data: { labels: pressure_level, datasets: [ { label: "Temperature (C)", data: temperature, borderColor: "red", yAxisID: "y" }, { label: "Humidity (%)", data: relative_humidity, borderColor: "blue", yAxisID: "y1" }, { label: "Wind Speed (km/h)", data: wind_speed, borderColor: "green", yAxisID: "y2" } ] }, options: { scales: { x: { title: { display: true, text: "Pressure (hPa)" }, reverse: true }, y: { type: 'linear', position: 'left', title: { display: true, text: 'Temp (C)' } }, y1: { type: 'linear', position: 'right', title: { display: true, text: 'Humidity (%)' }, grid: { drawOnChartArea: false } }, y2: { type: 'linear', position: 'right', title: { display: true, text: 'Wind (km/h)' }, grid: { drawOnChartArea: false } } } } }); }) .catch(error => { console.error("Error fetching cross-section:", error); document.getElementById("loadingIndicator").style.display = 'none'; }); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); } catch (error) { console.error('Critical error initializing Cesium:', error); document.getElementById('cesiumContainer').innerHTML = '

Error loading 3D scene.

'; } } document.addEventListener('DOMContentLoaded', () => { const sidebar = document.getElementById('sidebar'); const hamburgerMenu = document.getElementById('hamburger-menu'); const homeLink = document.getElementById('home-link'); const mainContent = document.getElementById('main-content'); // Initially hide the sidebar for a cleaner look on load sidebar.classList.add('sidebar-hidden'); mainContent.classList.add('sidebar-hidden'); hamburgerMenu.addEventListener('click', () => { sidebar.classList.toggle('sidebar-hidden'); mainContent.classList.toggle('sidebar-hidden'); }); homeLink.addEventListener('click', (e) => { e.preventDefault(); showPanel(cesiumContainer); }); initializeGlobe(); // --- Dashboard --- const cesiumContainer = document.getElementById('cesiumContainer'); const dashboardContent = document.getElementById('dashboard-content'); const analyticsContent = document.getElementById('analytics-content'); const reportsContent = document.getElementById('reports-content'); const settingsContent = document.getElementById('settings-content'); const summaryContent = document.getElementById('summary-content'); const helpContent = document.getElementById('help-content'); const contentPanels = [cesiumContainer, dashboardContent, analyticsContent, reportsContent, settingsContent, summaryContent, helpContent]; function showPanel(panelToShow) { contentPanels.forEach(panel => { if (panel === panelToShow) { panel.classList.remove('hidden'); } else { panel.classList.add('hidden'); } }); } function getRiskScoreClass(score) { if (score > 75) return 'risk-high'; if (score > 50) return 'risk-medium'; if (score > 25) return 'risk-low'; return 'risk-very-low'; } async function loadDashboardData() { const tbody = dashboardContent.querySelector('tbody'); tbody.innerHTML = 'Loading facility data...'; try { const facilitiesResponse = await fetch('api/facilities.php'); const facilitiesData = await facilitiesResponse.json(); if (!facilitiesData.success || facilitiesData.facilities.length === 0) { tbody.innerHTML = 'No facilities found.'; return; } let tableHtml = ''; facilitiesData.facilities.forEach(facility => { tableHtml += ` ${facility.name} ${facility.city} ${facility.state} ${facility.risk !== null ? facility.risk : 'N/A'} `; }); tbody.innerHTML = tableHtml; } catch (error) { console.error('Error loading dashboard data:', error); tbody.innerHTML = 'Error loading data.'; } } let facilitiesByStateChart = null; async function loadAnalyticsData() { const ctx = document.getElementById('facilitiesByStateChart').getContext('2d'); try { const facilitiesResponse = await fetch('api/facilities.php'); const facilitiesData = await facilitiesResponse.json(); if (!facilitiesData.success || facilitiesData.facilities.length === 0) { return; } const facilitiesByState = facilitiesData.facilities.reduce((acc, facility) => { const state = facility.state || 'Unknown'; acc[state] = (acc[state] || 0) + 1; return acc; }, {}); const chartData = { labels: Object.keys(facilitiesByState), datasets: [{ label: 'Number of Facilities', data: Object.values(facilitiesByState), backgroundColor: 'rgba(0, 170, 255, 0.5)', borderColor: 'rgba(0, 170, 255, 1)', borderWidth: 1 }] }; if (facilitiesByStateChart) { facilitiesByStateChart.destroy(); } facilitiesByStateChart = new Chart(ctx, { type: 'bar', data: chartData, options: { scales: { y: { beginAtZero: true } } } }); } catch (error) { console.error('Error loading analytics data:', error); } } async function downloadReport() { try { const facilitiesResponse = await fetch('api/facilities.php'); const facilitiesData = await facilitiesResponse.json(); if (!facilitiesData.success || facilitiesData.facilities.length === 0) { alert('No facilities to generate a report for.'); return; } let csvContent = 'data:text/csv;charset=utf-8,'; csvContent += 'Facility Name,City,State,Latitude,Longitude,Risk Score\n'; facilitiesData.facilities.forEach(facility => { csvContent += `"${facility.name}","${facility.city}","${facility.state}",${facility.latitude},${facility.longitude},${facility.risk || 'N/A'}\n`; }); const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); link.setAttribute('download', 'facility_risk_report.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (error) { console.error('Error generating report:', error); alert('Failed to generate report.'); } } document.getElementById('dashboard-nav-link').addEventListener('click', (e) => { e.preventDefault(); showPanel(dashboardContent); loadDashboardData(); }); document.getElementById('analytics-nav-link').addEventListener('click', (e) => { e.preventDefault(); showPanel(analyticsContent); loadAnalyticsData(); }); document.getElementById('reports-nav-link').addEventListener('click', (e) => { e.preventDefault(); showPanel(reportsContent); }); const downloadReportBtn = document.getElementById('download-report-btn'); downloadReportBtn.addEventListener('click', () => { downloadReport(); }); document.getElementById('settings-nav-link').addEventListener('click', (e) => { e.preventDefault(); showPanel(settingsContent); }); document.getElementById('summary-nav-link').addEventListener('click', (e) => { e.preventDefault(); showPanel(summaryContent); }); document.getElementById('help-nav-link').addEventListener('click', (e) => { e.preventDefault(); showPanel(helpContent); }); });