## Changelog ### New Features - Modularized 3D earth HTML page from single 1918-line file into ES Modules - Split CSS into separate module files (base, info-panel, coordinates-display, legend, earth-stats) - Split JS into separate modules (constants, utils, ui, earth, cables, controls, main) ### 3D Earth Rendering - Use Three.js r128 (via esm.sh CDN) for color consistency with original - Earth with 8K satellite texture and proper material settings - Cloud layer with transparency and additive blending - Starfield background (8000 stars) - Latitude/longitude grid lines that rotate with Earth ### Cable System - Load cable data from geo.json with great circle path calculation - Support for MultiLineString and LineString geometry types - Cable color from geo.json properties.color field - Landing points loading from landing-point-geo.geojson ### User Interactions - Mouse hover: highlight cable and show details - Mouse click: lock cable with pulsing glow effect - Click cable to pause rotation, click elsewhere to resume - Click rotation toggle button to resume rotation and clear highlight - Reset view with smooth animation (800ms cubic ease-out) - Mouse wheel zoom support - Drag to rotate Earth ### UI/UX Improvements - Tooltip shows latitude, longitude, and altitude - Prevent tooltip text selection during drag - Hide tooltip during drag operation - Blue border tooltip styling matching original - Cursor changes to grabbing during drag - Front-facing cable detection (only detect cables facing camera) ### Bug Fixes - Grid lines now rotate with Earth (added as Earth child) - Reset view button now works correctly - Fixed camera reference in reset view - Fixed autoRotate state management when clearing locked cable ### Original HTML - Copied original 3dearthmult.html to public folder for reference
1912 lines
75 KiB
HTML
1912 lines
75 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>3D球形地图 - 海底电缆系统</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background-color: #0a0a1a;
|
||
color: #fff;
|
||
overflow: hidden;
|
||
}
|
||
#container {
|
||
position: relative;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
}
|
||
#info-panel {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
background-color: rgba(10, 10, 30, 0.85);
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
width: 320px;
|
||
z-index: 10;
|
||
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
|
||
border: 1px solid rgba(0, 150, 255, 0.2);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
h1 {
|
||
font-size: 1.8rem;
|
||
margin-bottom: 5px;
|
||
color: #4db8ff;
|
||
text-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
|
||
}
|
||
.subtitle {
|
||
color: #aaa;
|
||
margin-bottom: 20px;
|
||
font-size: 0.9rem;
|
||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||
padding-bottom: 10px;
|
||
}
|
||
.cable-info {
|
||
margin-top: 15px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
.cable-info h3 {
|
||
color: #4db8ff;
|
||
margin-bottom: 8px;
|
||
font-size: 1.2rem;
|
||
}
|
||
.cable-property {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 5px;
|
||
font-size: 0.9rem;
|
||
}
|
||
.property-label {
|
||
color: #aaa;
|
||
}
|
||
.property-value {
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
.controls {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 20px;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
button {
|
||
background: linear-gradient(135deg, #0066cc, #004c99);
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 15px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
transition: all 0.3s;
|
||
flex: 1;
|
||
min-width: 120px;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||
}
|
||
button:hover {
|
||
background: linear-gradient(135deg, #0088ff, #0066cc);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(0,102,204,0.4);
|
||
}
|
||
.zoom-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 15px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
.zoom-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
.zoom-buttons button {
|
||
flex: 1;
|
||
min-width: 60px;
|
||
}
|
||
#loading {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 1.2rem;
|
||
color: #4db8ff;
|
||
z-index: 100;
|
||
text-align: center;
|
||
background-color: rgba(10, 10, 30, 0.95);
|
||
padding: 30px;
|
||
border-radius: 10px;
|
||
border: 1px solid #4db8ff;
|
||
box-shadow: 0 0 30px rgba(77,184,255,0.3);
|
||
}
|
||
#loading-spinner {
|
||
border: 4px solid rgba(77, 184, 255, 0.3);
|
||
border-top: 4px solid #4db8ff;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 15px;
|
||
}
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
#legend {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 20px;
|
||
background-color: rgba(10, 10, 30, 0.85);
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
width: 220px;
|
||
z-index: 10;
|
||
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
|
||
border: 1px solid rgba(0, 150, 255, 0.2);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
.legend-color {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
margin-right: 10px;
|
||
}
|
||
#earth-stats {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background-color: rgba(10, 10, 30, 0.85);
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
width: 250px;
|
||
z-index: 10;
|
||
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
|
||
border: 1px solid rgba(0, 150, 255, 0.2);
|
||
font-size: 0.9rem;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
.stats-item {
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
.stats-label {
|
||
color: #aaa;
|
||
}
|
||
.stats-value {
|
||
color: #4db8ff;
|
||
font-weight: 500;
|
||
}
|
||
.error-message {
|
||
color: #ff4444;
|
||
margin-top: 10px;
|
||
font-size: 0.9rem;
|
||
display: none;
|
||
padding: 10px;
|
||
background-color: rgba(255, 68, 68, 0.1);
|
||
border-radius: 5px;
|
||
border-left: 3px solid #ff4444;
|
||
}
|
||
.terrain-controls {
|
||
margin-top: 15px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
.slider-container {
|
||
margin-bottom: 10px;
|
||
}
|
||
.slider-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 5px;
|
||
font-size: 0.9rem;
|
||
}
|
||
input[type="range"] {
|
||
width: 100%;
|
||
height: 8px;
|
||
-webkit-appearance: none;
|
||
background: rgba(0, 102, 204, 0.3);
|
||
border-radius: 4px;
|
||
outline: none;
|
||
}
|
||
input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background: #4db8ff;
|
||
cursor: pointer;
|
||
box-shadow: 0 0 10px #4db8ff;
|
||
}
|
||
.status-message {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
background-color: rgba(10, 10, 30, 0.85);
|
||
border-radius: 10px;
|
||
padding: 10px 15px;
|
||
z-index: 10;
|
||
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
|
||
border: 1px solid rgba(0, 150, 255, 0.2);
|
||
font-size: 0.9rem;
|
||
display: none;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
.success {
|
||
color: #44ff44;
|
||
border-left: 3px solid #44ff44;
|
||
}
|
||
.warning {
|
||
color: #ffff44;
|
||
border-left: 3px solid #ffff44;
|
||
}
|
||
.error {
|
||
color: #ff4444;
|
||
border-left: 3px solid #ff4444;
|
||
}
|
||
#coordinates-display {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 250px;
|
||
background-color: rgba(10, 10, 30, 0.85);
|
||
border-radius: 10px;
|
||
padding: 10px 15px;
|
||
z-index: 10;
|
||
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
|
||
border: 1px solid rgba(0, 150, 255, 0.2);
|
||
font-size: 0.9rem;
|
||
min-width: 180px;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
.coord-item {
|
||
margin-bottom: 5px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
.coord-label {
|
||
color: #aaa;
|
||
}
|
||
.coord-value {
|
||
color: #4db8ff;
|
||
font-weight: 500;
|
||
}
|
||
#zoom-level {
|
||
margin-top: 5px;
|
||
color: #ffff44;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
font-size: 1rem;
|
||
}
|
||
.mouse-coords {
|
||
font-size: 0.8rem;
|
||
color: #aaa;
|
||
margin-top: 5px;
|
||
padding-top: 5px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
.tooltip {
|
||
position: absolute;
|
||
background-color: rgba(10, 10, 30, 0.95);
|
||
color: white;
|
||
padding: 5px 10px;
|
||
border-radius: 5px;
|
||
font-size: 0.8rem;
|
||
pointer-events: none;
|
||
z-index: 100;
|
||
border: 1px solid #4db8ff;
|
||
box-shadow: 0 0 10px rgba(77, 184, 255, 0.3);
|
||
display: none;
|
||
}
|
||
.grid-lines {
|
||
pointer-events: none;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="container">
|
||
<div id="info-panel">
|
||
<h1>全球海底电缆系统</h1>
|
||
<div class="subtitle">3D地形球形地图可视化 | 高分辨率卫星图</div>
|
||
<div class="zoom-controls">
|
||
<div style="width: 100%;">
|
||
<h3 style="color:#4db8ff; margin-bottom:10px; font-size:1.1rem;">缩放控制</h3>
|
||
<div class="zoom-buttons">
|
||
<button id="zoom-in">放大</button>
|
||
<button id="zoom-out">缩小</button>
|
||
<button id="zoom-reset">重置</button>
|
||
</div>
|
||
<div class="slider-container" style="margin-top: 10px;">
|
||
<div class="slider-label">
|
||
<span>缩放级别:</span>
|
||
<span id="zoom-value">1.0x</span>
|
||
</div>
|
||
<input type="range" id="zoom-slider" min="0.5" max="5" step="0.1" value="1">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="cable-details" class="cable-info">
|
||
<h3 id="cable-name">点击电缆查看详情</h3>
|
||
<div class="cable-property">
|
||
<span class="property-label">所有者:</span>
|
||
<span id="cable-owner" class="property-value">-</span>
|
||
</div>
|
||
<div class="cable-property">
|
||
<span class="property-label">状态:</span>
|
||
<span id="cable-status" class="property-value">-</span>
|
||
</div>
|
||
<div class="cable-property">
|
||
<span class="property-label">长度:</span>
|
||
<span id="cable-length" class="property-value">-</span>
|
||
</div>
|
||
<div class="cable-property">
|
||
<span class="property-label">经纬度:</span>
|
||
<span id="cable-coords" class="property-value">-</span>
|
||
</div>
|
||
<div class="cable-property">
|
||
<span class="property-label">投入使用时间:</span>
|
||
<span id="cable-rfs" class="property-value">-</span>
|
||
</div>
|
||
</div>
|
||
<div class="controls">
|
||
<button id="rotate-toggle">暂停旋转</button>
|
||
<button id="reset-view">重置视图</button>
|
||
<button id="toggle-terrain">显示地形</button>
|
||
<button id="reload-data">重新加载数据</button>
|
||
</div>
|
||
<div id="error-message" class="error-message"></div>
|
||
</div>
|
||
<div id="coordinates-display">
|
||
<h3 style="color:#4db8ff; margin-bottom:8px; font-size:1.1rem;">坐标信息</h3>
|
||
<div class="coord-item">
|
||
<span class="coord-label">经度:</span>
|
||
<span id="longitude-value" class="coord-value">0.00°</span>
|
||
</div>
|
||
<div class="coord-item">
|
||
<span class="coord-label">纬度:</span>
|
||
<span id="latitude-value" class="coord-value">0.00°</span>
|
||
</div>
|
||
|
||
<div id="zoom-level">缩放: 1.0x</div>
|
||
<div class="mouse-coords" id="mouse-coords">
|
||
鼠标位置: 无
|
||
</div>
|
||
</div>
|
||
<div id="legend">
|
||
<h3 style="color:#4db8ff; margin-bottom:10px; font-size:1.1rem;">图例</h3>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background-color: #ff4444;"></div>
|
||
<span>Americas II</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background-color: #44ff44;"></div>
|
||
<span>AU Aleutian A</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background-color: #4444ff;"></div>
|
||
<span>AU Aleutian B</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background-color: #ffff44;"></div>
|
||
<span>其他电缆</span>
|
||
</div>
|
||
</div>
|
||
<div id="earth-stats">
|
||
<h3 style="color:#4db8ff; margin-bottom:10px; font-size:1.1rem;">地球信息</h3>
|
||
<div class="stats-item">
|
||
<span class="stats-label">电缆系统:</span>
|
||
<span class="stats-value" id="cable-count">0个</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span class="stats-label">状态:</span>
|
||
<span class="stats-value" id="cable-status-summary">-</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span class="stats-label">地形:</span>
|
||
<span class="stats-value" id="terrain-status">开启</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span class="stats-label">视角距离:</span>
|
||
<span class="stats-value" id="camera-distance">300 km</span>
|
||
</div>
|
||
<div class="stats-item">
|
||
<span class="stats-label">纹理质量:</span>
|
||
<span class="stats-value" id="texture-quality">8K 卫星图</span>
|
||
</div>
|
||
</div>
|
||
<div id="loading">
|
||
<div id="loading-spinner"></div>
|
||
<div>正在加载3D地球和电缆数据...</div>
|
||
<div style="font-size:0.9rem; margin-top:10px; color:#aaa;">使用8K高分辨率卫星纹理 | 大陆轮廓更清晰</div>
|
||
</div>
|
||
<div id="status-message" class="status-message" style="display: none;"></div>
|
||
<div id="tooltip" class="tooltip"></div>
|
||
</div>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.js"></script>
|
||
<script>
|
||
// 全局变量
|
||
let scene, camera, renderer;
|
||
let earth, clouds, terrain, gridLines;
|
||
let cableLines = [];
|
||
let autoRotate = true;
|
||
let rotationSpeed = 0.002;
|
||
let targetRotationX = 0;
|
||
let targetRotationY = 0;
|
||
let isDragging = false;
|
||
let previousMousePosition = { x: 0, y: 0 };
|
||
let simplex = new SimplexNoise();
|
||
let showTerrain = false;
|
||
let terrainHeightMultiplier = 1.0;
|
||
let terrainDetail = 3;
|
||
// 新增:锁定相关变量
|
||
let lockedCable = null; // 当前锁定的电缆
|
||
let isLocked = false; // 是否处于锁定状态
|
||
let lastMouseX = 0; // 上次鼠标位置
|
||
let lastMouseY = 0;
|
||
|
||
|
||
// 缩放相关变量
|
||
let zoomLevel = 1.0;
|
||
let minZoom = 0.5;
|
||
let maxZoom = 5.0;
|
||
let defaultCameraZ = 300;
|
||
let isZooming = false;
|
||
|
||
// 经纬度网格相关
|
||
let latitudeLines = [];
|
||
let longitudeLines = [];
|
||
let showGrid = true;
|
||
let landingPoints = []; // 新增
|
||
// 固定GeoJSON文件路径
|
||
const GEOJSON_PATH = './geo.json';
|
||
|
||
// 电缆颜色映射
|
||
const cableColors = {
|
||
'Americas II': 0xff4444,
|
||
'AU Aleutian A': 0x44ff44,
|
||
'AU Aleutian B': 0x4444ff,
|
||
'default': 0xffff44
|
||
};
|
||
|
||
// 显示状态消息
|
||
function showStatusMessage(message, type = 'info') {
|
||
const statusEl = document.getElementById('status-message');
|
||
statusEl.textContent = message;
|
||
statusEl.className = `status-message ${type}`;
|
||
statusEl.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
statusEl.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
// 计算两点间的球面距离(简化版,用欧氏距离近似)
|
||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||
const dx = lon2 - lon1;
|
||
const dy = lat2 - lat1;
|
||
return Math.sqrt(dx*dx + dy*dy);
|
||
}
|
||
|
||
|
||
// 显示工具提示
|
||
function showTooltip(x, y, content) {
|
||
const tooltip = document.getElementById('tooltip');
|
||
tooltip.innerHTML = content;
|
||
tooltip.style.left = x + 'px';
|
||
tooltip.style.top = y + 'px';
|
||
tooltip.style.display = 'block';
|
||
}
|
||
|
||
function hideTooltip() {
|
||
document.getElementById('tooltip').style.display = 'none';
|
||
}
|
||
|
||
// 将3D坐标转换为经纬度
|
||
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);
|
||
const lon = (Math.atan2(vector.z, -vector.x) * 180 / Math.PI) - 180;
|
||
|
||
return {
|
||
lat: parseFloat(lat.toFixed(4)),
|
||
lon: parseFloat(lon.toFixed(4)),
|
||
alt: radius - 100 // 地球半径为100
|
||
};
|
||
}
|
||
|
||
// 将屏幕坐标转换为地球表面的3D坐标
|
||
function screenToEarthCoords(x, y) {
|
||
const raycaster = new THREE.Raycaster();
|
||
const mouse = new THREE.Vector2(
|
||
(x / window.innerWidth) * 2 - 1,
|
||
-(y / window.innerHeight) * 2 + 1
|
||
);
|
||
|
||
raycaster.setFromCamera(mouse, camera);
|
||
|
||
// 检查与地球的交点
|
||
const intersects = raycaster.intersectObject(earth);
|
||
|
||
if (intersects.length > 0) {
|
||
return intersects[0].point;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 更新坐标显示
|
||
function updateCoordinates(x, y) {
|
||
const earthPoint = screenToEarthCoords(x, y);
|
||
|
||
if (earthPoint) {
|
||
const coords = vector3ToLatLon(earthPoint);
|
||
|
||
document.getElementById('longitude-value').textContent = coords.lon.toFixed(2) + '°';
|
||
document.getElementById('latitude-value').textContent = coords.lat.toFixed(2) + '°';
|
||
|
||
|
||
// 更新鼠标坐标显示
|
||
document.getElementById('mouse-coords').textContent =
|
||
`鼠标: ${coords.lat.toFixed(2)}°, ${coords.lon.toFixed(2)}°`;
|
||
|
||
// 显示工具提示
|
||
showTooltip(x, y,
|
||
`经度: ${coords.lon.toFixed(4)}°<br>纬度: ${coords.lat.toFixed(4)}°<br>海拔: ${Math.abs(coords.alt).toFixed(2)} km`
|
||
);
|
||
} else {
|
||
// 如果没有交点,显示默认值
|
||
document.getElementById('mouse-coords').textContent = '鼠标位置: 地球外';
|
||
hideTooltip();
|
||
}
|
||
}
|
||
|
||
// 创建经纬度网格
|
||
function createGridLines() {
|
||
// 清除现有网格
|
||
latitudeLines.forEach(line => scene.remove(line));
|
||
longitudeLines.forEach(line => scene.remove(line));
|
||
latitudeLines = [];
|
||
longitudeLines = [];
|
||
|
||
if (!showGrid) return;
|
||
|
||
const earthRadius = 100.1; // 比地球稍大一点
|
||
const gridMaterial = new THREE.LineBasicMaterial({
|
||
color: 0x44aaff,
|
||
transparent: true,
|
||
opacity: 0.2,
|
||
linewidth: 1
|
||
});
|
||
|
||
// 创建纬度线(每15度一条)
|
||
for (let lat = -75; lat <= 75; lat += 15) {
|
||
const points = [];
|
||
for (let lon = -180; lon <= 180; lon += 5) {
|
||
const point = latLonToVector3(lat, lon, earthRadius);
|
||
points.push(point);
|
||
}
|
||
|
||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||
const line = new THREE.Line(geometry, gridMaterial);
|
||
line.userData = { type: 'latitude', value: lat };
|
||
scene.add(line);
|
||
latitudeLines.push(line);
|
||
}
|
||
|
||
// 创建经度线(每30度一条)
|
||
for (let lon = -180; lon <= 180; lon += 30) {
|
||
const points = [];
|
||
for (let lat = -90; lat <= 90; lat += 5) {
|
||
const point = latLonToVector3(lat, lon, earthRadius);
|
||
points.push(point);
|
||
}
|
||
|
||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||
const line = new THREE.Line(geometry, gridMaterial);
|
||
line.userData = { type: 'longitude', value: lon };
|
||
scene.add(line);
|
||
longitudeLines.push(line);
|
||
}
|
||
}
|
||
|
||
// 应用缩放
|
||
function applyZoom() {
|
||
// 更新相机位置
|
||
camera.position.z = defaultCameraZ / zoomLevel;
|
||
|
||
// 更新UI显示
|
||
document.getElementById('zoom-value').textContent = zoomLevel.toFixed(1) + 'x';
|
||
document.getElementById('zoom-level').textContent = '缩放: ' + zoomLevel.toFixed(1) + 'x';
|
||
document.getElementById('zoom-slider').value = zoomLevel;
|
||
|
||
// 更新相机距离显示
|
||
const distance = camera.position.z.toFixed(0);
|
||
document.getElementById('camera-distance').textContent = distance + ' km';
|
||
|
||
// 调整网格透明度(缩放越大,网格越明显)
|
||
const gridOpacity = Math.min(0.4, 0.2 * zoomLevel);
|
||
latitudeLines.forEach(line => {
|
||
if (line.material) line.material.opacity = gridOpacity;
|
||
});
|
||
longitudeLines.forEach(line => {
|
||
if (line.material) line.material.opacity = gridOpacity;
|
||
});
|
||
}
|
||
|
||
// 初始化Three.js场景
|
||
function init() {
|
||
// 创建场景
|
||
scene = new THREE.Scene();
|
||
|
||
// 创建相机
|
||
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||
camera.position.z = 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();
|
||
|
||
// 创建地球(使用高分辨率纹理)
|
||
createEarth();
|
||
// 添加地球倾斜角 - 约 23.5 度(地轴倾角)
|
||
// 绕 X 轴旋转,使北极指向斜上方
|
||
earth.rotation.x = 23.5 * Math.PI / 180; // 转换为弧度
|
||
// 创建地形
|
||
createTerrain();
|
||
|
||
// 创建云层
|
||
createClouds();
|
||
|
||
// 创建星空背景
|
||
createStars();
|
||
|
||
// 创建经纬度网格
|
||
createGridLines();
|
||
|
||
// 设置事件监听器
|
||
setupEventListeners();
|
||
|
||
// 应用初始缩放
|
||
applyZoom();
|
||
|
||
// 加载GeoJSON数据
|
||
loadGeoJSONFromPath();
|
||
loadLandingPoints();
|
||
|
||
// 开始动画循环
|
||
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);
|
||
}
|
||
|
||
// 创建地球 - 使用高分辨率纹理
|
||
function createEarth() {
|
||
const earthRadius = 100;
|
||
|
||
// 使用更高分段数的几何体,让地球更圆滑
|
||
const earthGeometry = new THREE.SphereGeometry(earthRadius, 128, 128);
|
||
|
||
// 使用标准材质,支持光照
|
||
const earthMaterial = new THREE.MeshPhongMaterial({
|
||
color: 0xffffff,
|
||
specular: 0x111111,
|
||
shininess: 10,
|
||
emissive: 0x000000,
|
||
|
||
transparent: true, // 开启透明度
|
||
opacity: 0.8, // 整体透明度 0.8(0=完全透明,1=完全不透明)
|
||
side: THREE.DoubleSide
|
||
});
|
||
|
||
earth = new THREE.Mesh(earthGeometry, earthMaterial);
|
||
scene.add(earth);
|
||
|
||
// 加载高分辨率地球纹理(8K卫星图)
|
||
const textureLoader = new THREE.TextureLoader();
|
||
|
||
// 使用多个备选纹理源,确保至少有一个能加载成功
|
||
const textureUrls = [
|
||
//'gebco_08_rev_elev_21600x10800.png',
|
||
'./8k_earth_daymap.jpg',
|
||
'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/planets/earth_atmos_2048.jpg',
|
||
'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg',
|
||
'https://assets.codepen.io/982762/earth_texture_2048.jpg'
|
||
];
|
||
|
||
let textureLoaded = false;
|
||
|
||
// 尝试加载第一个纹理
|
||
textureLoader.load(
|
||
textureUrls[0],
|
||
function(texture) {
|
||
console.log('高分辨率地球纹理加载成功');
|
||
textureLoaded = true;
|
||
|
||
// 配置纹理以获得最佳清晰度
|
||
texture.wrapS = THREE.RepeatWrapping;
|
||
texture.wrapT = THREE.ClampToEdgeWrapping;
|
||
texture.anisotropy = 16; // 提高各向异性过滤,让倾斜视角更清晰
|
||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||
texture.magFilter = THREE.LinearFilter;
|
||
|
||
earthMaterial.map = texture;
|
||
earthMaterial.needsUpdate = true;
|
||
|
||
// 隐藏加载提示
|
||
document.getElementById('loading').style.display = 'none';
|
||
},
|
||
function(xhr) {
|
||
// 加载进度
|
||
console.log('纹理加载中: ' + (xhr.loaded / xhr.total * 100) + '%');
|
||
},
|
||
function(err) {
|
||
console.log('第一个纹理加载失败,尝试第二个...');
|
||
|
||
// 尝试第二个纹理
|
||
textureLoader.load(
|
||
textureUrls[1],
|
||
function(texture) {
|
||
console.log('第二个纹理加载成功');
|
||
textureLoaded = true;
|
||
|
||
texture.wrapS = THREE.RepeatWrapping;
|
||
texture.wrapT = THREE.ClampToEdgeWrapping;
|
||
texture.anisotropy = 16;
|
||
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
||
texture.magFilter = THREE.LinearFilter;
|
||
|
||
earthMaterial.map = texture;
|
||
earthMaterial.needsUpdate = true;
|
||
|
||
document.getElementById('loading').style.display = 'none';
|
||
},
|
||
null,
|
||
function(err) {
|
||
console.log('所有纹理加载失败,使用程序化生成的地球');
|
||
|
||
// 如果所有纹理都加载失败,使用程序化生成的地球
|
||
// 创建一个Canvas纹理作为备选
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = 2048;
|
||
canvas.height = 1024;
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
// 绘制简单的蓝色海洋和绿色大陆
|
||
ctx.fillStyle = '#1a4d8c';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// 绘制一些随机的大陆形状(简化版)
|
||
ctx.fillStyle = '#3a9e3a';
|
||
|
||
// 欧亚大陆
|
||
ctx.beginPath();
|
||
ctx.rect(400, 300, 600, 200);
|
||
ctx.fill();
|
||
|
||
// 非洲
|
||
ctx.beginPath();
|
||
ctx.rect(550, 550, 250, 200);
|
||
ctx.fill();
|
||
|
||
// 美洲
|
||
ctx.beginPath();
|
||
ctx.rect(100, 350, 250, 300);
|
||
ctx.fill();
|
||
|
||
// 添加一些噪点让大陆看起来更自然
|
||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||
const data = imageData.data;
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
if (data[i] > 100) { // 如果是绿色区域
|
||
// 添加一些变化
|
||
data[i] += Math.random() * 30 - 15;
|
||
data[i+1] += Math.random() * 30 - 15;
|
||
data[i+2] += Math.random() * 30 - 15;
|
||
}
|
||
}
|
||
ctx.putImageData(imageData, 0, 0);
|
||
|
||
const fallbackTexture = new THREE.CanvasTexture(canvas);
|
||
earthMaterial.map = fallbackTexture;
|
||
earthMaterial.needsUpdate = true;
|
||
|
||
document.getElementById('loading').style.display = 'none';
|
||
showStatusMessage('使用程序化生成的地球纹理', 'warning');
|
||
}
|
||
);
|
||
}
|
||
);
|
||
}
|
||
|
||
// 创建地形
|
||
function createTerrain() {
|
||
const earthRadius = 100;
|
||
const segments = 64 * terrainDetail;
|
||
|
||
// 创建带地形的球体
|
||
const terrainGeometry = new THREE.SphereGeometry(earthRadius, segments, segments);
|
||
|
||
// 修改顶点高度以创建地形
|
||
const positionAttribute = terrainGeometry.getAttribute('position');
|
||
const vertex = new THREE.Vector3();
|
||
|
||
for (let i = 0; i < positionAttribute.count; i++) {
|
||
vertex.fromBufferAttribute(positionAttribute, i);
|
||
vertex.normalize();
|
||
|
||
// 使用Simplex噪声生成地形高度
|
||
const noise = simplex.noise3D(
|
||
vertex.x * 2,
|
||
vertex.y * 2,
|
||
vertex.z * 2
|
||
);
|
||
|
||
// 添加更多细节的噪声
|
||
const noise2 = simplex.noise3D(
|
||
vertex.x * 8,
|
||
vertex.y * 8,
|
||
vertex.z * 8
|
||
) * 0.3;
|
||
|
||
// 组合噪声
|
||
const totalNoise = noise * 0.7 + noise2 * 0.3;
|
||
|
||
// 计算高度
|
||
const height = 0;//totalNoise * terrainHeightMultiplier;
|
||
|
||
// 应用高度
|
||
vertex.multiplyScalar(1 + height / earthRadius);
|
||
|
||
positionAttribute.setXYZ(i, vertex.x, vertex.y, vertex.z);
|
||
}
|
||
|
||
positionAttribute.needsUpdate = true;
|
||
terrainGeometry.computeVertexNormals();
|
||
|
||
// 创建地形材质 - 使用半透明材质,让地球纹理透出来
|
||
const terrainMaterial = new THREE.MeshPhongMaterial({
|
||
color: 0x88aa88,
|
||
shininess: 10,
|
||
transparent: true,
|
||
opacity: 0.2,
|
||
side: THREE.DoubleSide
|
||
});
|
||
|
||
// 创建地形网格
|
||
terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
|
||
terrain.visible = showTerrain;
|
||
earth.add(terrain);
|
||
}
|
||
|
||
// 更新地形
|
||
function updateTerrain() {
|
||
if (terrain) {
|
||
earth.remove(terrain);
|
||
}
|
||
createTerrain();
|
||
}
|
||
|
||
// 创建云层
|
||
function createClouds() {
|
||
const earthRadius = 100;
|
||
const cloudGeometry = new THREE.SphereGeometry(earthRadius + 3, 64, 64);
|
||
|
||
// 尝试加载云层纹理
|
||
const cloudMaterial = new THREE.MeshPhongMaterial({
|
||
transparent: true,
|
||
linewidth: 2,
|
||
opacity: 0.15,
|
||
depthTest: true,
|
||
depthWrite: false,
|
||
blending: THREE.AdditiveBlending
|
||
});
|
||
|
||
clouds = new THREE.Mesh(cloudGeometry, cloudMaterial);
|
||
earth.add(clouds);
|
||
|
||
// 加载云层纹理
|
||
const textureLoader = new THREE.TextureLoader();
|
||
textureLoader.load(
|
||
'https://threejs.org/examples/textures/planets/earth_clouds_1024.png',
|
||
function(texture) {
|
||
cloudMaterial.map = texture;
|
||
cloudMaterial.needsUpdate = true;
|
||
},
|
||
undefined,
|
||
function(err) {
|
||
console.log('云层纹理加载失败,使用纯色云层');
|
||
}
|
||
);
|
||
}
|
||
|
||
// 创建星空背景
|
||
function createStars() {
|
||
const starGeometry = new THREE.BufferGeometry();
|
||
const starCount = 8000; // 增加星星数量
|
||
const starPositions = new Float32Array(starCount * 3);
|
||
|
||
for (let i = 0; i < starCount * 3; i += 3) {
|
||
// 让星星分布在一个球面上,而不是立方体,这样看起来更自然
|
||
const r = 800 + Math.random() * 200;
|
||
const theta = Math.random() * Math.PI * 2;
|
||
const phi = Math.acos(2 * Math.random() - 1);
|
||
|
||
starPositions[i] = r * Math.sin(phi) * Math.cos(theta);
|
||
starPositions[i + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||
starPositions[i + 2] = r * Math.cos(phi);
|
||
}
|
||
|
||
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
|
||
|
||
const starMaterial = new THREE.PointsMaterial({
|
||
color: 0xffffff,
|
||
size: 0.5,
|
||
transparent: true,
|
||
blending: THREE.AdditiveBlending
|
||
});
|
||
|
||
const stars = new THREE.Points(starGeometry, starMaterial);
|
||
scene.add(stars);
|
||
}
|
||
|
||
// 转换坐标函数:将经纬度转换为球面坐标
|
||
function latLonToVector3(lat, lon, radius) {
|
||
|
||
if (lat === undefined || lon === undefined || radius === undefined ||
|
||
lat === null || lon === null || radius === null ||
|
||
typeof lat !== 'number' || typeof lon !== 'number' || typeof radius !== 'number' ||
|
||
isNaN(lat) || isNaN(lon) || isNaN(radius) ||
|
||
!isFinite(lat) || !isFinite(lon) || !isFinite(radius)) {
|
||
console.warn('无效的坐标输入:', { lat, lon, radius });
|
||
return new THREE.Vector3(0, 0, 0); // 返回原点
|
||
}
|
||
|
||
|
||
const phi = (90 - lat) * (Math.PI / 180);
|
||
const theta = (lon + 180) * (Math.PI / 180);
|
||
|
||
return new THREE.Vector3(
|
||
-radius * Math.sin(phi) * Math.cos(theta),
|
||
radius * Math.cos(phi),
|
||
radius * Math.sin(phi) * Math.sin(theta)
|
||
);
|
||
}
|
||
|
||
// 从固定路径加载GeoJSON数据
|
||
async function loadGeoJSONFromPath() {
|
||
try {
|
||
console.log(`正在从 ${GEOJSON_PATH} 加载GeoJSON数据...`);
|
||
showStatusMessage('正在加载电缆数据...', 'warning');
|
||
|
||
// 使用fetch API加载本地文件
|
||
const response = await fetch(GEOJSON_PATH);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP错误: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
const geoJsonData = await response.json();
|
||
|
||
if (geoJsonData.features && Array.isArray(geoJsonData.features)) {
|
||
loadCablesFromGeoJSON(geoJsonData);
|
||
showStatusMessage(`成功加载 ${geoJsonData.features.length} 条电缆数据`, 'success');
|
||
} else {
|
||
throw new Error('无效的GeoJSON格式: 缺少features数组');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('加载GeoJSON数据失败:', error);
|
||
|
||
// 显示错误信息
|
||
const errorEl = document.getElementById('error-message');
|
||
errorEl.textContent = `无法加载文件 ${GEOJSON_PATH}: ${error.message}`;
|
||
errorEl.style.display = 'block';
|
||
|
||
showStatusMessage('数据加载失败,请检查文件路径', 'error');
|
||
|
||
// 加载示例数据作为后备
|
||
setTimeout(() => {
|
||
loadFallbackData();
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
|
||
async function loadLandingPoints() {
|
||
try {
|
||
console.log(`正在从 ${GEOJSON_PATH} 加载GeoJSON数据...`);
|
||
// 方式1:从本地文件加载(如果你下载了landing_points.geojson)
|
||
const response = await fetch('./landing-point-geo.geojson');
|
||
|
||
// 方式2:或者直接从API加载(取消下面注释)
|
||
// const response = await fetch('https://services.arcgis.com/6DIQcwlPy8knb6sg/arcgis/rest/services/SubmarineCables/FeatureServer/0/query?where=1%3D1&outFields=*&returnGeometry=true&f=geojson');
|
||
|
||
if (!response.ok) {
|
||
console.error(`HTTP错误: ${response.status}`);
|
||
throw new Error(`HTTP错误: ${response.status}`);
|
||
}
|
||
|
||
const geoJsonData = await response.json();
|
||
|
||
if (geoJsonData.features && Array.isArray(geoJsonData.features)) {
|
||
createLandingPoints(geoJsonData);
|
||
showStatusMessage(`成功加载 ${geoJsonData.features.length} 个登陆点`, 'success');
|
||
} else {
|
||
console.error('无效的GeoJSON格式');
|
||
throw new Error('无效的GeoJSON格式');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('加载登陆点数据失败:', error);
|
||
showStatusMessage('登陆点数据加载失败', 'error');
|
||
}
|
||
}
|
||
|
||
|
||
// 创建登陆点标记
|
||
function createLandingPoints(geoJsonData) {
|
||
console.log(`开始创建登陆点,数据量: ${geoJsonData.features.length}个`);
|
||
let validCount = 0;
|
||
|
||
geoJsonData.features.forEach((feature, index) => {
|
||
if (!feature.geometry || !feature.geometry.coordinates) return;
|
||
|
||
// GeoJSON点坐标格式:[经度, 纬度]
|
||
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) {
|
||
console.warn(`跳过无效坐标: 索引=${index}, lon=${lon}, lat=${lat}`);
|
||
return;
|
||
}
|
||
|
||
// 转换为3D坐标(略高于地球表面)
|
||
const position = latLonToVector3(lat, lon, 100.1);
|
||
|
||
// 验证转换结果
|
||
if (isNaN(position.x) || isNaN(position.y) || isNaN(position.z)) {
|
||
console.warn(`转换失败: 索引=${index}, lat=${lat}, lon=${lon}`);
|
||
return;
|
||
}
|
||
|
||
// 创建小球体作为登陆点标记 - 半径0.8
|
||
const sphereGeometry = new THREE.SphereGeometry(0.4, 16, 16);
|
||
|
||
// 使用亮黄色,更容易看到
|
||
const sphereMaterial = new THREE.MeshStandardMaterial({
|
||
color: 0xffaa00,
|
||
emissive: 0x442200,
|
||
emissiveIntensity: 0.5
|
||
});
|
||
|
||
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
|
||
sphere.position.copy(position);
|
||
|
||
// 存储属性信息
|
||
sphere.userData = {
|
||
type: 'landingPoint',
|
||
name: properties.name || '未知登陆站',
|
||
cableName: properties.cable_system || '未知系统',
|
||
country: properties.country || '未知国家',
|
||
status: properties.status || 'Unknown'
|
||
};
|
||
|
||
earth.add(sphere);
|
||
landingPoints.push(sphere); // 新增:添加到数组
|
||
validCount++;
|
||
|
||
// 每100个点输出一次进度
|
||
if (validCount % 100 === 0) {
|
||
console.log(`已创建 ${validCount} 个登陆点`);
|
||
}
|
||
});
|
||
|
||
console.log(`成功创建 ${validCount} 个登陆点标记`);
|
||
|
||
// 更新统计信息
|
||
const statsEl = document.getElementById('earth-stats');
|
||
if (statsEl) {
|
||
// 检查是否已有登陆点统计项
|
||
let landingStats = document.getElementById('landing-points-stats');
|
||
if (!landingStats) {
|
||
const statsDiv = document.createElement('div');
|
||
statsDiv.className = 'stats-item';
|
||
statsDiv.id = 'landing-points-stats';
|
||
statsDiv.innerHTML = `
|
||
<span class="stats-label">登陆点:</span>
|
||
<span class="stats-value" id="landing-count">${validCount}个</span>`;
|
||
statsEl.appendChild(statsDiv);
|
||
} else {
|
||
document.getElementById('landing-count').textContent = validCount + '个';
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// 加载后备示例数据
|
||
function loadFallbackData() {
|
||
// 创建示例数据
|
||
const exampleData = {
|
||
"features": [
|
||
{
|
||
"geometry": {
|
||
"type": "Polygon",
|
||
"coordinates": [[
|
||
[-79.80487231205339, 25.84309713986656],
|
||
[-79.82884143894199, 25.852705623494085],
|
||
[-79.89715540216453, 25.90738622952227],
|
||
[-79.90297919991438, 25.91173310223529],
|
||
[-79.9330472713379, 25.933550569840435],
|
||
[-80.0379272651, 26.009899594453753],
|
||
[-80.07128581869685, 26.020839382223816]
|
||
]]
|
||
},
|
||
"id": 1,
|
||
"type": "Feature",
|
||
"properties": {
|
||
"owner": "AT&T Corp",
|
||
"cableName": "Americas II",
|
||
"region": "East Coast",
|
||
"SHAPE__Length": 108697.59906772723,
|
||
"shortname": "Americas II-12",
|
||
"objectid": 1,
|
||
"status": "In Service"
|
||
}
|
||
},
|
||
{
|
||
"geometry": {
|
||
"type": "Polygon",
|
||
"coordinates": [[
|
||
[-164.46138139455303, 54.31598789321679],
|
||
[-164.36341875726205, 54.3254748233416],
|
||
[-164.2416666229016, 54.34798813510413],
|
||
[-164.12076651633362, 54.3702811974151],
|
||
[-163.9997614308449, 54.39249307699343]
|
||
]]
|
||
},
|
||
"id": 2,
|
||
"type": "Feature",
|
||
"properties": {
|
||
"owner": "GCI Communication Corp",
|
||
"cableName": "AU Aleutian Submarine Cable System",
|
||
"region": "Pacific Northwest",
|
||
"SHAPE__Length": 1129434.7040758668,
|
||
"shortname": "AU-Aleutian A",
|
||
"objectid": 2,
|
||
"status": "In Service"
|
||
}
|
||
}
|
||
]
|
||
};
|
||
|
||
loadCablesFromGeoJSON(exampleData);
|
||
showStatusMessage('已加载示例数据', 'success');
|
||
}
|
||
|
||
// 将坐标转换为3D点
|
||
// 将坐标转换为3D点
|
||
// 将坐标转换为3D点
|
||
function calculateGreatCirclePoints(lat1, lon1, lat2, lon2, radius, segments = 50) {
|
||
const points = [];
|
||
|
||
// 将经纬度转换为弧度
|
||
const φ1 = lat1 * Math.PI / 180;
|
||
const λ1 = lon1 * Math.PI / 180;
|
||
const φ2 = lat2 * Math.PI / 180;
|
||
const λ2 = lon2 * Math.PI / 180;
|
||
|
||
// 计算两点间的角距离,考虑经度差
|
||
const dλ = Math.min(Math.abs(λ2 - λ1), 2 * Math.PI - Math.abs(λ2 - λ1));
|
||
const cosΔ = Math.sin(φ1) * Math.sin(φ2) + Math.cos(φ1) * Math.cos(φ2) * Math.cos(dλ);
|
||
|
||
// 防止浮点误差
|
||
let Δ = Math.acos(Math.max(-1, Math.min(1, cosΔ)));
|
||
|
||
// 如果两点太近,直接返回直线
|
||
if (Δ < 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 sinΔ = Math.sin(Δ);
|
||
const A = Math.sin((1 - t) * Δ) / sinΔ;
|
||
const B = Math.sin(t * Δ) / sinΔ;
|
||
|
||
// 计算插值点的笛卡尔坐标
|
||
const x1 = Math.cos(φ1) * Math.cos(λ1);
|
||
const y1 = Math.cos(φ1) * Math.sin(λ1);
|
||
const z1 = Math.sin(φ1);
|
||
|
||
const x2 = Math.cos(φ2) * Math.cos(λ2);
|
||
const y2 = Math.cos(φ2) * Math.sin(λ2);
|
||
const z2 = Math.sin(φ2);
|
||
|
||
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;
|
||
|
||
// 直接使用 latLonToVector3 的坐标系
|
||
// 从笛卡尔坐标计算经纬度,然后用 latLonToVector3 转换
|
||
const lat = Math.asin(z / radius) * 180 / Math.PI;
|
||
let lon = Math.atan2(y, x) * 180 / Math.PI;
|
||
|
||
// 调整经度范围到 -180 到 180
|
||
if (lon > 180) lon -= 360;
|
||
if (lon < -180) lon += 360;
|
||
|
||
const point = latLonToVector3(lat, lon, radius);
|
||
points.push(point);
|
||
}
|
||
|
||
return points;
|
||
}
|
||
|
||
|
||
// 从GeoJSON数据加载电缆
|
||
function loadCablesFromGeoJSON(geoJsonData) {
|
||
// 清除现有的电缆
|
||
cableLines.forEach(line => earth.remove(line));
|
||
cableLines = [];
|
||
|
||
// 更新电缆计数
|
||
const cableCount = geoJsonData.features.length;
|
||
document.getElementById('cable-count').textContent = cableCount + '个';
|
||
|
||
// 计算运行中的电缆数量
|
||
const inServiceCount = geoJsonData.features.filter(
|
||
feature => feature.properties && feature.properties.status === 'In Service'
|
||
).length;
|
||
|
||
document.getElementById('cable-status-summary').textContent =
|
||
`${inServiceCount}/${cableCount} 运行中`;
|
||
|
||
// 处理每个电缆特征
|
||
geoJsonData.features.forEach((feature, index) => {
|
||
if (!feature.geometry || !feature.geometry.coordinates) {
|
||
console.warn(`特征 ${index} 缺少几何数据`, feature);
|
||
return;
|
||
}
|
||
|
||
const coordinates = feature.geometry.coordinates[0];
|
||
if (!coordinates || coordinates.length === 0) {
|
||
console.warn(`特征 ${index} 缺少坐标数据`, feature);
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
// 处理 MultiLineString 类型
|
||
let allCoordinates = [];
|
||
|
||
// 处理 MultiLineString 类型
|
||
if (feature.geometry.type === 'MultiLineString') {
|
||
const segments = feature.geometry.coordinates;
|
||
console.log(`MultiLineString 电缆 ${index} 有 ${segments.length} 个线段`);
|
||
|
||
// 先分别处理每个线段,存储它们的起点和终点
|
||
const processedSegments = [];
|
||
|
||
segments.forEach((segment, segIndex) => {
|
||
if (segment.length < 2) return;
|
||
|
||
// 获取线段的起点和终点
|
||
const start = { lon: segment[0][0], lat: segment[0][1] };
|
||
const end = { lon: segment[segment.length - 1][0], lat: segment[segment.length - 1][1] };
|
||
|
||
processedSegments.push({
|
||
points: segment,
|
||
start: start,
|
||
end: end,
|
||
index: segIndex,
|
||
connected: false
|
||
});
|
||
});
|
||
|
||
if (processedSegments.length === 0) return;
|
||
|
||
// 尝试连接相邻的线段
|
||
const mergedSegments = [];
|
||
const used = new Array(processedSegments.length).fill(false);
|
||
|
||
for (let i = 0; i < processedSegments.length; i++) {
|
||
if (used[i]) continue;
|
||
|
||
let currentSegment = processedSegments[i];
|
||
used[i] = true;
|
||
|
||
// 尝试向后连接
|
||
let changed = true;
|
||
while (changed) {
|
||
changed = false;
|
||
|
||
for (let j = 0; j < processedSegments.length; j++) {
|
||
if (used[j]) continue;
|
||
|
||
const nextSegment = processedSegments[j];
|
||
|
||
// 计算当前线段终点与下一个线段起点的距离
|
||
const dist1 = calculateDistance(
|
||
currentSegment.end.lat, currentSegment.end.lon,
|
||
nextSegment.start.lat, nextSegment.start.lon
|
||
);
|
||
|
||
// 计算当前线段终点与下一个线段终点的距离
|
||
const dist2 = calculateDistance(
|
||
currentSegment.end.lat, currentSegment.end.lon,
|
||
nextSegment.end.lat, nextSegment.end.lon
|
||
);
|
||
|
||
// 计算当前线段起点与下一个线段起点的距离
|
||
const dist3 = calculateDistance(
|
||
currentSegment.start.lat, currentSegment.start.lon,
|
||
nextSegment.start.lat, nextSegment.start.lon
|
||
);
|
||
|
||
// 计算当前线段起点与下一个线段终点的距离
|
||
const dist4 = calculateDistance(
|
||
currentSegment.start.lat, currentSegment.start.lon,
|
||
nextSegment.end.lat, nextSegment.end.lon
|
||
);
|
||
|
||
const threshold = 5; // 距离阈值(度),小于这个值认为可以连接
|
||
|
||
// 如果终点和下一个起点很近,正向连接
|
||
if (dist1 < threshold) {
|
||
// 合并线段
|
||
const mergedPoints = [...currentSegment.points, ...nextSegment.points];
|
||
currentSegment = {
|
||
points: mergedPoints,
|
||
start: currentSegment.start,
|
||
end: nextSegment.end,
|
||
index: currentSegment.index,
|
||
connected: true
|
||
};
|
||
used[j] = true;
|
||
changed = true;
|
||
console.log(`连接线段 ${currentSegment.index} 和 ${nextSegment.index} (正向)`);
|
||
}
|
||
// 如果终点和下一个终点很近,需要反转下一个线段
|
||
else if (dist2 < threshold) {
|
||
// 反转下一个线段再合并
|
||
const reversedPoints = [...nextSegment.points].reverse();
|
||
const mergedPoints = [...currentSegment.points, ...reversedPoints];
|
||
currentSegment = {
|
||
points: mergedPoints,
|
||
start: currentSegment.start,
|
||
end: nextSegment.start,
|
||
index: currentSegment.index,
|
||
connected: true
|
||
};
|
||
used[j] = true;
|
||
changed = true;
|
||
console.log(`连接线段 ${currentSegment.index} 和 ${nextSegment.index} (反向)`);
|
||
}
|
||
// 如果起点和下一个起点很近,需要反转当前线段
|
||
else if (dist3 < threshold) {
|
||
// 反转当前线段再合并
|
||
const reversedCurrent = [...currentSegment.points].reverse();
|
||
const mergedPoints = [...reversedCurrent, ...nextSegment.points];
|
||
currentSegment = {
|
||
points: mergedPoints,
|
||
start: currentSegment.end,
|
||
end: nextSegment.end,
|
||
index: currentSegment.index,
|
||
connected: true
|
||
};
|
||
used[j] = true;
|
||
changed = true;
|
||
console.log(`连接线段 ${currentSegment.index} 和 ${nextSegment.index} (反转当前)`);
|
||
}
|
||
// 如果起点和下一个终点很近,需要反转两者之一
|
||
else if (dist4 < threshold) {
|
||
// 反转下一个线段
|
||
const reversedNext = [...nextSegment.points].reverse();
|
||
const reversedCurrent = [...currentSegment.points].reverse();
|
||
const mergedPoints = [...reversedCurrent, ...reversedNext];
|
||
currentSegment = {
|
||
points: mergedPoints,
|
||
start: currentSegment.start,
|
||
end: nextSegment.start,
|
||
index: currentSegment.index,
|
||
connected: true
|
||
};
|
||
used[j] = true;
|
||
changed = true;
|
||
console.log(`连接线段 ${currentSegment.index} 和 ${nextSegment.index} (双反转)`);
|
||
}
|
||
}
|
||
}
|
||
|
||
mergedSegments.push(currentSegment);
|
||
}
|
||
|
||
console.log(`合并后剩余 ${mergedSegments.length} 个线段`);
|
||
|
||
// 分别处理每个合并后的线段
|
||
mergedSegments.forEach((segment, segIdx) => {
|
||
const points = [];
|
||
const segmentPoints = segment.points;
|
||
|
||
// 处理当前线段的连续点对
|
||
for (let i = 0; i < segmentPoints.length - 1; i++) {
|
||
const coord1 = segmentPoints[i];
|
||
const coord2 = segmentPoints[i + 1];
|
||
|
||
const lon1 = coord1[0];
|
||
const lat1 = coord1[1];
|
||
const lon2 = coord2[0];
|
||
const lat2 = coord2[1];
|
||
|
||
// 检查是否跨过 180° 经线
|
||
const lonDiff = Math.abs(lon2 - lon1);
|
||
if (lonDiff > 180) {
|
||
// 处理跨经线的情况
|
||
const midLon = lon2 > lon1 ? 180 : -180;
|
||
const midLat = (lat1 + lat2) / 2;
|
||
|
||
const segment1 = calculateGreatCirclePoints(lat1, lon1, midLat, midLon, 100.2, 50);
|
||
const segment2 = calculateGreatCirclePoints(midLat, midLon, lat2, lon2, 100.2, 50);
|
||
|
||
if (i === 0 && points.length === 0) {
|
||
points.push(...segment1);
|
||
points.push(...segment2.slice(1));
|
||
} else {
|
||
points.push(...segment1.slice(1));
|
||
points.push(...segment2.slice(1));
|
||
}
|
||
} else {
|
||
const segment = calculateGreatCirclePoints(lat1, lon1, lat2, lon2, 100.2, 50);
|
||
|
||
if (i === 0 && points.length === 0) {
|
||
points.push(...segment);
|
||
} else {
|
||
points.push(...segment.slice(1));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 为当前合并后的线段创建电缆线
|
||
if (points.length >= 2) {
|
||
createCableLine(points, feature.properties, index, segIdx);
|
||
}
|
||
});
|
||
|
||
return; // 已经处理完,跳过后面的代码
|
||
|
||
} else if (feature.geometry.type === 'LineString') {
|
||
// 如果是 LineString,直接使用
|
||
allCoordinates = feature.geometry.coordinates;
|
||
} else {
|
||
console.warn(`不支持的几何类型: ${feature.geometry.type}`);
|
||
return;
|
||
}
|
||
|
||
if (!allCoordinates || allCoordinates.length === 0) {
|
||
console.warn(`特征 ${index} 缺少坐标数据`, feature);
|
||
return;
|
||
}
|
||
|
||
const points = [];
|
||
const segmentsCount = 50; // 每条线段的分段数,越大曲线越平滑
|
||
|
||
// 处理连续的点对,生成大圆路径
|
||
for (let i = 0; i < allCoordinates.length - 1; i++) {
|
||
const coord1 = allCoordinates[i];
|
||
const coord2 = allCoordinates[i + 1];
|
||
|
||
const lon1 = coord1[0];
|
||
const lat1 = coord1[1];
|
||
const lon2 = coord2[0];
|
||
const lat2 = coord2[1];
|
||
|
||
// 检查是否跨过 180° 经线
|
||
const lonDiff = Math.abs(lon2 - lon1);
|
||
if (lonDiff > 180) {
|
||
// 处理跨经线的情况
|
||
// 分成两段处理
|
||
const midLon = lon2 > lon1 ? 180 : -180;
|
||
const midLat = (lat1 + lat2) / 2;
|
||
|
||
// 第一段:从 lon1 到 midLon
|
||
const segment1 = calculateGreatCirclePoints(lat1, lon1, midLat, midLon, 100.2, segmentsCount);
|
||
// 第二段:从 midLon 到 lon2
|
||
const segment2 = calculateGreatCirclePoints(midLat, midLon, lat2, lon2, 100.2, segmentsCount);
|
||
|
||
// 添加点,避免重复
|
||
if (i === 0) {
|
||
points.push(...segment1);
|
||
points.push(...segment2.slice(1));
|
||
} else {
|
||
points.push(...segment1.slice(1));
|
||
points.push(...segment2.slice(1));
|
||
}
|
||
} else {
|
||
// 正常情况,生成大圆路径
|
||
const segment = calculateGreatCirclePoints(lat1, lon1, lat2, lon2, 100.2, segmentsCount);
|
||
|
||
if (i === 0) {
|
||
points.push(...segment);
|
||
} else {
|
||
points.push(...segment.slice(1));
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
});
|
||
|
||
// 隐藏加载提示
|
||
document.getElementById('loading').style.display = 'none';
|
||
|
||
console.log(`成功加载 ${cableCount} 条电缆数据`);
|
||
}
|
||
|
||
//创建电缆线的辅助函数
|
||
function createCableLine(points, properties, featureIndex, segmentIndex) {
|
||
if (points.length < 2) return;
|
||
|
||
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
|
||
|
||
let cableColor = cableColors.default;
|
||
|
||
if (properties.color) {
|
||
if (typeof properties.color === 'string' && properties.color.startsWith('#')) {
|
||
cableColor = parseInt(properties.color.substring(1), 16);
|
||
} else if (typeof properties.color === 'number') {
|
||
cableColor = properties.color;
|
||
}
|
||
} else {
|
||
const cableName = properties.cableName || properties.shortname || '';
|
||
if (cableName.includes('Americas II')) {
|
||
cableColor = cableColors['Americas II'];
|
||
} else if (cableName.includes('AU Aleutian A')) {
|
||
cableColor = cableColors['AU Aleutian A'];
|
||
} else if (cableName.includes('AU Aleutian B')) {
|
||
cableColor = cableColors['AU Aleutian B'];
|
||
}
|
||
}
|
||
|
||
const lineMaterial = new THREE.LineBasicMaterial({
|
||
color: cableColor,
|
||
linewidth: 3,
|
||
transparent: true,
|
||
opacity: 0.9,
|
||
depthTest: true,
|
||
depthWrite: false
|
||
});
|
||
|
||
const cableLine = new THREE.Line(lineGeometry, lineMaterial);
|
||
cableLine.userData = { ...properties, segmentIndex: segmentIndex };
|
||
cableLine.userData.index = featureIndex;
|
||
cableLine.renderOrder = 1;
|
||
|
||
earth.add(cableLine);
|
||
cableLines.push(cableLine);
|
||
cableLine.userData.originalColor = cableColor;
|
||
}
|
||
|
||
|
||
// 设置事件监听器
|
||
function setupEventListeners() {
|
||
const raycaster = new THREE.Raycaster();
|
||
const mouse = new THREE.Vector2();
|
||
|
||
// 鼠标移动事件
|
||
function onMouseMove(event) {
|
||
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||
|
||
// 更新坐标显示
|
||
updateCoordinates(event.clientX, event.clientY);
|
||
|
||
// 更新射线投射器
|
||
raycaster.setFromCamera(mouse, camera);
|
||
|
||
// 检查与电缆线的交点
|
||
const intersects = raycaster.intersectObjects(cableLines);
|
||
|
||
// 重置所有电缆线颜色
|
||
cableLines.forEach(line => {
|
||
line.material.color.setHex(line.userData.originalColor);
|
||
line.material.opacity = 0.8;
|
||
});
|
||
|
||
// 如果有交点,高亮显示电缆线
|
||
if (intersects.length > 0) {
|
||
const cable = intersects[0].object;
|
||
cable.material.color.setHex(0xffffff);
|
||
cable.material.opacity = 1;
|
||
|
||
// 更新信息面板
|
||
const userData = cable.userData;
|
||
document.getElementById('cable-name').textContent =
|
||
userData.Name || userData.name || userData.shortname || userData.cablesystem || '未命名电缆';
|
||
|
||
document.getElementById('cable-owner').textContent = userData.owner || userData.owners || '未知';
|
||
document.getElementById('cable-status').textContent = userData.status || '未知';
|
||
|
||
if (userData.SHAPE__Length || userData.length) {
|
||
const length = userData.SHAPE__Length || userData.length;
|
||
//document.getElementById('cable-length').textContent = (length / 1000).toFixed(2) + ' km';
|
||
document.getElementById('cable-length').textContent = length ;
|
||
} else {
|
||
document.getElementById('cable-length').textContent = '未知';
|
||
}
|
||
document.getElementById('cable-rfs').textContent = userData.rfs || userData.RFS || '未知';
|
||
// 显示电缆的经纬度
|
||
if (userData.startCoords && userData.endCoords) {
|
||
const start = userData.startCoords;
|
||
const end = userData.endCoords;
|
||
|
||
// 检查 start 和 end 对象是否存在且包含有效的 lat/lon
|
||
if (start && end &&
|
||
typeof start.lat === 'number' && typeof start.lon === 'number' &&
|
||
typeof end.lat === 'number' && typeof end.lon === 'number' &&
|
||
!isNaN(start.lat) && !isNaN(start.lon) &&
|
||
!isNaN(end.lat) && !isNaN(end.lon)) {
|
||
|
||
document.getElementById('cable-coords').textContent =
|
||
`${start.lat.toFixed(2)}°, ${start.lon.toFixed(2)}° 到 ${end.lat.toFixed(2)}°, ${end.lon.toFixed(2)}°`;
|
||
} else {
|
||
document.getElementById('cable-coords').textContent = '坐标数据无效';
|
||
}
|
||
} else {
|
||
document.getElementById('cable-coords').textContent = '未知';
|
||
}
|
||
} else {
|
||
// 如果没有交点,恢复默认信息
|
||
document.getElementById('cable-name').textContent = '点击电缆查看详情';
|
||
document.getElementById('cable-owner').textContent = '-';
|
||
document.getElementById('cable-status').textContent = '-';
|
||
document.getElementById('cable-length').textContent = '-';
|
||
document.getElementById('cable-coords').textContent = '-';
|
||
document.getElementById('cable-rfs').textContent = '-'; // 新增这行
|
||
}
|
||
|
||
|
||
// 如果正在拖拽,更新目标旋转
|
||
if (isDragging) {
|
||
const deltaMove = {
|
||
x: event.clientX - previousMousePosition.x,
|
||
y: event.clientY - previousMousePosition.y
|
||
};
|
||
|
||
targetRotationY += deltaMove.x * 0.005;
|
||
targetRotationX += deltaMove.y * 0.005;
|
||
|
||
previousMousePosition = {
|
||
x: event.clientX,
|
||
y: event.clientY
|
||
};
|
||
}
|
||
}
|
||
|
||
// 鼠标按下事件
|
||
function onMouseDown(event) {
|
||
isDragging = true;
|
||
previousMousePosition = {
|
||
x: event.clientX,
|
||
y: event.clientY
|
||
};
|
||
hideTooltip();
|
||
}
|
||
|
||
// 鼠标松开事件
|
||
function onMouseUp() {
|
||
isDragging = false;
|
||
}
|
||
|
||
// 鼠标离开画布
|
||
function onMouseLeave() {
|
||
hideTooltip();
|
||
document.getElementById('mouse-coords').textContent = '鼠标位置: 无';
|
||
}
|
||
|
||
// 滚轮缩放
|
||
function onMouseWheel(event) {
|
||
event.preventDefault();
|
||
|
||
const zoomFactor = 0.1;
|
||
if (event.deltaY < 0) {
|
||
// 向上滚动,放大
|
||
zoomLevel = Math.min(maxZoom, zoomLevel + zoomFactor);
|
||
} else {
|
||
// 向下滚动,缩小
|
||
zoomLevel = Math.max(minZoom, zoomLevel - zoomFactor);
|
||
}
|
||
|
||
applyZoom();
|
||
}
|
||
|
||
// 添加事件监听器
|
||
document.addEventListener('mousemove', onMouseMove, false);
|
||
document.addEventListener('mousedown', onMouseDown, false);
|
||
document.addEventListener('mouseup', onMouseUp, false);
|
||
document.addEventListener('mouseleave', onMouseLeave, false);
|
||
document.addEventListener('wheel', onMouseWheel, { passive: false });
|
||
|
||
// 窗口大小调整
|
||
function onWindowResize() {
|
||
camera.aspect = window.innerWidth / window.innerHeight;
|
||
camera.updateProjectionMatrix();
|
||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
}
|
||
|
||
window.addEventListener('resize', onWindowResize, false);
|
||
|
||
// 控制按钮事件
|
||
document.getElementById('rotate-toggle').addEventListener('click', function() {
|
||
autoRotate = !autoRotate;
|
||
this.textContent = autoRotate ? '暂停旋转' : '开始旋转';
|
||
});
|
||
document.getElementById('reset-view').addEventListener('click', function() {
|
||
// 中国中心大致经纬度:北纬35°,东经105°
|
||
const chinaLat = 40; // 纬度
|
||
const chinaLon = 116-270; // 经度 108+90-360
|
||
|
||
// 将经纬度转换为旋转角度
|
||
// 注意:Three.js 中,经度对应 Y 轴旋转,纬度对应 X 轴旋转
|
||
// 经度:东经为正,需要转换为弧度,并且要调整方向
|
||
const targetY = -(chinaLon * Math.PI / 180); // 负号让中国朝向相机
|
||
|
||
// 纬度:北纬为正,转换为弧度,但要考虑地球倾斜角
|
||
// 地球本身有 23.5° 倾斜,所以目标 X 旋转需要加上倾斜角
|
||
const baseTilt = 23.5 * Math.PI / 180; // 地球固定倾斜
|
||
const latRot = chinaLat * Math.PI / 180; // 纬度旋转
|
||
|
||
// 目标 X 旋转 = 基础倾斜 + 纬度调整
|
||
// 注意:纬度越高,需要旋转的角度越大
|
||
targetRotationX = baseTilt + latRot * 0.5; // 0.5 是调整系数,让视觉效果更好
|
||
|
||
// Y 轴旋转:让中国朝向相机
|
||
targetRotationY = targetY;
|
||
|
||
// 重置缩放
|
||
zoomLevel = 1.0;
|
||
applyZoom();
|
||
|
||
// 立即应用旋转
|
||
earth.rotation.x = targetRotationX;
|
||
earth.rotation.y = targetRotationY;
|
||
|
||
// 同时重置网格线的旋转
|
||
latitudeLines.forEach(line => {
|
||
line.rotation.x = earth.rotation.x;
|
||
line.rotation.y = earth.rotation.y;
|
||
});
|
||
longitudeLines.forEach(line => {
|
||
line.rotation.x = earth.rotation.x;
|
||
line.rotation.y = earth.rotation.y;
|
||
});
|
||
|
||
console.log('视角已重置到中国区域');
|
||
console.log('旋转角度:', {
|
||
x: (targetRotationX * 180 / Math.PI).toFixed(1) + '°',
|
||
y: (targetRotationY * 180 / Math.PI).toFixed(1) + '°'
|
||
});
|
||
});
|
||
|
||
|
||
document.getElementById('reload-data').addEventListener('click', function() {
|
||
document.getElementById('loading').style.display = 'block';
|
||
document.getElementById('error-message').style.display = 'none';
|
||
loadGeoJSONFromPath();
|
||
});
|
||
|
||
|
||
|
||
// 缩放按钮事件
|
||
document.getElementById('zoom-in').addEventListener('click', function() {
|
||
zoomLevel = Math.min(maxZoom, zoomLevel + 0.5);
|
||
applyZoom();
|
||
});
|
||
|
||
document.getElementById('zoom-out').addEventListener('click', function() {
|
||
zoomLevel = Math.max(minZoom, zoomLevel - 0.5);
|
||
applyZoom();
|
||
});
|
||
|
||
document.getElementById('zoom-reset').addEventListener('click', function() {
|
||
zoomLevel = 1.0;
|
||
applyZoom();
|
||
});
|
||
|
||
// 缩放滑块事件
|
||
document.getElementById('zoom-slider').addEventListener('input', function() {
|
||
zoomLevel = parseFloat(this.value);
|
||
applyZoom();
|
||
});
|
||
}
|
||
|
||
|
||
// 动画循环
|
||
function animate() {
|
||
requestAnimationFrame(animate);
|
||
|
||
// 地球自转
|
||
if (autoRotate) {
|
||
// 只旋转地球本身
|
||
earth.rotation.y += rotationSpeed;
|
||
}
|
||
// 鼠标拖拽控制(不影响自动旋转)
|
||
if (isDragging) {
|
||
// 拖拽时,在 onMouseMove 中已经更新了 targetRotation
|
||
// 这里只需要平滑过渡
|
||
earth.rotation.x += (targetRotationX - earth.rotation.x) * 0.05;
|
||
earth.rotation.y += (targetRotationY - earth.rotation.y) * 0.05;
|
||
}
|
||
// 所有子对象跟随地球旋转(因为它们都是 earth 的子对象)
|
||
// 不需要额外代码,因为子对象会自动跟随父对象旋转
|
||
|
||
// 但网格线不是 earth 的子对象,需要手动跟随
|
||
latitudeLines.forEach(line => {
|
||
line.rotation.x = earth.rotation.x;
|
||
line.rotation.y = earth.rotation.y;
|
||
});
|
||
longitudeLines.forEach(line => {
|
||
line.rotation.x = earth.rotation.x;
|
||
line.rotation.y = earth.rotation.y;
|
||
});
|
||
|
||
// 渲染场景
|
||
renderer.render(scene, camera);
|
||
}
|
||
|
||
// 初始化应用
|
||
window.addEventListener('DOMContentLoaded', init);
|
||
</script>
|
||
</body>
|
||
</html> |