風場可視化)
簡介這份資源面向需要在 Web 地圖中呈現動態(tài)風場的開發(fā)者尤其是使用 Leaflet 構建氣象可視化頁面的前端與 GIS 學習者。包內共 3 個文件包含 1 個 js 插件腳本、1 個 css 樣式文件和 1 個 json 數據文件壓縮包約 231KB分別承擔粒子動畫邏輯、圖層視覺樣式與全球風向風速網格數據的職責。其中 js 插件借助 D3 解析流體動力學數據將風速與風向轉化為直觀的粒子流動效果css 用于調整粒子大小、顏色與透明度json 則以網格形式存儲各點位的風向風速信息加載后即可驅動圖層動態(tài)渲染。目前已有 873 人學習下載適合希望快速搭建風向粒子演示、理解數據驅動地圖渲染流程的讀者參考也可作為氣象類地圖應用的原型素材。1. 從一張“會呼吸”的風場圖說起leaflet 風向粒子資源能干什么如果你在地圖項目里見過那種密密麻麻、順著風向流動的白色粒子線第一反應大概率是“這玩意兒怎么做的”。它不像普通 Marker 那樣一個個點上去也不像熱力圖那樣鋪一層色塊而是用成千上萬個粒子在 canvas 上實時演算把風向、風速用流動軌跡直接畫出來。這套 leaflet 風向粒子調用的 js 和 json 資源解決的就是這個問題它把 leaflet-velocity 這類粒子圖層所需的腳本文件和風場數據文件打包在一起讓你不用從零去啃 GRIB 或 NetCDF 氣象格式直接拿 json 就能在 Leaflet 地圖上疊一層動態(tài)風場。適合誰用做氣象可視化、環(huán)境監(jiān)測大屏、物流航線示意、甚至游戲地圖氛圍的開發(fā)者都能直接上手。尤其是那些已經用 Leaflet 搭好底圖、只想加一層風場動效的前端這份資源省掉了數據轉換和圖層適配的重復勞動。json 里存的是規(guī)則網格上的 U/V 分量js 負責把分量插值成粒子運動兩者配合就是一套能跑的風場渲染方案。2. 資源構成與運行鏈路js 腳本和 json 數據怎么對上2.1 目錄里到底放了什么拿到這份資源先別急著往項目里塞。我一般會先掃一眼文件構成因為 leaflet-velocity 對數據格式和腳本加載順序都有要求放錯位置就是白屏。典型的結構是這樣leaflet-velocity/ ├── leaflet-velocity.js # 粒子圖層核心腳本 ├── leaflet-velocity.css # 圖層控件與 canvas 樣式 ├── data/ │ ├── wind-global.json # 全球風場 U/V 分量數據 │ └── wind-region.json # 區(qū)域高分辨率風場數據 └── demo/ └── index.html # 最小可運行示例leaflet-velocity.js是主腳本內部實現了粒子生成、速度插值、canvas 重繪和動畫循環(huán)。leaflet-velocity.css管的是圖層切換按鈕和 canvas 容器的定位少了它粒子會蓋住地圖控件。data目錄下的 json 才是重點它們不是隨便的鍵值對而是有固定 schema 的網格數據。2.2 json 數據的字段含義風場 json 的結構直接決定粒子能不能畫出來。常見做法是采用類似下面的格式{ header: { parameterCategory: 2, parameterNumber: 2, nx: 360, ny: 181, lo1: 0, la1: 90, lo2: 359, la2: -90, dx: 1, dy: 1 }, data: [ { header: { parameterNumber: 2, parameterCategory: 2 }, data: [ ... ] }, { header: { parameterNumber: 3, parameterCategory: 2 }, data: [ ... ] } ] }這里header描述網格范圍nx、ny是經緯方向格點數lo1、la1是起始經緯度dx、dy是步長。data數組里通常放兩組第一組對應 U 分量東西向風速第二組對應 V 分量南北向風速。leaflet-velocity 會按parameterNumber區(qū)分2 是 U3 是 V。如果 json 里只有一組數據粒子就只會朝一個方向跑看起來像被風吹歪的直線。提示json 里的data數組長度必須等于nx * ny順序一般是從北到南、從西到東。順序錯了粒子會出現在錯誤經緯度上排查時優(yōu)先核對這個。2.3 最小運行示例把資源放進項目后一個能跑起來的頁面長這樣!DOCTYPE html html head meta charsetutf-8 link relstylesheet hrefhttps://unpkg.com/leaflet1.9.4/dist/leaflet.css / link relstylesheet href./leaflet-velocity.css / style #map { width: 100%; height: 600px; } /style /head body div idmap/div script srchttps://unpkg.com/leaflet1.9.4/dist/leaflet.js/script script src./leaflet-velocity.js/script script // 初始化底圖 var map L.map(map).setView([35, 105], 4); L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png).addTo(map); // 加載風場 json 并創(chuàng)建粒子圖層 fetch(./data/wind-global.json) .then(function (res) { return res.json(); }) .then(function (data) { var velocityLayer L.velocityLayer({ displayValues: true, // 鼠標懸停顯示風速風向 displayOptions: { velocityType: Global Wind, position: bottomleft, emptyString: 無風場數據, angleConvention: bearingCCW, speedUnit: m/s }, data: data, maxVelocity: 15, // 最大風速影響粒子顏色映射 colorScale: [#3288bd, #66c2a5, #abdda4, #e6f598, #fee08b, #fdae61, #f46d43], particleAge: 90, // 粒子壽命幀數 lineWidth: 1.2, particleMultiplier: 1 / 300, frameRate: 15, opacity: 0.9 }).addTo(map); }); /script /body /html這段代碼的關鍵在L.velocityLayer的配置。data直接吃 jsonmaxVelocity決定顏色映射上限超過這個值的風速會被截斷成最深的顏色。particleAge控制粒子存活幀數太小粒子剛生成就消失太大又會拖尾嚴重。particleMultiplier是粒子密度系數數值越小粒子越多但性能壓力也越大。frameRate是每秒重繪次數15 幀在普通筆記本上比較穩(wěn)調到 30 會明顯吃 CPU。2.4 腳本加載順序與依賴leaflet-velocity 依賴 Leaflet 的L.Layer擴展機制所以 Leaflet 必須在它之前加載。如果用的是模塊打包工具常見做法是import L from leaflet; import leaflet-velocity; import leaflet-velocity/dist/leaflet-velocity.css;注意leaflet-velocity在 npm 上的包名和文件路徑可能因版本不同有差異資源里給的 js 是已經構建好的 UMD 版本直接 script 引入最省事。如果項目用 webpack要確保 Leaflet 的全局變量L在 velocity 腳本執(zhí)行前已經掛到 window 上否則會報L is not defined。3. 核心模塊拆解粒子從生成到繪制的完整鏈路3.1 數據插值把網格點變成任意經緯度的風速json 里存的是離散網格點上的 U/V 分量但粒子可能出現在任意經緯度所以第一步是插值。leaflet-velocity 內部用的是雙線性插值核心邏輯可以簡化為// 根據經緯度找到網格索引并做雙線性插值 function interpolateField(lon, lat, header, uData, vData) { var i Math.floor((lon - header.lo1) / header.dx); var j Math.floor((header.la1 - lat) / header.dy); // 邊界保護防止越界 if (i 0 || j 0 || i header.nx - 1 || j header.ny - 1) { return { u: 0, v: 0 }; } var x (lon - header.lo1) / header.dx - i; var y (header.la1 - lat) / header.dy - j; var idx j * header.nx i; // 四個角點加權 var u (1 - x) * (1 - y) * uData[idx] x * (1 - y) * uData[idx 1] (1 - x) * y * uData[idx header.nx] x * y * uData[idx header.nx 1]; var v (1 - x) * (1 - y) * vData[idx] x * (1 - y) * vData[idx 1] (1 - x) * y * vData[idx header.nx] x * y * vData[idx header.nx 1]; return { u: u, v: v }; }header提供網格原點、步長和格點數uData、vData是從 json 里拆出來的兩個一維數組。i、j是左上角格點索引x、y是落在格子內的比例。四個角點按距離加權求和得到該經緯度的風速分量。如果粒子跑到網格范圍外直接返回零風速粒子就會原地消散不會出現亂飛的情況。3.2 粒子運動速度、壽命與邊界處理每個粒子有當前位置、年齡和速度。每一幀根據插值得到的 U/V 更新位置// 粒子單幀更新 function updateParticle(particle, field, dt) { var wind interpolateField(particle.lon, particle.lat, field.header, field.u, field.v); // 經緯度增量0.01 是經驗縮放系數讓粒子速度看起來自然 particle.lon wind.u * dt * 0.01; particle.lat wind.v * dt * 0.01; particle.age 1; // 超出壽命或跑出地圖范圍就重置 if (particle.age field.particleAge || particle.lon field.bounds.west || particle.lon field.bounds.east || particle.lat field.bounds.south || particle.lat field.bounds.north) { resetParticle(particle, field); } }dt是幀間隔系數0.01這個縮放值不是固定的風速單位是 m/s 時經緯度變化太快會像閃電太慢又像靜止。我一般會先跑一遍看效果再微調這個系數。resetParticle會把粒子隨機撒回地圖范圍內年齡歸零這樣粒子流看起來是連續(xù)的。3.3 canvas 繪制與顏色映射粒子位置算好后用 canvas 的fillRect或lineTo畫出來。顏色根據風速大小從colorScale里取// 根據風速取顏色 function getColor(speed, maxVelocity, colorScale) { var index Math.floor((speed / maxVelocity) * (colorScale.length - 1)); index Math.max(0, Math.min(index, colorScale.length - 1)); return colorScale[index]; } // 繪制單幀 function drawFrame(ctx, particles, field) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.globalAlpha field.opacity; particles.forEach(function (p) { var wind interpolateField(p.lon, p.lat, field.header, field.u, field.v); var speed Math.sqrt(wind.u * wind.u wind.v * wind.v); ctx.fillStyle getColor(speed, field.maxVelocity, field.colorScale); var point field.map.latLngToContainerPoint([p.lat, p.lon]); ctx.fillRect(point.x, point.y, field.lineWidth, field.lineWidth); }); }latLngToContainerPoint把經緯度轉成 canvas 像素坐標這一步依賴 Leaflet 的地圖投影。clearRect每幀清空畫布所以粒子是重繪而不是疊加看起來才有流動感。globalAlpha控制整體透明度太低會看不清太高又會蓋住底圖。3.4 性能取舍粒子數量與幀率的平衡粒子圖層最怕的就是卡。particleMultiplier和frameRate是兩個主要調節(jié)旋鈕。我一般會按地圖容器面積估算粒子數參數建議值影響particleMultiplier1/300 1/500值越小粒子越多越吃 CPUframeRate12 20越高越流暢但重繪壓力大particleAge60 120越大拖尾越長粒子數累積越多l(xiāng)ineWidth1 2太粗會糊太細看不清如果頁面還有別的動畫或圖表建議把frameRate壓到 12particleMultiplier調到 1/500先保證不卡再談效果。移動端更要保守粒子數控制在 2000 以內比較穩(wěn)。4. 避坑與排查風場粒子不顯示、亂飛、卡頓的常見原因4.1 粒子完全不顯示現象地圖出來了但沒有任何粒子線。原因通常是 json 沒加載成功或者data字段結構不對。先看瀏覽器 Network 面板確認 json 返回 200 且內容完整。如果 json 里data是空數組或者header缺少nx、ny粒子初始化時就會直接跳過。解決方法是拿資源里的示例 json 對比字段確保data數組長度等于nx * ny且兩組數據分別對應 U 和 V。4.2 粒子朝一個方向直線跑現象所有粒子都往同一個方向飛沒有旋轉和彎曲。原因一般是 json 里只放了一組數據或者兩組數據的parameterNumber都是 2。leaflet-velocity 靠parameterNumber區(qū)分 U/V2 是 U3 是 V。如果兩組都是 2V 分量就是零粒子只受東西向風影響。解決方法是檢查 json 第二組數據的parameterNumber是否為 3不是就手動改過來。4.3 粒子位置偏移或跑到地圖外現象粒子集中在某個角落或者跑到沒有數據的區(qū)域還在動。原因通常是header里的lo1、la1和實際數據順序不匹配。有些氣象數據是從南到北存有些是從北到南存la1要對應第一行數據的緯度。如果順序反了插值出來的風速會整體翻轉。解決方法是拿一個已知坐標點比如北京附近手動算一下索引看插值結果是否合理。4.4 頁面卡頓、風扇狂轉現象粒子圖層一開瀏覽器 CPU 飆升滾動地圖明顯掉幀。原因是粒子數太多或幀率太高。先看particleMultiplier是不是設成了 1/100 以下再檢查frameRate是否超過 25。解決方法是把particleMultiplier調到 1/400 以上frameRate降到 15particleAge降到 80。如果還卡考慮只在縮放級別大于 4 時才顯示粒子圖層小比例尺下用靜態(tài)風場箭頭代替。4.5 鼠標懸停不顯示風速值現象displayValues設為 true但鼠標放上去沒有數值提示。原因是displayOptions里的velocityType和position沒配對或者 css 文件沒引入。leaflet-velocity 的數值面板依賴leaflet-velocity.css里的定位樣式少了它面板會跑到頁面底部看不見。解決方法是確認 css 已加載并且position用bottomleft或bottomright這類 Leaflet 控件位置。5. 二次開發(fā)與改造換數據源、調樣式、接業(yè)務字段5.1 換成自己的風場數據資源里的 json 是示例數據實際項目多半要接自己的氣象接口。改造思路是保持 schema 不變只替換data內容。如果后端返回的是 GRIB 或 NetCDF常見做法是用 Python 的xarray或cfgrib轉成 jsonimport xarray as xr import json # 讀取 GRIB 文件 ds xr.open_dataset(wind.grib, enginecfgrib) u ds[u10].values.flatten().tolist() v ds[v10].values.flatten().tolist() # 構造 leaflet-velocity 需要的結構 header { nx: ds.dims[longitude], ny: ds.dims[latitude], lo1: float(ds.longitude.min()), la1: float(ds.latitude.max()), dx: float(ds.longitude[1] - ds.longitude[0]), dy: float(ds.latitude[0] - ds.latitude[1]) } result { header: header, data: [ {header: {parameterNumber: 2, parameterCategory: 2}, data: u}, {header: {parameterNumber: 3, parameterCategory: 2}, data: v} ] } with open(wind-custom.json, w) as f: json.dump(result, f)注意la1要取最大緯度因為 leaflet-velocity 默認第一行是北邊。dx、dy用相鄰格點差值算別硬編碼。轉出來的 json 直接喂給L.velocityLayer就能用。5.2 自定義顏色和粒子樣式colorScale是數組按風速從小到大映射。想改成業(yè)務色系直接換數組值就行。比如用藍色系表示低風速、紅色系表示高風速colorScale: [#313695, #4575b4, #74add1, #abd9e9, #e0f3f8, #ffffbf, #fee090, #fdae61, #f46d43, #d73027]lineWidth和opacity也可以按地圖底圖深淺調。深色底圖用亮色粒子、opacity0.8 左右淺色底圖用深色粒子、opacity0.6 左右避免蓋住路網。5.3 把風速值接到業(yè)務面板displayValues只顯示鼠標位置的風速如果要在側邊欄展示區(qū)域平均風速可以監(jiān)聽地圖mousemove事件自己調interpolateFieldmap.on(mousemove, function (e) { var wind interpolateField(e.latlng.lng, e.latlng.lat, header, uData, vData); var speed Math.sqrt(wind.u * wind.u wind.v * wind.v); document.getElementById(wind-speed).innerText speed.toFixed(2) m/s; });這樣就能把風場數據和業(yè)務指標綁在一起比如超過某個閾值就觸發(fā)告警樣式。5.4 一個我踩過的坑最早做風場圖層時我直接把 json 里的data數組當成[u, v]二維數組用結果粒子全擠在左上角。后來才發(fā)現 leaflet-velocity 要的是兩個獨立對象每個對象里再包一層data數組。從那以后我每次接新數據源都先拿一個已知坐標點手動算一遍插值結果確認 U/V 方向沒反、網格原點沒偏再往地圖上疊。這個習慣幫我省了不少返工時間。如果你也在做地圖風場可視化這份 leaflet 風向粒子調用的 js 和 json 資源可以直接拿去跑改數據源和調樣式都有現成的入口。希望幫到你。本文還有配套的精品資源點擊獲取