597 lines
19 KiB
JavaScript
597 lines
19 KiB
JavaScript
import * as THREE from "three";
|
|
|
|
import { CONNECTOR_CONFIG, CRUISE_CONFIG, PATHS } from "./constants.js";
|
|
import {
|
|
computeNearestPerimeterAnchor,
|
|
createConnectorPath,
|
|
resolveConnectorAnchor,
|
|
} from "./callout-connector.js";
|
|
|
|
const scratchBGPWorldPosition = new THREE.Vector3();
|
|
const CRUISE_CARD_ESTIMATED_HEIGHT_PX = 420;
|
|
const CRUISE_CARD_ESTIMATED_WIDTH_PX = 300;
|
|
const CRUISE_CARD_VIEWPORT_PADDING_PX = 32;
|
|
const CRUISE_CARD_SCREEN_MARGIN_PX = 12;
|
|
const CRUISE_MOBILE_POPUP_ESTIMATED_WIDTH_PX = 220;
|
|
const CRUISE_MOBILE_POPUP_ESTIMATED_HEIGHT_PX = 68;
|
|
const CRUISE_MOBILE_POPUP_TOP_RATIO = 0.17;
|
|
const CRUISE_MOBILE_POPUP_MARGIN_PX = 14;
|
|
const CRUISE_MOBILE_DRAWER_CLEARANCE_PX = 52;
|
|
const CRUISE_MOBILE_SLOT_OVERFLOW_WEIGHT = 3;
|
|
const CRUISE_CONNECTOR_READY_TIMEOUT_MS = 1200;
|
|
const CRUISE_CONNECTOR_DRAW_MS = 420;
|
|
const CRUISE_PRESENTATION_HIDE_MS = 220;
|
|
const MOBILE_POPUP_OBSTACLE_PADDING_PX = 16;
|
|
const DESKTOP_PANEL_OBSTACLE_PADDING_PX = 12;
|
|
const CRUISE_MARKER_SCREEN_PADDING_PX = 4;
|
|
|
|
function getDockAxisOffsets(dockSide, gapPx) {
|
|
return {
|
|
offsetX:
|
|
dockSide === "right" ? gapPx : dockSide === "left" ? -gapPx : 0,
|
|
offsetY:
|
|
dockSide === "bottom" ? gapPx : dockSide === "top" ? -gapPx : 0,
|
|
};
|
|
}
|
|
|
|
function getObstaclePaddingBySide(side, paddingPx) {
|
|
if (side === "left") {
|
|
return { left: 0, top: paddingPx, right: paddingPx, bottom: paddingPx };
|
|
}
|
|
if (side === "right") {
|
|
return { left: paddingPx, top: paddingPx, right: 0, bottom: paddingPx };
|
|
}
|
|
if (side === "top") {
|
|
return { left: paddingPx, top: 0, right: paddingPx, bottom: paddingPx };
|
|
}
|
|
return { left: paddingPx, top: paddingPx, right: paddingPx, bottom: 0 };
|
|
}
|
|
|
|
const scratchMarkerWorldScale = new THREE.Vector3();
|
|
const scratchCameraQuaternion = new THREE.Quaternion();
|
|
const scratchCameraRight = new THREE.Vector3();
|
|
const scratchCameraUp = new THREE.Vector3();
|
|
const scratchMarkerRightPoint = new THREE.Vector3();
|
|
const scratchMarkerLeftPoint = new THREE.Vector3();
|
|
const scratchMarkerTopPoint = new THREE.Vector3();
|
|
const scratchMarkerBottomPoint = new THREE.Vector3();
|
|
|
|
function projectWorldToScreen(point, camera) {
|
|
if (!point || !camera) return null;
|
|
const projected = point.clone().project(camera);
|
|
if (!Number.isFinite(projected.x) || !Number.isFinite(projected.y)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
x: ((projected.x + 1) * 0.5) * window.innerWidth,
|
|
y: ((1 - projected.y) * 0.5) * window.innerHeight,
|
|
};
|
|
}
|
|
|
|
function getMarkerTimestamp(marker) {
|
|
const rawValue = marker?.userData?.created_at_raw;
|
|
const parsedValue = rawValue ? new Date(rawValue).getTime() : 0;
|
|
return Number.isFinite(parsedValue) ? parsedValue : 0;
|
|
}
|
|
|
|
export function createBGPCruiseAdapter({
|
|
camera,
|
|
earth,
|
|
getMarkers,
|
|
connector,
|
|
focusView,
|
|
setMarkerLocked,
|
|
clearMarkerState,
|
|
showMarkerOverlay,
|
|
applySatelliteHighlights,
|
|
showMarkerInfo,
|
|
hideInfo,
|
|
isInfoVisible,
|
|
getLockedObject,
|
|
refreshMarkers,
|
|
}) {
|
|
let currentMarkerId = null;
|
|
let cardPlacement = null;
|
|
let knownEventIds = new Set();
|
|
|
|
function getCurrentMarker() {
|
|
if (!currentMarkerId) return null;
|
|
return getMarkers().find((marker) => marker?.userData?.id === currentMarkerId) || null;
|
|
}
|
|
|
|
function getSortedMarkers() {
|
|
return getMarkers()
|
|
.slice()
|
|
.sort((a, b) => getMarkerTimestamp(b) - getMarkerTimestamp(a));
|
|
}
|
|
|
|
function getMarkerScreenCoords(marker) {
|
|
if (!marker || !camera) return null;
|
|
scratchBGPWorldPosition.copy(marker.position);
|
|
if (marker.parent) {
|
|
marker.parent.localToWorld(scratchBGPWorldPosition);
|
|
} else {
|
|
const earthObject = typeof earth === "function" ? earth() : earth;
|
|
earthObject?.updateMatrixWorld(true);
|
|
earthObject?.localToWorld(scratchBGPWorldPosition);
|
|
}
|
|
return projectWorldToScreen(scratchBGPWorldPosition, camera);
|
|
}
|
|
|
|
function getVisibleMobilePopup() {
|
|
const mobilePopup = document.getElementById("earth-mobile-popup");
|
|
return mobilePopup instanceof HTMLElement && !mobilePopup.hasAttribute("hidden")
|
|
? mobilePopup
|
|
: null;
|
|
}
|
|
|
|
function getVisibleInfoPanel() {
|
|
const infoPanel = document.getElementById("info-panel");
|
|
return infoPanel instanceof HTMLElement && !infoPanel.hasAttribute("hidden")
|
|
? infoPanel
|
|
: null;
|
|
}
|
|
|
|
function getMarkerScreenRect(marker) {
|
|
const center = getMarkerScreenCoords(marker);
|
|
if (!center || !camera || !marker) return null;
|
|
|
|
marker.getWorldScale(scratchMarkerWorldScale);
|
|
const worldWidth = Math.max(
|
|
0.0001,
|
|
Number(marker.userData?.baseScale ?? scratchMarkerWorldScale.x ?? 0) || scratchMarkerWorldScale.x,
|
|
);
|
|
const worldHeight = Math.max(
|
|
0.0001,
|
|
Number(scratchMarkerWorldScale.y || worldWidth),
|
|
);
|
|
|
|
camera.getWorldQuaternion(scratchCameraQuaternion);
|
|
scratchCameraRight.set(1, 0, 0).applyQuaternion(scratchCameraQuaternion).normalize();
|
|
scratchCameraUp.set(0, 1, 0).applyQuaternion(scratchCameraQuaternion).normalize();
|
|
|
|
scratchMarkerRightPoint
|
|
.copy(scratchBGPWorldPosition)
|
|
.addScaledVector(scratchCameraRight, worldWidth * 0.5);
|
|
scratchMarkerLeftPoint
|
|
.copy(scratchBGPWorldPosition)
|
|
.addScaledVector(scratchCameraRight, -worldWidth * 0.5);
|
|
scratchMarkerTopPoint
|
|
.copy(scratchBGPWorldPosition)
|
|
.addScaledVector(scratchCameraUp, worldHeight * 0.5);
|
|
scratchMarkerBottomPoint
|
|
.copy(scratchBGPWorldPosition)
|
|
.addScaledVector(scratchCameraUp, -worldHeight * 0.5);
|
|
|
|
const rightPoint = projectWorldToScreen(scratchMarkerRightPoint, camera);
|
|
const leftPoint = projectWorldToScreen(scratchMarkerLeftPoint, camera);
|
|
const topPoint = projectWorldToScreen(scratchMarkerTopPoint, camera);
|
|
const bottomPoint = projectWorldToScreen(scratchMarkerBottomPoint, camera);
|
|
if (!rightPoint || !leftPoint || !topPoint || !bottomPoint) {
|
|
return null;
|
|
}
|
|
|
|
const halfWidth = Math.max(
|
|
Math.abs(rightPoint.x - center.x),
|
|
Math.abs(leftPoint.x - center.x),
|
|
1,
|
|
);
|
|
const halfHeight = Math.max(
|
|
Math.abs(topPoint.y - center.y),
|
|
Math.abs(bottomPoint.y - center.y),
|
|
1,
|
|
);
|
|
|
|
return {
|
|
x: center.x - halfWidth - CRUISE_MARKER_SCREEN_PADDING_PX,
|
|
y: center.y - halfHeight - CRUISE_MARKER_SCREEN_PADDING_PX,
|
|
width: halfWidth * 2 + CRUISE_MARKER_SCREEN_PADDING_PX * 2,
|
|
height: halfHeight * 2 + CRUISE_MARKER_SCREEN_PADDING_PX * 2,
|
|
};
|
|
}
|
|
|
|
function getCardScreenCoords(marker) {
|
|
const markerCoords = getMarkerScreenCoords(marker);
|
|
if (!markerCoords) return null;
|
|
|
|
if (document.body.classList.contains("layout-mode-mobile")) {
|
|
const safeBottom =
|
|
parseFloat(
|
|
getComputedStyle(document.documentElement).getPropertyValue("--safe-bottom"),
|
|
) || 0;
|
|
const estimatedCardWidth = Math.min(
|
|
CRUISE_MOBILE_POPUP_ESTIMATED_WIDTH_PX,
|
|
window.innerWidth - CRUISE_MOBILE_POPUP_MARGIN_PX * 2,
|
|
);
|
|
const estimatedCardHeight = CRUISE_MOBILE_POPUP_ESTIMATED_HEIGHT_PX;
|
|
const topBound = Math.max(
|
|
CRUISE_MOBILE_POPUP_MARGIN_PX,
|
|
Math.min(
|
|
window.innerHeight * CRUISE_MOBILE_POPUP_TOP_RATIO,
|
|
window.innerHeight -
|
|
CRUISE_MOBILE_DRAWER_CLEARANCE_PX -
|
|
safeBottom -
|
|
estimatedCardHeight -
|
|
CRUISE_MOBILE_POPUP_MARGIN_PX,
|
|
),
|
|
);
|
|
const rightSlotLeft = Math.max(
|
|
CRUISE_MOBILE_POPUP_MARGIN_PX,
|
|
window.innerWidth - estimatedCardWidth - CRUISE_MOBILE_POPUP_MARGIN_PX,
|
|
);
|
|
const leftSlotLeft = CRUISE_MOBILE_POPUP_MARGIN_PX;
|
|
const rightSlotCenterX = rightSlotLeft + estimatedCardWidth * 0.5;
|
|
const leftSlotCenterX = leftSlotLeft + estimatedCardWidth * 0.5;
|
|
const rightClearance = rightSlotLeft - markerCoords.x;
|
|
const leftClearance = markerCoords.x - (leftSlotLeft + estimatedCardWidth);
|
|
const rightCost =
|
|
Math.max(0, -rightClearance) * CRUISE_MOBILE_SLOT_OVERFLOW_WEIGHT +
|
|
Math.abs(rightSlotCenterX - markerCoords.x);
|
|
const leftCost =
|
|
Math.max(0, -leftClearance) * CRUISE_MOBILE_SLOT_OVERFLOW_WEIGHT +
|
|
Math.abs(markerCoords.x - leftSlotCenterX);
|
|
const placeOnRight = rightCost <= leftCost;
|
|
const left = placeOnRight ? rightSlotLeft : leftSlotLeft;
|
|
return {
|
|
x: left,
|
|
y: topBound,
|
|
width: estimatedCardWidth,
|
|
height: estimatedCardHeight,
|
|
dockSide: placeOnRight ? "left" : "right",
|
|
};
|
|
}
|
|
|
|
const hudScale =
|
|
Number.parseFloat(
|
|
getComputedStyle(document.documentElement).getPropertyValue("--hud-scale"),
|
|
) || 1;
|
|
const estimatedCardHeight = Math.min(
|
|
CRUISE_CARD_ESTIMATED_HEIGHT_PX * hudScale,
|
|
window.innerHeight * 0.7,
|
|
);
|
|
const estimatedCardWidth = Math.min(
|
|
CRUISE_CARD_ESTIMATED_WIDTH_PX * hudScale,
|
|
window.innerWidth - CRUISE_CARD_VIEWPORT_PADDING_PX,
|
|
);
|
|
|
|
const x =
|
|
window.innerWidth * CRUISE_CONFIG.cardAnchorXRatio - estimatedCardWidth * 0.5;
|
|
const y =
|
|
window.innerHeight * CRUISE_CONFIG.cardAnchorYRatio - estimatedCardHeight * 0.5;
|
|
const margin = CRUISE_CARD_SCREEN_MARGIN_PX;
|
|
const clampedX = Math.min(
|
|
Math.max(margin, x),
|
|
Math.max(margin, window.innerWidth - estimatedCardWidth - margin),
|
|
);
|
|
const clampedY = Math.min(
|
|
Math.max(margin, y),
|
|
Math.max(margin, window.innerHeight - estimatedCardHeight - margin),
|
|
);
|
|
return {
|
|
x: clampedX,
|
|
y: clampedY,
|
|
width: estimatedCardWidth,
|
|
height: estimatedCardHeight,
|
|
};
|
|
}
|
|
|
|
function getCardAnchorTarget() {
|
|
const mobilePopup = getVisibleMobilePopup();
|
|
if (
|
|
document.body.classList.contains("layout-mode-mobile") &&
|
|
mobilePopup
|
|
) {
|
|
const dockSide = mobilePopup.dataset.dockSide || "left";
|
|
const { offsetX, offsetY } = getDockAxisOffsets(
|
|
dockSide,
|
|
CONNECTOR_CONFIG.panelGapPx,
|
|
);
|
|
return {
|
|
element: mobilePopup,
|
|
side: dockSide,
|
|
alignRatio: 0.5,
|
|
offsetX,
|
|
offsetY,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getCardObstacleTarget(fallbackPlacement = null) {
|
|
const mobilePopup = getVisibleMobilePopup();
|
|
if (
|
|
document.body.classList.contains("layout-mode-mobile") &&
|
|
mobilePopup
|
|
) {
|
|
const side = mobilePopup.dataset.dockSide || "left";
|
|
return {
|
|
element: mobilePopup,
|
|
padding: getObstaclePaddingBySide(side, MOBILE_POPUP_OBSTACLE_PADDING_PX),
|
|
};
|
|
}
|
|
|
|
const infoPanel = getVisibleInfoPanel();
|
|
if (infoPanel) {
|
|
return {
|
|
element: infoPanel,
|
|
padding: getObstaclePaddingBySide("left", DESKTOP_PANEL_OBSTACLE_PADDING_PX),
|
|
};
|
|
}
|
|
|
|
if (fallbackPlacement) {
|
|
return {
|
|
x: fallbackPlacement.x,
|
|
y: fallbackPlacement.y,
|
|
width: fallbackPlacement.width ?? 0,
|
|
height: fallbackPlacement.height ?? 0,
|
|
padding: DESKTOP_PANEL_OBSTACLE_PADDING_PX,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveAdaptiveDockSide(markerCoords, fallbackPlacement = null) {
|
|
const mobilePopup = getVisibleMobilePopup();
|
|
const popupRect =
|
|
mobilePopup
|
|
? mobilePopup.getBoundingClientRect()
|
|
: fallbackPlacement
|
|
? {
|
|
left: fallbackPlacement.x,
|
|
top: fallbackPlacement.y,
|
|
right: fallbackPlacement.x + (fallbackPlacement.width ?? 0),
|
|
bottom: fallbackPlacement.y + (fallbackPlacement.height ?? 0),
|
|
}
|
|
: null;
|
|
if (!markerCoords || !popupRect) return fallbackPlacement?.dockSide === "right" ? "right" : "left";
|
|
|
|
return computeNearestPerimeterAnchor(markerCoords, popupRect, 0)?.side || "left";
|
|
}
|
|
|
|
function syncMobileDockSide(markerCoords, fallbackPlacement = null) {
|
|
const mobilePopup = getVisibleMobilePopup();
|
|
const dockSide = resolveAdaptiveDockSide(markerCoords, fallbackPlacement);
|
|
if (mobilePopup) {
|
|
mobilePopup.dataset.dockSide = dockSide;
|
|
}
|
|
}
|
|
|
|
function getConnectorPath(marker) {
|
|
const markerCoords = getMarkerScreenCoords(marker);
|
|
const markerRect = getMarkerScreenRect(marker);
|
|
if (!markerCoords) return null;
|
|
|
|
if (document.body.classList.contains("layout-mode-mobile")) {
|
|
const targetCardCoords = cardPlacement || getCardScreenCoords(marker);
|
|
syncMobileDockSide(markerCoords, targetCardCoords);
|
|
const cardAnchorTarget = getCardAnchorTarget();
|
|
const cardAnchorCoords = resolveConnectorAnchor(cardAnchorTarget);
|
|
const cardObstacleTarget = getCardObstacleTarget(targetCardCoords);
|
|
if (!cardAnchorCoords) return null;
|
|
|
|
return createConnectorPath(markerCoords, cardAnchorTarget ?? cardAnchorCoords, {
|
|
routingMode: "adaptive",
|
|
sourceRect: markerRect,
|
|
targetAnchor: cardAnchorTarget ?? cardAnchorCoords,
|
|
obstacles: cardObstacleTarget ? [cardObstacleTarget] : [],
|
|
startFrom: "source",
|
|
sourceGapPx: CONNECTOR_CONFIG.markerGapPx,
|
|
targetGapPx: CONNECTOR_CONFIG.panelGapPx,
|
|
obstacleClearancePx: CONNECTOR_CONFIG.obstacleClearancePx,
|
|
});
|
|
}
|
|
|
|
const targetCardCoords = cardPlacement || getCardScreenCoords(marker);
|
|
const cardObstacleTarget = getCardObstacleTarget(targetCardCoords);
|
|
if (!targetCardCoords) return null;
|
|
|
|
const infoPanel = getVisibleInfoPanel();
|
|
const desktopTarget =
|
|
infoPanel
|
|
? infoPanel
|
|
: {
|
|
x: targetCardCoords.x,
|
|
y: targetCardCoords.y,
|
|
width: targetCardCoords.width,
|
|
height: targetCardCoords.height,
|
|
};
|
|
|
|
return createConnectorPath(
|
|
markerCoords,
|
|
desktopTarget,
|
|
{
|
|
routingMode: "adaptive",
|
|
sourceRect: markerRect,
|
|
obstacles: cardObstacleTarget ? [cardObstacleTarget] : [],
|
|
obstacleClearancePx: CONNECTOR_CONFIG.obstacleClearancePx,
|
|
startFrom: "source",
|
|
sourceGapPx: CONNECTOR_CONFIG.markerGapPx,
|
|
targetGapPx: CONNECTOR_CONFIG.panelGapPx,
|
|
},
|
|
);
|
|
}
|
|
|
|
function renderConnector(marker, { animate = false } = {}) {
|
|
const path = getConnectorPath(marker);
|
|
if (!path) return false;
|
|
return connector.render(path, { animate });
|
|
}
|
|
|
|
function extractFeatureIds(features = []) {
|
|
return features
|
|
.map((feature) => {
|
|
const properties = feature?.properties || {};
|
|
const coords = feature?.geometry?.coordinates || [];
|
|
return (
|
|
properties.id ||
|
|
properties.incident_key ||
|
|
`${properties.collector || properties.incident_type || properties.anomaly_type || "event"}-${coords[1]}-${coords[0]}`
|
|
);
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
return {
|
|
getSortedMarkers,
|
|
getCurrentMarker,
|
|
isPresentationVisible() {
|
|
return cardPlacement != null;
|
|
},
|
|
clearCurrentHighlight() {
|
|
const marker = getCurrentMarker();
|
|
if (marker && getLockedObject() !== marker) {
|
|
clearMarkerState(marker);
|
|
}
|
|
currentMarkerId = null;
|
|
},
|
|
async focusMarker(marker, { interrupt = false } = {}) {
|
|
if (!marker) return;
|
|
currentMarkerId = marker.userData?.id || null;
|
|
setMarkerLocked(marker);
|
|
showMarkerOverlay(marker);
|
|
|
|
await focusView({
|
|
lat: marker.userData?.latitude ?? 0,
|
|
lon: marker.userData?.longitude ?? 0,
|
|
rotLon: (marker.userData?.longitude ?? 0) - 270,
|
|
duration: interrupt
|
|
? Math.round(CRUISE_CONFIG.focusDurationMs * 0.78)
|
|
: CRUISE_CONFIG.focusDurationMs,
|
|
suppressStatus: true,
|
|
});
|
|
|
|
cardPlacement = getCardScreenCoords(marker);
|
|
},
|
|
async presentMarker(marker, { context }) {
|
|
if (!marker) return false;
|
|
|
|
const showCruiseMarkerInfo = ({ reveal = true } = {}) =>
|
|
showMarkerInfo(marker, {
|
|
x: cardPlacement?.x,
|
|
y: cardPlacement?.y,
|
|
absolute: true,
|
|
reveal,
|
|
anchorStable: true,
|
|
dockSide: cardPlacement?.dockSide,
|
|
});
|
|
|
|
showCruiseMarkerInfo({ reveal: false });
|
|
await context.nextFrame();
|
|
if (!context.isCurrent()) {
|
|
cardPlacement = null;
|
|
connector.hide();
|
|
hideInfo();
|
|
return false;
|
|
}
|
|
|
|
const startedAt = performance.now();
|
|
let connectorReady = false;
|
|
while (context.isCurrent()) {
|
|
connectorReady = renderConnector(marker, { animate: !connectorReady });
|
|
if (connectorReady) break;
|
|
if (performance.now() - startedAt >= CRUISE_CONNECTOR_READY_TIMEOUT_MS) {
|
|
break;
|
|
}
|
|
await context.nextFrame();
|
|
}
|
|
|
|
if (!connectorReady || !context.isCurrent()) {
|
|
cardPlacement = null;
|
|
connector.hide();
|
|
hideInfo();
|
|
return false;
|
|
}
|
|
|
|
applySatelliteHighlights(marker);
|
|
|
|
const connectorDelayCompleted = await context.wait(CRUISE_CONNECTOR_DRAW_MS);
|
|
if (!connectorDelayCompleted || !context.isCurrent()) {
|
|
cardPlacement = null;
|
|
connector.hide();
|
|
hideInfo();
|
|
return false;
|
|
}
|
|
|
|
showCruiseMarkerInfo();
|
|
await context.nextFrame();
|
|
if (!isInfoVisible()) {
|
|
showCruiseMarkerInfo();
|
|
await context.nextFrame();
|
|
}
|
|
|
|
if (!isInfoVisible() || !context.isCurrent()) {
|
|
cardPlacement = null;
|
|
connector.hide();
|
|
hideInfo();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
async hidePresentation({ context }) {
|
|
if (!getLockedObject()) {
|
|
hideInfo();
|
|
}
|
|
connector.hide();
|
|
const hideDelayCompleted = await context.wait(CRUISE_PRESENTATION_HIDE_MS, {
|
|
secondary: true,
|
|
});
|
|
if (!hideDelayCompleted) return;
|
|
cardPlacement = null;
|
|
},
|
|
repositionConnector(marker) {
|
|
if (!cardPlacement || !marker || !connector.isVisible() || connector.isAnimating()) {
|
|
return;
|
|
}
|
|
renderConnector(marker, { animate: false });
|
|
},
|
|
resetPresentation() {
|
|
cardPlacement = null;
|
|
connector.hide();
|
|
},
|
|
syncKnownEventIds() {
|
|
knownEventIds = new Set(
|
|
getMarkers()
|
|
.map((marker) => marker?.userData?.id)
|
|
.filter(Boolean),
|
|
);
|
|
return knownEventIds;
|
|
},
|
|
async pollForNewMarkerIds() {
|
|
const [incidentResponse, anomalyResponse] = await Promise.all([
|
|
fetch(`${PATHS.bgpIncidentsApi}?limit=${CRUISE_CONFIG.maxPolledEvents}`),
|
|
fetch(`${PATHS.bgpApi}?limit=${CRUISE_CONFIG.maxPolledEvents}`),
|
|
]);
|
|
|
|
if (!incidentResponse.ok || !anomalyResponse.ok) {
|
|
return [];
|
|
}
|
|
|
|
const [incidentPayload, anomalyPayload] = await Promise.all([
|
|
incidentResponse.json(),
|
|
anomalyResponse.json(),
|
|
]);
|
|
|
|
const incidentFeatures = Array.isArray(incidentPayload?.features)
|
|
? incidentPayload.features
|
|
: [];
|
|
const anomalyFeatures = Array.isArray(anomalyPayload?.features)
|
|
? anomalyPayload.features
|
|
: [];
|
|
const selectedFeatures =
|
|
incidentFeatures.length > 0 ? incidentFeatures : anomalyFeatures;
|
|
|
|
const nextIds = extractFeatureIds(selectedFeatures);
|
|
const newIds = nextIds.filter((id) => !knownEventIds.has(id));
|
|
if (newIds.length === 0) return [];
|
|
|
|
await refreshMarkers();
|
|
this.syncKnownEventIds();
|
|
return newIds;
|
|
},
|
|
};
|
|
}
|