34935-vm/assets/js/main.js
2025-10-17 01:31:35 +00:00

579 lines
25 KiB
JavaScript

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: `
<h1>SPC Convective Outlook</h1>
<p><strong>Risk Level:</strong> ${riskLevel}</p>
`
});
});
}
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 = '<p>No active alerts in the covered area.</p>';
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 = `
<h4>${alert.event}</h4>
<p><strong>Sender:</strong> ${alert.sender_name}</p>
<p>${alert.description || 'No description provided.'}</p>
`;
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: `
<h1>${alert.event}</h1>
<p><strong>Sender:</strong> ${alert.sender_name}</p>
<p>${alert.description || 'No description provided.'}</p>
`
});
});
}
});
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 = '<p>Error loading 3D scene.</p>';
}
}
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 = '<tr><td colspan="4">Loading facility data...</td></tr>';
try {
const facilitiesResponse = await fetch('api/facilities.php');
const facilitiesData = await facilitiesResponse.json();
if (!facilitiesData.success || facilitiesData.facilities.length === 0) {
tbody.innerHTML = '<tr><td colspan="4">No facilities found.</td></tr>';
return;
}
let tableHtml = '';
facilitiesData.facilities.forEach(facility => {
tableHtml += `
<tr>
<td>${facility.name}</td>
<td>${facility.city}</td>
<td>${facility.state}</td>
<td class="${getRiskScoreClass(facility.risk)}">${facility.risk !== null ? facility.risk : 'N/A'}</td>
</tr>
`;
});
tbody.innerHTML = tableHtml;
} catch (error) {
console.error('Error loading dashboard data:', error);
tbody.innerHTML = '<tr><td colspan="4">Error loading data.</td></tr>';
}
}
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);
});
});