基于Cesium实现卫星轨迹和通信链路模拟

160 阅读9分钟

在航天、通信等领域,3D 可视化技术能够将复杂的卫星轨道、通信链路、地面设施等数据直观呈现,极大提升管理效率和决策准确性。本文将带大家基于 Cesium 引擎,从零构建一套激光网络管理可视化系统,实现卫星轨道模拟、星地 / 星间通信链路动态展示、多轨道切换等核心功能,深入解析 Cesium 在空间数据可视化中的实践应用。

一、系统效果预览

1.gif 本系统实现了以下核心功能:

  • 地球底图加载(天地图影像 + 路网注记),支持星空、太阳、月亮等天文要素显示
  • 多轨道卫星管理(高轨、倾斜轨、近地轨),支持轨道切换和相机自动适配
  • 卫星轨道实时计算与飞行模拟,基于 TLE 数据精准还原卫星运行轨迹
  • 星间、星地(卫星 - 雷达)、地地(雷达 - 雷达)通信链路动态渲染,支持通断状态和业务质量展示
  • 交互功能:鼠标双击聚焦雷达、右键查看卫星 / 链路信息、时间轴控制播放速度
  • 信息面板:实体点击弹窗展示详情,支持拖拽调整位置

二、技术选型与核心依赖

1. 核心技术栈

  • 可视化引擎:Cesium(Web 端 3D 地理信息可视化核心,支持地球、卫星、轨道等空间数据渲染)
  • 前端框架:Vue.js(组件化开发,便于功能拆分和维护)
  • 数据处理:satellite.js(解析 TLE 卫星轨道数据,计算卫星实时位置)
  • 样式预处理:SCSS(高效编写结构化样式)

2. 关键依赖说明

  • Cesium:提供地球渲染、实体管理、相机控制、时间轴等核心能力,是整个系统的基础
  • satellite.js:专业的卫星轨道计算库,支持 TLE 数据解析、ECI 坐标系转大地坐标系等关键计算
  • 自定义工具库:包含时间格式化、坐标转换、通信链路材质等工具函数,适配业务需求

三、核心原理与架构设计

1. 系统整体架构

  • 数据层:卫星 TLE 数据(按轨道类型分文件存储)、雷达点位数据(含经纬度、类型等)
  • 核心层:Cesium 引擎封装(地球初始化、实体管理、相机控制)、轨道计算(基于 satellite.js)、通信链路判断
  • 交互层:轨道切换、鼠标事件、时间轴控制、信息弹窗
  • 表现层:卫星 3D 模型、雷达模型、动态通信链路、信息面板

2. 关键技术原理

(1)卫星轨道计算原理

卫星轨道数据采用 TLE(两行轨道根数)格式存储,通过satellite.jstwoline2satrec方法解析 TLE 数据,得到卫星轨道参数。再通过propagate方法根据当前时间计算卫星的地心惯性坐标系(ECI)位置,最后通过eciToGeodetic方法转换为大地坐标系(经纬度 + 高度),实现卫星实时位置的精准计算。

(2)通信链路动态渲染

通信链路的显示状态由时间区间和通断状态控制:

  • 预定义每条链路的有效时间区间和对应状态(正常、质量差、中断)
  • 监听 Cesium 时钟onTick事件,实时获取当前时间戳
  • 遍历卫星 / 雷达实体,判断当前时间是否在链路有效区间内
  • 对有效链路,通过CallbackProperty实时更新链路端点坐标,结合自定义流动材质实现动态飞线效果

(3)多轨道切换与相机适配

不同轨道类型(高轨、近地轨)的卫星高度差异巨大,切换轨道时:

  • 清空当前轨道的卫星、链路等实体数据
  • 加载对应轨道的卫星 TLE 数据,重新计算轨道和位置
  • 根据轨道高度自动调整相机视角距离(高轨 1.2 亿米,近地轨 2500 万米)
  • 重新初始化通信链路计算逻辑

四、完整实现步骤

1. 项目依赖安装

# 安装核心依赖
npm install cesium satellite.js --save

2. 核心配置(Cesium 引入与 Token 设置)

public/index.html中引入 Cesium 静态资源(或通过模块化引入):

