风场可视化实战:从数据到动态地图的完整技术实现
本文是风场系列文章的第四篇,将详细介绍如何使用Leaflet和leaflet-velocity插件实现风场数据的可视化,重点讲解中国及各省份风场数据的特殊处理方法。
前言
在前面的系列文章中,我们分别介绍了:
第一篇:风场数据全面分析 - 深入解析NOAA和真气网数据
第二篇:风场数据服务详解 - API接口设计与在线测试
第三篇:私有化部署指南 - Docker镜像与部署方案
今天,我们将进入最激动人心的环节——风场可视化实战!本文将从技术角度,详细讲解如何将风场数据转换为动态、可交互的地图可视化效果。

一、技术选型
1.1 核心技术栈
1.2 为什么选择这些技术?
Leaflet的优势:
轻量级,完整版仅约40KB
文档完善,社区活跃
支持丰富的插件生态
跨平台兼容性好
leaflet-velocity的优势:
专门用于风场可视化
支持多种数据格式(GRIB、JSON等)
性能优化良好,支持大量粒子动画
可自定义颜色映射和显示选项
天地图的优势:
中国境内地图数据精准
提供多种底图样式(影像、矢量、地形)
免费使用(需申请密钥)
二、系统架构设计
2.1 整体架构

2.2 数据流程
用户操作 → 参数构建 → API请求 → 数据获取 → 数据预处理 → 可视化渲染
三、核心功能实现
3.1 地图初始化
首先创建基础地图,使用天地图作为底图:
// 创建地图实例
map = L.map('map', {
center: [35.0, 105.0], // 中心点设在中国
zoom: 4,
zoomControl: true
});
// 添加天地图影像底图
const tiandituImg = L.tileLayer(
'https://t{s}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=YOUR_TOKEN',
{
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
maxZoom: 18,
attribution: '天地图'
}
);
// 添加天地图影像注记
const tiandituCia = L.tileLayer(
'https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=YOUR_TOKEN',
{
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
maxZoom: 18,
attribution: '天地图'
}
);
tiandituImg.addTo(map);
tiandituCia.addTo(map);
注意:使用天地图需要申请密钥(tk参数),请访问天地图官网申请。
3.2 数据源配置
支持两种数据源:
const CONFIG = {
domain: 'https://yougis.com.cn',
cnBounds: [[15.0, 62.0], [56.0, 147.0]], // 中国范围边界
playHours: 24
};
// 数据源类型
const DATA_SOURCES = {
'common': '真气网数据', // 覆盖中国及周边
'noaa': 'NOAA数据' // 覆盖全球
};
3.3 省份配置
支持34个省份(含台湾):
const PROVINCES = {
'ah': '安徽', 'bj': '北京', 'cq': '重庆', 'fj': '福建',
'gs': '甘肃', 'gd': '广东', 'gx': '广西', 'gz': '贵州',
'hi': '海南', 'he': '河北', 'hl': '黑龙江', 'ha': '河南',
'hb': '湖北', 'hn': '湖南', 'js': '江苏', 'jx': '江西',
'jl': '吉林', 'ln': '辽宁', 'nm': '内蒙', 'nx': '宁夏',
'qh': '青海', 'sn': '陕西', 'sd': '山东', 'sh': '上海',
'sx': '山西', 'sc': '四川', 'tj': '天津', 'xj': '新疆',
'xz': '西藏', 'yn': '云南', 'zj': '浙江', 'tw': '台湾'
};
四、关键技术实现
4.1 数据加载与API调用
构建API请求URL:
async function loadWindyData() {
// 验证省域范围是否选择了省份
if (currentExtent === 'province' && !currentProvince) {
alert('请选择省份');
return;
}
showLoading(true);
// 格式化时间参数
const hour = formatTimeForUrl(currentTime);
// 构建数据范围参数
let extentParam = currentExtent;
if (currentExtent === 'province' && currentProvince) {
extentParam = currentProvince;
}
// 构建API URL
const url = `${CONFIG.domain}/yougis-windy-server/windfield/api/v1.0/show-windy/${currentDataSource}/${extentParam}/${hour}.json`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 检查数据有效性
const hasData = data && Array.isArray(data) && data.length > 0 &&
data[0].data.length > 0 && data[1].data.length > 0;
if (hasData) {
// 数据预处理
const processedData = processWindyData(data);
drawWind(processedData);
} else {
handleEmptyData();
}
} catch (error) {
console.error('加载风场数据失败:', error);
alert('加载风场数据失败,请检查网络连接或稍后重试');
} finally {
showLoading(false);
}
}
API URL格式说明:
https://yougis.com.cn/yougis-windy-server/windfield/api/v1.0/show-windy/{dataSource}/{extent}/{hour}.json
参数说明:
dataSource: 数据源(common/noaa)extent: 数据范围(cn/global/省份代码)hour: 时间参数(格式:YYYYMMDDHH)
4.2 数据预处理:解决scanMode=64偏移问题
这是本文最重要的技术点!
NOAA数据中存在scanMode=64的情况,表示数据是从南到北排列的,这与leaflet-velocity期望的从北到南排列相反,会导致风场显示位置偏移。我们需要对数据进行重新排列:
function processWindyData(data) {
if (!data || data.length === 0) return data;
// 查找U分量和V分量数据
const uData = data.find(d => d.header && d.header.parameterNumber === 2);
const vData = data.find(d => d.header && d.header.parameterNumber === 3);
// 检查是否需要处理scanMode=64
if (uData && uData.header && uData.header.scanMode === 64) {
console.log('检测到scanMode=64,需要重新排列数据');
const nx = uData.header.nx; // 经度方向网格数
const ny = uData.header.ny; // 纬度方向网格数
// 重新排列U数据:从南到北 → 从北到南
const uDataNew = [];
for (let row = ny - 1; row >= 0; row--) {
for (let col = 0; col < nx; col++) {
const index = row * nx + col;
uDataNew.push(uData.data[index]);
}
}
uData.data = uDataNew;
// 重新排列V数据
if (vData) {
const vDataNew = [];
for (let row = ny - 1; row >= 0; row--) {
for (let col = 0; col < nx; col++) {
const index = row * nx + col;
vDataNew.push(vData.data[index]);
}
}
vData.data = vDataNew;
}
// 修改scanMode为0
uData.header.scanMode = 0;
if (vData) {
vData.header.scanMode = 0;
}
}
return data;
}
原理解析:
原始数据(scanMode=64,从南到北):
[行0] [南端]
[行1]
[行2]
...
[行n-1] [北端]
处理后(scanMode=0,从北到南):
[行0] [北端] ← 原始的行n-1
[行1] ← 原始的行n-2
[行2] ← 原始的行n-3
...
[行n-1] [南端] ← 原始的行0
4.3 中国及省份风场数据的特殊处理
4.3.1 中国范围边界定义
const CONFIG = {
// 中国范围边界 [西南角, 东北角]
cnBounds: [[15.0, 62.0], [56.0, 147.0]]
};
4.3.2 地图范围自动检测
当用户拖动地图时,自动判断当前视野范围,并切换对应的数据源:
function checkExtent() {
// 如果当前是省域范围,不进行自动切换
if (currentExtent === 'province') {
return;
}
const bounds = map.getBounds();
const cnBounds = L.latLngBounds(CONFIG.cnBounds[0], CONFIG.cnBounds[1]);
let newExtent;
if (cnBounds.contains(bounds)) {
newExtent = 'cn'; // 视野在中国范围内
} else {
newExtent = 'global'; // 视野超出中国范围
}
if (newExtent !== currentExtent) {
currentExtent = newExtent;
document.getElementById('dataExtent').value = newExtent;
loadWindyData(); // 重新加载数据
}
}
// 监听地图移动事件
map.on('moveend', function() {
checkExtent();
});
4.3.3 省域范围的特殊处理
省域范围风场数据有以下特点:
数据密度更高:省域数据使用更精细的网格,能显示更详细的风场细节
边界裁剪:数据仅包含该省份范围内的风场信息
数据时效性:真气网数据更新频率更高,适合省域实时监测
省份选择实现:
function handleExtentChange() {
const extent = document.getElementById('dataExtent').value;
currentExtent = extent;
const provinceGroup = document.getElementById('provinceGroup');
if (extent === 'province') {
// 显示省份选择下拉框
provinceGroup.style.display = 'block';
// 如果没有选择省份,提示用户选择
if (!currentProvince) {
document.getElementById('provinceSelect').value = '';
}
} else {
// 隐藏省份选择下拉框
provinceGroup.style.display = 'none';
currentProvince = '';
}
loadWindyData();
}
function handleProvinceChange() {
const province = document.getElementById('provinceSelect').value;
currentProvince = province;
if (currentExtent === 'province') {
loadWindyData();
}
}
4.4 风场绘制
使用leaflet-velocity插件绘制风场:
function drawWind(data) {
// 移除现有风场图层
if (windLayer) {
map.removeLayer(windLayer);
}
// 创建新的风场图层
windLayer = L.velocityLayer({
displayValues: true, // 显示数值
displayOptions: {
velocityType: 'Wind',
position: 'bottomright',
emptyString: 'No wind data',
angleConvention: 'bearingCW', // 角度约定:顺时针
displayPosition: 'bottomright',
displayEmptyString: 'No velocity data',
speedUnit: 'm/s' // 速度单位
},
maxVelocity: 10, // 最大速度(用于颜色映射)
data: data
});
map.addLayer(windLayer);
// 更新时间显示
updateTimeDisplay();
}
显示选项说明:
displayValues: 是否显示鼠标悬停时的风速数值angleConvention: 角度约定,bearingCW表示顺时针方向(气象学标准)maxVelocity: 最大风速,用于颜色映射的归一化speedUnit: 速度单位,支持m/s、km/h、mph等
4.5 时间控制与回放
4.5.1 时间格式化
// 格式化时间为YYYYMMDDHH(用于API请求)
function formatTimeForUrl(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
return year + month + day + hour;
}
// 格式化时间用于显示
function formatTimeForDisplay(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:00`;
}
4.5.2 播放控制
let isPlaying = false;
let playInterval;
function play() {
if (isPlaying) {
pause();
return;
}
isPlaying = true;
document.getElementById('playBtn').textContent = '暂停';
// 每5秒播放一小时
playInterval = setInterval(() => {
currentTime.setHours(currentTime.getHours() + 1);
updateTimeDisplay();
loadWindyData();
}, 5000);
}
function pause() {
isPlaying = false;
document.getElementById('playBtn').textContent = '播放';
clearInterval(playInterval);
}
function prevHour() {
pause();
currentTime.setHours(currentTime.getHours() - 1);
updateTimeDisplay();
loadWindyData();
}
function nextHour() {
pause();
currentTime.setHours(currentTime.getHours() + 1);
updateTimeDisplay();
loadWindyData();
}
五、完整代码示例
以下是完整的HTML文件结构:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>风场可视化 - YouGIS顽石工坊</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
/* CSS样式 */
</style>
</head>
<body>
<div id="map"></div>
<div class="control-panel">
<h3>风场控制面板</h3>
<div class="control-group">
<label for="dataSource">数据源</label>
<select id="dataSource">
<option value="common">真气网数据</option>
<option value="noaa">NOAA数据</option>
</select>
</div>
<div class="control-group">
<label for="dataExtent">数据范围</label>
<select id="dataExtent">
<option value="cn">中国范围</option>
<option value="global">全球范围</option>
<option value="province">省域范围</option>
</select>
</div>
<div class="control-group" id="provinceGroup" style="display: none;">
<label for="provinceSelect">选择省份</label>
<select id="provinceSelect"></select>
</div>
<div class="control-group">
<label for="timeSelect">选择时间</label>
<input type="datetime-local" id="timeSelect">
</div>
<div class="play-controls">
<button id="prevBtn">上一小时</button>
<button id="playBtn">播放</button>
<button id="nextBtn">下一小时</button>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-velocity@1.7.0/dist/leaflet-velocity.min.js"></script>
<script>
// JavaScript代码
</script>
</body>
</html>
六、常见问题与解决方案
6.1 风场显示位置偏移
问题:风场显示的位置与实际地理位置不符。
原因:NOAA数据的scanMode=64导致数据排列方向错误。
解决方案:使用processWindyData()函数重新排列数据(见4.2节)。
6.2 省域数据加载失败
问题:选择某些省份时提示"对不起,选择省份没有数据"。
原因:
该省份暂时没有风场数据
选择的时间点数据不存在
数据源不支持该省份
解决方案:
切换到其他省份
选择最近的时间点
尝试切换数据源
6.3 性能问题
问题:风场动画卡顿,页面响应慢。
原因:
数据量过大
浏览器性能不足
网络延迟
解决方案:
减少粒子数量(修改leaflet-velocity配置)
使用省域范围而非全球范围
优化网络请求,使用CDN加速
七、在线演示
完整代码已部署在线,可以直接访问体验:
🌐 风场可视化在线演示:https://yougis.com.cn/yougis-windy-server/show-windy.html
🔧 数据服务API测试:https://yougis.com.cn/yougis-windy-server/api-test.html
八、总结
本文详细介绍了风场可视化的完整技术实现,重点讲解了:
技术选型:Leaflet + leaflet-velocity + 天地图
核心功能:多数据源、多尺度、时间控制
关键技术:
scanMode=64数据预处理
中国范围边界定义
省域数据特殊处理
地图范围自动检测
完整代码:从初始化到渲染的完整流程
通过本文的学习,你应该能够:
理解风场可视化的基本原理
掌握Leaflet和leaflet-velocity的使用方法
了解中国及省份风场数据的特殊处理
实现一个完整的风场可视化系统
九、后续计划
未来我们计划:
支持3D风场可视化
增加更多气象要素(温度、湿度、气压)
优化性能,支持更大规模数据
开发移动端应用

评论区