diff --git a/api/hurricanes.php b/api/hurricanes.php index 64f4763..5678950 100644 --- a/api/hurricanes.php +++ b/api/hurricanes.php @@ -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); } diff --git a/api/wind.json b/api/wind.json new file mode 100644 index 0000000..ebd841d --- /dev/null +++ b/api/wind.json @@ -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]}] \ No newline at end of file diff --git a/api/wind.php b/api/wind.php index 15db656..f7adfc0 100644 --- a/api/wind.php +++ b/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; ?> \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index cb1b0d6..8dc7f86 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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'); diff --git a/assets/js/wind.js b/assets/js/wind.js index 18aae3f..87795ce 100644 --- a/assets/js/wind.js +++ b/assets/js/wind.js @@ -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); + } } diff --git a/index.php b/index.php index b48dc6a..bdc06b6 100644 --- a/index.php +++ b/index.php @@ -31,64 +31,96 @@
-

Wind Altitude

- - 10000 m -
-
-

Wind Animation

- - -
-
-

CAT Simulation

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

Wind Altitude

+ + 10000 m +
+
+

Wind Animation

+ +
-
-

Probabilistic Analysis

- -