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
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// URL for the NHC active hurricane KML data
|
try {
|
||||||
$nhc_kmz_url = 'https://www.nhc.noaa.gov/gis/kml/nhc.kmz';
|
$pdo = db();
|
||||||
|
|
||||||
// Temporary file to store the KMZ
|
if (isset($_GET['id'])) {
|
||||||
$tmp_kmz_file = tempnam(sys_get_temp_dir(), 'nhc_kmz');
|
// 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
|
if (!$hurricane) {
|
||||||
$kmz_data = file_get_contents($nhc_kmz_url);
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Hurricane not found']);
|
||||||
if ($kmz_data === false) {
|
exit;
|
||||||
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
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for LineString
|
$stmt = $pdo->prepare('SELECT lat, lon, wind_speed_mph FROM hurricane_tracks WHERE hurricane_id = ? ORDER BY timestamp ASC');
|
||||||
$linestring = $placemark->xpath('.//kml:LineString');
|
$stmt->execute([$hurricaneId]);
|
||||||
if ($linestring && isset($linestring[0]->coordinates)) {
|
$trackData = $stmt->fetchAll();
|
||||||
$coordinates_str = (string)$linestring[0]->coordinates;
|
|
||||||
$coordinates = parse_coordinates($coordinates_str);
|
// Format for existing frontend: [lon, lat, wind]
|
||||||
if (!empty($coordinates)) {
|
$formattedTrack = array_map(function($point) {
|
||||||
$features[] = [
|
return [(float)$point['lon'], (float)$point['lat'], (int)$point['wind_speed_mph']];
|
||||||
'name' => $name,
|
}, $trackData);
|
||||||
'type' => 'LineString',
|
|
||||||
'coordinates' => $coordinates
|
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);
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
} else {
|
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
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() {
|
function initializeGlobe() {
|
||||||
|
let epCurveChart = null; // To hold the chart instance
|
||||||
console.log('Cesium is defined, initializing globe.');
|
console.log('Cesium is defined, initializing globe.');
|
||||||
// Set your Cesium Ion default access token
|
// Set your Cesium Ion default access token
|
||||||
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjZTY0ZTQ1Yi0zYmYxLTQ5MjItODdkOS05ZDY0ZGRjYjQwM2QiLCJpZCI6MjA5ODgwLCJpYXQiOjE3MTM4MTY3OTB9.A-3Jt_G0K81s-A-XLpT2bn5aY2H3s-n2p-2jYf-i-g';
|
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjZTY0ZTQ1Yi0zYmYxLTQ5MjItODdkOS05ZDY0ZGRjYjQwM2QiLCJpZCI6MjA5ODgwLCJpYXQiOjE3MTM4MTY3OTB9.A-3Jt_G0K81s-A-XLpT2bn5aY2H3s-n2p-2jYf-i-g';
|
||||||
@ -81,6 +82,25 @@ function initializeGlobe() {
|
|||||||
windLayer.setOptions({ particleHeight: altitude });
|
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
|
// Function to load wildfire data
|
||||||
const loadWildfireData = async () => {
|
const loadWildfireData = async () => {
|
||||||
try {
|
try {
|
||||||
@ -190,6 +210,545 @@ function initializeGlobe() {
|
|||||||
loadWildfireData();
|
loadWildfireData();
|
||||||
loadSpcData();
|
loadSpcData();
|
||||||
loadWeatherAlerts();
|
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) {
|
} catch (error) {
|
||||||
console.error('A critical error occurred while initializing the Cesium viewer:', 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);
|
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 {
|
class ParticleSystem {
|
||||||
@ -221,4 +239,18 @@ class ParticleSystem {
|
|||||||
this.options = Object.assign(this.options, options);
|
this.options = Object.assign(this.options, options);
|
||||||
// Re-create particles or update properties as needed
|
// 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>
|
<title>Worldsphere.ai - 3D Weather Map</title>
|
||||||
<link href="assets/cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
|
<link href="assets/cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="cesiumContainer"></div>
|
<div id="cesiumContainer"></div>
|
||||||
@ -34,6 +35,59 @@
|
|||||||
<input type="range" id="windAltitudeSlider" min="0" max="15000" step="500" value="10000">
|
<input type="range" id="windAltitudeSlider" min="0" max="15000" step="500" value="10000">
|
||||||
<span id="windAltitudeLabel">10000 m</span>
|
<span id="windAltitudeLabel">10000 m</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<script src="assets/cesium/Build/Cesium/Cesium.js"></script>
|
<script src="assets/cesium/Build/Cesium/Cesium.js"></script>
|
||||||
<script src="assets/js/wind.js?v=<?php echo time(); ?>"></script>
|
<script src="assets/js/wind.js?v=<?php echo time(); ?>"></script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user