一根杆与一张场:把你的机场画上地图

《风不是一个数》系列第 2 篇。上一篇你学会了读 :一根杆、一张场、一个扇区、一条廓线各管什么。这一篇开始画 ------四种画法里先讲最基础的两种:一个点、一片场。读完你能做一件事:换一个 ICAO 代码,你机场此刻的风就出现在地图上。

先看四条真实报文。2026-10-24 13:00 UTC(下同记 13Z),成都、北京、广州、上海四个机场的例行观测,同一时刻各一条:

yaml 复制代码
ZUUU 241300Z 33002MPS 8000 FEW020 SCT040 19/16 Q1014 NOSIG

ZBAA 241300Z 00000MPS 6000 -RA OVC040 13/11 Q1023 NOSIG

ZGGG 241300Z 02004MPS CAVOK 26/17 Q1010 NOSIG

ZSSS 241300Z 02006MPS 9999 -RA BKN011 20/19 Q1013 BECMG TL1430 BKN015

四个风组,四种状态:成都 33002MPS(风从 330° 来、2 m/s)、北京 00000MPS(静风 )、广州 02004MPS(020°、4 m/s)、上海 02006MPS(020°、6 m/s)。

上一篇你已经会把它们读成编码:成都 2 m/s 是一根短划,广州 4 m/s 是一根长划,上海 6 m/s 是一长一短,北京静风------没有杆(方向无从谈起)。这一篇的问题是:这四根杆,怎么放到一张真正的地图上? 放上去之后,方向怎么保证不歪、几百根杆怎么不打架、读图的人怎么知道你用哪套刻度------每一个都是纸面上不存在、地图上逃不掉的问题。

一、第一公里:一条报文,一根杆,上地图

先把环境搭好。本篇的代码分两类运行环境,先说清边界:编码与批量解析的段落是纯 Node,.mjs 文件直接跑;「上地图」的段落要浏览器------地图本来就是浏览器里的东西。

bash 复制代码
mkdir wind-map && cd wind-map

npm init -y

npm pkg set type=module # 全文代码都是 ESM(import 写法),这一行别漏

npm i @metweave/parser @metweave/grid @metweave/leaflet @metweave/render leaflet

地图侧的最小骨架,三样东西:一个装地图的 <div>、建一张图、铺一层免费底图(OpenStreetMap 公开瓦片,不需要 key):

html 复制代码
<!-- index.html -->

<div id="map" style="height:100vh"></div>

<script type="module" src="./main.mjs"></script>
js 复制代码
// main.mjs

import L from "leaflet";

import "leaflet/dist/leaflet.css";

const map = L.map("map").setView([32, 110], 4);

L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png",

{ attribution: "© OpenStreetMap" }).addTo(map);

骨架有了,走第一公里。从一条报文到编码,上一篇的十行代码已经走通,这里直接消费结论;真正的新问题在下一步------编码怎么变成地图上的东西。 风羽上地图,最短的路是 addWindBarbGlyphs:你把「几根杆、各在哪、多大、从哪来」直接交给它,它负责画:

js 复制代码
import { parse } from "@metweave/parser";

import { windBarbOf } from "@metweave/grid";

import { addWindBarbGlyphs } from "@metweave/leaflet";

  


const w = parse("ZUUU 241300Z 33002MPS 8000 FEW020 SCT040 19/16 Q1014 NOSIG").wind.value;

console.log(windBarbOf(w.direction, w.speed.value, { unit: w.speed.unit }));

// → { unit: 'm/s', pennants: 0, longs: 0, shorts: 1, encodedSpeed: 2 } ← 一根短划

  


await addWindBarbGlyphs(map, [

{ lat: 30.67, lon: 104.02, direction: w.direction, speed: w.speed.value },

]);

跑完,成都的位置上出现一根杆:杆梢指向 330°(来向------杆梢指着哪边,风就从哪边来 ,上一篇的锚句在地图上原样成立),杆身一侧一根短划。不必先有格点场------这条入口就是为「手上只有报文」准备的,一条报文一根杆。

