UNPKG

@deepgis/dem-dynamic-terrain

Version:

使用 GDAL 制作地形瓦片,支持 mapbox 和 terrarium 两种编码输出格式

418 lines (417 loc) 16.1 kB
import * as __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__ from "node:fs"; import * as __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__ from "node:fs/promises"; import * as __WEBPACK_EXTERNAL_MODULE_node_os_74b4b876__ from "node:os"; import * as __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__ from "node:path"; import * as __WEBPACK_EXTERNAL_MODULE_p_queue_c587facd__ from "p-queue"; import * as __WEBPACK_EXTERNAL_MODULE_yocto_spinner_579f0326__ from "yocto-spinner"; import * as __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__ from "gdal-async"; import * as __WEBPACK_EXTERNAL_MODULE_tinypool__ from "tinypool"; import * as __WEBPACK_EXTERNAL_MODULE_node_crypto_9ba42079__ 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 __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_Average; case 2: return __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_Bilinear; case 3: return __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_Cubic; case 4: return __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_CubicSpline; case 5: return __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_Lanczos; case 6: return __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_Mode; case 7: return __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_NearestNeighbor; default: return __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].GRA_Cubic; } } function reprojectImage(src_ds, reproject_path, t_epsg, resampling = 1) { let s_ds; s_ds = 'string' == typeof src_ds ? __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].open(src_ds) : src_ds; const s_srs = s_ds.srs; const t_srs = __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].SpatialReference.fromEPSGA(t_epsg); const { rasterSize, geoTransform } = __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].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); __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].reprojectImage({ src: s_ds, dst: t_ds, s_srs, t_srs, resampling: gdal_resampling }); t_ds.bands.get(1).noDataValue = s_ds.bands.get(1).noDataValue; t_ds.close(); if ('string' == typeof src_ds) s_ds.close(); } 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 }; } const pool = new __WEBPACK_EXTERNAL_MODULE_tinypool__["default"]({ filename: new URL("./create-tile.js", import.meta.url).href, runtime: "child_process" }); const statistics = { tileCount: 0, completeCount: 0, levelInfo: {} }; let sourceDs; let projectDs; let projectPath; let encodePath; let tileBoundTool; async function recycle() { if (sourceDs) { try { sourceDs.close(); } catch (e) { console.error(e); } sourceDs = null; } if (projectDs) { try { projectDs.close(); } catch (e) { console.error(e); } projectDs = null; } if (projectPath && (0, __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__.existsSync)(projectPath)) { await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rm(projectPath, { recursive: true }); projectPath = null; } if (encodePath && (0, __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__.existsSync)(encodePath)) { await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rm(encodePath, { recursive: true }); encodePath = null; } const ovrPath = `${encodePath}.ovr`; if ((0, __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__.existsSync)(ovrPath)) await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rm(ovrPath, { recursive: true }); } async function reproject(ds, epsg, resampling) { const projectDatasetPath = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(__WEBPACK_EXTERNAL_MODULE_node_os_74b4b876__["default"].tmpdir(), package_namespaceObject.KR, `${(0, __WEBPACK_EXTERNAL_MODULE_node_crypto_9ba42079__.randomUUID)()}.tif`); await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].mkdir(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].dirname(projectDatasetPath), { recursive: true }); reprojectImage(ds, projectDatasetPath, epsg, resampling); return projectDatasetPath; } function 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 = (tileBoundTool.xmax - 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 function generateTile(input, output, options) { const { minZoom, maxZoom, epsg, encoding, isClean, resampling } = options; const tileSize = 256; tileBoundTool = tileBoundMap.get(epsg); const isSavaMbtiles = ".mbtiles" === __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].extname(output); let outputDir = output; if (true === isSavaMbtiles) outputDir = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(__WEBPACK_EXTERNAL_MODULE_node_os_74b4b876__["default"].tmpdir(), (0, __WEBPACK_EXTERNAL_MODULE_node_crypto_9ba42079__.randomUUID)()); let stepIndex = 0; if (isClean) { if ((0, __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__.existsSync)(output)) await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].rm(output, { recursive: true }); await __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].mkdir(output, { recursive: true }); console.log(`- 步骤${++stepIndex}: 清空输出文件夹 - 完成`); } sourceDs = __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].open(input, "r"); if (sourceDs.srs?.getAuthorityCode() !== epsg) { projectPath = await reproject(sourceDs, epsg, resampling); projectDs = __WEBPACK_EXTERNAL_MODULE_gdal_async_691d88a0__["default"].open(projectPath, "r+"); sourceDs.close(); console.log(`- 步骤${++stepIndex}: 重投影至 EPSG:${epsg} - 完成`); } else projectDs = sourceDs; sourceDs = null; const overViewInfo = buildPyramid(projectDs, minZoom, resampling); console.log(`- 步骤${++stepIndex}: 构建影像金字塔索引 - 完成`); 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 pileUpCount = 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, tileBoundTool); const maxRC = getTileByCoors(endPoint, tz, tileBoundTool); statistics.tileCount += (maxRC.row - minRC.row + 1) * (maxRC.column - minRC.column + 1); 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; const spinner = (0, __WEBPACK_EXTERNAL_MODULE_yocto_spinner_579f0326__["default"])({ text: `步骤${++stepIndex}: 开始准备切片队列 - 完成` }).start(); const queue = new __WEBPACK_EXTERNAL_MODULE_p_queue_c587facd__["default"]({ concurrency: __WEBPACK_EXTERNAL_MODULE_node_os_74b4b876__["default"].cpus().length, autoStart: false }); const jobs = []; queue.on("completed", (result)=>{ jobs.push(result); const percent = Math.floor(jobs.length / pileUpCount * 100); spinner.text = `切片进度: ${percent}%`; }); queue.on("idle", async (error)=>{ if (jobs.length !== pileUpCount) spinner.error(`步骤${++stepIndex}: 切片 - 失败`); if (jobs.length === pileUpCount) spinner.success(`步骤${++stepIndex}: 切片 - 完成`); await pool.destroy(); await recycle(); resetStats(); }); for(let tz = minZoom; tz <= maxZoom; tz++){ const { tminx, tminy, tmaxx, tmaxy } = 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 __WEBPACK_EXTERNAL_MODULE_node_fs_promises_153e37e0__["default"].mkdir(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(outputDir, tz.toString(), j.toString()), { recursive: true }); for(let i = tminy; i <= tmaxy; i++){ const tileBound = ST_TileEnvelope(tz, j, i, buffer, 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 }; pileUpCount++; queue.add(()=>pool.run(createInfo)); } } } console.log(`\n- 步骤${++stepIndex}: 生成切片任务队列: ${pileUpCount} - 完成`); queue.start(); } function resetStats() { statistics.tileCount = 0; statistics.completeCount = 0; 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 } }; } const src_rslib_entry_ = generateTile; export { src_rslib_entry_ as default, statistics };