基于 WRF 模式输出的交互式台风路径可视化项目。
地图合规:本项目底图使用 天地图(国家地理信息公共服务平台,自带审图号
GS(2026)4921号),并按《地图审核管理规定》第六条落入不需送审豁免。 页面不自行绘制国界/省界等政治边界,也不使用任何已知表示有误的底图源。 详见 合规整改记录.md 与 天地图操作方法.md。
本项目用于可视化台风 "桦加沙" 的路径演化过程。通过解析 WRF 模式 d02 嵌套域的输出数据,提取海洋区域最低气压作为台风中心位置,并使用 Leaflet.js 生成交互式地图页面。
线上服务部署于 43.154.210.202:8899,由 tile_proxy.py 常驻进程提供静态文件与瓦片代理。
页面为历史个例回放(非实时预报):所展示的是 2025 年 9 月的历史过程回放, 不是实时气象预报或灾害性天气警报,避免触及《气象法》第二十二条的 「公众气象预报统一发布制度」。
- 交互式地图:基于 Leaflet.js + 天地图,支持缩放与拖拽
- 台风路径:红色虚线显示完整生命周期路径
- 时间滑块:拖动查看任意时刻的台风位置与强度
- 气流漩涡:台风中心三层同心圆环,直观展示气流结构
- 强度可视化:台风中心点颜色/大小随气压变化(气压越低越强)
- 风场叠加:逐时风羽(msgpack + Canvas),可开关
- 峰图图表:气压/风速历史曲线
- 港口风险:港口危险等级可视化
- 测距/关注区:距离测量与自定义关注区域
- 多台风切换:顶部台风名切换,路径/风场/警戒线/峰图联动
- 底图双源:天地图(行政地图,自带审图号)+ Esri 卫星影像
- 手机扫码进入:桌面端右上角「📱 手机扫码」弹出二维码,自动编码当前访问地址(可手动改址 / 复制链接),手机相机扫码即可打开同一站点(离线可用,二维码为自研内联编码器,无外部依赖)
- 手机端专用布局:≤768px 自动切换为底部 Dock(时间轴 + 播放 + 5 个标签)+ 抽屉式面板(图层 / 工具 / 设置 / 数据 / 图例),桌面端布局与交互完全不受影响
已移除:国界/省界自绘图层(原 Natural Earth 国界沿麦克马洪线、台湾按独立国家 要素描边、无十段线,违反《公开地图内容表示规范》)、OSM 行政底图与 OpenTopoMap 地形底图(同源问题)、高德备选底图(直连瓦片违反其服务协议 3.5 且无法依法标注审图号)。详见 合规整改记录.md。
手机端能力以「模块化源码 + 构建内联」的方式维护,避免把 4000+ 行的单文件页面越改越乱:
| 文件 | 作用 |
|---|---|
mobile/mobile.css |
≤768px 专用布局(底部 Dock / Sheet / 弹层 / 安全区 / 触控目标 / 文字自适应缩放) |
mobile/mobile-ui.js |
手机端交互层(构建 Dock、搬移节点、Sheet 开合、手势让位、Dock 高度同步、全屏/视口重算) |
mobile/i18n.js |
中英双语引擎(window.I18N:精确查表 + 长串优先短语替换 + DOM 遍历 + 原文还原) |
mobile/i18n-dict.js |
中→英词表(唯一权威词表,383 条;由 .dev/gen-i18n-dict.mjs 生成 + 该脚本内 ATTR_EXTRA 的手工条目) |
mobile/qr-encoder.js |
零依赖二维码编码器(ISO/IEC 18004,byte mode UTF-8,经典脚本,window.QRCode) |
mobile/qr-popup.js |
「手机扫码」弹窗(地址编辑 / 复制链接 / 局域网提示,仅桌面端初始化) |
mobile/build.py |
把上面这些文件内联进 index.html(幂等,可 --check 校验是否同步) |
mobile/mobile-ui.js 内 injectMobileOverrides() |
组合层微调(手机端隐藏扫码入口、播放键宽度等实测补丁) |
mobile/SPEC.md |
接口契约与验收标准(DOM 契约、断点门控、各产物职责) |
mobile/test-qr.mjs |
二维码位级自检(860 断言:往返 / 全版本容量 / 8 掩码) |
mobile/selftest-css.mjs |
CSS 自检(30 项:括号配平、无外链、门控、选择器矩阵、Dock 高度预算 + 需求回归守卫) |
mobile/test-i18n.mjs |
双语层自检(27 项:词表完整性、漏译、标签配平 + 引擎/集成回归守卫) |
改手机端请改
mobile/下的源码,然后运行python mobile/build.py重新内联; 直接改index.html的内联块会在下次构建时被覆盖。python mobile/build.py --check可检测是否已同步。
⚠️ mobile/build.py是整块替换、不做合并,所以只改index.html的内嵌 i18n 块是无效的:mobile/i18n-dict.js才是权威词表。若它落后于页面文案,一次构建就会悄悄回退已有改动 ——本项目真踩过:合规整改后的「【历史回放 · 非实时预报】」曾被词表里的旧「【台风快讯】」覆盖回退。 改完词表后跑python mobile/build.py,再跑node .dev/check-dict.mjs核对两边键集一致。词表条目的三个来源(改之前先看清该改哪里):
.dev/i18n-en.json—— 扫描器主词表(大部分条目).dev/gen-i18n-dict.mjs里的ATTR_EXTRA—— 手工条目都加这里(否则重跑生成器会丢)mobile/i18n-dict.js—— 生成物,不要手改
验证命令:
node mobile/test-qr.mjs # 二维码编码器自检(ALL GREEN 才可发布)
node mobile/selftest-css.mjs # 移动端 CSS 自检(含「白框」等需求回归守卫)
node mobile/test-i18n.mjs # 中英双语层自检(词表 + 引擎 + index.html 集成契约)
python mobile/build.py --check # index.html 是否与 mobile/ 源码一致| # | 现象 | 根因 | 处理 |
|---|---|---|---|
| 1 | 全屏后操作有 bug | ① 进全屏不关 Sheet,设置面板继续盖住大半地图;② Dock 被 display:none 但 --dock-h 仍停在 161px,图例/测距条/关注区小窗全部悬在半空;③ 视口变化后 Leaflet 未 invalidateSize,瓦片画在旧范围、点击坐标整体偏移 |
进全屏自动收 Sheet;fullscreenchange / visualViewport.resize 重算 --dock-h(含 :has(body.clean-mode) 零时差兜底)并派发 resize;退出键避安全区、44px+ |
| 2 | 拖动地图时底部闪白框 | .md-interacting 给 Dock 整体加 opacity:.55 + translateY(12px):白色 Dock 半透明 → 底图与地图注记透出来;同时收起态的 #left-panel 只靠 translateY(105%) 挡不住,顶边留在视口内透出表头 |
Dock 背景改纯 #fff 且不位移,只淡化子元素;#left-panel 收起加 visibility:hidden(延迟切换,不吃动画) |
| 3 | 文字不随屏幕缩放 | 根字号恒为 16px | html.mobile-ui { font-size: clamp(15px, calc(13.9px + .53vw), 18px) } → 320px/15.6、390px/16.0、430px/16.2、768px/18.0;桌面端双门控(媒体查询 + .mobile-ui 类)不生效 |
| 4 | 缺英文版与语言切换 | 无 i18n 层 | 新增 mobile/i18n.js + 364 条词表;#btn-lang 在 #top-controls(桌面端脱离文档流,不推挤既有工具栏);覆盖静态 DOM、title/data-tip/aria-label 属性、动态拼接句、CSS 生成内容(#legend::before);选择持久化 + html[lang] 同步;切回中文逐节点还原 |
| 5 | 手机界面拥挤 | Dock 19.1% 屏高、顶部快讯条与湾区标签重叠、快讯滚动字幕大半时间在屏外、Dock 时钟永远显示 -- |
Dock 161→151px;播放键 40→43px 宽、标签字号 10.6→11.5px;快讯条 46→34px 且改静态省略、与标签错开;Dock 时钟接上真实读数;里程碑标签按真实宽度排版(英文用短标签),不再糊成一团 |
桌面端(≥769px)保持像素级不变:
node .dev/verify-desktop.mjs对比升级前后的截图, 差异仅限新增的#btn-lang按钮矩形(三档分辨率 22/22 全绿)。 手机端验收:node .dev/verify-mobile.mjs(42 项)。
这两处不是样式问题,而是功能在触摸设备上完全不可用,必须在主逻辑里修:
- 关注区矩形绘制:原实现只监听
map.on('mousedown'/'mousemove'/'mouseup')。 Leaflet 1.9 的 map 事件层不转发pointer*事件(容器上收得到,但内部只订阅mousedown/mousemove/mouseup与touchstart/touchmove/touchend),触摸设备也不会产生mousedown—— 结果手机端「按住拖拽画矩形」毫无反应。 现改为在容器上直接绑定原生事件:window.PointerEvent可用时走pointerdown/move/up/cancel, 否则退化为「鼠标 + 触摸」双通道;坐标由focusRectLatLng()从原生事件换算。 - 关注区多边形绘制:桌面靠
dblclick闭合,而触摸端双击会被识别为缩放手势 (且绘制期间doubleClickZoom已禁用),永远不会触发dblclick—— 多边形无法闭合。 现新增移动端专用浮动按钮#focus-done-btn(「✓ 完成 / ✕ 取消」,触控高 44px), 仅在html.mobile-ui下显示;桌面端仍用双击,行为不变。
回归方式(本地):node .dev/touch-test.mjs(真触摸手势 21 项)与 node .dev/mouse-test.mjs
(桌面鼠标 8 项)——两者都必须全绿。
index.html 的「底图配置」代码块(搜 TDT_KEY)是底图的唯一配置入口。改动前请注意:
| 约束 | 说明 |
|---|---|
| 只允许两种底图 | 天地图(行政)与 Esri 卫星影像(纯影像) |
| 不要恢复 OSM / OpenTopoMap | 其瓦片把藏南标为 Arunachal Pradesh、国界沿麦克马洪线、台湾按独立国家要素表示、无十段线,违反《公开地图内容表示规范》五(三)/七(一)/八(一)/十 |
| 不要恢复高德直连瓦片 | 违反《高德地图开放平台服务协议》3.5(禁止技术手段抓取),且官网不公开固定审图号、无法按《地图审核管理规定》第 27 条标注 |
| 不要恢复自绘国界/省界层 | 豁免前提是「未对国界、行政区域界线或范围进行编辑调整」(《地图审核管理规定》第十条),一旦叠加即豁免失效、须重新送审 |
buildLandMask() 必须保留 |
风场渲染与航线避让依赖它算陆地掩膜;它只产出内部栅格、不往地图上画线 |
| 审图号必须标注 | 已固定在地图左下角角标(第 27 条要求左下角)与 Leaflet 署名中;审图号变更时更新 TDT_APPROVAL |
坐标系:页面数据一律 WGS-84,仅在交给 Leaflet 绘制时经 patchDisplayCRS()
转换为 GCJ-02(天地图瓦片坐标系)。不要修改数据本身的坐标系,
也不要给 L.latLng 加转换(会二次偏移并破坏风场画布变换)。详见 合规整改记录.md 第八节。
服务器目录结构(2026-10-07 校核):
/home/haike/tile_proxy.py # 常驻 HTTP 服务:静态文件 + 瓦片代理缓存(:8899)
# ← 在 web 根【之外】,chmod 600,含明文天地图 Key
/home/haike/test_web/ # ← 站点根目录:只放可以公开的文件
├── index.html # 交互式地图页面(前端核心,唯一入口)
├── wind_field/ # 逐时风场 msgpack 风羽数据(264 个时次,wind_field_XXXX.bin)
├── tiles/ # 瓦片磁盘缓存
│ ├── tdt/<Key哈希>/... # 天地图:服务端把 vec_w 底图 + cva_w 注记合成一张 PNG
│ └── satellite/... # Esri 卫星影像
├── data/ # 陆地掩膜数据(countries-50m.json)等
├── vendor/ # Leaflet / msgpack 等随页面分发的第三方文件
├── TY_correction_data/ # 各台风的订正场 WRF 输出
└── wind_wrfout_d02_* # WRF 原始输出(约 253 MB)
⚠️ 脚本实体不在站点根目录内(2026-10-07 起因安全外移)。web 根里只放可以公开的文件: 服务端已启用静态文件黑名单(DENY_RE),/tile_proxy.py、*.bak、*.log、*.py、*.pyc等一律 404;实测页面资产不受影响(/、/tiles/…、/data/countries-50m.json、/wind_field/*.bin仍 200)。取备份 / 脚本请走 scp / sftp(SSH 不受该黑名单约束)。 历史教训:在 22:17 重启之前,旧进程没有黑名单,/data/*.py与 web 根里那份index.html.bak-*一度都是 200(可匿名下载);日志里也能看到有人在按精确文件名 探测/tile_proxy.py.bak-…与 7 个index.html.bak-*(当时即 404,未泄露)。2026-10-07 又修掉一个绕过:黑名单最初拿
urlparse().path的 basename 去匹配, 而urlparse()不做 percent-decode、后面的translate_path()却会 —— 于是/data/convert_simplify%2epy直接 200 返回源码全文(%2E大小写都行,index.html%2ebak同理),等于把上面那条黑名单整个架空。现已改为先unquote再匹配, 并且覆写list_directory()让/data/、/tiles/、/vendor/、/wind_field/一律 404(在此之前它们会匿名列出目录内容,等于把「站内有哪些 .py/.bak」的清单 递给攻击者)。回归:.dev/check-tileproxy-deny.py(本地)+.dev/verify-live-hardening.py(线上)。
服务管理:
# 启动(前台;脚本在 web 根之外,cwd 仍取站点目录)
# -u = 不缓冲;不加则 stdout/stderr 是块缓冲,访问日志要攒到 4-8KB 才落盘
cd /home/haike/test_web && python3 -u /home/haike/tile_proxy.py
# 后台常驻
cd /home/haike/test_web && nohup python3 -u /home/haike/tile_proxy.py > /tmp/tile_proxy.log 2>&1 &
# 重启(无 systemd 单元)
# ⚠ 下面这条单行命令里的 PID 提取实测取不到值(kill 只报 usage)→ 新进程会因
# "Address already in use" 退出,而端口看起来仍在监听(旧进程还在),
# 于是改动**静默没生效**。稳妥做法:先 `ss -lntp | grep ':8899'` 在本地读出 pid,
# 再 `ssh … "kill <pid>"`、等 2 秒、最后启动;启动后确认 PID 变了、
# `head -2 /tmp/tile_proxy.log` 里有 `tdt_key=set`(天地图 Key 没丢)。
kill $(ss -lntp | grep ":8899" | grep -oP "pid=\K[0-9]+" | head -1)
cd /home/haike/test_web && nohup python3 -u /home/haike/tile_proxy.py > /tmp/tile_proxy.log 2>&1 &
# 可选:本地验证时覆盖端口/缓存目录/Key
python3 tile_proxy.py 8898 /tmp/cache <你的天地图Key>- 监听端口
8899,同时服务静态文件与/tiles/<layer>/<z>/<x>/<y>.png瓦片 - 瓦片命中缓存直接返回,未命中回源并写盘缓存;只有
satellite与tdt两层, 其余路径(如/tiles/osm/...)一律 404,不会再取回不合规的表示 - 天地图 Key 只放在服务端(
/home/haike/tile_proxy.py的TDT_KEY,位置在 web 根之外、 权限600),不下发到浏览器 - 天地图回源必须用浏览器 UA(实测自定义 UA / 无 UA 会被其 WAF 403,
表现为瓦片全部 404、底图空白);
tiles/tdt/缓存目录名带 Key 哈希,换 Key 自动换缓存
⚠️ tile_proxy.py目前没有开机自启(无 systemd 单元、无 crontab)。 服务器重启后地图会挂,需手动重启。systemd 单元模板见 天地图操作方法.md 四点五节。
新增台风请先阅读
~/typhoon_workflow/README.md该文档包含完整的接口契约(track.json / wind_field bin / TYPHOON_DATA 注册表)、 路径提取与风场处理参数说明,以及一键流水线的全部用法。
接入新台风简要流程(详见 typhoon_workflow/README.md):
- 放数据:把该台风时段的 WRF d02 输出文件上传到服务器
- 注册:编辑
~/typhoon_workflow/typhoons.json,新增该台风配置项 - 计算:
python3 run_pipeline.py --name <台风名> --steps track,wind --out out - 接入页面:
python3 build_registry.py --config typhoons.json --patch-index ...+deploy.py部署 - 刷新页面:顶部点击台风名切换查看
独立模块,把「WRF 台风数据 → 台风路径 → 风场风羽 → 网页可视化」收敛为可重复执行的流水线:
| 文件 | 作用 |
|---|---|
typhoons.json |
台风注册表(唯一的输入配置入口) |
extract_track.py |
台风中心路径提取(文件模式 / WRF 自动模式) |
process_wind.py |
WRF 10m 风场 → 逐时 msgpack 风羽 bin |
build_registry.py |
生成/自动更新网页 TYPHOON_DATA 注册表 JS |
run_pipeline.py |
一键流水线:track → wind → registry |
deploy.py |
显式部署产物到服务器 |
verify_assets.py |
产物校验 |
selftest.py |
自测 |
README.md |
工作流完整文档(必读) |
- 磁盘:
tiles/瓦片缓存随浏览持续增长(天地图瓦片为 vec+cva 合成, 单张约 20~50 KB,比原 OSM 瓦片大),需定期关注 - 备份:
index.html/tile_proxy.py每次改版会保留*.bak-<时间戳>-<说明>备份,勿随意删除近期备份 - 服务:
tile_proxy.py为单进程常驻,崩溃或服务器重启后需手动重启 (无开机自启,见上文);重启后检查端口 8899。脚本实体在/home/haike/tile_proxy.py(web 根之外),重启命令见上文 - 防火墙:需放行 TCP 8899
- 换天地图 Key:改
/home/haike/tile_proxy.py的TDT_KEY后重启即可; 旧 Key 的tiles/tdt/<旧哈希>/缓存可删(换 Key 会自动换目录,不会串号)。 Key 不要复制到本地或写进受版本控制的文件
| 数据 | 来源 | 许可 / 说明 |
|---|---|---|
| 模式输出 | wind_wrfout_d02_2025-09-15_00:00:00(WRF V4.6.1) |
自有成果 |
| 台风路径提取 | 海洋区域(HGT <= 0)最低 PSFC |
自研方法 |
| 行政底图 | 天地图(国家地理信息公共服务平台) | 审图号 GS(2026)4921号(甲测资字 11110974);命中《地图审核管理规定》第六条不需送审豁免 |
| 卫星影像 | Esri World Imagery | Source: Esri, Vantor, Earthstar Geographics, and the GIS User Community(非商业用途许可,需署名) |
| 陆地掩膜 | Natural Earth 1:50m | 公有领域 |
- 时间范围:2025-09-15 00:00 UTC 至 2025-09-26 11:00 UTC
- 台风生命周期:2025-09-18 20:00 至 2025-09-25 20:00(北京时),共 7 天
- 本页不自行绘制国界、省界等政治边界,边界表示以天地图经审核批准的内容为准
根据中心气压(PSFC)划分:
| 气压范围 | 强度 |
|---|---|
| ≤ 950 hPa | 超强台风 |
| 950-970 hPa | 强台风 |
| 970-985 hPa | 台风 |
| 985-1000 hPa | 热带风暴 |
| > 1000 hPa | 低压 |
- 前端:HTML5 + CSS3 + JavaScript (ES6+)
- 地图库:Leaflet.js v1.9.4(BSD-2-Clause)
- 底图:天地图(行政,自带审图号)/ Esri 卫星影像
- 坐标:数据层 WGS-84;展示层经
patchDisplayCRS()转 GCJ-02(匹配天地图瓦片) - 陆地掩膜:Natural Earth 1:50m(公有领域)
- 数据提取:Python + netCDF4 + NumPy + msgpack
- 图例内已列全部开源许可:Leaflet(BSD-2) / topojson-client(ISC) / html2canvas(MIT) / @msgpack/msgpack(ISC) / 自研 QR 编码器(MIT)
-
克隆仓库:
git clone https://github.com/mango12q/SmartOceanProject.git
-
直接用浏览器打开
index.html即可查看台风路径可视化。 -
拖动底部时间滑块,查看不同时刻的台风位置与强度。
wind_wrfout_d02_*为原始 WRF 输出文件(约 241 MB),已加入.gitignore,需自行准备。- 天地图 Key 不在版本库里:
index.html的TDT_KEY保持为空, 线上由服务端tile_proxy.py持有并使用(瓦片经同源/tiles/tdt/下发,Key 不下发浏览器)。 临时验证可加 URL 参数?tk=你的Key走直连模式。 - 页面的"历史回放 · 非实时预报"声明位于滚动快讯条内,关掉快讯后页面上不再显示。 若将来接入实时数据,性质即变为公众气象预报发布,须另行取得发布主体授权。
- 陆地掩膜数据
data/countries-50m.json经同源加载,需随站点一起部署。 - 时间显示为北京时间(UTC+8)。
- 线上访问必须走瓦片代理(
:8899):天地图瓦片由服务端合成并缓存, 既避免 Key 外泄,也让比赛/会展现场断网时已缓存区域仍可显示。
| 脚本 | 作用 |
|---|---|
.dev/check-syntax.mjs |
校验 index.html 全部内联 <script> 语法(带引号状态机切标签,能正确处理跨行 importmap) |
.dev/check-dict.mjs |
校验内嵌 __I18N_DICT 是否为合法 JS 并能取到词条(防「对象里混注释」导致 i18n 静默失效) |
.dev/check-gcj.mjs |
校验 WGS-84→GCJ-02 转换,基准取自 eviltransform 官方测试夹具 |
.dev/jig-proxy.py |
本地测试台:静态文件优先本地、其余回源线上,浏览器只发同源请求 |
.dev/verify-page.mjs |
页面端到端验证(底图、审图号、Key 不泄露、改名生效等) |
推荐一次性跑完(全部 exit 0 才算通过):
node .dev/check-syntax.mjs index.html
node .dev/check-dict.mjs
node .dev/check-gcj.mjs index.html
node mobile/test-qr.mjs && node mobile/selftest-css.mjs && node mobile/test-i18n.mjs
python mobile/build.py --check
.dev/下另有大量历史界面回归脚本(verify-mobile.mjs/touch-test.mjs/page-check.mjs等)。这些都需要playwright-core与本机 Chrome, 先准备一个带该依赖的目录再运行(Node 的 ESM 解析看脚本所在目录)。
MIT
mango12q