/* * 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); } } pause() { if (this.particleSystem) { this.scene.preRender.removeEventListener(this.particleSystem.update, this.particleSystem); } } play() { if (this.particleSystem) { this.scene.preRender.addEventListener(this.particleSystem.update, this.particleSystem); } } setParticleDensity(density) { if (this.particleSystem) { this.particleSystem.setParticleCount(density); } } } 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 } 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.particles.length = 0; // Clear the array for (let i = 0; i < newParticleCount; i++) { this.particles.push(this.createParticle()); } } }