OpenLayers:ol-wind之渲染风场图全解析

1.数据介绍

想要使用openlayers的插件库ol-wind,需要一种特殊的数据,我将这种数据叫做 ol wind data 。ol wind data是json格式,它来源于GFS (全球预报系统),由GFS中的 UGRD(风的U分量)和 VGRD(风的V分量)两种数据组成。

1.1 GFS介绍

全球预报系统(Global Forecast System,GFS)是由美国国家气象局(NWS)运行的全球数值天气预报系统。以下是具体介绍:

  • 模型架构:GFS 模型分为 127 个垂直层,从地表延伸到中间层顶(约 80 公里)。全球网格点之间的基本水平分辨率为 13 公里,能覆盖整个地球。
  • 运行机制:每天运行 4 次,可生成最长 16 天的天气预报。前 120 小时每小时产生一次预报输出,第 5 天到第 16 天则每 3 小时输出一次。
  • 预报原理:依赖卫星、雷达、探空气球和地面站等全球气象观测数据,将其作为模型初始条件。基于流体力学的纳维 - 斯托克斯方程,计算空气运动状态,同时考虑辐射传输、地表与大气相互作用以及湿度和降水变化等因素。通过将地球表面划分为多个网格单元,对初始状态进行数值积分,逐步计算未来天气状态。
  • 数据特点:数据主要以 GRIB 格式存储,保留 30 天。数据集包含数百个大气和陆地土壤变量,如温度、风速、降水、土壤湿度和大气臭氧浓度等。

1.2 GFS数据构建原理

GFS数据是以经纬网格为单位进行组织的,GFS的相关模型会按照特定的步长构建一套经纬网格(例如 GFS 1.00 Degree 模型的步长就是 1°),并计算出每个单元格中的气象数据。

1.3 ol wind data 格式介绍

ol wind data 数据是一个包含两个对象元素的数组,这两个对象分别代表风的U分量和风的V分量。

每一个分量对象中,又包含headerdata两个属性。

  • header是头文件,里面记录了GFS数据的许多基本参数
  • data是数据文件,里面保存了该分量的数据。它是一个数组,里面存储了每个网格的数据,是按照从上到下,从左到右的原则将经纬网中每个网格的数据都汇集到了这个数组中。也就是说data中的第一个数据代表 1行1列 的数据,第二个数据代表 1行2列 的数据,然后以此类推。

1.3.1 头文件

分量对象的header属性中记录这组GFS数据的元数据。

其中主要的属性如下:

参数 描述
parameterCategory 它配置了数据记录内容,不同的数值对应不同的气象参数类别。例如,当parameterCategory为 2 时,其类别名称为 "Momentum"(动量),表示相关数据与动量有关。(风的UV分量的parameterCategory属性便为2)
parameterNumber 在特定的参数类别下,进一步明确具体的参数。例如,在parameterCategory为 2(动量)的情况下,parameterNumber为 2 时,对应的参数名称是 "U - component_of_wind"(风的 U 分量),parameterNumber为 3 时,对应的是 "V - component_of_wind"(风的 V 分量),用于区分同一类别下的不同具体参数。
la1 GFS网格的最小纬度
la2 GFS网格的最大纬度
lo1 GFS网格的最小经度
lo2 GFS网格的最大经度
dx 经度的网格步长
dy 纬度的网格步长
nx 数据网格的列数,可以通过(lo2 - lo1) / dx + 1 计算得到
ny 数据网格的行数,可以通过(la2 - la1) / dy + 1 计算得到
numberPoints 总的网格数量,可以通过nx * ny 计算得到

1.3.2 数据

分量对象的data属性存储了按照 header 定义的格式和规则组织的实际气象数据值。

数据按经纬度网格点排列,每个数据对应了经纬网中的一个单元格,数据的排列顺序遵循"从上到下,从左到右,先行后列"。

例如,假设经纬网是10 x 10,则data数组中的第一个数据对应了经纬网中的第一行第一列的单元格,第二个数据对应了第一行第二列的单元格,第三个数据对应了第一行第三列的单元格,第11个数据对应了第二行第一列的单元格,第21个数据对应了第三行第一列的单元格,以此类推。

2.数据下载

2.1GFS官网手动下载

进入网站

可以从美国国家环境预报中心(NCEP)旗下的数据网站中获取GFS数据:

