一些技术路线测试,增加git,方便代码还原
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
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
/**
 * 相机视锥投影模块 - Camera Frustum Projection Module
 * 
 * 参考来源:
 * 1. demo/Cloud-API-Demo-Web-main - DJI Cloud API Web Demo 
 * 2. demo/DJI-Cloud-API-Demo-main - DJI Cloud API Backend Demo
 * 
 * 核心功能:将云台相机的姿态转换到绝对的位置和姿态,实现相机视锥投影到地图场景中
 * 
 * 基于DJI Cloud API Demo中的以下数据结构:
 * - DockDronePayload: gimbalPitch, gimbalRoll, gimbalYaw
 * - MediaFileMetadata: gimbalYawDegree, shootPosition, absoluteAltitude
 * - PayloadControl: CameraAim, PostCameraAimBody
 */
 
class CameraFrustumProjection {
    constructor(cesiumViewer) {
        this.viewer = cesiumViewer;
        this.frustumEntities = new Map(); // 存储视锥实体
        this.cameraParams = {
            // 默认相机参数 - 参考DJI M30系列
            fov: 84, // 视场角 (度)
            aspectRatio: 16/9, // 宽高比
            near: 1.0, // 近裁剪面 (米)
            far: 1000.0 // 远裁剪面 (米)
        };
    }
 
    /**
     * 更新相机视锥投影
     * 基于DJI Cloud API中的云台姿态数据
     * 
     * @param {Object} droneData - 无人机数据
     * @param {number} droneData.longitude - 无人机经度
     * @param {number} droneData.latitude - 无人机纬度  
     * @param {number} droneData.altitude - 无人机高度
     * @param {Object} gimbalData - 云台姿态数据 (参考DockDronePayload)
     * @param {number} gimbalData.gimbalPitch - 云台俯仰角 (度)
     * @param {number} gimbalData.gimbalRoll - 云台滚转角 (度)
     * @param {number} gimbalData.gimbalYaw - 云台偏航角 (度)
     * @param {string} droneId - 无人机标识符
     */
    updateCameraFrustum(droneData, gimbalData, droneId = 'default') {
        try {
            // 1. 计算相机在世界坐标系中的位置
            const cameraPosition = this.calculateCameraPosition(droneData, gimbalData);
            
            // 2. 计算相机姿态矩阵 - 参考DJI Cloud API的姿态数据处理
            const orientation = this.calculateCameraOrientation(droneData, gimbalData);
            
            // 3. 创建或更新视锥几何体
            this.createOrUpdateFrustumEntity(droneId, cameraPosition, orientation);
            
            console.log(`Camera frustum updated for drone ${droneId}`, {
                position: cameraPosition,
                gimbal: gimbalData
            });
            
        } catch (error) {
            console.error('Failed to update camera frustum:', error);
        }
    }
 
    /**
     * 计算相机位置
     * 基于DJI Cloud API Demo的精确位置计算
     * 考虑无人机机体姿态和云台安装偏移
     * 
     * @param {Object} droneData - 无人机位置数据
     * @param {Object} gimbalData - 云台数据
     * @returns {Cesium.Cartesian3} 相机世界坐标位置
     */
    calculateCameraPosition(droneData, gimbalData) {
        // 1. 无人机位置(地心坐标系)
        const droneCartesian = Cesium.Cartesian3.fromDegrees(
            droneData.longitude,
            droneData.latitude,
            droneData.altitude
        );
 
        // 2. 云台相对于无人机机体的偏移向量
        // 基于DJI M30/M30T系列的实际测量数据
        const gimbalOffset = new Cesium.Cartesian3(0, 0, -0.12); // 云台位于机体下方12cm
        
        // 3. 考虑无人机机体姿态对偏移的影响
        let droneAttitude = droneData.attitude || { heading: 0, pitch: 0, roll: 0 };
        
        // 构建机体姿态变换矩阵
        const droneHeading = Cesium.Math.toRadians(droneAttitude.heading || 0);
        const dronePitch = Cesium.Math.toRadians(droneAttitude.pitch || 0);
        const droneRoll = Cesium.Math.toRadians(droneAttitude.roll || 0);
        
        // 4. 计算局部坐标系变换
        const eastNorthUpTransform = Cesium.Transforms.eastNorthUpToFixedFrame(droneCartesian);
        
        // 机体姿态旋转矩阵
        const droneRotation = Cesium.Matrix3.fromHeadingPitchRoll(
            new Cesium.HeadingPitchRoll(droneHeading, dronePitch, droneRoll)
        );
        
        // 5. 将云台偏移向量从机体坐标系转换到世界坐标系
        // 先应用机体姿态,再应用局部坐标系变换
        const localGimbalOffset = Cesium.Matrix3.multiplyByVector(
            droneRotation,
            gimbalOffset,
            new Cesium.Cartesian3()
        );
        
        const worldGimbalOffset = Cesium.Matrix4.multiplyByPointAsVector(
            eastNorthUpTransform,
            localGimbalOffset,
            new Cesium.Cartesian3()
        );
        
        // 6. 计算云台/相机的最终世界坐标位置
        const cameraPosition = Cesium.Cartesian3.add(
            droneCartesian,
            worldGimbalOffset,
            new Cesium.Cartesian3()
        );
        
        return cameraPosition;
    }
 