报文层还有三个特殊状态,地图上的表现值得亲眼见一次:

  • 静风 :北京那条 00000MPS,windBarbOf 返回 null------不是「零号杆」,是没有杆 。地图上是空位,想知道「这里确实有观测、只是没风」,配一块数值牌(第三节,牌上写「静风(00000)」)。两条背景:业务填图里静风其实有专门符号(站圈旁标 C),库的地图层选择留空、由牌补话,是产品取舍;编报层面的静风按观测规范定(平均风低于 0.5 m/s 编 00000),库的出杆阈值 1 m/s 是另一回事------那是渲染配置,不是编报规范。

  • 风向不定(VRB) :VRB01MPS 这种风组,解析出来 direction 是 null------方向不定,杆没法指。上图前必须连它一起拦(第四节的代码有这一步),否则 null 传进图层,画出一根指向北方的假杆。它并不罕见:就在今天这四站的报文里,00 到 12 时之间一共有九条 VRB。

  • 缺测 :风组整组缺失时 parse 的 wind 是 undefined(不是空对象)------批量上图前要先判存在,否则地图没等到杆,先等到一个报错。

还有一个编码层的细节值得亲手跑一次:有杆无羽 (只有杆身、没有任何羽)。1 m/s 没掉进出杆阈值------低于 1 m/s 才不出杆------所以它会出一根杆;但 1 m/s 向下取整到短划刻度是 0 根,于是画出一根光杆。这不是 bug,是「取整到刻度」的忠实结果(2 m/s 才有第一根短划)。顺带一句:真实报文里 1 m/s 几乎都以 VRB01MPS 出现------风小到 2 m/s 以下,风向本就难定,规范直接编「不定风向」;定向的 1 m/s 是构造例,拿它讲取整最干净。METAR 的风速是整数编报,不存在 0.5 m/s,所以 0 和 1 之间没有灰色地带。

顺手把上一篇埋的一颗雷在地图上排掉:取整是向下的。19.999 m/s 画 4 长划 + 1 短划(编码 18),20.001 m/s 才出第一面旗(编码 20)------差 0.002 m/s,跨过一面旗。你的数据是 19.9 还是 20.1,杆形完全不同。

报文带阵风时(比如上一篇那条 32009G14MPS),本库的画法是两根杆并排 :平均风一根(2 长划)、阵风一根(3 长 1 短)。并排的前提是两者同方向------报文只给一个风向,阵风没有独立的方向组,这是一种近似假设;标准站点模型其实不画阵风,要标就是把数字注在杆旁。addWindBarbGlyphs 调两次、同一坐标,天然并列。(什么时候报文才带 G:瞬时风与平均风的差超过编报门槛才编,全年只有 1.6% 的时次------上一篇算过这笔账。)

二、把杆摆正:32×32 的盒子、一次 rotate、恒定像素

addWindBarbGlyphs 画的杆,长什么样、为什么方向不会歪?这节回答两个问题:库画的杆为什么不歪;以及哪天你要自绘(或者排查一根歪了的杆),契约在哪。

库的几何函数 windBarbPath 返回的不是成品图,是SVG 路径数据:

js 复制代码
import { windBarbPath } from "@metweave/leaflet";

  


const { stroke, fill } = windBarbPath(2, { unit: "m/s" });

// stroke = "M16 16 L16 2 M16 2 L20 6" ← 杆身(站心→盒顶)+ 1 根短划(羽在东侧)

// fill = "" ← 2 m/s 没有旗

  


windBarbPath(20, { unit: "m/s" }).fill;

// → "M16 2 L16 10 L24 6 Z" ← 三角旗的闭合填充(旗尖在东侧)

契约一:图元盒 32×32,站点在盒心 (16,16)。 上面那串坐标全部落在这个盒子里------杆身从 (16,16) 竖到 (16,2),羽从杆梢往站心方向排。自绘时你的 SVG viewBox 就用 0 0 32 32,按 32×32 像素一比一呈现(放大渲染杆就跟着变长);挂在站点上时锚盒心不锚盒角(Leaflet 侧就是 iconAnchor: [16, 16])。羽的坐标永远问 windBarbPath 要------旗/长划/短划怎么排是它的事,别自己算。

契约二:windBarbPath 出的杆恒指北,方向靠旋转。 想要 330° 的杆,自己套一层 transform:

js 复制代码
const glyph = (direction, { stroke, fill }) =>

`<g transform="rotate(${direction} 16 16)">

<path d="${stroke}" fill="none" stroke="#2c3e50" stroke-width="1.4"/>

${fill ? `<path d="${fill}" fill="#2c3e50"/>` : ""}

</g>`;

  


