huweili-cesium 1.2.99 → 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-v2'
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: 4px;
42
- padding: 2px 8px 2px 6px;
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
76
  inset 0 0 8px rgba(0, 229, 255, 0.08),
48
77
  0 0 10px rgba(0, 229, 255, 0.15);
49
78
  overflow: hidden;
50
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,7 +131,7 @@ 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
  }
@@ -68,10 +143,10 @@ const ensureGlbInfoBadgeStyle = () => {
68
143
  box-shadow: 0 0 5px currentColor;
69
144
  }
70
145
  .glb-info-badge-text {
71
- font-size: 10px;
146
+ font-size: 9px;
72
147
  font-weight: 600;
73
148
  line-height: 1;
74
- letter-spacing: 0.06em;
149
+ letter-spacing: 0.04em;
75
150
  color: #e8f7ff;
76
151
  text-shadow: 0 0 4px rgba(0, 229, 255, 0.5);
77
152
  }
@@ -87,8 +162,11 @@ const ensureGlbInfoBadgeStyle = () => {
87
162
  .glb-info-badge.is-online .glb-info-badge-inner {
88
163
  border-color: rgba(0, 255, 170, 0.55);
89
164
  box-shadow:
90
- inset 0 0 12px rgba(0, 255, 170, 0.1),
91
- 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);
92
170
  }
