绘制网格参数, 创建windField.ts
export type TWindFieldHeader = {
lo1: number // 西(起点)
la1: number
lo2: number
la2: number
ny: number
nx: number
dy: number
dx: number
parameterUnit: string
refTime: string
[key: string]: unknown
}
export type TWindField = {
uComponent: number[]
vComponent: number[]
header: TWindFieldHeader
}
/**
* 网格类
* 根据风场数据生产风场网格
*/
export default class WindField {
public west: number = 0
public east: number = 0
public south: number = 0
public north: number = 0
public rows: number = 1
public cols: number = 1
public dx: number = 1
public dy: number = 1
public unit: string = ''
public refTime: string = ''
public grid: any = []
constructor(obj: TWindField) {
const { uComponent, vComponent, header } = obj
this.west = Number(header.lo1)
this.east = Number(header.lo2)
this.north = Number(header.la1)
this.south = Number(header.la2)
this.rows = Number(header.ny)
this.cols = Number(header.nx)
this.dx = Number(header.dx)
this.dy = Number(header.dy)
this.unit = header.parameterUnit
this.refTime = header.refTime
let rows: any = [], k: number = 0, uv = null
for (let j = 0
rows = []
for (let i = 0
uv = this.calcUV(uComponent[k], vComponent[k])
rows.push(uv)
}
this.grid.push(rows)
}
}
/**
* 经纬度中间的斜向速度
* @param u
* @param v
* @returns
*/
calcUV(u: number, v: number) {
// 经度, 纬度, 斜向速度(真实的粒子移动速度)
return [u, v, Math.sqrt(u * u + v + v)]
}
/**
* 二分差值算法计算给定节点的速度
* @param x
* @param y
* @param g00
* @param g10
* @param g01
* @param g11
* @returns
*/
bilinearInterpolation(x: number, y: number, g00: number[], g10: number[], g01: number[], g11: number[]) {
const rx = (1 - x)
const ry = (1 - y)
const a = rx * ry, b = x * ry, c = rx * y, d = x * y
const u = g00[0] * a + g10[0] * b + g01[0] * c + g11[0] * d
const v = g00[1] * a + g10[1] * b + g01[1] * c + g11[1] * d
return this.calcUV(u, v)
}
getIn(x: number, y: number) {
// console.log('getIn', x , y)
if (x < 0 || x >= 359 || y >= 180) {
return [0, 0, 0]
}
const x0 = Math.floor(x)
const y0 = Math.floor(y)
if (x0 === x && y0 === y) return this.grid[y][x]
const x1 = x0 + 1
const y1 = y0 + 1
const g00 = this.getIn(x0, y0),
g10 = this.getIn(x1, y0),
g01 = this.getIn(x0, y1),
g11 = this.getIn(x1, y1)
let result = null
try {
result = this.bilinearInterpolation(x - x0, y - y0, g00, g10, g01, g11)
} catch (e) {
// console.log(x,y)
}
return result
}
isInBound(x: number, y: number) {
if ((x >= 0 && x < this.cols - 1) && (y >= 0 && y < this.rows - 1)) return true
return false
}
}
创建单个粒子, 创建particle.ts
export type TParticle = {
longitude: number
latitude: number
x: number
y: number
nextLongitude: number
nextLatitude: number
age: number
speed: number
}
/**
* 粒子对象
*/
export default class Particle {
public longitude: number = 0
public latitude: number = 0
public x: number = 0
public y: number = 0
public nextLongitude: number = 0
public nextLatitude: number = 0
public age: number = 0
public speed: number = 0
constructor (maxAge: number) {
this.age = Math.round(Math.random() * maxAge)
// this.lng = obj.lng
// this.lat = obj.lat
// this.x = obj.x
// this.y = obj.y
// this.nextLng = obj.nextLng
// this.nextLat = obj.nextLat
// this.speed = obj.speed
}
}
创建风场windy.ts
import WindField, { type TWindFieldHeader } from './windField'
import * as Cesium from 'cesium'
import Particle, { type TParticle } from './particle'
import {
getCurrentMultiple,
cartesian3ToLatLong,
windowPositionToCartesian3
} from '../useCoordinate'
export type TWindData = {
header: {
parameterCategory: string
parameterNumber: string
[key: string]: string
}
data: number[]
}
export type TExtent = {
latitude: [number, number]
longitude: [number, number]
}
export type TWindExtent = [number, number, number, number]
type TParams = {
canvas?: HTMLCanvasElement
viewer: Cesium.Viewer
extent?: TExtent
canvasWidth?: number
canvasHeight?: number
speedRate?: number
particlesNumber?: number
maxAge?: number
frameRate?: number
color?: string
lineWidth?: number
}
export default class CanvasWindy {
public windData: TWindData[] = []
public viewer: Cesium.Viewer | undefined = undefined
public canvas: HTMLCanvasElement | undefined = undefined
public extent: TExtent | undefined = undefined
public canvasContext: CanvasRenderingContext2D | null = null
public canvasWidth: number | undefined = window.innerWidth
public canvasHeight: number | undefined = window.innerHeight
public speedRate: number | undefined = 15000
public particlesNumber: number = 2000
public maxAge: number | undefined = 120
public frameTime: number | undefined
public color: string | undefined = '#fff'
public lineWidth: number | undefined = 2
public initExtent: TWindExtent = [0, 360, -90, 90]
public windField: WindField | null = null
public particles: TParticle[] = []
public destroyRequestAnimationFrame: number | null = null
public isDistorted: boolean = false
constructor(json: TWindData[], params: TParams) {
this.windData = json
this.viewer = params.viewer
this.extent = params.extent
this.canvasWidth = params.canvasWidth ? params.canvasWidth : this.canvasWidth
this.canvasHeight = params.canvasHeight ? params.canvasHeight : this.canvasHeight
this.initWindyCanvas()
this.canvasContext = (this.canvas as HTMLCanvasElement).getContext('2d')
this.speedRate = params.speedRate ? params.speedRate : this.speedRate
this.particlesNumber = params.particlesNumber ? params.particlesNumber : this.particlesNumber
this.maxAge = params.maxAge ? params.maxAge : this.maxAge
this.frameTime = 1000 / (params.frameRate || 100)
this.color = params.color ? params.color : this.color
this.lineWidth = params.lineWidth ? params.lineWidth : this.lineWidth
this.init()
}
initWindyCanvas() {
const canvas: HTMLCanvasElement = document.createElement('canvas')
canvas.width = this.canvasWidth as number
canvas.height = this.canvasHeight as number
canvas.style.position = 'absolute'
canvas.style.pointerEvents = 'none'
canvas.style.zIndex = '10'
canvas.style.top = '0'
canvas.style.left = '0'
this.canvas = canvas
document.getElementById('cesiumContainer')?.appendChild(canvas)
}
parseWindJson() {
let uComponent: number[] = [],
vComponent: number[] = [],
header: TWindFieldHeader | {} = {}
this.windData.forEach((record) => {
const type = `${record.header.parameterCategory},${record.header.parameterNumber}`
switch (type) {
case '2,2':
uComponent = record.data
header = record.header
break
case '2,3':
vComponent = record.data
break
default:
break
}
})
return {
header: header as TWindFieldHeader,
uComponent,
vComponent
}
}
createWindField() {
const data = this.parseWindJson()
this.windField = new WindField(data)
this.initExtent = [
this.windField.west,
this.windField.east,
this.windField.south,
this.windField.north
]
}
createParticles() {
for (let i = 0; i < this.particlesNumber; i++) {
const particle = new Particle(this.maxAge as number)
if (particle) {
this.particles.push(particle)
}
}
}
init() {
this.createWindField();
this.createParticles();
(this.canvasContext as CanvasRenderingContext2D).fillStyle = 'red';
let then = Date.now();
const self = this;
(function frame() {
if (!self.isDistorted) {
self.destroyRequestAnimationFrame = requestAnimationFrame(frame)
const now = Date.now()
const delta = now - then
if (delta > (self.frameTime as number)) {
then = now - (delta % (self.frameTime as number))
self.animate()
}
} else {
self.removeLines()
}
})()
}
computedCalcStep() {
const calcExtent = this.initExtent
const currentSpeedRate =
(this.speedRate as number) / getCurrentMultiple(this.viewer as Cesium.Viewer)
const calcSpeedRate = [
(calcExtent[1] - calcExtent[0]) / (currentSpeedRate as number),
(calcExtent[3] - calcExtent[2]) / (currentSpeedRate as number)
]
return calcSpeedRate
}
animate() {
this.particles.forEach((particle) => {
if (particle.age === null || particle.age <= 0) {
particle.age = Math.round(Math.random() * (this.maxAge as number))
this.initParticle(particle)
}
if (particle.age > 0) {
const { nextLongitude, nextLatitude } = particle
if (this.isInExtent(nextLongitude, nextLatitude)) {
this.assignmentParticle(particle)
particle.age--
} else {
particle.age = 0
}
}
})
if (this.particles.length <= 0) {
this.removeLines()
}
this.drawLines()
}
judgeParticleExtent(
particle: TParticle
) {
const extent = this.extent as TExtent
const { longitude, latitude } = extent
const lng = particle.longitude;
const lat = particle.latitude
if (!lng || !lat) {
return true
};
if (extent && particle) {
if (lat > longitude[1] || lat < longitude[0]) {
particle.age = 0
return true
}
if (lng > latitude[1] || lng < latitude[0]) {
particle.age = 0
return true
}
}
return false
}
assignmentParticle (particle: TParticle, location?: {
longitude: number | null;
latitude: number | null;
}) {
let x = 0, y = 0
const field = this.windField as WindField
const longitude = location ? location.longitude : particle.nextLongitude
const latitude = location ? location.latitude : particle.nextLatitude
particle.longitude = longitude as number
particle.latitude = latitude as number
if (longitude !== null && latitude !== null) {
const gridPosition = this.computedGridLocation(longitude, latitude)
x = gridPosition[0]
y = gridPosition[1]
particle.x = x
particle.y = y
}
const _calcSpeedRate = this.computedCalcStep()
const uv = field.getIn(x, y)
const nextLongitude = (longitude as number) + _calcSpeedRate[0] * uv[0]
const nextLatitude = (latitude as number) + _calcSpeedRate[1] * uv[1]
particle.nextLongitude = nextLongitude
particle.nextLatitude = nextLatitude
}
initParticle(particle: TParticle) {
let longitude = null,
latitude = null
const calcExtent = this.initExtent
try {
longitude = this.generateFloat(calcExtent[0], calcExtent[1])
latitude = this.generateFloat(calcExtent[2], calcExtent[3])
} catch (error: any) {
console.log('发生异常', error)
}
try {
this.assignmentParticle(particle, {
longitude,
latitude
})
} catch (e) {
console.log('为粒子赋值发生异常', e)
}
return particle
}
generateFloat(start: number, end: number) {
return start + Math.random() * (end - start)
}
generateIntegers(start: number, end: number) {
switch (arguments.length) {
case 1:
return parseInt(Math.random() * start + 1 + '')
case 2:
return parseInt(Math.random() * (end - start + 1) + start + '')
default:
return 0
}
}
computedGridLocation(longitude: number, latitude: number) {
const field = this.windField as WindField
const _initExtent = this.initExtent as [number, number, number, number]
const x = ((longitude - _initExtent[0]) / (_initExtent[1] - _initExtent[0])) * (field.cols - 1)
const y = ((_initExtent[3] - latitude) / (_initExtent[3] - _initExtent[2])) * (field.rows - 1)
return [x, y]
}
isInExtent(lng: number, lat: number) {
const calcExtent = this.initExtent as [number, number, number, number]
return (
lng >= calcExtent[0] && lng <= calcExtent[1] && lat >= calcExtent[2] && lat <= calcExtent[3]
)
}
removeLines() {
window.cancelAnimationFrame(this.destroyRequestAnimationFrame as number)
this.isDistorted = true
;(this.canvas as HTMLCanvasElement).width = 1
document.getElementById('cesiumContainer')?.removeChild(this.canvas as HTMLCanvasElement)
}
drawLines() {
const particles = this.particles
const ctx = this.canvasContext as CanvasRenderingContext2D
ctx.lineWidth = this.lineWidth as number
ctx.globalCompositeOperation = 'destination-in'
ctx.fillRect(0, 0, this.canvasWidth as number, this.canvasHeight as number)
ctx.globalCompositeOperation = 'lighter'
ctx.globalAlpha = 0.9
particles.forEach((particle) => {
if (this.judgeParticleExtent(particle)) {
return
}
const moveTopos = this.computedLocationByLngAndLat(particle.longitude, particle.latitude, particle)
const lineTopos = this.computedLocationByLngAndLat(
particle.nextLongitude,
particle.nextLatitude,
particle
)
this.canvasContext?.beginPath()
if (moveTopos !== null && lineTopos !== null) {
ctx.moveTo(moveTopos[0], moveTopos[1])
ctx.lineTo(lineTopos[0], lineTopos[1])
ctx.strokeStyle = this.color as string
ctx.stroke()
}
})
}
computedLocationByLngAndLat(lng: number, lat: number, particle: TParticle) {
try {
const ct3 = Cesium.Cartesian3.fromDegrees(lng, lat, 0)
const position = Cesium.SceneTransforms.wgs84ToWindowCoordinates(
(this.viewer as Cesium.Viewer)?.scene,
ct3
)
const isVisible = new (Cesium as any).EllipsoidalOccluder(
Cesium.Ellipsoid.WGS84,
this.viewer?.camera.position
).isPointVisible(ct3)
if (!isVisible) {
particle.age = 0
}
return (position && position.x) || position.y ? [position.x, position.y] : null
} catch (e: any) {
return null
}
}
resize(width: number, height: number) {
this.canvasWidth = width
this.canvasHeight = height
}
redraw() {
window.cancelAnimationFrame(this.destroyRequestAnimationFrame as number)
this.particles = []
this.init()
}
}
调用
const geTWindData = () => {
get('/map/data/windy/gfs.json', {}, {
baseURL: '/'
}).then(res => {
windInstance = new CanvasWindy(res as any, {
viewer,
canvasWidth: mapRef.value?.clientWidth,
canvasHeight: mapRef.value?.clientHeight,
extent: extent
})
})
}