mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
128 lines
4.6 KiB
JavaScript
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,
|
|
};
|
|
}
|