t21-terrain.mjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /**
  2. * T21 地形与本地影像(配置驱动)纯逻辑检查。
  3. *
  4. * 覆盖:`map.terrain` / `map.overlays` 的归一化与计划层;以及在**本机**数据存在时,
  5. * 让配置与磁盘上的真实数据对齐核对(layer.json 的 scheme/投影、meta.json 的范围与配置里的 bounds)。
  6. * 数据目录不存在时(别的机器)自动跳过对齐核对,只跑纯逻辑断言。
  7. *
  8. * 运行:node .scratch/cockpit-shell-panel-layout/verify/t21-terrain.mjs
  9. */
  10. import assert from 'node:assert/strict'
  11. import { existsSync, readFileSync } from 'node:fs'
  12. import { planOverlays, planTerrain, terrainStatusText, TERRAIN_STATE } from '../../../src/modules/cockpit/domain/mapLayers.js'
  13. import { builtinConfig, normalizeConfig } from '../../../src/modules/cockpit/services/cockpitConfig.js'
  14. // ---------------------------------------------------------------- 1. 默认不配地形/叠加
  15. const defaults = builtinConfig()
  16. assert.equal(defaults.map.terrain.url, '', '默认不配地形')
  17. assert.equal(defaults.map.terrain.requestVertexNormals, true, '默认要顶点法线(有光照才立体)')
  18. assert.deepEqual(defaults.map.overlays, [], '默认没有叠加影像')
  19. assert.equal(planTerrain(defaults.map).state, TERRAIN_STATE.NONE, '未配置时地形状态是 none')
  20. assert.equal(planTerrain(defaults.map).ready, false, '未配置时不可用')
  21. assert.match(terrainStatusText(null), /椭球/, '未配置时的措辞应说明退化成椭球')
  22. // ---------------------------------------------------------------- 2. 归一化
  23. const normalized = normalizeConfig({
  24. map: {
  25. terrain: { url: ' /gis/dem ', requestVertexNormals: false },
  26. overlays: [
  27. { url: ' /gis/pic3/{z}/{x}/{reverseY}.png ', minimumLevel: 0, maximumLevel: 19, bounds: [109.9, 27.9, 111.1, 29.1], credit: ' 本地影像 ' },
  28. { url: '', minimumLevel: 'x', bounds: [1, 2, 3] },
  29. ],
  30. },
  31. })
  32. assert.equal(normalized.config.map.terrain.url, '/gis/dem', '地形地址应去空白')
  33. assert.equal(normalized.config.map.terrain.requestVertexNormals, false, '显式关顶点法线应被接受')
  34. assert.equal(normalized.config.map.overlays.length, 2, '两个叠加项都保留(空 url 由计划层过滤)')
  35. assert.equal(normalized.config.map.overlays[0].url, '/gis/pic3/{z}/{x}/{reverseY}.png', '叠加模板应去空白')
  36. assert.deepEqual(normalized.config.map.overlays[0].bounds, [109.9, 27.9, 111.1, 29.1], '范围应透传')
  37. assert.equal(normalized.config.map.overlays[0].credit, '本地影像', '署名应去空白')
  38. assert.equal(normalized.config.map.overlays[1].minimumLevel, null, '非整数级别应被忽略')
  39. assert.equal(normalized.config.map.overlays[1].bounds, null, '长度不对的 bounds 应被忽略')
  40. assert.ok(
  41. normalized.issues.some((issue) => /minimumLevel/.test(issue)) && normalized.issues.some((issue) => /bounds/.test(issue)),
  42. `非法级别与非法范围应各记一条 issue,实际 ${JSON.stringify(normalized.issues)}`,
  43. )
  44. const badTypes = normalizeConfig({ map: { terrain: { url: 123, requestVertexNormals: 'yes' }, overlays: 'nope' } })
  45. assert.equal(badTypes.config.map.terrain.url, '', '非字符串地形地址应退回默认')
  46. assert.equal(badTypes.config.map.terrain.requestVertexNormals, true, '非布尔开关应退回默认')
  47. assert.deepEqual(badTypes.config.map.overlays, [], '非数组 overlays 应退回空')
  48. assert.equal(badTypes.issues.length, 3, `三类错各记一条,实际 ${JSON.stringify(badTypes.issues)}`)
  49. // ---------------------------------------------------------------- 3. 计划层
  50. const terrainPlan = planTerrain({ terrain: { url: '/gis/dem' } })
  51. assert.equal(terrainPlan.state, TERRAIN_STATE.READY, '配了地址即为 ready')
  52. assert.equal(terrainPlan.requestVertexNormals, true, '默认带顶点法线')
  53. assert.match(terrainStatusText(terrainPlan), /dem/, '措辞应能看出用的是哪个目录')
  54. const overlays = planOverlays({
  55. overlays: [
  56. { url: 'a/{z}/{x}/{reverseY}.png', minimumLevel: 0, maximumLevel: 19, bounds: [1, 2, 3, 4], credit: 'x' },
  57. { url: '' },
  58. { url: 'b/{z}/{x}/{y}.png', bounds: [1, 2, 3] },
  59. ],
  60. })
  61. assert.equal(overlays.length, 2, '空 url 应被过滤')
  62. assert.equal(overlays[0].bounds.length, 4, '合法 bounds 应保留')
  63. assert.equal(overlays[1].bounds, null, '非法 bounds 应为 null(引擎侧不设 rectangle)')
  64. assert.equal(overlays[0].maximumLevel, 19, '级别应透传')
  65. // ---------------------------------------------------------------- 4. 与磁盘真实数据对齐(仅本机有数据时)
  66. const configPath = new URL('../../../public/cockpit/config.json', import.meta.url)
  67. if (existsSync(configPath)) {
  68. const raw = JSON.parse(readFileSync(configPath, 'utf8'))
  69. const { config } = normalizeConfig(raw)
  70. const terrainDir = config.map.terrain.url
  71. // 配置里的地形指向的目录里应真有 layer.json,且是量化网格 + EPSG:4326
  72. const mounts = (process.env.COCKPIT_GIS_MOUNTS || '').includes('yuanlingDEM')
  73. if (mounts) {
  74. const demDir = (process.env.COCKPIT_GIS_MOUNTS.match(/"dir":"([^"]*yuanlingDEM)"/) || [])[1]
  75. if (demDir && existsSync(`${demDir}/layer.json`)) {
  76. const layer = JSON.parse(readFileSync(`${demDir}/layer.json`, 'utf8'))
  77. assert.equal(layer.format, 'quantized-mesh-1.0', '地形应是量化网格')
  78. assert.equal(layer.projection, 'EPSG:4326', '地形应是地理坐标(global-geodetic)')
  79. assert.equal(layer.scheme, 'tms', '地形 layer.json 声明 tms —— Cesium 认这个字段,服务端不用翻 y')
  80. assert.ok(layer.extensions.includes('octvertexnormals'), '地形带八面体顶点法线(对应 requestVertexNormals)')
  81. assert.equal(config.map.terrain.url, '/gis/dem', '配置里的地形 URL 应与挂载前缀一致')
  82. }
  83. const picDir = (process.env.COCKPIT_GIS_MOUNTS.match(/"dir":"([^"]*yuanlingPic3)"/) || [])[1]
  84. if (picDir && existsSync(`${picDir}/meta.json`)) {
  85. const meta = JSON.parse(readFileSync(`${picDir}/meta.json`, 'utf8'))
  86. assert.equal(meta.tiletrans, 'tms', '影像是 TMS —— 模板必须用 {reverseY}')
  87. assert.equal(meta.proj, 3857, '影像是 Web Mercator')
  88. assert.match(config.map.overlays[0].url, /\{reverseY\}/, '叠加模板应使用 {reverseY} 以匹配 TMS')
  89. const [w, s, e, n] = config.map.overlays[0].bounds
  90. assert.ok(
  91. Math.abs(w - meta.bounds.west) < 1e-6 && Math.abs(s - meta.bounds.south) < 1e-6 &&
  92. Math.abs(e - meta.bounds.east) < 1e-6 && Math.abs(n - meta.bounds.north) < 1e-6,
  93. '配置里的 bounds 应与 meta.json 一致(范围外不请求瓦片)',
  94. )
  95. console.log(' (已按本机数据核对:layer.json 的 tms/EPSG:4326/顶点法线,meta.json 的 tms/3857/bounds)')
  96. }
  97. }
  98. }
  99. console.log(
  100. 'T21 检查通过:地形与叠加影像的归一化 + 计划层(默认不配、缺字段忽略、bounds/级别校验),并在本机有数据时把配置与 layer.json / meta.json 对齐核对。',
  101. )