424 lines
15 KiB
TypeScript
424 lines
15 KiB
TypeScript
import * as Cesium from 'cesium';
|
||
import { eciToEcf, gstime, propagate, type SatRec, twoline2satrec } from 'satellite.js';
|
||
|
||
import type { GroundStationOptions, SatelliteOptions } from './h-cesium-viewer-class.types'; // 2小时
|
||
|
||
import { VIEWER_OPTIONS_FN } from './helper/_VIEWER_OPTIONS';
|
||
import { configureCesium } from './helper/configureCesium';
|
||
import { configureTimeLine } from './helper/configureTimeLine';
|
||
|
||
const 默认轨道时长秒 = 2 * 60 * 60;
|
||
|
||
export { type GroundStationOptions, type SatelliteOptions } from './h-cesium-viewer-class.types';
|
||
|
||
Cesium.Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_ION_TOKEN; // 用了离线地图的情况是不需要的。
|
||
|
||
Object.assign(globalThis, { Cesium });
|
||
|
||
configureCesium();
|
||
|
||
export class HCesiumViewerCls {
|
||
viewer: Cesium.Viewer | null = null;
|
||
// 用于存储当前地面站实体的 Map
|
||
currentStationEntities: Map<string, Cesium.Entity> = new Map();
|
||
// 用于存储当前卫星实体的 Map (包括轨道实体)
|
||
currentSatelliteEntities: Map<
|
||
string,
|
||
{
|
||
coverageEntity?: Cesium.Entity;
|
||
entity: Cesium.Entity;
|
||
orbitEntity?: Cesium.Entity;
|
||
}
|
||
> = new Map();
|
||
|
||
/**
|
||
* 初始化 Cesium Viewer
|
||
* @param container - 用于承载 Cesium Viewer 的 DOM 元素或其 ID
|
||
*/
|
||
initCesiumViewer(container: ConstructorParameters<typeof Cesium.Viewer>[0]) {
|
||
this.viewer = new Cesium.Viewer(container, VIEWER_OPTIONS_FN());
|
||
|
||
configureTimeLine(this.viewer);
|
||
|
||
this.viewer.scene.debugShowFramesPerSecond = true;
|
||
|
||
// 初始化时清空可能存在的旧实体引用
|
||
this.currentStationEntities.clear();
|
||
this.currentSatelliteEntities.clear();
|
||
}
|
||
|
||
// region 地面站点相关方法
|
||
|
||
/**
|
||
* 向视图中添加地面站实体
|
||
* @param options - 地面站的选项参数
|
||
* @returns 添加的实体对象,如果添加失败则返回 null。
|
||
*/
|
||
addGroundStation(options: GroundStationOptions): Cesium.Entity | null {
|
||
if (!this.viewer) {
|
||
console.error('视图未初始化。无法添加地面站。');
|
||
return null;
|
||
}
|
||
|
||
// 检查是否已存在相同 ID 的实体
|
||
if (this.currentStationEntities.has(options.id)) {
|
||
console.warn(`ID 为 "${options.id}" 的地面站实体已存在,跳过添加。`);
|
||
return this.currentStationEntities.get(options.id) || null;
|
||
}
|
||
|
||
// 解构赋值获取站点信息
|
||
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,
|
||
label: {
|
||
font: '14pt sans-serif',
|
||
outlineWidth: 2,
|
||
pixelOffset: new Cesium.Cartesian2(0, -12), // 标签略微偏移到点的上方
|
||
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
||
text: name,
|
||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
|
||
},
|
||
name: name,
|
||
point: {
|
||
color: Cesium.Color.fromRandom(), // 随机颜色
|
||
outlineColor: Cesium.Color.WHITE,
|
||
outlineWidth: 2,
|
||
pixelSize,
|
||
},
|
||
position: position,
|
||
});
|
||
|
||
const addedEntity = this.viewer.entities.add(groundStationEntity);
|
||
// 添加成功后,将其存入 Map
|
||
this.currentStationEntities.set(id, addedEntity);
|
||
return addedEntity;
|
||
}
|
||
|
||
/**
|
||
* 从视图中移除指定的地面站实体 (通过 ID)
|
||
* @param entityId - 要移除的地面站实体的 ID
|
||
* @returns 如果成功移除则返回 true,否则返回 false
|
||
*/
|
||
removeGroundStationById(entityId: string): boolean {
|
||
if (!this.viewer) {
|
||
console.error('视图未初始化。无法移除地面站。');
|
||
return false;
|
||
}
|
||
const entityToRemove = this.currentStationEntities.get(entityId);
|
||
if (entityToRemove) {
|
||
const removed = this.viewer.entities.remove(entityToRemove);
|
||
if (removed) {
|
||
// 移除成功后,从 Map 中删除
|
||
this.currentStationEntities.delete(entityId);
|
||
return true;
|
||
} else {
|
||
console.warn(`尝试从 Cesium 移除 ID 为 "${entityId}" 的实体失败。`);
|
||
return false; // Cesium 移除失败
|
||
}
|
||
} else {
|
||
// console.warn(`未在 Map 中找到 ID 为 "${entityId}" 的地面站实体,无法移除。`); // 可能在 clearAll 时触发,不一定是警告
|
||
return false; // Map 中未找到
|
||
}
|
||
}
|
||
|
||
clearAllGroundStations() {
|
||
if (!this.viewer) return;
|
||
for (const entity of this.currentStationEntities.values()) {
|
||
this.viewer?.entities.remove(entity);
|
||
}
|
||
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,
|
||
orbitDurationSeconds = 默认轨道时长秒,
|
||
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 randomBaseColor = Cesium.Color.fromRandom();
|
||
|
||
// 创建卫星实体
|
||
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: randomBaseColor, // 使用随机基色
|
||
outlineColor: Cesium.Color.WHITE,
|
||
outlineWidth: 1,
|
||
},
|
||
// 动态轨迹路径
|
||
path: {
|
||
resolution: 1,
|
||
material: new Cesium.PolylineGlowMaterialProperty({
|
||
glowPower: 0.15,
|
||
color: randomBaseColor, // 使用随机基色
|
||
}),
|
||
width: 2,
|
||
leadTime: orbitDurationSeconds / 2, // 显示未来一半时间的轨迹
|
||
trailTime: orbitDurationSeconds / 2, // 显示过去一半时间的轨迹
|
||
},
|
||
});
|
||
|
||
// --- 计算轨道 ---
|
||
const startTime = this.viewer.clock.currentTime; // 使用当前 viewer 的时间作为起点
|
||
const positionProperty = new Cesium.SampledPositionProperty();
|
||
const orbitPositions: Cesium.Cartesian3[] = []; // 用于存储完整轨道点
|
||
|
||
for (let i = 0; i <= orbitDurationSeconds; 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);
|
||
|
||
// --- 添加卫星地面覆盖范围 ---
|
||
// 使用 CallbackProperty 动态计算星下点位置
|
||
const subsatellitePosition = new Cesium.CallbackPositionProperty(
|
||
(time, result) => {
|
||
// 从 satelliteEntity 获取当前时间的精确位置
|
||
const satelliteCartesian = positionProperty.getValue(time, result);
|
||
if (!satelliteCartesian) {
|
||
return; // 如果位置无效,则不返回任何内容
|
||
}
|
||
// 转换为地理坐标(包含高度)
|
||
const satelliteCartographic = Cesium.Cartographic.fromCartesian(satelliteCartesian);
|
||
if (!satelliteCartographic) {
|
||
return; // 如果转换失败,则不返回任何内容
|
||
}
|
||
// 创建星下点地理坐标(高度设为0)
|
||
const subsatelliteCartographic = new Cesium.Cartographic(
|
||
satelliteCartographic.longitude,
|
||
satelliteCartographic.latitude,
|
||
0,
|
||
);
|
||
// 转换回笛卡尔坐标
|
||
return Cesium.Cartographic.toCartesian(subsatelliteCartographic, Cesium.Ellipsoid.WGS84, result);
|
||
},
|
||
false,
|
||
Cesium.ReferenceFrame.FIXED,
|
||
); // isConstant: false, referenceFrame: FIXED
|
||
|
||
// 使用 CallbackProperty 动态计算覆盖半径 (基于高度的简单估算)
|
||
const coverageRadius = new Cesium.CallbackProperty((time) => {
|
||
const satelliteCartesian = positionProperty.getValue(time);
|
||
if (!satelliteCartesian) {
|
||
return 100_000; // 默认半径 100km
|
||
}
|
||
const satelliteCartographic = Cesium.Cartographic.fromCartesian(satelliteCartesian);
|
||
if (!satelliteCartographic) {
|
||
return 100_000;
|
||
}
|
||
const altitude = satelliteCartographic.height;
|
||
// 简化的估算:半径约为高度的 0.8 倍,最小 50km
|
||
// 实际应用中应基于卫星的视场角 (FOV) 或波束宽度计算
|
||
const calculatedRadius = altitude * 0.8;
|
||
return Math.max(calculatedRadius, 50_000); // 最小半径 50km
|
||
}, false); // isConstant 设置为 false
|
||
|
||
const coverageEntity = this.viewer.entities.add({
|
||
id: `${id}-coverage`,
|
||
name: `${name} 覆盖范围`,
|
||
position: subsatellitePosition,
|
||
ellipse: {
|
||
semiMajorAxis: coverageRadius,
|
||
semiMinorAxis: coverageRadius, // 假设圆形覆盖
|
||
material: randomBaseColor.withAlpha(0.2 + 0.3), // 基于随机基色的半透明填充
|
||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, // 贴合地形
|
||
outline: true,
|
||
outlineColor: randomBaseColor.withAlpha(0.8), // 基于随机基色的较深半透明轮廓
|
||
outlineWidth: 2, //
|
||
granularity: Cesium.Math.toRadians(1), // 控制椭圆边缘平滑度
|
||
},
|
||
});
|
||
// --- 覆盖范围结束 ---
|
||
|
||
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: randomBaseColor.withAlpha(0.5), // 基于随机基色的半透明虚线
|
||
dashLength: 16,
|
||
}),
|
||
clampToGround: false, // 轨道不贴地
|
||
},
|
||
});
|
||
}
|
||
|
||
// 存储实体引用
|
||
this.currentSatelliteEntities.set(id, { entity: satelliteEntity, orbitEntity, coverageEntity });
|
||
|
||
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;
|
||
let removedCoverage = 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}" 的实体失败。`);
|
||
}
|
||
}
|
||
|
||
// 移除覆盖范围
|
||
if (satelliteData.coverageEntity) {
|
||
removedCoverage = this.viewer.entities.remove(satelliteData.coverageEntity);
|
||
if (!removedCoverage) {
|
||
console.warn(`尝试从 Cesium 移除卫星覆盖范围 ID 为 "${satelliteData.coverageEntity.id}" 的实体失败。`);
|
||
}
|
||
}
|
||
|
||
// 如果主体和轨道(如果存在)都移除成功或不存在,则从 Map 中删除
|
||
// 如果主体、轨道(如果存在)和覆盖范围(如果存在)都移除成功或不存在,则从 Map 中删除
|
||
if (removedMain && removedOrbit && removedCoverage) {
|
||
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(); // 确保清空
|
||
}
|
||
}
|