Browse Source

add @dc-modules/wind

tags/2.0.0
Caven Chen 4 years ago
parent
commit
3fe5beed0e
4 changed files with 1056 additions and 0 deletions
  1. 433
    0
      modules/wind/Field.js
  2. 44
    0
      modules/wind/Vector.js
  3. 351
    0
      modules/wind/WindCanvas.js
  4. 228
    0
      modules/wind/WindLayer.js

+ 433
- 0
modules/wind/Field.js View File

@@ -0,0 +1,433 @@
/**
* @Author: Caven
* @Date: 2021-01-18 20:13:30
*/

import Vector from './Vector'

class Field {
constructor(params) {
this.grid = []
this.xmin = params.xmin
this.xmax = params.xmax
this.ymin = params.ymin
this.ymax = params.ymax
this.cols = params.cols // 列数
this.rows = params.rows // 行数
this.us = params.us //
this.vs = params.vs
this.deltaX = params.deltaX // x 方向增量
this.deltaY = params.deltaY // y方向增量
if (this.deltaY < 0 && this.ymin < this.ymax) {
// eslint-disable-next-line no-console
console.warn('[wind-core]: The data is flipY')
} else {
this.ymin = Math.min(params.ymax, params.ymin)
this.ymax = Math.max(params.ymax, params.ymin)
}
this.isFields = true
let cols = Math.ceil((this.xmax - this.xmin) / params.deltaX) // 列
let rows = Math.ceil((this.ymax - this.ymin) / params.deltaY) // 行
if (cols !== this.cols || rows !== this.rows) {
// eslint-disable-next-line no-console
console.warn('[wind-core]: The data grid not equal')
}
// Math.floor(ni * Δλ) >= 360;
this.isContinuous = Math.floor(this.cols * params.deltaX) >= 360
this.wrappedX = 'wrappedX' in params ? params.wrappedX : this.xmax > 180 // [0, 360] --> [-180, 180];
this.grid = this.buildGrid()
this.range = this.calculateRange()
}
// from https://github.com/sakitam-fdd/wind-layer/blob/95368f9433/src/windy/windy.js#L110
buildGrid() {
let grid = []
let p = 0
let _a = this,
rows = _a.rows,
cols = _a.cols,
us = _a.us,
vs = _a.vs
for (let j = 0; j < rows; j++) {
let row = []
for (let i = 0; i < cols; i++, p++) {
let u = us[p]
let v = vs[p]
let valid = this.isValid(u) && this.isValid(v)
row[i] = valid ? new Vector(u, v) : null
}
if (this.isContinuous) {
row.push(row[0])
}
grid[j] = row
}
return grid
}

/**
*
*/
release() {
this.grid = []
}

/***
*
* @returns {(*)[]}
*/
extent() {
return [this.xmin, this.ymin, this.xmax, this.ymax]
}

/**
* Bilinear interpolation for Vector
* https://en.wikipedia.org/wiki/Bilinear_interpolation
* @param {Number} x
* @param {Number} y
* @param {Number[]} g00
* @param {Number[]} g10
* @param {Number[]} g01
* @param {Number[]} g11
* @returns {Vector}
*/
bilinearInterpolateVector(x, y, g00, g10, g01, g11) {
let rx = 1 - x
let ry = 1 - y
let a = rx * ry
let b = x * ry
let c = rx * y
let d = x * y
let u = g00.u * a + g10.u * b + g01.u * c + g11.u * d
let v = g00.v * a + g10.v * b + g01.v * c + g11.v * d
return new Vector(u, v)
}

/**
* calculate vector value range
*/
calculateRange() {
if (!this.grid || !this.grid[0]) {
return
}
let rows = this.grid.length
let cols = this.grid[0].length
// const vectors = [];
let min
let max
// @from: https://stackoverflow.com/questions/13544476/how-to-find-max-and-min-in-array-using-minimum-comparisons
for (let j = 0; j < rows; j++) {
for (let i = 0; i < cols; i++) {
let vec = this.grid[j][i]
if (vec !== null) {
let val = vec.m || vec.magnitude()
// vectors.push();
if (min === undefined) {
min = val
} else if (max === undefined) {
max = val
// update min max
// 1. Pick 2 elements(a, b), compare them. (say a > b)
min = Math.min(min, max)
max = Math.max(min, max)
} else {
// 2. Update min by comparing (min, b)
// 3. Update max by comparing (max, a)
min = Math.min(val, min)
max = Math.max(val, max)
}
}
}
}
return [min, max]
}
/**
*
* @param x
* @private
*/
isValid(x) {
return x !== null && x !== undefined
}

getWrappedLongitudes() {
let xmin = this.xmin
let xmax = this.xmax
if (this.wrappedX) {
if (this.isContinuous) {
xmin = -180
xmax = 180
} else {
xmax = this.xmax - 360
xmin = this.xmin - 360
}
}
return [xmin, xmax]
}

contains(lon, lat) {
let _a = this.getWrappedLongitudes(),
xmin = _a[0],
xmax = _a[1]
let longitudeIn = lon >= xmin && lon <= xmax
let latitudeIn
if (this.deltaY >= 0) {
latitudeIn = lat >= this.ymin && lat <= this.ymax
} else {
latitudeIn = lat >= this.ymax && lat <= this.ymin
}
return longitudeIn && latitudeIn
}

/**
*
* @param a
* @param n
* @returns {number}
*/
floorMod(a, n) {
return a - n * Math.floor(a / n)
}

/**
*
* @param lon
* @param lat
*/
getDecimalIndexes(lon, lat) {
let i = this.floorMod(lon - this.xmin, 360) / this.deltaX // calculate longitude index in wrapped range [0, 360)
let j = (this.ymax - lat) / this.deltaY // calculate latitude index in direction +90 to -90
return [i, j]
}

/**
* Nearest value at lon-lat coordinates
*
* @param lon
* @param lat
*/
valueAt(lon, lat) {
if (!this.contains(lon, lat)) {
return null
}
let indexes = this.getDecimalIndexes(lon, lat)
let ii = Math.floor(indexes[0])
let jj = Math.floor(indexes[1])
let ci = this.clampColumnIndex(ii)
let cj = this.clampRowIndex(jj)
return this.valueAtIndexes(ci, cj)
}
/**
* Get interpolated grid value lon-lat coordinates
* @param lon
* @param lat
*/
interpolatedValueAt(lon, lat) {
if (!this.contains(lon, lat)) {
return null
}
let _a = this.getDecimalIndexes(lon, lat),
i = _a[0],
j = _a[1]
return this.interpolatePoint(i, j)
}

hasValueAt(lon, lat) {
let value = this.valueAt(lon, lat)
return value !== null
}

/**
*
* @param i
* @param j
*/
interpolatePoint(i, j) {
// 1 2 After converting λ and φ to fractional grid indexes i and j, we find the
// fi i ci four points 'G' that enclose point (i, j). These points are at the four
// | =1.4 | corners specified by the floor and ceiling of i and j. For example, given
// ---G--|---G--- fj 8 i = 1.4 and j = 8.3, the four surrounding grid points are (1, 8), (2, 8),
// j ___|_ . | (1, 9) and (2, 9).
// =8.3 | |
// ---G------G--- cj 9 Note that for wrapped grids, the first column is duplicated as the last
// | | column, so the index ci can be used without taking a modulo.
let indexes = this.getFourSurroundingIndexes(i, j)
let fi = indexes[0],
ci = indexes[1],
fj = indexes[2],
cj = indexes[3]
let values = this.getFourSurroundingValues(fi, ci, fj, cj)
if (values) {
let g00 = values[0],
g10 = values[1],
g01 = values[2],
g11 = values[3]
// @ts-ignore
return this.bilinearInterpolateVector(i - fi, j - fj, g00, g10, g01, g11)
}
return null
}
/**
* Check the column index is inside the field,
* adjusting to min or max when needed
* @private
* @param {Number} ii - index
* @returns {Number} i - inside the allowed indexes
*/
clampColumnIndex(ii) {
let i = ii
if (ii < 0) {
i = 0
}
let maxCol = this.cols - 1
if (ii > maxCol) {
i = maxCol
}
return i
}

/**
* Check the row index is inside the field,
* adjusting to min or max when needed
* @private
* @param {Number} jj index
* @returns {Number} j - inside the allowed indexes
*/
clampRowIndex(jj) {
let j = jj
if (jj < 0) {
j = 0
}
let maxRow = this.rows - 1
if (jj > maxRow) {
j = maxRow
}
return j
}

/**
* from: https://github.com/IHCantabria/Leaflet.CanvasLayer.Field/blob/master/src/Field.js#L252
* @private
* @param {Number} i - decimal index
* @param {Number} j - decimal index
* @returns {Array} [fi, ci, fj, cj]
*/
getFourSurroundingIndexes(i, j) {
let fi = Math.floor(i) // 左
let ci = fi + 1 // 右
// duplicate colum to simplify interpolation logic (wrapped value)
if (this.isContinuous && ci >= this.cols) {
ci = 0
}
ci = this.clampColumnIndex(ci)
let fj = this.clampRowIndex(Math.floor(j)) // 上 纬度方向索引(取整)
let cj = this.clampRowIndex(fj + 1) // 下
return [fi, ci, fj, cj]
}

/**
* from https://github.com/IHCantabria/Leaflet.CanvasLayer.Field/blob/master/src/Field.js#L277
* Get four surrounding values or null if not available,
* from 4 integer indexes
* @private
* @param {Number} fi
* @param {Number} ci
* @param {Number} fj
* @param {Number} cj
* @returns {Array}
*/
getFourSurroundingValues(fi, ci, fj, cj) {
let row
if ((row = this.grid[fj])) {
let g00 = row[fi] // << left
let g10 = row[ci] // right >>
if (this.isValid(g00) && this.isValid(g10) && (row = this.grid[cj])) {
// lower row vv
let g01 = row[fi] // << left
let g11 = row[ci] // right >>
if (this.isValid(g01) && this.isValid(g11)) {
return [g00, g10, g01, g11] // 4 values found!
}
}
}
return null
}

/**
* Value for grid indexes
* @param {Number} i - column index (integer)
* @param {Number} j - row index (integer)
* @returns {Vector|Number}
*/
valueAtIndexes(i, j) {
return this.grid[j][i] // <-- j,i !!
}

/**
* Lon-Lat for grid indexes
* @param {Number} i - column index (integer)
* @param {Number} j - row index (integer)
* @returns {Number[]} [lon, lat]
*/
lonLatAtIndexes(i, j) {
let lon = this.longitudeAtX(i)
let lat = this.latitudeAtY(j)
return [lon, lat]
}

/**
* Longitude for grid-index
* @param {Number} i - column index (integer)
* @returns {Number} longitude at the center of the cell
*/
longitudeAtX(i) {
let halfXPixel = this.deltaX / 2.0
let lon = this.xmin + halfXPixel + i * this.deltaX
if (this.wrappedX) {
lon = lon > 180 ? lon - 360 : lon
}
return lon
}

/**
* Latitude for grid-index
* @param j
* @returns {number}
*/
latitudeAtY(j) {
let halfYPixel = this.deltaY / 2.0
return this.ymax - halfYPixel - j * this.deltaY
}

/**
*
* @param o
* @param width
* @param height
* @param unproject
* @returns {{}}
*/
randomize(o, width, height, unproject) {
if (o === void 0) {
o = {}
}
let i = (Math.random() * (width || this.cols)) | 0
let j = (Math.random() * (height || this.rows)) | 0
let coords = unproject([i, j])
if (coords !== null) {
o.x = coords[0]
o.y = coords[1]
} else {
o.x = this.longitudeAtX(i)
o.y = this.latitudeAtY(j)
}
return o
}

/**
* check is custom field
*/
checkFields() {
return this.isFields
}
}

