/* * 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 OF 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.particleSystem = null; this.visible = false; // A promise that resolves when the layer is ready this.readyPromise = this.init(); } 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'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); this.windData = this.processWindData(data); } catch (error) { console.error('Error loading or processing wind data:', error); throw error; // Re-throw to be caught by the init promise chain } } 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; return { nx: header.nx, ny: header.ny, lo1: header.lo1, la1: header.la1, dx: header.dx, dy: header.dy, u: uComponent.data, v: vComponent.data }; } setVisible(visible) { this.visible = visible; // Use the promise to safely apply visibility this.readyPromise.then(() => { if (this.particleSystem) { this.particleSystem.polylines.show = this.visible; } }); } setOptions(options) { this.readyPromise.then(() => { if (this.particleSystem) { this.particleSystem.applyOptions(options); } }); } pause() { this.readyPromise.then(() => { if (this.particleSystem) { this.particleSystem.pause(); } }); } play() { this.readyPromise.then(() => { if (this.particleSystem) { this.particleSystem.play(); } }); } setParticleDensity(density) { this.readyPromise.then(() => { if (this.particleSystem) { this.particleSystem.setParticleCount(density); } }); } } class ParticleSystem { constructor(scene, options) { 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.createParticles(); this.scene.preRender.addEventListener(this.update, this); } createParticles() { const particleCount = this.options.particleCount || 10000; for (let i = 0; i < particleCount; i++) { 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 } } }) }); } } 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, 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) }; } 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.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); 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); const speed = particle.speed; const metersPerDegree = 111320; 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); particle.position = Cesium.Cartesian3.fromRadians( cartographic.longitude, cartographic.latitude, cartographic.height ); particle.age++; // 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)); } } } applyOptions(options) { this.options = Object.assign(this.options, options); // 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) { const maxParticles = this.options.maxParticles || this.options.particleCount || 10000; if (!this.options.maxParticles) { this.options.maxParticles = maxParticles; } const newParticleCount = Math.floor(maxParticles * density); this.polylines.removeAll(); this.particles = []; for (let i = 0; i < newParticleCount; i++) { 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); } }