huweili-cesium 1.2.98 → 1.3.0

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/basis.js CHANGED
@@ -22,6 +22,7 @@ import { createZoomController } from './toolbar/zoomController.js'
22
22
  import { useEventBus } from './utils/useEventBus.js'
23
23
  import { applyOrthographicZoomFactor } from './utils/orthographicCameraZoom.js'
24
24
  import { measureDistance } from './measureDistance.js'
25
+ import { measureBearing } from './measureBearing.js'
25
26
 
26
27
  export function basicConfig() {
27
28
 
@@ -746,6 +747,7 @@ export function basicConfig() {
746
747
  const { zoomIn: zoomInDefault, zoomOut: zoomOutDefault } = createZoomController()
747
748
  const { toggleBasemapPickerPanel } = createBasemapSwitcher()
748
749
  const { startMeasure } = measureDistance()
750
+ const { startBearingMeasure } = measureBearing()
749
751
  const { emit } = useEventBus()
750
752
 
751
753
  const zoomIn = (factor = 0.8) => {
@@ -865,6 +867,18 @@ export function basicConfig() {
865
867
  },
866
868
  {
867
869
  index: 2,
870
+ title: '测向',
871
+ iconSrc: `${baseUrl}/images/new/cx.svg`,
872
+ onClick: () => {
873
+ startBearingMeasure({
874
+ id: `home_bearing_${Date.now()}`,
875
+ mapId,
876
+ color: '#ffd400',
877
+ })
878
+ },
879
+ },
880
+ {
881
+ index: 3,
868
882
  title: '返回初始点位',
869
883
  iconSrc: `${baseUrl}/images/new/home.svg`,
870
884
  onClick: (v) => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GLB 模型头顶信息牌(在线/离线状态)
2
+ * GLB 模型头顶信息牌(车辆信息 + 在线/离线状态)
3
3
  *
4
4
  * @author huweili
5
5
  * @version 1.0.0
@@ -7,7 +7,7 @@
7
7
  import * as Cesium from 'cesium'
8
8
  import { getDroneLabelBatchManager } from './cardPool'
9
9
 
10
- const STYLE_ID = 'glb-info-badge-style'
10
+ const STYLE_ID = 'glb-info-badge-style-v3'
11
11
  const DEFAULT_LABEL_OFFSET = 1
12
12
  const DEFAULT_PIXEL_GAP = 0
13
13
  const DEFAULT_HEIGHT_SCALE = 0.72
@@ -20,6 +20,36 @@ const scratchUp = new Cesium.Cartesian3()
20
20
  const scratchOffset = new Cesium.Cartesian3()
21
21
  const scratchTop = new Cesium.Cartesian3()
22
22
 
23
+ /**
24
+ * @typedef {Object} GlbInfoBadgeData
25
+ * @property {string} [vehicleName] 车辆名称
26
+ * @property {number|string} [lng] 经度
27
+ * @property {number|string} [lat] 纬度
28
+ * @property {string} [address] 位置描述
29
+ * @property {boolean} [isOffline] 是否离线
30
+ */
31
+
32
+ /**
33
+ * @param {unknown} value
34
+ * @returns {string}
35
+ */
36
+ const formatCoord = (value) => {
37
+ const num = Number(value)
38
+ return Number.isFinite(num) ? num.toFixed(4) : '--'
39
+ }
40
+
41
+ /**
42
+ * @param {GlbInfoBadgeData} info
43
+ * @returns {GlbInfoBadgeData}
44
+ */
45
+ const normalizeInfo = (info = {}) => ({
46
+ vehicleName: info.vehicleName ?? '--',
47
+ lng: formatCoord(info.lng),
48
+ lat: formatCoord(info.lat),
49
+ address: info.address || '--',
50
+ isOffline: Boolean(info.isOffline),
51
+ })
52
+
23
53
  const ensureGlbInfoBadgeStyle = () => {
24
54
  if (document.getElementById(STYLE_ID)) return
25
55
 
@@ -36,19 +66,64 @@ const ensureGlbInfoBadgeStyle = () => {
36
66
  }
37
67
  .glb-info-badge-inner {
38
68
  position: relative;
39
- display: inline-flex;
40
- align-items: center;
41
- gap: 6px;
42
- padding: 4px 12px 4px 10px;
43
- border-radius: 999px;
44
- background: linear-gradient(135deg, rgba(8, 20, 40, 0.92) 0%, rgba(12, 32, 58, 0.88) 100%);
69
+ min-width: 128px;
70
+ max-width: 168px;
71
+ padding: 5px 8px;
72
+ border-radius: 8px;
73
+ background: linear-gradient(135deg, rgba(8, 20, 40, 0.94) 0%, rgba(12, 32, 58, 0.9) 100%);
45
74
  border: 1px solid rgba(0, 229, 255, 0.45);
46
75
  box-shadow:
47
- inset 0 0 12px rgba(0, 229, 255, 0.08),
48
- 0 0 16px rgba(0, 229, 255, 0.15);
76
+ inset 0 0 8px rgba(0, 229, 255, 0.08),
77
+ 0 0 10px rgba(0, 229, 255, 0.15);
49
78
  overflow: hidden;
50
- backdrop-filter: blur(6px);
79
+ backdrop-filter: blur(4px);
80
+ }
81
+ .glb-info-badge-header {
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ gap: 6px;
86
+ margin-bottom: 4px;
87
+ padding-bottom: 4px;
88
+ border-bottom: 1px solid rgba(0, 229, 255, 0.18);
89
+ }
90
+ .glb-info-badge-name {
91
+ flex: 1;
92
+ min-width: 0;
93
+ font-size: 10px;
94
+ font-weight: 700;
95
+ line-height: 1.2;
96
+ color: #e8f7ff;
51
97
  white-space: nowrap;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ }
101
+ .glb-info-badge-status {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ gap: 3px;
105
+ flex-shrink: 0;
106
+ }
107
+ .glb-info-badge-row {
108
+ display: flex;
109
+ align-items: flex-start;
110
+ gap: 2px;
111
+ font-size: 9px;
112
+ line-height: 1.35;
113
+ color: #b8d4e8;
114
+ }
115
+ .glb-info-badge-row + .glb-info-badge-row {
116
+ margin-top: 2px;
117
+ }
118
+ .glb-info-badge-label {
119
+ flex-shrink: 0;
120
+ color: rgba(184, 212, 232, 0.82);
121
+ }
122
+ .glb-info-badge-value {
123
+ flex: 1;
124
+ min-width: 0;
125
+ color: #e8f7ff;
126
+ word-break: break-all;
52
127
  }
53
128
  .glb-info-badge-scan {
54
129
  position: absolute;
@@ -56,38 +131,42 @@ const ensureGlbInfoBadgeStyle = () => {
56
131
  left: -100%;
57
132
  width: 60%;
58
133
  height: 100%;
59
- background: linear-gradient(90deg, transparent, rgba(0, 229, 255, 0.18), transparent);
134
+ background: linear-gradient(90deg, transparent, rgba(0, 229, 255, 0.14), transparent);
60
135
  animation: glb-info-badge-scan 2.8s ease-in-out infinite;
61
136
  pointer-events: none;
62
137
  }
63
138
  .glb-info-badge-dot {
64
- width: 8px;
65
- height: 8px;
139
+ width: 5px;
140
+ height: 5px;
66
141
  border-radius: 50%;
67
142
  flex-shrink: 0;
68
- box-shadow: 0 0 8px currentColor;
143
+ box-shadow: 0 0 5px currentColor;
69
144
  }
70
145
  .glb-info-badge-text {
71
- font-size: 12px;
146
+ font-size: 9px;
72
147
  font-weight: 600;
73
- letter-spacing: 0.08em;
148
+ line-height: 1;
149
+ letter-spacing: 0.04em;
74
150
  color: #e8f7ff;
75
- text-shadow: 0 0 6px rgba(0, 229, 255, 0.5);
151
+ text-shadow: 0 0 4px rgba(0, 229, 255, 0.5);
76
152
  }
77
153
  .glb-info-badge-pointer {
78
154
  width: 0;
79
155
  height: 0;
80
- margin: 2px auto 0;
81
- border-left: 5px solid transparent;
82
- border-right: 5px solid transparent;
83
- border-top: 6px solid rgba(0, 229, 255, 0.55);
84
- filter: drop-shadow(0 0 4px rgba(0, 229, 255, 0.4));
156
+ margin: 1px auto 0;
157
+ border-left: 4px solid transparent;
158
+ border-right: 4px solid transparent;
159
+ border-top: 5px solid rgba(0, 229, 255, 0.55);
160
+ filter: drop-shadow(0 0 3px rgba(0, 229, 255, 0.4));
85
161
  }
86
162
  .glb-info-badge.is-online .glb-info-badge-inner {
87
163
  border-color: rgba(0, 255, 170, 0.55);
88
164
  box-shadow:
89
- inset 0 0 12px rgba(0, 255, 170, 0.1),
90
- 0 0 16px rgba(0, 255, 170, 0.2);
165
+ inset 0 0 8px rgba(0, 255, 170, 0.1),
166
+ 0 0 12px rgba(0, 255, 170, 0.2);
167
+ }
168
+ .glb-info-badge.is-online .glb-info-badge-header {
169
+ border-bottom-color: rgba(0, 255, 170, 0.22);
91
170
  }
92
171
  .glb-info-badge.is-online .glb-info-badge-dot {
93
172
  background: #00ffaa;
@@ -96,7 +175,7 @@ const ensureGlbInfoBadgeStyle = () => {
96
175
  }
97
176
  .glb-info-badge.is-online .glb-info-badge-text {
98
177
  color: #d6fff0;
99
- text-shadow: 0 0 6px rgba(0, 255, 170, 0.45);
178
+ text-shadow: 0 0 4px rgba(0, 255, 170, 0.45);
100
179
  }
101
180
  .glb-info-badge.is-online .glb-info-badge-pointer {
102
181
  border-top-color: rgba(0, 255, 170, 0.6);
@@ -104,9 +183,11 @@ const ensureGlbInfoBadgeStyle = () => {
104
183
  .glb-info-badge.is-offline .glb-info-badge-inner {
105
184
  border-color: rgba(255, 92, 92, 0.55);
106
185
  box-shadow:
107
- inset 0 0 12px rgba(255, 92, 92, 0.12),
108
- 0 0 18px rgba(255, 92, 92, 0.25);
109
- animation: glb-info-badge-shake 4s ease-in-out infinite;
186
+ inset 0 0 8px rgba(255, 92, 92, 0.12),
187
+ 0 0 14px rgba(255, 92, 92, 0.22);
188
+ }
189
+ .glb-info-badge.is-offline .glb-info-badge-header {
190
+ border-bottom-color: rgba(255, 92, 92, 0.22);
110
191
  }
111
192
  .glb-info-badge.is-offline .glb-info-badge-dot {
112
193
  background: #ff5c5c;
@@ -115,7 +196,7 @@ const ensureGlbInfoBadgeStyle = () => {
115
196
  }
116
197
  .glb-info-badge.is-offline .glb-info-badge-text {
117
198
  color: #ffe8e8;
118
- text-shadow: 0 0 8px rgba(255, 92, 92, 0.55);
199
+ text-shadow: 0 0 6px rgba(255, 92, 92, 0.55);
119
200
  }
120
201
  .glb-info-badge.is-offline .glb-info-badge-pointer {
121
202
  border-top-color: rgba(255, 92, 92, 0.65);
@@ -132,17 +213,65 @@ const ensureGlbInfoBadgeStyle = () => {
132
213
  0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 8px #ff5c5c; }
133
214
  50% { opacity: 0.45; transform: scale(1.25); box-shadow: 0 0 14px #ff5c5c, 0 0 22px rgba(255, 92, 92, 0.6); }
134
215
  }
135
- @keyframes glb-info-badge-shake {
136
- 0%, 92%, 100% { transform: translateX(0); }
137
- 93% { transform: translateX(-1px); }
138
- 94% { transform: translateX(1px); }
139
- 95% { transform: translateX(-1px); }
140
- 96% { transform: translateX(0); }
141
- }
142
216
  `
143
217
  document.head.appendChild(style)
144
218
  }
145
219
 
220
+ /**
221
+ * @param {GlbInfoBadgeData} info
222
+ * @returns {string}
223
+ */
224
+ const buildBadgeHtml = (info) => {
225
+ const data = normalizeInfo(info)
226
+ return `
227
+ <div class="glb-info-badge-inner">
228
+ <div class="glb-info-badge-header">
229
+ <span class="glb-info-badge-name">${data.vehicleName}</span>
230
+ <span class="glb-info-badge-status">
231
+ <span class="glb-info-badge-dot"></span>
232
+ <span class="glb-info-badge-text">${data.isOffline ? '离线' : '在线'}</span>
233
+ </span>
234
+ </div>
235
+ <div class="glb-info-badge-row">
236
+ <span class="glb-info-badge-label">经度:</span>
237
+ <span class="glb-info-badge-value glb-info-badge-lng">${data.lng}</span>
238
+ </div>
239
+ <div class="glb-info-badge-row">
240
+ <span class="glb-info-badge-label">纬度:</span>
241
+ <span class="glb-info-badge-value glb-info-badge-lat">${data.lat}</span>
242
+ </div>
243
+ <div class="glb-info-badge-row">
244
+ <span class="glb-info-badge-label">位置:</span>
245
+ <span class="glb-info-badge-value glb-info-badge-address">${data.address}</span>
246
+ </div>
247
+ <span class="glb-info-badge-scan"></span>
248
+ </div>
249
+ <div class="glb-info-badge-pointer"></div>
250
+ `
251
+ }
252
+
253
+ /**
254
+ * @param {HTMLElement} badgeEl
255
+ * @param {GlbInfoBadgeData} info
256
+ */
257
+ const renderBadgeContent = (badgeEl, info) => {
258
+ const data = normalizeInfo(info)
259
+ badgeEl.classList.toggle('is-offline', data.isOffline)
260
+ badgeEl.classList.toggle('is-online', !data.isOffline)
261
+
262
+ const nameEl = badgeEl.querySelector('.glb-info-badge-name')
263
+ const textEl = badgeEl.querySelector('.glb-info-badge-text')
264
+ const lngEl = badgeEl.querySelector('.glb-info-badge-lng')
265
+ const latEl = badgeEl.querySelector('.glb-info-badge-lat')
266
+ const addressEl = badgeEl.querySelector('.glb-info-badge-address')
267
+
268
+ if (nameEl) nameEl.textContent = data.vehicleName
269
+ if (textEl) textEl.textContent = data.isOffline ? '离线' : '在线'
270
+ if (lngEl) lngEl.textContent = data.lng
271
+ if (latEl) latEl.textContent = data.lat
272
+ if (addressEl) addressEl.textContent = data.address
273
+ }
274
+
146
275
  /**
147
276
  * 读取 Entity 模型 minimumPixelSize(远景会被放大显示,需同步到屏幕偏移)
148
277
  * @param {Cesium.Entity} entity
@@ -162,11 +291,6 @@ const getEntityMinimumPixelSize = (entity, time) => {
162
291
 
163
292
  /**
164
293
  * 根据模型包围球估算车顶世界坐标
165
- * @param {Cesium.Viewer} map
166
- * @param {Cesium.Entity} entity
167
- * @param {Cesium.Cartesian3} basePos
168
- * @param {number} fallbackOffset
169
- * @returns {Cesium.Cartesian3|null}
170
294
  */
171
295
  const resolveModelTopWorldPosition = (map, entity, basePos, fallbackOffset) => {
172
296
  Cesium.Transforms.eastNorthUpToFixedFrame(basePos, undefined, scratchEnu)
@@ -191,13 +315,6 @@ const resolveModelTopWorldPosition = (map, entity, basePos, fallbackOffset) => {
191
315
 
192
316
  /**
193
317
  * 将车顶世界坐标转为屏幕坐标,并兼顾 minimumPixelSize 的可视高度
194
- * @param {Cesium.Scene} scene
195
- * @param {Cesium.Cartesian3} basePos
196
- * @param {Cesium.Cartesian3} topWorldPos
197
- * @param {number} minPixelSize
198
- * @param {number} pixelGap
199
- * @param {number} heightScale
200
- * @returns {{ x: number, y: number }|null}
201
318
  */
202
319
  const resolveBadgeScreenPosition = (scene, basePos, topWorldPos, minPixelSize, pixelGap, heightScale) => {
203
320
  const anchorWin = scene.cartesianToCanvasCoordinates(basePos)
@@ -212,7 +329,6 @@ const resolveBadgeScreenPosition = (scene, basePos, topWorldPos, minPixelSize, p
212
329
  offsetY = (anchorWin.y - topWin.y) * heightScale
213
330
  }
214
331
 
215
- // 包围球常大于实际 mesh,对过大偏移做软压缩
216
332
  if (offsetY > 0 && minPixelSize > 0 && offsetY > minPixelSize * 0.5) {
217
333
  const base = minPixelSize * 0.3
218
334
  offsetY = base + (offsetY - base) * 0.35
@@ -236,17 +352,18 @@ const resolveBadgeScreenPosition = (scene, basePos, topWorldPos, minPixelSize, p
236
352
  }
237
353
 
238
354
  /**
239
- * 在模型头顶创建在线/离线信息牌
355
+ * 在模型头顶创建车辆信息牌
240
356
  * @param {Object} options
241
357
  * @param {Cesium.Viewer} options.map
242
- * @param {Cesium.Entity} [options.entity] GLB 实体(用于读取包围球与 minimumPixelSize)
358
+ * @param {Cesium.Entity} [options.entity]
243
359
  * @param {string|number} [options.mapId]
244
- * @param {Function} options.getPosition 获取模型世界坐标
245
- * @param {Function} [options.getVisible] 是否显示信息牌
246
- * @param {boolean} [options.isOffline=false] true=离线,false=在线
247
- * @param {number} [options.labelOffset=1] 模型未加载完成时的兜底高度(米)
248
- * @param {number} [options.pixelGap=0] 信息牌与车顶之间的像素间距
249
- * @param {number} [options.heightScale=0.72] 屏幕偏移收紧系数(越小越贴近车顶)
360
+ * @param {Function} options.getPosition
361
+ * @param {Function} [options.getInfo] 获取车辆信息
362
+ * @param {Function} [options.getVisible]
363
+ * @param {GlbInfoBadgeData} [options.info] 初始信息
364
+ * @param {number} [options.labelOffset=1]
365
+ * @param {number} [options.pixelGap=0]
366
+ * @param {number} [options.heightScale=0.72]
250
367
  */
251
368
  export function createGlbInfoBadge(options = {}) {
252
369
  const {
@@ -254,8 +371,9 @@ export function createGlbInfoBadge(options = {}) {
254
371
  entity,
255
372
  mapId,
256
373
  getPosition,
374
+ getInfo,
257
375
  getVisible,
258
- isOffline = false,
376
+ info: initialInfo = {},
259
377
  labelOffset = DEFAULT_LABEL_OFFSET,
260
378
  pixelGap = DEFAULT_PIXEL_GAP,
261
379
  heightScale = DEFAULT_HEIGHT_SCALE,
@@ -269,17 +387,9 @@ export function createGlbInfoBadge(options = {}) {
269
387
 
270
388
  const batchManager = getDroneLabelBatchManager(map, mapId)
271
389
  const badgeEl = document.createElement('div')
272
- badgeEl.className = `glb-info-badge ${isOffline ? 'is-offline' : 'is-online'}`
273
- badgeEl.innerHTML = `
274
- <div class="glb-info-badge-inner">
275
- <span class="glb-info-badge-dot"></span>
276
- <span class="glb-info-badge-text">${isOffline ? '离线' : '在线'}</span>
277
- <span class="glb-info-badge-scan"></span>
278
- </div>
279
- <div class="glb-info-badge-pointer"></div>
280
- `
281
-
282
- const textEl = badgeEl.querySelector('.glb-info-badge-text')
390
+ badgeEl.className = 'glb-info-badge'
391
+ badgeEl.innerHTML = buildBadgeHtml(initialInfo)
392
+ renderBadgeContent(badgeEl, initialInfo)
283
393
 
284
394
  const batchEntry = {
285
395
  updatePosition(hideByHeight) {
@@ -337,14 +447,14 @@ export function createGlbInfoBadge(options = {}) {
337
447
  batchManager.container.appendChild(badgeEl)
338
448
  batchManager.add(batchEntry)
339
449
 
450
+ const updateInfo = (info = {}) => {
451
+ renderBadgeContent(badgeEl, info)
452
+ return normalizeInfo(info)
453
+ }
454
+
340
455
  const setOfflineStatus = (offline) => {
341
- const nextOffline = Boolean(offline)
342
- badgeEl.classList.toggle('is-offline', nextOffline)
343
- badgeEl.classList.toggle('is-online', !nextOffline)
344
- if (textEl) {
345
- textEl.textContent = nextOffline ? '离线' : '在线'
346
- }
347
- return nextOffline
456
+ const currentInfo = typeof getInfo === 'function' ? getInfo() : initialInfo
457
+ return updateInfo({ ...currentInfo, isOffline: Boolean(offline) })
348
458
  }
349
459
 
350
460
  const destroy = () => {
@@ -353,6 +463,7 @@ export function createGlbInfoBadge(options = {}) {
353
463
  }
354
464
 
355
465
  return {
466
+ updateInfo,
356
467
  setOfflineStatus,
357
468
  destroy,
358
469
  }
package/js/index.js CHANGED
@@ -9,6 +9,7 @@ export { createCustomToolbarButtons } from './customToolbarButtons.js'
9
9
  export { drawFence } from './drawFence.js'
10
10
  export { drawFenceNew } from './drawFenceNew.js'
11
11
  export { measureDistance } from './measureDistance.js'
12
+ export { measureBearing } from './measureBearing.js'
12
13
  export { createDroneRippleDom } from './droneRipple.js'
13
14
  export { createGlbInfoBadge } from './glbInfoBadge.js'
14
15
  export { geometryConfig } from './geometry.js'
@@ -0,0 +1,379 @@
1
+ /**
2
+ * 地图测向:点击起点与终点,测量第二点相对于第一点的北向角(北向 0°,顺时针)
3
+ *
4
+ * @author huweili
5
+ * @email czxyhuweili@163.com
6
+ * @version 1.0.0
7
+ */
8
+ import * as Cesium from 'cesium'
9
+ import { useMapStore } from './stores/mapStore.js'
10
+
11
+ function formatBearingDegrees(deg) {
12
+ if (!Number.isFinite(deg)) return '--'
13
+ return `${(((deg % 360) + 360) % 360).toFixed(1)}°`
14
+ }
15
+
16
+ function getGroundCartesianFromScreen(map, screenPosition) {
17
+ if (!screenPosition) return null
18
+
19
+ const scene = map.scene
20
+ let cartesian = null
21
+
22
+ if (scene.pickPositionSupported) {
23
+ cartesian = scene.pickPosition(screenPosition)
24
+ }
25
+
26
+ if (!Cesium.defined(cartesian)) {
27
+ const ray = map.camera.getPickRay(screenPosition)
28
+ if (!ray) return null
29
+ cartesian = scene.globe.pick(ray, scene)
30
+ }
31
+
32
+ if (!Cesium.defined(cartesian)) return null
33
+
34
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
35
+ return Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0)
36
+ }
37
+
38
+ function cartesianToLngLat(cartesian) {
39
+ const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
40
+ return {
41
+ lng: Cesium.Math.toDegrees(cartographic.longitude),
42
+ lat: Cesium.Math.toDegrees(cartographic.latitude),
43
+ }
44
+ }
45
+
46
+ function computeGeodesicBearing(lng1, lat1, lng2, lat2) {
47
+ const φ1 = Cesium.Math.toRadians(lat1)
48
+ const φ2 = Cesium.Math.toRadians(lat2)
49
+ const Δλ = Cesium.Math.toRadians(lng2 - lng1)
50
+ const y = Math.sin(Δλ) * Math.cos(φ2)
51
+ const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ)
52
+ return (Cesium.Math.toDegrees(Math.atan2(y, x)) + 360) % 360
53
+ }
54
+
55
+ function computeBearingResult(start, end) {
56
+ const azimuth = computeGeodesicBearing(start.lng, start.lat, end.lng, end.lat)
57
+ const dist = Cesium.Cartesian3.distance(
58
+ Cesium.Cartesian3.fromDegrees(start.lng, start.lat, 0),
59
+ Cesium.Cartesian3.fromDegrees(end.lng, end.lat, 0),
60
+ )
61
+ return {
62
+ azimuth,
63
+ azimuthText: formatBearingDegrees(azimuth),
64
+ distance: dist,
65
+ }
66
+ }
67
+
68
+ export function measureBearing() {
69
+ const mapStore = useMapStore()
70
+
71
+ const createMouseTipEntity = (map, options) => {
72
+ return map.entities.add({
73
+ id: options.id,
74
+ position: new Cesium.CallbackProperty(() => options.getPosition(), false),
75
+ label: {
76
+ text: new Cesium.CallbackProperty(() => options.getText(), false),
77
+ font: options.font || 'bold 10px Microsoft YaHei',
78
+ fillColor: options.fillColor || Cesium.Color.WHITE,
79
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
80
+ outlineColor: Cesium.Color.BLACK,
81
+ outlineWidth: 2,
82
+ showBackground: true,
83
+ backgroundColor: Cesium.Color.fromCssColorString(options.backgroundColor || 'rgba(0, 0, 0, 0.7)'),
84
+ backgroundPadding: options.backgroundPadding || new Cesium.Cartesian2(4, 4),
85
+ verticalOrigin: options.verticalOrigin || Cesium.VerticalOrigin.BOTTOM,
86
+ horizontalOrigin: options.horizontalOrigin || Cesium.HorizontalOrigin.LEFT,
87
+ pixelOffset: options.pixelOffset || new Cesium.Cartesian2(18, -18),
88
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
89
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, options.maxDistance || 2000000),
90
+ },
91
+ })
92
+ }
93
+
94
+ /**
95
+ * @param {Object} options
96
+ * @param {string} options.id
97
+ * @param {string} [options.mapId]
98
+ * @param {string} [options.color='#ffd400']
99
+ * @param {(result: Object) => void} [options.onFinish]
100
+ * @returns {Object|null}
101
+ */
102
+ const startBearingMeasure = (options = {}) => {
103
+ const map = mapStore.getMap(options.mapId)
104
+ if (!map) {
105
+ console.error('[测向] 地图实例不存在')
106
+ return null
107
+ }
108
+ if (!options.id) {
109
+ console.error('[测向] 必须提供 id')
110
+ return null
111
+ }
112
+ if (mapStore.hasGraphicMap(options.id, options.mapId)) {
113
+ console.warn(`[测向] id ${options.id} 已存在`)
114
+ return null
115
+ }
116
+
117
+ const color = Cesium.Color.fromCssColorString(options.color || '#ffd400')
118
+
119
+ let startPosition = null
120
+ let endPosition = null
121
+ let floatingPosition = null
122
+ let cursorPosition = null
123
+ let startPointEntity = null
124
+ let endPointEntity = null
125
+ let lineEntity = null
126
+ let labelEntity = null
127
+ let tipEntity = null
128
+ let deleteButtonEntity = null
129
+ let handler = null
130
+ let deleteHandler = null
131
+ let isFinished = false
132
+
133
+ const tempIds = {
134
+ startPoint: `${options.id}_bearing_start`,
135
+ endPoint: `${options.id}_bearing_end`,
136
+ line: `${options.id}_bearing_line`,
137
+ label: `${options.id}_bearing_label`,
138
+ tip: `${options.id}_bearing_tip`,
139
+ deleteButton: `${options.id}_bearing_delete`,
140
+ }
141
+
142
+ const getLinePositions = () => {
143
+ if (!startPosition) return []
144
+ const target = endPosition || floatingPosition
145
+ if (!target) return [startPosition]
146
+ return [startPosition, target]
147
+ }
148
+
149
+ const getMidPosition = () => {
150
+ const target = endPosition || floatingPosition
151
+ if (!startPosition || !target) return startPosition || target || cursorPosition || null
152
+ const s = Cesium.Cartographic.fromCartesian(startPosition)
153
+ const e = Cesium.Cartographic.fromCartesian(target)
154
+ return Cesium.Cartesian3.fromRadians(
155
+ (s.longitude + e.longitude) / 2,
156
+ (s.latitude + e.latitude) / 2,
157
+ 0,
158
+ )
159
+ }
160
+
161
+ const getCurrentBearingText = () => {
162
+ const target = endPosition || floatingPosition
163
+ if (!startPosition || !target) return '--'
164
+ const start = cartesianToLngLat(startPosition)
165
+ const end = cartesianToLngLat(target)
166
+ return computeBearingResult(start, end).azimuthText
167
+ }
168
+
169
+ const getTipText = () => {
170
+ if (!startPosition) return '点击确定起点'
171
+ if (!endPosition) return `点击确定终点,当前北向角:${getCurrentBearingText()}`
172
+ return `测向完成,北向角:${getCurrentBearingText()}`
173
+ }
174
+
175
+ const ensurePreviewEntities = () => {
176
+ if (!startPointEntity) {
177
+ startPointEntity = map.entities.add({
178
+ id: tempIds.startPoint,
179
+ position: startPosition,
180
+ point: {
181
+ pixelSize: 6,
182
+ color,
183
+ outlineColor: Cesium.Color.WHITE,
184
+ outlineWidth: 1.5,
185
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
186
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
187
+ scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 5000000, 0.5),
188
+ },
189
+ })
190
+ }
191
+
192
+ if (!lineEntity) {
193
+ lineEntity = map.entities.add({
194
+ id: tempIds.line,
195
+ polyline: {
196
+ positions: new Cesium.CallbackProperty(() => getLinePositions(), false),
197
+ width: 3,
198
+ material: new Cesium.PolylineGlowMaterialProperty({ color, glowPower: 0.25 }),
199
+ clampToGround: true,
200
+ },
201
+ })
202
+ }
203
+
204
+ if (!labelEntity) {
205
+ labelEntity = map.entities.add({
206
+ id: tempIds.label,
207
+ position: new Cesium.CallbackProperty(() => getMidPosition(), false),
208
+ label: {
209
+ text: new Cesium.CallbackProperty(() => `北向角 ${getCurrentBearingText()}`, false),
210
+ font: 'bold 10px Microsoft YaHei',
211
+ fillColor: Cesium.Color.WHITE,
212
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
213
+ outlineColor: Cesium.Color.BLACK,
214
+ outlineWidth: 2,
215
+ showBackground: true,
216
+ backgroundColor: Cesium.Color.fromCssColorString('rgba(0, 0, 0, 0.75)'),
217
+ backgroundPadding: new Cesium.Cartesian2(4, 4),
218
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
219
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
220
+ pixelOffset: new Cesium.Cartesian2(0, -16),
221
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
222
+ distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 2000000),
223
+ },
224
+ })
225
+ }
226
+ }
227
+
228
+ const createEndPointEntity = () => {
229
+ if (endPointEntity || !endPosition) return
230
+ endPointEntity = map.entities.add({
231
+ id: tempIds.endPoint,
232
+ position: endPosition,
233
+ point: {
234
+ pixelSize: 6,
235
+ color,
236
+ outlineColor: Cesium.Color.WHITE,
237
+ outlineWidth: 1.5,
238
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
239
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
240
+ scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 5000000, 0.5),
241
+ },
242
+ })
243
+ }
244
+
245
+ const removeEntities = () => {
246
+ [startPointEntity, endPointEntity, lineEntity, labelEntity, deleteButtonEntity, tipEntity].forEach((entity) => {
247
+ if (entity) map.entities.remove(entity)
248
+ })
249
+ startPointEntity = null
250
+ endPointEntity = null
251
+ lineEntity = null
252
+ labelEntity = null
253
+ deleteButtonEntity = null
254
+ tipEntity = null
255
+ }
256
+
257
+ const destroyGraphic = () => {
258
+ if (handler) {
259
+ handler.destroy()
260
+ handler = null
261
+ }
262
+ if (deleteHandler) {
263
+ deleteHandler.destroy()
264
+ deleteHandler = null
265
+ }
266
+ removeEntities()
267
+ mapStore.removeGraphicMap(options.id, options.mapId)
268
+ }
269
+
270
+ const cancel = () => {
271
+ if (isFinished) return
272
+ if (handler) {
273
+ handler.destroy()
274
+ handler = null
275
+ }
276
+ removeEntities()
277
+ }
278
+
279
+ const finalize = () => {
280
+ if (isFinished || !startPosition || !endPosition) return null
281
+ isFinished = true
282
+ if (handler) {
283
+ handler.destroy()
284
+ handler = null
285
+ }
286
+ if (tipEntity) {
287
+ map.entities.remove(tipEntity)
288
+ tipEntity = null
289
+ }
290
+
291
+ createEndPointEntity()
292
+
293
+ deleteButtonEntity = map.entities.add({
294
+ id: tempIds.deleteButton,
295
+ position: new Cesium.CallbackProperty(() => getMidPosition(), false),
296
+ label: {
297
+ text: '✕',
298
+ font: 'bold 12px Arial',
299
+ fillColor: color,
300
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
301
+ outlineColor: Cesium.Color.BLACK,
302
+ outlineWidth: 2,
303
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
304
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
305
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
306
+ },
307
+ })
308
+
309
+ deleteHandler = new Cesium.ScreenSpaceEventHandler(map.scene.canvas)
310
+ deleteHandler.setInputAction((movement) => {
311
+ const picked = map.scene.pick(movement.position)
312
+ if (!Cesium.defined(picked?.id)) return
313
+ if (picked.id === deleteButtonEntity || picked.id === tempIds.deleteButton) {
314
+ destroyGraphic()
315
+ }
316
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
317
+
318
+ const start = cartesianToLngLat(startPosition)
319
+ const end = cartesianToLngLat(endPosition)
320
+ const result = { id: options.id, start, end, ...computeBearingResult(start, end) }
321
+
322
+ mapStore.setGraphicMap(options.id, {
323
+ id: options.id,
324
+ type: 'measureBearing',
325
+ result,
326
+ destroy: destroyGraphic,
327
+ }, options.mapId)
328
+
329
+ if (typeof options.onFinish === 'function') options.onFinish(result)
330
+ return result
331
+ }
332
+
333
+ tipEntity = createMouseTipEntity(map, {
334
+ id: tempIds.tip,
335
+ getPosition: () => cursorPosition || endPosition || floatingPosition || startPosition,
336
+ getText: getTipText,
337
+ })
338
+
339
+ handler = new Cesium.ScreenSpaceEventHandler(map.scene.canvas)
340
+
341
+ handler.setInputAction((movement) => {
342
+ if (isFinished) return
343
+ cursorPosition = getGroundCartesianFromScreen(map, movement.endPosition)
344
+ if (startPosition && cursorPosition) {
345
+ floatingPosition = cursorPosition
346
+ }
347
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)
348
+
349
+ handler.setInputAction((click) => {
350
+ if (isFinished) return
351
+ const cartesian = getGroundCartesianFromScreen(map, click.position)
352
+ if (!cartesian) return
353
+
354
+ if (!startPosition) {
355
+ startPosition = cartesian
356
+ floatingPosition = cartesian
357
+ cursorPosition = cartesian
358
+ ensurePreviewEntities()
359
+ return
360
+ }
361
+
362
+ endPosition = cartesian
363
+ floatingPosition = null
364
+ createEndPointEntity()
365
+ finalize()
366
+ }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
367
+
368
+ handler.setInputAction(() => {
369
+ cancel()
370
+ }, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
371
+
372
+ return { id: options.id, finish: finalize, cancel, destroy: destroyGraphic }
373
+ }
374
+
375
+ return {
376
+ startBearingMeasure,
377
+ formatBearingDegrees,
378
+ }
379
+ }
package/js/movePoint.js CHANGED
@@ -84,7 +84,7 @@ export function movePointConfig(baseUrl) {
84
84
  }
85
85
 
86
86
  /**
87
- * 移动 GLB 信息牌点位到新的坐标位置,并可选更新在线/离线状态
87
+ * 移动 GLB 信息牌点位到新的坐标位置,并更新车辆信息
88
88
  * @param options 配置选项
89
89
  * @param options.pointId 点位唯一标识
90
90
  * @param options.lng 新的经度
@@ -92,6 +92,8 @@ export function movePointConfig(baseUrl) {
92
92
  * @param options.height 高度(可选,默认0)
93
93
  * @param options.heading 朝向(可选,默认0)
94
94
  * @param options.pitch 俯仰(可选,默认0)
95
+ * @param options.vehicleName 车辆名称(可选)
96
+ * @param options.address 位置描述(可选)
95
97
  * @param options.isOffline 是否离线(true=离线,false=在线;不传则保持原状态)
96
98
  * @param options.mapId 地图ID
97
99
  */
@@ -137,7 +139,13 @@ export function movePointConfig(baseUrl) {
137
139
  )
138
140
  }
139
141
 
140
- if (options.isOffline !== undefined && typeof point.setOfflineStatus === 'function') {
142
+ if (typeof point.updateInfo === 'function') {
143
+ const infoPatch = { lng, lat }
144
+ if (options.vehicleName !== undefined) infoPatch.vehicleName = options.vehicleName
145
+ if (options.address !== undefined) infoPatch.address = options.address
146
+ if (options.isOffline !== undefined) infoPatch.isOffline = options.isOffline
147
+ point.updateInfo(infoPatch)
148
+ } else if (options.isOffline !== undefined && typeof point.setOfflineStatus === 'function') {
141
149
  point.setOfflineStatus(options.isOffline)
142
150
  }
143
151
 
package/js/setPoint.js CHANGED
@@ -221,7 +221,7 @@ export function setPoint(baseUrl) {
221
221
  }
222
222
 
223
223
  /**
224
- * 设置点位(GLB 模型 + 头顶在线/离线信息牌)
224
+ * 设置点位(GLB 模型 + 头顶车辆信息牌)
225
225
  * @param options 配置选项
226
226
  * @param options.url GLB模型路径(本地/网络)
227
227
  * @param options.id 点位唯一标识
@@ -229,8 +229,9 @@ export function setPoint(baseUrl) {
229
229
  * @param options.lat 纬度
230
230
  * @param options.height 高度(可选,默认0)
231
231
  * @param options.heading 朝向(可选,默认0)
232
+ * @param options.vehicleName 车辆名称
233
+ * @param options.address 位置描述
232
234
  * @param options.isOffline 是否离线(true=离线,false=在线,默认 false)
233
- * @param options.modelHeight 模型高度兜底值(米,可选,默认 1;未加载包围球时使用)
234
235
  * @param options.mapId 地图ID
235
236
  * @returns 创建的点位包装对象
236
237
  */
@@ -275,6 +276,13 @@ export function setPoint(baseUrl) {
275
276
  const glbPoint = {
276
277
  entity: modelEntity,
277
278
  isOffline,
279
+ info: {
280
+ vehicleName: options.vehicleName || '--',
281
+ lng,
282
+ lat,
283
+ address: options.address || '--',
284
+ isOffline,
285
+ },
278
286
  infoBadge: null,
279
287
  get position() {
280
288
  return this.entity.position
@@ -288,10 +296,20 @@ export function setPoint(baseUrl) {
288
296
  set orientation(value) {
289
297
  this.entity.orientation = value
290
298
  },
299
+ updateInfo(partial = {}) {
300
+ if (partial.lng !== undefined) this.info.lng = Number(partial.lng)
301
+ if (partial.lat !== undefined) this.info.lat = Number(partial.lat)
302
+ if (partial.vehicleName !== undefined) this.info.vehicleName = partial.vehicleName
303
+ if (partial.address !== undefined) this.info.address = partial.address
304
+ if (partial.isOffline !== undefined) {
305
+ this.isOffline = Boolean(partial.isOffline)
306
+ this.info.isOffline = this.isOffline
307
+ }
308
+ this.infoBadge?.updateInfo?.(this.info)
309
+ return this.info
310
+ },
291
311
  setOfflineStatus(offline) {
292
- this.isOffline = Boolean(offline)
293
- this.infoBadge?.setOfflineStatus?.(this.isOffline)
294
- return this.isOffline
312
+ return this.updateInfo({ isOffline: offline })
295
313
  },
296
314
  destroy() {
297
315
  this.infoBadge?.destroy?.()
@@ -305,8 +323,8 @@ export function setPoint(baseUrl) {
305
323
  map,
306
324
  mapId,
307
325
  entity: modelEntity,
308
- isOffline,
309
- labelOffset: Number(options.modelHeight) > 0 ? Number(options.modelHeight) : undefined,
326
+ info: glbPoint.info,
327
+ getInfo: () => glbPoint.info,
310
328
  getPosition: () => {
311
329
  const posProp = modelEntity.position
312
330
  if (posProp instanceof Cesium.Cartesian3) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huweili-cesium",
3
- "version": "1.2.98",
3
+ "version": "1.3.0",
4
4
  "description": "基于 Cesium 的地图工具库(无人机态势、轨迹、围栏、工具栏等)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -41,6 +41,8 @@
41
41
  "./drawFenceNew.js": "./js/drawFenceNew.js",
42
42
  "./measureDistance": "./js/measureDistance.js",
43
43
  "./measureDistance.js": "./js/measureDistance.js",
44
+ "./measureBearing": "./js/measureBearing.js",
45
+ "./measureBearing.js": "./js/measureBearing.js",
44
46
  "./dronePlannedRoute": "./js/dronePlannedRoute.js",
45
47
  "./dronePlannedRoute.js": "./js/dronePlannedRoute.js",
46
48
  "./droneRipple": "./js/droneRipple.js",