在 Cesium 上画全球网格:我们怎么把 GeoSOT 接到瓦片调度上

低空项目里总有人问:这片空域怎么存、怎么查、怎么跟隔壁系统对上号。多边形能画,一到相交、套娃、跨系统传,就开始扯皮。

我们后来在自己的三维地图里做了全球网格这一层。相机往下滚,格子跟着变细;点一下能看到北斗位置码。先看成品,免得读了三千字还不知道长什么样。

国标二维一共 10 级。下面这张是第 7 级,赤道附近一格大约 7.7 米,不是第 8 级。白线是网格,字是国标码,比如 N50H675...

合肥附近 7 级全球网格

再拉远一点,格子还在,字会收掉------一屏上百个 N50H67xxxx 叠在一起谁也看不清:

7 级密网格,编号按视距隐藏

下面不讲组件 API 清单。主要想说两件事:格子到底在剖什么,以及 Cesium 里怎么画才不会把自己累死。


多边形为什么不够用

管制区用 GeoJSON 画一个面,存库、上图都顺。麻烦在后面。

无人机在不在区内,得做点面判断。两片空域叠不叠,得做多边形布尔,凹的、自交的都要防。按航线去扫全省空域,索引基本帮不上忙。把这串坐标丢给另一个系统,精度、点序、闭不闭合,每次都能吵一架。

根子就一句:经纬度擅长描述点,业务里几乎全是区域。区域需要能包含、能分级、能当地址传的东西。网格干的就是这件事。

一个格子一个码。子格的码以父格为前缀,所以:

复制代码
N50J47539
N50J47539B8

点在不在区里,比前缀就行。SQL 也能 LIKE 'N50J47539%'。这比每次上几何库便宜得多。

低空还多一个高度。国标把高度也切层,层厚跟该级赤道上的格子边长对齐,高空不至于扁成一张纸。空域、航线、禁飞,都能落成「哪一格 + 哪一层」。


GeoSOT 和北斗网格位置码

国内落地绕不开这两个名字。

GeoSOT 是北大程承旗团队那套:二维(三维)空间映射成一维整数,剖分粒度走 2 的幂,父子是整倍数,不会除出一堆缝。

GB/T 39409-2020《北斗网格位置码》在这套模型上定了国家标准,2021 年 6 月实施。二维十级:

级别 格子大小(赤道附近) 大概什么尺度
1 6° × 4° 跟 1:100 万图幅对得上,北京那一块就是常说的 N50J
2 30′ × 30′ 市域,大概 56 km
3 15′ × 10′ 县,跟 1:5 万图幅接近
4 1′ × 1′ 乡镇,约 1.8 km
5 4″ × 4″ 街区,约 124 m
6 2″ × 2″ 建筑,约 62 m
7 1/4″ × 1/4″ 房间,约 7.7 m
8~10 1/32″ → 1/2048″ 米、分米、厘米

码本身以 N / S 开头,后面按级往下拼。合肥在 1 级是 N50H,2 级大概 N50H67。我们组件上的 geoLevel 就是这套级数,不是另起的「业务层级」。早期文章里我写错过一次,这里改回来:可视化用 17,编码规则跟国标一致,810 太细,球面上没必要铺。

极区(±88° 以外)国标单独处理,第一级合成 N000 / S000。我们全球底图直接裁在 ±88,北极空一块是故意的。

剖分本身没那么玄。把地球当一张纸,经向、纬向切开,再切,就是四叉树:

!四叉树递归剖分

这点重要,因为它跟 Web 地图的瓦片金字塔是同构的:都是 (列, 行, 级),子块落在父块里。能换算,就不用自己从视锥开始重写一套。


三个会把人劝退的问题

7 级全球铺开是亿级格子。全画,浏览器先死。

所以只能画眼前这一层,还得跟缩放走:放大变细,缩小变粗。这就是 LOD。

自己算「当前视野有哪些格」也不划算。视锥和椭球求交、跨日界线、贴地、每帧剔除,Cesium 已经做了,而且有屏幕空间误差、瓦片池、加载优先级。我们再手写一版,大概率又慢又脆。

