Files
planet/frontend/legacy/3dearthmult/3dearthmult.html
rayd1o c2eba54da0 refactor: 整理资源文件,添加legacy路由
- 将原版文件移到frontend/legacy/3dearthmult/
- 纹理文件移到frontend/public/earth/assets/
- vite.config添加/legacy/earth路由支持
- earth.js纹理路径改为assets/
2026-03-19 11:10:33 +08:00

1912 lines
75 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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.80=完全透明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 = 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();
// 防止浮点误差
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>