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分量。
每一个分量对象中,又包含header
和data
两个属性。
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数据:
网站首页是这样:

✅选择模型
首先要选择数据模型,要在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 分量计算得出:
- 合成风速:

- 合成风向:

因此我们要下载的就是风的两个分量数据。它们分别是 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上去下载:
我把这个项目下载到本地之后,发现它是个Java项目,我不懂Java,使用的时候卡在了安装依赖这一步上,我失败了o(╥﹏╥)o。
如果你想尝试使用grib2json,以下的资料或许对你有帮助:
第39节:cesium 获取并转换风场数据(含源码+视频)_风场数据下载-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-wind
是 wind-layer
生态中专门为 OpenLayers 地图库 设计的风场可视化适配器 ,而 wind-layer
是一个跨地图引擎的气象数据可视化核心框架。
4.1.1 官方文档
ol-wind
(wind-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
。
FlowField
在Field
的基础上新增了三个方法:
- 增加了一个工厂方法
createFromGFS
,用于根据 ol wind data 数据创建Field
实例; - 增加了
forEach
方法,可以遍历Field
实例中的每一个单元格,获取单元格中的x风速
、y风速
; - 增加了
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...
参考资料
- 第39节:cesium 获取并转换风场数据(含源码+视频)_风场数据下载-CSDN博客
- JAVA在线调用Grib2Json-CSDN博客
- 修正GFS风场数据展示-CSDN博客
- OpenLayers实战,OpenLayers使用wind-layer插件实现风场动态效果_openlayers 绘制风场-CSDN博客
- openlayers扩展:风场可视化(wind-layer)-CSDN博客
- openlayers 6+版本绘制风场粒子效果_ol-wind-CSDN博客
- Cesium实战记录(八)三维风场+风速热力图(水平+垂直)_cesium风场-CSDN博客
- 通过加载girb2数据在页面上显示粒子流海面风场数据_风场数据nomads-CSDN博客
- 开源!Cesium中实现酷炫三维风场模拟,助力仿真预测!
- Cesium实战记录(八)三维风场+风速热力图(水平+垂直)_cesium风场-CSDN博客、
- 【Canvas】绘制风速热力图_canvas 热力图-CSDN博客
- ol河南风场+热力图-语雀
- wind-layer 官方文档