NOMADS at ncep.noaa.gov

网站首页是这样:

选择模型

首先要选择数据模型,要在Global Models下面的模型。

我所参考的一些资料中推荐从下图中的三个模型中选择(但是我个人认为只要是GFS开头的模型估计都没有问题)。

无论你选择的是哪种GFS模型都要注意它有一个度数,例如 GFS 0.25 Degree 是 0.25度, GFS 0.50 Degree 是 0.5度, GFS 1.00 Degree 是 1度。这个参数是网格步长 , 0.25 就代表网格的大小为 0.25° * 0.25° (网格的具体含义我会在后面介绍)

选择数据访问方式

网站中提供多种数据获取途径(表格中 "https""gds" 列标注):

  • HTTPS 下载:直接通过网页链接下载 GRIB 格式数据文件。
  • OpenDAP:通过开放数据访问协议(OpenDAP)在线访问和提取数据,支持按需筛选变量和区域。
  • GRIB 过滤器(grib filter) :可通过过滤器筛选特定气象要素(如温度、降水、风速等)

我只尝试了第一种 grib filter 的数据访问方式,另外两种方式感兴趣的可以自行尝试。

我最终选择采用了grib filter 方式去访问 GFS 0.50 Degree 模型的数据,于是我就要点击如下的位置:

选择数据的日期与时间

接下来就进行了详细的数据筛选页面,首先要选择数据的日期和时间。

选择需要下载的数据条目

可以看到GFS中支持很多种数据,我们只需要下载其中的与风速风向相关的数据。

那什么数据是跟风速风向相关的呢?这里就得先介绍一些气象学的知识了:

在气象学中,风通常被分解为两个水平分量(U 和 V)和一个垂直分量(W),用于更精确地描述风的方向和速度。其中:

  • U 分量:代表风在东西方向(E-W) 上的分量。
    • 当 U 为正值时,表示风从西向东吹(西风);
    • 当 U 为负值时,表示风从东向西吹(东风)。
  • V 分量:代表风在南北方向(N-S) 上的分量。
    • 当 V 为正值时,表示风从南向北吹(南风);
    • 当 V 为负值时,表示风从北向南吹(北风)。

风矢量(风速和风向)可通过 U 和 V 分量计算得出:

  1. 合成风速:
  1. 合成风向:

因此我们要下载的就是风的两个分量数据。它们分别是 UGRD (U-Component of Wind) 和 VGRD (V-Component of Wind)

选择数据级别

这一部分的这些选项我也没搞清楚都是什么意思,我只能模仿网上的选择了 "10 m above ground" (离地10m)

选择数据的范围

如果不设置默认下载全球的数据

也可以手动去设置一个数据的范围

下载

最后点击下载按钮下载数据

下载的数据文件有的后缀有可能是 .f000 或 .anl 。尚不清楚为什么会有这种现象,但是据我测试这两种后缀的数据文件都是可以正常使用的。

2.2 请求数据URL下载

也可以直接通过请求GFS官方的URL来获取数据:

Python 复制代码
import requests
from datetime import datetime, timedelta, timezone
import json
import numpy as np
import xarray as xr

