Source: core/WindChart.js

/**
 * 风场效果,采用canvas绘制,并未采用webgl进行渲染,未作测试,参考使用
 * 
 * 
 */
export class WindChart {
 
  /**
   * 
   * @param {*} isdistory  是否销毁
   * @param {*} json       风场数据
   * @param {Object} params    
   * @param {viewer} params.viewer   Cesium.Viewer
   * @param {canvas} params.canvas   
   * @param {Array} params.extent    [110,120,30,36] 顺序:west/east/south/north
   * @param {Number} params.canvasWidth   300
   * @param {Number} params.canvasHeight    100
   * @param {Number} params.speedRate    100
   * @param {Number} params.particlesNumber    20000  初始粒子总数,根据实际需要进行调节
   * @param {Number} params.maxAge   每个粒子的最大生存周期 128
   * @param {Number} params.frameRate   每个粒子的最大生存周期 128
   * @param {Number} params.lineWidth   1 线宽,像素单位
   * @param {Number} params.color   "#ffffff"
   * @constructor
   */
  constructor(isdistory,json, params) {
    this.isdistory = isdistory; //是否销毁,进行删除操作
    if(isdistory){
      this.removeLines();
    }
    else{
    //风场json数据
    this.windData = json;
    //可配置参数
    this.viewer = params.viewer;
    this.canvas = params.canvas;
    this.extent = params.extent || []; //风场绘制时的地图范围,范围不应该大于风场数据的范围,顺序:west/east/south/north,有正负区分,如:[110,120,30,36]
    this.canvasContext = params.canvas.getContext("2d"); //canvas上下文
    this.canvasWidth = params.canvasWidth || 300; //画板宽度
    this.canvasHeight = params.canvasHeight || 180; //画板高度
    this.speedRate = params.speedRate || 100; //风前进速率,意思是将当前风场横向纵向分成100份,再乘以风速就能得到移动位置,无论地图缩放到哪一级别都是一样的速度,可以用该数值控制线流动的快慢,值越大,越慢,
    this.particlesNumber = params.particlesNumber || 20000; //初始粒子总数,根据实际需要进行调节
    // this.particlesNumber = 51860;
    this.maxAge = params.maxAge || 120; //每个粒子的最大生存周期
    this.frameTime = 1000 / (params.frameRate || 10); //每秒刷新次数,因为requestAnimationFrame固定每秒60次的渲染,所以如果不想这么快,就把该数值调小一些
    this.color = params.color || "#ffffff"; //线颜色,提供几个示例颜色['#14208e','#3ac32b','#e0761a']
    this.lineWidth = params.lineWidth || 1; //线宽度
    //内置参数
    this.initExtent = []; //风场初始范围
    this.calc_speedRate = [0, 0]; //根据speedRate参数计算经纬度步进长度
    this.windField = null;
    this.particles = [];
    this.animateFrame = null; //requestAnimationFrame事件句柄,用来清除操作
    
    this._init();
   }
  }

  _init() {
    var self = this;
    // 创建风场网格
    this.windField = this.createField();
    console.log(this.windField)
    this.initExtent = [
      this.windField.west,
      this.windField.east,
      this.windField.south,
      this.windField.north
    ];
    //如果风场创建时,传入的参数有extent,就根据给定的extent,让随机生成的粒子落在extent范围内
    if (this.extent.length != 0) {
      this.extent = [
        Math.max(this.initExtent[0], this.extent[0]),
        Math.min(this.initExtent[1], this.extent[1]),
        Math.max(this.initExtent[2], this.extent[2]),
        Math.min(this.initExtent[3], this.extent[3]),
      ];

      console.log(this.extent);
    }
    // console.log(this.extent);
    this._calcStep();
    // 创建风场粒子
    for (var i = 0; i < this.particlesNumber; i++) {
      // console.log(this.randomParticle(new CanvasParticle()))
      this.particles.push(this.randomParticle(new CanvasParticle()));
    }
    this.canvasContext.fillStyle = "rgba(0, 0, 0, 0.97)";
    this.canvasContext.globalAlpha = 0.6;
    this.animate();

    var then = Date.now();
    (function frame() {
      if (!self.isdistory) {
        self.animateFrame = requestAnimationFrame(frame);
        var now = Date.now();
        var delta = now - then;
        if (delta > self.frameTime) {
          then = now - (delta % self.frameTime);
          self.animate();
        }
      } else {
        self.removeLines();
      }
    })();
  }