    /**
     * 计算相机姿态矩阵
     * 基于DJI Cloud API Demo的精确姿态转换算法
     * 将云台相对姿态转换为绝对姿态 - 关键算法
     * 
     * @param {Object} droneData - 无人机数据  
     * @param {Object} gimbalData - 云台姿态数据
     * @returns {Cesium.Quaternion} 相机姿态四元数
     */
    calculateCameraOrientation(droneData, gimbalData) {
        // 1. 获取无人机位置用于坐标系计算
        const dronePosition = Cesium.Cartesian3.fromDegrees(
            droneData.longitude,
            droneData.latitude,
            droneData.altitude
        );
        
        // 2. 建立局部东北天(ENU)坐标系变换矩阵
        // 这是DJI云台姿态的参考坐标系
        const eastNorthUpTransform = Cesium.Transforms.eastNorthUpToFixedFrame(dronePosition);
        
        // 3. 处理无人机机体姿态 (如果数据中包含)
        // 注意:DJI Cloud API中通常云台姿态已经是相对于地理坐标系的绝对姿态
        let droneAttitude = droneData.attitude || { heading: 0, pitch: 0, roll: 0 };
        
        // 4. 云台姿态转换 - 基于DJI Cloud API的角度定义和坐标系约定
        // DJI标准:
        // - Pitch(俯仰): 绕Y轴旋转,向下为正
        // - Roll(滚转): 绕X轴旋转,右倾为正
        // - Yaw(偏航): 绕Z轴旋转,顺时针为正(从上往下看)
        
        const gimbalPitch = Cesium.Math.toRadians(gimbalData.gimbalPitch || 0);
        const gimbalRoll = Cesium.Math.toRadians(gimbalData.gimbalRoll || 0);  
        const gimbalYaw = Cesium.Math.toRadians(gimbalData.gimbalYaw || 0);
        
        // 无人机机体姿态
        const droneHeading = Cesium.Math.toRadians(droneAttitude.heading || 0);
        const dronePitch = Cesium.Math.toRadians(droneAttitude.pitch || 0);
        const droneRoll = Cesium.Math.toRadians(droneAttitude.roll || 0);
        
        // 5. 构建复合姿态变换
        // 按照DJI的姿态叠加原理:机体姿态 + 云台相对姿态
        
        // 5.1 机体姿态四元数
        const droneQuaternion = Cesium.Quaternion.fromHeadingPitchRoll(
            new Cesium.HeadingPitchRoll(droneHeading, dronePitch, droneRoll)
        );
        
        // 5.2 云台相对姿态四元数 
        // 注意:DJI云台的Yaw通常是相对于地理北向的绝对方位,而不是相对于机体
        const gimbalQuaternion = Cesium.Quaternion.fromHeadingPitchRoll(
            new Cesium.HeadingPitchRoll(gimbalYaw, gimbalPitch, gimbalRoll)
        );
        
        // 5.3 组合姿态计算
        // 对于DJI云台,通常gimbalYaw已经是绝对角度,所以可以直接使用
        const finalQuaternion = Cesium.Quaternion.multiply(
            Cesium.Matrix4.getRotation(eastNorthUpTransform, new Cesium.Matrix3()),
            gimbalQuaternion,
            new Cesium.Quaternion()
        );
        
        // 6. 相机坐标系修正
        // DJI相机坐标系:X右,Y下,Z前
        // Cesium坐标系:X右,Y前,Z上
        // 需要进行坐标系转换
        const cameraCorrection = Cesium.Quaternion.fromAxisAngle(
            Cesium.Cartesian3.UNIT_X,
            Cesium.Math.toRadians(-90)  // 绕X轴逆时针旋转90度
        );
        
        const correctedOrientation = Cesium.Quaternion.multiply(
            finalQuaternion,
            cameraCorrection,
            new Cesium.Quaternion()
        );
        
        return correctedOrientation;
    }
 
