cc-tools-utils 0.0.1 → 0.0.4

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.
@@ -0,0 +1,1127 @@
1
+ import {
2
+ SingleTileImageryProvider,
3
+ Rotation,
4
+ JulianDate,
5
+ Math as CesiumMath,
6
+ ScreenSpaceEventHandler,
7
+ ScreenSpaceEventType,
8
+ Transforms,
9
+ Matrix4,
10
+ Cartesian3,
11
+ HeadingPitchRoll,
12
+ SampledProperty,
13
+ Ellipsoid,
14
+ Cartesian2,
15
+ HorizontalOrigin,
16
+ VerticalOrigin,
17
+ HeightReference,
18
+ VelocityOrientationProperty,
19
+ CallbackProperty,
20
+ Color,
21
+ } from "cesium";
22
+ import * as Cesium from "cesium";
23
+
24
+ // 默认镜头高度
25
+ export const DEFAULT_HEIGHT = 25000000;
26
+ // 默认坐标,显示中国
27
+ export const DEFAULT_POSITION = [109, 35.5, DEFAULT_HEIGHT];
28
+ // 模型显示的距离阈值(米)
29
+ export const MODEL_DISPLAY_DISTANCE = 5000000;
30
+
31
+ /**
32
+ * 设置Cesium Ion访问令牌
33
+ * @param {string} token - Cesium Ion访问令牌
34
+ */
35
+ export const setCesiumAccessToken = (token) => {
36
+ if (!token || typeof token !== "string") {
37
+ console.warn("setCesiumAccessToken: token必须是非空字符串");
38
+ return;
39
+ }
40
+ Cesium.Ion.defaultAccessToken = token;
41
+ };
42
+
43
+ // 判断是否是同一个对象
44
+ export function isArrayEqual(arr1, arr2) {
45
+ const sortedArr1 = arr1.slice().sort();
46
+ const sortedArr2 = arr2.slice().sort();
47
+ return (
48
+ sortedArr1.length === sortedArr2.length &&
49
+ sortedArr1.every((value, index) => value === sortedArr2[index])
50
+ );
51
+ }
52
+
53
+ // 获取航向角
54
+ export const getHeading = (pointA, pointB) => {
55
+ const transform = Transforms.eastNorthUpToFixedFrame(pointA);
56
+ const positionVector = Cartesian3.subtract(pointB, pointA, new Cartesian3());
57
+ const vector = Matrix4.multiplyByPointAsVector(
58
+ Matrix4.inverse(transform, new Matrix4()),
59
+ positionVector,
60
+ new Cartesian3(),
61
+ );
62
+ const direction = Cartesian3.normalize(vector, new Cartesian3());
63
+ // heading
64
+ const heading = Math.atan2(direction.y, direction.x) - CesiumMath.PI_OVER_TWO;
65
+ return CesiumMath.TWO_PI - CesiumMath.zeroToTwoPi(heading);
66
+ };
67
+
68
+ /**
69
+ * 计算两点间的恒向线方位角(Rhumb Bearing)
70
+ * @param {Array} pos1 - 起点 [经度, 纬度]
71
+ * @param {Array} pos2 - 终点 [经度, 纬度]
72
+ * @returns {number} 方位角(度数)
73
+ */
74
+ export const getRhumbBearing = (pos1, pos2) => {
75
+ if (!Array.isArray(pos1) || !Array.isArray(pos2)) {
76
+ throw new Error("getRhumbBearing: 参数必须是数组");
77
+ }
78
+ if (pos1.length < 2 || pos2.length < 2) {
79
+ throw new Error("getRhumbBearing: 参数数组至少需要两个元素[经度, 纬度]");
80
+ }
81
+
82
+ const [lon1, lat1] = pos1;
83
+ const [lon2, lat2] = pos2;
84
+
85
+ // 将度数转换为弧度
86
+ const φ1 = CesiumMath.toRadians(lat1);
87
+ const φ2 = CesiumMath.toRadians(lat2);
88
+ const Δλ = CesiumMath.toRadians(lon2 - lon1);
89
+
90
+ // 计算恒向线方位角
91
+ const Δψ = Math.log(
92
+ Math.tan(φ2 / 2 + Math.PI / 4) / Math.tan(φ1 / 2 + Math.PI / 4),
93
+ );
94
+ const θ = Math.atan2(Δλ, Δψ);
95
+
96
+ // 转换为度数并标准化到0-360范围
97
+ return (CesiumMath.toDegrees(θ) + 360) % 360;
98
+ };
99
+
100
+ // 获取俯仰角
101
+ export const getPitch = (pointA, pointB) => {
102
+ const transform = Transforms.eastNorthUpToFixedFrame(pointA);
103
+ const vector = Cartesian3.subtract(pointB, pointA, new Cartesian3());
104
+ const direction = Matrix4.multiplyByPointAsVector(
105
+ Matrix4.inverse(transform, transform),
106
+ vector,
107
+ vector,
108
+ );
109
+ Cartesian3.normalize(direction, direction);
110
+ return CesiumMath.PI_OVER_TWO - CesiumMath.acosClamped(direction.z);
111
+ };
112
+
113
+ // 更新模型的位置
114
+ export const updataModelPosition = (
115
+ viewer,
116
+ { model, targetPosition, totalDuration = 2 },
117
+ ) => {
118
+ // 在每帧更新时更新实体位置
119
+ viewer.clock.onTick.addEventListener(function (clock) {
120
+ const currentTime = clock.currentTime.secondsOfDay;
121
+
122
+ if (currentTime <= totalDuration) {
123
+ // 创建一个用于存储结果的 Cartesian3 对象
124
+ const currentPosition = new Cesium.Cartesian3();
125
+ // 计算当前位置
126
+ Cesium.Cartesian3.lerp(
127
+ model.position.getValue(clock.currentTime),
128
+ targetPosition,
129
+ currentTime / totalDuration,
130
+ currentPosition,
131
+ );
132
+
133
+ // 更新实体位置
134
+ model.position.setValue(currentPosition);
135
+ } else {
136
+ // 移动完成后可以进行其他操作或停止移动
137
+ // console.log("移动完成");
138
+ }
139
+ });
140
+ };
141
+
142
+ // 笛卡尔坐标转地理位置坐标
143
+ export const transformCartesian3ToWGS84 = (cartesian3) => {
144
+ // 1. 使用 Cesium 提供的 Ellipsoid.WGS84 对象将笛卡尔坐标转换为地理坐标(经纬度高度)
145
+ let cartographic = Ellipsoid.WGS84.cartesianToCartographic(cartesian3);
146
+
147
+ // 2. 将经度(longitude)和纬度(latitude)从弧度转换为度数
148
+ let longitude = CesiumMath.toDegrees(cartographic.longitude);
149
+ let latitude = CesiumMath.toDegrees(cartographic.latitude);
150
+
151
+ // 3. 返回一个包含经度、纬度和高度的数组
152
+ return [longitude, latitude, cartographic.height];
153
+ };
154
+
155
+ // 颜色
156
+ export const toCesiumColor = (color) => {
157
+ if (!color || typeof color !== "string") {
158
+ console.warn("toCesiumColor: color必须是字符串,使用默认颜色");
159
+ return Cesium.Color.WHITE;
160
+ }
161
+ try {
162
+ return Cesium.Color.fromCssColorString(color);
163
+ } catch (error) {
164
+ console.warn(`toCesiumColor: 无效的颜色值 ${color},使用默认颜色`);
165
+ return Cesium.Color.WHITE;
166
+ }
167
+ };
168
+
169
+ // 位置
170
+ export const toCesiumPosition = (position) => {
171
+ if (!Array.isArray(position)) {
172
+ throw new Error("toCesiumPosition: position必须是数组");
173
+ }
174
+ if (position.length < 2) {
175
+ throw new Error(
176
+ "toCesiumPosition: position数组至少需要两个元素[经度, 纬度]",
177
+ );
178
+ }
179
+ if (position.some((v) => typeof v !== "number" || isNaN(v))) {
180
+ throw new Error("toCesiumPosition: position数组元素必须是有效数字");
181
+ }
182
+ return Cesium.Cartesian3.fromDegrees(...position);
183
+ };
184
+
185
+ // 返回中国视角
186
+ export const backToChina = (viewer, callback, time) => {
187
+ if (!viewer) {
188
+ throw new Error("backToChina: viewer参数是必需的");
189
+ }
190
+ viewer.camera.flyTo({
191
+ destination: toCesiumPosition(DEFAULT_POSITION),
192
+ duration: time || 1.5,
193
+ complete: () => {
194
+ callback && callback();
195
+ },
196
+ });
197
+ };
198
+
199
+ // 初始化 一切
200
+ export const initCesium = (boxID, doSomething) => {
201
+ if (!boxID) {
202
+ throw new Error("initCesium: boxID参数是必需的");
203
+ }
204
+ // 1. 创建Viewer
205
+ const viewer = new Cesium.Viewer(boxID, {
206
+ contextOptions: {
207
+ webgl: { alpha: true, powerPreference: "high-performance" },
208
+ },
209
+ // MapTiler的标准瓦片URL格式
210
+ // imageryProvider: new Cesium.UrlTemplateImageryProvider({
211
+ // url: 'https://api.maptiler.com/maps/topo-v4/{z}/{x}/{y}.png?key=dKYoUxF6uo5WfiCaaoUd',
212
+ // minimumLevel: 0,
213
+ // maximumLevel: 20,
214
+ // tileWidth: 512, // MapTiler支持512px大瓦片
215
+ // tileHeight: 512,
216
+ // credit: '© MapTiler © OpenStreetMap contributors'
217
+ // }),
218
+ // 替换为高德地图 - 选择你喜欢的样式
219
+ // imageryProvider: new Cesium.UrlTemplateImageryProvider({
220
+ // 方案1:标准街道地图(推荐,最清晰)
221
+ // url: 'https://webst01.is.autonavi.com/appmaptile?style=7&x={x}&y={y}&z={z}',
222
+ // 方案2:卫星影像(无标注)
223
+ // url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
224
+ // 方案3:卫星+路网混合
225
+ // url: 'https://webst01.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}',
226
+ // 方案4:精简版(速度快)
227
+ // url: 'https://webst01.is.autonavi.com/appmaptile?style=7&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1',
228
+ // 必要参数
229
+ // minimumLevel: 1,
230
+ // maximumLevel: 19, // 高德支持到19级
231
+ // tileWidth: 256,
232
+ // tileHeight: 256,
233
+ // credit: '高德地图',
234
+ // enablePickFeatures: false // 禁用要素拾取,提高性能
235
+ // }),
236
+
237
+ infoBox: false, // 禁用信息框(默认右下角的详情弹窗)
238
+ timeline: false, // 禁用底部时间轴控件(用于时间动画)
239
+ geocoder: false, // 禁用地理编码器(搜索位置功能)
240
+ homeButton: false, // 禁用主页按钮(返回初始视图)
241
+ sceneModePicker: true, // 禁用场景模式切换器(2D/2.5D/3D切换按钮)
242
+ baseLayerPicker: false, // 禁用底图切换器(地图图层选择控件)
243
+ navigationHelpButton: false, // 禁用导航帮助按钮(操作说明)
244
+ animation: false, // 禁用动画控件(播放/暂停时间动画)
245
+ selectionIndicator: false, // 禁用选择指示器(选中实体时的绿色框)
246
+ });
247
+ if (doSomething) {
248
+ // 用setTimeout确保在下一个事件循环执行
249
+ setTimeout(() => {
250
+ console.log("执行回调函数");
251
+ doSomething(viewer);
252
+ }, 0);
253
+ }
254
+
255
+ // 3. 返回viewer
256
+ return { viewer };
257
+ };
258
+
259
+ // 初始化时间特征 时间加快5倍数
260
+ const initTime = (viewer) => {
261
+ viewer.clock.multiplier = 1; // 时间倍数
262
+ viewer.clock.shouldAnimate = true; // 继续移动
263
+ if (viewer.animation) {
264
+ const minutes = 0 - new Date().getTimezoneOffset(); // 0 - (-480);
265
+ let animation = viewer.animation;
266
+ animation.viewModel.timeFormatter = function (date, viewModel) {
267
+ let dataZone8 = JulianDate.addMinutes(date, minutes, new JulianDate());
268
+ return JulianDate.toIso8601(dataZone8).slice(11, 19);
269
+ };
270
+ animation.viewModel.dateFormatter = function (date, viewModel) {
271
+ let dataZone8 = JulianDate.addMinutes(date, minutes, new JulianDate());
272
+ return JulianDate.toIso8601(dataZone8).slice(0, 10);
273
+ };
274
+ }
275
+ };
276
+
277
+ // 添加发散波
278
+ export const divergingWaves = (
279
+ viewer,
280
+ {
281
+ defaultPosition = [120.38017292236532, 31.480874872499374],
282
+ r = 0,
283
+ r2 = 0,
284
+ step = 2000,
285
+ max = 80000,
286
+ speed = 1,
287
+ defaultColor = "#ff0000",
288
+ id = "",
289
+ } = {},
290
+ ) => {
291
+ if (!Cesium.defined(viewer)) {
292
+ throw new Error("divergingWaves: viewer参数是必需的");
293
+ }
294
+ if (!Array.isArray(defaultPosition) || defaultPosition.length < 2) {
295
+ throw new Error("divergingWaves: defaultPosition必须是有效的坐标数组");
296
+ }
297
+ const entity = viewer.entities.add({
298
+ id,
299
+ // position: new Cesium.CallbackProperty(() => {
300
+ // return toCesiumPosition(defaultPosition);
301
+ // }, false), // 点的经纬度坐标
302
+ position: toCesiumPosition(defaultPosition), // 点的经纬度坐标
303
+ ellipse: {
304
+ semiMinorAxis: new CallbackProperty(() => {
305
+ return r < max ? (r += step * speed) : (r = 0);
306
+ }, false),
307
+ semiMajorAxis: new CallbackProperty(() => {
308
+ return r2 < max ? (r2 += step * speed) : (r2 = 0);
309
+ }, false),
310
+
311
+ height: 0,
312
+ // 设置透明度递减
313
+ material: toCesiumColor(defaultColor),
314
+ outline: false,
315
+ },
316
+ });
317
+
318
+ return entity;
319
+ };
320
+
321
+ // 添加赤道线
322
+ export const addEquatorialLine = (viewer, color = "#DC143C") => {
323
+ // 赤道数组集
324
+ const equatorialLinePositions = [
325
+ [180.0, 0.0, 100000],
326
+ [60.0, 0.0, 100000],
327
+ [-60.0, 0.0, 100000],
328
+ [-180.0, 0.0, 100000],
329
+ ];
330
+ // 将二维数组扁平化为一维数组
331
+ const flatPositions = equatorialLinePositions.reduce(
332
+ (acc, val) => acc.concat(val),
333
+ [],
334
+ );
335
+ viewer.entities.add({
336
+ id: "赤道线",
337
+ polyline: {
338
+ positions: Cesium.Cartesian3.fromDegreesArrayHeights(flatPositions),
339
+ width: 5, // 线宽
340
+ material: toCesiumColor(color), // 线的颜色
341
+ },
342
+ });
343
+ };
344
+
345
+ // @TEST 空中飞机
346
+ export const airRouteFn = (viewer) => {
347
+ const allHeight = 1000000; // 所有的高度
348
+ const clock = viewer.clock; // 时间信息
349
+ // 航线 信息
350
+ const airRoute = {
351
+ from: [102.5, 30.0, allHeight], // 起点经纬度及高度
352
+ to: [150.5, -33.0, allHeight], // 终点经纬度及高度
353
+ last: 15, // 飞行持续时间(分钟)
354
+ stopFlag: false, // 停止标志,可能用于控制飞行状态
355
+ };
356
+ //添加飞机和自身的sampler
357
+ const samplerTimeAndPosition = new SampledProperty(Cartesian3); // 采样器初始化 <时间 位置>
358
+ const samplerRotation = new SampledProperty(Rotation); // 采集容器 <旋转角度>
359
+ // 此刻时间
360
+ const startTime = clock.currentTime.clone();
361
+ // 结束时间
362
+ const endTime = JulianDate.addMinutes(
363
+ startTime,
364
+ airRoute.last,
365
+ new JulianDate(),
366
+ );
367
+ // 容器添加初始化的时间戳和经纬度
368
+ samplerTimeAndPosition.addSample(
369
+ clock.currentTime,
370
+ toCesiumPosition(airRoute.from),
371
+ );
372
+ // 生成了一个时间序列数组times,从startTime开始,每隔一分钟计算一个时间点,直到飞行结束的前一分钟
373
+ const times = Array.from({ length: airRoute.last - 1 }, (_, index) =>
374
+ JulianDate.addMinutes(startTime, index + 1, new JulianDate()),
375
+ );
376
+ // 步径
377
+ const xStep = (airRoute.to[0] - airRoute.from[0]) / airRoute.last;
378
+ const yStep = (airRoute.to[1] - airRoute.from[1]) / airRoute.last;
379
+ // 根据 步径切割 得到沿途位置
380
+ const positions = Array.from({ length: airRoute.last - 1 }, (_, index) =>
381
+ toCesiumPosition([
382
+ airRoute.from[0] + (index + 1) * xStep,
383
+ airRoute.from[1] + (index + 1) * yStep,
384
+ airRoute.from[2],
385
+ ]),
386
+ );
387
+
388
+ // 航线的 起始坐标
389
+ const airRouteLine = [
390
+ airRoute.from,
391
+ ...positions.map((item) => transformCartesian3ToWGS84(item)),
392
+ airRoute.to,
393
+ ];
394
+
395
+ // 添加 航班连线
396
+ viewer.entities.add({
397
+ id: "添加航班连线",
398
+ labelVisible: true,
399
+ polyline: {
400
+ positions: Cartesian3.fromDegreesArrayHeights(
401
+ airRouteLine.flat(Infinity),
402
+ ),
403
+ width: 2,
404
+ material: new Cesium.PolylineDashMaterialProperty({
405
+ color: toCesiumColor("#8A2BE2"),
406
+ }),
407
+ loop: true, // 闭合线条以创建完整轨道
408
+ // clampToGround: true, //是否贴地
409
+ },
410
+ });
411
+
412
+ samplerTimeAndPosition.addSamples(times, positions); // 全部点位时间
413
+ samplerTimeAndPosition.addSample(endTime, toCesiumPosition(airRoute.to)); // 结束时间 和 位置
414
+
415
+ // 处理返回 rotation的采集器信息
416
+ const createRotationSampler = () => {
417
+ // < rotation采集器 >位置
418
+ const SP_Time_Position = samplerTimeAndPosition; // sampler 它是涵盖了时间和位置信息的采集器
419
+ // 取出时间
420
+ const times = SP_Time_Position._times;
421
+ // 把对应时间 和位置 放置在采集器中
422
+ times.map((julianDate, index) => {
423
+ const pos1 = transformCartesian3ToWGS84(
424
+ SP_Time_Position.getValue(julianDate),
425
+ ).slice(0, 2);
426
+
427
+ const pos2 = times[index + 1]
428
+ ? transformCartesian3ToWGS84(
429
+ SP_Time_Position.getValue(times[index + 1]),
430
+ ).slice(0, 2)
431
+ : null;
432
+ //最后时刻没有值的情况下,保持上一个角度 将度数转换为弧度
433
+ const result = pos2
434
+ ? -CesiumMath.toRadians(getRhumbBearing(pos1, pos2))
435
+ : samplerRotation.getValue(times[index - 1]);
436
+
437
+ // 添加 <time> <postion>
438
+ samplerRotation.addSample(julianDate, result);
439
+ });
440
+
441
+ return samplerRotation;
442
+ };
443
+
444
+ // 添加客机的标签 实体
445
+ const airPlan = viewer.entities.add({
446
+ id: "空中客机",
447
+ position: samplerTimeAndPosition,
448
+ usePlain: true,
449
+ setRotation: true,
450
+ label: {
451
+ text: new CallbackProperty((time) => {
452
+ if (!samplerTimeAndPosition.getValue(time)) return "";
453
+ let [lon, lat] = transformCartesian3ToWGS84(
454
+ samplerTimeAndPosition.getValue(time),
455
+ );
456
+ // 小数会缺少0,需要补齐
457
+ return `经度:${lon.toFixed(2)}\n纬度:${lat.toFixed(2)}`;
458
+ }, false),
459
+ font: "12px Helvetica Neue",
460
+ scale: 1,
461
+ outlineWidth: 1,
462
+ showBackground: true,
463
+ backgroundColor: new Color(0.165, 0.165, 0.165, 0.5),
464
+ fillColor: Color.fromCssColorString("#59cfb5"),
465
+ pixelOffset: new Cartesian2(-20, -40),
466
+ horizontalOrigin: HorizontalOrigin.LEFT,
467
+ verticalOrigin: VerticalOrigin.CENTER,
468
+ },
469
+ billboard: {
470
+ width: 36,
471
+ height: 36,
472
+ image: "/plain.svg", // 改为 public 文件夹路径
473
+ heightReference: HeightReference.NONE,
474
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
475
+ // 图片旋转
476
+ rotation: createRotationSampler(),
477
+ },
478
+ // model: {
479
+ // uri: "./sat.glb",
480
+ // scale: 100,
481
+ // },
482
+ // 方向
483
+ orientation:
484
+ samplerTimeAndPosition.constructor === Cartesian3
485
+ ? null
486
+ : new VelocityOrientationProperty(samplerTimeAndPosition),
487
+ // 创建 转向例子
488
+ createRotationSampler: createRotationSampler(),
489
+ });
490
+
491
+ // 两边打点 起
492
+ viewer.entities.add({
493
+ id: "点位起点",
494
+ point: {
495
+ // pixelSize: 15,
496
+ },
497
+ position: toCesiumPosition(airRoute.from),
498
+ billboard: {
499
+ width: 20,
500
+ height: 25,
501
+ image: "/location.svg", // 改为 public 文件夹路径
502
+ heightReference: HeightReference.NONE,
503
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
504
+ // 图片旋转
505
+ // rotation: createRotationSampler(),
506
+ },
507
+ });
508
+ // 两边打点 终
509
+ viewer.entities.add({
510
+ id: "点位终点",
511
+ point: {
512
+ // pixelSize: 15,
513
+ },
514
+ position: toCesiumPosition(airRoute.to),
515
+ billboard: {
516
+ width: 20,
517
+ height: 25,
518
+ image: "/location.svg", // 改为 public 文件夹路径
519
+ heightReference: HeightReference.NONE,
520
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
521
+ // 图片旋转
522
+ // rotation: createRotationSampler(),
523
+ },
524
+ });
525
+
526
+ // 获取漫游位置
527
+ const getRoamingPosition = (time) => {
528
+ // viewer.trackedEntity = airPlan; //TODO 直接追踪目标
529
+ // return;
530
+ const position = airPlan.position.getValue(time); // 位置
531
+ const orientation = airPlan.orientation.getValue(time); // 方向
532
+ setCameraPosition({ position, airPlan, orientation, allHeight }, viewer);
533
+ };
534
+
535
+ // 在每次场景渲染之后触发
536
+ viewer.scene.postRender.addEventListener((scene, time) =>
537
+ getRoamingPosition(time),
538
+ );
539
+ };
540
+
541
+ // 设置相机位置
542
+ const setCameraPosition = (
543
+ { position, airPlan, orientation, allHeight },
544
+ viewer,
545
+ ) => {
546
+ if (!position) return;
547
+ // 取出对应的位置要求
548
+ const [x, y, z] = [...transformCartesian3ToWGS84(position)];
549
+ // 假设 toCesiumPosition 函数将其他位置格式转换为 Cesium.Cartesian3
550
+ const cartesianPosition = toCesiumPosition([x, y, z]); // x, y, z 是位置的坐标
551
+ // 使用 Cesium 的地理坐标转换方法
552
+ const cartographicPosition =
553
+ Cesium.Cartographic.fromCartesian(cartesianPosition);
554
+ // 获取经度和纬度
555
+ const longitude = Cesium.Math.toDegrees(cartographicPosition.longitude);
556
+ const latitude = Cesium.Math.toDegrees(cartographicPosition.latitude);
557
+ // 上帝视角
558
+ (() => {
559
+ // console.log("经度:", longitude, "纬度:", latitude);
560
+ const center = Cesium.Cartesian3.fromDegrees(longitude, latitude);
561
+ viewer.camera.lookAt(
562
+ center,
563
+ new Cesium.Cartesian3(0.0, 0, allHeight * 1.3),
564
+ );
565
+ })();
566
+ (() => {
567
+ return;
568
+ // 创建四元数
569
+ const quaternion = new Cesium.Quaternion(
570
+ orientation.w,
571
+ orientation.x,
572
+ orientation.y,
573
+ orientation.z,
574
+ );
575
+ // 将四元数转换为旋转矩阵
576
+ const rotationMatrix = Cesium.Matrix3.fromQuaternion(quaternion);
577
+
578
+ //第一视角核心代码------------
579
+ const headingPitchRoll = new Cesium.HeadingPitchRoll();
580
+ const center = Cesium.Cartesian3.fromDegrees(
581
+ longitude,
582
+ latitude,
583
+ allHeight,
584
+ );
585
+ // 位置
586
+ let transform = Cesium.Transforms.eastNorthUpToFixedFrame(center);
587
+ // 位置 转换
588
+ transform = Cesium.Matrix4.fromRotationTranslation(
589
+ Cesium.Matrix3.fromQuaternion(orientation),
590
+ center,
591
+ );
592
+ // 目标指向
593
+ viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(-100, 0, 0));
594
+ // 拉远相机视角
595
+ viewer.scene.postRender.addEventListener(() => viewer.camera.zoomOut(100));
596
+
597
+ // console.log(" airPlan :", airPlan);
598
+ })();
599
+ };
600
+
601
+ // 从旋转矩阵计算方位角
602
+ function matrix3ToHeadingPitchRoll(matrix3) {
603
+ var heading, pitch, roll;
604
+ // Extract individual rotations from the rotation matrix
605
+ pitch = Math.asin(-matrix3[7]); // pitch
606
+ roll = Math.atan2(matrix3[6], matrix3[8]); // roll
607
+ heading = Math.atan2(matrix3[1], matrix3[4]); // heading
608
+
609
+ return {
610
+ heading: heading,
611
+ pitch: pitch,
612
+ roll: roll,
613
+ };
614
+ }
615
+
616
+ // 相机 聚焦
617
+ export const cameraAnimation = (viewer, position, orientation) => {
618
+ // 带飞行动画 飞行到指定的笛卡尔坐标
619
+ viewer.camera.flyTo({
620
+ destination: Cesium.Cartesian3.fromDegrees(...position),
621
+ duration: 2, // 单位
622
+ orientation: {
623
+ // 默认(0,-90,0)
624
+ ...orientation,
625
+ },
626
+ });
627
+
628
+ // 相机 聚焦到笛卡尔坐标
629
+ // viewer.camera.setView({
630
+ // destination: position,
631
+ // orientation: {
632
+ // // 默认(0,-90,0)
633
+ // heading: Cesium.Math.toRadians(0), // 左右
634
+ // pitch: Cesium.Math.toRadians(-70), // 上下
635
+ // roll: Cesium.Math.toRadians(0), // 歪头
636
+ // },
637
+ // });
638
+
639
+ // 相机 看的指定视角 类似theejs相机lookAt
640
+ /**
641
+ * heading 、 pitch 、range
642
+ */
643
+ // viewer.camera.lookAt(
644
+ // position,
645
+ // new Cesium.HeadingPitchRange(
646
+ // Cesium.Math.toRadians(0),
647
+ // Cesium.Math.toRadians(-90),
648
+ // 4000
649
+ // )
650
+ // );
651
+ };
652
+
653
+ // 添加点 和 图片 和模型 <组合实体> billboard label line
654
+ export const addPointer = (
655
+ viewer,
656
+ {
657
+ position,
658
+ pointColor,
659
+ pointerSize,
660
+ id,
661
+ label = {},
662
+ billboard,
663
+ // 近处和远处的缩放比例
664
+ nearScale = 1.0,
665
+ farScale = 0.85,
666
+ // 距离阈值(米)
667
+ nearDistance = 1000,
668
+ farDistance = 400000000,
669
+ hoverImage,
670
+ } = {},
671
+ ) => {
672
+ if (!viewer) {
673
+ throw new Error("addPointer: viewer参数是必需的");
674
+ }
675
+ if (!position) {
676
+ throw new Error("addPointer: position参数是必需的");
677
+ }
678
+ // 构建实体配置
679
+ const entityConfig = {
680
+ id,
681
+ position: toCesiumPosition([...position]),
682
+ usePlain: true,
683
+ hoverImage: hoverImage || "",
684
+ };
685
+ // 只有在 pointColor 有值且 pointerSize > 0 时才添加 point
686
+ if (pointColor && pointColor.trim() !== "" && pointerSize > 0) {
687
+ try {
688
+ entityConfig.point = {
689
+ pixelSize: pointerSize,
690
+ color: toCesiumColor(pointColor),
691
+ };
692
+ } catch (e) {
693
+ console.warn("创建 point 失败:", e);
694
+ }
695
+ }
696
+
697
+ // 添加 label
698
+ if (label) {
699
+ entityConfig.label = {
700
+ font: "12px Helvetica Neue",
701
+ scale: 1,
702
+ outlineWidth: 1,
703
+ showBackground: true,
704
+ horizontalOrigin: HorizontalOrigin.LEFT,
705
+ verticalOrigin: VerticalOrigin.CENTER,
706
+ scaleByDistance: new Cesium.NearFarScalar(
707
+ nearDistance,
708
+ 1.0,
709
+ farDistance,
710
+ 0.2,
711
+ ),
712
+ ...label,
713
+ };
714
+ }
715
+ // 添加 billboard(图标)
716
+ if (billboard) {
717
+ entityConfig.billboard = {
718
+ heightReference: HeightReference.NONE,
719
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
720
+ scaleByDistance: new Cesium.NearFarScalar(
721
+ nearDistance, // 当距离 <= 1000米时
722
+ nearScale, // 缩放比例为 1.0
723
+ farDistance, // 当距离 >= 1000000米时
724
+ farScale, // 缩放比例为 0.1
725
+ ),
726
+ ...billboard,
727
+ };
728
+ }
729
+ // 添加实体
730
+ const pointer = viewer.entities.add(entityConfig);
731
+ return pointer;
732
+ };
733
+
734
+ // 添加线
735
+ export const addLine = (viewer) => {
736
+ // 画线
737
+ const ployLine = viewer.entities.add({
738
+ polyline: {
739
+ // 无高度
740
+ // positions: Cesium.Cartesian3.fromDegreesArray([
741
+ // 120, 20, 121, 20, 121, 20.5,
742
+ // ]),
743
+
744
+ // 有高度
745
+ // positions: Cesium.Cartesian3.fromDegreesArrayHeights([
746
+ // 120, 20, 0, 121, 20, 50, 121, 20.5, 100,
747
+ // ]),
748
+
749
+ width: 10,
750
+ material: Cesium.Color.GREEN,
751
+ },
752
+ });
753
+ // viewer.zoomTo(ployLine);
754
+ };
755
+
756
+ // 添加模型的俩种方法
757
+ export const add3DModel = (viewer, airShipPosition, cameraPosition) => {
758
+ // entities 添加模型 到cesiume
759
+ const model = viewer.entities.add({
760
+ position: airShipPosition,
761
+ name: "飞船一号",
762
+ model: {
763
+ uri: "./airship.glb",
764
+ },
765
+ });
766
+ viewer.flyTo(model); // 相机聚焦到指定的位置
767
+ // console.log("entities添加的模型:", model);
768
+
769
+ // primitives 添加模型 到cesiume
770
+ // const matrix = Cesium.Transforms.eastNorthUpToFixedFrame(cameraPosition);
771
+ // viewer.scene.primitives
772
+ // .add(
773
+ // Cesium.Model.fromGltf({
774
+ // url: "./airship.glb",
775
+ // modelMatrix: matrix,
776
+ // })
777
+ // )
778
+ // .readyPromise.then((model) => {
779
+ // // 等待模型加载完成后获取模型的位置信息
780
+ // const boundingSphere = model.boundingSphere;
781
+ // const modelPosition = boundingSphere.center;
782
+
783
+ // // 设置相机视角
784
+ // viewer.camera.setView({
785
+ // destination: modelPosition,
786
+ // orientation: {
787
+ // heading: Cesium.Math.toRadians(0), // 左右
788
+ // pitch: Cesium.Math.toRadians(-45), // 上下
789
+ // roll: Cesium.Math.toRadians(0), // 歪头
790
+ // },
791
+ // });
792
+
793
+ // console.log("primitives添加的模型: :", model);
794
+ // });
795
+ };
796
+
797
+ // 初始化 事件
798
+ export const initEvents = ({ viewer, scene, camera, entities, clock }) => {
799
+ // 添加实体和设置点击事件处理器的代码 每一帧运行
800
+ // viewer.scene.postRender.addEventListener(updatePosition());
801
+ // viewer.scene.preRender.addEventListener(updatePosition());
802
+ // 禁用双击事件
803
+ viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(
804
+ ScreenSpaceEventType.LEFT_DOUBLE_CLICK,
805
+ );
806
+
807
+ let allEntitiesIds;
808
+ // 监听实体添加
809
+ entities.collectionChanged.addEventListener((collection, added, removed) => {
810
+ let old = allEntitiesIds;
811
+ let current = collection.values.map((item) => item.id);
812
+ if (!isArrayEqual(old, current)) {
813
+ allEntitiesIds = entities.values.map((item) => item.id);
814
+ }
815
+ });
816
+
817
+ //实体点击事件
818
+ const handler = new ScreenSpaceEventHandler(scene.canvas);
819
+ handler.setInputAction(
820
+ (e) => emitClickEvent(e, ScreenSpaceEventType.LEFT_CLICK),
821
+ ScreenSpaceEventType.LEFT_CLICK,
822
+ );
823
+ // 左键双击
824
+ // handler.setInputAction(
825
+ // (e) => emitClickEvent(e, ScreenSpaceEventType.LEFT_DOUBLE_CLICK),
826
+ // ScreenSpaceEventType.LEFT_DOUBLE_CLICK
827
+ // );
828
+ // 右键
829
+ // handler.setInputAction(
830
+ // (e) => emitClickEvent(e, ScreenSpaceEventType.RIGHT_CLICK),
831
+ // ScreenSpaceEventType.RIGHT_CLICK
832
+ // );
833
+ const emitClickEvent = (event, type) => {
834
+ // 直接输出可复制的格式
835
+ const currentHeading = Cesium.Math.toDegrees(viewer.camera.heading);
836
+ const currentPitch = Cesium.Math.toDegrees(viewer.camera.pitch);
837
+ const currentRoll = Cesium.Math.toDegrees(viewer.camera.roll);
838
+
839
+ const cameraPosition = viewer.camera.positionCartographic;
840
+ const longitude = Cesium.Math.toDegrees(cameraPosition.longitude);
841
+ const latitude = Cesium.Math.toDegrees(cameraPosition.latitude);
842
+ const height = cameraPosition.height;
843
+
844
+ // 输出可以直接复制粘贴的代码
845
+ console.log(`位置: [${longitude}, ${latitude}, ${height}]`);
846
+ console.log(
847
+ `角度: { heading: ${currentHeading}, pitch: ${currentPitch}, roll: ${currentRoll} }`,
848
+ );
849
+
850
+ // id模型 primitive模型的集合图形材质
851
+ const { id, primitive } = scene.pick(event.position) || {
852
+ id: undefined,
853
+ primitive: undefined,
854
+ };
855
+ if (id) {
856
+ console.log(" 点击的模型 :", id);
857
+ }
858
+ };
859
+
860
+ const ellipsoid = Cesium.Ellipsoid.WGS84; // 使用默认的WGS84地球椭球体
861
+ // 鼠标hover位置
862
+ handler.setInputAction((movement) => {
863
+ const cartesian = viewer.camera.pickEllipsoid(
864
+ movement.endPosition,
865
+ ellipsoid,
866
+ );
867
+ if (cartesian) {
868
+ const cartographic =
869
+ viewer.scene.globe.ellipsoid.cartesianToCartographic(cartesian);
870
+ //将地图坐标(弧度)转为十进制的度数
871
+ const lat_String = Cesium.Math.toDegrees(cartographic.latitude).toFixed(
872
+ 4,
873
+ ); //经
874
+ const log_String = Cesium.Math.toDegrees(cartographic.longitude).toFixed(
875
+ 4,
876
+ ); //纬
877
+ const alti_String = (
878
+ viewer.camera.positionCartographic.height / 1000
879
+ ).toFixed(2); //高
880
+
881
+ // console.log("鼠标位置经纬度:", log_String, lat_String, alti_String);
882
+ }
883
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
884
+
885
+ return allEntitiesIds;
886
+ };
887
+
888
+ // 渲染更新 位置
889
+ export const followModel = (viewer, options) => {
890
+ const { targetModel, selfModel, startPosition, endPosition } = options;
891
+ // 获取当前时间
892
+ const currentTime = Cesium.JulianDate.now();
893
+ // 获取猎物和自身的初始位置
894
+ const initialTargetPosition = targetModel._position.getValue(currentTime);
895
+ const initialOwnPosition = selfModel._position.getValue(currentTime);
896
+
897
+ // 使用 CallbackProperty 更新位置
898
+ targetModel._position = new Cesium.CallbackProperty(() => {
899
+ initialTargetPosition.x += 100;
900
+ return initialTargetPosition;
901
+ }, false);
902
+
903
+ selfModel._posistion = new Cesium.CallbackProperty(() => {
904
+ // 计算自身位置,例如通过中点
905
+ return Cartesian3.midpoint(
906
+ toCesiumPosition(startPosition),
907
+ initialTargetPosition, // 获取当前猎物位置
908
+ new Cartesian3(),
909
+ );
910
+ }, false);
911
+
912
+ selfModel._orientation = new Cesium.CallbackProperty(() => {
913
+ const satellitePosition = toCesiumPosition(startPosition); // 光波头
914
+ const targetPosition = initialTargetPosition; // 光波尾部
915
+ // 获取航向角
916
+ const h = getHeading(satellitePosition, targetPosition);
917
+ // 获取俯仰角
918
+ const p = getPitch(satellitePosition, targetPosition);
919
+ const hpr = new HeadingPitchRoll(
920
+ CesiumMath.toRadians(90),
921
+ CesiumMath.toRadians(90),
922
+ CesiumMath.toRadians(0),
923
+ );
924
+ hpr.pitch -= p;
925
+ hpr.heading += h;
926
+ return Transforms.headingPitchRollQuaternion(satellitePosition, hpr);
927
+ }, false);
928
+
929
+ selfModel._cylinder.length = new Cesium.CallbackProperty(() => {
930
+ const satellitePosition = toCesiumPosition(startPosition);
931
+ const targetPosition = initialTargetPosition;
932
+ return Cesium.Cartesian3.distance(satellitePosition, targetPosition);
933
+ }, false);
934
+
935
+ // 监听每帧更新
936
+ viewer.scene.postRender.addEventListener(() => {});
937
+ };
938
+
939
+ /**
940
+ * @description: 点聚合功能效果
941
+ * @param {*} viewer source:资源文件 image:主图 imgs:聚合图标数组
942
+ * @return {*}
943
+ */
944
+ export const initCluster = (viewer, { source, image: mainImage, imgs }) => {
945
+ const [A1, B1, C1, D1] = imgs;
946
+ // return;
947
+ // TODO: 加载geojson数据
948
+ new Cesium.GeoJsonDataSource().load(source).then((dataSource) => {
949
+ viewer.dataSources.add(dataSource);
950
+ // 设置聚合参数
951
+ dataSource.clustering.enabled = true;
952
+ dataSource.clustering.pixelRange = 60;
953
+ dataSource.clustering.minimumClusterSize = 2;
954
+
955
+ // foreach用于调用数组的每个元素,并将元素传递给回调函数。
956
+ dataSource.entities.values.forEach((entity) => {
957
+ console.log(" 每一项entity :", entity);
958
+
959
+ // 将点拉伸一定高度,防止被地形压盖
960
+ entity.position._value.z += 50.0;
961
+ // 使用大小为64*64的icon,缩小展示poi
962
+ entity.billboard = {
963
+ image: mainImage,
964
+ width: 32,
965
+ height: 32,
966
+ };
967
+ entity.label = {
968
+ text: "POI",
969
+ font: "bold 15px Microsoft YaHei",
970
+ // 竖直对齐方式
971
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
972
+ // 水平对齐方式
973
+ horizontalOrigin: Cesium.HorizontalOrigin.LEFT,
974
+ // 偏移量
975
+ pixelOffset: new Cesium.Cartesian2(15, 0),
976
+ };
977
+ });
978
+
979
+ // 添加监听函数
980
+ dataSource.clustering.clusterEvent.addEventListener(
981
+ function (clusteredEntities, cluster) {
982
+ // 关闭自带的显示聚合数量的标签
983
+ cluster.label.show = false;
984
+ cluster.billboard.show = true;
985
+ cluster.billboard.verticalOrigin = Cesium.VerticalOrigin.BOTTOM;
986
+
987
+ // 根据聚合数量的多少设置不同层级的图片以及大小
988
+ if (clusteredEntities.length >= 20) {
989
+ cluster.billboard.image = combineIconAndLabel(
990
+ D1,
991
+ clusteredEntities.length,
992
+ 64,
993
+ );
994
+ cluster.billboard.width = 72;
995
+ cluster.billboard.height = 72;
996
+ } else if (clusteredEntities.length >= 12) {
997
+ cluster.billboard.image = combineIconAndLabel(
998
+ C1,
999
+ clusteredEntities.length,
1000
+ 64,
1001
+ );
1002
+ cluster.billboard.width = 56;
1003
+ cluster.billboard.height = 56;
1004
+ } else if (clusteredEntities.length >= 8) {
1005
+ cluster.billboard.image = combineIconAndLabel(
1006
+ B1,
1007
+ clusteredEntities.length,
1008
+ 64,
1009
+ );
1010
+ cluster.billboard.width = 48;
1011
+ cluster.billboard.height = 48;
1012
+ } else {
1013
+ cluster.billboard.image = combineIconAndLabel(
1014
+ A1,
1015
+ clusteredEntities.length,
1016
+ 64,
1017
+ );
1018
+ cluster.billboard.width = 40;
1019
+ cluster.billboard.height = 40;
1020
+ }
1021
+ },
1022
+ );
1023
+ });
1024
+ };
1025
+
1026
+ /**
1027
+ * @description: 将图片和文字合成新图标使用(参考Cesium源码)
1028
+ * @param {*} url:图片地址
1029
+ * @param {*} label:文字
1030
+ * @param {*} size:画布大小
1031
+ * @return {*} 返回canvas
1032
+ */
1033
+ export const combineIconAndLabel = (url, label, size) => {
1034
+ // 创建画布对象
1035
+ let canvas = document.createElement("canvas");
1036
+ canvas.width = size;
1037
+ canvas.height = size;
1038
+ let ctx = canvas.getContext("2d");
1039
+
1040
+ let promise = new Cesium.Resource.fetchImage(url).then((image) => {
1041
+ // 异常判断
1042
+ try {
1043
+ ctx.drawImage(image, 0, 0);
1044
+ } catch (e) {
1045
+ console.log(e);
1046
+ }
1047
+
1048
+ // 渲染字体
1049
+ // font属性设置顺序:font-style, font-variant, font-weight, font-size, line-height, font-family
1050
+ ctx.fillStyle = Cesium.Color.WHITE.toCssColorString();
1051
+ ctx.font = "bold 20px Microsoft YaHei";
1052
+ ctx.textAlign = "center";
1053
+ ctx.textBaseline = "middle";
1054
+ ctx.fillText(label, size / 2, size / 2);
1055
+
1056
+ return canvas;
1057
+ });
1058
+ return promise;
1059
+ };
1060
+
1061
+ /**
1062
+ * 最简单的悬停功能 - 只有30行代码
1063
+ * @param {Viewer} viewer Cesium Viewer
1064
+ * @param {Function} onHover 悬停回调
1065
+ */
1066
+ export const addHover = (viewer, onHover) => {
1067
+ const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
1068
+ let hoverTimer = null;
1069
+ let currentEntity = null;
1070
+ handler.setInputAction((movement) => {
1071
+ const picked = viewer.scene.pick(movement.endPosition);
1072
+ if (picked && picked.id) {
1073
+ const entity = picked.id;
1074
+ if (currentEntity !== entity) {
1075
+ // 清除上一个悬停
1076
+ if (hoverTimer) clearTimeout(hoverTimer);
1077
+ if (currentEntity && currentEntity.billboard) {
1078
+ // currentEntity.billboard.scale = 1.0
1079
+ // 恢复图片
1080
+ if (currentEntity._originalImage) {
1081
+ currentEntity.billboard.image = currentEntity._originalImage;
1082
+ delete currentEntity._originalImage;
1083
+ }
1084
+ }
1085
+ // 设置新悬停
1086
+ currentEntity = entity;
1087
+ viewer.scene.canvas.style.cursor = "pointer";
1088
+ hoverTimer = setTimeout(() => {
1089
+ if (entity.billboard) {
1090
+ // entity.billboard.scale = 1.2
1091
+ // 如果有hoverImage就替换
1092
+ if (entity.hoverImage) {
1093
+ // 保存原始图片
1094
+ entity._originalImage = entity.billboard.image;
1095
+ // 替换为hover图片
1096
+ entity.billboard.image = entity.hoverImage;
1097
+ }
1098
+ }
1099
+ if (onHover) onHover(entity);
1100
+ }, 300);
1101
+ }
1102
+ } else {
1103
+ // 没有悬停
1104
+ if (hoverTimer) clearTimeout(hoverTimer);
1105
+ if (currentEntity && currentEntity.billboard) {
1106
+ // currentEntity.billboard.scale = 1.0
1107
+ // 恢复图片
1108
+ if (currentEntity._originalImage) {
1109
+ currentEntity.billboard.image = currentEntity._originalImage;
1110
+ delete currentEntity._originalImage;
1111
+ }
1112
+ }
1113
+ viewer.scene.canvas.style.cursor = "default";
1114
+ currentEntity = null;
1115
+ }
1116
+ }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
1117
+
1118
+ // 返回清理函数
1119
+ return () => {
1120
+ handler.destroy();
1121
+ if (hoverTimer) clearTimeout(hoverTimer);
1122
+ };
1123
+ };
1124
+
1125
+ export const cc = () => {
1126
+ return 1;
1127
+ };