788 lines
22 KiB
JavaScript
788 lines
22 KiB
JavaScript
import * as THREE from "three";
|
|
|
|
import { BGP_CONFIG, CONFIG, PATHS } from "./constants.js";
|
|
import { latLonToVector3 } from "./utils.js";
|
|
|
|
const bgpGroup = new THREE.Group();
|
|
const bgpOverlayGroup = new THREE.Group();
|
|
const collectorMarkers = [];
|
|
const anomalyMarkers = [];
|
|
const anomalyCountByCollector = new Map();
|
|
|
|
let showBGP = true;
|
|
let totalAnomalyCount = 0;
|
|
let textureCache = null;
|
|
let activeEventOverlay = null;
|
|
const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
|
|
numeric: "auto",
|
|
});
|
|
|
|
function getMarkerTexture() {
|
|
if (textureCache) return textureCache;
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 128;
|
|
canvas.height = 128;
|
|
|
|
const context = canvas.getContext("2d");
|
|
if (!context) {
|
|
textureCache = new THREE.Texture(canvas);
|
|
return textureCache;
|
|
}
|
|
|
|
const gradient = context.createRadialGradient(64, 64, 8, 64, 64, 56);
|
|
gradient.addColorStop(0, "rgba(255,255,255,1)");
|
|
gradient.addColorStop(0.24, "rgba(255,255,255,0.92)");
|
|
gradient.addColorStop(0.58, "rgba(255,255,255,0.35)");
|
|
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
|
|
context.fillStyle = gradient;
|
|
context.beginPath();
|
|
context.arc(64, 64, 56, 0, Math.PI * 2);
|
|
context.fill();
|
|
|
|
textureCache = new THREE.CanvasTexture(canvas);
|
|
return textureCache;
|
|
}
|
|
|
|
function normalizeSeverity(severity) {
|
|
const value = String(severity || "").trim().toLowerCase();
|
|
|
|
if (value === "critical") return "critical";
|
|
if (value === "high" || value === "major") return "high";
|
|
if (value === "medium" || value === "moderate" || value === "warning") {
|
|
return "medium";
|
|
}
|
|
if (value === "low" || value === "info" || value === "informational") {
|
|
return "low";
|
|
}
|
|
|
|
return "medium";
|
|
}
|
|
|
|
function getSeverityColor(severity) {
|
|
return BGP_CONFIG.severityColors[normalizeSeverity(severity)];
|
|
}
|
|
|
|
function getSeverityScale(severity) {
|
|
return BGP_CONFIG.severityScales[normalizeSeverity(severity)];
|
|
}
|
|
|
|
function formatLocalDateTime(value) {
|
|
if (!value) return "-";
|
|
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return String(value);
|
|
|
|
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
|
|
}
|
|
|
|
function toDate(value) {
|
|
if (!value) return null;
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return null;
|
|
return date;
|
|
}
|
|
|
|
function formatRelativeTime(value) {
|
|
const date = toDate(value);
|
|
if (!date) return null;
|
|
|
|
const diffMs = date.getTime() - Date.now();
|
|
const absMs = Math.abs(diffMs);
|
|
|
|
if (absMs < 60 * 1000) {
|
|
return relativeTimeFormatter.format(Math.round(diffMs / 1000), "second");
|
|
}
|
|
if (absMs < 60 * 60 * 1000) {
|
|
return relativeTimeFormatter.format(Math.round(diffMs / (60 * 1000)), "minute");
|
|
}
|
|
if (absMs < 24 * 60 * 60 * 1000) {
|
|
return relativeTimeFormatter.format(Math.round(diffMs / (60 * 60 * 1000)), "hour");
|
|
}
|
|
return relativeTimeFormatter.format(
|
|
Math.round(diffMs / (24 * 60 * 60 * 1000)),
|
|
"day",
|
|
);
|
|
}
|
|
|
|
export function formatBGPSeverityLabel(severity) {
|
|
const normalized = normalizeSeverity(severity);
|
|
switch (normalized) {
|
|
case "critical":
|
|
return "严重";
|
|
case "high":
|
|
return "高";
|
|
case "medium":
|
|
return "中";
|
|
case "low":
|
|
return "低";
|
|
default:
|
|
return "中";
|
|
}
|
|
}
|
|
|
|
export function formatBGPAnomalyTypeLabel(type) {
|
|
const value = String(type || "").trim().toLowerCase();
|
|
if (!value) return "-";
|
|
|
|
if (value.includes("hijack")) return "前缀劫持";
|
|
if (value.includes("leak")) return "路由泄露";
|
|
if (value.includes("withdraw")) return "大规模撤销";
|
|
if (value.includes("subprefix") || value.includes("more_specific")) {
|
|
return "更具体前缀异常";
|
|
}
|
|
if (value.includes("path")) return "路径突变";
|
|
if (value.includes("flap")) return "路由抖动";
|
|
|
|
return String(type);
|
|
}
|
|
|
|
export function formatBGPStatusLabel(status) {
|
|
const value = String(status || "").trim().toLowerCase();
|
|
if (!value) return "-";
|
|
if (value === "active") return "活跃";
|
|
if (value === "resolved") return "已恢复";
|
|
if (value === "suppressed") return "已抑制";
|
|
return String(status);
|
|
}
|
|
|
|
export function formatBGPCollectorStatus(status) {
|
|
const value = String(status || "").trim().toLowerCase();
|
|
if (!value) return "在线";
|
|
if (value === "online") return "在线";
|
|
if (value === "offline") return "离线";
|
|
return String(status);
|
|
}
|
|
|
|
export function formatBGPConfidence(value) {
|
|
if (value === null || value === undefined || value === "") return "-";
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return String(value);
|
|
if (number >= 0 && number <= 1) {
|
|
return `${Math.round(number * 100)}%`;
|
|
}
|
|
return `${Math.round(number)}%`;
|
|
}
|
|
|
|
export function formatBGPLocation(city, country) {
|
|
const cityText = city || "";
|
|
const countryText = country || "";
|
|
if (cityText && countryText) return `${cityText}, ${countryText}`;
|
|
return cityText || countryText || "-";
|
|
}
|
|
|
|
export function formatBGPRouteChange(originAsn, newOriginAsn) {
|
|
const from = originAsn ?? "-";
|
|
const to = newOriginAsn ?? "-";
|
|
|
|
if ((from === "-" || from === "" || from === null) && (to === "-" || to === "" || to === null)) {
|
|
return "-";
|
|
}
|
|
if (to === "-" || to === "" || to === null) {
|
|
return `AS${from}`;
|
|
}
|
|
return `AS${from} -> AS${to}`;
|
|
}
|
|
|
|
export function formatBGPObservedTime(value) {
|
|
const absolute = formatLocalDateTime(value);
|
|
const relative = formatRelativeTime(value);
|
|
if (!relative || absolute === "-") return absolute;
|
|
return `${relative} (${absolute})`;
|
|
}
|
|
|
|
export function formatBGPASPath(asPath) {
|
|
if (!Array.isArray(asPath) || asPath.length === 0) return "-";
|
|
return asPath.map((asn) => `AS${asn}`).join(" -> ");
|
|
}
|
|
|
|
export function formatBGPObservedBy(collectors) {
|
|
if (!Array.isArray(collectors) || collectors.length === 0) return "-";
|
|
const preview = collectors.slice(0, 3).join(", ");
|
|
if (collectors.length <= 3) {
|
|
return `${collectors.length}个观测站 (${preview})`;
|
|
}
|
|
return `${collectors.length}个观测站 (${preview} 等)`;
|
|
}
|
|
|
|
export function formatBGPImpactedScope(regions) {
|
|
if (!Array.isArray(regions) || regions.length === 0) return "-";
|
|
const labels = regions
|
|
.map((region) => {
|
|
const city = region?.city || "";
|
|
const country = region?.country || "";
|
|
return city && country ? `${city}, ${country}` : city || country || "";
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (labels.length === 0) return "-";
|
|
if (labels.length <= 3) return labels.join(" / ");
|
|
return `${labels.slice(0, 3).join(" / ")} 等${labels.length}地`;
|
|
}
|
|
|
|
function buildCollectorFeatureData(feature) {
|
|
const coordinates = feature?.geometry?.coordinates || [];
|
|
const [longitude, latitude] = coordinates;
|
|
if (
|
|
typeof latitude !== "number" ||
|
|
typeof longitude !== "number" ||
|
|
Number.isNaN(latitude) ||
|
|
Number.isNaN(longitude)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const properties = feature?.properties || {};
|
|
return {
|
|
latitude,
|
|
longitude,
|
|
collector: properties.collector || "-",
|
|
city: properties.city || "-",
|
|
country: properties.country || "-",
|
|
status: properties.status || "online",
|
|
};
|
|
}
|
|
|
|
function spreadCollectorPositions(markers) {
|
|
const groups = new Map();
|
|
|
|
markers.forEach((marker) => {
|
|
const key = `${marker.latitude.toFixed(4)}|${marker.longitude.toFixed(4)}`;
|
|
if (!groups.has(key)) {
|
|
groups.set(key, []);
|
|
}
|
|
groups.get(key).push(marker);
|
|
});
|
|
|
|
groups.forEach((group) => {
|
|
if (group.length <= 1) return;
|
|
|
|
const radius = 0.9;
|
|
group.forEach((marker, index) => {
|
|
const angle = (Math.PI * 2 * index) / group.length;
|
|
marker.displayLatitude =
|
|
marker.latitude + Math.sin(angle) * radius * 0.18;
|
|
marker.displayLongitude =
|
|
marker.longitude + Math.cos(angle) * radius * 0.18;
|
|
marker.isSpread = true;
|
|
marker.groupSize = group.length;
|
|
});
|
|
});
|
|
|
|
markers.forEach((marker) => {
|
|
if (marker.displayLatitude === undefined) {
|
|
marker.displayLatitude = marker.latitude;
|
|
marker.displayLongitude = marker.longitude;
|
|
marker.isSpread = false;
|
|
marker.groupSize = 1;
|
|
}
|
|
});
|
|
|
|
return markers;
|
|
}
|
|
|
|
function buildAnomalyFeatureData(feature) {
|
|
const coordinates = feature?.geometry?.coordinates || [];
|
|
const [longitude, latitude] = coordinates;
|
|
if (
|
|
typeof latitude !== "number" ||
|
|
typeof longitude !== "number" ||
|
|
Number.isNaN(latitude) ||
|
|
Number.isNaN(longitude)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const properties = feature?.properties || {};
|
|
const severity = normalizeSeverity(properties.severity);
|
|
const createdAt = properties.created_at || null;
|
|
|
|
return {
|
|
latitude,
|
|
longitude,
|
|
rawSeverity: properties.severity || severity,
|
|
severity,
|
|
collector: properties.collector || "-",
|
|
city: properties.city || "-",
|
|
country: properties.country || "-",
|
|
source: properties.source || "-",
|
|
anomaly_type: properties.anomaly_type || "-",
|
|
status: properties.status || "-",
|
|
prefix: properties.prefix || "-",
|
|
origin_asn: properties.origin_asn ?? "-",
|
|
new_origin_asn: properties.new_origin_asn ?? "-",
|
|
as_path: Array.isArray(properties.as_path) ? properties.as_path : [],
|
|
collectors: Array.isArray(properties.collectors) ? properties.collectors : [],
|
|
collector_count: properties.collector_count ?? 1,
|
|
impacted_regions: Array.isArray(properties.impacted_regions)
|
|
? properties.impacted_regions
|
|
: [],
|
|
confidence: properties.confidence ?? "-",
|
|
summary: properties.summary || "-",
|
|
created_at: formatLocalDateTime(createdAt),
|
|
created_at_raw: createdAt,
|
|
id:
|
|
properties.id ||
|
|
`${properties.collector || "unknown"}-${latitude}-${longitude}`,
|
|
};
|
|
}
|
|
|
|
function clearMarkerArray(markers) {
|
|
while (markers.length > 0) {
|
|
const marker = markers.pop();
|
|
marker.material?.dispose();
|
|
bgpGroup.remove(marker);
|
|
}
|
|
}
|
|
|
|
function clearGroup(group) {
|
|
while (group.children.length > 0) {
|
|
const child = group.children[group.children.length - 1];
|
|
group.remove(child);
|
|
if (child.geometry) child.geometry.dispose();
|
|
if (child.material) child.material.dispose();
|
|
}
|
|
}
|
|
|
|
function createSpriteMaterial({ color, opacity }) {
|
|
return new THREE.SpriteMaterial({
|
|
map: getMarkerTexture(),
|
|
color,
|
|
transparent: true,
|
|
opacity,
|
|
depthWrite: false,
|
|
depthTest: true,
|
|
blending: THREE.AdditiveBlending,
|
|
});
|
|
}
|
|
|
|
function createOverlaySprite({ color, opacity, scale }) {
|
|
const sprite = new THREE.Sprite(createSpriteMaterial({ color, opacity }));
|
|
sprite.scale.setScalar(scale);
|
|
return sprite;
|
|
}
|
|
|
|
function createArcLine(start, end, color) {
|
|
const midpoint = start
|
|
.clone()
|
|
.add(end)
|
|
.multiplyScalar(0.5)
|
|
.normalize()
|
|
.multiplyScalar(CONFIG.earthRadius + BGP_CONFIG.eventHubAltitudeOffset * 0.8);
|
|
|
|
const curve = new THREE.QuadraticBezierCurve3(start, midpoint, end);
|
|
const points = curve.getPoints(32);
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
const material = new THREE.LineBasicMaterial({
|
|
color,
|
|
transparent: true,
|
|
opacity: 0.82,
|
|
depthWrite: false,
|
|
blending: THREE.AdditiveBlending,
|
|
});
|
|
|
|
return new THREE.Line(geometry, material);
|
|
}
|
|
|
|
function createCollectorMarker(markerData) {
|
|
const sprite = new THREE.Sprite(
|
|
createSpriteMaterial({
|
|
color: BGP_CONFIG.collectorColor,
|
|
opacity: BGP_CONFIG.opacity.collector,
|
|
}),
|
|
);
|
|
|
|
const position = latLonToVector3(
|
|
markerData.displayLatitude,
|
|
markerData.displayLongitude,
|
|
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset,
|
|
);
|
|
|
|
sprite.position.copy(position);
|
|
sprite.scale.setScalar(BGP_CONFIG.collectorScale);
|
|
sprite.renderOrder = 3;
|
|
sprite.visible = showBGP;
|
|
sprite.userData = {
|
|
type: "bgp_collector",
|
|
state: "normal",
|
|
baseScale: BGP_CONFIG.collectorScale,
|
|
pulseOffset: Math.random() * Math.PI * 2,
|
|
anomaly_count: 0,
|
|
...markerData,
|
|
};
|
|
|
|
collectorMarkers.push(sprite);
|
|
bgpGroup.add(sprite);
|
|
}
|
|
|
|
function createAnomalyMarker(markerData) {
|
|
const sprite = new THREE.Sprite(
|
|
createSpriteMaterial({
|
|
color: getSeverityColor(markerData.severity),
|
|
opacity: BGP_CONFIG.opacity.normal,
|
|
}),
|
|
);
|
|
|
|
const position = latLonToVector3(
|
|
markerData.latitude,
|
|
markerData.longitude,
|
|
CONFIG.earthRadius + BGP_CONFIG.altitudeOffset,
|
|
);
|
|
|
|
const baseScale = BGP_CONFIG.baseScale * getSeverityScale(markerData.severity);
|
|
sprite.position.copy(position);
|
|
sprite.scale.setScalar(baseScale);
|
|
sprite.renderOrder = 5;
|
|
sprite.visible = showBGP;
|
|
sprite.userData = {
|
|
type: "bgp",
|
|
state: "normal",
|
|
baseScale,
|
|
pulseOffset: Math.random() * Math.PI * 2,
|
|
...markerData,
|
|
};
|
|
|
|
anomalyMarkers.push(sprite);
|
|
bgpGroup.add(sprite);
|
|
}
|
|
|
|
function dedupeAnomalies(features) {
|
|
const latestByCollector = new Map();
|
|
|
|
features.forEach((feature) => {
|
|
const data = buildAnomalyFeatureData(feature);
|
|
if (!data) return;
|
|
|
|
anomalyCountByCollector.set(
|
|
data.collector,
|
|
(anomalyCountByCollector.get(data.collector) || 0) + 1,
|
|
);
|
|
|
|
const dedupeKey = `${data.collector}|${data.latitude.toFixed(4)}|${data.longitude.toFixed(4)}`;
|
|
const previous = latestByCollector.get(dedupeKey);
|
|
const currentTime = data.created_at_raw
|
|
? new Date(data.created_at_raw).getTime()
|
|
: 0;
|
|
const previousTime = previous?.created_at_raw
|
|
? new Date(previous.created_at_raw).getTime()
|
|
: 0;
|
|
|
|
if (!previous || currentTime >= previousTime) {
|
|
latestByCollector.set(dedupeKey, data);
|
|
}
|
|
});
|
|
|
|
return Array.from(latestByCollector.values())
|
|
.sort((a, b) => {
|
|
const timeA = a.created_at_raw ? new Date(a.created_at_raw).getTime() : 0;
|
|
const timeB = b.created_at_raw ? new Date(b.created_at_raw).getTime() : 0;
|
|
return timeB - timeA;
|
|
})
|
|
.slice(0, BGP_CONFIG.maxRenderedMarkers);
|
|
}
|
|
|
|
function applyCollectorCounts() {
|
|
collectorMarkers.forEach((marker) => {
|
|
marker.userData.anomaly_count =
|
|
anomalyCountByCollector.get(marker.userData.collector) || 0;
|
|
});
|
|
}
|
|
|
|
export async function loadBGPAnomalies(scene, earth) {
|
|
clearBGPData(earth);
|
|
|
|
const [collectorsResponse, anomaliesResponse] = await Promise.all([
|
|
fetch(PATHS.bgpCollectorsApi),
|
|
fetch(`${PATHS.bgpApi}?limit=${BGP_CONFIG.defaultFetchLimit}`),
|
|
]);
|
|
|
|
if (!collectorsResponse.ok) {
|
|
throw new Error(`BGP collectors HTTP ${collectorsResponse.status}`);
|
|
}
|
|
if (!anomaliesResponse.ok) {
|
|
throw new Error(`BGP anomalies HTTP ${anomaliesResponse.status}`);
|
|
}
|
|
|
|
const collectorsPayload = await collectorsResponse.json();
|
|
const anomaliesPayload = await anomaliesResponse.json();
|
|
const collectorFeatures = Array.isArray(collectorsPayload?.features)
|
|
? collectorsPayload.features
|
|
: [];
|
|
const anomalyFeatures = Array.isArray(anomaliesPayload?.features)
|
|
? anomaliesPayload.features
|
|
: [];
|
|
|
|
totalAnomalyCount = anomaliesPayload?.count ?? anomalyFeatures.length;
|
|
anomalyCountByCollector.clear();
|
|
|
|
spreadCollectorPositions(
|
|
collectorFeatures
|
|
.map(buildCollectorFeatureData)
|
|
.filter(Boolean),
|
|
).forEach(createCollectorMarker);
|
|
|
|
dedupeAnomalies(anomalyFeatures).forEach(createAnomalyMarker);
|
|
applyCollectorCounts();
|
|
|
|
if (!bgpGroup.parent) {
|
|
earth.add(bgpGroup);
|
|
}
|
|
if (!bgpOverlayGroup.parent) {
|
|
earth.add(bgpOverlayGroup);
|
|
}
|
|
|
|
bgpGroup.visible = showBGP;
|
|
bgpOverlayGroup.visible = showBGP;
|
|
|
|
if (scene && !scene.children.includes(earth)) {
|
|
scene.add(earth);
|
|
}
|
|
|
|
return {
|
|
totalCount: totalAnomalyCount,
|
|
renderedCount: anomalyMarkers.length,
|
|
collectorCount: collectorMarkers.length,
|
|
};
|
|
}
|
|
|
|
export function updateBGPVisualState(lockedObjectType, lockedObject) {
|
|
const now = performance.now();
|
|
const hasLockedLayer = Boolean(
|
|
lockedObject && ["cable", "satellite", "bgp", "bgp_collector"].includes(lockedObjectType),
|
|
);
|
|
|
|
collectorMarkers.forEach((marker) => {
|
|
const isLocked =
|
|
(lockedObjectType === "bgp_collector" || lockedObjectType === "bgp") &&
|
|
lockedObject?.userData?.collector === marker.userData.collector;
|
|
const isHovered =
|
|
marker.userData.state === "hover" || marker.userData.state === "linked";
|
|
const pulse =
|
|
0.5 +
|
|
0.5 *
|
|
Math.sin(
|
|
now * BGP_CONFIG.collectorPulseSpeed + marker.userData.pulseOffset,
|
|
);
|
|
|
|
let scale = marker.userData.baseScale;
|
|
let opacity = BGP_CONFIG.opacity.collector;
|
|
|
|
if (isLocked) {
|
|
scale *= 1.1 + 0.14 * pulse;
|
|
opacity = BGP_CONFIG.opacity.collectorHover;
|
|
} else if (isHovered) {
|
|
scale *= 1.08;
|
|
opacity = BGP_CONFIG.opacity.collectorHover;
|
|
} else if (hasLockedLayer) {
|
|
scale *= BGP_CONFIG.dimmedScale;
|
|
opacity = BGP_CONFIG.opacity.dimmed;
|
|
} else {
|
|
scale *= 1 + 0.05 * pulse;
|
|
}
|
|
|
|
marker.scale.setScalar(scale);
|
|
marker.material.opacity = opacity;
|
|
marker.visible = showBGP;
|
|
});
|
|
|
|
anomalyMarkers.forEach((marker) => {
|
|
const isLocked = lockedObjectType === "bgp" && lockedObject === marker;
|
|
const isLinkedCollectorLocked =
|
|
lockedObjectType === "bgp_collector" &&
|
|
lockedObject?.userData?.collector === marker.userData.collector;
|
|
const isOtherLocked = hasLockedLayer && !isLocked && !isLinkedCollectorLocked;
|
|
const isHovered = marker.userData.state === "hover";
|
|
const pulse =
|
|
0.5 +
|
|
0.5 * Math.sin(now * BGP_CONFIG.pulseSpeed + marker.userData.pulseOffset);
|
|
|
|
let scale = marker.userData.baseScale;
|
|
let opacity = BGP_CONFIG.opacity.normal;
|
|
|
|
if (isLocked || isLinkedCollectorLocked) {
|
|
scale *= 1 + BGP_CONFIG.lockedPulseAmplitude * pulse;
|
|
opacity =
|
|
BGP_CONFIG.opacity.lockedMin +
|
|
(BGP_CONFIG.opacity.lockedMax - BGP_CONFIG.opacity.lockedMin) * pulse;
|
|
} else if (isHovered) {
|
|
scale *= BGP_CONFIG.hoverScale;
|
|
opacity = BGP_CONFIG.opacity.hover;
|
|
} else if (isOtherLocked) {
|
|
scale *= BGP_CONFIG.dimmedScale;
|
|
opacity = BGP_CONFIG.opacity.dimmed;
|
|
} else {
|
|
scale *= 1 + BGP_CONFIG.normalPulseAmplitude * pulse;
|
|
opacity = BGP_CONFIG.opacity.normal;
|
|
}
|
|
|
|
marker.scale.setScalar(scale);
|
|
marker.material.opacity = opacity;
|
|
marker.visible = showBGP;
|
|
});
|
|
}
|
|
|
|
export function setBGPMarkerState(marker, state = "normal") {
|
|
if (!marker?.userData) return;
|
|
if (marker.userData.type !== "bgp" && marker.userData.type !== "bgp_collector") {
|
|
return;
|
|
}
|
|
marker.userData.state = state;
|
|
}
|
|
|
|
export function clearBGPSelection() {
|
|
collectorMarkers.forEach((marker) => {
|
|
marker.userData.state = "normal";
|
|
});
|
|
anomalyMarkers.forEach((marker) => {
|
|
marker.userData.state = "normal";
|
|
});
|
|
clearBGPEventOverlay();
|
|
}
|
|
|
|
export function clearBGPData(earth) {
|
|
clearMarkerArray(collectorMarkers);
|
|
clearMarkerArray(anomalyMarkers);
|
|
clearBGPEventOverlay();
|
|
anomalyCountByCollector.clear();
|
|
totalAnomalyCount = 0;
|
|
|
|
if (earth && bgpGroup.parent === earth) {
|
|
earth.remove(bgpGroup);
|
|
}
|
|
if (earth && bgpOverlayGroup.parent === earth) {
|
|
earth.remove(bgpOverlayGroup);
|
|
}
|
|
}
|
|
|
|
export function toggleBGP(show) {
|
|
showBGP = Boolean(show);
|
|
bgpGroup.visible = showBGP;
|
|
bgpOverlayGroup.visible = showBGP;
|
|
collectorMarkers.forEach((marker) => {
|
|
marker.visible = showBGP;
|
|
});
|
|
anomalyMarkers.forEach((marker) => {
|
|
marker.visible = showBGP;
|
|
});
|
|
}
|
|
|
|
export function getShowBGP() {
|
|
return showBGP;
|
|
}
|
|
|
|
export function getBGPMarkers() {
|
|
return [...anomalyMarkers, ...collectorMarkers];
|
|
}
|
|
|
|
export function getBGPAnomalyMarkers() {
|
|
return anomalyMarkers;
|
|
}
|
|
|
|
export function getBGPCollectorMarkers() {
|
|
return collectorMarkers;
|
|
}
|
|
|
|
export function getBGPCount() {
|
|
return totalAnomalyCount;
|
|
}
|
|
|
|
export function showBGPEventOverlay(marker, earth) {
|
|
if (!marker?.userData || marker.userData.type !== "bgp" || !earth) return;
|
|
|
|
clearBGPEventOverlay();
|
|
|
|
const impactedRegions =
|
|
Array.isArray(marker.userData.impacted_regions) &&
|
|
marker.userData.impacted_regions.length > 0
|
|
? marker.userData.impacted_regions
|
|
: [
|
|
{
|
|
collector: marker.userData.collector,
|
|
city: marker.userData.city,
|
|
country: marker.userData.country,
|
|
latitude: marker.userData.latitude,
|
|
longitude: marker.userData.longitude,
|
|
},
|
|
];
|
|
|
|
const validRegions = impactedRegions.filter(
|
|
(region) =>
|
|
typeof region?.latitude === "number" &&
|
|
typeof region?.longitude === "number",
|
|
);
|
|
if (validRegions.length === 0) return;
|
|
|
|
const averageLatitude =
|
|
validRegions.reduce((sum, region) => sum + region.latitude, 0) /
|
|
validRegions.length;
|
|
const averageLongitude =
|
|
validRegions.reduce((sum, region) => sum + region.longitude, 0) /
|
|
validRegions.length;
|
|
|
|
const hubPosition = latLonToVector3(
|
|
averageLatitude,
|
|
averageLongitude,
|
|
CONFIG.earthRadius + BGP_CONFIG.eventHubAltitudeOffset,
|
|
);
|
|
const hub = createOverlaySprite({
|
|
color: BGP_CONFIG.eventHubColor,
|
|
opacity: 0.95,
|
|
scale: BGP_CONFIG.eventHubScale,
|
|
});
|
|
hub.position.copy(hubPosition);
|
|
hub.renderOrder = 6;
|
|
bgpOverlayGroup.add(hub);
|
|
|
|
const overlayItems = [hub];
|
|
|
|
validRegions.forEach((region) => {
|
|
const regionPosition = latLonToVector3(
|
|
region.latitude,
|
|
region.longitude,
|
|
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset + 0.3,
|
|
);
|
|
|
|
const link = createArcLine(regionPosition, hubPosition, BGP_CONFIG.linkColor);
|
|
link.renderOrder = 4;
|
|
bgpOverlayGroup.add(link);
|
|
overlayItems.push(link);
|
|
|
|
const halo = createOverlaySprite({
|
|
color: BGP_CONFIG.regionColor,
|
|
opacity: 0.24,
|
|
scale: BGP_CONFIG.regionScale,
|
|
});
|
|
halo.position.copy(
|
|
latLonToVector3(
|
|
region.latitude,
|
|
region.longitude,
|
|
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset - 0.1,
|
|
),
|
|
);
|
|
halo.renderOrder = 2;
|
|
bgpOverlayGroup.add(halo);
|
|
overlayItems.push(halo);
|
|
});
|
|
|
|
activeEventOverlay = overlayItems;
|
|
bgpOverlayGroup.visible = showBGP;
|
|
}
|
|
|
|
export function clearBGPEventOverlay() {
|
|
activeEventOverlay = null;
|
|
clearGroup(bgpOverlayGroup);
|
|
}
|
|
|
|
export function getBGPLegendItems() {
|
|
return [
|
|
{ color: "#6db7ff", label: "观测站" },
|
|
{ color: "#8af5ff", label: "事件连线 / 枢纽" },
|
|
{ color: "#2dd4bf", label: "影响区域" },
|
|
{ color: "#ff4d4f", label: "严重异常" },
|
|
{ color: "#ff9f43", label: "高危异常" },
|
|
{ color: "#ffd166", label: "中危异常" },
|
|
{ color: "#4dabf7", label: "低危异常" },
|
|
];
|
|
}
|