Files
planet/frontend/public/earth/js/compute-centers.js
linkong e1984c7a35 release: bump version to 0.49.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:42:27 +08:00

411 lines
13 KiB
JavaScript

import { COMPUTE_CENTER_CONFIG, PATHS } from "./constants.js";
import {
createInteractableLayer,
SURFACE_AVOIDANCE_PROFILES,
} 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;
let unresolvedComputeCenters = [];
const COLLECT_LOCATION_API_BASE = "/api/v1/visualization/compute-centers";
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 shouldShowComputeCenterEstimatedBadge(data) {
if (!data) return false;
if (data.needs_confirmation === true) return true;
if (data.location_source === "nominatim_online_geocode") return true;
return false;
}
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 }) {
const data = marker?.userData || item;
drawComputeCenterEstimatedBadge(
context,
shouldShowComputeCenterEstimatedBadge(data),
);
},
},
getPosition: (item) => ({
latitude: item.displayLatitude,
longitude: item.displayLongitude,
}),
getKind: (item) => item.site_type || "gpu_cluster",
getBucketKey: (marker) =>
[
marker.userData?.site_type || "gpu_cluster",
shouldShowComputeCenterEstimatedBadge(marker.userData) ? "estimated" : "verified",
].join(":"),
getUserData: (item) => ({
...item,
pulseOffset: Math.random() * Math.PI * 2,
}),
avoidance: SURFACE_AVOIDANCE_PROFILES.city,
});
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 === "site") return "站点级位置";
if (precision === "city") return "城市级位置";
return "位置未确认";
}
const COMPUTE_CENTER_LOCATION_SOURCE_LABELS = {
source_coordinates: "源数据自带坐标",
ror_organization_registry: "ROR 组织注册 API",
nominatim_online_geocode: "Nominatim 在线搜索",
};
export function formatComputeCenterLocationSource(markerData) {
const source = markerData?.location_source;
if (!source) return "未知来源";
return COMPUTE_CENTER_LOCATION_SOURCE_LABELS[source] || source;
}
export function formatComputeCenterNeedsConfirmation(markerData) {
if (markerData?.needs_confirmation === true) return "待人工核验";
if (markerData?.is_estimated === true) return "估算位置";
return "已确认";
}
export function formatComputeCenterLocationConfidence(markerData) {
const confidence = Number(markerData?.location_confidence);
if (!Number.isFinite(confidence)) return "-";
return `${Math.round(confidence * 100)}%`;
}
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;
unresolvedComputeCenters = [];
computeCenterIconLayer.clearData(earth);
}
export function getUnresolvedComputeCenters() {
return unresolvedComputeCenters.slice();
}
// Generic helper used by every entity type that wires the shared
// /collect-location backend pipeline. The endpoint shape (sources, payload
// keys) is domain-specific; the request/response envelope is unified
// (success / candidates / attempted_queries / failure_reason / context).
export async function collectLocationCandidates(endpoint, payload = {}) {
if (!endpoint) throw new Error("endpoint is required");
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload || {}),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`Collect location failed: HTTP ${response.status} ${text}`.trim(),
);
}
return response.json();
}
export async function collectComputeCenterLocation(sourceId, context = {}) {
if (!sourceId) {
throw new Error("sourceId is required");
}
const url = `${COLLECT_LOCATION_API_BASE}/${encodeURIComponent(sourceId)}/collect-location`;
return collectLocationCandidates(url, {
name: context?.name ?? null,
operator: context?.operator ?? null,
site: context?.site ?? null,
organization: context?.organization ?? null,
city: context?.city ?? null,
country: context?.country ?? null,
source: context?.source ?? null,
id: context?.record_id ?? null,
});
}
export async function saveComputeCenterLocation(sourceId, candidate = {}, context = {}) {
if (!sourceId) {
throw new Error("sourceId is required");
}
const latitude = Number(candidate?.latitude);
const longitude = Number(candidate?.longitude);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
throw new Error("candidate latitude/longitude are required");
}
const url = `${COLLECT_LOCATION_API_BASE}/${encodeURIComponent(sourceId)}/location`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
source: context?.source ?? null,
name: context?.name ?? candidate?.matched_location_name ?? candidate?.display_name ?? null,
operator: context?.operator ?? null,
site: context?.site ?? null,
city: candidate?.city ?? context?.city ?? null,
country: candidate?.country ?? context?.country ?? null,
latitude,
longitude,
precision: candidate?.precision ?? "city",
confidence: candidate?.confidence ?? null,
location_source: candidate?.source ?? "manual_selection",
source_url: candidate?.source_url ?? null,
source_note: candidate?.source_note ?? null,
raw_payload: candidate || {},
needs_confirmation: false,
verification_status: "verified",
}),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(
`Save compute center location failed: HTTP ${response.status} ${text}`.trim(),
);
}
return response.json();
}
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 : [];
const unresolved = Array.isArray(payload?.unresolved) ? payload.unresolved : [];
clearComputeCenterData(earth);
unresolvedComputeCenters = unresolved;
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,
unresolvedCount: unresolvedComputeCenters.length,
unresolved: unresolvedComputeCenters.slice(),
summary: getComputeCenterStatusSummary(),
};
}
export function getComputeCenterPointerIntersections(options) {
return computeCenterIconLayer.getPointerIntersections(options);
}
export function updateComputeCenterVisualState(lockedObjectType, lockedObject, camera) {
computeCenterIconLayer.updateVisualState(lockedObjectType, lockedObject, camera);
}