问题就变成:怎么把 Cesium 算完的瓦片结果接出来。


办法:挂一张不发请求的假底图

Cesium 加影像底图的路径是:

scss 复制代码
ImageryLayer → ImageryProvider.requestImage(x, y, level) → 一张图

它只要你返回一张图,不追问图从哪来。那我们就给一张空白 canvas,顺便把 (x, y, level) 和瓦片范围拿走。

ts 复制代码
requestImage(x: number, y: number, level: number): Promise<HTMLCanvasElement> {
  const canvas = document.createElement("canvas");
  if (level < 3) {
    return Promise.resolve(canvas);
  }
  const bbox = this.getTileCornerCoordinates(x, y, level);
  this._callback?.(x, y, level, bbox);
  return Promise.resolve(canvas);
}

零次 HTTP。调度还是 Cesium 那套。

挂到场景上大概这样:

ts 复制代码
layer = new GridBasemapLayer({
  url: "",
  extent: [-180, -90, 180, 90],
  zIndex: 88,
  callback: callBack,
} as GridMapSourceParams);

viewer.basemaps?.add(layer);

内部会变成一个 GradientImageryProvider。Cesium 以为在加载透明底图,其实在帮我们跑四叉树。

!假底图调度链路

requestImage 只在「有新瓦片要加载」时来。已经在场的瓦片不会再回调,所以相机停稳后我们还补一刀:去读当前正在画的地形瓦片。

ts 复制代码
const tiles = (viewer.scene.globe as any)._surface._tilesToRender;

这是私有字段。现在能用,Cesium 升大版本可能直接没了。生产里判空、try/catch,升级列入回归。不要把它当正式 API。


瓦片级对不上网格级

手里有一堆 (x, y, level, bbox) 之后,不能拿瓦片级当网格级用。

瓦片每深一级,边长大致折半。北斗网格不是这样均分的:1 级 6°×4°,2 级 0.5°,3 级又变成 15′×10′,后面还有 1′、4″。所以我们做了一张表,让 Cesium 的瓦片级去对某一个国标级,中间用子瓦片或父瓦片把尺寸找齐。

ts 复制代码
const TILE_LEVEL_MAP: Record<number, { geo: number; mode: "next" | "grid" | "pre" }> = {
  5: { geo: 2, mode: "next" },
  6: { geo: 2, mode: "grid" },
  7: { geo: 2, mode: "pre" },
  // 8~10 → 3 级,11~13 → 4 级......18~20 → 7 级
};

next 是拆成 4 个子瓦片再画,grid 按当前 bbox 画,pre 回到父瓦片再画。连续缩放时,屏幕上的格子不会突然密成一团,也不会突然稀掉。

!瓦片级与网格级映射

组件加的也不是「一格一个 Feature」。一个瓦片 bbox 进图层,引擎里再用 GridDivisiongeoLevel 剖成真正的 GridCell。去重用 x-y-level 当 key,主路径和补洞路径可能打到同一块。


画什么、留什么、删什么

填充只在 7 级加。更粗的级只描边。低级格子在屏幕上很大,半透明一铺,底图就糊了。所以 color 注释写着「目前只在北斗 7 级展示」,不是没做完。

格子会一直加。不删会把内存吃满。我们现在两头清:

  • 缩放过程中 200ms 节流,按当前 zoom 丢掉过期级别,不然新旧叠在一起会花屏
  • 相机停稳后再补当前视野,再清一次

1 级是写死的全球底衬(大约 ±178 / ±88),清的时候留着,否则缩到很远会空一块。

点击不靠填充面拾取------1~6 级根本没有填充。点到边框用边框的级别,点空白就取当前最深的可见级,再用坐标反算格子。弹窗里是北斗码,下面带着级别和内部行列,方便对计算。

编号标签是另一路:格子中心一个点要素,白字黑描边。细级别会抽稀,并且用视距藏字。第 7 级一格大约 7.7 米,相机稍远字就比格子还宽,必须藏。第 8~10 级到米、厘米,球面上铺不开,我们可视化停在 7 级。


