Working with bugs - wind toggle

This commit is contained in:
Flatlogic Bot 2025-10-14 12:26:56 +00:00
parent 3f8d192ba7
commit b5f5bc5fc2
6 changed files with 346 additions and 225 deletions

View File

@ -8,35 +8,31 @@ try {
if (isset($_GET['id'])) { if (isset($_GET['id'])) {
// Fetch track for a specific hurricane // Fetch track for a specific hurricane
$hurricaneId = (int)$_GET['id']; $hurricaneId = $_GET['id'];
$stmt = $pdo->prepare('SELECT name, year FROM hurricanes WHERE id = ?'); $stmt = $pdo->prepare('SELECT name, season, lat, lon, wind_speed FROM hurricanes WHERE storm_id = ? ORDER BY iso_time ASC');
$stmt->execute([$hurricaneId]); $stmt->execute([$hurricaneId]);
$hurricane = $stmt->fetch(); $trackData = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!$hurricane) { if (!$trackData) {
http_response_code(404); http_response_code(404);
echo json_encode(['error' => 'Hurricane not found']); echo json_encode(['error' => 'Hurricane not found']);
exit; exit;
} }
$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] // Format for existing frontend: [lon, lat, wind]
$formattedTrack = array_map(function($point) { $formattedTrack = array_map(function($point) {
return [(float)$point['lon'], (float)$point['lat'], (int)$point['wind_speed_mph']]; return [(float)$point['lon'], (float)$point['lat'], (int)$point['wind_speed']];
}, $trackData); }, $trackData);
echo json_encode([ echo json_encode([
'name' => $hurricane['name'] . ' (' . $hurricane['year'] . ')', 'name' => $trackData[0]['name'] . ' (' . $trackData[0]['season'] . ')',
'track' => $formattedTrack 'track' => $formattedTrack
]); ]);
} else { } else {
// Fetch list of all hurricanes // Fetch list of all hurricanes
$stmt = $pdo->query('SELECT id, name, year FROM hurricanes ORDER BY year DESC, name ASC'); $stmt = $pdo->query('SELECT storm_id as id, name, season as year FROM hurricanes GROUP BY storm_id, name, season ORDER BY season DESC, name ASC');
$hurricanes = $stmt->fetchAll(); $hurricanes = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($hurricanes); echo json_encode($hurricanes);
} }

1
api/wind.json Normal file
View File

