Not sure if its working but it has alot of stuff
This commit is contained in:
parent
f7aa8776f6
commit
e4f30e4fa3
@ -1,118 +1,47 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// URL for the NHC active hurricane KML data
|
||||
$nhc_kmz_url = 'https://www.nhc.noaa.gov/gis/kml/nhc.kmz';
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// Temporary file to store the KMZ
|
||||
$tmp_kmz_file = tempnam(sys_get_temp_dir(), 'nhc_kmz');
|
||||
if (isset($_GET['id'])) {
|
||||
// Fetch track for a specific hurricane
|
||||
$hurricaneId = (int)$_GET['id'];
|
||||
$stmt = $pdo->prepare('SELECT name, year FROM hurricanes WHERE id = ?');
|
||||
$stmt->execute([$hurricaneId]);
|
||||
$hurricane = $stmt->fetch();
|
||||
|
||||
// Fetch the KMZ file
|
||||
$kmz_data = file_get_contents($nhc_kmz_url);
|
||||
|
||||
if ($kmz_data === false) {
|
||||
echo json_encode(['error' => 'Failed to fetch NHC data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Save the data to the temporary file
|
||||
file_put_contents($tmp_kmz_file, $kmz_data);
|
||||
|
||||
if (!class_exists('ZipArchive')) {
|
||||
echo json_encode(['error' => 'ZipArchive class does not exist.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Use ZipArchive to open the KMZ file
|
||||
$zip = new ZipArchive;
|
||||
if ($zip->open($tmp_kmz_file) === TRUE) {
|
||||
// NHC KMZ files typically contain a single KML file, often named doc.kml or similar.
|
||||
// We will look for the first .kml file in the archive.
|
||||
$kml_content = false;
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$filename = $zip->getNameIndex($i);
|
||||
if (strtolower(substr($filename, -4)) === '.kml') {
|
||||
$kml_content = $zip->getFromIndex($i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
if ($kml_content === false) {
|
||||
echo json_encode(['error' => 'KML file not found in the KMZ archive.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Parse the KML content
|
||||
$xml = simplexml_load_string($kml_content, "SimpleXMLElement", LIBXML_NOCDATA);
|
||||
if ($xml === false) {
|
||||
echo json_encode(['error' => 'Failed to parse KML data.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Register the KML namespace
|
||||
$xml->registerXPathNamespace('kml', 'http://www.opengis.net/kml/2.2');
|
||||
|
||||
$features = [];
|
||||
|
||||
// Find all Placemarks in the KML
|
||||
foreach ($xml->xpath('//kml:Placemark') as $placemark) {
|
||||
$placemark->registerXPathNamespace('kml', 'http://www.opengis.net/kml/2.2');
|
||||
$name = (string)$placemark->name;
|
||||
|
||||
// Look for Polygon
|
||||
$polygon = $placemark->xpath('.//kml:Polygon');
|
||||
if ($polygon && isset($polygon[0]->outerBoundaryIs->LinearRing->coordinates)) {
|
||||
$coordinates_str = (string)$polygon[0]->outerBoundaryIs->LinearRing->coordinates;
|
||||
$coordinates = parse_coordinates($coordinates_str);
|
||||
if (!empty($coordinates)) {
|
||||
$features[] = [
|
||||
'name' => $name,
|
||||
'type' => 'Polygon',
|
||||
'coordinates' => $coordinates
|
||||
];
|
||||
}
|
||||
if (!$hurricane) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Hurricane not found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Look for LineString
|
||||
$linestring = $placemark->xpath('.//kml:LineString');
|
||||
if ($linestring && isset($linestring[0]->coordinates)) {
|
||||
$coordinates_str = (string)$linestring[0]->coordinates;
|
||||
$coordinates = parse_coordinates($coordinates_str);
|
||||
if (!empty($coordinates)) {
|
||||
$features[] = [
|
||||
'name' => $name,
|
||||
'type' => 'LineString',
|
||||
'coordinates' => $coordinates
|
||||
];
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT lat, lon, wind_speed_mph FROM hurricane_tracks WHERE hurricane_id = ? ORDER BY timestamp ASC');
|
||||
$stmt->execute([$hurricaneId]);
|
||||
$trackData = $stmt->fetchAll();
|
||||
|
||||
// Format for existing frontend: [lon, lat, wind]
|
||||
$formattedTrack = array_map(function($point) {
|
||||
return [(float)$point['lon'], (float)$point['lat'], (int)$point['wind_speed_mph']];
|
||||
}, $trackData);
|
||||
|
||||
echo json_encode([
|
||||
'name' => $hurricane['name'] . ' (' . $hurricane['year'] . ')',
|
||||
'track' => $formattedTrack
|
||||
]);
|
||||
|
||||
} else {
|
||||
// Fetch list of all hurricanes
|
||||
$stmt = $pdo->query('SELECT id, name, year FROM hurricanes ORDER BY year DESC, name ASC');
|
||||
$hurricanes = $stmt->fetchAll();
|
||||
echo json_encode($hurricanes);
|
||||
}
|
||||
|
||||
echo json_encode($features);
|
||||
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to open KMZ file.']);
|
||||
}
|
||||
|
||||
// Clean up the temporary file
|
||||
unlink($tmp_kmz_file);
|
||||
|
||||
function parse_coordinates($coordinates_str) {
|
||||
$coords = [];
|
||||
$pairs = explode(' ', trim($coordinates_str));
|
||||
foreach ($pairs as $pair) {
|
||||
$parts = explode(',', $pair);
|
||||
if (count($parts) >= 2) {
|
||||
$lon = floatval($parts[0]);
|
||||
$lat = floatval($parts[1]);
|
||||
// Ensure coordinates are valid
|
||||
if (is_finite($lat) && is_finite($lon)) {
|
||||
$coords[] = $lon;
|
||||
$coords[] = $lat;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $coords;
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
20
api/stochastic_events.php
Normal file
20
api/stochastic_events.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once '../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT id, event_name, probability, track_data FROM stochastic_events");
|
||||
$events = $stmt->fetchAll();
|
||||
|
||||
// Decode the JSON track_data for each event
|
||||
foreach ($events as &$event) {
|
||||
$event['track_data'] = json_decode($event['track_data']);
|
||||
}
|
||||
|
||||
echo json_encode($events);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
function initializeGlobe() {
|
||||
let epCurveChart = null; // To hold the chart instance
|
||||
console.log('Cesium is defined, initializing globe.');
|
||||
// Set your Cesium Ion default access token
|
||||
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjZTY0ZTQ1Yi0zYmYxLTQ5MjItODdkOS05ZDY0ZGRjYjQwM2QiLCJpZCI6MjA5ODgwLCJpYXQiOjE3MTM4MTY3OTB9.A-3Jt_G0K81s-A-XLpT2bn5aY2H3s-n2p-2jYf-i-g';
|
||||
@ -81,6 +82,25 @@ function initializeGlobe() {
|
||||
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 {
|
||||
@ -190,6 +210,545 @@ function initializeGlobe() {
|
||||
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('Stochastic Events:', stochasticEvents);
|
||||
|
||||
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.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
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('A critical error occurred while initializing the Cesium viewer:', error);
|
||||
@ -207,4 +766,4 @@ function waitForCesium() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', waitForCesium);
|
||||
document.addEventListener('DOMContentLoaded', waitForCesium);
|
||||
@ -105,6 +105,24 @@ class WindLayer {
|
||||
this.particleSystem.applyOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.particleSystem) {
|
||||
this.scene.preRender.removeEventListener(this.particleSystem.update, this.particleSystem);
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.particleSystem) {
|
||||
this.scene.preRender.addEventListener(this.particleSystem.update, this.particleSystem);
|
||||
}
|
||||
}
|
||||
|
||||
setParticleDensity(density) {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.setParticleCount(density);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ParticleSystem {
|
||||
@ -221,4 +239,18 @@ class ParticleSystem {
|
||||
this.options = Object.assign(this.options, options);
|
||||
// Re-create particles or update properties as needed
|
||||
}
|
||||
|
||||
setParticleCount(density) {
|
||||
const maxParticles = this.options.maxParticles || this.options.particleCount || 10000;
|
||||
if (!this.options.maxParticles) {
|
||||
this.options.maxParticles = maxParticles;
|
||||
}
|
||||
const newParticleCount = Math.floor(maxParticles * density);
|
||||
|
||||
this.particles.length = 0; // Clear the array
|
||||
|
||||
for (let i = 0; i < newParticleCount; i++) {
|
||||
this.particles.push(this.createParticle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
assets/portfolios/fl_coastal.csv
Normal file
11
assets/portfolios/fl_coastal.csv
Normal file
@ -0,0 +1,11 @@
|
||||
lat,lon,tiv
|
||||
25.7617,-80.1918,1500000
|
||||
25.7743,-80.1902,2200000
|
||||
25.7907,-80.1353,3000000
|
||||
26.1224,-80.1373,1800000
|
||||
26.1195,-80.1259,950000
|
||||
26.1333,-80.1111,2500000
|
||||
26.7153,-80.0534,4500000
|
||||
26.7160,-80.0520,1200000
|
||||
26.6558,-80.0455,750000
|
||||
25.9880,-80.1220,1100000
|
||||
|
9
assets/portfolios/fl_inland.csv
Normal file
9
assets/portfolios/fl_inland.csv
Normal file
@ -0,0 +1,9 @@
|
||||
lat,lon,tiv
|
||||
28.5383,-81.3792,500000
|
||||
28.5421,-81.3750,750000
|
||||
28.4736,-81.4642,1200000
|
||||
28.0395,-81.9498,450000
|
||||
28.0836,-81.9618,600000
|
||||
28.0550,-81.9300,850000
|
||||
28.4044,-81.5761,950000
|
||||
28.6139,-81.2089,400000
|
||||
|
8
assets/portfolios/la_coastal.csv
Normal file
8
assets/portfolios/la_coastal.csv
Normal file
@ -0,0 +1,8 @@
|
||||
lat,lon,tiv
|
||||
29.9511,-90.0715,1200000
|
||||
29.9757,-90.0653,2000000
|
||||
29.9289,-90.0922,800000
|
||||
29.9547,-90.1011,3500000
|
||||
29.8885,-90.0592,650000
|
||||
30.0010,-90.1240,1500000
|
||||
29.9696,-89.9969,900000
|
||||
|
36
db/migrate.php
Normal file
36
db/migrate.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
echo "Connected to database successfully.\n";
|
||||
|
||||
$migrationsDir = __DIR__ . '/migrations';
|
||||
$files = glob($migrationsDir . '/*.sql');
|
||||
|
||||
if (empty($files)) {
|
||||
echo "No migration files found.\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
sort($files);
|
||||
|
||||
foreach ($files as $file) {
|
||||
echo "Applying migration: " . basename($file) . "...\n";
|
||||
$sql = file_get_contents($file);
|
||||
// PDO does not support multiple queries in one call, so we need to split them.
|
||||
$statements = array_filter(array_map('trim', explode(';', $sql)));
|
||||
foreach ($statements as $statement) {
|
||||
if (!empty($statement)) {
|
||||
$pdo->exec($statement);
|
||||
}
|
||||
}
|
||||
echo "Applied successfully.\n";
|
||||
}
|
||||
|
||||
echo "All migrations applied.\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Database error: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
60
db/migrations/20251014_create_hurricanes_table.sql
Normal file
60
db/migrations/20251014_create_hurricanes_table.sql
Normal file
@ -0,0 +1,60 @@
|
||||
CREATE TABLE IF NOT EXISTS `hurricanes` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`year` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `hurricane_tracks` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`hurricane_id` int(11) NOT NULL,
|
||||
`lat` decimal(9,6) NOT NULL,
|
||||
`lon` decimal(9,6) NOT NULL,
|
||||
`wind_speed_mph` int(11) NOT NULL,
|
||||
`timestamp` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `hurricane_id` (`hurricane_id`),
|
||||
CONSTRAINT `hurricane_tracks_ibfk_1` FOREIGN KEY (`hurricane_id`) REFERENCES `hurricanes` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Clear existing data to prevent duplicates on re-run
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
DELETE FROM `hurricanes`;
|
||||
DELETE FROM `hurricane_tracks`;
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
ALTER TABLE `hurricanes` AUTO_INCREMENT = 1;
|
||||
ALTER TABLE `hurricane_tracks` AUTO_INCREMENT = 1;
|
||||
|
||||
-- Populate data
|
||||
-- Hurricane Andrew (1992)
|
||||
INSERT INTO `hurricanes` (`name`, `year`) VALUES ('Andrew', 1992);
|
||||
SET @andrew_id = LAST_INSERT_ID();
|
||||
INSERT INTO `hurricane_tracks` (`hurricane_id`, `lat`, `lon`, `wind_speed_mph`, `timestamp`) VALUES
|
||||
(@andrew_id, 25.200000, -75.700000, 150, 1661299200),
|
||||
(@andrew_id, 25.400000, -77.700000, 160, 1661310000),
|
||||
(@andrew_id, 25.500000, -79.700000, 175, 1661320800),
|
||||
(@andrew_id, 25.600000, -81.700000, 165, 1661331600),
|
||||
(@andrew_id, 25.800000, -83.700000, 145, 1661342400),
|
||||
(@andrew_id, 26.200000, -85.700000, 135, 1661353200);
|
||||
|
||||
-- Hurricane Katrina (2005)
|
||||
INSERT INTO `hurricanes` (`name`, `year`) VALUES ('Katrina', 2005);
|
||||
SET @katrina_id = LAST_INSERT_ID();
|
||||
INSERT INTO `hurricane_tracks` (`hurricane_id`, `lat`, `lon`, `wind_speed_mph`, `timestamp`) VALUES
|
||||
(@katrina_id, 23.100000, -80.100000, 80, 1124928000),
|
||||
(@katrina_id, 24.500000, -81.800000, 95, 1124985600),
|
||||
(@katrina_id, 26.000000, -85.000000, 110, 1125043200),
|
||||
(@katrina_id, 28.200000, -89.600000, 175, 1125100800),
|
||||
(@katrina_id, 30.200000, -89.600000, 125, 1125158400),
|
||||
(@katrina_id, 31.100000, -89.600000, 75, 1125216000);
|
||||
|
||||
-- Hurricane Irma (2017)
|
||||
INSERT INTO `hurricanes` (`name`, `year`) VALUES ('Irma', 2017);
|
||||
SET @irma_id = LAST_INSERT_ID();
|
||||
INSERT INTO `hurricane_tracks` (`hurricane_id`, `lat`, `lon`, `wind_speed_mph`, `timestamp`) VALUES
|
||||
(@irma_id, 16.100000, -55.000000, 175, 1504656000),
|
||||
(@irma_id, 18.200000, -63.000000, 185, 1504742400),
|
||||
(@irma_id, 22.000000, -75.000000, 160, 1504828800),
|
||||
(@irma_id, 23.500000, -81.500000, 130, 1504915200),
|
||||
(@irma_id, 25.900000, -81.700000, 115, 1505001600),
|
||||
(@irma_id, 28.900000, -82.400000, 75, 1505088000);
|
||||
18
db/migrations/20251014_create_stochastic_events_table.sql
Normal file
18
db/migrations/20251014_create_stochastic_events_table.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS `stochastic_events` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`event_name` varchar(255) NOT NULL,
|
||||
`probability` float NOT NULL,
|
||||
`track_data` json NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Clear existing data to prevent duplicates on re-run
|
||||
TRUNCATE TABLE `stochastic_events`;
|
||||
|
||||
-- Sample Stochastic Events (variations of Irma)
|
||||
INSERT INTO `stochastic_events` (`event_name`, `probability`, `track_data`) VALUES
|
||||
('Irma-Track-01-SlightlyWest', 0.05, '[[-55.5,16.1,175],[-63.5,18.2,185],[-75.5,22,160],[-82,23.5,130],[-82.2,25.9,115],[-82.9,28.9,75]]'),
|
||||
('Irma-Track-02-Standard', 0.10, '[[-55,16.1,175],[-63,18.2,185],[-75,22,160],[-81.5,23.5,130],[-81.7,25.9,115],[-82.4,28.9,75]]'),
|
||||
('Irma-Track-03-SlightlyEast', 0.05, '[[-54.5,16.1,175],[-62.5,18.2,185],[-74.5,22,160],[-81,23.5,130],[-81.2,25.9,115],[-81.9,28.9,75]]'),
|
||||
('Irma-Track-04-Weaker', 0.15, '[[-55,16.1,140],[-63,18.2,148],[-75,22,128],[-81.5,23.5,104],[-81.7,25.9,92],[-82.4,28.9,60]]'),
|
||||
('Irma-Track-05-Stronger', 0.02, '[[-55,16.1,193],[-63,18.2,204],[-75,22,176],[-81.5,23.5,143],[-81.7,25.9,127],[-82.4,28.9,83]]');
|
||||
54
index.php
54
index.php
@ -7,6 +7,7 @@
|
||||
<title>Worldsphere.ai - 3D Weather Map</title>
|
||||
<link href="assets/cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="cesiumContainer"></div>
|
||||
@ -34,6 +35,59 @@
|
||||
<input type="range" id="windAltitudeSlider" min="0" max="15000" step="500" value="10000">
|
||||
<span id="windAltitudeLabel">10000 m</span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>Wind Animation</h3>
|
||||
<button id="playPauseWind">Pause</button>
|
||||
<label>
|
||||
Particle Density:
|
||||
<input type="range" id="particleDensitySlider" min="0.1" max="1.0" step="0.1" value="0.5">
|
||||
</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>CAT Simulation</h3>
|
||||
<label for="hurricane-select">Select Hurricane:</label>
|
||||
<select id="hurricane-select" name="hurricane-select">
|
||||
<option value="">Loading storms...</option>
|
||||
</select>
|
||||
<label for="portfolio-select">Sample Portfolio:</label>
|
||||
<select id="portfolio-select" name="portfolio-select">
|
||||
<option value="">Select a portfolio</option>
|
||||
<option value="assets/portfolios/fl_coastal.csv">Florida Coastal</option>
|
||||
<option value="assets/portfolios/fl_inland.csv">Florida Inland</option>
|
||||
<option value="assets/portfolios/la_coastal.csv">Louisiana Coastal</option>
|
||||
</select>
|
||||
<label for="portfolio-upload">Custom Portfolio (CSV):</label>
|
||||
<small>CSV must contain 'lat', 'lon', 'tiv', and optionally 'deductible' and 'limit' headers.</small>
|
||||
<input type="file" id="portfolio-upload" accept=".csv" />
|
||||
<div id="portfolio-status"></div>
|
||||
<button id="runCatSimulation">Run Simulation</button>
|
||||
<div id="cat-simulation-results" style="display: none;">
|
||||
<h4>Simulation Results</h4>
|
||||
<p>Total Loss: <span id="total-loss-value">$0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>Probabilistic Analysis</h3>
|
||||
<button id="runProbabilisticAnalysis">Run Analysis</button>
|
||||
<div id="probabilistic-results" style="display: none;">
|
||||
<h4>Analysis Results</h4>
|
||||
<p>Average Annual Loss (AAL): <span id="aal-value">$0</span></p>
|
||||
<canvas id="epCurveChart"></canvas>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Portfolio Comparison</h4>
|
||||
<label for="portfolio-upload-2">Portfolio 2 (CSV):</label>
|
||||
<input type="file" id="portfolio-upload-2" accept=".csv" />
|
||||
<div id="portfolio-status-2"></div>
|
||||
<button id="runComparison">Compare Portfolios</button>
|
||||
<div id="comparison-results" style="display: none;">
|
||||
<h4>Comparison Results</h4>
|
||||
<p>Portfolio 1 AAL: <span id="aal-value-1">$0</span></p>
|
||||
<p>Portfolio 2 AAL: <span id="aal-value-2">$0</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="assets/cesium/Build/Cesium/Cesium.js"></script>
|
||||
<script src="assets/js/wind.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user