Stabilize Earth module and fix satellite TLE handling

This commit is contained in:
linkong
2026-03-26 10:29:50 +08:00
parent 3fd6cbb6f7
commit ce5feba3b9
14 changed files with 2132 additions and 1069 deletions

View File

@@ -1,329 +1,398 @@
// cables.js - Cable loading and rendering module
import * as THREE from 'three';
import * as THREE from "three";
import { CONFIG, CABLE_COLORS, PATHS, CABLE_STATE } from './constants.js';
import { latLonToVector3 } from './utils.js';
import { updateEarthStats, showStatusMessage } from './ui.js';
import { showInfoCard } from './info-card.js';
import { CONFIG, CABLE_COLORS, PATHS, CABLE_STATE } from "./constants.js";
import { latLonToVector3 } from "./utils.js";
import { updateEarthStats, showStatusMessage } from "./ui.js";
import { showInfoCard } from "./info-card.js";
export let cableLines = [];
export let landingPoints = [];
export let lockedCable = null;
let cableIdMap = new Map();
let cableStates = new Map();
let cablesVisible = true;
function disposeMaterial(material) {
if (!material) return;
if (Array.isArray(material)) {
material.forEach(disposeMaterial);
return;
}
if (material.map) {
material.map.dispose();
}
material.dispose();
}
function disposeObject(object, parent) {
if (!object) return;
const owner = parent || object.parent;
if (owner) {
owner.remove(object);
}
if (object.geometry) {
object.geometry.dispose();
}
if (object.material) {
disposeMaterial(object.material);
}
}
function getCableColor(properties) {
if (properties.color) {
if (typeof properties.color === 'string' && properties.color.startsWith('#')) {
if (
typeof properties.color === "string" &&
properties.color.startsWith("#")
) {
return parseInt(properties.color.substring(1), 16);
} else if (typeof properties.color === 'number') {
}
if (typeof properties.color === "number") {
return properties.color;
}
}
const cableName = properties.Name || properties.cableName || properties.shortname || '';
if (cableName.includes('Americas II')) {
return CABLE_COLORS['Americas II'];
} else if (cableName.includes('AU Aleutian A')) {
return CABLE_COLORS['AU Aleutian A'];
} else if (cableName.includes('AU Aleutian B')) {
return CABLE_COLORS['AU Aleutian B'];
const cableName =
properties.Name || properties.cableName || properties.shortname || "";
if (cableName.includes("Americas II")) {
return CABLE_COLORS["Americas II"];
}
if (cableName.includes("AU Aleutian A")) {
return CABLE_COLORS["AU Aleutian A"];
}
if (cableName.includes("AU Aleutian B")) {
return CABLE_COLORS["AU Aleutian B"];
}
return CABLE_COLORS.default;
}
function createCableLine(points, color, properties, earthObj) {
function createCableLine(points, color, properties) {
if (points.length < 2) return null;
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const lineMaterial = new THREE.LineBasicMaterial({
color: color,
lineGeometry.computeBoundingSphere();
const lineMaterial = new THREE.LineBasicMaterial({
color,
linewidth: 1,
transparent: true,
opacity: 1.0,
depthTest: true,
depthWrite: true
depthWrite: true,
});
const cableLine = new THREE.Line(lineGeometry, lineMaterial);
const cableId = properties.cable_id || properties.id || properties.Name || Math.random().toString(36);
const cableId =
properties.cable_id ||
properties.id ||
properties.Name ||
Math.random().toString(36);
cableLine.userData = {
type: 'cable',
cableId: cableId,
name: properties.Name || properties.cableName || 'Unknown',
owner: properties.owner || properties.owners || '-',
status: properties.status || '-',
length: properties.length || '-',
coords: '-',
rfs: properties.rfs || '-',
originalColor: color
type: "cable",
cableId,
name: properties.Name || properties.cableName || "Unknown",
owner: properties.owner || properties.owners || "-",
status: properties.status || "-",
length: properties.length || "-",
coords: "-",
rfs: properties.rfs || "-",
originalColor: color,
localCenter:
lineGeometry.boundingSphere?.center?.clone() || new THREE.Vector3(),
};
cableLine.renderOrder = 1;
if (!cableIdMap.has(cableId)) {
cableIdMap.set(cableId, []);
}
cableIdMap.get(cableId).push(cableLine);
return cableLine;
}
function calculateGreatCirclePoints(lat1, lon1, lat2, lon2, radius, segments = 50) {
function calculateGreatCirclePoints(
lat1,
lon1,
lat2,
lon2,
radius,
segments = 50,
) {
const points = [];
const phi1 = lat1 * Math.PI / 180;
const lambda1 = lon1 * Math.PI / 180;
const phi2 = lat2 * Math.PI / 180;
const lambda2 = lon2 * Math.PI / 180;
const dLambda = Math.min(Math.abs(lambda2 - lambda1), 2 * Math.PI - Math.abs(lambda2 - lambda1));
const cosDelta = Math.sin(phi1) * Math.sin(phi2) + Math.cos(phi1) * Math.cos(phi2) * Math.cos(dLambda);
const phi1 = (lat1 * Math.PI) / 180;
const lambda1 = (lon1 * Math.PI) / 180;
const phi2 = (lat2 * Math.PI) / 180;
const lambda2 = (lon2 * Math.PI) / 180;
const dLambda = Math.min(
Math.abs(lambda2 - lambda1),
2 * Math.PI - Math.abs(lambda2 - lambda1),
);
const cosDelta =
Math.sin(phi1) * Math.sin(phi2) +
Math.cos(phi1) * Math.cos(phi2) * Math.cos(dLambda);
let delta = Math.acos(Math.max(-1, Math.min(1, cosDelta)));
if (delta < 0.01) {
const p1 = latLonToVector3(lat1, lon1, radius);
const p2 = latLonToVector3(lat2, lon2, radius);
return [p1, p2];
}
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const sinDelta = Math.sin(delta);
const A = Math.sin((1 - t) * delta) / sinDelta;
const B = Math.sin(t * delta) / sinDelta;
const x1 = Math.cos(phi1) * Math.cos(lambda1);
const y1 = Math.cos(phi1) * Math.sin(lambda1);
const z1 = Math.sin(phi1);
const x2 = Math.cos(phi2) * Math.cos(lambda2);
const y2 = Math.cos(phi2) * Math.sin(lambda2);
const z2 = Math.sin(phi2);
let x = A * x1 + B * x2;
let y = A * y1 + B * y2;
let z = A * z1 + B * z2;
const norm = Math.sqrt(x*x + y*y + z*z);
x = x / norm * radius;
y = y / norm * radius;
z = z / norm * radius;
const lat = Math.asin(z / radius) * 180 / Math.PI;
let lon = Math.atan2(y, x) * 180 / Math.PI;
const norm = Math.sqrt(x * x + y * y + z * z);
x = (x / norm) * radius;
y = (y / norm) * radius;
z = (z / norm) * radius;
const lat = (Math.asin(z / radius) * 180) / Math.PI;
let lon = (Math.atan2(y, x) * 180) / Math.PI;
if (lon > 180) lon -= 360;
if (lon < -180) lon += 360;
const point = latLonToVector3(lat, lon, radius);
points.push(point);
points.push(latLonToVector3(lat, lon, radius));
}
return points;
}
export function clearCableLines(earthObj = null) {
cableLines.forEach((line) => disposeObject(line, earthObj));
cableLines = [];
cableIdMap = new Map();
cableStates.clear();
}
export function clearLandingPoints(earthObj = null) {
landingPoints.forEach((point) => disposeObject(point, earthObj));
landingPoints = [];
}
export function clearCableData(earthObj = null) {
clearCableSelection();
clearCableLines(earthObj);
clearLandingPoints(earthObj);
}
export async function loadGeoJSONFromPath(scene, earthObj) {
try {
console.log('正在加载电缆数据...');
showStatusMessage('正在加载电缆数据...', 'warning');
const response = await fetch(PATHS.cablesApi);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
cableLines.forEach(line => earthObj.remove(line));
cableLines = [];
if (!data.features || !Array.isArray(data.features)) {
throw new Error('无效的GeoJSON格式');
}
const cableCount = data.features.length;
document.getElementById('cable-count').textContent = cableCount + '个';
const inServiceCount = data.features.filter(
feature => feature.properties && feature.properties.status === 'In Service'
).length;
const statusEl = document.getElementById('cable-status-summary');
if (statusEl) {
statusEl.textContent = `${inServiceCount}/${cableCount} 运行中`;
}
for (const feature of data.features) {
const geometry = feature.geometry;
const properties = feature.properties || {};
if (!geometry || !geometry.coordinates) continue;
const color = getCableColor(properties);
console.log('电缆 properties:', JSON.stringify(properties));
if (geometry.type === 'MultiLineString') {
for (const lineCoords of geometry.coordinates) {
if (!lineCoords || lineCoords.length < 2) continue;
const points = [];
for (let i = 0; i < lineCoords.length - 1; i++) {
const lon1 = lineCoords[i][0];
const lat1 = lineCoords[i][1];
const lon2 = lineCoords[i + 1][0];
const lat2 = lineCoords[i + 1][1];
const segment = calculateGreatCirclePoints(lat1, lon1, lat2, lon2, 100.2, 50);
if (i === 0) {
points.push(...segment);
} else {
points.push(...segment.slice(1));
}
}
if (points.length >= 2) {
const line = createCableLine(points, color, properties, earthObj);
if (line) {
cableLines.push(line);
earthObj.add(line);
console.log('添加线缆成功');
}
}
}
} else if (geometry.type === 'LineString') {
const allCoords = geometry.coordinates;
console.log("正在加载电缆数据...");
showStatusMessage("正在加载电缆数据...", "warning");
const response = await fetch(PATHS.cablesApi);
if (!response.ok) {
throw new Error(`电缆接口返回 HTTP ${response.status}`);
}
const data = await response.json();
if (!data.features || !Array.isArray(data.features)) {
throw new Error("无效的电缆 GeoJSON 格式");
}
clearCableLines(earthObj);
for (const feature of data.features) {
const geometry = feature.geometry;
const properties = feature.properties || {};
if (!geometry || !geometry.coordinates) continue;
const color = getCableColor(properties);
if (geometry.type === "MultiLineString") {
for (const lineCoords of geometry.coordinates) {
if (!lineCoords || lineCoords.length < 2) continue;
const points = [];
for (let i = 0; i < allCoords.length - 1; i++) {
const lon1 = allCoords[i][0];
const lat1 = allCoords[i][1];
const lon2 = allCoords[i + 1][0];
const lat2 = allCoords[i + 1][1];
const segment = calculateGreatCirclePoints(lat1, lon1, lat2, lon2, 100.2, 50);
if (i === 0) {
points.push(...segment);
} else {
points.push(...segment.slice(1));
}
for (let i = 0; i < lineCoords.length - 1; i++) {
const lon1 = lineCoords[i][0];
const lat1 = lineCoords[i][1];
const lon2 = lineCoords[i + 1][0];
const lat2 = lineCoords[i + 1][1];
const segment = calculateGreatCirclePoints(
lat1,
lon1,
lat2,
lon2,
CONFIG.earthRadius + 0.2,
50,
);
points.push(...(i === 0 ? segment : segment.slice(1)));
}
if (points.length >= 2) {
const line = createCableLine(points, color, properties, earthObj);
if (line) {
cableLines.push(line);
earthObj.add(line);
}
const line = createCableLine(points, color, properties);
if (line) {
cableLines.push(line);
earthObj.add(line);
}
}
} else if (geometry.type === "LineString") {
const points = [];
for (let i = 0; i < geometry.coordinates.length - 1; i++) {
const lon1 = geometry.coordinates[i][0];
const lat1 = geometry.coordinates[i][1];
const lon2 = geometry.coordinates[i + 1][0];
const lat2 = geometry.coordinates[i + 1][1];
const segment = calculateGreatCirclePoints(
lat1,
lon1,
lat2,
lon2,
CONFIG.earthRadius + 0.2,
50,
);
points.push(...(i === 0 ? segment : segment.slice(1)));
}
const line = createCableLine(points, color, properties);
if (line) {
cableLines.push(line);
earthObj.add(line);
}
}
updateEarthStats({
cableCount: cableLines.length,
landingPointCount: landingPoints.length,
terrainOn: false,
textureQuality: '8K 卫星图'
});
showStatusMessage(`成功加载 ${cableLines.length} 条电缆`, 'success');
document.getElementById('loading').style.display = 'none';
} catch (error) {
console.error('加载电缆数据失败:', error);
showStatusMessage('加载电缆数据失败: ' + error.message, 'error');
}
const cableCount = data.features.length;
const inServiceCount = data.features.filter(
(feature) =>
feature.properties && feature.properties.status === "In Service",
).length;
const cableCountEl = document.getElementById("cable-count");
const statusEl = document.getElementById("cable-status-summary");
if (cableCountEl) cableCountEl.textContent = cableCount + "个";
if (statusEl) statusEl.textContent = `${inServiceCount}/${cableCount} 运行中`;
updateEarthStats({
cableCount: cableLines.length,
landingPointCount: landingPoints.length,
terrainOn: false,
textureQuality: "8K 卫星图",
});
showStatusMessage(`成功加载 ${cableLines.length} 条电缆`, "success");
return cableLines.length;
}
export async function loadLandingPoints(scene, earthObj) {
console.log("正在加载登陆点数据...");
const response = await fetch(PATHS.landingPointsApi);
if (!response.ok) {
throw new Error(`登陆点接口返回 HTTP ${response.status}`);
}
const data = await response.json();
if (!data.features || !Array.isArray(data.features)) {
throw new Error("无效的登陆点 GeoJSON 格式");
}
clearLandingPoints(earthObj);
const sphereGeometry = new THREE.SphereGeometry(0.4, 16, 16);
let validCount = 0;
try {
console.log('正在加载登陆点数据...');
const response = await fetch(PATHS.landingPointsApi);
if (!response.ok) {
console.error('HTTP错误:', response.status);
return;
}
const data = await response.json();
if (!data.features || !Array.isArray(data.features)) {
console.error('无效的GeoJSON格式');
return;
}
landingPoints = [];
let validCount = 0;
const sphereGeometry = new THREE.SphereGeometry(0.4, 16, 16);
const sphereMaterial = new THREE.MeshStandardMaterial({
color: 0xffaa00,
emissive: 0x442200,
emissiveIntensity: 0.5
});
for (const feature of data.features) {
if (!feature.geometry || !feature.geometry.coordinates) continue;
const [lon, lat] = feature.geometry.coordinates;
const properties = feature.properties || {};
if (typeof lon !== 'number' || typeof lat !== 'number' ||
isNaN(lon) || isNaN(lat) ||
Math.abs(lat) > 90 || Math.abs(lon) > 180) {
if (
typeof lon !== "number" ||
typeof lat !== "number" ||
Number.isNaN(lon) ||
Number.isNaN(lat) ||
Math.abs(lat) > 90 ||
Math.abs(lon) > 180
) {
continue;
}
const position = latLonToVector3(lat, lon, 100.1);
if (isNaN(position.x) || isNaN(position.y) || isNaN(position.z)) {
const position = latLonToVector3(lat, lon, CONFIG.earthRadius + 0.1);
if (
Number.isNaN(position.x) ||
Number.isNaN(position.y) ||
Number.isNaN(position.z)
) {
continue;
}
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial.clone());
const sphere = new THREE.Mesh(
sphereGeometry.clone(),
new THREE.MeshStandardMaterial({
color: 0xffaa00,
emissive: 0x442200,
emissiveIntensity: 0.5,
transparent: true,
opacity: 1,
}),
);
sphere.position.copy(position);
sphere.userData = {
type: 'landingPoint',
name: properties.name || '未知登陆站',
type: "landingPoint",
name: properties.name || "未知登陆站",
cableNames: properties.cable_names || [],
country: properties.country || '未知国家',
status: properties.status || 'Unknown'
country: properties.country || "未知国家",
status: properties.status || "Unknown",
};
earthObj.add(sphere);
landingPoints.push(sphere);
validCount++;
}
console.log(`成功创建 ${validCount} 个登陆点标记`);
showStatusMessage(`成功加载 ${validCount} 个登陆点`, 'success');
const lpCountEl = document.getElementById('landing-point-count');
if (lpCountEl) {
lpCountEl.textContent = validCount + '个';
}
} catch (error) {
console.error('加载登陆点数据失败:', error);
} finally {
sphereGeometry.dispose();
}
const landingPointCountEl = document.getElementById("landing-point-count");
if (landingPointCountEl) {
landingPointCountEl.textContent = validCount + "个";
}
showStatusMessage(`成功加载 ${validCount} 个登陆点`, "success");
return validCount;
}
export function handleCableClick(cable) {
lockedCable = cable;
const data = cable.userData;
showInfoCard('cable', {
showInfoCard("cable", {
name: data.name,
owner: data.owner,
status: data.status,
length: data.length,
coords: data.coords,
rfs: data.rfs
rfs: data.rfs,
});
showStatusMessage(`已锁定: ${data.name}`, 'info');
showStatusMessage(`已锁定: ${data.name}`, "info");
}
export function clearCableSelection() {
@@ -342,8 +411,6 @@ export function getLandingPoints() {
return landingPoints;
}
const cableStates = new Map();
export function getCableState(cableId) {
return cableStates.get(cableId) || CABLE_STATE.NORMAL;
}
@@ -365,7 +432,9 @@ export function getCableStateInfo() {
}
export function getLandingPointsByCableName(cableName) {
return landingPoints.filter(lp => lp.userData.cableNames?.includes(cableName));
return landingPoints.filter((lp) =>
lp.userData.cableNames?.includes(cableName),
);
}
export function getAllLandingPoints() {
@@ -375,10 +444,11 @@ export function getAllLandingPoints() {
export function applyLandingPointVisualState(lockedCableName, dimAll = false) {
const pulse = (Math.sin(Date.now() * 0.003) + 1) * 0.5;
const brightness = 0.3;
landingPoints.forEach(lp => {
const isRelated = !dimAll && lp.userData.cableNames?.includes(lockedCableName);
landingPoints.forEach((lp) => {
const isRelated =
!dimAll && lp.userData.cableNames?.includes(lockedCableName);
if (isRelated) {
lp.material.color.setHex(0xffaa00);
lp.material.emissive.setHex(0x442200);
@@ -388,8 +458,7 @@ export function applyLandingPointVisualState(lockedCableName, dimAll = false) {
} else {
const r = 255 * brightness;
const g = 170 * brightness;
const b = 0 * brightness;
lp.material.color.setRGB(r / 255, g / 255, b / 255);
lp.material.color.setRGB(r / 255, g / 255, 0);
lp.material.emissive.setHex(0x000000);
lp.material.emissiveIntensity = 0;
lp.material.opacity = 0.3;
@@ -399,7 +468,7 @@ export function applyLandingPointVisualState(lockedCableName, dimAll = false) {
}
export function resetLandingPointVisualState() {
landingPoints.forEach(lp => {
landingPoints.forEach((lp) => {
lp.material.color.setHex(0xffaa00);
lp.material.emissive.setHex(0x442200);
lp.material.emissiveIntensity = 0.5;
@@ -410,10 +479,10 @@ export function resetLandingPointVisualState() {
export function toggleCables(show) {
cablesVisible = show;
cableLines.forEach(cable => {
cableLines.forEach((cable) => {
cable.visible = cablesVisible;
});
landingPoints.forEach(lp => {
landingPoints.forEach((lp) => {
lp.visible = cablesVisible;
});
}

View File

@@ -54,7 +54,7 @@ export const CABLE_STATE = {
};
export const SATELLITE_CONFIG = {
maxCount: 5000,
maxCount: 10000,
trailLength: 10,
dotSize: 4,
ringSize: 0.07,

View File

@@ -1,11 +1,16 @@
// controls.js - Zoom, rotate and toggle controls
import { CONFIG, EARTH_CONFIG } from './constants.js';
import { updateZoomDisplay, showStatusMessage } from './ui.js';
import { toggleTerrain } from './earth.js';
import { reloadData, clearLockedObject } from './main.js';
import { toggleSatellites, toggleTrails, getShowSatellites, getSatelliteCount } from './satellites.js';
import { toggleCables, getShowCables } from './cables.js';
import { CONFIG, EARTH_CONFIG } from "./constants.js";
import { updateZoomDisplay, showStatusMessage } from "./ui.js";
import { toggleTerrain } from "./earth.js";
import { reloadData, clearLockedObject } from "./main.js";
import {
toggleSatellites,
toggleTrails,
getShowSatellites,
getSatelliteCount,
} from "./satellites.js";
import { toggleCables, getShowCables } from "./cables.js";
export let autoRotate = true;
export let zoomLevel = 1.0;
@@ -13,8 +18,26 @@ export let showTerrain = false;
export let isDragging = false;
let earthObj = null;
let listeners = [];
let cleanupFns = [];
function bindListener(element, eventName, handler, options) {
if (!element) return;
element.addEventListener(eventName, handler, options);
listeners.push(() =>
element.removeEventListener(eventName, handler, options),
);
}
function resetCleanup() {
cleanupFns.forEach((cleanup) => cleanup());
cleanupFns = [];
listeners.forEach((cleanup) => cleanup());
listeners = [];
}
export function setupControls(camera, renderer, scene, earth) {
resetCleanup();
earthObj = earth;
setupZoomControls(camera);
setupWheelZoom(camera, renderer);
@@ -29,39 +52,40 @@ function setupZoomControls(camera) {
const HOLD_THRESHOLD = 150;
const LONG_PRESS_TICK = 50;
const CLICK_STEP = 10;
const MIN_PERCENT = CONFIG.minZoom * 100;
const MAX_PERCENT = CONFIG.maxZoom * 100;
function doZoomStep(direction) {
let currentPercent = Math.round(zoomLevel * 100);
let newPercent = direction > 0 ? currentPercent + CLICK_STEP : currentPercent - CLICK_STEP;
let newPercent =
direction > 0 ? currentPercent + CLICK_STEP : currentPercent - CLICK_STEP;
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
zoomLevel = newPercent / 100;
applyZoom(camera);
}
function doContinuousZoom(direction) {
let currentPercent = Math.round(zoomLevel * 100);
let newPercent = direction > 0 ? currentPercent + 1 : currentPercent - 1;
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
zoomLevel = newPercent / 100;
applyZoom(camera);
}
function startContinuousZoom(direction) {
doContinuousZoom(direction);
zoomInterval = setInterval(() => {
zoomInterval = window.setInterval(() => {
doContinuousZoom(direction);
}, LONG_PRESS_TICK);
}
function stopZoom() {
if (zoomInterval) {
clearInterval(zoomInterval);
@@ -72,15 +96,15 @@ function setupZoomControls(camera) {
holdTimeout = null;
}
}
function handleMouseDown(direction) {
startTime = Date.now();
stopZoom();
holdTimeout = setTimeout(() => {
holdTimeout = window.setTimeout(() => {
startContinuousZoom(direction);
}, HOLD_THRESHOLD);
}
function handleMouseUp(direction) {
const heldTime = Date.now() - startTime;
stopZoom();
@@ -88,48 +112,72 @@ function setupZoomControls(camera) {
doZoomStep(direction);
}
}
document.getElementById('zoom-in').addEventListener('mousedown', () => handleMouseDown(1));
document.getElementById('zoom-in').addEventListener('mouseup', () => handleMouseUp(1));
document.getElementById('zoom-in').addEventListener('mouseleave', stopZoom);
document.getElementById('zoom-in').addEventListener('touchstart', (e) => { e.preventDefault(); handleMouseDown(1); });
document.getElementById('zoom-in').addEventListener('touchend', () => handleMouseUp(1));
document.getElementById('zoom-out').addEventListener('mousedown', () => handleMouseDown(-1));
document.getElementById('zoom-out').addEventListener('mouseup', () => handleMouseUp(-1));
document.getElementById('zoom-out').addEventListener('mouseleave', stopZoom);
document.getElementById('zoom-out').addEventListener('touchstart', (e) => { e.preventDefault(); handleMouseDown(-1); });
document.getElementById('zoom-out').addEventListener('touchend', () => handleMouseUp(-1));
document.getElementById('zoom-value').addEventListener('click', function() {
cleanupFns.push(stopZoom);
const zoomIn = document.getElementById("zoom-in");
const zoomOut = document.getElementById("zoom-out");
const zoomValue = document.getElementById("zoom-value");
bindListener(zoomIn, "mousedown", () => handleMouseDown(1));
bindListener(zoomIn, "mouseup", () => handleMouseUp(1));
bindListener(zoomIn, "mouseleave", stopZoom);
bindListener(zoomIn, "touchstart", (e) => {
e.preventDefault();
handleMouseDown(1);
});
bindListener(zoomIn, "touchend", () => handleMouseUp(1));
bindListener(zoomOut, "mousedown", () => handleMouseDown(-1));
bindListener(zoomOut, "mouseup", () => handleMouseUp(-1));
bindListener(zoomOut, "mouseleave", stopZoom);
bindListener(zoomOut, "touchstart", (e) => {
e.preventDefault();
handleMouseDown(-1);
});
bindListener(zoomOut, "touchend", () => handleMouseUp(-1));
bindListener(zoomValue, "click", () => {
const startZoomVal = zoomLevel;
const targetZoom = 1.0;
const startDistance = CONFIG.defaultCameraZ / startZoomVal;
const targetDistance = CONFIG.defaultCameraZ / targetZoom;
animateValue(0, 1, 600, (progress) => {
const ease = 1 - Math.pow(1 - progress, 3);
zoomLevel = startZoomVal + (targetZoom - startZoomVal) * ease;
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
const distance = startDistance + (targetDistance - startDistance) * ease;
updateZoomDisplay(zoomLevel, distance.toFixed(0));
}, () => {
zoomLevel = 1.0;
showStatusMessage('缩放已重置到100%', 'info');
});
animateValue(
0,
1,
600,
(progress) => {
const ease = 1 - Math.pow(1 - progress, 3);
zoomLevel = startZoomVal + (targetZoom - startZoomVal) * ease;
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
const distance =
startDistance + (targetDistance - startDistance) * ease;
updateZoomDisplay(zoomLevel, distance.toFixed(0));
},
() => {
zoomLevel = 1.0;
showStatusMessage("缩放已重置到100%", "info");
},
);
});
}
function setupWheelZoom(camera, renderer) {
renderer.domElement.addEventListener('wheel', (e) => {
e.preventDefault();
if (e.deltaY < 0) {
zoomLevel = Math.min(zoomLevel + 0.1, CONFIG.maxZoom);
} else {
zoomLevel = Math.max(zoomLevel - 0.1, CONFIG.minZoom);
}
applyZoom(camera);
}, { passive: false });
bindListener(
renderer?.domElement,
"wheel",
(e) => {
e.preventDefault();
if (e.deltaY < 0) {
zoomLevel = Math.min(zoomLevel + 0.1, CONFIG.maxZoom);
} else {
zoomLevel = Math.max(zoomLevel - 0.1, CONFIG.minZoom);
}
applyZoom(camera);
},
{ passive: false },
);
}
function applyZoom(camera) {
@@ -140,149 +188,186 @@ function applyZoom(camera) {
function animateValue(start, end, duration, onUpdate, onComplete) {
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3);
const current = start + (end - start) * easeProgress;
onUpdate(current);
if (progress < 1) {
requestAnimationFrame(update);
} else if (onComplete) {
onComplete();
}
}
requestAnimationFrame(update);
}
export function resetView(camera) {
if (!earthObj) return;
function animateToView(targetLat, targetLon, targetRotLon) {
const latRot = targetLat * Math.PI / 180;
const targetRotX = EARTH_CONFIG.tiltRad + latRot * EARTH_CONFIG.latCoefficient;
const targetRotY = -(targetRotLon * Math.PI / 180);
const latRot = (targetLat * Math.PI) / 180;
const targetRotX =
EARTH_CONFIG.tiltRad + latRot * EARTH_CONFIG.latCoefficient;
const targetRotY = -((targetRotLon * Math.PI) / 180);
const startRotX = earthObj.rotation.x;
const startRotY = earthObj.rotation.y;
const startZoom = zoomLevel;
const targetZoom = 1.0;
animateValue(0, 1, 800, (progress) => {
const ease = 1 - Math.pow(1 - progress, 3);
earthObj.rotation.x = startRotX + (targetRotX - startRotX) * ease;
earthObj.rotation.y = startRotY + (targetRotY - startRotY) * ease;
zoomLevel = startZoom + (targetZoom - startZoom) * ease;
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
}, () => {
zoomLevel = 1.0;
showStatusMessage('视角已重置', 'info');
});
animateValue(
0,
1,
800,
(progress) => {
const ease = 1 - Math.pow(1 - progress, 3);
earthObj.rotation.x = startRotX + (targetRotX - startRotX) * ease;
earthObj.rotation.y = startRotY + (targetRotY - startRotY) * ease;
zoomLevel = startZoom + (targetZoom - startZoom) * ease;
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
},
() => {
zoomLevel = 1.0;
showStatusMessage("视角已重置", "info");
},
);
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) => animateToView(pos.coords.latitude, pos.coords.longitude, -pos.coords.longitude),
() => animateToView(EARTH_CONFIG.chinaLat, EARTH_CONFIG.chinaLon, EARTH_CONFIG.chinaRotLon),
{ timeout: 5000, enableHighAccuracy: false }
(pos) =>
animateToView(
pos.coords.latitude,
pos.coords.longitude,
-pos.coords.longitude,
),
() =>
animateToView(
EARTH_CONFIG.chinaLat,
EARTH_CONFIG.chinaLon,
EARTH_CONFIG.chinaRotLon,
),
{ timeout: 5000, enableHighAccuracy: false },
);
} else {
animateToView(EARTH_CONFIG.chinaLat, EARTH_CONFIG.chinaLon, EARTH_CONFIG.chinaRotLon);
}
if (typeof window.clearLockedCable === 'function') {
window.clearLockedCable();
animateToView(
EARTH_CONFIG.chinaLat,
EARTH_CONFIG.chinaLon,
EARTH_CONFIG.chinaRotLon,
);
}
clearLockedObject();
}
function setupRotateControls(camera, earth) {
const rotateBtn = document.getElementById('rotate-toggle');
rotateBtn.addEventListener('click', function() {
function setupRotateControls(camera) {
const rotateBtn = document.getElementById("rotate-toggle");
const resetViewBtn = document.getElementById("reset-view");
bindListener(rotateBtn, "click", () => {
const isRotating = toggleAutoRotate();
showStatusMessage(isRotating ? '自动旋转已开启' : '自动旋转已暂停', 'info');
showStatusMessage(isRotating ? "自动旋转已开启" : "自动旋转已暂停", "info");
});
updateRotateUI();
document.getElementById('reset-view').addEventListener('click', function() {
bindListener(resetViewBtn, "click", () => {
resetView(camera);
});
}
function setupTerrainControls() {
document.getElementById('toggle-terrain').addEventListener('click', function() {
const terrainBtn = document.getElementById("toggle-terrain");
const satellitesBtn = document.getElementById("toggle-satellites");
const trailsBtn = document.getElementById("toggle-trails");
const cablesBtn = document.getElementById("toggle-cables");
const reloadBtn = document.getElementById("reload-data");
const toolbarToggle = document.getElementById("toolbar-toggle");
const toolbar = document.getElementById("control-toolbar");
bindListener(terrainBtn, "click", function () {
showTerrain = !showTerrain;
toggleTerrain(showTerrain);
this.classList.toggle('active', showTerrain);
this.querySelector('.tooltip').textContent = showTerrain ? '隐藏地形' : '显示地形';
document.getElementById('terrain-status').textContent = showTerrain ? '开启' : '关闭';
showStatusMessage(showTerrain ? '地形已显示' : '地形已隐藏', 'info');
this.classList.toggle("active", showTerrain);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = showTerrain ? "隐藏地形" : "显示地形";
const terrainStatus = document.getElementById("terrain-status");
if (terrainStatus)
terrainStatus.textContent = showTerrain ? "开启" : "关闭";
showStatusMessage(showTerrain ? "地形已显示" : "地形已隐藏", "info");
});
document.getElementById('toggle-satellites').addEventListener('click', function() {
bindListener(satellitesBtn, "click", function () {
const showSats = !getShowSatellites();
if (!showSats) {
clearLockedObject();
}
toggleSatellites(showSats);
this.classList.toggle('active', showSats);
this.querySelector('.tooltip').textContent = showSats ? '隐藏卫星' : '显示卫星';
document.getElementById('satellite-count').textContent = getSatelliteCount() + ' 颗';
showStatusMessage(showSats ? '卫星已显示' : '卫星已隐藏', 'info');
this.classList.toggle("active", showSats);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = showSats ? "隐藏卫星" : "显示卫星";
const satelliteCountEl = document.getElementById("satellite-count");
if (satelliteCountEl)
satelliteCountEl.textContent = getSatelliteCount() + " 颗";
showStatusMessage(showSats ? "卫星已显示" : "卫星已隐藏", "info");
});
document.getElementById('toggle-trails').addEventListener('click', function() {
const isActive = this.classList.contains('active');
const showTrails = !isActive;
toggleTrails(showTrails);
this.classList.toggle('active', showTrails);
this.querySelector('.tooltip').textContent = showTrails ? '隐藏轨迹' : '显示轨迹';
showStatusMessage(showTrails ? '轨迹已显示' : '轨迹已隐藏', 'info');
bindListener(trailsBtn, "click", function () {
const isActive = this.classList.contains("active");
const nextShowTrails = !isActive;
toggleTrails(nextShowTrails);
this.classList.toggle("active", nextShowTrails);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = nextShowTrails ? "隐藏轨迹" : "显示轨迹";
showStatusMessage(nextShowTrails ? "轨迹已显示" : "轨迹已隐藏", "info");
});
document.getElementById('toggle-cables').addEventListener('click', function() {
const showCables = !getShowCables();
if (!showCables) {
bindListener(cablesBtn, "click", function () {
const showNextCables = !getShowCables();
if (!showNextCables) {
clearLockedObject();
}
toggleCables(showCables);
this.classList.toggle('active', showCables);
this.querySelector('.tooltip').textContent = showCables ? '隐藏线缆' : '显示线缆';
showStatusMessage(showCables ? '线缆已显示' : '线缆已隐藏', 'info');
toggleCables(showNextCables);
this.classList.toggle("active", showNextCables);
const tooltip = this.querySelector(".tooltip");
if (tooltip) tooltip.textContent = showNextCables ? "隐藏线缆" : "显示线缆";
showStatusMessage(showNextCables ? "线缆已显示" : "线缆已隐藏", "info");
});
document.getElementById('reload-data').addEventListener('click', async () => {
bindListener(reloadBtn, "click", async () => {
await reloadData();
showStatusMessage('数据已重新加载', 'success');
});
const toolbarToggle = document.getElementById('toolbar-toggle');
const toolbar = document.getElementById('control-toolbar');
if (toolbarToggle && toolbar) {
toolbarToggle.addEventListener('click', () => {
toolbar.classList.toggle('collapsed');
bindListener(toolbarToggle, "click", () => {
toolbar.classList.toggle("collapsed");
});
}
}
export function teardownControls() {
resetCleanup();
}
export function getAutoRotate() {
return autoRotate;
}
function updateRotateUI() {
const btn = document.getElementById('rotate-toggle');
const btn = document.getElementById("rotate-toggle");
if (btn) {
btn.classList.toggle('active', autoRotate);
btn.innerHTML = autoRotate ? '⏸️' : '▶️';
const tooltip = btn.querySelector('.tooltip');
if (tooltip) tooltip.textContent = autoRotate ? '暂停旋转' : '开始旋转';
btn.classList.toggle("active", autoRotate);
btn.innerHTML = autoRotate ? "⏸️" : "▶️";
const tooltip = btn.querySelector(".tooltip");
if (tooltip) tooltip.textContent = autoRotate ? "暂停旋转" : "开始旋转";
}
}
@@ -294,9 +379,7 @@ export function setAutoRotate(value) {
export function toggleAutoRotate() {
autoRotate = !autoRotate;
updateRotateUI();
if (window.clearLockedCable) {
window.clearLockedCable();
}
clearLockedObject();
return autoRotate;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +1,125 @@
// ui.js - UI update functions
let statusTimeoutId = null;
// Show status message
export function showStatusMessage(message, type = 'info') {
const statusEl = document.getElementById('status-message');
export function showStatusMessage(message, type = "info") {
const statusEl = document.getElementById("status-message");
if (!statusEl) return;
if (statusTimeoutId) {
clearTimeout(statusTimeoutId);
statusTimeoutId = null;
}
statusEl.textContent = message;
statusEl.className = `status-message ${type}`;
statusEl.style.display = 'block';
setTimeout(() => {
statusEl.style.display = 'none';
statusEl.style.display = "block";
statusTimeoutId = setTimeout(() => {
statusEl.style.display = "none";
statusEl.textContent = "";
statusTimeoutId = null;
}, 3000);
}
// Update coordinates display
export function updateCoordinatesDisplay(lat, lon, alt = 0) {
document.getElementById('longitude-value').textContent = lon.toFixed(2) + '°';
document.getElementById('latitude-value').textContent = lat.toFixed(2) + '°';
document.getElementById('mouse-coords').textContent =
`鼠标: ${lat.toFixed(2)}°, ${lon.toFixed(2)}°`;
const longitudeEl = document.getElementById("longitude-value");
const latitudeEl = document.getElementById("latitude-value");
const mouseCoordsEl = document.getElementById("mouse-coords");
if (longitudeEl) longitudeEl.textContent = lon.toFixed(2) + "°";
if (latitudeEl) latitudeEl.textContent = lat.toFixed(2) + "°";
if (mouseCoordsEl) {
mouseCoordsEl.textContent = `鼠标: ${lat.toFixed(2)}°, ${lon.toFixed(2)}°`;
}
}
// Update zoom display
export function updateZoomDisplay(zoomLevel, distance) {
const percent = Math.round(zoomLevel * 100);
document.getElementById('zoom-value').textContent = percent + '%';
document.getElementById('zoom-level').textContent = '缩放: ' + percent + '%';
const slider = document.getElementById('zoom-slider');
const zoomValueEl = document.getElementById("zoom-value");
const zoomLevelEl = document.getElementById("zoom-level");
const slider = document.getElementById("zoom-slider");
const cameraDistanceEl = document.getElementById("camera-distance");
if (zoomValueEl) zoomValueEl.textContent = percent + "%";
if (zoomLevelEl) zoomLevelEl.textContent = "缩放: " + percent + "%";
if (slider) slider.value = zoomLevel;
document.getElementById('camera-distance').textContent = distance + ' km';
if (cameraDistanceEl) cameraDistanceEl.textContent = distance + " km";
}
// Update earth stats
export function updateEarthStats(stats) {
document.getElementById('cable-count').textContent = stats.cableCount || 0;
document.getElementById('landing-point-count').textContent = stats.landingPointCount || 0;
document.getElementById('terrain-status').textContent = stats.terrainOn ? '开启' : '关闭';
document.getElementById('texture-quality').textContent = stats.textureQuality || '8K 卫星图';
const cableCountEl = document.getElementById("cable-count");
const landingPointCountEl = document.getElementById("landing-point-count");
const terrainStatusEl = document.getElementById("terrain-status");
const textureQualityEl = document.getElementById("texture-quality");
if (cableCountEl) cableCountEl.textContent = stats.cableCount || 0;
if (landingPointCountEl)
landingPointCountEl.textContent = stats.landingPointCount || 0;
if (terrainStatusEl)
terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭";
if (textureQualityEl)
textureQualityEl.textContent = stats.textureQuality || "8K 卫星图";
}
// Show/hide loading
export function setLoading(loading) {
const loadingEl = document.getElementById('loading');
loadingEl.style.display = loading ? 'block' : 'none';
const loadingEl = document.getElementById("loading");
if (!loadingEl) return;
loadingEl.style.display = loading ? "block" : "none";
}
// Show tooltip
export function showTooltip(x, y, content) {
const tooltip = document.getElementById('tooltip');
const tooltip = document.getElementById("tooltip");
if (!tooltip) return;
tooltip.innerHTML = content;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
tooltip.style.display = 'block';
tooltip.style.left = x + "px";
tooltip.style.top = y + "px";
tooltip.style.display = "block";
}
// Hide tooltip
export function hideTooltip() {
document.getElementById('tooltip').style.display = 'none';
const tooltip = document.getElementById("tooltip");
if (tooltip) {
tooltip.style.display = "none";
}
}
// Show error message
export function showError(message) {
const errorEl = document.getElementById('error-message');
const errorEl = document.getElementById("error-message");
if (!errorEl) return;
errorEl.textContent = message;
errorEl.style.display = 'block';
errorEl.style.display = "block";
}
// Hide error message
export function hideError() {
document.getElementById('error-message').style.display = 'none';
const errorEl = document.getElementById("error-message");
if (errorEl) {
errorEl.style.display = "none";
errorEl.textContent = "";
}
}
export function clearUiState() {
if (statusTimeoutId) {
clearTimeout(statusTimeoutId);
statusTimeoutId = null;
}
const statusEl = document.getElementById("status-message");
if (statusEl) {
statusEl.style.display = "none";
statusEl.textContent = "";
}
hideTooltip();
hideError();
}

View File

@@ -1,8 +1,8 @@
// utils.js - Utility functions for coordinate conversion
import * as THREE from 'three';
import * as THREE from "three";
import { CONFIG } from './constants.js';
import { CONFIG } from "./constants.js";
// Convert latitude/longitude to 3D vector
export function latLonToVector3(lat, lon, radius = CONFIG.earthRadius) {
@@ -18,26 +18,33 @@ export function latLonToVector3(lat, lon, radius = CONFIG.earthRadius) {
// Convert 3D vector to latitude/longitude
export function vector3ToLatLon(vector) {
const radius = Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
const lat = 90 - (Math.acos(vector.y / radius) * 180 / Math.PI);
let lon = (Math.atan2(vector.z, -vector.x) * 180 / Math.PI) - 180;
const radius = Math.sqrt(
vector.x * vector.x + vector.y * vector.y + vector.z * vector.z,
);
const lat = 90 - (Math.acos(vector.y / radius) * 180) / Math.PI;
let lon = (Math.atan2(vector.z, -vector.x) * 180) / Math.PI - 180;
while (lon <= -180) lon += 360;
while (lon > 180) lon -= 360;
return {
lat: parseFloat(lat.toFixed(4)),
lon: parseFloat(lon.toFixed(4)),
alt: radius - CONFIG.earthRadius
alt: radius - CONFIG.earthRadius,
};
}
// Convert screen coordinates to Earth surface 3D coordinates
export function screenToEarthCoords(clientX, clientY, camera, earth, domElement = document.body) {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
export function screenToEarthCoords(
clientX,
clientY,
camera,
earth,
domElement = document.body,
raycaster = new THREE.Raycaster(),
mouse = new THREE.Vector2(),
) {
if (domElement === document.body) {
mouse.x = (clientX / window.innerWidth) * 2 - 1;
mouse.y = -(clientY / window.innerHeight) * 2 + 1;
@@ -60,17 +67,26 @@ export function screenToEarthCoords(clientX, clientY, camera, earth, domElement
}
// Calculate accurate spherical distance between two points (Haversine formula)
export function calculateDistance(lat1, lon1, lat2, lon2, radius = CONFIG.earthRadius) {
export function calculateDistance(
lat1,
lon1,
lat2,
lon2,
radius = CONFIG.earthRadius,
) {
const toRad = (angle) => (angle * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) *
Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}

View File

@@ -3,14 +3,14 @@ function Earth() {
<iframe
src="/earth/index.html"
style={{
width: '100%',
height: '100%',
border: 'none',
display: 'block',
width: "100%",
height: "100%",
border: "none",
display: "block",
}}
title="3D Earth"
/>
)
);
}
export default Earth
export default Earth;