feat: expand bgp observability surfaces
This commit is contained in:
@@ -13,10 +13,12 @@ let showBGP = true;
|
||||
let totalAnomalyCount = 0;
|
||||
let totalIncidentCount = 0;
|
||||
let textureCache = null;
|
||||
let collectorTextureCache = null;
|
||||
let activeEventOverlay = null;
|
||||
const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
|
||||
numeric: "auto",
|
||||
});
|
||||
const MATERIAL_ACCESS_POINT_PATH = "M4.93 4.93A9.97 9.97 0 0 0 2 12c0 2.76 1.12 5.26 2.93 7.07l1.41-1.41A7.94 7.94 0 0 1 4 12c0-2.21.89-4.22 2.34-5.66zm14.14 0l-1.41 1.41A7.96 7.96 0 0 1 20 12c0 2.22-.89 4.22-2.34 5.66l1.41 1.41A9.97 9.97 0 0 0 22 12c0-2.76-1.12-5.26-2.93-7.07M7.76 7.76A5.98 5.98 0 0 0 6 12c0 1.65.67 3.15 1.76 4.24l1.41-1.41A4 4 0 0 1 8 12c0-1.11.45-2.11 1.17-2.83zm8.48 0l-1.41 1.41A4 4 0 0 1 16 12c0 1.11-.45 2.11-1.17 2.83l1.41 1.41A5.98 5.98 0 0 0 18 12c0-1.65-.67-3.15-1.76-4.24M12 10a2 2 0 0 0-2 2a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2";
|
||||
|
||||
function getMarkerTexture() {
|
||||
if (textureCache) return textureCache;
|
||||
@@ -46,6 +48,42 @@ function getMarkerTexture() {
|
||||
return textureCache;
|
||||
}
|
||||
|
||||
function getCollectorTexture() {
|
||||
if (collectorTextureCache) return collectorTextureCache;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
collectorTextureCache = new THREE.Texture(canvas);
|
||||
return collectorTextureCache;
|
||||
}
|
||||
|
||||
context.clearRect(0, 0, 128, 128);
|
||||
|
||||
const glow = context.createRadialGradient(64, 64, 8, 64, 64, 34);
|
||||
glow.addColorStop(0, "rgba(255,255,255,0.36)");
|
||||
glow.addColorStop(0.55, "rgba(255,255,255,0.12)");
|
||||
glow.addColorStop(1, "rgba(255,255,255,0)");
|
||||
context.fillStyle = glow;
|
||||
context.beginPath();
|
||||
context.arc(64, 64, 34, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
|
||||
context.save();
|
||||
context.translate(16, 16);
|
||||
context.scale(4, 4);
|
||||
context.fillStyle = "rgba(255,255,255,0.98)";
|
||||
context.shadowColor = "rgba(255,255,255,0.3)";
|
||||
context.shadowBlur = 3;
|
||||
context.fill(new Path2D(MATERIAL_ACCESS_POINT_PATH));
|
||||
context.restore();
|
||||
|
||||
collectorTextureCache = new THREE.CanvasTexture(canvas);
|
||||
return collectorTextureCache;
|
||||
}
|
||||
|
||||
function normalizeSeverity(severity) {
|
||||
const value = String(severity || "").trim().toLowerCase();
|
||||
|
||||
@@ -69,6 +107,50 @@ function getSeverityScale(severity) {
|
||||
return BGP_CONFIG.severityScales[normalizeSeverity(severity)];
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function getCollectorActivityProfile(markerData) {
|
||||
const recent24h = Number(markerData?.recent_24h_observation_count || 0);
|
||||
const recent7d = Number(markerData?.recent_7d_observation_count || 0);
|
||||
const prefixes = Number(markerData?.prefix_count || 0);
|
||||
const origins = Number(markerData?.origin_asn_count || 0);
|
||||
|
||||
const activityScore =
|
||||
recent24h * 1.7 +
|
||||
recent7d * 0.3 +
|
||||
prefixes * 0.16 +
|
||||
origins * 0.12;
|
||||
|
||||
let tier = "idle";
|
||||
if (activityScore >= 50 || recent24h >= 24) tier = "hot";
|
||||
else if (activityScore >= 20 || recent24h >= 10) tier = "high";
|
||||
else if (activityScore >= 8 || recent24h >= 4) tier = "medium";
|
||||
else if (activityScore > 0) tier = "low";
|
||||
|
||||
const scaleBoost = clamp(1 + Math.log2(activityScore + 1) * 0.12, 1, 1.55);
|
||||
const haloScale =
|
||||
BGP_CONFIG.collectorHaloScale +
|
||||
clamp(Math.log2(recent24h + prefixes + 1) * 2.2, 0, 12);
|
||||
const pulseHaloScale =
|
||||
BGP_CONFIG.collectorPulseHaloScale +
|
||||
clamp(Math.log2(recent24h + recent7d + 1) * 2.8, 0, 14);
|
||||
const coverageHaloScale =
|
||||
BGP_CONFIG.collectorCoverageHaloScale +
|
||||
clamp(Math.log2(prefixes + origins + 1) * 3.4, 0, 18);
|
||||
|
||||
return {
|
||||
tier,
|
||||
color: BGP_CONFIG.collectorHeatColors[tier] || BGP_CONFIG.collectorColor,
|
||||
scaleBoost,
|
||||
haloScale,
|
||||
pulseHaloScale,
|
||||
coverageHaloScale,
|
||||
activityScore,
|
||||
};
|
||||
}
|
||||
|
||||
function formatLocalDateTime(value) {
|
||||
if (!value) return "-";
|
||||
|
||||
@@ -222,6 +304,62 @@ export function formatBGPImpactedScope(regions) {
|
||||
return `${labels.slice(0, 3).join(" / ")} 等${labels.length}地`;
|
||||
}
|
||||
|
||||
export function formatBGPRelatedCables(items) {
|
||||
if (!Array.isArray(items) || items.length === 0) return "-";
|
||||
|
||||
const labels = items
|
||||
.slice(0, 3)
|
||||
.map((item) => {
|
||||
const landing = item?.landing_point || "";
|
||||
const cables = Array.isArray(item?.cable_names) ? item.cable_names : [];
|
||||
const cableText = cables.length > 0 ? cables.slice(0, 2).join(", ") : "附近登陆点";
|
||||
const distance = item?.distance_km !== undefined ? ` ${item.distance_km}km` : "";
|
||||
return `${landing || cableText} (${cableText}${distance})`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (labels.length === 0) return "-";
|
||||
if (items.length <= 3) return labels.join(" / ");
|
||||
return `${labels.join(" / ")} 等${items.length}处`;
|
||||
}
|
||||
|
||||
export function formatBGPScope(scope) {
|
||||
const countries = Array.isArray(scope?.countries) ? scope.countries : [];
|
||||
const cities = Array.isArray(scope?.cities) ? scope.cities : [];
|
||||
const cityText = cities.slice(0, 3).join(" / ");
|
||||
const countryText = countries.slice(0, 3).join(" / ");
|
||||
|
||||
if (cityText && countryText) {
|
||||
return `${cityText} | ${countryText}`;
|
||||
}
|
||||
return cityText || countryText || "-";
|
||||
}
|
||||
|
||||
export function formatBGPTopEventTypes(items) {
|
||||
if (!Array.isArray(items) || items.length === 0) return "-";
|
||||
return items
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item?.event_type || "-"} x${item?.count || 0}`)
|
||||
.join(" / ");
|
||||
}
|
||||
|
||||
export function formatBGPCollectorCoverageHalo(markerData) {
|
||||
const prefixes = Number(
|
||||
markerData?.recent_24h_prefix_count ||
|
||||
markerData?.recent_7d_prefix_count ||
|
||||
markerData?.prefix_count ||
|
||||
0,
|
||||
);
|
||||
const observations = Number(
|
||||
markerData?.recent_24h_observation_count ||
|
||||
markerData?.recent_7d_observation_count ||
|
||||
markerData?.observation_count ||
|
||||
0,
|
||||
);
|
||||
if (prefixes <= 0 && observations <= 0) return "静态观测站";
|
||||
return `近24h ${observations}条事件 / ${prefixes}个前缀`;
|
||||
}
|
||||
|
||||
function buildCollectorFeatureData(feature) {
|
||||
const coordinates = feature?.geometry?.coordinates || [];
|
||||
const [longitude, latitude] = coordinates;
|
||||
@@ -242,6 +380,19 @@ function buildCollectorFeatureData(feature) {
|
||||
city: properties.city || "-",
|
||||
country: properties.country || "-",
|
||||
status: properties.status || "online",
|
||||
observation_count: properties.observation_count || 0,
|
||||
recent_24h_observation_count: properties.recent_24h_observation_count || 0,
|
||||
recent_7d_observation_count: properties.recent_7d_observation_count || 0,
|
||||
prefix_count: properties.prefix_count || 0,
|
||||
recent_24h_prefix_count: properties.recent_24h_prefix_count || 0,
|
||||
recent_7d_prefix_count: properties.recent_7d_prefix_count || 0,
|
||||
origin_asn_count: properties.origin_asn_count || 0,
|
||||
latest_observed_at: properties.latest_observed_at || null,
|
||||
latest_event_type: properties.latest_event_type || null,
|
||||
top_event_types: Array.isArray(properties.top_event_types)
|
||||
? properties.top_event_types
|
||||
: [],
|
||||
baseline_scope: properties.baseline_scope || { countries: [], cities: [] },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -381,6 +532,12 @@ function buildIncidentFeatureData(feature) {
|
||||
collectors: affectedCollectors,
|
||||
collector_count: affectedCollectors.length || 1,
|
||||
impacted_regions: affectedRegions,
|
||||
related_cables: Array.isArray(properties.related_cables)
|
||||
? properties.related_cables
|
||||
: [],
|
||||
related_ixps: Array.isArray(properties.related_ixps)
|
||||
? properties.related_ixps
|
||||
: [],
|
||||
confidence: properties.confidence ?? "-",
|
||||
summary: properties.summary || properties.title || "-",
|
||||
created_at: formatLocalDateTime(startedAt),
|
||||
@@ -404,6 +561,10 @@ function buildIncidentFeatureData(feature) {
|
||||
function clearMarkerArray(markers) {
|
||||
while (markers.length > 0) {
|
||||
const marker = markers.pop();
|
||||
while (marker.children.length > 0) {
|
||||
const child = marker.children.pop();
|
||||
child.material?.dispose();
|
||||
}
|
||||
marker.material?.dispose();
|
||||
bgpGroup.remove(marker);
|
||||
}
|
||||
@@ -459,10 +620,15 @@ function createArcLine(start, end, color) {
|
||||
}
|
||||
|
||||
function createCollectorMarker(markerData) {
|
||||
const activity = getCollectorActivityProfile(markerData);
|
||||
const sprite = new THREE.Sprite(
|
||||
createSpriteMaterial({
|
||||
color: BGP_CONFIG.collectorColor,
|
||||
new THREE.SpriteMaterial({
|
||||
map: getCollectorTexture(),
|
||||
color: activity.color,
|
||||
transparent: true,
|
||||
opacity: BGP_CONFIG.opacity.collector,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -473,15 +639,46 @@ function createCollectorMarker(markerData) {
|
||||
);
|
||||
|
||||
sprite.position.copy(position);
|
||||
sprite.scale.setScalar(BGP_CONFIG.collectorScale);
|
||||
sprite.scale.set(BGP_CONFIG.collectorScale * 0.88 * activity.scaleBoost, BGP_CONFIG.collectorScale * 1.08 * activity.scaleBoost, 1);
|
||||
sprite.renderOrder = 3;
|
||||
sprite.visible = showBGP;
|
||||
|
||||
const heatHalo = createOverlaySprite({
|
||||
color: activity.color,
|
||||
opacity: 0.018,
|
||||
scale: activity.haloScale * 0.78,
|
||||
});
|
||||
heatHalo.renderOrder = 1;
|
||||
sprite.add(heatHalo);
|
||||
|
||||
const pulseHalo = createOverlaySprite({
|
||||
color: activity.color,
|
||||
opacity: 0.008,
|
||||
scale: activity.pulseHaloScale * 0.72,
|
||||
});
|
||||
pulseHalo.renderOrder = 0;
|
||||
sprite.add(pulseHalo);
|
||||
|
||||
const coverageHalo = createOverlaySprite({
|
||||
color: BGP_CONFIG.regionColor,
|
||||
opacity: 0.01,
|
||||
scale: activity.coverageHaloScale * 0.7,
|
||||
});
|
||||
coverageHalo.renderOrder = 0;
|
||||
coverageHalo.scale.set(activity.coverageHaloScale * 0.82, activity.coverageHaloScale * 0.56, 1);
|
||||
sprite.add(coverageHalo);
|
||||
|
||||
sprite.userData = {
|
||||
type: "bgp_collector",
|
||||
state: "normal",
|
||||
baseScale: BGP_CONFIG.collectorScale,
|
||||
baseScale: BGP_CONFIG.collectorScale * activity.scaleBoost,
|
||||
baseColor: activity.color,
|
||||
pulseOffset: Math.random() * Math.PI * 2,
|
||||
anomaly_count: 0,
|
||||
activity,
|
||||
heatHalo,
|
||||
pulseHalo,
|
||||
coverageHalo,
|
||||
...markerData,
|
||||
};
|
||||
|
||||
@@ -512,6 +709,7 @@ function createAnomalyMarker(markerData) {
|
||||
type: "bgp",
|
||||
state: "normal",
|
||||
baseScale,
|
||||
baseColor: getSeverityColor(markerData.severity),
|
||||
pulseOffset: Math.random() * Math.PI * 2,
|
||||
...markerData,
|
||||
};
|
||||
@@ -692,23 +890,61 @@ export function updateBGPVisualState(lockedObjectType, lockedObject) {
|
||||
|
||||
let scale = marker.userData.baseScale;
|
||||
let opacity = BGP_CONFIG.opacity.collector;
|
||||
let haloOpacity = 0.018;
|
||||
let pulseOpacity = 0.008;
|
||||
let coverageOpacity = 0.01;
|
||||
let markerColor = marker.userData.baseColor || BGP_CONFIG.collectorColor;
|
||||
|
||||
if (isLocked) {
|
||||
scale *= 1.1 + 0.14 * pulse;
|
||||
opacity = BGP_CONFIG.opacity.collectorHover;
|
||||
haloOpacity = 0.07;
|
||||
pulseOpacity = 0.04;
|
||||
coverageOpacity = 0.035;
|
||||
} else if (isHovered) {
|
||||
scale *= 1.08;
|
||||
opacity = BGP_CONFIG.opacity.collectorHover;
|
||||
haloOpacity = 0.05;
|
||||
pulseOpacity = 0.03;
|
||||
coverageOpacity = 0.028;
|
||||
} else if (hasLockedLayer) {
|
||||
scale *= BGP_CONFIG.dimmedScale;
|
||||
opacity = BGP_CONFIG.opacity.dimmed;
|
||||
opacity = 0.12;
|
||||
haloOpacity = 0.0;
|
||||
pulseOpacity = 0.0;
|
||||
coverageOpacity = 0.0;
|
||||
markerColor = 0x7d8ca3;
|
||||
} else {
|
||||
scale *= 1 + 0.05 * pulse;
|
||||
}
|
||||
|
||||
marker.scale.setScalar(scale);
|
||||
marker.material.color.setHex(markerColor);
|
||||
marker.material.opacity = opacity;
|
||||
marker.visible = showBGP;
|
||||
|
||||
if (marker.userData.heatHalo) {
|
||||
marker.userData.heatHalo.material.opacity = haloOpacity;
|
||||
marker.userData.heatHalo.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor);
|
||||
marker.userData.heatHalo.scale.setScalar(
|
||||
marker.userData.activity?.haloScale * 0.78 * (1 + pulse * 0.03),
|
||||
);
|
||||
}
|
||||
if (marker.userData.pulseHalo) {
|
||||
marker.userData.pulseHalo.material.opacity = pulseOpacity;
|
||||
marker.userData.pulseHalo.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor);
|
||||
marker.userData.pulseHalo.scale.setScalar(
|
||||
marker.userData.activity?.pulseHaloScale * 0.72 * (1 + pulse * 0.05),
|
||||
);
|
||||
}
|
||||
if (marker.userData.coverageHalo) {
|
||||
marker.userData.coverageHalo.material.opacity = coverageOpacity;
|
||||
marker.userData.coverageHalo.scale.set(
|
||||
marker.userData.activity?.coverageHaloScale * 0.82 * (1 + pulse * 0.012),
|
||||
marker.userData.activity?.coverageHaloScale * 0.56 * (1 + pulse * 0.012),
|
||||
1,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
anomalyMarkers.forEach((marker) => {
|
||||
@@ -724,6 +960,7 @@ export function updateBGPVisualState(lockedObjectType, lockedObject) {
|
||||
|
||||
let scale = marker.userData.baseScale;
|
||||
let opacity = BGP_CONFIG.opacity.normal;
|
||||
let markerColor = marker.userData.baseColor || getSeverityColor(marker.userData.severity);
|
||||
|
||||
if (isLocked || isLinkedCollectorLocked) {
|
||||
scale *= 1 + BGP_CONFIG.lockedPulseAmplitude * pulse;
|
||||
@@ -735,13 +972,15 @@ export function updateBGPVisualState(lockedObjectType, lockedObject) {
|
||||
opacity = BGP_CONFIG.opacity.hover;
|
||||
} else if (isOtherLocked) {
|
||||
scale *= BGP_CONFIG.dimmedScale;
|
||||
opacity = BGP_CONFIG.opacity.dimmed;
|
||||
opacity = 0.1;
|
||||
markerColor = 0x7d8ca3;
|
||||
} else {
|
||||
scale *= 1 + BGP_CONFIG.normalPulseAmplitude * pulse;
|
||||
opacity = BGP_CONFIG.opacity.normal;
|
||||
}
|
||||
|
||||
marker.scale.setScalar(scale);
|
||||
marker.material.color.setHex(markerColor);
|
||||
marker.material.opacity = opacity;
|
||||
marker.visible = showBGP;
|
||||
});
|
||||
@@ -915,6 +1154,63 @@ export function showBGPEventOverlay(marker, earth) {
|
||||
bgpOverlayGroup.visible = showBGP;
|
||||
}
|
||||
|
||||
export function showBGPCollectorCoverageOverlay(marker, earth) {
|
||||
if (!marker?.userData || marker.userData.type !== "bgp_collector" || !earth) return;
|
||||
|
||||
clearBGPEventOverlay();
|
||||
|
||||
const prefixCount = Number(
|
||||
marker.userData.recent_24h_prefix_count ||
|
||||
marker.userData.recent_7d_prefix_count ||
|
||||
marker.userData.prefix_count ||
|
||||
0,
|
||||
);
|
||||
const observationCount = Number(
|
||||
marker.userData.recent_24h_observation_count ||
|
||||
marker.userData.recent_7d_observation_count ||
|
||||
marker.userData.observation_count ||
|
||||
0,
|
||||
);
|
||||
const scaleBoost = Math.min(10, Math.log2(prefixCount + observationCount + 1) * 1.8);
|
||||
const haloScale = BGP_CONFIG.regionScale * 0.7 + scaleBoost;
|
||||
const pulseHaloScale = haloScale * 1.32;
|
||||
|
||||
const halo = createOverlaySprite({
|
||||
color: BGP_CONFIG.regionColor,
|
||||
opacity: 0.11,
|
||||
scale: haloScale * 0.78,
|
||||
});
|
||||
halo.position.copy(
|
||||
latLonToVector3(
|
||||
marker.userData.displayLatitude ?? marker.userData.latitude,
|
||||
marker.userData.displayLongitude ?? marker.userData.longitude,
|
||||
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset - 0.15,
|
||||
),
|
||||
);
|
||||
halo.renderOrder = 2;
|
||||
bgpOverlayGroup.add(halo);
|
||||
|
||||
const pulseHalo = createOverlaySprite({
|
||||
color: BGP_CONFIG.collectorColor,
|
||||
opacity: 0.065,
|
||||
scale: pulseHaloScale * 0.82,
|
||||
});
|
||||
pulseHalo.position.copy(halo.position);
|
||||
pulseHalo.renderOrder = 1;
|
||||
bgpOverlayGroup.add(pulseHalo);
|
||||
const innerRing = createOverlaySprite({
|
||||
color: BGP_CONFIG.collectorColor,
|
||||
opacity: 0.12,
|
||||
scale: Math.max(haloScale * 0.34, 5.5),
|
||||
});
|
||||
innerRing.position.copy(halo.position);
|
||||
innerRing.renderOrder = 3;
|
||||
bgpOverlayGroup.add(innerRing);
|
||||
|
||||
activeEventOverlay = [halo, pulseHalo, innerRing];
|
||||
bgpOverlayGroup.visible = showBGP;
|
||||
}
|
||||
|
||||
export function clearBGPEventOverlay() {
|
||||
activeEventOverlay = null;
|
||||
clearGroup(bgpOverlayGroup);
|
||||
@@ -922,7 +1218,9 @@ export function clearBGPEventOverlay() {
|
||||
|
||||
export function getBGPLegendItems() {
|
||||
return [
|
||||
{ color: "#6db7ff", label: "观测站" },
|
||||
{ color: "#6db7ff", label: "静态观测站" },
|
||||
{ color: "#fbbf24", label: "中活跃观测站" },
|
||||
{ color: "#ff5f57", label: "高活跃观测站" },
|
||||
{ color: "#8af5ff", label: "事件连线 / 枢纽" },
|
||||
{ color: "#2dd4bf", label: "影响区域" },
|
||||
{ color: "#ff4d4f", label: "严重事件" },
|
||||
|
||||
Reference in New Issue
Block a user