export default Field

+ 44
- 0
modules/wind/Vector.js View File

@@ -0,0 +1,44 @@
/**
* @Author: Caven
* @Date: 2021-01-18 20:25:48
*/

class Vector {
constructor(u, v) {
this.u = u
this.v = v
this.m = this.magnitude()
}
/**
* the vector value
* @returns {Number}
*/
magnitude() {
return Math.sqrt(this.u * this.u + this.v * this.v)
}
/**
* Angle in degrees (0 to 360º) --> Towards
* N is 0º and E is 90º
* @returns {Number}
*/
directionTo() {
let verticalAngle = Math.atan2(this.u, this.v)
let inDegrees = verticalAngle * (180.0 / Math.PI)
if (inDegrees < 0) {
inDegrees += 360.0
}
return inDegrees
}

/**
* Angle in degrees (0 to 360º) From x-->
* N is 0º and E is 90º
* @returns {Number}
*/
directionFrom() {
let a = this.directionTo()
return (a + 180.0) % 360.0
}
}

export default Vector

+ 351
- 0
modules/wind/WindCanvas.js View File

@@ -0,0 +1,351 @@
/**
* @Author: Caven
* @Date: 2021-01-18 17:46:40
*/

class WindCanvas {
constructor(ctx) {
this.options = {}
this.particles = []
this.ctx = ctx
this.animationLoop = undefined
this.animate = this.animate.bind(this)
}

/**
*
* @param m
* @param min
* @param max
* @param colorScale
* @returns {number}
* @private
*/
_indexFor(m, min, max, colorScale) {
return Math.max(
0,
Math.min(
colorScale.length - 1,
Math.round(((m - min) / (max - min)) * (colorScale.length - 1))
)
)
}

/**
*
* @private
*/
_moveParticles() {
if (!this.particles || !this.particles.length) {
return
}
let width = this.ctx.canvas.width
let height = this.ctx.canvas.height
let particles = this.particles
let maxAge = this.options.maxAge
let velocityScale =
typeof this.options.velocityScale === 'function'
? this.options.velocityScale()
: this.options.velocityScale
for (let i = 0; i < particles.length; i++) {
let particle = particles[i]
if (particle.age > maxAge) {
particle.age = 0
this.field.randomize(particle, width, height, this.unProject)
}
let x = particle.x
let y = particle.y
let vector = this.field.interpolatedValueAt(x, y)
if (vector === null) {
particle.age = maxAge
} else {
let xt = x + vector.u * velocityScale
let yt = y + vector.v * velocityScale
if (this.field.hasValueAt(xt, yt)) {
particle.xt = xt
particle.yt = yt
particle.m = vector.m
} else {
particle.x = xt
particle.y = yt
particle.age = maxAge
}
}
particle.age++
}
}

/**
*
* @private
*/
_drawParticles() {
if (!this.particles || !this.particles.length) {
return
}
let particles = this.particles
let prev = this.ctx.globalCompositeOperation
this.ctx.globalCompositeOperation = 'destination-in'
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
this.ctx.globalCompositeOperation = prev
this.ctx.globalAlpha = this.options.globalAlpha
this.ctx.fillStyle = 'rgba(0, 0, 0, ' + this.options.globalAlpha + ')'
this.ctx.lineWidth = this.options.lineWidth ? this.options.lineWidth : 1
this.ctx.strokeStyle = this.options.colorScale
? this.options.colorScale
: '#fff'
let i = 0
let len = particles.length
if (this.field && len > 0) {
let min = void 0
let max = void 0
if (this.options.minVelocity && this.options.maxVelocity) {
min = this.options.minVelocity
max = this.options.maxVelocity
} else {
let _a = this.field.range
min = _a[0]
max = _a[1]
}
for (; i < len; i++) {
this[
this.options.useCoordsDraw
? '_drawCoordsParticle'
: '_drawPixelParticle'
](particles[i], min, max)
}
}
}

/**
*
* @param particle
* @param min
* @param max
*/
_drawPixelParticle(particle, min, max) {
let pointPrev = [particle.x, particle.y]
let pointNext = [particle.xt, particle.yt]
if (
pointNext &&
pointPrev &&
pointNext[0] &&
pointNext[1] &&
pointPrev[0] &&
pointPrev[1] &&
particle.age <= this.options.maxAge
) {
this._drawStroke(pointPrev, pointNext, particle, min, max)
}
}

/**
*
* @param particle
* @param min
* @param max
*/
_drawCoordsParticle(particle, min, max) {
let source = [particle.x, particle.y]
let target = [particle.xt, particle.yt]
if (
target &&
source &&
target[0] &&
target[1] &&
source[0] &&
source[1] &&
this.intersectsCoordinate(target) &&
particle.age <= this.options.maxAge
) {
let pointPrev = this.project(source)
let pointNext = this.project(target)
this._drawStroke(pointPrev, pointNext, particle, min, max)
}
}

/**
*
* @param pointPrev
* @param pointNext
* @param particle
* @param min
* @param max
* @private
*/
_drawStroke(pointPrev, pointNext, particle, min, max) {
if (pointPrev && pointNext) {
this.ctx.beginPath()
this.ctx.moveTo(pointPrev[0], pointPrev[1])
this.ctx.lineTo(pointNext[0], pointNext[1])
if (typeof this.options.colorScale === 'function') {
this.ctx.strokeStyle = this.options.colorScale(particle.m)
} else if (Array.isArray(this.options.colorScale)) {
let colorIdx = this._indexFor(
particle.m,
min,
max,
this.options.colorScale
)
this.ctx.strokeStyle = this.options.colorScale[colorIdx]
}
if (typeof this.options.lineWidth === 'function') {
this.ctx.lineWidth = this.options.lineWidth(particle.m)
}
particle.x = particle.xt
particle.y = particle.yt
this.ctx.stroke()
}
}

/**
*
* @returns {[]|*[]}
* @private
*/
_prepareParticlePaths() {
let width = this.ctx.canvas.width
let height = this.ctx.canvas.height
let particleCount =
typeof this.options.paths === 'function'
? this.options.paths(this)
: this.options.paths
let particles = []
if (!this.field) {
return []
}
for (let i = 0; i < particleCount; i++) {
particles.push(
this.field.randomize(
{
age: Math.floor(Math.random() * this.options.maxAge)
},
width,
height,
this.unProject
)
)
}
return particles
}

/**
*
*/
project() {
throw new Error('project must be overriden')
}

/**
*
*/
unProject() {
throw new Error('unProject must be overriden')
}

/**
*
* @param coordinates
*/
intersectsCoordinate(coordinates) {
throw new Error('must be override')
}

/**
*
*/
prerender() {
if (!this.field) {
return
}
this.particles = this._prepareParticlePaths()
if (!this.starting && !this.forceStop) {
this.starting = true
this._then = Date.now()
this.animate()
}
}

/**
*
* @returns {WindCanvas}
*/
render() {
this._moveParticles()
this._drawParticles()
return this
}

clearCanvas() {
this.stop()
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
this.forceStop = false
}

/**
*
*/
start() {
this.starting = true
this.forceStop = false
this._then = Date.now()
this.animate()
}

/**
*
*/
stop() {
cancelAnimationFrame(this.animationLoop)
this.starting = false
this.forceStop = true
}

/**
*
*/
animate() {
if (this.animationLoop) {
cancelAnimationFrame(this.animationLoop)
}
this.animationLoop = requestAnimationFrame(this.animate)
let now = Date.now()
let delta = now - this._then
if (delta > this.options.frameRate) {
this._then = now - (delta % this.options.frameRate)
this.render()
}
}

/**
*
* @param field
* @returns {WindCanvas}
*/
setData(field) {
this.field = field
return this
}

/**
*
* @param options
* @returns {WindCanvas}
*/
setOptions(options) {
this.options = options
if (!this.options?.maxAge && this.options?.particleAge) {
this.options.maxAge = Number(this.options.particleAge)
}
if (!this.options?.paths && this.options?.particleMultiplier) {
this.options.paths = Math.round(
this.options.width *
this.options.height *
Number(this.options.particleMultiplier)
)
}
return this
}
}