<!-- 引入Cesium样式 -->
<link rel="stylesheet" href="https://cesium.com/downloads/cesiumjs/releases/1.110/Build/Cesium/Widgets/widgets.css" />
<!-- 引入Cesium核心库 -->
<script src="https://cesium.com/downloads/cesiumjs/releases/1.110/Build/Cesium/Cesium.js"></script>

在 Vue 组件中配置 Cesium Ion Token(用于加载默认资源):

// 组件初始化时设置
Cesium.Ion.defaultAccessToken = '你的Cesium Ion Token';

3. 核心组件实现(带关键注释)

<template>
  <div id="myCesium">
    <!-- 头部标题 -->
    <div class="header">
      <p class="title">激光网路管理</p>
    </div>
    <!-- Cesium地球容器 -->
    <div id="cesiumContainer" />
    <!-- 加载遮罩 -->
    <div id="loadingOverlay">
      <h1>Loading...</h1>
    </div>
    <!-- 轨道切换标签 -->
    <div class="tabContainer">
      <ul class="tabs">
        <li
          v-for="(item, index) in tabs"
          :key="index"
          class="tab"
          :class="tabIndex === index ? 'active' : ''"
          @click="changeTab(index)"
        >
          {{ item }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
import * as Cesium from 'cesium'
import { DateTimeFormatter, JulianDateToTimestamp, TimeFormatter, degreesFromCartesian } from '@/utils/tools.js'
import { twoline2satrec, propagate, gstime, eciToGeodetic, degreesLong, degreesLat } from 'satellite.js'
import LineFlowMaterialProperty from '@/utils/lineFlowMaterialProperty.js'

// 全局变量定义
let viewer // Cesium视图实例
const myMapToken = '你的天地图Token' // 天地图Token(需自行申请)
const time = new Date(2024, 1, 2, 12, 0, 0) // 基准时间
let max = time.getTime()
const nowTime = time.getTime()
const day = 1000 * 60 * 60 * 24 // 一天的毫秒数
let interval = 1000 * 10 // 卫星位置计算间隔(10秒)
let min = max - day // 时间轴起始时间(基准时间前一天)
let multiplier = 60 // 时间轴快进倍数
let showFlyObject = {} // 星间/星地通信链路存储
const showGroundObject = {} // 地面通信链路存储
let satelliteObj = {} // 卫星信息存储
// Cesium数据源定义
let satellites = new Cesium.CustomDataSource('satellite') // 卫星数据源
let polylines = new Cesium.CustomDataSource('statelliteLine') // 卫星轨道数据源
let radars = new Cesium.CustomDataSource('radar') // 雷达数据源
let connection = new Cesium.CustomDataSource('connection') // 通信链路数据源
// 3D模型映射
let satellitesModelMap = { 'LEO': '../models/satellite.glb' } // 卫星模型
let radarsModelMap = { 'radar': '../models/radar.glb', 'radar2': '../models/radar2.glb', 'ykCenter': '../models/ykCenter.glb' } // 雷达模型
// 通信链路状态映射
let polylineMaterialMap = {
  'default': new Cesium.Color(1.0, 1.0, 1.0, 1),
  'success': new Cesium.Color(0.0, 1.0, 0.0, 1),
  'warning': new Cesium.Color(1.0, 1.0, 0.0, 1),
  'error': new Cesium.Color(1.0, 0.0, 0.0, 1)
}
let connectionStateMap = {
  'default': '无业务时线路通达',
  'success': '有业务流、业务流正常',
  'warning': '有业务流、质量差',
  'error': '有业务流、线路断'
}
let radarPoints = [] // 雷达点位数据存储

export default {
  name: 'LaserNetworkManager',
  data () {
    return {
      tabs: ['高轨', '倾斜轨', '近地轨'],
      tabIndex: 1, // 默认选中倾斜轨
      handler: null // 鼠标事件处理器
    }
  },
  mounted () {
    this.initCesium() // 初始化Cesium
  },
  destroyed () {
    this.destroyCesiumViewer() // 销毁Cesium实例,释放资源
  },
  methods: {
    // 销毁Cesium视图
    destroyCesiumViewer () {
      if (viewer) {
        viewer.destroy()
        viewer = null
      }
      console.log('Cesium viewer destroyed')
    },

    // 初始化Cesium地球
    initCesium () {
      // 初始化Cesium视图
      viewer = new Cesium.Viewer('cesiumContainer', {
        homeButton: true, // 主页按钮
        sceneModePicker: false, // 隐藏场景模式切换
        baseLayerPicker: false, // 隐藏底图切换
        animation: true, // 显示动画控件
        infoBox: true, // 显示信息弹窗
        selectionIndicator: true, // 显示选中指示器
        geocoder: false, // 隐藏地名搜索
        timeline: true, // 显示时间轴
        fullscreenButton: true, // 显示全屏按钮
        shouldAnimate: true, // 启用动画
        navigationHelpButton: false // 隐藏帮助按钮
      })

      // 基础配置优化
      viewer._cesiumWidget._creditContainer.style.display = 'none' // 隐藏Cesium Logo
      viewer.scene.skyBox.show = true // 显示星空
      viewer.scene.backgroundColor = Cesium.Color.BLACK // 背景色设为黑色
      viewer.scene.sun.show = true // 显示太阳
      viewer.scene.moon.show = true // 显示月亮
      viewer.scene.skyAtmosphere.show = true // 显示大气层
      viewer.scene.postProcessStages.fxaa.enabled = true // 开启抗锯齿
      viewer.scene.globe.depthTestAgainstTerrain = false // 关闭地形深度检测

      // 加载天地图底图(影像+路网注记)
      viewer.imageryLayers.addImageryProvider(new Cesium.WebMapTileServiceImageryProvider({
        url: `http://t0.tianditu.gov.cn/cia_w/wmts?tk=${myMapToken}`,
        layer: 'cia',
        style: 'default',
        tileMatrixSetID: 'w',
        format: 'tiles',
        maximumLevel: 18
      }))

      // 设置初始相机视角
      viewer.scene.camera.setView({
        destination: new Cesium.Cartesian3.fromDegrees(116.4074, 0.9042, this.tabIndex === 0 ? 120000000 : 25000000),
        orientation: {}
      })

      // 重写主页按钮行为
      viewer.homeButton.viewModel.command.beforeExecute.addEventListener((e) => {
        e.cancel = true
        viewer.camera.flyTo({
          destination: new Cesium.Cartesian3.fromDegrees(116.4074, 0.9042, this.tabIndex === 0 ? 120000000 : 25000000),
          duration: 2.0
        })
      })

      // 监听地球瓦片加载完成事件,隐藏加载遮罩
      viewer.scene.globe.tileLoadProgressEvent.addEventListener(() => {
        if (viewer.scene.globe.tilesLoaded) {
          document.getElementById('loadingOverlay').style.display = 'none'
        }
      })

      // 初始化时间轴
      this.setTimeline()
      // 加载核心数据
      this.onViewerLoaded(viewer, this.tabIndex)
    },

    // 视图加载完成后初始化数据和事件
    onViewerLoaded (Viewer, tabIndex) {
      viewer = Viewer
      this.handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas)
      // 加载卫星、雷达数据
      this.loadSatelliteData(`satellite${tabIndex}`)
      this.loadRadarData()
      // 绑定鼠标事件
      this.bindMouseEvents()
      // 监听时钟 tick 事件,实时更新链路状态
      viewer.clock.onTick.addEventListener(() => {
        this.computeSatelliteToRadarRange() // 星地链路计算
        this.computeSatellitesRange() // 星间链路计算
        this.computeRadarsRange() // 地地链路计算
      })
    },

    // 初始化时间轴(东八区时间,循环播放)
    setTimeline () {
      const startTime = Cesium.JulianDate.fromDate(new Date(min))
      const stopTime = Cesium.JulianDate.fromDate(new Date(max))
      viewer.clock.startTime = startTime.clone()
      viewer.clock.stopTime = stopTime.clone()
      viewer.clock.currentTime = stopTime.clone()
      viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP // 循环播放
      viewer.clock.multiplier = multiplier // 快进倍数
      viewer.timeline.zoomTo(startTime, stopTime) // 时间轴适配范围
      // 自定义时间格式化
      viewer.animation.viewModel.dateFormatter = DateTimeFormatter
      viewer.animation.viewModel.timeFormatter = TimeFormatter
      viewer.timeline.makeLabel = DateTimeFormatter
    },

    // 加载卫星数据(基于TLE)
    loadSatelliteData (jsonName) {
      document.getElementById('loadingOverlay').style.display = 'block'
      fetch(`../json/${jsonName}.json`)
        .then((res) => res.json())
        .then((data) => {
          satelliteObj = {} // 清空卫星数据
          // 遍历卫星数据,解析TLE并计算位置
          for (const key in data) {
            if (Object.prototype.hasOwnProperty.call(data, key)) {
              const element = data[key]
              // 解析TLE数据
              const satrec = twoline2satrec(element.tle[0], element.tle[1])
              // 存储卫星基础信息
              satelliteObj[key] = {
                id: key,
                name: key,
                country: element.country,
                role: element.role,
                networkSystem: element.networkSystem,
                type: element.type,
                networkState: element.networkState,
                times: [], // 时间戳数组
                positions: [], // 位置数组(经纬度+高度)
                toSatellitePolyLines: element.toSatellitePolyLines, // 星间链路配置
                toRadarPolyLines: element.toRadarPolyLines // 星地链路配置
              }

              // 预计算卫星在时间区间内的所有位置
              let longitude, latitude, height
              for (let index = min; index <= nowTime; index += interval) {
                // 计算卫星在当前时间的位置和速度
                const positionAndVelocity = propagate(satrec, new Date(index))
                const positionEci = positionAndVelocity.position // ECI坐标系位置
                const gmst = gstime(new Date(index)) // 格林威治恒星时
                const positionGd = eciToGeodetic(positionEci, gmst) // 转换为大地坐标系
                // 转换为角度并存储
                longitude = degreesLong(positionGd.longitude)
                latitude = degreesLat(positionGd.latitude)
                height = positionGd.height * 1000 // 高度转换为米
                satelliteObj[key].times.push(index)
                satelliteObj[key].positions.push([longitude, latitude, height])
              }
            }
          }
          // 计算卫星飞行轨迹并添加到视图
          this.computeCircularFlight(satelliteObj)
          document.getElementById('loadingOverlay').style.display = 'none'
        }).catch(() => {
          document.getElementById('loadingOverlay').style.display = 'none'
          console.error('卫星数据加载失败')
        })
    },

    // 加载雷达数据
    loadRadarData () {
      fetch('../json/radar.json')
        .then((res) => res.json())
        .then((data) => {
          radars.entities.removeAll() // 清空原有雷达实体
          radarPoints = data // 存储雷达数据
          // 遍历创建雷达实体
          data.forEach((item) => {
            this.createRadar(item.id, item.lon, item.lat, item.type, item.toRadarPolyLines)
          })
        })
    },

    // 创建雷达实体
    createRadar (id, lon, lat, type, toRadarPolyLines) {
      radars.entities.add({
        id: id,
        model: {
          uri: radarsModelMap[type], // 雷达3D模型
          minimumPixelSize: 32, // 最小像素大小
          maximumScale: 10000000 // 最大缩放比例
        },
        label: new Cesium.LabelGraphics({ // 雷达标签
          text: id,
          font: '12px sans-serif',
          fillColor: Cesium.Color.RED,
          style: Cesium.LabelStyle.FILL_AND_OUTLINE,
          outlineColor: Cesium.Color.WHITE,
          outlineWidth: 1,
          pixelOffset: new Cesium.Cartesian2(0, 14)
        }),
        position: Cesium.Cartesian3.fromDegrees(lon, lat, 0), // 雷达位置(经纬度+高度)
        toRadarPolyLines: toRadarPolyLines // 雷达间链路配置
      })
      viewer.dataSources.add(radars) // 添加雷达数据源到视图
    },

    // 计算卫星飞行轨迹并创建卫星实体
    computeCircularFlight (obj, hasLine = false) {
      for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          const element = obj[key]
          const property = new Cesium.SampledPositionProperty() // 采样位置属性(支持时间插值)
          const length = element.positions?.length
          const positions = []
          let p, t

          // 填充卫星位置数据
          for (let index = 0; index < length; index++) {
            p = element.positions[index]
            t = element.times[index]
            // 添加位置采样点(时间+坐标)
            property.addSample(
              Cesium.JulianDate.fromDate(new Date(t)),
              Cesium.Cartesian3.fromDegrees(p[0], p[1], p[2])
            )
            positions.push(...element.positions[index])
          }

          // 添加卫星实体到视图
          satellites.entities.add({
            id: key,
            name: key,
            model: {
              uri: satellitesModelMap[element.role], // 卫星3D模型
              minimumPixelSize: this.tabIndex === 0 ? 96 : 32, // 高轨卫星放大显示
              maximumScale: 200000,
              color: new Cesium.Color(1.0, 1.0, 1.0, 1.0)
            },
            // 卫星属性信息(用于弹窗展示)
            country: element.country,
            role: element.role,
            networkSystem: element.networkSystem,
            type: element.type,
            networkState: element.networkState,
            toSatellitePolyLines: element.toSatellitePolyLines,
            toRadarPolyLines: element.toRadarPolyLines,
            position: property, // 卫星位置(随时间变化)
            orientation: new Cesium.VelocityOrientationProperty(property), // 基于速度的朝向
            description: `
              卫星名称:${key}<br/>
              卫星标识:XXX <br/>
              研制单位:XXX <br/>
              模块组成:XXX <br/>
              首次入轨时间:XXX  <br/>
              首次通信时间:XXX `
          })

          // 可选:显示卫星轨道线
          if (hasLine) {
            polylines.entities.add({
              id: key,
              polyline: {
                width: 1,
                material: Cesium.Color.BLUE.withAlpha(0.5),
                positions: Cesium.Cartesian3.fromDegreesArrayHeights(positions)
              },
              description: `这是${key}的运行轨道`
            })
          }
        }
      }

      // 添加数据源到视图
      viewer.dataSources.add(satellites)
      viewer.dataSources.add(polylines)
      viewer.dataSources.add(connection)
    },

    // 绑定鼠标事件
    bindMouseEvents () {
      // 双击雷达聚焦
      this.handler.setInputAction((movement) => {
        const pickedObject = viewer.scene.pick(movement.position)
        if (Cesium.defined(pickedObject)) {
          const id = pickedObject.id.id
          console.log(`左击${id}`)
          // 显示可拖拽信息弹窗
          this.showDraggableInfoBox(movement.position)
          // 判断是否点击雷达
          const isFocusRadar = radarPoints.find(item => item.id === id)
          if (isFocusRadar) {
            // 相机飞至雷达上方
            const newCartesian = Cesium.Cartesian3.fromDegrees(
              isFocusRadar.lon,
              isFocusRadar.lat - 15, // 纬度减15度,便于观察
              1000000 // 高度100万米
            )
            viewer.camera.flyTo({
              destination: newCartesian,
              orientation: {
                heading: Cesium.Math.toRadians(0.0),
                pitch: Cesium.Math.toRadians(-30.0),
                roll: 0.0
              },
              duration: 2.0
            })
          }
        }
      }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK)

      // 右键查看信息
      this.handler.setInputAction((movement) => {
        const pickedObject = viewer.scene.pick(movement.position)
        if (Cesium.defined(pickedObject)) {
          const id = pickedObject.id.id
          console.log(`右击${id}`)
          // 可扩展:右键菜单(显示详情、隐藏实体等)
        }
      }, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
    },

    // 显示可拖拽信息弹窗
    showDraggableInfoBox (position) {
      const infoBoxElement = document.querySelector('.cesium-infoBox')
      if (!infoBoxElement) return
      // 设置弹窗初始位置
      infoBoxElement.style.top = position.y + 'px'
      infoBoxElement.style.left = position.x + 10 + 'px'
      infoBoxElement.style.position = 'absolute'
      infoBoxElement.style.zIndex = '9999'

      let isDragging = false
      let initialX = 0
      let initialY = 0

      // 拖拽逻辑
      infoBoxElement.addEventListener('mousedown', (event) => {
        isDragging = true
        initialX = event.clientX - infoBoxElement.offsetLeft
        initialY = event.clientY - infoBoxElement.offsetTop
      })

      document.addEventListener('mousemove', (event) => {
        if (isDragging) {
          const offsetX = event.clientX - initialX
          const offsetY = event.clientY - initialY
          infoBoxElement.style.left = offsetX + 'px'
          infoBoxElement.style.top = offsetY + 'px'
        }
      })

      document.addEventListener('mouseup', () => {
        isDragging = false
      })
    },

    // 通信链路计算核心方法(通用)
    computeRange (entities1, entities2, polyLinesProperty, resultObject) {
      entities1.entities.values.forEach((entity1) => {
        entities2.entities.values.forEach((entity2) => {
          if (entity1 === entity2) return // 跳过自身
          // 获取两个实体当前时间的位置
          const pos1 = entity1.position?.getValue(viewer.clock.currentTime)
          const pos2 = entity2.position?.getValue(viewer.clock.currentTime)
          const currentTimestamp = JulianDateToTimestamp(viewer.clock.currentTime)

          // 查找当前实体的链路配置
          const linkConfig = entity1[polyLinesProperty]?.find(item => item.id === `${entity1.id} - ${entity2.id}`)
          if (!linkConfig) return

          // 判断当前时间是否在链路有效区间内
          const isLinkActive = this.findBooleanByTimestamp(linkConfig.show, currentTimestamp)
          const linkState = this.findStateByTimestamp(linkConfig.show, currentTimestamp)

          if (pos1 && pos2 && isLinkActive) {
            // 转换为经纬度高度
            const po1 = degreesFromCartesian(pos1)
            const po2 = degreesFromCartesian(pos2)
            // 更新或创建链路实体
            if (resultObject[`${entity1.id} - ${entity2.id}`]) {
              resultObject[`${entity1.id} - ${entity2.id}`].show = true
              resultObject[`${entity1.id} - ${entity2.id}`].state = linkState
              resultObject[`${entity1.id} - ${entity2.id}`].po1 = po1
              resultObject[`${entity2.id} - ${entity1.id}`].po2 = po2
            } else {
              resultObject[`${entity1.id} - ${entity2.id}`] = {
                entity: null,
                show: true,
                state: linkState,
                po1: po1,
                po2: po2
              }
            }
          } else if (resultObject[`${entity1.id} - ${entity2.id}`]) {
            // 链路无效时隐藏
            resultObject[`${entity1.id} - ${entity2.id}`].show = false
          }
        })
      })
    },

    // 根据时间戳查找链路是否显示
    findBooleanByTimestamp (showIntervals, timestamp) {
      for (const interval of showIntervals || []) {
        const [start, end] = interval.interval.split('/').map(Number)
        if (timestamp >= start && timestamp < end) {
          return interval.boolean
        }
      }
      return false
    },

    // 根据时间戳查找链路状态
    findStateByTimestamp (showIntervals, timestamp) {
      for (const interval of showIntervals || []) {
        const [start, end] = interval.interval.split('/').map(Number)
        if (timestamp >= start && timestamp < end) {
          return interval.state
        }
      }
      return 'default'
    },

    // 星地链路计算
    computeSatelliteToRadarRange () {
      this.computeRange(satellites, radars, 'toRadarPolyLines', showFlyObject)
      this.updateFlyLine()
    },

    // 星间链路计算
    computeSatellitesRange () {
      this.computeRange(satellites, satellites, 'toSatellitePolyLines', showFlyObject)
      this.updateFlyLine()
    },

    // 地地链路计算
    computeRadarsRange () {
      this.computeRange(radars, radars, 'toRadarPolyLines', showGroundObject)
      this.updateGroundLine()
    },

    // 更新星间/星地链路(飞线)
    updateFlyLine () {
      for (const key in showFlyObject) {
        const link = showFlyObject[key]
        if (!link.entity) {
          link.entity = this.createFlyLine(key)
        }
        link.entity.show = link.show
        // 更新链路材质颜色(根据状态)
        link.entity.polyline.material.color = polylineMaterialMap[link.state]
      }
    },

    // 创建飞线实体
    createFlyLine (id) {
      const linePositions = new Cesium.CallbackProperty(() => {
        const link = showFlyObject[id]
        return Cesium.Cartesian3.fromDegreesArrayHeights([
          link.po1.longitude, link.po1.latitude, link.po1.height,
          link.po2.longitude, link.po2.latitude, link.po2.height
        ])
      }, false)

      return connection.entities.add({
        id: id,
        polyline: {
          positions: linePositions,
          width: 2,
          arcType: Cesium.ArcType.NONE,
          // 自定义流动材质
          material: new LineFlowMaterialProperty({
            color: Cesium.Color.WHITE,
            speed: 5,
            percent: 0.5,
            gradient: 0.5
          })
        },
        description: `
          链路名称:${id}<br/>
          通断状态:${connectionStateMap[showFlyObject[id].state]}<br/>
          链路类型:星间同轨 <br/>
          链路距离:XXX km <br/>
          相位差:XXX μrad(微弧度)<br/>
          轨道高度:XXX km<br/>
          双端终端ID:${id} <br/>
          A/B状态:A <br/>
          通信速率:XXX Mbps/Gbps`
      })
    },

    // 更新地面链路
    updateGroundLine () {
      for (const key in showGroundObject) {
        const link = showGroundObject[key]
        if (!link.entity) {
          link.entity = this.createGroundLine(key)
        }
        link.entity.show = link.show
      }
    },

    // 创建地面链路实体
    createGroundLine (id) {
      return connection.entities.add({
        id: id,
        polyline: {
          positions: new Cesium.CallbackProperty(() => {
            const link = showGroundObject[id]
            return Cesium.Cartesian3.fromDegreesArrayHeights([
              link.po1.longitude, link.po1.latitude, link.po1.height,
              link.po2.longitude, link.po2.latitude, link.po2.height
            ])
          }, false),
          width: 3,
          material: new LineFlowMaterialProperty({
            color: showGroundObject[id].state,
            speed: 10,
            percent: 0.5,
            gradient: 0.5
          }),
          clampToGround: true // 贴地显示
        },
        description: `这是${id}通信线`
      })
    },

    // 轨道切换
    changeTab (index) {
      this.tabIndex = index
      // 清空原有数据
      satellites.entities.removeAll()
      polylines.entities.removeAll()
      connection.entities.removeAll()
      showFlyObject = {}
      // 重新加载卫星数据
      this.loadSatelliteData(`satellite${index}`)
      // 调整相机视角
      viewer.scene.camera.setView({
        destination: new Cesium.Cartesian3.fromDegrees(116.4074, 0.9042, this.tabIndex === 0 ? 120000000 : 25000000),
        orientation: {}
      })
      // 重新计算链路
      this.computeSatelliteToRadarRange()
      this.computeSatellitesRange()
      this.computeRadarsRange()
    }
  }
}
</script>