const svg = `<svg viewBox="0 0 32 32">${glyph(330, windBarbPath(2, { unit: "m/s" }))}</svg>`;

rotate 的轴心必须是盒心 (16,16)------转的不是杆,是整个图元盒。羽画在杆的东侧(未旋转视角 x>16)、随盒一起转------转完恰好落在北半球惯例的右手边,不用你处理。

但羽在哪一侧,这里要放一篇勘误 ------上一篇说错了,库的旧版也跟着错了。上一篇讲「北半球羽在左手边、白捡」------查证之后这句是错的:北半球的业务惯例是羽画在顺来向看的右手边 (气象来历:北半球低压在风的行进路径左侧,羽因此指向低压------matplotlib 的风羽函数、NWS 的官方示例图都是这个口径,南半球才翻到另一侧)。库 0.3.2 及更早把羽画在了左侧=南半球侧;0.3.3 起已修正------上面的 path 输出就是修正后的形态:短划梢在 (20,6)、旗尖在 (24,6),全在杆的东侧。

为什么敢直接 rotate(风向)?因为 Web 墨卡托在小尺度上方向不歪------北就是上、东就是右(严格的投影学上它有约 0.3° 的角度变形,在这个尺寸的符号上不可见)。注意这条只对过这一点的方向成立;「北京看向上海」那种到远处的方位角是另一回事。哪天你换了不保角的投影,或者把杆画进「跟随地图一起旋转」的图层,杆就开始集体歪斜。

杆恒定像素 是另一个独立的设计:杆是屏幕上的符号 ,不是地面上的矢量------不随缩放变大变小,长度和风速无关,永远 14 像素左右。缩放时它原地重定位,尺寸不变形。

契约三:自绘算角度时的两个坑。 屏幕坐标系的 y 轴向下 ,数学坐标系向上------符号会反。而且气象风向是来向 、u/v 分量指去向 ------还有 180° 的翻转。两个坑叠在一起,就是上一篇还原 u/v 那个 270° 公式的来历。顺带说破一个「秘密」:SVG 的 rotate 正角为什么恰好是顺时针(罗盘向)?就是因为 y 轴向下------契约二的「直接转」和契约三的「符号反」,是同一个坐标系事实的两面。方向判断永远拿「杆梢测试」验收:画出来,杆梢该指着来向。

三、点的三种表达:杆、箭头、数值牌

一个点的风,库里给三种表达,各管一段:

表达 函数 给你什么 丢什么
风羽杆 addWindBarbGlyphs 方向 + 风速,一套编码 精确读数(要数划)
箭头 windArrowPath 只有方向,一眼可读 风速完全没有
数值牌 renderWindChip 精确数字 全场扫一眼的模式感(牌要逐个点开看)

箭头是纯方向件,自带朝向 ------和 windBarbPath 的恒指北不同,它的方向在函数里已经转好,拿到直接摆。这个不对称是刻意的:杆要数羽,整盒一次旋转更省事;箭头只表方向,转好给你。

js 复制代码
import { windArrowPath } from "@metweave/leaflet";

  


windArrowPath(330);

// → { stroke: "M16 16 L12.425 9.808", fill: "M10.5 6.474 L14.092 8.845 L10.758 10.77 Z" }

指向的口径要说清楚,因为惯例和库的缺省不一致 :水平面上的风矢量箭头,主流惯例指去向 (顺着流动看------探空图上的风杆也是指向去向的,这和地面填图杆正好相反);库的缺省跟风羽杆同向、指来向 ,这是本库自选的口径,{ pointing: "to" } 切到去向。同一面图上不要混用两种指向------半屏指来向、半屏指去向,读者第一反应是「这库有 bug」。

数值牌是三行小数字块,直接吃 parse 的结果,三态各有各的文案:

js 复制代码
import { parse } from "@metweave/parser";

import { renderWindChip } from "@metweave/render";

  


renderWindChip(parse("ZUUU 241300Z 33002MPS 8000 FEW020 SCT040 19/16 Q1014 NOSIG")).outerHTML;

// → <div class="mw-wind-chip" data-state="value">...风向 330°...风速 2 m/s...</div>

  


renderWindChip(parse("ZBAA 241300Z 00000MPS 6000 -RA OVC040 13/11 Q1023 NOSIG")).outerHTML;

// → <div class="mw-wind-chip" data-state="calm">静风(00000)</div>

