gis-view-webgl2 0.1.0 → 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.
Files changed (87) hide show
  1. package/README.md +40 -8
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1036 -249
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
  7. package/docs/GisView.md +9 -8
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
  9. 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 +172 -0
  10. package/docs/api/index.html +545 -59
  11. package/docs/demos/BackendSyncDemo.vue +10 -8
  12. package/docs/demos/BasicDemo.vue +101 -101
  13. package/docs/demos/BatchAddDemo.vue +5 -5
  14. package/docs/demos/ChannelDemo.vue +617 -10
  15. package/docs/demos/ConnectDemo.vue +392 -0
  16. package/docs/demos/CopyMergeDemo.vue +980 -929
  17. package/docs/demos/DecorationDraftDemo.vue +1300 -0
  18. package/docs/demos/DemoShell.vue +569 -240
  19. package/docs/demos/DragDropDemo.vue +11 -11
  20. package/docs/demos/EffectsDemo.vue +6 -6
  21. package/docs/demos/EventDemo.vue +327 -327
  22. package/docs/demos/ExtensionDemo.vue +458 -0
  23. package/docs/demos/GisViewDemo.vue +61 -48
  24. package/docs/demos/GraphicDemo.vue +338 -0
  25. package/docs/demos/LabelDemo.vue +553 -497
  26. package/docs/demos/MinimapDemo.vue +358 -0
  27. package/docs/demos/PerfDemo.vue +29 -0
  28. package/docs/demos/SearchFilterDemo.vue +412 -0
  29. package/docs/demos/StyleDemo.vue +121 -121
  30. package/docs/demos/api-index.js +98 -77
  31. package/docs/demos/demos.js +159 -135
  32. package/docs/demos/shared/demoData.js +107 -107
  33. package/docs/demos/shared/demoStyle.css +249 -249
  34. package/docs/demos/shared/fakeBackend.js +93 -6
  35. 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 +28 -8
  36. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
  37. package/docs/format/06-/345/257/274/345/205/245/345/257/274/345/207/272/344/270/216/346/240/207/345/207/206/346/226/207/346/241/243.md +8 -6
  38. 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 +6 -6
  39. package/docs/guide/sample-document.html +4 -4
  40. package/docs/index.html +180 -9
  41. package/docs/internals/11-/350/246/206/347/233/226/345/261/202/346/240/207/347/255/276/344/270/216/346/217/220/347/244/272.md +1 -1
  42. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
  43. package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +16 -0
  44. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -0
  45. package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
  46. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
  47. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
  48. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
  49. 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 +111 -10
  50. 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 +47 -9
  51. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
  52. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +61 -11
  53. 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 +74 -7
  54. package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
  55. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
  56. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
  57. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
  58. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
  59. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +31 -29
  60. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
  61. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
  62. 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 +4 -4
  63. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +42 -2
  64. 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 +22 -6
  65. package/docs/playground/index.html +5 -5
  66. package/docs/steps/README.md +14 -10
  67. package/docs/steps/check-doc-truth.mjs +1 -1
  68. package/docs/steps/selftest-actions.html +10 -10
  69. package/docs/steps/selftest-data.html +96 -14
  70. package/docs/steps/selftest-decoration.html +577 -0
  71. package/docs/steps/selftest-demo.html +6 -5
  72. package/docs/steps/selftest-demos.html +2201 -1612
  73. package/docs/steps/selftest-docs.html +89 -16
  74. package/docs/steps/selftest-effects.html +5 -5
  75. package/docs/steps/selftest-features.html +757 -86
  76. package/docs/steps/selftest-full.html +583 -583
  77. package/docs/steps/selftest-labels.html +1034 -652
  78. package/docs/steps/selftest-perf.html +1899 -1844
  79. package/docs/steps/selftest-scale.html +117 -7
  80. package/docs/steps/selftest-select.html +24 -17
  81. package/docs/steps/step-10-full.js +1 -1
  82. package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
  83. package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
  84. package/package.json +43 -43
  85. package/docs/demos/HistoryDemo.vue +0 -230
  86. package/docs/demos/InterfaceDemo.vue +0 -309
  87. package/docs/demos/SampleDemo.vue +0 -283
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "gis-view-webgl2",
3
- "version": "0.1.0",
4
- "description": "WebGL2 矢量视图:点、线与 GeoJSON / 图片背景层,无内置界面,点击 / 双击 / 拖拽可配置",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc && vite build",
9
- "build:lib": "tsc && node scripts/clean-dist.mjs && vite build --config vite.config.lib.js && node scripts/obfuscate-dist.mjs && node scripts/copy-types.mjs",
10
- "prepack": "npm run build:lib",
11
- "pack:lib": "npm pack --dry-run",
12
- "preview": "vite preview",
13
- "check:docs": "node docs/steps/check-doc-coverage.mjs && node docs/steps/check-doc-truth.mjs && node docs/steps/check-sample-fields.mjs && node docs/steps/check-doc-center.mjs"
14
- },
15
- "main": "./dist/index.cjs",
16
- "module": "./dist/index.js",
17
- "types": "./dist/index.d.ts",
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.js",
22
- "require": "./dist/index.cjs",
23
- "default": "./dist/index.js"
24
- },
25
- "./style.css": "./dist/gis-view.css"
26
- },
27
- "files": [
28
- "dist",
29
- "README.md",
30
- "docs"
31
- ],
32
- "sideEffects": [
33
- "**/*.css"
34
- ],
35
- "devDependencies": {
36
- "@vitejs/plugin-vue": "^6.0.5",
37
- "javascript-obfuscator": "^5.6.0",
38
- "typescript": "~6.0.2",
39
- "vite": "^8.2.2",
40
- "vite-plugin-lib-inject-css": "^2.2.2",
41
- "vue": "^3.5.32"
42
- }
43
- }
1
+ {
2
+ "name": "gis-view-webgl2",
3
+ "version": "0.1.2",
4
+ "description": "WebGL2 矢量视图:点、线与 GeoJSON / 图片背景层,无内置界面,点击 / 双击 / 拖拽可配置",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "build:lib": "tsc && node scripts/clean-dist.mjs && vite build --config vite.config.lib.js && node scripts/obfuscate-dist.mjs && node scripts/copy-types.mjs",
10
+ "prepack": "npm run build:lib",
11
+ "pack:lib": "npm pack --dry-run",
12
+ "preview": "vite preview",
13
+ "check:docs": "node docs/steps/check-doc-coverage.mjs && node docs/steps/check-doc-truth.mjs && node docs/steps/check-sample-fields.mjs && node docs/steps/check-doc-center.mjs"
14
+ },
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./style.css": "./dist/gis-view.css"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "docs"
31
+ ],
32
+ "sideEffects": [
33
+ "**/*.css"
34
+ ],
35
+ "devDependencies": {
36
+ "@vitejs/plugin-vue": "^6.0.5",
37
+ "javascript-obfuscator": "^5.6.0",
38
+ "typescript": "~6.0.2",
39
+ "vite": "^8.2.2",
40
+ "vite-plugin-lib-inject-css": "^2.2.2",
41
+ "vue": "^3.5.32"
42
+ }
43
+ }
@@ -1,230 +0,0 @@
1
- <script setup>
2
- /**
3
- * 历史与批量:把「改数据」这件事的账本摊开 —— 一批改动作一条记录,history 给出撤销 / 重做与两份栈。
4
- *
5
- * 面板上四件事:
6
- * 1. 「批量移动 / 批量换色」:view.batch(fn) 里逐个 updatePoint / setPointColor —— 回调里立刻生效,
7
- * 收尾只发一条 onDataChange(type: 'batch',changes 里是每一笔)、历史里也只多一步;
8
- * 2. 「撤销 / 重做」:history 缺省是关的(记录要占内存),这一页显式打开(history: { limit: 100 })。
9
- * 读数来自 onHistory;快捷键 Ctrl+Z / Ctrl+Shift+Z 与按钮走同一条路,没得撤销 / 重做时按钮灰掉;
10
- * 3. 「画质」:四档只影响绘制 —— 换档不重排场景、不动数据,也不改选中与筛选;
11
- * 4. 「功能预设」:features 的 edit(全开)/ view(只读浏览)/ static(静态图)一次切好交互口径。
12
- *
13
- * 注意:把改回原样的操作(拖过去又拖回来)不会进历史 —— 记的是「改动本身」的净效果,不是操作流水。
14
- */
15
- import { onBeforeUnmount, onMounted, ref } from 'vue'
16
- import { createGisView } from '../../src/index.js'
17
- import { buildDemoData } from './shared/demoData.js'
18
- import './shared/demoStyle.css'
19
-
20
- const emit = defineEmits(['notice'])
21
- const container = ref(null)
22
- const batchCount = ref(8)
23
- const quality = ref('high')
24
- const preset = ref('edit')
25
- const historyState = ref({ enabled: true, undoCount: 0, redoCount: 0, canUndo: false, canRedo: false })
26
- const readouts = ref({ change: '—', quality: '—', features: '—', last: '—', stats: '—' })
27
- const dataset = buildDemoData({ count: 28 })
28
- let view = null
29
- let round = 0
30
- /** 面板下一次刷新读数的时刻(fps 每帧都变,节流之后再写响应式数据) */
31
- let statsDrawnAt = Number.NEGATIVE_INFINITY
32
-
33
- const PALETTE = ['#ff4fa3', '#a06bff', '#3fa7ff', '#4fd1a5', '#ffb454', '#ff6b6b']
34
- const QUALITY_CYCLE = [
35
- { id: 'high', label: 'high 全开' },
36
- { id: 'balanced', label: 'balanced 关光晕' },
37
- { id: 'lean', label: 'lean 关装饰' },
38
- { id: 'fast', label: 'fast 降分辨率' }
39
- ]
40
- const FEATURE_PRESETS = [
41
- { id: 'edit', label: '全功能编辑' },
42
- { id: 'view', label: '只读浏览' },
43
- { id: 'static', label: '静态图' }
44
- ]
45
-
46
- /** 开关表的读数:挑当前最影响手感的那几项摆在面板上 */
47
- const describeFeatures = (state) => {
48
- const on = (name) => (state[name] ? '开' : '关')
49
- return `平移 ${on('pan')} · 缩放 ${on('zoom')} · 悬停 ${on('hover')} · 选中 ${on('select')} · 拖动 ${on('drag')} · 拐点 ${on('waypoint')} · 快捷键 ${on('keyboard')}`
50
- }
51
- const describeQuality = (state) => `${state.name}(第 ${state.level} 档 · ${state.scale === 1 ? '原生分辨率' : `${Math.round(state.scale * 100)}% 分辨率`})`
52
-
53
- const buildOptions = () => ({
54
- points: dataset.points,
55
- lines: dataset.lines,
56
- channels: dataset.channels,
57
- channelWidth: 2.4,
58
- channelInset: 1.8,
59
- pointStyle: { size: 6, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1.5 },
60
- lineStyle: { width: 1.4, color: 'rgba(120, 168, 226, 0.55)' },
61
- showGrid: true,
62
- gridColor: 'rgba(126, 168, 214, 0.22)',
63
- fitPadding: 40,
64
- // 这一页的主角:历史显式打开、画质从 high 起、功能开关先全开
65
- history: { limit: 100 },
66
- quality: 'high',
67
- features: 'edit',
68
- onReady: () => emit('notice', `已加载 ${dataset.points.length} 个节点 / ${dataset.lines.length} 条线路,历史已打开`),
69
- onHistory: (payload) => {
70
- historyState.value = { enabled: true, canUndo: payload.canUndo, canRedo: payload.canRedo, undoCount: payload.undoCount, redoCount: payload.redoCount }
71
- const action = payload.action === 'push' ? '记下' : payload.action === 'undo' ? '撤销' : payload.action === 'redo' ? '重做' : '清空'
72
- readouts.value = { ...readouts.value, last: payload.action === 'clear' ? `清空了两份栈` : `${action}「${payload.entry?.label ?? '—'}」` }
73
- },
74
- onQualityChange: (state) => {
75
- quality.value = state.name
76
- readouts.value = { ...readouts.value, quality: describeQuality(state) }
77
- },
78
- onFeatureChange: (state) => {
79
- readouts.value = { ...readouts.value, features: describeFeatures(state) }
80
- },
81
- onDataChange: (payload) => {
82
- const changes = payload?.changes?.length ?? 0
83
- const counts = payload?.counts ?? {}
84
- readouts.value = {
85
- ...readouts.value,
86
- change: payload?.type === 'batch'
87
- ? `batch · ${changes} 笔合并成一条(${counts.points ?? 0} 点 / ${counts.lines ?? 0} 线 / ${counts.channels ?? 0} 通道)`
88
- : `${payload?.type ?? '—'} · ${counts.points ?? 0} 点 / ${counts.lines ?? 0} 线 / ${counts.channels ?? 0} 通道`
89
- }
90
- },
91
- onStats: (next) => {
92
- const now = performance.now()
93
- if (now - statsDrawnAt < 300) {
94
- return
95
- }
96
- statsDrawnAt = now
97
- readouts.value = { ...readouts.value, stats: `${next.idle ? '空闲' : `${Math.round(next.fps)} fps`} · 最近一帧 ${next.frameMs.toFixed(2)} ms · 绘制 ${next.drawCalls} 次` }
98
- }
99
- })
100
-
101
- /** 一批改动:8 / 20 / 28 个节点一起动,收尾只留一条记录、一步历史 */
102
- const batchMove = () => {
103
- if (!view) {
104
- return
105
- }
106
- round += 1
107
- const step = round % 2 === 1 ? 12 : -12
108
- const picked = view.getData().points.slice(0, batchCount.value)
109
- view.batch(() => {
110
- for (const point of picked) {
111
- view.updatePoint(point.id, { x: point.x + step, y: point.y })
112
- }
113
- }, { label: `批量移动 ${picked.length} 个节点` })
114
- emit('notice', `batch:${picked.length} 次 updatePoint 收成一条记录(${step > 0 ? '+' : ''}${step})`)
115
- }
116
- const batchColor = () => {
117
- if (!view) {
118
- return
119
- }
120
- round += 1
121
- const picked = view.getData().points.slice(0, batchCount.value)
122
- view.batch(() => {
123
- picked.forEach((point, index) => view.setPointColor(point.id, PALETTE[(index + round) % PALETTE.length]))
124
- }, { label: `批量换色 ${picked.length} 个节点` })
125
- emit('notice', `batch:${picked.length} 次 setPointColor 收成一条记录`)
126
- }
127
- const undo = () => {
128
- const entry = view?.undo()
129
- emit('notice', entry ? `撤销「${entry.label}」` : '没得撤销')
130
- }
131
- const redo = () => {
132
- const entry = view?.redo()
133
- emit('notice', entry ? `重做「${entry.label}」` : '没得重做')
134
- }
135
- const clearHistory = () => {
136
- const removed = view?.clearHistory() ?? 0
137
- emit('notice', `clearHistory:清掉 ${removed} 条记录(数据保持现状)`)
138
- }
139
- /** 换档:读数由 onQualityChange 写,这里只把选中的 chip 点亮 */
140
- const setQuality = (id) => {
141
- view?.setQuality(id)
142
- quality.value = id
143
- }
144
- const setPreset = (id) => {
145
- view?.setFeatures(id)
146
- preset.value = id
147
- }
148
- const fitView = () => {
149
- view?.fitView(40)
150
- }
151
- const zoomBy = (factor) => {
152
- view?.zoomBy(factor)
153
- }
154
-
155
- onMounted(() => {
156
- view = createGisView(container.value, buildOptions())
157
- readouts.value = {
158
- ...readouts.value,
159
- quality: describeQuality(view.getQuality()),
160
- features: describeFeatures(view.getFeatures())
161
- }
162
- historyState.value = { enabled: true, ...view.getHistoryState() }
163
- })
164
-
165
- onBeforeUnmount(() => {
166
- view?.destroy()
167
- view = null
168
- })
169
- </script>
170
- <template>
171
- <section class="dv-demo">
172
- <div class="dv-stage">
173
- <div ref="container" class="dv-canvas"></div>
174
- <div class="dv-toolbar">
175
- <button type="button" @click="fitView">适应全部</button>
176
- <button type="button" @click="zoomBy(1.4)">放大</button>
177
- <button type="button" @click="zoomBy(1 / 1.4)">缩小</button>
178
- </div>
179
- </div>
180
- <aside class="dv-panel">
181
- <div class="dv-group">
182
- <h3>批量改动(batch)</h3>
183
- <div class="dv-row">
184
- <label>一批改
185
- <select v-model.number="batchCount">
186
- <option :value="8">8 个节点</option>
187
- <option :value="20">20 个节点</option>
188
- <option :value="28">全部 28 个</option>
189
- </select>
190
- </label>
191
- <button type="button" class="dv-button dv-button--primary" @click="batchMove">批量移动</button>
192
- <button type="button" class="dv-button" @click="batchColor">批量换色</button>
193
- </div>
194
- <p class="dv-hint">回调里的每一次 updatePoint / setPointColor 都立刻生效,但收尾只发一条 onDataChange(type: batch)、历史里只多一步。</p>
195
- </div>
196
- <div class="dv-group">
197
- <h3>历史(history)</h3>
198
- <div class="dv-row">
199
- <button type="button" class="dv-button" :disabled="!historyState.canUndo" @click="undo">撤销</button>
200
- <button type="button" class="dv-button" :disabled="!historyState.canRedo" @click="redo">重做</button>
201
- <button type="button" class="dv-button dv-button--danger" @click="clearHistory">清空历史</button>
202
- </div>
203
- <p class="dv-readout">栈:可撤销 {{ historyState.undoCount }} 步 / 可重做 {{ historyState.redoCount }} 步</p>
204
- <p class="dv-readout">最近一条:{{ readouts.last }}</p>
205
- <p class="dv-hint">缺省是关的,这一页写的是 history: { limit: 100 };快捷键 Ctrl+Z / Ctrl+Shift+Z 走同一条路,把节点移回去的那次改动不进历史(记的是净改动)。</p>
206
- </div>
207
- <div class="dv-group">
208
- <h3>画质(quality)</h3>
209
- <div class="dv-chips">
210
- <button v-for="item in QUALITY_CYCLE" :key="item.id" type="button" class="dv-chip" :class="{ 'dv-chip--active': quality === item.id }" @click="setQuality(item.id)">{{ item.label }}</button>
211
- </div>
212
- <p class="dv-readout">当前:{{ readouts.quality }}</p>
213
- <p class="dv-hint">换档只影响绘制 —— 不重排场景、不动数据、也不改选中与筛选。</p>
214
- </div>
215
- <div class="dv-group">
216
- <h3>功能预设(features)</h3>
217
- <div class="dv-chips">
218
- <button v-for="item in FEATURE_PRESETS" :key="item.id" type="button" class="dv-chip" :class="{ 'dv-chip--active': preset === item.id }" @click="setPreset(item.id)">{{ item.label }}</button>
219
- </div>
220
- <p class="dv-readout">{{ readouts.features }}</p>
221
- <p class="dv-hint">缺省全关:'edit' 全开、'view' 只读(能看能选、改不动数据)、'static' 只剩画布与数据。</p>
222
- </div>
223
- <div class="dv-group">
224
- <h3>读数</h3>
225
- <p class="dv-readout">最近变更:{{ readouts.change }}</p>
226
- <p class="dv-readout">性能:{{ readouts.stats }}</p>
227
- </div>
228
- </aside>
229
- </section>
230
- </template>
@@ -1,309 +0,0 @@
1
- <script setup>
2
- /**
3
- * 接口数据:后端返回的 station_data / channel_data 口径怎么接进组件。
4
- *
5
- * 组件本身只认 points / lines / channels 三列(见 docs/GisView.md 的「动态换数据」),
6
- * 接口的列名不一样就在这里映射一次 —— 这一份映射逻辑属于业务侧,放在 demo 里,
7
- * 组件一个字节都不用改。页面上演示三件事:
8
- *
9
- * 1. 整体换:把接口载荷映射成三列之后走 updateData(points, lines, channels),一次换完只发一条数据变更;
10
- * 2. 分批补:通道一条条到货时走 addChannels(逐条落地,只重排一次场景);
11
- * 3. 收 loading:onDataRendered / whenDataRendered 在「新数据真的画出来」之后兑现,见读数栏。
12
- * 4. 先建实例后拉数据:createGisView 在 onMounted 就位(空场景照样先铺底图 / 网格),
13
- * 「模拟接口(延迟 900ms)」演示「清空 + loading → 接口返回 → updateData → whenDataRendered」这一段。
14
- *
15
- * real.json 是仓库里那份真实接口样本(1 个场站 + 1 条两回通道),
16
- * 「生成场景」另造一份结构相同的多场站载荷,用来演示切场景。
17
- */
18
- import { onBeforeUnmount, onMounted, ref } from 'vue'
19
- import { createGisView } from '../../src/index.js'
20
- import realPayload from '../../real.json'
21
- import './shared/demoStyle.css'
22
-
23
- const emit = defineEmits(['notice'])
24
- const container = ref(null)
25
- const readouts = ref({ scene: '—', counts: '—', frame: '—', warnings: '0 条' })
26
- const loading = ref(false)
27
- const activeScene = ref('real')
28
- let view = null
29
- let sceneA = null
30
-
31
- /** 单条对象与数组都收:接口里这两种写法都出现过 */
32
- const toList = value => (Array.isArray(value) ? value : value && typeof value === 'object' ? [value] : [])
33
- const pick = (...values) => values.find(value => value !== undefined && value !== null)
34
- const number = (...values) => {
35
- for (const value of values) {
36
- const parsed = Number(value)
37
- if (Number.isFinite(parsed)) {
38
- return parsed
39
- }
40
- }
41
- return null
42
- }
43
-
44
- /**
45
- * 接口载荷 → 组件三列。映射规则(列名不一样时改这一个函数就行):
46
- * - 场站:id 取 station_id / id / station_name;坐标取 lon / lng / longitude / x 与 lat / latitude / y;
47
- * - 通道:两端取 from_station / to_station,回线在 line_data,整条路由在 coords(首尾就是两端节点,去掉当拐点);
48
- * - 两端有一个不在 station_data 里时,按 coords 画成一条独立线路并记一条提醒(不丢数据、也不抛错)。
49
- */
50
- const mapInterfacePayload = (payload) => {
51
- const stations = toList(payload?.station_data)
52
- const channelList = toList(payload?.channel_data)
53
- const points = []
54
- const lines = []
55
- const channels = []
56
- const warnings = []
57
- const usedStationIds = new Set()
58
- const usedLineIds = new Set()
59
- stations.forEach((station, index) => {
60
- const raw = pick(station.station_id, station.id, station.station_name, `station-${index}`)
61
- const x = number(station.lon, station.lng, station.longitude, station.x)
62
- const y = number(station.lat, station.latitude, station.y)
63
- if (x === null || y === null) {
64
- warnings.push(`场站 ${String(raw)} 读不出坐标,已跳过`)
65
- return
66
- }
67
- let id = String(raw)
68
- // id 撞车往后排:label 与 from / to 都按它对齐,不能重复
69
- for (let suffix = 2; usedStationIds.has(id); suffix += 1) {
70
- id = `${String(raw)}-${suffix}`
71
- }
72
- usedStationIds.add(id)
73
- points.push({ ...station, id, x, y, name: pick(station.station_name, station.name, id) })
74
- })
75
- channelList.forEach((channel, channelIndex) => {
76
- const fromId = String(pick(channel.from_station, channel.from, ''))
77
- const toId = String(pick(channel.to_station, channel.to, ''))
78
- const fromOk = usedStationIds.has(fromId)
79
- const toOk = usedStationIds.has(toId)
80
- const channelId = pick(channel.channel_id, channel.id, `channel-${channelIndex}`)
81
- const channelName = pick(channel.channel_name, channel.name, channelId)
82
- // 路由:coords / route 是整条(首尾就是两端节点,去掉当拐点);path / waypoints 本来就是拐点
83
- const route = toList(pick(channel.coords, channel.coords_list, channel.route))
84
- const path = (toList(pick(channel.path, channel.waypoints)).length
85
- ? toList(pick(channel.path, channel.waypoints))
86
- : route.slice(1, -1)
87
- ).map(([x, y]) => [Number(x), Number(y)]).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y))
88
- const circuits = toList(channel.line_data).length || toList(channel.lines).length || 1
89
- if (!fromOk || !toOk) {
90
- // 两端缺一个:按路由坐标画一条独立线路(还是走组件自己的 lines),提醒留给人看
91
- warnings.push(`通道 ${String(channelName)} 的两端不在 station_data 里,已按 coords 画成独立线路`)
92
- const start = route[0] ?? path[0]
93
- const end = route[route.length - 1] ?? path[path.length - 1]
94
- const extra = route.length > 2 ? route.slice(1, -1).map(([x, y]) => [Number(x), Number(y)]) : path
95
- const startOk = Array.isArray(start) && number(start[0]) !== null
96
- const endOk = Array.isArray(end) && number(end[0]) !== null
97
- if (!startOk || !endOk) {
98
- warnings.push(`通道 ${String(channelName)} 既没有可用端点也没有 coords,已跳过`)
99
- return
100
- }
101
- let lineId = `${String(channelId)}#1`
102
- while (usedLineIds.has(lineId)) {
103
- lineId = `${lineId}-2`
104
- }
105
- usedLineIds.add(lineId)
106
- lines.push({
107
- ...channel,
108
- id: lineId,
109
- name: String(channelName),
110
- start: [Number(start[0]), Number(start[1])],
111
- end: [Number(end[0]), Number(end[1])],
112
- path: extra,
113
- channel_id: channelId
114
- })
115
- return
116
- }
117
- const channelLines = []
118
- for (let index = 0; index < circuits; index += 1) {
119
- const member = toList(channel.line_data)[index] ?? toList(channel.lines)[index] ?? {}
120
- let lineId = circuits > 1 ? `${String(channelId)}#${index + 1}` : String(pick(member.line_id, member.id, channelId))
121
- while (usedLineIds.has(lineId)) {
122
- lineId = `${lineId}-2`
123
- }
124
- usedLineIds.add(lineId)
125
- const line = {
126
- ...member,
127
- id: lineId,
128
- name: member.name ?? (circuits > 1 ? `${String(channelName)} #${index + 1}` : String(channelName)),
129
- from: fromId,
130
- to: toId,
131
- channel_id: channelId
132
- }
133
- lines.push(line)
134
- channelLines.push(lineId)
135
- }
136
- channels.push({ ...channel, id: String(channelId), name: String(channelName), lineIds: channelLines })
137
- })
138
- return { points, lines, channels, warnings }
139
- }
140
-
141
- /** 另造一份结构相同的接口载荷(多场站 + 通道 + 路由拐点),用来演示切场景 */
142
- const buildScenePayload = (count, offset) => {
143
- const station_data = []
144
- const channel_data = []
145
- const at = index => {
146
- const angle = (index / count) * Math.PI * 2
147
- return { lon: 20 + Math.cos(angle) * (12 + offset), lat: 4 + Math.sin(angle) * (12 + offset) }
148
- }
149
- for (let index = 0; index < count; index += 1) {
150
- const spot = at(index)
151
- station_data.push({
152
- station_id: `S${index}`,
153
- station_name: `场站 ${index + 1}`,
154
- lon: spot.lon,
155
- lat: spot.lat,
156
- vn_kv: index % 2 ? 220 : 525,
157
- station_type: index % 3 === 0 ? 'storage' : 'substation'
158
- })
159
- }
160
- for (let index = 0; index < count; index += 1) {
161
- const next = (index + 1) % count
162
- const a = at(index)
163
- const b = at(next)
164
- channel_data.push({
165
- channel_id: `C${index}`,
166
- channel_name: `通道 ${index + 1}`,
167
- from_station: `S${index}`,
168
- to_station: `S${next}`,
169
- line_data: [{ name: `回线 ${index + 1}-1` }, { name: `回线 ${index + 1}-2` }],
170
- coords: [[a.lon, a.lat], [(a.lon + b.lon) / 2 + 1.5, (a.lat + b.lat) / 2 - 1.5], [b.lon, b.lat]]
171
- })
172
- }
173
- return { station_data, channel_data }
174
- }
175
-
176
- const showWarnings = (warnings) => {
177
- readouts.value = { ...readouts.value, warnings: warnings.length ? warnings.join(';') : '0 条' }
178
- }
179
-
180
- /** 读接口载荷 → 映射 → 整体换:切场景就走这一条 */
181
- const loadScene = async (payload, name) => {
182
- if (!view) {
183
- return
184
- }
185
- const mapped = mapInterfacePayload(payload)
186
- loading.value = true
187
- view.updateData(mapped.points, mapped.lines, mapped.channels)
188
- const rendered = await view.whenDataRendered()
189
- loading.value = false
190
- readouts.value = {
191
- ...readouts.value,
192
- scene: name,
193
- counts: `${mapped.points.length} 场站 / ${mapped.lines.length} 回线 / ${mapped.channels.length} 通道`,
194
- frame: rendered ? `第 ${rendered.frame} 帧画完(${rendered.frameMs.toFixed(1)} ms)` : '—'
195
- }
196
- showWarnings(mapped.warnings)
197
- activeScene.value = name
198
- emit('notice', `已切到「${name}」:${mapped.points.length} 场站 / ${mapped.lines.length} 回线`)
199
- }
200
-
201
- /**
202
- * 「先建实例、后拉数据」:实例在 onMounted 里已经建好(空场景也照样先铺网格 / 底图),
203
- * 接口没回来之前画面停在空场景 + loading;数据到了走同一条 updateData,收尾等 whenDataRendered。
204
- * 首屏落位不用自己管:组件在「首次拿到数据」时会按 initialView 自动适应一次。
205
- */
206
- const loadFromApi = async () => {
207
- if (!view || loading.value) {
208
- return
209
- }
210
- loading.value = true
211
- view.updateData([], [], [])
212
- readouts.value = { ...readouts.value, scene: '接口请求中…(实例已建好,空场景)', counts: '0 / 0 / 0', frame: '等待接口返回' }
213
- await new Promise(resolve => setTimeout(resolve, 900))
214
- await loadScene(buildScenePayload(12, 3), '接口场景(延迟 900ms 后到货)')
215
- }
216
- const loadReal = () => loadScene(realPayload, 'real.json')
217
- const loadGenerated = (offset) => loadScene(buildScenePayload(10, offset), `生成场景(半径 ${12 + offset})`)
218
-
219
- /** 分批补:通道一条条到货时的写法(逐条落地,只重排一次场景、只记一步历史) */
220
- const appendChannels = () => {
221
- if (!view) {
222
- return
223
- }
224
- const payload = buildScenePayload(6, 9)
225
- const stations = mapInterfacePayload(payload)
226
- const extra = payload.channel_data.slice(2, 5).map((channel, index) => ({
227
- from: `S${index + 2}`,
228
- to: `S${index + 3}`,
229
- circuits: 1,
230
- name: `补进来的通道 ${index + 1}`
231
- }))
232
- view.addPoints(stations.points)
233
- const result = view.addChannels(extra)
234
- readouts.value = {
235
- ...readouts.value,
236
- scene: '依次补通道',
237
- counts: `本次补进 ${result.count} 条通道(成功 ${result.lineIds.length} 回线)`,
238
- frame: '—'
239
- }
240
- emit('notice', `addChannels:${result.count} 条通道一次落地`)
241
- }
242
-
243
- const clearData = () => {
244
- view?.updateData([], [], [])
245
- readouts.value = { ...readouts.value, scene: '已清空', counts: '0 / 0 / 0', frame: '—' }
246
- }
247
-
248
- onMounted(() => {
249
- sceneA = mapInterfacePayload(realPayload)
250
- view = createGisView(container.value, {
251
- points: sceneA.points,
252
- lines: sceneA.lines,
253
- channels: sceneA.channels,
254
- // 功能开关缺省都是关的:demo 要能看能选,就显式打开查看类那几个(见 docs/GisView.md 的「功能开关」)
255
- features: 'view',
256
- showGrid: true,
257
- fitPadding: 48,
258
- pointStyle: { size: 8, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1.5 },
259
- lineStyle: { width: 1.6, color: 'rgba(120, 168, 226, 0.7)' },
260
- channelWidth: 2.2,
261
- channelInset: 1.6,
262
- onDataRendered: (payload) => {
263
- readouts.value = { ...readouts.value, frame: `第 ${payload.frame} 帧画完(${payload.frameMs.toFixed(1)} ms,合并 ${payload.changes} 笔)` }
264
- },
265
- onWarning: (text) => showWarnings([text])
266
- })
267
- readouts.value = {
268
- ...readouts.value,
269
- scene: 'real.json',
270
- counts: `${sceneA.points.length} 场站 / ${sceneA.lines.length} 回线 / ${sceneA.channels.length} 通道`
271
- }
272
- showWarnings(sceneA.warnings)
273
- })
274
-
275
- onBeforeUnmount(() => {
276
- view?.destroy()
277
- view = null
278
- })
279
- </script>
280
- <template>
281
- <section class="dv-demo">
282
- <div class="dv-stage">
283
- <div ref="container" class="dv-canvas"></div>
284
- <div class="dv-toolbar">
285
- <button type="button" @click="loadReal">real.json</button>
286
- <button type="button" @click="loadGenerated(0)">生成场景 A</button>
287
- <button type="button" @click="loadGenerated(4)">生成场景 B</button>
288
- <button type="button" :disabled="loading" @click="loadFromApi">模拟接口(延迟 900ms)</button>
289
- <button type="button" @click="appendChannels">依次补通道</button>
290
- <button type="button" @click="clearData">清空</button>
291
- </div>
292
- <p v-if="loading" class="dv-hint">正在加载:接口 / 切场景期间停在这句提示上,等 whenDataRendered() 兑现再撤下…</p>
293
- </div>
294
- <aside class="dv-panel">
295
- <div class="dv-group">
296
- <h3>接口 → 组件</h3>
297
- <p class="dv-readout">当前:{{ readouts.scene }}</p>
298
- <p class="dv-readout">条数:{{ readouts.counts }}</p>
299
- <p class="dv-readout">渲染:{{ readouts.frame }}</p>
300
- <p class="dv-readout">提醒:{{ readouts.warnings }}</p>
301
- </div>
302
- <div class="dv-group">
303
- <h3>映射口径</h3>
304
- <p class="dv-hint">这一页的 mapInterfacePayload 就是全部转换逻辑:场站取 station_id / lon / lat,通道取 from_station / to_station,line_data 展开成回线,coords 去掉首尾当拐点;两端不在 station_data 里的通道按 coords 画成独立线路并记一条提醒。</p>
305
- <p class="dv-hint">组件只认 points / lines / channels:切场景走 updateData 一次换完,通道一条条到货走 addChannels 批量落地,收 loading 用 onDataRendered / whenDataRendered;「模拟接口(延迟 900ms)」演示推荐的「先 createGisView(空场景)→ 接口回来 → updateData」—— 首屏落位由组件在首次拿到数据时自动做一次,不用等接口再建实例。</p>
306
- </div>
307
- </aside>
308
- </section>
309
- </template>