<style scoped lang="scss">
#myCesium {
  width: 100vw;
  height: 100vh;
  position: relative;

  .header {
    position: absolute;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 100;

    .title {
      font-size: 24px;
      color: #fff;
      font-weight: bold;
      text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
    }
  }

  #cesiumContainer {
    width: 100%;
    height: 100%;
  }

  #loadingOverlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.8);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    color: #fff;
    font-size: 24px;
  }

  .tabContainer {
    position: absolute;
    bottom: 30px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 100;

    .tabs {
      display: flex;
      gap: 20px;
      list-style: none;

      .tab {
        padding: 8px 16px;
        background: rgba(0, 0, 0, 0.6);
        color: #fff;
        border-radius: 4px;
        cursor: pointer;
        transition: all 0.3s;

        &:hover {
          background: rgba(0, 0, 0, 0.8);
        }

        &.active {
          background: #1E90FF;
          color: #fff;
        }
      }
    }
  }
}
</style>

4. 自定义流动材质(LineFlowMaterialProperty.js)

实现通信链路的流动效果,核心是通过 Shader 动态修改纹理坐标:

import * as Cesium from 'cesium';
class LineFlowMaterialProperty {
  constructor (options) {
    this._definitionChanged = new Cesium.Event()
    this._color = undefined
    this._speed = undefined
    this._percent = undefined
    this._gradient = undefined
    this._positionOffset = undefined
    this.color = options.color
    this.speed = options.speed
    this.percent = options.percent
    this.gradient = options.gradient
    this.positionOffset = options.positionOffset
  }