  //计算经纬度步进长度
  _calcStep = function () {
    var isextent = this.extent.length != 0;
    var calcExtent = isextent ? this.extent : this.initExtent;
    var calcSpeed = this.speedRate;
    this.calc_speedRate = [
      (calcExtent[1] - calcExtent[0]) / calcSpeed,
      (calcExtent[3] - calcExtent[2]) / calcSpeed,
    ];
  };

  //根据现有参数重新生成风场
  redraw = function () {
    window.cancelAnimationFrame(this.animateFrame);
    this.particles = [];
    this._init();
  };

  createField = function () {
    var data = this._parseWindJson();
    return new CanvasWindField(data);
  };

  animate = function () {
    var self = this,
      field = self.windField;
    var nextLng = null,
      nextLat = null,
      uv = null;
    self.particles.forEach(function (particle) {
      if (particle.age <= 0) {
        self.randomParticle(particle);
      }
      if (particle.age > 0) {
        var x = particle.x,
          y = particle.y,
          tlng = particle.tlng,
          tlat = particle.tlat;
        var gridpos = self._togrid(tlng, tlat);
        var tx = gridpos[0];
        var ty = gridpos[1];
        if (!self.isInExtent(tlng, tlat)) {
          particle.age = 0;
        } else {
          uv = field.getIn(tx, ty);
          nextLng = tlng + self.calc_speedRate[0] * uv[0];
          nextLat = tlat + self.calc_speedRate[1] * uv[1];
          particle.lng = tlng;
          particle.lat = tlat;
          particle.x = tx;
          particle.y = ty;
          particle.tlng = nextLng;
          particle.tlat = nextLat;
          particle.age--;
        }
      }
    });
    if (self.particles.length <= 0) this.removeLines();
    self._drawLines();
  };

  // 粒子是否在地图范围内
  isInExtent = function (lng,lat) {
    var calcExtent = this.initExtent;
    if (
      lng >= calcExtent[0] &&
      lng <= calcExtent[1] &&
      lat >= calcExtent[2] &&
      lat <= calcExtent[3]
    )
      return true;
    return false;
  };

  _resize = function (width,height) {
    this.canvasWidth = width;
    this.canvasHeight = height;
  };

  _parseWindJson = function () {
    var uComponent = null,
      vComponent = null,
      header = null;
      // console.log(this.windData)
      this.windData.forEach(function (record) {
      var type =
        record.header.parameterCategory + "," + record.header.parameterNumber;
      switch (type) {
        case "2,2":
          uComponent = record["data"];
          header = record["header"];
          break;
        case "2,3":
          vComponent = record["data"];
          break;
        default:
          break;
      }
    });
    return {
      header: header,
      uComponent: uComponent,
      vComponent: vComponent,
    };
  };

  /**
   * 
   * @param {*} pid cesium所在div的上级节点
   */
  removeLines = function (pid) {
      window.cancelAnimationFrame(this.animateFrame);
      this.isdistory = true;
      this.canvas.width = 1;
      document.getElementById("cesiumMap0").removeChild(this.canvas);
    
  };

