feat: 更新地面站和卫星状态管理为使用 Set
Some checks failed
/ build-and-deploy-to-vercel (push) Successful in 3m13s
/ lint-build-and-check (push) Successful in 4m38s
/ playwright (push) Failing after 12m17s
/ surge (push) Successful in 2m47s

This commit is contained in:
严浩
2025-04-02 19:09:00 +08:00
parent 02ce0fa9a0
commit 7261a45cab
12 changed files with 180 additions and 68 deletions

View File

@ -1,15 +1,27 @@
import * as Cesium from 'cesium';
import { eciToEcf, gstime, propagate, type SatRec, twoline2satrec } from 'satellite.js';
import {
eciToEcf,
gstime,
propagate,
type SatRec,
twoline2satrec,
} from 'satellite.js';
import type { GroundStationOptions, SatelliteOptions } from './h-cesium-viewer-class.types'; // 2小时
import type {
GroundStationOptions,
SatelliteOptions,
} from './h-cesium-viewer-class.types';
import { VIEWER_OPTIONS_FN } from './helper/_VIEWER_OPTIONS';
import { configureCesium } from './helper/configureCesium';
import { configureTimeLine } from './helper/configureTimeLine';
const = 2 * 60 * 60;
const = 2 * 60 * 60; // 2小时
export { type GroundStationOptions, type SatelliteOptions } from './h-cesium-viewer-class.types';
export {
type GroundStationOptions,
type SatelliteOptions,
} from './h-cesium-viewer-class.types';
Cesium.Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_ION_TOKEN; // 用了离线地图的情况是不需要的。
@ -67,12 +79,19 @@ export class HCesiumViewerCls {
}
// 解构赋值获取站点信息
const { height = 0, id, latitude, longitude, name, pixelSize = 10 } = options;
const {
height = 0,
id,
latitude,
longitude,
name,
pixelSize = 10,
} = options;
const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);
const groundStationEntity = new Cesium.Entity({
// 使用传入的 id 作为实体的唯一标识符
id: id,
id,
label: {
font: '14pt sans-serif',
outlineWidth: 2,
@ -81,14 +100,14 @@ export class HCesiumViewerCls {
text: name,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
},
name: name,
name,
point: {
color: Cesium.Color.fromRandom(), // 随机颜色
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2,
pixelSize,
},
position: position,
position,
});
const addedEntity = this.viewer.entities.add(groundStationEntity);
@ -161,9 +180,11 @@ export class HCesiumViewerCls {
} = options;
// --- 从 tle 字符串解析 name, tle1, tle2 ---
const tleLines = tle.trim().split('\n');
const tleLines = tle.trim().split('\n') as [string, string, string];
if (tleLines.length < 3) {
console.error(`无效的 TLE 格式 (ID: ${id}): TLE 字符串 "${tle}" 至少需要三行`);
console.error(
`无效的 TLE 格式 (ID: ${id}): TLE 字符串 "${tle}" 至少需要三行`,
);
return null;
}
const name = tleLines[0].trim();
@ -185,8 +206,8 @@ export class HCesiumViewerCls {
// 创建卫星实体
const satelliteEntity = this.viewer.entities.add({
id: id,
name: name,
id,
name,
label: {
text: name,
font: '14pt sans-serif',
@ -223,7 +244,11 @@ export class HCesiumViewerCls {
const orbitPositions: Cesium.Cartesian3[] = []; // 用于存储完整轨道点
for (let i = 0; i <= orbitDurationSeconds; i += timeStepSeconds) {
const time = Cesium.JulianDate.addSeconds(startTime, i, new Cesium.JulianDate());
const time = Cesium.JulianDate.addSeconds(
startTime,
i,
new Cesium.JulianDate(),
);
const jsDate = Cesium.JulianDate.toDate(time);
try {
@ -238,7 +263,11 @@ export class HCesiumViewerCls {
const positionEcf = eciToEcf(positionAndVelocity.position, gmst);
// 转换为 Cesium 坐标(单位:米)
const cesiumPosition = new Cesium.Cartesian3(positionEcf.x * 1000, positionEcf.y * 1000, positionEcf.z * 1000);
const cesiumPosition = new Cesium.Cartesian3(
positionEcf.x * 1000,
positionEcf.y * 1000,
positionEcf.z * 1000,
);
// 添加位置样本
positionProperty.addSample(time, cesiumPosition);
@ -254,7 +283,9 @@ export class HCesiumViewerCls {
// 设置卫星的位置和方向
satelliteEntity.position = positionProperty;
satelliteEntity.orientation = new Cesium.VelocityOrientationProperty(positionProperty);
satelliteEntity.orientation = new Cesium.VelocityOrientationProperty(
positionProperty,
);
// --- 添加卫星地面覆盖范围 ---
// 使用 CallbackProperty 动态计算星下点位置
@ -266,7 +297,8 @@ export class HCesiumViewerCls {
return; // 如果位置无效,则不返回任何内容
}
// 转换为地理坐标(包含高度)
const satelliteCartographic = Cesium.Cartographic.fromCartesian(satelliteCartesian);
const satelliteCartographic =
Cesium.Cartographic.fromCartesian(satelliteCartesian);
if (!satelliteCartographic) {
return; // 如果转换失败,则不返回任何内容
}
@ -277,7 +309,11 @@ export class HCesiumViewerCls {
0,
);
// 转换回笛卡尔坐标
return Cesium.Cartographic.toCartesian(subsatelliteCartographic, Cesium.Ellipsoid.WGS84, result);
return Cesium.Cartographic.toCartesian(
subsatelliteCartographic,
Cesium.Ellipsoid.WGS84,
result,
);
},
false,
Cesium.ReferenceFrame.FIXED,
@ -289,7 +325,8 @@ export class HCesiumViewerCls {
if (!satelliteCartesian) {
return 100_000; // 默认半径 100km
}
const satelliteCartographic = Cesium.Cartographic.fromCartesian(satelliteCartesian);
const satelliteCartographic =
Cesium.Cartographic.fromCartesian(satelliteCartesian);
if (!satelliteCartographic) {
return 100_000;
}
@ -336,7 +373,11 @@ export class HCesiumViewerCls {
}
// 存储实体引用
this.currentSatelliteEntities.set(id, { entity: satelliteEntity, orbitEntity, coverageEntity });
this.currentSatelliteEntities.set(id, {
entity: satelliteEntity,
orbitEntity,
coverageEntity,
});
return satelliteEntity;
}
@ -361,7 +402,9 @@ export class HCesiumViewerCls {
if (satelliteData.entity) {
removedMain = this.viewer.entities.remove(satelliteData.entity);
if (!removedMain) {
console.warn(`尝试从 Cesium 移除卫星主体 ID 为 "${entityId}" 的实体失败。`);
console.warn(
`尝试从 Cesium 移除卫星主体 ID 为 "${entityId}" 的实体失败。`,
);
}
}
@ -369,15 +412,21 @@ export class HCesiumViewerCls {
if (satelliteData.orbitEntity) {
removedOrbit = this.viewer.entities.remove(satelliteData.orbitEntity);
if (!removedOrbit) {
console.warn(`尝试从 Cesium 移除卫星轨道 ID 为 "${satelliteData.orbitEntity.id}" 的实体失败。`);
console.warn(
`尝试从 Cesium 移除卫星轨道 ID 为 "${satelliteData.orbitEntity.id}" 的实体失败。`,
);
}
}
// 移除覆盖范围
if (satelliteData.coverageEntity) {
removedCoverage = this.viewer.entities.remove(satelliteData.coverageEntity);
removedCoverage = this.viewer.entities.remove(
satelliteData.coverageEntity,
);
if (!removedCoverage) {
console.warn(`尝试从 Cesium 移除卫星覆盖范围 ID 为 "${satelliteData.coverageEntity.id}" 的实体失败。`);
console.warn(
`尝试从 Cesium 移除卫星覆盖范围 ID 为 "${satelliteData.coverageEntity.id}" 的实体失败。`,
);
}
}

View File

@ -1,19 +1,19 @@
<script setup lang="ts">
import 'cesium/Build/Cesium/Widgets/widgets.css';
import type { GroundStationState, SatelliteState } from './types';
import { useHCesiumViewerCls } from './useHCesiumViewerCls';
import { useHCesiumViewerClsGroundStation } from './useHCesiumViewerClsGroundStation';
import { useHCesiumViewerClsSatellite } from './useHCesiumViewerClsSatellite';
import 'cesium/Build/Cesium/Widgets/widgets.css';
const props = defineProps<{
groundStationState?: GroundStationState;
satelliteState?: SatelliteState;
}>();
// 1. 管理 Cesium Viewer 实例生命周期
const { hCesiumViewerInst } = useHCesiumViewerCls('cesiumContainer');
const { hCesiumViewerInst } = useHCesiumViewerCls('cesium-container');
// 2. 同步地面站实体
// 将实例的 getter 和 props 的 getter 传递给组合函数
@ -32,14 +32,14 @@ useHCesiumViewerClsSatellite(
</script>
<template>
<div id="cesiumContainer" class="h-full w-full relative inset-0 isolate">
<slot />
<div id="cesium-container" class="relative inset-0 isolate h-full w-full">
<slot></slot>
</div>
</template>
<style scoped>
#cesiumContainer {
min-height: 300px;
#cesium-container {
min-height: 100px;
background-color: #000;
}
</style>

View File

@ -16,5 +16,5 @@ export const TOOLTIP_MAP = {
ARG_OF_PERICENTER_近地点幅角: "8位数字如'181.9338'表示近地点相对升交点的角度范围0°-360°",
MEAN_ANOMALY_平近点角: "8位数字如'171.6150'表示历元时的平近点角范围0°-360°",
MEAN_MOTION_平均运动: "11位数字如'2.21786616',表示卫星每天绕地球的圈数(圈/天)",
REV_AT_EPOCH_历元时的圈数: "5位数字如'13',表示卫星在历元时完成的轨道圈数最后以为是校验和共6位",
REV_AT_EPOCH_历元时的圈数: "5位数字如'13',表示卫星在历元时完成的轨道圈数",
};

View File

@ -6,7 +6,9 @@ export const VIEWER_OPTIONS_FN = (): Viewer.ConstructorOptions => {
return {
animation: true, // .cesium-viewer-animationContainer https://cesium.com/learn/ion-sdk/ref-doc/Animation.html
baseLayer: Cesium.ImageryLayer.fromProviderAsync(
Cesium.TileMapServiceImageryProvider.fromUrl(Cesium.buildModuleUrl('Assets/Textures/NaturalEarthII')),
Cesium.TileMapServiceImageryProvider.fromUrl(
Cesium.buildModuleUrl('Assets/Textures/NaturalEarthII'),
),
),
baseLayerPicker: false,
fullscreenButton: !true, // 全屏按钮

View File

@ -20,18 +20,30 @@ export function configureCesium() {
// Animation 的时间日期格式化
Cesium.AnimationViewModel.defaultDateFormatter = function (date) {
const dataZone8 = Cesium.JulianDate.addMinutes(date, minutes, new Cesium.JulianDate());
const dataZone8 = Cesium.JulianDate.addMinutes(
date,
minutes,
new Cesium.JulianDate(),
);
return Cesium.JulianDate.toIso8601(dataZone8).slice(0, 10);
};
Cesium.AnimationViewModel.defaultTimeFormatter = function (time) {
const dataZone8 = Cesium.JulianDate.addMinutes(time, minutes, new Cesium.JulianDate());
const dataZone8 = Cesium.JulianDate.addMinutes(
time,
minutes,
new Cesium.JulianDate(),
);
return Cesium.JulianDate.toIso8601(dataZone8).slice(11, 19);
};
// Timeline 的时间日期格式化
// @ts-expect-error node_modules/@cesium/widgets/Source/Timeline/Timeline.js
Cesium.Timeline.prototype.makeLabel = function (time) {
const dataZone8 = Cesium.JulianDate.addMinutes(time, minutes, new Cesium.JulianDate());
const dataZone8 = Cesium.JulianDate.addMinutes(
time,
minutes,
new Cesium.JulianDate(),
);
return Cesium.JulianDate.toIso8601(dataZone8).slice(0, 19);
};
}

View File

@ -19,8 +19,11 @@ export function configureMapTile(viewer: Cesium.Viewer) {
tooltip: '高德地图',
});
// 设置高德地图为默认图层
viewer.baseLayerPicker.viewModel.imageryProviderViewModels.unshift(customLayerViewModel);
const selectedViewModel = viewer.baseLayerPicker.viewModel.imageryProviderViewModels[0];
viewer.baseLayerPicker.viewModel.imageryProviderViewModels.unshift(
customLayerViewModel,
);
const selectedViewModel =
viewer.baseLayerPicker.viewModel.imageryProviderViewModels[0];
if (!selectedViewModel) {
console.error('未找到默认底图');
return;

View File

@ -2,9 +2,16 @@ import * as Cesium from 'cesium';
const = 2 * 60 * 60; // 2小时
export function configureTimeLine(viewer: Cesium.Viewer, totalSeconds = ) {
export function configureTimeLine(
viewer: Cesium.Viewer,
totalSeconds = ,
) {
const start = Cesium.JulianDate.fromIso8601(new Date().toISOString());
const stop = Cesium.JulianDate.addSeconds(start, totalSeconds, new Cesium.JulianDate());
const stop = Cesium.JulianDate.addSeconds(
start,
totalSeconds,
new Cesium.JulianDate(),
);
// 设置时钟范围
viewer.clock.startTime = start.clone();

View File

@ -1,4 +1,7 @@
import type { GroundStationOptions, SatelliteOptions } from './h-cesium-viewer-class';
import type {
GroundStationOptions,
SatelliteOptions,
} from './h-cesium-viewer-class';
/**
* 地面站状态接口
@ -7,8 +10,8 @@ import type { GroundStationOptions, SatelliteOptions } from './h-cesium-viewer-c
export interface GroundStationState {
/** 地面站配置数组 */
groundStations: GroundStationOptions[];
/** 选中的地面站 ID 数组 */
selectedIds: string[];
/** 选中的地面站 ID 集合 */
selectedIds: Set<string>;
}
/**
@ -18,6 +21,6 @@ export interface GroundStationState {
export interface SatelliteState {
/** 卫星配置数组 */
satellites: SatelliteOptions[];
/** 选中的卫星 ID 数组 */
selectedIds: string[];
/** 选中的卫星 ID 集合 */
selectedIds: Set<string>;
}

View File

@ -1,5 +1,6 @@
// src/utils/useHCesiumViewerCls.ts
import { HCesiumViewerCls } from '@/components/h-cesium-viewer/h-cesium-viewer-class';
import { HCesiumViewerCls } from './h-cesium-viewer-class';
/**
* 管理 HCesiumViewerCls 实例的生命周期。

View File

@ -1,17 +1,22 @@
// src/utils/useHCesiumViewerClsGroundStation.ts
import type { GroundStationOptions, HCesiumViewerCls } from '@/components/h-cesium-viewer/h-cesium-viewer-class';
import type { MaybeRefOrGetter } from 'vue';
import type {
GroundStationOptions,
HCesiumViewerCls,
} from './h-cesium-viewer-class';
/**
* 管理 Cesium Viewer 中的地面站实体,根据选中的 ID 列表进行同步。
* @param hCesiumViewerInst - HCesiumViewerCls 实例或其 getter。
* @param groundStationList - 包含所有可用地面站选项的数组或 getter。
* @param selectedStationIds - 包含当前选中地面站 ID 的数组或 getter。
* @param selectedStationIds - 包含当前选中地面站 ID 的 Set 或 getter。
*/
export function useHCesiumViewerClsGroundStation(
hCesiumViewerInst: MaybeRefOrGetter<HCesiumViewerCls | null>,
groundStationList: MaybeRefOrGetter<Array<GroundStationOptions> | undefined>,
selectedStationIds: MaybeRefOrGetter<string[] | undefined>,
selectedStationIds: MaybeRefOrGetter<Set<string> | undefined>,
) {
// 创建一个从 ID 到站点选项的映射,方便查找
const stationMap = computed(() => {
@ -31,9 +36,10 @@ export function useHCesiumViewerClsGroundStation(
return;
}
const selectedIds = toValue(selectedStationIds) ?? [];
const selectedIdsSet = new Set(selectedIds); // 需要显示的站点 ID
const currentEntityIds = new Set(viewerInstance.currentStationEntities.keys()); // 当前已显示的站点 ID
const selectedIdsSet = toValue(selectedStationIds) ?? new Set<string>(); // 直接获取 Set如果为 undefined 则创建空 Set
const currentEntityIds = new Set(
viewerInstance.currentStationEntities.keys(),
); // 当前已显示的站点 ID
// 1. 移除不再选中的站点
for (const entityId of currentEntityIds) {
@ -52,7 +58,9 @@ export function useHCesiumViewerClsGroundStation(
viewerInstance.addGroundStation(stationToAdd);
} else {
// 如果在 groundStationList 中找不到对应的站点信息,发出警告
consola.warn(`无法找到 ID 为 "${selectedId}" 的站点信息,无法添加到地图。`);
console.warn(
`无法找到 ID 为 "${selectedId}" 的站点信息,无法添加到地图。`,
);
}
}
}

View File

@ -1,17 +1,22 @@
// 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';
import type {
HCesiumViewerCls,
SatelliteOptions,
} from './h-cesium-viewer-class';
/**
* 管理 Cesium Viewer 中的卫星实体,根据选中的 ID 列表进行同步。
* @param hCesiumViewerInst - HCesiumViewerCls 实例或其 getter。
* @param satelliteList - 包含所有可用卫星选项的数组或 getter。
* @param selectedSatelliteIds - 包含当前选中卫星 ID 的数组或 getter。
* @param selectedSatelliteIds - 包含当前选中卫星 ID 的 Set 或 getter。
*/
export function useHCesiumViewerClsSatellite(
hCesiumViewerInst: MaybeRefOrGetter<HCesiumViewerCls | null>,
satelliteList: MaybeRefOrGetter<Array<SatelliteOptions> | undefined>,
selectedSatelliteIds: MaybeRefOrGetter<string[] | undefined>,
selectedSatelliteIds: MaybeRefOrGetter<Set<string> | undefined>,
) {
// 创建一个从 ID 到卫星选项的映射,方便查找
const satelliteMap = computed(() => {
@ -31,9 +36,10 @@ export function useHCesiumViewerClsSatellite(
return;
}
const selectedIds = toValue(selectedSatelliteIds) ?? [];
const selectedIdsSet = new Set(selectedIds); // 需要显示的卫星 ID
const currentEntityIds = new Set(viewerInstance.currentSatelliteEntities.keys()); // 当前已显示的卫星 ID
const selectedIdsSet = toValue(selectedSatelliteIds) ?? new Set<string>(); // 直接获取 Set如果为 undefined 则创建空 Set
const currentEntityIds = new Set(
viewerInstance.currentSatelliteEntities.keys(),
); // 当前已显示的卫星 ID
// 1. 移除不再选中的卫星
for (const entityId of currentEntityIds) {
@ -49,10 +55,15 @@ export function useHCesiumViewerClsSatellite(
// 如果选中的 ID 对应的实体当前未显示,则添加
const satelliteToAdd = satelliteMap.value.get(selectedId); // 从映射中查找卫星信息
if (satelliteToAdd) {
console.debug(
`添加卫星 "${satelliteToAdd.id}" 到地图TLE: ${satelliteToAdd.tle}`,
);
viewerInstance.addSatellite(satelliteToAdd);
} else {
// 如果在 satelliteList 中找不到对应的卫星信息,发出警告
consola.warn(`无法找到 ID 为 "${selectedId}" 的卫星信息,无法添加到地图。`);
console.warn(
`无法找到 ID 为 "${selectedId}" 的卫星信息,无法添加到地图。`,
);
}
}
}

View File

@ -9,7 +9,7 @@ import type { GroundStationState, SatelliteState } from '@/components/h-cesium-v
// 地面站和选中状态
const groundStationState = reactive<GroundStationState>({
groundStations: [],
selectedIds: [],
selectedIds: new Set<string>(),
});
groundStationState.groundStations = [
{ height: 50, id: 'gs-bj', latitude: 39.9042, longitude: 116.4074, name: '北京站' },
@ -20,7 +20,7 @@ groundStationState.groundStations = [
// >>>>>>>>> 卫星状态管理 >>>>>>>>>
const satelliteState = reactive<SatelliteState>({
satellites: [],
selectedIds: [],
selectedIds: new Set<string>(),
});
// 初始化卫星列表
@ -51,6 +51,14 @@ const satelliteCheckboxOptions = computed(() =>
})),
);
// 为卫星 a-checkbox-group 创建计算属性,处理 Set 和 Array 的转换
const selectedSatelliteIdsArray = computed({
get: () => [...satelliteState.selectedIds], // 从 Set 转换为 Array
set: (val: string[]) => {
satelliteState.selectedIds = new Set(val); // 从 Array 转换回 Set
},
});
// <<<<<<<<< 卫星状态管理 <<<<<<<<<
// 计算 Checkbox Group 的 options
@ -71,7 +79,7 @@ const addRandomStation = () => {
longitude: randomLon,
name: `随机站 ${randomId.slice(-4)}`,
});
groundStationState.selectedIds.push(randomId); // 同时将新站点 ID 添加到选中项
groundStationState.selectedIds.add(randomId); // 同时将新站点 ID 添加到选中项
consola.info('添加随机站点:', groundStationState.groundStations.at(-1)); // 使用 .at() 访问最后一个元素
};
@ -82,7 +90,7 @@ const removeLastStation = () => {
if (removedStation) {
consola.info('移除站点:', removedStation);
// 同时从选中项中移除
groundStationState.selectedIds = groundStationState.selectedIds.filter((id) => id !== removedStation.id);
groundStationState.selectedIds.delete(removedStation.id);
}
}
};
@ -90,9 +98,17 @@ const removeLastStation = () => {
// 清空所有选中项
const clearAllStations = () => {
groundStationState.groundStations = [];
groundStationState.selectedIds = []; // 清空选中项
groundStationState.selectedIds.clear(); // 清空选中项
consola.info('清空所有站点');
};
// 为 a-checkbox-group 创建计算属性,处理 Set 和 Array 的转换
const selectedStationIdsArray = computed({
get: () => [...groundStationState.selectedIds], // 从 Set 转换为 Array
set: (val: string[]) => {
groundStationState.selectedIds = new Set(val); // 从 Array 转换回 Set
},
});
</script>
<template>
@ -113,22 +129,22 @@ const clearAllStations = () => {
>清空所有</a-button
>
<span>当前站点数: {{ groundStationState.groundStations.length }}</span>
<span> | 选中站点数: {{ groundStationState.selectedIds.length }}</span>
<span> | 选中站点数: {{ groundStationState.selectedIds.size }}</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" />
<a-checkbox-group v-model:value="selectedStationIdsArray" :options="stationCheckboxOptions" />
</div>
</a-card>
<!-- 卫星控制 -->
<a-card title="卫星控制" size="small">
<span>当前卫星数: {{ satelliteState.satellites.length }}</span>
<span> | 选中卫星数: {{ satelliteState.selectedIds.length }}</span>
<span> | 选中卫星数: {{ satelliteState.selectedIds.size }}</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" />
<a-checkbox-group v-model:value="selectedSatelliteIdsArray" :options="satelliteCheckboxOptions" />
</div>
</a-card>
</div>