release: bump version to 0.36.0
This commit is contained in:
@@ -70,6 +70,13 @@ body.earth-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html.is-globe-dragging,
|
||||
body.earth-page.is-globe-dragging,
|
||||
body.earth-page.is-globe-dragging * {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
}
|
||||
|
||||
.earth-app {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
|
||||
@@ -132,6 +132,16 @@
|
||||
<span class="layer-row-toggle-track"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="layer-row" data-layer-name="算力中心 compute centers">
|
||||
<span class="material-symbols-rounded layer-row-icon">memory</span>
|
||||
<div class="layer-row-copy">
|
||||
<span class="layer-row-label">算力中心</span>
|
||||
<span class="layer-row-meta">Compute Centers</span>
|
||||
</div>
|
||||
<button id="toggle-compute-centers" class="layer-row-toggle active" type="button" role="switch" aria-checked="true" title="切换算力中心显示">
|
||||
<span class="layer-row-toggle-track"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="layer-row" data-layer-name="bgp观测 routing signals">
|
||||
<span class="material-symbols-rounded layer-row-icon">hub</span>
|
||||
<div class="layer-row-copy">
|
||||
@@ -293,6 +303,10 @@
|
||||
<span class="stat-num" id="satellite-count" data-earth-stat="satellite-count">—</span>
|
||||
<span class="stat-label">在轨卫星</span>
|
||||
</div>
|
||||
<div class="stat-cell">
|
||||
<span class="stat-num" id="compute-center-count" data-earth-stat="compute-center-count">—</span>
|
||||
<span class="stat-label">算力中心</span>
|
||||
</div>
|
||||
<div class="stat-cell">
|
||||
<span class="stat-num" id="bgp-anomaly-count" data-earth-stat="bgp-anomaly-count">—</span>
|
||||
<span class="stat-label">BGP 事件</span>
|
||||
@@ -456,7 +470,7 @@
|
||||
<div class="earth-mobile-page earth-mobile-page--search">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">Object Search</span>
|
||||
<span class="earth-mobile-page-summary">搜索海缆、登陆点、卫星和 BGP 事件</span>
|
||||
<span class="earth-mobile-page-summary">搜索海缆、登陆点、卫星、算力中心和 BGP 事件</span>
|
||||
</div>
|
||||
<div class="earth-mobile-search-shell">
|
||||
<span class="material-symbols-rounded earth-mobile-search-icon" aria-hidden="true">search</span>
|
||||
@@ -475,7 +489,7 @@
|
||||
</div>
|
||||
<div id="mobile-earth-search-meta" class="earth-mobile-search-meta">输入关键词以搜索当前地球对象</div>
|
||||
<div id="mobile-earth-search-results" class="earth-mobile-search-results" role="listbox" aria-label="移动端搜索结果"></div>
|
||||
<div id="mobile-earth-search-empty" class="earth-mobile-search-empty">支持搜索海缆、登陆点、卫星、BGP 事件与观测站。</div>
|
||||
<div id="mobile-earth-search-empty" class="earth-mobile-search-empty">支持搜索海缆、登陆点、卫星、算力中心、BGP 事件与观测站。</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="earth-mobile-drawer-slot earth-mobile-drawer-slot--situation" data-drawer-slot="situation">
|
||||
@@ -497,6 +511,10 @@
|
||||
<span id="mobile-satellite-count" class="earth-mobile-stat-num" data-earth-stat="satellite-count">—</span>
|
||||
<span class="earth-mobile-stat-label">在轨卫星</span>
|
||||
</div>
|
||||
<div class="earth-mobile-stat-card">
|
||||
<span id="mobile-compute-center-count" class="earth-mobile-stat-num" data-earth-stat="compute-center-count">—</span>
|
||||
<span class="earth-mobile-stat-label">算力中心</span>
|
||||
</div>
|
||||
<div class="earth-mobile-stat-card">
|
||||
<span id="mobile-bgp-anomaly-count" class="earth-mobile-stat-num" data-earth-stat="bgp-anomaly-count">—</span>
|
||||
<span class="earth-mobile-stat-label">BGP 事件</span>
|
||||
@@ -666,7 +684,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-info-card-content" class="earth-mobile-detail-content">
|
||||
<div class="earth-mobile-detail-empty">点击海缆、BGP 事件或卫星后在这里查看详情。</div>
|
||||
<div class="earth-mobile-detail-empty">点击海缆、算力中心、BGP 事件或卫星后在这里查看详情。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -697,7 +715,7 @@
|
||||
inputmode="search"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder="搜索海缆、登陆点、卫星、BGP 事件..."
|
||||
placeholder="搜索海缆、登陆点、卫星、算力中心、BGP 事件..."
|
||||
>
|
||||
<button id="earth-search-clear" class="earth-search-clear hud-panel__action" type="button" aria-label="清除搜索" hidden>
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
@@ -705,7 +723,7 @@
|
||||
</div>
|
||||
<div id="earth-search-meta" class="earth-search-meta">输入关键词以搜索当前地球对象</div>
|
||||
<div id="earth-search-results" class="earth-search-results" role="listbox" aria-label="搜索结果"></div>
|
||||
<div id="earth-search-empty" class="earth-search-empty">支持搜索海缆、登陆点、卫星、BGP 事件与观测站。</div>
|
||||
<div id="earth-search-empty" class="earth-search-empty">支持搜索海缆、登陆点、卫星、算力中心、BGP 事件与观测站。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
334
frontend/public/earth/js/compute-centers.js
Normal file
334
frontend/public/earth/js/compute-centers.js
Normal file
@@ -0,0 +1,334 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import { COMPUTE_CENTER_CONFIG, CONFIG, PATHS } from "./constants.js";
|
||||
import { latLonToVector3 } from "./utils.js";
|
||||
|
||||
const computeCenterGroup = new THREE.Group();
|
||||
const computeCenterMarkers = [];
|
||||
const textureCache = new Map();
|
||||
const scratchMarkerWorldPosition = new THREE.Vector3();
|
||||
|
||||
let showComputeCenters = true;
|
||||
let supercomputerCount = 0;
|
||||
let gpuClusterCount = 0;
|
||||
|
||||
function createMarkerTexture(siteType, isEstimated = false) {
|
||||
const textureKey = `${siteType}:${isEstimated ? "estimated" : "precise"}`;
|
||||
if (textureCache.has(textureKey)) {
|
||||
return textureCache.get(textureKey);
|
||||
}
|
||||
|
||||
const color =
|
||||
COMPUTE_CENTER_CONFIG.colors[siteType] ||
|
||||
COMPUTE_CENTER_CONFIG.colors.gpu_cluster;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const context = canvas.getContext("2d");
|
||||
const centerX = 64;
|
||||
const centerY = 64;
|
||||
const baseFill = color;
|
||||
|
||||
function fillPath(draw, options = {}) {
|
||||
const { fillStyle = color } = options;
|
||||
context.save();
|
||||
context.fillStyle = fillStyle;
|
||||
context.beginPath();
|
||||
draw();
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
|
||||
context.clearRect(0, 0, 128, 128);
|
||||
|
||||
if (siteType === "supercomputer") {
|
||||
fillPath(() => {
|
||||
context.roundRect(40, 42, 48, 30, 7);
|
||||
}, {
|
||||
fillStyle: baseFill,
|
||||
});
|
||||
fillPath(() => {
|
||||
context.roundRect(58, 74, 12, 8, 3);
|
||||
context.roundRect(50, 84, 28, 5, 2.5);
|
||||
}, {
|
||||
fillStyle: baseFill,
|
||||
});
|
||||
} else {
|
||||
fillPath(() => {
|
||||
context.ellipse(centerX, 46, 18, 8, 0, 0, Math.PI * 2);
|
||||
context.rect(46, 46, 36, 28);
|
||||
context.ellipse(centerX, 74, 18, 8, 0, 0, Math.PI);
|
||||
}, {
|
||||
fillStyle: baseFill,
|
||||
});
|
||||
fillPath(() => {
|
||||
context.ellipse(centerX, 58, 12, 4.5, 0, 0, Math.PI * 2);
|
||||
context.rect(52, 58, 24, 6);
|
||||
context.ellipse(centerX, 64, 12, 4.5, 0, 0, Math.PI);
|
||||
}, {
|
||||
fillStyle: baseFill,
|
||||
});
|
||||
}
|
||||
|
||||
if (isEstimated) {
|
||||
fillPath(() => {
|
||||
context.arc(94, 36, 12, 0, Math.PI * 2);
|
||||
}, {
|
||||
fillStyle: "rgba(15,23,42,0.92)",
|
||||
});
|
||||
context.save();
|
||||
context.fillStyle = "rgba(255,255,255,0.98)";
|
||||
context.font = "bold 18px sans-serif";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.fillText("?", 94, 36);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.needsUpdate = true;
|
||||
textureCache.set(textureKey, texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
function normalizeSiteType(siteType) {
|
||||
return siteType === "supercomputer" ? "supercomputer" : "gpu_cluster";
|
||||
}
|
||||
|
||||
function getBaseScale(siteType) {
|
||||
return siteType === "supercomputer"
|
||||
? COMPUTE_CENTER_CONFIG.marker.supercomputerScale
|
||||
: COMPUTE_CENTER_CONFIG.marker.gpuClusterScale;
|
||||
}
|
||||
|
||||
function getDistanceScale(marker, camera) {
|
||||
if (!marker || !camera || COMPUTE_CENTER_CONFIG.sizeStabilization.enabled === false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const distanceToCamera = camera.position.distanceTo(
|
||||
marker.getWorldPosition(scratchMarkerWorldPosition),
|
||||
);
|
||||
const referenceDistance =
|
||||
CONFIG.defaultCameraZ - CONFIG.earthRadius + COMPUTE_CENTER_CONFIG.altitudeOffset;
|
||||
const referenceFovRad = (75 * Math.PI) / 180;
|
||||
const cameraFovRad = ((camera.fov || 75) * Math.PI) / 180;
|
||||
const worldPerPixel = distanceToCamera * Math.tan(cameraFovRad / 2);
|
||||
const referenceWorldPerPixel = referenceDistance * Math.tan(referenceFovRad / 2);
|
||||
const scale = worldPerPixel / referenceWorldPerPixel;
|
||||
return THREE.MathUtils.clamp(
|
||||
scale,
|
||||
COMPUTE_CENTER_CONFIG.sizeStabilization.min,
|
||||
COMPUTE_CENTER_CONFIG.sizeStabilization.max,
|
||||
);
|
||||
}
|
||||
|
||||
function clearGroup(group) {
|
||||
for (let index = group.children.length - 1; index >= 0; index -= 1) {
|
||||
const child = group.children[index];
|
||||
child.material?.dispose?.();
|
||||
group.remove(child);
|
||||
}
|
||||
}
|
||||
|
||||
function createComputeCenterMarker(feature) {
|
||||
const props = feature?.properties || {};
|
||||
const coordinates = feature?.geometry?.coordinates || [];
|
||||
const longitude = Number(coordinates[0]);
|
||||
const latitude = Number(coordinates[1]);
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const siteType = normalizeSiteType(props.site_type);
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: createMarkerTexture(siteType, Boolean(props.is_estimated)),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
opacity: COMPUTE_CENTER_CONFIG.marker.baseOpacity,
|
||||
});
|
||||
const marker = new THREE.Sprite(material);
|
||||
const baseScale = getBaseScale(siteType);
|
||||
marker.position.copy(
|
||||
latLonToVector3(
|
||||
latitude,
|
||||
longitude,
|
||||
CONFIG.earthRadius + COMPUTE_CENTER_CONFIG.altitudeOffset,
|
||||
),
|
||||
);
|
||||
marker.scale.setScalar(baseScale);
|
||||
marker.renderOrder = 8;
|
||||
marker.visible = showComputeCenters;
|
||||
marker.userData = {
|
||||
...props,
|
||||
latitude,
|
||||
longitude,
|
||||
site_type: siteType,
|
||||
type: "compute_center",
|
||||
baseScale,
|
||||
state: "normal",
|
||||
pulseOffset: Math.random() * Math.PI * 2,
|
||||
};
|
||||
computeCenterGroup.add(marker);
|
||||
computeCenterMarkers.push(marker);
|
||||
return marker;
|
||||
}
|
||||
|
||||
export function formatComputeCenterTypeLabel(siteType) {
|
||||
return siteType === "supercomputer" ? "超算中心" : "GPU 集群";
|
||||
}
|
||||
|
||||
export function formatComputeCenterCapacity(markerData) {
|
||||
const value = markerData?.capacity_value;
|
||||
const unit = markerData?.capacity_unit;
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
return `${value}${unit ? ` ${unit}` : ""}`;
|
||||
}
|
||||
|
||||
export function formatComputeCenterUpdatedAt(value) {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value);
|
||||
return date.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
export function formatComputeCenterLocationPrecision(markerData) {
|
||||
const precision = markerData?.location_precision;
|
||||
if (precision === "precise") return "精确坐标";
|
||||
if (precision === "estimated_site") return "估算位置(站点级)";
|
||||
if (precision === "estimated_country") return "估算位置(国家级)";
|
||||
return "位置未知";
|
||||
}
|
||||
|
||||
export function getComputeCenterLegendItems() {
|
||||
return [
|
||||
{
|
||||
label: "超算中心",
|
||||
color: COMPUTE_CENTER_CONFIG.colors.supercomputer,
|
||||
},
|
||||
{
|
||||
label: "GPU 集群",
|
||||
color: COMPUTE_CENTER_CONFIG.colors.gpu_cluster,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getComputeCenterMarkers() {
|
||||
return computeCenterMarkers;
|
||||
}
|
||||
|
||||
export function getComputeCenterCount() {
|
||||
return computeCenterMarkers.length;
|
||||
}
|
||||
|
||||
export function getComputeCenterSupercomputerCount() {
|
||||
return supercomputerCount;
|
||||
}
|
||||
|
||||
export function getComputeCenterGPUClusterCount() {
|
||||
return gpuClusterCount;
|
||||
}
|
||||
|
||||
export function getComputeCenterStatusSummary() {
|
||||
if (computeCenterMarkers.length === 0) return "暂无算力中心数据";
|
||||
return `${supercomputerCount} 台超算 / ${gpuClusterCount} 个 GPU 集群`;
|
||||
}
|
||||
|
||||
export function setComputeCenterMarkerState(marker, state = "normal") {
|
||||
if (!marker || marker.userData?.type !== "compute_center") return;
|
||||
marker.userData.state = state;
|
||||
}
|
||||
|
||||
export function clearComputeCenterSelection() {
|
||||
computeCenterMarkers.forEach((marker) => setComputeCenterMarkerState(marker, "normal"));
|
||||
}
|
||||
|
||||
export function clearComputeCenterData(earth) {
|
||||
computeCenterMarkers.length = 0;
|
||||
supercomputerCount = 0;
|
||||
gpuClusterCount = 0;
|
||||
clearGroup(computeCenterGroup);
|
||||
if (earth && computeCenterGroup.parent === earth) {
|
||||
earth.remove(computeCenterGroup);
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleComputeCenters(show) {
|
||||
showComputeCenters = Boolean(show);
|
||||
computeCenterGroup.visible = showComputeCenters;
|
||||
computeCenterMarkers.forEach((marker) => {
|
||||
marker.visible = showComputeCenters;
|
||||
});
|
||||
}
|
||||
|
||||
export function getShowComputeCenters() {
|
||||
return showComputeCenters;
|
||||
}
|
||||
|
||||
export async function loadComputeCenters(_scene, earth) {
|
||||
const response = await fetch(PATHS.computeCentersApi);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Compute centers HTTP ${response.status}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
const features = Array.isArray(payload?.features) ? payload.features : [];
|
||||
|
||||
clearComputeCenterData(earth);
|
||||
|
||||
features
|
||||
.slice(0, COMPUTE_CENTER_CONFIG.maxRenderedMarkers)
|
||||
.forEach((feature) => {
|
||||
const marker = createComputeCenterMarker(feature);
|
||||
if (!marker) return;
|
||||
if (marker.userData.site_type === "supercomputer") {
|
||||
supercomputerCount += 1;
|
||||
} else {
|
||||
gpuClusterCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (earth && !computeCenterGroup.parent) {
|
||||
earth.add(computeCenterGroup);
|
||||
}
|
||||
computeCenterGroup.visible = showComputeCenters;
|
||||
|
||||
return {
|
||||
totalCount: computeCenterMarkers.length,
|
||||
supercomputerCount,
|
||||
gpuClusterCount,
|
||||
summary: getComputeCenterStatusSummary(),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateComputeCenterVisualState(lockedObjectType, lockedObject, camera) {
|
||||
const hasFocus = lockedObjectType === "compute_center" && lockedObject;
|
||||
const now = Date.now();
|
||||
|
||||
computeCenterMarkers.forEach((marker) => {
|
||||
const isLocked = lockedObjectType === "compute_center" && lockedObject === marker;
|
||||
const state = marker.userData?.state || "normal";
|
||||
const pulse =
|
||||
1 +
|
||||
COMPUTE_CENTER_CONFIG.marker.pulseAmplitude *
|
||||
Math.sin(now * COMPUTE_CENTER_CONFIG.marker.pulseSpeed + marker.userData.pulseOffset);
|
||||
|
||||
let opacity = COMPUTE_CENTER_CONFIG.marker.baseOpacity;
|
||||
let scaleMultiplier = 1;
|
||||
|
||||
if (isLocked) {
|
||||
opacity = 1;
|
||||
scaleMultiplier = COMPUTE_CENTER_CONFIG.marker.lockedScale * pulse;
|
||||
} else if (state === "hover") {
|
||||
opacity = 0.98;
|
||||
scaleMultiplier = COMPUTE_CENTER_CONFIG.marker.hoverScale;
|
||||
} else if (hasFocus) {
|
||||
opacity = COMPUTE_CENTER_CONFIG.marker.dimmedOpacity;
|
||||
scaleMultiplier = COMPUTE_CENTER_CONFIG.marker.dimmedScale;
|
||||
}
|
||||
|
||||
const distanceScale = getDistanceScale(marker, camera);
|
||||
marker.material.opacity = showComputeCenters ? opacity : 0;
|
||||
marker.scale.setScalar(marker.userData.baseScale * scaleMultiplier * distanceScale);
|
||||
marker.visible = showComputeCenters;
|
||||
});
|
||||
}
|
||||
@@ -156,11 +156,38 @@ export const TERRAIN_CONFIG = {
|
||||
export const PATHS = {
|
||||
cablesApi: '/api/v1/visualization/geo/cables',
|
||||
landingPointsApi: '/api/v1/visualization/geo/landing-points',
|
||||
computeCentersApi: '/api/v1/visualization/geo/compute-centers',
|
||||
bgpApi: '/api/v1/visualization/geo/bgp-anomalies',
|
||||
bgpIncidentsApi: '/api/v1/visualization/geo/bgp-incidents',
|
||||
bgpCollectorsApi: '/api/v1/visualization/geo/bgp-collectors',
|
||||
};
|
||||
|
||||
export const COMPUTE_CENTER_CONFIG = {
|
||||
altitudeOffset: 0.48,
|
||||
maxRenderedMarkers: 300,
|
||||
marker: {
|
||||
baseOpacity: 0.88,
|
||||
supercomputerScale: 12,
|
||||
gpuClusterScale: 12,
|
||||
hoverScale: 1.16,
|
||||
lockedScale: 1.22,
|
||||
dimmedScale: 0.82,
|
||||
dimmedOpacity: 0.34,
|
||||
pulseSpeed: 0.0038,
|
||||
pulseAmplitude: 0.03,
|
||||
},
|
||||
colors: {
|
||||
supercomputer: "#fb923c",
|
||||
gpu_cluster: "#60a5fa",
|
||||
linked: "#f8fafc",
|
||||
},
|
||||
sizeStabilization: {
|
||||
enabled: true,
|
||||
min: 0.12,
|
||||
max: 3.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Cable colors mapping
|
||||
export const CABLE_COLORS = {
|
||||
'Americas II': 0xff4444,
|
||||
|
||||
37
frontend/public/earth/js/controls.js
vendored
37
frontend/public/earth/js/controls.js
vendored
@@ -26,6 +26,11 @@ import {
|
||||
} from "./satellites.js";
|
||||
import { getShowCables } from "./cables.js";
|
||||
import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js";
|
||||
import {
|
||||
toggleComputeCenters,
|
||||
getShowComputeCenters,
|
||||
getComputeCenterCount,
|
||||
} from "./compute-centers.js";
|
||||
import { ensureTVPanelReady, isTVPanelVisible, setTVPanelVisible } from "./tv.js";
|
||||
import { createHUDPanel } from "./hud-panels.js";
|
||||
import {
|
||||
@@ -910,6 +915,22 @@ function setBGPLayerEnabled(button, enabled, { persist = true, silent = false }
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function setComputeCentersLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
|
||||
clearSelectionIfHiding(!enabled);
|
||||
toggleComputeCenters(enabled);
|
||||
setLayerButtonState(button, {
|
||||
active: enabled,
|
||||
tooltip: enabled ? "隐藏算力中心" : "显示算力中心",
|
||||
});
|
||||
setEarthStatValue("compute-center-count", `${getComputeCenterCount()} 个`);
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
if (!silent) {
|
||||
showStatusMessage(enabled ? "算力中心已显示" : "算力中心已隐藏", "info");
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function setTrailsLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
|
||||
toggleTrails(enabled);
|
||||
setLayerButtonState(button, {
|
||||
@@ -1017,6 +1038,22 @@ function getBuiltinLayerDefinitions() {
|
||||
setVisible: (visible, options = {}) =>
|
||||
setCablesLayerEnabled(getLayerButton("cables"), visible, options),
|
||||
},
|
||||
{
|
||||
id: "computeCenters",
|
||||
buttonId: "toggle-compute-centers",
|
||||
icon: "memory",
|
||||
label: "算力中心",
|
||||
meta: "Compute Centers",
|
||||
keywords: "算力中心 compute centers gpu 超算",
|
||||
defaultActive: true,
|
||||
startupPriority: 35,
|
||||
startupMode: "preload",
|
||||
startupLabel: "算力中心",
|
||||
startupMessage: "正在加载算力中心...",
|
||||
getVisible: () => getShowComputeCenters(),
|
||||
setVisible: (visible, options = {}) =>
|
||||
setComputeCentersLayerEnabled(getLayerButton("computeCenters"), visible, options),
|
||||
},
|
||||
{
|
||||
id: "bgp",
|
||||
buttonId: "toggle-bgp",
|
||||
|
||||
@@ -274,15 +274,22 @@ const CARD_CONFIG = {
|
||||
},
|
||||
supercomputer: {
|
||||
icon: '🖥️',
|
||||
title: '超算详情',
|
||||
title: '超算中心详情',
|
||||
className: 'supercomputer',
|
||||
fields: [
|
||||
{ key: 'name', label: '名称' },
|
||||
{ key: 'site_type_label', label: '类型' },
|
||||
{ key: 'rank', label: '排名' },
|
||||
{ key: 'r_max', label: 'Rmax', unit: 'GFlops' },
|
||||
{ key: 'r_peak', label: 'Rpeak', unit: 'GFlops' },
|
||||
{ key: 'capacity', label: '实测算力' },
|
||||
{ key: 'vendor', label: '厂商' },
|
||||
{ key: 'operator', label: '运营方' },
|
||||
{ key: 'cores', label: '核心数' },
|
||||
{ key: 'power', label: '功耗', unit: 'kW' },
|
||||
{ key: 'country', label: '国家' },
|
||||
{ key: 'city', label: '城市' }
|
||||
{ key: 'city', label: '城市' },
|
||||
{ key: 'location_precision_label', label: '位置精度' },
|
||||
{ key: 'source', label: '来源' },
|
||||
{ key: 'updated_at', label: '更新时间' }
|
||||
]
|
||||
},
|
||||
gpu_cluster: {
|
||||
@@ -291,8 +298,17 @@ const CARD_CONFIG = {
|
||||
className: 'gpu_cluster',
|
||||
fields: [
|
||||
{ key: 'name', label: '名称' },
|
||||
{ key: 'site_type_label', label: '类型' },
|
||||
{ key: 'capacity', label: '估算算力' },
|
||||
{ key: 'gpu_count', label: 'GPU 数量' },
|
||||
{ key: 'gpu_type', label: 'GPU 型号' },
|
||||
{ key: 'vendor', label: '芯片/平台' },
|
||||
{ key: 'operator', label: '运营方' },
|
||||
{ key: 'country', label: '国家' },
|
||||
{ key: 'city', label: '城市' }
|
||||
{ key: 'city', label: '城市' },
|
||||
{ key: 'location_precision_label', label: '位置精度' },
|
||||
{ key: 'source', label: '来源' },
|
||||
{ key: 'updated_at', label: '更新时间' }
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -368,9 +384,11 @@ function setupInfoCardDrag(panel) {
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
window.addEventListener('pointermove', onMove, { passive: false });
|
||||
window.addEventListener('pointerup', stopDragging);
|
||||
window.addEventListener('pointercancel', stopDragging);
|
||||
// Listen in capture phase so card-level stopPropagation used to shield the
|
||||
// globe canvas does not swallow the drag stream before we can reposition.
|
||||
window.addEventListener('pointermove', onMove, { passive: false, capture: true });
|
||||
window.addEventListener('pointerup', stopDragging, { capture: true });
|
||||
window.addEventListener('pointercancel', stopDragging, { capture: true });
|
||||
handle.addEventListener('lostpointercapture', stopDragging);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
loadBGPAnomalies,
|
||||
toggleBGP,
|
||||
} from "./bgp.js";
|
||||
import {
|
||||
loadComputeCenters,
|
||||
toggleComputeCenters,
|
||||
} from "./compute-centers.js";
|
||||
|
||||
/**
|
||||
* Layer startup task registry.
|
||||
@@ -70,6 +74,7 @@ function registerBuiltinLayerStartupTasks() {
|
||||
startupTaskRegistry.clear();
|
||||
registerCableStartupTask();
|
||||
registerSatelliteStartupTask();
|
||||
registerComputeCenterStartupTask();
|
||||
registerBGPStartupTask();
|
||||
}
|
||||
|
||||
@@ -171,4 +176,26 @@ function registerBGPStartupTask() {
|
||||
});
|
||||
}
|
||||
|
||||
function registerComputeCenterStartupTask() {
|
||||
registerLayerStartupTask("computeCenters", (context) => async (layer) => {
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载算力中心..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
const computeCenterResult = await loadComputeCenters(context.scene, context.earth);
|
||||
if (!context.isCancelled()) {
|
||||
toggleComputeCenters(context.getShowComputeCenters());
|
||||
context.updateComputeCenterHud(computeCenterResult);
|
||||
context.setLegendItems("computeCenters", context.getComputeCenterLegendItems());
|
||||
context.refreshLegend();
|
||||
}
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "算力中心", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
registerBuiltinLayerStartupTasks();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createHUDPanel } from "./hud-panels.js";
|
||||
|
||||
const LEGEND_MODES = {
|
||||
cables: { title: "海缆" },
|
||||
satellites: { title: "卫星" },
|
||||
bgp: { title: "BGP" },
|
||||
cables: { title: "海缆" },
|
||||
satellites: { title: "卫星" },
|
||||
computeCenters: { title: "算力" },
|
||||
bgp: { title: "BGP" },
|
||||
};
|
||||
|
||||
let currentLegendMode = "cables";
|
||||
@@ -11,6 +12,7 @@ let legendPanel = null;
|
||||
let legendItemsByMode = {
|
||||
cables: [],
|
||||
satellites: [],
|
||||
computeCenters: [],
|
||||
bgp: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -128,6 +128,22 @@ import {
|
||||
showBGPEventOverlay,
|
||||
showBGPCollectorCoverageOverlay,
|
||||
} from "./bgp.js";
|
||||
import {
|
||||
clearComputeCenterData,
|
||||
clearComputeCenterSelection,
|
||||
formatComputeCenterCapacity,
|
||||
formatComputeCenterLocationPrecision,
|
||||
formatComputeCenterTypeLabel,
|
||||
formatComputeCenterUpdatedAt,
|
||||
getComputeCenterCount,
|
||||
getComputeCenterLegendItems,
|
||||
getComputeCenterMarkers,
|
||||
getShowComputeCenters,
|
||||
loadComputeCenters,
|
||||
setComputeCenterMarkerState,
|
||||
toggleComputeCenters,
|
||||
updateComputeCenterVisualState,
|
||||
} from "./compute-centers.js";
|
||||
import {
|
||||
setupControls,
|
||||
getAutoRotate,
|
||||
@@ -177,6 +193,7 @@ let targetRotation = { x: 0, y: 0 };
|
||||
let inertialVelocity = { x: 0, y: 0 };
|
||||
let hoveredCable = null;
|
||||
let hoveredBGP = null;
|
||||
let hoveredComputeCenter = null;
|
||||
let hoveredSatellite = null;
|
||||
let hoveredSatelliteIndex = null;
|
||||
let lockedSatellite = null;
|
||||
@@ -222,6 +239,8 @@ const scratchCableCenter = new THREE.Vector3();
|
||||
const scratchCableDirection = new THREE.Vector3();
|
||||
const scratchBGPDirection = new THREE.Vector3();
|
||||
const scratchBGPWorldPosition = new THREE.Vector3();
|
||||
const scratchComputeCenterDirection = new THREE.Vector3();
|
||||
const scratchComputeCenterWorldPosition = new THREE.Vector3();
|
||||
const scratchViewCenterWorld = new THREE.Vector3();
|
||||
|
||||
const cleanupFns = [];
|
||||
@@ -234,6 +253,7 @@ const TOOLTIP_CURSOR_OFFSET = 14; // px offset from cursor for hover tooltips
|
||||
const TOOLTIP_COORDS_OFFSET = 10; // px offset for earth-coordinate tooltip
|
||||
const RELATED_SATELLITE_HIGHLIGHT_COLOR = "#7dd3fc";
|
||||
const DRAG_POINTER_THRESHOLD_PX = 8;
|
||||
const GLOBE_DRAGGING_CLASS = "is-globe-dragging";
|
||||
const HUD_INTERACTIVE_SELECTORS = [
|
||||
".earth-left-column",
|
||||
".earth-left-column *",
|
||||
@@ -276,6 +296,17 @@ function isEventOnHud(event) {
|
||||
return HUD_INTERACTIVE_SELECTORS.some((selector) => target.closest(selector));
|
||||
}
|
||||
|
||||
function clearDocumentSelection() {
|
||||
const selection = window.getSelection?.();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
|
||||
function setGlobeDraggingUiState(active) {
|
||||
document.body.classList.toggle(GLOBE_DRAGGING_CLASS, active);
|
||||
document.documentElement.classList.toggle(GLOBE_DRAGGING_CLASS, active);
|
||||
}
|
||||
|
||||
function getDragRotationFactor() {
|
||||
const zoom = Math.max(getZoomLevel(), 0.01);
|
||||
const scale = THREE.MathUtils.clamp(
|
||||
@@ -338,6 +369,7 @@ function disposeSceneObject(object) {
|
||||
function clearRuntimeSelection() {
|
||||
hoveredCable = null;
|
||||
hoveredBGP = null;
|
||||
hoveredComputeCenter = null;
|
||||
hoveredSatellite = null;
|
||||
hoveredSatelliteIndex = null;
|
||||
lockedObject = null;
|
||||
@@ -353,6 +385,7 @@ export function clearLockedObject() {
|
||||
clearAllCableStates();
|
||||
clearCableSelection();
|
||||
clearBGPSelection();
|
||||
clearComputeCenterSelection();
|
||||
clearRelatedSatelliteHighlights();
|
||||
setSatelliteRingState(null, "none", null);
|
||||
clearRuntimeSelection();
|
||||
@@ -388,6 +421,14 @@ function isSameBGPMarker(marker1, marker2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSameComputeCenter(marker1, marker2) {
|
||||
if (!marker1 || !marker2) return false;
|
||||
if (marker1.userData?.type !== "compute_center" || marker2.userData?.type !== "compute_center") {
|
||||
return false;
|
||||
}
|
||||
return marker1.userData?.id === marker2.userData?.id;
|
||||
}
|
||||
|
||||
function getBGPCollectorMarkerByName(collector) {
|
||||
return getBGPCollectorMarkers().find(
|
||||
(marker) => marker.userData?.collector === collector,
|
||||
@@ -407,9 +448,19 @@ function resetTransientBGPStates() {
|
||||
});
|
||||
}
|
||||
|
||||
function resetTransientComputeCenterStates() {
|
||||
getComputeCenterMarkers().forEach((marker) => {
|
||||
if (marker !== lockedObject) {
|
||||
setComputeCenterMarkerState(marker, "normal");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearTransientHoverState() {
|
||||
resetTransientBGPStates();
|
||||
resetTransientComputeCenterStates();
|
||||
hoveredBGP = null;
|
||||
hoveredComputeCenter = null;
|
||||
|
||||
if (hoveredCable && !isSameCable(hoveredCable, lockedObject)) {
|
||||
setCableState(hoveredCable.userData.cableId, CABLE_STATE.NORMAL);
|
||||
@@ -445,6 +496,18 @@ function applyBGPHoverState(marker) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyComputeCenterHoverState(marker) {
|
||||
resetTransientComputeCenterStates();
|
||||
if (!marker) {
|
||||
hoveredComputeCenter = null;
|
||||
return;
|
||||
}
|
||||
hoveredComputeCenter = marker;
|
||||
if (marker !== lockedObject) {
|
||||
setComputeCenterMarkerState(marker, "hover");
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimaryBGPHoverTarget(bgpAnomalyIntersects, bgpCollectorIntersects) {
|
||||
if (bgpAnomalyIntersects.length > 0) {
|
||||
return bgpAnomalyIntersects[0].object;
|
||||
@@ -525,6 +588,40 @@ function getSatelliteBriefHtml(props) {
|
||||
return `<strong>${name}</strong>${id ? `<br>${id}` : ""}`;
|
||||
}
|
||||
|
||||
function showComputeCenterInfo(marker, coords) {
|
||||
const siteType = marker.userData?.site_type === "supercomputer"
|
||||
? "supercomputer"
|
||||
: "gpu_cluster";
|
||||
setLegendMode("computeCenters");
|
||||
showInfoCard(siteType, {
|
||||
name: marker.userData?.name || "-",
|
||||
site_type_label: formatComputeCenterTypeLabel(siteType),
|
||||
rank: marker.userData?.rank ?? "-",
|
||||
capacity: formatComputeCenterCapacity(marker.userData),
|
||||
vendor: marker.userData?.vendor || "-",
|
||||
operator: marker.userData?.operator || "-",
|
||||
gpu_count: marker.userData?.gpu_count ?? "-",
|
||||
gpu_type: marker.userData?.gpu_type || "-",
|
||||
cores: marker.userData?.cores ?? "-",
|
||||
power: marker.userData?.power ?? "-",
|
||||
country: marker.userData?.country || "-",
|
||||
city: marker.userData?.city || "-",
|
||||
location_precision_label: formatComputeCenterLocationPrecision(marker.userData),
|
||||
source: marker.userData?.source || "-",
|
||||
updated_at: formatComputeCenterUpdatedAt(marker.userData?.updated_at),
|
||||
}, coords);
|
||||
}
|
||||
|
||||
function getComputeCenterBriefHtml(marker) {
|
||||
const name = marker.userData?.name || "算力中心";
|
||||
const type = formatComputeCenterTypeLabel(marker.userData?.site_type);
|
||||
const location = [marker.userData?.city, marker.userData?.country]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const precision = marker.userData?.is_estimated ? " · 估算位置" : "";
|
||||
return `<strong>${name}</strong><br>${type}${location ? ` · ${location}` : ""}${precision}`;
|
||||
}
|
||||
|
||||
function showBGPInfo(marker, coords) {
|
||||
setLegendMode("bgp");
|
||||
const impactedRegions =
|
||||
@@ -700,6 +797,13 @@ function getBGPFocusCoords(marker) {
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
function getComputeCenterFocusCoords(marker) {
|
||||
const lat = marker?.userData?.latitude;
|
||||
const lon = marker?.userData?.longitude;
|
||||
if (typeof lat !== "number" || typeof lon !== "number") return null;
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
async function focusSearchTarget(coords, zoom = Math.max(getZoomLevel(), 1.12)) {
|
||||
if (!coords || !camera) return;
|
||||
await focusEarthView(camera, {
|
||||
@@ -847,6 +951,29 @@ async function focusSearchBGPMarker(marker) {
|
||||
}
|
||||
}
|
||||
|
||||
async function focusSearchComputeCenter(marker) {
|
||||
if (!getShowComputeCenters()) {
|
||||
toggleComputeCenters(true);
|
||||
}
|
||||
interruptCruisePresentation({ resetLoop: true });
|
||||
clearLockedObject();
|
||||
setAutoRotate(false);
|
||||
|
||||
const coords = getComputeCenterFocusCoords(marker);
|
||||
if (coords) {
|
||||
await focusSearchTarget(coords, Math.max(getZoomLevel(), 1.16));
|
||||
}
|
||||
|
||||
setComputeCenterMarkerState(marker, "locked");
|
||||
lockedObject = marker;
|
||||
lockedObjectType = "compute_center";
|
||||
showComputeCenterInfo(marker, getSearchCardCoords());
|
||||
showStatusMessage(
|
||||
`已定位算力中心:${marker.userData?.name || "未知节点"}`,
|
||||
"info",
|
||||
);
|
||||
}
|
||||
|
||||
function resolveEarthSearchResults(query) {
|
||||
const results = [];
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
@@ -985,6 +1112,34 @@ function resolveEarthSearchResults(query) {
|
||||
});
|
||||
});
|
||||
|
||||
getComputeCenterMarkers().forEach((marker) => {
|
||||
const score = computeSearchScore(
|
||||
normalizedQuery,
|
||||
marker.userData?.name,
|
||||
marker.userData?.country,
|
||||
marker.userData?.city,
|
||||
marker.userData?.operator,
|
||||
marker.userData?.vendor,
|
||||
marker.userData?.site_type,
|
||||
"算力中心 compute center gpu 超算",
|
||||
);
|
||||
if (score < 0) return;
|
||||
results.push({
|
||||
id: `compute:${marker.userData?.id || marker.uuid}`,
|
||||
kind: "compute_center",
|
||||
icon: "memory",
|
||||
typeLabel: "算力中心",
|
||||
title: marker.userData?.name || "未知算力中心",
|
||||
subtitle: [
|
||||
formatComputeCenterTypeLabel(marker.userData?.site_type),
|
||||
marker.userData?.city,
|
||||
marker.userData?.country,
|
||||
].filter(Boolean).join(" · ") || "算力基础设施",
|
||||
score,
|
||||
entity: marker,
|
||||
});
|
||||
});
|
||||
|
||||
return results
|
||||
.sort((left, right) => {
|
||||
if (right.score !== left.score) return right.score - left.score;
|
||||
@@ -1010,6 +1165,10 @@ async function handleSearchSelection(result) {
|
||||
}
|
||||
if (result.kind === "bgp" || result.kind === "bgp_collector") {
|
||||
await focusSearchBGPMarker(result.entity);
|
||||
return;
|
||||
}
|
||||
if (result.kind === "compute_center") {
|
||||
await focusSearchComputeCenter(result.entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1023,6 +1182,19 @@ function getBGPStatusText(bgpResult) {
|
||||
return "当前无活跃事件";
|
||||
}
|
||||
|
||||
function updateComputeCenterHud(computeCenterResult) {
|
||||
const computeBtn = document.getElementById("toggle-compute-centers");
|
||||
if (computeBtn) {
|
||||
computeBtn.classList.add("active");
|
||||
const tooltip = computeBtn.querySelector(".earth-toolbar-tooltip");
|
||||
if (tooltip) {
|
||||
tooltip.textContent = "隐藏算力中心";
|
||||
}
|
||||
}
|
||||
|
||||
setEarthStatValue("compute-center-count", `${computeCenterResult.totalCount} 个`);
|
||||
}
|
||||
|
||||
function updateBGPHud(bgpResult) {
|
||||
const bgpBtn = document.getElementById("toggle-bgp");
|
||||
if (bgpBtn) {
|
||||
@@ -1530,6 +1702,7 @@ function updateStatsSummary() {
|
||||
updateEarthStats({
|
||||
cableCount: getCableLines().length,
|
||||
landingPointCount: getLandingPoints().length,
|
||||
computeCenterCount: `${getComputeCenterCount()} 个`,
|
||||
bgpAnomalyCount: `${getBGPCount()} 条`,
|
||||
bgpCollectorCount: `${getBGPCollectorCount()} 个`,
|
||||
bgpStatusSummary: getBGPStatusSummary(),
|
||||
@@ -1605,6 +1778,7 @@ export function init() {
|
||||
initLegend();
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
setLegendItems("computeCenters", getComputeCenterLegendItems());
|
||||
setLegendItems("bgp", getBGPLegendItems());
|
||||
const earthObj = createEarth(scene);
|
||||
applyImmediateView(earthObj, camera);
|
||||
@@ -1762,6 +1936,7 @@ async function loadData() {
|
||||
clearEarthTexture();
|
||||
clearBGPData(earth);
|
||||
clearCableData(earth);
|
||||
clearComputeCenterData(earth);
|
||||
clearSatelliteData();
|
||||
|
||||
setLoadingMessage("正在初始化...");
|
||||
@@ -1789,9 +1964,12 @@ async function loadData() {
|
||||
yieldFrame,
|
||||
refreshLegend,
|
||||
setLegendItems,
|
||||
getComputeCenterLegendItems,
|
||||
updateCableToggleUi,
|
||||
updateSatelliteToggleUi,
|
||||
updateComputeCenterHud,
|
||||
updateBGPHud,
|
||||
getShowComputeCenters,
|
||||
getShowBGP,
|
||||
getInitialSatelliteLoadLimit,
|
||||
shouldHydrateFullSatelliteSet,
|
||||
@@ -1838,6 +2016,7 @@ async function loadData() {
|
||||
updateSatelliteToggleUi(satellitesEnabled);
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
setLegendItems("satellites", getSatelliteLegendItems());
|
||||
setLegendItems("computeCenters", getComputeCenterLegendItems());
|
||||
setLegendItems("bgp", getBGPLegendItems());
|
||||
refreshLegend();
|
||||
setLoading(false);
|
||||
@@ -2055,6 +2234,25 @@ function getFrontFacingBGPMarkers(markers) {
|
||||
});
|
||||
}
|
||||
|
||||
function getFrontFacingComputeCenterMarkers(markers) {
|
||||
const earth = getEarth();
|
||||
if (!earth) return markers;
|
||||
|
||||
scratchCameraToEarth.subVectors(camera.position, earth.position).normalize();
|
||||
|
||||
return markers.filter((marker) => {
|
||||
scratchComputeCenterWorldPosition.copy(marker.position);
|
||||
marker.parent?.localToWorld(scratchComputeCenterWorldPosition);
|
||||
scratchComputeCenterDirection
|
||||
.subVectors(scratchComputeCenterWorldPosition, earth.position)
|
||||
.normalize();
|
||||
return (
|
||||
scratchCameraToEarth.dot(scratchComputeCenterDirection) >
|
||||
SATELLITE_CONFIG.frontFacingDotThreshold
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
const earth = getEarth();
|
||||
if (!earth) return;
|
||||
@@ -2112,6 +2310,12 @@ function onMouseMove(event) {
|
||||
const bgpCollectorIntersects = getShowBGP()
|
||||
? interactionRaycaster.intersectObjects(frontFacingBGPCollectorMarkers)
|
||||
: [];
|
||||
const frontFacingComputeCenterMarkers = getFrontFacingComputeCenterMarkers(
|
||||
getComputeCenterMarkers(),
|
||||
);
|
||||
const computeCenterIntersects = getShowComputeCenters()
|
||||
? interactionRaycaster.intersectObjects(frontFacingComputeCenterMarkers)
|
||||
: [];
|
||||
|
||||
let hoveredSat = null;
|
||||
let hoveredSatIndexFromIntersect = null;
|
||||
@@ -2138,6 +2342,16 @@ function onMouseMove(event) {
|
||||
clearTransientHoverState();
|
||||
}
|
||||
|
||||
const hoveredComputeCenterMarker =
|
||||
computeCenterIntersects.length > 0 ? computeCenterIntersects[0].object : null;
|
||||
|
||||
if (
|
||||
hoveredComputeCenter &&
|
||||
!isSameComputeCenter(hoveredComputeCenter, hoveredComputeCenterMarker)
|
||||
) {
|
||||
clearTransientHoverState();
|
||||
}
|
||||
|
||||
if (
|
||||
hoveredCable &&
|
||||
(!cableIntersects.length ||
|
||||
@@ -2168,6 +2382,18 @@ function onMouseMove(event) {
|
||||
showTooltip(event.clientX + TOOLTIP_CURSOR_OFFSET, event.clientY + TOOLTIP_CURSOR_OFFSET, getBGPCollectorBriefHtml(hoveredBGPMarker));
|
||||
}
|
||||
objectTooltipShown = true;
|
||||
} else if (
|
||||
hoveredComputeCenterMarker &&
|
||||
getShowComputeCenters() &&
|
||||
lockedObjectType !== "compute_center"
|
||||
) {
|
||||
applyComputeCenterHoverState(hoveredComputeCenterMarker);
|
||||
showTooltip(
|
||||
event.clientX + TOOLTIP_CURSOR_OFFSET,
|
||||
event.clientY + TOOLTIP_CURSOR_OFFSET,
|
||||
getComputeCenterBriefHtml(hoveredComputeCenterMarker),
|
||||
);
|
||||
objectTooltipShown = true;
|
||||
} else if (cableIntersects.length > 0 && getShowCables()) {
|
||||
const cable = cableIntersects[0].object;
|
||||
hoveredCable = cable;
|
||||
@@ -2195,8 +2421,11 @@ function onMouseMove(event) {
|
||||
applyBGPHoverState(lockedObject);
|
||||
} else if (lockedObjectType === "bgp_collector" && lockedObject) {
|
||||
applyBGPHoverState(lockedObject);
|
||||
} else if (lockedObjectType === "compute_center" && lockedObject) {
|
||||
applyComputeCenterHoverState(lockedObject);
|
||||
} else if (!lockedObjectType && !isCruisePresentationPinned()) {
|
||||
resetTransientBGPStates();
|
||||
resetTransientComputeCenterStates();
|
||||
hideInfoCard();
|
||||
}
|
||||
|
||||
@@ -2247,12 +2476,16 @@ function onMouseDown(event) {
|
||||
y: earth.rotation.y,
|
||||
};
|
||||
}
|
||||
clearDocumentSelection();
|
||||
setGlobeDraggingUiState(true);
|
||||
document.getElementById("container")?.classList.add("dragging");
|
||||
hideTooltip();
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
isDragging = false;
|
||||
setGlobeDraggingUiState(false);
|
||||
clearDocumentSelection();
|
||||
document.getElementById("container")?.classList.remove("dragging");
|
||||
}
|
||||
|
||||
@@ -2260,6 +2493,9 @@ function onPointerDown(event) {
|
||||
if (isEventOnHud(event)) return;
|
||||
if (event.pointerType !== "touch" && event.button !== 0) return;
|
||||
|
||||
renderer?.domElement?.setPointerCapture?.(event.pointerId);
|
||||
clearDocumentSelection();
|
||||
|
||||
if (event.pointerType === "touch") {
|
||||
activeTouchPoints.set(event.pointerId, {
|
||||
clientX: event.clientX,
|
||||
@@ -2286,6 +2522,10 @@ function onPointerDown(event) {
|
||||
}
|
||||
|
||||
function onPointerMove(event) {
|
||||
if (activeDragPointerId === event.pointerId && isDragging) {
|
||||
clearDocumentSelection();
|
||||
}
|
||||
|
||||
if (event.pointerType === "touch") {
|
||||
if (activeTouchPoints.has(event.pointerId)) {
|
||||
activeTouchPoints.set(event.pointerId, {
|
||||
@@ -2344,6 +2584,7 @@ function onPointerUp(event) {
|
||||
suppressNextClick = true;
|
||||
isLongDrag = true;
|
||||
}
|
||||
renderer?.domElement?.releasePointerCapture?.(event.pointerId);
|
||||
activeDragPointerId = null;
|
||||
pointerDragDistance = 0;
|
||||
onMouseUp();
|
||||
@@ -2380,6 +2621,11 @@ function onClick(event) {
|
||||
const bgpCollectorIntersects = getShowBGP()
|
||||
? interactionRaycaster.intersectObjects(frontFacingBGPCollectorMarkers)
|
||||
: [];
|
||||
const computeCenterIntersects = getShowComputeCenters()
|
||||
? interactionRaycaster.intersectObjects(
|
||||
getFrontFacingComputeCenterMarkers(getComputeCenterMarkers()),
|
||||
)
|
||||
: [];
|
||||
const satIntersects = getShowSatellites()
|
||||
? interactionRaycaster.intersectObject(getSatellitePoints())
|
||||
: [];
|
||||
@@ -2387,6 +2633,9 @@ function onClick(event) {
|
||||
const clickedBGPMarker = getShowBGP()
|
||||
? getPrimaryBGPClickTarget(event, bgpAnomalyIntersects, bgpCollectorIntersects)
|
||||
: null;
|
||||
const clickedComputeCenterMarker = computeCenterIntersects.length > 0
|
||||
? computeCenterIntersects[0].object
|
||||
: null;
|
||||
|
||||
if (clickedBGPMarker?.userData?.type === "bgp") {
|
||||
interruptCruisePresentation();
|
||||
@@ -2437,6 +2686,23 @@ function onClick(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickedComputeCenterMarker?.userData?.type === "compute_center") {
|
||||
interruptCruisePresentation();
|
||||
clearLockedObject();
|
||||
|
||||
const clickedMarker = clickedComputeCenterMarker;
|
||||
setComputeCenterMarkerState(clickedMarker, "locked");
|
||||
lockedObject = clickedMarker;
|
||||
lockedObjectType = "compute_center";
|
||||
setAutoRotate(false);
|
||||
showComputeCenterInfo(clickedMarker, { x: event.clientX, y: event.clientY });
|
||||
showStatusMessage(
|
||||
`已选择算力中心: ${clickedMarker.userData?.name || "未知节点"}`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cableIntersects.length > 0 && getShowCables()) {
|
||||
interruptCruisePresentation();
|
||||
clearLockedObject();
|
||||
@@ -2594,6 +2860,7 @@ function animate() {
|
||||
? cruiseSequencer?.getCurrentItem() ?? null
|
||||
: null;
|
||||
updateBGPVisualState(lockedObjectType, lockedObject, camera, activeCruiseMarker);
|
||||
updateComputeCenterVisualState(lockedObjectType, lockedObject, camera);
|
||||
|
||||
if (lockedObjectType === "cable" && lockedObject) {
|
||||
applyLandingPointVisualState(lockedObject.userData.name, false, camera);
|
||||
@@ -2641,6 +2908,8 @@ export function destroy() {
|
||||
destroyed = true;
|
||||
currentLoadToken += 1;
|
||||
isDataLoading = false;
|
||||
setGlobeDraggingUiState(false);
|
||||
clearDocumentSelection();
|
||||
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
@@ -2656,6 +2925,7 @@ export function destroy() {
|
||||
clearLockedObject();
|
||||
clearCableData(getEarth());
|
||||
clearBGPData(getEarth());
|
||||
clearComputeCenterData(getEarth());
|
||||
resetSatelliteState();
|
||||
clearUiState();
|
||||
disposeCelestialLayer();
|
||||
|
||||
@@ -48,7 +48,7 @@ function updateEmptyState(query) {
|
||||
const { empty } = getElements();
|
||||
if (!empty) return;
|
||||
if (!query) {
|
||||
empty.textContent = "支持搜索海缆、登陆点、卫星、BGP 事件与观测站。";
|
||||
empty.textContent = "支持搜索海缆、登陆点、卫星、算力中心、BGP 事件与观测站。";
|
||||
return;
|
||||
}
|
||||
empty.textContent = "未找到匹配对象,可尝试名称、地点、NORAD、ASN、前缀等关键词。";
|
||||
|
||||
@@ -187,6 +187,7 @@ export function updateZoomDisplay(zoomLevel, distance) {
|
||||
export function updateEarthStats(stats) {
|
||||
setEarthStatValue("cable-count", String(stats.cableCount || 0));
|
||||
setEarthStatValue("landing-point-count", String(stats.landingPointCount || 0));
|
||||
setEarthStatValue("compute-center-count", String(stats.computeCenterCount || 0));
|
||||
setEarthStatValue("bgp-anomaly-count", String(stats.bgpAnomalyCount || 0));
|
||||
setEarthStatValue("bgp-collector-count", String(stats.bgpCollectorCount || 0));
|
||||
setEarthStatValue("bgp-status-summary", stats.bgpStatusSummary || "-");
|
||||
|
||||
Reference in New Issue
Block a user