| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||