  get isConstant () {
    return false
  }

  get definitionChanged () {
    return this._definitionChanged
  }

  getType (time) {
    return Cesium.Material.LineFlowMaterialType
  }

  getValue (time, result) {
    if (!Cesium.defined(result)) {
      result = {}
    }

    result.color = Cesium.Property.getValueOrDefault(this._color, time, Cesium.Color.RED, result.color)
    result.speed = Cesium.Property.getValueOrDefault(this._speed, time, 5.0, result.speed)
    result.percent = Cesium.Property.getValueOrDefault(this._percent, time, 0.1, result.percent)
    result.gradient = Cesium.Property.getValueOrDefault(this._gradient, time, 0.01, result.gradient)
    result.positionOffset = Cesium.Property.getValueOrDefault(this._positionOffset, time, 0.01, result.positionOffset)
    return result
  }

  equals (other) {
    return (this === other ||
          (other instanceof LineFlowMaterialProperty &&
              Cesium.Property.equals(this._color, other._color) &&
              Cesium.Property.equals(this._speed, other._speed) &&
              Cesium.Property.equals(this._percent, other._percent) &&
              Cesium.Property.equals(this._gradient, other._gradient) &&
              Cesium.Property.equals(this._positionOffset, other._positionOffset))
    )
  }
}

