34935-vm/assets/js/main.js
2025-10-14 12:26:56 +00:00

821 lines
38 KiB
JavaScript

// Set your Cesium Ion default access token immediately
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjZTY0ZTQ1Yi0zYmYxLTQ5MjItODdkOS05ZDY0ZGRjYjQwM2QiLCJpZCI6MjA5ODgwLCJpYXQiOjE3MTM4MTY3OTB9.A-3Jt_G0K81s-A-XLpT2bn5aY2H3s-n2p-2jYf-i-g';
function initializeGlobe() {
let epCurveChart = null; // To hold the chart instance
console.log('Cesium is defined, initializing globe.');
try {
console.log('Initializing Cesium Viewer');
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: true,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
navigationHelpButton: false,
scene3DOnly: true
});
viewer.scene.globe.depthTestAgainstTerrain = false;
console.log('Cesium Viewer initialized successfully');
// Add Weather Layer
const weatherImageryProvider = new Cesium.UrlTemplateImageryProvider({
url: `api/weather.php?layer=clouds_new&z={z}&x={x}&y={y}`,
credit: 'Weather data © OpenWeatherMap'
});
const weatherLayer = viewer.imageryLayers.addImageryProvider(weatherImageryProvider);
weatherLayer.alpha = 0.6;
// UI Controls
const weatherCheckbox = document.getElementById('weatherLayerCheckbox');
weatherCheckbox.addEventListener('change', function() {
weatherLayer.show = this.checked;
});
let wildfireDataSource;
const wildfireCheckbox = document.getElementById('wildfireLayerCheckbox');
wildfireCheckbox.addEventListener('change', function() {
if (wildfireDataSource) {
wildfireDataSource.show = this.checked;
}
});
let spcDataSource = new Cesium.CustomDataSource('spcOutlook');
viewer.dataSources.add(spcDataSource);
const spcCheckbox = document.getElementById('spcLayerCheckbox');
spcCheckbox.addEventListener('change', function() {
spcDataSource.show = this.checked;
});
let weatherAlertsDataSource = new Cesium.CustomDataSource('weatherAlerts');
viewer.dataSources.add(weatherAlertsDataSource);
const weatherAlertsCheckbox = document.getElementById('weatherAlertsLayerCheckbox');
weatherAlertsCheckbox.addEventListener('change', function() {
weatherAlertsDataSource.show = this.checked;
});
// Wind Layer
const windLayer = new WindLayer(viewer, {
particleHeight: 10000,
particleCount: 10000,
maxAge: 120,
particleSpeed: 5
});
const windCheckbox = document.getElementById('windLayerCheckbox');
windCheckbox.addEventListener('change', function() {
windLayer.setVisible(this.checked);
});
const windAltitudeSlider = document.getElementById('windAltitudeSlider');
const windAltitudeLabel = document.getElementById('windAltitudeLabel');
windAltitudeSlider.addEventListener('input', function() {
const altitude = parseInt(this.value, 10);
windAltitudeLabel.textContent = `${altitude} m`;
windLayer.setOptions({ particleHeight: altitude });
});
const playPauseButton = document.getElementById('playPauseWind');
let isPlaying = true;
playPauseButton.addEventListener('click', function() {
if (isPlaying) {
windLayer.pause();
this.textContent = 'Play';
} else {
windLayer.play();
this.textContent = 'Pause';
}
isPlaying = !isPlaying;
});
const particleDensitySlider = document.getElementById('particleDensitySlider');
particleDensitySlider.addEventListener('input', function() {
const density = parseFloat(this.value);
windLayer.setParticleDensity(density);
});
// Function to load wildfire data
const loadWildfireData = async () => {
try {
console.log('Fetching wildfire data...');
const response = await fetch('api/wildfires.php');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const geojsonData = await response.json();
console.log('Wildfire data fetched successfully.');
wildfireDataSource = new Cesium.GeoJsonDataSource();
await wildfireDataSource.load(geojsonData, {
stroke: Cesium.Color.RED,
fill: Cesium.Color.RED.withAlpha(0.5),
strokeWidth: 2
});
viewer.dataSources.add(wildfireDataSource);
console.log('Wildfire data source added to viewer.');
} catch (error) {
console.error('Error loading wildfire data:', error);
}
};
const loadSpcData = async () => {
try {
console.log('Fetching SPC data...');
const response = await fetch('api/spc.php');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const spcData = await response.json();
console.log('SPC data fetched successfully.');
spcDataSource.entities.removeAll();
const selectedSpcLevels = Array.from(document.querySelectorAll('.spc-checkbox:checked')).map(cb => cb.value);
const spcColors = {
'TSTM': Cesium.Color.fromCssColorString('#00FF00').withAlpha(0.5), // General Thunderstorms
'MRGL': Cesium.Color.fromCssColorString('#00C800').withAlpha(0.5), // Marginal
'SLGT': Cesium.Color.fromCssColorString('#FFFF00').withAlpha(0.5), // Slight
'ENH': Cesium.Color.fromCssColorString('#FFA500').withAlpha(0.5), // Enhanced
'MDT': Cesium.Color.fromCssColorString('#FF0000').withAlpha(0.5), // Moderate
'HIGH': Cesium.Color.fromCssColorString('#FF00FF').withAlpha(0.5) // High
};
if (Array.isArray(spcData)) {
spcData.forEach(feature => {
if (feature && feature.name && Array.isArray(feature.coordinates) && selectedSpcLevels.includes(feature.name)) {
const color = spcColors[feature.name] || Cesium.Color.GRAY.withAlpha(0.5);
spcDataSource.entities.add({
name: `SPC Outlook: ${feature.name}`,
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray(feature.coordinates),
material: color,
outline: true,
outlineColor: Cesium.Color.BLACK
}
});
}
});
}
console.log('SPC data source updated.');
} catch (error) {
console.error('Error loading SPC data:', error);
}
};
const loadWeatherAlerts = async () => {
try {
console.log('Fetching weather alerts...');
const response = await fetch('api/weather_alerts.php');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const alertsData = await response.json();
console.log('Weather alerts fetched successfully.');
weatherAlertsDataSource.entities.removeAll();
const selectedAlerts = Array.from(document.querySelectorAll('.alert-checkbox:checked')).map(cb => cb.value);
const alertColors = {
'tornado': Cesium.Color.RED.withAlpha(0.7),
'thunderstorm': Cesium.Color.YELLOW.withAlpha(0.7),
'flood': Cesium.Color.BLUE.withAlpha(0.7),
'wind': Cesium.Color.CYAN.withAlpha(0.7),
'snow_ice': Cesium.Color.WHITE.withAlpha(0.7),
'fog': Cesium.Color.GRAY.withAlpha(0.7),
'extreme_high_temperature': Cesium.Color.ORANGE.withAlpha(0.7)
};
if (Array.isArray(alertsData)) {
alertsData.forEach(alert => {
if (alert && alert.properties && typeof alert.properties.event === 'string') {
const eventType = alert.properties.event.toLowerCase().replace(/\s/g, '_');
if (selectedAlerts.includes(eventType)) {
const alertColor = alertColors[eventType] || Cesium.Color.PURPLE.withAlpha(0.7);
if (alert.geometry && alert.geometry.type === 'Polygon' && Array.isArray(alert.geometry.coordinates) && Array.isArray(alert.geometry.coordinates[0])) {
const coordinates = alert.geometry.coordinates[0].flat();
weatherAlertsDataSource.entities.add({
name: alert.properties.event || 'Weather Alert',
description: alert.properties.description || 'No description available.',
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray(coordinates),
material: alertColor,
outline: true,
outlineColor: Cesium.Color.BLACK
}
});
}
}
}
});
}
console.log('Weather alerts data source updated.');
} catch (error) {
console.error('Error loading weather alerts:', error);
}
};
document.querySelectorAll('.spc-checkbox').forEach(cb => {
cb.addEventListener('change', loadSpcData);
});
document.querySelectorAll('.alert-checkbox').forEach(cb => {
cb.addEventListener('change', loadWeatherAlerts);
});
// Load all data sources
loadWildfireData();
loadSpcData();
loadWeatherAlerts();
loadHurricaneSelector();
// --- CAT Simulation Logic ---
const catSimulationDataSource = new Cesium.CustomDataSource('catSimulation');
viewer.dataSources.add(catSimulationDataSource);
let customPortfolioData = null;
const portfolio = [
{ id: 1, name: 'Miami Property', lat: 25.7617, lon: -80.1918, value: 1000000 },
{ id: 2, name: 'Homestead Property', lat: 25.4687, lon: -80.4776, value: 750000 },
{ id: 3, name: 'Fort Lauderdale Property', lat: 26.1224, lon: -80.1373, value: 1200000 },
{ id: 4, name: 'Naples Property', lat: 26.1420, lon: -81.7948, value: 1500000 }
];
document.getElementById('portfolio-upload').addEventListener('change', handleFileUpload);
document.getElementById('portfolio-select').addEventListener('change', handlePortfolioSelect);
async function handlePortfolioSelect(event) {
const selectedPortfolioUrl = event.target.value;
const portfolioStatus = document.getElementById('portfolio-status');
const portfolioUpload = document.getElementById('portfolio-upload');
if (!selectedPortfolioUrl) {
customPortfolioData = null;
portfolioStatus.textContent = '';
portfolioUpload.value = ''; // Clear file input
return;
}
try {
const response = await fetch(selectedPortfolioUrl);
if (!response.ok) {
throw new Error(`Failed to load portfolio: ${response.statusText}`);
}
const contents = await response.text();
const lines = contents.split('\n').filter(line => line.trim() !== '');
const headers = lines.shift().trim().split(',').map(h => h.toLowerCase());
const latIndex = headers.indexOf('lat');
const lonIndex = headers.indexOf('lon');
const tivIndex = headers.indexOf('tiv');
const deductibleIndex = headers.indexOf('deductible');
const limitIndex = headers.indexOf('limit');
if (latIndex === -1 || lonIndex === -1 || tivIndex === -1) {
throw new Error('CSV must contain "lat", "lon", and "tiv" headers.');
}
const parsedData = lines.map((line, index) => {
const values = line.split(',');
if (values.length <= Math.max(latIndex, lonIndex, tivIndex)) {
console.error(`Skipping malformed CSV line ${index + 2}: ${line}`);
return null;
}
const lat = parseFloat(values[latIndex]);
const lon = parseFloat(values[lonIndex]);
const tiv = parseFloat(values[tivIndex]);
if (isNaN(lat) || isNaN(lon) || isNaN(tiv)) {
console.error(`Skipping line with invalid number ${index + 2}: ${line}`);
return null;
}
const deductible = deductibleIndex > -1 ? parseFloat(values[deductibleIndex]) || 0 : 0;
const limit = limitIndex > -1 ? parseFloat(values[limitIndex]) || Number.POSITIVE_INFINITY : Number.POSITIVE_INFINITY;
return {
id: `custom-${index + 1}`,
name: `Property ${index + 1}`,
lat: lat,
lon: lon,
value: tiv,
deductible: deductible,
limit: limit
};
}).filter(p => p !== null);
customPortfolioData = parsedData;
portfolioStatus.textContent = `${parsedData.length} properties loaded from sample portfolio.`;
portfolioUpload.value = ''; // Clear file input as we are using a sample
console.log('Sample portfolio loaded:', customPortfolioData);
} catch (error) {
console.error('Error loading sample portfolio:', error);
portfolioStatus.textContent = `Error: ${error.message}`;
customPortfolioData = null;
}
}
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) {
return;
}
// Clear the sample portfolio dropdown
document.getElementById('portfolio-select').value = '';
const reader = new FileReader();
reader.onload = function(e) {
const contents = e.target.result;
try {
const lines = contents.split('\n').filter(line => line.trim() !== '');
const headers = lines.shift().trim().split(',').map(h => h.toLowerCase());
const latIndex = headers.indexOf('lat');
const lonIndex = headers.indexOf('lon');
const tivIndex = headers.indexOf('tiv');
const deductibleIndex = headers.indexOf('deductible');
const limitIndex = headers.indexOf('limit');
if (latIndex === -1 || lonIndex === -1 || tivIndex === -1) {
throw new Error('CSV must contain "lat", "lon", and "tiv" headers.');
}
const parsedData = lines.map((line, index) => {
const values = line.split(',');
if (values.length <= Math.max(latIndex, lonIndex, tivIndex)) {
console.error(`Skipping malformed CSV line ${index + 2}: ${line}`);
return null;
}
const lat = parseFloat(values[latIndex]);
const lon = parseFloat(values[lonIndex]);
const tiv = parseFloat(values[tivIndex]);
if (isNaN(lat) || isNaN(lon) || isNaN(tiv)) {
console.error(`Skipping line with invalid number ${index + 2}: ${line}`);
return null;
}
const deductible = deductibleIndex > -1 ? parseFloat(values[deductibleIndex]) || 0 : 0;
const limit = limitIndex > -1 ? parseFloat(values[limitIndex]) || Number.POSITIVE_INFINITY : Number.POSITIVE_INFINITY;
return {
id: `custom-${index + 1}`,
name: `Property ${index + 1}`,
lat: lat,
lon: lon,
value: tiv,
deductible: deductible,
limit: limit
};
}).filter(p => p !== null);
customPortfolioData = parsedData;
document.getElementById('portfolio-status').textContent = `${parsedData.length} properties loaded.`;
console.log('Custom portfolio loaded:', customPortfolioData);
} catch (error) {
console.error('Error parsing CSV:', error);
document.getElementById('portfolio-status').textContent = `Error: ${error.message}`;
customPortfolioData = null;
}
};
reader.readAsText(file);
}
async function loadHurricaneSelector() {
try {
const response = await fetch('api/hurricanes.php');
const hurricanes = await response.json();
const selectElement = document.getElementById('hurricane-select');
selectElement.innerHTML = ''; // Clear "Loading..."
hurricanes.forEach(h => {
const option = document.createElement('option');
option.value = h.id;
option.textContent = `${h.name} (${h.year})`;
selectElement.appendChild(option);
});
} catch (error) {
console.error('Error loading hurricane list:', error);
const selectElement = document.getElementById('hurricane-select');
selectElement.innerHTML = '<option value="">Error loading storms</option>';
}
}
function getEstimatedWindSpeed(propertyPosition, stormTrack, propertyName, eventId) {
console.log(`[getEstimatedWindSpeed] Called for property: ${propertyName}, event: ${eventId}`);
let minDistance = Number.MAX_VALUE;
let closestSegmentIndex = -1;
if (!stormTrack || stormTrack.length === 0) {
console.error(`[getEstimatedWindSpeed] Received empty or invalid stormTrack for event ${eventId}`);
return { windSpeed: 0, distanceKm: 0, closestTrackPoint: null };
}
console.log(`[getEstimatedWindSpeed] propertyPosition:`, propertyPosition);
console.log(`[getEstimatedWindSpeed] stormTrack length:`, stormTrack.length);
for (let i = 0; i < stormTrack.length; i++) {
const lon = stormTrack[i][0];
const lat = stormTrack[i][1];
if (isNaN(lon) || isNaN(lat)) {
console.error(`[getEstimatedWindSpeed] Invalid coordinates in stormTrack for event ${eventId} at index ${i}:`, stormTrack[i]);
continue;
}
try {
const trackPointPosition = Cesium.Cartesian3.fromDegrees(lon, lat);
const distance = Cesium.Cartesian3.distance(propertyPosition, trackPointPosition);
if (i < 5) { // Log first 5 distances
console.log(`[getEstimatedWindSpeed] Index ${i}: lon=${lon}, lat=${lat}, distance=${distance}`);
}
if (distance < minDistance) {
minDistance = distance;
closestSegmentIndex = i;
}
} catch (e) {
console.error(`[getEstimatedWindSpeed] Error processing track point ${i} for event ${eventId}: lon=${lon}, lat=${lat}`, e);
}
}
if (closestSegmentIndex === -1) {
console.error(`[getEstimatedWindSpeed] Could not find closest track point for event ${eventId}`);
return { windSpeed: 0, distanceKm: 0, closestTrackPoint: null };
}
console.log(`[getEstimatedWindSpeed] Closest segment index: ${closestSegmentIndex}`);
const distanceKm = minDistance / 1000;
const trackPoint = stormTrack[closestSegmentIndex];
const windOnTrackMph = trackPoint[2];
console.log(`[getEstimatedWindSpeed] minDistance (m): ${minDistance}`);
console.log(`[getEstimatedWindSpeed] distanceKm: ${distanceKm}`);
console.log(`[getEstimatedWindSpeed] closest trackPoint:`, trackPoint);
console.log(`[getEstimatedWindSpeed] windOnTrackMph: ${windOnTrackMph}`);
const decayConstant = 0.01386; // -ln(0.5) / 50km
const estimatedWindMph = windOnTrackMph * Math.exp(-decayConstant * distanceKm);
console.log(`[getEstimatedWindSpeed] decayConstant: ${decayConstant}`);
console.log(`[getEstimatedWindSpeed] Math.exp term: ${Math.exp(-decayConstant * distanceKm)}`);
console.log(`[getEstimatedWindSpeed] Final estimatedWindMph: ${estimatedWindMph}`);
if (isNaN(estimatedWindMph)) {
console.error(`[getEstimatedWindSpeed] FATAL: Resulting wind speed is NaN. Inputs: windOnTrack=${windOnTrackMph}, distanceKm=${distanceKm}`);
}
return {
windSpeed: estimatedWindMph,
distanceKm: distanceKm,
closestTrackPoint: {
lon: trackPoint[0],
lat: trackPoint[1],
wind: windOnTrackMph
}
};
}
function calculateNetLoss(grossLoss, deductible, limit) {
const lossAfterDeductible = Math.max(0, grossLoss - deductible);
return Math.min(lossAfterDeductible, limit);
}
function getDamageRatio(windSpeedMph) {
if (windSpeedMph < 74) return 0.0;
if (windSpeedMph <= 95) return 0.03;
if (windSpeedMph <= 110) return 0.08;
if (windSpeedMph <= 129) return 0.15;
if (windSpeedMph <= 156) return 0.30;
return 0.50;
}
function renderEPCurve(epData) {
const ctx = document.getElementById('epCurveChart').getContext('2d');
if (epCurveChart) {
epCurveChart.destroy();
}
const chartData = {
datasets: [{
label: 'Exceedance Probability Curve',
data: epData.map(p => ({ x: p.loss, y: p.exceedanceProbability })),
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderWidth: 2,
showLine: true,
pointRadius: 4,
pointBackgroundColor: 'rgba(75, 192, 192, 1)',
}]
};
epCurveChart = new Chart(ctx, {
type: 'scatter',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: 'Exceedance Probability (EP) Curve'
},
scales: {
xAxes: [{
type: 'linear',
position: 'bottom',
scaleLabel: {
display: true,
labelString: 'Loss ($)'
},
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
}
}
}],
yAxes: [{
type: 'linear',
scaleLabel: {
display: true,
labelString: 'Exceedance Probability'
},
ticks: {
min: 0,
max: 1,
callback: function(value) {
return (value * 100).toFixed(0) + '%';
}
}
}]
},
tooltips: {
callbacks: {
label: function(tooltipItem, data) {
const datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
const loss = tooltipItem.xLabel.toLocaleString();
const prob = (tooltipItem.yLabel * 100).toFixed(2);
return `${datasetLabel} Loss: $${loss} (EP: ${prob}%)`;
}
}
}
}
});
}
const runProbabilisticAnalysisButton = document.getElementById('runProbabilisticAnalysis');
runProbabilisticAnalysisButton.addEventListener('click', runProbabilisticAnalysis);
async function runProbabilisticAnalysis() {
console.log("Starting probabilistic analysis...");
catSimulationDataSource.entities.removeAll();
document.getElementById('cat-simulation-results').style.display = 'none';
const resultsDiv = document.getElementById('probabilistic-results');
resultsDiv.style.display = 'none';
const activePortfolio = customPortfolioData || portfolio;
if (!activePortfolio || activePortfolio.length === 0) {
alert("Please load a portfolio to run the analysis.");
return;
}
try {
const response = await fetch('api/stochastic_events.php');
if (!response.ok) {
throw new Error(`Failed to fetch stochastic events: ${response.statusText}`);
}
const stochasticEvents = await response.json();
console.log(`Fetched ${stochasticEvents.length} stochastic events.`);
console.log('Full stochastic events data:', stochasticEvents);
console.log('Stochastic Events:', JSON.stringify(stochasticEvents, null, 2));
let averageAnnualLoss = 0;
const eventLosses = [];
stochasticEvents.forEach(event => {
console.log('Processing event:', event);
let eventTotalLoss = 0;
activePortfolio.forEach(property => {
try {
console.log(`Processing property: ${property.name}, lon: ${property.lon}, lat: ${property.lat}`);
if (isNaN(property.lon) || isNaN(property.lat)) {
console.error('Invalid coordinates for property:', property);
return;
}
const propertyPosition = Cesium.Cartesian3.fromDegrees(property.lon, property.lat);
if (!event.track_data || !Array.isArray(event.track_data)) {
console.error('Invalid track_data for event:', event);
return;
}
const windData = getEstimatedWindSpeed(propertyPosition, event.track_data, property.name, event.event_id);
if (!windData || windData.closestTrackPoint === null || isNaN(windData.windSpeed)) {
console.error('Could not calculate valid wind data for property, skipping.', {property, event});
return;
}
const damageRatio = getDamageRatio(windData.windSpeed);
const grossLoss = property.value * damageRatio;
const netLoss = calculateNetLoss(grossLoss, property.deductible || 0, property.limit || Number.POSITIVE_INFINITY);
eventTotalLoss += netLoss;
} catch (e) {
console.error(`An error occurred while processing property ${property.name} for event ${event.id}:`, e);
}
});
averageAnnualLoss += eventTotalLoss * event.probability;
eventLosses.push({ loss: eventTotalLoss, probability: event.probability });
});
console.log(`Calculated Average Annual Loss (AAL): ${averageAnnualLoss}`);
eventLosses.sort((a, b) => b.loss - a.loss);
let cumulativeProbability = 0;
const epData = eventLosses.map(event => {
cumulativeProbability += event.probability;
return {
loss: event.loss,
exceedanceProbability: cumulativeProbability
};
});
epData.push({ loss: 0, exceedanceProbability: 1.0 });
console.log("EP Curve Data:", epData);
document.getElementById('aal-value').textContent = `$${Math.round(averageAnnualLoss).toLocaleString()}`;
resultsDiv.style.display = 'block';
renderEPCurve(epData);
activePortfolio.forEach(property => {
if (isNaN(property.lon) || isNaN(property.lat)) {
return;
}
catSimulationDataSource.entities.add({
id: `property-${property.id}`,
position: Cesium.Cartesian3.fromDegrees(property.lon, property.lat),
point: {
pixelSize: 10,
color: Cesium.Color.BLUE,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
label: {
text: property.name,
font: '12pt monospace',
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth: 2,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -9)
}
});
});
viewer.flyTo(catSimulationDataSource.entities);
} catch (error) {
console.error("Error during probabilistic analysis:", error);
alert(`An error occurred during analysis: ${error.message}`);
}
}
const runCatSimulationButton = document.getElementById('runCatSimulation');
runCatSimulationButton.addEventListener('click', runCatSimulation);
async function runCatSimulation() {
catSimulationDataSource.entities.removeAll();
document.getElementById('cat-simulation-results').style.display = 'none';
let totalLoss = 0;
const hurricaneSelect = document.getElementById('hurricane-select');
const selectedHurricaneId = hurricaneSelect.value;
if (!selectedHurricaneId) {
alert('Please select a hurricane to run the simulation.');
return;
}
const activePortfolio = customPortfolioData || portfolio;
activePortfolio.forEach(property => {
catSimulationDataSource.entities.add({
id: `property-${property.id}`,
position: Cesium.Cartesian3.fromDegrees(property.lon, property.lat),
point: {
pixelSize: 10,
color: Cesium.Color.GREEN,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
label: {
text: property.name,
font: '12pt monospace',
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth: 2,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -9)
}
});
});
const response = await fetch(`api/hurricanes.php?id=${selectedHurricaneId}`);
const hurricaneData = await response.json();
const trackPositions = hurricaneData.track.flatMap(p => [p[0], p[1]]);
catSimulationDataSource.entities.add({
name: hurricaneData.name,
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(trackPositions),
width: 3,
material: Cesium.Color.RED.withAlpha(0.8)
}
});
activePortfolio.forEach(property => {
const propertyPosition = Cesium.Cartesian3.fromDegrees(property.lon, property.lat);
const windData = getEstimatedWindSpeed(propertyPosition, hurricaneData.track, property.name, hurricaneData.id);
const damageRatio = getDamageRatio(windData.windSpeed);
const grossLoss = property.value * damageRatio;
const netLoss = calculateNetLoss(grossLoss, property.deductible || 0, property.limit || Number.POSITIVE_INFINITY);
totalLoss += netLoss;
const entity = catSimulationDataSource.entities.getById(`property-${property.id}`);
let pointColor = Cesium.Color.GREEN;
if (damageRatio > 0) {
if (damageRatio <= 0.03) pointColor = Cesium.Color.YELLOW;
else if (damageRatio <= 0.08) pointColor = Cesium.Color.ORANGE;
else if (damageRatio <= 0.15) pointColor = Cesium.Color.RED;
else pointColor = Cesium.Color.PURPLE;
}
entity.point.color = pointColor;
entity.label.text = `${property.name}\nWind: ${windData.windSpeed.toFixed(0)} mph\nLoss: $${Math.round(netLoss).toLocaleString()}`;
});
document.getElementById('total-loss-value').textContent = `$${Math.round(totalLoss).toLocaleString()}`;
document.getElementById('cat-simulation-results').style.display = 'block';
viewer.flyTo(catSimulationDataSource.entities, {
duration: 2.0
});
}
const displayModeSelect = document.getElementById('displayModeSelect');
const windControls = document.getElementById('wind-controls');
const catControls = document.getElementById('cat-controls');
displayModeSelect.addEventListener('change', function(e) {
if (this.value === 'wind') {
windControls.style.display = 'block';
catControls.style.display = 'none';
windLayer.setVisible(true);
catSimulationDataSource.entities.removeAll();
} else {
windControls.style.display = 'none';
catControls.style.display = 'block';
windLayer.setVisible(false);
}
});
// Trigger the change event to set the initial state
displayModeSelect.dispatchEvent(new Event('change'));
} catch (error) {
console.error('A critical error occurred while initializing the Cesium viewer:', error);
const cesiumContainer = document.getElementById('cesiumContainer');
cesiumContainer.innerHTML = '<div class="alert alert-danger">Error: Could not load the 3D scene. Please check the console for details.</div>';
}
}
function waitForCesium() {
if (typeof Cesium !== 'undefined') {
initializeGlobe();
} else {
console.log('Waiting for Cesium to load...');
setTimeout(waitForCesium, 100);
}
}
document.addEventListener('DOMContentLoaded', waitForCesium);