import * as THREE from "three"; import { COMPUTE_CENTER_CONFIG, CONFIG, PATHS } from "./constants.js"; import { getSurfaceMarkerCameraScale, latLonToVector3 } from "./utils.js"; const computeCenterGroup = new THREE.Group(); const computeCenterMarkers = []; const COMPUTE_CENTER_RENDER_ORDER = 4.5; const textureCache = new Map(); let showComputeCenters = true; let supercomputerCount = 0; let gpuClusterCount = 0; function buildComputeCenterMarkerData(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; } return { ...props, latitude, longitude, displayLatitude: latitude, displayLongitude: longitude, site_type: normalizeSiteType(props.site_type), }; } function spreadComputeCenterPositions(markers) { const groups = new Map(); const precision = COMPUTE_CENTER_CONFIG.overlapSpread.groupPrecision; markers.forEach((marker) => { const key = `${marker.latitude.toFixed(precision)}|${marker.longitude.toFixed(precision)}`; if (!groups.has(key)) { groups.set(key, []); } groups.get(key).push(marker); }); groups.forEach((group) => { if (group.length <= 1) return; const radius = COMPUTE_CENTER_CONFIG.overlapSpread.radius; const offsetStep = COMPUTE_CENTER_CONFIG.overlapSpread.offsetStep; group.forEach((marker, index) => { const angle = (Math.PI * 2 * index) / group.length; marker.displayLatitude = marker.latitude + Math.sin(angle) * radius * offsetStep; marker.displayLongitude = marker.longitude + Math.cos(angle) * radius * offsetStep; marker.isSpread = true; marker.groupSize = group.length; }); }); markers.forEach((marker) => { if (marker.isSpread) return; marker.displayLatitude = marker.latitude; marker.displayLongitude = marker.longitude; marker.isSpread = false; marker.groupSize = 1; }); return markers; } 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; } return getSurfaceMarkerCameraScale(camera, { altitudeOffset: COMPUTE_CENTER_CONFIG.altitudeOffset, referenceFov: 75, min: COMPUTE_CENTER_CONFIG.sizeStabilization.min, max: 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(markerData) { const siteType = markerData.site_type; const material = new THREE.SpriteMaterial({ map: createMarkerTexture(siteType, Boolean(markerData.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( markerData.displayLatitude, markerData.displayLongitude, CONFIG.earthRadius + COMPUTE_CENTER_CONFIG.altitudeOffset, ), ); marker.scale.setScalar(baseScale); marker.renderOrder = COMPUTE_CENTER_RENDER_ORDER; marker.visible = showComputeCenters; marker.userData = { ...markerData, 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); spreadComputeCenterPositions( features .map((feature) => buildComputeCenterMarkerData(feature)) .filter(Boolean), ) .slice(0, COMPUTE_CENTER_CONFIG.maxRenderedMarkers) .forEach((markerData) => { const marker = createComputeCenterMarker(markerData); 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; }); }