无人机轨迹回放

5 阅读18分钟
import * as Cesium from "cesium";
/**
 * 无人机轨迹点接口
 * 表示无人机在某个时间点的状态
 */
export interface UAVTrackPoint {
  altitude: number; // 高度(米)
  latitude: number; // 纬度(度)
  longitude: number; // 经度(度)
  timestamp: string; // 时间戳(ISO 8601 格式)
  isAbnormal?: boolean; // 是否为异常节点
  heading: number; // 航向角(度)
  pitchAngle?: number; // 俯角(度)
  rollAngle?: number; // 仰角(度)
  remainingTime?: number; // 剩余时间(秒)
  remainingDistance?: number; // 剩余距离(米)
  flightSpeed?: number; // 飞行速度(米/秒)
  totalDistance?: number; // 总距离(米)
  totalTime?: number; // 总时间(秒)
  realtimeMonitorItem?: any;
  weatherItem?: any;
  communicationItem?: any;
}

/**
 * 无人机类型枚举
 */
export enum UAVType {
  IMAGE = 'image', // 使用图片渲染
  MODEL = 'model', // 使用 3D 模型渲染
}

/**
 * 无人机轨迹工具配置选项接口
 * 用于配置轨迹渲染和回放的各种参数
 */
export interface UAVUtilOptions {
  uavType?: UAVType; // 无人机渲染类型
  uavIcon?: string; // 无人机图标路径
  uavIconSize?: [number, number]; // 无人机图标大小 [width, height]
  uavModel?: string; // 无人机 3D 模型路径
  uavModelScale?: number; // 无人机 3D 模型缩放比例
  pathColor?: Cesium.Color; // 路径颜色
  pathWidth?: number; // 路径宽度
  keyPointIcon?: string; // 关键节点图标路径
  keyPointSize?: [number, number]; // 关键节点图标大小 [width, height]
  keyPointInterval?: number; // 关键节点间隔(秒)
  abnormalPointIcon?: string; // 异常节点图标路径
  abnormalPointSize?: [number, number]; // 异常节点图标大小 [width, height]
  playbackSpeed?: number; // 回放速度
  panelOffset?: [number, number]; // 面板偏移量 [x, y]
  abnormalPanelOffset?: [number, number]; // 异常点面板偏移量 [x, y]
  enableViewModeToggle?: boolean; // 是否启用切换视角功能
  onProgress?: (progress: number, currentTime: Cesium.JulianDate) => void; // 进度回调函数
}

/**
 * 回放进度接口
 * 包含轨迹回放的详细进度信息
 */
export interface PlaybackProgress {
  percentage: number; // 进度百分比(0-1)
  currentTime: Cesium.JulianDate; // 当前时间
  startTime: Cesium.JulianDate; // 开始时间
  endTime: Cesium.JulianDate; // 结束时间
  currentPointIndex: number; // 当前轨迹点索引
  totalPoints: number; // 总轨迹点数量
}

/**
 * 窗口管理器类型
 * 扩展了 Window 接口,添加了项目特定的全局变量
 */
type WindowManagers = Window & {
  _viewer?: Cesium.Viewer; // Cesium Viewer 实例
  _objectManager?: { // 对象管理器
    addObjGroup: (objGroup: string, groups: any[]) => void; // 添加对象组
    removeObjectsByGroup: (group: string) => void; // 按组移除对象
  };
};
// 无人机图标路径
const UVA_ICON = new URL("@/assets/images/flightScenarioRefactoring/uav.png", import.meta.url).href;
// 正常节点图标路径
const NORMAL_ICON = new URL("@/assets/images/flightScenarioRefactoring/normalNode.png", import.meta.url).href;
// 异常节点图标路径
const ABNORMAL_ICON = new URL("@/assets/images/flightScenarioRefactoring/warningNode.png", import.meta.url).href;
const DIALOG_BG = new URL("@/assets/images/flightScenarioRefactoring/uavDialogBg.png", import.meta.url).href;
const MODEL_URL = new URL("/data/tb2.glb", import.meta.url).href;

/**
 * 无人机轨迹工具类
 * 功能:静态渲染无人机和路径、轨迹模拟回放、无人机面板跟随显示、路径关键节点渲染、回放进度计算和回调
 */
export class UAVUtil {
  private viewer: Cesium.Viewer | null = null; // Cesium  viewer 实例
  private uavEntity: Cesium.Entity | null = null; // 无人机实体
  private pathEntity: Cesium.Entity | null = null; // 路径实体
  private keyPointEntities: Cesium.Entity[] = []; // 关键节点实体数组
  private panelEntity: Cesium.Entity | null = null; // 面板实体
  private clock: Cesium.Clock | null = null; // 时钟实例,用于控制回放
  private isPlaying = false; // 回放状态
  private currentTrackData: UAVTrackPoint[] = []; // 当前轨迹数据
  private options: Required<UAVUtilOptions>; // 工具配置选项
  private groupName = "uav-track"; // 实体分组名称
  private panelContent: { name: string; value: string }[] = []; // 面板内容
  private panelTitle = "详情"; // 面板标题
  private panelElement: HTMLDivElement | null = null; // 面板 DOM 元素

  private startTime: Cesium.JulianDate | null = null; // 轨迹开始时间
  private endTime: Cesium.JulianDate | null = null; // 轨迹结束时间
  private progressCallback: ((progress: number, currentTime: Cesium.JulianDate) => void) | null = null; // 进度回调函数
  private cameraUpdateCallback: ((event: any) => void) | null = null; // 相机更新回调函数引用
  private isFirstPersonView = false; // 当前是否为第一人称视角
  private abnormalPointPanels: { // 异常点面板数组,用于存储面板、实体和回调的关联
    entity: Cesium.Entity;
    panel: HTMLDivElement;
    updateCallback: (event: any) => void;
  }[] = [];

  /**
   * 构造函数
   * @param options 工具配置选项
   *  - uavIcon: 无人机图标路径
   *  - uavIconSize: 无人机图标大小 [width, height]
   *  - pathColor: 路径颜色
   *  - pathWidth: 路径宽度
   *  - keyPointIcon: 关键节点图标路径
   *  - keyPointSize: 关键节点图标大小 [width, height]
   *  - keyPointInterval: 关键节点间隔(秒)
   *  - playbackSpeed: 回放速度
   *  - panelOffset: 面板偏移量 [x, y]
   *  - panelBackgroundColor: 面板背景颜色
   *  - panelTextColor: 面板文本颜色
   *  - onProgress: 进度回调函数
   */
  constructor(options?: UAVUtilOptions) {
    this.options = {
      uavType: UAVType.IMAGE, // 默认使用图片渲染
      uavIcon: UVA_ICON, // 默认无人机图标
      uavIconSize: [40, 40], // 默认无人机图标大小
      uavModel: MODEL_URL, // 默认无 3D 模型
      // uavModelScale: 0.01, // 默认模型缩放比例
      uavModelScale: 10, // 默认模型缩放比例
      pathColor: Cesium.Color.fromBytes(45, 165, 241, 255), // 默认路径颜色
      pathWidth: 3, // 默认路径宽度
      keyPointIcon: NORMAL_ICON, // 默认关键节点图标
      keyPointSize: [10, 10], // 默认关键节点图标大小
      keyPointInterval: 10, // 默认关键节点间隔
      abnormalPointIcon: ABNORMAL_ICON, // 默认异常节点图标
      abnormalPointSize: [10, 10], // 默认异常节点图标大小
      playbackSpeed: 1, // 默认回放速度
      panelOffset: [-150, -180], // 默认面板偏移量
      abnormalPanelOffset: [-120, -200], // 默认异常点面板偏移量
      enableViewModeToggle: false, // 默认启用切换视角功能
      onProgress: () => { }, // 默认空进度回调
      ...options, // 覆盖默认选项
    };

    // 绑定相机更新回调函数
    this.cameraUpdateCallback = this.updateCameraPosition.bind(this);

    // 初始化鼠标事件处理器
    this.initMouseHandler();
  }

