release: bump version to 0.46.0
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
import { BGP_CONFIG, CONFIG, PATHS } from "./constants.js";
|
||||
import { createInteractableLayer } from "./interactable.js";
|
||||
import { getSurfaceMarkerCameraScale, latLonToVector3 } from "./utils.js";
|
||||
|
||||
const bgpGroup = new THREE.Group();
|
||||
const bgpOverlayGroup = new THREE.Group();
|
||||
const bgpEventOverlayGroup = new THREE.Group();
|
||||
const bgpCollectorRadarGroup = new THREE.Group();
|
||||
const collectorMarkers = [];
|
||||
const anomalyMarkers = [];
|
||||
const activeEventCountByCollector = new Map();
|
||||
@@ -14,7 +17,6 @@ let totalAnomalyCount = 0;
|
||||
let totalIncidentCount = 0;
|
||||
let textureCache = null;
|
||||
let eventRingTextureCache = null;
|
||||
let collectorTextureCache = null;
|
||||
const eventTextureCache = new Map();
|
||||
let activeEventOverlay = null;
|
||||
let activeCollectorOverlayContext = null;
|
||||
@@ -22,17 +24,27 @@ const relativeTimeFormatter = new Intl.RelativeTimeFormat("zh-CN", {
|
||||
numeric: "auto",
|
||||
});
|
||||
const collectorWorldPosition = new THREE.Vector3();
|
||||
const collectorSurfaceNormal = new THREE.Vector3();
|
||||
const collectorNorthPole = new THREE.Vector3(0, 1, 0);
|
||||
const collectorFallbackForward = new THREE.Vector3(0, 0, 1);
|
||||
const collectorNorthTangent = new THREE.Vector3();
|
||||
const collectorEastTangent = new THREE.Vector3();
|
||||
const collectorOrientationMatrix = new THREE.Matrix4();
|
||||
const colorScratchA = new THREE.Color();
|
||||
const colorScratchB = new THREE.Color();
|
||||
const COLLECTOR_SCAN_SPEED_RAD = 0.00018;
|
||||
const COLLECTOR_SCAN_REBUILD_MS = 80;
|
||||
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";
|
||||
const BGP_EVENT_RENDER_ORDER = 4.5;
|
||||
const BGP_EVENT_POINT_SIZE = 34;
|
||||
const BGP_EVENT_SYMBOL_SIZE = 60;
|
||||
const BGP_COLLECTOR_RENDER_ORDER = 4.4;
|
||||
const BGP_COLLECTOR_ALTITUDE_OFFSET = BGP_CONFIG.collectorAltitudeOffset;
|
||||
const BGP_COLLECTOR_POINT_SIZE = 36;
|
||||
const BGP_COLLECTOR_ICON_FIT_SIZE = 60;
|
||||
const BGP_COLLECTOR_ICON_SOURCE = "/earth/assets/icons/bgp-broadcast-pin.svg";
|
||||
const BGP_COLLECTOR_HOVER_SCALE = 1.08;
|
||||
const BGP_COLLECTOR_LOCKED_SCALE = 1.12;
|
||||
const BGP_COLLECTOR_PULSE_AMPLITUDE = 0.14;
|
||||
|
||||
bgpOverlayGroup.name = "bgp-overlay-root";
|
||||
bgpEventOverlayGroup.name = "bgp-event-overlay-layer";
|
||||
bgpCollectorRadarGroup.name = "bgp-collector-radar-layer";
|
||||
bgpOverlayGroup.add(bgpEventOverlayGroup);
|
||||
bgpOverlayGroup.add(bgpCollectorRadarGroup);
|
||||
|
||||
function getMarkerTexture() {
|
||||
if (textureCache) return textureCache;
|
||||
@@ -85,48 +97,6 @@ function getEventRingTexture() {
|
||||
return eventRingTextureCache;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
context.strokeStyle = BGP_CONFIG.collectorIcon.ringStroke;
|
||||
context.lineWidth = BGP_CONFIG.collectorIcon.ringLineWidth;
|
||||
context.beginPath();
|
||||
context.arc(64, 64, BGP_CONFIG.collectorIcon.ringRadius, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
|
||||
context.save();
|
||||
context.translate(16, 16);
|
||||
context.scale(4, 4);
|
||||
const path = new Path2D(MATERIAL_ACCESS_POINT_PATH);
|
||||
context.lineJoin = "round";
|
||||
context.lineCap = "round";
|
||||
context.lineWidth = BGP_CONFIG.collectorIcon.pathLineWidth;
|
||||
context.strokeStyle = BGP_CONFIG.collectorIcon.pathStroke;
|
||||
context.stroke(path);
|
||||
context.fillStyle = BGP_CONFIG.collectorIcon.pathFill;
|
||||
context.shadowBlur = 0;
|
||||
context.fill(path);
|
||||
context.fillStyle = BGP_CONFIG.collectorIcon.centerFill;
|
||||
context.beginPath();
|
||||
context.arc(12, 12, BGP_CONFIG.collectorIcon.centerRadius, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
context.restore();
|
||||
|
||||
collectorTextureCache = new THREE.CanvasTexture(canvas);
|
||||
return collectorTextureCache;
|
||||
}
|
||||
|
||||
function getEventSymbolKind(anomalyType) {
|
||||
const value = String(anomalyType || "").toLowerCase();
|
||||
if (value.includes("origin")) return "triangle";
|
||||
@@ -266,6 +236,169 @@ function getSeverityScale(severity) {
|
||||
return BGP_CONFIG.severityScales[normalizeSeverity(severity)];
|
||||
}
|
||||
|
||||
function severityColorHex(severity) {
|
||||
return `#${getSeverityColor(severity).toString(16).padStart(6, "0")}`;
|
||||
}
|
||||
|
||||
function colorNumberHex(colorNumber) {
|
||||
return `#${Number(colorNumber || 0xffffff).toString(16).padStart(6, "0")}`;
|
||||
}
|
||||
|
||||
function drawBGPEventIcon(context, { marker, color = "#ffffff", glow = false }) {
|
||||
const kind = getEventSymbolKind(
|
||||
marker?.userData?.incident_type || marker?.userData?.anomaly_type,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.fillStyle = color;
|
||||
context.strokeStyle = color;
|
||||
context.lineJoin = "round";
|
||||
context.lineCap = "round";
|
||||
context.shadowColor = color;
|
||||
context.shadowBlur = glow ? 14 : 0;
|
||||
|
||||
const inset = (128 - BGP_EVENT_SYMBOL_SIZE) / 2;
|
||||
context.translate(inset, inset);
|
||||
context.scale(BGP_EVENT_SYMBOL_SIZE / 128, BGP_EVENT_SYMBOL_SIZE / 128);
|
||||
|
||||
if (kind === "triangle") {
|
||||
drawTriangleSymbol(context);
|
||||
} else if (kind === "exclamation") {
|
||||
drawExclamationSymbol(context);
|
||||
} else if (kind === "wave") {
|
||||
drawWaveSymbol(context);
|
||||
} else if (kind === "burst") {
|
||||
drawBurstSymbol(context);
|
||||
} else if (kind === "leak") {
|
||||
drawLeakSymbol(context);
|
||||
} else {
|
||||
drawDotSymbol(context);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
const bgpEventIconLayer = createInteractableLayer({
|
||||
id: "bgp-events",
|
||||
objectType: "bgp",
|
||||
renderOrder: BGP_EVENT_RENDER_ORDER,
|
||||
altitudeOffset: BGP_CONFIG.altitudeOffset,
|
||||
pointSize: BGP_EVENT_POINT_SIZE,
|
||||
colors: {
|
||||
byKind: Object.fromEntries(
|
||||
Object.keys(BGP_CONFIG.severityColors).map((severity) => [
|
||||
severity,
|
||||
severityColorHex(severity),
|
||||
]),
|
||||
),
|
||||
normal: severityColorHex("medium"),
|
||||
},
|
||||
opacity: {
|
||||
normal: BGP_CONFIG.opacity.normal,
|
||||
hover: BGP_CONFIG.opacity.hover,
|
||||
locked: BGP_CONFIG.opacity.lockedMax,
|
||||
dimmed: BGP_CONFIG.opacity.dimmed,
|
||||
},
|
||||
stateScale: {
|
||||
hover: BGP_CONFIG.marker.hoverScale,
|
||||
locked: BGP_CONFIG.marker.hoverScale,
|
||||
dimmed: BGP_CONFIG.marker.dimmedScale,
|
||||
},
|
||||
pulse: {
|
||||
enabled: true,
|
||||
speed: BGP_CONFIG.pulse.eventSpeed,
|
||||
amplitude: BGP_CONFIG.pulse.lockedAmplitude,
|
||||
},
|
||||
icon: {
|
||||
coordinates: "canvas",
|
||||
draw: drawBGPEventIcon,
|
||||
},
|
||||
getPosition: (item) => ({
|
||||
latitude: item.latitude,
|
||||
longitude: item.longitude,
|
||||
}),
|
||||
getKind: (item) => normalizeSeverity(item.severity),
|
||||
getBucketKey: (marker) => [
|
||||
getEventSymbolKind(marker.userData?.incident_type || marker.userData?.anomaly_type),
|
||||
normalizeSeverity(marker.userData?.severity),
|
||||
].join(":"),
|
||||
getPointSizeMultiplier: (marker) => getSeverityScale(marker.userData?.severity),
|
||||
getUserData: (item) => ({
|
||||
...item,
|
||||
baseScale: BGP_CONFIG.marker.eventBaseScale * getSeverityScale(item.severity),
|
||||
baseColor: getSeverityColor(item.severity),
|
||||
pulseOffset: Math.random() * Math.PI * 2,
|
||||
}),
|
||||
});
|
||||
|
||||
const bgpCollectorIconLayer = createInteractableLayer({
|
||||
id: "bgp-collectors",
|
||||
objectType: "bgp_collector",
|
||||
renderOrder: BGP_COLLECTOR_RENDER_ORDER,
|
||||
altitudeOffset: BGP_COLLECTOR_ALTITUDE_OFFSET,
|
||||
pointSize: BGP_COLLECTOR_POINT_SIZE,
|
||||
colors: {
|
||||
byKind: Object.fromEntries(
|
||||
Object.entries(BGP_CONFIG.collectorHeatColors).map(([tier, color]) => [
|
||||
tier,
|
||||
colorNumberHex(color),
|
||||
]),
|
||||
),
|
||||
normal: colorNumberHex(BGP_CONFIG.collectorColor),
|
||||
},
|
||||
opacity: {
|
||||
normal: BGP_CONFIG.collectorIcon.idleOpacity,
|
||||
hover: 0.98,
|
||||
locked: 1,
|
||||
dimmed: BGP_CONFIG.opacity.dimmed,
|
||||
},
|
||||
stateScale: {
|
||||
hover: BGP_COLLECTOR_HOVER_SCALE,
|
||||
locked: BGP_COLLECTOR_LOCKED_SCALE,
|
||||
dimmed: BGP_CONFIG.marker.dimmedScale,
|
||||
},
|
||||
pulse: {
|
||||
enabled: true,
|
||||
speed: BGP_CONFIG.pulse.collectorSpeed,
|
||||
amplitude: BGP_COLLECTOR_PULSE_AMPLITUDE,
|
||||
},
|
||||
icon: {
|
||||
coordinates: "canvas",
|
||||
fitSize: BGP_COLLECTOR_ICON_FIT_SIZE,
|
||||
glowBlur: 16,
|
||||
source: BGP_COLLECTOR_ICON_SOURCE,
|
||||
},
|
||||
getPosition: (item) => ({
|
||||
latitude: item.displayLatitude,
|
||||
longitude: item.displayLongitude,
|
||||
}),
|
||||
getKind: (item) => getCollectorActivityProfile(item).tier,
|
||||
getBucketKey: (marker) => {
|
||||
const scaleBoost = marker.userData?.activity?.scaleBoost ?? 1;
|
||||
return `${marker.userData?.activity?.tier || "idle"}:${scaleBoost.toFixed(2)}`;
|
||||
},
|
||||
getPointSizeMultiplier: (marker) => marker.userData?.activity?.scaleBoost ?? 1,
|
||||
getUserData: (item) => {
|
||||
const activity = getCollectorActivityProfile(item);
|
||||
const baseColor = activity.color;
|
||||
const idleColor = blendHexColors(
|
||||
BGP_CONFIG.collectorIcon.idleBaseColor,
|
||||
baseColor,
|
||||
BGP_CONFIG.collectorIcon.idleBlend,
|
||||
);
|
||||
|
||||
return {
|
||||
...item,
|
||||
baseScale: BGP_CONFIG.marker.collectorBaseScale * activity.scaleBoost,
|
||||
baseColor,
|
||||
idleColor,
|
||||
pulseOffset: Math.random() * Math.PI * 2,
|
||||
anomaly_count: 0,
|
||||
activity,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
@@ -281,7 +414,7 @@ function getCollectorDistanceScale(marker, camera) {
|
||||
if (!marker || !camera || BGP_CONFIG.sizeStabilization?.enabled === false) return 1;
|
||||
|
||||
return getSurfaceMarkerCameraScale(camera, {
|
||||
altitudeOffset: BGP_CONFIG.collectorAltitudeOffset,
|
||||
altitudeOffset: BGP_COLLECTOR_ALTITUDE_OFFSET,
|
||||
referenceFov: 75,
|
||||
min: Number(BGP_CONFIG.sizeStabilization?.collectorMin ?? 0.6),
|
||||
max: Number(BGP_CONFIG.sizeStabilization?.collectorMax ?? 1.9),
|
||||
@@ -299,32 +432,6 @@ function getEventDistanceScale(marker, camera) {
|
||||
});
|
||||
}
|
||||
|
||||
function orientCollectorMarkerToSurface(marker, position) {
|
||||
collectorSurfaceNormal.copy(position).normalize();
|
||||
collectorNorthTangent
|
||||
.copy(collectorNorthPole)
|
||||
.projectOnPlane(collectorSurfaceNormal);
|
||||
|
||||
if (collectorNorthTangent.lengthSq() < 1e-6) {
|
||||
collectorNorthTangent
|
||||
.copy(collectorFallbackForward)
|
||||
.projectOnPlane(collectorSurfaceNormal);
|
||||
}
|
||||
|
||||
collectorNorthTangent.normalize();
|
||||
collectorEastTangent
|
||||
.copy(collectorNorthTangent)
|
||||
.cross(collectorSurfaceNormal)
|
||||
.normalize();
|
||||
|
||||
collectorOrientationMatrix.makeBasis(
|
||||
collectorEastTangent,
|
||||
collectorNorthTangent,
|
||||
collectorSurfaceNormal,
|
||||
);
|
||||
marker.quaternion.setFromRotationMatrix(collectorOrientationMatrix);
|
||||
}
|
||||
|
||||
function getCollectorActivityProfile(markerData) {
|
||||
const recent24h = Number(markerData?.recent_24h_observation_count || 0);
|
||||
const recent7d = Number(markerData?.recent_7d_observation_count || 0);
|
||||
@@ -784,13 +891,26 @@ function clearMarkerArray(markers) {
|
||||
const marker = markers.pop();
|
||||
while (marker.children.length > 0) {
|
||||
const child = marker.children.pop();
|
||||
child.geometry?.dispose?.();
|
||||
child.material?.dispose();
|
||||
}
|
||||
disposeCollectorEffectSprites(marker);
|
||||
disposeEventRingSprite(marker.userData?.ringA);
|
||||
disposeEventRingSprite(marker.userData?.ringB);
|
||||
marker.material?.dispose();
|
||||
bgpGroup.remove(marker);
|
||||
marker.parent?.remove?.(marker);
|
||||
}
|
||||
}
|
||||
|
||||
function disposeAnomalyRingSprites() {
|
||||
anomalyMarkers.forEach((marker) => {
|
||||
disposeEventRingSprite(marker.userData?.ringA);
|
||||
disposeEventRingSprite(marker.userData?.ringB);
|
||||
delete marker.userData.ringA;
|
||||
delete marker.userData.ringB;
|
||||
});
|
||||
}
|
||||
|
||||
function clearGroup(group) {
|
||||
while (group.children.length > 0) {
|
||||
const child = group.children[group.children.length - 1];
|
||||
@@ -979,46 +1099,14 @@ function createRadialBoundaryPoints(
|
||||
return points;
|
||||
}
|
||||
|
||||
function createCollectorMarker(markerData) {
|
||||
const activity = getCollectorActivityProfile(markerData);
|
||||
const baseColor = activity.color;
|
||||
const idleColor = blendHexColors(
|
||||
BGP_CONFIG.collectorIcon.idleBaseColor,
|
||||
baseColor,
|
||||
BGP_CONFIG.collectorIcon.idleBlend,
|
||||
);
|
||||
const marker = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(1, 1),
|
||||
new THREE.MeshBasicMaterial({
|
||||
map: getCollectorTexture(),
|
||||
color: idleColor,
|
||||
transparent: true,
|
||||
opacity: BGP_CONFIG.collectorIcon.idleOpacity,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
side: THREE.DoubleSide,
|
||||
}),
|
||||
);
|
||||
|
||||
const position = latLonToVector3(
|
||||
markerData.displayLatitude,
|
||||
markerData.displayLongitude,
|
||||
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset,
|
||||
);
|
||||
|
||||
marker.position.copy(position);
|
||||
marker.scale.set(BGP_CONFIG.marker.collectorBaseScale * 0.88 * activity.scaleBoost, BGP_CONFIG.marker.collectorBaseScale * 1.08 * activity.scaleBoost, 1);
|
||||
marker.renderOrder = 3;
|
||||
marker.visible = showBGP;
|
||||
orientCollectorMarkerToSurface(marker, position);
|
||||
|
||||
function attachCollectorEffectSprites(marker) {
|
||||
const activity = marker.userData.activity;
|
||||
const heatHalo = createOverlaySprite({
|
||||
color: activity.color,
|
||||
opacity: 0.0,
|
||||
scale: activity.haloScale * 0.58,
|
||||
});
|
||||
heatHalo.renderOrder = 1;
|
||||
marker.add(heatHalo);
|
||||
|
||||
const pulseHalo = createOverlaySprite({
|
||||
color: activity.color,
|
||||
@@ -1026,7 +1114,6 @@ function createCollectorMarker(markerData) {
|
||||
scale: activity.pulseHaloScale * 0.48,
|
||||
});
|
||||
pulseHalo.renderOrder = 0;
|
||||
marker.add(pulseHalo);
|
||||
|
||||
const statusCore = createOverlaySprite({
|
||||
color: activity.color,
|
||||
@@ -1037,9 +1124,7 @@ function createCollectorMarker(markerData) {
|
||||
BGP_CONFIG.marker.collectorStatusCoreMinScale,
|
||||
),
|
||||
});
|
||||
statusCore.position.set(0, 0, 0.02);
|
||||
statusCore.renderOrder = 4;
|
||||
marker.add(statusCore);
|
||||
|
||||
const coverageHalo = createOverlaySprite({
|
||||
color: BGP_CONFIG.regionColor,
|
||||
@@ -1048,65 +1133,52 @@ function createCollectorMarker(markerData) {
|
||||
});
|
||||
coverageHalo.renderOrder = 0;
|
||||
coverageHalo.scale.set(activity.coverageHaloScale * 0.82, activity.coverageHaloScale * 0.56, 1);
|
||||
marker.add(coverageHalo);
|
||||
|
||||
marker.userData = {
|
||||
type: "bgp_collector",
|
||||
state: "normal",
|
||||
baseScale: BGP_CONFIG.marker.collectorBaseScale * activity.scaleBoost,
|
||||
baseColor,
|
||||
idleColor,
|
||||
pulseOffset: Math.random() * Math.PI * 2,
|
||||
anomaly_count: 0,
|
||||
activity,
|
||||
heatHalo,
|
||||
pulseHalo,
|
||||
statusCore,
|
||||
coverageHalo,
|
||||
...markerData,
|
||||
};
|
||||
marker.userData.heatHalo = heatHalo;
|
||||
marker.userData.pulseHalo = pulseHalo;
|
||||
marker.userData.statusCore = statusCore;
|
||||
marker.userData.coverageHalo = coverageHalo;
|
||||
|
||||
collectorMarkers.push(marker);
|
||||
bgpGroup.add(marker);
|
||||
[heatHalo, pulseHalo, statusCore, coverageHalo].forEach((sprite) => {
|
||||
sprite.position.copy(marker.position);
|
||||
sprite.visible = showBGP;
|
||||
bgpGroup.add(sprite);
|
||||
});
|
||||
}
|
||||
|
||||
function createAnomalyMarker(markerData) {
|
||||
const sprite = new THREE.Sprite(
|
||||
new THREE.SpriteMaterial({
|
||||
map: getEventTexture(markerData.incident_type || markerData.anomaly_type),
|
||||
color: getSeverityColor(markerData.severity),
|
||||
transparent: true,
|
||||
opacity: BGP_CONFIG.opacity.normal,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.NormalBlending,
|
||||
}),
|
||||
);
|
||||
function disposeCollectorEffectSprites(marker) {
|
||||
[
|
||||
marker?.userData?.heatHalo,
|
||||
marker?.userData?.pulseHalo,
|
||||
marker?.userData?.statusCore,
|
||||
marker?.userData?.coverageHalo,
|
||||
].forEach((sprite) => {
|
||||
if (!sprite) return;
|
||||
sprite.parent?.remove?.(sprite);
|
||||
sprite.material?.dispose?.();
|
||||
sprite.geometry?.dispose?.();
|
||||
});
|
||||
}
|
||||
|
||||
const position = latLonToVector3(
|
||||
markerData.latitude,
|
||||
markerData.longitude,
|
||||
CONFIG.earthRadius + BGP_CONFIG.altitudeOffset,
|
||||
);
|
||||
async function setCollectorMarkers(markerData, earth) {
|
||||
collectorMarkers.forEach(disposeCollectorEffectSprites);
|
||||
collectorMarkers.length = 0;
|
||||
await bgpCollectorIconLayer.preloadAssets(markerData);
|
||||
bgpCollectorIconLayer.setData(markerData);
|
||||
bgpCollectorIconLayer.attach(earth);
|
||||
bgpCollectorIconLayer.setVisible(showBGP);
|
||||
|
||||
const baseScale = BGP_CONFIG.marker.eventBaseScale * getSeverityScale(markerData.severity);
|
||||
sprite.position.copy(position);
|
||||
sprite.scale.setScalar(baseScale);
|
||||
sprite.renderOrder = 5;
|
||||
sprite.visible = showBGP;
|
||||
sprite.userData = {
|
||||
type: "bgp",
|
||||
state: "normal",
|
||||
baseScale,
|
||||
baseColor: getSeverityColor(markerData.severity),
|
||||
pulseOffset: Math.random() * Math.PI * 2,
|
||||
...markerData,
|
||||
};
|
||||
bgpCollectorIconLayer.getMarkers().forEach((marker) => {
|
||||
attachCollectorEffectSprites(marker);
|
||||
collectorMarkers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
const ringA = new THREE.Sprite(
|
||||
function createEventRingSprite(marker) {
|
||||
const ring = new THREE.Sprite(
|
||||
new THREE.SpriteMaterial({
|
||||
map: getEventRingTexture(),
|
||||
color: getSeverityColor(markerData.severity),
|
||||
color: marker.userData.baseColor || getSeverityColor(marker.userData.severity),
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
depthWrite: false,
|
||||
@@ -1114,30 +1186,40 @@ function createAnomalyMarker(markerData) {
|
||||
blending: THREE.AdditiveBlending,
|
||||
}),
|
||||
);
|
||||
ringA.scale.setScalar(baseScale * BGP_CONFIG.ring.scaleA);
|
||||
ringA.position.set(0, 0, -0.01);
|
||||
sprite.add(ringA);
|
||||
ring.position.copy(marker.position);
|
||||
ring.renderOrder = BGP_EVENT_RENDER_ORDER - 0.05;
|
||||
ring.visible = showBGP;
|
||||
return ring;
|
||||
}
|
||||
|
||||
const ringB = new THREE.Sprite(
|
||||
new THREE.SpriteMaterial({
|
||||
map: getEventRingTexture(),
|
||||
color: getSeverityColor(markerData.severity),
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
}),
|
||||
);
|
||||
ringB.scale.setScalar(baseScale * BGP_CONFIG.ring.scaleB);
|
||||
ringB.position.set(0, 0, -0.02);
|
||||
sprite.add(ringB);
|
||||
function disposeEventRingSprite(ring) {
|
||||
if (!ring) return;
|
||||
ring.parent?.remove?.(ring);
|
||||
ring.material?.dispose?.();
|
||||
ring.geometry?.dispose?.();
|
||||
}
|
||||
|
||||
sprite.userData.ringA = ringA;
|
||||
sprite.userData.ringB = ringB;
|
||||
function attachAnomalyRingSprites(marker) {
|
||||
const ringA = createEventRingSprite(marker);
|
||||
const ringB = createEventRingSprite(marker);
|
||||
ringB.visible = false;
|
||||
marker.userData.ringA = ringA;
|
||||
marker.userData.ringB = ringB;
|
||||
bgpGroup.add(ringA);
|
||||
bgpGroup.add(ringB);
|
||||
}
|
||||
|
||||
anomalyMarkers.push(sprite);
|
||||
bgpGroup.add(sprite);
|
||||
function setAnomalyMarkers(markerData, earth) {
|
||||
disposeAnomalyRingSprites();
|
||||
anomalyMarkers.length = 0;
|
||||
bgpEventIconLayer.setData(markerData);
|
||||
bgpEventIconLayer.attach(earth);
|
||||
bgpEventIconLayer.setVisible(showBGP);
|
||||
|
||||
bgpEventIconLayer.getMarkers().forEach((marker) => {
|
||||
attachAnomalyRingSprites(marker);
|
||||
anomalyMarkers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function dedupeAnomalies(features) {
|
||||
@@ -1304,17 +1386,18 @@ export async function loadBGPAnomalies(scene, earth) {
|
||||
totalIncidentCount = selectedEventData.totalIncidentCount;
|
||||
activeEventCountByCollector.clear();
|
||||
|
||||
spreadCollectorPositions(
|
||||
const collectorMarkersData = spreadCollectorPositions(
|
||||
collectorFeatures
|
||||
.map(buildCollectorFeatureData)
|
||||
.filter(Boolean),
|
||||
).forEach(createCollectorMarker);
|
||||
);
|
||||
await setCollectorMarkers(collectorMarkersData, earth);
|
||||
|
||||
if (selectedEventData.mode === "incident") {
|
||||
dedupeIncidents(selectedEventData.features).forEach(createAnomalyMarker);
|
||||
} else {
|
||||
dedupeAnomalies(selectedEventData.features).forEach(createAnomalyMarker);
|
||||
}
|
||||
const eventMarkers =
|
||||
selectedEventData.mode === "incident"
|
||||
? dedupeIncidents(selectedEventData.features)
|
||||
: dedupeAnomalies(selectedEventData.features);
|
||||
setAnomalyMarkers(eventMarkers, earth);
|
||||
applyCollectorCounts();
|
||||
|
||||
if (!bgpGroup.parent) {
|
||||
@@ -1360,7 +1443,6 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
);
|
||||
|
||||
let scale = marker.userData.baseScale * getCollectorDistanceScale(marker, camera);
|
||||
let opacity = BGP_CONFIG.collectorIcon.idleOpacity;
|
||||
let haloOpacity = 0.0;
|
||||
let pulseOpacity = 0.0;
|
||||
let coverageOpacity = 0.0;
|
||||
@@ -1374,14 +1456,12 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
|
||||
if (isLocked) {
|
||||
scale *= 1.1 + 0.14 * pulse;
|
||||
opacity = 0.96;
|
||||
haloOpacity = 0.05;
|
||||
pulseOpacity = 0.024;
|
||||
coverageOpacity = 0.036;
|
||||
markerColor = 0xcff2ff;
|
||||
} else if (isHovered) {
|
||||
scale *= 1.08;
|
||||
opacity = 0.88;
|
||||
haloOpacity = 0.03;
|
||||
pulseOpacity = 0.014;
|
||||
coverageOpacity = 0.02;
|
||||
@@ -1392,7 +1472,6 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
);
|
||||
} else if (hasLockedLayer) {
|
||||
scale *= 0.98;
|
||||
opacity = BGP_CONFIG.collectorIcon.idleOpacity;
|
||||
haloOpacity = 0.0;
|
||||
pulseOpacity = 0.0;
|
||||
coverageOpacity = 0.0;
|
||||
@@ -1401,12 +1480,8 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
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.position.copy(marker.position);
|
||||
marker.userData.heatHalo.material.opacity = haloOpacity;
|
||||
marker.userData.heatHalo.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor);
|
||||
marker.userData.heatHalo.scale.setScalar(
|
||||
@@ -1414,6 +1489,7 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
);
|
||||
}
|
||||
if (marker.userData.pulseHalo) {
|
||||
marker.userData.pulseHalo.position.copy(marker.position);
|
||||
marker.userData.pulseHalo.material.opacity = pulseOpacity;
|
||||
marker.userData.pulseHalo.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor);
|
||||
marker.userData.pulseHalo.scale.setScalar(
|
||||
@@ -1421,6 +1497,7 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
);
|
||||
}
|
||||
if (marker.userData.statusCore) {
|
||||
marker.userData.statusCore.position.copy(marker.position);
|
||||
marker.userData.statusCore.material.opacity =
|
||||
isLocked ? 0.58 : isHovered ? 0.4 : hasLockedLayer ? 0.0 : 0.18;
|
||||
marker.userData.statusCore.material.color.setHex(marker.userData.baseColor || BGP_CONFIG.collectorColor);
|
||||
@@ -1433,6 +1510,7 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
);
|
||||
}
|
||||
if (marker.userData.coverageHalo) {
|
||||
marker.userData.coverageHalo.position.copy(marker.position);
|
||||
marker.userData.coverageHalo.material.opacity = coverageOpacity;
|
||||
marker.userData.coverageHalo.scale.set(
|
||||
marker.userData.activity?.coverageHaloScale * 0.82 * (1 + pulse * 0.012),
|
||||
@@ -1442,6 +1520,20 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
}
|
||||
});
|
||||
|
||||
const focusedCollector =
|
||||
lockedObjectType === "bgp"
|
||||
? collectorMarkers.find(
|
||||
(marker) => marker.userData.collector === lockedObject?.userData?.collector,
|
||||
)
|
||||
: lockedObjectType === "bgp_collector"
|
||||
? lockedObject
|
||||
: null;
|
||||
bgpCollectorIconLayer.updateVisualState(
|
||||
focusedCollector ? "bgp_collector" : lockedObjectType,
|
||||
focusedCollector || lockedObject,
|
||||
camera,
|
||||
);
|
||||
|
||||
anomalyMarkers.forEach((marker) => {
|
||||
const isLocked = lockedObjectType === "bgp" && lockedObject === marker;
|
||||
const isLinkedCollectorLocked =
|
||||
@@ -1459,7 +1551,6 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
const iconAnchorScale =
|
||||
marker.userData.baseScale * getEventDistanceScale(marker, camera);
|
||||
let scale = iconAnchorScale;
|
||||
let opacity = BGP_CONFIG.opacity.normal;
|
||||
let markerColor = marker.userData.baseColor || getSeverityColor(marker.userData.severity);
|
||||
const isIncidentMarker = marker.userData.source === "bgp_incident";
|
||||
let ringBaseOpacity = isIncidentMarker
|
||||
@@ -1468,49 +1559,38 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
|
||||
if (isLocked || isLinkedCollectorLocked) {
|
||||
scale *= 1 + BGP_CONFIG.pulse.lockedAmplitude * pulse;
|
||||
opacity = 0.9 + 0.1 * pulse;
|
||||
markerColor = 0xfff1a8;
|
||||
ringBaseOpacity *= 1.2;
|
||||
} else if (isCruise) {
|
||||
scale *= 1 + BGP_CONFIG.pulse.lockedAmplitude * pulse;
|
||||
opacity = 0.9 + 0.1 * pulse;
|
||||
ringBaseOpacity *= 1.2;
|
||||
} else if (isHovered) {
|
||||
scale *= BGP_CONFIG.marker.hoverScale;
|
||||
opacity = 0.9;
|
||||
ringBaseOpacity *= 1.05;
|
||||
} else if (isOtherLocked) {
|
||||
scale *= BGP_CONFIG.marker.dimmedScale;
|
||||
opacity = 0.22;
|
||||
markerColor = 0x7d8ca3;
|
||||
ringBaseOpacity = 0.02;
|
||||
} else {
|
||||
scale *= 1 + BGP_CONFIG.pulse.normalAmplitude * pulse;
|
||||
opacity = isIncidentMarker ? 0.7 : 0.62;
|
||||
}
|
||||
|
||||
marker.scale.setScalar(scale);
|
||||
marker.material.color.setHex(markerColor);
|
||||
marker.material.opacity = opacity;
|
||||
marker.visible = showBGP;
|
||||
marker.renderOrder = isActive ? 7 : 3;
|
||||
|
||||
const ringPhaseA = (now * BGP_CONFIG.ring.speed + marker.userData.pulseOffset) % 1;
|
||||
const ringPhaseA = (now * BGP_CONFIG.ring.speed + marker.userData.pulseOffset) % 1;
|
||||
const applyRingState = (ring, phase, maxScale) => {
|
||||
if (!ring) return;
|
||||
const progress = Math.max(0, Math.min(1, phase));
|
||||
const minScale = 1.28;
|
||||
const desiredWorldScale =
|
||||
iconAnchorScale * (minScale + progress * (maxScale - minScale));
|
||||
const parentScale = Math.max(scale, 0.0001);
|
||||
const localRingScale = desiredWorldScale / parentScale;
|
||||
scale * (minScale + progress * (maxScale - minScale));
|
||||
const fadeIn = Math.max(0, Math.min(1, (progress - 0.08) / 0.14));
|
||||
const fadeOut = 1 - progress;
|
||||
const visibility = fadeIn * fadeOut;
|
||||
ring.scale.setScalar(localRingScale);
|
||||
ring.position.copy(marker.position);
|
||||
ring.scale.setScalar(desiredWorldScale);
|
||||
ring.material.color.setHex(markerColor);
|
||||
ring.material.opacity = showBGP ? ringBaseOpacity * visibility : 0;
|
||||
ring.visible = showBGP;
|
||||
ring.renderOrder = isActive ? BGP_EVENT_RENDER_ORDER + 0.15 : BGP_EVENT_RENDER_ORDER - 0.05;
|
||||
};
|
||||
|
||||
applyRingState(marker.userData.ringA, ringPhaseA, BGP_CONFIG.ring.scaleA);
|
||||
@@ -1519,6 +1599,8 @@ export function updateBGPVisualState(lockedObjectType, lockedObject, camera, cru
|
||||
marker.userData.ringB.visible = false;
|
||||
}
|
||||
});
|
||||
|
||||
bgpEventIconLayer.updateVisualState(lockedObjectType, lockedObject, camera);
|
||||
}
|
||||
|
||||
export function setBGPMarkerState(marker, state = "normal") {
|
||||
@@ -1526,15 +1608,19 @@ export function setBGPMarkerState(marker, state = "normal") {
|
||||
if (marker.userData.type !== "bgp" && marker.userData.type !== "bgp_collector") {
|
||||
return;
|
||||
}
|
||||
marker.userData.state = state;
|
||||
if (marker.userData.type === "bgp") {
|
||||
bgpEventIconLayer.setMarkerState(marker, state);
|
||||
return;
|
||||
}
|
||||
bgpCollectorIconLayer.setMarkerState(marker, state);
|
||||
}
|
||||
|
||||
export function clearBGPSelection() {
|
||||
collectorMarkers.forEach((marker) => {
|
||||
marker.userData.state = "normal";
|
||||
bgpCollectorIconLayer.setMarkerState(marker, "normal");
|
||||
});
|
||||
anomalyMarkers.forEach((marker) => {
|
||||
marker.userData.state = "normal";
|
||||
bgpEventIconLayer.setMarkerState(marker, "normal");
|
||||
});
|
||||
clearBGPEventOverlay();
|
||||
}
|
||||
@@ -1542,6 +1628,8 @@ export function clearBGPSelection() {
|
||||
export function clearBGPData(earth) {
|
||||
clearMarkerArray(collectorMarkers);
|
||||
clearMarkerArray(anomalyMarkers);
|
||||
bgpCollectorIconLayer.clearData(earth);
|
||||
bgpEventIconLayer.clearData(earth);
|
||||
clearBGPEventOverlay();
|
||||
activeEventCountByCollector.clear();
|
||||
totalAnomalyCount = 0;
|
||||
@@ -1559,11 +1647,21 @@ export function toggleBGP(show) {
|
||||
showBGP = Boolean(show);
|
||||
bgpGroup.visible = showBGP;
|
||||
bgpOverlayGroup.visible = showBGP;
|
||||
bgpCollectorIconLayer.setVisible(showBGP);
|
||||
bgpEventIconLayer.setVisible(showBGP);
|
||||
collectorMarkers.forEach((marker) => {
|
||||
marker.visible = showBGP;
|
||||
[
|
||||
marker.userData.heatHalo,
|
||||
marker.userData.pulseHalo,
|
||||
marker.userData.statusCore,
|
||||
marker.userData.coverageHalo,
|
||||
].forEach((sprite) => {
|
||||
if (sprite) sprite.visible = showBGP;
|
||||
});
|
||||
});
|
||||
anomalyMarkers.forEach((marker) => {
|
||||
marker.visible = showBGP;
|
||||
marker.userData.ringA && (marker.userData.ringA.visible = showBGP);
|
||||
marker.userData.ringB && (marker.userData.ringB.visible = false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1579,6 +1677,14 @@ export function getBGPAnomalyMarkers() {
|
||||
return anomalyMarkers;
|
||||
}
|
||||
|
||||
export function getBGPAnomalyPointerIntersections(options = {}) {
|
||||
return bgpEventIconLayer.getPointerIntersections(options);
|
||||
}
|
||||
|
||||
export function getBGPCollectorPointerIntersections(options = {}) {
|
||||
return bgpCollectorIconLayer.getPointerIntersections(options);
|
||||
}
|
||||
|
||||
export function getBGPCollectorMarkers() {
|
||||
return collectorMarkers;
|
||||
}
|
||||
@@ -1644,11 +1750,11 @@ export function showBGPEventOverlay(marker, earth) {
|
||||
latLonToVector3(
|
||||
region.latitude,
|
||||
region.longitude,
|
||||
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset - 0.1,
|
||||
CONFIG.earthRadius + BGP_COLLECTOR_ALTITUDE_OFFSET - 0.1,
|
||||
),
|
||||
);
|
||||
halo.renderOrder = 2;
|
||||
bgpOverlayGroup.add(halo);
|
||||
bgpEventOverlayGroup.add(halo);
|
||||
overlayItems.push(halo);
|
||||
});
|
||||
|
||||
@@ -1687,11 +1793,11 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
latLonToVector3(
|
||||
marker.userData.displayLatitude ?? marker.userData.latitude,
|
||||
marker.userData.displayLongitude ?? marker.userData.longitude,
|
||||
CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset - 0.15,
|
||||
CONFIG.earthRadius + BGP_COLLECTOR_ALTITUDE_OFFSET - 0.15,
|
||||
),
|
||||
);
|
||||
halo.renderOrder = 2;
|
||||
bgpOverlayGroup.add(halo);
|
||||
bgpCollectorRadarGroup.add(halo);
|
||||
|
||||
const pulseHalo = createOverlaySprite({
|
||||
color: BGP_CONFIG.collectorColor,
|
||||
@@ -1700,7 +1806,7 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
});
|
||||
pulseHalo.position.copy(halo.position);
|
||||
pulseHalo.renderOrder = 1;
|
||||
bgpOverlayGroup.add(pulseHalo);
|
||||
bgpCollectorRadarGroup.add(pulseHalo);
|
||||
const innerRing = createOverlaySprite({
|
||||
color: BGP_CONFIG.collectorColor,
|
||||
opacity: 0.12,
|
||||
@@ -1708,7 +1814,7 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
});
|
||||
innerRing.position.copy(halo.position);
|
||||
innerRing.renderOrder = 3;
|
||||
bgpOverlayGroup.add(innerRing);
|
||||
bgpCollectorRadarGroup.add(innerRing);
|
||||
|
||||
const overlayItems = [halo, pulseHalo, innerRing];
|
||||
const anchorLatitude = marker.userData.displayLatitude ?? marker.userData.latitude;
|
||||
@@ -1723,8 +1829,8 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
const startBearing = (sectorRotation - sectorHalfWidth) * (180 / Math.PI);
|
||||
const endBearing = (sectorRotation + sectorHalfWidth) * (180 / Math.PI);
|
||||
const coverageColor = marker.userData.baseColor || BGP_CONFIG.collectorColor;
|
||||
const boundaryAltitude = CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset + 0.44;
|
||||
const fillAltitude = CONFIG.earthRadius + BGP_CONFIG.collectorAltitudeOffset + 0.4;
|
||||
const boundaryAltitude = CONFIG.earthRadius + BGP_COLLECTOR_ALTITUDE_OFFSET + 0.44;
|
||||
const fillAltitude = CONFIG.earthRadius + BGP_COLLECTOR_ALTITUDE_OFFSET + 0.4;
|
||||
const leftBoundaryPoints = createRadialBoundaryPoints(
|
||||
anchorLatitude,
|
||||
anchorLongitude,
|
||||
@@ -1765,7 +1871,7 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
0.12,
|
||||
);
|
||||
sectorFill.renderOrder = 2;
|
||||
bgpOverlayGroup.add(sectorFill);
|
||||
bgpCollectorRadarGroup.add(sectorFill);
|
||||
overlayItems.push(sectorFill);
|
||||
|
||||
const outerArc = createCoverageBoundaryLine(
|
||||
@@ -1774,7 +1880,7 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
0.9,
|
||||
);
|
||||
outerArc.renderOrder = 3;
|
||||
bgpOverlayGroup.add(outerArc);
|
||||
bgpCollectorRadarGroup.add(outerArc);
|
||||
overlayItems.push(outerArc);
|
||||
|
||||
const leftBoundary = createCoverageBoundaryLine(
|
||||
@@ -1783,7 +1889,7 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
0.76,
|
||||
);
|
||||
leftBoundary.renderOrder = 3;
|
||||
bgpOverlayGroup.add(leftBoundary);
|
||||
bgpCollectorRadarGroup.add(leftBoundary);
|
||||
overlayItems.push(leftBoundary);
|
||||
|
||||
const rightBoundary = createCoverageBoundaryLine(
|
||||
@@ -1792,7 +1898,7 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
0.76,
|
||||
);
|
||||
rightBoundary.renderOrder = 3;
|
||||
bgpOverlayGroup.add(rightBoundary);
|
||||
bgpCollectorRadarGroup.add(rightBoundary);
|
||||
overlayItems.push(rightBoundary);
|
||||
|
||||
activeEventOverlay = overlayItems;
|
||||
@@ -1808,7 +1914,8 @@ export function showBGPCollectorCoverageOverlay(marker, earth, options = {}) {
|
||||
export function clearBGPEventOverlay() {
|
||||
activeEventOverlay = null;
|
||||
activeCollectorOverlayContext = null;
|
||||
clearGroup(bgpOverlayGroup);
|
||||
clearGroup(bgpEventOverlayGroup);
|
||||
clearGroup(bgpCollectorRadarGroup);
|
||||
}
|
||||
|
||||
function updateCollectorOverlayScan(lockedObjectType, lockedObject) {
|
||||
|
||||
Reference in New Issue
Block a user