我们踩过的坑

编号别自己发明。 第一版标签是 L7-1684915-1725728,后来改成 7-4915-5728,别人看了还是一脸问号。行业里认的是 N50J 这种。格子尺寸已经按国标剖了,码却用内部行列,等于白剖。现在展示走 GB/T 39409 二维码。

视距按「正俯视」估会吃亏。 distanceDisplayCondition 用的是相机到点的直线距离。Story 默认高度 1000 米、俯仰 −45°,距离已经到 1.4 公里。L7 还按「格子米数 × 20」藏,字整级消失。斜视要把系数放开,或者按屏幕上格子够不够放下字来藏。

黑底小标签在卫星图上很脏。 半透明黑胶囊对比度是够了,也像调试 stamp。后来改成字加描边。引擎里浅色描边用 lighter 合成没问题,深色不行,纯黑会被吃掉,深色得走普通描边。

缩放过程只在 move-end 清,会花屏。 连续滚轮时新级别往上堆,旧的还在。深层级留在低 zoom 上,就是一片白网。过程中也要清。

西半球邻接。 航线 26 邻接里如果写 col > -1,西经列号是负的,邻居会被扔掉。全球底图不走这条,沿航线占格的那条会。

点拾取别 split("-") 几何实例 id 是 JSON.stringify(gridCell)-guid。西半球坐标带负号,split 会截断,JSON.parse 直接炸。用最后一个 }- 定位。


怎么挂到图上

图层本身几乎只干三件事:听相机停稳、听缩放过程、听点击。真正剖格、加要素、清过期级别,都在 hook 里。它要往矢量图层上丢 Feature,所以得套在矢量图层里面,不能单独扔在场景外。

vue 复制代码
<cesium-map @ready="onReady">
  <vector-layer>
    <globe-grid
      :show-label="true"
      @click-grid="onClickGrid" />
  </vector-layer>
</cesium-map>

填充色只在 7 级生效,线框各级都画。编号和点击弹窗可以关掉。点到格子时,回调里带上 GridCell:范围、级别、行列、高度层都在。

同一套剖分还能喂给区域、航线、单点------输入从「当前视野」换成面、线、点,格子还是那些格子。


还想做的

国标三维码还没接完整,高度层现在是我们自己的 layerIdx。极区球冠也没画。颜色更新还在扫整层要素,格子多了会顿一下。_tilesToRender 那条备用路径,等哪次 Cesium 升级坏了再换正式口子。

这套东西值钱的不是「图上多了几条线」。是区域计算可以从几何运算收成编码比较,而可视化可以借引擎已经在算的瓦片树,不必再造一个四叉树。

假 Provider 这招别处也能用。下次未必还是 requestImage,换个合法的扩展点就行。


GB/T 39409-2020《北斗网格位置码》;GB/T 40087-2021《地球空间网格编码规则》;程承旗等,GeoSOT。文中代码按真实实现精简过,变量名改过。

相关推荐
aixingpan1 小时前
aixingpan.cn API开发文档:api_docs_trichart_natal_progression_ter_transit2接口指南
前端·php
liuyt20221 小时前
【javaweb】day3
前端
传奇开心果编程1 小时前
【xilem0.4基础语法学与练】第45课 xilem_core
学习·rust·前端框架
CoderYanger1 小时前
Java EE 进阶:2.3 JavaScript
java·开发语言·前端·javascript·css·职场和发展·java-ee
恋猫de小郭1 小时前
ADB Wi-Fi 2.0 ,Android 17 把无线调试的连接链路重新做了一遍
android·前端·flutter
薛一半1 小时前
React路由初始
前端·react.js·前端框架
问心无愧05131 小时前
ctf show web 178
前端·笔记
岁岁种桃花儿1 小时前
Vue组件化编程第二篇:非单位件组件
前端·javascript·vue.js
晓得迷路了2 小时前
栗子前端技术周刊第 146 期 - React 19.3、jQuery 1.0 20 周年、Bun 1.4.2...
前端·javascript·react.js