Files
gods-eye-view/src/layers/cctv/hover.js
T

128 lines
4.6 KiB
JavaScript

import {
CCTV_CARD_FETCH_BURST_LIMIT,
frameFetchDue,
} from '../../data/cctvCards.js';
import { staticFrameRefreshMs } from '../../data/cctvLod.js';
import { HOVER_PICK_THROTTLE_MS, HOVER_RELEASE_MS } from './policy.js';
export function createHover({ state: layerState, services, parts, source }) {
/**
* Throttled MOUSE_MOVE hover pass (owner round 2, item B): pointing at a
* camera icon that has no card summons its card immediately. This is
* EVENT-DRIVEN picking on a user gesture, not steady-state work — the
* ≥120 ms throttle caps it at ~8 scene.pick calls/s while the pointer is
* actually moving (a still pointer costs nothing), so it can never approach
* per-frame cost. Skipped while the camera is in motion, while ADJUST mode
* owns the pointer (gizmo drags), and while the layer is disabled.
* @param {Cesium.Cartesian2} position - Pointer position (CSS px).
*/
function handleHoverMove(position) {
if (
!layerState._enabled ||
layerState._cameraMoving ||
layerState._calibrationMode ||
!position
)
return;
if (!layerState._viewer || layerState._viewer.isDestroyed()) return;
const now = Date.now();
if (now - layerState._hoverLastPickAt < HOVER_PICK_THROTTLE_MS) return;
layerState._hoverLastPickAt = now;
let picked = null;
try {
picked = layerState._viewer.scene.pick(position);
} catch {
picked = null;
}
const cameraId = parts.selection.extractPickedCameraId(picked);
if (cameraId && cameraId === layerState._hoverCardId) {
// Still on the hovered camera — keep the card alive.
cancelHoverRelease();
return;
}
const record = cameraId ? layerState._recordById.get(cameraId) : null;
// Hovering the active camera or a camera that already has a card is a
// no-op; video feeds stay icon-only until activated (ambient tier is
// stills-only — same rule as the LOD selection).
const eligible =
!!record &&
cameraId !== layerState._activeCameraId &&
!layerState._cardIds.has(cameraId) &&
!parts.model.isVideoFeedType(
parts.model.normalizeFeedType(record.camera.feedType),
);
if (eligible) {
cancelHoverRelease();
layerState._hoverCardId = cameraId;
// Immediacy: the pinned entry publishes now — chrome may paint before
// the first frame arrives (documented exception in cctvCards.js).
parts.cards.pushAmbientCardEntries();
hoverFetchCardFrame(record);
} else if (layerState._hoverCardId) {
// Pointer left the hovered icon: linger ~1 s, then release.
scheduleHoverRelease();
}
}
/** Cancels a pending hover-card release. */
function cancelHoverRelease() {
if (layerState._hoverReleaseTimer) {
clearTimeout(layerState._hoverReleaseTimer);
layerState._hoverReleaseTimer = 0;
}
}
/**
* Schedules the hover card's release ~1 s after unhover. On release the pin
* drops; the card stays only if the LOD selection has adopted the camera
* (it is then a normal budgeted entry in `_cardIds`).
*/
function scheduleHoverRelease() {
if (layerState._hoverReleaseTimer) return;
layerState._hoverReleaseTimer = setTimeout(() => {
layerState._hoverReleaseTimer = 0;
layerState._hoverCardId = null;
parts.cards.pushAmbientCardEntries();
}, HOVER_RELEASE_MS);
}
/** Clears the hover-card state (teardown / activation of the hovered camera). */
function clearHoverCard() {
cancelHoverRelease();
layerState._hoverCardId = null;
layerState._hoverLastPickAt = 0;
}
/**
* Fast-tracked frame fetch for the hover-summoned card: launches
* immediately, bypassing the pacer's launch-spacing gate (a single
* user-gesture fetch is fine even during the geometry drain), but still
* respecting the per-camera failure backoff / freshness check
* (frameFetchDue), the no-double-fetch pending set, and the burst in-flight
* cap of 4.
* @param {Object} record - The hovered camera's record.
*/
function hoverFetchCardFrame(record) {
const cameraId = record.camera.id;
if (layerState._cardFetchPendingIds.has(cameraId)) return;
if (layerState._cardFetchInFlightCount >= CCTV_CARD_FETCH_BURST_LIMIT)
return;
const slot = parts.cards.ensureCardFrameSlot(cameraId);
const refreshMs = staticFrameRefreshMs(record.camera);
if (!frameFetchDue(slot, refreshMs, Date.now())) return;
parts.cards.fetchCardFrame(record, slot, refreshMs, { userGesture: true });
}
return {
handleHoverMove,
cancelHoverRelease,
scheduleHoverRelease,
clearHoverCard,
hoverFetchCardFrame,
};
}