原文链接及内容

效果如下图所示:

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
const viewer = new Cesium.Viewer("cesiumContainer", {
geocoder: false,
homeButton: false,
navigationHelpButton: false,
navigationInstructionsInitiallyVisible: false,
animation: false,
timeline: false,
fullscreenButton: false,
skyBox: false,
sceneModePicker: false,
baseLayerPicker: false,
selectionIndicator: false,//禁用选中实体时的绿色指示器
});
viewer.cesiumWidget.creditContainer.style.display = "none";

// 在同一位置添加聚类标签
const numBillboards = 30;
for (let i = 0; i < numBillboards; ++i) {
//这30个实体的文字默认隐藏
const position = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883);
viewer.entities.add({
position: position,
billboard: {
image: "../images/facility.gif",
scale: 2.5,
},
label: {
text: `Label${i}`,
show: false,
font: "24px 'Maple Mono Normal NF CN'",//这里的字体用的我电脑上自己安装的字体
fillColor: Cesium.Color.RED,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
},
});
}

const scene = viewer.scene;
const camera = scene.camera;
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);

handler.setInputAction(function (movement) {
// 调用 starBurst 函数,触发广告牌分散。
starBurst(movement.position);
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

handler.setInputAction(function (movement) {
// 鼠标移动(MOUSE_MOVE):调用 updateStarBurst 函数,更新标签显示或撤销星爆。
updateStarBurst(movement.endPosition);
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

camera.moveStart.addEventListener(function () {
// 相机移动(moveStart):调用 undoStarBurst 函数,重置星爆状态。
undoStarBurst();
});

// 保存星爆效果的状态,用于跟踪交互过程中的数据。
const starBurstState = {
enabled: false, //星爆是否激活
pickedEntities: undefined,//点击时拾取的广告牌实体列表
billboardEyeOffsets: undefined,//保存广告牌的原始偏移量,用于恢复
labelEyeOffsets: undefined,//保存标签的原始偏移量,用于恢复
linePrimitive: undefined,//连接线的图元对象
radius: undefined,//星爆圆形的最大半径
center: undefined,//星爆中心位置
pixelPadding: 10.0,//广告牌之间的像素间距(10 像素)
// 星爆的角度范围(0 到 π,半圆)
angleStart: 0.0,
angleEnd: Cesium.Math.PI,
maxDimension: undefined,//广告牌的最大尺寸(宽度或高度)
};

/**
* 将广告牌和标签偏移到指定角度(angle)和距离(magnitude)的位置,生成连接线端点。
*/
function offsetBillboard(
entity,
entityPosition,
angle,
magnitude,
lines,
billboardEyeOffsets,
labelEyeOffsets,
) {
/**
* 1. 偏移计算:根据角度和距离计算屏幕空间偏移
* x = magnitude * cos(angle),y = magnitude * sin(angle)
*/
const x = magnitude * Math.cos(angle);
const y = magnitude * Math.sin(angle);

const offset = new Cesium.Cartesian2(x, y);

const drawingBufferWidth = scene.drawingBufferWidth;
const drawingBufferHeight = scene.drawingBufferHeight;
const pixelRatio = scene.pixelRatio;

const diff = Cesium.Cartesian3.subtract(
entityPosition,
camera.positionWC,
new Cesium.Cartesian3(),
);
const distance = Cesium.Cartesian3.dot(camera.directionWC, diff);

/**
* 2. 像素调整:使用相机的像素尺寸(frustum.getPixelDimensions)
* 将偏移量转换为屏幕像素,确保视觉一致性
*/
const dimensions = camera.frustum.getPixelDimensions(
drawingBufferWidth,
drawingBufferHeight,
distance,
pixelRatio,
new Cesium.Cartesian2(),
);
Cesium.Cartesian2.multiplyByScalar(
offset,
Cesium.Cartesian2.maximumComponent(dimensions),
offset,
);

let labelOffset;
const billboardOffset = entity.billboard.eyeOffset;

// 3. 设置广告牌偏移和标签偏移
const eyeOffset = new Cesium.Cartesian3(offset.x, offset.y, 0.0);
entity.billboard.eyeOffset = eyeOffset;
if (Cesium.defined(entity.label)) {
labelOffset = entity.label.eyeOffset;
entity.label.eyeOffset = new Cesium.Cartesian3(offset.x, offset.y, -10.0);
}

// 4. 计算连接线的端点(从中心到偏移后的广告牌位置),存储在 lines 数组。
const endPoint = Cesium.Matrix4.multiplyByPoint(
camera.viewMatrix,
entityPosition,
new Cesium.Cartesian3(),
);
Cesium.Cartesian3.add(eyeOffset, endPoint, endPoint);
Cesium.Matrix4.multiplyByPoint(camera.inverseViewMatrix, endPoint, endPoint);
lines.push(endPoint);

// 5. 记录原始偏移量(billboardEyeOffsets 和 labelEyeOffsets),用于恢复。
billboardEyeOffsets.push(billboardOffset);
labelEyeOffsets.push(labelOffset);
}

/**
* 在鼠标点击位置触发星爆效果,将重叠的广告牌分散排列,绘制白色连接线。
*/
function starBurst(mousePosition) {
if (Cesium.defined(starBurstState.pickedEntities)) {
return;
}

// 使用 scene.drillPick 获取鼠标位置下的所有对象,仅处理至少 2 个广告牌的情况
const pickedObjects = scene.drillPick(mousePosition);
if (!Cesium.defined(pickedObjects) || pickedObjects.length < 2) {
return;
}

// 从拾取对象中提取广告牌实体,存储在 pickedEntities
const billboardEntities = [];
let length = pickedObjects.length;
let i;

for (i = 0; i < length; ++i) {
const pickedObject = pickedObjects[i];
if (pickedObject.primitive instanceof Cesium.Billboard) {
billboardEntities.push(pickedObject);
}
}

if (billboardEntities.length === 0) {
return;
}

const pickedEntities = (starBurstState.pickedEntities = []);
const billboardEyeOffsets = (starBurstState.billboardEyeOffsets = []);
const labelEyeOffsets = (starBurstState.labelEyeOffsets = []);
const lines = [];
starBurstState.maxDimension = Number.NEGATIVE_INFINITY;

const angleStart = starBurstState.angleStart;
const angleEnd = starBurstState.angleEnd;

let angle = angleStart;
let angleIncrease;
let magnitude;
let magIncrease;
let maxDimension;

// 使用Drill pick获取鼠标指针下的所有实体。找到广告牌并以圆形模式设置它们的像素偏移量。
length = billboardEntities.length;
i = 0;
while (i < length) {
let object = billboardEntities[i];
if (pickedEntities.length === 0) {
starBurstState.center = Cesium.Cartesian3.clone(object.primitive.position);
}

if (!Cesium.defined(angleIncrease)) {
const width = object.primitive.width;
const height = object.primitive.height;
maxDimension =
Math.max(width, height) * object.primitive.scale +
starBurstState.pixelPadding;
magnitude = maxDimension + maxDimension * 0.5;
magIncrease = magnitude;
angleIncrease = maxDimension / magnitude;
}

offsetBillboard(
object.id,
object.primitive.position,
angle,
magnitude,
lines,
billboardEyeOffsets,
labelEyeOffsets,
);
pickedEntities.push(object);

const reflectedAngle = angleEnd - angle;
if (
i + 1 < length &&
reflectedAngle - angleIncrease * 0.5 > angle + angleIncrease * 0.5
) {
object = billboardEntities[++i];
offsetBillboard(
object.id,
object.primitive.position,
reflectedAngle,
magnitude,
lines,
billboardEyeOffsets,
labelEyeOffsets,
);
pickedEntities.push(object);
}

angle += angleIncrease;
if (reflectedAngle - angleIncrease * 0.5 < angle + angleIncrease * 0.5) {
magnitude += magIncrease;
angle = angleStart;
angleIncrease = maxDimension / magnitude;
}

++i;
}

// 从拾取中心向外添加线条至平移后的广告牌。
const instances = [];
length = lines.length;
for (i = 0; i < length; ++i) {
const pickedEntity = pickedEntities[i];
starBurstState.maxDimension = Math.max(
pickedEntity.primitive.width,
pickedEntity.primitive.height,
starBurstState.maxDimension,
);

instances.push(
new Cesium.GeometryInstance({
geometry: new Cesium.SimplePolylineGeometry({
positions: [starBurstState.center, lines[i]],
arcType: Cesium.ArcType.NONE,
granularity: Cesium.Math.PI_OVER_FOUR,
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.WHITE,
),
},
}),
);
}

starBurstState.linePrimitive = scene.primitives.add(
new Cesium.Primitive({
geometryInstances: instances,
appearance: new Cesium.PerInstanceColorAppearance({
flat: true,
translucent: false,
}),
asynchronous: false,
}),
);

viewer.selectedEntity = undefined;
starBurstState.radius = magnitude + magIncrease;
}

/**
*
* 根据鼠标位置更新星爆状态:
* - 如果鼠标移出星爆圆形范围或过高,撤销星爆(undoStarBurst)。
* - 如果鼠标在范围内,显示悬停广告牌的标签(showLabels)。
*/
function updateStarBurst(mousePosition) {
if (!Cesium.defined(starBurstState.pickedEntities)) {
return;
}

if (!starBurstState.enabled) {
// 由于某些原因,点击时会触发鼠标移动事件,因此不要在首次事件时显示标签。
starBurstState.enabled = true;
return;
}

// 如果鼠标离开屏幕空间圆,则移除星爆效果。如果鼠标在圆内,则显示鼠标悬停的招牌的标签。
const screenPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
scene,
starBurstState.center,
);
const fromCenter = Cesium.Cartesian2.subtract(
mousePosition,
screenPosition,
new Cesium.Cartesian2(),
);
const radius = starBurstState.radius;

if (
Cesium.Cartesian2.magnitudeSquared(fromCenter) > radius * radius ||
fromCenter.y >
3.0 * (starBurstState.maxDimension + starBurstState.pixelPadding)
) {
undoStarBurst();
} else {
showLabels(mousePosition);
}
}

function undoStarBurst() {
const pickedEntities = starBurstState.pickedEntities;
if (!Cesium.defined(pickedEntities)) {
return;
}

const billboardEyeOffsets = starBurstState.billboardEyeOffsets;
const labelEyeOffsets = starBurstState.labelEyeOffsets;

// 重置广告牌和标签的像素偏移。隐藏重叠的标签。
for (let i = 0; i < pickedEntities.length; ++i) {
const entity = pickedEntities[i].id;
entity.billboard.eyeOffset = billboardEyeOffsets[i];
if (Cesium.defined(entity.label)) {
entity.label.eyeOffset = labelEyeOffsets[i];
entity.label.show = false;
}
}

// 移除场景中的线条。释放资源并重置状态。
scene.primitives.remove(starBurstState.linePrimitive);
starBurstState.linePrimitive = undefined;
starBurstState.pickedEntities = undefined;
starBurstState.billboardEyeOffsets = undefined;
starBurstState.labelEyeOffsets = undefined;
starBurstState.radius = undefined;
starBurstState.enabled = false;
}

let currentObject;

function showLabels(mousePosition) {
const pickedObjects = scene.drillPick(mousePosition);
let pickedObject;

if (Cesium.defined(pickedObjects)) {
const length = pickedObjects.length;
for (let i = 0; i < length; ++i) {
if (pickedObjects[i].primitive instanceof Cesium.Billboard) {
pickedObject = pickedObjects[i];
break;
}
}
}

if (pickedObject !== currentObject) {
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id.label)) {
if (Cesium.defined(currentObject)) {
currentObject.id.label.show = false;
}

currentObject = pickedObject;
pickedObject.id.label.show = true;
} else if (Cesium.defined(currentObject)) {
currentObject.id.label.show = false;
currentObject = undefined;
}
}
}