285 lines
8.4 KiB
JavaScript
285 lines
8.4 KiB
JavaScript
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);
|
|
}
|