gis-view-webgl2 0.1.2 → 0.1.4

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.
Files changed (66) hide show
  1. package/README.md +21 -14
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +421 -76
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +151 -0
  7. package/docs/GisView.md +4 -4
  8. 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 +137 -0
  9. package/docs/api/index.html +23 -9
  10. package/docs/demos/ApiCoverageDemo.vue +644 -0
  11. package/docs/demos/ChannelDemo.vue +14 -12
  12. package/docs/demos/ClusterDemo.vue +251 -0
  13. package/docs/demos/CopyMergeDemo.vue +3 -4
  14. package/docs/demos/EventDemo.vue +3 -3
  15. package/docs/demos/ExtensionDemo.vue +69 -12
  16. package/docs/demos/GisViewDemo.vue +64 -8809
  17. package/docs/demos/SearchFilterDemo.vue +13 -11
  18. package/docs/demos/api-index.js +58 -47
  19. package/docs/demos/demos.js +34 -17
  20. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +29 -0
  21. package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
  22. package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
  23. package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
  24. package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
  25. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
  26. package/docs/demos/gis-view-demo/demo.css +1230 -0
  27. package/docs/demos/gis-view-demo/demoContext.js +12 -0
  28. package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
  29. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
  30. package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
  31. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
  32. package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
  33. package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +3 -2
  34. package/docs/index.html +41 -4
  35. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +1 -1
  36. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +3 -2
  37. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +104 -16
  38. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +15 -14
  39. 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 +40 -3
  40. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +50 -3
  41. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +6 -2
  42. 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 +39 -7
  43. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +13 -4
  44. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
  45. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +121 -49
  46. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +37 -3
  47. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +17 -8
  48. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +45 -25
  49. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +91 -20
  50. package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +3 -3
  51. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +25 -1
  52. 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 +11 -8
  53. package/docs/steps/README.md +17 -11
  54. package/docs/steps/selftest-actions.html +3 -5
  55. package/docs/steps/selftest-demo.html +27 -18
  56. package/docs/steps/selftest-demos.html +409 -32
  57. package/docs/steps/selftest-docs.html +33 -4
  58. package/docs/steps/selftest-effects.html +200 -0
  59. package/docs/steps/selftest-features.html +603 -21
  60. package/docs/steps/selftest-labels.html +74 -0
  61. package/docs/steps/selftest-perf.html +293 -20
  62. package/docs/steps/selftest-scale.html +150 -29
  63. package/docs/steps/selftest-select.html +90 -0
  64. package/docs/steps/step-10-full.js +1 -1
  65. package/docs/steps/step-12-events.js +1 -1
  66. package/package.json +1 -1
