gis-view-webgl2 0.1.11 → 0.1.13
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 +0 -24
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +524 -33
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +156 -8
- package/docs/GisView.md +6 -6
- 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 +157 -7
- package/docs/demos/BackgroundDemo.vue +600 -0
- package/docs/demos/ChannelDemo.vue +145 -3
- package/docs/demos/StyleDemo.vue +280 -69
- package/docs/demos/api-index.js +35 -19
- package/docs/demos/demos.js +26 -18
- package/docs/demos/gis-view-demo/demo.css +76 -0
- package/docs/demos/gis-view-demo/panels/StylePanels.vue +24 -8
- package/docs/demos/gis-view-demo/panels/ViewPanels.vue +33 -3
- package/docs/demos/gis-view-demo/useGisViewDemo.js +236 -22
- package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -1
- 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 +3 -3
- package/docs/from0/06-/344/273/216/346/225/260/346/215/256/345/210/260/345/256/236/344/276/213-/350/247/243/346/236/220/345/261/202.md +13 -11
- package/docs/from0/15-/351/231/204/345/275/225-/345/270/270/351/207/217/351/200/237/346/237/245/344/270/216/350/270/251/345/235/221/346/270/205/345/215/225.md +1 -1
- package/docs/index.html +2 -2
- package/docs/internals/10-/350/247/204/345/210/231/346/235/241/344/273/266/344/270/216/347/212/266/346/200/201/346/240/267/345/274/217.md +3 -3
- package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +1 -1
- package/docs/internals/14-/350/203/214/346/231/257/345/261/202.md +1 -0
- 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 +199 -0
- 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
- package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +32 -0
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
- package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +217 -26
- 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 +2 -2
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +37 -4
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +58 -1
- package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +59 -8
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +1 -1
- package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +220 -2
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +77 -16
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +30 -17
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +11 -4
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +17 -8
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +128 -4
- 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 +2 -2
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +13 -1
- 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 +4 -1
- package/docs/steps/index.html +1 -1
- package/docs/steps/selftest-demo.html +2 -2
- package/docs/steps/selftest-demos.html +626 -11
- package/docs/steps/selftest-docs.html +8 -4
- package/docs/steps/selftest-features.html +1428 -2
- package/docs/steps/selftest-perf.html +296 -25
- package/docs/steps/selftest-scale.html +61 -3
- package/docs/steps/selftest-select.html +303 -1
- package/docs/steps/step-06-style.js +3 -2
- package/package.json +2 -2
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* 背景与网架:先只渲染网架,再添加图片或 GeoJSON 底图。
|
|
4
|
+
* 编辑本身完全由组件的 beginBackgroundEdit / updateBackgroundEdit / endBackgroundEdit 接管;
|
|
5
|
+
* 这个页面只负责导入素材、调背景透明度与内置编辑器的配置。
|
|
6
|
+
*/
|
|
7
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
8
|
+
import { createGisView } from '../../src/index.js'
|
|
9
|
+
import { buildDemoData } from './shared/demoData.js'
|
|
10
|
+
import { buildDemoBasemap } from './demoBackground.js'
|
|
11
|
+
import './shared/demoStyle.css'
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits(['notice'])
|
|
14
|
+
const container = ref(null)
|
|
15
|
+
const dataset = buildDemoData({ count: 32 })
|
|
16
|
+
const dataBounds = (() => {
|
|
17
|
+
let minX = Number.POSITIVE_INFINITY
|
|
18
|
+
let minY = Number.POSITIVE_INFINITY
|
|
19
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
20
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
21
|
+
for (const point of dataset.points) {
|
|
22
|
+
minX = Math.min(minX, point.x)
|
|
23
|
+
minY = Math.min(minY, point.y)
|
|
24
|
+
maxX = Math.max(maxX, point.x)
|
|
25
|
+
maxY = Math.max(maxY, point.y)
|
|
26
|
+
}
|
|
27
|
+
return { minX, minY, maxX, maxY }
|
|
28
|
+
})()
|
|
29
|
+
const paddedBounds = (bounds, ratio = 0.28) => {
|
|
30
|
+
const padX = Math.max(bounds.maxX - bounds.minX, 1e-4) * ratio
|
|
31
|
+
const padY = Math.max(bounds.maxY - bounds.minY, 1e-4) * ratio
|
|
32
|
+
return { minX: bounds.minX - padX, minY: bounds.minY - padY, maxX: bounds.maxX + padX, maxY: bounds.maxY + padY }
|
|
33
|
+
}
|
|
34
|
+
const formatWorld = value => Number(value.toFixed(3)).toString()
|
|
35
|
+
const normalizeRotation = (value) => {
|
|
36
|
+
const degrees = Number(value)
|
|
37
|
+
if (!Number.isFinite(degrees)) {
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
const wrapped = ((degrees + 180) % 360 + 360) % 360 - 180
|
|
41
|
+
return Math.abs(wrapped) < 1e-9 ? 0 : wrapped
|
|
42
|
+
}
|
|
43
|
+
const rotateOffset = (x, y, degrees) => {
|
|
44
|
+
const radians = degrees * Math.PI / 180
|
|
45
|
+
const cosine = Math.cos(radians)
|
|
46
|
+
const sine = Math.sin(radians)
|
|
47
|
+
return [x * cosine - y * sine, x * sine + y * cosine]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 内置背景图:SVG data URL,不请求网络;换成任意宽高比也不会牵连外部资源 */
|
|
51
|
+
const buildSampleImage = () => {
|
|
52
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800">
|
|
53
|
+
<defs>
|
|
54
|
+
<pattern id="grid" width="64" height="64" patternUnits="userSpaceOnUse"><path d="M 64 0 L 0 0 0 64" fill="none" stroke="#c4d9ec" stroke-width="1" opacity="0.72"/></pattern>
|
|
55
|
+
<linearGradient id="water" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#b6ddf7"/><stop offset="1" stop-color="#8ec9ee"/></linearGradient>
|
|
56
|
+
</defs>
|
|
57
|
+
<rect width="1280" height="800" fill="#eef6fc"/><rect width="1280" height="800" fill="url(#grid)"/>
|
|
58
|
+
<path d="M -40 590 C 160 500 280 640 470 560 S 800 420 980 500 S 1210 600 1320 470 L 1320 860 L -40 860 Z" fill="url(#water)" opacity="0.82"/>
|
|
59
|
+
<g fill="#dceaf6" stroke="#9ebed9" stroke-width="2">
|
|
60
|
+
<rect x="90" y="90" width="220" height="130" rx="18"/><rect x="350" y="72" width="300" height="165" rx="22"/>
|
|
61
|
+
<rect x="700" y="105" width="210" height="125" rx="16"/><rect x="955" y="78" width="230" height="155" rx="20"/>
|
|
62
|
+
<rect x="120" y="260" width="270" height="175" rx="18"/><rect x="445" y="280" width="190" height="140" rx="16"/>
|
|
63
|
+
<rect x="685" y="275" width="260" height="180" rx="22"/><rect x="990" y="270" width="190" height="150" rx="16"/>
|
|
64
|
+
</g>
|
|
65
|
+
<g fill="#cfe8d7" stroke="#8fb99d" stroke-width="2"><rect x="175" y="115" width="88" height="72" rx="35"/><rect x="735" y="135" width="130" height="66" rx="30"/><rect x="1025" y="300" width="115" height="84" rx="30"/></g>
|
|
66
|
+
<g stroke="#f7fbfe" stroke-width="9" opacity="0.92" fill="none"><path d="M 0 235 L 1280 235"/><path d="M 0 455 L 1280 455"/><path d="M 325 0 L 325 800"/><path d="M 660 0 L 660 520"/><path d="M 960 0 L 960 800"/></g>
|
|
67
|
+
<g fill="#537b9f" font-family="Arial, sans-serif" font-size="22" font-weight="700"><text x="96" y="55">NORTH GRID</text><text x="1010" y="55">SECTOR 04</text><text x="152" y="690">RIVER FRONT</text><text x="914" y="690">SOUTH DOCK</text></g>
|
|
68
|
+
<g fill="#7897b3" font-family="Arial, sans-serif" font-size="14"><text x="96" y="76">CUSTOM BACKGROUND · DRAG TO RESIZE</text><text x="1008" y="76">WGS GRID / DEMO</text></g>
|
|
69
|
+
<circle cx="660" cy="455" r="13" fill="#ffffff" stroke="#3d7cb3" stroke-width="5"/><circle cx="660" cy="455" r="36" fill="none" stroke="#3d7cb3" stroke-width="2" opacity="0.45"/>
|
|
70
|
+
</svg>`
|
|
71
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`
|
|
72
|
+
}
|
|
73
|
+
const imageSource = ref(buildSampleImage())
|
|
74
|
+
const imageName = ref('内置示意底图')
|
|
75
|
+
const geoJsonSource = ref(buildDemoBasemap(dataBounds, { grid: true, padding: 0.28 }))
|
|
76
|
+
const geoJsonName = ref('内置街区 / 网格')
|
|
77
|
+
const backgroundMode = ref('none')
|
|
78
|
+
const backgroundOpacity = ref(0.92)
|
|
79
|
+
const networkVisible = ref(true)
|
|
80
|
+
const networkOpacity = ref(1)
|
|
81
|
+
const aspectLocked = ref(true)
|
|
82
|
+
const rotationEnabled = ref(false)
|
|
83
|
+
const editorStyle = reactive({
|
|
84
|
+
color: '#2f78b7',
|
|
85
|
+
lineWidth: 1.5,
|
|
86
|
+
dashed: true,
|
|
87
|
+
handleSize: 15,
|
|
88
|
+
handleShape: 'circle'
|
|
89
|
+
})
|
|
90
|
+
const initialBounds = paddedBounds(dataBounds)
|
|
91
|
+
const editCenter = ref([(initialBounds.minX + initialBounds.maxX) / 2, (initialBounds.minY + initialBounds.maxY) / 2])
|
|
92
|
+
const editWidth = ref(initialBounds.maxX - initialBounds.minX)
|
|
93
|
+
const editHeight = ref(initialBounds.maxY - initialBounds.minY)
|
|
94
|
+
const editRotation = ref(0)
|
|
95
|
+
const editBounds = computed(() => ({
|
|
96
|
+
minX: editCenter.value[0] - editWidth.value / 2,
|
|
97
|
+
minY: editCenter.value[1] - editHeight.value / 2,
|
|
98
|
+
maxX: editCenter.value[0] + editWidth.value / 2,
|
|
99
|
+
maxY: editCenter.value[1] + editHeight.value / 2
|
|
100
|
+
}))
|
|
101
|
+
const backgroundEditing = ref(false)
|
|
102
|
+
const widthDraft = ref(formatWorld(editWidth.value))
|
|
103
|
+
const heightDraft = ref(formatWorld(editHeight.value))
|
|
104
|
+
const rotationDraft = ref('0')
|
|
105
|
+
const resolvedReadout = ref('尚未添加背景')
|
|
106
|
+
const notice = ref('先看网架;再添加背景。编辑背景时网架不可点击,滚轮仍可整体缩放。')
|
|
107
|
+
let view = null
|
|
108
|
+
let readoutTimer = 0
|
|
109
|
+
|
|
110
|
+
const modeLabel = computed(() => ({ none: '无背景', image: '图片', geojson: 'GeoJSON' })[backgroundMode.value])
|
|
111
|
+
const sizeReadout = computed(() => `${editWidth.value.toFixed(2)} × ${editHeight.value.toFixed(2)} 世界单位 · ${formatWorld(editRotation.value)}°`)
|
|
112
|
+
const centerReadout = computed(() => `中心 [${formatWorld(editCenter.value[0])}, ${formatWorld(editCenter.value[1])}]`)
|
|
113
|
+
const networkStateText = computed(() => networkVisible.value ? '网架显示中' : '网架已隐藏')
|
|
114
|
+
const sourceName = computed(() => backgroundMode.value === 'image' ? imageName.value : backgroundMode.value === 'geojson' ? geoJsonName.value : '—')
|
|
115
|
+
const resizeModeText = computed(() => aspectLocked.value ? '四角等比缩放' : '八向自由缩放')
|
|
116
|
+
const editorPointSummary = computed(() => {
|
|
117
|
+
const resizeCount = aspectLocked.value ? 4 : 8
|
|
118
|
+
return rotationEnabled.value
|
|
119
|
+
? `${resizeCount + 1} 个编辑点(${resizeCount} 缩放 + 1 旋转)`
|
|
120
|
+
: `${resizeCount} 个编辑点(仅缩放)`
|
|
121
|
+
})
|
|
122
|
+
const editorGestureHint = computed(() => `拖背景移动 · ${resizeModeText.value} · ${rotationEnabled.value ? '顶部圆点旋转' : '开启旋转后显示旋转圆点'}`)
|
|
123
|
+
|
|
124
|
+
const rotatedCorners = () => {
|
|
125
|
+
const halfWidth = editWidth.value / 2
|
|
126
|
+
const halfHeight = editHeight.value / 2
|
|
127
|
+
return [
|
|
128
|
+
[-halfWidth, halfHeight],
|
|
129
|
+
[halfWidth, halfHeight],
|
|
130
|
+
[halfWidth, -halfHeight],
|
|
131
|
+
[-halfWidth, -halfHeight]
|
|
132
|
+
].map(([x, y]) => {
|
|
133
|
+
const [offsetX, offsetY] = rotateOffset(x, y, -editRotation.value)
|
|
134
|
+
return [editCenter.value[0] + offsetX, editCenter.value[1] + offsetY]
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
const buildBackground = () => {
|
|
138
|
+
const base = { color: '#f4f8fc', opacity: backgroundOpacity.value }
|
|
139
|
+
if (backgroundMode.value === 'image') {
|
|
140
|
+
return { ...base, image: { src: imageSource.value, corners: rotatedCorners(), opacity: 1, shadow: { color: '#3f6388', opacity: 0.2, offsetX: 8, offsetY: 10, blur: 18 } } }
|
|
141
|
+
}
|
|
142
|
+
if (backgroundMode.value === 'geojson') {
|
|
143
|
+
return {
|
|
144
|
+
...base,
|
|
145
|
+
geoJson: geoJsonSource.value,
|
|
146
|
+
geoJsonStyle: {
|
|
147
|
+
bounds: editBounds.value,
|
|
148
|
+
rotation: editRotation.value,
|
|
149
|
+
aspectScale: 1,
|
|
150
|
+
fill: '#b9d5ee',
|
|
151
|
+
fillOpacity: 0.48,
|
|
152
|
+
stroke: '#6f9bc5',
|
|
153
|
+
borderWidth: 1.2,
|
|
154
|
+
opacity: 0.82,
|
|
155
|
+
pointColor: '#4676a4',
|
|
156
|
+
pointSize: 4,
|
|
157
|
+
label: { show: true, color: '#31597f', fontSize: 12, fontWeight: '600' }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return base
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const refreshResolvedReadout = () => {
|
|
165
|
+
if (readoutTimer) {
|
|
166
|
+
window.clearTimeout(readoutTimer)
|
|
167
|
+
readoutTimer = 0
|
|
168
|
+
}
|
|
169
|
+
const read = () => {
|
|
170
|
+
const resolved = view?.getResolvedBackground() ?? null
|
|
171
|
+
if (backgroundMode.value === 'none' || !resolved) {
|
|
172
|
+
resolvedReadout.value = '尚未添加背景'
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (backgroundMode.value === 'image') {
|
|
176
|
+
const corners = view.getOptions().background?.image?.corners ?? []
|
|
177
|
+
resolvedReadout.value = resolved.image?.quad?.length
|
|
178
|
+
? `图片四角已解析:${corners.map(corner => `[${corner.map(value => Number(value).toFixed(2)).join(', ')}]`).join(' / ')}`
|
|
179
|
+
: '图片正在解码,四角解析完成后会更新'
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const geo = resolved.geoJson
|
|
183
|
+
resolvedReadout.value = geo
|
|
184
|
+
? `GeoJSON ${geo.regions.length} 个区域 · ${formatWorld(geo.rotation)}° · 显示范围 ${geo.bounds ? `[${Number(geo.bounds.minX).toFixed(2)}, ${Number(geo.bounds.minY).toFixed(2)}, ${Number(geo.bounds.maxX).toFixed(2)}, ${Number(geo.bounds.maxY).toFixed(2)}]` : '—'} · ${geo.labels.length} 条区域文字`
|
|
185
|
+
: 'GeoJSON 正在解析'
|
|
186
|
+
}
|
|
187
|
+
read()
|
|
188
|
+
if (backgroundMode.value === 'image') {
|
|
189
|
+
readoutTimer = window.setTimeout(() => {
|
|
190
|
+
readoutTimer = 0
|
|
191
|
+
read()
|
|
192
|
+
}, 160)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const syncFrameInputs = () => {
|
|
197
|
+
widthDraft.value = formatWorld(editWidth.value)
|
|
198
|
+
heightDraft.value = formatWorld(editHeight.value)
|
|
199
|
+
rotationDraft.value = formatWorld(editRotation.value)
|
|
200
|
+
}
|
|
201
|
+
const syncEditorState = (state) => {
|
|
202
|
+
if (!state) {
|
|
203
|
+
backgroundEditing.value = false
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
backgroundEditing.value = state.active
|
|
207
|
+
editCenter.value = [...state.center]
|
|
208
|
+
editWidth.value = state.width
|
|
209
|
+
editHeight.value = state.height
|
|
210
|
+
editRotation.value = state.rotation
|
|
211
|
+
syncFrameInputs()
|
|
212
|
+
refreshResolvedReadout()
|
|
213
|
+
}
|
|
214
|
+
const editorOptions = ({ fitView = false, onChange = syncEditorState, onEnd } = {}) => ({
|
|
215
|
+
aspectLocked: aspectLocked.value,
|
|
216
|
+
rotation: rotationEnabled.value,
|
|
217
|
+
snapAngle: 15,
|
|
218
|
+
minSize: 12,
|
|
219
|
+
rotationDistance: 30,
|
|
220
|
+
fitView,
|
|
221
|
+
fitPadding: 64,
|
|
222
|
+
showHint: true,
|
|
223
|
+
style: { ...editorStyle },
|
|
224
|
+
onChange,
|
|
225
|
+
onEnd: onEnd ?? ((state) => {
|
|
226
|
+
syncEditorState(state)
|
|
227
|
+
notice.value = `背景编辑完成:${editorGestureHint.value}。`
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
const beginEditor = ({ fitView = false } = {}) => {
|
|
231
|
+
if (!view || backgroundMode.value === 'none') {
|
|
232
|
+
backgroundEditing.value = false
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
view.beginBackgroundEdit(editorOptions({ fitView }))
|
|
236
|
+
syncEditorState(view.getBackgroundEdit())
|
|
237
|
+
backgroundEditing.value = view.getBackgroundEdit() !== null
|
|
238
|
+
}
|
|
239
|
+
const applyBackground = () => {
|
|
240
|
+
if (!view) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
view.updateBackground(buildBackground())
|
|
244
|
+
view.refresh({ reflow: false })
|
|
245
|
+
refreshResolvedReadout()
|
|
246
|
+
}
|
|
247
|
+
const setMode = (mode) => {
|
|
248
|
+
if (!view) {
|
|
249
|
+
backgroundMode.value = mode
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
view.endBackgroundEdit()
|
|
253
|
+
backgroundEditing.value = false
|
|
254
|
+
backgroundMode.value = mode
|
|
255
|
+
if (mode === 'none') {
|
|
256
|
+
applyBackground()
|
|
257
|
+
notice.value = '背景已清空,网架与相机位置保持不变。'
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
applyBackground()
|
|
261
|
+
beginEditor({ fitView: true })
|
|
262
|
+
notice.value = mode === 'image'
|
|
263
|
+
? `背景图已添加:${editorGestureHint.value}。`
|
|
264
|
+
: `GeoJSON 已添加:位置写 bounds、旋转写 geoJsonStyle.rotation;${editorGestureHint.value}。`
|
|
265
|
+
}
|
|
266
|
+
const setEditing = (value) => {
|
|
267
|
+
if (value && backgroundMode.value === 'none') {
|
|
268
|
+
backgroundEditing.value = false
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
if (value) {
|
|
272
|
+
beginEditor()
|
|
273
|
+
notice.value = `背景编辑模式:${editorGestureHint.value};滚轮只缩放不操作网架。`
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
view?.endBackgroundEdit()
|
|
277
|
+
backgroundEditing.value = false
|
|
278
|
+
notice.value = '已退出背景编辑,网架恢复浏览交互。'
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const focusBackground = () => {
|
|
282
|
+
if (backgroundMode.value === 'none') {
|
|
283
|
+
view?.fitView(72)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
if (!backgroundEditing.value) {
|
|
287
|
+
beginEditor()
|
|
288
|
+
}
|
|
289
|
+
view?.updateBackgroundEdit({ fitView: true })
|
|
290
|
+
emit('notice', '已把整块背景和编辑框放回视野。')
|
|
291
|
+
}
|
|
292
|
+
const fitViewport = () => {
|
|
293
|
+
if (backgroundMode.value === 'none') {
|
|
294
|
+
view?.fitView(72)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
focusBackground()
|
|
298
|
+
}
|
|
299
|
+
const applySize = (axis) => {
|
|
300
|
+
if (!view || !backgroundEditing.value) {
|
|
301
|
+
syncFrameInputs()
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
const value = axis === 'width' ? Number(widthDraft.value) : Number(heightDraft.value)
|
|
305
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
306
|
+
syncFrameInputs()
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
view.updateBackgroundEdit(axis === 'width' ? { width: value } : { height: value })
|
|
310
|
+
syncEditorState(view.getBackgroundEdit())
|
|
311
|
+
}
|
|
312
|
+
const applyRotation = () => {
|
|
313
|
+
if (!view || !backgroundEditing.value) {
|
|
314
|
+
syncFrameInputs()
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
const next = normalizeRotation(Number(rotationDraft.value) || 0)
|
|
318
|
+
view.updateBackgroundEdit({ angle: next })
|
|
319
|
+
syncEditorState(view.getBackgroundEdit())
|
|
320
|
+
}
|
|
321
|
+
const rotateBy = (delta) => {
|
|
322
|
+
if (!view || !backgroundEditing.value) {
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
view.updateBackgroundEdit({ angle: normalizeRotation(editRotation.value + delta) })
|
|
326
|
+
syncEditorState(view.getBackgroundEdit())
|
|
327
|
+
}
|
|
328
|
+
const toggleNetwork = () => {
|
|
329
|
+
networkVisible.value = !networkVisible.value
|
|
330
|
+
}
|
|
331
|
+
const onImageFile = (event) => {
|
|
332
|
+
const file = event.target.files?.[0]
|
|
333
|
+
if (!file) {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
const reader = new FileReader()
|
|
337
|
+
reader.onload = () => {
|
|
338
|
+
imageSource.value = String(reader.result)
|
|
339
|
+
imageName.value = file.name
|
|
340
|
+
setMode('image')
|
|
341
|
+
emit('notice', `已载入背景图 ${file.name}`)
|
|
342
|
+
}
|
|
343
|
+
reader.readAsDataURL(file)
|
|
344
|
+
event.target.value = ''
|
|
345
|
+
}
|
|
346
|
+
const onGeoJsonFile = (event) => {
|
|
347
|
+
const file = event.target.files?.[0]
|
|
348
|
+
if (!file) {
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
const reader = new FileReader()
|
|
352
|
+
reader.onload = () => {
|
|
353
|
+
try {
|
|
354
|
+
geoJsonSource.value = JSON.parse(String(reader.result))
|
|
355
|
+
geoJsonName.value = file.name
|
|
356
|
+
setMode('geojson')
|
|
357
|
+
emit('notice', `已载入 GeoJSON ${file.name}`)
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
notice.value = `无法解析 ${file.name}:请选择合法的 GeoJSON JSON 文件。`
|
|
361
|
+
emit('notice', notice.value)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
reader.readAsText(file, 'utf-8')
|
|
365
|
+
event.target.value = ''
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
watch(networkVisible, value => {
|
|
369
|
+
view?.updateOptions({ networkVisible: value })
|
|
370
|
+
notice.value = value ? '网架已恢复;背景仍保持当前范围。' : '网架已完全隐藏,只留下背景与编辑手柄。'
|
|
371
|
+
})
|
|
372
|
+
watch(networkOpacity, value => {
|
|
373
|
+
view?.updateOptions({ networkOpacity: value })
|
|
374
|
+
})
|
|
375
|
+
watch(backgroundOpacity, () => applyBackground())
|
|
376
|
+
watch(aspectLocked, (value) => {
|
|
377
|
+
if (backgroundEditing.value) {
|
|
378
|
+
view?.updateBackgroundEdit({ aspectLocked: value })
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
watch(rotationEnabled, (value) => {
|
|
382
|
+
if (backgroundEditing.value) {
|
|
383
|
+
view?.updateBackgroundEdit({ rotation: value })
|
|
384
|
+
notice.value = `背景编辑模式:${editorGestureHint.value}${value ? ';拖动顶部圆点即可旋转' : ';当前只显示缩放手柄'}。`
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
watch(editorStyle, () => {
|
|
388
|
+
if (backgroundEditing.value) {
|
|
389
|
+
view?.updateBackgroundEdit({ style: { ...editorStyle } })
|
|
390
|
+
}
|
|
391
|
+
}, { deep: true })
|
|
392
|
+
|
|
393
|
+
onMounted(() => {
|
|
394
|
+
view = createGisView(container.value, {
|
|
395
|
+
points: dataset.points,
|
|
396
|
+
lines: dataset.linesWithoutChannel,
|
|
397
|
+
channels: [],
|
|
398
|
+
pointStyle: { size: 7, color: '#ffffff', strokeColor: '#3d7cb3', strokeWidth: 1.8 },
|
|
399
|
+
lineStyle: { width: 1.8, color: 'rgba(64, 112, 158, 0.72)' },
|
|
400
|
+
fitPadding: 72,
|
|
401
|
+
showGrid: false,
|
|
402
|
+
features: 'view',
|
|
403
|
+
networkVisible: networkVisible.value,
|
|
404
|
+
networkOpacity: networkOpacity.value,
|
|
405
|
+
background: buildBackground(),
|
|
406
|
+
onReady: () => {
|
|
407
|
+
emit('notice', '网架已就绪:添加图片或 GeoJSON,再拖动内置编辑框调背景。')
|
|
408
|
+
},
|
|
409
|
+
onWarning: (text) => {
|
|
410
|
+
notice.value = `warning:${text}`
|
|
411
|
+
emit('notice', notice.value)
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
onBeforeUnmount(() => {
|
|
417
|
+
if (readoutTimer) {
|
|
418
|
+
window.clearTimeout(readoutTimer)
|
|
419
|
+
readoutTimer = 0
|
|
420
|
+
}
|
|
421
|
+
view?.destroy()
|
|
422
|
+
view = null
|
|
423
|
+
})
|
|
424
|
+
</script>
|
|
425
|
+
|
|
426
|
+
<template>
|
|
427
|
+
<section class="dv-demo">
|
|
428
|
+
<div class="dv-stage">
|
|
429
|
+
<div ref="container" class="dv-canvas"></div>
|
|
430
|
+
<div class="dv-toolbar">
|
|
431
|
+
<button type="button" data-demo="background-fit" @click="fitViewport">{{ backgroundMode === 'none' ? '适应全部' : '适应背景' }}</button>
|
|
432
|
+
<button v-if="backgroundMode !== 'none'" type="button" data-demo="background-toggle-editing" @click="setEditing(!backgroundEditing)">
|
|
433
|
+
{{ backgroundEditing ? '退出背景编辑' : '编辑背景' }}
|
|
434
|
+
</button>
|
|
435
|
+
<button type="button" @click="setMode('image')">切到图片</button>
|
|
436
|
+
<button type="button" @click="setMode('geojson')">切到 GeoJSON</button>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
<aside class="dv-panel">
|
|
440
|
+
<div class="dv-group">
|
|
441
|
+
<h3>添加背景<span class="dv-chip">{{ modeLabel }}</span></h3>
|
|
442
|
+
<div class="dv-row">
|
|
443
|
+
<button type="button" class="dv-button dv-button--primary" data-demo="background-add-image" @click="setMode('image')">添加背景图</button>
|
|
444
|
+
<button type="button" class="dv-button dv-button--primary" data-demo="background-add-geojson" @click="setMode('geojson')">添加 GeoJSON</button>
|
|
445
|
+
<button type="button" class="dv-button dv-button--danger" data-demo="background-clear" @click="setMode('none')">清空</button>
|
|
446
|
+
</div>
|
|
447
|
+
<div class="dv-row">
|
|
448
|
+
<label class="bg-upload">上传图片<input data-demo="background-image-file" type="file" accept="image/*" @change="onImageFile" /></label>
|
|
449
|
+
<label class="bg-upload">上传 GeoJSON<input data-demo="background-geojson-file" type="file" accept=".json,.geojson,application/json" @change="onGeoJsonFile" /></label>
|
|
450
|
+
</div>
|
|
451
|
+
<p class="dv-hint">先显示网架,再选一种背景。上传的文件只在这一页本地解析,不会发请求。</p>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
<div class="dv-group">
|
|
455
|
+
<h3>位置、大小与旋转<span class="dv-chip">{{ sizeReadout }}</span></h3>
|
|
456
|
+
<div class="dv-row">
|
|
457
|
+
<label><input v-model="aspectLocked" data-demo="background-lock-ratio" type="checkbox" /> 锁定宽高比</label>
|
|
458
|
+
<label><input v-model="rotationEnabled" data-demo="background-rotation-enabled" type="checkbox" /> 允许旋转</label>
|
|
459
|
+
<span class="dv-chip" data-demo="background-edit-point-summary">{{ editorPointSummary }}</span>
|
|
460
|
+
</div>
|
|
461
|
+
<p class="dv-hint">锁定时只显示四个角点并等比缩放;解锁后显示四角与四边中点,共八向自由缩放;开启旋转时再多一个顶部旋转点,共九个编辑点。</p>
|
|
462
|
+
<div class="dv-row">
|
|
463
|
+
<label>宽 <input v-model="widthDraft" class="dv-number" data-demo="background-width" type="number" min="0.001" step="0.1" @change="applySize('width')" /></label>
|
|
464
|
+
<label>高 <input v-model="heightDraft" class="dv-number" data-demo="background-height" type="number" min="0.001" step="0.1" @change="applySize('height')" /></label>
|
|
465
|
+
</div>
|
|
466
|
+
<div v-if="rotationEnabled" class="dv-row">
|
|
467
|
+
<label>旋转 <input v-model="rotationDraft" class="dv-number" data-demo="background-rotation" type="number" step="1" @change="applyRotation" /><span class="dv-unit">°</span></label>
|
|
468
|
+
<button type="button" class="dv-button" data-demo="background-rotate-left" @click="rotateBy(-15)">−15°</button>
|
|
469
|
+
<button type="button" class="dv-button" data-demo="background-rotate-right" @click="rotateBy(15)">+15°</button>
|
|
470
|
+
</div>
|
|
471
|
+
<div class="dv-row">
|
|
472
|
+
<label><input :checked="backgroundEditing" :disabled="backgroundMode === 'none'" data-demo="background-edit-mode" type="checkbox" @change="setEditing($event.target.checked)" /> 背景编辑模式</label>
|
|
473
|
+
<button type="button" class="dv-button" :disabled="backgroundMode === 'none'" data-demo="background-fit-panel" @click="focusBackground">把背景放回视野</button>
|
|
474
|
+
</div>
|
|
475
|
+
<p class="dv-readout" data-demo="background-center-read">{{ centerReadout }}</p>
|
|
476
|
+
<p class="dv-hint">页面只调用 `beginBackgroundEdit()` / `updateBackgroundEdit()` / `endBackgroundEdit()`;组件负责编辑层、命中与同帧刷新。图片写回 `image.corners`,GeoJSON 写回 `geoJsonStyle.bounds` + `rotation`。</p>
|
|
477
|
+
<p class="dv-readout" data-demo="background-size-read">当前背景范围:{{ sizeReadout }}</p>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div class="dv-group">
|
|
481
|
+
<h3>编辑框样式<span class="dv-chip">内置 API</span></h3>
|
|
482
|
+
<div class="dv-row">
|
|
483
|
+
<label>颜色 <input v-model="editorStyle.color" class="bg-color-input" data-demo="background-edit-color" type="color" /></label>
|
|
484
|
+
<label>线宽
|
|
485
|
+
<input v-model.number="editorStyle.lineWidth" data-demo="background-edit-line-width" type="range" min="0.5" max="5" step="0.5" />
|
|
486
|
+
<span>{{ editorStyle.lineWidth }} px</span>
|
|
487
|
+
</label>
|
|
488
|
+
</div>
|
|
489
|
+
<div class="dv-row">
|
|
490
|
+
<label><input v-model="editorStyle.dashed" data-demo="background-edit-dashed" type="checkbox" /> 虚线轮廓</label>
|
|
491
|
+
<label>手柄大小
|
|
492
|
+
<input v-model.number="editorStyle.handleSize" data-demo="background-edit-handle-size" type="range" min="8" max="28" step="1" />
|
|
493
|
+
<span>{{ editorStyle.handleSize }} px</span>
|
|
494
|
+
</label>
|
|
495
|
+
<label>形状
|
|
496
|
+
<select v-model="editorStyle.handleShape" class="bg-select" data-demo="background-edit-handle-shape">
|
|
497
|
+
<option value="circle">圆形</option>
|
|
498
|
+
<option value="square">方形</option>
|
|
499
|
+
</select>
|
|
500
|
+
</label>
|
|
501
|
+
</div>
|
|
502
|
+
<p class="dv-hint">这些控件走 `updateBackgroundEdit({ style })`,实时作用到轮廓、旋转连线和缩放手柄;组件背景本身的渲染不依赖编辑框样式。</p>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<div class="dv-group">
|
|
506
|
+
<h3>网架显隐与透明度<span class="dv-chip">{{ networkStateText }}</span></h3>
|
|
507
|
+
<div class="dv-row">
|
|
508
|
+
<button type="button" class="dv-button" :class="{ 'dv-button--primary': !networkVisible }" data-demo="background-toggle-network" @click="toggleNetwork">
|
|
509
|
+
{{ networkVisible ? '一键隐藏网架' : '恢复显示网架' }}
|
|
510
|
+
</button>
|
|
511
|
+
<label><input v-model="networkVisible" data-demo="background-network-visible" type="checkbox" /> 显示网架</label>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="dv-row">
|
|
514
|
+
<label>整体透明度
|
|
515
|
+
<input v-model.number="networkOpacity" data-demo="background-network-opacity" type="range" min="0" max="1" step="0.05" />
|
|
516
|
+
<span>{{ Math.round(networkOpacity * 100) }}%</span>
|
|
517
|
+
</label>
|
|
518
|
+
</div>
|
|
519
|
+
<p class="dv-hint">隐藏时网架节点、线路、动画、标签和拾取一起收掉,背景与网格照常显示;若只想让网架退到背景后面,把透明度拉到 0 ~ 40%。</p>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<div class="dv-group">
|
|
523
|
+
<h3>背景层<span class="dv-chip">{{ sourceName }}</span></h3>
|
|
524
|
+
<div class="dv-row">
|
|
525
|
+
<label>背景透明度
|
|
526
|
+
<input v-model.number="backgroundOpacity" data-demo="background-opacity" type="range" min="0" max="1" step="0.05" />
|
|
527
|
+
<span>{{ Math.round(backgroundOpacity * 100) }}%</span>
|
|
528
|
+
</label>
|
|
529
|
+
</div>
|
|
530
|
+
<p class="dv-readout" data-demo="background-readout">
|
|
531
|
+
{{ modeLabel }} · {{ sizeReadout }} · {{ networkStateText }} · 整体不透明度 {{ Math.round(networkOpacity * 100) }}%
|
|
532
|
+
</p>
|
|
533
|
+
<p class="dv-readout" data-demo="background-resolved">{{ resolvedReadout }}</p>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div class="dv-group">
|
|
537
|
+
<h3>操作提示</h3>
|
|
538
|
+
<p class="dv-hint" data-demo="background-notice">{{ notice }}</p>
|
|
539
|
+
<p class="dv-hint">调背景时可以先隐藏网架,只留编辑框和手柄;恢复网架后,透明度滑块能直接预览「网架压住背景」的效果。GeoJSON 的区域文字属于背景层,不受网架显隐影响。</p>
|
|
540
|
+
</div>
|
|
541
|
+
</aside>
|
|
542
|
+
</section>
|
|
543
|
+
</template>
|
|
544
|
+
|
|
545
|
+
<style scoped>
|
|
546
|
+
.dv-toolbar {
|
|
547
|
+
z-index: 5;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.bg-upload {
|
|
551
|
+
position: relative;
|
|
552
|
+
padding: 5px 9px;
|
|
553
|
+
border: 1px solid #cfdcec;
|
|
554
|
+
border-radius: 6px;
|
|
555
|
+
background: #f6f9fd;
|
|
556
|
+
color: #2f5e94;
|
|
557
|
+
overflow: hidden;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.bg-upload input {
|
|
561
|
+
position: absolute;
|
|
562
|
+
inset: 0;
|
|
563
|
+
opacity: 0;
|
|
564
|
+
cursor: pointer;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.bg-color-input {
|
|
568
|
+
width: 36px;
|
|
569
|
+
height: 26px;
|
|
570
|
+
padding: 2px;
|
|
571
|
+
border: 1px solid #cfdcec;
|
|
572
|
+
border-radius: 6px;
|
|
573
|
+
background: #fff;
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.bg-select {
|
|
578
|
+
padding: 4px 6px;
|
|
579
|
+
border: 1px solid #d6e0ec;
|
|
580
|
+
border-radius: 6px;
|
|
581
|
+
background: #fff;
|
|
582
|
+
color: #1f2a44;
|
|
583
|
+
font-size: 12px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.dv-number {
|
|
587
|
+
width: 88px;
|
|
588
|
+
padding: 4px 6px;
|
|
589
|
+
border: 1px solid #d6e0ec;
|
|
590
|
+
border-radius: 6px;
|
|
591
|
+
background: #fff;
|
|
592
|
+
color: #1f2a44;
|
|
593
|
+
font-size: 12px;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.dv-unit {
|
|
597
|
+
color: #60718a;
|
|
598
|
+
font-size: 12px;
|
|
599
|
+
}
|
|
600
|
+
</style>
|