静风的牌写「静风(00000)」------第一节数杆为什么静风没有杆?牌替你把话说了。缺测的牌写「风组缺测」,同样不是空白。

什么时候用哪个:地图上密密麻麻的站点,用杆 (远看有方向有量级);只要方向模式、不要读者去数划,用箭头 ;点开单个站点看精确值,用牌。三者经常组合:杆在图上,牌在弹窗里。

顺带交代这根杆的身世,三句讲完:早期天气图用风箭的羽枝表示风------羽(feather)本来就是箭羽的隐喻,早期羽数曾和蒲福风级(1805 年英国海军按海面状况定出的风力等级)对应;今天「旗 50/长划 10/短划 5 节」的刻度是二十世纪定型的国际站点模型,1941 年以来基本没变。它活到数字地图上,因为「数」仍然是人眼最抗噪的读法------填图员在几秒内就能读出方向和量级。

四、多站批量:从一根到一片

一个机场一根杆是玩具,几十个机场同时上图才是活儿。数据还是 IEM 的归档(上一篇玫瑰的同一来源),多站一单取回------四个站,00--13Z 十四个整点各一条、共 56 条报文:

bash 复制代码
curl -o iem.csv "https://mesonet.agron.iastate.edu/cgi-bin/request/asos.py?station=ZUUU,ZBAA,ZGGG,ZSSS&data=all&year1=2026&month1=10&day1=24&year2=2026&month2=10&day2=24&tz=UTC&format=comma&latlon=yes&missing=M&trace=T&report_type=3"

CSV 长这样(列很多,挑要紧的看------注意经度在纬度前面 ,报文原文在 metar 列):

yaml 复制代码
station,valid,lon,lat,...,metar

ZUUU,2026-10-24 13:00,104.02,30.67,...,ZUUU 241300Z 33002MPS 8000 FEW020 SCT040 19/16 Q1014 NOSIG

批量上图就是把「解析→攒杆→一次交给地图」串起来。列按列名取(别按位置数------IEM 的列序跟着请求参数走,经纬谁前谁后,数位置必然踩坑):

js 复制代码
import { parse } from "@metweave/parser";

import { addWindBarbGlyphs } from "@metweave/leaflet";

import { readFileSync } from "node:fs";

  


const lines = readFileSync("./iem.csv", "utf8").split("\n").filter((l) => l.trim() && !l.startsWith("#"));

const head = lines[0].split(",");

const iLat = head.indexOf("lat"), iLon = head.indexOf("lon"),

iRaw = head.indexOf("metar"); // 报文原文列

  


const barbs = [];

for (const row of lines.slice(1)) {

const col = row.split(",");

if (!col[iRaw]?.includes(" 241300Z ")) continue; // 13Z 那批

const w = parse(col[iRaw]).wind; // 判别联合:可能没有风组

if (w?.value && w.value.direction !== null && w.value.speed.value >= 1)

barbs.push({ lat: +col[iLat], lon: +col[iLon], // VRB 方向是 null、静风速度是 0,都在这拦

direction: w.value.direction, speed: w.value.speed.value });

}

  


await addWindBarbGlyphs(map, barbs); // 一次交 3 根杆

判据里那两个条件就是第一节的落地:静风速度是 0(>= 1 拦住),VRB 的 direction 是 null(!== null 拦住)。上图入口不替你判这些------速度 0 传进去会画一根光杆(和 1 m/s 同形态),null 方向会画假杆。批量的最后一道闸在你手里,这是设计出来的决定权:数据侧最清楚哪些值该上图。

时间口径,批量时必须交代两层 :一是例行 METAR 按规范整点发布(大站半点加发)------四站同刻是规范的常态,不是运气;URL 里 report_type=3 就是「只取例行报」的过滤器,把它去掉,非例行的 SPECI 会混进「同一时刻」。二是风组本身是观测前 10 分钟的平均(美国站是 2 分钟)------「同一时刻」从来是一个窗,图上标注「13Z ±30 min」才诚实。报文、格点、探空------各类数据源的发布节律都不同,同屏时窗口径是第一件要写进图例的事。

