/** * T21 地形与本地影像(配置驱动)纯逻辑检查。 * * 覆盖:`map.terrain` / `map.overlays` 的归一化与计划层;以及在**本机**数据存在时, * 让配置与磁盘上的真实数据对齐核对(layer.json 的 scheme/投影、meta.json 的范围与配置里的 bounds)。 * 数据目录不存在时(别的机器)自动跳过对齐核对,只跑纯逻辑断言。 * * 运行:node .scratch/cockpit-shell-panel-layout/verify/t21-terrain.mjs */ import assert from 'node:assert/strict' import { existsSync, readFileSync } from 'node:fs' import { planOverlays, planTerrain, terrainStatusText, TERRAIN_STATE } from '../../../src/modules/cockpit/domain/mapLayers.js' import { builtinConfig, normalizeConfig } from '../../../src/modules/cockpit/services/cockpitConfig.js' // ---------------------------------------------------------------- 1. 默认不配地形/叠加 const defaults = builtinConfig() assert.equal(defaults.map.terrain.url, '', '默认不配地形') assert.equal(defaults.map.terrain.requestVertexNormals, true, '默认要顶点法线(有光照才立体)') assert.deepEqual(defaults.map.overlays, [], '默认没有叠加影像') assert.equal(planTerrain(defaults.map).state, TERRAIN_STATE.NONE, '未配置时地形状态是 none') assert.equal(planTerrain(defaults.map).ready, false, '未配置时不可用') assert.match(terrainStatusText(null), /椭球/, '未配置时的措辞应说明退化成椭球') // ---------------------------------------------------------------- 2. 归一化 const normalized = normalizeConfig({ map: { terrain: { url: ' /gis/dem ', requestVertexNormals: false }, overlays: [ { url: ' /gis/pic3/{z}/{x}/{reverseY}.png ', minimumLevel: 0, maximumLevel: 19, bounds: [109.9, 27.9, 111.1, 29.1], credit: ' 本地影像 ' }, { url: '', minimumLevel: 'x', bounds: [1, 2, 3] }, ], }, }) assert.equal(normalized.config.map.terrain.url, '/gis/dem', '地形地址应去空白') assert.equal(normalized.config.map.terrain.requestVertexNormals, false, '显式关顶点法线应被接受') assert.equal(normalized.config.map.overlays.length, 2, '两个叠加项都保留(空 url 由计划层过滤)') assert.equal(normalized.config.map.overlays[0].url, '/gis/pic3/{z}/{x}/{reverseY}.png', '叠加模板应去空白') assert.deepEqual(normalized.config.map.overlays[0].bounds, [109.9, 27.9, 111.1, 29.1], '范围应透传') assert.equal(normalized.config.map.overlays[0].credit, '本地影像', '署名应去空白') assert.equal(normalized.config.map.overlays[1].minimumLevel, null, '非整数级别应被忽略') assert.equal(normalized.config.map.overlays[1].bounds, null, '长度不对的 bounds 应被忽略') assert.ok( normalized.issues.some((issue) => /minimumLevel/.test(issue)) && normalized.issues.some((issue) => /bounds/.test(issue)), `非法级别与非法范围应各记一条 issue,实际 ${JSON.stringify(normalized.issues)}`, ) const badTypes = normalizeConfig({ map: { terrain: { url: 123, requestVertexNormals: 'yes' }, overlays: 'nope' } }) assert.equal(badTypes.config.map.terrain.url, '', '非字符串地形地址应退回默认') assert.equal(badTypes.config.map.terrain.requestVertexNormals, true, '非布尔开关应退回默认') assert.deepEqual(badTypes.config.map.overlays, [], '非数组 overlays 应退回空') assert.equal(badTypes.issues.length, 3, `三类错各记一条,实际 ${JSON.stringify(badTypes.issues)}`) // ---------------------------------------------------------------- 3. 计划层 const terrainPlan = planTerrain({ terrain: { url: '/gis/dem' } }) assert.equal(terrainPlan.state, TERRAIN_STATE.READY, '配了地址即为 ready') assert.equal(terrainPlan.requestVertexNormals, true, '默认带顶点法线') assert.match(terrainStatusText(terrainPlan), /dem/, '措辞应能看出用的是哪个目录') const overlays = planOverlays({ overlays: [ { url: 'a/{z}/{x}/{reverseY}.png', minimumLevel: 0, maximumLevel: 19, bounds: [1, 2, 3, 4], credit: 'x' }, { url: '' }, { url: 'b/{z}/{x}/{y}.png', bounds: [1, 2, 3] }, ], }) assert.equal(overlays.length, 2, '空 url 应被过滤') assert.equal(overlays[0].bounds.length, 4, '合法 bounds 应保留') assert.equal(overlays[1].bounds, null, '非法 bounds 应为 null(引擎侧不设 rectangle)') assert.equal(overlays[0].maximumLevel, 19, '级别应透传') // ---------------------------------------------------------------- 4. 与磁盘真实数据对齐(仅本机有数据时) const configPath = new URL('../../../public/cockpit/config.json', import.meta.url) if (existsSync(configPath)) { const raw = JSON.parse(readFileSync(configPath, 'utf8')) const { config } = normalizeConfig(raw) const terrainDir = config.map.terrain.url // 配置里的地形指向的目录里应真有 layer.json,且是量化网格 + EPSG:4326 const mounts = (process.env.COCKPIT_GIS_MOUNTS || '').includes('yuanlingDEM') if (mounts) { const demDir = (process.env.COCKPIT_GIS_MOUNTS.match(/"dir":"([^"]*yuanlingDEM)"/) || [])[1] if (demDir && existsSync(`${demDir}/layer.json`)) { const layer = JSON.parse(readFileSync(`${demDir}/layer.json`, 'utf8')) assert.equal(layer.format, 'quantized-mesh-1.0', '地形应是量化网格') assert.equal(layer.projection, 'EPSG:4326', '地形应是地理坐标(global-geodetic)') assert.equal(layer.scheme, 'tms', '地形 layer.json 声明 tms —— Cesium 认这个字段,服务端不用翻 y') assert.ok(layer.extensions.includes('octvertexnormals'), '地形带八面体顶点法线(对应 requestVertexNormals)') assert.equal(config.map.terrain.url, '/gis/dem', '配置里的地形 URL 应与挂载前缀一致') } const picDir = (process.env.COCKPIT_GIS_MOUNTS.match(/"dir":"([^"]*yuanlingPic3)"/) || [])[1] if (picDir && existsSync(`${picDir}/meta.json`)) { const meta = JSON.parse(readFileSync(`${picDir}/meta.json`, 'utf8')) assert.equal(meta.tiletrans, 'tms', '影像是 TMS —— 模板必须用 {reverseY}') assert.equal(meta.proj, 3857, '影像是 Web Mercator') assert.match(config.map.overlays[0].url, /\{reverseY\}/, '叠加模板应使用 {reverseY} 以匹配 TMS') const [w, s, e, n] = config.map.overlays[0].bounds assert.ok( Math.abs(w - meta.bounds.west) < 1e-6 && Math.abs(s - meta.bounds.south) < 1e-6 && Math.abs(e - meta.bounds.east) < 1e-6 && Math.abs(n - meta.bounds.north) < 1e-6, '配置里的 bounds 应与 meta.json 一致(范围外不请求瓦片)', ) console.log(' (已按本机数据核对:layer.json 的 tms/EPSG:4326/顶点法线,meta.json 的 tms/3857/bounds)') } } } console.log( 'T21 检查通过:地形与叠加影像的归一化 + 计划层(默认不配、缺字段忽略、bounds/级别校验),并在本机有数据时把配置与 layer.json / meta.json 对齐核对。', )