| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- /**
- * GIS 分级资产裁剪(同心 LOD)—— 从原始切片目录生成驾驶舱用的分级静态资产。
- *
- * 用法:node tools/gis-asset/crop-gis-lod.mjs [--src D:/GIS/GIS] [--out D:/GIS/cockpit-gis]
- *
- * 设计(维护者 2026-11-03):
- * 主场景边缘曾出现一圈低清影像(z19 直落 z14,32 倍骤降且无雾遮挡)。
- * 修复 = 同心分级,清晰度随距离递减,衔接落差降到 2~8 倍:
- * ring0 主场景 bbox z0..19(0.26 m/px,城市细节)
- * ring1 主场景外扩 R1=3 km z15..17(1.05 m/px)
- * ring2 R1..R2=8 km z15(4.19 m/px)
- * ring3 影像数据全范围 z0..14(8.39 m/px;覆盖到数据边界,让雾边界内外都是真实影像)
- * 地形 DEM 整份保留:低层级是全球覆盖,按兴趣区裁剪会在地球上留下空洞(v3 教训)。
- *
- * 配套契约(public/cockpit/config.json):
- * overlays 按「外→内」顺序声明四层,后加者在上面;
- * 每层 maximumLevel 与数据一致(z14/z15/z17/z19)——某层缺高层时 Cesium 自动用
- * 该层已有瓦片向上放大,**不会露底**;若把 maxLevel 声明得比数据高,缺瓦片的
- * 层级会 404 并露出更底的底图(v4 教训:绿带就是这么来的)。
- *
- * 防呆(v5 增补):
- * · 源层级白名单:ring3 只认 z0..14,绝不触碰 z15+(曾误拷 52GB / 41.7GB 后才发现);
- * · 输出前总量断言:瓦片数超出预算即中止,防止配置改错后无声吞盘。
- */
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
- import path from 'node:path'
- const argv = process.argv
- const readOpt = (name, fallback) => {
- const i = argv.indexOf(name)
- return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback
- }
- const SRC = readOpt('--src', 'D:/GIS/GIS')
- const OUT = readOpt('--out', 'D:/GIS/cockpit-gis')
- const PIC_SRC = path.join(SRC, 'yuanlingPic3')
- const DEM_SRC = path.join(SRC, 'yuanlingDEM')
- // —— 契约参数(与 public/cockpit/config.json 同源;换部署按自己的 primaryArea/home/fog 重算)——
- const PRIMARY = [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907]
- const HOME = [110.39028164, 28.4436787]
- const FOG_RADIUS_KM = 42 // 与 map.fog.endDistance 同源
- const RING1_KM = 3
- const RING2_KM = 8
- const FINE_MAX_Z = 19
- const RING1_MAX_Z = 17
- const MID_MAX_Z = 15
- const OUTER_MAX_Z = 14
- const DEM_MAX_Z = 14 // 源 DEM 的 maxzoom(layer.json)
- const TILE_BUDGET = 80_000 // 影像总瓦片预算(超出即中止,防止误配吞盘)
- const KM_LAT = 111.32
- const expand = (b, km) => {
- const dLat = km / KM_LAT
- const dLon = km / (KM_LAT * Math.cos(HOME[1] * Math.PI / 180))
- return [b[0] - dLon, b[1] - dLat, b[2] + dLon, b[3] + dLat]
- }
- const RING1 = expand(PRIMARY, RING1_KM)
- const RING2 = expand(PRIMARY, RING2_KM)
- const FOG = expand([HOME[0], HOME[1], HOME[0], HOME[1]], FOG_RADIUS_KM)
- const lon2x = (lon, z) => Math.floor((lon + 180) / 360 * 2 ** z)
- const lat2yXyz = (lat, z) => {
- const r = lat * Math.PI / 180
- return Math.floor((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2 * 2 ** z)
- }
- const yTms = (lat, z) => (2 ** z - 1) - lat2yXyz(lat, z)
- const box = (b, z) => {
- const a = yTms(b[3], z), c = yTms(b[1], z)
- return { x0: lon2x(b[0], z), x1: lon2x(b[2], z), y0: Math.min(a, c), y1: Math.max(a, c) }
- }
- const inBox = (bx, x, y) => x >= bx.x0 && x <= bx.x1 && y >= bx.y0 && y <= bx.y1
- /** 复制 outer 内、排除 excludes 各盒的瓦片;zMin..zMax 逐层。 */
- function copyRing(srcDir, dstDir, outer, excludes, zMin, zMax, label) {
- let files = 0, bytes = 0
- for (let z = zMin; z <= zMax; z++) {
- const o = box(outer, z)
- const ex = excludes.map((b) => box(b, z))
- let dirs = []
- try { dirs = readdirSync(path.join(srcDir, String(z))) } catch { continue }
- let zf = 0
- for (const xs of dirs) {
- const xi = Number(xs)
- if (!Number.isFinite(xi) || xi < o.x0 || xi > o.x1) continue
- let ys = []
- try { ys = readdirSync(path.join(srcDir, String(z), xs)) } catch { continue }
- for (const yf of ys) {
- const yi = Number(yf.replace('.png', ''))
- if (!Number.isFinite(yi) || yi < o.y0 || yi > o.y1) continue
- if (ex.some((bx) => inBox(bx, xi, yi))) continue
- const dst = path.join(dstDir, String(z), xs, yf)
- mkdirSync(path.dirname(dst), { recursive: true })
- copyFileSync(path.join(srcDir, String(z), xs, yf), dst)
- files++; zf++
- try { bytes += statSync(path.join(srcDir, String(z), xs, yf)).size } catch {}
- }
- }
- if (zf) console.log(` ${label} z${z}: ${zf}`)
- }
- return { files, bytes }
- }
- /** 地形整份拷贝(含 layer.json 原样)。 */
- function copyDemWhole() {
- let files = 0, bytes = 0
- const dstRoot = path.join(OUT, 'dem')
- for (const z of readdirSync(DEM_SRC).filter((n) => /^\d+$/.test(n))) {
- let xdirs = []
- try { xdirs = readdirSync(path.join(DEM_SRC, z)) } catch { continue }
- for (const xs of xdirs) {
- let ys = []
- try { ys = readdirSync(path.join(DEM_SRC, z, xs)) } catch { continue }
- for (const yf of ys) {
- const src = path.join(DEM_SRC, z, xs, yf)
- const dst = path.join(dstRoot, z, xs, yf)
- mkdirSync(path.dirname(dst), { recursive: true })
- copyFileSync(src, dst)
- files++
- try { bytes += statSync(src).size } catch {}
- }
- }
- }
- for (const name of readdirSync(DEM_SRC)) {
- if (/^\d+$/.test(name)) continue
- const src = path.join(DEM_SRC, name)
- try {
- if (statSync(src).isFile()) {
- mkdirSync(dstRoot, { recursive: true })
- copyFileSync(src, path.join(dstRoot, name))
- files++; bytes += statSync(src).size
- }
- } catch {}
- }
- return { files, bytes }
- }
- if (!existsSync(PIC_SRC) || !existsSync(DEM_SRC)) {
- console.error(`源目录不存在:${PIC_SRC} / ${DEM_SRC}`)
- process.exit(1)
- }
- console.log(`源:${SRC}\n产出:${OUT}`)
- console.log(`主场景:${PRIMARY.map((v) => v.toFixed(4)).join(', ')}`)
- console.log(`环1(+${RING1_KM}km) / 环2(+${RING2_KM}km) / 雾范围(${FOG_RADIUS_KM}km)\n`)
- console.log('1/5 ring0 主场景 z0-19 ...')
- const r0 = copyRing(PIC_SRC, path.join(OUT, 'pic-ring0'), PRIMARY, [], 0, FINE_MAX_Z, 'ring0')
- console.log(` ring0: ${r0.files} 文件 ${(r0.bytes / 1048576).toFixed(1)} MB`)
- console.log(`2/5 ring1 主场景..+${RING1_KM}km z15-17 ...`)
- const r1 = copyRing(PIC_SRC, path.join(OUT, 'pic-ring1'), RING1, [PRIMARY], 15, RING1_MAX_Z, 'ring1')
- console.log(` ring1: ${r1.files} 文件 ${(r1.bytes / 1048576).toFixed(1)} MB`)
- console.log(`3/5 ring2 +${RING1_KM}..+${RING2_KM}km z15 ...`)
- const r2 = copyRing(PIC_SRC, path.join(OUT, 'pic-ring2'), RING2, [RING1], MID_MAX_Z, MID_MAX_Z, 'ring2')
- console.log(` ring2: ${r2.files} 文件 ${(r2.bytes / 1048576).toFixed(1)} MB`)
- console.log('4/5 ring3 数据全范围 z0-14(严格层级白名单)...')
- let r3 = { files: 0, bytes: 0 }
- {
- const allowed = new Set(Array.from({ length: OUTER_MAX_Z + 1 }, (_, i) => String(i)))
- const dst = path.join(OUT, 'pic-ring3')
- for (const z of readdirSync(PIC_SRC)) {
- if (!allowed.has(z)) continue
- let xdirs = []
- try { xdirs = readdirSync(path.join(PIC_SRC, z)) } catch { continue }
- for (const xs of xdirs) {
- let ys = []
- try { ys = readdirSync(path.join(PIC_SRC, z, xs)) } catch { continue }
- for (const yf of ys) {
- const s = path.join(PIC_SRC, z, xs, yf)
- const d = path.join(dst, z, xs, yf)
- mkdirSync(path.dirname(d), { recursive: true })
- copyFileSync(s, d)
- r3.files++
- try { r3.bytes += statSync(s).size } catch {}
- }
- }
- }
- }
- console.log(` ring3: ${r3.files} 文件 ${(r3.bytes / 1048576).toFixed(1)} MB`)
- console.log('5/5 地形整份保留 ...')
- const dem = copyDemWhole()
- console.log(` 地形:${dem.files} 文件 ${(dem.bytes / 1048576).toFixed(1)} MB(layer.json 原样)`)
- // 预算断言:防误配吞盘
- const imgTiles = r0.files + r1.files + r2.files + r3.files
- if (imgTiles > TILE_BUDGET) {
- console.error(`影像瓦片 ${imgTiles.toLocaleString()} 超预算 ${TILE_BUDGET.toLocaleString()} —— 配置疑似有误,请检查后重跑(产出保留在 ${OUT},可手动删除)`)
- process.exit(1)
- }
- // 各层 meta(描述用)
- const picMeta = JSON.parse(readFileSync(path.join(PIC_SRC, 'meta.json'), 'utf8'))
- const metas = [
- ['pic-ring0', PRIMARY, FINE_MAX_Z],
- ['pic-ring1', RING1, RING1_MAX_Z],
- ['pic-ring2', RING2, MID_MAX_Z],
- ['pic-ring3', JSON.parse(readFileSync(path.join(PIC_SRC, 'meta.json'), 'utf8')).latLonBounds
- ? [
- picMeta.bounds.west, picMeta.bounds.south, picMeta.bounds.east, picMeta.bounds.north,
- ] : FOG, OUTER_MAX_Z],
- ]
- for (const [name, b, zmax] of metas) {
- writeFileSync(path.join(OUT, `${name}-meta.json`), JSON.stringify({
- bounds: { west: b[0], south: b[1], east: b[2], north: b[3] },
- minzoom: 0, maxzoom: zmax, proj: 3857, tilesize: picMeta.tilesize, tiletrans: 'tms', type: 'image', extension: 'png',
- }, null, 2))
- }
- const allBytes = r0.bytes + r1.bytes + r2.bytes + r3.bytes + dem.bytes
- console.log('\n================ 汇总 ================')
- console.log(`ring0 ${r0.files}(${(r0.bytes / 1048576).toFixed(0)}MB) ring1 ${r1.files}(${(r1.bytes / 1048576).toFixed(0)}MB) ring2 ${r2.files}(${(r2.bytes / 1048576).toFixed(0)}MB) ring3 ${r3.files}(${(r3.bytes / 1048576).toFixed(0)}MB) dem ${dem.files}(${(dem.bytes / 1048576).toFixed(0)}MB)`)
- console.log(`合计 ${(imgTiles + dem.files).toLocaleString()} 文件 ${(allBytes / 1048576).toFixed(0)} MB`)
- console.log('\n配套 config.json overlays(外→内叠加,后加者在上):')
- console.log(' pic-ring3 z0-14 bounds=影像全范围')
- console.log(' pic-ring2 z15 bounds=主场景+8km')
- console.log(' pic-ring1 z15-17 bounds=主场景+3km')
- console.log(' pic-ring0 z0-19 bounds=主场景')
- console.log(' terrain url 指向本脚本产出的 dem/ 目录')
|