  /**
   * 获取 Cesium Viewer 实例
   * @returns Cesium Viewer 实例,如果不存在则返回 null
   * 说明:优先使用缓存的 viewer 实例,若不存在则从 window._viewer 获取
   */
  private getViewer(): Cesium.Viewer | null {
    if (!this.viewer) {
      this.viewer = (window as WindowManagers)._viewer || null; // 从全局变量获取 viewer
    }
    return this.viewer;
  }
  /**
   * 初始化鼠标事件处理器
   * 功能:
   * 1. 创建鼠标事件处理器
   * 2. 添加鼠标点击事件监听器
   * 3. 处理异常点的点击事件
   */
  private initMouseHandler(): void {
    const viewer = this.getViewer();
    if (!viewer) return;
    // 鼠标点击
    const handler = new Cesium.ScreenSpaceEventHandler(
      viewer.scene.canvas,
    );
    handler.setInputAction((movement) => {
      // 检测点击位置是否有实体
      const pickedObject = viewer.scene.pick(movement.position);
      if (Cesium.defined(pickedObject) && pickedObject.id) {
        const entity = pickedObject.id;
        // 检查是否为异常点
        if (entity.id && typeof entity.id === 'string' && entity.id.includes('-abnormal-point-')) {
          // 显示异常点信息面板
          this.showAbnormalPointPanel(entity);
        }
      }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  }


  /**
   * 清理异常点面板
   * 功能:
   * 1. 移除所有异常点面板元素
   * 2. 移除对应的场景渲染回调
   * 3. 清空异常点面板数组
   */
  private clearAbnormalPointPanels(): void {
    const viewer = this.getViewer();
    if (!viewer) return;

    // 移除所有异常点面板和对应的事件监听器
    this.abnormalPointPanels.forEach(({ panel, updateCallback }) => {
      // 移除面板元素
      if (panel && panel.parentNode) {
        panel.parentNode.removeChild(panel);
      }
      // 移除事件监听器
      viewer.scene.postRender.removeEventListener(updateCallback);
    });

    // 清空数组
    this.abnormalPointPanels = [];
  }

  /**
   * 验证异常点实体和数据
   * @param entity 异常点实体
   * @returns 验证结果,包含异常点数据和索引
   */
  private validateAbnormalPointEntity(entity: Cesium.Entity): { valid: boolean; abnormalPoint?: UAVTrackPoint; index?: number } {
    if (!this.currentTrackData || this.currentTrackData.length === 0) {
      return { valid: false };
    }

    // 检查是否为同一个异常点重复创建面板
    const entityId = entity.id as string;
    const existingPanelIndex = this.abnormalPointPanels.findIndex(panelInfo => panelInfo.entity.id === entityId);
    if (existingPanelIndex !== -1) {
      return { valid: false };
    }

    // 从实体 ID 中提取索引
    const match = entityId.match(/-abnormal-point-(\d+)/);
    if (!match) {
      return { valid: false };
    }

    const index = parseInt(match[1], 10);
    if (isNaN(index) || index < 0 || index >= this.currentTrackData.length) {
      return { valid: false };
    }

    const abnormalPoint = this.currentTrackData[index];
    if (!abnormalPoint) {
      return { valid: false };
    }

    return { valid: true, abnormalPoint, index };
  }

  /**
   * 生成异常点面板内容
   * @param abnormalPoint 异常点数据
   * @returns 面板内容数据
   */
  private generateAbnormalPointPanelContent(abnormalPoint: UAVTrackPoint) {
    const { communicationItem, realtimeMonitorItem, weatherItem } = abnormalPoint || {};

    const panelContent1 = [
      { name: "电池电压", value: realtimeMonitorItem?.batteryVoltageName },
      { name: "电池电流", value: realtimeMonitorItem?.batteryCurrentName },
      { name: "电池温度", value: realtimeMonitorItem?.batteryTemperatureName },
      { name: "电机转速", value: realtimeMonitorItem?.motorSpeedName },
      { name: "电机温度", value: realtimeMonitorItem?.motorTemperatureStatusName },
      { name: "电机电流", value: realtimeMonitorItem?.motorCurrentName },
    ];

    const panelContent2 = [
      { name: "风速", value: `${weatherItem?.windSpeed || 0}m/s` },
      { name: "风向", value: weatherItem?.windDirection },
      { name: "能见度", value: `${weatherItem?.visibility || 0}km` },
      { name: "降水量", value: `${weatherItem?.precipitation || 0}mm` },
      { name: "温度", value: `${weatherItem?.temperature || 0}℃` },
    ];

    const panelContent3 = [
      { name: "油门指令值", value: communicationItem?.throttleCommand },
      { name: "偏航指令值", value: communicationItem?.yawCommand },
      { name: "横滚指令值", value: communicationItem?.rollCommand },
      { name: "俯仰指令值", value: communicationItem?.pitchCommand },
      { name: "模式开关状态", value: communicationItem?.modeSwitchStatus },
      { name: "功能按键状态", value: communicationItem?.functionButtonStatus },
      { name: "拔杆位置", value: communicationItem?.leverPosition },
      { name: "指令时间戳", value: communicationItem?.commandTimestamp },
      { name: "指令响应延迟", value: communicationItem?.commandResponseDelay },
      { name: "指令执行确认", value: communicationItem?.commandExecutionConfirm },
    ];

    return { panelContent1, panelContent2, panelContent3 };
  }

  /**
   * 创建异常点面板内容行
   * @param item 内容项
   * @returns 创建的行元素
   */
  private createContentRow(item: { name: string; value: string }): HTMLDivElement {
    const row = document.createElement('div');
    row.style.cssText = `
     display: flex; 
     justify-content: space-between;
    `;

    const label = document.createElement('span');
    label.textContent = item.name;
    label.style.cssText = `
      white-space: nowrap;
    `;

    const value = document.createElement('span');
    value.textContent = item.value;
    value.style.cssText = `
      margin-left: 12px;
    `;

    row.appendChild(label);
    row.appendChild(value);
    return row;
  }

  /**
   * 创建异常点面板
   * @param entity 异常点实体
   * @param contentData 面板内容数据
   * @param viewer Cesium Viewer 实例
   * @returns 创建的面板元素
   */
  private createAbnormalPointPanel(contentData: any, viewer: Cesium.Viewer): HTMLDivElement {
    const { panelContent1, panelContent2, panelContent3 } = contentData;

    // 创建面板元素
    const panelElement = document.createElement('div');
    panelElement.className = 'abnormal-point-panel';

    // 设置面板样式
    panelElement.style.cssText = `
      position: fixed;
      border-radius: 6px;
      background: rgba(5, 20, 40, 0.95);
      border: 2px solid rgba(61, 90, 254, 1);
      box-shadow: inset 0px 0px 24px rgba(61, 90, 254, 1);
      font-size: 14px;
      font-weight: 400;
      color: rgba(156, 163, 175, 1);
      z-index: 1000;
      min-width: 300px;
      max-width: 500px;
    `;

    // 添加面板标题
    const title = document.createElement('div');
    title.innerHTML = `
   <div style=" 
      font-weight: bold; 
      font-size: 16px;
      line-height: 36px;
      color: rgba(255, 255, 255, 1);
      font-family: youshebiaotihei;
      background: linear-gradient(270deg, #42c6ff 0%, #ffffff 100%);
      background-clip: text;
      color: transparent;"
      >异常点信息</div>
    </div>
  `;
    title.style.cssText = `
      height: 36px;
      background: url(${DIALOG_BG});
      background-size: 100% 100%;
      padding:0 10px 0 27px;
      display:flex;
      align-items: center;
      justify-content: space-between;
      gap: 20px;
    `;

    // 添加关闭按钮
    const closeButton = document.createElement('button');
    closeButton.textContent = 'x';
    closeButton.style.cssText = `
      color: white;
      border: none;
      cursor: pointer;
      font-size: 18px;
      font-weight: 400;
      line-height: 30px;
    `;

    closeButton.addEventListener('click', () => {
      // 只关闭当前点击的面板
      const index = this.abnormalPointPanels.findIndex(panelInfo => panelInfo.panel === panelElement);
      if (index !== -1) {
        const { panel, updateCallback } = this.abnormalPointPanels[index];
        // 移除面板元素
        if (panel && panel.parentNode) {
          panel.parentNode.removeChild(panel);
        }
        // 移除事件监听器
        viewer.scene.postRender.removeEventListener(updateCallback);
        // 从数组中移除
        this.abnormalPointPanels.splice(index, 1);
      }
    });

    title.appendChild(closeButton);
    panelElement.appendChild(title);

    // 添加标签页
    const tabBox = document.createElement('div');
    tabBox.style.cssText = `
      display: flex;
      justify-content: space-between;
      gap: 20px;
      padding: 10px 20px 5px;
    `;

    const tab1 = document.createElement('div');
    tab1.textContent = '设备运行状态';
    tab1.style.cssText = `
      cursor: pointer;
      border-radius: 6px;
      color: rgb(9, 175, 254);
    `;

    const tab2 = document.createElement('div');
    tab2.textContent = '气象数据';
    tab2.style.cssText = `
      cursor: pointer;
      border-radius: 6px;
    `;

    const tab3 = document.createElement('div');
    tab3.textContent = '通信记录';
    tab3.style.cssText = `
      cursor: pointer;
      border-radius: 6px;
    `;

    tabBox.appendChild(tab1);
    tabBox.appendChild(tab2);
    tabBox.appendChild(tab3);
    panelElement.appendChild(tabBox);

    // 添加面板内容
    const content1 = document.createElement('div');
    content1.style.cssText = `
     padding: 12px 20px;
    `;

    const content2 = document.createElement('div');
    content2.style.cssText = `
     padding: 12px 20px;
     display: none;
    `;

    const content3 = document.createElement('div');
    content3.style.cssText = `
     padding: 12px 20px;
     display: none;
    `;

    // 填充内容
    panelContent1.forEach(item => content1.appendChild(this.createContentRow(item)));
    panelContent2.forEach(item => content2.appendChild(this.createContentRow(item)));
    panelContent3.forEach(item => content3.appendChild(this.createContentRow(item)));

    panelElement.appendChild(content1);
    panelElement.appendChild(content2);
    panelElement.appendChild(content3);

    // 添加标签页切换事件
    tab1.addEventListener('click', () => {
      content1.style.display = 'block';
      content2.style.display = 'none';
      content3.style.display = 'none';
      tab1.style.color = 'rgb(9, 175, 254)';
      tab2.style.color = 'inherit';
      tab3.style.color = 'inherit';
    });

    tab2.addEventListener('click', () => {
      content1.style.display = 'none';
      content2.style.display = 'block';
      content3.style.display = 'none';
      tab1.style.color = 'inherit';
      tab2.style.color = 'rgb(9, 175, 254)';
      tab3.style.color = 'inherit';
    });

    tab3.addEventListener('click', () => {
      content1.style.display = 'none';
      content2.style.display = 'none';
      content3.style.display = 'block';
      tab1.style.color = 'inherit';
      tab2.style.color = 'inherit';
      tab3.style.color = 'rgb(9, 175, 254)';
    });

    // 添加面板到文档
    document.body.appendChild(panelElement);

    return panelElement;
  }

  /**
   * 设置异常点面板位置更新
   * @param entity 异常点实体
   * @param panelElement 面板元素
   * @param viewer Cesium Viewer 实例
   * @returns 更新回调函数
   */
  private setupAbnormalPointPanelPositioning(entity: Cesium.Entity, panelElement: HTMLDivElement, viewer: Cesium.Viewer): (event: any) => void {
    // 创建更新面板位置的回调函数
    const updateCallback = () => {
      if (!panelElement) return;

      const position = entity.position?.getValue(viewer.clock.currentTime);
      if (!position) return;

      // 将异常点的笛卡尔坐标转换为屏幕坐标
      const windowPosition = Cesium.SceneTransforms.worldToWindowCoordinates(viewer.scene, position);
      if (!Cesium.defined(windowPosition)) return;

      // 计算面板位置,添加偏移量
      let panelX = windowPosition.x + this.options.abnormalPanelOffset[0];
      let panelY = windowPosition.y + this.options.abnormalPanelOffset[1];

      // 添加屏幕边界检查,确保面板不会超出屏幕范围
      const safetyMargin = 70;
      const panelWidth = 400; // 估计告警面板宽度
      const panelHeight = 350; // 估计告警面板高度
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;

      // 检查左侧边界
      panelX = Math.max(safetyMargin, panelX);
      // 检查右侧边界
      panelX = Math.min(windowWidth - panelWidth - safetyMargin, panelX);
      // 检查顶部边界
      panelY = Math.max(safetyMargin, panelY);
      // 检查底部边界
      panelY = Math.min(windowHeight - panelHeight - safetyMargin, panelY);

      // 更新面板位置
      panelElement.style.left = `${panelX}px`;
      panelElement.style.top = `${panelY}px`;
    };

    // 添加到场景渲染回调
    viewer.scene.postRender.addEventListener(updateCallback);

    // 保存面板、实体和回调的关联
    this.abnormalPointPanels.push({
      entity,
      panel: panelElement,
      updateCallback
    });

    // 初始更新面板位置
    updateCallback();

    return updateCallback;
  }

  /**
   * 显示异常点信息面板
   * @param entity 异常点实体
   * 功能:
   * 1. 验证异常点实体和数据
   * 2. 生成面板内容
   * 3. 创建面板 DOM 元素
   * 4. 设置面板位置更新逻辑
   */
  private showAbnormalPointPanel(entity: Cesium.Entity): void {
    // 验证异常点实体和数据
    const validationResult = this.validateAbnormalPointEntity(entity);
    if (!validationResult.valid || !validationResult.abnormalPoint) {
      return;
    }

    const { abnormalPoint } = validationResult;
    const viewer = this.getViewer();
    if (!viewer) {
      return;
    }

    // 生成面板内容
    const contentData = this.generateAbnormalPointPanelContent(abnormalPoint);

    // 创建面板
    const panelElement = this.createAbnormalPointPanel(contentData, viewer);

    // 设置面板位置更新
    this.setupAbnormalPointPanelPositioning(entity, panelElement, viewer);
  }

  /**
   * 静态渲染无人机和路径
   * @param trackData 无人机轨迹数据数组
   * 功能:
   * 1. 清除之前的渲染
   * 2. 保存当前轨迹数据
   * 3. 渲染飞行路径
   * 4. 渲染关键节点
   * 5. 在起始位置渲染无人机
   * 6. 根据视角模式设置相机位置
   */
  renderStatic(trackData: UAVTrackPoint[]): void {
    this.clear(); // 清除之前的渲染
    this.currentTrackData = trackData; // 保存当前轨迹数据
    const viewer = this.getViewer();
    if (!viewer || trackData.length === 0) return; // 检查 viewer 是否存在以及轨迹数据是否为空

    // 将轨迹点转换为笛卡尔坐标
    const positions = trackData.map((point) =>
      Cesium.Cartesian3.fromDegrees(
        point.longitude,
        point.latitude,
        point.altitude,
      ),
    );

    this.renderPath(trackData); // 渲染飞行路径
    this.renderKeyPoints(trackData); // 渲染关键节点
    this.renderUAV(positions[0], trackData[0]); // 在起始位置渲染无人机,传递轨迹点数据

    // 根据 enableViewModeToggle 和视角模式设置相机位置
    const shouldUseFlyToTrack = !this.options.enableViewModeToggle || !this.isFirstPersonView;

    if (shouldUseFlyToTrack) {
      // 禁用视角切换时或第三人称视角时,使用原始的 2D 俯瞰视角
      this.flyToTrack(trackData);
    } else {
      // 启用视角切换且第一人称视角时,设置第一人称相机
      if (this.uavEntity) {
        const position = this.uavEntity.position?.getValue(viewer.clock.currentTime);
        if (position) {
          const camera = viewer.scene.camera;
          if (camera) {
            this.setupFirstPersonCamera(camera, position);
          }
        }
      }
    }

    // 当启用视角切换功能时,自动启用相机跟踪
    if (this.options.enableViewModeToggle && viewer && this.uavEntity && this.cameraUpdateCallback) {
      viewer.scene.postRender.addEventListener(this.cameraUpdateCallback);
    }
  }

  /**
   * 开始轨迹回放
   * @param trackData 无人机轨迹数据数组
   * 功能:
   * 1. 清除之前的渲染
   * 2. 保存当前轨迹数据
   * 3. 渲染飞行路径
   * 4. 渲染关键节点
   * 5. 设置并开始回放
   */
  startPlayback(trackData: UAVTrackPoint[]): void {
    this.clear(); // 清除之前的渲染
    this.currentTrackData = trackData; // 保存当前轨迹数据
    const viewer = this.getViewer();
    if (!viewer || trackData.length === 0) return; // 检查 viewer 是否存在以及轨迹数据是否为空
    this.renderPath(trackData); // 渲染飞行路径
    this.renderKeyPoints(trackData); // 渲染关键节点
    if (!this.options.enableViewModeToggle || !this.isFirstPersonView) {
      // 使用统一的视角切换逻辑
      this.flyToTrack(trackData); // 视角切换到轨迹
    }


    this.setupPlayback(trackData); // 设置并开始回放

    // 自动启用相机跟踪,使用默认视角模式
    if (viewer && this.uavEntity && this.cameraUpdateCallback && this.options.enableViewModeToggle) {
      // 统一默认切换到第三人称视角
      viewer.scene.postRender.addEventListener(this.cameraUpdateCallback);
    }
  }

  /**
   * 暂停轨迹回放
   * 功能:
   * 1. 停止时钟动画
   * 2. 更新回放状态为暂停
   */
  pausePlayback(): void {
    if (this.clock) {
      this.clock.shouldAnimate = false; // 停止时钟动画
      this.isPlaying = false; // 更新回放状态
    }
  }

  /**
   * 恢复轨迹回放
   * 功能:
   * 1. 重新开始时钟动画
   * 2. 更新回放状态为播放中
   * 3. 重新添加相机跟踪回调,确保视角跟随无人机
   */
  resumePlayback(): void {
    if (this.clock) {
      this.clock.shouldAnimate = true;
      this.isPlaying = true;
    }

    // 恢复播放时,重新添加相机跟踪回调
    const viewer = this.getViewer();
    if (viewer && this.cameraUpdateCallback && this.options.enableViewModeToggle) {
      // 确保只添加一次回调
      try {
        viewer.scene.postRender.removeEventListener(this.cameraUpdateCallback);
      } catch (e) {
        console.error('Error removing camera update callback:', e);
      }
      viewer.scene.postRender.addEventListener(this.cameraUpdateCallback);
    }
  }

  /**
   * 停止轨迹回放
   * 功能:
   * 1. 暂停回放
   * 2. 将时钟重置到开始时间
   */
  stopPlayback(): void {
    this.pausePlayback(); // 暂停回放
    if (this.clock) {
      this.clock.currentTime = this.clock.startTime.clone(); // 重置时钟到开始时
    }
  }

  /**
   * 设置回放速度
   * @param speed 回放速度倍率
   * 功能:
   * 1. 更新配置中的回放速度
   * 2. 更新时钟的速度倍率
   */
  setPlaybackSpeed(speed: number): void {
    this.options.playbackSpeed = speed; // 更新配置中的回放速度
    if (this.clock) {
      this.clock.multiplier = speed; // 更新时钟的速度倍率
    }
  }

  /**
   * 跳转到指定回放进度
   * @param progress 目标进度,范围 [0, 1]
   * 功能:
   * 1. 检查必要的参数是否存在
   * 2. 限制进度范围在 [0, 1]
   * 3. 计算总回放时长
   * 4. 计算目标时间点
   * 5. 设置时钟到目标时间点
   */
  seekToProgress(progress: number): void {
    if (!this.clock || !this.startTime || !this.endTime) return; // 检查必要的参数是否存在

    const clampedProgress = Math.max(0, Math.min(1, progress)); // 限制进度范围
    const totalDuration = Cesium.JulianDate.secondsDifference(
      this.endTime,
      this.startTime,
    ); // 计算总回放时长
    const seekSeconds = totalDuration * clampedProgress; // 计算目标时间点的秒数
    const seekTime = Cesium.JulianDate.addSeconds(
      this.startTime,
      seekSeconds,
      new Cesium.JulianDate(),
    ); // 计算目标时间点

    this.clock.currentTime = seekTime; // 设置时钟到目标时间点
  }

  /**
   * 跳转到指定时间点
   * @param time 目标时间点
   * 功能:
   * 1. 检查必要的参数是否存在
   * 2. 限制时间点在开始时间和结束时间之间
   * 3. 设置时钟到目标时间点
   */
  seekToTime(time: Cesium.JulianDate): void {
    if (!this.clock || !this.startTime || !this.endTime) return;

    const clampedTime = this.clampJulianDate(time, this.startTime, this.endTime);
    this.clock.currentTime = clampedTime;
  }

  /**
   * 跳转到指定轨迹点索引
   * @param index 目标轨迹点索引
   * 功能:
   * 1. 检查索引是否有效
   * 2. 获取指定索引的轨迹点
   * 3. 将轨迹点的时间戳转换为 Julian 日期
   * 4. 调用 seekToTime 跳转到该时间点
   */
  seekToPointIndex(index: number): void {
    if (index < 0 || index >= this.currentTrackData.length) return;

    const point = this.currentTrackData[index];
    const time = Cesium.JulianDate.fromIso8601(new Date(point.timestamp).toISOString());
    this.seekToTime(time);
  }

  /**
   * 设置面板标题
   * @param title 新的面板标题
   * 功能:
   * 1. 更新面板标题
   * 2. 如果面板元素已存在,更新面板标题的显示
   */
  setPanelTitle(title: string): void {
    this.panelTitle = title;
    if (this.panelElement) {
      const titleElement = this.panelElement.querySelector('.uav-panel-title');
      if (titleElement) {
        titleElement.textContent = title;
      }
    }
  }

  /**
   * 更新面板内容
   * @param content 新的面板内容
   * 功能:
   * 1. 更新面板内容
   * 2. 如果无人机实体存在,更新面板显示
   * 3. 计算面板在屏幕上的位置,使其跟随无人机
   */
  updatePanelContent(content: { name: string; value: string }[]): void {
    this.panelContent = content;
    if (this.uavEntity) {
      const viewer = this.getViewer();
      if (viewer) {
        const position = this.uavEntity.position?.getValue(
          viewer.clock.currentTime,
        );

        if (position) {
          this.updatePanel(position);
        }
      }
    }
  }

  /**
   * 设置进度回调函数
   * @param callback 进度回调函数
   *  - progress: 回放进度(0-1)
   *  - currentTime: 当前时间
   * 功能:
   * 1. 更新进度回调函数
   * 2. 用于实时获取回放进度并更新 UI
   */
  setProgressCallback(callback: (progress: number, currentTime: Cesium.JulianDate) => void): void {
    this.progressCallback = callback; // 更新进度回调函数
  }

  /**
   * 获取回放进度(0-1)
   * @returns 回放进度百分比,范围 [0, 1]
   * 功能:
   * 1. 计算总回放时长
   * 2. 计算已播放时长
   * 3. 计算进度百分比并限制在 [0, 1] 范围内
   */
  getPlaybackProgress(): number {
    if (!this.clock || !this.startTime || !this.endTime) return 0; // 检查必要的参数是否存在

    // 计算总回放时长
    const totalDuration = Cesium.JulianDate.secondsDifference(
      this.endTime,
      this.startTime,
    );
    if (totalDuration <= 0) return 0; // 防止除以零

    // 计算已播放时长
    const elapsed = Cesium.JulianDate.secondsDifference(
      this.clock.currentTime,
      this.startTime,
    );

    // 计算进度百分比并限制范围
    return Math.max(0, Math.min(1, elapsed / totalDuration));
  }

  getPlaybackProgressDetail(): PlaybackProgress | null {
    if (!this.clock || !this.startTime || !this.endTime) return null;

    const percentage = this.getPlaybackProgress();
    const currentPointIndex = this.findCurrentPointIndex(this.clock.currentTime);

    return {
      percentage,
      currentTime: this.clock.currentTime.clone(),
      startTime: this.startTime.clone(),
      endTime: this.endTime.clone(),
      currentPointIndex,
      totalPoints: this.currentTrackData.length,
    };
  }



  /**
   * 清除所有渲染和状态
   * 功能:
   * 1. 清除无人机实体
   * 2. 清除路径实体
   * 3. 清除关键节点实体
   * 4. 清除面板实体和 DOM 元素
   * 5. 停止时钟动画
   * 6. 清除相机跟踪
   * 7. 重置所有状态变量
   */
  clear(): void {
    const viewer = this.getViewer();
    if (!viewer) return; // 检查 viewer 是否存在

    // 清除相机跟踪
    if (this.cameraUpdateCallback) {
      viewer.scene.postRender.removeEventListener(this.cameraUpdateCallback);
    }

    // 清除无人机实体
    if (this.uavEntity) {
      viewer.entities.remove(this.uavEntity);
      this.uavEntity = null;
    }

    // 清除路径实体
    if (this.pathEntity) {
      viewer.entities.remove(this.pathEntity);
      this.pathEntity = null;
    }

    // 清除关键节点实体
    this.keyPointEntities.forEach((entity) => {
      viewer.entities.remove(entity);
    });
    this.keyPointEntities = [];

    // 清除面板实体
    if (this.panelEntity) {
      viewer.entities.remove(this.panelEntity);
      this.panelEntity = null;
    }

    // 清除面板 DOM 元素
    if (this.panelElement) {
      this.panelElement.remove();
      this.panelElement = null;
    }

    // 清除异常点信息面板
    this.clearAbnormalPointPanels();

    // 停止时钟动画
    if (this.clock) {
      this.clock.shouldAnimate = false;
    }

    // 重置状态变量
    this.isPlaying = false;
    this.currentTrackData = [];
    this.startTime = null;
    this.endTime = null;
    this.progressCallback = null;
    // 不重置isFirstPersonView,保持用户选择的视角模式
  }
  /**
   * 计算轨迹的地理范围
   * @param trackData 无人机轨迹数据数组
   * @returns 轨迹的地理范围信息
   */
  private calculateTrackBounds(trackData: UAVTrackPoint[]) {
    let minLon = Infinity;
    let maxLon = -Infinity;
    let minLat = Infinity;
    let maxLat = -Infinity;
    let sumLon = 0;
    let sumLat = 0;

    for (const point of trackData) {
      minLon = Math.min(minLon, point.longitude);
      maxLon = Math.max(maxLon, point.longitude);
      minLat = Math.min(minLat, point.latitude);
      maxLat = Math.max(maxLat, point.latitude);
      sumLon += point.longitude;
      sumLat += point.latitude;
    }

    // 计算轨迹中心点
    const centerLon = sumLon / trackData.length;
    const centerLat = sumLat / trackData.length;

    // 计算轨迹的地理宽度(使用近似值,1度约等于111公里)
    const lonDistance = (maxLon - minLon) * 111000; // 转换为米
    const latDistance = (maxLat - minLat) * 111000; // 转换为米
    const maxDistance = Math.max(lonDistance, latDistance);

    return { minLon, maxLon, minLat, maxLat, centerLon, centerLat, maxDistance };
  }

  /**
   * 计算轨迹的相机位置
   * @param viewer Cesium Viewer 实例
   * @param centerLon 轨迹中心点经度
   * @param centerLat 轨迹中心点纬度
   * @param maxDistance 轨迹最大距离
   * @returns 相机位置
   */
  private calculateTrackCameraPosition(viewer: Cesium.Viewer, centerLon: number, centerLat: number, maxDistance: number): Cesium.Cartesian3 {
    // 考虑左右各400px面板的存在,计算可用屏幕宽度
    const canvasWidth = viewer.scene.canvas.width;
    const leftPanelWidth = 400; // 左侧面板宽度
    const rightPanelWidth = 400; // 右侧面板宽度
    const availableWidth = canvasWidth - leftPanelWidth - rightPanelWidth;

    // 计算合适的相机高度,确保能够看到整个轨迹
    // 基于轨迹宽度和可用屏幕宽度的比例,确保轨迹完整显示在可用区域
    const screenRatio = availableWidth / canvasWidth;
    // 减小乘数和固定值,使相机位置更低,距离更近
    const cameraHeight = (maxDistance * 1.5 + 1000) / screenRatio;

    // 计算相机位置:轨迹中心点上方合适高度
    return Cesium.Cartesian3.fromDegrees(centerLon, centerLat, cameraHeight);
  }

  /**
   * 视角切换到轨迹(2D 俯瞰视角)
   * @param trackData 无人机轨迹数据数组
   * 功能:
   * 1. 计算轨迹的地理范围(最小/最大经纬度)
   * 2. 基于地理范围计算合适的相机高度
   * 3. 使用相机飞行到轨迹中心点上方合适高度
   * 4. 考虑左右各400px面板的存在,调整相机高度以确保轨迹完整显示
   */
  private flyToTrack(trackData: UAVTrackPoint[]): void {
    const viewer = this.getViewer();
    if (!viewer || trackData.length === 0) return; // 检查 viewer 是否存在以及轨迹数据是否为空

    // 计算轨迹的地理范围
    const { centerLon, centerLat, maxDistance } = this.calculateTrackBounds(trackData);

    // 计算相机位置:轨迹中心点上方合适高度
    const cameraPosition = this.calculateTrackCameraPosition(viewer, centerLon, centerLat, maxDistance);

    // 飞行到轨迹
    viewer.scene.camera.flyTo({
      destination: cameraPosition,
      orientation: {
        heading: Cesium.Math.toRadians(0), // 朝向
        pitch: Cesium.Math.toRadians(-90), // 垂直向下看,以便看到整个轨迹
        roll: 0, // 翻滚角
      },
      duration: 2, // 飞行持续时间(秒)
    });
  }



  /**
   * 渲染飞行路径
   * @param trackData 无人机轨迹数据数组
   * 功能:
   * 1. 将轨迹点转换为笛卡尔坐标
   * 2. 创建路径实体
   * 3. 设置路径样式
   * 4. 特殊标识连接异常节点的线段
   */
  private renderPath(trackData: UAVTrackPoint[]): void {
    const viewer = this.getViewer();
    if (!viewer || trackData.length < 2) return; // 检查 viewer 是否存在以及轨迹点数量是否足够
    // 如果不存在异常节点,正常渲染整个路径
    const positions = trackData.map((point) =>
      Cesium.Cartesian3.fromDegrees(
        point.longitude,
        point.latitude,
        point.altitude,
      ),
    );

    // 创建路径实体
    this.pathEntity = viewer.entities.add({
      id: `${this.groupName}-path`,
      polyline: {
        positions: positions, // 路径点坐标
        width: this.options.pathWidth, // 路径宽度
        material: this.options.pathColor, // 路径颜色
        clampToGround: false, // 不贴地
      },
    });

  }

  /**
   * 渲染路径关键节点
   * @param trackData 无人机轨迹数据数组
   * 功能:
   * 1. 按照指定间隔渲染关键节点
   * 2. 为每个节点添加图标
   * 3. 检测并特殊渲染异常节点
   */
  private renderKeyPoints(trackData: UAVTrackPoint[]): void {
    const viewer = this.getViewer();
    if (!viewer) return; // 检查 viewer 是否存在

    const interval = this.options.keyPointInterval; // 获取关键节点间隔
    for (let i = 0; i < trackData.length; i += interval) {
      const point = trackData[i];
      // 转换为笛卡尔坐标
      const position = Cesium.Cartesian3.fromDegrees(
        point.longitude,
        point.latitude,
        point.altitude,
      );

      // 检查是否为异常节点
      const isAbnormal = point.isAbnormal || false;
      // 根据节点类型选择图标和大小
      const icon = isAbnormal ? this.options.abnormalPointIcon : this.options.keyPointIcon;
      const size = isAbnormal ? this.options.abnormalPointSize : this.options.keyPointSize;

      // 创建关键节点实体
      const entity = viewer.entities.add({
        id: `${this.groupName}-keypoint-${i}`,
        position: position,
        billboard: {
          image: icon, // 节点图标
          width: size[0], // 图标宽度
          height: size[1], // 图标高度
          verticalOrigin: Cesium.VerticalOrigin.CENTER,
          horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
          disableDepthTestDistance: Number.POSITIVE_INFINITY,
        }
      });

      this.keyPointEntities.push(entity);
    }

    // 单独渲染所有异常节点(即使不在关键节点间隔上)
    for (let i = 0; i < trackData.length; i++) {
      const point = trackData[i];
      if (point.isAbnormal && i % interval !== 0) { // 确保不重复渲染
        const position = Cesium.Cartesian3.fromDegrees(
          point.longitude,
          point.latitude,
          point.altitude,
        );

        const entity = viewer.entities.add({
          id: `${this.groupName}-abnormal-point-${i}`,
          position: position,
          billboard: {
            image: this.options.abnormalPointIcon, // 异常节点图标
            width: this.options.abnormalPointSize[0], // 图标宽度
            height: this.options.abnormalPointSize[1], // 图标高度
            verticalOrigin: Cesium.VerticalOrigin.CENTER,
            horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
          },
        });

        this.keyPointEntities.push(entity);
      }
    }
  }

  /**
   * 渲染无人机
   * @param position 无人机位置(笛卡尔坐标)
   * @param trackPoint 轨迹点数据
   * 功能:
   * 1. 创建无人机实体
   * 2. 设置位置和朝向
   * 3. 添加无人机图标
   * 4. 设置图标旋转
   */
  private renderUAV(
    position: Cesium.Cartesian3,
    trackPoint?: UAVTrackPoint,
  ): void {
    const viewer = this.getViewer();
    if (!viewer) return; // 检查 viewer 是否存在

    // 创建无人机实体
    const entityOptions: any = {
      id: `${this.groupName}-uav`,
      position: position, // 无人机位置
    };

    // 如果提供了轨迹点数据,设置朝向
    if (trackPoint) {
      // 计算无人机朝向
      let heading = -45; // 默认朝向(正北)

      // 尝试根据当前轨迹点在数据中的位置计算朝向
      if (this.currentTrackData && this.currentTrackData.length > 1) {
        const currentIndex = this.currentTrackData.findIndex(p => p.timestamp === trackPoint.timestamp);
        if (currentIndex > 0) {
          // 获取当前点和前一个点的经纬度
          const currentPoint = trackPoint;
          const prevPoint = this.currentTrackData[currentIndex - 1];

          // 计算两点之间的方向
          const deltaLon = currentPoint.longitude - prevPoint.longitude;
          const deltaLat = currentPoint.latitude - prevPoint.latitude;

          // 计算航向角(从正北开始计算)
          heading = Math.atan2(deltaLon, deltaLat) * (180 / Math.PI);
        }
      }

      const orientation = Cesium.Transforms.headingPitchRollQuaternion(
        position,
        new Cesium.HeadingPitchRoll(
          Cesium.Math.toRadians(heading), // 航向角
          0, // 俯仰角
          0, // 翻滚角
        ),
      );
      entityOptions.orientation = orientation;
    }

    // 根据配置决定使用图标还是 3D 模型
    if (this.options.uavType === UAVType.MODEL && this.options.uavModel) {
      // 使用 3D 模型
      entityOptions.model = {
        uri: this.options.uavModel,
        scale: this.options.uavModelScale || 0.5, // 减小模型缩放比例,使模型显得更小
        minimumPixelSize: 64, // 减小最小像素大小,使模型在远处显得更小
        maximumScale: 20000,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      };
    } else {
      // 使用图标
      entityOptions.billboard = {
        image: this.options.uavIcon, // 无人机图标
        width: this.options.uavIconSize[0], // 图标宽度
        height: this.options.uavIconSize[1], // 图标高度
        verticalOrigin: Cesium.VerticalOrigin.CENTER,
        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      };
    }

    this.uavEntity = viewer.entities.add(entityOptions);
  }

  /**
   * 设置轨迹回放
   * @param trackData 无人机轨迹数据数组
   * @param positions 轨迹点笛卡尔坐标数组
   * 功能:
   * 1. 设置开始和结束时间
   * 2. 配置时钟参数
   * 3. 创建无人机实体
   * 4. 添加位置和朝向的采样属性
   * 5. 设置面板
   * 6. 开始动画
   */
  private setupPlayback(
    trackData: UAVTrackPoint[],
  ): void {
    const viewer = this.getViewer();
    if (!viewer) return; // 检查 viewer 是否存在
    // 设置开始和结束时间
    this.startTime = Cesium.JulianDate.fromIso8601(new Date(trackData[0].timestamp).toISOString());
    this.endTime = Cesium.JulianDate.fromIso8601(
      new Date(trackData[trackData.length - 1].timestamp).toISOString(),
    );

    // 配置时钟
    this.clock = viewer.clock;
    this.clock.startTime = this.startTime.clone(); // 开始时间
    this.clock.stopTime = this.endTime.clone(); // 结束时间
    this.clock.currentTime = this.startTime.clone(); // 当前时间
    this.clock.clockRange = Cesium.ClockRange.CLAMPED; // 钳制模式,到达终点后停止
    this.clock.multiplier = this.options.playbackSpeed; // 播放速度
    this.clock.shouldAnimate = true; // 开始动画

    // 转换轨迹点为笛卡尔坐标
    const timePositions = trackData.map((point) =>
      Cesium.Cartesian3.fromDegrees(
        point.longitude,
        point.latitude,
        point.altitude,
      ),
    );

    // 创建无人机实体
    const entityOptions: any = {
      id: `${this.groupName}-uav`,
      position: new Cesium.SampledPositionProperty(), // 时间采样位置属性
    };

    // 配置SampledPositionProperty的插值算法,提供更平滑的飞行效果
    const sampledPosition = entityOptions.position as Cesium.SampledPositionProperty;
    sampledPosition.setInterpolationOptions({
      interpolationDegree: 3 // 使用三次插值,Cesium会自动选择合适的插值算法
    });

    // 轨迹回放时根据速度方向更新朝向,确保与轨迹运动方向一致
    entityOptions.orientation = new Cesium.VelocityOrientationProperty(
      entityOptions.position as Cesium.PositionProperty,
    );

    // 根据配置决定使用图标还是 3D 模型
    if (this.options.uavType === UAVType.MODEL && this.options.uavModel) {
      // 使用 3D 模型
      entityOptions.model = {
        uri: this.options.uavModel,
        scale: this.options.uavModelScale || 0.3, // 减小模型缩放比例,使模型显得更小
        minimumPixelSize: 32, // 减小最小像素大小,使模型在远处显得更小
        maximumScale: 10000,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      };
    } else {
      // 使用图标
      entityOptions.billboard = {
        image: this.options.uavIcon,
        width: this.options.uavIconSize[0],
        height: this.options.uavIconSize[1],
        verticalOrigin: Cesium.VerticalOrigin.CENTER,
        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
        disableDepthTestDistance: Number.POSITIVE_INFINITY, // 禁用深度测试
      };
    }

    this.uavEntity = viewer.entities.add(entityOptions);

    const uavSampledPosition = this.uavEntity.position as Cesium.SampledPositionProperty;

    // 为每个轨迹点添加时间采样
    trackData.forEach((point, index) => {
      const time = Cesium.JulianDate.fromIso8601(new Date(point.timestamp).toISOString());
      const position = timePositions[index];

      uavSampledPosition.addSample(time, position); // 添加位置采样

    });

    this.setupPanel(viewer); // 设置面板
    this.isPlaying = true; // 更新回放状态
  }

  /**
   * 设置无人机面板
   * @param viewer Cesium Viewer 实例
   * 功能:
   * 1. 创建面板 DOM 元素
   * 2. 添加场景渲染回调,实时更新面板位置和内容
   * 3. 在回调中更新面板和进度
   */
  private setupPanel(viewer: Cesium.Viewer): void {
    this.createPanelElement();

    viewer.scene.postRender.addEventListener(() => {
      if (!this.isPlaying || !this.uavEntity) return;

      const currentTime = viewer.clock.currentTime;
      const position = this.uavEntity.position?.getValue(currentTime);

      if (position) {
        this.updatePanel(position);
        this.updateProgress(currentTime);
      }
    });
  }

  /**
   * 更新回放进度
   * @param currentTime 当前时间
   * 功能:
   * 1. 检查开始时间和结束时间是否存在
   * 2. 计算总回放时长和已播放时长
   * 3. 计算进度百分比并限制在 [0, 1] 范围内
   * 4. 调用进度回调函数
   * 5. 当进度达到 100% 时,自动暂停播放
   */
  private updateProgress(currentTime: Cesium.JulianDate): void {
    if (!this.startTime || !this.endTime) return;

    const totalDuration = Cesium.JulianDate.secondsDifference(
      this.endTime,
      this.startTime,
    );
    if (totalDuration <= 0) return;

    const elapsed = Cesium.JulianDate.secondsDifference(
      currentTime,
      this.startTime,
    );
    const progress = Math.max(0, Math.min(1, elapsed / totalDuration));

    if (this.progressCallback) {
      this.progressCallback(progress, currentTime);
    }

    if (this.options.onProgress) {
      this.options.onProgress(progress, currentTime);
    }

    // 当播放进度达到 100% 时,自动暂停播放,允许用户控制视角
    if (progress >= 1 && this.isPlaying) {
      this.pausePlayback();
    }
  }

  /**
   * 创建无人机信息面板 DOM 元素
   * 功能:
   * 1. 移除已存在的面板元素
   * 2. 创建新的面板元素
   * 3. 设置面板样式
   * 4. 设置面板内容结构
   * 5. 将面板添加到文档中
   */
  private createPanelElement(): void {
    if (this.panelElement) {
      this.panelElement.remove(); // 移除已存在的面板元素
    }

    this.panelElement = document.createElement('div');
    this.panelElement.className = 'uav-track-panel';
    // 设置面板样式
    this.panelElement.style.cssText = `
      position: absolute;
      border-radius: 6px;
      background: rgba(5, 20, 40, 0.95);
      border: 2px solid rgba(61, 90, 254, 1);
      box-shadow:inset 0px 0px 24px  rgba(61, 90, 254, 1);
      font-size: 14px;
      font-weight: 400;
      color: rgba(156, 163, 175, 1);
      pointer-events: none;
      z-index: 1000;
      min-width: 180px;
    `;

    // 设置面板内容结构
    this.panelElement.innerHTML = `
      <div class="uav-panel-title" 
        style="uav-panel-contentight: 36px;
          background: url(${DIALOG_BG});
          background-size: 100% 100%;
          padding-left:27px;
        "
       >
        <div style=" 
           font-weight: bold; 
           font-size: 16px;
           line-height: 36px;
           color: rgba(255, 255, 255, 1);
            font-family: youshebiaotihei;
            background: linear-gradient(270deg, #42c6ff 0%, #ffffff 100%);
            background-clip: text;
            color: transparent;"
            >${this.panelTitle}</div>
          </div>
      <div class="uav-panel-content" style="padding: 12px 20px;"></div>
    `;

    document.body.appendChild(this.panelElement); // 将面板添加到文档中
  }

  /**
   * 更新无人机面板
   * @param position 无人机当前位置(笛卡尔坐标)
   * 功能:
   * 1. 获取当前时间对应的轨迹点
   * 2. 生成面板内容
   * 3. 更新面板显示
   * 4. 计算面板在屏幕上的位置
   * 5. 更新面板位置,使其跟随无人机
   */
  private updatePanel(position: Cesium.Cartesian3): void {
    const viewer = this.getViewer();
    if (!viewer || !this.panelElement) return; // 检查 viewer 和面板元素是否存在

    const currentTime = viewer.clock.currentTime;
    const currentPoint = this.findCurrentTrackPoint(currentTime); // 查找当前时间对应的轨迹点

    let content = this.panelContent.length > 0 ? this.panelContent : []; // 使用自定义内容或默认内容

    // 如果没有自定义内容且找到了当前轨迹点,生成默认内容
    if (currentPoint && content.length === 0) {
      content = [
        { name: "总距离", value: `${currentPoint?.totalDistance || 0}m` },
        { name: "总时间", value: `${currentPoint?.totalTime || 0}s` },
        { name: "当前时间", value: `${(currentPoint?.totalTime || 0) - (currentPoint?.remainingTime || 0)}s` },
        { name: "飞行速度", value: `${currentPoint?.flightSpeed || 0}m/s` },
        { name: "高度", value: `${currentPoint?.altitude || 0}m` },
        { name: "航向", value: `${currentPoint?.heading || 0}°` },
        { name: "俯角", value: `${currentPoint?.pitchAngle || 0}°` },
        { name: "仰角", value: `${currentPoint?.rollAngle || 0}°` },
        { name: "剩余时间", value: `${currentPoint?.remainingTime || 0}s` },
        { name: "剩余距离", value: `${currentPoint?.remainingDistance || 0}m` },
      ];
    }

    // 更新面板内容
    const contentElement = this.panelElement.querySelector('.uav-panel-content');
    if (contentElement) {
      contentElement.innerHTML = content
        .map((item) => `<div style="display: flex; justify-content: space-between;"><span>${item.name}:</span><span style="margin-left: 12px;">${item.value}</span></div>`)
        .join('');
    }
    const canvasPosition = this.wgs84ToWindowCoordinates(viewer, position);

    if (canvasPosition) {
      // 计算面板位置,添加偏移量
      let left = canvasPosition.x + this.options.panelOffset[0];
      let top = canvasPosition.y + this.options.panelOffset[1] + (this.isFirstPersonView ? - 120 : 0);

      // 添加屏幕边界检查,确保面板不会超出屏幕范围,留出70px的安全距离
      const safetyMargin = 70;
      const panelWidth = this.panelElement.offsetWidth || 200; // 使用默认宽度作为备选
      const panelHeight = this.panelElement.offsetHeight || 300; // 使用默认高度作为备选
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;

      // 检查左侧边界
      left = Math.max(safetyMargin, left);
      // 检查右侧边界
      left = Math.min(windowWidth - panelWidth - safetyMargin, left);
      // 检查顶部边界
      top = Math.max(safetyMargin, top);
      // 检查底部边界
      top = Math.min(windowHeight - panelHeight - safetyMargin, top);

      // 更新面板位置
      this.panelElement.style.left = `${left}px`;
      this.panelElement.style.top = `${top}px`;
    }
  }

  /**
   * 查找当前时间对应的轨迹点
   * @param currentTime 当前时间
   * @returns 最接近当前时间的轨迹点,如果没有轨迹数据则返回 null
   * 功能:
   * 1. 检查轨迹数据是否为空
   * 2. 将当前时间转换为时间戳(毫秒)
   * 3. 遍历所有轨迹点,找到时间戳与当前时间最接近的点
   * 4. 返回最接近的轨迹点
   */
  private findCurrentTrackPoint(
    currentTime: Cesium.JulianDate,
  ): UAVTrackPoint | null {
    if (this.currentTrackData.length === 0) return null;

    const currentTimeSeconds = Cesium.JulianDate.toDate(currentTime).getTime();
    let closestPoint = this.currentTrackData[0];
    let minDiff = Math.abs(
      parseInt(closestPoint.timestamp) - currentTimeSeconds,
    );

    for (const point of this.currentTrackData) {
      const diff = Math.abs(parseInt(point.timestamp) - currentTimeSeconds);
      if (diff < minDiff) {
        minDiff = diff;
        closestPoint = point;
      }
    }

    return closestPoint;
  }

  /**
   * 查找当前时间对应的轨迹点索引
   * @param currentTime 当前时间
   * @returns 最接近当前时间的轨迹点索引,如果没有轨迹数据则返回 0
   * 功能:
   * 1. 检查轨迹数据是否为空
   * 2. 将当前时间转换为时间戳(毫秒)
   * 3. 遍历所有轨迹点,找到时间戳与当前时间最接近的点的索引
   * 4. 返回最接近的轨迹点索引
   */
  private findCurrentPointIndex(currentTime: Cesium.JulianDate): number {
    if (this.currentTrackData.length === 0) return 0;

    const currentTimeSeconds = Cesium.JulianDate.toDate(currentTime).getTime();
    let closestIndex = 0;
    let minDiff = Math.abs(
      parseInt(this.currentTrackData[0].timestamp) - currentTimeSeconds,
    );

    for (let i = 1; i < this.currentTrackData.length; i++) {
      const diff = Math.abs(
        parseInt(this.currentTrackData[i].timestamp) - currentTimeSeconds,
      );
      if (diff < minDiff) {
        minDiff = diff;
        closestIndex = i;
      }
    }

    return closestIndex;
  }

  /**
   * 限制 Julian 日期在指定范围内
   * @param date 要限制的日期
   * @param min 最小日期
   * @param max 最大日期
   * @returns 限制后的日期
   * 功能:
   * 1. 克隆输入日期
   * 2. 如果日期小于最小日期,返回最小日期的克隆
   * 3. 如果日期大于最大日期,返回最大日期的克隆
   * 4. 否则返回原始日期的克隆
   */
  private clampJulianDate(date: Cesium.JulianDate, min: Cesium.JulianDate, max: Cesium.JulianDate): Cesium.JulianDate {
    const result = date.clone();
    if (Cesium.JulianDate.lessThan(result, min)) {
      return min.clone();
    }
    if (Cesium.JulianDate.greaterThan(result, max)) {
      return max.clone();
    }
    return result;
  }

  /**
   * 将 WGS84 坐标转换为窗口坐标
   * @param viewer Cesium Viewer 实例
   * @param position 3D 位置(笛卡尔坐标)
   * @returns 窗口坐标,如果转换失败则返回 undefined
   * 功能:
   * 1. 获取场景实例
   * 2. 创建窗口坐标对象
   * 3. 使用场景的 cartesianToCanvasCoordinates 方法进行转换
   * 4. 返回转换后的窗口坐标
   */
  private wgs84ToWindowCoordinates(viewer: Cesium.Viewer, position: Cesium.Cartesian3): Cesium.Cartesian2 | undefined {
    const scene = viewer.scene;
    const canvasPosition = new Cesium.Cartesian2();
    const result = scene.cartesianToCanvasCoordinates(position, canvasPosition);
    return result;
  }

  /**
   * 获取无人机当前位置
   * @returns 无人机当前位置(笛卡尔坐标),如果无法获取则返回 null
   * 功能:
   * 1. 检查 Viewer 和无人机实体是否存在
   * 2. 获取当前时间无人机的位置
   * 3. 返回位置,如果不存在则返回 null
   */
  getUAVPosition(): Cesium.Cartesian3 | null {
    const viewer = this.getViewer();
    if (!viewer || !this.uavEntity) return null;

    return (
      this.uavEntity.position?.getValue(viewer.clock.currentTime) || null
    );
  }

  /**
   * 获取当前时间对应的轨迹点
   * @returns 最接近当前时间的轨迹点,如果无法获取则返回 null
   * 功能:
   * 1. 检查 Viewer 是否存在
   * 2. 调用 findCurrentTrackPoint 方法查找当前时间对应的轨迹点
   * 3. 返回找到的轨迹点
   */
  getCurrentTrackPoint(): UAVTrackPoint | null {
    const viewer = this.getViewer();
    if (!viewer) return null;

    return this.findCurrentTrackPoint(viewer.clock.currentTime);
  }

  /**
   * 检查回放是否活跃
   * @returns 如果回放正在进行中则返回 true,否则返回 false
   * 功能:
   * 1. 返回当前的回放状态
   */
  isPlaybackActive(): boolean {
    return this.isPlaying;
  }

  /**
   * 切换第一人称和第三人称视角
   * @param isFirstPerson 是否切换到第一人称视角
   * 功能:
   * 1. 第一人称视角:将相机位置设置在无人机位置,朝向与无人机一致
   * 2. 第三人称视角:将相机位置设置在无人机后方一定距离,朝向无人机
   * 3. 直接更新相机位置,无论当前是否处于播放状态
   */
  toggleViewMode(isFirstPerson: boolean): void {
    // 始终更新视角模式,确保轨迹播放正常工作
    this.isFirstPersonView = isFirstPerson;

    // 检查是否启用了切换视角功能
    if (!this.options.enableViewModeToggle) return;

    const viewer = this.getViewer();
    if (!viewer || !this.uavEntity) return;

    // 直接更新相机位置,根据当前无人机位置实现视角切换效果
    // 无论当前是否处于播放状态
    try {
      const currentTime = viewer.clock.currentTime;
      const position = this.getUAVPositionAtTime(currentTime);
      if (!position || !this.isValidCartesian3(position)) {
        console.warn('Invalid UAV position in toggleViewMode');
        return;
      }

      const camera = viewer.scene.camera;
      if (!camera) return;

      if (isFirstPerson) {
        // 第一人称视角:相机位置在无人机尾部,确保无人机在屏幕中央
        // 使用复用的相机设置方法
        this.setupFirstPersonCamera(camera, position);

        // 更新面板位置,确保切换视角后面板位置正确
        this.updatePanel(position);
      } else {
        // 非播放状态:相机位置参考flyToTrack函数执行完的效果
        // 计算轨迹的地理范围
        const { centerLon, centerLat, maxDistance } = this.calculateTrackBounds(this.currentTrackData);

        // 计算相机位置:轨迹中心点上方合适高度
        const cameraPosition = this.calculateTrackCameraPosition(viewer, centerLon, centerLat, maxDistance);

        // 设置相机位置
        camera.setView({
          destination: cameraPosition,
          orientation: {
            heading: Cesium.Math.toRadians(0), // 朝向
            pitch: Cesium.Math.toRadians(-90), // 垂直向下看,以便看到整个轨迹
            roll: 0, // 翻滚角
          }
        });

        // 更新面板位置,确保切换视角后面板位置正确
        if (position) {
          this.updatePanel(position);
        }

      }
    } catch (error) {
      console.warn('Error updating camera position in toggleViewMode:', error);
    }

    // 处理相机跟踪回调
    if (this.cameraUpdateCallback) {
      // 清除现有的相机跟踪
      viewer.scene.postRender.removeEventListener(this.cameraUpdateCallback);

      // 只在播放状态下添加相机跟踪
      if (this.isPlaying) {
        viewer.scene.postRender.addEventListener(this.cameraUpdateCallback);
      }
    }
  }

  /**
   * 设置第一人称视角相机
   * @param camera 相机实例
   * @param position 无人机位置
   * 功能:
   * 1. 第一人称视角:相机位于无人机垂尾顶端附近,朝前拍摄
   * 2. 与updateCameraPosition方法中的相机设置保持一致
   */
  private setupFirstPersonCamera(camera: Cesium.Camera, position: Cesium.Cartesian3): void {
    // 固定的尾部顶端视角参数(类似客机垂尾顶端向前拍摄)
    const followDistance = 80; // 向后偏移
    const tailTopHeight = 18; // 向上偏移
    const slightForward = 4; // 轻微向前,模拟镜头在尾部上沿略前方

    // 获取无人机的运动方向,确保相机视角与运动方向一致
    let uavHeading = -45; // 默认朝向
    let uavPitch = 0;
    const viewer = this.getViewer();
    if (viewer) {
      const currentTime = viewer.clock.currentTime;
      const nextTime = Cesium.JulianDate.addSeconds(currentTime, 0.1, new Cesium.JulianDate());
      const nextPosition = this.uavEntity?.position?.getValue(nextTime);

      if (nextPosition && this.isValidCartesian3(nextPosition)) {
        const velocityDir = Cesium.Cartesian3.normalize(
          Cesium.Cartesian3.subtract(nextPosition, position, new Cesium.Cartesian3()),
          new Cesium.Cartesian3()
        );

        const transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
        const inverseTransform = Cesium.Matrix4.inverse(transform, new Cesium.Matrix4());
        const localDir = Cesium.Matrix4.multiplyByPointAsVector(
          inverseTransform,
          velocityDir,
          new Cesium.Cartesian3()
        );

        const heading = Math.atan2(localDir.x, localDir.y);
        const pitch = Math.atan2(
          localDir.z,
          Math.sqrt(localDir.x * localDir.x + localDir.y * localDir.y)
        );

        uavHeading = Cesium.Math.toDegrees(heading);
        uavPitch = Cesium.Math.toDegrees(pitch);
      } else if (this.uavEntity && this.uavEntity.orientation) {
        const uavOrientation = this.uavEntity.orientation.getValue(currentTime);
        if (uavOrientation) {
          const hpr = Cesium.HeadingPitchRoll.fromQuaternion(uavOrientation);
          uavHeading = Cesium.Math.toDegrees(hpr.heading);
          uavPitch = Cesium.Math.toDegrees(hpr.pitch);
        }
      }
    }

    // 创建一个变换矩阵,考虑地球曲率
    const transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);

    // 计算相机在本地坐标系中的偏移
    // 注意:在本地坐标系中,x轴指向东,y轴指向北,z轴指向天
    // 相机位于尾部上方,略微前移,朝向机头方向
    const cameraOffset = new Cesium.Cartesian3(
      0, // 东方向偏移
      -followDistance + slightForward, // 北方向偏移(负表示向南,即机尾方向)
      tailTopHeight // 天方向偏移(上方)
    );

    // 旋转相机偏移,使其与无人机朝向一致
    const rotationMatrix = Cesium.Matrix3.fromHeadingPitchRoll(
      new Cesium.HeadingPitchRoll(
        Cesium.Math.toRadians(uavHeading),
        Cesium.Math.toRadians(uavPitch),
        0
      )
    );
    const rotatedOffset = Cesium.Matrix3.multiplyByVector(
      rotationMatrix,
      cameraOffset,
      new Cesium.Cartesian3()
    );

    // 计算相机在世界坐标系中的位置
    const cameraPosition = Cesium.Matrix4.multiplyByPoint(
      transform,
      rotatedOffset,
      new Cesium.Cartesian3()
    );

    // 适当下倾视线,让无人机在屏幕中部略偏上
    const viewPitch = uavPitch - 10; // 单位:度

    // 设置相机位置和朝向
    camera.setView({
      destination: cameraPosition,
      orientation: {
        heading: Cesium.Math.toRadians(uavHeading), // 相机朝向与无人机航向大致一致
        pitch: Cesium.Math.toRadians(viewPitch), // 略微俯视,让模型从底部抬到中上部
        roll: 0
      }
    });
  }

  /**
   * 更新相机位置以跟随无人机
   * @param event 渲染事件
   */
  private updateCameraPosition(): void {
    // 只在启用了视角切换功能时执行相机更新逻辑
    if (!this.options.enableViewModeToggle) return;

    // 只在播放状态下强制更新相机位置,非播放状态下允许用户自由移动视角
    if (!this.isPlaying) return;

    try {
      const viewer = this.getViewer();
      if (!viewer || !this.uavEntity) return;

      // 检查场景和画布是否有效
      if (!this.isSceneValid(viewer)) return;

      const currentTime = viewer.clock.currentTime;
      const position = this.getUAVPositionAtTime(currentTime);
      if (!position || !this.isValidCartesian3(position)) {
        console.warn('Invalid UAV position in updateCameraPosition');
        return;
      }

      const camera = viewer.scene.camera;
      if (!camera) return;

      // 启用了视角切换功能,使用新逻辑
      if (this.isFirstPersonView) {
        // 第一人称视角:使用复用的相机设置方法
        this.setupFirstPersonCamera(camera, position);
      }
    } catch (error) {
      console.warn('Error in updateCameraPosition:', error);
    }
  }

  private isSceneValid(viewer: Cesium.Viewer): boolean {
    if (!viewer.scene || !viewer.scene.camera || !viewer.scene.canvas) return false;
    const canvas = viewer.scene.canvas;
    return canvas.width > 0 && canvas.height > 0;
  }

  private getUAVPositionAtTime(currentTime: Cesium.JulianDate): Cesium.Cartesian3 | undefined {
    if (!this.uavEntity || !this.uavEntity.position) return undefined;

    // 对于有 getValue 方法的属性(如 SampledPositionProperty)
    if (typeof this.uavEntity.position.getValue === 'function') {
      const result = this.uavEntity.position.getValue(currentTime);
      // 确保返回值不是 null
      if (result && typeof result === 'object' && 'x' in result && 'y' in result && 'z' in result) {
        return result as Cesium.Cartesian3;
      }
      return undefined;
    }
    // 对于 ConstantPositionProperty
    else if ('value' in this.uavEntity.position) {
      const value = this.uavEntity.position.value;
      if (value && typeof value === 'object' && 'x' in value && 'y' in value && 'z' in value) {
        return value as Cesium.Cartesian3;
      }
    }
    return undefined;
  }



  /**
   * 计算两个地理坐标点之间的方位角
   * @param lon1 起点经度
   * @param lat1 起点纬度
   * @param lon2 终点经度
   * @param lat2 终点纬度
   * @returns 方位角(度)
   */


  private isValidCartesian3(position: Cesium.Cartesian3): boolean {
    if (!position) return false;

    // 检查是否包含有效的数值
    const hasValidCoordinates = (
      typeof position.x === 'number' &&
      typeof position.y === 'number' &&
      typeof position.z === 'number' &&
      !isNaN(position.x) &&
      !isNaN(position.y) &&
      !isNaN(position.z) &&
      isFinite(position.x) &&
      isFinite(position.y) &&
      isFinite(position.z)
    );

    return hasValidCoordinates;
  }
}
export default UAVUtil;