import { COMPUTE_CENTER_CONFIG, PATHS } from "./constants.js"; import { createInteractableLayer } from "./interactable.js"; const COMPUTE_CENTER_RENDER_ORDER = 4.5; const COMPUTE_CENTER_POINT_SIZE = 36; const COMPUTE_CENTER_ICON_FIT_SIZE = 60; const COMPUTE_CENTER_ATLAS_CELL_SIZE = 128; const COMPUTE_CENTER_ICON_SOURCES = { supercomputer: "/earth/assets/icons/compute-supercomputer.svg", gpu_cluster: "/earth/assets/icons/compute-gpu-cluster.svg", infrastructure: "/earth/assets/icons/compute-hdd-network.svg", }; 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 drawComputeCenterEstimatedBadge(context, isEstimated = false) { if (isEstimated) { context.save(); context.fillStyle = "rgba(15,23,42,0.92)"; context.beginPath(); context.arc(94, 36, 12, 0, Math.PI * 2); context.fill(); 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(); } } function normalizeSiteType(siteType) { return siteType === "supercomputer" ? "supercomputer" : "gpu_cluster"; } const computeCenterIconLayer = createInteractableLayer({ id: "computeCenters", objectType: "compute_center", renderOrder: COMPUTE_CENTER_RENDER_ORDER, altitudeOffset: COMPUTE_CENTER_CONFIG.altitudeOffset, pointSize: COMPUTE_CENTER_POINT_SIZE, atlasCellSize: COMPUTE_CENTER_ATLAS_CELL_SIZE, colors: { byKind: COMPUTE_CENTER_CONFIG.colors, normal: COMPUTE_CENTER_CONFIG.colors.gpu_cluster, }, opacity: { normal: COMPUTE_CENTER_CONFIG.marker.baseOpacity, dimmed: COMPUTE_CENTER_CONFIG.marker.dimmedOpacity, hover: 0.98, locked: 1, }, stateScale: { hover: COMPUTE_CENTER_CONFIG.marker.hoverScale, locked: COMPUTE_CENTER_CONFIG.marker.lockedScale, dimmed: COMPUTE_CENTER_CONFIG.marker.dimmedScale, }, pulse: { enabled: true, speed: COMPUTE_CENTER_CONFIG.marker.pulseSpeed, amplitude: COMPUTE_CENTER_CONFIG.marker.pulseAmplitude, }, icon: { coordinates: "canvas", colorable: false, fitSize: COMPUTE_CENTER_ICON_FIT_SIZE, glowBlur: 16, getSource({ marker, item }) { const siteType = marker?.userData?.site_type || item?.site_type || "gpu_cluster"; return ( COMPUTE_CENTER_ICON_SOURCES[siteType] || COMPUTE_CENTER_ICON_SOURCES.infrastructure ); }, afterDraw(context, { marker, item }) { drawComputeCenterEstimatedBadge( context, Boolean(marker?.userData?.is_estimated ?? item?.is_estimated), ); }, }, getPosition: (item) => ({ latitude: item.displayLatitude, longitude: item.displayLongitude, }), getKind: (item) => item.site_type || "gpu_cluster", getBucketKey: (marker) => [ marker.userData?.site_type || "gpu_cluster", marker.userData?.is_estimated ? "estimated" : "precise", ].join(":"), getUserData: (item) => ({ ...item, pulseOffset: Math.random() * Math.PI * 2, }), }); 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 computeCenterIconLayer.getMarkers(); } export function getComputeCenterCount() { return computeCenterIconLayer.getCount(); } export function getComputeCenterSupercomputerCount() { return supercomputerCount; } export function getComputeCenterGPUClusterCount() { return gpuClusterCount; } export function getComputeCenterStatusSummary() { if (getComputeCenterCount() === 0) return "暂无算力中心数据"; return `${supercomputerCount} 台超算 / ${gpuClusterCount} 个 GPU 集群`; } export function setComputeCenterMarkerState(marker, state = "normal") { computeCenterIconLayer.setMarkerState(marker, state); } export function clearComputeCenterSelection() { getComputeCenterMarkers().forEach((marker) => setComputeCenterMarkerState(marker, "normal")); } export function clearComputeCenterData(earth) { supercomputerCount = 0; gpuClusterCount = 0; computeCenterIconLayer.clearData(earth); } export function toggleComputeCenters(show) { showComputeCenters = Boolean(show); computeCenterIconLayer.setVisible(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); const markerData = spreadComputeCenterPositions( features .map((feature) => buildComputeCenterMarkerData(feature)) .filter(Boolean), ) .slice(0, COMPUTE_CENTER_CONFIG.maxRenderedMarkers); markerData.forEach((item) => { if (item.site_type === "supercomputer") { supercomputerCount += 1; } else { gpuClusterCount += 1; } }); await computeCenterIconLayer.preloadAssets(markerData); computeCenterIconLayer.setData(markerData); computeCenterIconLayer.attach(earth); computeCenterIconLayer.setVisible(showComputeCenters); return { totalCount: getComputeCenterCount(), supercomputerCount, gpuClusterCount, summary: getComputeCenterStatusSummary(), }; } export function getComputeCenterPointerIntersections(options) { return computeCenterIconLayer.getPointerIntersections(options); } export function updateComputeCenterVisualState(lockedObjectType, lockedObject, camera) { computeCenterIconLayer.updateVisualState(lockedObjectType, lockedObject, camera); }