# 简介

根据 DEM 数据生成地形切片工具，使用 NodeJS + GDAL（NodeBinding）开发制作。可用于用户自定义 DEM 高程数据源生产地形瓦片，以便局域网离线使用。

特点：

- 支持 `mapbox` 和 `terrarium` 两种地形瓦片编码格式供mapboxgl使用，其中terrarium格式是[tangram](https://www.mapzen.com/products/tangram/)引擎的官方地形格式，tangram是另外一款开源的webgl二三维一体化的引擎；
- 固定瓦片尺寸256，瓦片周围有1cell的buffer，即实际瓦片是258*258.
- 自动读取数据源的坐标系统，重编码输入的 DEM 栅格文件，并重投影至指定的坐标系4490、4326、3857，默认3857，然后生成瓦片；
- 支持适用于3857、4490、4326的地形切片生产；
- 内置了影像金字塔索引和多进程实现（暂未使用多线程），加速瓦片生成速度；
- 支持地形瓦片以文件目录或mbtiles两种格式存储；
- 命令行提供了瓦片生成的进图条提示，便于用户查看生成进度。
- 内置一些异常导致的临时文件清理工作。

![生成进度条](./doc/progressbar.png)


# 1. 安装与配置

## 1.1. 配置 GDAL_DATA

由于使用了 GDAL，用户需要 [下载 gdal-data.zip](./third-party/gdal-data.zip) 数据包，并配置 `GDAL_DATA` 环境变量为解压缩的目录。

以 Windows 操作系统为例（此例直接使用 PostgreSQL 附带的资源，若用户有安装 PostgreSQL，也可以直接使用对应目录）：

![配置环境变量](./doc/gdal-data.png)

## 1.2. 安装

从网络全局安装，成为命令行工具：

```bash
npm i dem2terrain -g
yarn add dem2terrain -g
pnpm add dem2terrain -g
```

源码安装（先将当前目录定位至工程根目录，且明白什么是 link 命令）：

```bash
npm install && npm link
# 或
yarn && yarn link
# 或
pnpm install && pnpm link --global
```

安装完毕后就可以当普通命令行程序使用了。

## 1.3. 测试
测试地形数据生产：
```bash
npm run test

```
基于maplibregl地形预览：

```bash
npm run server

```
浏览器输入地址进行预览：http://[ip]:[port]/terrain.html

效果图如下：

![地形测试可视化](./doc/terrain-test.png)

# 2. 用法与说明

当前支持作为命令行使用：

```plaintext
> dem2terrain --help
Usage: dem2terrain [options]

使用 GDAL 制作地形瓦片，支持 mapbox 和 terrarium 两种编码输出格式，当前仅输出 PNG 容器格式。

Options:
  -v, --version               当前版本
  -i, --input <string>        <必填> 输入 tif 格式的 DEM 文件路径，支持相对路径
  -o, --output <string>       <必填> 输出目录，支持相对路径
  -f, --configFile <File>     <可选> 通过配置文件执行任务，输入绝对路径，可参考配置模板
  -g, --epsg <number>         <可选> Tile适用坐标系，3857 | 4490 | 4326 (default: 3857)
  -r, --resampling <number>,  <可选> 构建影像金字塔或重投影时设置重采样策略，默认3，1:AVERAGE|2:BILINEAR|3:CUBIC|4:CUBICSPLINE|5:LANCZOS|6:MODE|7:NEAREST
  -c, --clean <number>        <可选> 是否清空输出目录，0 | 1 (default: 0)
  -z, --zoom <number-number>  <可选> 指定瓦片的等级生成范围。例如，想生成 7 ~ 12 级的瓦片，则输入 -z 7-12 (default: "5-14")
  -e, --encoding <string>     <可选> 指定瓦片的数据编码规则（mapbox 或 terrarium） (default: "mapbox")
  -h, --help                  帮助
```

可选参数说明：
- `-i`: 输入 tif 格式的 DEM 文件路径，支持相对路径；
- `-o`: 输出目录，支持相对路径；
- `-g`: 指定地形Tile适用坐标系，默认是适用3857坐标系；
- `-r`:  构建影像金字塔或重投影时设置重采样策略，默认3 CUBIC 采样；
- `-z`: 由于地形栅格数据通常是 90m、30m 的空间分辨率，等级太大意义不大，等级太低时起伏辨识也不高，所以默认生成中间的 `5-14` 级；
- `-c`: 指定是否预先清理输出瓦片的存储目录，默认0，不清理；
- `-e`: 指定切片编码规则，默认 mapbox，用户可指定 terrarium 规则输出。
- `-f`: 以上参数可以都放到一个配置json文件里，使用-f执行切片任务，简化操作；

## 2.1 任务执行

有两种形式执行命令行任务

* 方式1：通过命令行参数执行任务
```bash
dem2terrain -z 4-15 -e terrarium -i ./ZONE.tiff -o ./output -c 1 -g 3857
```

* 方式2：通过配置文件执行任务

1）配置参数
```
{
    "zoom":"5-13",
    "epsg": 3857,
    "encoding": "mapbox",
    "input": "./data/xxx.tif",
    "output": "./data/tile",
    "clean": true
}
```
2）执行任务
```bash
dem2terrain -f d://config.json
```

## 2.2 数据输出
支持地形切片生成以文件或mbtiles目录两种形式存储。

* 以文件存储

-o参数为文件目录，则以文件形式存储：
```bash
dem2terrain -z 4-15 -e terrarium -i ./ZONE.tiff -o ./output -c 1 -g 3857
```

* 以mbtiles存储

-o参数带.mbtiles扩展名，则以mbtiles形式存储：
```bash
dem2terrain -z 4-15 -e terrarium -i ./ZONE.tiff -o ./output/tile.mbtiles -c 1 -g 3857
```



# 3. 使用输出成果

使用 HTTP(S) 协议的 Web 服务器（例如 `nginx`、`IIS`）将生成的地形瓦片作为静态资源发布，即可使用。

举例：根据 [MapboxGL 地形示例](https://docs.mapbox.com/mapbox-gl-js/example/add-terrain/) 简单修改，将在线数据源换成本地 Web 服务器发布的地址即可，注意编码格式要与生成时输入的编码格式一致。

```javascript
// 数据编码，'mapbox'或'terrarium'
const encoding = 'mapbox';
const maxZoom = 14;
map.addSource('my-custom-terrain', {
  type: 'raster-dem',
  encoding:encoding,
  // 使用tiles方式替换本地发布的地形切片服务
  tiles: ['./mapbox/{z}/{x}/{y}.png'],
  // 注释掉官方的服务url，替换自己的
	//'url': 'mapbox://mapbox.mapbox-terrain-dem-v1',
  tileSize: 256,
  maxzoom: maxZoom,
})
```

如下图所示：

![本地离线切片可视化](./doc/demo.webp)

# 4. TODO

当前版本足够 MapboxGL.js 使用，但仍然有新功能未开发，留待以后扩展功能，初步拟定待扩展功能如下：

- 大数据量的dem tif改用分块读取分块处理
- 扩展 gdal 驱动，使其支持 webp，直接生成 webp 格式的切片
- 重构核心模块，解耦，扩展使其支持生成 CesiumJS 支持的地形切片格式

欢迎参与贡献，包括但不限于文档、功能扩展、性能优化！

# 5. 安装问题

## 5.1 c++编译环境问题

由于node-gdal、node-gdal-next或node-gdal-async都会内置编译安装一个gdal，编译是需要依赖c++环境的，在win上常出现问题如下：
```bash
npm ERR! gyp ERR! stack Error: Could not find any Visual Studio installation to use
```
如果用户未安装vs c++环境，请最好安装下vs c++编译环境。
如果用户已经安装了vs，例如vs2022仍提示找不到，配置如下：
```bash
# 配置npm的msvs版本号，例如安装了vs 2022
npm config set msvs_version 2022
# 更新下node-gyp
npm install node-gyp@latest -g
```
## 5.2 环境变量冲突
在node-gdal系列相关包npm安装成功后，使用过程中遇到投影操作定义就会报各种错误，典型错误如下：
```
const srs= gdal.SpatialReference.fromProj4('+init=epsg:4326'); ^ Error: Corrupt Data
```
主要原因是win上各种软件包都会内置安装，环境变量冲突导致。

例如，用户安装了PostGIS，会内置安装proj,geos,gdal并自动生成proj_lib的环境变量。
```
PROJ_LIB=C:\Program Files\PostgreSQL\14\share\contrib\postgis-3.1\proj
```
当安装node-gdal-next或noe-gdal-async时，内置的proj和proj.db由于冲突不能生效，就会在使用过程中报各种错误。
依此类推，如果用户安装了独立的GDAL，又有内置的proj，geos和环境变量也会有这种问题。
解决办法：从系统环境变量中删除这些冲突的环境变量重启机器即可。

## 5.3 最好的办法
使用本机已编译好的gdal编译binding下node的gdal环境：
```
npm install gdal-next --build-from-source --shared_gdal

npm install gdal-async --build-from-source --shared_gdal
```

这样，node的gdal环境可以和本机的gdal环境一致，能使用更多的驱动例如webp，内置驱动是没有的。
这种操作可以让公用软件装一次，不会产生更多的冲突，linux上很方便。但由于windows上都不是源码编译，从.exe安装，因此不太适用。该操作适合c++编译环境熟悉的高级用户可定制安装多项扩展。


# 6. 知识补充

## 6.1. 参考资料

- [GitHub - tilezen/joerd - terrarium](https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium)

- [MapboxDocs - raster-dem](https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/)

## 6.2. 编解码差异

`mapbox` 和 `terrarium` 都将高程值编码成 RGB 数组存储，下面以简单的编解码函数说明两种编码格式的差异。

MapboxGL：

```typescript
function mapboxEncode(height: number) {
  const value = Math.floor((height + 10000) * 10);
  const r = value >> 16;
  const g = value >> 8 & 0x0000FF;
  const b = value & 0x0000FF;
  return [r, g, b];
}

function mapboxDecode(
  color: [number, number, number]
) {
  return -10000 + ((color[0] * 256 * 256 + color[1] * 256 + color[2]) * 0.1);
}
```

terrarium：

```typescript
function terrariumEncode(height: number) {
  height += 32768;
  const r = Math.floor(height / 256.0);
  const g = Math.floor(height % 256);
  const b = Math.floor((height - Math.floor(height)) * 256.0);
  return [r, g, b];
}

function terrariumDecode(
  color: [number, number, number]
) {
  return (color[0] * 256 + color[1] + color[2] / 256.0) - 32768;
}
```

对于 cesium 的地形编码和解码：

```typescript
// 每个点像素值是 int16
function cesiumEncode(height: number) {
  return Math.floor((height + 1000) / 0.2);
}

function cesiumDecode(pixel: number){
  return (pixel * 0.2) - 1000;
}
```
