huweili-cesium 1.2.37 → 1.2.38
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/js/captureFenceScreenshot.js +139 -989
- package/package.json +1 -1
|
@@ -1,1041 +1,191 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 提供防区设置页面所需的地图相关功能,包括:
|
|
5
|
-
* 1. 指挥所半球区域的初始化和控制
|
|
6
|
-
* 2. 多边形电子围栏的绘制和管理
|
|
7
|
-
* 3. 围栏历史记录的管理
|
|
8
|
-
*
|
|
9
|
-
* @author
|
|
10
|
-
* @version 1.0.0
|
|
2
|
+
* 电子围栏绘制完成后,按围栏在屏幕上的范围裁剪地图截图并输出 base64
|
|
11
3
|
*/
|
|
12
|
-
|
|
13
|
-
// 导入依赖
|
|
14
4
|
import * as Cesium from 'cesium'
|
|
15
|
-
import {
|
|
16
|
-
import { ElMessage } from 'element-plus'
|
|
17
|
-
import { basicConfig } from 'huweili-cesium/basis'
|
|
18
|
-
import { drawFenceNew } from 'huweili-cesium/drawFenceNew'
|
|
19
|
-
import { useMapStore } from 'huweili-cesium/stores/mapStore'
|
|
20
|
-
import { BaseConfig } from '@/config/global'
|
|
21
|
-
import {
|
|
22
|
-
initCommandHemisphereZones, // 初始化指挥所半球区域
|
|
23
|
-
updateRadiusCommandHemisphereZones, // 更新半球区域半径
|
|
24
|
-
setVisibilityCommandHemisphereZones, // 设置半球区域可见性
|
|
25
|
-
} from '@/utils/cesium/initCommandHemisphereZones.js'
|
|
26
|
-
import { captureFenceScreenshot } from 'huweili-cesium/captureFenceScreenshot.js'
|
|
27
|
-
import { listElectronicFence } from '@/api/fence'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* UI 提示文字常量(使用 Unicode 转义避免 Windows 编辑器编码问题)
|
|
31
|
-
*/
|
|
32
|
-
const MSG = {
|
|
33
|
-
fileDesc: '\u9632\u533A\u8BBE\u7F6E\u9875 \u2014 \u5730\u56FE\u76F8\u5173\u80FD\u529B\uFF08\u6307\u6325\u6240\u534A\u7403\u3001\u591A\u8FB9\u5F62\u56F4\u680F\u7ED8\u5236\u7B49\uFF09',
|
|
34
|
-
fenceHistoryList: '\u56F4\u680F\u5386\u53F2\u5217\u8868',
|
|
35
|
-
zoneEnabled: '\u9632\u533A\u5F00\u5173',
|
|
36
|
-
maxFenceCount: '\u6700\u5927\u56F4\u680F\u6570\u91CF',
|
|
37
|
-
openFenceFirst: '\u8BF7\u5148\u5F00\u542F\u7535\u5B50\u56F4\u680F',
|
|
38
|
-
fenceListFull: '\u56F4\u680F\u5386\u53F2\u5217\u8868\u5DF2\u6EE1\uFF0C\u65E0\u6CD5\u7ED8\u5236',
|
|
39
|
-
mapNotReady: '\u5730\u56FE\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210',
|
|
40
|
-
polygonDrawingInProgress: '\u6B63\u5728\u7ED8\u5236\u591A\u8FB9\u5F62\u56F4\u680F\uFF0C\u8BF7\u5148\u5B8C\u6210\u6216\u70B9\uFF0C\u53CC\u51FB\u6216\u53F3\u952E\u7ED3\u675F',
|
|
41
|
-
circleDrawingInProgress: '\u6B63\u5728\u7ED8\u5236\u5706\u5F62\u56F4\u680F\uFF0C\u8BF7\u5148\u5B8C\u6210\u6216\u53D6\u6D88',
|
|
42
|
-
polygonFencePrefix: '\u591A\u8FB9\u5F62\u56F4\u680F',
|
|
43
|
-
circleFencePrefix: '\u5706\u5F62\u56F4\u680F',
|
|
44
|
-
drawDone: '\u7ED8\u5236\u5B8C\u6210',
|
|
45
|
-
polygonDrawingHint: '\u591A\u8FB9\u5F62\u56F4\u680F\u7ED8\u5236\u4E2D\uFF1A\u5DE6\u952E\u52A0\u70B9\uFF0C\u53CC\u51FB\u6216\u53F3\u952E\u7ED3\u675F',
|
|
46
|
-
circleDrawingHint: '\u5706\u5F62\u56F4\u680F\u7ED8\u5236\u4E2D\uFF1A\u7B2C\u4E00\u6B21\u70B9\u51FB\u786E\u5B9A\u5706\u5FC3\uFF0C\u7B2C\u4E8C\u6B21\u70B9\u51FB\u786E\u5B9A\u534A\u5F84',
|
|
47
|
-
polygonDrawCancelled: '\u591A\u8FB9\u5F62\u56F4\u680F\u7ED8\u5236\u5DF2\u53D6\u6D88',
|
|
48
|
-
circleDrawCancelled: '\u5706\u5F62\u56F4\u680F\u7ED8\u5236\u5DF2\u53D6\u6D88',
|
|
49
|
-
cannotStartPolygonDraw: '\u65E0\u6CD5\u5F00\u59CB\u591A\u8FB9\u5F62\u7ED8\u5236',
|
|
50
|
-
cannotStartCircleDraw: '\u65E0\u6CD5\u5F00\u59CB\u5706\u5F62\u7ED8\u5236',
|
|
51
|
-
userCancelled: '\u5DF2\u53D6\u6D88\u7ED8\u5236',
|
|
52
|
-
fenceOutsideIdentifyZone: '\u7535\u5B50\u56F4\u680F\u5FC5\u987B\u5728\u8BC6\u522B\u533A\u4E4B\u5185\uFF0C\u8BF7\u91CD\u65B0\u7ED8\u5236',
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 防区设置页面地图实例ID
|
|
57
|
-
* 优先从全局配置获取,若未配置则使用默认值
|
|
58
|
-
*/
|
|
59
|
-
export const DEFENSE_ZONE_MAP_ID =
|
|
60
|
-
window.MapInstanceIds?.DEFENSE_ZONE_SETTING || 'defense-zone-setting-map'
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 最大围栏数量限制
|
|
64
|
-
*/
|
|
65
|
-
const MAX_FENCE_COUNT = 6
|
|
5
|
+
import { useMapStore } from './stores/mapStore.js'
|
|
66
6
|
|
|
67
|
-
|
|
68
|
-
* 获取地图状态管理实例
|
|
69
|
-
*/
|
|
70
|
-
const mapStore = useMapStore()
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 获取基础地图配置工具函数
|
|
74
|
-
*/
|
|
75
|
-
const { setBeiJingTime, setCesiumCenter } = basicConfig()
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 获取多边形围栏绘制工具函数
|
|
79
|
-
*/
|
|
80
|
-
const { drawPolygonFenceNoLabel, destroyPolygonFenceNoLabel, destroyPolygonFence, showPolygonFence, drawCircleFence, destroyCircleFence, showCircleFence } = drawFenceNew()
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 当前地图实例ID(运行时动态赋值)
|
|
84
|
-
*/
|
|
85
|
-
let currentMapId = ''
|
|
7
|
+
const scratchCartesian2 = new Cesium.Cartesian2()
|
|
86
8
|
|
|
87
9
|
/**
|
|
88
|
-
*
|
|
89
|
-
* @
|
|
10
|
+
* 将围栏顶点经纬度转为 Cartesian3
|
|
11
|
+
* @param {Array<{ lng: number, lat: number, height?: number }>} positions
|
|
90
12
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* @type {{ cancel?: () => void } | null}
|
|
96
|
-
*/
|
|
97
|
-
let circleDrawController = null
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 是否已显示多边形绘制提示信息(防止重复提示)
|
|
101
|
-
*/
|
|
102
|
-
let polygonDrawingTipShown = false
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 是否已显示圆形绘制提示信息(防止重复提示)
|
|
106
|
-
*/
|
|
107
|
-
let circleDrawingTipShown = false
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 识别区信息(用于检测电子围栏是否在识别区内)
|
|
111
|
-
*/
|
|
112
|
-
let identifyZoneInfo = {
|
|
113
|
-
center: [0, 0], // [lng, lat]
|
|
114
|
-
radius: 0 // 半径(米)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 获取当前地图ID
|
|
119
|
-
* @returns {string} 地图实例ID
|
|
120
|
-
*/
|
|
121
|
-
const getMapId = () => currentMapId || DEFENSE_ZONE_MAP_ID
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 更新识别区信息
|
|
125
|
-
* @param {number} lng - 中心点经度
|
|
126
|
-
* @param {number} lat - 中心点纬度
|
|
127
|
-
* @param {number} radius - 半径(米)
|
|
128
|
-
*/
|
|
129
|
-
const updateIdentifyZoneInfo = (lng, lat, radius) => {
|
|
130
|
-
identifyZoneInfo.center = [lng, lat]
|
|
131
|
-
identifyZoneInfo.radius = radius
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* 判断点是否在识别区内
|
|
136
|
-
* @param {number} lng - 点的经度
|
|
137
|
-
* @param {number} lat - 点的纬度
|
|
138
|
-
* @returns {boolean} - 是否在识别区内
|
|
139
|
-
*/
|
|
140
|
-
const isPointInIdentifyZone = (lng, lat) => {
|
|
141
|
-
const { center, radius } = identifyZoneInfo
|
|
142
|
-
|
|
143
|
-
// 如果识别区未正确初始化,允许绘制(避免因配置问题导致无法绘制)
|
|
144
|
-
if (!center || center.length < 2 || typeof center[0] !== 'number' || typeof center[1] !== 'number') {
|
|
145
|
-
console.warn('识别区中心点未正确初始化,允许绘制')
|
|
146
|
-
return true
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 如果识别区半径未启用或无效,允许绘制
|
|
150
|
-
if (!radius || radius <= 0 || typeof radius !== 'number') {
|
|
151
|
-
console.warn('识别区半径未正确初始化,允许绘制')
|
|
152
|
-
return true
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 使用Haversine公式计算两点之间的距离(米)
|
|
156
|
-
const R = 6371000 // 地球半径(米)
|
|
157
|
-
const dLat = (lat - center[1]) * Math.PI / 180
|
|
158
|
-
const dLng = (lng - center[0]) * Math.PI / 180
|
|
159
|
-
const a =
|
|
160
|
-
Math.sin(dLat/2) * Math.sin(dLat/2) +
|
|
161
|
-
Math.cos(center[1] * Math.PI / 180) * Math.cos(lat * Math.PI / 180) *
|
|
162
|
-
Math.sin(dLng/2) * Math.sin(dLng/2)
|
|
163
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
|
|
164
|
-
const distance = R * c
|
|
165
|
-
|
|
166
|
-
const isInside = distance <= radius
|
|
167
|
-
console.log(`点(${lng}, ${lat}) 到中心点(${center[0]}, ${center[1]}) 的距离: ${distance.toFixed(2)}m, 半径: ${radius}m, 是否在圈内: ${isInside}`)
|
|
168
|
-
|
|
169
|
-
return isInside
|
|
13
|
+
function toCartesians(positions) {
|
|
14
|
+
return positions.map((p) =>
|
|
15
|
+
Cesium.Cartesian3.fromDegrees(Number(p.lng), Number(p.lat), Number(p.height) || 0)
|
|
16
|
+
)
|
|
170
17
|
}
|
|
171
18
|
|
|
172
19
|
/**
|
|
173
|
-
*
|
|
174
|
-
* @param {Object} cartesian - Cesium Cartesian3对象
|
|
175
|
-
* @param {Object} map - Cesium地图实例
|
|
176
|
-
* @returns {Object} - 包含lng和lat的对象
|
|
20
|
+
* 相机飞到刚好能看清围栏
|
|
177
21
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
lng: Cesium.Math.toDegrees(cartographic.longitude),
|
|
187
|
-
lat: Cesium.Math.toDegrees(cartographic.latitude)
|
|
188
|
-
}
|
|
22
|
+
function flyCameraToFence(map, positions, options = {}) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const cartesians = toCartesians(positions)
|
|
25
|
+
const sphere = Cesium.BoundingSphere.fromPoints(cartesians)
|
|
26
|
+
if (!sphere || !Number.isFinite(sphere.radius)) {
|
|
27
|
+
resolve()
|
|
28
|
+
return
|
|
189
29
|
}
|
|
190
|
-
} catch (e) {
|
|
191
|
-
console.warn('Cartesian3转换经纬度失败:', e)
|
|
192
|
-
}
|
|
193
|
-
return null
|
|
194
|
-
}
|
|
195
30
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const pos = positions[i]
|
|
216
|
-
let lng, lat
|
|
217
|
-
|
|
218
|
-
// 处理不同的数据格式
|
|
219
|
-
if (Array.isArray(pos)) {
|
|
220
|
-
// 格式1: [lng, lat, height] 数组
|
|
221
|
-
lng = pos[0]
|
|
222
|
-
lat = pos[1]
|
|
223
|
-
console.log(`第${i}个顶点(数组格式): (${lng}, ${lat})`)
|
|
224
|
-
} else if (pos && typeof pos === 'object') {
|
|
225
|
-
// 格式2: 对象格式
|
|
226
|
-
if (pos.longitude !== undefined || pos.latitude !== undefined) {
|
|
227
|
-
// GeoJSON格式
|
|
228
|
-
lng = pos.longitude
|
|
229
|
-
lat = pos.latitude
|
|
230
|
-
} else if (pos.lng !== undefined || pos.lat !== undefined) {
|
|
231
|
-
// 简化格式
|
|
232
|
-
lng = pos.lng
|
|
233
|
-
lat = pos.lat
|
|
234
|
-
} else if (pos.x !== undefined && pos.y !== undefined && pos.z !== undefined) {
|
|
235
|
-
// 可能是Cartesian3格式,尝试转换
|
|
236
|
-
const latLng = cartesianToLngLat(pos, map)
|
|
237
|
-
if (latLng) {
|
|
238
|
-
lng = latLng.lng
|
|
239
|
-
lat = latLng.lat
|
|
240
|
-
console.log(`第${i}个顶点(Cartesian3转换): (${lng}, ${lat})`)
|
|
241
|
-
} else {
|
|
242
|
-
console.warn(`第${i}个顶点Cartesian3转换失败:`, pos)
|
|
243
|
-
continue
|
|
244
|
-
}
|
|
245
|
-
} else {
|
|
246
|
-
console.warn(`第${i}个顶点格式未知:`, pos)
|
|
247
|
-
continue
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
console.warn(`第${i}个顶点格式未知:`, pos)
|
|
251
|
-
continue
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 检查坐标是否有效
|
|
255
|
-
if (typeof lng !== 'number' || typeof lat !== 'number' || isNaN(lng) || isNaN(lat)) {
|
|
256
|
-
console.warn(`第${i}个顶点坐标无效:`, lng, lat)
|
|
257
|
-
continue
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 检查坐标范围是否合理(经纬度范围检查)
|
|
261
|
-
if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
|
|
262
|
-
console.warn(`第${i}个顶点坐标超出范围: (${lng}, ${lat}),可能是未转换的Cartesian3坐标`)
|
|
263
|
-
// 尝试当作Cartesian3转换
|
|
264
|
-
const latLng = cartesianToLngLat({ x: lng, y: lat, z: pos?.z || 0 }, map)
|
|
265
|
-
if (latLng) {
|
|
266
|
-
lng = latLng.lng
|
|
267
|
-
lat = latLng.lat
|
|
268
|
-
console.log(`转换后坐标: (${lng}, ${lat})`)
|
|
269
|
-
} else {
|
|
270
|
-
continue
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!isPointInIdentifyZone(lng, lat)) {
|
|
275
|
-
console.warn(`第${i}个顶点(${lng}, ${lat})不在识别区内`)
|
|
276
|
-
return false
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return true
|
|
280
|
-
}
|
|
31
|
+
const range = Math.max(sphere.radius * 2.8, 80)
|
|
32
|
+
const duration = options.flyDuration ?? 0.6
|
|
33
|
+
let settled = false
|
|
34
|
+
const done = () => {
|
|
35
|
+
if (settled) return
|
|
36
|
+
settled = true
|
|
37
|
+
resolve()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
map.camera.flyToBoundingSphere(sphere, {
|
|
41
|
+
duration,
|
|
42
|
+
offset: new Cesium.HeadingPitchRange(
|
|
43
|
+
0,
|
|
44
|
+
Cesium.Math.toRadians(options.pitch ?? -55),
|
|
45
|
+
range
|
|
46
|
+
),
|
|
47
|
+
complete: done,
|
|
48
|
+
cancel: done,
|
|
49
|
+
})
|
|
281
50
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
* @returns {string} 格式化的时间字符串,格式:YYYY-MM-DD HH:MM:SS
|
|
285
|
-
*/
|
|
286
|
-
const formatFenceTime = () => {
|
|
287
|
-
const pad = (n) => String(n).padStart(2, '0')
|
|
288
|
-
const d = new Date()
|
|
289
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
51
|
+
setTimeout(done, duration * 1000 + 400)
|
|
52
|
+
})
|
|
290
53
|
}
|
|
291
54
|
|
|
292
55
|
/**
|
|
293
|
-
*
|
|
294
|
-
* @param {string} dataUrl
|
|
295
|
-
* @param {{ maxWidth?: number, quality?: number }} [options]
|
|
56
|
+
* 等待地图完成一次真实绘制(postRender 后再截图,避免画布为空)
|
|
296
57
|
*/
|
|
297
|
-
|
|
298
|
-
new Promise((resolve) => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
img.onload = () => {
|
|
305
|
-
const scale = img.width > maxWidth ? maxWidth / img.width : 1
|
|
306
|
-
const w = Math.max(1, Math.round(img.width * scale))
|
|
307
|
-
const h = Math.max(1, Math.round(img.height * scale))
|
|
308
|
-
const canvas = document.createElement('canvas')
|
|
309
|
-
canvas.width = w
|
|
310
|
-
canvas.height = h
|
|
311
|
-
const ctx = canvas.getContext('2d')
|
|
312
|
-
if (!ctx) {
|
|
313
|
-
resolve(dataUrl)
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
ctx.drawImage(img, 0, 0, w, h)
|
|
317
|
-
try {
|
|
318
|
-
resolve(canvas.toDataURL('image/jpeg', quality))
|
|
319
|
-
} catch {
|
|
320
|
-
resolve(dataUrl)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
img.onerror = () => resolve(dataUrl)
|
|
324
|
-
img.src = dataUrl.startsWith('data:') ? dataUrl : `data:image/png;base64,${dataUrl}`
|
|
58
|
+
function waitScenePostRender(map) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const remover = map.scene.postRender.addEventListener(() => {
|
|
61
|
+
remover()
|
|
62
|
+
resolve()
|
|
63
|
+
})
|
|
64
|
+
map.scene.requestRender()
|
|
325
65
|
})
|
|
326
|
-
|
|
327
|
-
/** 是否围栏样式对象(含 opacity / outlineWidth) */
|
|
328
|
-
const isFenceStyleObject = (obj) =>
|
|
329
|
-
obj && typeof obj === 'object' && !Array.isArray(obj) && ('opacity' in obj || 'outlineWidth' in obj)
|
|
66
|
+
}
|
|
330
67
|
|
|
331
68
|
/**
|
|
332
|
-
*
|
|
69
|
+
* 计算围栏顶点在 canvas 绘图缓冲中的包围盒(像素)
|
|
333
70
|
*/
|
|
334
|
-
|
|
335
|
-
const
|
|
71
|
+
function computeFenceScreenBBox(map, positions) {
|
|
72
|
+
const scene = map.scene
|
|
73
|
+
let minX = Infinity
|
|
74
|
+
let minY = Infinity
|
|
75
|
+
let maxX = -Infinity
|
|
76
|
+
let maxY = -Infinity
|
|
77
|
+
let valid = 0
|
|
336
78
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
79
|
+
for (const p of positions) {
|
|
80
|
+
const cartesian = Cesium.Cartesian3.fromDegrees(
|
|
81
|
+
Number(p.lng),
|
|
82
|
+
Number(p.lat),
|
|
83
|
+
Number(p.height) || 0
|
|
340
84
|
)
|
|
85
|
+
const win = Cesium.SceneTransforms.worldToDrawingBufferCoordinates(
|
|
86
|
+
scene,
|
|
87
|
+
cartesian,
|
|
88
|
+
scratchCartesian2
|
|
89
|
+
)
|
|
90
|
+
if (!win || !Number.isFinite(win.x) || !Number.isFinite(win.y)) continue
|
|
91
|
+
|
|
92
|
+
minX = Math.min(minX, win.x)
|
|
93
|
+
minY = Math.min(minY, win.y)
|
|
94
|
+
maxX = Math.max(maxX, win.x)
|
|
95
|
+
maxY = Math.max(maxY, win.y)
|
|
96
|
+
valid++
|
|
341
97
|
}
|
|
342
98
|
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
return { ...dbData, base64 }
|
|
346
|
-
}
|
|
347
|
-
for (const key of Object.keys(dbData)) {
|
|
348
|
-
const val = dbData[key]
|
|
349
|
-
if (isFenceStyleObject(val)) {
|
|
350
|
-
return { ...dbData, [key]: { ...val, base64 } }
|
|
351
|
-
}
|
|
352
|
-
}
|
|
99
|
+
if (valid < 2 || !Number.isFinite(minX)) {
|
|
100
|
+
return null
|
|
353
101
|
}
|
|
354
102
|
|
|
355
|
-
return
|
|
103
|
+
return { minX, minY, maxX, maxY }
|
|
356
104
|
}
|
|
357
105
|
|
|
358
106
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* @param {Object} options - 配置选项
|
|
362
|
-
* @param {import('vue').Ref<Array>} options.fenceHistoryList - 围栏历史列表(响应式)
|
|
363
|
-
* @param {import('vue').Reactive<{ fence: boolean, identify: boolean }>} options.zoneEnabled - 防区开关状态(响应式)
|
|
364
|
-
* @param {number} [options.maxFenceCount=6] - 最大围栏数量限制
|
|
365
|
-
*
|
|
366
|
-
* @returns {Object} 包含地图操作方法和计算属性的对象
|
|
107
|
+
* 按包围盒裁剪 Cesium 画布并转为 PNG base64
|
|
367
108
|
*/
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
*/
|
|
377
|
-
const canDraw = computed(() => fenceHistoryList.value.length < maxFenceCount)
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* 是否可以绘制多边形围栏(电子围栏已开启且数量未达上限)
|
|
381
|
-
* @type {import('vue').ComputedRef<boolean>}
|
|
382
|
-
*/
|
|
383
|
-
const canDrawPolygon = computed(
|
|
384
|
-
() => zoneEnabled.fence && fenceHistoryList.value.length < maxFenceCount
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* 是否可以绘制圆形围栏(电子围栏已开启且数量未达上限)
|
|
389
|
-
* @type {import('vue').ComputedRef<boolean>}
|
|
390
|
-
*/
|
|
391
|
-
const canDrawCircle = computed(
|
|
392
|
-
() => zoneEnabled.fence && fenceHistoryList.value.length < maxFenceCount
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* 多边形绘制按钮的提示标题(根据状态显示不同提示)
|
|
397
|
-
* @type {import('vue').ComputedRef<string>}
|
|
398
|
-
*/
|
|
399
|
-
const polygonDrawTitle = computed(() => {
|
|
400
|
-
if (!zoneEnabled.fence) return MSG.openFenceFirst // 电子围栏未开启
|
|
401
|
-
if (fenceHistoryList.value.length >= maxFenceCount) return MSG.fenceListFull // 围栏数量已满
|
|
402
|
-
return '' // 可以绘制,无提示
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* 圆形绘制按钮的提示标题(根据状态显示不同提示)
|
|
407
|
-
* @type {import('vue').ComputedRef<string>}
|
|
408
|
-
*/
|
|
409
|
-
const circleDrawTitle = computed(() => {
|
|
410
|
-
if (!zoneEnabled.fence) return MSG.openFenceFirst // 电子围栏未开启
|
|
411
|
-
if (fenceHistoryList.value.length >= maxFenceCount) return MSG.fenceListFull // 围栏数量已满
|
|
412
|
-
return '' // 可以绘制,无提示
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* 地图加载完成回调函数
|
|
417
|
-
* 初始化地图时间、指挥所半球区域和地图中心点
|
|
418
|
-
*
|
|
419
|
-
* @param {Object} options - 地图加载完成参数
|
|
420
|
-
* @param {Object} options.map - Cesium地图实例
|
|
421
|
-
* @param {Object} options.center - 地图中心点信息(包含lng, lat)
|
|
422
|
-
*/
|
|
423
|
-
const mapOnLoad = (options) => {
|
|
424
|
-
const { map, center } = options
|
|
425
|
-
currentMapId = mapStore.getCurrentMapId() || DEFENSE_ZONE_MAP_ID
|
|
426
|
-
setBeiJingTime(currentMapId) // 设置地图时间为北京时间
|
|
427
|
-
|
|
428
|
-
// 调试信息:查看center对象的实际属性
|
|
429
|
-
console.log('mapOnLoad center:', center)
|
|
430
|
-
console.log('center属性名:', Object.keys(center))
|
|
431
|
-
|
|
432
|
-
// 处理center可能的不同属性名
|
|
433
|
-
const centerLng = center.lng || center.longitude || center.x || 0
|
|
434
|
-
const centerLat = center.lat || center.latitude || center.y || 0
|
|
435
|
-
|
|
436
|
-
// 获取识别区半径配置
|
|
437
|
-
const commandIdentificationRadius = window.BaseConfig?.commandHemisphereZone?.identificationRadius || 3000
|
|
438
|
-
|
|
439
|
-
// 更新识别区信息(用于后续电子围栏检测)
|
|
440
|
-
updateIdentifyZoneInfo(centerLng, centerLat, commandIdentificationRadius)
|
|
441
|
-
|
|
442
|
-
console.log('识别区信息已初始化:', identifyZoneInfo)
|
|
443
|
-
|
|
444
|
-
initCommandHemisphereZones({ mapId: currentMapId, lng: centerLng, lat: centerLat }) // 初始化指挥所半球
|
|
445
|
-
setCesiumCenter({ lng: centerLng, lat: centerLat, map }) // 设置地图中心点
|
|
446
|
-
|
|
447
|
-
loadFences()
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const loadFences = async () => {
|
|
451
|
-
try {
|
|
452
|
-
const response = await listElectronicFence()
|
|
453
|
-
const data = response?.rows || response || []
|
|
109
|
+
function cropCanvasToBase64(map, bbox, padding = 48) {
|
|
110
|
+
const source = map.scene.canvas
|
|
111
|
+
const x = Math.max(0, Math.floor(bbox.minX - padding))
|
|
112
|
+
const y = Math.max(0, Math.floor(bbox.minY - padding))
|
|
113
|
+
const right = Math.min(source.width, Math.ceil(bbox.maxX + padding))
|
|
114
|
+
const bottom = Math.min(source.height, Math.ceil(bbox.maxY + padding))
|
|
115
|
+
const width = Math.max(1, right - x)
|
|
116
|
+
const height = Math.max(1, bottom - y)
|
|
454
117
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
118
|
+
const target = document.createElement('canvas')
|
|
119
|
+
target.width = width
|
|
120
|
+
target.height = height
|
|
121
|
+
const ctx = target.getContext('2d')
|
|
122
|
+
if (!ctx) return null
|
|
458
123
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const color = fenceData.style?.color || '#E81224'
|
|
463
|
-
const opacity = fenceData.style?.opacity || 0.35
|
|
464
|
-
const outlineWidth = fenceData.style?.outlineWidth || 2
|
|
465
|
-
const height = fenceData.height || 2
|
|
466
|
-
|
|
467
|
-
if (fenceData.type === 'polygon') {
|
|
468
|
-
if (!fenceData.positions || fenceData.positions.length < 3) return
|
|
469
|
-
|
|
470
|
-
showPolygonFence({
|
|
471
|
-
id: fenceId,
|
|
472
|
-
mapId: currentMapId,
|
|
473
|
-
positions: fenceData.positions,
|
|
474
|
-
name: item.fenceName,
|
|
475
|
-
color,
|
|
476
|
-
opacity,
|
|
477
|
-
outlineWidth,
|
|
478
|
-
height,
|
|
479
|
-
})
|
|
480
|
-
} else if (fenceData.type === 'circle') {
|
|
481
|
-
if (!fenceData.center || typeof fenceData.center.lng !== 'number') return
|
|
124
|
+
ctx.drawImage(source, x, y, width, height, 0, 0, width, height)
|
|
125
|
+
return target.toDataURL('image/png')
|
|
126
|
+
}
|
|
482
127
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
} catch (error) {
|
|
500
|
-
console.error('加载电子围栏列表失败:', error)
|
|
501
|
-
}
|
|
128
|
+
/**
|
|
129
|
+
* 围栏绘制完成后截图:先定位围栏,再裁剪仅包含围栏的区域
|
|
130
|
+
*
|
|
131
|
+
* @param {import('cesium').Viewer} map Cesium Viewer
|
|
132
|
+
* @param {Array<{ lng: number, lat: number, height?: number }>} positions 围栏顶点(drawFence onFinish 的 result.positions)
|
|
133
|
+
* @param {Object} [options]
|
|
134
|
+
* @param {boolean} [options.flyTo=false] 截图前是否飞行定位到围栏
|
|
135
|
+
* @param {number} [options.padding=48] 裁剪边距(像素)
|
|
136
|
+
* @param {number} [options.flyDuration=0.6] 飞行动画时长(秒)
|
|
137
|
+
* @param {boolean} [options.printBase64=true] 是否在控制台打印 base64
|
|
138
|
+
* @returns {Promise<string|null>} PNG data URL(含 data:image/png;base64, 前缀)
|
|
139
|
+
*/
|
|
140
|
+
export async function captureFenceScreenshot(map, positions, options = {}) {
|
|
141
|
+
if (!map?.scene?.canvas) {
|
|
142
|
+
console.warn('[captureFenceScreenshot] 地图实例无效')
|
|
143
|
+
return null
|
|
502
144
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
* @param {boolean} visible - 是否可见
|
|
507
|
-
*/
|
|
508
|
-
const setIdentifyZoneVisible = (visible) => {
|
|
509
|
-
setVisibilityCommandHemisphereZones({ mapId: getMapId(), visible })
|
|
145
|
+
if (!Array.isArray(positions) || positions.length < 3) {
|
|
146
|
+
console.warn('[captureFenceScreenshot] 围栏顶点不足,无法截图')
|
|
147
|
+
return null
|
|
510
148
|
}
|
|
511
149
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
* 将公里值转换为米后调用更新方法
|
|
515
|
-
*
|
|
516
|
-
* @param {number|string} kmValue - 半径值(单位:公里)
|
|
517
|
-
*/
|
|
518
|
-
const updateIdentifyZoneRadius = (kmValue) => {
|
|
519
|
-
const radius = parseFloat(kmValue)
|
|
520
|
-
if (!Number.isFinite(radius)) return // 无效值则不更新
|
|
521
|
-
const radiusInMeters = radius * 1000
|
|
522
|
-
updateRadiusCommandHemisphereZones({ mapId: getMapId(), radius: radiusInMeters })
|
|
523
|
-
|
|
524
|
-
// 同步更新识别区信息(用于电子围栏检测)
|
|
525
|
-
const { center } = identifyZoneInfo
|
|
526
|
-
updateIdentifyZoneInfo(center[0], center[1], radiusInMeters)
|
|
527
|
-
}
|
|
150
|
+
const padding = options.padding ?? 48
|
|
151
|
+
const printBase64 = options.printBase64 !== false
|
|
528
152
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
* @param {Object} item - 围栏历史记录项
|
|
532
|
-
*/
|
|
533
|
-
const removeFenceFromMap = (item) => {
|
|
534
|
-
if (!item?.fenceId) return
|
|
535
|
-
|
|
536
|
-
if (item.type === 'polygon') {
|
|
537
|
-
destroyPolygonFenceNoLabel(item.fenceId, getMapId())
|
|
538
|
-
} else if (item.type === 'circle') {
|
|
539
|
-
destroyCircleFence(item.fenceId, getMapId())
|
|
540
|
-
}
|
|
153
|
+
if (options.flyTo === true) {
|
|
154
|
+
await flyCameraToFence(map, positions, options)
|
|
541
155
|
}
|
|
542
156
|
|
|
543
|
-
|
|
544
|
-
* 切换围栏的显隐状态
|
|
545
|
-
* @param {Object} item - 围栏历史记录项
|
|
546
|
-
* @param {boolean} visible - 是否可见
|
|
547
|
-
*/
|
|
548
|
-
const toggleFenceVisibility = (item, visible) => {
|
|
549
|
-
const mapId = getMapId()
|
|
550
|
-
|
|
551
|
-
if (!item?.fenceId || !item.pointParam) return
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
const fenceData = JSON.parse(item.pointParam)
|
|
555
|
-
|
|
556
|
-
if (fenceData.type === 'polygon') {
|
|
557
|
-
if (!fenceData.positions || fenceData.positions.length < 3) return
|
|
558
|
-
|
|
559
|
-
if (visible) {
|
|
560
|
-
const fenceId = fenceData.id || `fence_${item.id}`
|
|
561
|
-
const color = fenceData.style?.color || '#E81224'
|
|
562
|
-
const opacity = fenceData.style?.opacity || 0.35
|
|
563
|
-
const outlineWidth = fenceData.style?.outlineWidth || 2
|
|
564
|
-
const height = fenceData.height || 2
|
|
157
|
+
await waitScenePostRender(map)
|
|
565
158
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
name: item.name,
|
|
571
|
-
color,
|
|
572
|
-
opacity,
|
|
573
|
-
outlineWidth,
|
|
574
|
-
height,
|
|
575
|
-
})
|
|
576
|
-
} else {
|
|
577
|
-
const fenceId = fenceData.id || `fence_${item.id}`
|
|
578
|
-
destroyPolygonFence(fenceId, mapId)
|
|
579
|
-
}
|
|
580
|
-
} else if (fenceData.type === 'circle') {
|
|
581
|
-
if (!fenceData.center || typeof fenceData.center.lng !== 'number') return
|
|
582
|
-
|
|
583
|
-
if (visible) {
|
|
584
|
-
const fenceId = fenceData.id || `fence_${item.id}`
|
|
585
|
-
const color = fenceData.style?.color || '#E81224'
|
|
586
|
-
const opacity = fenceData.style?.opacity || 0.35
|
|
587
|
-
const outlineWidth = fenceData.style?.outlineWidth || 2
|
|
588
|
-
const height = fenceData.height || 2
|
|
589
|
-
|
|
590
|
-
showCircleFence({
|
|
591
|
-
id: fenceId,
|
|
592
|
-
mapId: mapId,
|
|
593
|
-
center: fenceData.center,
|
|
594
|
-
radius: fenceData.radius,
|
|
595
|
-
name: item.name,
|
|
596
|
-
color,
|
|
597
|
-
opacity,
|
|
598
|
-
outlineWidth,
|
|
599
|
-
height,
|
|
600
|
-
})
|
|
601
|
-
} else {
|
|
602
|
-
const fenceId = fenceData.id || `fence_${item.id}`
|
|
603
|
-
destroyCircleFence(fenceId, mapId)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
} catch (error) {
|
|
607
|
-
console.error('切换围栏显隐失败:', error)
|
|
608
|
-
}
|
|
159
|
+
const bbox = computeFenceScreenBBox(map, positions)
|
|
160
|
+
if (!bbox) {
|
|
161
|
+
console.warn('[captureFenceScreenshot] 围栏不在可视区域,截图失败')
|
|
162
|
+
return null
|
|
609
163
|
}
|
|
610
164
|
|
|
611
|
-
|
|
612
|
-
* 开始绘制多边形电子围栏
|
|
613
|
-
* 处理绘制状态检查、创建绘制控制器、绑定回调函数
|
|
614
|
-
*/
|
|
615
|
-
const drawPolygonWall = () => {
|
|
616
|
-
// 检查是否可以绘制
|
|
617
|
-
if (!canDrawPolygon.value) return
|
|
618
|
-
|
|
619
|
-
const mapId = getMapId()
|
|
620
|
-
|
|
621
|
-
// 检查地图是否已加载
|
|
622
|
-
if (!mapStore.getMap(mapId)) {
|
|
623
|
-
ElMessage.warning(MSG.mapNotReady)
|
|
624
|
-
return
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// 检查是否正在绘制中
|
|
628
|
-
if (polygonDrawController) {
|
|
629
|
-
ElMessage.info(MSG.polygonDrawingInProgress)
|
|
630
|
-
return
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// 如果正在绘制圆形,先取消
|
|
634
|
-
if (circleDrawController) {
|
|
635
|
-
circleDrawController.cancel()
|
|
636
|
-
circleDrawController = null
|
|
637
|
-
}
|
|
165
|
+
await waitScenePostRender(map)
|
|
638
166
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
// 重置绘制状态
|
|
645
|
-
polygonDrawingTipShown = false
|
|
646
|
-
|
|
647
|
-
// 创建绘制控制器
|
|
648
|
-
polygonDrawController = drawPolygonFenceNoLabel({
|
|
649
|
-
mapId,
|
|
650
|
-
id: polygonDrawId,
|
|
651
|
-
name,
|
|
652
|
-
color: fenceColor,
|
|
653
|
-
|
|
654
|
-
/** 绘制完成回调 */
|
|
655
|
-
onFinish: async (result) => {
|
|
656
|
-
polygonDrawController = null
|
|
657
|
-
polygonDrawingTipShown = false
|
|
658
|
-
|
|
659
|
-
// 调试信息:查看识别区信息和positions格式
|
|
660
|
-
console.log('识别区信息:', identifyZoneInfo)
|
|
661
|
-
console.log('positions:', result?.positions)
|
|
662
|
-
console.log('positions类型:', Array.isArray(result?.positions))
|
|
663
|
-
|
|
664
|
-
// 如果识别区未启用或未正确初始化,直接跳过检测
|
|
665
|
-
if (!identifyZoneInfo.center ||
|
|
666
|
-
identifyZoneInfo.center.length < 2 ||
|
|
667
|
-
identifyZoneInfo.center[0] === 0 ||
|
|
668
|
-
identifyZoneInfo.center[1] === 0 ||
|
|
669
|
-
!identifyZoneInfo.radius ||
|
|
670
|
-
identifyZoneInfo.radius <= 0) {
|
|
671
|
-
console.log('识别区未启用或未正确初始化,跳过检测')
|
|
672
|
-
} else {
|
|
673
|
-
// 检测电子围栏是否在识别区内
|
|
674
|
-
if (!isPolygonInIdentifyZone(result?.positions)) {
|
|
675
|
-
// 销毁已绘制的围栏
|
|
676
|
-
destroyPolygonFenceNoLabel(polygonDrawId, mapId)
|
|
677
|
-
ElMessage.warning(MSG.fenceOutsideIdentifyZone)
|
|
678
|
-
return
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const map = mapStore.getMap(mapId)
|
|
683
|
-
let thumbnail = null
|
|
684
|
-
if (map && result?.positions?.length >= 3) {
|
|
685
|
-
await nextTick()
|
|
686
|
-
await new Promise((resolve) => requestAnimationFrame(() => resolve()))
|
|
687
|
-
await new Promise((resolve) => requestAnimationFrame(() => resolve()))
|
|
688
|
-
|
|
689
|
-
const rawThumbnail = await captureFenceScreenshot(map, result.positions, {
|
|
690
|
-
printBase64: true,
|
|
691
|
-
flyTo: false,
|
|
692
|
-
})
|
|
693
|
-
if (rawThumbnail) {
|
|
694
|
-
thumbnail = await compressBase64Image(rawThumbnail)
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
pendingPolygonFence.value = {
|
|
699
|
-
polygonDrawId,
|
|
700
|
-
name,
|
|
701
|
-
thumbnail,
|
|
702
|
-
dbData: result?.dbData,
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
ElMessage.success(`\u300C${name}\u300D${MSG.drawDone}`)
|
|
706
|
-
},
|
|
707
|
-
|
|
708
|
-
/** 绘制过程中回调(显示提示) */
|
|
709
|
-
onChange: () => {
|
|
710
|
-
if (!polygonDrawingTipShown) {
|
|
711
|
-
polygonDrawingTipShown = true
|
|
712
|
-
ElMessage.info(MSG.polygonDrawingHint)
|
|
713
|
-
}
|
|
714
|
-
},
|
|
715
|
-
|
|
716
|
-
/** 取消绘制回调 */
|
|
717
|
-
onCancel: () => {
|
|
718
|
-
polygonDrawController = null
|
|
719
|
-
polygonDrawingTipShown = false
|
|
720
|
-
ElMessage.info(MSG.polygonDrawCancelled)
|
|
721
|
-
},
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
// 检查绘制控制器是否创建成功
|
|
725
|
-
if (!polygonDrawController) {
|
|
726
|
-
ElMessage.error(MSG.cannotStartPolygonDraw)
|
|
727
|
-
}
|
|
167
|
+
const base64 = cropCanvasToBase64(map, bbox, padding)
|
|
168
|
+
if (!base64) {
|
|
169
|
+
console.warn('[captureFenceScreenshot] 裁剪画布失败')
|
|
170
|
+
return null
|
|
728
171
|
}
|
|
729
172
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
* 处理绘制状态检查、创建绘制控制器、绑定回调函数
|
|
733
|
-
*/
|
|
734
|
-
const drawCircleWall = () => {
|
|
735
|
-
// 检查是否可以绘制
|
|
736
|
-
if (!canDrawCircle.value) return
|
|
737
|
-
|
|
738
|
-
const mapId = getMapId()
|
|
739
|
-
|
|
740
|
-
// 检查地图是否已加载
|
|
741
|
-
if (!mapStore.getMap(mapId)) {
|
|
742
|
-
ElMessage.warning(MSG.mapNotReady)
|
|
743
|
-
return
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// 检查是否正在绘制中
|
|
747
|
-
if (circleDrawController) {
|
|
748
|
-
ElMessage.info(MSG.circleDrawingInProgress)
|
|
749
|
-
return
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// 如果正在绘制多边形,先取消
|
|
753
|
-
if (polygonDrawController) {
|
|
754
|
-
polygonDrawController.cancel()
|
|
755
|
-
polygonDrawController = null
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// 生成唯一的围栏ID和名称
|
|
759
|
-
const circleDrawId = `circle_fence_${Date.now()}`
|
|
760
|
-
const name = `${MSG.circleFencePrefix}${fenceHistoryList.value.length + 1}`
|
|
761
|
-
const fenceColor = BaseConfig.fenceColor || '#E81224'
|
|
762
|
-
|
|
763
|
-
// 重置绘制状态
|
|
764
|
-
circleDrawingTipShown = false
|
|
765
|
-
|
|
766
|
-
// 创建绘制控制器
|
|
767
|
-
circleDrawController = drawCircleFence({
|
|
768
|
-
mapId,
|
|
769
|
-
id: circleDrawId,
|
|
770
|
-
name,
|
|
771
|
-
color: fenceColor,
|
|
772
|
-
|
|
773
|
-
/** 绘制完成回调 */
|
|
774
|
-
onFinish: async (result) => {
|
|
775
|
-
circleDrawController = null
|
|
776
|
-
circleDrawingTipShown = false
|
|
777
|
-
|
|
778
|
-
// 调试信息:查看识别区信息
|
|
779
|
-
console.log('识别区信息:', identifyZoneInfo)
|
|
780
|
-
console.log('circle center:', result?.center)
|
|
781
|
-
console.log('circle radius:', result?.radius)
|
|
782
|
-
|
|
783
|
-
// 如果识别区未启用或未正确初始化,直接跳过检测
|
|
784
|
-
if (!identifyZoneInfo.center ||
|
|
785
|
-
identifyZoneInfo.center.length < 2 ||
|
|
786
|
-
identifyZoneInfo.center[0] === 0 ||
|
|
787
|
-
identifyZoneInfo.center[1] === 0 ||
|
|
788
|
-
!identifyZoneInfo.radius ||
|
|
789
|
-
identifyZoneInfo.radius <= 0) {
|
|
790
|
-
console.log('识别区未启用或未正确初始化,跳过检测')
|
|
791
|
-
} else {
|
|
792
|
-
// 检测电子围栏圆心是否在识别区内
|
|
793
|
-
// 圆形围栏的圆心必须在识别区内,且半径不能超过到识别区边界的距离
|
|
794
|
-
const center = result?.center
|
|
795
|
-
if (center) {
|
|
796
|
-
const isCenterInside = isPointInIdentifyZone(center.lng, center.lat)
|
|
797
|
-
if (!isCenterInside) {
|
|
798
|
-
// 销毁已绘制的围栏
|
|
799
|
-
destroyCircleFence(circleDrawId, mapId)
|
|
800
|
-
ElMessage.warning(MSG.fenceOutsideIdentifyZone)
|
|
801
|
-
return
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// 检查圆上最远点是否在识别区内(圆心到识别区边界的距离 >= 圆半径)
|
|
805
|
-
const centerDistance = getDistanceToIdentifyZoneCenter(center.lng, center.lat)
|
|
806
|
-
const maxAllowedRadius = identifyZoneInfo.radius - centerDistance
|
|
807
|
-
if (result.radius > maxAllowedRadius) {
|
|
808
|
-
// 销毁已绘制的围栏
|
|
809
|
-
destroyCircleFence(circleDrawId, mapId)
|
|
810
|
-
ElMessage.warning(MSG.fenceOutsideIdentifyZone)
|
|
811
|
-
return
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
pendingCircleFence.value = {
|
|
817
|
-
circleDrawId,
|
|
818
|
-
name,
|
|
819
|
-
thumbnail: null,
|
|
820
|
-
dbData: result?.dbData,
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
ElMessage.success(`\u300C${name}\u300D${MSG.drawDone}`)
|
|
824
|
-
},
|
|
825
|
-
|
|
826
|
-
/** 绘制过程中回调(显示提示) */
|
|
827
|
-
onChange: () => {
|
|
828
|
-
if (!circleDrawingTipShown) {
|
|
829
|
-
circleDrawingTipShown = true
|
|
830
|
-
ElMessage.info(MSG.circleDrawingHint)
|
|
831
|
-
}
|
|
832
|
-
},
|
|
833
|
-
|
|
834
|
-
/** 取消绘制回调 */
|
|
835
|
-
onCancel: () => {
|
|
836
|
-
circleDrawController = null
|
|
837
|
-
circleDrawingTipShown = false
|
|
838
|
-
ElMessage.info(MSG.circleDrawCancelled)
|
|
839
|
-
},
|
|
840
|
-
})
|
|
841
|
-
|
|
842
|
-
// 检查绘制控制器是否创建成功
|
|
843
|
-
if (!circleDrawController) {
|
|
844
|
-
ElMessage.error(MSG.cannotStartCircleDraw)
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* 计算点到识别区中心点的距离(米)
|
|
850
|
-
* @param {number} lng - 点的经度
|
|
851
|
-
* @param {number} lat - 点的纬度
|
|
852
|
-
* @returns {number} - 距离(米)
|
|
853
|
-
*/
|
|
854
|
-
const getDistanceToIdentifyZoneCenter = (lng, lat) => {
|
|
855
|
-
const { center, radius } = identifyZoneInfo
|
|
856
|
-
if (!center || center.length < 2) return 0
|
|
857
|
-
|
|
858
|
-
const R = 6371000
|
|
859
|
-
const dLat = (lat - center[1]) * Math.PI / 180
|
|
860
|
-
const dLng = (lng - center[0]) * Math.PI / 180
|
|
861
|
-
const a =
|
|
862
|
-
Math.sin(dLat/2) * Math.sin(dLat/2) +
|
|
863
|
-
Math.cos(center[1] * Math.PI / 180) * Math.cos(lat * Math.PI / 180) *
|
|
864
|
-
Math.sin(dLng/2) * Math.sin(dLng/2)
|
|
865
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
|
|
866
|
-
return R * c
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
/**
|
|
870
|
-
* 取消正在进行的绘制(多边形或圆形)
|
|
871
|
-
*/
|
|
872
|
-
const cancelDraw = () => {
|
|
873
|
-
if (polygonDrawController?.cancel) {
|
|
874
|
-
polygonDrawController.cancel()
|
|
875
|
-
polygonDrawController = null
|
|
876
|
-
polygonDrawingTipShown = false
|
|
877
|
-
ElMessage.info(MSG.userCancelled)
|
|
878
|
-
} else if (circleDrawController?.cancel) {
|
|
879
|
-
circleDrawController.cancel()
|
|
880
|
-
circleDrawController = null
|
|
881
|
-
circleDrawingTipShown = false
|
|
882
|
-
ElMessage.info(MSG.userCancelled)
|
|
883
|
-
} else if (pendingPolygonFence.value) {
|
|
884
|
-
const mapId = getMapId()
|
|
885
|
-
const polygonDrawId = pendingPolygonFence.value.polygonDrawId
|
|
886
|
-
if (polygonDrawId) {
|
|
887
|
-
destroyPolygonFenceNoLabel(polygonDrawId, mapId)
|
|
888
|
-
}
|
|
889
|
-
pendingPolygonFence.value = null
|
|
890
|
-
polygonDrawingTipShown = false
|
|
891
|
-
ElMessage.info(MSG.userCancelled)
|
|
892
|
-
} else if (pendingCircleFence.value) {
|
|
893
|
-
const mapId = getMapId()
|
|
894
|
-
const circleDrawId = pendingCircleFence.value.circleDrawId
|
|
895
|
-
if (circleDrawId) {
|
|
896
|
-
destroyCircleFence(circleDrawId, mapId)
|
|
897
|
-
}
|
|
898
|
-
pendingCircleFence.value = null
|
|
899
|
-
circleDrawingTipShown = false
|
|
900
|
-
ElMessage.info(MSG.userCancelled)
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* 保存待提交的多边形围栏:写入历史列表并调用后端接口(由页面「保存」按钮触发)
|
|
906
|
-
* @param {Function} saveToServer - (payload) => Promise,由页面注入 addElectronicFence 等
|
|
907
|
-
*/
|
|
908
|
-
const savePolygonFence = async (saveToServer) => {
|
|
909
|
-
const pending = pendingPolygonFence.value
|
|
910
|
-
if (!pending) {
|
|
911
|
-
ElMessage.warning('\u8BF7\u5148\u5B8C\u6210\u591A\u8FB9\u5F62\u56F4\u680F\u7ED8\u5236')
|
|
912
|
-
return
|
|
913
|
-
}
|
|
914
|
-
if (!pending.dbData) {
|
|
915
|
-
ElMessage.error('\u56F4\u680F\u6570\u636E\u65E0\u6548\uFF0C\u65E0\u6CD5\u4FDD\u5B58')
|
|
916
|
-
return
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
fenceHistoryList.value.push({
|
|
920
|
-
id: Date.now(),
|
|
921
|
-
time: formatFenceTime(),
|
|
922
|
-
fenceId: pending.polygonDrawId,
|
|
923
|
-
name: pending.name,
|
|
924
|
-
type: 'polygon',
|
|
925
|
-
thumbnail: pending.thumbnail,
|
|
926
|
-
})
|
|
927
|
-
|
|
928
|
-
const pointParamData = embedBase64InDbData(pending.dbData, pending.thumbnail)
|
|
929
|
-
|
|
930
|
-
console.log(pointParamData)
|
|
931
|
-
|
|
932
|
-
try {
|
|
933
|
-
const response = await saveToServer({
|
|
934
|
-
fenceName: pending.name,
|
|
935
|
-
pointParam: JSON.stringify(pointParamData),
|
|
936
|
-
isEnable: 1,
|
|
937
|
-
})
|
|
938
|
-
if (response?.code === 200) {
|
|
939
|
-
ElMessage.success('\u7535\u5B50\u56F4\u680F\u4FDD\u5B58\u6210\u529F')
|
|
940
|
-
pendingPolygonFence.value = null
|
|
941
|
-
} else {
|
|
942
|
-
ElMessage.error(response?.msg || '\u7535\u5B50\u56F4\u680F\u4FDD\u5B58\u5931\u8D25')
|
|
943
|
-
}
|
|
944
|
-
} catch {
|
|
945
|
-
ElMessage.error('\u7535\u5B50\u56F4\u680F\u4FDD\u5B58\u5931\u8D25')
|
|
946
|
-
}
|
|
173
|
+
if (printBase64) {
|
|
174
|
+
console.log('[围栏截图 base64]', base64)
|
|
947
175
|
}
|
|
948
176
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
* @param {Function} saveToServer - (payload) => Promise,由页面注入 addElectronicFence 等
|
|
952
|
-
*/
|
|
953
|
-
const saveCircleFence = async (saveToServer) => {
|
|
954
|
-
const pending = pendingCircleFence.value
|
|
955
|
-
if (!pending) {
|
|
956
|
-
ElMessage.warning('\u8BF7\u5148\u5B8C\u6210\u5706\u5F62\u56F4\u680F\u7ED8\u5236')
|
|
957
|
-
return
|
|
958
|
-
}
|
|
959
|
-
if (!pending.dbData) {
|
|
960
|
-
ElMessage.error('\u56F4\u680F\u6570\u636E\u65E0\u6548\uFF0C\u65E0\u6CD5\u4FDD\u5B58')
|
|
961
|
-
return
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
fenceHistoryList.value.push({
|
|
965
|
-
id: Date.now(),
|
|
966
|
-
time: formatFenceTime(),
|
|
967
|
-
fenceId: pending.circleDrawId,
|
|
968
|
-
name: pending.name,
|
|
969
|
-
type: 'circle',
|
|
970
|
-
thumbnail: pending.thumbnail,
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
const pointParamData = embedBase64InDbData(pending.dbData, pending.thumbnail)
|
|
974
|
-
|
|
975
|
-
console.log(pointParamData)
|
|
976
|
-
|
|
977
|
-
try {
|
|
978
|
-
const response = await saveToServer({
|
|
979
|
-
fenceName: pending.name,
|
|
980
|
-
pointParam: JSON.stringify(pointParamData),
|
|
981
|
-
isEnable: 1,
|
|
982
|
-
})
|
|
983
|
-
if (response?.code === 200) {
|
|
984
|
-
ElMessage.success('\u7535\u5B50\u56F4\u680F\u4FDD\u5B58\u6210\u529F')
|
|
985
|
-
pendingCircleFence.value = null
|
|
986
|
-
} else {
|
|
987
|
-
ElMessage.error(response?.msg || '\u7535\u5B50\u56F4\u680F\u4FDD\u5B58\u5931\u8D25')
|
|
988
|
-
}
|
|
989
|
-
} catch {
|
|
990
|
-
ElMessage.error('\u7535\u5B50\u56F4\u680F\u4FDD\u5B58\u5931\u8D25')
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* 保存围栏(根据类型自动选择保存方法)
|
|
996
|
-
*/
|
|
997
|
-
const saveFence = async (saveToServer) => {
|
|
998
|
-
if (pendingPolygonFence.value) {
|
|
999
|
-
await savePolygonFence(saveToServer)
|
|
1000
|
-
} else if (pendingCircleFence.value) {
|
|
1001
|
-
await saveCircleFence(saveToServer)
|
|
1002
|
-
} else {
|
|
1003
|
-
ElMessage.warning('\u8BF7\u5148\u5B8C\u6210\u56F4\u680F\u7ED8\u5236')
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* 取消正在进行的多边形绘制(保留旧方法名以保持兼容性)
|
|
1009
|
-
*/
|
|
1010
|
-
const cancelPolygonDraw = () => {
|
|
1011
|
-
cancelDraw()
|
|
1012
|
-
}
|
|
177
|
+
return base64
|
|
178
|
+
}
|
|
1013
179
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
canDrawCircle, // 是否可绘制圆形(计算属性)
|
|
1024
|
-
polygonDrawTitle, // 多边形绘制按钮提示(计算属性)
|
|
1025
|
-
circleDrawTitle, // 圆形绘制按钮提示(计算属性)
|
|
1026
|
-
drawPolygonWall, // 绘制多边形围栏
|
|
1027
|
-
drawCircleWall, // 绘制圆形围栏
|
|
1028
|
-
cancelDraw, // 取消绘制(通用)
|
|
1029
|
-
cancelPolygonDraw, // 取消绘制(兼容旧接口)
|
|
1030
|
-
pendingPolygonFence, // 待保存的多边形围栏绘制结果
|
|
1031
|
-
pendingCircleFence, // 待保存的圆形围栏绘制结果
|
|
1032
|
-
savePolygonFence, // 保存多边形围栏(历史 + 后端)
|
|
1033
|
-
saveCircleFence, // 保存圆形围栏(历史 + 后端)
|
|
1034
|
-
saveFence, // 保存围栏(自动检测类型)
|
|
1035
|
-
removeFenceFromMap, // 从地图移除围栏
|
|
1036
|
-
toggleFenceVisibility, // 切换围栏显隐
|
|
1037
|
-
setIdentifyZoneVisible, // 设置识别区可见性
|
|
1038
|
-
updateIdentifyZoneRadius, // 更新识别区半径
|
|
1039
|
-
captureFenceScreenshot, // 围栏区域截图(base64)
|
|
180
|
+
/**
|
|
181
|
+
* 根据 mapId 截图(便于在 onFinish 回调里直接调用)
|
|
182
|
+
*/
|
|
183
|
+
export async function captureFenceScreenshotByMapId(mapId, positions, options = {}) {
|
|
184
|
+
const mapStore = useMapStore()
|
|
185
|
+
const map = mapStore.getMap(mapId)
|
|
186
|
+
if (!map) {
|
|
187
|
+
console.warn('[captureFenceScreenshot] 地图未加载, mapId:', mapId)
|
|
188
|
+
return null
|
|
1040
189
|
}
|
|
190
|
+
return captureFenceScreenshot(map, positions, options)
|
|
1041
191
|
}
|