gis-view-webgl2 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +255 -3
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +65 -0
- package/docs/GisView.md +1 -1
- package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +58 -0
- package/docs/demos/ChannelDemo.vue +566 -6
- package/docs/demos/CopyMergeDemo.vue +57 -6
- package/docs/demos/LabelDemo.vue +67 -11
- package/docs/demos/api-index.js +13 -3
- package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +4 -2
- package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +7 -2
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +21 -5
- package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +100 -0
- package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +38 -0
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +5 -5
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +58 -8
- package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +27 -2
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +45 -1
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +7 -1
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +8 -0
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +28 -0
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +3 -0
- package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +1 -1
- package/docs/steps/README.md +8 -6
- package/docs/steps/selftest-demos.html +194 -29
- package/docs/steps/selftest-docs.html +7 -6
- package/docs/steps/selftest-features.html +213 -1
- package/docs/steps/selftest-labels.html +249 -4
- package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +17 -11
- package/package.json +2 -2
|
@@ -3044,7 +3044,8 @@
|
|
|
3044
3044
|
channels: csmBuilt.channels,
|
|
3045
3045
|
fitPadding: 60,
|
|
3046
3046
|
showGrid: false,
|
|
3047
|
-
//
|
|
3047
|
+
// 点回线摊开选中集:先关拐点编辑,Ctrl 点击才能测「整批取反」;加点那一段再打开
|
|
3048
|
+
waypointEditable: false,
|
|
3048
3049
|
features: { select: true }
|
|
3049
3050
|
})
|
|
3050
3051
|
await settle(140)
|
|
@@ -3085,9 +3086,99 @@
|
|
|
3085
3086
|
// 通道外的线路没有分组可摊,照旧只选它自己
|
|
3086
3087
|
await csmClick(csmView.project(100, 160))
|
|
3087
3088
|
check('通道外的线路不受选中模式影响(还是只选自己)', csmIds() === 'csm-solo', csmIds())
|
|
3089
|
+
// Ctrl + 点击给通道回线加点:加完后的选中 / 高亮范围与普通单击共用 channelSelectMode
|
|
3090
|
+
csmView.updateOptions({ waypointEditable: true })
|
|
3091
|
+
await settle(60)
|
|
3092
|
+
const csmGroupSkeleton = () => csmView.getChannelGroups('csm-ch').find(group => String(group.id) === 'csm-g2')?.skeleton.length ?? 0
|
|
3093
|
+
const csmAddBefore = csmGroupSkeleton()
|
|
3094
|
+
csmView.updateOptions({ channelSelectMode: 'line' })
|
|
3095
|
+
await csmClick(csmView.project(50, 60), { ctrlKey: true })
|
|
3096
|
+
const csmLineAdd = csmView.getSelectionData()
|
|
3097
|
+
check('Ctrl + 点击加点(line):只高亮被加点的那一条回线', csmLineAdd.lineCount === 1 && csmGroupSkeleton() === csmAddBefore + 1 && csmIds() !== 'csm-2,csm-3' && csmIds() !== 'csm-1,csm-2,csm-3', JSON.stringify({ ids: csmIds(), waypoints: csmGroupSkeleton() }))
|
|
3098
|
+
csmView.updateOptions({ channelSelectMode: 'group' })
|
|
3099
|
+
await csmClick(csmView.project(100, 60), { ctrlKey: true })
|
|
3100
|
+
check('Ctrl + 点击加点(group):加完连所在分组一起高亮', csmIds() === 'csm-2,csm-3' && csmGroupSkeleton() === csmAddBefore + 2, JSON.stringify({ ids: csmIds(), waypoints: csmGroupSkeleton() }))
|
|
3101
|
+
csmView.updateOptions({ channelSelectMode: 'channel' })
|
|
3102
|
+
await csmClick(csmView.project(150, 60), { ctrlKey: true })
|
|
3103
|
+
check('Ctrl + 点击加点(channel):加完整条通道一起高亮', csmIds() === 'csm-1,csm-2,csm-3' && csmView.getSelectionData().channelCount === 1 && csmGroupSkeleton() === csmAddBefore + 3, JSON.stringify({ ids: csmIds(), waypoints: csmGroupSkeleton() }))
|
|
3088
3104
|
csmView.destroy()
|
|
3089
3105
|
csmHost.remove()
|
|
3090
3106
|
|
|
3107
|
+
/* 分组拐点 vertex 拖动:只平移当前分组同一 pathIndex 的顶点,其它拐点与分组不动 */
|
|
3108
|
+
const vwdHost = document.createElement('div')
|
|
3109
|
+
vwdHost.style.cssText = 'width:560px;height:360px'
|
|
3110
|
+
document.body.appendChild(vwdHost)
|
|
3111
|
+
const vwdView = createGisView(vwdHost, {
|
|
3112
|
+
points: [{ id: 'vwd-a', x: 0, y: 0 }, { id: 'vwd-b', x: 360, y: 0 }],
|
|
3113
|
+
lines: [
|
|
3114
|
+
{ id: 'vwd-1', from: 'vwd-a', to: 'vwd-b' },
|
|
3115
|
+
{ id: 'vwd-2', from: 'vwd-a', to: 'vwd-b' },
|
|
3116
|
+
{ id: 'vwd-3', from: 'vwd-a', to: 'vwd-b' },
|
|
3117
|
+
{ id: 'vwd-4', from: 'vwd-a', to: 'vwd-b' }
|
|
3118
|
+
],
|
|
3119
|
+
channels: [{
|
|
3120
|
+
id: 'vwd-ch',
|
|
3121
|
+
from: 'vwd-a',
|
|
3122
|
+
to: 'vwd-b',
|
|
3123
|
+
groups: [
|
|
3124
|
+
{ id: 'vwd-g1', lineIds: ['vwd-1', 'vwd-2'], path: [[100, 80], [260, 80]] },
|
|
3125
|
+
{ id: 'vwd-g2', lineIds: ['vwd-3', 'vwd-4'], path: [[100, -80], [260, -80]], waypointOffsets: { '1': { x: 0, y: 6 } } }
|
|
3126
|
+
]
|
|
3127
|
+
}],
|
|
3128
|
+
waypointEditable: true,
|
|
3129
|
+
waypointVisibility: 'always',
|
|
3130
|
+
channelWaypointDragMode: 'vertex',
|
|
3131
|
+
fitPadding: 70,
|
|
3132
|
+
showGrid: false
|
|
3133
|
+
})
|
|
3134
|
+
await settle(160)
|
|
3135
|
+
const vwdSend = (type, screen, extra = {}) => {
|
|
3136
|
+
const box = vwdView.canvas.getBoundingClientRect()
|
|
3137
|
+
vwdView.canvas.dispatchEvent(new PointerEvent(type, {
|
|
3138
|
+
bubbles: true,
|
|
3139
|
+
cancelable: true,
|
|
3140
|
+
pointerId: 79,
|
|
3141
|
+
pointerType: 'mouse',
|
|
3142
|
+
button: extra.button ?? 0,
|
|
3143
|
+
buttons: extra.buttons ?? (type === 'pointerup' ? 0 : 1),
|
|
3144
|
+
clientX: box.left + screen[0],
|
|
3145
|
+
clientY: box.top + screen[1],
|
|
3146
|
+
...extra
|
|
3147
|
+
}))
|
|
3148
|
+
}
|
|
3149
|
+
const vwdBefore = vwdView.getChannelGroupWaypoints('vwd-ch', 'vwd-g1')
|
|
3150
|
+
const vwdOtherBefore = vwdView.getChannelGroupWaypoints('vwd-ch', 'vwd-g2')
|
|
3151
|
+
const vwdWorldStart = vwdBefore.lines[0].waypoints[0]
|
|
3152
|
+
const vwdWorldTarget = [vwdWorldStart[0] + 42, vwdWorldStart[1] + 30]
|
|
3153
|
+
const vwdStart = vwdView.project(vwdWorldStart[0], vwdWorldStart[1])
|
|
3154
|
+
const vwdTarget = vwdView.project(vwdWorldTarget[0], vwdWorldTarget[1])
|
|
3155
|
+
const vwdObjectOffset = vwdView.getChannelGroups('vwd-ch').find(group => String(group.id) === 'vwd-g2')?.waypointOffsets?.['1']
|
|
3156
|
+
const vwdObjectLane = vwdView.getChannels().find(group => String(group.id) === 'vwd-g2')?.offsets?.[0] ?? NaN
|
|
3157
|
+
check('分组拐点偏移:{ x, y } 声明归一成 [dx, dy] 并参与实际路由', Array.isArray(vwdObjectOffset) && Math.abs(vwdObjectOffset[0]) <= 1e-3 && Math.abs(vwdObjectOffset[1] - 6) <= 1e-3 && Math.abs(vwdOtherBefore.lines[0].waypoints[0][1] - (vwdOtherBefore.skeleton[1][1] + vwdObjectLane + 6)) <= 1e-3, JSON.stringify({ offset: vwdObjectOffset, lane: vwdObjectLane, skeleton: vwdOtherBefore.skeleton, line: vwdOtherBefore.lines[0].waypoints[0] }))
|
|
3158
|
+
vwdSend('pointermove', vwdStart, { buttons: 0 })
|
|
3159
|
+
vwdSend('pointerdown', vwdStart, { buttons: 1 })
|
|
3160
|
+
vwdSend('pointermove', [(vwdStart[0] + vwdTarget[0]) / 2, (vwdStart[1] + vwdTarget[1]) / 2], { buttons: 1 })
|
|
3161
|
+
vwdSend('pointermove', vwdTarget, { buttons: 1 })
|
|
3162
|
+
vwdSend('pointerup', vwdTarget, { buttons: 0 })
|
|
3163
|
+
await settle(180)
|
|
3164
|
+
const vwdAfter = vwdView.getChannelGroupWaypoints('vwd-ch', 'vwd-g1')
|
|
3165
|
+
const vwdOtherAfter = vwdView.getChannelGroupWaypoints('vwd-ch', 'vwd-g2')
|
|
3166
|
+
const vwdClose = (left, right, tolerance = 1e-3) => Math.abs(left - right) <= tolerance
|
|
3167
|
+
const vwdMoved = vwdAfter.lines[0].waypoints[0]
|
|
3168
|
+
const vwdPartnerBefore = vwdBefore.lines[1].waypoints[0]
|
|
3169
|
+
const vwdPartnerAfter = vwdAfter.lines[1].waypoints[0]
|
|
3170
|
+
const vwdAdjacent = vwdBefore.lines[0].waypoints[1]
|
|
3171
|
+
const vwdAdjacentAfter = vwdAfter.lines[0].waypoints[1]
|
|
3172
|
+
const vwdRoute = vwdView.getParallelGroups().find(group => String(group.group_id) === 'vwd-g1')
|
|
3173
|
+
const vwdOffset = vwdAfter.waypointOffsets['1']
|
|
3174
|
+
const vwdDeclared = vwdView.getOptions().channels[0].groups.find(group => String(group.id) === 'vwd-g1')?.waypointOffsets
|
|
3175
|
+
check('分组拐点 vertex:目标分组的同序号拐点按世界增量平移', vwdClose(vwdMoved[0] - vwdWorldStart[0], 42) && vwdClose(vwdMoved[1] - vwdWorldStart[1], 30) && vwdClose(vwdPartnerAfter[0] - vwdPartnerBefore[0], 42) && vwdClose(vwdPartnerAfter[1] - vwdPartnerBefore[1], 30), JSON.stringify({ moved: vwdMoved, partnerDelta: [vwdPartnerAfter[0] - vwdPartnerBefore[0], vwdPartnerAfter[1] - vwdPartnerBefore[1]] }))
|
|
3176
|
+
check('分组拐点 vertex:相邻拐点与其它分组一个都不动', vwdClose(vwdAdjacentAfter[0], vwdAdjacent[0]) && vwdClose(vwdAdjacentAfter[1], vwdAdjacent[1]) && JSON.stringify(vwdOtherAfter.lines.map(line => line.path)) === JSON.stringify(vwdOtherBefore.lines.map(line => line.path)), JSON.stringify({ adjacent: vwdAdjacentAfter, other: vwdOtherAfter.lines[0].waypoints }))
|
|
3177
|
+
check('分组拐点 vertex:只写 waypointOffsets,不改分组公共 path', vwdOffset && Object.keys(vwdAfter.waypointOffsets).join(',') === '1' && JSON.stringify(vwdAfter.path) === JSON.stringify(vwdBefore.path) && vwdDeclared && vwdClose(vwdDeclared['1']?.[0] ?? NaN, vwdOffset[0]) && vwdClose(vwdDeclared['1']?.[1] ?? NaN, vwdOffset[1]), JSON.stringify({ offsets: vwdAfter.waypointOffsets, declared: vwdDeclared, path: vwdAfter.path }))
|
|
3178
|
+
check('分组拐点 vertex:parallel_groups.group_route 带刚性偏移(导出不回到旧骨架)', Array.isArray(vwdOffset) && vwdClose(vwdRoute?.group_route?.[1]?.[0] ?? NaN, vwdBefore.skeleton[1][0] + vwdOffset[0]) && vwdClose(vwdRoute?.group_route?.[1]?.[1] ?? NaN, vwdBefore.skeleton[1][1] + vwdOffset[1]), JSON.stringify(vwdRoute?.group_route ?? null))
|
|
3179
|
+
vwdView.destroy()
|
|
3180
|
+
vwdHost.remove()
|
|
3181
|
+
|
|
3091
3182
|
/* 删除(Delete 键):先出载荷说清「删什么 / 连带什么 / 能按哪些范围删」,确认之后才动数据 */
|
|
3092
3183
|
const rmHost = document.createElement('div')
|
|
3093
3184
|
rmHost.style.cssText = 'width:520px;height:320px'
|
|
@@ -4801,6 +4892,127 @@
|
|
|
4801
4892
|
check('clearWebGLLayers 清空并同步 getOptions().webglLayers;requestRender 可链式调用',
|
|
4802
4893
|
clearedLayers === 1 && draftView.getWebGLLayers().length === 0 && (draftView.getOptions().webglLayers ?? []).length === 0 && draftView.requestRender() === draftView,
|
|
4803
4894
|
JSON.stringify({ cleared: clearedLayers, layers: draftView.getWebGLLayers().length, options: draftView.getOptions().webglLayers?.length }))
|
|
4895
|
+
|
|
4896
|
+
/* 隐藏 / 显示、节点拆分、通道变化对账:接口级自测 */
|
|
4897
|
+
const splitHost = document.createElement('div')
|
|
4898
|
+
splitHost.style.cssText = 'width:640px;height:380px'
|
|
4899
|
+
document.body.append(splitHost)
|
|
4900
|
+
const splitView = createGisView(splitHost, {
|
|
4901
|
+
points: [
|
|
4902
|
+
{ id: 'sv-a', x: 0, y: 0, name: 'A' },
|
|
4903
|
+
{ id: 'sv-b', x: 8, y: 0, name: 'B' },
|
|
4904
|
+
{ id: 'sv-c', x: 0, y: 8, name: 'C' }
|
|
4905
|
+
],
|
|
4906
|
+
lines: [
|
|
4907
|
+
{ id: 'sv-ab', from: 'sv-a', to: 'sv-b', name: 'AB' },
|
|
4908
|
+
{ id: 'sv-ab2', from: 'sv-a', to: 'sv-b', name: 'AB-2' },
|
|
4909
|
+
{ id: 'sv-ac', from: 'sv-a', to: 'sv-c', name: 'AC' }
|
|
4910
|
+
],
|
|
4911
|
+
channels: [{ id: 'sv-ch', from: 'sv-a', to: 'sv-b', lineIds: ['sv-ab', 'sv-ab2'] }],
|
|
4912
|
+
initialView: { scale: 1, center: [2, 2] },
|
|
4913
|
+
labelOptimize: false,
|
|
4914
|
+
showGrid: false,
|
|
4915
|
+
waypointEditable: true,
|
|
4916
|
+
history: { limit: 20 }
|
|
4917
|
+
})
|
|
4918
|
+
const splitOutcome = splitView.splitPoint('sv-a', [
|
|
4919
|
+
{ x: -1, y: -1, name: 'A-左' },
|
|
4920
|
+
{ x: 1, y: 1, name: 'A-右', lines: ['sv-ab'] }
|
|
4921
|
+
])
|
|
4922
|
+
const splitData = splitView.getData()
|
|
4923
|
+
const splitB = splitData.points.find(point => point.name === 'A-右')
|
|
4924
|
+
const splitAb = splitData.lines.find(line => line.id === 'sv-ab')
|
|
4925
|
+
const splitAb2 = splitData.lines.find(line => line.id === 'sv-ab2')
|
|
4926
|
+
const splitAc = splitData.lines.find(line => line.id === 'sv-ac')
|
|
4927
|
+
const splitChannel = splitView.getOptions().channels.find(channel => channel.id === 'sv-ch')
|
|
4928
|
+
check('splitPoint:第一份复用原下标 / id,第二份追加并拿到新 id', splitOutcome.ok === true && splitOutcome.items.length === 2 && splitOutcome.indexes[0] === 0 && splitData.points.length === 4 && splitOutcome.pointIds[0] === 'sv-a' && splitOutcome.pointIds[1] !== 'sv-a', JSON.stringify({ ids: splitOutcome.pointIds, indexes: splitOutcome.indexes, points: splitData.points.length }))
|
|
4929
|
+
check('splitPoint:显式分给第二份的线路改绑,未分配的线路跟第一份', splitAb?.from === splitB?.id && splitAb2?.from === 'sv-a' && splitAc?.from === 'sv-a', JSON.stringify({ ab: splitAb?.from, ab2: splitAb2?.from, ac: splitAc?.from, b: splitB?.id }))
|
|
4930
|
+
check('splitPoint:通道没写 channels 时跟随第一条被分配过的成员回线', String(splitChannel?.from) === String(splitB?.id) && String(splitChannel?.to) === 'sv-b', JSON.stringify({ from: splitChannel?.from, to: splitChannel?.to, b: splitB?.id }))
|
|
4931
|
+
check('splitPoint:返回可直接提交的 changeSet 与 splitPoint 计数', splitOutcome.changes?.removedPoint?.id === 'sv-a' && splitOutcome.changes?.points?.length === 2 && splitOutcome.changes?.lines?.some(line => line.id === 'sv-ab') && splitOutcome.changes?.channels?.some(channel => channel.id === 'sv-ch') && splitOutcome.linesChanged === 3 && splitOutcome.channelsChanged === 1, JSON.stringify({ lines: splitOutcome.changes?.lines?.map(line => line.id), channels: splitOutcome.changes?.channels?.map(channel => channel.id) }))
|
|
4932
|
+
const changedAfterSplit = splitView.getChangedChannels().find(channel => channel.id === 'sv-ch')
|
|
4933
|
+
check('getChangedChannels:拆分后的端点变化入账,reason 带 endpoints', Boolean(changedAfterSplit) && changedAfterSplit.endpointsChanged && changedAfterSplit.reasons.includes('endpoints'), JSON.stringify(changedAfterSplit?.reasons))
|
|
4934
|
+
splitView.undo()
|
|
4935
|
+
const undoData = splitView.getData()
|
|
4936
|
+
check('splitPoint:撤销一步回到拆分前(节点、线路、通道端点全恢复)', undoData.points.length === 3 && undoData.lines.find(line => line.id === 'sv-ab')?.from === 'sv-a' && splitView.getOptions().channels.find(channel => channel.id === 'sv-ch')?.from === 'sv-a', JSON.stringify({ points: undoData.points.length, from: undoData.lines.find(line => line.id === 'sv-ab')?.from }))
|
|
4937
|
+
splitView.redo()
|
|
4938
|
+
check('splitPoint:重做恢复拆分结果', splitView.getData().points.length === 4 && splitView.getData().lines.find(line => line.id === 'sv-ab')?.from !== 'sv-a', JSON.stringify({ points: splitView.getData().points.length }))
|
|
4939
|
+
splitView.clearChangedChannels()
|
|
4940
|
+
check('clearChangedChannels:全部对账后最近变化为空', splitView.getChangedChannels().length === 0, JSON.stringify(splitView.getChangedChannels().map(item => item.id)))
|
|
4941
|
+
const addedSplitGroup = splitView.addChannelGroup('sv-ch', ['sv-ab2'])
|
|
4942
|
+
const fullGroupChange = splitView.getChangedChannels().find(channel => channel.id === 'sv-ch')
|
|
4943
|
+
const ignoredGroupChange = splitView.getChangedChannels({ ignoreGroups: true }).find(channel => channel.id === 'sv-ch')
|
|
4944
|
+
check('getChangedChannels({ ignoreGroups }):分组新增 / 删除不入账', addedSplitGroup.ok === true && fullGroupChange?.groupsChanged === true && !ignoredGroupChange, JSON.stringify({ added: addedSplitGroup.ok, full: fullGroupChange?.reasons, ignored: ignoredGroupChange?.reasons }))
|
|
4945
|
+
splitView.clearChangedChannels()
|
|
4946
|
+
if (addedSplitGroup.ok) {
|
|
4947
|
+
splitView.removeChannelGroup('sv-ch', addedSplitGroup.groupId)
|
|
4948
|
+
splitView.clearChangedChannels()
|
|
4949
|
+
}
|
|
4950
|
+
const splitGroup = splitView.getChannelGroups('sv-ch')[0]
|
|
4951
|
+
splitView.addChannelGroupWaypoint('sv-ch', splitGroup?.id)
|
|
4952
|
+
const waypointChange = splitView.getChangedChannels({ reasons: ['waypoints'] }).find(channel => channel.id === 'sv-ch')
|
|
4953
|
+
const ignoredWaypointChange = splitView.getChangedChannels({ ignoreGroups: true, reasons: ['waypoints'] }).find(channel => channel.id === 'sv-ch')
|
|
4954
|
+
check('getChangedChannels({ reasons: [waypoints] }):只返回拐点变化并收窄 reasons', Boolean(waypointChange) && waypointChange.waypointsChanged && waypointChange.reasons.length === 1 && waypointChange.reasons[0] === 'waypoints', JSON.stringify(waypointChange?.reasons))
|
|
4955
|
+
check('getChangedChannels({ ignoreGroups }):已有分组的拐点变化照常报', Boolean(ignoredWaypointChange) && ignoredWaypointChange.waypointsChanged && ignoredWaypointChange.reasons.length === 1 && ignoredWaypointChange.reasons[0] === 'waypoints', JSON.stringify(ignoredWaypointChange?.reasons))
|
|
4956
|
+
splitView.clearChangedChannels('sv-ch')
|
|
4957
|
+
splitView.clearChangedLines()
|
|
4958
|
+
splitView.updateLine('sv-ac', { name: 'AC-变更' })
|
|
4959
|
+
const fieldLineChange = splitView.getChangedLines({ reasons: ['fields'] }).find(line => line.id === 'sv-ac')
|
|
4960
|
+
check('getChangedLines({ reasons: [fields] }):线路字段变化入账', Boolean(fieldLineChange) && fieldLineChange.fields.includes('name') && fieldLineChange.reasons.length === 1 && fieldLineChange.reasons[0] === 'fields', JSON.stringify(fieldLineChange?.reasons))
|
|
4961
|
+
splitView.clearChangedLines('sv-ac')
|
|
4962
|
+
splitView.setLineWaypoints('sv-ac', [[4, 4]])
|
|
4963
|
+
const pathLineChange = splitView.getChangedLines({ reasons: ['path'] }).find(line => line.id === 'sv-ac')
|
|
4964
|
+
check('getChangedLines:path 变化独立记 path,读取不改基准', Boolean(pathLineChange) && pathLineChange.pathChanged && pathLineChange.reasons.includes('path') && splitView.getChangedLines({ reasons: ['path'] }).length === 1, JSON.stringify(pathLineChange?.reasons))
|
|
4965
|
+
splitView.clearChangedLines()
|
|
4966
|
+
const hideChannel = splitView.hideFeatures('channel', 'sv-ch')
|
|
4967
|
+
const hiddenChannelRead = splitView.getChannel('sv-ch')
|
|
4968
|
+
const hideChanged = splitView.getChangedChannels().find(channel => channel.id === 'sv-ch')
|
|
4969
|
+
check('hideFeatures(channel):visible 落数据、通道退场,变化账本记 visibility', hideChannel.ok === true && splitView.getOptions().channels.find(channel => channel.id === 'sv-ch')?.visible === false && hiddenChannelRead?.lineIndexes?.length === 0 && hideChanged?.visibilityChanged === true, JSON.stringify({ ok: hideChannel.ok, paths: hiddenChannelRead?.lineIndexes?.length, reasons: hideChanged?.reasons }))
|
|
4970
|
+
const showChannel = splitView.showFeatures('channel', 'sv-ch')
|
|
4971
|
+
check('showFeatures(channel):隐藏后原样恢复(回线、分组、拐点都还在)', showChannel.ok === true && splitView.getChannel('sv-ch')?.lineIndexes?.length === 2 && splitView.getOptions().channels.find(channel => channel.id === 'sv-ch')?.visible === true && splitView.getChannelGroups('sv-ch').length === 1, JSON.stringify({ lines: splitView.getChannel('sv-ch')?.lineIndexes?.length, groups: splitView.getChannelGroups('sv-ch').length }))
|
|
4972
|
+
const generatedId = splitView.getData().points.find(point => point.name === 'A-右')?.id
|
|
4973
|
+
const hidePoint = splitView.hideFeatures('point', generatedId)
|
|
4974
|
+
const hideLine = splitView.hideFeatures('line', 'sv-ac')
|
|
4975
|
+
check('hideFeatures(point / line):只改 visible,数据不删', hidePoint.ok === true && hideLine.ok === true && splitView.getData().points.find(point => point.id === generatedId)?.visible === false && splitView.getData().lines.find(line => line.id === 'sv-ac')?.visible === false, JSON.stringify({ point: hidePoint.ok, line: hideLine.ok }))
|
|
4976
|
+
const hiddenLineChange = splitView.getChangedLines().find(line => line.id === 'sv-ac')
|
|
4977
|
+
check('getChangedLines:线路显隐变化入账', Boolean(hiddenLineChange) && hiddenLineChange.visibilityChanged && hiddenLineChange.reasons.includes('visibility'), JSON.stringify(hiddenLineChange?.reasons))
|
|
4978
|
+
splitView.showFeatures('point', generatedId)
|
|
4979
|
+
splitView.showFeatures('line', 'sv-ac')
|
|
4980
|
+
check('showFeatures(point / line):恢复后 visible 回到 true', splitView.getData().points.find(point => point.id === generatedId)?.visible === true && splitView.getData().lines.find(line => line.id === 'sv-ac')?.visible === true, '')
|
|
4981
|
+
splitView.clearChangedLines('sv-ac')
|
|
4982
|
+
splitView.updateLine('sv-ac', { name: 'AC-删除前' })
|
|
4983
|
+
const lineChangedBeforeDelete = splitView.getChangedLines().some(line => line.id === 'sv-ac')
|
|
4984
|
+
splitView.removeLine('sv-ac')
|
|
4985
|
+
const lineAfterDelete = splitView.getChangedLines().find(line => line.id === 'sv-ac')
|
|
4986
|
+
check('getChangedLines:线路删除同步清账,不再返回已删线路', lineChangedBeforeDelete && !lineAfterDelete, JSON.stringify({ before: lineChangedBeforeDelete, after: lineAfterDelete ?? null }))
|
|
4987
|
+
splitView.clearChangedChannels()
|
|
4988
|
+
splitView.updateChannel('sv-ch', { name: '待删通道' })
|
|
4989
|
+
const channelChangedBeforeDelete = splitView.getChangedChannels().some(channel => channel.id === 'sv-ch')
|
|
4990
|
+
splitView.removeChannel('sv-ch')
|
|
4991
|
+
const channelAfterDelete = splitView.getChangedChannels().find(channel => channel.id === 'sv-ch')
|
|
4992
|
+
check('getChangedChannels:通道删除同步清账,不再返回已删通道', channelChangedBeforeDelete && !channelAfterDelete, JSON.stringify({ before: channelChangedBeforeDelete, after: channelAfterDelete ?? null }))
|
|
4993
|
+
const lineOnlyHost = document.createElement('div')
|
|
4994
|
+
lineOnlyHost.style.cssText = 'width:480px;height:280px'
|
|
4995
|
+
document.body.append(lineOnlyHost)
|
|
4996
|
+
const lineOnlyView = createGisView(lineOnlyHost, {
|
|
4997
|
+
points: [
|
|
4998
|
+
{ id: 'lo-a', x: 0, y: 0 },
|
|
4999
|
+
{ id: 'lo-b', x: 8, y: 0 }
|
|
5000
|
+
],
|
|
5001
|
+
lines: [{ id: 'lo-ab', from: 'lo-a', to: 'lo-b', name: 'AB' }],
|
|
5002
|
+
initialView: { scale: 1, center: [4, 0] },
|
|
5003
|
+
labelOptimize: false,
|
|
5004
|
+
showGrid: false
|
|
5005
|
+
})
|
|
5006
|
+
const lineOnlyInitial = lineOnlyView.getChangedLines().length === 0
|
|
5007
|
+
lineOnlyView.updateLine('lo-ab', { name: 'AB-变更' })
|
|
5008
|
+
const lineOnlyChange = lineOnlyView.getChangedLines({ reasons: ['fields'] }).find(line => line.id === 'lo-ab')
|
|
5009
|
+
lineOnlyView.removeLine('lo-ab')
|
|
5010
|
+
const lineOnlyAfterDelete = lineOnlyView.getChangedLines().find(line => line.id === 'lo-ab')
|
|
5011
|
+
check('getChangedLines:只有节点和线路时独立对账,删线路后同步清账', lineOnlyInitial && Boolean(lineOnlyChange) && lineOnlyChange.fields.includes('name') && !lineOnlyAfterDelete, JSON.stringify({ initial: lineOnlyInitial, change: lineOnlyChange?.reasons, after: lineOnlyAfterDelete ?? null }))
|
|
5012
|
+
lineOnlyView.destroy()
|
|
5013
|
+
lineOnlyHost.remove()
|
|
5014
|
+
splitView.destroy()
|
|
5015
|
+
splitHost.remove()
|
|
4804
5016
|
draftView.destroy()
|
|
4805
5017
|
draftHost.remove()
|
|
4806
5018
|
auView.destroy()
|
|
@@ -411,10 +411,66 @@
|
|
|
411
411
|
const quietState = addView.addLabel('point', 'p1', { content: '别拖我', position: 'bottom', draggable: false, editable: false })
|
|
412
412
|
await settle(160)
|
|
413
413
|
const quietNode = addHost.querySelector(`.gis-view__label[data-label-label='${quietState.labelId}']`)
|
|
414
|
-
check('逐条写 draggable / editable: false 的标签单独把指针还回去', Boolean(quietNode) && !quietNode.classList.contains('gis-view__label--draggable') && !quietNode.classList.contains('gis-view__label--editable'), quietNode ? quietNode.className : '没有标签')
|
|
415
|
-
addView.destroy()
|
|
416
|
-
addHost.remove()
|
|
417
|
-
//
|
|
414
|
+
check('逐条写 draggable / editable: false 的标签单独把指针还回去', Boolean(quietNode) && !quietNode.classList.contains('gis-view__label--draggable') && !quietNode.classList.contains('gis-view__label--editable'), quietNode ? quietNode.className : '没有标签')
|
|
415
|
+
addView.destroy()
|
|
416
|
+
addHost.remove()
|
|
417
|
+
// 14b. 数据里自带的 labels:单条 draggable / editable 覆盖全局,DOM 指针与事件也按单条走
|
|
418
|
+
const perItemHost = document.createElement('div')
|
|
419
|
+
perItemHost.style.cssText = 'width:360px;height:240px'
|
|
420
|
+
document.body.append(perItemHost)
|
|
421
|
+
const perItemClicks = []
|
|
422
|
+
const perItemDrags = []
|
|
423
|
+
const perItemView = createGisViewRaw(perItemHost, {
|
|
424
|
+
points: [{
|
|
425
|
+
id: 'per-item-point',
|
|
426
|
+
x: 0,
|
|
427
|
+
y: 0,
|
|
428
|
+
name: '逐条',
|
|
429
|
+
labels: [
|
|
430
|
+
{ id: 'editable', content: 'A 可拖可点', position: 'top', gap: 24, draggable: true, editable: true },
|
|
431
|
+
{ id: 'locked', content: 'B 只读', position: 'bottom', gap: 24, draggable: false, editable: false }
|
|
432
|
+
]
|
|
433
|
+
}],
|
|
434
|
+
lines: [],
|
|
435
|
+
label: { enabled: true, content: '{name}', maxCount: 5, draggable: true, editable: true },
|
|
436
|
+
labelOptimize: false,
|
|
437
|
+
showGrid: false,
|
|
438
|
+
onLabelClick: payload => perItemClicks.push(payload),
|
|
439
|
+
onLabelDrag: payload => perItemDrags.push(payload)
|
|
440
|
+
})
|
|
441
|
+
await settle(240)
|
|
442
|
+
const perItemNode = id => perItemHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-label='${id}']`)
|
|
443
|
+
const itemEditable = perItemNode('editable')
|
|
444
|
+
const itemLocked = perItemNode('locked')
|
|
445
|
+
check('数据级逐条 labels:A 可拖可点,B 的 false 覆盖全局开关',
|
|
446
|
+
Boolean(itemEditable) && Boolean(itemLocked) &&
|
|
447
|
+
itemEditable.classList.contains('gis-view__label--draggable') && itemEditable.classList.contains('gis-view__label--editable') &&
|
|
448
|
+
!itemLocked.classList.contains('gis-view__label--draggable') && !itemLocked.classList.contains('gis-view__label--editable') &&
|
|
449
|
+
getComputedStyle(itemLocked).pointerEvents === 'none',
|
|
450
|
+
`A=${itemEditable?.className ?? '没有'} / B=${itemLocked?.className ?? '没有'}`)
|
|
451
|
+
if (itemEditable && itemLocked) {
|
|
452
|
+
const lockedAt = centerOf(itemLocked)
|
|
453
|
+
fire(itemLocked, 'pointerdown', lockedAt[0], lockedAt[1])
|
|
454
|
+
fire(window, 'pointerup', lockedAt[0], lockedAt[1])
|
|
455
|
+
fire(itemLocked, 'pointerdown', lockedAt[0], lockedAt[1])
|
|
456
|
+
fire(window, 'pointermove', lockedAt[0] + 36, lockedAt[1] + 18)
|
|
457
|
+
fire(window, 'pointerup', lockedAt[0] + 36, lockedAt[1] + 18)
|
|
458
|
+
await settle(120)
|
|
459
|
+
check('逐条只读标签点不发 labelClick、拖不写偏移', perItemClicks.length === 0 && perItemDrags.length === 0 && !perItemView.getLabelStates('point').some(state => state.labelId === 'locked'), `${perItemClicks.length} 次点击 / ${perItemDrags.length} 次拖动 / ${JSON.stringify(perItemView.getLabelStates('point'))}`)
|
|
460
|
+
const editableAt = centerOf(itemEditable)
|
|
461
|
+
fire(itemEditable, 'pointerdown', editableAt[0], editableAt[1])
|
|
462
|
+
fire(window, 'pointerup', editableAt[0], editableAt[1])
|
|
463
|
+
await settle(80)
|
|
464
|
+
fire(itemEditable, 'pointerdown', editableAt[0], editableAt[1])
|
|
465
|
+
fire(window, 'pointermove', editableAt[0] + 32, editableAt[1] + 20)
|
|
466
|
+
fire(window, 'pointerup', editableAt[0] + 32, editableAt[1] + 20)
|
|
467
|
+
await settle(160)
|
|
468
|
+
const editableState = perItemView.getLabelStates('point').find(state => state.labelId === 'editable')
|
|
469
|
+
check('逐条可交互标签照常点选与拖动,偏移按 labelId 分开记', perItemClicks.length === 1 && perItemClicks[0]?.labelId === 'editable' && perItemDrags.some(payload => payload.phase === 'end' && payload.labelId === 'editable') && Math.abs(editableState?.offset?.[0] - 32) <= 1 && Math.abs(editableState?.offset?.[1] - 20) <= 1, JSON.stringify({ clicks: perItemClicks.map(payload => payload.labelId), drag: perItemDrags.map(payload => `${payload.labelId}:${payload.phase}`), offset: editableState?.offset ?? null }))
|
|
470
|
+
}
|
|
471
|
+
perItemView.destroy()
|
|
472
|
+
perItemHost.remove()
|
|
473
|
+
// 15. 线路在通道里:parent.channel 说清「哪条通道的哪一回线」(id / groupId / 通道下标 / 分组里的位置)
|
|
418
474
|
const chanHost = document.createElement('div')
|
|
419
475
|
chanHost.style.cssText = 'width:320px;height:220px'
|
|
420
476
|
document.body.append(chanHost)
|
|
@@ -1048,6 +1104,195 @@
|
|
|
1048
1104
|
else {
|
|
1049
1105
|
delete window.devicePixelRatio
|
|
1050
1106
|
}
|
|
1107
|
+
// 26. HTML 标签:全局 / 逐条覆盖 / 模板转义 / 运行时切换 / 导出回灌 / 只改样式不重写内容
|
|
1108
|
+
const richHost = document.createElement('div')
|
|
1109
|
+
richHost.style.cssText = 'width:800px;height:500px'
|
|
1110
|
+
document.body.append(richHost)
|
|
1111
|
+
const richClicks = []
|
|
1112
|
+
const richPoints = [
|
|
1113
|
+
{ id: 'rich-a', x: -180, y: 0, name: '<i>甲</i>', labels: [{ id: 'default', content: '<b class="rich">{name}</b>', html: true }] },
|
|
1114
|
+
{ id: 'rich-b', x: 0, y: 0, name: '乙', labels: [{ id: 'default', content: '<b>{name}</b>', html: false }] },
|
|
1115
|
+
{ id: 'rich-c', x: 180, y: 0, name: '丙' },
|
|
1116
|
+
{ id: 'rich-d', x: 270, y: 100, name: '<i>丁</i>', labels: [
|
|
1117
|
+
{ id: 'field', field: 'name', html: true, position: 'bottom' },
|
|
1118
|
+
{ id: 'fallback', html: true, position: 'top' }
|
|
1119
|
+
] }
|
|
1120
|
+
]
|
|
1121
|
+
const richView = createGisView(richHost, {
|
|
1122
|
+
points: richPoints,
|
|
1123
|
+
lines: [{ id: 'rich-line', from: 'rich-a', to: 'rich-b', name: '<u>线</u>' }],
|
|
1124
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1125
|
+
label: { enabled: true, content: '<b>{name}</b>', html: false, offset: [12, 8], maxCount: 10, draggable: true, editable: true },
|
|
1126
|
+
lineLabel: { enabled: true, content: '<em>{name}</em>', html: true, maxCount: 10 },
|
|
1127
|
+
labelOptimize: false,
|
|
1128
|
+
showGrid: false,
|
|
1129
|
+
onLabelClick: payload => richClicks.push(payload)
|
|
1130
|
+
})
|
|
1131
|
+
await settle(240)
|
|
1132
|
+
const richPoint = index => richHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-index='${index}']`)
|
|
1133
|
+
const richLine = () => richHost.querySelector(`.gis-view__label[data-label-kind='line']`)
|
|
1134
|
+
const firstRich = richPoint(0)
|
|
1135
|
+
check('逐条 html: true:字符串按标记渲染,模板取值仍转义', firstRich?.querySelector('b.rich') !== null && firstRich.querySelector('i') === null && firstRich.textContent.trim() === '<i>甲</i>', firstRich ? `${firstRich.innerHTML} / ${firstRich.textContent}` : '没有标签')
|
|
1136
|
+
check('逐条 html: false:即使全局是 HTML,这一条也只显示字面标记', richPoint(1)?.querySelector('b') === null && richPoint(1)?.textContent.trim() === '<b>乙</b>', richPoint(1)?.textContent ?? '没有标签')
|
|
1137
|
+
check('全局 html: false:没有逐条覆盖的标签按纯文本渲染', richPoint(2)?.querySelector('b') === null && richPoint(2)?.textContent.trim() === '<b>丙</b>', richPoint(2)?.textContent ?? '没有标签')
|
|
1138
|
+
check('线路标签 html: true 同样渲染标记且转义模板取值', richLine()?.querySelector('em') !== null && richLine().querySelector('u') === null && richLine().textContent.trim() === '<u>线</u>', richLine()?.innerHTML ?? '没有线路标签')
|
|
1139
|
+
const richByLabelId = id => richHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-label='${id}']`)
|
|
1140
|
+
check('HTML 模式下 field / 默认名称只按纯文本转义', richByLabelId('field')?.querySelector('i') === null && richByLabelId('field')?.textContent.trim() === '<i>丁</i>' && richByLabelId('fallback')?.querySelector('i') === null && richByLabelId('fallback')?.textContent.trim() === '<i>丁</i>', `${richByLabelId('field')?.innerHTML ?? ''} | ${richByLabelId('fallback')?.innerHTML ?? ''}`)
|
|
1141
|
+
richView.updateOptions({ label: { enabled: true, content: '<b>{name}</b>', html: true, offset: [12, 8], maxCount: 10, draggable: true, editable: true } })
|
|
1142
|
+
await settle(200)
|
|
1143
|
+
check('全局 html: true:没有逐条覆盖的标签改成按 HTML 渲染', richPoint(2)?.querySelector('b') !== null && richPoint(2)?.textContent.trim() === '丙', richPoint(2)?.innerHTML ?? '没有标签')
|
|
1144
|
+
check('全局 HTML 打开时,逐条 html: false 仍压过全局', richPoint(1)?.querySelector('b') === null && richPoint(1)?.textContent.trim() === '<b>乙</b>', richPoint(1)?.textContent ?? '没有标签')
|
|
1145
|
+
richView.updateLabel('point', 'rich-c', { html: false })
|
|
1146
|
+
await settle(160)
|
|
1147
|
+
check('updateLabel html: false 把这一条从 HTML 切回纯文本', richPoint(2)?.querySelector('b') === null && richPoint(2)?.textContent.trim() === '<b>丙</b>', richPoint(2)?.textContent ?? '没有标签')
|
|
1148
|
+
const richOffsetWithState = richView.getLabels('point', 'rich-c')?.find(row => row.id === 'default')
|
|
1149
|
+
check('getLabels 的偏移不被运行时 html 状态洗成 [0, 0]', JSON.stringify(richOffsetWithState?.offset) === '[12,8]', JSON.stringify(richOffsetWithState?.offset ?? null))
|
|
1150
|
+
richView.updateLabel('point', 'rich-c', { html: true, offset: [23, 7] })
|
|
1151
|
+
await settle(160)
|
|
1152
|
+
check('updateLabel html: true 能再切回 HTML', richPoint(2)?.querySelector('b') !== null && richPoint(2)?.textContent.trim() === '丙', richPoint(2)?.innerHTML ?? '没有标签')
|
|
1153
|
+
const richOffsetRuntime = richView.getLabels('point', 'rich-c')?.find(row => row.id === 'default')
|
|
1154
|
+
check('getLabels 优先报运行时拖出来的偏移', JSON.stringify(richOffsetRuntime?.offset) === '[23,7]', JSON.stringify(richOffsetRuntime?.offset ?? null))
|
|
1155
|
+
const richClickNode = richPoint(0)
|
|
1156
|
+
const richClickAt = centerOf(richClickNode)
|
|
1157
|
+
fire(richClickNode, 'pointerdown', richClickAt[0], richClickAt[1])
|
|
1158
|
+
fire(window, 'pointerup', richClickAt[0], richClickAt[1])
|
|
1159
|
+
await settle(60)
|
|
1160
|
+
check('labelClick 载荷带 html 与原始标记(编辑面板不会把富文本洗成纯文本)', richClicks.length === 1 && richClicks[0].label?.html === true && richClicks[0].label?.content === richClickNode.innerHTML && richClicks[0].label.content.includes('<i>'), JSON.stringify(richClicks[0]?.label ?? null))
|
|
1161
|
+
const richBoldBefore = richPoint(0)?.querySelector('b')
|
|
1162
|
+
richView.updateLabel('point', 'rich-a', { style: { fontSize: '15px' } })
|
|
1163
|
+
await settle(160)
|
|
1164
|
+
check('只改样式不重写 HTML 内容(子节点身份保持不变)', richBoldBefore !== null && richPoint(0)?.querySelector('b') === richBoldBefore && getComputedStyle(richPoint(0)).fontSize === '15px', `${richPoint(0)?.innerHTML ?? ''} / ${richPoint(0) ? getComputedStyle(richPoint(0)).fontSize : ''}`)
|
|
1165
|
+
const richExport = richView.exportData({ format: 'input', parts: ['points'] })
|
|
1166
|
+
const exportedRichB = richExport.points?.find(row => row.id === 'rich-b')
|
|
1167
|
+
const exportedRichA = richExport.points?.find(row => row.id === 'rich-a')
|
|
1168
|
+
check('导出 input 保留逐条 html: true / false(导出再回灌不会改回全局语义)', exportedRichA?.labels?.find(row => row.id === 'default')?.html === true && exportedRichB?.labels?.find(row => row.id === 'default')?.html === false, JSON.stringify({ a: exportedRichA?.labels, b: exportedRichB?.labels }))
|
|
1169
|
+
const reRichHost = document.createElement('div')
|
|
1170
|
+
reRichHost.style.cssText = 'width:800px;height:500px'
|
|
1171
|
+
document.body.append(reRichHost)
|
|
1172
|
+
const reRichView = createGisView(reRichHost, {
|
|
1173
|
+
points: richExport.points,
|
|
1174
|
+
lines: [],
|
|
1175
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1176
|
+
label: { enabled: true, content: '<b>{name}</b>', html: true, maxCount: 10 },
|
|
1177
|
+
labelOptimize: false,
|
|
1178
|
+
showGrid: false
|
|
1179
|
+
})
|
|
1180
|
+
await settle(240)
|
|
1181
|
+
const reRichPoint = index => reRichHost.querySelector(`.gis-view__label[data-label-kind='point'][data-label-index='${index}']`)
|
|
1182
|
+
check('回灌后逐条 html: false 仍是纯文本,html: true 仍是富文本', reRichPoint(1)?.querySelector('b') === null && reRichPoint(1)?.textContent.trim() === '<b>乙</b>' && reRichPoint(0)?.querySelector('b.rich') !== null, `${reRichPoint(0)?.innerHTML ?? ''} | ${reRichPoint(1)?.innerHTML ?? ''}`)
|
|
1183
|
+
// 27. 同内容标签共用一次量测:120 条 HTML 标签首轮排版不应逐条读布局
|
|
1184
|
+
const batchLabelHost = document.createElement('div')
|
|
1185
|
+
batchLabelHost.id = 'label-batch-host'
|
|
1186
|
+
batchLabelHost.style.cssText = 'width:800px;height:500px'
|
|
1187
|
+
document.body.append(batchLabelHost)
|
|
1188
|
+
const batchLabelPoints = []
|
|
1189
|
+
for (let row = 0; row < 10; row += 1) {
|
|
1190
|
+
for (let column = 0; column < 12; column += 1) {
|
|
1191
|
+
batchLabelPoints.push({ id: `batch-label-${row}-${column}`, x: (column - 5.5) * 60, y: (row - 4.5) * 40, name: '同名' })
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
let batchLabelMeasures = 0
|
|
1195
|
+
const nativeLabelRect = Element.prototype.getBoundingClientRect
|
|
1196
|
+
Element.prototype.getBoundingClientRect = function (...args) {
|
|
1197
|
+
if (this.classList?.contains('gis-view__label') && this.closest?.('#label-batch-host')) {
|
|
1198
|
+
batchLabelMeasures += 1
|
|
1199
|
+
}
|
|
1200
|
+
return nativeLabelRect.apply(this, args)
|
|
1201
|
+
}
|
|
1202
|
+
let batchLabelView = null
|
|
1203
|
+
try {
|
|
1204
|
+
batchLabelView = createGisView(batchLabelHost, {
|
|
1205
|
+
points: batchLabelPoints,
|
|
1206
|
+
lines: [],
|
|
1207
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1208
|
+
label: { enabled: true, content: '<b>同名</b>', html: true, maxCount: 120, scanLimit: 0 },
|
|
1209
|
+
showGrid: false
|
|
1210
|
+
})
|
|
1211
|
+
await settle(300)
|
|
1212
|
+
}
|
|
1213
|
+
finally {
|
|
1214
|
+
Element.prototype.getBoundingClientRect = nativeLabelRect
|
|
1215
|
+
}
|
|
1216
|
+
check('120 条同内容 HTML 标签首次排版共用尺寸缓存(真实布局读取 ≤ 4 次)', batchLabelHost.querySelectorAll('.gis-view__label').length === 120 && batchLabelMeasures <= 4, `标签 ${batchLabelHost.querySelectorAll('.gis-view__label').length} 条 / 量测 ${batchLabelMeasures} 次`)
|
|
1217
|
+
batchLabelView?.destroy()
|
|
1218
|
+
batchLabelHost.remove()
|
|
1219
|
+
// 28. 高密度纯文本标签:默认 auto 在强制显示档改走单画布,HTML / 交互标签仍留 DOM
|
|
1220
|
+
const canvasLabelHost = document.createElement('div')
|
|
1221
|
+
canvasLabelHost.style.cssText = 'width:800px;height:500px'
|
|
1222
|
+
document.body.append(canvasLabelHost)
|
|
1223
|
+
const canvasLabelPoints = []
|
|
1224
|
+
for (let row = 0; row < 25; row += 1) {
|
|
1225
|
+
for (let column = 0; column < 44; column += 1) {
|
|
1226
|
+
canvasLabelPoints.push({
|
|
1227
|
+
id: `canvas-label-${row}-${column}`,
|
|
1228
|
+
x: (column - 21.5) * 15,
|
|
1229
|
+
y: (row - 12) * 17,
|
|
1230
|
+
name: `L${row * 44 + column}`
|
|
1231
|
+
})
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const canvasLabelView = createGisViewRaw(canvasLabelHost, {
|
|
1235
|
+
points: canvasLabelPoints,
|
|
1236
|
+
lines: [],
|
|
1237
|
+
initialView: { scale: 1, center: [0, 0] },
|
|
1238
|
+
label: { enabled: true, content: '{name}', hideOnInteract: false },
|
|
1239
|
+
labelOptimize: false,
|
|
1240
|
+
labelRender: 'auto',
|
|
1241
|
+
showGrid: false
|
|
1242
|
+
})
|
|
1243
|
+
await settle(600)
|
|
1244
|
+
const canvasLayer = canvasLabelHost.querySelector('.gis-view__label-layer--point')
|
|
1245
|
+
const labelCanvas = canvasLayer?.querySelector('.gis-view__label-canvas')
|
|
1246
|
+
const canvasCount = () => Number(labelCanvas?.dataset.labelCount || 0)
|
|
1247
|
+
const canvasDomCount = () => canvasLayer?.querySelectorAll('.gis-view__label').length ?? 0
|
|
1248
|
+
check('1100 条纯文本标签自动改走单画布(不再逐条建 div)', canvasCount() >= 1024 && canvasDomCount() === 0, `画布 ${canvasCount()} 条 / DOM ${canvasDomCount()} 条`)
|
|
1249
|
+
check('标签画布按视口分配了非零像素尺寸', Number(labelCanvas?.width) > 0 && Number(labelCanvas?.height) > 0 && parseFloat(labelCanvas.style.width) > 0 && parseFloat(labelCanvas.style.height) > 0, `${labelCanvas?.width ?? 0}×${labelCanvas?.height ?? 0} 像素 / ${labelCanvas?.style.width ?? ''}×${labelCanvas?.style.height ?? ''}`)
|
|
1250
|
+
const mixedCanvasPoints = canvasLabelPoints.map((point, index) => index === 0
|
|
1251
|
+
? { ...point, labels: [{ id: 'rich', content: '<b>富</b>', html: true }] }
|
|
1252
|
+
: point)
|
|
1253
|
+
canvasLabelView.updateOptions({ points: mixedCanvasPoints })
|
|
1254
|
+
await settle(600)
|
|
1255
|
+
const mixedDomLabels = [...(canvasLayer?.querySelectorAll('.gis-view__label') ?? [])]
|
|
1256
|
+
check('混入数据级 html: true:纯文本仍进画布,HTML 单独保留 DOM', canvasCount() >= 1100 && mixedDomLabels.length === 1 && mixedDomLabels[0].querySelector('b') !== null, `画布 ${canvasCount()} 条 / DOM ${mixedDomLabels.length} 条 / ${mixedDomLabels[0]?.innerHTML ?? ''}`)
|
|
1257
|
+
canvasLabelView.updateOptions({ labelRender: 'dom' })
|
|
1258
|
+
await settle(600)
|
|
1259
|
+
check('labelRender: dom 强制回退逐条 DOM,并清空画布', canvasDomCount() >= 1100 && canvasCount() === 0, `DOM ${canvasDomCount()} 条 / 画布 ${canvasCount()} 条`)
|
|
1260
|
+
canvasLabelView.updateOptions({ labelRender: 'auto' })
|
|
1261
|
+
await settle(600)
|
|
1262
|
+
check('从 dom 档切回 auto:画布重新接管,上一轮 div 全部回收', canvasCount() >= 1100 && canvasDomCount() === 1, `画布 ${canvasCount()} 条 / DOM ${canvasDomCount()} 条`)
|
|
1263
|
+
const decodeSvg = (url) => {
|
|
1264
|
+
const comma = url.indexOf(',')
|
|
1265
|
+
const body = url.slice(comma + 1)
|
|
1266
|
+
if (url.slice(0, comma).includes('base64')) {
|
|
1267
|
+
const binary = atob(body)
|
|
1268
|
+
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0))
|
|
1269
|
+
return new TextDecoder().decode(bytes)
|
|
1270
|
+
}
|
|
1271
|
+
return decodeURIComponent(body)
|
|
1272
|
+
}
|
|
1273
|
+
const exportedCanvasLabel = async (text = 'L550') => {
|
|
1274
|
+
const markup = decodeSvg(await canvasLabelView.exportImage({ type: 'svg', includeLabels: true, background: 'none' }))
|
|
1275
|
+
const doc = new DOMParser().parseFromString(markup, 'image/svg+xml')
|
|
1276
|
+
const texts = [...doc.querySelectorAll('text')].map(node => node.textContent)
|
|
1277
|
+
return { node: [...doc.querySelectorAll('text')].find(node => node.textContent === text) ?? null, texts }
|
|
1278
|
+
}
|
|
1279
|
+
const exportedBefore = (await exportedCanvasLabel()).node
|
|
1280
|
+
check('导出 SVG 包含画布档的纯文本标签', exportedBefore !== null, exportedBefore ? exportedBefore.outerHTML : '没有找到 L550')
|
|
1281
|
+
const anchorPoint = canvasLabelPoints[550]
|
|
1282
|
+
const anchorGap = Number(exportedBefore?.getAttribute('x')) - canvasLabelView.project(anchorPoint.x, anchorPoint.y)[0]
|
|
1283
|
+
const cameraBeforeCanvasZoom = canvasLabelView.getCamera()
|
|
1284
|
+
canvasLabelView.setCamera({ ...cameraBeforeCanvasZoom, scale: cameraBeforeCanvasZoom.scale * 2 })
|
|
1285
|
+
await settle(80)
|
|
1286
|
+
const exportedZoom = await exportedCanvasLabel()
|
|
1287
|
+
const exportedZoomed = exportedZoom.node
|
|
1288
|
+
const expectedZoomX = canvasLabelView.project(anchorPoint.x, anchorPoint.y)[0] + anchorGap
|
|
1289
|
+
check('Canvas 标签随缩放只推一次,不与导出坐标二次叠加', Number.isFinite(anchorGap) && exportedZoomed !== null && Math.abs(Number(exportedZoomed.getAttribute('x')) - expectedZoomX) <= 1, `期望 x=${expectedZoomX.toFixed(1)} / 实际 ${exportedZoomed?.getAttribute('x') ?? '没有标签'} / 文本 ${exportedZoom.texts.length} 条:${exportedZoom.texts.slice(0, 8).join(',')} / 画布 ${canvasCount()} / DOM ${canvasDomCount()} / 收起 ${canvasLayer?.classList.contains('gis-view__label-layer--hidden')} / 相机 ${JSON.stringify(canvasLabelView.getCamera())}`)
|
|
1290
|
+
canvasLabelView.destroy()
|
|
1291
|
+
canvasLabelHost.remove()
|
|
1292
|
+
richView.destroy()
|
|
1293
|
+
reRichView.destroy()
|
|
1294
|
+
richHost.remove()
|
|
1295
|
+
reRichHost.remove()
|
|
1051
1296
|
multi.destroy()
|
|
1052
1297
|
reView.destroy()
|
|
1053
1298
|
multiHost.remove()
|