mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Three-model review (astra, sol, fable), round 3: 17 of 18 findings
applied (some in own wording, one in part); "Missões espac." rejected.
- pt: layer cards «{label}: camada ativada/desativada», pointer and
Directions toasts name «A ferramenta "{owner}"», counters after a
colon where the count can be 1, traffic «sem dados de fluxo» and
«Fluxo: {percent}% cob», «limite de requisições atingido», FECHAR,
ANT., PREDEF. VISUAIS, quoted "Nível da rua", infinitive tooltips,
and more (see docs/translations/pt.md).
- CCTV summary line localizes its CAL, SRC and CONTEXT values through
existing catalog keys (all locales); codes stay underneath.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
318 lines
12 KiB
JavaScript
318 lines
12 KiB
JavaScript
import { CCTV_AMBIENT_CARD_MAX } from '../../data/cctvLod.js';
|
|
import { msg, t } from '../../i18n/index.js';
|
|
import { ACTIVE_FRAME_REFRESH_MS, IDLE_FRAME_REFRESH_MS } from './policy.js';
|
|
import { headingHudToken, isHeadingEstimated } from './headingConfidence.js';
|
|
import {
|
|
specSummary,
|
|
specFovCapabilityToken,
|
|
isDatasheetFov,
|
|
} from './modelSpecs.js';
|
|
|
|
// The summary line names the CAL badge, the source kind and the view band in
|
|
// the page language; the codes underneath stay English.
|
|
const CAL_BADGE_LABELS = Object.freeze({
|
|
calibrated: msg('CALIBRATED'),
|
|
curated: msg('CURATED'),
|
|
'raw-prior': msg('RAW PRIOR'),
|
|
});
|
|
|
|
// The same source-kind words as the CCTV panel (src/ui/cctvFrames.js).
|
|
const SOURCE_KIND_LABELS = Object.freeze({
|
|
snapshot: msg('SNAPSHOT'),
|
|
live: msg('LIVE'),
|
|
fallback: msg('FALLBACK'),
|
|
upstream: msg('UPSTREAM'),
|
|
synthetic: msg('SYNTHETIC'),
|
|
configured: msg('CONFIGURED'),
|
|
seed: msg('SEED'),
|
|
image: msg('IMAGE'),
|
|
unknown: msg('UNKNOWN'),
|
|
});
|
|
|
|
/** CAL badge id ('raw-prior') as the summary shows it; unknown ids stay codes. */
|
|
export function summaryCalLabel(badge) {
|
|
const key = String(badge || '');
|
|
return CAL_BADGE_LABELS[key]
|
|
? t(CAL_BADGE_LABELS[key])
|
|
: key.replace('-', ' ').toUpperCase();
|
|
}
|
|
|
|
/** Source kind ('image', 'snapshot') for the summary; HLS, MJPEG… stay codes. */
|
|
export function summarySourceLabel(kind) {
|
|
const key = String(kind || 'image').toLowerCase();
|
|
return SOURCE_KIND_LABELS[key]
|
|
? t(SOURCE_KIND_LABELS[key])
|
|
: key.toUpperCase();
|
|
}
|
|
|
|
/** View band id ('street', 'city'…) as the HUD names it. */
|
|
export function summaryViewBandLabel(band) {
|
|
switch (band) {
|
|
case 'street':
|
|
return t('STREET', { context: 'view band' });
|
|
case 'city':
|
|
return t('CITY', { context: 'view band' });
|
|
case 'regional':
|
|
return t('REGIONAL', { context: 'view band' });
|
|
case 'global':
|
|
return t('GLOBAL', { context: 'view band' });
|
|
default:
|
|
return String(band || '').toUpperCase();
|
|
}
|
|
}
|
|
|
|
export function createPresentation({
|
|
state: layerState,
|
|
services,
|
|
parts,
|
|
source,
|
|
}) {
|
|
/**
|
|
* Builds a single-line summary string for the active camera, including city,
|
|
* heading, FOV, coverage area, overlap count, projection mode, alignment
|
|
* confidence, source type, and view context.
|
|
* @returns {string} Summary text separated by mid-dots.
|
|
*/
|
|
|
|
function buildSummaryText() {
|
|
const active = parts.selection.getActiveRecord();
|
|
if (!active) {
|
|
return layerState._records.length
|
|
? t(
|
|
'{count} CAMERAS STANDING BY · NO CAMERA SELECTED · CLICK A CAMERA TO ACTIVATE',
|
|
{
|
|
count: layerState._records.length,
|
|
},
|
|
)
|
|
: t('No cameras available in catalog.');
|
|
}
|
|
|
|
const area = parts.model.sectorAreaKm2(
|
|
active.camera.rangeM,
|
|
active.camera.fovDeg,
|
|
);
|
|
const overlapCount = parts.geometry.coverageNeighborCount(active);
|
|
const viewKey = parts.model.currentViewContext();
|
|
const viewBand = viewKey.split(':')[0] || 'global';
|
|
const health = layerState._healthById.get(active.camera.id) || null;
|
|
const calBadge = parts.calibration.deriveCalBadge(active.camera);
|
|
|
|
return [
|
|
t('{city} CCTV', {
|
|
city: active.camera.city.toUpperCase(),
|
|
}),
|
|
`${active.camera.name.toUpperCase()}`,
|
|
// A synthetic bearing (headingConfidence 'low', no human calibration)
|
|
// is tagged so a hashed guess never reads as a surveyed facing (#639).
|
|
headingHudToken(active.camera),
|
|
// Identified hardware labels its FOV as datasheet-sourced (catalog.js)
|
|
// only while no manual calibration offset has moved it.
|
|
isDatasheetFov(active.camera)
|
|
? t('FOV {value}° (DATASHEET)', {
|
|
value: Math.round(active.camera.fovDeg),
|
|
})
|
|
: t('FOV {value}°', {
|
|
value: Math.round(active.camera.fovDeg),
|
|
}),
|
|
// A PTZ/varifocal datasheet range is what the lens can do, not its
|
|
// current zoom, so it is shown beside the estimate, never as it.
|
|
specFovCapabilityToken(active.camera, (span) =>
|
|
t('SPEC FOV {span}° (CAPABILITY)', { span }),
|
|
),
|
|
t('COVERAGE {value}km²', { value: area.toFixed(2) }),
|
|
overlapCount > 0
|
|
? t('OVERLAP {count} cams', { count: overlapCount })
|
|
: t('ISOLATED VIEW'),
|
|
layerState._showProjection ? t('PROJ MONITOR') : t('PROJ OFF'),
|
|
layerState._coverageMode === 'viewshed' ? t('VIEWSHED') : null,
|
|
t('CAL {value}', { value: summaryCalLabel(calBadge) }),
|
|
t('SRC {value}', {
|
|
value: summarySourceLabel(
|
|
health?.sourceKind || active.camera.feedType || 'image',
|
|
),
|
|
}),
|
|
t('{value} CONTEXT', { value: summaryViewBandLabel(viewBand) }),
|
|
// Identified hardware: append the model's datasheet specs + attribution.
|
|
active.camera.spec
|
|
? t('{spec} · SPECS: CCTV-DATABASE.COM', {
|
|
spec: specSummary(active.camera.spec).toUpperCase(),
|
|
})
|
|
: null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
}
|
|
|
|
/**
|
|
* Builds a public-facing camera state object for UI consumption.
|
|
* Includes all pose, calibration, CAL badge, projection, and feed metadata.
|
|
* @param {Object} record - Camera record.
|
|
* @param {string|null} [activeId=null] - Active camera ID for the `active` flag.
|
|
* @returns {Object} Public camera state.
|
|
*/
|
|
|
|
function getPublicCameraState(record, activeId = null) {
|
|
const resolvedActiveId =
|
|
activeId || parts.selection.getActiveRecord()?.camera.id || null;
|
|
const camera = record.camera;
|
|
const health = layerState._healthById.get(camera.id) || null;
|
|
const isActive = camera.id === resolvedActiveId;
|
|
const refreshMs = isActive
|
|
? ACTIVE_FRAME_REFRESH_MS
|
|
: IDLE_FRAME_REFRESH_MS;
|
|
return {
|
|
id: camera.id,
|
|
name: camera.name,
|
|
city: camera.city,
|
|
provider: camera.provider,
|
|
lat: camera.lat,
|
|
lon: camera.lon,
|
|
headingDeg: camera.headingDeg,
|
|
// Bearing provenance (#639): the pack's confidence flag plus the derived
|
|
// "is this a synthetic guess" bit (calibration-aware), so UI consumers
|
|
// never have to re-derive it.
|
|
headingConfidence: camera.headingConfidence || null,
|
|
headingEstimated: isHeadingEstimated(camera),
|
|
pitchDeg: camera.pitchDeg,
|
|
fovDeg: camera.fovDeg,
|
|
// Identified-hardware enrichment passthrough (catalog.js / modelSpecs.js).
|
|
fovSource: isDatasheetFov(camera)
|
|
? 'datasheet'
|
|
: camera.fovSource === 'datasheet'
|
|
? 'calibrated'
|
|
: camera.fovSource || 'estimated',
|
|
specFovMinDeg: camera.specFovMinDeg ?? null,
|
|
specFovMaxDeg: camera.specFovMaxDeg ?? null,
|
|
spec: camera.spec || null,
|
|
rangeM: camera.rangeM,
|
|
elevationM: camera.absoluteHeightM,
|
|
mountHeightM: camera.mountHeightM,
|
|
active: isActive,
|
|
feedType: camera.feedType,
|
|
isVideo:
|
|
parts.model.isVideoFeedType(camera.feedType) &&
|
|
record.projection?.mode !== 'image',
|
|
sourceKind:
|
|
health?.sourceKind ||
|
|
camera.sourceKind ||
|
|
(camera.feedConfigured ? 'configured' : 'seed'),
|
|
sourceStatus: health?.status || 'unknown',
|
|
sourceMessage: health?.message || '',
|
|
sourceLabel: health?.label || camera.provider || '',
|
|
credit: camera.credit || '',
|
|
calibration: {
|
|
...parts.calibration.normalizeCalibration(camera.calibration),
|
|
},
|
|
// Save-gated persistence (design §3e): true while the live pose carries
|
|
// edits that have not been SAVEd (or RESET). Drives the CAL · EDITED chip.
|
|
calDirty: !!record.calDirty,
|
|
// Deterministic QA seam: counts commit-grade anchor resolutions (E/N drag
|
|
// release, numeric E/N edit, or reset), never transient gizmo moves.
|
|
groundResolveCount: record.calibrationGroundResolveCount || 0,
|
|
// Per-record QA seam for proving transient gizmo moves never enter the
|
|
// shared mesh-floor sampler while unrelated catalog cells finish.
|
|
groundMeshSampleRequestCount: record.groundMeshSampleRequestCount || 0,
|
|
// Datum QA seam: expose the immutable Re:Earth ellipsoidal prior
|
|
// separately from the currently applied frustum ground. Google-3D may
|
|
// legitimately refine the latter to the rendered mesh, so callers must
|
|
// not infer the prior by subtracting mount height from live geometry.
|
|
groundPriorM: Number.isFinite(record.groundPrior?.ellipsoid)
|
|
? record.groundPrior.ellipsoid
|
|
: null,
|
|
intrinsics: camera.intrinsics ? { ...camera.intrinsics } : null,
|
|
extrinsics: camera.extrinsics ? { ...camera.extrinsics } : null,
|
|
anchor: camera.anchor ? { ...camera.anchor } : null,
|
|
// Panel-only trust signal (design §3b, amended by LOCKED §9.2/§9.3): no
|
|
// in-world rendering reads this, no score-based quality math backs it.
|
|
calBadge: parts.calibration.deriveCalBadge(camera),
|
|
poseSource: camera.poseSource || null,
|
|
basePose: camera.basePose ? { ...camera.basePose } : null,
|
|
frameUrl: parts.frames.frameUrlFor(camera, refreshMs),
|
|
mediaUrl: parts.frames.mediaUrlFor(camera),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Assembles the full UI state payload containing layer toggles, camera list,
|
|
* active camera details, summary text, and error state.
|
|
* @returns {Object} Complete UI state for subscribers.
|
|
*/
|
|
|
|
function uiState() {
|
|
const active = parts.selection.getActiveRecord();
|
|
const activeId = active?.camera.id || null;
|
|
const payload = {
|
|
enabled: layerState._enabled,
|
|
// Compat boolean + the full tri-state (viewshed design §3b).
|
|
showCoverage: layerState._coverageMode !== 'off',
|
|
coverageMode: layerState._coverageMode,
|
|
showProjection: layerState._showProjection,
|
|
calibrationMode: layerState._calibrationMode,
|
|
autoHop: layerState._autoHop,
|
|
autoHopSuspended: layerState._autoHopSuspended,
|
|
autoHopSec: layerState._autoHopSec,
|
|
count: layerState._count,
|
|
lastUpdate: layerState._lastUpdate,
|
|
error: layerState._lastError,
|
|
loading: {
|
|
active: layerState._geoLoading,
|
|
loaded: Math.min(layerState._geoLoadDone, layerState._geoLoadTotal),
|
|
total: layerState._geoLoadTotal,
|
|
},
|
|
// Ambient card tier telemetry (QA harnesses assert the fetch pacing —
|
|
// minFrameFetchSpacingMs reads together with fetchMode: cold-fill bursts
|
|
// legitimately reach ~250 ms, steady state stays >=1000 ms).
|
|
ambientCards: {
|
|
count: layerState._cardIds.size,
|
|
limit: CCTV_AMBIENT_CARD_MAX,
|
|
frameFetches: layerState._cardFetchCount,
|
|
minFrameFetchSpacingMs: layerState._cardMinFetchSpacingMs,
|
|
fetchMode: layerState._cardFetchMode,
|
|
fetchesInFlight: layerState._cardFetchInFlightCount,
|
|
// Item B QA seam: the hover-summoned pinned card, if any.
|
|
hoverId: layerState._hoverCardId,
|
|
},
|
|
activeCameraId: activeId,
|
|
activeCamera: active ? getPublicCameraState(active, activeId) : null,
|
|
cameras: layerState._records.map((record) =>
|
|
getPublicCameraState(record, activeId),
|
|
),
|
|
summary: buildSummaryText(),
|
|
};
|
|
return payload;
|
|
}
|
|
|
|
/** Dispatches the current UI state to all registered subscriber callbacks. */
|
|
|
|
function notifyListeners() {
|
|
const payload = uiState();
|
|
for (const callback of layerState._listeners) {
|
|
try {
|
|
callback(payload);
|
|
} catch (error) {
|
|
console.warn('[Data:CCTV] listener error:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throttled notifyListeners for transient (mid-drag) calibration patches —
|
|
* the panel re-render is DOM-heavy, so live gizmo drags publish state at
|
|
* ≤10 Hz while the in-world geometry still tracks every processed move.
|
|
*/
|
|
|
|
function notifyListenersThrottled() {
|
|
const now = Date.now();
|
|
if (now - layerState._lastTransientNotifyAt < 100) return;
|
|
layerState._lastTransientNotifyAt = now;
|
|
notifyListeners();
|
|
}
|
|
return {
|
|
buildSummaryText,
|
|
getPublicCameraState,
|
|
uiState,
|
|
notifyListeners,
|
|
notifyListenersThrottled,
|
|
};
|
|
}
|