Files
planet/frontend/public/earth/js/motion-cruise-adapter.js
2026-05-10 22:06:01 +08:00

187 lines
6.2 KiB
JavaScript

import { CONNECTOR_CONFIG, CRUISE_CONFIG } from "./constants.js";
const MOTION_CONNECTOR_READY_TIMEOUT_MS = 1200;
const MOTION_CONNECTOR_DRAW_MS = 420;
const MOTION_PRESENTATION_HIDE_MS = 220;
const MOTION_CARD_ESTIMATED_WIDTH_PX = 300;
const MOTION_CARD_ESTIMATED_HEIGHT_PX = 420;
const MOTION_CARD_SCREEN_MARGIN_PX = 12;
const MOTION_MOBILE_POPUP_ESTIMATED_WIDTH_PX = 220;
const MOTION_MOBILE_POPUP_ESTIMATED_HEIGHT_PX = 68;
const MOTION_MOBILE_POPUP_TOP_RATIO = 0.17;
const MOTION_MOBILE_POPUP_MARGIN_PX = 14;
function getMotionCardScreenCoords() {
if (document.body.classList.contains("layout-mode-mobile")) {
const estimatedWidth = Math.min(
MOTION_MOBILE_POPUP_ESTIMATED_WIDTH_PX,
window.innerWidth - MOTION_MOBILE_POPUP_MARGIN_PX * 2,
);
const safeBottom =
Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--safe-bottom")) || 0;
const y = Math.max(
MOTION_MOBILE_POPUP_MARGIN_PX,
Math.min(
window.innerHeight * MOTION_MOBILE_POPUP_TOP_RATIO,
window.innerHeight -
safeBottom -
MOTION_MOBILE_POPUP_ESTIMATED_HEIGHT_PX -
MOTION_MOBILE_POPUP_MARGIN_PX,
),
);
return {
x: Math.max(MOTION_MOBILE_POPUP_MARGIN_PX, window.innerWidth - estimatedWidth - MOTION_MOBILE_POPUP_MARGIN_PX),
y,
width: estimatedWidth,
height: MOTION_MOBILE_POPUP_ESTIMATED_HEIGHT_PX,
dockSide: "left",
};
}
const hudScale =
Number.parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--hud-scale")) || 1;
const width = Math.min(MOTION_CARD_ESTIMATED_WIDTH_PX * hudScale, window.innerWidth - 32);
const height = Math.min(MOTION_CARD_ESTIMATED_HEIGHT_PX * hudScale, window.innerHeight * 0.7);
const x = window.innerWidth * CRUISE_CONFIG.cardAnchorXRatio - width * 0.5;
const y = window.innerHeight * CRUISE_CONFIG.cardAnchorYRatio - height * 0.5;
return {
x: Math.min(Math.max(MOTION_CARD_SCREEN_MARGIN_PX, x), window.innerWidth - width - MOTION_CARD_SCREEN_MARGIN_PX),
y: Math.min(Math.max(MOTION_CARD_SCREEN_MARGIN_PX, y), window.innerHeight - height - MOTION_CARD_SCREEN_MARGIN_PX),
width,
height,
};
}
function getMotionInfoOptions({ reveal = true } = {}) {
const placement = getMotionCardScreenCoords();
return {
x: placement.x,
y: placement.y,
absolute: true,
reveal,
anchorStable: true,
dockSide: placement.dockSide,
};
}
export function createMotionCruiseAdapter({
presentationController,
focusView,
getItems = () => [],
getItemId = (item) => item?.id || null,
resolveLatestItem = (item) => item,
getCandidateAnchor,
getCandidateFocusCoords,
showCandidateInfo,
hideInfo,
clearCandidateVisual,
}) {
let currentItemId = null;
function getCurrentItem() {
if (!currentItemId) return null;
return getItems().find((item) => getItemId(item) === currentItemId) || null;
}
function getCandidate(item) {
return item?.payload || item || null;
}
function getCandidateId(candidate) {
return (
candidate?.id ||
candidate?.object?.userData?.id ||
candidate?.object?.userData?.collector ||
candidate?.object?.userData?.mmsi ||
candidate?.object?.userData?.name ||
(Number.isInteger(candidate?.index) ? `satellite:${candidate.index}` : null)
);
}
function getVisibleCardTarget() {
const mobileTarget = document.getElementById("earth-mobile-popup");
const visibleMobileTarget =
mobileTarget instanceof HTMLElement && !mobileTarget.hasAttribute("hidden")
? mobileTarget
: null;
if (visibleMobileTarget) return visibleMobileTarget;
const infoPanel = document.getElementById("info-panel");
return infoPanel instanceof HTMLElement && !infoPanel.hasAttribute("hidden")
? infoPanel
: null;
}
return {
getSortedItems() {
return getItems();
},
getCurrentItem,
clearCurrentHighlight() {
const candidate = getCandidate(getCurrentItem());
currentItemId = null;
clearCandidateVisual?.(candidate);
},
async focusItem(item, { interrupt = false } = {}) {
const candidate = getCandidate(item);
currentItemId = getItemId(item) || getCandidateId(candidate);
const coords = getCandidateFocusCoords?.(candidate);
if (!coords) return;
await focusView({
lat: coords.lat,
lon: coords.lon,
rotLon: coords.lon - 270,
zoom: Math.max(coords.zoom || 1.08, 1.08),
duration: interrupt
? Math.round(CRUISE_CONFIG.focusDurationMs * 0.58)
: CRUISE_CONFIG.focusDurationMs,
suppressStatus: true,
});
},
async presentItem(item, { context }) {
const latestItem = resolveLatestItem(item) || item;
const candidate = getCandidate(latestItem);
if (!candidate) return false;
return presentationController.present(
{
id: getItemId(latestItem) || getCandidateId(candidate),
owner: "motion",
card: {
render: ({ reveal = true } = {}) =>
showCandidateInfo(candidate, getMotionInfoOptions({ reveal })),
hide: () => hideInfo?.(),
},
connector: {
enabled: true,
sourceProvider: () => getCandidateAnchor?.(getCandidate(resolveLatestItem(latestItem) || latestItem)),
targetProvider: getVisibleCardTarget,
options: {
routingMode: "adaptive",
sourceGapPx: 0,
targetGapPx: CONNECTOR_CONFIG.panelGapPx,
obstacleClearancePx: CONNECTOR_CONFIG.obstacleClearancePx,
},
readyTimeoutMs: MOTION_CONNECTOR_READY_TIMEOUT_MS,
drawMs: MOTION_CONNECTOR_DRAW_MS,
},
lifetime: { mode: "persistent" },
},
{ context },
);
},
async hidePresentation({ context }) {
presentationController.dismiss("sequenced_hide");
const hidden = context?.wait
? await context.wait(MOTION_PRESENTATION_HIDE_MS, { secondary: true })
: true;
return hidden;
},
repositionConnector() {
presentationController.update();
},
resetPresentation() {
presentationController.dismiss("owner_stop");
},
};
}