  //根据粒子当前所处的位置(棋盘网格位置),计算经纬度,在根据经纬度返回屏幕坐标
  _tomap = function (lng, lat, particle) {
    var ct3 = Cesium.Cartesian3.fromDegrees(lng, lat, 0);
    // 判断当前点是否在地球可见端
    var isVisible = new Cesium.EllipsoidalOccluder(
      Cesium.Ellipsoid.WGS84,
      this.viewer.camera.position
    ).isPointVisible(ct3);
    var pos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(
      this.viewer.scene,
      ct3
    );
    if (!isVisible) {
      particle.age = 0;
    }
    // console.log(pos);
    return pos ? [pos.x, pos.y] : null;
  };

  //根据经纬度,算出棋盘格位置
  _togrid = function (lng, lat, particle) {
    var field = this.windField;
    // var x =
    //   ((lng - this.initExtent[0]) / (this.initExtent[1] - this.initExtent[0])) *
    //   (field.cols - 1);
    // var y =
    //   ((this.initExtent[3] - lat) / (this.initExtent[3] - this.initExtent[2])) *
    //   (field.rows - 1);
    // return [x, y];
    return [lng,lat];
  };

  _drawLines = function (lng, lat, particle) {
    var self = this;
    var particles = this.particles;
    this.canvasContext.lineWidth = self.lineWidth;
    //后绘制的图形和前绘制的图形如果发生遮挡的话,只显示后绘制的图形跟前一个绘制的图形重合的前绘制的图形部分,示例:https://www.w3school.com.cn/tiy/t.asp?f=html5_canvas_globalcompop_all
    this.canvasContext.globalCompositeOperation = "destination-in";
    this.canvasContext.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
    this.canvasContext.globalCompositeOperation = "lighter"; //重叠部分的颜色会被重新计算
    this.canvasContext.globalAlpha = 0.9;
    this.canvasContext.beginPath();
    this.canvasContext.strokeStyle = this.color;
    particles.forEach(function (particle) {
      var movetopos = self._tomap(particle.lng, particle.lat, particle);
      var linetopos = self._tomap(particle.tlng, particle.tlat, particle);
      // console.log(movetopos,linetopos);
      if (movetopos != null && linetopos != null) {
        self.canvasContext.moveTo(movetopos[0], movetopos[1]);
        self.canvasContext.lineTo(linetopos[0], linetopos[1]);
      }
    });
    this.canvasContext.stroke();
  };

  //随机数生成器(小数)
  fRandomByfloat = function (under, over) {
    return under + Math.random() * (over - under);
  };

  //随机数生成器(整数)
  fRandomBy = function (under, over) {
    switch (arguments.length) {
      case 1:
        return parseInt(Math.random() * under + 1);
      case 2:
        return parseInt(Math.random() * (over - under + 1) + under);
      default:
        return 0;
    }
  };

  //根据当前风场extent随机生成粒子
  randomParticle = function (particle) {
    var safe = 30,
      x = -1,
      y = -1,
      lng = null,
      lat = null;
    var hasextent = this.extent.length != 0;
    var calc_extent = hasextent ? this.extent : this.initExtent;
    do {
      try {
        if (hasextent) {
          var pos_x = this.fRandomBy(0, this.canvasWidth);
          var pos_y = this.fRandomBy(0, this.canvasHeight);
          var cartesian = this.viewer.camera.pickEllipsoid(
            new Cesium.Cartesian2(pos_x, pos_y),
            this.viewer.scene.globe.ellipsoid
          );
          var cartographic =
            this.viewer.scene.globe.ellipsoid.cartesianToCartographic(
              cartesian
            );
          if (cartographic) {
            //将弧度转为度的十进制度表示
            lng = Cesium.Math.toDegrees(cartographic.longitude);
            lat = Cesium.Math.toDegrees(cartographic.latitude);
          }
        } else {
          lng = this.fRandomByfloat(calc_extent[0], calc_extent[1]);
          lat = this.fRandomByfloat(calc_extent[2], calc_extent[3]);
        }
      } catch (e) {}
      if (lng) {
        var gridpos = this._togrid(lng, lat);
        x = gridpos[0];
        y = gridpos[1];
      }
    } while (this.windField.getIn(x, y)[2] <= 0 && safe++ < 30);
    var field = this.windField;
    var uv = field.getIn(x, y);
    var nextLng = lng + this.calc_speedRate[0] * uv[0];
    var nextLat = lat + this.calc_speedRate[1] * uv[1];
    particle.lng = lng;
    particle.lat = lat;
    particle.x = x;
    particle.y = y;
    particle.tlng = nextLng;
    particle.tlat = nextLat;
    particle.speed = uv[2];
    particle.age = Math.round(Math.random() * this.maxAge); //每一次生成都不一样
    return particle;
  };
}