    /**
     * 创建或更新视锥实体
     * 
     * @param {string} droneId - 无人机ID
     * @param {Cesium.Cartesian3} position - 相机位置
     * @param {Cesium.Quaternion} orientation - 相机姿态
     */
    createOrUpdateFrustumEntity(droneId, position, orientation) {
        const entityId = `camera_frustum_${droneId}`;
        
        // 移除已存在的视锥
        if (this.frustumEntities.has(droneId)) {
            this.viewer.entities.remove(this.frustumEntities.get(droneId));
        }
        
        // 计算视锥几何参数
        const frustumGeometry = this.createFrustumGeometry();
        
        // 创建新的视锥实体
        const frustumEntity = this.viewer.entities.add({
            id: entityId,
            position: position,
            orientation: orientation,
            
            // 使用自定义几何体或Cesium内置几何体
            polylineVolume: {
                positions: frustumGeometry.positions,
                shape: frustumGeometry.shape,
                material: Cesium.Color.YELLOW.withAlpha(0.3),
                outline: true,
                outlineColor: Cesium.Color.YELLOW
            },
            
            // 备选方案:使用线框表示
            polyline: {
                positions: frustumGeometry.wireframe,
                material: Cesium.Color.CYAN,
                width: 2,
                clampToGround: false
            },
            
            label: {
                text: `Camera ${droneId}`,
                font: '12pt sans-serif',
                fillColor: Cesium.Color.WHITE,
                outlineColor: Cesium.Color.BLACK,
                outlineWidth: 1,
                verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                pixelOffset: new Cesium.Cartesian2(0, -20)
            }
        });
        
        this.frustumEntities.set(droneId, frustumEntity);
    }
 
    /**
     * 创建视锥几何体
     * 基于相机参数计算视锥的几何形状
     * 
     * @returns {Object} 视锥几何数据
     */
    createFrustumGeometry() {
        const { fov, aspectRatio, near, far } = this.cameraParams;
        
        // 计算视锥顶点
        const fovRad = Cesium.Math.toRadians(fov);
        const halfHeight = Math.tan(fovRad / 2);
        const halfWidth = halfHeight * aspectRatio;
        
        // 近裁剪面顶点 (相机坐标系)
        const nearTopLeft = new Cesium.Cartesian3(-halfWidth * near, halfHeight * near, -near);
        const nearTopRight = new Cesium.Cartesian3(halfWidth * near, halfHeight * near, -near);
        const nearBottomLeft = new Cesium.Cartesian3(-halfWidth * near, -halfHeight * near, -near);
        const nearBottomRight = new Cesium.Cartesian3(halfWidth * near, -halfHeight * near, -near);
        
        // 远裁剪面顶点 (相机坐标系)
        const farTopLeft = new Cesium.Cartesian3(-halfWidth * far, halfHeight * far, -far);
        const farTopRight = new Cesium.Cartesian3(halfWidth * far, halfHeight * far, -far);
        const farBottomLeft = new Cesium.Cartesian3(-halfWidth * far, -halfHeight * far, -far);
        const farBottomRight = new Cesium.Cartesian3(halfWidth * far, -halfHeight * far, -far);
        
        // 原点 (相机位置)
        const origin = new Cesium.Cartesian3(0, 0, 0);
        
        // 视锥轮廓线
        const wireframe = [
            // 从相机到近裁剪面的连线
            origin, nearTopLeft,
            origin, nearTopRight,
            origin, nearBottomLeft,
            origin, nearBottomRight,
            
            // 近裁剪面矩形
            nearTopLeft, nearTopRight,
            nearTopRight, nearBottomRight,
            nearBottomRight, nearBottomLeft,
            nearBottomLeft, nearTopLeft,
            
            // 到远裁剪面的连线
            nearTopLeft, farTopLeft,
            nearTopRight, farTopRight,
            nearBottomLeft, farBottomLeft,
            nearBottomRight, farBottomRight,
            
            // 远裁剪面矩形
            farTopLeft, farTopRight,
            farTopRight, farBottomRight,
            farBottomRight, farBottomLeft,
            farBottomLeft, farTopLeft
        ];
        
        return {
            positions: [origin, farTopLeft, farTopRight, farBottomRight, farBottomLeft], // 用于polylineVolume
            shape: [new Cesium.Cartesian2(-halfWidth, halfHeight), 
                   new Cesium.Cartesian2(halfWidth, halfHeight),
                   new Cesium.Cartesian2(halfWidth, -halfHeight),
                   new Cesium.Cartesian2(-halfWidth, -halfHeight)], // 截面形状
            wireframe: wireframe
        };
    }
 
