diff --git a/api/hurricanes.php b/api/hurricanes.php index 7186da6..64f4763 100644 --- a/api/hurricanes.php +++ b/api/hurricanes.php @@ -1,118 +1,47 @@ 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()]); } ?> \ No newline at end of file diff --git a/api/stochastic_events.php b/api/stochastic_events.php new file mode 100644 index 0000000..75deb93 --- /dev/null +++ b/api/stochastic_events.php @@ -0,0 +1,20 @@ +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()]); +} diff --git a/assets/js/main.js b/assets/js/main.js index 5708527..d7dd41b 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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 = ''; + } + } + + 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); \ No newline at end of file diff --git a/assets/js/wind.js b/assets/js/wind.js index 43123f6..18aae3f 100644 --- a/assets/js/wind.js +++ b/assets/js/wind.js @@ -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()); + } + } } diff --git a/assets/portfolios/fl_coastal.csv b/assets/portfolios/fl_coastal.csv new file mode 100644 index 0000000..6904645 --- /dev/null +++ b/assets/portfolios/fl_coastal.csv @@ -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 diff --git a/assets/portfolios/fl_inland.csv b/assets/portfolios/fl_inland.csv new file mode 100644 index 0000000..f5f74a1 --- /dev/null +++ b/assets/portfolios/fl_inland.csv @@ -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 diff --git a/assets/portfolios/la_coastal.csv b/assets/portfolios/la_coastal.csv new file mode 100644 index 0000000..d798b6f --- /dev/null +++ b/assets/portfolios/la_coastal.csv @@ -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 diff --git a/db/migrate.php b/db/migrate.php new file mode 100644 index 0000000..f583d68 --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,36 @@ +exec($statement); + } + } + echo "Applied successfully.\n"; + } + + echo "All migrations applied.\n"; + +} catch (PDOException $e) { + die("Database error: " . $e->getMessage() . "\n"); +} + diff --git a/db/migrations/20251014_create_hurricanes_table.sql b/db/migrations/20251014_create_hurricanes_table.sql new file mode 100644 index 0000000..aada1a3 --- /dev/null +++ b/db/migrations/20251014_create_hurricanes_table.sql @@ -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); diff --git a/db/migrations/20251014_create_stochastic_events_table.sql b/db/migrations/20251014_create_stochastic_events_table.sql new file mode 100644 index 0000000..afb9f2f --- /dev/null +++ b/db/migrations/20251014_create_stochastic_events_table.sql @@ -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]]'); diff --git a/index.php b/index.php index 2bc5ff2..b48dc6a 100644 --- a/index.php +++ b/index.php @@ -7,6 +7,7 @@ Worldsphere.ai - 3D Weather Map +
@@ -34,6 +35,59 @@ 10000 m +
+

Wind Animation

+ + +
+
+

CAT Simulation

+ + + + + + CSV must contain 'lat', 'lon', 'tiv', and optionally 'deductible' and 'limit' headers. + +
+ + +
+
+

Probabilistic Analysis

+ + + +
+ +

Portfolio Comparison

+ + +
+ + +