export class CanvasWindField {
  constructor(obj) {
    this.west = null;
    this.east = null;
    this.south = null;
    this.north = null;
    this.rows = null;
    this.cols = null;
    this.dx = null;
    this.dy = null;
    this.unit = null;
    this.date = null;

    this.grid = null;
    this._init(obj);
  }

  _init(obj) {
    var header = obj.header,
    uComponent = obj["uComponent"],
    vComponent = obj["vComponent"];

    // console.log(uComponent.length+"uComponent");
    // console.log(vComponent.length+"vComponent");
    // console.log("uComponent"+uComponent);
    // console.log("vComponent"+vComponent);


    this.west = +header["lo1"];
    this.east = +header["lo2"];
    this.south = +header["la2"];
    this.north = +header["la1"];
    this.rows = +header["ny"];
    this.cols = +header["nx"];
    this.dx = +header["dx"];
    this.dy = +header["dy"];
    this.unit = header["parameterUnit"];
    this.date = header["refTime"];

    this.grid = [];
    var k = 0,
    rows = null,
    uv = null;
    for (var j = 0; j < this.rows; j++) {
      rows = [];
      for (var i = 0; i < this.cols; i++, k++) {
        uv = this._calcUV(uComponent[j], vComponent[i]);
        rows.push(uv);
      }
      this.grid.push(rows);
    }
  }

  _calcUV(u, v) {
    // console.log(u+"-"+v);
    return [+u, +v, Math.sqrt(u * u + v * v)];
  }

  // 二分差值算法计算给定节点的速度
  _bilinearInterpolation(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 this._calcUV(u, v);
  }

  getIn(x, y) {
    if (x < 0 || x >= 359 || y >= 180) {
      return [0, 0, 0];
    }
    var x0 = Math.floor(x),
      y0 = Math.floor(y),
      x1,
      y1;
    if (x0 === x && y0 === y) return this.grid[y][x];

    x1 = x0 + 1;
    y1 = y0 + 1;

    var g00 = this.getIn(x0, y0),
      g10 = this.getIn(x1, y0),
      g01 = this.getIn(x0, y1),
      g11 = this.getIn(x1, y1);
    var result = null;
    try {
      result = this._bilinearInterpolation(x - x0, y - y0, g00, g10, g01, g11);
    } catch (e) {
      console.log(x, y);
    }
    return result;
  }

  isInBound(x, y) {
    if (x >= 0 && x < this.cols - 1 && y >= 0 && y < this.rows - 1) return true;
    return false;
  }
}

export class CanvasParticle {
  constructor() {
    this.lng = null; //粒子初始经度
    this.lat = null; //粒子初始纬度
    this.x = null; //粒子初始x位置(相对于棋盘网格,比如x方向有360个格,x取值就是0-360,这个是初始化时随机生成的)
    this.y = null; //粒子初始y位置(同上)
    this.tlng = null; //粒子下一步将要移动的经度,这个需要计算得来
    this.tlat = null; //粒子下一步将要移动的y纬度,这个需要计算得来
    this.age = null; //粒子生命周期计时器,每次-1
    this.speed = null; //粒子移动速度,可以根据速度渲染不同颜色
  }
}