Object.defineProperties(LineFlowMaterialProperty.prototype, {
  color: Cesium.createPropertyDescriptor('color'),
  speed: Cesium.createPropertyDescriptor('speed'),
  percent: Cesium.createPropertyDescriptor('percent'),
  gradient: Cesium.createPropertyDescriptor('gradient'),
  positionOffset: Cesium.createPropertyDescriptor('positionOffset')
})

Cesium.LineFlowMaterialProperty = LineFlowMaterialProperty
Cesium.Material.LineFlowMaterialProperty = 'LineFlowMaterialProperty'
Cesium.Material.LineFlowMaterialType = 'LineFlowMaterialType'
Cesium.Material.LineFlowMaterialSource =
  `
  uniform vec4 color;
  uniform float speed;
  uniform float percent;
  uniform float gradient;
  uniform float positionOffset;
  
  czm_material czm_getMaterial(czm_materialInput materialInput){
    czm_material material = czm_getDefaultMaterial(materialInput);
    vec2 st = materialInput.st;
    float t =fract(czm_frameNumber * speed/ 1000.0);
    t *= (1.0 + percent);
    float position = mod(st.s + positionOffset, 1.0);
    float alpha = smoothstep(t- percent, t, st.s) * step(-t, -st.s);
    alpha += gradient;
    material.diffuse = color.rgb;
    material.alpha = alpha;
    return material;
  }
  `

