feat: enrich earth bgp event visualization
This commit is contained in:
4
TODO.md
Normal file
4
TODO.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- [ ] 把 BGP 观测站和异常点的 `hover/click` 手感再磨细一点
|
||||||
|
- [ ] 开始做 BGP 异常和海缆/区域的关联展示
|
||||||
@@ -282,6 +282,18 @@ def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any
|
|||||||
for record in records:
|
for record in records:
|
||||||
evidence = record.evidence or {}
|
evidence = record.evidence or {}
|
||||||
collectors = evidence.get("collectors") or record.peer_scope or []
|
collectors = evidence.get("collectors") or record.peer_scope or []
|
||||||
|
if not collectors:
|
||||||
|
nested = evidence.get("events") or []
|
||||||
|
collectors = [
|
||||||
|
str((item or {}).get("collector") or "").strip()
|
||||||
|
for item in nested
|
||||||
|
if (item or {}).get("collector")
|
||||||
|
]
|
||||||
|
|
||||||
|
collectors = [collector for collector in collectors if collector]
|
||||||
|
if not collectors:
|
||||||
|
collectors = []
|
||||||
|
|
||||||
collector = collectors[0] if collectors else None
|
collector = collectors[0] if collectors else None
|
||||||
location = None
|
location = None
|
||||||
if collector:
|
if collector:
|
||||||
@@ -299,6 +311,40 @@ def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any
|
|||||||
if location is None:
|
if location is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
as_path = []
|
||||||
|
if isinstance(evidence.get("as_path"), list):
|
||||||
|
as_path = evidence.get("as_path") or []
|
||||||
|
if not as_path:
|
||||||
|
nested = evidence.get("events") or []
|
||||||
|
for item in nested:
|
||||||
|
candidate_path = (item or {}).get("as_path")
|
||||||
|
if isinstance(candidate_path, list) and candidate_path:
|
||||||
|
as_path = candidate_path
|
||||||
|
break
|
||||||
|
|
||||||
|
impacted_regions = []
|
||||||
|
seen_regions = set()
|
||||||
|
for collector_name in collectors:
|
||||||
|
collector_location = RIPE_RIS_COLLECTOR_COORDS.get(str(collector_name))
|
||||||
|
if not collector_location:
|
||||||
|
continue
|
||||||
|
region_key = (
|
||||||
|
collector_location.get("country"),
|
||||||
|
collector_location.get("city"),
|
||||||
|
)
|
||||||
|
if region_key in seen_regions:
|
||||||
|
continue
|
||||||
|
seen_regions.add(region_key)
|
||||||
|
impacted_regions.append(
|
||||||
|
{
|
||||||
|
"collector": collector_name,
|
||||||
|
"country": collector_location.get("country"),
|
||||||
|
"city": collector_location.get("city"),
|
||||||
|
"latitude": collector_location.get("latitude"),
|
||||||
|
"longitude": collector_location.get("longitude"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
features.append(
|
features.append(
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
@@ -318,6 +364,10 @@ def convert_bgp_anomalies_to_geojson(records: List[BGPAnomaly]) -> Dict[str, Any
|
|||||||
"prefix": record.prefix,
|
"prefix": record.prefix,
|
||||||
"origin_asn": record.origin_asn,
|
"origin_asn": record.origin_asn,
|
||||||
"new_origin_asn": record.new_origin_asn,
|
"new_origin_asn": record.new_origin_asn,
|
||||||
|
"collectors": collectors,
|
||||||
|
"collector_count": len(collectors) or 1,
|
||||||
|
"as_path": as_path,
|
||||||
|
"impacted_regions": impacted_regions,
|
||||||
"confidence": record.confidence,
|
"confidence": record.confidence,
|
||||||
"summary": record.summary,
|
"summary": record.summary,
|
||||||
"created_at": to_iso8601_utc(record.created_at),
|
"created_at": to_iso8601_utc(record.created_at),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BGP_CONFIG, CONFIG, PATHS } from "./constants.js";
|
|||||||
import { latLonToVector3 } from "./utils.js";
|
import { latLonToVector3 } from "./utils.js";
|
||||||
|
|
||||||
const bgpGroup = new THREE.Group();
|
const bgpGroup = new THREE.Group();
|
||||||
|
const bgpOverlayGroup = new THREE.Group();
|
||||||
const collectorMarkers = [];
|
const collectorMarkers = [];
|
||||||
const anomalyMarkers = [];
|
const anomalyMarkers = [];
|
||||||
const anomalyCountByCollector = new Map();
|
const anomalyCountByCollector = new Map();
|
||||||
@@ -11,6 +12,10 @@ const anomalyCountByCollector = new Map();
|
|||||||
let showBGP = true;
|
let showBGP = true;
|
||||||
let totalAnomalyCount = 0;
|
let totalAnomalyCount = 0;
|
||||||
let textureCache = null;
|
let textureCache = null;
|
||||||
|
let activeEventOverlay = null;
|
||||||
|
const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
|
||||||
|
numeric: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
function getMarkerTexture() {
|
function getMarkerTexture() {
|
||||||
if (textureCache) return textureCache;
|
if (textureCache) return textureCache;
|
||||||
@@ -72,6 +77,150 @@ function formatLocalDateTime(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")}`;
|
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) {
|
function buildCollectorFeatureData(feature) {
|
||||||
const coordinates = feature?.geometry?.coordinates || [];
|
const coordinates = feature?.geometry?.coordinates || [];
|
||||||
const [longitude, latitude] = coordinates;
|
const [longitude, latitude] = coordinates;
|
||||||
@@ -163,6 +312,12 @@ function buildAnomalyFeatureData(feature) {
|
|||||||
prefix: properties.prefix || "-",
|
prefix: properties.prefix || "-",
|
||||||
origin_asn: properties.origin_asn ?? "-",
|
origin_asn: properties.origin_asn ?? "-",
|
||||||
new_origin_asn: properties.new_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 ?? "-",
|
confidence: properties.confidence ?? "-",
|
||||||
summary: properties.summary || "-",
|
summary: properties.summary || "-",
|
||||||
created_at: formatLocalDateTime(createdAt),
|
created_at: formatLocalDateTime(createdAt),
|
||||||
@@ -181,6 +336,15 @@ function clearMarkerArray(markers) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }) {
|
function createSpriteMaterial({ color, opacity }) {
|
||||||
return new THREE.SpriteMaterial({
|
return new THREE.SpriteMaterial({
|
||||||
map: getMarkerTexture(),
|
map: getMarkerTexture(),
|
||||||
@@ -193,6 +357,34 @@ function createSpriteMaterial({ color, opacity }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function createCollectorMarker(markerData) {
|
||||||
const sprite = new THREE.Sprite(
|
const sprite = new THREE.Sprite(
|
||||||
createSpriteMaterial({
|
createSpriteMaterial({
|
||||||
@@ -336,8 +528,12 @@ export async function loadBGPAnomalies(scene, earth) {
|
|||||||
if (!bgpGroup.parent) {
|
if (!bgpGroup.parent) {
|
||||||
earth.add(bgpGroup);
|
earth.add(bgpGroup);
|
||||||
}
|
}
|
||||||
|
if (!bgpOverlayGroup.parent) {
|
||||||
|
earth.add(bgpOverlayGroup);
|
||||||
|
}
|
||||||
|
|
||||||
bgpGroup.visible = showBGP;
|
bgpGroup.visible = showBGP;
|
||||||
|
bgpOverlayGroup.visible = showBGP;
|
||||||
|
|
||||||
if (scene && !scene.children.includes(earth)) {
|
if (scene && !scene.children.includes(earth)) {
|
||||||
scene.add(earth);
|
scene.add(earth);
|
||||||
@@ -441,22 +637,28 @@ export function clearBGPSelection() {
|
|||||||
anomalyMarkers.forEach((marker) => {
|
anomalyMarkers.forEach((marker) => {
|
||||||
marker.userData.state = "normal";
|
marker.userData.state = "normal";
|
||||||
});
|
});
|
||||||
|
clearBGPEventOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearBGPData(earth) {
|
export function clearBGPData(earth) {
|
||||||
clearMarkerArray(collectorMarkers);
|
clearMarkerArray(collectorMarkers);
|
||||||
clearMarkerArray(anomalyMarkers);
|
clearMarkerArray(anomalyMarkers);
|
||||||
|
clearBGPEventOverlay();
|
||||||
anomalyCountByCollector.clear();
|
anomalyCountByCollector.clear();
|
||||||
totalAnomalyCount = 0;
|
totalAnomalyCount = 0;
|
||||||
|
|
||||||
if (earth && bgpGroup.parent === earth) {
|
if (earth && bgpGroup.parent === earth) {
|
||||||
earth.remove(bgpGroup);
|
earth.remove(bgpGroup);
|
||||||
}
|
}
|
||||||
|
if (earth && bgpOverlayGroup.parent === earth) {
|
||||||
|
earth.remove(bgpOverlayGroup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleBGP(show) {
|
export function toggleBGP(show) {
|
||||||
showBGP = Boolean(show);
|
showBGP = Boolean(show);
|
||||||
bgpGroup.visible = showBGP;
|
bgpGroup.visible = showBGP;
|
||||||
|
bgpOverlayGroup.visible = showBGP;
|
||||||
collectorMarkers.forEach((marker) => {
|
collectorMarkers.forEach((marker) => {
|
||||||
marker.visible = showBGP;
|
marker.visible = showBGP;
|
||||||
});
|
});
|
||||||
@@ -485,12 +687,101 @@ export function getBGPCount() {
|
|||||||
return totalAnomalyCount;
|
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() {
|
export function getBGPLegendItems() {
|
||||||
return [
|
return [
|
||||||
{ color: "#6db7ff", label: "观测站" },
|
{ color: "#6db7ff", label: "观测站" },
|
||||||
{ color: "#ff4d4f", label: "Critical 异常" },
|
{ color: "#8af5ff", label: "事件连线 / 枢纽" },
|
||||||
{ color: "#ff9f43", label: "High / Major 异常" },
|
{ color: "#2dd4bf", label: "影响区域" },
|
||||||
{ color: "#ffd166", label: "Medium 异常" },
|
{ color: "#ff4d4f", label: "严重异常" },
|
||||||
{ color: "#4dabf7", label: "Low / Info 异常" },
|
{ color: "#ff9f43", label: "高危异常" },
|
||||||
|
{ color: "#ffd166", label: "中危异常" },
|
||||||
|
{ color: "#4dabf7", label: "低危异常" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export const BGP_CONFIG = {
|
|||||||
dimmedScale: 0.92,
|
dimmedScale: 0.92,
|
||||||
pulseSpeed: 0.0045,
|
pulseSpeed: 0.0045,
|
||||||
collectorPulseSpeed: 0.0024,
|
collectorPulseSpeed: 0.0024,
|
||||||
|
eventHubAltitudeOffset: 7.2,
|
||||||
|
eventHubScale: 4.8,
|
||||||
|
regionScale: 11.5,
|
||||||
normalPulseAmplitude: 0.08,
|
normalPulseAmplitude: 0.08,
|
||||||
lockedPulseAmplitude: 0.28,
|
lockedPulseAmplitude: 0.28,
|
||||||
opacity: {
|
opacity: {
|
||||||
@@ -105,7 +108,10 @@ export const BGP_CONFIG = {
|
|||||||
medium: 1.0,
|
medium: 1.0,
|
||||||
low: 0.94
|
low: 0.94
|
||||||
},
|
},
|
||||||
collectorColor: 0x6db7ff
|
collectorColor: 0x6db7ff,
|
||||||
|
eventHubColor: 0x8af5ff,
|
||||||
|
linkColor: 0x54d2ff,
|
||||||
|
regionColor: 0x2dd4bf
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREDICTED_ORBIT_CONFIG = {
|
export const PREDICTED_ORBIT_CONFIG = {
|
||||||
|
|||||||
@@ -38,14 +38,17 @@ const CARD_CONFIG = {
|
|||||||
{ key: 'anomaly_type', label: '异常类型' },
|
{ key: 'anomaly_type', label: '异常类型' },
|
||||||
{ key: 'severity', label: '严重度' },
|
{ key: 'severity', label: '严重度' },
|
||||||
{ key: 'status', label: '状态' },
|
{ key: 'status', label: '状态' },
|
||||||
|
{ key: 'route_change', label: '路由变更' },
|
||||||
{ key: 'prefix', label: '前缀' },
|
{ key: 'prefix', label: '前缀' },
|
||||||
|
{ key: 'as_path_display', label: '传播路径' },
|
||||||
{ key: 'origin_asn', label: '原始 ASN' },
|
{ key: 'origin_asn', label: '原始 ASN' },
|
||||||
{ key: 'new_origin_asn', label: '新 ASN' },
|
{ key: 'new_origin_asn', label: '新 ASN' },
|
||||||
{ key: 'confidence', label: '置信度' },
|
{ key: 'confidence', label: '置信度' },
|
||||||
{ key: 'collector', label: '采集器' },
|
{ key: 'collector', label: '采集器' },
|
||||||
{ key: 'country', label: '国家' },
|
{ key: 'observed_by', label: '观测范围' },
|
||||||
{ key: 'city', label: '城市' },
|
{ key: 'impacted_scope', label: '影响区域' },
|
||||||
{ key: 'created_at', label: '创建时间' },
|
{ key: 'location', label: '观测位置' },
|
||||||
|
{ key: 'created_at', label: '发生时间' },
|
||||||
{ key: 'summary', label: '摘要' }
|
{ key: 'summary', label: '摘要' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -55,8 +58,7 @@ const CARD_CONFIG = {
|
|||||||
className: 'bgp',
|
className: 'bgp',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'collector', label: '采集器' },
|
{ key: 'collector', label: '采集器' },
|
||||||
{ key: 'country', label: '国家' },
|
{ key: 'location', label: '观测位置' },
|
||||||
{ key: 'city', label: '城市' },
|
|
||||||
{ key: 'anomaly_count', label: '当前异常数' },
|
{ key: 'anomaly_count', label: '当前异常数' },
|
||||||
{ key: 'status', label: '状态' }
|
{ key: 'status', label: '状态' }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ import {
|
|||||||
updateBGPVisualState,
|
updateBGPVisualState,
|
||||||
clearBGPData,
|
clearBGPData,
|
||||||
toggleBGP,
|
toggleBGP,
|
||||||
|
formatBGPAnomalyTypeLabel,
|
||||||
|
formatBGPASPath,
|
||||||
|
formatBGPCollectorStatus,
|
||||||
|
formatBGPConfidence,
|
||||||
|
formatBGPImpactedScope,
|
||||||
|
formatBGPLocation,
|
||||||
|
formatBGPObservedTime,
|
||||||
|
formatBGPObservedBy,
|
||||||
|
formatBGPRouteChange,
|
||||||
|
formatBGPSeverityLabel,
|
||||||
|
formatBGPStatusLabel,
|
||||||
|
showBGPEventOverlay,
|
||||||
} from "./bgp.js";
|
} from "./bgp.js";
|
||||||
import {
|
import {
|
||||||
setupControls,
|
setupControls,
|
||||||
@@ -140,6 +152,7 @@ const scratchCameraToEarth = new THREE.Vector3();
|
|||||||
const scratchCableCenter = new THREE.Vector3();
|
const scratchCableCenter = new THREE.Vector3();
|
||||||
const scratchCableDirection = new THREE.Vector3();
|
const scratchCableDirection = new THREE.Vector3();
|
||||||
const scratchBGPDirection = new THREE.Vector3();
|
const scratchBGPDirection = new THREE.Vector3();
|
||||||
|
const scratchBGPWorldPosition = new THREE.Vector3();
|
||||||
|
|
||||||
const cleanupFns = [];
|
const cleanupFns = [];
|
||||||
const DRAG_ROTATION_FACTOR = 0.005;
|
const DRAG_ROTATION_FACTOR = 0.005;
|
||||||
@@ -262,18 +275,36 @@ function showSatelliteInfo(props) {
|
|||||||
|
|
||||||
function showBGPInfo(marker) {
|
function showBGPInfo(marker) {
|
||||||
setLegendMode("bgp");
|
setLegendMode("bgp");
|
||||||
|
const impactedRegions =
|
||||||
|
Array.isArray(marker.userData.impacted_regions) &&
|
||||||
|
marker.userData.impacted_regions.length > 0
|
||||||
|
? marker.userData.impacted_regions
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
city: marker.userData.city,
|
||||||
|
country: marker.userData.country,
|
||||||
|
},
|
||||||
|
];
|
||||||
showInfoCard("bgp", {
|
showInfoCard("bgp", {
|
||||||
anomaly_type: marker.userData.anomaly_type,
|
anomaly_type: formatBGPAnomalyTypeLabel(marker.userData.anomaly_type),
|
||||||
severity: marker.userData.rawSeverity || marker.userData.severity,
|
severity: formatBGPSeverityLabel(
|
||||||
status: marker.userData.status,
|
marker.userData.rawSeverity || marker.userData.severity,
|
||||||
|
),
|
||||||
|
status: formatBGPStatusLabel(marker.userData.status),
|
||||||
|
route_change: formatBGPRouteChange(
|
||||||
|
marker.userData.origin_asn,
|
||||||
|
marker.userData.new_origin_asn,
|
||||||
|
),
|
||||||
prefix: marker.userData.prefix,
|
prefix: marker.userData.prefix,
|
||||||
|
as_path_display: formatBGPASPath(marker.userData.as_path),
|
||||||
origin_asn: marker.userData.origin_asn,
|
origin_asn: marker.userData.origin_asn,
|
||||||
new_origin_asn: marker.userData.new_origin_asn,
|
new_origin_asn: marker.userData.new_origin_asn,
|
||||||
confidence: marker.userData.confidence,
|
confidence: formatBGPConfidence(marker.userData.confidence),
|
||||||
collector: marker.userData.collector,
|
collector: marker.userData.collector,
|
||||||
country: marker.userData.country,
|
observed_by: formatBGPObservedBy(marker.userData.collectors),
|
||||||
city: marker.userData.city,
|
impacted_scope: formatBGPImpactedScope(impactedRegions),
|
||||||
created_at: marker.userData.created_at,
|
location: formatBGPLocation(marker.userData.city, marker.userData.country),
|
||||||
|
created_at: formatBGPObservedTime(marker.userData.created_at_raw),
|
||||||
summary: marker.userData.summary,
|
summary: marker.userData.summary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -282,10 +313,9 @@ function showBGPCollectorInfo(marker) {
|
|||||||
setLegendMode("bgp");
|
setLegendMode("bgp");
|
||||||
showInfoCard("bgp_collector", {
|
showInfoCard("bgp_collector", {
|
||||||
collector: marker.userData.collector,
|
collector: marker.userData.collector,
|
||||||
country: marker.userData.country,
|
location: formatBGPLocation(marker.userData.city, marker.userData.country),
|
||||||
city: marker.userData.city,
|
|
||||||
anomaly_count: marker.userData.anomaly_count ?? 0,
|
anomaly_count: marker.userData.anomaly_count ?? 0,
|
||||||
status: marker.userData.status || "online",
|
status: formatBGPCollectorStatus(marker.userData.status || "online"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,7 +823,11 @@ function getFrontFacingBGPMarkers(markers) {
|
|||||||
scratchCameraToEarth.subVectors(camera.position, earth.position).normalize();
|
scratchCameraToEarth.subVectors(camera.position, earth.position).normalize();
|
||||||
|
|
||||||
return markers.filter((marker) => {
|
return markers.filter((marker) => {
|
||||||
scratchBGPDirection.copy(marker.position).normalize();
|
scratchBGPWorldPosition.copy(marker.position);
|
||||||
|
marker.parent?.localToWorld(scratchBGPWorldPosition);
|
||||||
|
scratchBGPDirection
|
||||||
|
.subVectors(scratchBGPWorldPosition, earth.position)
|
||||||
|
.normalize();
|
||||||
return scratchCameraToEarth.dot(scratchBGPDirection) > 0;
|
return scratchCameraToEarth.dot(scratchBGPDirection) > 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1032,6 +1066,7 @@ function onClick(event) {
|
|||||||
lockedObject = clickedMarker;
|
lockedObject = clickedMarker;
|
||||||
lockedObjectType = "bgp";
|
lockedObjectType = "bgp";
|
||||||
setAutoRotate(false);
|
setAutoRotate(false);
|
||||||
|
showBGPEventOverlay(clickedMarker, earth);
|
||||||
showBGPInfo(clickedMarker);
|
showBGPInfo(clickedMarker);
|
||||||
showStatusMessage(
|
showStatusMessage(
|
||||||
`已选择BGP异常: ${clickedMarker.userData.collector}`,
|
`已选择BGP异常: ${clickedMarker.userData.collector}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user