/** * T29 底图影像(配置驱动)纯逻辑检查。 * * 覆盖:配置归一化(source 白名单 / 子域 / 级别)、四种来源的计划、天地图模板拼装(开发代理 vs 生产子域)、 * 缺 tk 与未知来源的失败态、以及宿主状态措辞。 * * 运行:node .scratch/cockpit-shell-panel-layout/verify/t29-basemap.mjs */ import assert from 'node:assert/strict' import { BASEMAP_SOURCE, BASEMAP_STATE, NATURAL_EARTH_TMS, TIANDITU_SUBDOMAINS, basemapStatusText, planBasemap, tiandituTileUrl, } from '../../../src/modules/cockpit/domain/basemap.js' import { builtinConfig, normalizeConfig } from '../../../src/modules/cockpit/services/cockpitConfig.js' // ---------------------------------------------------------------- 1. 默认不配底图(票 27 行为不变) const defaults = builtinConfig() assert.equal(defaults.map.basemap.source, BASEMAP_SOURCE.NONE, '默认必须是 none(不伪造配准)') assert.equal(defaults.map.tianditu.layer, 'img', '天地图默认影像底图') const nonePlan = planBasemap(defaults.map, { dev: true }) assert.equal(nonePlan.ready, false, 'none 不可用') assert.equal(nonePlan.state, BASEMAP_STATE.NOT_CONFIGURED, 'none 的状态是 not-configured') assert.match(basemapStatusText(nonePlan), /底图未配置/, '未配置时保持票 27 的原话') // ---------------------------------------------------------------- 2. 归一化 const normalized = normalizeConfig({ map: { basemap: { source: 'natural-earth', url: ' x ', subdomains: ['a', ' b ', ''], minimumLevel: 2, maximumLevel: 'ten' }, tianditu: { token: ' tk ', layer: 'vec' }, }, }) assert.equal(normalized.config.map.basemap.source, 'natural-earth', '合法 source 应被接受') assert.equal(normalized.config.map.basemap.url, 'x', 'url 应去空白') assert.deepEqual(normalized.config.map.basemap.subdomains, ['a', 'b'], '子域应过滤空串并去空白') assert.equal(normalized.config.map.basemap.minimumLevel, 2, '级别应接受非负整数') assert.equal(normalized.config.map.basemap.maximumLevel, null, '非整数级别应被忽略') assert.equal(normalized.config.map.tianditu.layer, 'vec', '天地图图层应被接受') assert.equal(normalized.config.map.tianditu.token, 'tk', 'tk 应去空白') assert.equal(normalized.issues.length, 1, `只应记一条 issue,实际 ${JSON.stringify(normalized.issues)}`) const badSource = normalizeConfig({ map: { basemap: { source: 'baidu' }, tianditu: { layer: '影像' } } }) assert.equal(badSource.config.map.basemap.source, BASEMAP_SOURCE.NONE, '未知来源应退回 none') assert.equal(badSource.config.map.tianditu.layer, 'img', '未知图层应退回 img') assert.equal(badSource.issues.length, 2, `两条非法取值各记一条,实际 ${JSON.stringify(badSource.issues)}`) const badSubdomains = normalizeConfig({ map: { basemap: { source: 'xyz', url: 't/{z}/{x}/{y}.png', subdomains: 't0' } } }) assert.deepEqual(badSubdomains.config.map.basemap.subdomains, [], '非数组子域应被忽略') assert.equal(badSubdomains.issues.length, 1, '非数组子域应记一条 issue') // ---------------------------------------------------------------- 3. 内置离线影像 const naturalPlan = planBasemap({ basemap: { source: 'natural-earth' } }) assert.equal(naturalPlan.ready, true, 'natural-earth 应可用') assert.equal(naturalPlan.kind, 'tms', 'natural-earth 走 TMS') assert.equal(naturalPlan.url, NATURAL_EARTH_TMS, 'TMS 路径应是 Cesium 自带资源目录(相对 CESIUM_BASE_URL)') assert.match(naturalPlan.label, /离线/, '标签应说明是离线影像') // 免费免 key 的在线全局层(本机外网被挡,模板与署名在这里断言) const arcgisPlan = planBasemap({ basemap: { source: 'arcgis-imagery' } }) assert.equal(arcgisPlan.ready, true, 'arcgis-imagery 应可用(免费免 key)') assert.equal(arcgisPlan.kind, 'xyz', 'arcgis 走 XYZ 模板') assert.match(arcgisPlan.url, /^https:\/\/services\.arcgisonline\.com\/.*\/tile\/\{z\}\/\{y\}\/\{x\}$/, 'ArcGIS 的模板顺序是 {z}/{y}/{x}') assert.match(arcgisPlan.credit, /Esri/, '应带上 Esri 署名') // ---------------------------------------------------------------- 4. 自定义 XYZ const xyzPlan = planBasemap({ basemap: { source: 'xyz', url: 'tiles/{z}/{x}/{y}.png', subdomains: ['a'], minimumLevel: 1, maximumLevel: 18, credit: '甲方' }, }) assert.equal(xyzPlan.ready, true, 'xyz 应可用') assert.equal(xyzPlan.kind, 'xyz', 'xyz 走模板 provider') assert.equal(xyzPlan.url, 'tiles/{z}/{x}/{y}.png', '模板应原样透传') assert.deepEqual(xyzPlan.subdomains, ['a'], '子域应透传') assert.equal(xyzPlan.minimumLevel, 1, '最小级别应透传') assert.equal(xyzPlan.maximumLevel, 18, '最大级别应透传') assert.equal(xyzPlan.credit, '甲方', '署名应透传') const xyzNoUrl = planBasemap({ basemap: { source: 'xyz' } }) assert.equal(xyzNoUrl.ready, false, 'xyz 缺 url 不可用') assert.equal(xyzNoUrl.state, BASEMAP_STATE.INVALID, '缺字段应是 invalid 而不是静默无底图') assert.match(xyzNoUrl.issue, /url/, '失败原因应点明缺 url') // ---------------------------------------------------------------- 5. 天地图(开发代理 vs 生产子域) const tiandituDev = planBasemap({ basemap: { source: 'tianditu' }, tianditu: { token: 'TK', layer: 'img' } }, { dev: true }) assert.equal(tiandituDev.ready, true, '带 tk 的天地图应可用') assert.match(tiandituDev.url, /^\/tianditu\.map\/img_w\/wmts\?/, '开发态应走仓库自带的 /tianditu.map 代理') assert.match(tiandituDev.url, /tk=TK/, 'tk 应拼进查询串') assert.match(tiandituDev.url, /TILEMATRIX=\{z\}&TILEROW=\{y\}&TILECOL=\{x\}/, 'WMTS 的占位符顺序应是 {z}/{y}/{x}') assert.deepEqual(tiandituDev.subdomains, [], '代理是单主机,不加子域') assert.equal(tiandituDev.credit, '天地图', '应有署名') const tiandituProd = planBasemap({ basemap: { source: 'tianditu' }, tianditu: { token: 'TK', layer: 'vec' } }, { dev: false }) assert.match(tiandituProd.url, /^https:\/\/t\{s\}\.tianditu\.gov\.cn\/vec_w\//, '生产应直连带子域的主机') assert.deepEqual(tiandituProd.subdomains, TIANDITU_SUBDOMAINS, '生产应给出子域列表') assert.match(tiandituProd.label, /矢量/, '标签应反映图层') const tiandituNoToken = planBasemap({ basemap: { source: 'tianditu' } }, { dev: true }) assert.equal(tiandituNoToken.ready, false, '缺 tk 不可用') assert.equal(tiandituNoToken.state, BASEMAP_STATE.INVALID, '缺 tk 应是 invalid') assert.match(tiandituNoToken.issue, /tk/, '失败原因应点明缺 tk') assert.match(basemapStatusText(tiandituNoToken), /配置无效.*回退临时影像/, '失败措辞应说明回退') assert.equal( planBasemap({ basemap: { source: 'baidu' } }).state, BASEMAP_STATE.INVALID, '未知来源(绕过归一化直接调计划)也应判 invalid', ) assert.match(tiandituTileUrl('/x/', 'T', 'ter'), /^\/x\/ter_w\/wmts\?.*tk=T$/, '模板拼装应处理尾部斜杠') // ---------------------------------------------------------------- 6. 状态措辞 assert.match(basemapStatusText(naturalPlan), /底图:/, '可用时措辞应带底图名') assert.equal(basemapStatusText(null), '底图未配置(正式底图见票 21)', '缺省计划应回落到未配置措辞') console.log( `T29 检查通过:默认不配底图(票 27 行为不变)/ 归一化(source 白名单、子域、级别、图层)/ 四来源计划(none · natural-earth · xyz · tianditu)/ 天地图开发代理与生产子域模板 / 缺 tk 与未知来源的失败态(${BASEMAP_STATE.INVALID})。`, )