huweili-cesium 1.2.12 → 1.2.14

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/index.js CHANGED
@@ -8,6 +8,7 @@ export { getClickHandler, openDroneDetailById, closeDroneDetailById, bindDroneLa
8
8
  export { createCustomToolbarButtons } from './customToolbarButtons.js'
9
9
  export { drawFence } from './drawFence.js'
10
10
  export { drawFenceNew } from './drawFenceNew.js'
11
+ export { measureDistance } from './measureDistance.js'
11
12
  export { createDroneRippleDom } from './droneRipple.js'
12
13
  export { geometryConfig } from './geometry.js'
13
14
  export { groundLinkConfig } from './groundLink.js'
@@ -0,0 +1,413 @@
1
+ /**
2
+ * 两点地面直线测距模块
3
+ *
4
+ * 点击地图上第一个点作为起点,再点击第二个点作为终点,测量两点之间的地面直线距离。
5
+ * 无论地图处于 2D 还是 3D,采样点都会被转换为贴地坐标(height = 0),不携带高度。
6
+ *
7
+ * @author huweili
8
+ * @email czxyhuweili@163.com
9
+ * @version 1.0.0
10
+ */
11
+ import * as Cesium from 'cesium'
12
+ import { useMapStore } from './stores/mapStore.js'
13
+
14
+ export function measureDistance() {
15
+ const mapStore = useMapStore()
16
+
17
+ const formatDistance = (distance) => {
18
+ if (!Number.isFinite(distance)) return '0 m'
19
+ if (distance >= 1000) return `${(distance / 1000).toFixed(2)} km`
20
+ return `${distance.toFixed(2)} m`
21
+ }
22
+
23
+ const createMouseTipEntity = (map, options) => {
24
+ return map.entities.add({
25
+ id: options.id,
26
+ position: new Cesium.CallbackProperty(() => options.getPosition(), false),
27
+ label: {
28
+ text: new Cesium.CallbackProperty(() => options.getText(), false),
29
+ font: options.font || '10px Microsoft YaHei',
30
+ fillColor: options.fillColor || Cesium.Color.WHITE,
31
+ showBackground: true,
32
+ backgroundColor: Cesium.Color.fromCssColorString(options.backgroundColor || 'rgba(0, 0, 0, 0.6)'),
33
+ backgroundPadding: options.backgroundPadding || new Cesium.Cartesian2(10, 6),
34
+ verticalOrigin: options.verticalOrigin || Cesium.VerticalOrigin.BOTTOM,
35
+ horizontalOrigin: options.horizontalOrigin || Cesium.HorizontalOrigin.LEFT,
36
+ pixelOffset: options.pixelOffset || new Cesium.Cartesian2(16, -16),
37
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
38
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, options.maxDistance || Number.POSITIVE_INFINITY)
39
+ }
40
+ })
41
+ }
42
+
43
+ /**
44
+ * 从屏幕坐标拾取地面点,并强制转换为 height = 0 的地面坐标。
45
+ * @param {Cesium.Viewer} map Cesium Viewer 实例
46
+ * @param {Cesium.Cartesian2} screenPosition 屏幕坐标
47
+ * @returns {Cesium.Cartesian3|null}
48
+ */
49
+ const getGroundCartesianFromScreen = (map, screenPosition) => {
50
+ if (!screenPosition) return null
51
+
52
+ const scene = map.scene
53
+ let cartesian = null
54
+
55
+ if (scene.pickPositionSupported) {
56
+ cartesian = scene.pickPosition(screenPosition)
57
+ }
58
+
59
+ if (!Cesium.defined(cartesian)) {
60
+ const ray = map.camera.getPickRay(screenPosition)
61
+ if (!ray) return null
62
+ cartesian = scene.globe.pick(ray, scene)
63
+ }
64
+
65
+ if (!Cesium.defined(cartesian)) return null
66
+
67
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
68
+ return Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0)
69
+ }
70
+
71
+ const cartesianToLngLatHeight = (cartesian) => {
72
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
73
+ return {
74
+ lng: Cesium.Math.toDegrees(cartographic.longitude),
75
+ lat: Cesium.Math.toDegrees(cartographic.latitude),
76
+ height: 0
77
+ }
78
+ }
79
+
80
+ const createGroundPolylinePositions = (startPosition, endPosition, segmentCount = 32) => {
81
+ if (!startPosition || !endPosition) return []
82
+
83
+ const start = Cesium.Cartographic.fromCartesian(startPosition)
84
+ const end = Cesium.Cartographic.fromCartesian(endPosition)
85
+ const positions = []
86
+
87
+ for (let i = 0; i <= segmentCount; i += 1) {
88
+ const t = i / segmentCount
89
+ positions.push(
90
+ Cesium.Cartesian3.fromRadians(
91
+ Cesium.Math.lerp(start.longitude, end.longitude, t),
92
+ Cesium.Math.lerp(start.latitude, end.latitude, t),
93
+ 0
94
+ )
95
+ )
96
+ }
97
+
98
+ return positions
99
+ }
100
+
101
+ const getGroundDistance = (startPosition, endPosition) => {
102
+ if (!startPosition || !endPosition) return 0
103
+ return Cesium.Cartesian3.distance(startPosition, endPosition)
104
+ }
105
+
106
+ /**
107
+ * 开始两点地面直线测距
108
+ * @param {Object} options 配置项
109
+ * @param {string} [options.mapId] 地图实例 ID
110
+ * @param {string} options.id 测距图形唯一标识
111
+ * @param {string} [options.color='#00ffff'] 线和点颜色
112
+ * @param {number} [options.width=3] 线宽
113
+ * @param {(result: Object) => void} [options.onFinish] 测距完成回调
114
+ * @param {(result: Object) => void} [options.onChange] 测距过程回调
115
+ * @param {() => void} [options.onCancel] 取消回调
116
+ * @returns {Object|null} 测距控制器
117
+ */
118
+ const startMeasure = (options = {}) => {
119
+ const map = mapStore.getMap(options.mapId)
120
+ if (!map) {
121
+ console.error('地图实例不存在')
122
+ return null
123
+ }
124
+
125
+ if (!options.id) {
126
+ console.error('测距时必须提供 id')
127
+ return null
128
+ }
129
+
130
+ if (mapStore.hasGraphicMap(options.id, options.mapId)) {
131
+ console.warn(`id: ${options.id} 测距结果已存在`)
132
+ return null
133
+ }
134
+
135
+ const color = Cesium.Color.fromCssColorString(options.color || '#00ffff')
136
+ const width = options.width ?? 3
137
+ const clampToGround = options.clampToGround ?? true
138
+ const lineMaterial =
139
+ options.lineMaterial ||
140
+ new Cesium.PolylineGlowMaterialProperty({
141
+ color: Cesium.Color.fromAlpha(color, options.opacity ?? 0.95),
142
+ glowPower: options.glowPower ?? 0.12,
143
+ taperPower: options.taperPower ?? 0
144
+ })
145
+
146
+ let startPosition = null
147
+ let endPosition = null
148
+ let floatingPosition = null
149
+ let cursorPosition = null
150
+ let startPointEntity = null
151
+ let endPointEntity = null
152
+ let lineEntity = null
153
+ let distanceLabelEntity = null
154
+ let tipEntity = null
155
+ let handler = null
156
+ let isFinished = false
157
+
158
+ const tempIds = {
159
+ startPoint: `${options.id}_measure_start_point`,
160
+ endPoint: `${options.id}_measure_end_point`,
161
+ line: `${options.id}_measure_line`,
162
+ label: `${options.id}_measure_label`,
163
+ tip: `${options.id}_measure_tip`
164
+ }
165
+
166
+ const getLinePositions = () => {
167
+ if (!startPosition) return []
168
+ const targetPosition = endPosition || floatingPosition
169
+ if (!targetPosition) return [startPosition]
170
+ return createGroundPolylinePositions(startPosition, targetPosition, options.segmentCount ?? 48)
171
+ }
172
+
173
+ const getCurrentDistance = () => getGroundDistance(startPosition, endPosition || floatingPosition)
174
+
175
+ const getMidPosition = () => {
176
+ const targetPosition = endPosition || floatingPosition
177
+ if (!startPosition || !targetPosition) return startPosition || targetPosition || cursorPosition || null
178
+ const startCartographic = Cesium.Cartographic.fromCartesian(startPosition)
179
+ const endCartographic = Cesium.Cartographic.fromCartesian(targetPosition)
180
+ return Cesium.Cartesian3.fromRadians(
181
+ (startCartographic.longitude + endCartographic.longitude) / 2,
182
+ (startCartographic.latitude + endCartographic.latitude) / 2,
183
+ 0
184
+ )
185
+ }
186
+
187
+ const getTipText = () => {
188
+ if (!startPosition) return '点击确定起点'
189
+ if (!endPosition) return `点击确定终点,当前距离:${formatDistance(getCurrentDistance())}`
190
+ return `测距完成:${formatDistance(getCurrentDistance())}`
191
+ }
192
+
193
+ const buildResult = () => ({
194
+ id: options.id,
195
+ start: startPosition ? cartesianToLngLatHeight(startPosition) : null,
196
+ end: endPosition ? cartesianToLngLatHeight(endPosition) : null,
197
+ distance: getGroundDistance(startPosition, endPosition),
198
+ distanceText: formatDistance(getGroundDistance(startPosition, endPosition))
199
+ })
200
+
201
+ const emitChange = () => {
202
+ if (typeof options.onChange !== 'function' || !startPosition) return
203
+ options.onChange({
204
+ id: options.id,
205
+ start: cartesianToLngLatHeight(startPosition),
206
+ end: (endPosition || floatingPosition) ? cartesianToLngLatHeight(endPosition || floatingPosition) : null,
207
+ distance: getCurrentDistance(),
208
+ distanceText: formatDistance(getCurrentDistance())
209
+ })
210
+ }
211
+
212
+ const ensurePreviewEntities = () => {
213
+ if (!startPointEntity) {
214
+ startPointEntity = map.entities.add({
215
+ id: tempIds.startPoint,
216
+ position: startPosition,
217
+ point: {
218
+ pixelSize: 10,
219
+ color,
220
+ outlineColor: Cesium.Color.WHITE,
221
+ outlineWidth: 2,
222
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
223
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
224
+ }
225
+ })
226
+ }
227
+
228
+ if (!lineEntity) {
229
+ lineEntity = map.entities.add({
230
+ id: tempIds.line,
231
+ polyline: {
232
+ positions: new Cesium.CallbackProperty(() => getLinePositions(), false),
233
+ width,
234
+ material: lineMaterial,
235
+ clampToGround,
236
+ arcType: Cesium.ArcType.GEODESIC,
237
+ granularity: options.granularity ?? Cesium.Math.toRadians(0.2),
238
+ depthFailMaterial: options.depthFailMaterial || new Cesium.ColorMaterialProperty(Cesium.Color.fromAlpha(color, 0.7))
239
+ }
240
+ })
241
+ }
242
+
243
+ if (!distanceLabelEntity) {
244
+ distanceLabelEntity = map.entities.add({
245
+ id: tempIds.label,
246
+ position: new Cesium.CallbackProperty(() => getMidPosition(), false),
247
+ label: {
248
+ text: new Cesium.CallbackProperty(() => formatDistance(getCurrentDistance()), false),
249
+ font: '14px Microsoft YaHei',
250
+ fillColor: Cesium.Color.WHITE,
251
+ showBackground: true,
252
+ backgroundColor: Cesium.Color.fromCssColorString('rgba(0, 0, 0, 0.65)'),
253
+ backgroundPadding: new Cesium.Cartesian2(10, 6),
254
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
255
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
256
+ pixelOffset: new Cesium.Cartesian2(0, -12),
257
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
258
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, Number.POSITIVE_INFINITY)
259
+ }
260
+ })
261
+ }
262
+ }
263
+
264
+ const createEndPointEntity = () => {
265
+ if (endPointEntity || !endPosition) return
266
+ endPointEntity = map.entities.add({
267
+ id: tempIds.endPoint,
268
+ position: endPosition,
269
+ point: {
270
+ pixelSize: 10,
271
+ color,
272
+ outlineColor: Cesium.Color.WHITE,
273
+ outlineWidth: 2,
274
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
275
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
276
+ }
277
+ })
278
+ }
279
+
280
+ const removeTemporaryEntities = () => {
281
+ ;[startPointEntity, endPointEntity, lineEntity, distanceLabelEntity, tipEntity].forEach((entity) => {
282
+ if (entity) map.entities.remove(entity)
283
+ })
284
+ startPointEntity = null
285
+ endPointEntity = null
286
+ lineEntity = null
287
+ distanceLabelEntity = null
288
+ tipEntity = null
289
+ }
290
+
291
+ const destroyHandler = () => {
292
+ if (handler) {
293
+ handler.destroy()
294
+ handler = null
295
+ }
296
+ }
297
+
298
+ const cancel = () => {
299
+ if (isFinished) return
300
+ destroyHandler()
301
+ removeTemporaryEntities()
302
+ if (typeof options.onCancel === 'function') options.onCancel()
303
+ }
304
+
305
+ const finalize = () => {
306
+ if (isFinished || !startPosition || !endPosition) return null
307
+ isFinished = true
308
+ destroyHandler()
309
+ if (tipEntity) {
310
+ map.entities.remove(tipEntity)
311
+ tipEntity = null
312
+ }
313
+ createEndPointEntity()
314
+ const result = buildResult()
315
+ const graphic = {
316
+ id: options.id,
317
+ startPointEntity,
318
+ endPointEntity,
319
+ lineEntity,
320
+ labelEntity: distanceLabelEntity,
321
+ result,
322
+ type: 'measureDistance'
323
+ }
324
+ mapStore.setGraphicMap(options.id, graphic, options.mapId)
325
+ if (typeof options.onFinish === 'function') options.onFinish(result)
326
+ return result
327
+ }
328
+
329
+ tipEntity = createMouseTipEntity(map, {
330
+ id: tempIds.tip,
331
+ getPosition: () => cursorPosition || endPosition || floatingPosition || startPosition,
332
+ getText: getTipText
333
+ })
334
+
335
+ handler = new Cesium.ScreenSpaceEventHandler(map.scene.canvas)
336
+
337
+ handler.setInputAction((movement) => {
338
+ if (isFinished) return
339
+ cursorPosition = getGroundCartesianFromScreen(map, movement.endPosition)
340
+ if (startPosition && cursorPosition) {
341
+ floatingPosition = cursorPosition
342
+ emitChange()
343
+ }
344
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
345
+
346
+ handler.setInputAction((click) => {
347
+ if (isFinished) return
348
+ const cartesian = getGroundCartesianFromScreen(map, click.position)
349
+ if (!cartesian) return
350
+
351
+ if (!startPosition) {
352
+ startPosition = cartesian
353
+ floatingPosition = cartesian
354
+ cursorPosition = cartesian
355
+ ensurePreviewEntities()
356
+ emitChange()
357
+ return
358
+ }
359
+
360
+ endPosition = cartesian
361
+ floatingPosition = null
362
+ createEndPointEntity()
363
+ finalize()
364
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
365
+
366
+ handler.setInputAction(() => {
367
+ cancel()
368
+ }, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
369
+
370
+ return {
371
+ id: options.id,
372
+ finish: finalize,
373
+ cancel,
374
+ destroy: cancel,
375
+ getStart: () => (startPosition ? cartesianToLngLatHeight(startPosition) : null),
376
+ getEnd: () => (endPosition ? cartesianToLngLatHeight(endPosition) : null),
377
+ getDistance: () => getGroundDistance(startPosition, endPosition || floatingPosition),
378
+ getDistanceText: () => formatDistance(getGroundDistance(startPosition, endPosition || floatingPosition))
379
+ }
380
+ }
381
+
382
+ /**
383
+ * 删除测距结果
384
+ * @param {string} id 测距图形 ID
385
+ * @param {string} [mapId] 地图实例 ID
386
+ * @returns {boolean}
387
+ */
388
+ const destroyMeasure = (id, mapId) => {
389
+ const map = mapStore.getMap(mapId)
390
+ if (!map) {
391
+ console.error('地图实例不存在')
392
+ return false
393
+ }
394
+
395
+ const graphic = mapStore.getGraphicMap(id, mapId)
396
+ if (!graphic) {
397
+ console.warn(`id: ${id} 测距结果不存在`)
398
+ return false
399
+ }
400
+
401
+ ;[graphic.startPointEntity, graphic.endPointEntity, graphic.lineEntity, graphic.labelEntity].forEach((entity) => {
402
+ if (entity) map.entities.remove(entity)
403
+ })
404
+ mapStore.removeGraphicMap(id, mapId)
405
+ return true
406
+ }
407
+
408
+ return {
409
+ startMeasure,
410
+ destroyMeasure,
411
+ formatDistance
412
+ }
413
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huweili-cesium",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
4
4
  "description": "基于 Cesium 的地图工具库(无人机态势、轨迹、围栏、工具栏等)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -39,6 +39,8 @@
39
39
  "./drawFence.js": "./js/drawFence.js",
40
40
  "./drawFenceNew": "./js/drawFenceNew.js",
41
41
  "./drawFenceNew.js": "./js/drawFenceNew.js",
42
+ "./measureDistance": "./js/measureDistance.js",
43
+ "./measureDistance.js": "./js/measureDistance.js",
42
44
  "./dronePlannedRoute": "./js/dronePlannedRoute.js",
43
45
  "./dronePlannedRoute.js": "./js/dronePlannedRoute.js",
44
46
  "./droneRipple": "./js/droneRipple.js",