460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
import * as THREE from 'three';
|
|
import { createNoise3D } from 'simplex-noise';
|
|
|
|
import { CONFIG, CABLE_CONFIG, CABLE_STATE } from './constants.js';
|
|
import { latLonToVector3, vector3ToLatLon, screenToEarthCoords } from './utils.js';
|
|
import {
|
|
showStatusMessage,
|
|
updateCoordinatesDisplay,
|
|
updateZoomDisplay,
|
|
updateEarthStats,
|
|
setLoading,
|
|
showTooltip,
|
|
hideTooltip
|
|
} from './ui.js';
|
|
import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js';
|
|
import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates } from './cables.js';
|
|
import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints } from './satellites.js';
|
|
import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js';
|
|
import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js';
|
|
|
|
export let scene, camera, renderer;
|
|
let simplex;
|
|
let isDragging = false;
|
|
let previousMousePosition = { x: 0, y: 0 };
|
|
let hoveredCable = null;
|
|
let hoveredSatellite = null;
|
|
let cableLockedData = null;
|
|
let lockedSatellite = null;
|
|
let lockedObject = null;
|
|
let lockedObjectType = null;
|
|
let dragStartTime = 0;
|
|
let isLongDrag = false;
|
|
|
|
function clearLockedObject() {
|
|
hoveredCable = null;
|
|
clearAllCableStates();
|
|
lockedObject = null;
|
|
lockedObjectType = null;
|
|
lockedSatellite = null;
|
|
cableLockedData = null;
|
|
}
|
|
|
|
function isSameCable(cable1, cable2) {
|
|
if (!cable1 || !cable2) return false;
|
|
const id1 = cable1.userData?.cableId;
|
|
const id2 = cable2.userData?.cableId;
|
|
if (id1 === undefined || id2 === undefined) return false;
|
|
return id1 === id2;
|
|
}
|
|
|
|
function showCableInfo(cable) {
|
|
showInfoCard('cable', {
|
|
name: cable.userData.name,
|
|
owner: cable.userData.owner,
|
|
status: cable.userData.status,
|
|
length: cable.userData.length,
|
|
coords: cable.userData.coords,
|
|
rfs: cable.userData.rfs
|
|
});
|
|
}
|
|
|
|
function showSatelliteInfo(props) {
|
|
const meanMotion = props?.mean_motion || 0;
|
|
const period = meanMotion > 0 ? (1440 / meanMotion).toFixed(1) : '-';
|
|
const ecc = props?.eccentricity || 0;
|
|
const perigee = (6371 * (1 - ecc)).toFixed(0);
|
|
const apogee = (6371 * (1 + ecc)).toFixed(0);
|
|
|
|
showInfoCard('satellite', {
|
|
name: props?.name || '-',
|
|
norad_id: props?.norad_cat_id,
|
|
inclination: props?.inclination ? props.inclination.toFixed(2) : '-',
|
|
period: period,
|
|
perigee: perigee,
|
|
apogee: apogee
|
|
});
|
|
}
|
|
|
|
function applyCableVisualState() {
|
|
const allCables = getCableLines();
|
|
const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5;
|
|
|
|
allCables.forEach(c => {
|
|
const cableId = c.userData.cableId;
|
|
const state = getCableState(cableId);
|
|
|
|
switch (state) {
|
|
case CABLE_STATE.LOCKED:
|
|
c.material.opacity = CABLE_CONFIG.lockedOpacityMin + pulse * CABLE_CONFIG.pulseCoefficient;
|
|
c.material.color.setRGB(1, 1, 1);
|
|
break;
|
|
case CABLE_STATE.HOVERED:
|
|
c.material.opacity = 1;
|
|
c.material.color.setRGB(1, 1, 1);
|
|
break;
|
|
case CABLE_STATE.NORMAL:
|
|
default:
|
|
if (lockedObjectType === 'cable' && lockedObject) {
|
|
c.material.opacity = CABLE_CONFIG.otherOpacity;
|
|
const origColor = c.userData.originalColor;
|
|
const brightness = CABLE_CONFIG.otherBrightness;
|
|
c.material.color.setRGB(
|
|
((origColor >> 16) & 255) / 255 * brightness,
|
|
((origColor >> 8) & 255) / 255 * brightness,
|
|
(origColor & 255) / 255 * brightness
|
|
);
|
|
} else {
|
|
c.material.opacity = 1;
|
|
c.material.color.setHex(c.userData.originalColor);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener('error', (e) => {
|
|
console.error('全局错误:', e.error);
|
|
showStatusMessage('加载错误: ' + e.error?.message, 'error');
|
|
});
|
|
|
|
window.addEventListener('unhandledrejection', (e) => {
|
|
console.error('未处理的Promise错误:', e.reason);
|
|
});
|
|
|
|
export function init() {
|
|
simplex = createNoise3D();
|
|
|
|
scene = new THREE.Scene();
|
|
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.z = CONFIG.defaultCameraZ;
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setClearColor(0x0a0a1a, 1);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
document.getElementById('container').appendChild(renderer.domElement);
|
|
|
|
addLights();
|
|
initInfoCard();
|
|
const earthObj = createEarth(scene);
|
|
createClouds(scene, earthObj);
|
|
createTerrain(scene, earthObj, simplex);
|
|
createStars(scene);
|
|
createGridLines(scene, earthObj);
|
|
createSatellites(scene, earthObj);
|
|
|
|
setupControls(camera, renderer, scene, earthObj);
|
|
resetView(camera);
|
|
setupEventListeners(camera, renderer);
|
|
|
|
loadData();
|
|
|
|
animate();
|
|
}
|
|
|
|
function addLights() {
|
|
const ambientLight = new THREE.AmbientLight(0x404060);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
directionalLight.position.set(5, 3, 5);
|
|
scene.add(directionalLight);
|
|
|
|
const backLight = new THREE.DirectionalLight(0x446688, 0.3);
|
|
backLight.position.set(-5, 0, -5);
|
|
scene.add(backLight);
|
|
|
|
const pointLight = new THREE.PointLight(0xffffff, 0.4);
|
|
pointLight.position.set(10, 10, 10);
|
|
scene.add(pointLight);
|
|
}
|
|
|
|
let earthTexture = null;
|
|
|
|
async function loadData(showWhiteSphere = false) {
|
|
if (showWhiteSphere) {
|
|
const earth = getEarth();
|
|
if (earth && earth.material) {
|
|
earthTexture = earth.material.map;
|
|
earth.material.map = null;
|
|
earth.material.color.setHex(0xffffff);
|
|
earth.material.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
console.log('开始加载电缆数据...');
|
|
await loadGeoJSONFromPath(scene, getEarth());
|
|
console.log('电缆数据加载完成');
|
|
await loadLandingPoints(scene, getEarth());
|
|
console.log('登陆点数据加载完成');
|
|
|
|
const satCount = await loadSatellites();
|
|
console.log(`卫星数据加载完成: ${satCount} 颗`);
|
|
updateSatellitePositions();
|
|
console.log('卫星位置已更新');
|
|
} catch (error) {
|
|
console.error('加载数据失败:', error);
|
|
showStatusMessage('加载数据失败: ' + error.message, 'error');
|
|
}
|
|
setLoading(false);
|
|
|
|
if (showWhiteSphere) {
|
|
const earth = getEarth();
|
|
if (earth && earth.material) {
|
|
earth.material.map = earthTexture;
|
|
earth.material.color.setHex(0xffffff);
|
|
earth.material.needsUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function reloadData() {
|
|
await loadData(true);
|
|
}
|
|
|
|
function setupEventListeners(camera, renderer) {
|
|
window.addEventListener('resize', () => onWindowResize(camera, renderer));
|
|
|
|
renderer.domElement.addEventListener('mousemove', (e) => onMouseMove(e, camera));
|
|
renderer.domElement.addEventListener('mousedown', onMouseDown);
|
|
renderer.domElement.addEventListener('mouseup', onMouseUp);
|
|
renderer.domElement.addEventListener('click', (e) => onClick(e, camera, renderer));
|
|
}
|
|
|
|
function onWindowResize(camera, renderer) {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function getFrontFacingCables(cableLines, camera) {
|
|
const earth = getEarth();
|
|
if (!earth) return cableLines;
|
|
|
|
const cameraDir = new THREE.Vector3();
|
|
camera.getWorldDirection(cameraDir);
|
|
|
|
return cableLines.filter(cable => {
|
|
const cablePos = new THREE.Vector3();
|
|
cable.geometry.computeBoundingBox();
|
|
const boundingBox = cable.geometry.boundingBox;
|
|
if (boundingBox) {
|
|
boundingBox.getCenter(cablePos);
|
|
cable.localToWorld(cablePos);
|
|
}
|
|
|
|
const toCamera = new THREE.Vector3().subVectors(camera.position, earth.position).normalize();
|
|
const toCable = new THREE.Vector3().subVectors(cablePos, earth.position).normalize();
|
|
|
|
return toCamera.dot(toCable) > 0;
|
|
});
|
|
}
|
|
|
|
function onMouseMove(event, camera) {
|
|
const earth = getEarth();
|
|
if (!earth) return;
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
const mouse = new THREE.Vector2(
|
|
(event.clientX / window.innerWidth) * 2 - 1,
|
|
-(event.clientY / window.innerHeight) * 2 + 1
|
|
);
|
|
|
|
raycaster.setFromCamera(mouse, camera);
|
|
|
|
const allCableLines = getCableLines();
|
|
const frontCables = getFrontFacingCables(allCableLines, camera);
|
|
const intersects = raycaster.intersectObjects(frontCables);
|
|
|
|
const hasHoveredCable = intersects.length > 0;
|
|
let hoveredSat = null;
|
|
if (getShowSatellites()) {
|
|
const satPoints = getSatellitePoints();
|
|
if (satPoints) {
|
|
const satIntersects = raycaster.intersectObject(satPoints);
|
|
if (satIntersects.length > 0) {
|
|
const index = satIntersects[0].index;
|
|
hoveredSat = selectSatellite(index);
|
|
}
|
|
}
|
|
}
|
|
const hasHoveredSatellite = hoveredSat && hoveredSat.properties;
|
|
|
|
if (hoveredCable) {
|
|
if (!hasHoveredCable || !isSameCable(intersects[0]?.object, hoveredCable)) {
|
|
if (!isSameCable(hoveredCable, lockedObject)) {
|
|
setCableState(hoveredCable.userData.cableId, CABLE_STATE.NORMAL);
|
|
}
|
|
hoveredCable = null;
|
|
}
|
|
}
|
|
|
|
if (hasHoveredCable) {
|
|
const cable = intersects[0].object;
|
|
if (!isSameCable(cable, lockedObject)) {
|
|
hoveredCable = cable;
|
|
setCableState(cable.userData.cableId, CABLE_STATE.HOVERED);
|
|
} else {
|
|
hoveredCable = cable;
|
|
}
|
|
|
|
showCableInfo(cable);
|
|
setInfoCardNoBorder(true);
|
|
hideTooltip();
|
|
} else if (hasHoveredSatellite) {
|
|
hoveredSatellite = hoveredSat;
|
|
showSatelliteInfo(hoveredSat.properties);
|
|
setInfoCardNoBorder(true);
|
|
} else if (lockedObjectType === 'cable' && lockedObject) {
|
|
showCableInfo(lockedObject);
|
|
} else if (lockedObjectType === 'satellite' && lockedSatellite) {
|
|
showSatelliteInfo(lockedSatellite.properties);
|
|
} else {
|
|
hideInfoCard();
|
|
}
|
|
|
|
const earthPoint = screenToEarthCoords(event.clientX, event.clientY, camera, earth);
|
|
if (earthPoint) {
|
|
const coords = vector3ToLatLon(earthPoint);
|
|
updateCoordinatesDisplay(coords.lat, coords.lon, coords.alt);
|
|
|
|
if (!isDragging) {
|
|
showTooltip(event.clientX + 10, event.clientY + 10,
|
|
`纬度: ${coords.lat}°<br>经度: ${coords.lon}°<br>海拔: ${coords.alt.toFixed(1)} km`);
|
|
}
|
|
} else {
|
|
hideTooltip();
|
|
}
|
|
|
|
if (isDragging) {
|
|
if (Date.now() - dragStartTime > 500) {
|
|
isLongDrag = true;
|
|
}
|
|
|
|
const deltaX = event.clientX - previousMousePosition.x;
|
|
const deltaY = event.clientY - previousMousePosition.y;
|
|
|
|
earth.rotation.y += deltaX * 0.005;
|
|
earth.rotation.x += deltaY * 0.005;
|
|
|
|
previousMousePosition = { x: event.clientX, y: event.clientY };
|
|
}
|
|
}
|
|
|
|
function onMouseDown(event) {
|
|
isDragging = true;
|
|
dragStartTime = Date.now();
|
|
isLongDrag = false;
|
|
previousMousePosition = { x: event.clientX, y: event.clientY };
|
|
document.getElementById('container').classList.add('dragging');
|
|
hideTooltip();
|
|
}
|
|
|
|
function onMouseUp() {
|
|
isDragging = false;
|
|
document.getElementById('container').classList.remove('dragging');
|
|
}
|
|
|
|
function onClick(event, camera, renderer) {
|
|
const earth = getEarth();
|
|
if (!earth) return;
|
|
|
|
const raycaster = new THREE.Raycaster();
|
|
const mouse = new THREE.Vector2(
|
|
(event.clientX / window.innerWidth) * 2 - 1,
|
|
-(event.clientY / window.innerHeight) * 2 + 1
|
|
);
|
|
|
|
raycaster.setFromCamera(mouse, camera);
|
|
|
|
const allCableLines = getCableLines();
|
|
const frontCables = getFrontFacingCables(allCableLines, camera);
|
|
const intersects = raycaster.intersectObjects(frontCables);
|
|
const satIntersects = getShowSatellites() ? raycaster.intersectObject(getSatellitePoints()) : [];
|
|
|
|
if (intersects.length > 0) {
|
|
clearLockedObject();
|
|
|
|
const clickedCable = intersects[0].object;
|
|
const cableId = clickedCable.userData.cableId;
|
|
|
|
setCableState(cableId, CABLE_STATE.LOCKED);
|
|
|
|
lockedObject = clickedCable;
|
|
lockedObjectType = 'cable';
|
|
cableLockedData = { ...clickedCable.userData };
|
|
|
|
setAutoRotate(false);
|
|
handleCableClick(clickedCable);
|
|
} else if (satIntersects.length > 0) {
|
|
const index = satIntersects[0].index;
|
|
const sat = selectSatellite(index);
|
|
|
|
if (sat && sat.properties) {
|
|
clearLockedObject();
|
|
|
|
lockedObject = sat;
|
|
lockedObjectType = 'satellite';
|
|
lockedSatellite = sat;
|
|
setAutoRotate(false);
|
|
|
|
const props = sat.properties;
|
|
|
|
const meanMotion = props.mean_motion || 0;
|
|
const period = meanMotion > 0 ? (1440 / meanMotion).toFixed(1) : '-';
|
|
|
|
const ecc = props.eccentricity || 0;
|
|
const earthRadius = 6371;
|
|
const perigee = (earthRadius * (1 - ecc)).toFixed(0);
|
|
const apogee = (earthRadius * (1 + ecc)).toFixed(0);
|
|
|
|
showInfoCard('satellite', {
|
|
name: props.name,
|
|
norad_id: props.norad_cat_id,
|
|
inclination: props.inclination ? props.inclination.toFixed(2) : '-',
|
|
period: period,
|
|
perigee: perigee,
|
|
apogee: apogee
|
|
});
|
|
|
|
showStatusMessage('已选择: ' + props.name, 'info');
|
|
}
|
|
} else {
|
|
if (!isLongDrag) {
|
|
clearLockedObject();
|
|
setAutoRotate(true);
|
|
clearCableSelection();
|
|
}
|
|
}
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const earth = getEarth();
|
|
|
|
if (getAutoRotate() && earth) {
|
|
earth.rotation.y += CONFIG.rotationSpeed;
|
|
}
|
|
|
|
applyCableVisualState();
|
|
|
|
updateSatellitePositions(16);
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
window.clearLockedCable = function() {
|
|
clearLockedObject();
|
|
};
|
|
|
|
window.clearSelection = function() {
|
|
hideInfoCard();
|
|
window.clearLockedCable();
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|