Not sure if its working but it has alot of stuff

This commit is contained in:
Flatlogic Bot 2025-10-14 04:07:36 +00:00
parent f7aa8776f6
commit e4f30e4fa3
11 changed files with 844 additions and 108 deletions

View File

@ -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
View 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()]);
}

View File

@ -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);

View File

@ -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());
}
}
}

View 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
1 lat lon tiv
2 25.7617 -80.1918 1500000
3 25.7743 -80.1902 2200000
4 25.7907 -80.1353 3000000
5 26.1224 -80.1373 1800000
6 26.1195 -80.1259 950000
7 26.1333 -80.1111 2500000
8 26.7153 -80.0534 4500000
9 26.7160 -80.0520 1200000
10 26.6558 -80.0455 750000
11 25.9880 -80.1220 1100000

View 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
1 lat lon tiv
2 28.5383 -81.3792 500000
3 28.5421 -81.3750 750000
4 28.4736 -81.4642 1200000
5 28.0395 -81.9498 450000
6 28.0836 -81.9618 600000
7 28.0550 -81.9300 850000
8 28.4044 -81.5761 950000
9 28.6139 -81.2089 400000

View 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
1 lat lon tiv
2 29.9511 -90.0715 1200000
3 29.9757 -90.0653 2000000
4 29.9289 -90.0922 800000
5 29.9547 -90.1011 3500000
6 29.8885 -90.0592 650000
7 30.0010 -90.1240 1500000
8 29.9696 -89.9969 900000

36
db/migrate.php Normal file
View 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");
}

View 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);

View 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]]');

View File

@ -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>