mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Preserve source identity and history references through layer state
This commit is contained in:
1 parent
80c5a09f15
commit
73135a7ae4
8 files changed
+92
-37
No files matched your search
@@ -10,7 +10,8 @@ retain separate heading/course and sea-surface datum. History is a best-effort
|
|||||||
addition to the locally accumulated trail, never a promise of complete coverage.
|
addition to the locally accumulated trail, never a promise of complete coverage.
|
||||||
|
|
||||||
Snapshot coverage, completeness and freshness are separate fields. A partially
|
Snapshot coverage, completeness and freshness are separate fields. A partially
|
||||||
admitted aircraft snapshot cannot evict absent contacts. An invalid nonempty
|
admitted aircraft snapshot retains absent contacts for up to five minutes before
|
||||||
|
the usual missed-poll eviction. Unknown snapshot times remain unknown in stats. An invalid nonempty
|
||||||
snapshot retains the previous display. Empty vessel refreshes retain the existing
|
snapshot retains the previous display. Empty vessel refreshes retain the existing
|
||||||
first-connect grace and warm-data behavior. Known source failures have bounded
|
first-connect grace and warm-data behavior. Known source failures have bounded
|
||||||
messages; arbitrary HTTP response bodies are not surfaced as diagnostics.
|
messages; arbitrary HTTP response bodies are not surfaced as diagnostics.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createAisStreamSource } from '../sources/live/standalone.js';
|
import { createAisStreamSource } from '../sources/live/standalone.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
let _source = createAisStreamSource({ apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live' });
|
|
||||||
import {
|
import {
|
||||||
registerEntityContext,
|
registerEntityContext,
|
||||||
selectEntityContext,
|
selectEntityContext,
|
||||||
@@ -48,6 +47,8 @@ import {
|
|||||||
import { requestWorldFocus } from '../worldFocus.js';
|
import { requestWorldFocus } from '../worldFocus.js';
|
||||||
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
|
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
|
||||||
|
|
||||||
|
let _source = createAisStreamSource({ apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live' });
|
||||||
|
|
||||||
const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
|
const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
|
||||||
|
|
||||||
/** Camera pose signature at the last vessel rotation pass. */
|
/** Camera pose signature at the last vessel rotation pass. */
|
||||||
@@ -349,6 +350,7 @@ const aisLiveVesselsLayer = {
|
|||||||
if (state.viewer) throw new Error('Configure the source before layer initialization');
|
if (state.viewer) throw new Error('Configure the source before layer initialization');
|
||||||
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
||||||
_source = source;
|
_source = source;
|
||||||
|
this.source = source.label || this.source;
|
||||||
},
|
},
|
||||||
|
|
||||||
init(viewer) {
|
init(viewer) {
|
||||||
@@ -1639,7 +1641,7 @@ function startSelectedVesselTrail(record) {
|
|||||||
state.trail = createTrail(state.viewer, { color: TRAIL_COLOR, width: 2.5 });
|
state.trail = createTrail(state.viewer, { color: TRAIL_COLOR, width: 2.5 });
|
||||||
}
|
}
|
||||||
if (state.trail) state.trail.setPositions(state.trailPositions);
|
if (state.trail) state.trail.setPositions(state.trailPositions);
|
||||||
backfillVesselTrail(record.mmsi, state.trailBackfillToken);
|
backfillVesselTrail(record.mmsi, state.trailBackfillToken, record.reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1641,3 +1641,28 @@ test('a vessel analyst record carries the MMSI the tracker keys on', () => {
|
|||||||
assert.equal(nameless.id, '366999124');
|
assert.equal(nameless.id, '366999124');
|
||||||
assert.equal(nameless.mmsi, '366999124');
|
assert.equal(nameless.mmsi, '366999124');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('vessel selection passes the opaque source reference to optional history', async () => {
|
||||||
|
const { createAisStreamSource } = await import('../sources/live/standalone.js');
|
||||||
|
_setVesselStateForTest({ enabled: false });
|
||||||
|
const requests = [];
|
||||||
|
aisLiveVesselsLayer.setSource({
|
||||||
|
label: 'Test vessel source',
|
||||||
|
getSnapshot: async () => ({ records: [] }),
|
||||||
|
getTrack: async (reference, options) => { requests.push({ reference, options }); return { records: [] }; },
|
||||||
|
});
|
||||||
|
const harness = installWireHarness(undefined, { selectedRecord: null, trailMmsi: null });
|
||||||
|
try {
|
||||||
|
harness.record.reference = 'opaque:test-reference';
|
||||||
|
assert.equal(aisLiveVesselsLayer.selectById(harness.record.mmsi), true);
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.equal(requests.length, 1);
|
||||||
|
assert.equal(requests[0].reference, 'opaque:test-reference');
|
||||||
|
assert.ok(requests[0].options.signal instanceof AbortSignal);
|
||||||
|
assert.equal(aisLiveVesselsLayer.source, 'Test vessel source');
|
||||||
|
} finally {
|
||||||
|
harness.cleanup();
|
||||||
|
_setVesselStateForTest({ enabled: false });
|
||||||
|
aisLiveVesselsLayer.setSource(createAisStreamSource());
|
||||||
|
}
|
||||||
|
});
|
||||||
+24
-23
@@ -21,7 +21,6 @@ import { createOpenSkySource } from '../sources/live/standalone.js';
|
|||||||
* looking at (owner decision 2026-07-02).
|
* looking at (owner decision 2026-07-02).
|
||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
let _source = createOpenSkySource();
|
|
||||||
import { aircraftIncludedInNearby } from './aircraftNearbyPolicy.js';
|
import { aircraftIncludedInNearby } from './aircraftNearbyPolicy.js';
|
||||||
import { registerPickOwner, unregisterPickOwner, isOwnedByOtherLayer, resolvePickId } from './pickRegistry.js';
|
import { registerPickOwner, unregisterPickOwner, isOwnedByOtherLayer, resolvePickId } from './pickRegistry.js';
|
||||||
import {
|
import {
|
||||||
@@ -97,6 +96,8 @@ import {
|
|||||||
import { CONTACT_MATCH_TIER, contactMatchWins, rankContactMatch } from './contactMatch.js';
|
import { CONTACT_MATCH_TIER, contactMatchWins, rankContactMatch } from './contactMatch.js';
|
||||||
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
|
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
|
||||||
|
|
||||||
|
let _source = createOpenSkySource();
|
||||||
|
|
||||||
const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
|
const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
|
||||||
|
|
||||||
/** Amber tint for known-military aircraft rendered by this layer (matches the military layer's icon color). */
|
/** Amber tint for known-military aircraft rendered by this layer (matches the military layer's icon color). */
|
||||||
@@ -340,7 +341,7 @@ let _lastTrackingRefreshOutcome = {
|
|||||||
epoch: 0,
|
epoch: 0,
|
||||||
status: 'unavailable',
|
status: 'unavailable',
|
||||||
ids: new Set(),
|
ids: new Set(),
|
||||||
source: 'OpenSky Network',
|
source: _lastSource,
|
||||||
coverage: null,
|
coverage: null,
|
||||||
};
|
};
|
||||||
/** @type {Cesium.Entity|null} Entity used for camera tracking */
|
/** @type {Cesium.Entity|null} Entity used for camera tracking */
|
||||||
@@ -412,7 +413,7 @@ function _contextSubjectMetadata(icao24) {
|
|||||||
id: icao24,
|
id: icao24,
|
||||||
layerId: 'flights',
|
layerId: 'flights',
|
||||||
layerName: 'Live Flights',
|
layerName: 'Live Flights',
|
||||||
source: 'OpenSky Network',
|
source: _lastSource,
|
||||||
label: _contactLabel(icao24, _flightData.get(icao24)),
|
label: _contactLabel(icao24, _flightData.get(icao24)),
|
||||||
latitude: described.latitude,
|
latitude: described.latitude,
|
||||||
longitude: described.longitude,
|
longitude: described.longitude,
|
||||||
@@ -996,13 +997,6 @@ let _drReconcileValid = false;
|
|||||||
/** @type {string|null} icao the reconciliation state currently belongs to */
|
/** @type {string|null} icao the reconciliation state currently belongs to */
|
||||||
let _drReconcileIcao = null;
|
let _drReconcileIcao = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a value to a trimmed lowercase string.
|
|
||||||
* @param {*} value - Any value (typically a header string or null).
|
|
||||||
* @returns {string} Lowercase trimmed string, or '' if falsy.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a value to a trimmed string. A whitespace-only field (" ") is
|
* Normalize a value to a trimmed string. A whitespace-only field (" ") is
|
||||||
* truthy, so every label chain must trim FIRST and then fall through.
|
* truthy, so every label chain must trim FIRST and then fall through.
|
||||||
@@ -2991,7 +2985,7 @@ function _startTrail(icao24) {
|
|||||||
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
||||||
let path = null;
|
let path = null;
|
||||||
try {
|
try {
|
||||||
const track = await _source.getTrack?.(icao24, { signal: AbortSignal.timeout(8000) });
|
const track = await _source.getTrack?.(_flightData.get(icao24)?.sourceReference ?? icao24, { signal: AbortSignal.timeout(8000) });
|
||||||
path = track?.records ?? null;
|
path = track?.records ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
return; // silent fallback to the accumulated trail
|
return; // silent fallback to the accumulated trail
|
||||||
@@ -3822,26 +3816,28 @@ const flightsLayer = {
|
|||||||
id: 'flights',
|
id: 'flights',
|
||||||
name: 'Live Flights',
|
name: 'Live Flights',
|
||||||
icon: '✈️',
|
icon: '✈️',
|
||||||
source: 'OpenSky Network',
|
source: _lastSource,
|
||||||
// Browser-harness seam: isolates synthetic display-floor scenarios without
|
// Browser-harness seam: isolates synthetic display-floor scenarios without
|
||||||
// changing any production lifecycle or cache policy.
|
// changing any production lifecycle or cache policy.
|
||||||
_clearDisplayFloorStateForTest,
|
_clearDisplayFloorStateForTest,
|
||||||
/** @type {number} Polling interval (ms) between update() calls */
|
/** @type {number} Polling interval (ms) between update() calls */
|
||||||
updateInterval: 30000,
|
updateInterval: 30000,
|
||||||
|
|
||||||
|
/** Configure the source before initialization; an active layer keeps its owner. */
|
||||||
|
setSource(source) {
|
||||||
|
if (_viewer) throw new Error('Configure the source before layer initialization');
|
||||||
|
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
||||||
|
_source = source;
|
||||||
|
_lastSource = source.label || _lastSource;
|
||||||
|
this.source = _lastSource;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the flights layer.
|
* Initialize the flights layer.
|
||||||
* Creates the BillboardCollection, resets all state, and installs the
|
* Creates the BillboardCollection, resets all state, and installs the
|
||||||
* click-to-track handler on the scene canvas.
|
* click-to-track handler on the scene canvas.
|
||||||
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
|
||||||
*/
|
*/
|
||||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
|
||||||
setSource(source) {
|
|
||||||
if (_viewer) throw new Error('Configure the source before layer initialization');
|
|
||||||
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
|
||||||
_source = source;
|
|
||||||
},
|
|
||||||
|
|
||||||
init(viewer) {
|
init(viewer) {
|
||||||
clearFocusTarget('flights');
|
clearFocusTarget('flights');
|
||||||
_focusEvidenceIds.clear();
|
_focusEvidenceIds.clear();
|
||||||
@@ -3875,7 +3871,7 @@ const flightsLayer = {
|
|||||||
_retryAt = 0;
|
_retryAt = 0;
|
||||||
_lastError = null;
|
_lastError = null;
|
||||||
_lastStatus = null;
|
_lastStatus = null;
|
||||||
_lastSource = 'OpenSky Network';
|
_lastSource = _source.label || 'OpenSky Network';
|
||||||
_lastCoverage = 'worldwide upstream snapshot';
|
_lastCoverage = 'worldwide upstream snapshot';
|
||||||
_trackedIcao = null;
|
_trackedIcao = null;
|
||||||
_resetTrackedSelectionState();
|
_resetTrackedSelectionState();
|
||||||
@@ -4029,7 +4025,7 @@ const flightsLayer = {
|
|||||||
const usableStates = snapshot.records;
|
const usableStates = snapshot.records;
|
||||||
const sourceEpochMs = snapshot.observedAtMs;
|
const sourceEpochMs = snapshot.observedAtMs;
|
||||||
const sourceAgeMs = snapshot.ageMs;
|
const sourceAgeMs = snapshot.ageMs;
|
||||||
const sourceStale = snapshot.stale;
|
const sourceStale = snapshot.stale || snapshot.freshness === 'unknown';
|
||||||
_backoff = sourceStale;
|
_backoff = sourceStale;
|
||||||
_retryAt = 0;
|
_retryAt = 0;
|
||||||
_lastError = sourceStale
|
_lastError = sourceStale
|
||||||
@@ -4219,6 +4215,8 @@ const flightsLayer = {
|
|||||||
// Store flight metadata for click-to-track labels
|
// Store flight metadata for click-to-track labels
|
||||||
const cat = stickyNumber(category, prevMeta?.category, null);
|
const cat = stickyNumber(category, prevMeta?.category, null);
|
||||||
const meta = {
|
const meta = {
|
||||||
|
sourceReference: observation.reference,
|
||||||
|
observedReceiptMs: Date.now(),
|
||||||
callsign: stickyText(callsign, prevMeta?.callsign),
|
callsign: stickyText(callsign, prevMeta?.callsign),
|
||||||
altitude: alt,
|
altitude: alt,
|
||||||
// geoAltitudeM/renderAltitudeM are ADDITIVE fields alongside the
|
// geoAltitudeM/renderAltitudeM are ADDITIVE fields alongside the
|
||||||
@@ -4398,7 +4396,9 @@ const flightsLayer = {
|
|||||||
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
|
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
|
||||||
// feed gap, and the full grace left phantom planes parked at airports.
|
// feed gap, and the full grace left phantom planes parked at airports.
|
||||||
for (const [icao24, bb] of _billboards) {
|
for (const [icao24, bb] of _billboards) {
|
||||||
if (currentIcaos.has(icao24) || !snapshot.complete) continue;
|
if (currentIcaos.has(icao24)) continue;
|
||||||
|
// Partial admissions do not prove absence, but stale retention is bounded.
|
||||||
|
if (!snapshot.complete && Date.now() - (_flightData.get(icao24)?.observedReceiptMs ?? 0) < 300000) continue;
|
||||||
const misses = (_missingPolls.get(icao24) || 0) + 1;
|
const misses = (_missingPolls.get(icao24) || 0) + 1;
|
||||||
const limit = _likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
|
const limit = _likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
|
||||||
if (misses < limit) {
|
if (misses < limit) {
|
||||||
@@ -4477,7 +4477,7 @@ const flightsLayer = {
|
|||||||
_count = _billboards.size;
|
_count = _billboards.size;
|
||||||
// Freshness belongs to the source snapshot, not the moment this browser
|
// Freshness belongs to the source snapshot, not the moment this browser
|
||||||
// received a cached 200 response.
|
// received a cached 200 response.
|
||||||
_lastUpdate = sourceEpochMs ?? Date.now();
|
_lastUpdate = sourceEpochMs;
|
||||||
_lastTrackingRefreshOutcome = {
|
_lastTrackingRefreshOutcome = {
|
||||||
epoch: trackingRefreshEpoch,
|
epoch: trackingRefreshEpoch,
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
@@ -4496,6 +4496,7 @@ const flightsLayer = {
|
|||||||
_backoff = true;
|
_backoff = true;
|
||||||
_retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
|
_retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
|
||||||
_lastStatus = e?.status ?? null;
|
_lastStatus = e?.status ?? null;
|
||||||
|
if (e?.source) { _lastSource = e.source; this.source = _lastSource; }
|
||||||
_lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
|
_lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
|
||||||
} finally {
|
} finally {
|
||||||
_activeUpdateControllers.delete(resourceController);
|
_activeUpdateControllers.delete(resourceController);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createAdsbLolSource } from '../sources/live/standalone.js';
|
import { createAdsbLolSource } from '../sources/live/standalone.js';
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
let _source = createAdsbLolSource();
|
|
||||||
import { aircraftIncludedInNearby } from './aircraftNearbyPolicy.js';
|
import { aircraftIncludedInNearby } from './aircraftNearbyPolicy.js';
|
||||||
import { registerPickOwner, unregisterPickOwner, isOwnedByOtherLayer, resolvePickId } from './pickRegistry.js';
|
import { registerPickOwner, unregisterPickOwner, isOwnedByOtherLayer, resolvePickId } from './pickRegistry.js';
|
||||||
import { registerSpriteCollection, restoreSpriteOrder } from './spriteOrder.js';
|
import { registerSpriteCollection, restoreSpriteOrder } from './spriteOrder.js';
|
||||||
@@ -64,6 +63,8 @@ import {
|
|||||||
import { CONTACT_MATCH_TIER, contactMatchWins, rankContactMatch } from './contactMatch.js';
|
import { CONTACT_MATCH_TIER, contactMatchWins, rankContactMatch } from './contactMatch.js';
|
||||||
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
|
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
|
||||||
|
|
||||||
|
let _source = createAdsbLolSource();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module militaryFlights
|
* @module militaryFlights
|
||||||
* @description Real-time military flight tracking layer powered by the adsb.lol API.
|
* @description Real-time military flight tracking layer powered by the adsb.lol API.
|
||||||
@@ -2195,7 +2196,7 @@ function _startTrail(icao24) {
|
|||||||
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
|
||||||
let trace = null;
|
let trace = null;
|
||||||
try {
|
try {
|
||||||
const track = await _source.getTrack?.(icao24, { signal: AbortSignal.timeout(8000) });
|
const track = await _source.getTrack?.(_flightData.get(icao24)?.sourceReference ?? icao24, { signal: AbortSignal.timeout(8000) });
|
||||||
trace = track?.records ?? null;
|
trace = track?.records ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
return; // silent fallback to the accumulated trail
|
return; // silent fallback to the accumulated trail
|
||||||
@@ -2591,18 +2592,20 @@ const militaryFlightsLayer = {
|
|||||||
/** @type {number} Polling interval in ms between API fetches */
|
/** @type {number} Polling interval in ms between API fetches */
|
||||||
updateInterval: 15000,
|
updateInterval: 15000,
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the layer: create the billboard collection, reset all state,
|
|
||||||
* and install the click-to-track handler.
|
|
||||||
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
|
|
||||||
*/
|
|
||||||
/** Configure the source before initialization; an active layer keeps its owner. */
|
/** Configure the source before initialization; an active layer keeps its owner. */
|
||||||
setSource(source) {
|
setSource(source) {
|
||||||
if (_viewer) throw new Error('Configure the source before layer initialization');
|
if (_viewer) throw new Error('Configure the source before layer initialization');
|
||||||
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
|
||||||
_source = source;
|
_source = source;
|
||||||
|
_lastSource = source.label || _lastSource;
|
||||||
|
this.source = _lastSource;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the layer: create the billboard collection, reset all state,
|
||||||
|
* and install the click-to-track handler.
|
||||||
|
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
|
||||||
|
*/
|
||||||
init(viewer) {
|
init(viewer) {
|
||||||
clearFocusTarget('militaryFlights');
|
clearFocusTarget('militaryFlights');
|
||||||
_viewer = viewer;
|
_viewer = viewer;
|
||||||
@@ -2782,7 +2785,7 @@ const militaryFlightsLayer = {
|
|||||||
_lastSource = snapshot.source;
|
_lastSource = snapshot.source;
|
||||||
militaryFlightsLayer.source = _lastSource;
|
militaryFlightsLayer.source = _lastSource;
|
||||||
const usableAircraft = snapshot.records;
|
const usableAircraft = snapshot.records;
|
||||||
_backoff = snapshot.stale;
|
_backoff = snapshot.stale || snapshot.freshness === 'unknown';
|
||||||
_retryAt = 0;
|
_retryAt = 0;
|
||||||
_lastError = snapshot.freshness === 'unknown' ? 'Source snapshot time unavailable' : null;
|
_lastError = snapshot.freshness === 'unknown' ? 'Source snapshot time unavailable' : null;
|
||||||
const currentIcaos = new Set();
|
const currentIcaos = new Set();
|
||||||
@@ -2900,6 +2903,8 @@ const militaryFlightsLayer = {
|
|||||||
// last-known-good (bounded by the layer's eviction, which deletes the entry).
|
// last-known-good (bounded by the layer's eviction, which deletes the entry).
|
||||||
const stickyType = stickyText(type, prevMeta?.type);
|
const stickyType = stickyText(type, prevMeta?.type);
|
||||||
const meta = {
|
const meta = {
|
||||||
|
sourceReference: aircraft.reference,
|
||||||
|
observedReceiptMs: Date.now(),
|
||||||
callsign: stickyText(callsign, prevMeta?.callsign),
|
callsign: stickyText(callsign, prevMeta?.callsign),
|
||||||
type: stickyType,
|
type: stickyType,
|
||||||
// Type outranks category automatically inside classifyAircraft.
|
// Type outranks category automatically inside classifyAircraft.
|
||||||
@@ -3094,7 +3099,9 @@ const militaryFlightsLayer = {
|
|||||||
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
|
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
|
||||||
// feed gap, and the full grace left phantom planes parked at airports.
|
// feed gap, and the full grace left phantom planes parked at airports.
|
||||||
for (const [icao24, bb] of _billboards) {
|
for (const [icao24, bb] of _billboards) {
|
||||||
if (currentIcaos.has(icao24) || !snapshot.complete) continue;
|
if (currentIcaos.has(icao24)) continue;
|
||||||
|
// Partial admissions do not prove absence, but stale retention is bounded.
|
||||||
|
if (!snapshot.complete && Date.now() - (_flightData.get(icao24)?.observedReceiptMs ?? 0) < 300000) continue;
|
||||||
const misses = (_missingPolls.get(icao24) || 0) + 1;
|
const misses = (_missingPolls.get(icao24) || 0) + 1;
|
||||||
const limit = _likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
|
const limit = _likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
|
||||||
if (misses < limit) {
|
if (misses < limit) {
|
||||||
@@ -3155,6 +3162,7 @@ const militaryFlightsLayer = {
|
|||||||
_backoff = true;
|
_backoff = true;
|
||||||
_retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
|
_retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
|
||||||
_lastStatus = e?.status ?? null;
|
_lastStatus = e?.status ?? null;
|
||||||
|
if (e?.source) { _lastSource = e.source; this.source = _lastSource; }
|
||||||
_lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
|
_lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
|
||||||
} finally {
|
} finally {
|
||||||
_activeUpdateControllers.delete(resourceController);
|
_activeUpdateControllers.delete(resourceController);
|
||||||
|
|||||||
@@ -247,3 +247,17 @@ test('AIS reports limited received coverage and keeps connection state separate
|
|||||||
now,
|
now,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a malformed vessel row cannot prevent admission of valid positions', async () => {
|
||||||
|
const source = createAisStreamSource({
|
||||||
|
fetchImpl: async () =>
|
||||||
|
response({
|
||||||
|
status: 'live',
|
||||||
|
rows: [null, { mmsi: '123456789', lat: 30, lon: -97 }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const snapshot = await source.getSnapshot();
|
||||||
|
assert.equal(snapshot.records.length, 1);
|
||||||
|
assert.equal(snapshot.rejectedCount, 1);
|
||||||
|
assert.equal(snapshot.complete, false);
|
||||||
|
});
|
||||||
@@ -54,6 +54,7 @@ export function createOpenSkySource({
|
|||||||
now = () => Date.now(),
|
now = () => Date.now(),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
|
label: 'OpenSky Network',
|
||||||
async getSnapshot(query = {}, { signal } = {}) {
|
async getSnapshot(query = {}, { signal } = {}) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (Number.isFinite(query.latitude) && Number.isFinite(query.longitude)) {
|
if (Number.isFinite(query.latitude) && Number.isFinite(query.longitude)) {
|
||||||
@@ -111,6 +112,7 @@ export function createAdsbLolSource({
|
|||||||
now = () => Date.now(),
|
now = () => Date.now(),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
|
label: 'adsb.lol',
|
||||||
async getSnapshot(_query = {}, { signal } = {}) {
|
async getSnapshot(_query = {}, { signal } = {}) {
|
||||||
const { response, payload } = await readResponse(
|
const { response, payload } = await readResponse(
|
||||||
fetchImpl,
|
fetchImpl,
|
||||||
@@ -158,6 +160,7 @@ export function createAisStreamSource({
|
|||||||
origin = () => globalThis.location?.origin || 'http://localhost',
|
origin = () => globalThis.location?.origin || 'http://localhost',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
|
label: 'AISStream',
|
||||||
async getSnapshot({ maxRows = 12000 } = {}, { signal } = {}) {
|
async getSnapshot({ maxRows = 12000 } = {}, { signal } = {}) {
|
||||||
const url = new URL(apiUrl, origin());
|
const url = new URL(apiUrl, origin());
|
||||||
url.searchParams.set('maxRows', String(maxRows));
|
url.searchParams.set('maxRows', String(maxRows));
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export function vesselSnapshot(
|
|||||||
const records = [],
|
const records = [],
|
||||||
ids = new Set();
|
ids = new Set();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const record = normalizeVesselObservation(row, referenceFor(row));
|
const record = normalizeVesselObservation(row);
|
||||||
|
if (record) record.reference = referenceFor(row);
|
||||||
if (record && !ids.has(record.id)) {
|
if (record && !ids.has(record.id)) {
|
||||||
records.push(record);
|
records.push(record);
|
||||||
ids.add(record.id);
|
ids.add(record.id);
|
||||||
@@ -57,7 +58,7 @@ export function vesselSnapshot(
|
|||||||
records,
|
records,
|
||||||
source,
|
source,
|
||||||
coverage,
|
coverage,
|
||||||
complete: false,
|
complete: records.length === rows.length && !payload?.refreshing,
|
||||||
rejectedCount: rows.length - records.length,
|
rejectedCount: rows.length - records.length,
|
||||||
observedAtMs,
|
observedAtMs,
|
||||||
freshness: payload?.refreshing
|
freshness: payload?.refreshing
|
||||||
|
|||||||
Reference in new issue
Block a user