# ------------------------- 1. 下载 GFS 数据 -------------------------
def download_gfs_data(filename="global.gfs.grib2"):
    print("🚀 启动 GFS 风场数据下载任务...")
    now = datetime.now(timezone.utc)
    for attempt in range(6):
        hour = (now.hour // 6) * 6
        date_str = now.strftime("%Y%m%d")
        hour_str = f"{hour:02d}"
        print(f"📡 第 {attempt+1} 次尝试:准备下载 GFS {date_str} {hour_str}z 全球风场数据...")
        url = "https://nomads.ncep.noaa.gov/cgi-bin/filter_gfs_0p25.pl"
        params = {
            "file": f"gfs.t{hour_str}z.pgrb2.0p25.f000",
            "lev_10_m_above_ground": "on",
            "var_UGRD": "on",
            "var_VGRD": "on",
            "leftlon": 0, "rightlon": 360,  # 全球范围
            "toplat": 90, "bottomlat": -90, # 全球范围
            "dir": f"/gfs.{date_str}/{hour_str}/atmos/",
        }
        try:
            r = requests.get(url, params=params, stream=True, timeout=60)
        except Exception as e:
            print(f"⚠️ 网络请求异常:{e},尝试回退 6 小时...")
            now -= timedelta(hours=6)
            continue
        if r.headers.get("Content-Type", "").startswith("text/html"):
            print(f"❌ {date_str} {hour_str}z 数据暂不可用,自动回退 6 小时继续尝试...")
            now -= timedelta(hours=6)
            continue
        with open(filename, "wb") as f:
            for chunk in r.iter_content(chunk_size=1024):
                if chunk:
                    f.write(chunk)
        print(f"✅ 成功下载文件:{filename}")
        print(f"📅 下载数据时次为:{date_str} {hour_str}z")
        with open("gfs_latest_time.txt", "w", encoding="utf-8") as f_txt:
            f_txt.write(f"{date_str} {hour_str}z\n")
        print("📝 已将下载时次写入 gfs_latest_time.txt 文件")
        return filename
    raise Exception("❗未能找到可用的 GFS 数据(最近36小时内)")

3.数据转换

我们下载下来的GFS数据都是 grib 格式的,必须要将数据转换成json格式才可以在前端项目中使用。因此必需想办法读取grib数据,然后将其重新组织成 ol wind data 。

3.1 通过grib2json进行数据转换

网上基本都是推荐使用一个叫做 grib2json的工具来进行转换,可以在github上去下载:

github.com/cambecc/gri...

我把这个项目下载到本地之后,发现它是个Java项目,我不懂Java,使用的时候卡在了安装依赖这一步上,我失败了o(╥﹏╥)o。

如果你想尝试使用grib2json,以下的资料或许对你有帮助:

JAVA在线调用Grib2Json-CSDN博客

第39节:cesium 获取并转换风场数据(含源码+视频)_风场数据下载-CSDN博客

Maven安装与配置指南-CSDN博客


不过我在网上找到了一个别人配置好的grib2json,点击下面的链接 👇下载资源:

【免费】用于将grib格式转换为json格式的工具(基于grib2json)资源-CSDN下载

使用的方法也很简单,打开项目后只需要在命令行中输入如下的命令:

css 复制代码
./bin/grib2json --data --output 【json文件输出到的地址】 【需要转换的grib文件的地址】

举个栗子,我下载了一个grib文件 gfs.t00z.pgrb2.0p25.anl 放在了 demo文件夹下。

我希望可以将gfs.t00z.pgrb2.0p25.anl转换为一个json文件,并将其命名为test1.json,它也应该被放置到demo文件夹下。

只需要在命令行中执行如下的命令即可:

3.2 通过python代码进行数据转换

python中通过xarray库可以实现grib数据的读取,因此就可以实现对grib数据的转换。

python脚本代码如下:

Python 复制代码
import requests
from datetime import datetime, timedelta, timezone
import json
import numpy as np
import xarray as xr


# ------------------------- 2. 加载 GFS 数据 -------------------------
def load_gfs_data(filename="global.gfs.grib2"):
    print(f"📥 加载 GFS 数据文件:{filename}")
    ds = xr.open_dataset(filename, engine="cfgrib")
    print(f"✅ 加载成功,包含变量:{list(ds.variables)}")
    return ds

# ------------------------- 3. 提取风速分量 -------------------------
def extract_uv(ds):
    print("📊 正在读取经纬度和风速分量...")
    lats = ds.latitude.values
    lons = ds.longitude.values
    u10 = ds.u10.values
    v10 = ds.v10.values
    print(f"✅ 数据维度:u10={u10.shape}, v10={v10.shape}, lats={len(lats)}, lons={len(lons)}")
    return lats, lons, u10, v10

# ------------------------- 4. 构建 ol-wind header -------------------------
def build_header(lons, lats, ds, parameter_number):
    return {
        "nx": len(lons),
        "ny": len(lats),
        "lo1": float(lons[0]),
        "la1": float(lats[-1]),
        "lo2": float(lons[-1]),
        "la2": float(lats[0]),
        "dx": float(lons[1] - lons[0]),
        "dy": float(lats[0] - lats[1]),
        "refTime": str(ds.time.values),
        "parameterCategory": 2,
        "parameterNumber": parameter_number,
    }

# ------------------------- 5. 保存为 ol-wind JSON -------------------------
def save_wind_json(u, v, lons, lats, ds, out_file="global_wind_olwind.json"):
    print("💾 保存为 ol-wind JSON 格式(全球数据)")
    wind_data = [
        {
            "header": build_header(lons, lats, ds, 2),
            "data": u.flatten(order="C").tolist(),
        },
        {
            "header": build_header(lons, lats, ds, 3),
            "data": v.flatten(order="C").tolist(),
        }
    ]
    with open(out_file, "w") as f:
        json.dump(wind_data, f)
    print(f"✅ 保存完成!文件名:{out_file}")

4.ol-wind介绍

4.1简介

ol-wind 是专为 OpenLayers 地图库设计的风场图层扩展库 ,用于在地图上实现风场效果(如气象可视化.。ol-windwind-layer 生态中专门为 OpenLayers 地图库 设计的风场可视化适配器 ,而 wind-layer 是一个跨地图引擎的气象数据可视化核心框架

4.1.1 官方文档

ol-windwind-layer)官方文档 blog.sakitam.com/wind-layer/

4.1.2 安装方式

npm安装:

css 复制代码
npm install ol-wind

script引入:

HTML 复制代码
<!-- 
ol 类库依赖
-->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/ol/ol.css">
<script src="//cdn.jsdelivr.net/npm/ol@6.15.1/dist/ol-debug.js"></script>

<!-- 
ol-wind 风场依赖
-->
<script src="//cdn.jsdelivr.net/npm/ol-wind/dist/ol-wind.js"></script>

4.1.3 使用示例

可以参考官网上的使用示例:

blog.sakitam.com/wind-layer/...

4.2 WindLayer介绍

ol-wind库最主要的功能就是提供了一个图层对象WindLayer,通过使用WindLayer图层可以向地图中添加一个风场图层。

4.2.1 使用方式

先引入WindLayer图层:

JavaScript 复制代码
import { WindLayer } from "ol-wind"

然后使用WindLayer并添加到地图中:

JavaScript 复制代码
const windLayer = new WindLayer({
  ....
})

map.addlayer(windLayer)

4.2.2 参数说明

图层参数:

参数 说明 类型 默认值
windOptions 风场参数,具体配置如下 object ---
map 地图对象,必须配置,不需要调用 addLayer,具体可以参考 openlayer 官方文档 ol.Map ---
zIndex 图层层级 number ---

其他参数遵循 ol 基础图层参数。

windOptions:以下是提取后的 Markdown 格式表格:

参数 说明 类型 默认值
globalAlpha 全局透明度,主要影响粒子路径拖尾效果 number 0.9
lineWidth 粒子路径宽度 number function 1, 当为回调函数时,参数 function(m:对应点风速值) => number
colorScale 粒子颜色配置 string function string[] #fff, 当为回调函数时,参数 function(m:对应点风速值) => string
velocityScale 速度级别 number 1 / 25
maxAge particleAge(不推荐使用) 粒子路径能够生成的最大帧数 number 90
paths 生成的粒子路径数量 number function 800, 当为回调函数时,参数 function(m:对应点风速值) => number
particleMultiplier 粒子路径数量的系数,不推荐使用(视野宽度 * 高度 * 系数) number 1 / 300
frameRate 帧率(ms) number 20

4.3 Field 介绍

Field 类是 ol-wind 库中用于管理风场数据的核心类,负责处理风场数据的网格化、坐标转换、插值计算等功能。它提供了完整的风场数据处理能力,支持各种坐标系统和边界条件。

4.3.1 使用方式

我们也可以在项目中引入Field类来帮助我们处理风场数据。

JavaScript 复制代码
import { Field } from "ol-wind"

通过下面的方法就可以基于一个 ol wind data 创建一个 Field实例:

JavaScript 复制代码
  /**
   * 从GFS数据创建流场数据 (工厂方法)
   * @param {Array} gfs
   * @returns {FlowField}
   */
  function createFromGFS(gfs) {
    let uComp = void 0;
    let vComp = void 0;

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

    if (!vComp || !uComp) {
      throw new Error("无法找到U分量或V分量数据");
    }

    const header = uComp.header;
    const vectorField = new FlowField({
      xmin: header.lo1,
      ymin: header.la1,
      xmax: header.lo2,
      ymax: header.la2,
      deltaX: header.dx,
      deltaY: header.dy,
      cols: header.nx,
      rows: header.ny,
      us: uComp.data,
      vs: vComp.data,
    });

    return vectorField;
  }

4.3.2 实例属性和方法介绍

想要让 Field为我们所用就要了解它的各个实例属性与方法的作用。具体的属性方法介绍请参考我的这篇文章:

Opnelayers:ol-wind之Field 类属性和方法详解-CSDN博客

5.ol-wind使用实例

5.1 加载全球风场

只要准备好一份 ol wind data 形式的全球GFS数据,然后像下面这样使用,就可以实现对全球风场的渲染。

JavaScript 复制代码
// 引入WindLayer
import { WindLayer } from "ol-wind";

// 引入全球风场数据
import gfs_world_2025063000 from "./data/2025_06_30_00_00_00.json";

/**
 *  @abstract 添加风场图层
 *  @param gfs 风场数据  ol wind data 格式
 */
function addWindLayer(gfs) {
  // 创建风场图层
  const windLayer = new WindLayer(gfs, {
    windOptions: {
      globalAlpha: 0.95,
      velocityScale: 0.01,
      paths: 5000,
      colorScale: [
        "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)",
      ],
      lineWidth: 2,
      width: 3,
      generateParticleOption: false,
    },
    wrapX: false,
    projection: "EPSG:4326",
    zIndex: 1000,
  });

  windLayer.id = "wordWindLayer";
  windLayer.name = "全球风场图";

  map.addLayer(windLayer);

}


function main(){
  // 1.添加全球风场
  addWindLayer(gfs_world_2025063000)
}

main()

效果演示: 【OpenLayers:全球风场效果】www.bilibili.com/video/BV1Ln...

5.2 加载区域风场

如果是想渲染某个局部区域的风场那就比较复杂了,这里我以渲染河南省边界范围内的风场为例。想要只保留河南省内的风场,就要进行数据裁剪,将全球风场数据中位于河南省内的数据保留,其它的数据置为0。

为了方便对Field类的使用,我创建了一个继承Field的新类FlowField

FlowFieldField的基础上新增了三个方法:

  1. 增加了一个工厂方法 createFromGFS,用于根据 ol wind data 数据创建 Field实例;
  2. 增加了forEach方法,可以遍历Field实例中的每一个单元格,获取单元格中的 x风速y风速
  3. 增加了getSubGrid方法,可以从Field实例中提取一个子范围的数据。
JavaScript 复制代码
import { Field } from "ol-wind";

/**
 * @typedef {Object} FlowFieldOption
 * @property {number} xmin 最小经度
 * @property {number} ymin 最小纬度
 * @property {number} xmax 最大经度
 * @property {number} ymax 最大纬度
 * @property {number} deltaX 经度间隔
 * @property {number} deltaY 纬度间隔
 * @property {number} cols 列数
 * @property {number} rows 行数
 * @property {number[]} us 流体速度u分量
 * @property {number[]} vs 流体速度v分量
 * @property {boolean} flipY 是否翻转Y轴
 * @property {boolean} translateX 是否翻转X轴
 * @property {boolean} wrapX 是否环绕X轴
 */

class FlowField extends Field {
  /**
   * 创建流场数据
   * @param {FlowFieldOption} option
   */
  constructor(option) {
    super(option);
  }

  /**
   * 遍历流场数据
   * @param {Function} callback 回调函数
   * @param {number[]} callback.coord 网格中心点坐标 [lon, lat]
   * @param {Vector} callback.value 网格值 Vector对象
   * @param {number[]} callback.gridIndex 网格索引 [i, j]
   * @param {number} callback.globalIndex 全局索引
   */
  forEach(callback) {
    let p = 0;
    for (let j = 0; j < this.rows; j++) {
      for (let i = 0; i < this.cols; i++, p++) {
        const coord = this.lonLatAtIndexes(i, j);
        const value = this.valueAtIndexes(i, j);
        callback(coord, value, [i, j], p);
      }
    }
  }

  /**
   * 获取子网格
   * @param {number[]} extent 子网格范围 [xmin, ymin, xmax, ymax]
   * @returns {Vector[][]} 子网格数据
   */
  getSubGrid(extent) {
    const [ xmin, ymin, xmax, ymax ] = extent;

    const nx = Math.ceil((xmax - xmin) / this.deltaX);
    const ny = Math.ceil((ymax - ymin) / this.deltaY);


    const subGrid = [];

    for (let j = 0; j < ny; j++) {
      const row = [];
      for (let i = 0; i < nx; i++) {
        const lon = xmin + i * this.deltaX;
        const lat = ymax - j * this.deltaY;

        const [ii, jj] = this.getDecimalIndexes(lon, lat);

        const value = this.valueAtIndexes(ii, jj);
        row.push(value);
      }
      subGrid.push(row);
    }

    return subGrid;
  }

  /**
   * 从GFS数据创建流场数据 (工厂方法)
   * @param {Array} gfs
   * @returns {FlowField}
   */
  static createFromGFS(gfs) {
    let uComp = void 0;
    let vComp = void 0;

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

    if (!vComp || !uComp) {
      throw new Error("无法找到U分量或V分量数据");
    }

    const header = uComp.header;
    const vectorField = new FlowField({
      xmin: header.lo1,
      ymin: header.la1,
      xmax: header.lo2,
      ymax: header.la2,
      deltaX: header.dx,
      deltaY: header.dy,
      cols: header.nx,
      rows: header.ny,
      us: uComp.data,
      vs: vComp.data,
    });

    return vectorField;
  }
}

export default FlowField;

接着就可以进行数据裁剪,并将裁剪后的数据使用WindLayer进行渲染。

JavaScript 复制代码
// 引入WindLayer
import { WindLayer } from "ol-wind";

import FlowField from "./FlowField";

// 引入全球风场数据
import gfs_world_2025063000 from "./data/2025_06_30_00_00_00.json";

// 引入河南省边界geojson
import henan_boundary from "./data/henan_boundary.geojson";


/**
 * 提取子区域风场数据
 * @param gfs 全球风场数据
 * @param extent 子区域范围 [lo1, la1, lo2, la2]
 * @param options 选项
 * @returns 子区域风场数据
 */
function getSubAreaGFS(gfs, extent) {
  const flowField_world = FlowField.createFromGFS(gfs);

  const flowField_area = flowField_world.getSubGrid(extent);

  const { dx, dy, refTime } = gfs[0].header;

  const header = {
    lo1: extent[0],
    la1: extent[1],
    lo2: extent[2],
    la2: extent[3],
    dx: dx,
    dy: dy,
    nx: flowField_area.length,
    ny: flowField_area[0].length,
    refTime: refTime,
  };

  const us = [],
    vs = [],
    ms = [];

  for (let i = 0; i < flowField_area.length; i++) {
    for (let j = 0; j < flowField_area[i].length; j++) {
      const value = flowField_area[i][j];
      us.push(value.u);
      vs.push(value.v);
      ms.push(value.m);
    }
  }

  return {
    header,
    us,
    vs,
    ms,
  };
}

/**
 * 创建GFS数据
 * @param extent 子区域范围 [lo1, la1, lo2, la2]
 * @param dx x方向网格间距
 * @param dy y方向网格间距
 * @param refTime 参考时间
 * @param us u分量数据
 * @param vs v分量数据
 * @returns GFS数据
 */
function createGFS(extent, dx, dy, refTime, us, vs) {
  const header_u = buildHeader(extent, dx, dy, refTime, 2);
  const header_v = buildHeader(extent, dx, dy, refTime, 3);

  return [
    {
      header: header_u,
      data: us,
    },
    {
      header: header_v,
      data: vs,
    },
  ];
}

/**
 * 构建子区域头信息
 * @param extent 子区域范围 [lo1, la1, lo2, la2]
 * @param dx x方向网格间距
 * @param dy y方向网格间距
 * @param refTime 参考时间
 * @param parameterNumber  气象参数编号 2:u分量 3:v分量
 * @returns 子区域头信息
 */
function buildHeader(extent, dx, dy, refTime, parameterNumber) {
  return {
    lo1: extent[0],
    la1: extent[1],
    lo2: extent[2],
    la2: extent[3],
    dx: dx,
    dy: dy,
    nx: Math.ceil((extent[2] - extent[0]) / dx),
    ny: Math.ceil((extent[3] - extent[1]) / dy),
    refTime: refTime,
    parameterCategory: 2,
    parameterNumber: parameterNumber,
  };
}

/**
 * 按照边界裁剪GFS数据
 * @param gfs 原始GFS数据
 * @param boundary  边界 geojson
 * @param grid 网格
 */
function clipGFS(gfs, boundary) {
  const flowField = FlowField.createFromGFS(gfs);

  const us = [],
    vs = [];

  // 遍历流场数据,如果点在边界内部,则保留,否则设置为0
  flowField.forEach(function (coord, value, indexes, p) {
    let u = 0,
      v = 0;
    if (pointInPolygon(coord, boundary)) {
      u = value.u;
      v = value.v;
    }

    us.push(u);
    vs.push(v);
  });

  // 创建新的GFS数据
  const newGFS = gfs.map(function (item, index) {
    if (item.header.parameterNumber === 2) {
      item.data = us;
    } else if (item.header.parameterNumber === 3) {
      item.data = vs;
    }

    return item;
  });

  return newGFS;
}

/**
 * 判断点是否在多边形内部(射线法)
 * @param {Array} point - 点坐标 [x, y]
 * @param {Object} boundary - 边界 geojson对象
 * @returns {boolean} 点是否在多边形内部
 */
function pointInPolygon(point, boundary) {
  // 如果boundary是字符串,需要先解析
  let boundaryObj = boundary;
  if (typeof boundary === "string") {
    boundaryObj = JSON.parse(boundary);
  }

  // 获取第一个feature的geometry
  const geometry = boundaryObj.features[0].geometry;

  return turf.booleanPointInPolygon(turf.point(point), geometry);
}


/**
 *  @abstract 添加风场图层
 *  @param gfs 风场数据  ol wind data 格式
 */
function addWindLayer(gfs) {
  // 创建风场图层
  const windLayer = new WindLayer(gfs, {
    windOptions: {
      globalAlpha: 0.95,
      velocityScale: 0.01,
      paths: 5000,
      colorScale: [
        "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)",
      ],
      lineWidth: 2,
      width: 3,
      generateParticleOption: false,
    },
    wrapX: false,
    projection: "EPSG:4326",
    zIndex: 1000,
  });

  windLayer.id = "wordWindLayer";
  windLayer.name = "全球风场图";

  map.addLayer(windLayer);

}


function main(){
  // 1.从全球风场数据中获取河南省extent矩形范围内的风场数据
  const { header, us, vs } = getSubAreaGFS(gfs_world_2025063000, extent_henan);

  // 2.创建河南省风场数据
  const gfs_henan_2025063000 = createGFS(
    extent_henan,
    header.dx,
    header.dy,
    header.refTime,
    us,
    vs
  );

  // 3.按照边界裁剪河南省风场数据
  const gfs_henan_2025063000_clip = clipGFS(
    gfs_henan_2025063000,
    henan_boundary
  );

  // 4.添加河南省风场图层
  addWindLayer(gfs_henan_2025063000_clip);
  
}

main()

效果演示: 【OpenLayers:河南省风场效果】www.bilibili.com/video/BV1Rn...

参考资料

  1. 第39节:cesium 获取并转换风场数据(含源码+视频)_风场数据下载-CSDN博客
  2. JAVA在线调用Grib2Json-CSDN博客
  3. 修正GFS风场数据展示-CSDN博客
  4. OpenLayers实战,OpenLayers使用wind-layer插件实现风场动态效果_openlayers 绘制风场-CSDN博客
  5. openlayers扩展:风场可视化(wind-layer)-CSDN博客
  6. openlayers 6+版本绘制风场粒子效果_ol-wind-CSDN博客
  7. Cesium实战记录(八)三维风场+风速热力图(水平+垂直)_cesium风场-CSDN博客
  8. 通过加载girb2数据在页面上显示粒子流海面风场数据_风场数据nomads-CSDN博客
  9. 开源!Cesium中实现酷炫三维风场模拟,助力仿真预测!
  10. Cesium实战记录(八)三维风场+风速热力图(水平+垂直)_cesium风场-CSDN博客
  11. 【Canvas】绘制风速热力图_canvas 热力图-CSDN博客
  12. ol河南风场+热力图-语雀
  13. wind-layer 官方文档
相关推荐
paopaokaka_luck9 分钟前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
患得患失94943 分钟前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_1 小时前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路3 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔4 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang4 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔4 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任4 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴4 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔4 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js