@deepgis/dem-dynamic-terrain
Version:
使用 GDAL 制作地形瓦片,支持 mapbox 和 terrarium 两种编码输出格式
486 lines (485 loc) • 16.9 kB
JavaScript
import { existsSync } from "node:fs";
import promises from "node:fs/promises";
import node_os from "node:os";
import node_path from "node:path";
import emittery from "emittery";
import gdal_async from "gdal-async";
import p_queue from "p-queue";
import tinypool from "tinypool";
import yocto_spinner from "yocto-spinner";
import { randomUUID } from "node:crypto";
var package_namespaceObject = JSON.parse('{"KR":"dem-dynamic-terrain"}');
function getBuildOverviewResampling(resampling) {
switch(resampling){
case 1:
return 'AVERAGE';
case 2:
return 'BILINEAR';
case 3:
return 'CUBIC';
case 4:
return 'CUBICSPLINE';
case 5:
return 'LANCZOS';
case 6:
return 'MODE';
case 7:
return 'NEAREST';
default:
return 'CUBIC';
}
}
function getResampling(resampling) {
switch(resampling){
case 1:
return gdal_async.GRA_Average;
case 2:
return gdal_async.GRA_Bilinear;
case 3:
return gdal_async.GRA_Cubic;
case 4:
return gdal_async.GRA_CubicSpline;
case 5:
return gdal_async.GRA_Lanczos;
case 6:
return gdal_async.GRA_Mode;
case 7:
return gdal_async.GRA_NearestNeighbor;
default:
return gdal_async.GRA_Cubic;
}
}
function reprojectImage(src_ds, reproject_path, t_epsg, resampling = 1) {
let s_ds;
s_ds = 'string' == typeof src_ds ? gdal_async.open(src_ds) : src_ds;
const s_srs = s_ds.srs;
const t_srs = gdal_async.SpatialReference.fromEPSGA(t_epsg);
const { rasterSize, geoTransform } = gdal_async.suggestedWarpOutput({
src: s_ds,
s_srs,
t_srs
});
const dataType = s_ds.bands.get(1).dataType;
const t_driver = s_ds.driver;
const t_ds = t_driver.create(reproject_path, rasterSize.x, rasterSize.y, s_ds.bands.count(), dataType);
t_ds.srs = t_srs;
t_ds.geoTransform = geoTransform;
const gdal_resampling = getResampling(resampling);
gdal_async.reprojectImage({
src: s_ds,
dst: t_ds,
s_srs,
t_srs,
resampling: gdal_resampling
});
const bands = s_ds.bands.count();
for(let i = 0; i < bands; i++){
const s_band = s_ds.bands.get(i + 1);
const t_band = t_ds.bands.get(i + 1);
t_band.noDataValue = s_band.noDataValue;
s_band.dataType;
}
t_ds.close();
if ('string' == typeof src_ds) s_ds.close();
}
function createLogger(logger = true) {
const log = (msg)=>{
if (logger) console.log(msg);
};
return {
log
};
}
const tileBoundMap = new Map();
tileBoundMap.set(3857, {
xmin: -20037508.342789244,
ymin: -20037508.342789244,
xmax: 20037508.342789244,
ymax: 20037508.342789244
});
tileBoundMap.set(900913, {
xmin: -20037508.342789244,
ymin: -20037508.342789244,
xmax: 20037508.342789244,
ymax: 20037508.342789244
});
tileBoundMap.set(4490, {
xmin: -180,
ymin: -90,
xmax: 180,
ymax: 90
});
tileBoundMap.set(4326, {
xmin: -180,
ymin: -90,
xmax: 180,
ymax: 90
});
function ST_TileEnvelope(z, x, y, offset = 0, bbox = tileBoundMap.get(3857)) {
const tile_size = 256.0;
const boundsWidth = bbox.xmax - bbox.xmin;
const boundsHeight = bbox.ymax - bbox.ymin;
if (boundsWidth <= 0 || boundsHeight <= 0) throw new Error('Geometric bounds are too small');
if (z < 0 || z >= 32) throw new Error(`Invalid tile zoom value, ${z}`);
const worldTileSize = 0x01 << (z > 31 ? 31 : z);
if (x < 0 || x >= worldTileSize) throw new Error(`Invalid tile x value, ${x}`);
if (y < 0 || y >= worldTileSize) throw new Error(`Invalid tile y value, ${y}`);
const tileGeoSizeX = boundsWidth / worldTileSize;
const tileGeoSizeY = boundsHeight / worldTileSize;
const tileGeoSize = Math.max(tileGeoSizeX, tileGeoSizeY);
const x1 = bbox.xmin + tileGeoSize * x - tileGeoSize / tile_size * offset;
const x2 = bbox.xmin + tileGeoSize * (x + 1) + tileGeoSize / tile_size * offset;
const y1 = bbox.ymax - tileGeoSize * (y + 1) - tileGeoSize / tile_size * offset;
const y2 = bbox.ymax - tileGeoSize * y + tileGeoSize / tile_size * offset;
return [
x1,
y1,
x2,
y2
];
}
function getTileByCoors(coord, zoom, bbox = tileBoundMap.get(3857)) {
const left = bbox.xmin;
const top = bbox.ymax;
const _width = coord[0] - left;
const _height = top - coord[1];
const worldTileSize = 0x01 << zoom;
const boundsWidth = bbox.xmax - bbox.xmin;
const boundsHeight = bbox.ymax - bbox.ymin;
const tileGeoSize = Math.max(boundsWidth, boundsHeight) / worldTileSize;
const row = Math.floor(_height / tileGeoSize);
const column = Math.floor(_width / tileGeoSize);
return {
row,
column
};
}
class TIF2Tiles {
input;
output;
options;
queue;
pool;
log = ()=>{};
sourceDs;
projectDs;
projectPath;
encodePath;
tileBoundTool;
statistics = {
tileCount: 0,
completeCount: 0,
levelInfo: {}
};
id = randomUUID();
emitter = new emittery();
constructor(input, output, options){
this.input = input;
this.output = output;
this.options = options;
this.pool = new tinypool({
filename: this.getWorkerPath(),
runtime: 'child_process'
});
this.queue = new p_queue({
concurrency: node_os.cpus().length,
autoStart: false
});
this.sourceDs = null;
this.projectDs = null;
this.projectPath = null;
this.encodePath = null;
this.tileBoundTool = {
xmin: 0,
ymin: 0,
xmax: 0,
ymax: 0
};
const { log } = createLogger(options.log);
this.log = log;
}
getWorkerPath() {
return new URL('./create-tile.js', import.meta.url).href;
}
async recycle() {
const log = this.log;
if (this.sourceDs) {
try {
this.sourceDs.close();
} catch (e) {
log(e);
}
this.sourceDs = null;
}
if (this.projectDs) {
try {
this.projectDs.close();
} catch (e) {
log(e);
}
this.projectDs = null;
}
if (this.projectPath && existsSync(this.projectPath)) {
await promises.rm(this.projectPath, {
recursive: true
});
this.projectPath = null;
}
if (this.encodePath && existsSync(this.encodePath)) {
await promises.rm(this.encodePath, {
recursive: true
});
this.encodePath = null;
}
const ovrPath = `${this.encodePath}.ovr`;
if (existsSync(ovrPath)) await promises.rm(ovrPath, {
recursive: true
});
}
async reproject(ds, epsg, resampling) {
const projectDatasetPath = node_path.join(node_os.tmpdir(), package_namespaceObject.KR, `${this.id}.tif`);
await promises.mkdir(node_path.dirname(projectDatasetPath), {
recursive: true
});
reprojectImage(ds, projectDatasetPath, epsg, resampling);
return projectDatasetPath;
}
buildPyramid(ds, minZoom, resampling) {
const res = ds.geoTransform[1];
const maxPixel = Math.min(ds.rasterSize.x, ds.rasterSize.y);
let overviewNum = 1;
while(maxPixel / 2 ** overviewNum > 256)overviewNum++;
let res_zoom = (this.tileBoundTool.xmax - this.tileBoundTool.xmin) / 256;
let originZ = 0;
while(res_zoom / 2 > res){
res_zoom /= 2;
originZ++;
}
const overviews = [];
for(let zoom = originZ - 1; zoom >= originZ - 1 - overviewNum; zoom--){
if (zoom < minZoom) break;
const factor = 2 ** (originZ - zoom);
overviews.push(factor);
}
const buildOverviewResampling = getBuildOverviewResampling(resampling);
ds.buildOverviews(buildOverviewResampling, overviews);
return {
maxOverViewsZ: originZ - 1,
minOverViewsZ: originZ - overviews.length
};
}
async generateTile() {
const log = this.log;
const { input, output, options } = this;
const type = options.type;
const { minZoom, maxZoom, epsg, encoding, isClean, resampling } = options;
const tileSize = 256;
this.tileBoundTool = tileBoundMap.get(epsg);
const isSavaMbtiles = '.mbtiles' === node_path.extname(output);
let outputDir = output;
if (true === isSavaMbtiles) outputDir = node_path.join(node_os.tmpdir(), this.id);
let stepIndex = 0;
if (isClean) {
if (existsSync(output)) await promises.rm(output, {
recursive: true
});
await promises.mkdir(output, {
recursive: true
});
log(`- \u{6B65}\u{9AA4}${++stepIndex}: \u{6E05}\u{7A7A}\u{8F93}\u{51FA}\u{6587}\u{4EF6}\u{5939} - \u{5B8C}\u{6210}`);
}
this.sourceDs = gdal_async.open(input, 'r');
if (this.sourceDs.srs?.getAuthorityCode() !== epsg) {
this.projectPath = await this.reproject(this.sourceDs, epsg, resampling);
this.projectDs = gdal_async.open(this.projectPath, 'r+');
this.sourceDs.close();
log(`- \u{6B65}\u{9AA4}${++stepIndex}: \u{91CD}\u{6295}\u{5F71}\u{81F3} EPSG:${epsg} - \u{5B8C}\u{6210}`);
} else this.projectDs = this.sourceDs;
this.sourceDs = null;
const overViewInfo = this.buildPyramid(this.projectDs, minZoom, resampling);
log(`- \u{6B65}\u{9AA4}${++stepIndex}: \u{6784}\u{5EFA}\u{5F71}\u{50CF}\u{91D1}\u{5B57}\u{5854}\u{7D22}\u{5F15} - \u{5B8C}\u{6210}`);
const projectDs = this.projectDs;
const geoTransform = projectDs.geoTransform;
const dsInfo = {
width: projectDs.rasterSize.x,
height: projectDs.rasterSize.y,
resX: geoTransform[1],
resY: geoTransform[5],
startX: geoTransform[0],
startY: geoTransform[3],
endX: geoTransform[0] + projectDs.rasterSize.x * geoTransform[1],
endY: geoTransform[3] + projectDs.rasterSize.y * geoTransform[5],
path: projectDs.description
};
let tileCount = 0;
let miny;
let maxy;
if (dsInfo.startY < dsInfo.endY) {
miny = dsInfo.startY;
maxy = dsInfo.endY;
} else {
miny = dsInfo.endY;
maxy = dsInfo.startY;
}
const startPoint = [
dsInfo.startX,
maxy
];
const endPoint = [
dsInfo.endX,
miny
];
for(let tz = minZoom; tz <= maxZoom; ++tz){
const minRC = getTileByCoors(startPoint, tz, this.tileBoundTool);
const maxRC = getTileByCoors(endPoint, tz, this.tileBoundTool);
this.statistics.tileCount += (maxRC.row - minRC.row + 1) * (maxRC.column - minRC.column + 1);
this.statistics.levelInfo[tz] = {
tminx: minRC.column,
tminy: minRC.row,
tmaxx: maxRC.column,
tmaxy: maxRC.row
};
}
const buffer = 1;
let outTileSize = tileSize;
if ('mapbox' === encoding) outTileSize = tileSize + 2 * buffer;
if ('dom' === type) outTileSize = tileSize;
let spinner;
spinner = options.log ? yocto_spinner({
text: `\u{6B65}\u{9AA4}${++stepIndex}: \u{5F00}\u{59CB}\u{51C6}\u{5907}\u{5207}\u{7247}\u{961F}\u{5217} - \u{5B8C}\u{6210}`
}).start() : {
text: '',
error: ()=>{},
success: ()=>{}
};
const queue = this.queue;
const jobs = [];
queue.on('completed', (result)=>{
jobs.push(result);
const percent = Math.floor(jobs.length / tileCount * 100);
spinner.text = `\u{5207}\u{7247}\u{8FDB}\u{5EA6}: ${percent}%`;
this.emitter.emit('completed', {
id: this.id,
tileCount,
createTileCount: jobs.length
});
});
queue.on('idle', async ()=>{
if (jobs.length !== tileCount) spinner.error(`\u{6B65}\u{9AA4}${++stepIndex}: \u{5207}\u{7247} - \u{5931}\u{8D25}`);
if (jobs.length === tileCount) spinner.success(`\u{6B65}\u{9AA4}${++stepIndex}: \u{5207}\u{7247} - \u{5B8C}\u{6210}`);
this.resetStats();
await this.pool.destroy();
await this.recycle();
this.emitter.emit('idle', {
id: this.id,
tileCount,
createTileCount: jobs.length
});
});
for(let tz = minZoom; tz <= maxZoom; tz++){
const { tminx, tminy, tmaxx, tmaxy } = this.statistics.levelInfo[tz];
let overviewInfo;
if (tz > overViewInfo.maxOverViewsZ) overviewInfo = dsInfo;
else {
const startZ = Math.max(tz, overViewInfo.minOverViewsZ);
const factorZoom = overViewInfo.maxOverViewsZ - startZ;
const factor = 2 ** (factorZoom + 1);
overviewInfo = {
index: factorZoom,
startX: dsInfo.startX,
startY: dsInfo.startY,
width: Math.ceil(dsInfo.width / factor),
height: Math.ceil(dsInfo.height / factor),
resX: dsInfo.resX * factor,
resY: dsInfo.resY * factor
};
}
for(let j = tminx; j <= tmaxx; j++){
await promises.mkdir(node_path.join(outputDir, tz.toString(), j.toString()), {
recursive: true
});
for(let i = tminy; i <= tmaxy; i++){
const tileBound = ST_TileEnvelope(tz, j, i, buffer, this.tileBoundTool);
const { rb, wb } = geoQuery(overviewInfo, tileBound[0], tileBound[3], tileBound[2], tileBound[1], outTileSize);
const createInfo = {
outTileSize,
overviewInfo,
rb,
wb,
encoding,
dsPath: dsInfo.path,
x: j,
y: i,
z: tz,
outputTile: outputDir,
type
};
tileCount++;
queue.add(()=>this.pool.run(createInfo));
}
}
}
log(`
- \u{6B65}\u{9AA4}${++stepIndex}: \u{751F}\u{6210}\u{5207}\u{7247}\u{4EFB}\u{52A1}\u{961F}\u{5217}: ${tileCount} - \u{5B8C}\u{6210}`);
queue.start();
}
resetStats() {
this.statistics.tileCount = 0;
this.statistics.completeCount = 0;
this.statistics.levelInfo = {};
}
}
function geoQuery(overviewInfo, ulx, uly, lrx, lry, querysize = 0) {
const { startX, startY, width, height, resX, resY } = overviewInfo;
let rx = Math.floor((ulx - startX) / resX + 0.001);
let ry = Math.floor((uly - startY) / resY + 0.001);
let rxsize = Math.max(1, Math.floor((lrx - ulx) / resX + 0.5));
let rysize = Math.max(1, Math.floor((lry - uly) / resY + 0.5));
let wxsize, wysize;
if (querysize) {
wxsize = querysize;
wysize = querysize;
} else {
wxsize = rxsize;
wysize = rysize;
}
let wx = 0;
if (rx < 0) {
const rxshift = Math.abs(rx);
wx = Math.floor(rxshift / rxsize * wxsize);
wxsize -= wx;
rxsize -= Math.floor(rxshift / rxsize * rxsize);
rx = 0;
}
if (rx + rxsize > width) {
wxsize = Math.floor(wxsize * (width - rx) * 1.0 / rxsize);
rxsize = width - rx;
}
let wy = 0;
if (ry < 0) {
const ryshift = Math.abs(ry);
wy = Math.floor(ryshift / rysize * wysize);
wysize -= wy;
rysize -= Math.floor(ryshift / rysize * rysize);
ry = 0;
}
if (ry + rysize > height) {
wysize = Math.floor(wysize * (height - ry) * 1.0 / rysize);
rysize = height - ry;
}
return {
rb: {
rx,
ry,
rxsize,
rysize
},
wb: {
wx,
wy,
wxsize,
wysize
}
};
}
export { TIF2Tiles };