Cesium 离线地形数据处理与加载:从数据获取到加载

前言

在 Cesium 项目的实际落地场景中,部分项目因部署环境限制(如内网隔离、无网络访问权限)需采用离线部署模式。此时,依赖网络服务商提供的在线地形数据已无法满足需求,需要提前处理并部署离线地形数据。

  1. DEM 原始数据获取(地形数据的基础来源);

  2. 数据切片处理 ;

  3. Cesium 离线加载。

DEM 数据获取

DEM(数字高程模型)是描述地表高程信息的基础数据,是生成 Cesium 地形的核心前提,这里使用青花鱼推荐的 Qgis 插件OpenTopography DEM Downloade

安装之后打开插件

插件需要 API KEY,直接注册账号即可,再选择数据源、项目区域、导出地址即可,一般 30m 分辨率已经足够使用了

下载完成的数据

数据切片

笔者这里搜索了各种方案,主要有两种:

开源方案:

ctb-quantized-mesh

优点:免费、跨平台、方便后端部署

缺点:无法自动生成正确的layer.json描述文件(该文件是 Cesium 识别地形瓦片的关键配置文件),需额外处理

商业软件:GISBox、CesiumLab

优点:一键切片

缺点:部分功能收费

ctb-quantized-mesh

这是一个 docker 镜像,项目地址,笔者是 Windows 平台,所以以Docker Desktop为例

拉取镜像

docker pull tumgis/ctb-quantized-mesh

这时候出现 ctb-quantized-mesh 的镜像说明下载成功了

安装

docker run -v <你的硬盘地址>:/data -ti -i tumgis/ctb-quantized-mesh:latest bash

硬盘地址会被映射到容器的data目录下

先检查文件是否被正确挂载

ls -la /data/

可以看到,tif 文件就是我们需要切片的文件

根据项目文档,需要先创建 vrt 文件

gdalbuildvrt tiles.vrt /data/test.tif

生成瓦片,等待完成即可

ctb-tile -f Mesh -C -N -v -o ./terrain/ tiles.vrt

可以看到 terrain 文件夹下已经生成了瓦片

创建 layer.json 描述文件

ctb-tile -f Mesh -C -N -l -o ./terrain/ tiles.vrt

这里比较坑的是描述文件并不正确,范围bounds字段默认是全球范围,瓦片级别maxzoomminzoom也没有,导致 Cesium 加载失败,项目文档似乎也没有相关配置项,有兴趣的大佬找到解决办法可以留言

ai 老师给出自动化修改描述文件的代码,未经测试,有兴趣的大佬可以研究下

#!/bin/bash

# layer.json文件修正工具
# 用于修正CTB生成的layer.json文件中的bounds和缩放级别信息

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

echo -e "${BLUE}=== layer.json 修正工具 ===${NC}"
echo ""

