| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import vm from 'node:vm'
- import { transitionOverlapState, OVERLAP_STATE, fanLayout, groupOverlappingDevices } from '../../../src/modules/cockpit/domain/overlapDevices.js'
- import { MAP_LABEL_STYLE, mapLabelText } from '../../../src/modules/cockpit/domain/mapLabelStyle.js'
- const root = new URL('../../../src/modules/cockpit/', import.meta.url)
- const engine = readFileSync(new URL('services/cesiumEngine.js', root), 'utf8')
- const service = readFileSync(new URL('services/mapService.js', root), 'utf8')
- const extract = (text, name) => text.match(new RegExp(`(?:export )?function ${name}\\([^]*?\\n\\}`))[0].replace(/^export /, '')
- const prop = (value) => ({ getValue: () => value })
- const entities = [
- { id: 'aggregate', show: true, properties: { overlapGroup: prop('G'), overlapAggregate: prop(true) }, billboard: { scale: 1, color: { alpha: 1 } }, label: {} },
- ...['A', 'B'].map((code) => ({ id: code, show: false, properties: { overlapGroup: prop('G'), deviceCode: prop(code), overlapAggregate: prop(false) }, billboard: { pixelOffset: { x: 0, y: 0 } }, label: {} })),
- ]
- const ctx = vm.createContext({ deviceMarkerSource: () => ({ entities: { values: entities } }), Cartesian2: class { constructor(x,y){this.x=x;this.y=y} }, Color: { WHITE: { withAlpha: (alpha) => ({ alpha }) } }, viewer: {} })
- vm.runInContext(extract(engine, 'setOverlapPresentation'), ctx)
- let failed = 0
- const check = (name, run) => { try { run(); console.log(`PASS ${name}`) } catch(e) { failed++; console.error(`FAIL ${name}: ${String(e.message).split('\n')[0]}`) } }
- check('clicking aggregate keeps a dim clickable entry', () => {
- vm.runInContext("setOverlapPresentation(viewer, {groupKey:'G', state:'expanded', offsets:{A:{x:0,y:-48},B:{x:0,y:48}}})",ctx)
- assert.equal(entities[0].show,true)
- assert.ok(entities[0].billboard.color.alpha < 1)
- })
- check('selecting fan device leaves selected device visible at center', () => {
- vm.runInContext("setOverlapPresentation(viewer, {groupKey:'G', state:'selected', selectedCode:'A'})",ctx)
- assert.equal(entities[1].show,true)
- assert.equal(entities[1].billboard.pixelOffset.x,0)
- assert.equal(entities[2].show,false)
- })
- check('device click transitions to persistent selected state', () => assert.equal(transitionOverlapState('expanded','device-click'), 'selected'))
- check('aggregate symbol carries readable quantity and separate identity', () => {
- const ctx2 = vm.createContext({ deviceMarkerSource: () => ({ entities: { removeAll(){}, add(e){ return e } } }), deviceBaseStyle: { clear(){} }, stopDeviceEffects(){}, Cartesian2: class {}, Cartesian3: { fromDegrees(){} }, HeightReference:{}, VerticalOrigin:{}, Color:{WHITE:{},BLACK:{withAlpha:(alpha)=>({alpha})}}, LabelStyle:{}, viewer:{} })
- const created = []
- ctx2.deviceMarkerSource = () => ({entities:{removeAll(){},add(e){created.push(e);return e}}})
- ctx2.MAP_LABEL_STYLE = MAP_LABEL_STYLE
- ctx2.mapLabelText = mapLabelText
- ctx2.Color.fromCssColorString = () => ({ withAlpha: (alpha) => ({ alpha }) })
- vm.runInContext(extract(engine,'mapTextLabel'),ctx2)
- vm.runInContext(extract(engine,'syncAssociatedDevices'),ctx2)
- ctx2.devices = [{code:'A',pointCode:'P',longitude:1,latitude:2,status:'NORMAL'}]
- ctx2.groups = [{key:'G',pointCode:'P',k:2,rows:ctx2.devices,visual:{status:'NORMAL',shape:'circle'}}]
- vm.runInContext("syncAssociatedDevices(viewer, devices, ()=>'color', ()=>'image', groups)",ctx2)
- const aggregateLabel = created.find((e)=>e.id.startsWith('overlap|')).label
- assert.equal(aggregateLabel.text, '聚合 2 台')
- assert.equal(aggregateLabel.showBackground, false)
- assert.equal(created.find((e)=>e.id.startsWith('device|')).label.showBackground, false)
- assert.equal(aggregateLabel.font, MAP_LABEL_STYLE.count.font)
- })
- check('monitoring point creates a visible name label', () => {
- const created = []
- const pctx=vm.createContext({pointMarkerSource:()=>({entities:{removeAll(){},add(e){created.push(e);return e}}}),markerEntityIndex:new WeakMap(),Cartesian3:{fromDegrees(){}},Cartesian2:class{},HeightReference:{},VerticalOrigin:{},Color:{WHITE:{},BLACK:{withAlpha:(alpha)=>({alpha})}},LabelStyle:{},viewer:{}})
- pctx.MAP_LABEL_STYLE = MAP_LABEL_STYLE
- pctx.mapLabelText = mapLabelText
- pctx.Color.fromCssColorString = () => ({ withAlpha: (alpha) => ({ alpha }) })
- vm.runInContext(extract(engine,'mapTextLabel'),pctx)
- vm.runInContext(extract(engine,'markerFingerprint'),pctx)
- vm.runInContext(extract(engine,'syncPointMarkers'),pctx)
- vm.runInContext("syncPointMarkers(viewer,[{key:'monitoring-point|P',code:'P',kind:'monitoring-point',name:'城南监测点',longitude:1,latitude:2}],()=>'color',()=>'image')",pctx)
- assert.equal(created[0].label?.text,'⟦ 城南监测点 ⟧')
- assert.equal(created[0].label.font, MAP_LABEL_STYLE.name.font)
- assert.equal(created[0].label.showBackground, false)
- assert.equal(created[0].label.outlineWidth, 2)
- })
- check('device picking does not prewrite service selection before store callback',()=>{
- const body=extract(service,'pickDeviceOrPointAt')
- assert.ok(!body.includes('selectedDeviceCode = resolved.pick.code'))
- })
- // Run the real service selection/hover/click chain against the real engine presentation writer.
- const mutable = (value) => ({ getValue: () => value, setValue(next) { value = next } })
- for (const entity of entities.slice(1)) entity.properties.overlapEntry = mutable(false)
- let hit = null
- let selectedByCallback = ''
- const group = groupOverlappingDevices(['A', 'B'].map((code) => ({ code, pointCode: 'P', longitude: 1, latitude: 2, placement: 'drawn' })))[0]
- group.key = 'G'
- const runtime = vm.createContext({
- instance: { viewer: {}, engine: {
- setOverlapPresentation(_viewer, options) { ctx.options = options; vm.runInContext('setOverlapPresentation(viewer, options)', ctx) },
- pickDeviceMarkerAt() { return hit }, pickPointMarkerAt() { return null },
- pickDeviceMarkerForHover() { return hit }, nudgeOverlap() { return 1 },
- } },
- overlapGroups: [group], overlapExpandedGroup: '', overlapPreviewGroup: '', overlapState: 'collapsed',
- overlapCountValue: 0, overlapMaxFan: 10, selectedDeviceCode: '', hoveredDeviceCode: '', lastPicked: null,
- fanLayout, OVERLAP_STATE, transitionOverlapState, syncDeviceReactions() {},
- isDomHost: () => false, paintDeviceReactions() {}, resolveOverlapPick: ({ aggregateHit, deviceHit }) =>
- aggregateHit ? { target: 'overlap', pick: aggregateHit } : deviceHit ? { target: 'device', pick: deviceHit } : { target: 'none' },
- pointPickHandlers: new Set([(picked) => { selectedByCallback = picked.code; runtime.wanted = picked.code; vm.runInContext('setSelectedDevice(wanted)', runtime) }]),
- deviceHoverHandlers: new Set(),
- })
- for (const name of ['applyOverlapPresentation', 'paintOverlapState', 'collapseOverlap', 'resolveOverlapGroupKey',
- 'nudgeOverlap', 'expandOverlap', 'setSelectedDevice', 'pickDeviceOrPointAt', 'hoverDeviceAt', 'getOverlapState']) {
- vm.runInContext(extract(service, name), runtime)
- }
- const dispatch = (name, target) => { hit = target; return vm.runInContext(`${name}(500,300)`, runtime) }
- check('hover previews and click fixes the fan without losing the aggregate', () => {
- dispatch('hoverDeviceAt', { kind: 'monitoring-device-overlap', overlapGroup: 'G' })
- assert.equal(entities[0].show, true)
- assert.equal(entities[1].show, true)
- assert.equal(entities[0].billboard.scale, 1.2)
- dispatch('pickDeviceOrPointAt', { kind: 'monitoring-device-overlap', overlapGroup: 'G' })
- assert.equal(runtime.overlapExpandedGroup, 'G')
- assert.equal(entities[0].billboard.color.alpha, 0.35)
- })
- check('fan click goes through callback and leaves A as the visible central entry', () => {
- dispatch('pickDeviceOrPointAt', { kind: 'monitoring-device', code: 'A', pointCode: 'P', overlapGroup: 'G' })
- assert.equal(selectedByCallback, 'A')
- assert.equal(runtime.selectedDeviceCode, 'A')
- assert.equal(entities[1].show, true)
- assert.equal(entities[1].properties.overlapEntry.getValue(), true)
- assert.equal(entities[2].show, false)
- assert.equal(entities[1].billboard.pixelOffset.x, 0)
- assert.equal(entities[1].billboard.pixelOffset.y, 0)
- })
- check('selected entry still previews and click locks peers for switching', () => {
- const portal = { kind: 'monitoring-device', code: 'A', pointCode: 'P', overlapGroup: 'G', overlapEntry: true }
- dispatch('hoverDeviceAt', portal)
- assert.equal(entities[2].show, true)
- assert.equal(entities[1].billboard.pixelOffset.y, 0)
- dispatch('pickDeviceOrPointAt', portal)
- dispatch('hoverDeviceAt', null)
- assert.equal(entities[2].show, true)
- assert.equal(runtime.overlapExpandedGroup, 'G')
- })
- check('choosing B transfers center and preserves device identity', () => {
- dispatch('pickDeviceOrPointAt', { kind: 'monitoring-device', code: 'B', pointCode: 'P', overlapGroup: 'G' })
- assert.equal(selectedByCallback, 'B')
- assert.equal(entities[2].show, true)
- assert.equal(entities[2].properties.overlapEntry.getValue(), true)
- assert.equal(entities[1].show, false)
- assert.equal(entities[1].properties.overlapEntry.getValue(), false)
- assert.equal(entities[2].billboard.pixelOffset.y, 0)
- })
- check('blank click and preview leave do not hide the selected device', () => {
- dispatch('hoverDeviceAt', { kind: 'monitoring-device', code: 'B', pointCode: 'P', overlapGroup: 'G', overlapEntry: true })
- dispatch('hoverDeviceAt', null)
- dispatch('pickDeviceOrPointAt', null)
- assert.equal(entities[2].show, true)
- assert.equal(vm.runInContext('getOverlapState().state', runtime), 'selected')
- })
- check('selecting from one group clears another active preview', () => {
- runtime.overlapGroups = [group, { ...group, key: 'H', pointCode: 'Q', rows: [{code:'C',pointCode:'Q'},{code:'D',pointCode:'Q'}] }]
- runtime.overlapPreviewGroup = 'H'
- const calls = []
- const render = runtime.instance.engine.setOverlapPresentation
- runtime.instance.engine.setOverlapPresentation = (viewer, options) => { calls.push(options); render(viewer, options) }
- vm.runInContext("setSelectedDevice('A')", runtime)
- assert.ok(calls.some((call) => call.groupKey === 'H' && call.state === 'collapsed'))
- assert.ok(calls.some((call) => call.groupKey === 'G' && call.state === 'selected'))
- assert.equal(runtime.overlapPreviewGroup, '')
- runtime.instance.engine.setOverlapPresentation = render
- })
- check('clear device selection restores aggregate and removes selected portal', () => {
- vm.runInContext("setSelectedDevice('')", runtime)
- assert.equal(entities[0].show, true)
- assert.equal(entities[1].show, false)
- assert.equal(entities[2].show, false)
- })
- check('K11 selected device outside the first ten stays visible with only nine peers', () => {
- const rows = Array.from({ length: 11 }, (_, index) => ({code:`D${index}`,pointCode:'P',longitude:1,latitude:2,placement:'drawn'}))
- const large = groupOverlappingDevices(rows)[0]
- large.key = 'G'
- runtime.overlapGroups = [large]
- entities.splice(0, entities.length,
- {show:true, properties:{overlapGroup:prop('G'),overlapAggregate:prop(true)},billboard:{},label:{}},
- ...rows.map((row)=>({show:false,properties:{overlapGroup:prop('G'),overlapAggregate:prop(false),deviceCode:prop(row.code),overlapEntry:mutable(false)},billboard:{},label:{}})))
- vm.runInContext("setSelectedDevice('D10'); expandOverlap('G', true)", runtime)
- assert.equal(entities.filter((entity)=>entity.show && entity.properties.deviceCode).length,10)
- assert.equal(entities[11].show,true)
- assert.equal(entities[11].billboard.pixelOffset.x,0)
- assert.equal(entities[11].billboard.pixelOffset.y,0)
- assert.equal(vm.runInContext('getOverlapState().count',runtime),11)
- })
- check('expanded device panel clears the fan and stays anchored to its device', () => {
- runtime.DEVICE_LAYER_KIND = 'monitoring-device'
- runtime.instance.engine.projectDeviceMarker = () => ({x:500,y:300,iconSize:22})
- vm.runInContext(extract(service,'getMapMarkerAnchor'),runtime)
- const anchor = vm.runInContext("getMapMarkerAnchor('monitoring-device', 'D10', 'P')",runtime)
- assert.equal(anchor.panelGap,60)
- assert.equal(anchor.x,500)
- })
- // Actual locate helper must sample device terrain and dispatch its configured tier.
- const locateBody = service.match(/export async function locateMonitoringPoint[^]*?\n\}/)[0].replace(/^export /,'')
- const moves = []
- const locateContext = vm.createContext({
- pointLocateToken:0, instance:{cameraControl:{},viewer:{},engine:{sampleTerrainHeight:async()=>123,groundHeightAt:()=>123}},
- isRenderable: (row)=>row.coordinate?.renderable === true,
- terrainState:'ready',TERRAIN_STATE:{NONE:'none'},isStaleLocate:(a,b)=>a!==b,
- getCockpitConfig:()=>({map:{}}),planLocateView:(_map,tier)=>({tier}),
- applyViewPreset:(pivot,hpr)=>{moves.push({pivot,hpr});return true},
- setCameraFocus:(pivot)=>{moves.push({pivot});return true},
- })
- vm.runInContext(locateBody,locateContext)
- locateContext.device={longitude:1,latitude:2,coordinate:{renderable:true}}
- await vm.runInContext("locateMonitoringPoint(device,{tier:'device'})",locateContext)
- check('first device selection dispatches device view at sampled ground height',()=>{
- assert.equal(moves[0].hpr.tier,'device')
- assert.equal(moves[0].pivot.height,123)
- assert.ok(/void locateMonitoringPoint\(device,/.test(readFileSync(new URL('stores/deviceLayerStore.js',root),'utf8')))
- })
- await vm.runInContext("locateMonitoringPoint(device,{tier:'device',centerOnly:true})",locateContext)
- check('device switching only changes center using sampled terrain',()=>{
- assert.equal(moves[1].hpr,undefined)
- assert.equal(moves[1].pivot.height,123)
- })
- const pickedEntity = {
- show: true,
- properties: { deviceCode: prop('A'), monitoringPointCode: prop('P'), deviceStatus: prop('NORMAL'),
- overlapGroup: prop('G'), overlapEntry: prop(true), overlapAggregate: prop(false) },
- }
- const pickContext = vm.createContext({
- deviceMarkerSource: () => ({ entities: { values: [pickedEntity] } }),
- projectMarkerEntity: () => ({ x: 500, y: 300 }),
- Cartesian2: class { constructor(x, y) { this.x = x; this.y = y } },
- viewer: { scene: { pick: () => ({ id: pickedEntity }) }, canvas: { getBoundingClientRect: () => ({ left: 0, top: 0 }) } },
- })
- for (const name of ['readOverlapTargets', 'overlapTargetAt', 'pipelineSource', 'pickThroughPipelines', 'pickDeviceMarkerAt']) {
- vm.runInContext(extract(engine, name), pickContext)
- }
- check('projected selected-entry pick retains device and group identity', () => {
- const picked = vm.runInContext('pickDeviceMarkerAt(viewer, 500, 300)', pickContext)
- assert.equal(picked.code, 'A')
- assert.equal(picked.pointCode, 'P')
- assert.equal(picked.overlapGroup, 'G')
- assert.equal(picked.overlapEntry, true)
- })
- check('ordinary scene pick retains the same selected-entry metadata', () => {
- const picked = vm.runInContext('pickDeviceMarkerAt(viewer, 700, 500)', pickContext)
- assert.equal(picked.code, 'A')
- assert.equal(picked.overlapGroup, 'G')
- assert.equal(picked.overlapEntry, true)
- })
- console.log(`T45 selection continuity: ${failed ? 'FAILED' : 'passed'}`)
- process.exitCode=failed ? 1 : 0
|