侧边栏壁纸
博主头像
YouGIS博文 - YouGIS顽石工坊 博主等级

行动起来,活在当下

  • 累计撰写 25 篇文章
  • 累计创建 8 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

风场可视化实战:从数据到动态地图的完整技术实现

Administrator
2026-04-06 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

主页:yougis.com.cn
博文:
blog.yougis.com.cn
工具:
yougis.com.cn/tool/home

qr-wechat.jpg

扫码获取更多精彩内容

风场可视化实战:从数据到动态地图的完整技术实现

本文是风场系列文章的第四篇,将详细介绍如何使用Leaflet和leaflet-velocity插件实现风场数据的可视化,重点讲解中国及各省份风场数据的特殊处理方法。

前言

在前面的系列文章中,我们分别介绍了:

今天,我们将进入最激动人心的环节——风场可视化实战!本文将从技术角度,详细讲解如何将风场数据转换为动态、可交互的地图可视化效果。

图1-风场可视化效果展示图.png

一、技术选型

1.1 核心技术栈

技术

版本

用途

Leaflet

1.9.4

开源地图库,提供基础地图功能

leaflet-velocity

1.7.0

风场可视化插件,实现动态风场效果

天地图API

-

提供中国区域的高质量底图

1.2 为什么选择这些技术?

Leaflet的优势:

  • 轻量级,完整版仅约40KB

  • 文档完善,社区活跃

  • 支持丰富的插件生态

  • 跨平台兼容性好

leaflet-velocity的优势:

  • 专门用于风场可视化

  • 支持多种数据格式(GRIB、JSON等)

  • 性能优化良好,支持大量粒子动画

  • 可自定义颜色映射和显示选项

天地图的优势:

  • 中国境内地图数据精准

  • 提供多种底图样式(影像、矢量、地形)

  • 免费使用(需申请密钥)

二、系统架构设计

2.1 整体架构

整体架构图.png

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 省域范围的特殊处理

省域范围风场数据有以下特点:

  1. 数据密度更高:省域数据使用更精细的网格,能显示更详细的风场细节

  2. 边界裁剪:数据仅包含该省份范围内的风场信息

  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 省域数据加载失败

问题:选择某些省份时提示"对不起,选择省份没有数据"。

原因

  1. 该省份暂时没有风场数据

  2. 选择的时间点数据不存在

  3. 数据源不支持该省份

解决方案

  1. 切换到其他省份

  2. 选择最近的时间点

  3. 尝试切换数据源

6.3 性能问题

问题:风场动画卡顿,页面响应慢。

原因

  1. 数据量过大

  2. 浏览器性能不足

  3. 网络延迟

解决方案

  1. 减少粒子数量(修改leaflet-velocity配置)

  2. 使用省域范围而非全球范围

  3. 优化网络请求,使用CDN加速

七、在线演示

完整代码已部署在线,可以直接访问体验:

🌐 风场可视化在线演示https://yougis.com.cn/yougis-windy-server/show-windy.html

🔧 数据服务API测试https://yougis.com.cn/yougis-windy-server/api-test.html

八、总结

本文详细介绍了风场可视化的完整技术实现,重点讲解了:

  1. 技术选型:Leaflet + leaflet-velocity + 天地图

  2. 核心功能:多数据源、多尺度、时间控制

  3. 关键技术

    • scanMode=64数据预处理

    • 中国范围边界定义

    • 省域数据特殊处理

    • 地图范围自动检测

  4. 完整代码:从初始化到渲染的完整流程

通过本文的学习,你应该能够:

  • 理解风场可视化的基本原理

  • 掌握Leaflet和leaflet-velocity的使用方法

  • 了解中国及省份风场数据的特殊处理

  • 实现一个完整的风场可视化系统

九、后续计划

未来我们计划:

  1. 支持3D风场可视化

  2. 增加更多气象要素(温度、湿度、气压)

  3. 优化性能,支持更大规模数据

  4. 开发移动端应用

参考资源

0

评论区