93
171
  .glb-info-badge.is-online .glb-info-badge-dot {
94
172
  background: #00ffaa;
@@ -97,7 +175,7 @@ const ensureGlbInfoBadgeStyle = () => {
97
175
  }
98
176
  .glb-info-badge.is-online .glb-info-badge-text {
99
177
  color: #d6fff0;
100
- text-shadow: 0 0 6px rgba(0, 255, 170, 0.45);
178
+ text-shadow: 0 0 4px rgba(0, 255, 170, 0.45);
101
179
  }
102
180
  .glb-info-badge.is-online .glb-info-badge-pointer {
103
181
  border-top-color: rgba(0, 255, 170, 0.6);
@@ -105,9 +183,11 @@ const ensureGlbInfoBadgeStyle = () => {
105
183
  .glb-info-badge.is-offline .glb-info-badge-inner {
106
184
  border-color: rgba(255, 92, 92, 0.55);
107
185
  box-shadow:
108
- inset 0 0 12px rgba(255, 92, 92, 0.12),
109
- 0 0 18px rgba(255, 92, 92, 0.25);
110
- 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);
111
191
  }
112
192
  .glb-info-badge.is-offline .glb-info-badge-dot {
113
193
  background: #ff5c5c;
@@ -116,7 +196,7 @@ const ensureGlbInfoBadgeStyle = () => {
116
196
  }
117
197
  .glb-info-badge.is-offline .glb-info-badge-text {
118
198
  color: #ffe8e8;
119
- text-shadow: 0 0 8px rgba(255, 92, 92, 0.55);
199
+ text-shadow: 0 0 6px rgba(255, 92, 92, 0.55);
120
200
  }
121
201
  .glb-info-badge.is-offline .glb-info-badge-pointer {
122
202
  border-top-color: rgba(255, 92, 92, 0.65);
@@ -133,17 +213,65 @@ const ensureGlbInfoBadgeStyle = () => {
133
213
  0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 8px #ff5c5c; }
134
214
  50% { opacity: 0.45; transform: scale(1.25); box-shadow: 0 0 14px #ff5c5c, 0 0 22px rgba(255, 92, 92, 0.6); }
135
215
  }
136
- @keyframes glb-info-badge-shake {
137
- 0%, 92%, 100% { transform: translateX(0); }
138
- 93% { transform: translateX(-1px); }
139
- 94% { transform: translateX(1px); }
140
- 95% { transform: translateX(-1px); }
141
- 96% { transform: translateX(0); }
142
- }
143
216
  `
144
217
  document.head.appendChild(style)
145
218
  }
146
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
+
147
275
  /**
148
276
  * 读取 Entity 模型 minimumPixelSize(远景会被放大显示,需同步到屏幕偏移)
149
277
  * @param {Cesium.Entity} entity
@@ -163,11 +291,6 @@ const getEntityMinimumPixelSize = (entity, time) => {
163
291
 
164
292
  /**
165
293
  * 根据模型包围球估算车顶世界坐标
166
- * @param {Cesium.Viewer} map
167
- * @param {Cesium.Entity} entity
168
- * @param {Cesium.Cartesian3} basePos
169
- * @param {number} fallbackOffset
170
- * @returns {Cesium.Cartesian3|null}
171
294
  */
172
295
  const resolveModelTopWorldPosition = (map, entity, basePos, fallbackOffset) => {
173
296
  Cesium.Transforms.eastNorthUpToFixedFrame(basePos, undefined, scratchEnu)
@@ -192,13 +315,6 @@ const resolveModelTopWorldPosition = (map, entity, basePos, fallbackOffset) => {
192
315
 
193
316
  /**
194
317
  * 将车顶世界坐标转为屏幕坐标,并兼顾 minimumPixelSize 的可视高度
195
- * @param {Cesium.Scene} scene
196
- * @param {Cesium.Cartesian3} basePos
197
- * @param {Cesium.Cartesian3} topWorldPos
198
- * @param {number} minPixelSize
199
- * @param {number} pixelGap
200
- * @param {number} heightScale
201
- * @returns {{ x: number, y: number }|null}
202
318
  */
203
319
  const resolveBadgeScreenPosition = (scene, basePos, topWorldPos, minPixelSize, pixelGap, heightScale) => {
204
320
  const anchorWin = scene.cartesianToCanvasCoordinates(basePos)
@@ -213,7 +329,6 @@ const resolveBadgeScreenPosition = (scene, basePos, topWorldPos, minPixelSize, p
213
329
  offsetY = (anchorWin.y - topWin.y) * heightScale
214
330
  }
215
331
 
216
- // 包围球常大于实际 mesh,对过大偏移做软压缩
217
332
  if (offsetY > 0 && minPixelSize > 0 && offsetY > minPixelSize * 0.5) {
218
333
  const base = minPixelSize * 0.3
219
334
  offsetY = base + (offsetY - base) * 0.35
@@ -237,17 +352,18 @@ const resolveBadgeScreenPosition = (scene, basePos, topWorldPos, minPixelSize, p
237
352
  }
238
353
 
239
354
  /**
240
- * 在模型头顶创建在线/离线信息牌
355
+ * 在模型头顶创建车辆信息牌
241
356
  * @param {Object} options
242
357
  * @param {Cesium.Viewer} options.map
243
- * @param {Cesium.Entity} [options.entity] GLB 实体(用于读取包围球与 minimumPixelSize)
358
+ * @param {Cesium.Entity} [options.entity]
244
359
  * @param {string|number} [options.mapId]
245
- * @param {Function} options.getPosition 获取模型世界坐标
246
- * @param {Function} [options.getVisible] 是否显示信息牌
247
- * @param {boolean} [options.isOffline=false] true=离线,false=在线
248
- * @param {number} [options.labelOffset=1] 模型未加载完成时的兜底高度(米)
249
- * @param {number} [options.pixelGap=0] 信息牌与车顶之间的像素间距
250
- * @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]
251
367
  */
252
368
  export function createGlbInfoBadge(options = {}) {
253
369
  const {
@@ -255,8 +371,9 @@ export function createGlbInfoBadge(options = {}) {
255
371
  entity,
256
372
  mapId,
257
373
  getPosition,
374
+ getInfo,
258
375
  getVisible,
259
- isOffline = false,
376
+ info: initialInfo = {},
260
377
  labelOffset = DEFAULT_LABEL_OFFSET,
261
378
  pixelGap = DEFAULT_PIXEL_GAP,
262
379
  heightScale = DEFAULT_HEIGHT_SCALE,
@@ -270,17 +387,9 @@ export function createGlbInfoBadge(options = {}) {
270
387
 
271
388
  const batchManager = getDroneLabelBatchManager(map, mapId)
272
389
  const badgeEl = document.createElement('div')
273
- badgeEl.className = `glb-info-badge ${isOffline ? 'is-offline' : 'is-online'}`
274
- badgeEl.innerHTML = `
275
- <div class="glb-info-badge-inner">
276
- <span class="glb-info-badge-dot"></span>
277
- <span class="glb-info-badge-text">${isOffline ? '离线' : '在线'}</span>
278
- <span class="glb-info-badge-scan"></span>
279
- </div>
280
- <div class="glb-info-badge-pointer"></div>
281
- `
282
-
283
- const textEl = badgeEl.querySelector('.glb-info-badge-text')
390
+ badgeEl.className = 'glb-info-badge'
391
+ badgeEl.innerHTML = buildBadgeHtml(initialInfo)
392
+ renderBadgeContent(badgeEl, initialInfo)
284
393
 
285
394
  const batchEntry = {
286
395
  updatePosition(hideByHeight) {
@@ -338,14 +447,14 @@ export function createGlbInfoBadge(options = {}) {
338
447
  batchManager.container.appendChild(badgeEl)
339
448
  batchManager.add(batchEntry)
340
449
 
450
+ const updateInfo = (info = {}) => {
451
+ renderBadgeContent(badgeEl, info)
452
+ return normalizeInfo(info)
453
+ }
454
+
341
455
  const setOfflineStatus = (offline) => {
342
- const nextOffline = Boolean(offline)
343
- badgeEl.classList.toggle('is-offline', nextOffline)
344
- badgeEl.classList.toggle('is-online', !nextOffline)
345
- if (textEl) {
346
- textEl.textContent = nextOffline ? '离线' : '在线'
347
- }
348
- return nextOffline
456
+ const currentInfo = typeof getInfo === 'function' ? getInfo() : initialInfo
457
+ return updateInfo({ ...currentInfo, isOffline: Boolean(offline) })
349
458
  }
350
459
 
351
460
  const destroy = () => {
@@ -354,6 +463,7 @@ export function createGlbInfoBadge(options = {}) {
354
463
  }
355
464
 
356
465
  return {
466
+ updateInfo,
357
467
  setOfflineStatus,
358
468
  destroy,
359
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.99",
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",