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: `
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 = `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: `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 = '