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"); }, }; }