187 lines
6.2 KiB
JavaScript
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");
|
|
},
|
|
};
|
|
}
|