可视化,mapboxGL加载台风风场动画
这张酷似《星月夜》的气象数据——风场可视化地图是怎么做的? 在网上搜集了很多篇文章,今天总结出了使用最简单的方法,源码都在这了
核心代码
-
-
var Windy = function Windy(params) {
-
var MIN_VELOCITY_INTENSITY = params.minVelocity || 0; // velocity at which particle intensity is minimum (m/s)
-
-
var MAX_VELOCITY_INTENSITY = params.maxVelocity || 10; // velocity at which particle intensity is maximum (m/s)
-
-
var VELOCITY_SCALE = (params.velocityScale || 0.005) * (Math.pow(window.devicePixelRatio, 1 / 3) || 1); // scale for wind velocity (completely arbitrary--this value looks nice)
-
-
var MAX_PARTICLE_AGE = params.particleAge || 90; // max number of frames a particle is drawn before regeneration
-
-
var PARTICLE_LINE_WIDTH = params.lineWidth || 1; // line width of a drawn particle
-
-
var PARTICLE_MULTIPLIER = params.particleMultiplier || 1 / 300; // particle count scalar (completely arbitrary--this values looks nice)
-
-
var PARTICLE_REDUCTION = Math.pow(window.devicePixelRatio, 1 / 3) || 1.6; // multiply particle count for mobiles by this amount
-
-
var FRAME_RATE = params.frameRate || 15;
-
var FRAME_TIME = 1000 / FRAME_RATE; // desired frames per second
-
-
var OPACITY = 0.97;
-
var defaulColorScale = ["rgb(36,104, 180)", "rgb(60,157, 194)", "rgb(128,205,193 )", "rgb(151,218,168 )", "rgb(198,231,181)", "rgb(238,247,217)", "rgb(255,238,159)", "rgb(252,217,125)", "rgb(255,182,100)", "rgb(252,150,75)", "rgb(250,112,52)", "rgb(245,64,32)", "rgb(237,45,28)", "rgb(220,24,32)", "rgb(180,0,35)"];
-
var colorScale = params.colorScale || defaulColorScale;
-
var NULL_WIND_VECTOR = [NaN, NaN, null]; // singleton for no wind in the form: [u, v, magnitude]
-
-
var builder;
-
var grid;
-
var gridData = params.data;
-
var date;
-
var λ0, φ0, Δλ, Δφ, ni, nj;
-
-
var setData = function setData(data) {
-
gridData = data;
-
};
-
-
var setOptions = function setOptions(options) {
-
if (options.hasOwnProperty("minVelocity")) MIN_VELOCITY_INTENSITY = options.minVelocity;
-
if (options.hasOwnProperty("maxVelocity")) MAX_VELOCITY_INTENSITY = options.maxVelocity;
-
if (options.hasOwnProperty("velocityScale")) VELOCITY_SCALE = (options.velocityScale || 0.005) * (Math.pow(window.devicePixelRatio, 1 / 3) || 1);
-
if (options.hasOwnProperty("particleAge")) MAX_PARTICLE_AGE = options.particleAge;
-
if (options.hasOwnProperty("lineWidth")) PARTICLE_LINE_WIDTH = options.lineWidth;
-
if (options.hasOwnProperty("particleMultiplier")) PARTICLE_MULTIPLIER = options.particleMultiplier;
-
if (options.hasOwnProperty("opacity")) OPACITY = options.opacity;
-
if (options.hasOwnProperty("frameRate")) FRAME_RATE = options.frameRate;
-
FRAME_TIME = 1000 / FRAME_RATE;
-
}; // interpolation for vectors like wind (u,v,m)
-
-
-
var bilinearInterpolateVector = function bilinearInterpolateVector(x, y, g00, g10, g01, g11) {
-
var rx = 1 - x;
-
var ry = 1 - y;
-
var a = rx * ry,
-
b = x * ry,
-
c = rx * y,
-
d = x * y;
-
var u = g00[0] * a g10[0] * b g01[0] * c g11[0] * d;
-
var v = g00[1] * a g10[1] * b g01[1] * c g11[1] * d;
-
return [u, v, Math.sqrt(u * u v * v)];
-
};
-
-
var createWindBuilder = function createWindBuilder(uComp, vComp) {
-
var uData = uComp.data,
-
vData = vComp.data;
-
return {
-
header: uComp.header,
-
//recipe: recipeFor("wind-" uComp.header.surface1Value),
-
data: function data(i) {
-
return [uData[i], vData[i]];
-
},
-
interpolate: bilinearInterpolateVector
-
};
-
};
-
-
var createBuilder = function createBuilder(data) {
-
var uComp = null,
-
vComp = null,
-
scalar = null;
-
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;
-
-
default:
-
scalar = record;
-
}
-
});
-
return createWindBuilder(uComp, vComp);
-
};
-
-
var buildGrid = function buildGrid(data, callback) {
-
var supported = true;
-
if (data.length < 2) supported = false;
-
if (!supported) console.log("Windy Error: data must have at least two components (u,v)");
-
builder = createBuilder(data);
-
var header = builder.header;
-
if (header.hasOwnProperty("gridDefinitionTemplate") && header.gridDefinitionTemplate != 0) supported = false;
-
-
if (!supported) {
-
console.log("Windy Error: Only data with Latitude_Longitude coordinates is supported");
-
}
-
-
supported = true; // reset for futher checks
-
-
λ0 = header.lo1;
-
φ0 = header.la1; // the grid's origin (e.g., 0.0E, 90.0N)
-
-
Δλ = header.dx;
-
Δφ = header.dy; // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat)
-
-
ni = header.nx;
-
nj = header.ny; // number of grid points W-E and N-S (e.g., 144 x 73)
-
-
if (header.hasOwnProperty("scanMode")) {
-
var scanModeMask = header.scanMode.toString(2);
-
scanModeMask = ('0' scanModeMask).slice(-8);
-
var scanModeMaskArray = scanModeMask.split('').map(Number).map(Boolean);
-
if (scanModeMaskArray[0]) Δλ = -Δλ;
-
if (scanModeMaskArray[1]) Δφ = -Δφ;
-
if (scanModeMaskArray[2]) supported = false;
-
if (scanModeMaskArray[3]) supported = false;
-
if (scanModeMaskArray[4]) supported = false;
-
if (scanModeMaskArray[5]) supported = false;
-
if (scanModeMaskArray[6]) supported = false;
-
if (scanModeMaskArray[7]) supported = false;
-
if (!supported) console.log("Windy Error: Data with scanMode: " header.scanMode " is not supported.");
-
}
-
-
date = new Date(header.refTime);
-
date.setHours(date.getHours() header.forecastTime); // Scan modes 0, 64 allowed.
-
// http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table3-4.shtml
-
-
grid = [];
-
var p = 0;
-
var isContinuous = Math.floor(ni * Δλ) >= 360;
-
-
for (var j = 0; j < nj; j ) {
-
var row = [];
-
-
for (var i = 0; i < ni; i , p ) {
-
row[i] = builder.data(p);
-
}
-
-
if (isContinuous) {
-
// For wrapped grids, duplicate first column as last column to simplify interpolation logic
-
row.push(row[0]);
-
}
-
-
grid[j] = row;
-
}
-
-
callback({
-
date: date,
-
interpolate: interpolate
-
});
-
};
-
/**
-
* Get interpolated grid value from Lon/Lat position
-
* @param λ {Float} Longitude
-
* @param φ {Float} Latitude
-
* @returns {Object}
-
*/
-
-
-
var interpolate = function interpolate(λ, φ) {
-
if (!grid) return null;
-
var i = floorMod(λ - λ0, 360) / Δλ; // calculate longitude index in wrapped range [0, 360)
-
-
var j = (φ0 - φ) / Δφ; // calculate latitude index in direction 90 to -90
-
-
var fi = Math.floor(i),
-
ci = fi 1;
-
var fj = Math.floor(j),
-
cj = fj 1;
-
var row;
-
-
if (row = grid[fj]) {
-
var g00 = row[fi];
-
var g10 = row[ci];
-
-
if (isValue(g00) && isValue(g10) && (row = grid[cj])) {
-
var g01 = row[fi];
-
var g11 = row[ci];
-
-
if (isValue(g01) && isValue(g11)) {
-
// All four points found, so interpolate the value.
-
return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11);
-
}
-
}
-
}
-
-
return null;
-
};
-
/**
-
* @returns {Boolean} true if the specified value is not null and not undefined.
-
*/
-
-
-
var isValue = function isValue(x) {
-
return x !== null && x !== undefined;
-
};
-
/**
-
* @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
-
* of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
-
*/
-
-
-
var floorMod = function floorMod(a, n) {
-
return a - n * Math.floor(a / n);
-
};
-
/**
-
* @returns {Number} the value x clamped to the range [low, high].
-
*/
-
-
-
var clamp = function clamp(x, range) {
-
return Math.max(range[0], Math.min(x, range[1]));
-
};
-
/**
-
* @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate.
-
*/
-
-
-
var isMobile = function isMobile() {
-
return /android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(navigator.userAgent);
-
};
-
/**
-
* Calculate distortion of the wind vector caused by the shape of the projection at point (x, y). The wind
-
* vector is modified in place and returned by this function.
-
*/
-
-
-
var distort = function distort(projection, λ, φ, x, y, scale, wind) {
-
var u = wind[0] * scale;
-
var v = wind[1] * scale;
-
var d = distortion(projection, λ, φ, x, y); // Scale distortion vectors by u and v, then add.
-
-
wind[0] = d[0] * u d[2] * v;
-
wind[1] = d[1] * u d[3] * v;
-
return wind;
-
};
-
-
var distortion = function distortion(projection, λ, φ, x, y) {
-
var τ = 2 * Math.PI; // var H = Math.pow(10, -5.2); // 0.00000630957344480193
-
// var H = 0.0000360; // 0.0000360°φ ~= 4m (from https://github.com/cambecc/earth/blob/master/public/libs/earth/1.0.0/micro.js#L13)
-
-
var H = 5; // ToDo: Why does this work?
-
-
var hλ = λ < 0 ? H : -H;
-
var hφ = φ < 0 ? H : -H;
-
var pλ = project(φ, λ hλ);
-
var pφ = project(φ hφ, λ); // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1º λ
-
// changes depending on φ. Without this, there is a pinching effect at the poles.
-
-
var k = Math.cos(φ / 360 * τ);
-
return [(pλ[0] - x) / hλ / k, (pλ[1] - y) / hλ / k, (pφ[0] - x) / hφ, (pφ[1] - y) / hφ];
-
};
-
-
var createField = function createField(columns, bounds, callback) {
-
/**
-
* @returns {Array} wind vector [u, v, magnitude] at the point (x, y), or [NaN, NaN, null] if wind
-
* is undefined at that point.
-
*/
-
function field(x, y) {
-
var column = columns[Math.round(x)];
-
return column && column[Math.round(y)] || NULL_WIND_VECTOR;
-
} // Frees the massive "columns" array for GC. Without this, the array is leaked (in Chrome) each time a new
-
// field is interpolated because the field closure's context is leaked, for reasons that defy explanation.
-
-
-
field.release = function () {
-
columns = [];
-
};
-
-
field.randomize = function (o) {
-
// UNDONE: this method is terrible
-
var x, y;
-
var safetyNet = 0;
-
-
do {
-
x = Math.round(Math.floor(Math.random() * bounds.width) bounds.x);
-
y = Math.round(Math.floor(Math.random() * bounds.height) bounds.y);
-
} while (field(x, y)[2] === null && safetyNet < 30);
-
-
o.x = x;
-
o.y = y;
-
return o;
-
};
-
-
callback(bounds, field);
-
};
-
-
var buildBounds = function buildBounds(bounds, width, height) {
-
var upperLeft = bounds[0];
-
var lowerRight = bounds[1];
-
var x = Math.round(upperLeft[0]); //Math.max(Math.floor(upperLeft[0], 0), 0);
-
-
var y = Math.max(Math.floor(upperLeft[1], 0), 0);
-
var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1);
-
var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1);
-
return {
-
x: x,
-
y: y,
-
xMax: width,
-
yMax: yMax,
-
width: width,
-
height: height
-
};
-
};
-
-
var deg2rad = function deg2rad(deg) {
-
return deg / 180 * Math.PI;
-
};
-
-
var invert = function invert(x, y, windy) {
-
var latlon = params.map.unproject([x, y]);
-
return [latlon.lng, latlon.lat];
-
};
-
-
var project = function project(lat, lon, windy) {
-
var xy = params.map.project([lon, lat]);
-
return [xy.x, xy.y];
-
};
-
-
var interpolateField = function interpolateField(grid, bounds, extent, callback) {
-
var projection = {}; // map.crs used instead
-
-
var mapArea = (extent.south - extent.north) * (extent.west - extent.east);
-
var velocityScale = VELOCITY_SCALE * Math.pow(mapArea, 0.4);
-
var columns = [];
-
var x = bounds.x;
-
-
function interpolateColumn(x) {
-
var column = [];
-
-
for (var y = bounds.y; y <= bounds.yMax; y = 2) {
-
var coord = invert(x, y);
-
-
if (coord) {
-
var λ = coord[0],
-
φ = coord[1];
-
-
if (isFinite(λ)) {
-
var wind = grid.interpolate(λ, φ);
-
-
if (wind) {
-
wind = distort(projection, λ, φ, x, y, velocityScale, wind);
-
column[y 1] = column[y] = wind;
-
}
-
}
-
}
-
}
-
-
columns[x 1] = columns[x] = column;
-
}
-
-
(function batchInterpolate() {
-
var start = Date.now();
-
-
while (x < bounds.width) {
-
interpolateColumn(x);
-
x = 2;
-
-
if (Date.now() - start > 1000) {
-
//MAX_TASK_TIME) {
-
setTimeout(batchInterpolate, 25);
-
return;
-
}
-
}
-
-
createField(columns, bounds, callback);
-
})();
-
};
-
-
var animationLoop;
-
-
var animate = function animate(bounds, field) {
-
function windIntensityColorScale(min, max) {
-
colorScale.indexFor = function (m) {
-
// map velocity speed to a style
-
return Math.max(0, Math.min(colorScale.length - 1, Math.round((m - min) / (max - min) * (colorScale.length - 1))));
-
};
-
-
return colorScale;
-
}
-
-
var colorStyles = windIntensityColorScale(MIN_VELOCITY_INTENSITY, MAX_VELOCITY_INTENSITY);
-
var buckets = colorStyles.map(function () {
-
return [];
-
});
-
var particleCount = Math.round(bounds.width * bounds.height * PARTICLE_MULTIPLIER);
-
-
if (isMobile()) {
-
particleCount *= PARTICLE_REDUCTION;
-
}
-
-
var fadeFillStyle = "rgba(0, 0, 0, ".concat(OPACITY, ")");
-
var particles = [];
-
-
for (var i = 0; i < particleCount; i ) {
-
particles.push(field.randomize({
-
age: Math.floor(Math.random() * MAX_PARTICLE_AGE) 0
-
}));
-
}
-
-
function evolve() {
-
buckets.forEach(function (bucket) {
-
bucket.length = 0;
-
});
-
particles.forEach(function (particle) {
-
if (particle.age > MAX_PARTICLE_AGE) {
-
field.randomize(particle).age = 0;
-
}
-
-
var x = particle.x;
-
var y = particle.y;
-
var v = field(x, y); // vector at current position
-
-
var m = v[2];
-
-
if (m === null) {
-
particle.age = MAX_PARTICLE_AGE; // particle has escaped the grid, never to return...
-
} else {
-
var xt = x v[0];
-
var yt = y v[1];
-
-
if (field(xt, yt)[2] !== null) {
-
// Path from (x,y) to (xt,yt) is visible, so add this particle to the appropriate draw bucket.
-
particle.xt = xt;
-
particle.yt = yt;
-
buckets[colorStyles.indexFor(m)].push(particle);
-
} else {
-
// Particle isn't visible, but it still moves through the field.
-
particle.x = xt;
-
particle.y = yt;
-
}
-
}
-
-
particle.age = 1;
-
});
-
}
-
-
var g = params.canvas.getContext("2d");
-
g.lineWidth = PARTICLE_LINE_WIDTH;
-
g.fillStyle = fadeFillStyle;
-
g.globalAlpha = 0.6;
-
-
function draw() {
-
// Fade existing particle trails.
-
var prev = "lighter";
-
g.globalCompositeOperation = "destination-in";
-
g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
-
g.globalCompositeOperation = prev;
-
g.globalAlpha = OPACITY === 0 ? 0 : OPACITY * 0.9; // Draw new particle trails.
-
-
buckets.forEach(function (bucket, i) {
-
if (bucket.length > 0) {
-
g.beginPath();
-
g.strokeStyle = colorStyles[i];
-
bucket.forEach(function (particle) {
-
g.moveTo(particle.x, particle.y);
-
g.lineTo(particle.xt, particle.yt);
-
particle.x = particle.xt;
-
particle.y = particle.yt;
-
});
-
g.stroke();
-
}
-
});
-
}
-
-
var then = Date.now();
-
-
(function frame() {
-
animationLoop = requestAnimationFrame(frame);
-
var now = Date.now();
-
var delta = now - then;
-
-
if (delta > FRAME_TIME) {
-
then = now - delta % FRAME_TIME;
-
evolve();
-
draw();
-
}
-
})();
-
};
-
-
var start = function start(bounds, width, height, extent) {
-
var mapBounds = {
-
south: deg2rad(extent[0][1]),
-
north: deg2rad(extent[1][1]),
-
east: deg2rad(extent[1][0]),
-
west: deg2rad(extent[0][0]),
-
width: width,
-
height: height
-
};
-
stop(); // build grid
-
-
buildGrid(gridData, function (grid) {
-
// interpolateField
-
interpolateField(grid, buildBounds(bounds, width, height), mapBounds, function (bounds, field) {
-
// animate the canvas with random points
-
windy.field = field;
-
animate(bounds, field);
-
});
-
});
-
};
-
-
var stop = function stop() {
-
if (windy.field) windy.field.release();
-
if (animationLoop) cancelAnimationFrame(animationLoop);
-
};
-
-
var windy = {
-
params: params,
-
start: start,
-
stop: stop,
-
createField: createField,
-
interpolatePoint: interpolate,
-
setData: setData,
-
setOptions: setOptions
-
};
-
return windy;
-
};
-
-
if (!window.cancelAnimationFrame) {
-
window.cancelAnimationFrame = function (id) {
-
clearTimeout(id);
-
};
-
}
-
-
加载方法
-
var windyMap = {
-
windy: null,
-
map: null,
-
visible: true,
-
context: null,
-
timer: 0,
-
initWindy(data, map) {
-
const self = this;
-
self.visible = true;
-
self.map = map;
-
-
// 删除dom
-
self.hideWind();
-
-
let canvas = document.createElement('canvas');
-
canvas.id = 'windCanvas';
-
canvas.width = map.getCanvas().width;
-
canvas.height = map.getCanvas().height;
-
canvas.style.position = 'absolute';
-
canvas.style.top = 0;
-
canvas.style.left = 0;
-
map.getCanvasContainer().appendChild(canvas);
-
this.context = canvas.getContext("2d");
-
-
self.windy = new Windy({
-
canvas: canvas,
-
data: data,
-
map: map
-
});
-
-
if (self.timer) clearTimeout(self.timer);
-
this.timer = setTimeout(function () {
-
self._refreshWindy();
-
}, 750);
-
-
map.on("dragstart", function(){
-
if(self.context) self.context.clearRect(0, 0, 3000, 3000);
-
self.windy.stop();
-
});
-
-
map.on("dragend", function() {
-
self._refreshWindy();
-
});
-
-
map.on("zoomstart", function(){
-
if(self.context) self.context.clearRect(0, 0, 3000, 3000);
-
self.windy.stop();
-
});
-
-
map.on("zoomend", function() {
-
self._refreshWindy();
-
});
-
-
map.on("resize", function() {
-
self.clearWind();
-
});
-
},
-
_refreshWindy: function() {
-
const self = this;
-
const _canvas = self.windy.params.canvas;
-
if (!self.windy) return;
-
let bounds = self.map.getBounds();
-
let extent = [
-
bounds._sw.lng,
-
bounds._sw.lat,
-
bounds._ne.lng,
-
bounds._ne.lat
-
];
-
-
_canvas.width = map.getCanvas().width;
-
_canvas.height = map.getCanvas().height;
-
-
self.windy.start(
-
[[0, 0], [_canvas.width, _canvas.height]],
-
_canvas.width,
-
_canvas.height,
-
[[extent[0], extent[1]], [extent[2], extent[3]]]
-
);
-
},
-
-
hideWind: function() {
-
if(this.context) this.context.clearRect(0, 0, 3000, 3000);
-
let dom = document.getElementById('windCanvas');
-
if (dom) dom.parentNode.removeChild(dom);
-
},
-
-
clearWind: function() {
-
if (this.windy) this.windy.stop();
-
if(this.context) this.context.clearRect(0, 0, 3000, 3000);
-
},
-
-
setVisible: function(flag) {
-
const self = this;
-
self.visible = flag;
-
let dom = document.getElementById('windCanvas');
-
if (!dom) return;
-
if (flag) {
-
dom.style.display = 'block';
-
self._refreshWindy();
-
} else {
-
if (self.windy) self.windy.stop();
-
dom.style.display = 'none';
-
}
-
}
-
};
调用
-
map.on('load', function() {
-
axios.get(`https://datacenter.istrongcloud.com/data/gfs/time_fc.json`, function(res) {
-
-
axios.get(`https://datacenter.istrongcloud.com/data/gfs/fcdata/${res.data.data['024'].url}`).then(windRes => {
-
console.log('windRes: ', windRes.data);
-
windyMap.initWindy(Object.values(windRes.data)[0], map);
-
});
-
})
-
});
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhghafkg
系列文章
更多
同类精品
更多
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13