leaflet实现风场动态粒子效果

本文介绍了如何使用开源插件leaflet-velocity在web前端实现风场动态粒子效果。详细讲述了风向粒子参数设置、插件的代码示例,以及数据源的json格式。文章还提供了参考实现链接和相关资源。


前言

效果实现需要用到一款开源插件:leaflet-velocity,
来自Github:https://github.com/onaci/leaflet-velocity
演示地址:https://onaci.github.io/leaflet-velocity/
参考实现:https://www.windy.com/

本文只是针对个人实际项目做出的开发记录,无其他参考意义。


一、部分代码摘录

(一)velocity插件代码.js

风向粒子参数设置

// 粒子颜色数组为设置或只设置单值时,以下两个设置无效
// 粒子最小速度时颜色设置 
var MIN_VELOCITY_INTENSITY = params.minVelocity || 0; 
// 粒子最大速度时颜色设置 
var MAX_VELOCITY_INTENSITY = params.maxVelocity || 20;
 
// 风速比例,实际效果为粒子的拖尾长度 
var VELOCITY_SCALE = (params.velocityScale || 0.0015) * (Math.pow(window.devicePixelRatio, 1 / 3) || 1); 

// 粒子生命周期 
var MAX_PARTICLE_AGE = params.particleAge || 90; 

// 粒子的宽度
var PARTICLE_LINE_WIDTH = params.lineWidth || 1.5; 

// 粒子的密度 
var PARTICLE_MULTIPLIER = params.particleMultiplier || 0.9 / 300;
 
// 用于移动端减少粒子 
ar PARTICLE_REDUCTION = Math.pow(window.devicePixelRatio, 1 / 3) || 1.6; 

// 刷新帧率以及渲染时间,实际影响粒子速度 
// 用于控制帧的频率,越大,频率越快,例子速度越快 
var FRAME_RATE = params.frameRate || 60; 
// 渲染时间 
var FRAME_TIME = 1000 / FRAME_RATE; 

//不透明度 
var OPACITY = 1; 

//粒子颜色(单值) 
var colorScale = ["rgb(255,255,255)"]; 
//粒子颜色(数组) 
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];

鼠标事件代码L.Control.Velocity.js

L.Control.Velocity = L.Control.extend({
  options: {
    position: "bottomleft",
    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: "Direction",
    speedString: "Speed",
    onAdd: null,
    onRemove: null
  },
  onAdd: function onAdd(map) {
    this._container = L.DomUtil.create("div", "leaflet-control-velocity");
    L.DomEvent.disableClickPropagation(this._container);
    map.on("mousemove", this._onMouseMove, this);
    this._container.innerHTML = this.options.emptyString;
    if (this.options.leafletVelocity.options.onAdd) this.options.leafletVelocity.options.onAdd();
    return this._container;
  },
  onRemove: function onRemove(map) {
    map.off("mousemove", this._onMouseMove, this);
    if (this.options.leafletVelocity.options.onRemove) this.options.leafletVelocity.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.leafletVelocity._map.containerPointToLatLng(L.point(e.containerPoint.x, e.containerPoint.y));

    var gridValue = this.options.leafletVelocity._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);
    } else {
      htmlOut = this.options.emptyString;
    }

    self._container.innerHTML = htmlOut;
  }
});
L.Map.mergeOptions({
  positionControl: false
});
L.Map.addInitHook(function () {
  if (this.options.positionControl) {
    this.positionControl = new L.Control.MousePosition();
    this.addControl(this.positionControl);
  }
});

L.CanvasLayer.js

{...}

L.VelocityLayer.js

{...}

wind.js

wind.js代码注解

(二)插件使用示例代码

velocity.js

import '../leaflet-velocity'
import '../leaflet-velocity.css'
import '../windGlobal.json'

export function createVelocityWindLayer(map) {
    var globalWindData = windGlobal
    var velocityLayer = L.velocityLayer({
        displayValues: true, // 数据显示事件
        data: globalWindData,
        maxVelocity: 15
    })
    map.addLayer(velocityLayer)
}

wind.vue

import {createVelocityWindLayer} from '../velocity.js'

export default {
  components: {
  },
  mounted() {
    this.getMap()
  },
  methods: {
  // 生成地图
    getMap() {
    createVelocityWindLayer(this.map)
  }
}

二、leaflet-velocity数据源格式

leaflet-velocity插件数据源为json格式数据,以下为部分必要数据结构

附:参考文档地址
网格定义原理
气象数据解析

[
  {
    "header": {
      // 时间
      "refTime": "2022-05-20T12:00:00",
      // 配置了数据记录内容,风力数据默认为2
      "parameterCategory": 2, 
      // 风向数据默认:Momentum
      "parameterCategoryName": "Momentum",
      // 记录了数据方向:U向为2,V向为3
      "parameterNumber": 2,
      // 风力方向,U方向为经度方向的风力值,以正东为正方向
      "parameterNumberName": "2-component_of_wind",
      // 单位
      "parameterUnit": "m.s-1",
      // 数据点数量,data中风力数值的个数,与dx和dy有关
      "numberPoints": 65160,
      //栅格数量和步长以经纬度为标准划分,当步长为1时,直接划分经纬度,即划分为360*181=65160个栅格
      //横向划分栅格数量
      "nx": 360,
      //纵向划分栅格数量
      "ny": 181,
      //横向步长
      "dx": 1,
      // 纵向步长
      "dy": 1,
      // 横向起点
      "lo1": 0.0,
      // 纵向起点
      "la1": 90.0,
      // 横向终点,根据步长有所不同
      "lo2": 359.0,
      // 纵向终点
      "la2": -90
      //velocity插件经度方向计算时按由西向东计算,纬度方向按由北向南计算
    },
    // data数量与numberPoints一致
    "data": [...]
  },
  {
    "header": {
      "refTime": "2022-05-20T12:00:00",
      "parameterCategory": 2,
      "parameterCategoryName": "Momentum",
      "parameterNumber": 3,
      "parameterNumberName": "3-component_of_wind",
      "parameterUnit": "m.s-1",
      "numberPoints": 65160,
      "nx": 360,
      "ny": 181,
      "dx": 1,
      "dy": 1,
      "lo1": 0.0,
      "la1": 90.0,
      "lo2": 359.0,
      "la2": -90.0
    },
    "data": [...]
  }
]

三、总结

  1. 参考文档
    leaflet实现动态地图风场效果
    web前端利用leaflet生成粒子风场,类似windy
    leaflet实现动态地图风场效果

  2. 相关资源:leaflet插件实现风场示例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

XxHoo_

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值