性能账,从几十根说到几百根。 杆在库里是 DOM 标记------每根杆一个 marker 节点,里面再装 SVG 图元,实际是四到六个页面节点。几十根无感;当代硬件上几百根也还顺;上千根,缩放拖动时逐个更新位置的每帧开销就开始咬人。这篇用的中国区域格点场,默认密度是 529 根(下一节),还在舒适区;但如果你把抽稀步长砍到 2 个格点,全域会算出 13,141 根 ------若没有防御,浏览器会长时间无响应。库的两道防御:只画当前视口内的杆 (视口外留 15% 缓冲带),视口内超过 2500 根自动加大步长降密。后者在默认档位下几乎不会触发(正常缩放到不了这个量),它兜的是你自砍步长的场景。你不需要写一行代码,但要知道它们在------哪天杆图「缩放后杆变少了」,按三步排查:先对照下一节的档位表(档位切换本来就会变数)→ 再看是不是触发了降密 → 最后才怀疑 bug。

五、一张场:两层装配,谁吃什么

站点杆再密,也只是几十个点的采样。要看整片的风,得用格点场------上一篇你已经会读「色斑 + 风羽」的场图,这节讲装配。

数据从哪来:metweave 仓库(github.com/yaojaro/metweave )的 examples/data/grid/ 里有现成的 windu_cn.mwgrid(东西分量)和 windv_cn.mwgrid(南北分量)------把这两个文件放进你的静态目录(vite 的 public/ 或任意静态服务),fetch("/grid/...") 就能取;要更新的数据,仓库的 gen:grid 工具一条命令从 NOAA GFS 拉(GFS:美国全球预报系统,0.25° 格距的公开发布场)。注意文件里是两个分量场,不是「风速 + 风向」------上一篇那个「取数给风速风向、内核要 u/v」的还原问题,在这条数据路径上不存在:文件里存的本来就是 u/v。

装配是一句口径:色斑吃合成场,杆层吃分量原场。

js 复制代码
import { addGridLayer, addWindBarbLayer } from "@metweave/leaflet";

import { parseGrid, windSpeedGrid, windProfile } from "@metweave/grid";

  


const [uRes, vRes] = await Promise.all([

fetch("/grid/windu_cn.mwgrid"), fetch("/grid/windv_cn.mwgrid"),

]);

const u = parseGrid(await uRes.arrayBuffer());

const v = parseGrid(await vRes.arrayBuffer());

  


await addGridLayer(map, windSpeedGrid(u, v), windProfile); // 色斑:吃 √(u²+v²) 合成场

await addWindBarbLayer(map, u, v, windProfile); // 风羽:吃分量原场

windProfile 是库给 10m 风的预设配置 ------色标、档位、杆的密度与出杆阈值全在这一个对象里,两个图层共用它,所以色斑和杆永远口径一致。两层零交涉 :色斑要标量(先 windSpeedGrid 合成再交给它),杆要方向和大小(直接吃分量)。你可以只叠其一------杆层自己就带着量级(羽可数),色斑自己就带着分布。

层序 用 Leaflet 的 pane z 序控制:色斑 350 → 等值线 360 → 风羽 365,杆永远压在色斑和等值线上------杆被色斑盖住,这张图最值钱的方向信息就没了。但要知道这三个数在 Leaflet 默认谱系里的位置:底图瓦片 200 之上、默认矢量叠加层 400 之下------你随手 L.polyline() 画的一条线会盖住全部风羽。要压过默认层,自己开 pane。

这份数据是 280 列 × 208 行、0.25° 格距(西界 70°E、北界 55°N 起,向东南铺满中国区域;本篇用的是 f00 分析场------起报时刻的场,不是预报)。实测当前这个时次:合成风速 0.02--41.70 m/s(最大值在台湾以东海域的强风区),静风区(<1 m/s)占 9.7% 的格点------这些格点不画杆。留白是阈值规则的直接结果,也是好事:静风区真画杆,会画出一大片光杆海(9.7% × 58,240 ≈ 五千六百个格点),比留白难读得多。超过 32 m/s 的格点落最深一档兜底------台风极端同色,这是色标固定域的刻意取舍。

还有一条同屏口径必须声明:站点杆是观测(仪器实测),格点场是模式值(GFS 的格点平均)------两者物理意义不同,机场局地风和它所在格点的值可能差出不少。同屏不等于同源,图的读者应该知道这件事。

色斑的色标(暖色渐进、0,32 m/s 等距 8 档、档界 0/4/8/.../32)在上一篇和本系列讲格点填色的一篇里都拆过,这里不重讲。