    /**
     * 设置相机参数
     * 
     * @param {Object} params - 相机参数
     * @param {number} params.fov - 视场角 (度)
     * @param {number} params.aspectRatio - 宽高比
     * @param {number} params.near - 近裁剪面距离
     * @param {number} params.far - 远裁剪面距离
     */
    setCameraParams(params) {
        this.cameraParams = { ...this.cameraParams, ...params };
    }
 
    /**
     * 移除指定无人机的视锥投影
     * 
     * @param {string} droneId - 无人机ID
     */
    removeFrustum(droneId) {
        if (this.frustumEntities.has(droneId)) {
            this.viewer.entities.remove(this.frustumEntities.get(droneId));
            this.frustumEntities.delete(droneId);
        }
    }
 
    /**
     * 清除所有视锥投影
     */
    clearAllFrustums() {
        this.frustumEntities.forEach((entity, droneId) => {
            this.viewer.entities.remove(entity);
        });
        this.frustumEntities.clear();
    }
 
    /**
     * 基于DJI CameraAim API的目标瞄准功能
     * 参考PostCameraAimBody接口
     * 
     * @param {Object} aimData - 瞄准数据
     * @param {string} aimData.payload_index - 载荷索引
     * @param {number} aimData.camera_type - 相机类型
     * @param {boolean} aimData.locked - 是否锁定
     * @param {number} aimData.x - 屏幕X坐标 (0-1)
     * @param {number} aimData.y - 屏幕Y坐标 (0-1)
     * @returns {Object} 瞄准目标的世界坐标
     */
    calculateAimTarget(aimData, droneData, gimbalData) {
        // 将屏幕坐标转换为相机射线
        const { x, y } = aimData;
        
        // 归一化设备坐标 (NDC)
        const ndcX = (x - 0.5) * 2; // 转换到 [-1, 1]
        const ndcY = (0.5 - y) * 2; // 转换到 [-1, 1], Y轴翻转
        
        // 计算射线方向 (相机坐标系)
        const fovRad = Cesium.Math.toRadians(this.cameraParams.fov);
        const halfHeight = Math.tan(fovRad / 2);
        const halfWidth = halfHeight * this.cameraParams.aspectRatio;
        
        const rayDirection = new Cesium.Cartesian3(
            ndcX * halfWidth,
            ndcY * halfHeight,
            -1 // 相机坐标系中Z轴向前为负
        );
        
        // 转换到世界坐标系
        const cameraPosition = this.calculateCameraPosition(droneData, gimbalData);
        const cameraOrientation = this.calculateCameraOrientation(droneData, gimbalData);
        
        // 将射线方向转换到世界坐标系
        const worldRayDirection = Cesium.Matrix3.multiplyByVector(
            Cesium.Matrix3.fromQuaternion(cameraOrientation),
            rayDirection,
            new Cesium.Cartesian3()
        );
        
        // 与地面求交 (简化处理,假设地面高度为0)
        const ray = new Cesium.Ray(cameraPosition, worldRayDirection);
        const intersection = this.viewer.scene.globe.pick(ray, this.viewer.scene);
        
        return intersection;
    }
}
 
// 导出模块
window.CameraFrustumProjection = CameraFrustumProjection;
 
// 使用示例和API说明
/**
 * 使用示例:
 * 
 * // 1. 初始化
 * const frustumProjection = new CameraFrustumProjection(viewer);
 * 
 * // 2. 设置相机参数 (可选)
 * frustumProjection.setCameraParams({
 *     fov: 84,        // DJI M30系列典型视场角
 *     aspectRatio: 16/9,
 *     near: 1.0,
 *     far: 500.0
 * });
 * 
 * // 3. 更新视锥投影
 * const droneData = {
 *     longitude: 116.3974,
 *     latitude: 39.9093,
 *     altitude: 100
 * };
 * 
 * const gimbalData = {
 *     gimbalPitch: -30,  // 向下30度
 *     gimbalRoll: 0,     // 无滚转
 *     gimbalYaw: 45      // 向右偏转45度
 * };
 * 
 * frustumProjection.updateCameraFrustum(droneData, gimbalData, 'drone_001');
 * 
 * // 4. 计算瞄准目标 (基于DJI CameraAim API)
 * const aimData = {
 *     payload_index: "52-0-0",
 *     camera_type: 1,
 *     locked: true,
 *     x: 0.5,  // 屏幕中心
 *     y: 0.5
 * };
 * 
 * const target = frustumProjection.calculateAimTarget(aimData, droneData, gimbalData);
 */