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