export default WindCanvas

+ 228
- 0
modules/wind/WindLayer.js View File

@@ -0,0 +1,228 @@
/**
* @Author: Caven
* @Date: 2021-01-18 20:13:30
*/

import State from '@dc-modules/state/State'
import { Layer } from '@dc-modules/layer'
import Field from './Field'
import WindCanvas from './WindCanvas'

const DEF_OPTS = {
globalAlpha: 0.9,
lineWidth: 1,
colorScale: '#fff',
velocityScale: 1 / 25,
maxAge: 90,
paths: 800,
frameRate: 20,
useCoordsDraw: true,
gpet: true
}

const { Cesium } = DC.Namespace

class WindLayer extends Layer {
constructor(id, options = {}) {
super(id)
this._options = {
...DEF_OPTS,
...options
}
this._data = undefined
this._canvas = document.createElement('canvas')
this.type = Layer.getLayerType('wind')
this._state = State.INITIALIZED
}

set show(show) {
this._show = show
this._canvas.style.visibility = show ? 'visible' : 'hidden'
}

get show() {
return this._show
}

/**
*
* @param data
* @returns {Field|undefined}
* @private
*/
_formatData(data) {
let uComp
let vComp
data.forEach(function(record) {
switch (
record.header.parameterCategory +
',' +
record.header.parameterNumber
) {
case '1,2':
case '2,2':
uComp = record
break
case '1,3':
case '2,3':
vComp = record
break
}
})
if (!vComp || !uComp) {
return undefined
}
let header = uComp.header
return new Field({
xmin: header.lo1,
ymin: header.la1,
xmax: header.lo2,
ymax: header.la2,
deltaX: header.dx,
deltaY: header.dy,
cols: header.nx,
rows: header.ny,
us: uComp.data,
vs: vComp.data
})
}

/**
*
* @private
*/
_mountCanvas() {
if (!this._viewer || !this._canvas) {
return
}
this._canvas.style.cssText =
'position:absolute; left:0; top:0;user-select:none;pointer-events: none;'
this._canvas.className = 'dc-wind-layer'
const { width, height } = this._viewer.canvas
this._canvas.width = width
this._canvas.height = height
this._canvas.style.width = width + 'px'
this._canvas.style.height = height + 'px'
this._viewer.dcContainer.appendChild(this._canvas)
}

/**
*
* @private
*/
_addedHook() {
let scene = this._viewer.scene
let camera = this._viewer.camera
let ellipsoid = Cesium.Ellipsoid.WGS84
this._delegate.intersectsCoordinate = coordinate => {
let occluder = new Cesium.EllipsoidalOccluder(ellipsoid, camera.position)
let point = Cesium.Cartesian3.fromDegrees(coordinate[0], coordinate[1])
return occluder.isPointVisible(point)
}

this._delegate.project = coordinate => {
let position = Cesium.Cartesian3.fromDegrees(coordinate[0], coordinate[1])
let coord = Cesium.SceneTransforms.wgs84ToWindowCoordinates(
scene,
position
)
return [coord.x, coord.y]
}

this._delegate.unProject = pixel => {
let pick = new Cesium.Cartesian2(pixel[0], pixel[1])
let cartesian = scene.globe.pick(camera.getPickRay(pick), scene)
if (!cartesian) {
return null
}
let cartographic = ellipsoid.cartesianToCartographic(cartesian)
let lat = Cesium.Math.toDegrees(cartographic.latitude)
let lng = Cesium.Math.toDegrees(cartographic.longitude)
return [lng, lat]
}
}

/**
*
* @param viewer
* @private
*/
_onAdd(viewer) {
this._viewer = viewer
this._mountCanvas()
let ctx = this._canvas.getContext('2d')
if (!this._delegate) {
this._delegate = new WindCanvas(ctx)
this._delegate.setOptions(this._options)
this._addedHook()
}
if (this._data) {
this._delegate.setData(this._data)
this._delegate.prerender()
this._delegate.render()
}
}

/**
*
* @private
*/
_onRemove() {
if (this._delegate) {
this._delegate.stop()
}
if (this._canvas) {
this._viewer.dcContainer.removeChild(this._canvas)
}
delete this._canvas
}

/**
*
* @param data
* @param options
* @returns {WindLayer}
*/
setData(data, options) {
if (data && data.checkFields && data.checkFields()) {
this._data = data
} else if (Array.isArray(data)) {
this._data = this._formatData(data)
}
if (this._delegate) {
this._delegate.setData(this._data)
if (options) {
this._options = {
...this._options,
...options
}
this._delegate.setOptions(this._options)
}
this._delegate.prerender()
this._delegate.render()
}
return this
}

/**
*
* @param options
* @returns {WindLayer}
*/
setOptions(options) {
this._options = {
...this._options,
...options
}
if (this._delegate) {
this._delegate.setOptions(this._options)
this._delegate.prerender()
this._delegate.render()
}
return this
}
}

Layer.registerType('wind')

export default WindLayer

Loading…
Cancel
Save