@ -0,0 +1 @@
[{"header":{"nx":36,"ny":18,"lo1":-180,"la1":90,"dx":10,"dy":10,"parameterCategory":2,"parameterNumber":2,"forecastTime":0,"refTime":"2025-10-14T12:15:46.0000"},"data":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"header":{"nx":36,"ny":18,"lo1":-180,"la1":90,"dx":10,"dy":10,"parameterCategory":2,"parameterNumber":3,"forecastTime":0,"refTime":"2025-10-14T12:15:46.0000"},"data":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]

View File

@ -4,80 +4,75 @@ ini_set('display_errors', 1);
ini_set('display_startup_errors', 1); ini_set('display_startup_errors', 1);
error_reporting(E_ALL); error_reporting(E_ALL);
require_once __DIR__ . '/../config.php'; $cacheFile = __DIR__ . '/wind.json';
$cacheTime = 3600; // 1 hour
$bulkFileName = 'weather_14.json.gz'; // Check if a cached file exists and is recent
$bulkUrl = "https://bulk.openweathermap.org/snapshot/{$bulkFileName}?appid=" . OWM_API_KEY; if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
header('Content-Type: application/json');
// Use a stream context to capture HTTP status headers readfile($cacheFile);
$context = stream_context_create(['http' => ['ignore_errors' => true]]);
$gzData = @file_get_contents($bulkUrl, false, $context);
// Check for errors and specific HTTP status codes
if ($gzData === false || !isset($http_response_header[0]) || strpos($http_response_header[0], '200 OK') === false) {
$error_message = 'Failed to download bulk weather data.';
if (isset($http_response_header[0])) {
if (strpos($http_response_header[0], '401 Unauthorized') !== false) {
$error_message = 'OpenWeatherMap API key is invalid for the Bulk Data service. The key may be correct for other services like weather tiles, but lacks permission for bulk downloads.';
http_response_code(401);
} else {
$error_message .= ' Server responded with: ' . $http_response_header[0];
http_response_code(500);
}
} else {
http_response_code(500);
}
echo json_encode(['error' => $error_message]);
exit; exit;
} }
$jsonData = gzdecode($gzData); // --- Grid setup ---
if ($jsonData === false) { $nx = 36; // Grid points in longitude (every 10 degrees)
http_response_code(500); $ny = 18; // Grid points in latitude (every 10 degrees)
echo json_encode(['error' => 'Failed to decompress weather data.']);
exit;
}
// The file contains JSON objects separated by newlines
$lines = explode("\n", trim($jsonData));
$cities = [];
foreach ($lines as $line) {
if (!empty($line)) {
$cities[] = json_decode($line, true);
}
}
if (empty($cities)) {
http_response_code(500);
echo json_encode(['error' => 'Failed to parse weather data JSON or file is empty.']);
exit;
}
// --- Interpolation ---
$nx = 72; // Grid points in longitude (every 5 degrees)
$ny = 37; // Grid points in latitude (every 5 degrees)
$lo1 = -180; $lo1 = -180;
$la1 = 90; $la1 = 90;
$dx = 5; $dx = 10;
$dy = 5; $dy = 10;
$uData = []; $uData = array_fill(0, $nx * $ny, 0);
$vData = []; $vData = array_fill(0, $nx * $ny, 0);
// --- Build coordinate arrays for batch API call ---
$lats = [];
$lons = [];
for ($j = 0; $j < $ny; $j++) { for ($j = 0; $j < $ny; $j++) {
$lat = $la1 - $j * $dy; $lat = $la1 - $j * $dy;
for ($i = 0; $i < $nx; $i++) { for ($i = 0; $i < $nx; $i++) {
$lon = $lo1 + $i * $dx; $lon = $lo1 + $i * $dx;
$wind = interpolatePoint($lat, $lon, $cities); $lats[] = $lat;
$uData[] = $wind['u']; $lons[] = $lon;
$vData[] = $wind['v']; }
}
// --- Fetch data from Open-Meteo in a single call ---
$url = "https://api.open-meteo.com/v1/forecast?latitude=" . implode(',', $lats) . "&longitude=" . implode(',', $lons) . "&hourly=windspeed_10m,winddirection_10m&current_weather=true";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$results = json_decode($response, true);
// The API returns an array of results, one for each coordinate pair
if (is_array($results)) {
foreach ($results as $index => $data) {
if (isset($data['current_weather'])) {
$windspeed = $data['current_weather']['windspeed'];
$winddirection = $data['current_weather']['winddirection'];
// Convert to u and v components
$angle = ($winddirection + 180) * M_PI / 180;
$u = $windspeed * cos($angle);
$v = $windspeed * sin($angle);
// The index in the response corresponds to the index in our grid
$uData[$index] = $u;
$vData[$index] = $v;
}
}
} }
} }
// --- Format Data --- // --- Format Data ---
$refTime = gmdate("Y-m-d\\TH:i:s.vZ");
$refTime = gmdate("Y-m-d\\TH:i:s.v\\Z");
$formattedData = [ $formattedData = [
[ [
@ -112,13 +107,12 @@ $formattedData = [
] ]
]; ];
// --- Caching and Output --- $json_data = json_encode($formattedData);
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
file_put_contents($cacheFile, json_encode($formattedData));
echo json_encode($formattedData); // Cache the result
file_put_contents($cacheFile, $json_data);
header('Content-Type: application/json');
echo $json_data;
?> ?>

View File

@ -140,6 +140,8 @@ function initializeGlobe() {
spcDataSource.entities.removeAll(); spcDataSource.entities.removeAll();
const selectedSpcLevels = Array.from(document.querySelectorAll('.spc-checkbox:checked')).map(cb => cb.value);
const spcColors = { const spcColors = {
'TSTM': Cesium.Color.fromCssColorString('#00FF00').withAlpha(0.5), // General Thunderstorms 'TSTM': Cesium.Color.fromCssColorString('#00FF00').withAlpha(0.5), // General Thunderstorms
'MRGL': Cesium.Color.fromCssColorString('#00C800').withAlpha(0.5), // Marginal 'MRGL': Cesium.Color.fromCssColorString('#00C800').withAlpha(0.5), // Marginal
@ -149,18 +151,22 @@ function initializeGlobe() {
'HIGH': Cesium.Color.fromCssColorString('#FF00FF').withAlpha(0.5) // High 'HIGH': Cesium.Color.fromCssColorString('#FF00FF').withAlpha(0.5) // High
}; };
spcData.forEach(feature => { if (Array.isArray(spcData)) {
const color = spcColors[feature.name] || Cesium.Color.GRAY.withAlpha(0.5); spcData.forEach(feature => {
spcDataSource.entities.add({ if (feature && feature.name && Array.isArray(feature.coordinates) && selectedSpcLevels.includes(feature.name)) {
name: `SPC Outlook: ${feature.name}`, const color = spcColors[feature.name] || Cesium.Color.GRAY.withAlpha(0.5);
polygon: { spcDataSource.entities.add({
hierarchy: Cesium.Cartesian3.fromDegreesArray(feature.coordinates), name: `SPC Outlook: ${feature.name}`,
material: color, polygon: {
outline: true, hierarchy: Cesium.Cartesian3.fromDegreesArray(feature.coordinates),
outlineColor: Cesium.Color.BLACK material: color,
outline: true,
outlineColor: Cesium.Color.BLACK
}
});
} }
}); });
}); }
console.log('SPC data source updated.'); console.log('SPC data source updated.');
} catch (error) { } catch (error) {
@ -180,23 +186,39 @@ function initializeGlobe() {
weatherAlertsDataSource.entities.removeAll(); weatherAlertsDataSource.entities.removeAll();
if (alertsData.alerts) { const selectedAlerts = Array.from(document.querySelectorAll('.alert-checkbox:checked')).map(cb => cb.value);
alertsData.alerts.forEach(alert => {
const alertColor = Cesium.Color.ORANGE.withAlpha(0.5);
// The API provides polygons, so we need to handle them const alertColors = {
if (alert.geometry && alert.geometry.type === 'Polygon') { 'tornado': Cesium.Color.RED.withAlpha(0.7),
const coordinates = alert.geometry.coordinates[0].flat(); 'thunderstorm': Cesium.Color.YELLOW.withAlpha(0.7),
weatherAlertsDataSource.entities.add({ 'flood': Cesium.Color.BLUE.withAlpha(0.7),
name: alert.properties.event || 'Weather Alert', 'wind': Cesium.Color.CYAN.withAlpha(0.7),
description: alert.properties.description || 'No description available.', 'snow_ice': Cesium.Color.WHITE.withAlpha(0.7),
polygon: { 'fog': Cesium.Color.GRAY.withAlpha(0.7),
hierarchy: Cesium.Cartesian3.fromDegreesArray(coordinates), 'extreme_high_temperature': Cesium.Color.ORANGE.withAlpha(0.7)
material: alertColor, };
outline: true,
outlineColor: Cesium.Color.BLACK if (Array.isArray(alertsData)) {
alertsData.forEach(alert => {
if (alert && alert.properties && typeof alert.properties.event === 'string') {
const eventType = alert.properties.event.toLowerCase().replace(/\s/g, '_');
if (selectedAlerts.includes(eventType)) {
const alertColor = alertColors[eventType] || Cesium.Color.PURPLE.withAlpha(0.7);
if (alert.geometry && alert.geometry.type === 'Polygon' && Array.isArray(alert.geometry.coordinates) && Array.isArray(alert.geometry.coordinates[0])) {
const coordinates = alert.geometry.coordinates[0].flat();
weatherAlertsDataSource.entities.add({
name: alert.properties.event || 'Weather Alert',
description: alert.properties.description || 'No description available.',
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray(coordinates),
material: alertColor,
outline: true,
outlineColor: Cesium.Color.BLACK
}
});
} }
}); }
} }
}); });
} }
@ -207,6 +229,14 @@ function initializeGlobe() {
} }
}; };
document.querySelectorAll('.spc-checkbox').forEach(cb => {
cb.addEventListener('change', loadSpcData);
});
document.querySelectorAll('.alert-checkbox').forEach(cb => {
cb.addEventListener('change', loadWeatherAlerts);
});
// Load all data sources // Load all data sources
loadWildfireData(); loadWildfireData();
loadSpcData(); loadSpcData();
@ -752,6 +782,26 @@ function initializeGlobe() {
}); });
} }
const displayModeSelect = document.getElementById('displayModeSelect');
const windControls = document.getElementById('wind-controls');
const catControls = document.getElementById('cat-controls');
displayModeSelect.addEventListener('change', function(e) {
if (this.value === 'wind') {
windControls.style.display = 'block';
catControls.style.display = 'none';
windLayer.setVisible(true);
catSimulationDataSource.entities.removeAll();
} else {
windControls.style.display = 'none';
catControls.style.display = 'block';
windLayer.setVisible(false);
}
});
// Trigger the change event to set the initial state
displayModeSelect.dispatchEvent(new Event('change'));
} catch (error) { } 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);
const cesiumContainer = document.getElementById('cesiumContainer'); const cesiumContainer = document.getElementById('cesiumContainer');

