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

主页: yougis.com.cn 博文: blog.yougis.com.cn 工具: yougis.com.cn/tool/home 关注WX号 YouGIS顽石(yougis) 获取更多精彩内容

关注WX号 YouGIS顽石(yougis),获取更多数据资源和在线服务;

风场数据服务在线体验,请戳我 风场数据服务API测试

风场数可视化效果,请戳我 风场数据可视化

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

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

前言

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

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

一、技术选型

1.1 核心技术栈

技术 版本 用途
Leaflet 1.9.4 开源地图库,提供基础地图功能
leaflet-velocity 1.7.0 风场可视化插件,实现动态风场效果
天地图API - 提供中国区域的高质量底图

1.2 为什么选择这些技术?

Leaflet的优势:

  • 轻量级,完整版仅约40KB
  • 文档完善,社区活跃
  • 支持丰富的插件生态
  • 跨平台兼容性好

leaflet-velocity的优势:

  • 专门用于风场可视化
  • 支持多种数据格式(GRIB、JSON等)
  • 性能优化良好,支持大量粒子动画
  • 可自定义颜色映射和显示选项

天地图的优势:

  • 中国境内地图数据精准
  • 提供多种底图样式(影像、矢量、地形)
  • 免费使用(需申请密钥)

二、系统架构设计

2.1 整体架构

用户界面
控制面板
数据源选择
范围选择
时间控制
API请求
风场数据服务
数据预处理
Leaflet地图
leaflet-velocity
动态风场展示

2.2 数据流程

复制代码
用户操作 → 参数构建 → API请求 → 数据获取 → 数据预处理 → 可视化渲染

三、核心功能实现

3.1 地图初始化

首先创建基础地图,使用天地图作为底图:

javascript 复制代码
// 创建地图实例
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 数据源配置

支持两种数据源:

javascript 复制代码
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个省份(含台湾):

javascript 复制代码
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:

javascript 复制代码
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期望的从北到南排列相反,会导致风场显示位置偏移。我们需要对数据进行重新排列:

javascript 复制代码
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 中国范围边界定义
javascript 复制代码
const CONFIG = {
    // 中国范围边界 [西南角, 东北角]
    cnBounds: [[15.0, 62.0], [56.0, 147.0]]
};
4.3.2 地图范围自动检测

当用户拖动地图时,自动判断当前视野范围,并切换对应的数据源:

javascript 复制代码
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. 数据时效性:真气网数据更新频率更高,适合省域实时监测

省份选择实现:

javascript 复制代码
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插件绘制风场:

javascript 复制代码
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 时间格式化
javascript 复制代码
// 格式化时间为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 播放控制
javascript 复制代码
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文件结构:

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. 开发移动端应用

参考资源


相关推荐
Highcharts.js1 天前
Highcharts 渲染方式解析:SVG 默认渲染与 Boost 模块的 WebGL 加速,以及它与其他可视化库的不同
svg·webgl·可视化·canvas·highcharts·渲染方式
躺柒2 天前
读数据可视化18时变数据(下)
信息可视化·时序数据库·可视化·数据可视化·实时数据库·时变数据
Highcharts.js2 天前
Highcharts 双 Y 轴分组错位柱状图・代码详解
svg·可视化·demo·highcharts·双y轴·数据示例
躺柒3 天前
读数据可视化17时变数据(上)
信息可视化·时序数据库·可视化·数据可视化·时变数据·时变
统计学小王子3 天前
R语言入门——相关性热图(建模常用二)
r语言·可视化·相关性分析
躺柒4 天前
读数据可视化16大规模多变量空间数据(下)
人工智能·信息可视化·可视化·数据可视化·空间·大数据分析
ACQTEC研索仪器5 天前
Psylotech xTS 原位加载系统在颗粒材料力学研究中的应用实践
可视化·高分辨率·材料力学·原位加载·颗粒材料·应变率
躺柒5 天前
读数据可视化15大规模多变量空间数据(上)
信息可视化·可视化·数据可视化·空间计算·空间
躺柒8 天前
读数据可视化12可视化设计原则
信息可视化·数据分析·可视化·数据可视化·大数据分析
数据智研8 天前
【数据分享】中国农产品价格调查年鉴(2004-2025)
大数据·人工智能·数据分析·可视化·数据可视化