1364 lines
38 KiB
JavaScript
1364 lines
38 KiB
JavaScript
// satellites.js - Satellite visualization module with real SGP4 positions and animations
|
||
|
||
import * as THREE from "three";
|
||
import { twoline2satrec, propagate } from "satellite.js";
|
||
import { CONFIG, SATELLITE_CONFIG } from "./constants.js";
|
||
import { latLonToVector3 } from "./utils.js";
|
||
|
||
let satellitePoints = null;
|
||
let satelliteBackdropPoints = null;
|
||
let satelliteTrails = null;
|
||
let satelliteData = [];
|
||
let showSatellites = false;
|
||
let showTrails = true;
|
||
let selectedSatellite = null;
|
||
let satellitePositions = [];
|
||
let hoverRingSprite = null;
|
||
let lockedRingSprite = null;
|
||
let lockedDotSprite = null;
|
||
let predictedOrbitLine = null;
|
||
let relatedSatelliteSprites = [];
|
||
let highlightedSatelliteIndices = null;
|
||
let earthObjRef = null;
|
||
let sceneRef = null;
|
||
let cameraRef = null;
|
||
let lockedSatelliteIndex = null;
|
||
let hoveredSatelliteIndex = null;
|
||
let positionUpdateAccumulator = 0;
|
||
let satelliteCapacity = 0;
|
||
let satelliteSatrecCache = new Map();
|
||
|
||
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
||
const DOT_TEXTURE_SIZE = 32;
|
||
const POSITION_UPDATE_INTERVAL_MS = 250;
|
||
|
||
const scratchWorldSatellitePosition = new THREE.Vector3();
|
||
const scratchToCamera = new THREE.Vector3();
|
||
const scratchToSatellite = new THREE.Vector3();
|
||
|
||
export let breathingPhase = 0;
|
||
|
||
const SATELLITE_LEGEND_RULES = [
|
||
{
|
||
key: "equatorial",
|
||
label: "赤道轨道(0-30°)",
|
||
color: "#ff3333",
|
||
match: (props) => {
|
||
const inclination = Number(props?.inclination ?? 0);
|
||
return inclination >= 0 && inclination < 30;
|
||
},
|
||
},
|
||
{
|
||
key: "low",
|
||
label: "低倾角轨道(30-60°)",
|
||
color: "#ff9933",
|
||
match: (props) => {
|
||
const inclination = Number(props?.inclination ?? 0);
|
||
return inclination >= 30 && inclination < 60;
|
||
},
|
||
},
|
||
{
|
||
key: "medium",
|
||
label: "中倾角轨道(60-90°)",
|
||
color: "#ffff33",
|
||
match: (props) => {
|
||
const inclination = Number(props?.inclination ?? 0);
|
||
return inclination >= 60 && inclination < 90;
|
||
},
|
||
},
|
||
{
|
||
key: "high",
|
||
label: "高倾角轨道(90-120°)",
|
||
color: "#33ff33",
|
||
match: (props) => {
|
||
const inclination = Number(props?.inclination ?? 0);
|
||
return inclination >= 90 && inclination < 120;
|
||
},
|
||
},
|
||
{
|
||
key: "retrograde",
|
||
label: "逆行轨道(120-180°)",
|
||
color: "#3333ff",
|
||
match: (props) => {
|
||
const inclination = Number(props?.inclination ?? 0);
|
||
return inclination >= 120 && inclination <= 180;
|
||
},
|
||
},
|
||
{
|
||
key: "other",
|
||
label: "其他",
|
||
color: "#d7e2f4",
|
||
match: () => true,
|
||
},
|
||
];
|
||
|
||
const SATELLITE_RULE_COLOR_CACHE = new Map();
|
||
|
||
function getSatelliteLegendRule(props = {}) {
|
||
return (
|
||
SATELLITE_LEGEND_RULES.find((rule) => rule.match(props)) ||
|
||
SATELLITE_LEGEND_RULES[SATELLITE_LEGEND_RULES.length - 1]
|
||
);
|
||
}
|
||
|
||
function getSatelliteRuleColor(rule) {
|
||
if (!rule) {
|
||
return { r: 1, g: 1, b: 1 };
|
||
}
|
||
|
||
if (SATELLITE_RULE_COLOR_CACHE.has(rule.key)) {
|
||
return SATELLITE_RULE_COLOR_CACHE.get(rule.key);
|
||
}
|
||
|
||
const color = new THREE.Color(rule.color);
|
||
const rgb = { r: color.r, g: color.g, b: color.b };
|
||
SATELLITE_RULE_COLOR_CACHE.set(rule.key, rgb);
|
||
return rgb;
|
||
}
|
||
|
||
export function updateBreathingPhase(deltaTime = 16) {
|
||
breathingPhase += SATELLITE_CONFIG.breathingSpeed * (deltaTime / 16);
|
||
}
|
||
|
||
function getBreathingPulse(phase) {
|
||
return 0.5 + 0.5 * Math.sin(phase);
|
||
}
|
||
|
||
export function getSatelliteLegendItems() {
|
||
const presentKeys = new Set();
|
||
|
||
satelliteData.forEach((satellite) => {
|
||
const props = satellite?.properties || {};
|
||
const rule = SATELLITE_LEGEND_RULES.find((item) => item.match(props));
|
||
if (rule) {
|
||
presentKeys.add(rule.key);
|
||
}
|
||
});
|
||
|
||
if (presentKeys.size === 0) {
|
||
return SATELLITE_LEGEND_RULES.map(({ label, color }) => ({ label, color }));
|
||
}
|
||
|
||
const items = SATELLITE_LEGEND_RULES
|
||
.filter((item) => presentKeys.has(item.key))
|
||
.map(({ key, label, color }) => ({ key, label, color }));
|
||
|
||
return items.map(({ label, color }) => ({ label, color }));
|
||
}
|
||
|
||
export function setSelectedSatelliteLegend(props) {
|
||
return getSatelliteLegendRule(props || {});
|
||
}
|
||
|
||
export function clearSelectedSatelliteLegend() {
|
||
return null;
|
||
}
|
||
|
||
function disposeMaterial(material) {
|
||
if (!material) return;
|
||
if (Array.isArray(material)) {
|
||
material.forEach(disposeMaterial);
|
||
return;
|
||
}
|
||
if (material.map) {
|
||
material.map.dispose();
|
||
}
|
||
material.dispose();
|
||
}
|
||
|
||
function disposeObject3D(object, parent = earthObjRef) {
|
||
if (!object) return;
|
||
if (parent) {
|
||
parent.remove(object);
|
||
} else if (object.parent) {
|
||
object.parent.remove(object);
|
||
}
|
||
if (object.geometry) {
|
||
object.geometry.dispose();
|
||
}
|
||
if (object.material) {
|
||
disposeMaterial(object.material);
|
||
}
|
||
}
|
||
|
||
function createDotTexture() {
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = DOT_TEXTURE_SIZE;
|
||
canvas.height = DOT_TEXTURE_SIZE;
|
||
const ctx = canvas.getContext("2d");
|
||
const center = DOT_TEXTURE_SIZE / 2;
|
||
const radius = center - 2;
|
||
|
||
const gradient = ctx.createRadialGradient(
|
||
center,
|
||
center,
|
||
0,
|
||
center,
|
||
center,
|
||
radius,
|
||
);
|
||
gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
|
||
gradient.addColorStop(0.5, "rgba(255, 255, 255, 0.8)");
|
||
gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.arc(center, center, radius, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
const texture = new THREE.CanvasTexture(canvas);
|
||
texture.needsUpdate = true;
|
||
return texture;
|
||
}
|
||
|
||
function createBackdropDotTexture() {
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = DOT_TEXTURE_SIZE;
|
||
canvas.height = DOT_TEXTURE_SIZE;
|
||
const ctx = canvas.getContext("2d");
|
||
const center = DOT_TEXTURE_SIZE / 2;
|
||
const radius = center - 1;
|
||
|
||
const gradient = ctx.createRadialGradient(
|
||
center,
|
||
center,
|
||
0,
|
||
center,
|
||
center,
|
||
radius,
|
||
);
|
||
gradient.addColorStop(0, "rgba(7, 14, 27, 0.98)");
|
||
gradient.addColorStop(0.55, "rgba(7, 14, 27, 0.88)");
|
||
gradient.addColorStop(0.85, "rgba(7, 14, 27, 0.34)");
|
||
gradient.addColorStop(1, "rgba(7, 14, 27, 0)");
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.arc(center, center, radius, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
const texture = new THREE.CanvasTexture(canvas);
|
||
texture.needsUpdate = true;
|
||
return texture;
|
||
}
|
||
|
||
function createRingTexture(innerRadius, outerRadius, color = "#ffffff") {
|
||
const size = DOT_TEXTURE_SIZE * 2;
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = size;
|
||
canvas.height = size;
|
||
const ctx = canvas.getContext("2d");
|
||
const center = size / 2;
|
||
|
||
ctx.strokeStyle = color;
|
||
ctx.lineWidth = 3;
|
||
ctx.beginPath();
|
||
ctx.arc(center, center, (innerRadius + outerRadius) / 2, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
|
||
const texture = new THREE.CanvasTexture(canvas);
|
||
texture.needsUpdate = true;
|
||
return texture;
|
||
}
|
||
|
||
export function createSatellites(scene, earthObj) {
|
||
initSatelliteScene(scene, earthObj);
|
||
const dotTexture = createDotTexture();
|
||
const backdropTexture = createBackdropDotTexture();
|
||
|
||
const pointsGeometry = new THREE.BufferGeometry();
|
||
const backdropGeometry = new THREE.BufferGeometry();
|
||
|
||
const backdropMaterial = new THREE.PointsMaterial({
|
||
size: SATELLITE_CONFIG.dotSize * 1.28,
|
||
map: backdropTexture,
|
||
color: 0x0b1626,
|
||
transparent: true,
|
||
opacity: 0.42,
|
||
sizeAttenuation: false,
|
||
alphaTest: 0.04,
|
||
depthWrite: false,
|
||
});
|
||
|
||
const pointsMaterial = new THREE.PointsMaterial({
|
||
size: SATELLITE_CONFIG.dotSize,
|
||
map: dotTexture,
|
||
vertexColors: true,
|
||
transparent: true,
|
||
opacity: 0.9,
|
||
sizeAttenuation: false,
|
||
alphaTest: 0.1,
|
||
depthWrite: false,
|
||
});
|
||
|
||
satelliteBackdropPoints = new THREE.Points(backdropGeometry, backdropMaterial);
|
||
satelliteBackdropPoints.visible = false;
|
||
satelliteBackdropPoints.userData = { type: "satelliteBackdropPoints" };
|
||
satelliteBackdropPoints.renderOrder = 5;
|
||
|
||
satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial);
|
||
satellitePoints.visible = false;
|
||
satellitePoints.userData = { type: "satellitePoints" };
|
||
satellitePoints.renderOrder = 6;
|
||
|
||
const originalScale = { x: 1, y: 1, z: 1 };
|
||
const syncPointScale = () => {
|
||
if (earthObj && earthObj.scale.x !== 1) {
|
||
const scaleX = originalScale.x / earthObj.scale.x;
|
||
const scaleY = originalScale.y / earthObj.scale.y;
|
||
const scaleZ = originalScale.z / earthObj.scale.z;
|
||
satellitePoints.scale.set(scaleX, scaleY, scaleZ);
|
||
if (satelliteBackdropPoints) {
|
||
satelliteBackdropPoints.scale.set(scaleX, scaleY, scaleZ);
|
||
}
|
||
} else {
|
||
satellitePoints.scale.set(originalScale.x, originalScale.y, originalScale.z);
|
||
if (satelliteBackdropPoints) {
|
||
satelliteBackdropPoints.scale.set(
|
||
originalScale.x,
|
||
originalScale.y,
|
||
originalScale.z,
|
||
);
|
||
}
|
||
}
|
||
};
|
||
|
||
satelliteBackdropPoints.onBeforeRender = syncPointScale;
|
||
satellitePoints.onBeforeRender = syncPointScale;
|
||
|
||
earthObj.add(satelliteBackdropPoints);
|
||
earthObj.add(satellitePoints);
|
||
|
||
const trailGeometry = new THREE.BufferGeometry();
|
||
|
||
const trailMaterial = new THREE.LineBasicMaterial({
|
||
vertexColors: true,
|
||
transparent: true,
|
||
opacity: 0.3,
|
||
blending: THREE.AdditiveBlending,
|
||
});
|
||
|
||
satelliteTrails = new THREE.LineSegments(trailGeometry, trailMaterial);
|
||
satelliteTrails.visible = false;
|
||
satelliteTrails.userData = { type: "satelliteTrails" };
|
||
earthObj.add(satelliteTrails);
|
||
ensureSatelliteCapacity(0);
|
||
|
||
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
|
||
return satellitePoints;
|
||
}
|
||
|
||
function getRequestedSatelliteLimit(limitOverride) {
|
||
if (limitOverride === null) return null;
|
||
if (Number.isFinite(limitOverride) && limitOverride > 0) {
|
||
return Math.floor(limitOverride);
|
||
}
|
||
|
||
return SATELLITE_CONFIG.maxCount < 0 ? null : SATELLITE_CONFIG.maxCount;
|
||
}
|
||
|
||
function createSatellitePositionState() {
|
||
return {
|
||
current: new THREE.Vector3(),
|
||
trail: [],
|
||
trailIndex: 0,
|
||
trailCount: 0,
|
||
};
|
||
}
|
||
|
||
function ensureSatelliteCapacity(count) {
|
||
if (!satellitePoints || !satelliteBackdropPoints || !satelliteTrails) return;
|
||
|
||
const nextCapacity = Math.max(count, 0);
|
||
if (nextCapacity === satelliteCapacity) return;
|
||
|
||
const previousPointPositions =
|
||
satellitePoints.geometry.attributes.position?.array || null;
|
||
const previousBackdropPositions =
|
||
satelliteBackdropPoints.geometry.attributes.position?.array || null;
|
||
const previousColors = satellitePoints.geometry.attributes.color?.array || null;
|
||
const previousTrailPositions =
|
||
satelliteTrails.geometry.attributes.position?.array || null;
|
||
const previousTrailColors =
|
||
satelliteTrails.geometry.attributes.color?.array || null;
|
||
const previousSatellitePositions = satellitePositions;
|
||
const previousCapacity = satelliteCapacity;
|
||
|
||
const positions = new Float32Array(nextCapacity * 3);
|
||
const backdropPositions = new Float32Array(nextCapacity * 3);
|
||
const colors = new Float32Array(nextCapacity * 3);
|
||
if (previousPointPositions) {
|
||
positions.set(
|
||
previousPointPositions.subarray(0, Math.min(previousPointPositions.length, positions.length)),
|
||
);
|
||
}
|
||
if (previousBackdropPositions) {
|
||
backdropPositions.set(
|
||
previousBackdropPositions.subarray(
|
||
0,
|
||
Math.min(previousBackdropPositions.length, backdropPositions.length),
|
||
),
|
||
);
|
||
}
|
||
if (previousColors) {
|
||
colors.set(previousColors.subarray(0, Math.min(previousColors.length, colors.length)));
|
||
}
|
||
satelliteBackdropPoints.geometry.setAttribute(
|
||
"position",
|
||
new THREE.BufferAttribute(backdropPositions, 3),
|
||
);
|
||
satelliteBackdropPoints.geometry.setDrawRange(
|
||
0,
|
||
Math.min(previousCapacity, nextCapacity),
|
||
);
|
||
satellitePoints.geometry.setAttribute(
|
||
"position",
|
||
new THREE.BufferAttribute(positions, 3),
|
||
);
|
||
satellitePoints.geometry.setAttribute(
|
||
"color",
|
||
new THREE.BufferAttribute(colors, 3),
|
||
);
|
||
satellitePoints.geometry.setDrawRange(0, Math.min(previousCapacity, nextCapacity));
|
||
|
||
const trailPositions = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
|
||
const trailColors = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
|
||
if (previousTrailPositions) {
|
||
trailPositions.set(
|
||
previousTrailPositions.subarray(
|
||
0,
|
||
Math.min(previousTrailPositions.length, trailPositions.length),
|
||
),
|
||
);
|
||
}
|
||
if (previousTrailColors) {
|
||
trailColors.set(
|
||
previousTrailColors.subarray(
|
||
0,
|
||
Math.min(previousTrailColors.length, trailColors.length),
|
||
),
|
||
);
|
||
}
|
||
satelliteTrails.geometry.setAttribute(
|
||
"position",
|
||
new THREE.BufferAttribute(trailPositions, 3),
|
||
);
|
||
satelliteTrails.geometry.setAttribute(
|
||
"color",
|
||
new THREE.BufferAttribute(trailColors, 3),
|
||
);
|
||
|
||
satellitePositions = Array.from({ length: nextCapacity }, (_, index) => {
|
||
const previousState = previousSatellitePositions[index];
|
||
if (!previousState) {
|
||
return createSatellitePositionState();
|
||
}
|
||
|
||
return {
|
||
current: previousState.current.clone(),
|
||
trail: previousState.trail.slice(),
|
||
trailIndex: previousState.trailIndex,
|
||
trailCount: previousState.trailCount,
|
||
};
|
||
});
|
||
satelliteCapacity = nextCapacity;
|
||
}
|
||
|
||
function computeSatellitePosition(satellite, time) {
|
||
try {
|
||
const props = satellite.properties;
|
||
if (!props || !props.norad_cat_id) {
|
||
return null;
|
||
}
|
||
|
||
const satrec = getOrBuildSatrec(props, time);
|
||
if (!satrec || satrec.error) {
|
||
return null;
|
||
}
|
||
|
||
const positionAndVelocity = propagate(satrec, time);
|
||
if (!positionAndVelocity || !positionAndVelocity.position) {
|
||
return null;
|
||
}
|
||
|
||
const x = positionAndVelocity.position.x;
|
||
const y = positionAndVelocity.position.y;
|
||
const z = positionAndVelocity.position.z;
|
||
|
||
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
||
return null;
|
||
}
|
||
|
||
const r = Math.sqrt(x * x + y * y + z * z);
|
||
const displayRadius =
|
||
CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||
const scale = displayRadius / r;
|
||
|
||
return new THREE.Vector3(x * scale, y * scale, z * scale);
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function buildSatrecFromProperties(props, fallbackTime) {
|
||
if (props.tle_line1 && props.tle_line2) {
|
||
// Prefer source-provided TLE lines so the client does not need to rebuild them.
|
||
const satrec = twoline2satrec(props.tle_line1, props.tle_line2);
|
||
if (!satrec.error) {
|
||
return satrec;
|
||
}
|
||
}
|
||
|
||
const tleLines = buildTleLinesFromElements(props, fallbackTime);
|
||
if (!tleLines) {
|
||
return null;
|
||
}
|
||
|
||
return twoline2satrec(tleLines.line1, tleLines.line2);
|
||
}
|
||
|
||
function getSatelliteSatrecCacheKey(props) {
|
||
if (!props?.norad_cat_id) {
|
||
return null;
|
||
}
|
||
|
||
if (props.tle_line1 && props.tle_line2) {
|
||
return `tle:${props.norad_cat_id}:${props.tle_line1}:${props.tle_line2}`;
|
||
}
|
||
|
||
if (props.epoch) {
|
||
return [
|
||
"elements",
|
||
props.norad_cat_id,
|
||
props.epoch,
|
||
props.inclination,
|
||
props.raan,
|
||
props.eccentricity,
|
||
props.arg_of_perigee,
|
||
props.mean_anomaly,
|
||
props.mean_motion,
|
||
].join(":");
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getOrBuildSatrec(props, fallbackTime) {
|
||
const cacheKey = getSatelliteSatrecCacheKey(props);
|
||
if (cacheKey && satelliteSatrecCache.has(cacheKey)) {
|
||
return satelliteSatrecCache.get(cacheKey);
|
||
}
|
||
|
||
const satrec = buildSatrecFromProperties(props, fallbackTime);
|
||
if (cacheKey && satrec && !satrec.error) {
|
||
satelliteSatrecCache.set(cacheKey, satrec);
|
||
}
|
||
return satrec;
|
||
}
|
||
|
||
function computeTleChecksum(line) {
|
||
let sum = 0;
|
||
|
||
for (const char of line.slice(0, 68)) {
|
||
if (char >= "0" && char <= "9") {
|
||
sum += Number(char);
|
||
} else if (char === "-") {
|
||
sum += 1;
|
||
}
|
||
}
|
||
|
||
return String(sum % 10);
|
||
}
|
||
|
||
function buildTleLinesFromElements(props, fallbackTime) {
|
||
if (!props?.norad_cat_id) {
|
||
return null;
|
||
}
|
||
|
||
const requiredValues = [
|
||
props.inclination,
|
||
props.raan,
|
||
props.eccentricity,
|
||
props.arg_of_perigee,
|
||
props.mean_anomaly,
|
||
props.mean_motion,
|
||
];
|
||
if (requiredValues.some((value) => value === null || value === undefined)) {
|
||
return null;
|
||
}
|
||
|
||
const epochDate =
|
||
props.epoch && String(props.epoch).length >= 10
|
||
? new Date(props.epoch)
|
||
: fallbackTime;
|
||
if (Number.isNaN(epochDate.getTime())) {
|
||
return null;
|
||
}
|
||
|
||
const epochYear = epochDate.getUTCFullYear() % 100;
|
||
const startOfYear = new Date(Date.UTC(epochDate.getUTCFullYear(), 0, 1));
|
||
const dayOfYear = Math.floor((epochDate - startOfYear) / 86400000) + 1;
|
||
const msOfDay =
|
||
epochDate.getUTCHours() * 3600000 +
|
||
epochDate.getUTCMinutes() * 60000 +
|
||
epochDate.getUTCSeconds() * 1000 +
|
||
epochDate.getUTCMilliseconds();
|
||
const dayFraction = msOfDay / 86400000;
|
||
const epochStr =
|
||
String(epochYear).padStart(2, "0") +
|
||
String(dayOfYear).padStart(3, "0") +
|
||
dayFraction.toFixed(8).slice(1);
|
||
|
||
const eccentricityDigits = Math.round(Number(props.eccentricity) * 1e7)
|
||
.toString()
|
||
.padStart(7, "0");
|
||
|
||
// Keep a local fallback for historical rows that do not have stored TLE lines yet.
|
||
const line1Core = `1 ${String(props.norad_cat_id).padStart(5, "0")}U 00001A ${epochStr} .00000000 00000-0 00000-0 0 999`;
|
||
const line2Core = `2 ${String(props.norad_cat_id).padStart(5, "0")} ${Number(
|
||
props.inclination,
|
||
)
|
||
.toFixed(4)
|
||
.padStart(
|
||
8,
|
||
)} ${Number(props.raan).toFixed(4).padStart(8)} ${eccentricityDigits} ${Number(
|
||
props.arg_of_perigee,
|
||
)
|
||
.toFixed(4)
|
||
.padStart(8)} ${Number(props.mean_anomaly).toFixed(4).padStart(8)} ${Number(
|
||
props.mean_motion,
|
||
)
|
||
.toFixed(8)
|
||
.padStart(11)}00000`;
|
||
|
||
return {
|
||
line1: line1Core + computeTleChecksum(line1Core),
|
||
line2: line2Core + computeTleChecksum(line2Core),
|
||
};
|
||
}
|
||
|
||
function generateFallbackPosition(satellite, index, total) {
|
||
const radius = CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||
|
||
const noradId = satellite.properties?.norad_cat_id || index;
|
||
const inclination = satellite.properties?.inclination || 53;
|
||
const raan = satellite.properties?.raan || 0;
|
||
const meanAnomaly = satellite.properties?.mean_anomaly || 0;
|
||
|
||
const hash = String(noradId)
|
||
.split("")
|
||
.reduce((a, b) => a + b.charCodeAt(0), 0);
|
||
const randomOffset = (hash % 1000) / 1000;
|
||
|
||
const normalizedIndex = index / total;
|
||
const theta = normalizedIndex * Math.PI * 2 * 10 + (raan * Math.PI) / 180;
|
||
const phi =
|
||
(inclination * Math.PI) / 180 + ((meanAnomaly * Math.PI) / 180) * 0.1;
|
||
|
||
const adjustedPhi = Math.abs(phi % Math.PI);
|
||
const adjustedTheta = theta + randomOffset * Math.PI * 2;
|
||
|
||
const x = radius * Math.sin(adjustedPhi) * Math.cos(adjustedTheta);
|
||
const y = radius * Math.cos(adjustedPhi);
|
||
const z = radius * Math.sin(adjustedPhi) * Math.sin(adjustedTheta);
|
||
|
||
return new THREE.Vector3(x, y, z);
|
||
}
|
||
|
||
export async function loadSatellites(options = {}) {
|
||
const limit = getRequestedSatelliteLimit(options.limit);
|
||
const url = new URL(SATELLITE_CONFIG.apiPath, window.location.origin);
|
||
if (limit !== null) {
|
||
url.searchParams.set("limit", String(limit));
|
||
}
|
||
|
||
const response = await fetch(url.toString());
|
||
if (!response.ok) {
|
||
throw new Error(`卫星接口返回 HTTP ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
satelliteData = data.features || [];
|
||
satelliteSatrecCache = new Map();
|
||
ensureSatelliteCapacity(satelliteData.length);
|
||
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
|
||
return {
|
||
count: satelliteData.length,
|
||
requestedLimit: limit,
|
||
};
|
||
}
|
||
|
||
export function updateSatellitePositions(deltaTime = 0, force = false) {
|
||
if (!satellitePoints || !satelliteBackdropPoints || satelliteData.length === 0) return;
|
||
|
||
const shouldUpdateTrails =
|
||
showSatellites || showTrails || lockedSatelliteIndex !== null;
|
||
positionUpdateAccumulator += deltaTime;
|
||
|
||
if (!force && positionUpdateAccumulator < POSITION_UPDATE_INTERVAL_MS) {
|
||
return;
|
||
}
|
||
|
||
const elapsedMs = Math.max(
|
||
positionUpdateAccumulator,
|
||
POSITION_UPDATE_INTERVAL_MS,
|
||
);
|
||
positionUpdateAccumulator = 0;
|
||
|
||
const positions = satellitePoints.geometry.attributes.position.array;
|
||
const backdropPositions =
|
||
satelliteBackdropPoints.geometry.attributes.position.array;
|
||
const colors = satellitePoints.geometry.attributes.color.array;
|
||
const trailPositions = satelliteTrails.geometry.attributes.position.array;
|
||
const trailColors = satelliteTrails.geometry.attributes.color.array;
|
||
const baseTime = new Date(Date.now() + elapsedMs);
|
||
const count = Math.min(satelliteData.length, satelliteCapacity);
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const satellite = satelliteData[i];
|
||
const props = satellite.properties;
|
||
const timeOffset = (i / count) * 2 * Math.PI * 0.1;
|
||
const adjustedTime = new Date(
|
||
baseTime.getTime() + timeOffset * 1000 * 60 * 10,
|
||
);
|
||
|
||
let pos = computeSatellitePosition(satellite, adjustedTime);
|
||
if (!pos) {
|
||
pos = generateFallbackPosition(satellite, i, count);
|
||
}
|
||
|
||
satellitePositions[i].current.copy(pos);
|
||
|
||
if (shouldUpdateTrails && i !== lockedSatelliteIndex) {
|
||
const satPos = satellitePositions[i];
|
||
satPos.trail[satPos.trailIndex] = pos.clone();
|
||
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
|
||
if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++;
|
||
}
|
||
|
||
positions[i * 3] = pos.x;
|
||
positions[i * 3 + 1] = pos.y;
|
||
positions[i * 3 + 2] = pos.z;
|
||
backdropPositions[i * 3] = pos.x;
|
||
backdropPositions[i * 3 + 1] = pos.y;
|
||
backdropPositions[i * 3 + 2] = pos.z;
|
||
|
||
const rule = getSatelliteLegendRule(props);
|
||
const { r, g, b } = getSatelliteRuleColor(rule);
|
||
|
||
if (highlightedSatelliteIndices !== null && !highlightedSatelliteIndices.has(i)) {
|
||
const lum = r * 0.299 + g * 0.587 + b * 0.114;
|
||
colors[i * 3] = lum * 0.75 + r * 0.25;
|
||
colors[i * 3 + 1] = lum * 0.75 + g * 0.25;
|
||
colors[i * 3 + 2] = lum * 0.75 + b * 0.25;
|
||
} else {
|
||
colors[i * 3] = r;
|
||
colors[i * 3 + 1] = g;
|
||
colors[i * 3 + 2] = b;
|
||
}
|
||
|
||
const satPosition = satellitePositions[i];
|
||
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
||
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
||
|
||
if (j < satPosition.trailCount) {
|
||
const idx =
|
||
(satPosition.trailIndex - satPosition.trailCount + j + TRAIL_LENGTH) %
|
||
TRAIL_LENGTH;
|
||
const trailPoint = satPosition.trail[idx];
|
||
if (trailPoint) {
|
||
trailPositions[trailIdx] = trailPoint.x;
|
||
trailPositions[trailIdx + 1] = trailPoint.y;
|
||
trailPositions[trailIdx + 2] = trailPoint.z;
|
||
const alpha = (j + 1) / satPosition.trailCount;
|
||
trailColors[trailIdx] = r * alpha;
|
||
trailColors[trailIdx + 1] = g * alpha;
|
||
trailColors[trailIdx + 2] = b * alpha;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
trailPositions[trailIdx] = pos.x;
|
||
trailPositions[trailIdx + 1] = pos.y;
|
||
trailPositions[trailIdx + 2] = pos.z;
|
||
trailColors[trailIdx] = 0;
|
||
trailColors[trailIdx + 1] = 0;
|
||
trailColors[trailIdx + 2] = 0;
|
||
}
|
||
}
|
||
|
||
for (let i = count; i < satelliteCapacity; i++) {
|
||
positions[i * 3] = 0;
|
||
positions[i * 3 + 1] = 0;
|
||
positions[i * 3 + 2] = 0;
|
||
backdropPositions[i * 3] = 0;
|
||
backdropPositions[i * 3 + 1] = 0;
|
||
backdropPositions[i * 3 + 2] = 0;
|
||
|
||
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
||
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
||
trailPositions[trailIdx] = 0;
|
||
trailPositions[trailIdx + 1] = 0;
|
||
trailPositions[trailIdx + 2] = 0;
|
||
}
|
||
}
|
||
|
||
satellitePoints.geometry.attributes.position.needsUpdate = true;
|
||
satellitePoints.geometry.attributes.color.needsUpdate = true;
|
||
satellitePoints.geometry.setDrawRange(0, count);
|
||
satelliteBackdropPoints.geometry.attributes.position.needsUpdate = true;
|
||
satelliteBackdropPoints.geometry.setDrawRange(0, count);
|
||
|
||
satelliteTrails.geometry.attributes.position.needsUpdate = true;
|
||
satelliteTrails.geometry.attributes.color.needsUpdate = true;
|
||
|
||
// Keep the hover ring synced with the propagated satellite position even
|
||
// when the pointer stays still and no new hover event is emitted.
|
||
if (
|
||
hoveredSatelliteIndex !== null &&
|
||
hoveredSatelliteIndex >= 0 &&
|
||
hoveredSatelliteIndex < count &&
|
||
hoveredSatelliteIndex !== lockedSatelliteIndex
|
||
) {
|
||
updateHoverRingPosition(satellitePositions[hoveredSatelliteIndex].current);
|
||
}
|
||
}
|
||
|
||
export function toggleSatellites(visible) {
|
||
showSatellites = visible;
|
||
if (satelliteBackdropPoints) {
|
||
satelliteBackdropPoints.visible = visible;
|
||
}
|
||
if (satellitePoints) {
|
||
satellitePoints.visible = visible;
|
||
}
|
||
if (satelliteTrails) {
|
||
satelliteTrails.visible = visible && showTrails;
|
||
}
|
||
}
|
||
|
||
export function toggleTrails(visible) {
|
||
showTrails = visible;
|
||
if (satelliteTrails) {
|
||
satelliteTrails.visible = visible && showSatellites;
|
||
}
|
||
}
|
||
|
||
export function getShowTrails() {
|
||
return showTrails;
|
||
}
|
||
|
||
export function getShowSatellites() {
|
||
return showSatellites;
|
||
}
|
||
|
||
export function getSatelliteCount() {
|
||
return satelliteData.length;
|
||
}
|
||
|
||
export function getSatelliteAt(index) {
|
||
if (index >= 0 && index < satelliteData.length) {
|
||
return satelliteData[index];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function getSatelliteData() {
|
||
return satelliteData;
|
||
}
|
||
|
||
export function selectSatellite(index) {
|
||
selectedSatellite = index;
|
||
return getSatelliteAt(index);
|
||
}
|
||
|
||
export function getSatellitePoints() {
|
||
return satellitePoints;
|
||
}
|
||
|
||
export function getSatellitePositions() {
|
||
return satellitePositions;
|
||
}
|
||
|
||
export function setSatelliteCamera(camera) {
|
||
cameraRef = camera;
|
||
}
|
||
|
||
export function setLockedSatelliteIndex(index) {
|
||
lockedSatelliteIndex = index;
|
||
}
|
||
|
||
export function setHoveredSatelliteIndex(index) {
|
||
hoveredSatelliteIndex = index;
|
||
}
|
||
|
||
export function isSatelliteFrontFacing(index, camera = cameraRef) {
|
||
if (!earthObjRef || !camera) return true;
|
||
if (!satellitePositions || !satellitePositions[index]) return true;
|
||
|
||
const satPos = satellitePositions[index].current;
|
||
if (!satPos) return true;
|
||
|
||
scratchWorldSatellitePosition
|
||
.copy(satPos)
|
||
.applyMatrix4(earthObjRef.matrixWorld);
|
||
scratchToCamera.subVectors(camera.position, earthObjRef.position).normalize();
|
||
scratchToSatellite
|
||
.subVectors(scratchWorldSatellitePosition, earthObjRef.position)
|
||
.normalize();
|
||
|
||
return (
|
||
scratchToCamera.dot(scratchToSatellite) >
|
||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||
);
|
||
}
|
||
|
||
function createBrighterDotCanvas() {
|
||
const size = DOT_TEXTURE_SIZE * 2;
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = size;
|
||
canvas.height = size;
|
||
const ctx = canvas.getContext("2d");
|
||
const center = size / 2;
|
||
const gradient = ctx.createRadialGradient(
|
||
center,
|
||
center,
|
||
0,
|
||
center,
|
||
center,
|
||
center,
|
||
);
|
||
gradient.addColorStop(0, "rgba(255, 255, 200, 1)");
|
||
gradient.addColorStop(0.3, "rgba(255, 220, 100, 0.9)");
|
||
gradient.addColorStop(0.7, "rgba(255, 180, 50, 0.5)");
|
||
gradient.addColorStop(1, "rgba(255, 150, 0, 0)");
|
||
ctx.fillStyle = gradient;
|
||
ctx.fillRect(0, 0, size, size);
|
||
return canvas;
|
||
}
|
||
|
||
function createRingSprite(position, isLocked = false) {
|
||
if (!earthObjRef) return null;
|
||
|
||
const ringTexture = createRingTexture(
|
||
8,
|
||
12,
|
||
isLocked ? "#ffcc00" : "#ffffff",
|
||
);
|
||
const spriteMaterial = new THREE.SpriteMaterial({
|
||
map: ringTexture,
|
||
transparent: true,
|
||
opacity: 0.8,
|
||
depthTest: false,
|
||
sizeAttenuation: false,
|
||
});
|
||
|
||
const sprite = new THREE.Sprite(spriteMaterial);
|
||
sprite.position.copy(position);
|
||
sprite.scale.set(SATELLITE_CONFIG.ringSize, SATELLITE_CONFIG.ringSize, 1);
|
||
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||
earthObjRef.add(sprite);
|
||
return sprite;
|
||
}
|
||
|
||
function createRelatedSatelliteSprite(position, color = "#7dd3fc") {
|
||
if (!earthObjRef) return null;
|
||
|
||
const ringTexture = createRingTexture(7, 11, color);
|
||
const spriteMaterial = new THREE.SpriteMaterial({
|
||
map: ringTexture,
|
||
transparent: true,
|
||
opacity: 0.55,
|
||
depthTest: false,
|
||
sizeAttenuation: false,
|
||
});
|
||
|
||
const sprite = new THREE.Sprite(spriteMaterial);
|
||
sprite.position.copy(position);
|
||
sprite.scale.set(SATELLITE_CONFIG.ringSize * 0.8, SATELLITE_CONFIG.ringSize * 0.8, 1);
|
||
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||
earthObjRef.add(sprite);
|
||
return sprite;
|
||
}
|
||
|
||
export function showHoverRing(position, isLocked = false) {
|
||
if (!earthObjRef || !position) return null;
|
||
|
||
if (isLocked) {
|
||
hideLockedRing();
|
||
lockedRingSprite = createRingSprite(position, true);
|
||
|
||
const dotCanvas = createBrighterDotCanvas();
|
||
const dotTexture = new THREE.CanvasTexture(dotCanvas);
|
||
const dotMaterial = new THREE.SpriteMaterial({
|
||
map: dotTexture,
|
||
transparent: true,
|
||
opacity: 1.0,
|
||
depthTest: false,
|
||
});
|
||
lockedDotSprite = new THREE.Sprite(dotMaterial);
|
||
lockedDotSprite.position.copy(position);
|
||
lockedDotSprite.scale.set(4, 4, 1);
|
||
lockedDotSprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder + 1;
|
||
earthObjRef.add(lockedDotSprite);
|
||
return lockedRingSprite;
|
||
}
|
||
|
||
hideHoverRings();
|
||
hoverRingSprite = createRingSprite(position, false);
|
||
return hoverRingSprite;
|
||
}
|
||
|
||
export function hideHoverRings() {
|
||
if (hoverRingSprite) {
|
||
disposeObject3D(hoverRingSprite);
|
||
hoverRingSprite = null;
|
||
}
|
||
}
|
||
|
||
export function hideLockedRing() {
|
||
if (lockedRingSprite) {
|
||
disposeObject3D(lockedRingSprite);
|
||
lockedRingSprite = null;
|
||
}
|
||
if (lockedDotSprite) {
|
||
disposeObject3D(lockedDotSprite);
|
||
lockedDotSprite = null;
|
||
}
|
||
}
|
||
|
||
export function updateLockedRingPosition(position) {
|
||
if (!position) return;
|
||
if (!lockedRingSprite || !lockedDotSprite) {
|
||
showHoverRing(position, true);
|
||
}
|
||
if (lockedRingSprite) {
|
||
lockedRingSprite.position.copy(position);
|
||
const ringPulse = getBreathingPulse(breathingPhase);
|
||
const breathScale =
|
||
1 +
|
||
(ringPulse * 2 - 1) * SATELLITE_CONFIG.breathingScaleAmplitude;
|
||
lockedRingSprite.scale.set(
|
||
SATELLITE_CONFIG.ringSize * breathScale,
|
||
SATELLITE_CONFIG.ringSize * breathScale,
|
||
1,
|
||
);
|
||
lockedRingSprite.material.opacity =
|
||
SATELLITE_CONFIG.breathingOpacityMin +
|
||
ringPulse *
|
||
(SATELLITE_CONFIG.breathingOpacityMax -
|
||
SATELLITE_CONFIG.breathingOpacityMin);
|
||
}
|
||
|
||
if (lockedDotSprite) {
|
||
lockedDotSprite.position.copy(position);
|
||
const dotPulse = getBreathingPulse(breathingPhase);
|
||
const dotBreathScale =
|
||
1 +
|
||
(dotPulse * 2 - 1) * SATELLITE_CONFIG.dotBreathingScaleAmplitude;
|
||
lockedDotSprite.scale.set(4 * dotBreathScale, 4 * dotBreathScale, 1);
|
||
lockedDotSprite.material.opacity =
|
||
SATELLITE_CONFIG.dotOpacityMin +
|
||
dotPulse *
|
||
(SATELLITE_CONFIG.dotOpacityMax - SATELLITE_CONFIG.dotOpacityMin);
|
||
}
|
||
}
|
||
|
||
export function updateHoverRingPosition(position) {
|
||
if (hoverRingSprite && position) {
|
||
hoverRingSprite.position.copy(position);
|
||
hoverRingSprite.scale.set(
|
||
SATELLITE_CONFIG.ringSize,
|
||
SATELLITE_CONFIG.ringSize,
|
||
1,
|
||
);
|
||
}
|
||
}
|
||
|
||
export function setSatelliteRingState(index, state, position) {
|
||
switch (state) {
|
||
case "hover":
|
||
hoveredSatelliteIndex = index;
|
||
hideHoverRings();
|
||
showHoverRing(position, false);
|
||
break;
|
||
case "locked":
|
||
hoveredSatelliteIndex = null;
|
||
hideHoverRings();
|
||
showHoverRing(position, true);
|
||
break;
|
||
case "none":
|
||
hoveredSatelliteIndex = null;
|
||
hideHoverRings();
|
||
hideLockedRing();
|
||
break;
|
||
}
|
||
}
|
||
|
||
function applyDimMaterialState(isDimmed) {
|
||
if (satellitePoints) {
|
||
satellitePoints.material.opacity = isDimmed ? 0.32 : 0.9;
|
||
}
|
||
if (satelliteBackdropPoints) {
|
||
satelliteBackdropPoints.material.opacity = isDimmed ? 0.12 : 0.42;
|
||
}
|
||
}
|
||
|
||
export function clearRelatedSatelliteHighlights() {
|
||
relatedSatelliteSprites.forEach((item) => {
|
||
if (item.sprite) {
|
||
disposeObject3D(item.sprite);
|
||
}
|
||
});
|
||
relatedSatelliteSprites = [];
|
||
highlightedSatelliteIndices = null;
|
||
applyDimMaterialState(false);
|
||
}
|
||
|
||
export function highlightRelatedSatellites(indices, color = "#7dd3fc") {
|
||
clearRelatedSatelliteHighlights();
|
||
if (!Array.isArray(indices) || indices.length === 0) return;
|
||
|
||
highlightedSatelliteIndices = new Set(indices);
|
||
applyDimMaterialState(true);
|
||
indices.forEach((index) => {
|
||
const pos = satellitePositions?.[index]?.current;
|
||
if (!pos) return;
|
||
const sprite = createRelatedSatelliteSprite(pos, color);
|
||
if (!sprite) return;
|
||
relatedSatelliteSprites.push({ index, sprite, color });
|
||
});
|
||
}
|
||
|
||
export function updateRelatedSatelliteHighlights() {
|
||
if (relatedSatelliteSprites.length === 0) return;
|
||
relatedSatelliteSprites = relatedSatelliteSprites.filter((item) => {
|
||
const pos = satellitePositions?.[item.index]?.current;
|
||
if (!pos || !item.sprite) return false;
|
||
item.sprite.position.copy(pos);
|
||
return true;
|
||
});
|
||
}
|
||
|
||
export function getRelatedSatelliteIndicesForRegions(
|
||
regions,
|
||
{ limit = 6, maxAngleDeg = 22 } = {},
|
||
) {
|
||
if (!Array.isArray(regions) || regions.length === 0 || satellitePositions.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const regionVectors = regions
|
||
.filter(
|
||
(region) =>
|
||
typeof region?.latitude === "number" &&
|
||
typeof region?.longitude === "number",
|
||
)
|
||
.map((region) =>
|
||
latLonToVector3(region.latitude, region.longitude, CONFIG.earthRadius + 1)
|
||
.clone()
|
||
.normalize(),
|
||
);
|
||
|
||
if (regionVectors.length === 0) return [];
|
||
|
||
const threshold = Math.cos((maxAngleDeg * Math.PI) / 180);
|
||
const ranked = [];
|
||
|
||
satellitePositions.forEach((item, index) => {
|
||
const current = item?.current;
|
||
if (!current || current.lengthSq() === 0) return;
|
||
const satVector = current.clone().normalize();
|
||
let bestDot = -1;
|
||
regionVectors.forEach((regionVector) => {
|
||
bestDot = Math.max(bestDot, satVector.dot(regionVector));
|
||
});
|
||
if (bestDot >= threshold) {
|
||
ranked.push({ index, score: bestDot });
|
||
}
|
||
});
|
||
|
||
return ranked
|
||
.sort((a, b) => b.score - a.score)
|
||
.slice(0, limit)
|
||
.map((item) => item.index);
|
||
}
|
||
|
||
export function initSatelliteScene(scene, earth) {
|
||
sceneRef = scene;
|
||
earthObjRef = earth;
|
||
}
|
||
|
||
function calculateOrbitalPeriod(meanMotion) {
|
||
return 86400 / meanMotion;
|
||
}
|
||
|
||
function calculatePredictedOrbit(
|
||
satellite,
|
||
periodSeconds,
|
||
sampleInterval = 10,
|
||
) {
|
||
const points = [];
|
||
const samples = Math.ceil(periodSeconds / sampleInterval);
|
||
const now = new Date();
|
||
|
||
for (let i = 0; i <= samples; i++) {
|
||
const time = new Date(now.getTime() + i * sampleInterval * 1000);
|
||
const pos = computeSatellitePosition(satellite, time);
|
||
if (pos) points.push(pos);
|
||
}
|
||
|
||
if (points.length < samples * 0.5) {
|
||
points.length = 0;
|
||
const radius =
|
||
CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||
const inclination = satellite.properties?.inclination || 53;
|
||
const raan = satellite.properties?.raan || 0;
|
||
|
||
for (let i = 0; i <= samples; i++) {
|
||
const theta = (i / samples) * Math.PI * 2;
|
||
const phi = (inclination * Math.PI) / 180;
|
||
const x =
|
||
radius * Math.sin(phi) * Math.cos(theta + (raan * Math.PI) / 180);
|
||
const y = radius * Math.cos(phi);
|
||
const z =
|
||
radius * Math.sin(phi) * Math.sin(theta + (raan * Math.PI) / 180);
|
||
points.push(new THREE.Vector3(x, y, z));
|
||
}
|
||
}
|
||
|
||
return points;
|
||
}
|
||
|
||
export function showPredictedOrbit(satellite) {
|
||
hidePredictedOrbit();
|
||
if (!earthObjRef) return;
|
||
|
||
const meanMotion = satellite.properties?.mean_motion || 15;
|
||
const periodSeconds = calculateOrbitalPeriod(meanMotion);
|
||
const points = calculatePredictedOrbit(satellite, periodSeconds);
|
||
if (points.length < 2) return;
|
||
|
||
const positions = new Float32Array(points.length * 3);
|
||
const colors = new Float32Array(points.length * 3);
|
||
|
||
for (let i = 0; i < points.length; i++) {
|
||
positions[i * 3] = points[i].x;
|
||
positions[i * 3 + 1] = points[i].y;
|
||
positions[i * 3 + 2] = points[i].z;
|
||
|
||
const t = i / (points.length - 1);
|
||
colors[i * 3] = 1 - t * 0.4;
|
||
colors[i * 3 + 1] = 1 - t * 0.6;
|
||
colors[i * 3 + 2] = t;
|
||
}
|
||
|
||
const geometry = new THREE.BufferGeometry();
|
||
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
||
|
||
const material = new THREE.LineBasicMaterial({
|
||
vertexColors: true,
|
||
transparent: true,
|
||
opacity: 0.8,
|
||
blending: THREE.AdditiveBlending,
|
||
depthTest: true,
|
||
depthWrite: false,
|
||
});
|
||
|
||
predictedOrbitLine = new THREE.Line(geometry, material);
|
||
predictedOrbitLine.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
|
||
earthObjRef.add(predictedOrbitLine);
|
||
}
|
||
|
||
export function hidePredictedOrbit() {
|
||
if (predictedOrbitLine) {
|
||
disposeObject3D(predictedOrbitLine);
|
||
predictedOrbitLine = null;
|
||
}
|
||
}
|
||
|
||
export function clearSatelliteData() {
|
||
satelliteData = [];
|
||
satelliteSatrecCache = new Map();
|
||
selectedSatellite = null;
|
||
lockedSatelliteIndex = null;
|
||
hoveredSatelliteIndex = null;
|
||
positionUpdateAccumulator = 0;
|
||
breathingPhase = 0;
|
||
|
||
satellitePositions.forEach((position) => {
|
||
position.current.set(0, 0, 0);
|
||
position.trail = [];
|
||
position.trailIndex = 0;
|
||
position.trailCount = 0;
|
||
});
|
||
|
||
if (satellitePoints) {
|
||
const positionAttr = satellitePoints.geometry.attributes.position;
|
||
const colorAttr = satellitePoints.geometry.attributes.color;
|
||
if (positionAttr?.array) {
|
||
positionAttr.array.fill(0);
|
||
positionAttr.needsUpdate = true;
|
||
}
|
||
if (colorAttr?.array) {
|
||
colorAttr.array.fill(0);
|
||
colorAttr.needsUpdate = true;
|
||
}
|
||
satellitePoints.geometry.setDrawRange(0, 0);
|
||
}
|
||
|
||
if (satelliteBackdropPoints) {
|
||
const backdropPositionAttr =
|
||
satelliteBackdropPoints.geometry.attributes.position;
|
||
if (backdropPositionAttr?.array) {
|
||
backdropPositionAttr.array.fill(0);
|
||
backdropPositionAttr.needsUpdate = true;
|
||
}
|
||
satelliteBackdropPoints.geometry.setDrawRange(0, 0);
|
||
}
|
||
|
||
if (satelliteTrails) {
|
||
const trailPositionAttr = satelliteTrails.geometry.attributes.position;
|
||
const trailColorAttr = satelliteTrails.geometry.attributes.color;
|
||
if (trailPositionAttr?.array) {
|
||
trailPositionAttr.array.fill(0);
|
||
trailPositionAttr.needsUpdate = true;
|
||
}
|
||
if (trailColorAttr?.array) {
|
||
trailColorAttr.array.fill(0);
|
||
trailColorAttr.needsUpdate = true;
|
||
}
|
||
}
|
||
|
||
hideHoverRings();
|
||
hideLockedRing();
|
||
hidePredictedOrbit();
|
||
clearRelatedSatelliteHighlights();
|
||
}
|
||
|
||
export function resetSatelliteState() {
|
||
clearSatelliteData();
|
||
|
||
if (satelliteBackdropPoints) {
|
||
disposeObject3D(satelliteBackdropPoints);
|
||
satelliteBackdropPoints = null;
|
||
}
|
||
|
||
if (satellitePoints) {
|
||
disposeObject3D(satellitePoints);
|
||
satellitePoints = null;
|
||
}
|
||
|
||
if (satelliteTrails) {
|
||
disposeObject3D(satelliteTrails);
|
||
satelliteTrails = null;
|
||
}
|
||
|
||
satellitePositions = [];
|
||
satelliteCapacity = 0;
|
||
satelliteSatrecCache = new Map();
|
||
showSatellites = false;
|
||
showTrails = true;
|
||
}
|