v0.2.13
v0.2.13 —— 地形切片快 4~7 倍、高程精度好约 5 倍(新切的地形默认不带光照)+ 底图取不到瓦片会自动换一张
先说结论:升级即生效的有三件事 —— 底图不会再是一颗蓝球,选完 tif 立刻能看到它是什么数据,「数据处理」表单里多了切片档位与地形光照两个开关。地形变快变准那件事要重切才拿得到,而且新切的地形默认没有山体光照。 已有的地形瓦片不失效、不必重切。
地形切片换了一套默认做法:同一份 DEM,快 4~7 倍、高程精度好约 5 倍,代价是新切的地形默认不再带光照。已经切好的瓦片不失效、照常用,不必重切;但想拿到这些改进就必须重切 —— 切片参数是烘进产物的。
- 旧做法是「逐瓦片在规则网格和减面网格之间择优 + 无条件烘焙顶点法线」。这一版改成固定用规则网格、默认不烘法线。
- 为什么改:拿 6 个真实 Copernicus GLO-30 数据块(坡度中位 0.51
38.85,从极平原到黄土塬)、20 组配置做三轴(耗时 / 体积 / 高程误差)支配判定,旧默认一次都没进过 Pareto 前沿(即:总能找到另一组配置三项全面胜过它),而且在三个地形上都比现在最费的「精细」档还慢。它多花的 2.65.9 倍时间很大一部分是白花的:崎岖地形上它 98.8% 的瓦片本来就选了规则网格,等于把同一个产物用 6 倍 CPU 重算一遍。 - 华北平原那块 1°×1° 实测,同样切到 z14:耗时 15.36 s → 3.90 s,高程 RMS 误差 0.543 m → 0.082 m,体积 44.5 MB → 94.6 MB。江南丘陵、黄土塬同口径分别快 5.0 倍、7.2 倍。
- 六种地形取中位:时间约 1/5、精度 5.1 倍、体积 +58%。体积是唯一变差的一项,而且只在平缓地形上变差:最平的荷兰 12.9 MB → 18.9 MB(+46%),华北 +112%;而最崎岖的黄土塬反而从 171.1 MB 降到 94.5 MB(−45%)。
- 规则网格不减面,平缓地形上每张瓦片恒定 8192 个三角形,是旧做法的 7.86 倍 —— 这是本次改动最该被质疑的地方,所以拿仓库自带的 CesiumJS 1.143.0 在一块弱核显(Intel UHD 620)上真测了:同屏瓦片数逐格相同(23/23、45/45、48/48)—— Cesium 选层级看的是几何误差,与瓦片里有多少三角形无关;32 万同屏三角形的帧时中位 0.3~0.4 ms、显存 6 MB,比旧做法还略快一点。唯一真实的代价是首次加载按字节等比变长,且只在平缓地形上出现:华北冷缓存 45 张瓦片就位 1093 ms → 1905 ms(1.74 倍),黄土塬无差别。
- 完整选型实测见
docs/reference/terrain/tiling-presets-measured.md。
- 顶点法线(每顶点 2 字节的编码扩展段)对几何精度零贡献 —— 开关两侧的 RMS、P95、最大误差逐位相同 —— 却要吃 +35%~+100% 的体积和约 2.1 倍的切片时间(华北 +35%、江南 +67%、黄土塬 +100%)。而前端的「地形光照」本来就默认关着,所以默认不再烘它。
- 关掉的后果必须说在前面:
layer.json的extensions会写成[],而 Cesium 的hasVertexNormals是整个地形提供者一个标志。于是点「地形光照」不会有任何山体明暗,只得到全球日夜渐变,连随包底图自带的法线也一并作废,全程零报错、任务照样显示完成。这正是本仓栓过三次的那类「作业完成、HTTP 200、前端不报错、就是不对」,所以宁可写在发版说明里。 - 法线是烘焙进瓦片的:事后改配置对已经切完的产物没有任何影响,想要光照只能带着法线重切一遍。反过来,升级前切好的老瓦片自带法线,光照照常能用。
任务详情面板多了两行:「切片档位」和「顶点法线」
- 显示的是这个作业实际用的值,不是当前配置值 —— 同一份 DEM 换个档位重切一遍,两条记录并排摆着就知道差在哪。
- 档位不直吐后端的
precision/balanced/speed,写成「精细(比基准层级多切一级)」这种带参照物的说法 —— 那三个词本身说不清「和什么比、差在哪」。参照物特意写「基准层级」而不是「默认档位」:把默认改成快速之后,一个存成均衡的旧作业仍会被标成「默认」,而它其实比默认多切了一级。 - 法线关闭时写「未开启(无光照数据)」,鼠标悬停给出上一段那个后果的全文。认不出的档位值(手改过库、将来新增的档)原样显示,不冒充「均衡」。
- 一条读数上的局限:升级前切的作业会被显示成「均衡 / 未开启」,这是加列时填的默认值,不是它们当时真实用的参数(当时是择优 + 烘法线)。老作业的实际参数当时根本没存,补不出来。
「数据处理」表单里可以直接选档位和法线了
- 上传 DEM 建本地高程切片时,「最大层级」下面多了「切片档位」下拉(精细 / 均衡 / 快速)和「生成地形光照法线」复选框;对已经下载好的 DEM 起切也走同一组控件。旁边各有一行说明:档位那条讲清楚它是在你填的层级上 +1 / +0 / −1,法线那条把上面那个「静默失效」的后果原样写着。
- 两个控件的初值由服务端按配置渲染,不是写死的。 写死就意味着运维在配置页把默认调成了精细、表单却仍然发均衡 —— 那是个「改了没反应的假旋钮」。法线那条尤其不能写死:配置里开着、用户不动复选框却发了个 false 出去,几小时切完才发现光照点不亮。
- 配置页仍然没有这两项的控件,改默认还是走
PUT /api/config:terrain_quality_preset(precision/balanced/speed,默认balanced)与terrain_vertex_normals(true/false,默认false)。 - 接口调用方注意三态:不传
quality/vertex_normals是「走配置默认」,与「传了false」是两回事(本地地形走 multipart,字符串'true'/'false';DEM 起切走 JSON body,真布尔)。另外 HTML 复选框的.value恒为on,直接发它会 400 —— 要发String(el.checked)。 - 表单一直开着、期间配置被人改过的话,提交写回的是你打开页面时那一份值。刷新一下就同步了。
- 三档只差一件事:实际切到的最深层级 = 你填的「最大层级」+1 / +0 / −1。取值表全项目只有一份(
src/services/geo_validation.py的TILING_QUALITY_OFFSETS),配置键、数据库列、界面文案都从它派生 —— 不存第二份,就不会出现「界面写着一回事、切出来另一回事」。 - 为什么用「层级」而不是「简化网格」拉开档位:两个旋钮都能拿精度换体积,但层级的兑换率高 2.4~3.9 倍,而且层级省时间、简化网格反过来多花 2.6~5.9 倍时间。
- 每加一级约 3.3 倍体积换 2.8 倍精度,这个比例与源数据分辨率无关(同一块 DEM 重采样成 1″ / 3″ / 9″ 各建 5 个层级,六组数据全落在这个区间)。华北那块的三档完整金字塔:精细 z15 / 45560 张 / 354.2 MB / 13.31 s / RMS 0.029 m,均衡 z14 / 12071 张 / 94.6 MB / 3.90 s / RMS 0.082 m,快速 z13 / 3607 张 / 24.6 MB / 1.53 s / RMS 0.226 m。
- **在意磁盘或首次加载的,快速档是真划算:**24.6 MB 比旧默认的 44.5 MB 还小,高程精度还好 2.4 倍。
- 别拿瓦片张数判断档位有没有生效。 z0–z4 那 682 张是全球覆盖的固定底座,三档逐层完全相同,只有塔尖跟着档位变。以基准 z12 实测,三档是 3607 / 1445 / 893 张,而体积是 24.6 / 6.7 / 2.2 MB —— 体积逐档约 1/3.3,张数远不到。
几个现在就能踩到的点(走接口的人看)
- 档位名拼错当场 400,不静默退回均衡。「改了档位重切、结果一模一样且零报错」是这条路径最难查的假象。法线开关同理,只认
true/false两个字面量 —— HTML 复选框的.value恒为on,直接发它会 400(要发String(el.checked))。静默折成「关」的后果是:你勾了法线、瓦片没烘、任务显示完成。 - 最大层级填 21 再选精细,实际还是 21。 层级校验发生在加偏移之前,拦不到 21+1;偏移叠完会被钳在 0~21。反方向一样:填 8 选快速会压到 7,而地形切片的起始层级恒为 8(z0–z7 是随包底图的地盘)—— 这种情况程序会把起始层级一起压下去,不会出现「切了 0 张瓦片却报完成」。
- 配置里的
terrain_local_maxzoom填了越界值(比如 99),现在两个入口一致地退回出厂默认 14,并在日志里打出被丢弃的原值。 以前本地地形那条是裸取值,99 会让建任务直接 400;DEM 那条读同一个键却软退回照跑 —— 同一个坏配置两种结果。浏览器上传恒带层级,真正中招的是省略该字段的接口调用方。一个坏配置不应该让所有任务都建不起来,但它必须在日志里留痕 —— 静默吞掉你写过的 99,会让它在系统里一处都查不到。
两条没改、但应该说清楚的
- 基准层级仍然是你填的那个数,不看源数据分辨率。 93 m 的 DEM 切到 z14 是 77.4 MB,按分辨率估算只需 z12 / 6.9 MB —— 11 倍体积换不到任何新地形;反过来 5 m 的 DEM 该切到 z17,被 14 截断,细节根本进不了瓦片。选型实测顺手查出了这一条,但修它要动两张表的约束,本版没做。
- 全球随包底图不受影响,也不该受影响。 它是预先切好、随包分发的,构建脚本仍走旧的逐瓦片择优 —— 它覆盖海洋和大片平原,正是减面收益最大、规则网格字节代价最高的地方,而且只构建一次,CPU 代价无所谓。两边的取舍条件本来就是相反的。
底图取不到瓦片时会自动换一张,不再是一颗蓝球
- 实测过的真实故障:Esri 的 CDN(Akamai)封了代理的出口 IP,每块瓦片 403;而那台机器上 Google 只有走代理才通——两张卫星图分属两条网络路径,谁都可能单独挂掉。现在后端按 Esri 卫星 → Google 卫星 → OpenStreetMap 路网依次试,第一张通的就出图,挂掉的源冷却 60 秒后自动重试(上游恢复了不用你做任何事)。
- 换了会明说:界面弹一句「底图已自动切换到 Google 卫星影像:Esri 卫星影像取不到瓦片」,同时
/api/basemap会同步报出实际在用的源、最大层级和署名——底图默默换一张而你不知道,比蓝球更糟。换回来也会说:配置的源恢复之后弹一句「底图已恢复为 Esri 卫星影像」。整个会话每 30 秒查一次,只在真的换了的时候说话,不会重复弹同一句。 - 回退时最大层级和署名跟着实际那张图走。 Esri 封顶 z19、Google 封顶 z21,回退之后如果还按原来那个上限请求,多出来的层级全是 404 黑瓦片;署名不跟着换则是许可证问题(Esri 与 OSM 的署名是硬要求,不是装饰)。
- 回退链里只有 WGS-84 的源。 底图是用来框选下载范围的,静默换上一张 GCJ-02 的图等于让你框错地方(国内偏移 100–700 米)。Google 路网图(
lyrs=m)因此不在链里:它中国区是 GCJ-02,而且与 Google 卫星同主机——卫星取不到时它也取不到,放进来是拿坐标系风险换零可用性。想用它可以在配置页里显式选,那是你自己的决定。 - 配置值本身写错(不是 http(s)、或指向 169.254.x.x 这类链路本地地址)不会被回退掩盖,照旧当场 502:那是配置错误不是上游故障,盖掉的话你永远不知道自己写错了。
选完 tif 就能看到这份数据到底是什么(本地高程切片 + 等高线)
- 以前「数据处理」里选完文件只显示一个文件名,坐标系、覆盖范围、分辨率一概看不到 —— 层级填多少全靠猜,选错文件(比如拿了一份没有坐标系的 tif)要等任务跑起来失败了才知道。
- 现在选完文件当场列出:影像尺寸、坐标系(EPSG 码 + 名称)、WGS84 覆盖范围、像元分辨率(度 + 米)、数据类型 / 波段 / NoData、GDAL 算过统计时还有高程范围,以及按切片管线自己的算法给出的建议最大层级(与不填层级时实际切出来的一致)。多选时另有一块合并总览:合并范围、最细分辨率、按最细分辨率给的建议层级。
- 有问题会直接说,而不是等任务失败:缺地理参考 / 坐标系不可识别 / 读不出 TIFF 头部(红字,这些切不了片);不是 WGS84 会自动重投影、多波段只用第 1 波段、多个文件坐标系不一致(橙字,提醒)。
- 不会为此上传文件。浏览器只读文件开头几 KB 的 TIFF 目录,把标签发给后端做地理解释(EPSG → 坐标系名称、投影坐标 → 经纬度这些必须由 GDAL 来算)。实测一个 200 MB 的 DEM 从选中到出信息 117 毫秒,页面内存涨 0 —— 真正的上传仍然只发生在点「创建处理任务」的时候。
- 两个表单用的是同一张卡,只有「建议最大层级」分开算:高程切片按 Cesium 的经纬度分块、等高线按 Web Mercator 瓦片,各自调用它自己那条管线的函数 —— 卡片上写的数就是你不填层级时真正会切出来的那一级。
配置里的路径不再被限制在程序目录内
- 「拼接临时目录」
stitch_tmpdir、「等高线重投影临时目录」contour_warp_tmpdir、「随包底图位置」terrain_global_base_path现在可以指到任意一块盘。上一版把它们限制在安装目录 / 下载目录 / 缓存目录之内,而这条规则与这几个键的用途直接打架:两个临时目录存在的全部意义就是把 GB 级中间产物挪到另一块盘,terrain_global_base_path指的是 224 MB / 4.3 万个文件的随包底图,放大盘同样正当;而「保存目录」自 v0.2.4 起本来就是全盘可选 —— 同为路径键却两套口径。 - 仍然拒收的两种值只管功能正确性:两个临时目录不收相对路径(相对路径按进程当前目录解析,打包版从快捷方式启动时那不是安装目录,中间产物会落到谁也想不到的位置);
terrain_global_base_path不收空值(空值会把底图静态服务的根落到安装目录本身,而且底图判定必然失败)。
发版前的代码评审又拦下 11 条,其中 4 条会让你拿到错数据或看到 500
底图那三条都是回退功能自己带进来的新伤:
- 一张缺瓦片不再把整幅底图换掉。 404 是每个 XYZ 服务说「这里没有图」的正常方式(Esri 在覆盖空洞和层级上限之外就会 404),可原本的实现把它和 403 同等对待:一张 404 就给整个源判 60 秒死刑、后续瓦片全换成另一家、还弹一句「底图已自动切换」——而根本没有任何故障。现在 404 原样透传,只有 403 / 429 / 5xx 和网络层异常才算源挂了。
- 回退取到的瓦片不再被浏览器缓存一整天。 原本回退瓦片和正常瓦片一样发
max-age=86400,于是上游抖动 30 秒,另一家的影像就被烤进浏览器缓存 24 小时 —— 上游恢复之后你会看到两家影像拼在同一屏里,而且缓存不会再发请求,永远自己好不了。现在回退瓦片只缓存 60 秒。 - 整条链都挂掉时,报的仍是你配置的那个源的状态码。 冷却会把失败过的源排到队尾,原本的实现取「第一个试的」的状态码,于是配置源一旦进过冷却,你看到的就是链尾那家的错误 —— 想查 Esri 为什么 403,屏幕上却是别人家的 504。另外,Cloudflare 那类回 520/521/525 的自建镜像原本会让服务端抛
LookupError变成 500,真实状态码反而被埋掉,现在统一收敛成 502。
tif 信息卡那三条:
- 自定义投影的 DEM 不再被当成经纬度。 国内 GIS 软件导出的自定义 Albers / 兰勃特 / 高斯克吕格,投影码写的是「用户自定义」,而 GeoTIFF 规范要求同时写上它的基准地理坐标系(如 CGCS2000)—— 原本的实现读到后者就当成了像素单位,把 50 万米、300 万米这样的坐标当作经纬度,给出一个精确、自信、彻底错误的覆盖范围,且不发任何警告。现在这种情况老老实实报「坐标系不可识别」。宁可说不知道,也不能给错的范围。
- 服务端认不出 EPSG 码时,不再谎称「服务端缺少 GDAL」。 原本四种完全不同的失败(GDAL 没装、EPSG 码查不到、osr 抛异常、坐标换算失败)共用同一句提示,于是 GDAL 装得好好的人被指去排查一个根本没问题的安装。现在两种成因分开说。
- 坏数据不再变成 500。 一个超长整数、一个不是对象的 JSON、一个越界坐标算出的无穷大,原本都会让这条接口 500(最后那种还会吐出
Infinity这种 JSON 解析不了的字面量,卡片直接空白且不报错)。现在一律降级成正常的提示。顺带:这条接口只需要几 KB 的标签,原本却继承了 2 GB 的上传上限,现在超过 1 MB 直接 413;报错文案也终于跟着界面语言走,不再是中文界面里夹一句英文。
验证
- 本版全量测试 1930 项通过 / 3 项跳过(开发机 Linux;跳过的只在特定平台上有意义,CI 上各平台的跳过数略有不同)。上一版是 1758 项。
- 上面评审修的 11 条每条都配了会变红的用例:先在未修的实现上跑一遍确认它们确实失败,再修、再跑绿 —— 避免写出「怎么改都绿」的空断言。
- 底图回退、同源约束与信息卡这三项另做了真机验证:起真服务、真浏览器,全程 91 个请求全部同源、零外部依赖(离线可用与「浏览器永远看不到上游地址」这两条硬约束都是这么验的);回退发生时确实换了图层上限与署名,同一状态重复轮询不重复弹提示。
- 表单这两个从配置取初值的控件也顺手收了口:越界的
terrain_local_maxzoom(这个键没有写入校验,PUT /api/config收得下 99)此前会让整张「数据处理」表单变成:invalid—— 原生校验拦下 submit,「创建」点了没反应,连等高线任务一起建不了,且气泡弹不出来。现在渲染前就收敛掉,并且在日志里点名被丢弃的原值;认不出的档位同理。模板是这条链上唯一记不了日志的一环,在那儿悄悄修好的值,运维一辈子查不到。
通用说明
- 下载安装:从下方 Assets 下载对应平台压缩包(
terraforge-windows.zip/terraforge-linux.tar.gz/terraforge-macos.tar.gz),解压即用,无需安装 Python 环境。 - 下载体积:每个平台仍包含 167 MB 的全球底图分卷(自 v0.2.8 起)。
- 首次运行:启动可执行文件后,浏览器访问 http://localhost:5000 ;代理、并发、缓存管理等在「配置」页修改。
- 历史版本:完整更新历史见仓库 CHANGELOG.md。
- 使用文档:见仓库 README.md 与 docs/guides/QUICKSTART.md。