821 lines
38 KiB
JavaScript
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); |