六、缩放档密度:134、529、2,105、5,933

场图的杆是 step 抽稀的:每几个格点取一根。固定密度在纸面上够用,在地图上两头不是------拿真实的 529 根走一遍:缩到全国一屏(zoom 3 上下),杆间距只有十几像素,糊成一片;放大到 zoom 4,同样 529 根、间距 32 像素,一个舒服的默认密度。同一密度档,视角不同,可读性不同------这就是要按缩放档给密度的原因。

解法是按缩放档给密度(LOD,level of detail)。库的预设配置里是四级阶梯,zoom 是 Leaflet 的缩放级别,step 是格点数:

zoom step 全域杆数 这个档在看什么
0 20 134 世界全图(中国区域只有几十像素宽)------极疏示意,5° 一根
4 10 529 全国到区域:2.5° 一根,舒服的默认档(不传参数就是它)
6 5 2,105 省级:1.25° 一根,梯度区也看得清
8 3 5,933 局地:0.75° 一根,配视口裁剪才扛得住

四档的杆数都是真实中国区域数据实测的(介于两档之间的 zoom,取不超过它的最大锚点档;缩放跨档时层自动重绘,同档内不重建)。三个使用要点:

第一,默认档是 zoom 4 的 529 根------不传任何缩放参数就是它。「默认密度先保证可读」是刻意的。

第二,最密档停在 step 3,不是技术极限,是可读性极限。 step 2 会算出 13,141 根(第四节那个数字),step 3 是 5,933 根------砍一半还多。符号图有物理上限:杆密到互相压叠,「数划读风」就废了,信息量不升反降。所以预设层把最密档摁在 3、图层只画视口内的杆、视口内超量自动降密------三道闸一起把「放大后的杆数」摁在浏览器和人眼都舒服的区间。

第三,放大后杆数变少不一定是 bug。 视口在变小:zoom 8 时你只看一个省,全域 5,933 根里落在你视口里的可能只有一两百根(视口大小取决于你的屏幕)。看密度,别看总数------同屏单位面积的杆数是不是变密了,才是 LOD 在干活的证据。

七、等值线:能画,但默认不画

风速场还有第三种叠加:等值线 ------把风速相同的点连成线。库支持它,配置里也接了线,但默认是关的。

为什么?先说清楚等风速线不是不存在------高空急流分析、海面大风图都常规画它。但那些图上没有几百根杆同时在表达风速。地面风场这张图上,同一层已经有杆在表达 ,等值线是第三种声音;而且实测真的会糊:4 m/s 间隔的等值线在风速梯度区会生出过密细线,和风羽杆、色斑争视觉------整幅图读成噪点。所以间隔放宽到 8 m/s(落在色斑档界的偶数档 0/8/16/.../32 上,两套读数不打架),而且杆要素叠等值线时关掉数值标注(杆已经够密了,线再标数就是雪上加霜)。

「支持」和「默认开」是两件事。库把等值线做出来、把开关交给你,默认替你守住可读性------你勾上它,是在「我知道这片梯度平缓、线不会糊」的判断之后。

八、图例:刻度的最后一关

最后一关:读你图的人,怎么知道杆上那道划是 4 m/s 还是 10 kt?

答案是图例------但图例不是「贴一张说明」,它是刻度声明 ,而且必须和杆同源 。先补一句两套刻度的来历(上一篇只讲了差异没讲出处):它们是两套独立的传统,不是同一刻度换单位------20/4/2 是中国气象填图的国内口径,50/10/5 是国际通用口径;20 m/s 折算是 39 kt,不是 50,所以切换从来是「换刻度」而不是「换算」。库的图例组件自己不含刻度数字,数字由你传入(grid 包给了现成的、与编码同源的刻度表):

js 复制代码
import { WIND_LEGEND_UNITS } from "@metweave/grid";

import { renderWindLegend } from "@metweave/render";

  


document.body.append(renderWindLegend({ units: WIND_LEGEND_UNITS, unit: "m/s" }));

// → 「风羽图例」+ 三档刻度(三角旗 20 m/s/长划 4/短划 2)+ m/s·kt 切换按钮

为什么刻度要「传进来」而不是组件内置?因为同一份刻度同时被三处消费:编码函数按它算旗划、图层按它出杆形、图例按它标数字。三处只要有一处自己另写了一份数字,日后加第三种刻度时必有一处漏改------图例说 20、杆画的是 50,图就「合法地说谎」。