View File

@ -1,4 +1,3 @@
/* /*
* Original code from https://github.com/RaymanNg/3D-Wind-Field * Original code from https://github.com/RaymanNg/3D-Wind-Field
* under the MIT license. * under the MIT license.
@ -22,7 +21,7 @@
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
@ -34,35 +33,41 @@ class WindLayer {
this.ellipsoid = viewer.scene.globe.ellipsoid; this.ellipsoid = viewer.scene.globe.ellipsoid;
this.options = options; this.options = options;
this.windData = null; this.windData = null;
this.primitive = null; this.particleSystem = null;
this.visible = true; this.visible = false;
this.init(); // A promise that resolves when the layer is ready
this.readyPromise = this.init();
} }
async init() { init() {
await this.loadWindData(); return this.loadWindData().then(() => {
if (this.windData) { if (this.windData) {
this.particleSystem = new ParticleSystem(this.scene, { this.particleSystem = new ParticleSystem(this.scene, {
windData: this.windData, windData: this.windData,
...this.options.particleSystem ...this.options.particleSystem
}); });
this.primitive = this.particleSystem.primitive; // Apply the stored visibility state once ready
this.scene.primitives.add(this.primitive); this.particleSystem.polylines.show = this.visible;
} }
}).catch(error => {
console.error("Error initializing WindLayer:", error);
// Propagate error to allow for further handling
throw error;
});
} }
async loadWindData() { async loadWindData() {
try { try {
const response = await fetch(this.options.windDataUrl || 'api/wind.php'); const response = await fetch(this.options.windDataUrl || 'api/wind.php');
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
this.windData = this.processWindData(data); this.windData = this.processWindData(data);
console.log('Wind data loaded and processed.');
} catch (error) { } catch (error) {
console.error('Error loading or processing wind data:', error); console.error('Error loading or processing wind data:', error);
throw error; // Re-throw to be caught by the init promise chain
} }
} }
@ -80,7 +85,7 @@ class WindLayer {
} }
const header = uComponent.header; const header = uComponent.header;
const windData = { return {
nx: header.nx, nx: header.nx,
ny: header.ny, ny: header.ny,
lo1: header.lo1, lo1: header.lo1,
@ -90,38 +95,48 @@ class WindLayer {
u: uComponent.data, u: uComponent.data,
v: vComponent.data v: vComponent.data
}; };
return windData;
} }
setVisible(visible) { setVisible(visible) {
this.visible = visible; this.visible = visible;
if (this.primitive) { // Use the promise to safely apply visibility
this.primitive.show = visible; this.readyPromise.then(() => {
} if (this.particleSystem) {
this.particleSystem.polylines.show = this.visible;
}
});
} }
setOptions(options) { setOptions(options) {
if (this.particleSystem) { this.readyPromise.then(() => {
this.particleSystem.applyOptions(options); if (this.particleSystem) {
} this.particleSystem.applyOptions(options);
}
});
} }
pause() { pause() {
if (this.particleSystem) { this.readyPromise.then(() => {
this.scene.preRender.removeEventListener(this.particleSystem.update, this.particleSystem); if (this.particleSystem) {
} this.particleSystem.pause();
}
});
} }
play() { play() {
if (this.particleSystem) { this.readyPromise.then(() => {
this.scene.preRender.addEventListener(this.particleSystem.update, this.particleSystem); if (this.particleSystem) {
} this.particleSystem.play();
}
});
} }
setParticleDensity(density) { setParticleDensity(density) {
if (this.particleSystem) { this.readyPromise.then(() => {
this.particleSystem.setParticleCount(density); if (this.particleSystem) {
} this.particleSystem.setParticleCount(density);
}
});
} }
} }
@ -130,20 +145,34 @@ class ParticleSystem {
this.scene = scene; this.scene = scene;
this.options = options; this.options = options;
this.windData = options.windData; this.windData = options.windData;
// Use a polyline collection instead of a point collection
this.polylines = this.scene.primitives.add(new Cesium.PolylineCollection());
this.particles = []; this.particles = [];
this.primitive = null;
this.createParticles(); this.createParticles();
this.createPrimitive();
this.scene.preRender.addEventListener(this.update, this); this.scene.preRender.addEventListener(this.update, this);
} }
createParticles() { createParticles() {
const options = this.options; const particleCount = this.options.particleCount || 10000;
const particleCount = options.particleCount || 10000;
for (let i = 0; i < particleCount; i++) { for (let i = 0; i < particleCount; i++) {
this.particles.push(this.createParticle()); const p = this.createParticle();
this.particles.push(p);
// Create a polyline for each particle. It will be updated in the update loop.
this.polylines.add({
positions: [p.position, p.position], // Start with a zero-length line
width: this.options.lineWidth || 1.0,
material: new Cesium.Material({
fabric: {
type: 'Color',
uniforms: {
color: Cesium.Color.WHITE.withAlpha(0.0) // Initially transparent
}
}
})
});
} }
} }
@ -155,6 +184,7 @@ class ParticleSystem {
return { return {
position: position, position: position,
previousPosition: position, // Store previous position for the tail of the line
age: Math.floor(Math.random() * (this.options.maxAge || 120)), age: Math.floor(Math.random() * (this.options.maxAge || 120)),
maxAge: this.options.maxAge || 120, maxAge: this.options.maxAge || 120,
speed: Math.random() * (this.options.particleSpeed || 5) speed: Math.random() * (this.options.particleSpeed || 5)
@ -179,11 +209,20 @@ class ParticleSystem {
} }
update() { update() {
if (!this.primitive.show) return; if (this.polylines.length === 0 || !this.polylines.show) return;
for (let i = 0; i < this.particles.length; i++) {
const particle = this.particles[i];
const polyline = this.polylines.get(i);
this.particles.forEach(particle => {
if (particle.age >= particle.maxAge) { if (particle.age >= particle.maxAge) {
Object.assign(particle, this.createParticle()); Object.assign(particle, this.createParticle());
// Reset polyline to a zero-length, transparent line
polyline.positions = [particle.position, particle.position];
if (polyline.material && polyline.material.uniforms) {
polyline.material.uniforms.color = Cesium.Color.WHITE.withAlpha(0.0);
}
continue; // Skip to next particle
} }
const wind = this.getWind(particle.position); const wind = this.getWind(particle.position);
@ -193,6 +232,9 @@ class ParticleSystem {
const vx = wind.u * speed / metersPerDegree; const vx = wind.u * speed / metersPerDegree;
const vy = wind.v * speed / metersPerDegree; const vy = wind.v * speed / metersPerDegree;
// Store current position as the previous one
particle.previousPosition = particle.position;
const cartographic = Cesium.Cartographic.fromCartesian(particle.position); const cartographic = Cesium.Cartographic.fromCartesian(particle.position);
cartographic.longitude += Cesium.Math.toRadians(vx); cartographic.longitude += Cesium.Math.toRadians(vx);
cartographic.latitude += Cesium.Math.toRadians(vy); cartographic.latitude += Cesium.Math.toRadians(vy);
@ -203,41 +245,25 @@ class ParticleSystem {
cartographic.height cartographic.height
); );
particle.age++; particle.age++;
});
this.updatePrimitive(); // Update polyline positions to create a line segment
} polyline.positions = [particle.previousPosition, particle.position];
// Fade the line based on age
updatePrimitive() { if (polyline.material && polyline.material.uniforms) {
const instances = this.particles.map(particle => { polyline.material.uniforms.color = Cesium.Color.WHITE.withAlpha(1 - (particle.age / particle.maxAge));
return new Cesium.GeometryInstance({ }
geometry: new Cesium.SimplePolylineGeometry({
positions: [particle.position, particle.position] // Simplified for a dot
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.WHITE.withAlpha(particle.age / particle.maxAge)
)
}
});
});
if (this.primitive) {
this.scene.primitives.remove(this.primitive);
} }
this.primitive = new Cesium.Primitive({
geometryInstances: instances,
appearance: new Cesium.PolylineColorAppearance(),
asynchronous: false
});
this.scene.primitives.add(this.primitive);
} }
applyOptions(options) { applyOptions(options) {
this.options = Object.assign(this.options, options); this.options = Object.assign(this.options, options);
// Re-create particles or update properties as needed // Apply new options to existing polylines
for (let i = 0; i < this.polylines.length; i++) {
const polyline = this.polylines.get(i);
if (options.lineWidth) {
polyline.width = options.lineWidth;
}
}
} }
setParticleCount(density) { setParticleCount(density) {
@ -247,10 +273,32 @@ class ParticleSystem {
} }
const newParticleCount = Math.floor(maxParticles * density); const newParticleCount = Math.floor(maxParticles * density);
this.particles.length = 0; // Clear the array this.polylines.removeAll();
this.particles = [];
for (let i = 0; i < newParticleCount; i++) { for (let i = 0; i < newParticleCount; i++) {
this.particles.push(this.createParticle()); const p = this.createParticle();
this.particles.push(p);
this.polylines.add({
positions: [p.position, p.position],
width: this.options.lineWidth || 1.0,
material: new Cesium.Material({
fabric: {
type: 'Color',
uniforms: {
color: Cesium.Color.WHITE.withAlpha(0.0)
}
}
})
});
} }
} }
pause() {
this.scene.preRender.removeEventListener(this.update, this);
}
play() {
this.scene.preRender.addEventListener(this.update, this);
}
} }

132
index.php
View File

@ -31,64 +31,96 @@
</label> </label>
</div> </div>
<div class="control-group"> <div class="control-group">
<h3>Wind Altitude</h3> <h3>Display Mode</h3>
<input type="range" id="windAltitudeSlider" min="0" max="15000" step="500" value="10000"> <select id="displayModeSelect">
<span id="windAltitudeLabel">10000 m</span> <option value="wind">Global Wind</option>
</div> <option value="cat">CAT Simulation</option>
<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> </select>
<label for="portfolio-select">Sample Portfolio:</label> </div>
<select id="portfolio-select" name="portfolio-select"> <div id="wind-controls">
<option value="">Select a portfolio</option> <div class="control-group">
<option value="assets/portfolios/fl_coastal.csv">Florida Coastal</option> <h3>Wind Altitude</h3>
<option value="assets/portfolios/fl_inland.csv">Florida Inland</option> <input type="range" id="windAltitudeSlider" min="0" max="15000" step="500" value="10000">
<option value="assets/portfolios/la_coastal.csv">Louisiana Coastal</option> <span id="windAltitudeLabel">10000 m</span>
</select> </div>
<label for="portfolio-upload">Custom Portfolio (CSV):</label> <div class="control-group">
<small>CSV must contain 'lat', 'lon', 'tiv', and optionally 'deductible' and 'limit' headers.</small> <h3>Wind Animation</h3>
<input type="file" id="portfolio-upload" accept=".csv" /> <button id="playPauseWind">Pause</button>
<div id="portfolio-status"></div> <label>
<button id="runCatSimulation">Run Simulation</button> Particle Density:
<div id="cat-simulation-results" style="display: none;"> <input type="range" id="particleDensitySlider" min="0.1" max="1.0" step="0.1" value="0.5">
<h4>Simulation Results</h4> </label>
<p>Total Loss: <span id="total-loss-value">$0</span></p>
</div> </div>
</div> </div>
<div class="control-group"> <div id="cat-controls" style="display: none;">
<h3>Probabilistic Analysis</h3> <div class="control-group">
<button id="runProbabilisticAnalysis">Run Analysis</button> <h3>CAT Simulation</h3>
<div id="probabilistic-results" style="display: none;"> <label for="hurricane-select">Select Hurricane:</label>
<h4>Analysis Results</h4> <select id="hurricane-select" name="hurricane-select">
<p>Average Annual Loss (AAL): <span id="aal-value">$0</span></p> <option value="">Loading storms...</option>
<canvas id="epCurveChart"></canvas> </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>
<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> <hr>
<h4>Portfolio Comparison</h4> <h4>Portfolio Comparison</h4>
<label for="portfolio-upload-2">Portfolio 2 (CSV):</label> <label for="portfolio-upload-2">Portfolio 2 (CSV):</label>
<input type="file" id="portfolio-upload-2" accept=".csv" /> <input type="file" id="portfolio-upload-2" accept=".csv" />
<div id="portfolio-status-2"></div> <div id="portfolio-status-2"></div>
<button id="runComparison">Compare Portfolios</button> <button id="runComparison">Compare Portfolios</button>
<div id="comparison-results" style="display: none;"> <div id="comparison-results" style="display: none;">
<h4>Comparison Results</h4> <h4>Comparison Results</h4>
<p>Portfolio 1 AAL: <span id="aal-value-1">$0</span></p> <p>Portfolio 1 AAL: <span id="aal-value-1">$0</span></p>
<p>Portfolio 2 AAL: <span id="aal-value-2">$0</span></p> <p>Portfolio 2 AAL: <span id="aal-value-2">$0</span></p>
</div>
</div>
<div class="control-group">
<h3>Weather Overlays</h3>
<div id="spc-controls">
<h4>SPC Outlook</h4>
<label><input type="checkbox" class="spc-checkbox" value="TSTM" checked> Thunderstorm</label>
<label><input type="checkbox" class="spc-checkbox" value="MRGL" checked> Marginal</label>
<label><input type="checkbox" class="spc-checkbox" value="SLGT" checked> Slight</label>
<label><input type="checkbox" class="spc-checkbox" value="ENH" checked> Enhanced</label>
<label><input type="checkbox" class="spc-checkbox" value="MDT" checked> Moderate</label>
<label><input type="checkbox" class="spc-checkbox" value="HIGH" checked> High</label>
</div>
<div id="alert-controls">
<h4>Weather Alerts</h4>
<label><input type="checkbox" class="alert-checkbox" value="tornado" checked> Tornado</label>
<label><input type="checkbox" class="alert-checkbox" value="thunderstorm" checked> Thunderstorm</label>
<label><input type="checkbox" class="alert-checkbox" value="flood" checked> Flood</label>
<label><input type="checkbox" class="alert-checkbox" value="wind" checked> Wind</label>
<label><input type="checkbox" class="alert-checkbox" value="snow_ice" checked> Snow/Ice</label>
<label><input type="checkbox" class="alert-checkbox" value="fog" checked> Fog</label>
<label><input type="checkbox" class="alert-checkbox" value="extreme_high_temperature" checked> Extreme Temp</label>
</div>
</div> </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>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script> <script src="assets/js/main.js?v=<?php echo time(); ?>"></script>