feat: 添加卫星管理功能,新增 SatelliteOptions 接口,更新相关组件以支持卫星实体的添加和移除
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { eciToEcf, gstime, propagate, type SatRec, twoline2satrec } from 'satellite.js';
|
||||
|
||||
import { VIEWER_OPTIONS_FN } from './VIEWER_OPTIONS';
|
||||
|
||||
@ -11,10 +12,24 @@ export interface GroundStationOptions {
|
||||
pixelSize?: number; // 点的可选像素大小
|
||||
}
|
||||
|
||||
// 卫星选项接口
|
||||
export interface SatelliteOptions {
|
||||
id: string; // 卫星的唯一标识符
|
||||
orbitDurationHours?: number; // 轨道显示时长(小时),默认为 2
|
||||
showOrbit?: boolean; // 是否显示完整轨道线,默认为 true
|
||||
timeStepSeconds?: number; // 轨道计算步长(秒),默认为 30
|
||||
tle: string; // 包含卫星名称和两行 TLE 数据的字符串,格式如下:
|
||||
// NAME
|
||||
// TLE1
|
||||
// TLE2
|
||||
}
|
||||
|
||||
export class HCesiumViewerCls {
|
||||
private viewer: Cesium.Viewer | null = null;
|
||||
// 用于存储当前地面站实体的 Map
|
||||
currentStationEntities: Map<string, Cesium.Entity> = new Map();
|
||||
// 用于存储当前卫星实体的 Map (包括轨道实体)
|
||||
currentSatelliteEntities: Map<string, { entity: Cesium.Entity; orbitEntity?: Cesium.Entity }> = new Map();
|
||||
|
||||
/**
|
||||
* 初始化 Cesium Viewer
|
||||
@ -22,10 +37,14 @@ export class HCesiumViewerCls {
|
||||
*/
|
||||
initCesiumViewer(container: ConstructorParameters<typeof Cesium.Viewer>[0]) {
|
||||
this.viewer = new Cesium.Viewer(container, VIEWER_OPTIONS_FN());
|
||||
|
||||
// 初始化时清空可能存在的旧实体引用
|
||||
this.currentStationEntities.clear();
|
||||
this.currentSatelliteEntities.clear();
|
||||
}
|
||||
|
||||
// region 地面站点相关方法
|
||||
|
||||
/**
|
||||
* 向视图中添加地面站实体
|
||||
* @param options - 地面站的选项参数
|
||||
@ -108,13 +127,220 @@ export class HCesiumViewerCls {
|
||||
}
|
||||
this.currentStationEntities.clear();
|
||||
}
|
||||
// endregion 地面站点相关方法
|
||||
|
||||
// region 卫星相关方法
|
||||
|
||||
/**
|
||||
* 向视图中添加卫星实体及其轨道
|
||||
* @param options - 卫星的选项参数
|
||||
* @returns 添加的卫星实体对象,如果添加失败则返回 null。
|
||||
*/
|
||||
addSatellite(options: SatelliteOptions): Cesium.Entity | null {
|
||||
if (!this.viewer) {
|
||||
console.error('视图未初始化。无法添加卫星。');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否已存在相同 ID 的实体
|
||||
if (this.currentSatelliteEntities.has(options.id)) {
|
||||
console.warn(`ID 为 "${options.id}" 的卫星实体已存在,跳过添加。`);
|
||||
return this.currentSatelliteEntities.get(options.id)?.entity || null;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
tle,
|
||||
orbitDurationHours = 2, // 默认轨道时长 2 小时
|
||||
timeStepSeconds = 30, // 默认步长 30 秒
|
||||
showOrbit = true, // 默认显示轨道
|
||||
} = options;
|
||||
|
||||
// --- 从 tle 字符串解析 name, tle1, tle2 ---
|
||||
const tleLines = tle.trim().split('\n');
|
||||
if (tleLines.length < 3) {
|
||||
console.error(`无效的 TLE 格式 (ID: ${id}): TLE 字符串 "${tle}" 至少需要三行`);
|
||||
return null;
|
||||
}
|
||||
const name = tleLines[0].trim();
|
||||
const tle1 = tleLines[1].trim();
|
||||
const tle2 = tleLines[2].trim();
|
||||
// --- 解析结束 ---
|
||||
|
||||
let satrec: SatRec;
|
||||
try {
|
||||
// 解析 TLE 数据
|
||||
satrec = twoline2satrec(tle1, tle2);
|
||||
} catch (error) {
|
||||
console.error(`解析 TLE 失败 (ID: ${id}):`, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建卫星实体
|
||||
const satelliteEntity = this.viewer.entities.add({
|
||||
id: id,
|
||||
name: name,
|
||||
label: {
|
||||
text: name,
|
||||
font: '14pt sans-serif',
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||||
outlineWidth: 2,
|
||||
pixelOffset: new Cesium.Cartesian2(0, -10), // 标签偏移
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||||
fillColor: Cesium.Color.WHITE,
|
||||
outlineColor: Cesium.Color.BLACK,
|
||||
},
|
||||
// 使用点表示卫星
|
||||
point: {
|
||||
pixelSize: 8,
|
||||
color: Cesium.Color.YELLOW,
|
||||
outlineColor: Cesium.Color.WHITE,
|
||||
outlineWidth: 1,
|
||||
},
|
||||
// 动态轨迹路径
|
||||
path: {
|
||||
resolution: 1,
|
||||
material: new Cesium.PolylineGlowMaterialProperty({
|
||||
glowPower: 0.15,
|
||||
color: Cesium.Color.BLUE,
|
||||
}),
|
||||
width: 2,
|
||||
leadTime: (orbitDurationHours * 3600) / 2, // 显示未来一半时间的轨迹
|
||||
trailTime: (orbitDurationHours * 3600) / 2, // 显示过去一半时间的轨迹
|
||||
},
|
||||
});
|
||||
|
||||
// --- 计算轨道 ---
|
||||
const totalSeconds = orbitDurationHours * 60 * 60;
|
||||
const startTime = this.viewer.clock.currentTime; // 使用当前 viewer 的时间作为起点
|
||||
const positionProperty = new Cesium.SampledPositionProperty();
|
||||
const orbitPositions: Cesium.Cartesian3[] = []; // 用于存储完整轨道点
|
||||
|
||||
for (let i = 0; i <= totalSeconds; i += timeStepSeconds) {
|
||||
const time = Cesium.JulianDate.addSeconds(startTime, i, new Cesium.JulianDate());
|
||||
const jsDate = Cesium.JulianDate.toDate(time);
|
||||
|
||||
try {
|
||||
const positionAndVelocity = propagate(satrec, jsDate);
|
||||
if (typeof positionAndVelocity.position === 'boolean') {
|
||||
// 如果 propagate 返回布尔值,说明计算出错或卫星已衰减
|
||||
console.warn(`卫星 ${id} 在时间 ${jsDate} 位置计算失败或已衰减。`);
|
||||
continue; // 跳过这个时间点
|
||||
}
|
||||
|
||||
const gmst = gstime(jsDate);
|
||||
const positionEcf = eciToEcf(positionAndVelocity.position, gmst);
|
||||
|
||||
// 转换为 Cesium 坐标(单位:米)
|
||||
const cesiumPosition = new Cesium.Cartesian3(positionEcf.x * 1000, positionEcf.y * 1000, positionEcf.z * 1000);
|
||||
|
||||
// 添加位置样本
|
||||
positionProperty.addSample(time, cesiumPosition);
|
||||
if (showOrbit) {
|
||||
orbitPositions.push(cesiumPosition);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`计算卫星 ${id} 在时间 ${jsDate} 的位置时出错:`, error);
|
||||
// 可以在这里决定是跳过还是中断循环
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置卫星的位置和方向
|
||||
satelliteEntity.position = positionProperty;
|
||||
satelliteEntity.orientation = new Cesium.VelocityOrientationProperty(positionProperty);
|
||||
|
||||
let orbitEntity: Cesium.Entity | undefined;
|
||||
// 添加完整轨道线(如果需要)
|
||||
if (showOrbit && orbitPositions.length > 1) {
|
||||
orbitEntity = this.viewer.entities.add({
|
||||
id: `${id}-orbit`, // 轨道实体 ID
|
||||
name: `${name} 轨道`, // 使用解析出的 name
|
||||
polyline: {
|
||||
positions: orbitPositions,
|
||||
width: 1,
|
||||
material: new Cesium.PolylineDashMaterialProperty({
|
||||
color: Cesium.Color.CYAN.withAlpha(0.5), // 半透明青色虚线
|
||||
dashLength: 16,
|
||||
}),
|
||||
clampToGround: false, // 轨道不贴地
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 存储实体引用
|
||||
this.currentSatelliteEntities.set(id, { entity: satelliteEntity, orbitEntity });
|
||||
|
||||
return satelliteEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从视图中移除指定的卫星实体及其轨道 (通过 ID)
|
||||
* @param entityId - 要移除的卫星实体的 ID
|
||||
* @returns 如果成功移除则返回 true,否则返回 false
|
||||
*/
|
||||
removeSatelliteById(entityId: string): boolean {
|
||||
if (!this.viewer) {
|
||||
console.error('视图未初始化。无法移除卫星。');
|
||||
return false;
|
||||
}
|
||||
const satelliteData = this.currentSatelliteEntities.get(entityId);
|
||||
if (satelliteData) {
|
||||
let removedMain = true;
|
||||
let removedOrbit = true;
|
||||
|
||||
// 移除卫星主体
|
||||
if (satelliteData.entity) {
|
||||
removedMain = this.viewer.entities.remove(satelliteData.entity);
|
||||
if (!removedMain) {
|
||||
console.warn(`尝试从 Cesium 移除卫星主体 ID 为 "${entityId}" 的实体失败。`);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除轨道线
|
||||
if (satelliteData.orbitEntity) {
|
||||
removedOrbit = this.viewer.entities.remove(satelliteData.orbitEntity);
|
||||
if (!removedOrbit) {
|
||||
console.warn(`尝试从 Cesium 移除卫星轨道 ID 为 "${satelliteData.orbitEntity.id}" 的实体失败。`);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果主体和轨道(如果存在)都移除成功或不存在,则从 Map 中删除
|
||||
if (removedMain && removedOrbit) {
|
||||
this.currentSatelliteEntities.delete(entityId);
|
||||
return true;
|
||||
} else {
|
||||
// 如果有任何一个移除失败,保留在 Map 中可能有助于调试,但返回 false
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// console.warn(`未在 Map 中找到 ID 为 "${entityId}" 的卫星实体,无法移除。`); // 可能在 clearAll 时触发
|
||||
return false; // Map 中未找到
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除所有卫星实体和轨道 */
|
||||
clearAllSatellites() {
|
||||
if (!this.viewer) return;
|
||||
// 迭代 Map 的 ID 进行移除,避免在迭代 Map 值时修改 Map
|
||||
const idsToRemove = [...this.currentSatelliteEntities.keys()];
|
||||
for (const id of idsToRemove) {
|
||||
this.removeSatelliteById(id); // 使用封装好的移除方法
|
||||
}
|
||||
// 确保清空 Map,即使移除过程中有失败
|
||||
this.currentSatelliteEntities.clear();
|
||||
}
|
||||
|
||||
// endregion 卫星相关方法
|
||||
|
||||
destroy() {
|
||||
if (this.viewer) {
|
||||
this.clearAllGroundStations();
|
||||
this.clearAllSatellites();
|
||||
this.viewer.destroy();
|
||||
this.viewer = null;
|
||||
}
|
||||
this.currentStationEntities.clear(); // 确保清空
|
||||
this.currentSatelliteEntities.clear(); // 确保清空
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import 'cesium/Build/Cesium/Widgets/widgets.css';
|
||||
|
||||
import type { GroundStationState } from './types';
|
||||
import type { GroundStationState, SatelliteState } from './types';
|
||||
|
||||
import { useHCesiumViewerCls } from './useHCesiumViewerCls';
|
||||
import { useHCesiumViewerClsGroundStation } from './useHCesiumViewerClsGroundStation';
|
||||
import { useHCesiumViewerClsSatellite } from './useHCesiumViewerClsSatellite';
|
||||
|
||||
const props = defineProps<{
|
||||
groundStationState?: GroundStationState;
|
||||
satelliteState?: SatelliteState;
|
||||
}>();
|
||||
|
||||
// 1. 管理 Cesium Viewer 实例生命周期
|
||||
@ -20,6 +22,13 @@ useHCesiumViewerClsGroundStation(
|
||||
() => props.groundStationState?.groundStations, // 从新的 prop 中获取列表
|
||||
() => props.groundStationState?.selectedIds, // 从新的 prop 中获取选中 ID
|
||||
);
|
||||
|
||||
// 3. 同步卫星实体
|
||||
useHCesiumViewerClsSatellite(
|
||||
() => hCesiumViewerInst, // 传递 viewer 实例 getter
|
||||
() => props.satelliteState?.satellites, // 传递卫星列表 getter
|
||||
() => props.satelliteState?.selectedIds, // 传递选中卫星 ID getter
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { GroundStationOptions } from './h-cesium-viewer-class';
|
||||
import type { GroundStationOptions, SatelliteOptions } from './h-cesium-viewer-class';
|
||||
|
||||
/**
|
||||
* 地面站状态接口
|
||||
@ -10,3 +10,14 @@ export interface GroundStationState {
|
||||
/** 选中的地面站 ID 数组 */
|
||||
selectedIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 卫星状态接口
|
||||
* 包含卫星列表和当前选中的卫星 ID 列表
|
||||
*/
|
||||
export interface SatelliteState {
|
||||
/** 卫星配置数组 */
|
||||
satellites: SatelliteOptions[];
|
||||
/** 选中的卫星 ID 数组 */
|
||||
selectedIds: string[];
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
// src/components/h-cesium-viewer/useHCesiumViewerClsSatellite.ts
|
||||
import type { HCesiumViewerCls, SatelliteOptions } from '@/components/h-cesium-viewer/h-cesium-viewer-class';
|
||||
import type { MaybeRefOrGetter } from 'vue';
|
||||
|
||||
/**
|
||||
* 管理 Cesium Viewer 中的卫星实体,根据选中的 ID 列表进行同步。
|
||||
* @param hCesiumViewerInst - HCesiumViewerCls 实例或其 getter。
|
||||
* @param satelliteList - 包含所有可用卫星选项的数组或 getter。
|
||||
* @param selectedSatelliteIds - 包含当前选中卫星 ID 的数组或 getter。
|
||||
*/
|
||||
export function useHCesiumViewerClsSatellite(
|
||||
hCesiumViewerInst: MaybeRefOrGetter<HCesiumViewerCls | null>,
|
||||
satelliteList: MaybeRefOrGetter<Array<SatelliteOptions> | undefined>,
|
||||
selectedSatelliteIds: MaybeRefOrGetter<string[] | undefined>,
|
||||
) {
|
||||
// 创建一个从 ID 到卫星选项的映射,方便查找
|
||||
const satelliteMap = computed(() => {
|
||||
const map = new Map<string, SatelliteOptions>();
|
||||
const list = toValue(satelliteList) ?? [];
|
||||
for (const satellite of list) {
|
||||
map.set(satellite.id, satellite);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
// watchEffect: 同步 selectedSatelliteIds 到 Cesium 实体
|
||||
watchEffect(() => {
|
||||
const viewerInstance = toValue(hCesiumViewerInst);
|
||||
if (!viewerInstance) {
|
||||
// 如果 viewer 实例尚未初始化或已销毁,则不执行任何操作
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = toValue(selectedSatelliteIds) ?? [];
|
||||
const selectedIdsSet = new Set(selectedIds); // 需要显示的卫星 ID
|
||||
const currentEntityIds = new Set(viewerInstance.currentSatelliteEntities.keys()); // 当前已显示的卫星 ID
|
||||
|
||||
// 1. 移除不再选中的卫星
|
||||
for (const entityId of currentEntityIds) {
|
||||
if (!selectedIdsSet.has(entityId)) {
|
||||
// 如果当前显示的实体不在新的选中列表里,则移除
|
||||
viewerInstance.removeSatelliteById(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 添加新增的选中卫星
|
||||
for (const selectedId of selectedIdsSet) {
|
||||
if (!currentEntityIds.has(selectedId)) {
|
||||
// 如果选中的 ID 对应的实体当前未显示,则添加
|
||||
const satelliteToAdd = satelliteMap.value.get(selectedId); // 从映射中查找卫星信息
|
||||
if (satelliteToAdd) {
|
||||
viewerInstance.addSatellite(satelliteToAdd);
|
||||
} else {
|
||||
// 如果在 satelliteList 中找不到对应的卫星信息,发出警告
|
||||
consola.warn(`无法找到 ID 为 "${selectedId}" 的卫星信息,无法添加到地图。`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 返回 satelliteMap 可能在某些场景下有用
|
||||
return {
|
||||
satelliteMap,
|
||||
};
|
||||
}
|
@ -4,7 +4,7 @@ meta:
|
||||
</route>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GroundStationState } from '@/components/h-cesium-viewer/types';
|
||||
import type { GroundStationState, SatelliteState } from '@/components/h-cesium-viewer/types';
|
||||
|
||||
// 地面站和选中状态
|
||||
const groundStationState = reactive<GroundStationState>({
|
||||
@ -17,6 +17,42 @@ groundStationState.groundStations = [
|
||||
{ height: 20, id: 'gs-gz', latitude: 23.1291, longitude: 113.2644, name: '广州站' },
|
||||
];
|
||||
|
||||
// >>>>>>>>> 卫星状态管理 >>>>>>>>>
|
||||
const satelliteState = reactive<SatelliteState>({
|
||||
satellites: [],
|
||||
selectedIds: [],
|
||||
});
|
||||
|
||||
// 初始化卫星列表
|
||||
satelliteState.satellites = [
|
||||
{
|
||||
id: 'STARLINK-11371',
|
||||
tle: `STARLINK-11371
|
||||
1 62879U 25024A 25062.93300820 .00003305 00000+0 21841-4 0 9995
|
||||
2 62879 42.9977 257.3937 0001725 269.2925 90.7748 15.77864921 5143`,
|
||||
showOrbit: true, // 明确显示轨道
|
||||
},
|
||||
{
|
||||
id: 'ISS (ZARYA)',
|
||||
tle: `国际空间站
|
||||
1 25544U 98067A 25091.51178241 .00016717 00000+0 30771-3 0 9997
|
||||
2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391587775`,
|
||||
showOrbit: true,
|
||||
},
|
||||
// 可以添加更多卫星...
|
||||
];
|
||||
|
||||
// 计算卫星 Checkbox Group 的 options
|
||||
const satelliteCheckboxOptions = computed(() =>
|
||||
satelliteState.satellites.map((sat) => ({
|
||||
// 从 TLE 字符串的第一行提取名称作为标签
|
||||
label: sat.tle.split('\n')[0].trim(),
|
||||
value: sat.id,
|
||||
})),
|
||||
);
|
||||
|
||||
// <<<<<<<<< 卫星状态管理 <<<<<<<<<
|
||||
|
||||
// 计算 Checkbox Group 的 options
|
||||
const stationCheckboxOptions = computed(() =>
|
||||
groundStationState.groundStations.map((station) => ({ label: station.name, value: station.id })),
|
||||
@ -61,27 +97,44 @@ const clearAllStations = () => {
|
||||
|
||||
<template>
|
||||
<div class="h-screen w-screen p-4 flex flex-col">
|
||||
<div class="flex-shrink-0 mb-4 space-y-2">
|
||||
<div class="space-x-2">
|
||||
<a-button type="primary" @click="addRandomStation">添加随机站点</a-button>
|
||||
<a-button @click="removeLastStation" :disabled="groundStationState.groundStations.length === 0"
|
||||
>移除最后一个</a-button
|
||||
>
|
||||
<a-button danger @click="clearAllStations" :disabled="groundStationState.groundStations.length === 0"
|
||||
>清空所有</a-button
|
||||
>
|
||||
<span>当前站点数: {{ groundStationState.groundStations.length }}</span>
|
||||
<span> | 选中站点数: {{ groundStationState.selectedIds.length }}</span>
|
||||
</div>
|
||||
<!-- 站点选择 -->
|
||||
<div v-if="groundStationState.groundStations.length > 0">
|
||||
<span class="mr-2">选择要高亮的站点:</span>
|
||||
<a-checkbox-group v-model:value="groundStationState.selectedIds" :options="stationCheckboxOptions" />
|
||||
</div>
|
||||
<div class="flex-shrink-0 mb-4 space-y-4">
|
||||
<!-- 地面站控制 -->
|
||||
<a-card title="地面站控制" size="small">
|
||||
<div class="space-x-2">
|
||||
<a-button size="small" type="primary" @click="addRandomStation">添加随机站点</a-button>
|
||||
<a-button size="small" @click="removeLastStation" :disabled="groundStationState.groundStations.length === 0"
|
||||
>移除最后一个</a-button
|
||||
>
|
||||
<a-button
|
||||
size="small"
|
||||
danger
|
||||
@click="clearAllStations"
|
||||
:disabled="groundStationState.groundStations.length === 0"
|
||||
>清空所有</a-button
|
||||
>
|
||||
<span>当前站点数: {{ groundStationState.groundStations.length }}</span>
|
||||
<span> | 选中站点数: {{ groundStationState.selectedIds.length }}</span>
|
||||
</div>
|
||||
<!-- 站点选择 -->
|
||||
<div mt-2 v-if="groundStationState.groundStations.length > 0">
|
||||
<span class="mr-2">选择要显示的站点:</span>
|
||||
<a-checkbox-group v-model:value="groundStationState.selectedIds" :options="stationCheckboxOptions" />
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 卫星控制 -->
|
||||
<a-card title="卫星控制" size="small">
|
||||
<span>当前卫星数: {{ satelliteState.satellites.length }}</span>
|
||||
<span> | 选中卫星数: {{ satelliteState.selectedIds.length }}</span>
|
||||
<div v-if="satelliteState.satellites.length > 0" class="mt-2">
|
||||
<span class="mr-2">选择要显示的卫星:</span>
|
||||
<a-checkbox-group v-model:value="satelliteState.selectedIds" :options="satelliteCheckboxOptions" />
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
<div class="flex-grow w-full rounded-lg border overflow-hidden">
|
||||
<!-- 将响应式列表和选中 ID 列表绑定到 prop -->
|
||||
<h-cesium-viewer :ground-station-state>
|
||||
<!-- 将地面站和卫星状态都传递给组件 -->
|
||||
<h-cesium-viewer :ground-station-state :satellite-state>
|
||||
<div class="absolute top-0 left-0 z-10 p-2 bg-black/30 rounded-br-lg">
|
||||
<span class="text-white text-xs">叠加 UI 示例</span>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user