切换刻度时的联动也要成套。点图例切到 kt,三件事必须同时变:图例刻度 20/4/2 变 50/10/5、杆形按 kt 重新编码、读数给出折算值。图例组件给回调(onUnitChange),你据它切换;地图层的做法是「移除旧层、按新刻度重建」(map.removeLayer(层对象)------就是 add 时返回的那个)------视口内几百个标记重建一次,有一瞬间的闪,目前还没有「保留标记只换杆形」的更便宜做法。注意上一篇的警告在这里原样适用:unit 是量纲声明不是换算开关 ,m/s 的数值配 { unit: "kt" },杆会被画成一半的风(14 m/s 配 kt 刻度画出来是 1 长划------编码 10,量级直接砍半),画面完全合法、零告警。

九、换上你的机场

收个尾。这篇走完的路,从一条报文到一张场:

  1. 搭骨架:三行建一张最简地图(OSM 底图、无需 key);

  2. 取报文 :IEM 归档多站一单(换站只改 URL 里的 station=------把你机场的 ICAO 代码填进去);

  3. 出编码 :parse → windBarbOf,静风、VRB、缺测在数据层就分了型;

  4. 上地图 :单站 addWindBarbGlyphs,几十站攒一批一次上图(静风与 VRB 自己拦);

  5. 上场 :addGridLayer(合成场)+ addWindBarbLayer(分量场),层序 350/360/365,杆压最上;

  6. 密度:缩放档自动抽稀,最密全域 5,933 根,视口裁剪兜底;

  7. 图例:刻度与编码同源,切刻度三处联动。

第五节的数据文件、本篇全部代码和配套取数脚本(IEM 一条命令)在 github.com/yaojaro/metweave 的 code/ 目录。你拉到的报文和格点都是当下最新值------数值会变,代码不会。

最后把边界说清:这篇讲的是风怎么画上地图,是数据可视化的教程------杆画在站上,就止于站上;不做任何运行判断,也不碰跑道和侧风(报文的风向是相对真北的方位,跑道编号是磁北方位,两者本就不是一回事)。

下一篇见。上一篇的四种画法回答了「此刻、一片、一年、垂直」------可风是流动 的这件事,它们一个都回答不了:色斑告诉你多大,告诉不了你去哪。流线、粒子、箭头场怎么选,是下一篇《风怎么流动》的事------那一篇起,画法搬进 canvas,正是绕开这一篇 DOM 天花板的路。


署名:YaoJaro · 独立开发者

数据来源:四站 METAR(IEM ASOS 归档:公有领域,无准确性担保;正文取数时次 2026-10-24 13Z)|10m 风格点场(NOAA GFS 起、metweave gen:grid 裁中国区域 0.25°)|风渲染能力来自开源库 metweave(MIT,作者本人维护;npm @metweave/parser / @metweave/grid / @metweave/leaflet / @metweave/render,0.3.3;仓库带 1100+ 测试用例、API 快照逐版留痕)



下一篇《风怎么流动》------流线、粒子、箭头场,画法搬进 canvas。如果这一篇让你想把杆画上自己机场的地图,欢迎在掘金关注,系列接下来一篇一个台阶。

相关推荐
2601_949575861 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
Java后端的Ai之路1 小时前
【AIAgent项目-AI生成PPT-09】灵活排版
前端·vue.js·人工智能·python·powerpoint
MasonYyp1 小时前
安装npm依赖仓库Verdaccio
前端·npm·node.js
Antruly1 小时前
C++ Web 开发选型漫谈:从 Drogon 到 libuvcpp + uvp,八个项目的定位、兼容性与务实排名
开发语言·前端·c++
红红谈说1 小时前
同一套产品两个技术栈,双版本前端工程差异怎么收敛?
前端·uni-app·vue3·前后端分离·双版本维护·接口约定
不吃鱼的羊2 小时前
如何建立高自尊
java·服务器·前端
计算机魔术师2 小时前
编程智能体卡在"需要登录"这步,六种接法代价差得很远
前端
计算机魔术师2 小时前
出题人自己算错了,AI成绩从88%直接跳到98%
前端
Amos_Web2 小时前
Rspack 源码解析(三十一):HotModuleReplacementPlugin 如何用 hash 比对生成增量更新
前端