@@ -0,0 +1,644 @@
1
+ <script setup>
2
+ /**
3
+ * 接口能力:把最近几轮新增、但不容易在单个业务场景里看全的 API 放在一页。
4
+ *
5
+ * 四块:
6
+ * ① 级联高亮的三种触发(单击 / 多选 / 框选)可以独立开关;
7
+ * ② selectFeatures / selectWaypoint 的程序化选中(节点、线路、通道、分组、拐点);
8
+ * ③ focusOn 的 point / line / channel / group 四种显式类型;
9
+ * ④ lineFollowsNode 开关,隐藏端点节点时对照线路是否跟随;
10
+ * 另外补几个常用只读接口(坐标互转、视口与渲染器、场景快照、下一帧等待等)的现场读数。
11
+ */
12
+ import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
13
+ import { createGisView } from '../../src/index.js'
14
+ import { buildDemoData } from './shared/demoData.js'
15
+ import './shared/demoStyle.css'
16
+
17
+ const emit = defineEmits(['notice'])
18
+ const container = ref(null)
19
+ const view = shallowRef(null)
20
+ const dataset = buildDemoData({ count: 24 })
21
+
22
+ const triggerOnClick = ref(true)
23
+ const triggerOnMultiSelect = ref(true)
24
+ const triggerOnBoxSelect = ref(true)
25
+ const lineFollowsNode = ref(true)
26
+ const endpointHidden = ref(false)
27
+ const channelId = 'channel-a'
28
+ let groupId = ''
29
+
30
+ const cascadeReadout = ref('级联高亮:点节点 / 线路 / 通道回线试试;框选也会触发')
31
+ const selectionReadout = ref('尚未选中')
32
+ const focusReadout = ref('尚未聚焦')
33
+ const visibilityReadout = ref('')
34
+ const apiReadout = ref('点一个只读接口看结果')
35
+ const advancedReadout = ref('这里集中演示样式、装饰、通道写入、历史与运行时清理接口')
36
+ const styleApplied = ref(false)
37
+ const geoBackgroundOn = ref(false)
38
+ let timer = null
39
+
40
+ const toggleStyleApis = () => {
41
+ const current = view.value
42
+ if (!current) {
43
+ return
44
+ }
45
+ if (styleApplied.value) {
46
+ current.updatePointStyle({ color: '#fff', size: 6, strokeColor: '#4c8dff', strokeWidth: 1.4 })
47
+ current.updateLineStyle({ color: 'rgba(100, 150, 220, 0.55)', width: 1.5 })
48
+ }
49
+ else {
50
+ current.updatePointStyle({ color: '#fde68a', size: 8, strokeColor: '#f59e0b', strokeWidth: 2 })
51
+ current.updateLineStyle({ color: '#fbbf24', width: 2.6 })
52
+ }
53
+ styleApplied.value = !styleApplied.value
54
+ const options = current.getOptions()
55
+ advancedReadout.value = `updatePointStyle / updateLineStyle 字段级合并:点 size=${options.pointStyle?.size ?? '—'} / 线 width=${options.lineStyle?.width ?? '—'} · ${styleApplied.value ? '已应用高亮样式' : '已恢复演示样式'}`
56
+ }
57
+
58
+ const exercisePointDecorationApis = () => {
59
+ const current = view.value
60
+ if (!current) {
61
+ return
62
+ }
63
+ const before = current.getPointDecorations('node-0').length
64
+ const added = current.addPointDecoration('node-0', {
65
+ symbol: 'square',
66
+ color: '#fbbf24',
67
+ strokeColor: '#0f172a',
68
+ strokeWidth: 1,
69
+ size: 18,
70
+ offset: [20, 0]
71
+ })
72
+ const updated = current.updatePointDecoration('node-0', before, { size: 22 })
73
+ const afterUpdate = current.getPointDecorations('node-0').length
74
+ const removed = current.removePointDecoration('node-0', before)
75
+ advancedReadout.value = `节点装饰 CRUD:addPointDecoration=${added ? 'ok' : 'null'} · updatePointDecoration size=${updated?.size ?? '—'} · getPointDecorations=${afterUpdate} 条 · removePointDecoration=${removed ? 'ok' : 'null'}`
76
+ }
77
+
78
+ const exerciseLineDecorationApis = () => {
79
+ const current = view.value
80
+ if (!current) {
81
+ return
82
+ }
83
+ const before = current.getLineDecorations('line-0').length
84
+ const added = current.addLineDecoration('line-0', {
85
+ symbol: 'square',
86
+ color: '#22d3ee',
87
+ size: 14,
88
+ position: 0.5
89
+ })
90
+ const updated = current.updateLineDecoration('line-0', before, { size: 20 })
91
+ const removed = current.removeLineDecoration('line-0', before)
92
+ advancedReadout.value = `线路装饰 CRUD:addLineDecoration=${added ? 'ok' : 'null'} · updateLineDecoration size=${updated?.size ?? '—'} · removeLineDecoration=${removed ? 'ok' : 'null'}`
93
+ }
94
+
95
+ const exerciseChannelLineApis = () => {
96
+ const current = view.value
97
+ if (!current) {
98
+ return
99
+ }
100
+ const between = current.getChannelBetween('node-0', 'node-7')
101
+ current.setChannelLineWidth(channelId, 3.4)
102
+ const added = current.addLines({ id: 'api-temp-line', from: 'node-1', to: 'node-2', color: '#fb923c' })
103
+ current.clearChangedLines()
104
+ current.updateLine('api-temp-line', { color: '#22d3ee', width: 3 })
105
+ const changed = current.getChangedLines({ reasons: ['fields'] })
106
+ current.clearChangedLines()
107
+ const removed = current.removeLines('api-temp-line')
108
+ current.setChannelLineWidth(channelId, 2.6)
109
+ advancedReadout.value = `通道 / 线路写入:getChannelBetween=${between?.hasChannel ? `已有 ${between.groups.length} 组` : '无'} · setChannelLineWidth=3.4→2.6 · addLines=${added.count} 条 · getChangedLines=${changed.length} 条 · clearChangedLines + removeLines=${removed.ok ? 'ok' : 'failed'}`
110
+ }
111
+
112
+ const exerciseVisibilityApis = () => {
113
+ const current = view.value
114
+ const entry = current?.listPoints().find(item => String(item.item?.id) === 'node-2')
115
+ if (!current || !entry) {
116
+ return
117
+ }
118
+ const hiddenOutcome = current.setFeatureVisible('point', 'node-2', false)
119
+ const hidden = current.isPointVisible(entry.index)
120
+ const shownOutcome = current.setFeatureVisible('point', 'node-2', true)
121
+ const shown = current.isPointVisible(entry.index)
122
+ advancedReadout.value = `setFeatureVisible / isPointVisible:隐藏 ${hiddenOutcome.ok ? 'ok' : 'failed'} → isPointVisible=${hidden} · 恢复 ${shownOutcome.ok ? 'ok' : 'failed'} → isPointVisible=${shown}`
123
+ }
124
+
125
+ const toggleGeoBackgroundApis = () => {
126
+ const current = view.value
127
+ if (!current) {
128
+ return
129
+ }
130
+ if (geoBackgroundOn.value) {
131
+ const labels = current.getBackgroundLabels()
132
+ const projection = current.getGeoProjection()
133
+ current.updateBackground({})
134
+ geoBackgroundOn.value = false
135
+ advancedReadout.value = `移开演示背景:清理前 getBackgroundLabels=${labels?.length ?? 0} 条 · getGeoProjection=${projection ? '有' : 'null'} · updateBackground({}) 已恢复透明底`
136
+ return
137
+ }
138
+ const bounds = current.getBounds()
139
+ const ring = [
140
+ [bounds.minX, bounds.minY],
141
+ [bounds.maxX, bounds.minY],
142
+ [bounds.maxX, bounds.maxY],
143
+ [bounds.minX, bounds.maxY],
144
+ [bounds.minX, bounds.minY]
145
+ ]
146
+ current.updateBackground({
147
+ color: '#081321',
148
+ geoJson: {
149
+ type: 'FeatureCollection',
150
+ features: [{
151
+ type: 'Feature',
152
+ properties: { name: '接口演示范围' },
153
+ geometry: { type: 'Polygon', coordinates: [ring] }
154
+ }]
155
+ },
156
+ geoJsonStyle: {
157
+ areaColor: '#12253f',
158
+ borderColor: '#38bdf8',
159
+ borderWidth: 1.5,
160
+ fillOpacity: 0.18,
161
+ aspectScale: 1,
162
+ label: { show: true, formatter: '{name}', fontSize: 12, color: '#bae6fd' }
163
+ }
164
+ })
165
+ geoBackgroundOn.value = true
166
+ const labels = current.getBackgroundLabels()
167
+ const projection = current.getGeoProjection()
168
+ const mapped = current.mapDataToGeoJson(current.getData(), { mode: 'lngLat' })
169
+ advancedReadout.value = `updateBackground / GeoJSON:getBackgroundLabels=${labels?.length ?? 0} 条 · getGeoProjection=${projection ? '有' : 'null'} · mapDataToGeoJson=${mapped?.points?.length ?? 0} 个节点 / ${mapped?.lines?.length ?? 0} 条线路`
170
+ }
171
+
172
+ const exerciseHistoryApis = () => {
173
+ const current = view.value
174
+ if (!current) {
175
+ return
176
+ }
177
+ const original = current.getData().points.find(item => String(item.id) === 'node-1')
178
+ current.updatePoint('node-1', { name: `接口历史 ${Date.now()}` })
179
+ const undone = current.undo()
180
+ const redoReady = current.canRedo()
181
+ const redone = current.redo()
182
+ const restored = current.getData().points.find(item => String(item.id) === 'node-1')
183
+ const mergeCommitted = current.commitPointMerge()
184
+ const mergeReverted = current.revertPointMerge({ silent: true })
185
+ const preview = current.setDropPreview({ kind: 'point', world: [original?.x ?? 0, original?.y ?? 0], item: { size: 18, color: '#22d3ee' } })
186
+ const previewReadout = current.getDropPreview()
187
+ current.setDropPreview(null)
188
+ advancedReadout.value = `历史 / 收尾:undo=${undone ? 'ok' : 'null'} → canRedo=${redoReady} → redo=${redone ? 'ok' : 'null'} → name=${restored?.name ?? '—'} · isLayoutReady=${current.isLayoutReady()} · commitPointMerge=${mergeCommitted} / revertPointMerge=${mergeReverted ? 'payload' : 'null'} · getDropPreview=${previewReadout?.kind ?? 'null'}(${preview ? '已写入' : '失败'},随后清掉)`
189
+ }
190
+
191
+ const exerciseOnceApi = () => {
192
+ const current = view.value
193
+ if (!current) {
194
+ return
195
+ }
196
+ let fired = 0
197
+ const stop = current.once('cameraChange', () => { fired += 1 })
198
+ current.zoomBy(1.01)
199
+ current.zoomBy(1 / 1.01)
200
+ stop()
201
+ advancedReadout.value = `once('cameraChange'):两次相机变化只触发 ${fired} 次(订阅在第一次回调后自动摘掉,返回的 stop 可提前取消)`
202
+ }
203
+
204
+ const exerciseCopyApis = () => {
205
+ const current = view.value
206
+ const source = current?.getData().points.find(item => String(item.id) === 'node-2')
207
+ if (!current || !source) {
208
+ return
209
+ }
210
+ const started = current.beginCopyPoint({
211
+ id: 'node-2',
212
+ coordinate: [Number(source.x) + 0.2, Number(source.y) + 0.2],
213
+ autoCommit: false,
214
+ via: 'api-demo'
215
+ })
216
+ const updated = current.updateCopyPoint({ name: '接口复制预览' })
217
+ const committed = current.commitPointCopy()
218
+ const copyId = committed?.item?.id
219
+ if (copyId !== undefined && copyId !== null) {
220
+ current.removePoints([copyId])
221
+ }
222
+ advancedReadout.value = `复制收尾:beginCopyPoint=${started ? 'preview' : 'null'} · updateCopyPoint=${updated?.name ?? 'null'} · commitPointCopy=${committed ? 'ok' : 'null'} · 清理 removePoints=${copyId !== undefined && copyId !== null ? 'ok' : 'skipped'}`
223
+ }
224
+
225
+ const exerciseRemoveApis = () => {
226
+ const current = view.value
227
+ const anchor = current?.getData().points.find(item => String(item.id) === 'node-3')
228
+ if (!current || !anchor) {
229
+ return
230
+ }
231
+ current.addPoints({ id: 'api-remove-node', x: Number(anchor.x) + 0.08, y: Number(anchor.y) + 0.08, name: '待删除临时节点' })
232
+ current.selectFeatures('api-remove-node')
233
+ current.requestRemove()
234
+ const pending = current.getRemoveRequest()
235
+ pending?.cancel()
236
+ const canceled = current.getRemoveRequest()
237
+ const removed = current.removeSelected()
238
+ advancedReadout.value = `删除两条路:requestRemove → getRemoveRequest=${pending ? 'pending' : 'null'} → cancel=${canceled ? 'still pending' : 'cleared'} · removeSelected=${removed.ok ? `${removed.pointIds.length} 个节点` : removed.reason ?? 'failed'}`
239
+ }
240
+
241
+ const exerciseWebGLApis = () => {
242
+ const current = view.value
243
+ if (!current) {
244
+ return
245
+ }
246
+ current.addWebGLLayer({ id: 'api-temp-layer', order: 999, draw() {} })
247
+ const before = current.getWebGLLayers().length
248
+ const cleared = current.clearWebGLLayers()
249
+ const after = current.getWebGLLayers().length
250
+ advancedReadout.value = `WebGL 层:addWebGLLayer → ${before} 层 · clearWebGLLayers=${cleared} · 剩余 ${after} 层`
251
+ }
252
+
253
+ const exerciseRegistryApis = () => {
254
+ const current = view.value
255
+ if (!current) {
256
+ return
257
+ }
258
+ const disposeSymbol = current.registerSymbol('api-ring', () => {})
259
+ const disposeOperator = current.registerOperator('api-even', value => candidate => Number(candidate) % 2 === Number(value))
260
+ const symbols = current.listSymbols()
261
+ const operators = current.listOperators()
262
+ const removedSymbol = current.unregisterSymbol('api-ring')
263
+ const removedOperator = current.unregisterOperator('api-even')
264
+ disposeSymbol()
265
+ disposeOperator()
266
+ advancedReadout.value = `注册表:registerSymbol → listSymbols=${symbols.join(', ') || '空'} → unregisterSymbol=${removedSymbol} · registerOperator → listOperators 含 api-even=${operators.includes('api-even')} → unregisterOperator=${removedOperator}`
267
+ }
268
+
269
+ const readInitialOptions = () => {
270
+ const options = view.value?.getOptions()
271
+ if (!options) {
272
+ return
273
+ }
274
+ advancedReadout.value = `初始化 options:ariaLabel=${options.ariaLabel ?? '—'} · labelRender=${options.labelRender ?? 'auto'} · gridSize=${options.gridSize ?? '—'} · fitOnInit=${options.fitOnInit} · progressiveInit=${options.progressiveInit} · pixelRatio=${options.pixelRatio ?? 'devicePixelRatio'} · maxCanvasPixels=${options.maxCanvasPixels} · waypointStyle=${options.waypointStyle ? '已配置' : '缺省'} · zoomBaseScale=${options.zoomBaseScale ?? '首次适应'} · webglLayers=${options.webglLayers?.length ?? 0}`
275
+ }
276
+
277
+ const cascadeOptions = () => ({
278
+ scope: 'both',
279
+ triggerOnClick: triggerOnClick.value,
280
+ triggerOnMultiSelect: triggerOnMultiSelect.value,
281
+ triggerOnBoxSelect: triggerOnBoxSelect.value,
282
+ pointStyle: { color: '#ecfeff', strokeColor: '#06b6d4', strokeWidth: 2.5, sizeScale: 1.2 },
283
+ lineStyle: { color: '#06b6d4', width: 3, opacity: 0.95 },
284
+ clearOnEmptyClick: true
285
+ })
286
+
287
+ const describeCascade = (payload) => {
288
+ if (!payload) {
289
+ return '级联高亮:已收起(空白处、来源失效或数据刷新)'
290
+ }
291
+ const origin = payload.origin?.target === 'channel'
292
+ ? `通道 ${payload.origin.channelId ?? '—'} / 分组 ${payload.origin.groupId ?? '—'}`
293
+ : payload.origin?.target === 'line'
294
+ ? `线路 ${payload.origin.item?.id ?? payload.origin.index}`
295
+ : `节点 ${payload.origin?.item?.id ?? payload.origin?.index}`
296
+ return `${origin} → ${payload.pointIndexes.length} 个节点 / ${payload.lineIndexes.length} 条线路(与选中集独立)`
297
+ }
298
+
299
+ const refreshSelection = () => {
300
+ const data = view.value?.getSelectionData()
301
+ if (!data?.count) {
302
+ selectionReadout.value = '尚未选中'
303
+ return
304
+ }
305
+ const parts = []
306
+ if (data.pointCount) parts.push(`节点 ${data.pointCount}`)
307
+ if (data.lineCount) parts.push(`线路 ${data.lineCount}`)
308
+ if (data.channelCount) parts.push(`通道 ${data.channelCount}`)
309
+ selectionReadout.value = `${data.count} 项:${parts.join(' / ')}${data.ids.length ? ` · ids ${data.ids.slice(0, 6).join(', ')}` : ''}`
310
+ }
311
+
312
+ const refreshVisibility = () => {
313
+ const current = view.value
314
+ if (!current) {
315
+ return
316
+ }
317
+ const pointCount = current.listPoints({ visibleOnly: true }).length
318
+ const lineCount = current.listLines({ visibleOnly: true }).length
319
+ visibilityReadout.value = `${pointCount} 个可见节点 / ${lineCount} 条可见线路 · 端点 node-0 ${endpointHidden.value ? '已隐藏' : '可见'} · lineFollowsNode=${lineFollowsNode.value}`
320
+ }
321
+
322
+ const scheduleRefreshVisibility = () => {
323
+ window.clearTimeout(timer)
324
+ timer = window.setTimeout(refreshVisibility, 120)
325
+ }
326
+
327
+ const applyCascadeTriggers = () => {
328
+ view.value?.updateOptions({ cascadeHighlight: cascadeOptions() })
329
+ cascadeReadout.value = `级联触发:单击 ${triggerOnClick.value ? '开' : '关'} · 多选 ${triggerOnMultiSelect.value ? '开' : '关'} · 框选 ${triggerOnBoxSelect.value ? '开' : '关'}`
330
+ }
331
+
332
+ const applyLineFollowsNode = () => {
333
+ view.value?.updateOptions({ lineFollowsNode: lineFollowsNode.value })
334
+ scheduleRefreshVisibility()
335
+ emit('notice', `lineFollowsNode 已切换为 ${lineFollowsNode.value}`)
336
+ }
337
+
338
+ const toggleEndpoint = () => {
339
+ endpointHidden.value = !endpointHidden.value
340
+ view.value?.updatePoint('node-0', { visible: !endpointHidden.value })
341
+ scheduleRefreshVisibility()
342
+ }
343
+
344
+ const selectNode = () => {
345
+ view.value?.selectFeatures('node-0')
346
+ refreshSelection()
347
+ }
348
+
349
+ const selectLine = () => {
350
+ view.value?.selectFeatures({ type: 'line', id: 'line-0' })
351
+ refreshSelection()
352
+ }
353
+
354
+ const selectChannel = () => {
355
+ view.value?.selectFeatures({ type: 'channel', channelId })
356
+ refreshSelection()
357
+ }
358
+
359
+ const selectGroup = () => {
360
+ if (!groupId) {
361
+ groupId = view.value?.getChannelGroups(channelId)?.[0]?.id ?? ''
362
+ }
363
+ if (groupId !== '') {
364
+ view.value?.selectFeatures({ type: 'group', channelId, groupId })
365
+ }
366
+ refreshSelection()
367
+ }
368
+
369
+ const selectWaypoint = () => {
370
+ const current = view.value
371
+ if (!current) {
372
+ return
373
+ }
374
+ const entry = current.listLines().find(line => String(line.item?.id) === 'line-0')
375
+ if (!entry) {
376
+ selectionReadout.value = '没有找到 line-0'
377
+ return
378
+ }
379
+ if (!current.getWaypoints(entry.index).length) {
380
+ const data = current.getData()
381
+ const line = data.lines.find(item => String(item.id) === 'line-0')
382
+ const from = data.points.find(point => String(point.id) === String(line?.from))
383
+ const to = data.points.find(point => String(point.id) === String(line?.to))
384
+ if (from && to) {
385
+ current.addWaypoint('line-0', [(Number(from.x) + Number(to.x)) / 2 + 0.16, (Number(from.y) + Number(to.y)) / 2 + 0.16])
386
+ }
387
+ }
388
+ current.selectWaypoint({ lineIndex: entry.index, pathIndex: 1 })
389
+ refreshSelection()
390
+ }
391
+
392
+ const clearSelection = () => {
393
+ view.value?.selectFeatures(null)
394
+ refreshSelection()
395
+ }
396
+
397
+ const focusTarget = (target, label) => {
398
+ const current = view.value
399
+ if (!current) {
400
+ return
401
+ }
402
+ current.focusOn(target, { zoom: true, padding: 64, maxZoom: 20 })
403
+ const camera = current.getCamera()
404
+ focusReadout.value = `${label}:scale ${camera.scale.toPrecision(5)} · 中心 [${camera.center.map(value => Number(value).toFixed(3)).join(', ')}]`
405
+ refreshSelection()
406
+ }
407
+
408
+ const focusNode = () => focusTarget({ type: 'point', id: 'node-0' }, 'focusOn(point node-0)')
409
+ const focusLine = () => focusTarget({ type: 'line', id: 'line-0' }, 'focusOn(line line-0)')
410
+ const focusChannel = () => focusTarget({ type: 'channel', channelId }, 'focusOn(channel channel-a)')
411
+ const focusGroup = () => {
412
+ if (!groupId) {
413
+ groupId = view.value?.getChannelGroups(channelId)?.[0]?.id ?? ''
414
+ }
415
+ if (groupId !== '') {
416
+ focusTarget({ type: 'group', channelId, groupId }, 'focusOn(group channel-a 第一组)')
417
+ }
418
+ }
419
+
420
+ const readCoordinateRoundTrip = () => {
421
+ const current = view.value
422
+ const point = current?.getData().points.find(item => String(item.id) === 'node-0')
423
+ if (!current || !point) {
424
+ return
425
+ }
426
+ const screen = current.worldToClient(point.x, point.y)
427
+ const world = current.clientToWorld(screen[0], screen[1])
428
+ const lngLat = current.worldToLngLat(point.x, point.y)
429
+ const back = current.lngLatToWorld(lngLat[0], lngLat[1])
430
+ apiReadout.value = `worldToClient → [${screen.map(value => value.toFixed(2)).join(', ')}] · clientToWorld → [${world.map(value => value.toFixed(4)).join(', ')}] · lngLat → [${lngLat.map(value => value.toFixed(5)).join(', ')}] · lngLatToWorld → [${back.map(value => value.toFixed(4)).join(', ')}]`
431
+ }
432
+
433
+ const readViewportAndRenderer = () => {
434
+ const current = view.value
435
+ if (!current) {
436
+ return
437
+ }
438
+ const viewport = current.getViewport()
439
+ const renderer = current.getRendererInfo()
440
+ apiReadout.value = `getViewport ${viewport.cssWidth}×${viewport.cssHeight} CSS / ${viewport.width}×${viewport.height} 像素 · getRendererInfo keys=[${Object.keys(renderer).slice(0, 8).join(', ')}]`
441
+ }
442
+
443
+ const readSceneSnapshot = () => {
444
+ const snapshot = view.value?.snapshotScene({ background: false })
445
+ apiReadout.value = snapshot
446
+ ? `snapshotScene:${snapshot.points?.length ?? 0} 个节点 / ${snapshot.lines?.length ?? 0} 条线路 / ${snapshot.labels?.length ?? 0} 个标签 / ${snapshot.decorations?.length ?? 0} 个装饰`
447
+ : 'snapshotScene 没有返回'
448
+ }
449
+
450
+ const readMiscellaneous = () => {
451
+ const current = view.value
452
+ if (!current) {
453
+ return
454
+ }
455
+ const filter = current.getFilter()
456
+ const parallelGroups = current.getParallelGroups()
457
+ const hover = current.getHover()
458
+ apiReadout.value = `getFilter=${filter ? '有' : 'null'} · getParallelGroups=${parallelGroups?.length ?? 0} 组 · getHover=${hover?.target ?? 'null'}`
459
+ }
460
+
461
+ const waitForRendered = async () => {
462
+ const current = view.value
463
+ if (!current) {
464
+ return
465
+ }
466
+ const started = performance.now()
467
+ await current.whenDataRendered()
468
+ apiReadout.value = `whenDataRendered 已兑现 · ${(performance.now() - started).toFixed(1)} ms`
469
+ }
470
+
471
+ const countPointsInView = () => {
472
+ let count = 0
473
+ view.value?.forEachPointInView(() => { count += 1 })
474
+ apiReadout.value = `forEachPointInView:${count} 个节点落在当前视口`
475
+ }
476
+
477
+ onMounted(async () => {
478
+ view.value = createGisView(container.value, {
479
+ points: dataset.points,
480
+ lines: dataset.lines,
481
+ channels: dataset.channels,
482
+ features: {
483
+ pan: true,
484
+ zoom: true,
485
+ hover: true,
486
+ select: true,
487
+ boxSelect: true,
488
+ drag: true,
489
+ waypoint: true,
490
+ contextMenu: true,
491
+ keyboard: true
492
+ },
493
+ boxSelect: true,
494
+ multiSelect: true,
495
+ copy: { enabled: true, autoCommit: false },
496
+ history: { limit: 30 },
497
+ labelRender: 'dom',
498
+ gridSize: 96,
499
+ fitOnInit: true,
500
+ progressiveInit: true,
501
+ pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
502
+ maxCanvasPixels: 4_000_000,
503
+ ariaLabel: '接口能力演示画布:支持选中、框选、聚焦与只读接口读数',
504
+ waypointStyle: { size: 8, color: '#fff', strokeColor: '#38bdf8', strokeWidth: 1.5 },
505
+ zoomBaseScale: 1,
506
+ webglLayers: [],
507
+ clickable: true,
508
+ draggable: true,
509
+ waypointEditable: true,
510
+ waypointVisibility: 'always',
511
+ lineFollowsNode: true,
512
+ channelWidth: 2.6,
513
+ pointStyle: { size: 6, color: '#fff', strokeColor: '#4c8dff', strokeWidth: 1.4 },
514
+ lineStyle: { width: 1.5, color: 'rgba(100, 150, 220, 0.55)' },
515
+ fitPadding: 48,
516
+ cascadeHighlight: cascadeOptions(),
517
+ onReady: () => emit('notice', '接口能力演示已就绪'),
518
+ onSelect: (payload) => {
519
+ refreshSelection()
520
+ if (payload) {
521
+ selectionReadout.value += ` · cause=${payload.cause ?? '—'} / mode=${payload.mode ?? '—'}`
522
+ }
523
+ },
524
+ onCascadeHighlight: (payload) => {
525
+ cascadeReadout.value = describeCascade(payload)
526
+ }
527
+ })
528
+ groupId = String(view.value.getChannelGroups(channelId)?.[0]?.id ?? '')
529
+ await nextTick()
530
+ view.value.fitView(48)
531
+ refreshVisibility()
532
+ })
533
+
534
+ onBeforeUnmount(() => {
535
+ window.clearTimeout(timer)
536
+ view.value?.destroy()
537
+ view.value = null
538
+ })
539
+ </script>
540
+
541
+ <template>
542
+ <section class="dv-demo">
543
+ <div class="dv-stage">
544
+ <div ref="container" class="dv-canvas"></div>
545
+ <div class="dv-toolbar">
546
+ <button type="button" class="dv-button" @click="view?.fitView(48)">适应全部</button>
547
+ <button type="button" class="dv-button" @click="clearSelection">清空选中</button>
548
+ </div>
549
+ </div>
550
+ <aside class="dv-panel">
551
+ <div class="dv-group">
552
+ <h3>级联高亮三种触发</h3>
553
+ <div class="dv-row">
554
+ <label><input v-model="triggerOnClick" data-demo="cascade-trigger-click" type="checkbox" @change="applyCascadeTriggers" /> triggerOnClick</label>
555
+ <label><input v-model="triggerOnMultiSelect" data-demo="cascade-trigger-multi" type="checkbox" @change="applyCascadeTriggers" /> triggerOnMultiSelect</label>
556
+ <label><input v-model="triggerOnBoxSelect" data-demo="cascade-trigger-box" type="checkbox" @change="applyCascadeTriggers" /> triggerOnBoxSelect</label>
557
+ </div>
558
+ <p class="dv-readout">{{ cascadeReadout }}</p>
559
+ <p class="dv-hint">单击、多选点击和框选各走各的开关。开着框选触发后,画布上 Shift + 拖一个框,级联会把框内邻接节点与线路一起亮起;关掉时只改选中集。</p>
560
+ </div>
561
+
562
+ <div class="dv-group">
563
+ <h3>程序化选中</h3>
564
+ <div class="dv-row">
565
+ <button type="button" class="dv-button" @click="selectNode">选中节点</button>
566
+ <button type="button" class="dv-button" @click="selectLine">选中线路</button>
567
+ <button type="button" class="dv-button" data-demo="select-channel" @click="selectChannel">选中通道</button>
568
+ <button type="button" class="dv-button" data-demo="select-group" @click="selectGroup">选中分组</button>
569
+ <button type="button" class="dv-button" data-demo="select-waypoint" @click="selectWaypoint">选中拐点</button>
570
+ </div>
571
+ <p class="dv-readout">{{ selectionReadout }}</p>
572
+ <p class="dv-hint">选中拐点会补一个中间点,并把所在线路一起选上;选中通道 / 分组的读数来自 getSelectionData()。</p>
573
+ </div>
574
+
575
+ <div class="dv-group">
576
+ <h3>focusOn 显式类型</h3>
577
+ <div class="dv-row">
578
+ <button type="button" class="dv-button" @click="focusNode">point</button>
579
+ <button type="button" class="dv-button" @click="focusLine">line</button>
580
+ <button type="button" class="dv-button" data-demo="focus-channel" @click="focusChannel">channel</button>
581
+ <button type="button" class="dv-button" @click="focusGroup">group</button>
582
+ </div>
583
+ <p class="dv-readout">{{ focusReadout }}</p>
584
+ <p class="dv-hint">四种类型都走同一套包围盒适配;这里的 maxZoom 只是示例参数,组件不会额外更改放大倍率。</p>
585
+ </div>
586
+
587
+ <div class="dv-group">
588
+ <h3>线路是否跟随节点</h3>
589
+ <div class="dv-row">
590
+ <label><input v-model="lineFollowsNode" data-demo="line-follows-node" type="checkbox" @change="applyLineFollowsNode" /> lineFollowsNode</label>
591
+ <button type="button" class="dv-button" data-demo="toggle-endpoint" @click="toggleEndpoint">{{ endpointHidden ? '显示端点' : '隐藏端点' }}</button>
592
+ </div>
593
+ <p class="dv-readout" data-demo="visibility-readout">{{ visibilityReadout }}</p>
594
+ <p class="dv-hint">先隐藏 node-0 看对比,再勾掉 lineFollowsNode:开启时绑定线路一起退场;关闭后只收起节点,线路和通道停在最后坐标。</p>
595
+ </div>
596
+
597
+ <div class="dv-group">
598
+ <h3>只读接口</h3>
599
+ <div class="dv-row">
600
+ <button type="button" class="dv-button" @click="readCoordinateRoundTrip">坐标互转</button>
601
+ <button type="button" class="dv-button" @click="readViewportAndRenderer">视口 / 渲染器</button>
602
+ <button type="button" class="dv-button" @click="readSceneSnapshot">场景快照</button>
603
+ <button type="button" class="dv-button" @click="readMiscellaneous">筛选 / 分组 / 悬停</button>
604
+ <button type="button" class="dv-button" @click="waitForRendered">下一帧 Promise</button>
605
+ <button type="button" class="dv-button" @click="countPointsInView">视口内节点</button>
606
+ </div>
607
+ <p class="dv-readout" data-demo="api-readout">{{ apiReadout }}</p>
608
+ </div>
609
+
610
+ <div class="dv-group">
611
+ <h3>样式、装饰与通道写入</h3>
612
+ <div class="dv-row">
613
+ <button type="button" class="dv-button" data-demo="style-apis" @click="toggleStyleApis">{{ styleApplied ? '恢复字段级样式' : 'updatePointStyle / updateLineStyle' }}</button>
614
+ <button type="button" class="dv-button" data-demo="point-decoration-apis" @click="exercisePointDecorationApis">节点装饰 CRUD</button>
615
+ <button type="button" class="dv-button" data-demo="line-decoration-apis" @click="exerciseLineDecorationApis">线路装饰 CRUD</button>
616
+ <button type="button" class="dv-button" data-demo="channel-line-apis" @click="exerciseChannelLineApis">通道线宽 / 线路对账</button>
617
+ <button type="button" class="dv-button" data-demo="visibility-apis" @click="exerciseVisibilityApis">setFeatureVisible / isPointVisible</button>
618
+ </div>
619
+ <p class="dv-readout" data-demo="advanced-readout">{{ advancedReadout }}</p>
620
+ </div>
621
+
622
+ <div class="dv-group">
623
+ <h3>背景、投影与初始化 options</h3>
624
+ <div class="dv-row">
625
+ <button type="button" class="dv-button" data-demo="geo-background-apis" @click="toggleGeoBackgroundApis">{{ geoBackgroundOn ? '移除 GeoJSON 背景' : 'updateBackground / GeoJSON 投影' }}</button>
626
+ <button type="button" class="dv-button" data-demo="initial-options" @click="readInitialOptions">读取初始化 options</button>
627
+ </div>
628
+ <p class="dv-hint">GeoJSON 按钮会临时铺一块包住数据的区域:可读 getBackgroundLabels / getGeoProjection / mapDataToGeoJson,再点一次恢复透明底。初始化 options 在创建时生效,运行时按键读取当前值。</p>
629
+ </div>
630
+
631
+ <div class="dv-group">
632
+ <h3>历史、复制与运行时清理</h3>
633
+ <div class="dv-row">
634
+ <button type="button" class="dv-button" data-demo="history-apis" @click="exerciseHistoryApis">历史 / 收尾 / 落点预览</button>
635
+ <button type="button" class="dv-button" data-demo="once-api" @click="exerciseOnceApi">once 事件</button>
636
+ <button type="button" class="dv-button" data-demo="copy-apis" @click="exerciseCopyApis">复制预览收尾</button>
637
+ <button type="button" class="dv-button" data-demo="remove-apis" @click="exerciseRemoveApis">删除请求 / removeSelected</button>
638
+ <button type="button" class="dv-button" data-demo="webgl-apis" @click="exerciseWebGLApis">WebGL 层清理</button>
639
+ <button type="button" class="dv-button" data-demo="registry-apis" @click="exerciseRegistryApis">图元 / 运算符注销</button>
640
+ </div>
641
+ </div>
642
+ </aside>
643
+ </section>
644
+ </template>