release: bump version to 0.31.2
This commit is contained in:
@@ -138,6 +138,9 @@ import {
|
||||
import {
|
||||
setLayerButtonState,
|
||||
} from "./layer-button-state.js";
|
||||
import { CalloutConnector } from "./callout-connector.js";
|
||||
import { CruiseSequencer } from "./cruise-sequencer.js";
|
||||
import { createBGPCruiseAdapter } from "./bgp-cruise-adapter.js";
|
||||
import {
|
||||
initInfoCard,
|
||||
showInfoCard,
|
||||
@@ -190,43 +193,10 @@ let cableToggleToken = 0;
|
||||
let satelliteToggleToken = 0;
|
||||
let satelliteHydrationToken = 0;
|
||||
let sceneLights = null;
|
||||
let cruiseTimerId = null;
|
||||
let cruiseHideCardTimerId = null;
|
||||
let cruisePollTimerId = null;
|
||||
let cruiseCurrentMarkerId = null;
|
||||
let cruiseCurrentIndex = -1;
|
||||
let cruiseQueuedMarkerIds = [];
|
||||
let cruiseKnownEventIds = new Set();
|
||||
let cruiseCardPinned = false;
|
||||
let cruiseConnectorEl = null;
|
||||
let cruiseConnectorPolyline = null;
|
||||
let cruiseConnectorStartpoint = null;
|
||||
let cruiseConnectorEndpoint = null;
|
||||
let cruiseConnectorNeedsAnimation = false;
|
||||
let cruiseCardPlacement = null;
|
||||
let cruiseSequenceToken = 0;
|
||||
let cruisePresentationPhase = "hidden";
|
||||
let cruiseAdvanceInFlight = false;
|
||||
let cruiseAdvanceQueued = false;
|
||||
let cruiseAdvanceInterrupt = false;
|
||||
let cruiseCancelNotifier = null;
|
||||
|
||||
function createCruisePresentationTimer() {
|
||||
let endsAt = 0;
|
||||
return {
|
||||
start(durationMs) {
|
||||
endsAt = Date.now() + Math.max(0, durationMs);
|
||||
},
|
||||
stop() {
|
||||
endsAt = 0;
|
||||
},
|
||||
isActive() {
|
||||
return endsAt > 0 && Date.now() < endsAt;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const cruisePresentationTimer = createCruisePresentationTimer();
|
||||
let cruiseConnector = null;
|
||||
let cruiseBGPAdapter = null;
|
||||
let cruiseSequencer = null;
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
const interactionRaycaster = new THREE.Raycaster();
|
||||
@@ -242,10 +212,7 @@ const cleanupFns = [];
|
||||
const DRAG_SMOOTHING_FACTOR = 0.18;
|
||||
const INERTIA_DAMPING = 0.92;
|
||||
const INERTIA_MIN_VELOCITY = 0.00008;
|
||||
const CRUISE_CONNECTOR_DRAW_MS = 420;
|
||||
const CRUISE_TRANSITION_GAP_MS = 24;
|
||||
const CRUISE_PRESENTATION_HIDE_MS = 220;
|
||||
const CRUISE_CONNECTOR_READY_TIMEOUT_MS = 1200;
|
||||
const ACTIVE_BGP_TOOLTIP_TEXT = "隐藏BGP观测";
|
||||
const TOOLTIP_CURSOR_OFFSET = 14; // px offset from cursor for hover tooltips
|
||||
const TOOLTIP_COORDS_OFFSET = 10; // px offset for earth-coordinate tooltip
|
||||
@@ -273,69 +240,10 @@ function bindListener(target, eventName, handler, options) {
|
||||
);
|
||||
}
|
||||
|
||||
function waitForCruiseDelay(durationMs, sequenceToken, { useHideTimer = false } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const settle = (ok) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (cruiseCancelNotifier === onCancel) cruiseCancelNotifier = null;
|
||||
resolve(ok);
|
||||
};
|
||||
const onCancel = () => settle(false);
|
||||
|
||||
const timerId = window.setTimeout(() => {
|
||||
if (useHideTimer) {
|
||||
if (cruiseHideCardTimerId === timerId) cruiseHideCardTimerId = null;
|
||||
} else if (cruiseTimerId === timerId) {
|
||||
cruiseTimerId = null;
|
||||
}
|
||||
settle(sequenceToken === cruiseSequenceToken);
|
||||
}, durationMs);
|
||||
|
||||
if (useHideTimer) {
|
||||
cruiseHideCardTimerId = timerId;
|
||||
} else {
|
||||
cruiseTimerId = timerId;
|
||||
}
|
||||
|
||||
cruiseCancelNotifier = onCancel;
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForCruiseConnectorReady(sequenceToken) {
|
||||
const startedAt = performance.now();
|
||||
|
||||
while (sequenceToken === cruiseSequenceToken) {
|
||||
if (!isCruiseModeActive() || !getAutoRotate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ready = updateCruiseConnector();
|
||||
if (ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (performance.now() - startedAt >= CRUISE_CONNECTOR_READY_TIMEOUT_MS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await nextAnimationFrame();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getViewportAspect() {
|
||||
return window.innerWidth / window.innerHeight;
|
||||
}
|
||||
|
||||
function nextAnimationFrame() {
|
||||
return new Promise((resolve) => {
|
||||
window.requestAnimationFrame(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
function syncRendererViewport() {
|
||||
if (!camera || !renderer) return;
|
||||
camera.aspect = getViewportAspect();
|
||||
@@ -730,568 +638,150 @@ function updateBGPHud(bgpResult) {
|
||||
}
|
||||
}
|
||||
|
||||
function clearCruiseTimers() {
|
||||
if (cruiseTimerId) {
|
||||
clearTimeout(cruiseTimerId);
|
||||
cruiseTimerId = null;
|
||||
}
|
||||
if (cruiseHideCardTimerId) {
|
||||
clearTimeout(cruiseHideCardTimerId);
|
||||
cruiseHideCardTimerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCruiseConnector() {
|
||||
if (cruiseConnectorEl instanceof SVGSVGElement) {
|
||||
return cruiseConnectorEl;
|
||||
if (!cruiseConnector) {
|
||||
cruiseConnector = new CalloutConnector({ className: "info-card-cruise-link" });
|
||||
}
|
||||
return cruiseConnector;
|
||||
}
|
||||
|
||||
const container = document.getElementById("container");
|
||||
if (!(container instanceof HTMLElement)) return null;
|
||||
function ensureBGPCruiseAdapter() {
|
||||
if (cruiseBGPAdapter) return cruiseBGPAdapter;
|
||||
|
||||
const connector = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
connector.id = "info-card-cruise-link";
|
||||
connector.setAttribute("class", "info-card-cruise-link");
|
||||
connector.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`);
|
||||
connector.setAttribute("preserveAspectRatio", "none");
|
||||
|
||||
const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
||||
const startpoint = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||
const endpoint = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
||||
startpoint.setAttribute("r", "4");
|
||||
endpoint.setAttribute("r", "4");
|
||||
connector.appendChild(startpoint);
|
||||
connector.appendChild(polyline);
|
||||
connector.appendChild(endpoint);
|
||||
|
||||
container.appendChild(connector);
|
||||
cruiseConnectorEl = connector;
|
||||
cruiseConnectorPolyline = polyline;
|
||||
cruiseConnectorStartpoint = startpoint;
|
||||
cruiseConnectorEndpoint = endpoint;
|
||||
connector.addEventListener("animationend", (event) => {
|
||||
if (
|
||||
event.animationName === "cruiseConnectorDraw" &&
|
||||
cruiseConnectorEl?.classList.contains("is-visible")
|
||||
) {
|
||||
if (cruiseConnectorPolyline) {
|
||||
cruiseConnectorPolyline.style.strokeDashoffset = "0";
|
||||
}
|
||||
cruiseConnectorEl.classList.remove("is-animating");
|
||||
}
|
||||
cruiseBGPAdapter = createBGPCruiseAdapter({
|
||||
camera,
|
||||
getMarkers: () => getBGPAnomalyMarkers(),
|
||||
connector: ensureCruiseConnector(),
|
||||
focusView: (options) => focusEarthView(camera, options),
|
||||
setMarkerLocked: (marker) => {
|
||||
setLegendMode("bgp");
|
||||
setBGPMarkerState(marker, "locked");
|
||||
},
|
||||
clearMarkerState: (marker) => setBGPMarkerState(marker, "normal"),
|
||||
showMarkerOverlay: (marker) => {
|
||||
const earth = getEarth();
|
||||
if (!marker || !earth) return;
|
||||
showBGPEventOverlay(marker, earth);
|
||||
},
|
||||
applySatelliteHighlights: (marker) => {
|
||||
if (!marker) return;
|
||||
applyBGPEventSatelliteHighlights(marker);
|
||||
},
|
||||
showMarkerInfo: showBGPInfo,
|
||||
hideInfo: hideInfoCard,
|
||||
isInfoVisible: () =>
|
||||
document.getElementById("info-panel")?.classList.contains("is-visible") === true,
|
||||
getLockedObject: () => lockedObject,
|
||||
refreshMarkers: async () => {
|
||||
const bgpResult = await loadBGPAnomalies(scene, getEarth());
|
||||
updateBGPHud(bgpResult);
|
||||
setLegendItems("bgp", getBGPLegendItems());
|
||||
refreshLegend();
|
||||
},
|
||||
});
|
||||
return connector;
|
||||
|
||||
return cruiseBGPAdapter;
|
||||
}
|
||||
|
||||
function hideCruiseConnector() {
|
||||
const connector = ensureCruiseConnector();
|
||||
if (!connector) return;
|
||||
connector.classList.remove("is-visible");
|
||||
connector.classList.remove("is-animating");
|
||||
cruiseCardPlacement = null;
|
||||
cruisePresentationPhase = "hidden";
|
||||
function isCruisePresentationPinned() {
|
||||
return cruiseSequencer?.isPresentationPinned() === true;
|
||||
}
|
||||
|
||||
function hideCruiseConnectorVisual() {
|
||||
const connector = ensureCruiseConnector();
|
||||
if (!connector) return;
|
||||
connector.classList.remove("is-visible");
|
||||
connector.classList.remove("is-animating");
|
||||
}
|
||||
|
||||
function showCruiseConnectorVisual() {
|
||||
const connector = ensureCruiseConnector();
|
||||
if (!connector) return;
|
||||
connector.classList.add("is-visible");
|
||||
}
|
||||
|
||||
function setCruiseCardPinned(pinned) {
|
||||
cruiseCardPinned = pinned;
|
||||
if (!pinned) {
|
||||
cruisePresentationTimer.stop();
|
||||
hideCruiseConnector();
|
||||
function setCruisePresentationVisible(visible) {
|
||||
if (cruiseSequencer) {
|
||||
cruiseSequencer.setPresentationVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
function interruptCruisePresentation() {
|
||||
++cruiseSequenceToken;
|
||||
clearCruiseTimers();
|
||||
const notifier = cruiseCancelNotifier;
|
||||
cruiseCancelNotifier = null;
|
||||
notifier?.();
|
||||
setCruiseCardPinned(false);
|
||||
}
|
||||
|
||||
function beginCruisePresentationHide() {
|
||||
if (!lockedObject) {
|
||||
hideInfoCard();
|
||||
}
|
||||
cruisePresentationPhase = "hidden";
|
||||
const connector = ensureCruiseConnector();
|
||||
connector?.classList.remove("is-visible");
|
||||
connector?.classList.remove("is-animating");
|
||||
}
|
||||
|
||||
function scheduleCruiseCardHide() {
|
||||
if (cruiseHideCardTimerId) {
|
||||
clearTimeout(cruiseHideCardTimerId);
|
||||
cruiseHideCardTimerId = null;
|
||||
if (!visible) {
|
||||
ensureBGPCruiseAdapter().resetPresentation();
|
||||
}
|
||||
}
|
||||
|
||||
function clearCruiseMarkerHighlight() {
|
||||
if (!cruiseCurrentMarkerId) return;
|
||||
const marker = getBGPAnomalyMarkers().find(
|
||||
(item) => item.userData?.id === cruiseCurrentMarkerId,
|
||||
);
|
||||
if (marker && lockedObject !== marker) {
|
||||
setBGPMarkerState(marker, "normal");
|
||||
}
|
||||
cruiseCurrentMarkerId = null;
|
||||
}
|
||||
|
||||
function getBGPMarkerTimestamp(marker) {
|
||||
const rawValue = marker?.userData?.created_at_raw;
|
||||
const parsedValue = rawValue ? new Date(rawValue).getTime() : 0;
|
||||
return Number.isFinite(parsedValue) ? parsedValue : 0;
|
||||
ensureBGPCruiseAdapter().clearCurrentHighlight();
|
||||
}
|
||||
|
||||
function getCruiseMarkersSorted() {
|
||||
return getBGPAnomalyMarkers()
|
||||
.slice()
|
||||
.sort((a, b) => getBGPMarkerTimestamp(b) - getBGPMarkerTimestamp(a));
|
||||
}
|
||||
|
||||
function getCruiseMarkerScreenCoords(marker) {
|
||||
if (!marker || !camera) return null;
|
||||
scratchBGPWorldPosition.copy(marker.position);
|
||||
marker.parent?.localToWorld(scratchBGPWorldPosition);
|
||||
const projected = scratchBGPWorldPosition.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 getCruiseCardScreenCoords(marker) {
|
||||
const markerCoords = getCruiseMarkerScreenCoords(marker);
|
||||
if (!markerCoords) return null;
|
||||
|
||||
const hudScale =
|
||||
Number.parseFloat(
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--hud-scale"),
|
||||
) || 1;
|
||||
const estimatedCardHeight = Math.min(420 * hudScale, window.innerHeight * 0.7);
|
||||
const estimatedCardWidth = Math.min(300 * hudScale, window.innerWidth - 32);
|
||||
|
||||
const x =
|
||||
window.innerWidth * CRUISE_CONFIG.cardAnchorXRatio - estimatedCardWidth * 0.5;
|
||||
const y =
|
||||
window.innerHeight * CRUISE_CONFIG.cardAnchorYRatio - estimatedCardHeight * 0.5;
|
||||
const margin = 12;
|
||||
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),
|
||||
);
|
||||
const anchorY = clampedY + Math.max(18 * hudScale, estimatedCardHeight * 0.18);
|
||||
|
||||
return {
|
||||
x: clampedX,
|
||||
y: clampedY,
|
||||
width: estimatedCardWidth,
|
||||
height: estimatedCardHeight,
|
||||
anchorX: clampedX - CRUISE_CONFIG.linkPanelGapPx,
|
||||
anchorY,
|
||||
};
|
||||
}
|
||||
|
||||
function showCruiseEventCard(marker) {
|
||||
const coords = cruiseCardPlacement || getCruiseCardScreenCoords(marker);
|
||||
if (!coords) return;
|
||||
showBGPInfo(marker, {
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
absolute: true,
|
||||
});
|
||||
}
|
||||
|
||||
function isCruiseInfoCardVisible() {
|
||||
return document.getElementById("info-panel")?.classList.contains("is-visible") === true;
|
||||
}
|
||||
|
||||
function computeCruiseConnectorPoints(marker) {
|
||||
const markerCoords = getCruiseMarkerScreenCoords(marker);
|
||||
if (!markerCoords) return null;
|
||||
|
||||
const targetCardCoords = cruiseCardPlacement || getCruiseCardScreenCoords(marker);
|
||||
if (!targetCardCoords) return null;
|
||||
|
||||
const panelAnchorX = targetCardCoords.anchorX;
|
||||
const panelAnchorY = targetCardCoords.anchorY;
|
||||
const horizontalDirection = markerCoords.x <= panelAnchorX ? 1 : -1;
|
||||
const startX = markerCoords.x + horizontalDirection * CRUISE_CONFIG.linkMarkerGapPx;
|
||||
const startY = markerCoords.y;
|
||||
const elbowX =
|
||||
panelAnchorX -
|
||||
horizontalDirection * (CRUISE_CONFIG.linkElbowOffsetPx + CRUISE_CONFIG.linkPanelGapPx);
|
||||
const elbowY = Math.min(startY, panelAnchorY) + CRUISE_CONFIG.linkElbowDropPx;
|
||||
|
||||
if (Math.abs(panelAnchorX - startX) < 8 && Math.abs(panelAnchorY - startY) < 8) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { startX, startY, elbowX, elbowY, panelAnchorX, panelAnchorY };
|
||||
}
|
||||
|
||||
function applyCruiseConnectorPoints(pts) {
|
||||
const polyline = cruiseConnectorPolyline;
|
||||
const startpoint = cruiseConnectorStartpoint;
|
||||
const endpoint = cruiseConnectorEndpoint;
|
||||
if (!polyline || !startpoint || !endpoint) return 0;
|
||||
|
||||
polyline.setAttribute(
|
||||
"points",
|
||||
`${pts.startX.toFixed(2)},${pts.startY.toFixed(2)} ` +
|
||||
`${pts.elbowX.toFixed(2)},${pts.elbowY.toFixed(2)} ` +
|
||||
`${pts.panelAnchorX.toFixed(2)},${pts.panelAnchorY.toFixed(2)}`,
|
||||
);
|
||||
startpoint.setAttribute("cx", pts.startX.toFixed(2));
|
||||
startpoint.setAttribute("cy", pts.startY.toFixed(2));
|
||||
endpoint.setAttribute("cx", pts.panelAnchorX.toFixed(2));
|
||||
endpoint.setAttribute("cy", pts.panelAnchorY.toFixed(2));
|
||||
|
||||
const totalLength =
|
||||
typeof polyline.getTotalLength === "function" ? polyline.getTotalLength() : 0;
|
||||
return totalLength;
|
||||
}
|
||||
|
||||
function updateCruiseConnector() {
|
||||
const connector = ensureCruiseConnector();
|
||||
if (
|
||||
!connector ||
|
||||
!cruiseConnectorPolyline ||
|
||||
!cruiseConnectorStartpoint ||
|
||||
!cruiseConnectorEndpoint ||
|
||||
!cruiseCardPinned ||
|
||||
cruisePresentationPhase === "hidden" ||
|
||||
!cruiseCurrentMarkerId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const marker = getBGPAnomalyMarkers().find(
|
||||
(item) => item.userData?.id === cruiseCurrentMarkerId,
|
||||
);
|
||||
if (!marker) return false;
|
||||
|
||||
const pts = computeCruiseConnectorPoints(marker);
|
||||
if (!pts) return false;
|
||||
|
||||
connector.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`);
|
||||
const totalLength = applyCruiseConnectorPoints(pts);
|
||||
const polyline = cruiseConnectorPolyline;
|
||||
|
||||
polyline.style.strokeDasharray = totalLength > 0 ? `${totalLength}` : "";
|
||||
polyline.style.strokeDashoffset =
|
||||
totalLength > 0 ? `${cruiseConnectorNeedsAnimation ? totalLength : 0}` : "";
|
||||
connector.style.setProperty(
|
||||
"--connector-length",
|
||||
totalLength > 0 ? `${totalLength}` : "0px",
|
||||
);
|
||||
connector.classList.add("is-visible");
|
||||
|
||||
if (cruiseConnectorNeedsAnimation && totalLength > 0) {
|
||||
connector.classList.remove("is-animating");
|
||||
void connector.getBoundingClientRect();
|
||||
polyline.style.strokeDashoffset = `${totalLength}`;
|
||||
connector.classList.add("is-animating");
|
||||
cruiseConnectorNeedsAnimation = false;
|
||||
}
|
||||
|
||||
return totalLength > 0;
|
||||
return ensureBGPCruiseAdapter().getSortedMarkers();
|
||||
}
|
||||
|
||||
function repositionCruiseConnector() {
|
||||
if (!cruiseCardPinned || cruisePresentationPhase === "hidden" || !cruiseCurrentMarkerId) return;
|
||||
const connector = cruiseConnectorEl;
|
||||
if (!connector || !connector.classList.contains("is-visible")) return;
|
||||
|
||||
const marker = getBGPAnomalyMarkers().find(
|
||||
(item) => item.userData?.id === cruiseCurrentMarkerId,
|
||||
);
|
||||
if (!marker) return;
|
||||
|
||||
const pts = computeCruiseConnectorPoints(marker);
|
||||
if (!pts) return;
|
||||
|
||||
connector.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`);
|
||||
applyCruiseConnectorPoints(pts);
|
||||
}
|
||||
|
||||
function syncCruiseKnownEventIds() {
|
||||
cruiseKnownEventIds = new Set(
|
||||
getBGPAnomalyMarkers()
|
||||
.map((marker) => marker.userData?.id)
|
||||
.filter(Boolean),
|
||||
);
|
||||
if (!isCruisePresentationPinned()) return;
|
||||
const marker = cruiseSequencer?.getCurrentItem() ?? null;
|
||||
ensureBGPCruiseAdapter().repositionConnector(marker);
|
||||
}
|
||||
|
||||
function isCruiseModeActive() {
|
||||
return getRotationMode() === ROTATION_MODE.CRUISE;
|
||||
}
|
||||
|
||||
function stopCruiseMode({ preserveCard = false } = {}) {
|
||||
clearCruiseTimers();
|
||||
clearCruiseMarkerHighlight();
|
||||
clearBGPSelection();
|
||||
cruiseCurrentIndex = -1;
|
||||
cruiseQueuedMarkerIds = [];
|
||||
cruiseAdvanceQueued = false;
|
||||
cruiseAdvanceInterrupt = false;
|
||||
if (!preserveCard) {
|
||||
setCruiseCardPinned(false);
|
||||
}
|
||||
if (!preserveCard && !lockedObject) {
|
||||
hideInfoCard();
|
||||
}
|
||||
}
|
||||
function ensureCruiseSequencer() {
|
||||
if (cruiseSequencer) return cruiseSequencer;
|
||||
|
||||
async function focusCruiseMarker(marker, { interrupt = false } = {}) {
|
||||
const earth = getEarth();
|
||||
if (!marker || !earth || !isCruiseModeActive() || !getAutoRotate()) return;
|
||||
const sequenceToken = ++cruiseSequenceToken;
|
||||
const abortPresentation = () => {
|
||||
hideInfoCard();
|
||||
setCruiseCardPinned(false);
|
||||
};
|
||||
const shouldAbortSequence = () =>
|
||||
sequenceToken !== cruiseSequenceToken ||
|
||||
!isCruiseModeActive() ||
|
||||
!getAutoRotate();
|
||||
|
||||
clearCruiseTimers();
|
||||
clearCruiseMarkerHighlight();
|
||||
clearLockedObject();
|
||||
hideInfoCard();
|
||||
setCruiseCardPinned(false);
|
||||
|
||||
cruiseCurrentMarkerId = marker.userData?.id || null;
|
||||
cruiseConnectorNeedsAnimation = true;
|
||||
cruiseCardPlacement = getCruiseCardScreenCoords(marker);
|
||||
cruiseCurrentIndex = getCruiseMarkersSorted().findIndex(
|
||||
(item) => item.userData?.id === cruiseCurrentMarkerId,
|
||||
);
|
||||
setLegendMode("bgp");
|
||||
setBGPMarkerState(marker, "locked");
|
||||
showBGPEventOverlay(marker, earth);
|
||||
applyBGPEventSatelliteHighlights(marker);
|
||||
|
||||
await focusEarthView(camera, {
|
||||
lat: marker.userData?.latitude ?? 0,
|
||||
lon: marker.userData?.longitude ?? 0,
|
||||
rotLon: (marker.userData?.longitude ?? 0) - 270,
|
||||
zoom: 1.0,
|
||||
duration: interrupt ? Math.round(CRUISE_CONFIG.focusDurationMs * 0.78) : CRUISE_CONFIG.focusDurationMs,
|
||||
suppressStatus: true,
|
||||
cruiseSequencer = new CruiseSequencer({
|
||||
isActive: () => isCruiseModeActive() && getAutoRotate(),
|
||||
getItems: () => getCruiseMarkersSorted(),
|
||||
getItemId: (marker) => marker?.userData?.id || null,
|
||||
dwellMs: CRUISE_CONFIG.dwellMs,
|
||||
transitionGapMs: CRUISE_TRANSITION_GAP_MS,
|
||||
clearCurrent: () => {
|
||||
clearCruiseMarkerHighlight();
|
||||
clearLockedObject();
|
||||
hideInfoCard();
|
||||
setCruisePresentationVisible(false);
|
||||
},
|
||||
onStop: ({ preservePresentation }) => {
|
||||
clearBGPSelection();
|
||||
if (!preservePresentation && !lockedObject) {
|
||||
hideInfoCard();
|
||||
}
|
||||
},
|
||||
focusItem: async (marker, { interrupt }) =>
|
||||
ensureBGPCruiseAdapter().focusMarker(marker, { interrupt }),
|
||||
presentItem: async (marker, { context }) => {
|
||||
setCruisePresentationVisible(true);
|
||||
const presented = await ensureBGPCruiseAdapter().presentMarker(marker, {
|
||||
context,
|
||||
});
|
||||
if (!presented) {
|
||||
setCruisePresentationVisible(false);
|
||||
}
|
||||
return presented;
|
||||
},
|
||||
hideItem: async (_marker, { context }) => {
|
||||
await ensureBGPCruiseAdapter().hidePresentation({ context });
|
||||
setCruisePresentationVisible(false);
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
shouldAbortSequence()
|
||||
) {
|
||||
abortPresentation();
|
||||
return;
|
||||
}
|
||||
|
||||
setCruiseCardPinned(true);
|
||||
cruisePresentationPhase = "connector";
|
||||
showCruiseConnectorVisual();
|
||||
|
||||
const connectorReady = await waitForCruiseConnectorReady(sequenceToken);
|
||||
|
||||
if (
|
||||
!connectorReady ||
|
||||
shouldAbortSequence()
|
||||
) {
|
||||
abortPresentation();
|
||||
return;
|
||||
}
|
||||
|
||||
const connectorDelayCompleted = await waitForCruiseDelay(
|
||||
CRUISE_CONNECTOR_DRAW_MS,
|
||||
sequenceToken,
|
||||
);
|
||||
|
||||
if (
|
||||
!connectorDelayCompleted ||
|
||||
shouldAbortSequence()
|
||||
) {
|
||||
abortPresentation();
|
||||
return;
|
||||
}
|
||||
|
||||
cruisePresentationPhase = "card";
|
||||
showCruiseEventCard(marker);
|
||||
await nextAnimationFrame();
|
||||
if (!isCruiseInfoCardVisible()) {
|
||||
showCruiseEventCard(marker);
|
||||
await nextAnimationFrame();
|
||||
}
|
||||
if (!isCruiseInfoCardVisible() || shouldAbortSequence()) {
|
||||
abortPresentation();
|
||||
return;
|
||||
}
|
||||
cruisePresentationTimer.start(CRUISE_CONFIG.dwellMs);
|
||||
|
||||
const dwellDelayCompleted = await waitForCruiseDelay(
|
||||
CRUISE_CONFIG.dwellMs,
|
||||
sequenceToken,
|
||||
);
|
||||
if (
|
||||
!dwellDelayCompleted ||
|
||||
shouldAbortSequence()
|
||||
) {
|
||||
abortPresentation();
|
||||
return;
|
||||
}
|
||||
|
||||
beginCruisePresentationHide();
|
||||
const hideDelayCompleted = await waitForCruiseDelay(
|
||||
CRUISE_PRESENTATION_HIDE_MS,
|
||||
sequenceToken,
|
||||
{ useHideTimer: true },
|
||||
);
|
||||
if (
|
||||
!hideDelayCompleted ||
|
||||
shouldAbortSequence()
|
||||
) {
|
||||
abortPresentation();
|
||||
return;
|
||||
}
|
||||
|
||||
setCruiseCardPinned(false);
|
||||
const transitionGapCompleted = await waitForCruiseDelay(
|
||||
CRUISE_TRANSITION_GAP_MS,
|
||||
sequenceToken,
|
||||
);
|
||||
if (
|
||||
!transitionGapCompleted ||
|
||||
shouldAbortSequence()
|
||||
) {
|
||||
abortPresentation();
|
||||
return;
|
||||
}
|
||||
|
||||
void advanceCruiseEvent();
|
||||
return cruiseSequencer;
|
||||
}
|
||||
|
||||
async function performCruiseAdvance({ interrupt = false } = {}) {
|
||||
if (!isCruiseModeActive() || !getAutoRotate()) return;
|
||||
function interruptCruisePresentation({ resetLoop = false } = {}) {
|
||||
ensureCruiseSequencer().interruptPresentation({ resetLoop });
|
||||
setCruisePresentationVisible(false);
|
||||
}
|
||||
|
||||
const markers = getCruiseMarkersSorted();
|
||||
if (markers.length === 0) return;
|
||||
|
||||
let targetMarker = null;
|
||||
|
||||
while (cruiseQueuedMarkerIds.length > 0 && !targetMarker) {
|
||||
const queuedId = cruiseQueuedMarkerIds.shift();
|
||||
targetMarker = markers.find((marker) => marker.userData?.id === queuedId) || null;
|
||||
function stopCruiseMode({ preserveCard = false } = {}) {
|
||||
ensureCruiseSequencer().stop({ preservePresentation: preserveCard });
|
||||
if (!preserveCard) {
|
||||
setCruisePresentationVisible(false);
|
||||
}
|
||||
|
||||
if (!targetMarker) {
|
||||
const nextIndex =
|
||||
cruiseCurrentIndex >= 0
|
||||
? (cruiseCurrentIndex + 1) % markers.length
|
||||
: 0;
|
||||
targetMarker = markers[nextIndex] || markers[0];
|
||||
}
|
||||
|
||||
await focusCruiseMarker(targetMarker, { interrupt });
|
||||
}
|
||||
|
||||
async function advanceCruiseEvent({ interrupt = false } = {}) {
|
||||
if (!isCruiseModeActive() || !getAutoRotate()) return;
|
||||
|
||||
cruiseAdvanceQueued = true;
|
||||
cruiseAdvanceInterrupt = cruiseAdvanceInterrupt || interrupt;
|
||||
|
||||
if (cruiseAdvanceInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
cruiseAdvanceInFlight = true;
|
||||
try {
|
||||
while (cruiseAdvanceQueued && isCruiseModeActive() && getAutoRotate()) {
|
||||
const nextInterrupt = cruiseAdvanceInterrupt;
|
||||
cruiseAdvanceQueued = false;
|
||||
cruiseAdvanceInterrupt = false;
|
||||
await performCruiseAdvance({ interrupt: nextInterrupt });
|
||||
}
|
||||
} finally {
|
||||
cruiseAdvanceInFlight = false;
|
||||
}
|
||||
await ensureCruiseSequencer().advance({ interrupt });
|
||||
}
|
||||
|
||||
async function pollCruiseEventsIfNeeded() {
|
||||
if (!isCruiseModeActive() || !getAutoRotate() || !getShowBGP()) return;
|
||||
|
||||
try {
|
||||
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 = selectedFeatures
|
||||
.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);
|
||||
|
||||
const newIds = nextIds.filter((id) => !cruiseKnownEventIds.has(id));
|
||||
const newIds = await ensureBGPCruiseAdapter().pollForNewMarkerIds();
|
||||
if (newIds.length === 0) return;
|
||||
|
||||
const bgpResult = await loadBGPAnomalies(scene, getEarth());
|
||||
updateBGPHud(bgpResult);
|
||||
setLegendItems("bgp", getBGPLegendItems());
|
||||
refreshLegend();
|
||||
syncCruiseKnownEventIds();
|
||||
cruiseQueuedMarkerIds = Array.from(
|
||||
new Set([...newIds, ...cruiseQueuedMarkerIds]),
|
||||
);
|
||||
if (
|
||||
cruisePresentationPhase === "hidden" &&
|
||||
!cruiseCardPinned &&
|
||||
!cruisePresentationTimer.isActive()
|
||||
) {
|
||||
ensureCruiseSequencer().enqueue(newIds);
|
||||
if (!ensureCruiseSequencer().isBusy()) {
|
||||
await advanceCruiseEvent({ interrupt: true });
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1301,14 +791,11 @@ async function pollCruiseEventsIfNeeded() {
|
||||
|
||||
function ensureCruisePolling() {
|
||||
if (cruisePollTimerId) return;
|
||||
cruisePollTimerId = window.setInterval(
|
||||
() => {
|
||||
pollCruiseEventsIfNeeded().catch((error) => {
|
||||
console.warn("巡航轮询失败:", error);
|
||||
});
|
||||
},
|
||||
CRUISE_CONFIG.pollIntervalMs,
|
||||
);
|
||||
cruisePollTimerId = window.setInterval(() => {
|
||||
pollCruiseEventsIfNeeded().catch((error) => {
|
||||
console.warn("巡航轮询失败:", error);
|
||||
});
|
||||
}, CRUISE_CONFIG.pollIntervalMs);
|
||||
cleanupFns.push(() => {
|
||||
if (cruisePollTimerId) {
|
||||
clearInterval(cruisePollTimerId);
|
||||
@@ -1330,7 +817,7 @@ function handleRotationModeChange(event) {
|
||||
}
|
||||
|
||||
ensureCruisePolling();
|
||||
syncCruiseKnownEventIds();
|
||||
ensureBGPCruiseAdapter().syncKnownEventIds();
|
||||
|
||||
if (!detailActive) {
|
||||
stopCruiseMode({ preserveCard: true });
|
||||
@@ -1456,7 +943,7 @@ function applyCableVisualState() {
|
||||
(lockedObjectType === "cable" && lockedObject) ||
|
||||
(lockedObjectType === "satellite" && lockedSatellite) ||
|
||||
(lockedObjectType === "bgp" && lockedObject) ||
|
||||
(isCruiseModeActive() && cruiseCardPinned);
|
||||
(isCruiseModeActive() && isCruisePresentationPinned());
|
||||
|
||||
switch (state) {
|
||||
case CABLE_STATE.LOCKED:
|
||||
@@ -1960,7 +1447,7 @@ async function loadData() {
|
||||
if (loadToken === currentLoadToken) {
|
||||
toggleBGP(true);
|
||||
updateBGPHud(bgpResult);
|
||||
syncCruiseKnownEventIds();
|
||||
ensureBGPCruiseAdapter().syncKnownEventIds();
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push({ label: "BGP态势", reason: err });
|
||||
@@ -2162,7 +1649,7 @@ function onMouseMove(event) {
|
||||
applyBGPHoverState(lockedObject);
|
||||
} else if (lockedObjectType === "bgp_collector" && lockedObject) {
|
||||
applyBGPHoverState(lockedObject);
|
||||
} else if (!lockedObject && !lockedSatellite && !cruiseCardPinned) {
|
||||
} else if (!lockedObject && !lockedSatellite && !isCruisePresentationPinned()) {
|
||||
hideInfoCard();
|
||||
}
|
||||
hideTooltip();
|
||||
@@ -2288,7 +1775,7 @@ function onMouseMove(event) {
|
||||
applyBGPHoverState(lockedObject);
|
||||
} else if (lockedObjectType === "bgp_collector" && lockedObject) {
|
||||
applyBGPHoverState(lockedObject);
|
||||
} else if (!lockedObjectType && !cruiseCardPinned) {
|
||||
} else if (!lockedObjectType && !isCruisePresentationPinned()) {
|
||||
resetTransientBGPStates();
|
||||
hideInfoCard();
|
||||
}
|
||||
@@ -2512,7 +1999,15 @@ function onClick(event) {
|
||||
}
|
||||
|
||||
if (!isLongDrag) {
|
||||
interruptCruisePresentation();
|
||||
if (isCruiseModeActive()) {
|
||||
interruptCruisePresentation({ resetLoop: true });
|
||||
clearLockedObject();
|
||||
hideInfoCard();
|
||||
setAutoRotate(true);
|
||||
return;
|
||||
}
|
||||
|
||||
interruptCruisePresentation({ resetLoop: true });
|
||||
clearLockedObject();
|
||||
hideInfoCard();
|
||||
setAutoRotate(true);
|
||||
@@ -2570,9 +2065,10 @@ function animate() {
|
||||
}
|
||||
|
||||
applyCableVisualState();
|
||||
const activeCruiseMarker = (isCruiseModeActive() && cruiseCardPinned && cruiseCurrentMarkerId)
|
||||
? getBGPAnomalyMarkers().find((m) => m.userData?.id === cruiseCurrentMarkerId) ?? null
|
||||
: null;
|
||||
const activeCruiseMarker =
|
||||
isCruiseModeActive() && isCruisePresentationPinned()
|
||||
? cruiseSequencer?.getCurrentItem() ?? null
|
||||
: null;
|
||||
updateBGPVisualState(lockedObjectType, lockedObject, camera, activeCruiseMarker);
|
||||
|
||||
if (lockedObjectType === "cable" && lockedObject) {
|
||||
|
||||
Reference in New Issue
Block a user