Files
vue-ts-example/src/components/h-cesium-viewer/h-cesium-viewer-class.ts
严浩 02ce0fa9a0
Some checks failed
/ build-and-deploy-to-vercel (push) Successful in 2m45s
/ lint-build-and-check (push) Successful in 4m32s
/ playwright (push) Failing after 12m4s
/ surge (push) Successful in 2m34s
h-cesium-viewer
2025-04-02 16:39:15 +08:00

424 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(); // 确保清空
}
}