mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
keep contacts counts, rows, scope and provenance on one retained snapshot report capped cohorts as lower bounds separate sibling tool cancellation from newer user turns align track_entity validation across runtime and benchmark paths
205 lines
8.2 KiB
JavaScript
205 lines
8.2 KiB
JavaScript
import { AWARENESS_RADIUS_M } from '../../data/militaryAwarenessEngine.js';
|
|
import { DEPENDENCIES, AWARENESS_QUERY_LIMIT } from './policy.js';
|
|
|
|
export function createQueries({ state: layerState, services, parts, source }) {
|
|
const flightsLayer = services.flights;
|
|
const militaryFlightsLayer = services.military;
|
|
|
|
function sourceState(layerId) {
|
|
const lifecycle =
|
|
layerState.dataManager?.getLayerLifecycleState?.(layerId) || null;
|
|
const enabled =
|
|
lifecycle?.enabled === true ||
|
|
layerState.dataManager?.isEnabled(layerId) === true;
|
|
const enabling = lifecycle?.lifecycleState === 'enabling';
|
|
const moduleStats =
|
|
layerState.dataManager?.layers?.get(layerId)?.module?.getStats?.() || {};
|
|
const stats = enabling
|
|
? { ...moduleStats, loading: true, status: 'loading' }
|
|
: moduleStats;
|
|
// A deferred source that has started but not settled is not evidence of an
|
|
// empty cohort. Keep it explicitly unavailable so the panel cannot flash a
|
|
// false all-clear, even if a lifecycle adapter briefly reports the requested
|
|
// visibility intent as enabled.
|
|
//
|
|
// `enabling` and the never-answered predicate cover two DIFFERENT windows, and
|
|
// both are needed because these dependencies do not settle the same way.
|
|
//
|
|
// - military-installations is covered by `enabling` alone. Its enable() is
|
|
// SYNCHRONOUS and the manager awaits update(), which owns the first
|
|
// Overpass fetch, so the lifecycle stays `enabling` for that whole window
|
|
// however long the fetch takes. Confirmed live: a held 17 s first fetch
|
|
// read `enabling` across 34 samples with the panel non-numeric throughout,
|
|
// and a failing one settled to `enabled` with status 'unavailable'. Its
|
|
// getStats() also reports loading while a request is in progress —
|
|
// setInstallationStatus is only ever called with
|
|
// loading/zoom-in/ready/stale/empty/unavailable.
|
|
// - ais-live-vessels is what the predicate below is FOR. Its enable() and
|
|
// update() both resolve as soon as the first /api/ais-live poll answers,
|
|
// so the lifecycle settles to `enabled` — but until the server-side socket
|
|
// delivers a position, firstConnectPhase is 'loading' and getStats()
|
|
// reports loading: true, lastUpdate: null, count 0, and an UNDEFINED
|
|
// status. That zero is an absence of evidence, not evidence of absence,
|
|
// and neither the lifecycle nor the status list can tell.
|
|
//
|
|
// Deliberately `loading === true` and not truthiness: layers that never report
|
|
// a busy flag (flights, military) must keep answering for themselves, and a
|
|
// source that HAS answered once (lastUpdate set) keeps its last real count
|
|
// through every later poll rather than blanking to `?` on each refresh.
|
|
const neverAnswered = stats.loading === true && !stats.lastUpdate;
|
|
const unavailable =
|
|
enabling ||
|
|
!enabled ||
|
|
neverAnswered ||
|
|
['unavailable', 'zoom-in'].includes(stats.status) ||
|
|
Boolean(stats.error && stats.count === 0);
|
|
return {
|
|
available: !unavailable,
|
|
enabled,
|
|
stale: Boolean(stats.stale),
|
|
stats,
|
|
};
|
|
}
|
|
|
|
function collectSourceStates() {
|
|
return Object.fromEntries(
|
|
DEPENDENCIES.map((layerId) => [layerId, sourceState(layerId)]),
|
|
);
|
|
}
|
|
|
|
function sourceRevision(sourceStates) {
|
|
return DEPENDENCIES.map((layerId) => {
|
|
const source = sourceStates[layerId];
|
|
const stats = source?.stats || {};
|
|
return [
|
|
layerId,
|
|
source?.available,
|
|
source?.stale,
|
|
stats.lastUpdate || null,
|
|
stats.count ?? null,
|
|
stats.status || null,
|
|
stats.error || null,
|
|
];
|
|
})
|
|
.map((parts) => parts.join(':'))
|
|
.join('|');
|
|
}
|
|
|
|
function isSame(subject, item, prefix, key) {
|
|
return (
|
|
subject?.layerId === prefix && String(subject.id) === String(item?.[key])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Summarize viewport-backed installations without implying radius-complete coverage.
|
|
* @param {Array<object>} items Loaded installation matches.
|
|
* @param {object} source Current source availability and staleness.
|
|
* @returns {object} Awareness summary with viewport-honest reasoning.
|
|
*/
|
|
|
|
function summarizeInstallationViewport(items, source) {
|
|
const coverage = source.stats?.coverage;
|
|
// getNearby measures these distances from the current subject, independent
|
|
// of the retained square used to fetch installation tiles.
|
|
const withinRadius =
|
|
coverage?.kind === 'subject' && Number.isFinite(coverage.radiusM)
|
|
? items.filter(
|
|
(item) => (item.distanceM ?? item.distance) <= coverage.radiusM,
|
|
)
|
|
: items;
|
|
const summary = parts.navigation.summarizeAwarenessCohortForNavigation(
|
|
withinRadius,
|
|
source,
|
|
);
|
|
if (summary.count === null)
|
|
return source.stats?.statusMessage
|
|
? { ...summary, reason: source.stats.statusMessage }
|
|
: summary;
|
|
// A subject window is its own bounded area: name it instead of the viewport.
|
|
if (coverage?.kind === 'subject' && Number.isFinite(coverage.radiusM)) {
|
|
const km = Math.round(coverage.radiusM / 1000);
|
|
return {
|
|
...summary,
|
|
reason: summary.count
|
|
? `mapped matches within ${km} km of the subject`
|
|
: `none mapped within ${km} km; not a complete 250 km survey`,
|
|
};
|
|
}
|
|
return {
|
|
...summary,
|
|
reason: summary.count
|
|
? 'mapped matches from the loaded viewport'
|
|
: 'viewport feed is not a complete 250 km survey',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* THE aircraft-proximity engine. One computation, two consumers: the Contacts
|
|
* panel window and the voice analyst's entity-centred "how many nearby".
|
|
*
|
|
* They used to be separate. The panel read live billboard positions through
|
|
* `getNearby` with a 20 000 cap; the analyst re-derived its own answer from
|
|
* last-fix coordinates over a 2 000-record slice. Same question, same centre,
|
|
* two numbers — and in the owner's trial the spoken answer (15) and the panel
|
|
* (111) disagreed badly enough that the model narrated the difference away.
|
|
* Routing both through here makes them the same number BY CONSTRUCTION, so
|
|
* they cannot drift again.
|
|
*
|
|
* The subject is excluded from its own window, which is why the panel reads
|
|
* "contacts around X" rather than "including X".
|
|
* @param {Cesium.Cartesian3} position Window centre.
|
|
* @param {object} [options]
|
|
* @param {number} [options.radiusM=AWARENESS_RADIUS_M] Window radius.
|
|
* @param {object|null} [options.subject=null] Contact at the centre, excluded.
|
|
* @returns {{flights: Array, military: Array, aircraft: number,
|
|
* completeByLayer: {flights: boolean, military: boolean}}|null} Cohorts,
|
|
* their lower-bound completeness, and the combined aircraft count, or
|
|
* null without a position.
|
|
*/
|
|
|
|
function collectAircraftProximityWindow(
|
|
position,
|
|
{ radiusM = AWARENESS_RADIUS_M, subject = null } = {},
|
|
) {
|
|
if (!position) return null;
|
|
// Ask for one usable row beyond the retained limit so a capped cohort is
|
|
// reported as a lower bound instead of an exact count. The selected
|
|
// subject can consume one slot before exclusion, hence the extra two.
|
|
const sampleLimit = AWARENESS_QUERY_LIMIT + 2;
|
|
const flightSample = flightsLayer
|
|
.getNearby(position, radiusM, sampleLimit, {
|
|
includeHidden: true,
|
|
})
|
|
.filter(
|
|
(item) => !subject || !isSame(subject, item, 'flights', 'icao24'),
|
|
);
|
|
const militarySample = militaryFlightsLayer
|
|
.getNearby(position, radiusM, sampleLimit, {
|
|
includeHidden: true,
|
|
})
|
|
.filter(
|
|
(item) => !subject || !isSame(subject, item, 'military', 'icao24'),
|
|
);
|
|
const flights = flightSample.slice(0, AWARENESS_QUERY_LIMIT);
|
|
const military = militarySample.slice(0, AWARENESS_QUERY_LIMIT);
|
|
return {
|
|
flights,
|
|
military,
|
|
aircraft: flights.length + military.length,
|
|
completeByLayer: {
|
|
flights: flightSample.length <= AWARENESS_QUERY_LIMIT,
|
|
military: militarySample.length <= AWARENESS_QUERY_LIMIT,
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
sourceState,
|
|
collectSourceStates,
|
|
sourceRevision,
|
|
isSame,
|
|
summarizeInstallationViewport,
|
|
collectAircraftProximityWindow,
|
|
};
|
|
}
|