mapboxGL之风流图

概述

前面的文章说到了Openlayers4中风场的实现,本文将讲述如何在mapbox GL实现类似的效果。

效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZkUGd75p-1585748579661)(https://upload-images.jianshu.io/upload_images/6826673-a72b98f01e6917fa.gif?imageMogr2/auto-orient/strip)]

实现

实现是在windy-esri的基础上做了和mapboxGL的结合,结合代码如下:

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';
    }
  }
};

说明:这里面核心用到了mapboxGL的接口有:map.getCanvasContainer()map.getBounds()map.getCanvas()

windy.js的代码如下:

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= λ < 0 ? H : -H;
     var= φ < 0 ? H : -H;
     var= project(φ, λ +);
     var= project(φ +, λ); // 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 [([0] - x) // k, ([1] - y) // k, ([0] - x) /, ([1] - y) /];
   };

   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);
   };
 }

参考数据格式可从网盘下载:

链接:https://pan.baidu.com/s/1EiP90AH4YUR4nN79BT6xfw 
提取码:mes9 

调用时,可如下:

map.on('load', function() {
  $.get("../data/windy.json", function(res) {
    windyMap.initWindy(res, map);
  })
});
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

牛老师讲GIS

感谢老板支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值