# 检查参数
if [ $# -eq 0 ]; then
    echo -e "${RED}使用方法: $0 <TIFF文件> [layer.json路径]${NC}"
    echo ""
    echo "示例:"
    echo "  $0 aaa.tif                                    # 修正当前目录的layer.json"
    echo "  $0 aaa.tif /path/to/terrain/layer.json       # 指定特定的layer.json文件"
    echo ""
    exit 1
fi

INPUT_TIFF="$1"
LAYER_JSON="${2:-./layer.json}"

echo "输入TIFF文件: $INPUT_TIFF"
echo "目标layer.json: $LAYER_JSON"
echo ""

# 检查TIFF文件是否存在
if [ ! -f "$INPUT_TIFF" ]; then
    echo -e "${RED}错误: TIFF文件 '$INPUT_TIFF' 不存在${NC}"
    exit 1
fi

# 检查layer.json是否存在
if [ ! -f "$LAYER_JSON" ]; then
    echo -e "${RED}错误: layer.json文件 '$LAYER_JSON' 不存在${NC}"
    echo "请确保CTB处理已完成且layer.json文件已生成"
    exit 1
fi

echo -e "${YELLOW}步骤1: 分析TIFF文件的地理信息...${NC}"

# 获取TIFF文件的地理信息
GDAL_INFO=$(gdalinfo "$INPUT_TIFF" 2>/dev/null)

if [ $? -ne 0 ]; then
    echo -e "${RED}错误: 无法读取TIFF文件信息${NC}"
    exit 1
fi

# 提取坐标范围
echo "正在解析地理坐标范围..."

# 获取四个角点的坐标
UL_LON=$(echo "$GDAL_INFO" | grep "Upper Left" | sed 's/.*(\([^,]*\),.*/\1/')
UL_LAT=$(echo "$GDAL_INFO" | grep "Upper Left" | sed 's/.*, \([^)]*\)).*/\1/')
UR_LON=$(echo "$GDAL_INFO" | grep "Upper Right" | sed 's/.*(\([^,]*\),.*/\1/')
UR_LAT=$(echo "$GDAL_INFO" | grep "Upper Right" | sed 's/.*, \([^)]*\)).*/\1/')
LR_LON=$(echo "$GDAL_INFO" | grep "Lower Right" | sed 's/.*(\([^,]*\),.*/\1/')
LR_LAT=$(echo "$GDAL_INFO" | grep "Lower Right" | sed 's/.*, \([^)]*\)).*/\1/')
LL_LON=$(echo "$GDAL_INFO" | grep "Lower Left" | sed 's/.*(\([^,]*\),.*/\1/')
LL_LAT=$(echo "$GDAL_INFO" | grep "Lower Left" | sed 's/.*, \([^)]*\)).*/\1/')

# 计算实际的边界框
MIN_LON=$(echo -e "$UL_LON\n$UR_LON\n$LR_LON\n$LL_LON" | sort -n | head -1)
MAX_LON=$(echo -e "$UL_LON\n$UR_LON\n$LR_LON\n$LL_LON" | sort -n | tail -1)
MIN_LAT=$(echo -e "$UL_LAT\n$UR_LAT\n$LR_LAT\n$LL_LAT" | sort -n | head -1)
MAX_LAT=$(echo -e "$UL_LAT\n$UR_LAT\n$LR_LAT\n$LL_LAT" | sort -n | tail -1)

echo "检测到的实际范围:"
echo "  经度范围: $MIN_LON 到 $MAX_LON"
echo "  纬度范围: $MIN_LAT 到 $MAX_LAT"

# 计算对角线距离用于确定合适的缩放级别
DIAGONAL_DEGREES=$(echo "sqrt(($MAX_LON - $MIN_LON)^2 + ($MAX_LAT - $MIN_LAT)^2)" | bc -l 2>/dev/null || echo "0")

# 根据范围大小估算合适的最大缩放级别
if (( $(echo "$DIAGONAL_DEGREES < 0.5" | bc -l 2>/dev/null || echo "0") )); then
    MAX_ZOOM=16
    ZOOM_DESC="超精细 (小区域)"
elif (( $(echo "$DIAGONAL_DEGREES < 2" | bc -l 2>/dev/null || echo "0") )); then
    MAX_ZOOM=14
    ZOOM_DESC="精细 (中小区域)"
elif (( $(echo "$DIAGONAL_DEGREES < 10" | bc -l 2>/dev/null || echo "0") )); then
    MAX_ZOOM=12
    ZOOM_DESC="中等 (中等区域)"
elif (( $(echo "$DIAGONAL_DEGREES < 30" | bc -l 2>/dev/null || echo "0") )); then
    MAX_ZOOM=10
    ZOOM_DESC="标准 (大区域)"
else
    MAX_ZOOM=8
    ZOOM_DESC="粗略 (超大区域)"
fi

echo "建议的缩放级别: 0-$MAX_ZOOM ($ZOOM_DESC)"
echo "对角线距离: $DIAGONAL_DEGREES 度"

echo ""
echo -e "${YELLOW}步骤2: 读取并修正layer.json文件...${NC}"

# 备份原始文件
BACKUP_FILE="$LAYER_JSON.backup.$(date +%Y%m%d_%H%M%S)"
cp "$LAYER_JSON" "$BACKUP_FILE"
echo "已备份原始文件到: $BACKUP_FILE"

# 显示原始文件内容
echo ""
echo "原始layer.json内容:"
echo "================================"
cat "$LAYER_JSON"
echo "================================"

# 使用Python修正JSON文件
echo ""
echo "正在修正JSON文件..."

python3 << EOF
import json
import sys

try:
    # 读取原始JSON文件
    with open('$LAYER_JSON', 'r') as f:
        data = json.load(f)

    # 保存原始bounds用于对比
    original_bounds = data.get('bounds', 'unknown')

    # 修正bounds为实际范围
    data['bounds'] = [$MIN_LON, $MIN_LAT, $MAX_LON, $MAX_LAT]

    # 添加或修正缩放级别
    data['minzoom'] = 0
    data['maxzoom'] = $MAX_ZOOM

    # 添加center字段(数据中心点)
    center_lon = ($MIN_LON + $MAX_LON) / 2
    center_lat = ($MIN_LAT + $MAX_LAT) / 2
    data['center'] = [center_lon, center_lat, $MAX_ZOOM // 2]

    # 保存修正后的文件
    with open('$LAYER_JSON', 'w') as f:
        json.dump(data, f, indent=2)

    print(f"✓ layer.json文件修正成功")
    print(f"  原始bounds: {original_bounds}")
    print(f"  新bounds:   {data['bounds']}")
    print(f"  缩放级别:   {data['minzoom']} - {data['maxzoom']}")
    print(f"  数据中心:   {data['center']}")

except Exception as e:
    print(f"✗ 修正JSON文件时出错: {e}")
    print("恢复备份文件...")
    import shutil
    shutil.copy('$BACKUP_FILE', '$LAYER_JSON')
    sys.exit(1)
EOF

if [ $? -eq 0 ]; then
    echo ""
    echo -e "${GREEN}修正后的layer.json内容:${NC}"
    echo "================================"
    cat "$LAYER_JSON"
    echo "================================"

    echo ""
    echo -e "${GREEN}✓ layer.json文件修正完成!${NC}"
    echo ""
    echo -e "${BLUE}现在您可以在Cesium中这样使用:${NC}"
    echo "viewer.terrainProvider = await CesiumTerrainProvider.fromUrl('./$(dirname "$LAYER_JSON")/')"
    echo ""
    echo -e "${YELLOW}注意事项:${NC}"
    echo "1. bounds现在反映实际的地形数据范围"
    echo "2. 添加了minzoom(0)和maxzoom($MAX_ZOOM)字段"
    echo "3. 添加了center字段用于地图初始视角"
    echo "4. 备份文件保存在: $BACKUP_FILE"
else
    echo ""
    echo -e "${RED}修正失败,已恢复备份文件${NC}"
    exit 1
fi

商业软件

这里我推荐 GISBox,免费的基础版功能也足够使用了

选择切片转换 ➡ 地形切片 ➡ 选择 DEM 文件 ➡ 选择输出目录

可以看到描述文件的boundsmaxzoomminzoom是正确的

数据加载

这里就比较简单了,我直接使用的 Nginx 代理地址

viewer.terrainProvider = await CesiumTerrainProviderEdit.fromUrl(
  'http://localhost:666/model/staticData/tile/'
)

加载效果