Cesium.Material._materialCache.addMaterial(Cesium.Material.LineFlowMaterialType, {
  fabric: {
    type: Cesium.Material.LineFlowMaterialType,
    uniforms: {
      color: new Cesium.Color(0.0, 0.0, 0.0, 0),
      speed: 10.0,
      percent: 0.1,
      gradient: 0.1,
      positionOffset: 50
    },
    source: Cesium.Material.LineFlowMaterialSource
  },
  translucent: function (material) {
    return true
  }
})

5. 数据格式说明

(1)卫星 TLE 数据(satellite1.json)

{
  "卫星1": {
    "tle": [
      "1 25544U 98067A   24035.58333333  .00016717  00000+0  10270-3 0  9995",
      "2 25544  51.6400  45.4650 0006703  30.1234  329.8766 15.50000000 32425"
    ],
    "country": "中国",
    "role": "LEO",
    "networkSystem": "激光通信网",
    "type": "倾斜轨",
    "networkState": "正常",
    "toSatellitePolyLines": [
      {
        "id": "卫星1 - 卫星2",
        "show": [
          { "interval": "1704105600000/1704192000000", "boolean": true, "state": "success" },
          { "interval": "1704192000000/1704278400000", "boolean": false, "state": "default" }
        ]
      }
    ],
    "toRadarPolyLines": [
      {
        "id": "卫星1 - 雷达A",
        "show": [
          { "interval": "1704105600000/1704192000000", "boolean": true, "state": "success" }
        ]
      }
    ]
  }
}

(2)雷达数据(radar.json)

[
  {
    "id": "雷达A",
    "lon": 116.4074,
    "lat": 39.9042,
    "type": "radar",
    "toRadarPolyLines": [
      {
        "id": "雷达A - 雷达B",
        "show": [
          { "interval": "1704105600000/1704278400000", "boolean": true, "state": "success" }
        ]
      }
    ]
  }
]