wind plot
This commit is contained in:
parent
88f77eda28
commit
f7aa8776f6
91
api/gfs.php
91
api/gfs.php
@ -1,91 +0,0 @@
|
|||||||
<?php
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
ini_set('display_startup_errors', 1);
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
// --- Configuration ---
|
|
||||||
$gfs_url = 'https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod/gfs.20251013/00/atmos/gfs.t00z.pgrb2.0p25.f000';
|
|
||||||
$tmp_dir = '/tmp';
|
|
||||||
$grib_file = $tmp_dir . '/' . basename($gfs_url);
|
|
||||||
$xyz_file = $tmp_dir . '/gfs.xyz';
|
|
||||||
|
|
||||||
// --- Download the GFS file ---
|
|
||||||
if (!file_exists($grib_file)) {
|
|
||||||
$fp = fopen($grib_file, 'w');
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $gfs_url);
|
|
||||||
curl_setopt($ch, CURLOPT_FILE, $fp);
|
|
||||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
|
|
||||||
curl_exec($ch);
|
|
||||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
fclose($fp);
|
|
||||||
|
|
||||||
if ($http_code !== 200) {
|
|
||||||
unlink($grib_file); // Clean up failed download
|
|
||||||
echo json_encode(['error' => 'Failed to download GFS file', 'http_code' => $http_code]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Convert GRIB to XYZ using gdal_translate ---
|
|
||||||
// We'll use band 1 for this example. You can use gdalinfo to see available bands.
|
|
||||||
$gdal_command = 'gdal_translate -b 1 -of XYZ ' . escapeshellarg($grib_file) . ' ' . escapeshellarg($xyz_file);
|
|
||||||
$gdal_output = shell_exec($gdal_command);
|
|
||||||
|
|
||||||
if (!file_exists($xyz_file)) {
|
|
||||||
echo json_encode(['error' => 'Failed to convert GRIB to XYZ', 'gdal_output' => $gdal_output]);
|
|
||||||
unlink($grib_file);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Read the XYZ file and create GeoJSON features ---
|
|
||||||
$features = [];
|
|
||||||
$handle = fopen($xyz_file, 'r');
|
|
||||||
$line_count = 0;
|
|
||||||
$sample_rate = 100; // Process 1 in every 100 lines
|
|
||||||
|
|
||||||
if ($handle) {
|
|
||||||
while (($line = fgets($handle)) !== false) {
|
|
||||||
$line_count++;
|
|
||||||
if ($line_count % $sample_rate !== 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = preg_split('/\s+/', trim($line));
|
|
||||||
if (count($parts) === 3) {
|
|
||||||
$lon = floatval($parts[0]);
|
|
||||||
$lat = floatval($parts[1]);
|
|
||||||
$value = floatval($parts[2]);
|
|
||||||
|
|
||||||
// Skip points that are exactly zero, often represent no data
|
|
||||||
if ($value === 0.0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'properties' => ['value' => $value],
|
|
||||||
'geometry' => [
|
|
||||||
'type' => 'Point',
|
|
||||||
'coordinates' => [$lon, $lat]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fclose($handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Clean up temporary files ---
|
|
||||||
unlink($grib_file);
|
|
||||||
unlink($xyz_file);
|
|
||||||
|
|
||||||
// --- Output the GeoJSON ---
|
|
||||||
echo json_encode([
|
|
||||||
'type' => 'FeatureCollection',
|
|
||||||
'features' => $features
|
|
||||||
]);
|
|
||||||
|
|
||||||
?>
|
|
||||||
203
api/wind.php
Normal file
203
api/wind.php
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
// Enable error reporting for debugging
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
$cacheFile = __DIR__ . '/../assets/cache/wind.json';
|
||||||
|
$cacheTime = 3600; // 1 hour in seconds
|
||||||
|
|
||||||
|
// Check if a valid cache file exists
|
||||||
|
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
|
||||||
|
readfile($cacheFile);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the great-circle distance between two points on a sphere.
|
||||||
|
* @param float $lat1 Latitude of point 1
|
||||||
|
* @param float $lon1 Longitude of point 1
|
||||||
|
* @param float $lat2 Latitude of point 2
|
||||||
|
* @param float $lon2 Longitude of point 2
|
||||||
|
* @return float Distance in kilometers
|
||||||
|
*/
|
||||||
|
function haversineDistance($lat1, $lon1, $lat2, $lon2) {
|
||||||
|
$earthRadius = 6371; // km
|
||||||
|
$dLat = deg2rad($lat2 - $lat1);
|
||||||
|
$dLon = deg2rad($lon2 - $lon1);
|
||||||
|
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||||
|
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||||
|
sin($dLon / 2) * sin($dLon / 2);
|
||||||
|
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||||
|
return $earthRadius * $c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolates wind data for a grid point using Inverse Distance Weighting.
|
||||||
|
* @param float $gridLat Latitude of the grid point
|
||||||
|
* @param float $gridLon Longitude of the grid point
|
||||||
|
* @param array $cities Array of city weather data
|
||||||
|
* @param int $numNearest Number of nearest cities to use
|
||||||
|
* @param float $power Power parameter for IDW
|
||||||
|
* @return array ['u' => float, 'v' => float]
|
||||||
|
*/
|
||||||
|
function interpolatePoint($gridLat, $gridLon, &$cities, $numNearest = 4, $power = 2) {
|
||||||
|
// Find nearest cities
|
||||||
|
usort($cities, function($a, $b) use ($gridLat, $gridLon) {
|
||||||
|
$distA = haversineDistance($gridLat, $gridLon, $a['coord']['Lat'], $a['coord']['Lon']);
|
||||||
|
$distB = haversineDistance($gridLat, $gridLon, $b['coord']['Lat'], $b['coord']['Lon']);
|
||||||
|
return $distA - $distB;
|
||||||
|
});
|
||||||
|
|
||||||
|
$nearestCities = array_slice($cities, 0, $numNearest);
|
||||||
|
|
||||||
|
$totalWeight = 0;
|
||||||
|
$weightedU = 0;
|
||||||
|
$weightedV = 0;
|
||||||
|
|
||||||
|
foreach ($nearestCities as $city) {
|
||||||
|
$dist = haversineDistance($gridLat, $gridLon, $city['coord']['Lat'], $city['coord']['Lon']);
|
||||||
|
if ($dist == 0) { // If the grid point is exactly at a city, use its data directly
|
||||||
|
$windSpeed = $city['wind']['speed']; // m/s
|
||||||
|
$windDeg = $city['wind']['deg'];
|
||||||
|
$windRad = deg2rad($windDeg);
|
||||||
|
return [
|
||||||
|
'u' => -$windSpeed * sin($windRad),
|
||||||
|
'v' => -$windSpeed * cos($windRad)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$weight = 1 / pow($dist, $power);
|
||||||
|
|
||||||
|
$windSpeed = $city['wind']['speed']; // m/s
|
||||||
|
$windDeg = $city['wind']['deg'];
|
||||||
|
$windRad = deg2rad($windDeg);
|
||||||
|
|
||||||
|
// Convert to U/V components (meteorological convention)
|
||||||
|
$u = -$windSpeed * sin($windRad);
|
||||||
|
$v = -$windSpeed * cos($windRad);
|
||||||
|
|
||||||
|
$weightedU += $u * $weight;
|
||||||
|
$weightedV += $v * $weight;
|
||||||
|
$totalWeight += $weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalWeight == 0) return ['u' => 0, 'v' => 0];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'u' => $weightedU / $totalWeight,
|
||||||
|
'v' => $weightedV / $totalWeight
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Data Fetching and Processing ---
|
||||||
|
|
||||||
|
$bulkFileName = 'weather_14.json.gz';
|
||||||
|
$bulkUrl = "https://bulk.openweathermap.org/snapshot/{$bulkFileName}?appid=" . OWM_API_KEY;
|
||||||
|
|
||||||
|
$gzData = @file_get_contents($bulkUrl);
|
||||||
|
if ($gzData === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Failed to download bulk weather data. Check API key and URL.']);
|
||||||
|
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)
|
||||||
|
$lo1 = -180;
|
||||||
|
$la1 = 90;
|
||||||
|
$dx = 5;
|
||||||
|
$dy = 5;
|
||||||
|
|
||||||
|
$uData = [];
|
||||||
|
$vData = [];
|
||||||
|
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Format Data ---
|
||||||
|
|
||||||
|
$refTime = gmdate("Y-m-d\\TH:i:s.v\\Z");
|
||||||
|
|
||||||
|
$formattedData = [
|
||||||
|
[
|
||||||
|
"header" => [
|
||||||
|
"nx" => $nx,
|
||||||
|
"ny" => $ny,
|
||||||
|
"lo1" => $lo1,
|
||||||
|
"la1" => $la1,
|
||||||
|
"dx" => $dx,
|
||||||
|
"dy" => $dy,
|
||||||
|
"parameterCategory" => 2,
|
||||||
|
"parameterNumber" => 2,
|
||||||
|
"forecastTime" => 0,
|
||||||
|
"refTime" => $refTime,
|
||||||
|
],
|
||||||
|
"data" => $uData
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"header" => [
|
||||||
|
"nx" => $nx,
|
||||||
|
"ny" => $ny,
|
||||||
|
"lo1" => $lo1,
|
||||||
|
"la1" => $la1,
|
||||||
|
"dx" => $dx,
|
||||||
|
"dy" => $dy,
|
||||||
|
"parameterCategory" => 2,
|
||||||
|
"parameterNumber" => 3,
|
||||||
|
"forecastTime" => 0,
|
||||||
|
"refTime" => $refTime,
|
||||||
|
],
|
||||||
|
"data" => $vData
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Caching and Output ---
|
||||||
|
$cacheDir = dirname($cacheFile);
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
mkdir($cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
file_put_contents($cacheFile, json_encode($formattedData));
|
||||||
|
|
||||||
|
echo json_encode($formattedData);
|
||||||
|
|
||||||
|
?>
|
||||||
@ -135,4 +135,16 @@ html, body {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group #windAltitudeLabel {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@ -60,6 +60,27 @@ function initializeGlobe() {
|
|||||||
weatherAlertsDataSource.show = this.checked;
|
weatherAlertsDataSource.show = this.checked;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wind Layer
|
||||||
|
const windLayer = new WindLayer(viewer, {
|
||||||
|
particleHeight: 10000,
|
||||||
|
particleCount: 10000,
|
||||||
|
maxAge: 120,
|
||||||
|
particleSpeed: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
const windCheckbox = document.getElementById('windLayerCheckbox');
|
||||||
|
windCheckbox.addEventListener('change', function() {
|
||||||
|
windLayer.setVisible(this.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
const windAltitudeSlider = document.getElementById('windAltitudeSlider');
|
||||||
|
const windAltitudeLabel = document.getElementById('windAltitudeLabel');
|
||||||
|
windAltitudeSlider.addEventListener('input', function() {
|
||||||
|
const altitude = parseInt(this.value, 10);
|
||||||
|
windAltitudeLabel.textContent = `${altitude} m`;
|
||||||
|
windLayer.setOptions({ particleHeight: altitude });
|
||||||
|
});
|
||||||
|
|
||||||
// Function to load wildfire data
|
// Function to load wildfire data
|
||||||
const loadWildfireData = async () => {
|
const loadWildfireData = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
224
assets/js/wind.js
Normal file
224
assets/js/wind.js
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* Original code from https://github.com/RaymanNg/3D-Wind-Field
|
||||||
|
* under the MIT license.
|
||||||
|
*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 Rayman Ng
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* 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
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WindLayer {
|
||||||
|
constructor(viewer, options = {}) {
|
||||||
|
this.viewer = viewer;
|
||||||
|
this.scene = viewer.scene;
|
||||||
|
this.camera = viewer.camera;
|
||||||
|
this.ellipsoid = viewer.scene.globe.ellipsoid;
|
||||||
|
this.options = options;
|
||||||
|
this.windData = null;
|
||||||
|
this.primitive = null;
|
||||||
|
this.visible = true;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadWindData() {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processWindData(data) {
|
||||||
|
let uComponent = null, vComponent = null;
|
||||||
|
data.forEach(record => {
|
||||||
|
const type = record.header.parameterCategory + ',' + record.header.parameterNumber;
|
||||||
|
if (type === '2,2') uComponent = record;
|
||||||
|
if (type === '2,3') vComponent = record;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uComponent || !vComponent) {
|
||||||
|
console.error("Wind data components not found.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = uComponent.header;
|
||||||
|
const windData = {
|
||||||
|
nx: header.nx,
|
||||||
|
ny: header.ny,
|
||||||
|
lo1: header.lo1,
|
||||||
|
la1: header.la1,
|
||||||
|
dx: header.dx,
|
||||||
|
dy: header.dy,
|
||||||
|
u: uComponent.data,
|
||||||
|
v: vComponent.data
|
||||||
|
};
|
||||||
|
return windData;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(visible) {
|
||||||
|
this.visible = visible;
|
||||||
|
if (this.primitive) {
|
||||||
|
this.primitive.show = visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptions(options) {
|
||||||
|
if (this.particleSystem) {
|
||||||
|
this.particleSystem.applyOptions(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParticleSystem {
|
||||||
|
constructor(scene, options) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.options = options;
|
||||||
|
this.windData = options.windData;
|
||||||
|
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;
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
this.particles.push(this.createParticle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createParticle() {
|
||||||
|
const lon = Math.random() * 360 - 180;
|
||||||
|
const lat = Math.random() * 180 - 90;
|
||||||
|
const altitude = this.options.particleHeight || 10000;
|
||||||
|
const position = Cesium.Cartesian3.fromDegrees(lon, lat, altitude);
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: position,
|
||||||
|
age: Math.floor(Math.random() * (this.options.maxAge || 120)),
|
||||||
|
maxAge: this.options.maxAge || 120,
|
||||||
|
speed: Math.random() * (this.options.particleSpeed || 5)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getWind(position) {
|
||||||
|
const cartographic = Cesium.Cartographic.fromCartesian(position);
|
||||||
|
const lon = Cesium.Math.toDegrees(cartographic.longitude);
|
||||||
|
const lat = Cesium.Math.toDegrees(cartographic.latitude);
|
||||||
|
|
||||||
|
const { nx, ny, lo1, la1, dx, dy, u, v } = this.windData;
|
||||||
|
|
||||||
|
const i = Math.floor((lon - lo1) / dx);
|
||||||
|
const j = Math.floor((la1 - lat) / dy);
|
||||||
|
|
||||||
|
if (i >= 0 && i < nx && j >= 0 && j < ny) {
|
||||||
|
const index = j * nx + i;
|
||||||
|
return { u: u[index], v: v[index] };
|
||||||
|
}
|
||||||
|
return { u: 0, v: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.primitive.show) return;
|
||||||
|
|
||||||
|
this.particles.forEach(particle => {
|
||||||
|
if (particle.age >= particle.maxAge) {
|
||||||
|
Object.assign(particle, this.createParticle());
|
||||||
|
}
|
||||||
|
|
||||||
|
const wind = this.getWind(particle.position);
|
||||||
|
const speed = particle.speed;
|
||||||
|
|
||||||
|
const metersPerDegree = 111320;
|
||||||
|
const vx = wind.u * speed / metersPerDegree;
|
||||||
|
const vy = wind.v * speed / metersPerDegree;
|
||||||
|
|
||||||
|
const cartographic = Cesium.Cartographic.fromCartesian(particle.position);
|
||||||
|
cartographic.longitude += Cesium.Math.toRadians(vx);
|
||||||
|
cartographic.latitude += Cesium.Math.toRadians(vy);
|
||||||
|
|
||||||
|
particle.position = Cesium.Cartesian3.fromRadians(
|
||||||
|
cartographic.longitude,
|
||||||
|
cartographic.latitude,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,9 +25,18 @@
|
|||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="weatherAlertsLayerCheckbox" checked> Weather Alerts
|
<input type="checkbox" id="weatherAlertsLayerCheckbox" checked> Weather Alerts
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="windLayerCheckbox" checked> Wind
|
||||||
|
</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>
|
||||||
</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/main.js?v=<?php echo time(); ?>"></script>
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user