import * as Cesium from 'cesium'; import { staticFrameRefreshMs } from '../../data/cctvLod.js'; import { frameFetchDue, cardFetchPolicy } from '../../data/cctvCards.js'; import { DEFAULT_CAMERA_CALIBRATION } from './policy.js'; export function createModel({ state: layerState, services, parts, source }) { const { focusPassIsNeeded, getFocusTarget } = services.focus; /** * Converts degrees to radians. * @param {number} deg * @returns {number} */ function toRad(deg) { return Cesium.Math.toRadians(deg); } /** * Normalizes a heading angle to the [0, 360) range. * @param {number} deg * @returns {number} */ function normalizeHeading(deg) { let v = deg % 360; if (v < 0) v += 360; return v; } /** * Clamps a value to [min, max]. * @param {number} value * @param {number} min * @param {number} max * @returns {number} */ function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } /** * Normalizes an angle to the (-180, 180] range. * @param {number} deg * @returns {number} */ function normalizeSignedAngle(deg) { let value = deg % 360; if (value > 180) value -= 360; if (value <= -180) value += 360; return value; } /** * Returns the absolute angular difference between two headings in degrees. * @param {number} aDeg * @param {number} bDeg * @returns {number} Value in [0, 180]. */ function angularDeltaAbs(aDeg, bDeg) { return Math.abs(normalizeSignedAngle(aDeg - bDeg)); } /** * Normalizes a raw feed-type string to a canonical type (image, mjpeg, mp4, hls, webm). * @param {string|*} value - Raw feed type from source config. * @returns {string} Canonical feed type. */ function normalizeFeedType(value) { const raw = String(value || '') .trim() .toLowerCase(); if (!raw) return 'image'; if (raw === 'mjpg') return 'mjpeg'; if (raw === 'jpeg') return 'image'; if (raw === 'jpg') return 'image'; if (raw === 'video') return 'mp4'; if (raw === 'stream') return 'hls'; if (raw === 'png') return 'image'; if (raw === 'gif') return 'image'; return raw; } /** * Returns true if the feed type requires a