Working with bugs - wind toggle
This commit is contained in:
parent
3f8d192ba7
commit
b5f5bc5fc2
@ -8,35 +8,31 @@ try {
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
// Fetch track for a specific hurricane
|
||||
$hurricaneId = (int)$_GET['id'];
|
||||
$stmt = $pdo->prepare('SELECT name, year FROM hurricanes WHERE id = ?');
|
||||
$hurricaneId = $_GET['id'];
|
||||
$stmt = $pdo->prepare('SELECT name, season, lat, lon, wind_speed FROM hurricanes WHERE storm_id = ? ORDER BY iso_time ASC');
|
||||
$stmt->execute([$hurricaneId]);
|
||||
$hurricane = $stmt->fetch();
|
||||
$trackData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$hurricane) {
|
||||
if (!$trackData) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Hurricane not found']);
|
||||
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]
|
||||
$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);
|
||||
|
||||
echo json_encode([
|
||||
'name' => $hurricane['name'] . ' (' . $hurricane['year'] . ')',
|
||||
'name' => $trackData[0]['name'] . ' (' . $trackData[0]['season'] . ')',
|
||||
'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();
|
||||
$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(PDO::FETCH_ASSOC);
|
||||
echo json_encode($hurricanes);
|
||||
}
|
||||
|
||||
|
||||
1
api/wind.json
Normal file
1
api/wind.json
Normal 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]}]
|
||||
124
api/wind.php
124
api/wind.php
@ -4,80 +4,75 @@ ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
$cacheFile = __DIR__ . '/wind.json';
|
||||
$cacheTime = 3600; // 1 hour
|
||||
|
||||
$bulkFileName = 'weather_14.json.gz';
|
||||
$bulkUrl = "https://bulk.openweathermap.org/snapshot/{$bulkFileName}?appid=" . OWM_API_KEY;
|
||||
|
||||
// Use a stream context to capture HTTP status headers
|
||||
$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]);
|
||||
// Check if a cached file exists and is recent
|
||||
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
|
||||
header('Content-Type: application/json');
|
||||
readfile($cacheFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
$jsonData = gzdecode($gzData);
|
||||
if ($jsonData === false) {
|
||||
http_response_code(500);
|
||||
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)
|
||||
// --- Grid setup ---
|
||||
$nx = 36; // Grid points in longitude (every 10 degrees)
|
||||
$ny = 18; // Grid points in latitude (every 10 degrees)
|
||||
$lo1 = -180;
|
||||
$la1 = 90;
|
||||
$dx = 5;
|
||||
$dy = 5;
|
||||
$dx = 10;
|
||||
$dy = 10;
|
||||
|
||||
$uData = [];
|
||||
$vData = [];
|
||||
$uData = array_fill(0, $nx * $ny, 0);
|
||||
$vData = array_fill(0, $nx * $ny, 0);
|
||||
|
||||
// --- Build coordinate arrays for batch API call ---
|
||||
$lats = [];
|
||||
$lons = [];
|
||||
for ($j = 0; $j < $ny; $j++) {
|
||||
$lat = $la1 - $j * $dy;
|
||||
for ($i = 0; $i < $nx; $i++) {
|
||||
$lon = $lo1 + $i * $dx;
|
||||
$wind = interpolatePoint($lat, $lon, $cities);
|
||||
$uData[] = $wind['u'];
|
||||
$vData[] = $wind['v'];
|
||||
$lats[] = $lat;
|
||||
$lons[] = $lon;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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¤t_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 ---
|
||||
|
||||
$refTime = gmdate("Y-m-d\\TH:i:s.v\\Z");
|
||||
$refTime = gmdate("Y-m-d\\TH:i:s.vZ");
|
||||
|
||||
$formattedData = [
|
||||
[
|
||||
@ -112,13 +107,12 @@ $formattedData = [
|
||||
]
|
||||
];
|
||||
|
||||
// --- Caching and Output ---
|
||||
$cacheDir = dirname($cacheFile);
|
||||
if (!is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0755, true);
|
||||
}
|
||||
file_put_contents($cacheFile, json_encode($formattedData));
|
||||
$json_data = json_encode($formattedData);
|
||||
|
||||
echo json_encode($formattedData);
|
||||
// Cache the result
|
||||
file_put_contents($cacheFile, $json_data);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo $json_data;
|
||||
|
||||
?>
|
||||
@ -140,6 +140,8 @@ function initializeGlobe() {
|
||||
|
||||
spcDataSource.entities.removeAll();
|
||||
|
||||
const selectedSpcLevels = Array.from(document.querySelectorAll('.spc-checkbox:checked')).map(cb => cb.value);
|
||||
|
||||
const spcColors = {
|
||||
'TSTM': Cesium.Color.fromCssColorString('#00FF00').withAlpha(0.5), // General Thunderstorms
|
||||
'MRGL': Cesium.Color.fromCssColorString('#00C800').withAlpha(0.5), // Marginal
|
||||
@ -149,18 +151,22 @@ function initializeGlobe() {
|
||||
'HIGH': Cesium.Color.fromCssColorString('#FF00FF').withAlpha(0.5) // High
|
||||
};
|
||||
|
||||
spcData.forEach(feature => {
|
||||
const color = spcColors[feature.name] || Cesium.Color.GRAY.withAlpha(0.5);
|
||||
spcDataSource.entities.add({
|
||||
name: `SPC Outlook: ${feature.name}`,
|
||||
polygon: {
|
||||
hierarchy: Cesium.Cartesian3.fromDegreesArray(feature.coordinates),
|
||||
material: color,
|
||||
outline: true,
|
||||
outlineColor: Cesium.Color.BLACK
|
||||
if (Array.isArray(spcData)) {
|
||||
spcData.forEach(feature => {
|
||||
if (feature && feature.name && Array.isArray(feature.coordinates) && selectedSpcLevels.includes(feature.name)) {
|
||||
const color = spcColors[feature.name] || Cesium.Color.GRAY.withAlpha(0.5);
|
||||
spcDataSource.entities.add({
|
||||
name: `SPC Outlook: ${feature.name}`,
|
||||
polygon: {
|
||||
hierarchy: Cesium.Cartesian3.fromDegreesArray(feature.coordinates),
|
||||
material: color,
|
||||
outline: true,
|
||||
outlineColor: Cesium.Color.BLACK
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
console.log('SPC data source updated.');
|
||||
|
||||
} catch (error) {
|
||||
@ -180,23 +186,39 @@ function initializeGlobe() {
|
||||
|
||||
weatherAlertsDataSource.entities.removeAll();
|
||||
|
||||
if (alertsData.alerts) {
|
||||
alertsData.alerts.forEach(alert => {
|
||||
const alertColor = Cesium.Color.ORANGE.withAlpha(0.5);
|
||||
const selectedAlerts = Array.from(document.querySelectorAll('.alert-checkbox:checked')).map(cb => cb.value);
|
||||
|
||||
// The API provides polygons, so we need to handle them
|
||||
if (alert.geometry && alert.geometry.type === 'Polygon') {
|
||||
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
|
||||
const alertColors = {
|
||||
'tornado': Cesium.Color.RED.withAlpha(0.7),
|
||||
'thunderstorm': Cesium.Color.YELLOW.withAlpha(0.7),
|
||||
'flood': Cesium.Color.BLUE.withAlpha(0.7),
|
||||
'wind': Cesium.Color.CYAN.withAlpha(0.7),
|
||||
'snow_ice': Cesium.Color.WHITE.withAlpha(0.7),
|
||||
'fog': Cesium.Color.GRAY.withAlpha(0.7),
|
||||
'extreme_high_temperature': Cesium.Color.ORANGE.withAlpha(0.7)
|
||||
};
|
||||
|
||||
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
|
||||
loadWildfireData();
|
||||
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) {
|
||||
console.error('A critical error occurred while initializing the Cesium viewer:', error);
|
||||
const cesiumContainer = document.getElementById('cesiumContainer');
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
/*
|
||||
* Original code from https://github.com/RaymanNg/3D-Wind-Field
|
||||
* under the MIT license.
|
||||
@ -22,7 +21,7 @@
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@ -34,35 +33,41 @@ class WindLayer {
|
||||
this.ellipsoid = viewer.scene.globe.ellipsoid;
|
||||
this.options = options;
|
||||
this.windData = null;
|
||||
this.primitive = null;
|
||||
this.visible = true;
|
||||
this.particleSystem = null;
|
||||
this.visible = false;
|
||||
|
||||
this.init();
|
||||
// A promise that resolves when the layer is ready
|
||||
this.readyPromise = this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadWindData();
|
||||
if (this.windData) {
|
||||
this.particleSystem = new ParticleSystem(this.scene, {
|
||||
windData: this.windData,
|
||||
...this.options.particleSystem
|
||||
});
|
||||
this.primitive = this.particleSystem.primitive;
|
||||
this.scene.primitives.add(this.primitive);
|
||||
}
|
||||
init() {
|
||||
return this.loadWindData().then(() => {
|
||||
if (this.windData) {
|
||||
this.particleSystem = new ParticleSystem(this.scene, {
|
||||
windData: this.windData,
|
||||
...this.options.particleSystem
|
||||
});
|
||||
// Apply the stored visibility state once ready
|
||||
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() {
|
||||
try {
|
||||
const response = await fetch(this.options.windDataUrl || 'api/wind.php');
|
||||
const response = await fetch(this.options.windDataUrl || 'api/wind.php');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.windData = this.processWindData(data);
|
||||
console.log('Wind data loaded and processed.');
|
||||
} catch (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 windData = {
|
||||
return {
|
||||
nx: header.nx,
|
||||
ny: header.ny,
|
||||
lo1: header.lo1,
|
||||
@ -90,38 +95,48 @@ class WindLayer {
|
||||
u: uComponent.data,
|
||||
v: vComponent.data
|
||||
};
|
||||
return windData;
|
||||
}
|
||||
|
||||
setVisible(visible) {
|
||||
this.visible = visible;
|
||||
if (this.primitive) {
|
||||
this.primitive.show = visible;
|
||||
}
|
||||
// Use the promise to safely apply visibility
|
||||
this.readyPromise.then(() => {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.polylines.show = this.visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.applyOptions(options);
|
||||
}
|
||||
this.readyPromise.then(() => {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.applyOptions(options);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.particleSystem) {
|
||||
this.scene.preRender.removeEventListener(this.particleSystem.update, this.particleSystem);
|
||||
}
|
||||
this.readyPromise.then(() => {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.pause();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.particleSystem) {
|
||||
this.scene.preRender.addEventListener(this.particleSystem.update, this.particleSystem);
|
||||
}
|
||||
this.readyPromise.then(() => {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setParticleDensity(density) {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.setParticleCount(density);
|
||||
}
|
||||
this.readyPromise.then(() => {
|
||||
if (this.particleSystem) {
|
||||
this.particleSystem.setParticleCount(density);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,20 +145,34 @@ class ParticleSystem {
|
||||
this.scene = scene;
|
||||
this.options = options;
|
||||
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.primitive = null;
|
||||
|
||||
this.createParticles();
|
||||
this.createPrimitive();
|
||||
|
||||
this.scene.preRender.addEventListener(this.update, this);
|
||||
}
|
||||
|
||||
createParticles() {
|
||||
const options = this.options;
|
||||
const particleCount = options.particleCount || 10000;
|
||||
const particleCount = this.options.particleCount || 10000;
|
||||
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 {
|
||||
position: position,
|
||||
previousPosition: position, // Store previous position for the tail of the line
|
||||
age: Math.floor(Math.random() * (this.options.maxAge || 120)),
|
||||
maxAge: this.options.maxAge || 120,
|
||||
speed: Math.random() * (this.options.particleSpeed || 5)
|
||||
@ -179,11 +209,20 @@ class ParticleSystem {
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
@ -193,6 +232,9 @@ class ParticleSystem {
|
||||
const vx = wind.u * 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);
|
||||
cartographic.longitude += Cesium.Math.toRadians(vx);
|
||||
cartographic.latitude += Cesium.Math.toRadians(vy);
|
||||
@ -203,41 +245,25 @@ class ParticleSystem {
|
||||
cartographic.height
|
||||
);
|
||||
particle.age++;
|
||||
});
|
||||
|
||||
this.updatePrimitive();
|
||||
}
|
||||
|
||||
updatePrimitive() {
|
||||
const instances = this.particles.map(particle => {
|
||||
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);
|
||||
// Update polyline positions to create a line segment
|
||||
polyline.positions = [particle.previousPosition, particle.position];
|
||||
// Fade the line based on age
|
||||
if (polyline.material && polyline.material.uniforms) {
|
||||
polyline.material.uniforms.color = Cesium.Color.WHITE.withAlpha(1 - (particle.age / particle.maxAge));
|
||||
}
|
||||
}
|
||||
|
||||
this.primitive = new Cesium.Primitive({
|
||||
geometryInstances: instances,
|
||||
appearance: new Cesium.PolylineColorAppearance(),
|
||||
asynchronous: false
|
||||
});
|
||||
|
||||
this.scene.primitives.add(this.primitive);
|
||||
}
|
||||
|
||||
|
||||
applyOptions(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) {
|
||||
@ -247,10 +273,32 @@ class ParticleSystem {
|
||||
}
|
||||
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++) {
|
||||
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
132
index.php
@ -31,64 +31,96 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>Wind Altitude</h3>
|
||||
<input type="range" id="windAltitudeSlider" min="0" max="15000" step="500" value="10000">
|
||||
<span id="windAltitudeLabel">10000 m</span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>Wind Animation</h3>
|
||||
<button id="playPauseWind">Pause</button>
|
||||
<label>
|
||||
Particle Density:
|
||||
<input type="range" id="particleDensitySlider" min="0.1" max="1.0" step="0.1" value="0.5">
|
||||
</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>CAT Simulation</h3>
|
||||
<label for="hurricane-select">Select Hurricane:</label>
|
||||
<select id="hurricane-select" name="hurricane-select">
|
||||
<option value="">Loading storms...</option>
|
||||
<h3>Display Mode</h3>
|
||||
<select id="displayModeSelect">
|
||||
<option value="wind">Global Wind</option>
|
||||
<option value="cat">CAT Simulation</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 id="wind-controls">
|
||||
<div class="control-group">
|
||||
<h3>Wind Altitude</h3>
|
||||
<input type="range" id="windAltitudeSlider" min="0" max="15000" step="500" value="10000">
|
||||
<span id="windAltitudeLabel">10000 m</span>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3>Wind Animation</h3>
|
||||
<button id="playPauseWind">Pause</button>
|
||||
<label>
|
||||
Particle Density:
|
||||
<input type="range" id="particleDensitySlider" min="0.1" max="1.0" step="0.1" value="0.5">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<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 id="cat-controls" style="display: none;">
|
||||
<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>
|
||||
<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>
|
||||
<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 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>
|
||||
<script src="assets/cesium/Build/Cesium/Cesium.js"></script>
|
||||
<script src="assets/js/wind.js?v=<?php echo time(); ?>"></script>
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user