123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904 |
- "use strict";
- /*
- Generic Canvas Layer for bigemap 0.7 and 1.0-rc,
- copyright Stanislav Sumbera, 2016 , sumbera.com , license MIT
- originally created and motivated by BM.CanvasOverlay available here: https://gist.github.com/Sumbera/11114288
- */
- // -- BM.DomUtil.setTransform from bigemap 1.0.0 to work on 0.0.7
- //------------------------------------------------------------------------------
- if (!BM.DomUtil.setTransform) {
- BM.DomUtil.setTransform = function (el, offset, scale) {
- var pos = offset || new BM.Point(0, 0);
- el.style[BM.DomUtil.TRANSFORM] = (BM.Browser.ie3d ? "translate(" + pos.x + "px," + pos.y + "px)" : "translate3d(" + pos.x + "px," + pos.y + "px,0)") + (scale ? " scale(" + scale + ")" : "");
- };
- }
- // -- support for both 0.0.7 and 1.0.0 rc2 bigemap
- BM.CanvasLayer = (BM.Layer ? BM.Layer : BM.Class).extend({
- // -- initialized is called on prototype
- initialize: function initialize(options) {
- this._map = null;
- this._canvas = null;
- this._frame = null;
- this._delegate = null;
- BM.setOptions(this, options);
- },
- delegate: function delegate(del) {
- this._delegate = del;
- return this;
- },
- needRedraw: function needRedraw() {
- if (!this._frame) {
- this._frame = BM.Util.requestAnimFrame(this.drawLayer, this);
- }
- return this;
- },
- //-------------------------------------------------------------
- _onLayerDidResize: function _onLayerDidResize(resizeEvent) {
- this._canvas.width = resizeEvent.newSize.x;
- this._canvas.height = resizeEvent.newSize.y;
- },
- //-------------------------------------------------------------
- _onLayerDidMove: function _onLayerDidMove() {
- var topLeft = this._map.containerPointToLayerPoint([0, 0]);
- BM.DomUtil.setPosition(this._canvas, topLeft);
- this.drawLayer();
- },
- //-------------------------------------------------------------
- getEvents: function getEvents() {
- var events = {
- resize: this._onLayerDidResize,
- moveend: this._onLayerDidMove
- };
- if (this._map.options.zoomAnimation && BM.Browser.any3d) {
- events.zoomanim = this._animateZoom;
- }
- return events;
- },
- //-------------------------------------------------------------
- onAdd: function onAdd(map) {
- this._map = map;
- this._canvas = BM.DomUtil.create("canvas", "bigemap-layer");
- this.tiles = {};
- var size = this._map.getSize();
- this._canvas.width = size.x;
- this._canvas.height = size.y;
- var animated = this._map.options.zoomAnimation && BM.Browser.any3d;
- BM.DomUtil.addClass(this._canvas, "bigemap-zoom-" + (animated ? "animated" : "hide"));
- this.options.pane.appendChild(this._canvas);
- map.on(this.getEvents(), this);
- var del = this._delegate || this;
- del.onLayerDidMount && del.onLayerDidMount(); // -- callback
- this.needRedraw();
- var self = this;
- setTimeout(function () {
- self._onLayerDidMove();
- }, 0);
- },
- //-------------------------------------------------------------
- onRemove: function onRemove(map) {
- var del = this._delegate || this;
- del.onLayerWillUnmount && del.onLayerWillUnmount(); // -- callback
- this.options.pane.removeChild(this._canvas);
- map.off(this.getEvents(), this);
- this._canvas = null;
- },
- //------------------------------------------------------------
- addTo: function addTo(map) {
- map.addLayer(this);
- return this;
- },
- //------------------------------------------------------------------------------
- drawLayer: function drawLayer() {
- // -- todo make the viewInfo properties flat objects.
- var size = this._map.getSize();
- var bounds = this._map.getBounds();
- var zoom = this._map.getZoom();
- var center = this._map.options.crs.project(this._map.getCenter());
- var corner = this._map.options.crs.project(this._map.containerPointToLatLng(this._map.getSize()));
- var del = this._delegate || this;
- del.onDrawLayer && del.onDrawLayer({
- layer: this,
- canvas: this._canvas,
- bounds: bounds,
- size: size,
- zoom: zoom,
- center: center,
- corner: corner
- });
- this._frame = null;
- },
- // -- BM.DomUtil.setTransform from bigemap 1.0.0 to work on 0.0.7
- //------------------------------------------------------------------------------
- _setTransform: function _setTransform(el, offset, scale) {
- var pos = offset || new BM.Point(0, 0);
- el.style[BM.DomUtil.TRANSFORM] = (BM.Browser.ie3d ? "translate(" + pos.x + "px," + pos.y + "px)" : "translate3d(" + pos.x + "px," + pos.y + "px,0)") + (scale ? " scale(" + scale + ")" : "");
- },
- //------------------------------------------------------------------------------
- _animateZoom: function _animateZoom(e) {
- var scale = this._map.getZoomScale(e.zoom);
- // -- different calc of offset in bigemap 1.0.0 and 0.0.7 thanks for 1.0.0-rc2 calc @jduggan1
- var offset = BM.Layer ? this._map._latLngToNewLayerPoint(this._map.getBounds().getNorthWest(), e.zoom, e.center) : this._map._getCenterOffset(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos());
- BM.DomUtil.setTransform(this._canvas, offset, scale);
- }
- });
- BM.canvasLayer = function (pane) {
- return new BM.CanvasLayer(pane);
- };
- BM.Control.Velocity = BM.Control.extend({
- options: {
- position: "topleft",
- emptyString: "Unavailable",
- // Could be any combination of 'bearing' (angle toward which the flow goes) or 'meteo' (angle from which the flow comes)
- // and 'CW' (angle value increases clock-wise) or 'CCW' (angle value increases counter clock-wise)
- angleConvention: "bearingCCW",
- showCardinal: false,
- // Could be 'm/s' for meter per second, 'k/h' for kilometer per hour, 'mph' for miles per hour or 'kt' for knots
- speedUnit: "m/s",
- directionString: "风向",
- speedString: "速度",
- onAdd: null,
- onRemove: null,
- handMove: true,
- handClick: true
- },
- onAdd: function onAdd(map) {
- this._container = BM.DomUtil.create("div", "bigemap-control-velocity");
- BM.DomEvent.disableClickPropagation(this._container);
- if (this.options.handMove) map.on("mousemove", this._onMouseMove, this);
- if (this.options.handClick) map.on('click', this._onClick, this);
- this._container.innerHTML = this.options.emptyString;
- if (this.options.bigemapVelocity.options.onAdd) this.options.bigemapVelocity.options.onAdd();
- return this._container;
- },
- onRemove: function onRemove(map) {
- map.off("mousemove", this._onMouseMove, this);
- map.off('click', this._onClick, this);
- if (this.options.bigemapVelocity.options.onRemove) this.options.bigemapVelocity.options.onRemove();
- },
- vectorToSpeed: function vectorToSpeed(uMs, vMs, unit) {
- var velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2));
- // Default is m/s
- if (unit === "k/h") {
- return this.meterSec2kilometerHour(velocityAbs);
- } else if (unit === "kt") {
- return this.meterSec2Knots(velocityAbs);
- } else if (unit === "mph") {
- return this.meterSec2milesHour(velocityAbs);
- } else {
- return velocityAbs;
- }
- },
- vectorToDegrees: function vectorToDegrees(uMs, vMs, angleConvention) {
- // Default angle convention is CW
- if (angleConvention.endsWith("CCW")) {
- // vMs comes out upside-down..
- vMs = vMs > 0 ? vMs = -vMs : Math.abs(vMs);
- }
- var velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2));
- var velocityDir = Math.atan2(uMs / velocityAbs, vMs / velocityAbs);
- var velocityDirToDegrees = velocityDir * 180 / Math.PI + 180;
- if (angleConvention === "bearingCW" || angleConvention === "meteoCCW") {
- velocityDirToDegrees += 180;
- if (velocityDirToDegrees >= 360) velocityDirToDegrees -= 360;
- }
- return velocityDirToDegrees;
- },
- degreesToCardinalDirection: function degreesToCardinalDirection(deg) {
- var cardinalDirection = '';
- if (deg >= 0 && deg < 11.25 || deg >= 348.75) {
- cardinalDirection = 'N';
- } else if (deg >= 11.25 && deg < 33.75) {
- cardinalDirection = 'NNW';
- } else if (deg >= 33.75 && deg < 56.25) {
- cardinalDirection = 'NW';
- } else if (deg >= 56.25 && deg < 78.75) {
- cardinalDirection = 'WNW';
- } else if (deg >= 78.25 && deg < 101.25) {
- cardinalDirection = 'W';
- } else if (deg >= 101.25 && deg < 123.75) {
- cardinalDirection = 'WSW';
- } else if (deg >= 123.75 && deg < 146.25) {
- cardinalDirection = 'SW';
- } else if (deg >= 146.25 && deg < 168.75) {
- cardinalDirection = 'SSW';
- } else if (deg >= 168.75 && deg < 191.25) {
- cardinalDirection = 'S';
- } else if (deg >= 191.25 && deg < 213.75) {
- cardinalDirection = 'SSE';
- } else if (deg >= 213.75 && deg < 236.25) {
- cardinalDirection = 'SE';
- } else if (deg >= 236.25 && deg < 258.75) {
- cardinalDirection = 'ESE';
- } else if (deg >= 258.75 && deg < 281.25) {
- cardinalDirection = 'E';
- } else if (deg >= 281.25 && deg < 303.75) {
- cardinalDirection = 'ENE';
- } else if (deg >= 303.75 && deg < 326.25) {
- cardinalDirection = 'NE';
- } else if (deg >= 326.25 && deg < 348.75) {
- cardinalDirection = 'NNE';
- }
- return cardinalDirection;
- },
- meterSec2Knots: function meterSec2Knots(meters) {
- return meters / 0.514;
- },
- meterSec2kilometerHour: function meterSec2kilometerHour(meters) {
- return meters * 3.6;
- },
- meterSec2milesHour: function meterSec2milesHour(meters) {
- return meters * 2.23694;
- },
- _onMouseMove: function _onMouseMove(e) {
- var self = this;
- var pos = this.options.bigemapVelocity._map.containerPointToLatLng(BM.point(e.containerPoint.x, e.containerPoint.y));
- var gridValue = this.options.bigemapVelocity._windy.interpolatePoint(pos.lng, pos.lat);
- var htmlOut = "";
- if (gridValue && !isNaN(gridValue[0]) && !isNaN(gridValue[1]) && gridValue[2]) {
- var deg = self.vectorToDegrees(gridValue[0], gridValue[1], this.options.angleConvention);
- var cardinal = this.options.showCardinal ? " (".concat(self.degreesToCardinalDirection(deg), ") ") : '';
- htmlOut = "<strong> ".concat(this.options.velocityType, " ").concat(this.options.directionString, ": </strong> ").concat(deg.toFixed(2), "\xB0").concat(cardinal, ", <strong> ").concat(this.options.velocityType, " ").concat(this.options.speedString, ": </strong> ").concat(self.vectorToSpeed(gridValue[0], gridValue[1], this.options.speedUnit).toFixed(2), " ").concat(this.options.speedUnit);
- self._map.fire('VelocityLayerMove', {
- direct: "".concat(deg.toFixed(2), "\xB0").concat(cardinal),
- speed: "".concat(self.vectorToSpeed(gridValue[0], gridValue[1], this.options.speedUnit).toFixed(2), " ").concat(this.options.speedUnit),
- context: this,
- Event: e
- });
- } else {
- htmlOut = this.options.emptyString;
- }
- self._container.innerHTML = htmlOut;
- },
- _onClick: function _onClick(e) {
- var self = this;
- var pos = this.options.bigemapVelocity._map.containerPointToLatLng(BM.point(e.containerPoint.x, e.containerPoint.y));
- var gridValue = this.options.bigemapVelocity._windy.interpolatePoint(pos.lng, pos.lat);
- var htmlOut = "";
- if (gridValue && !isNaN(gridValue[0]) && !isNaN(gridValue[1]) && gridValue[2]) {
- var deg = self.vectorToDegrees(gridValue[0], gridValue[1], this.options.angleConvention);
- var cardinal = this.options.showCardinal ? " (".concat(self.degreesToCardinalDirection(deg), ") ") : '';
- htmlOut = "<strong> ".concat(this.options.velocityType, " ").concat(this.options.directionString, ": </strong> ").concat(deg.toFixed(2), "\xB0").concat(cardinal, ", <strong> ").concat(this.options.velocityType, " ").concat(this.options.speedString, ": </strong> ").concat(self.vectorToSpeed(gridValue[0], gridValue[1], this.options.speedUnit).toFixed(2), " ").concat(this.options.speedUnit);
- map.fire('VelocityLayerClick', {
- direct: "".concat(deg.toFixed(2), "\xB0").concat(cardinal),
- speed: "".concat(self.vectorToSpeed(gridValue[0], gridValue[1], this.options.speedUnit).toFixed(2), " ").concat(this.options.speedUnit),
- context: this,
- Event: e
- });
- } else {
- htmlOut = this.options.emptyString;
- }
- self._container.innerHTML = htmlOut;
- }
- });
- BM.Map.mergeOptions({
- positionControl: false
- });
- BM.Map.addInitHook(function () {
- if (this.options.positionControl) {
- this.positionControl = new BM.Control.MousePosition();
- this.addControl(this.positionControl);
- }
- });
- BM.control.velocity = function (options) {
- return new BM.Control.Velocity(options);
- };
- BM.VelocityLayer = (BM.Layer ? BM.Layer : BM.Class).extend({
- options: {
- displayValues: true,
- displayOptions: {
- velocityType: "",
- position: "topleft",
- emptyString: "No data"
- },
- maxVelocity: 10,
- // used to align color scale
- colorScale: null,
- data: null
- },
- _map: null,
- _canvasLayer: null,
- _windy: null,
- _context: null,
- _timer: 0,
- _mouseControl: null,
- initialize: function initialize(options) {
- BM.setOptions(this, options);
- },
- onAdd: function onAdd(map) {
- // determine where to add the layer
- this._paneName = this.options.paneName || "overlayPane";
- // fall back to overlayPane for bigemap < 1
- var pane = map._panes.overlayPane;
- if (map.getPane) {
- // attempt to get pane first to preserve parent (createPane voids this)
- pane = map.getPane(this._paneName);
- if (!pane) {
- pane = map.createPane(this._paneName);
- }
- }
- // create canvas, add to map pane
- this._canvasLayer = BM.canvasLayer({
- pane: pane
- }).delegate(this);
- this._canvasLayer.addTo(map);
- this._map = map;
- },
- onRemove: function onRemove(map) {
- this._destroyWind();
- },
- setData: function setData(data) {
- this.options.data = data;
- if (this._windy) {
- this._windy.setData(data);
- this._clearAndRestart();
- }
- this.fire("load");
- },
- setOpacity: function setOpacity(opacity) {
- this._canvasLayer.setOpacity(opacity);
- },
- setOptions: function setOptions(options) {
- this.options = Object.assign(this.options, options);
- if (options.hasOwnProperty("displayOptions")) {
- this.options.displayOptions = Object.assign(this.options.displayOptions, options.displayOptions);
- this._initMouseHandler(true);
- }
- if (options.hasOwnProperty("data")) this.options.data = options.data;
- if (this._windy) {
- this._windy.setOptions(options);
- if (options.hasOwnProperty("data")) this._windy.setData(options.data);
- this._clearAndRestart();
- }
- this.fire("load");
- },
- /*------------------------------------ PRIVATE ------------------------------------------*/
- onDrawLayer: function onDrawLayer(overlay, params) {
- var self = this;
- if (!this._windy) {
- this._initWindy(this);
- return;
- }
- if (!this.options.data) {
- return;
- }
- if (this._timer) clearTimeout(self._timer);
- this._timer = setTimeout(function () {
- self._startWindy();
- }, 750); // showing velocity is delayed
- },
- _startWindy: function _startWindy() {
- var bounds = this._map.getBounds();
- var size = this._map.getSize();
- // bounds, width, height, extent
- this._windy.start([[0, 0], [size.x, size.y]], size.x, size.y, [[bounds._southWest.lng, bounds._southWest.lat], [bounds._northEast.lng, bounds._northEast.lat]]);
- },
- _initWindy: function _initWindy(self) {
- // windy object, copy options
- var options = Object.assign({
- canvas: self._canvasLayer._canvas,
- map: this._map
- }, self.options);
- this._windy = new Windy(options);
- // prepare context global var, start drawing
- this._context = this._canvasLayer._canvas.getContext("2d");
- this._canvasLayer._canvas.classList.add("velocity-overlay");
- this.onDrawLayer();
- this._map.on("dragstart", self._windy.stop);
- this._map.on("dragend", self._clearAndRestart);
- this._map.on("zoomstart", self._windy.stop);
- this._map.on("zoomend", self._clearAndRestart);
- this._map.on("resize", self._clearWind);
- this._initMouseHandler(false);
- },
- _initMouseHandler: function _initMouseHandler(voidPrevious) {
- if (voidPrevious) {
- this._map.removeControl(this._mouseControl);
- this._mouseControl = false;
- }
- if (!this._mouseControl && this.options.displayValues) {
- var options = this.options.displayOptions || {};
- options["bigemapVelocity"] = this;
- this._mouseControl = BM.control.velocity(options).addTo(this._map);
- }
- },
- _clearAndRestart: function _clearAndRestart() {
- if (this._context) this._context.clearRect(0, 0, 3000, 3000);
- if (this._windy) this._startWindy();
- },
- _clearWind: function _clearWind() {
- if (this._windy) this._windy.stop();
- if (this._context) this._context.clearRect(0, 0, 3000, 3000);
- },
- _destroyWind: function _destroyWind() {
- if (this._timer) clearTimeout(this._timer);
- if (this._windy) this._windy.stop();
- if (this._context) this._context.clearRect(0, 0, 3000, 3000);
- if (this._mouseControl) this._map.removeControl(this._mouseControl);
- this._mouseControl = null;
- this._windy = null;
- this._map.removeLayer(this._canvasLayer);
- }
- });
- BM.velocityLayer = function (options) {
- return new BM.VelocityLayer(options);
- };
- /* Global class for simulating the movement of particle through a 1km wind grid
- credit: All the credit for this work goes to: https://github.com/cambecc for creating the repo:
- https://github.com/cambecc/earth. The majority of this code is directly take nfrom there, since its awesome.
- This class takes a canvas element and an array of data (1km GFS from http://www.emc.ncep.noaa.gov/index.php?branch=GFS)
- and then uses a mercator (forward/reverse) projection to correctly map wind vectors in "map space".
- The "start" method takes the bounds of the map at its current extent and starts the whole gridding,
- interpolation and animation process.
- */
- 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.containerPointToLatLng(BM.point(x, y));
- return [latlon.lng, latlon.lat];
- };
- var project = function project(lat, lon, windy) {
- var xy = params.map.latLngToContainerPoint(BM.latLng(lat, lon));
- 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,
- colorScale: colorScale
- };
- return windy;
- };
- if (!window.cancelAnimationFrame) {
- window.cancelAnimationFrame = function (id) {
- clearTimeout(id);
- };
- }
|