mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(gnss): separate GEV integrity classifier from GPSJAM aggregation; machine-readable provenance
Review on #855: the per-aircraft rule (NIC < 7 || NACp < 8, borrowed from the US 14 CFR 91.227 ADS-B Out minima) is GEV-defined, not gpsjam's, and gpsjam's map shows low navigation accuracy, not necessarily jamming. - Primary cell state is now `navigation-integrity-anomaly` (medium/high) or `nominal`; `suspected-gnss-interference` is a secondary `interpretation` marked validated: false. - GNSS_PROVENANCE / gnssProvenance({ windowMs }) records the classifier (gev-nic-nacp-v1, GEV, unvalidated), the gpsjam aggregation (formula, bands [0.02, 0.1], FAQ source) and the GEV 30-minute visited-view window, built from the constants the code applies. Exposed as layer.provenance and getStats().provenance; the proxy tags responses with `classifier`. - Visible name "GNSS Integrity", legend "... low accuracy", blurb leads with low navigation accuracy and states the threshold is a GEV choice. - Layer id `gnss-interference` and share token `0` unchanged. - Voice aliases add "gnss integrity", "navigation integrity", "low navigation accuracy"; the interference aliases stay. - Docs describe the method as GEV threshold + gpsjam aggregation. - Tests: provenance object, renamed state/label, proxy classifier tag; qa-gnss asserts provenance and wording in the real app. Hex project - Crew Leo Agile dev team. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
40b06a24d4
commit
6d4a2a02fe
16 files changed
+325
-67
No files matched your search
+19
-11
@@ -2,9 +2,12 @@ import * as Cesium from 'cesium';
|
||||
import {
|
||||
GNSS_LEVEL_COLORS,
|
||||
GNSS_MIN_AIRCRAFT,
|
||||
GNSS_NACP_THRESHOLD,
|
||||
GNSS_NIC_THRESHOLD,
|
||||
GNSS_WINDOW_MS,
|
||||
accumulateGnssObservations,
|
||||
binGnssCells,
|
||||
gnssProvenance,
|
||||
} from './records.js';
|
||||
export * from './records.js';
|
||||
export { createAdsbGnssSource } from './source.js';
|
||||
@@ -12,9 +15,9 @@ export { createAdsbGnssSource } from './source.js';
|
||||
/** Low stays visible over land so coverage reads apart from no data. */
|
||||
const FILL_ALPHA = Object.freeze({ low: 0.3, medium: 0.42, high: 0.55 });
|
||||
const LEVEL_LABELS = Object.freeze({
|
||||
low: 'Under 2% degraded',
|
||||
medium: '2–10% degraded',
|
||||
high: 'Over 10% degraded',
|
||||
low: 'Under 2% low accuracy',
|
||||
medium: '2–10% low accuracy',
|
||||
high: 'Over 10% low accuracy',
|
||||
});
|
||||
|
||||
/** Degrees at the centre of the current view, or null before the camera settles. */
|
||||
@@ -34,18 +37,19 @@ export function defaultGnssViewAnchor(viewer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Own one GNSS-interference display: rolling integrity observations around
|
||||
* the views the user visits, binned into cells coloured by the share of
|
||||
* aircraft reporting degraded navigation integrity.
|
||||
* Own one GNSS navigation-integrity display: rolling integrity observations
|
||||
* around the views the user visits, binned into cells coloured by the share
|
||||
* of aircraft reporting low navigation accuracy. The layer id keeps its
|
||||
* original `gnss-interference` spelling because share links depend on it.
|
||||
*/
|
||||
export function createGnssInterferenceLayer({
|
||||
export function createGnssIntegrityLayer({
|
||||
source,
|
||||
viewAnchor = defaultGnssViewAnchor,
|
||||
now = () => Date.now(),
|
||||
windowMs = GNSS_WINDOW_MS,
|
||||
} = {}) {
|
||||
if (typeof source?.getSnapshot !== 'function')
|
||||
throw new TypeError('GNSS interference requires a snapshot source');
|
||||
throw new TypeError('GNSS integrity layer requires a snapshot source');
|
||||
let _viewer = null;
|
||||
let _dataSource = null;
|
||||
let _request = null;
|
||||
@@ -56,6 +60,7 @@ export function createGnssInterferenceLayer({
|
||||
let _lastError = null;
|
||||
let _stale = false;
|
||||
const _observations = new Map();
|
||||
const provenance = gnssProvenance({ windowMs });
|
||||
|
||||
function render() {
|
||||
// Only the id and band change what is drawn; counts alone do not.
|
||||
@@ -91,14 +96,16 @@ export function createGnssInterferenceLayer({
|
||||
|
||||
const layer = {
|
||||
id: 'gnss-interference',
|
||||
name: 'GNSS Interference',
|
||||
name: 'GNSS Integrity',
|
||||
icon: '📡',
|
||||
source: 'adsb.lol',
|
||||
/** Where each part of the method comes from; see records.js. */
|
||||
provenance,
|
||||
updateInterval: 60000,
|
||||
|
||||
init(viewer) {
|
||||
if (_viewer)
|
||||
throw new Error('GNSS interference layer is already initialized');
|
||||
throw new Error('GNSS integrity layer is already initialized');
|
||||
_viewer = viewer;
|
||||
_dataSource = new Cesium.CustomDataSource('gnss-interference');
|
||||
_dataSource.show = false;
|
||||
@@ -179,7 +186,7 @@ export function createGnssInterferenceLayer({
|
||||
count: _cells.filter((cell) => cell.level === level).length,
|
||||
...(index === 0
|
||||
? {
|
||||
blurb: `Share of ADS-B aircraft reporting low navigation integrity (NIC/NACp) over the last 30 minutes, around the views you visit. Cells with fewer than ${GNSS_MIN_AIRCRAFT} aircraft are not drawn, so blank areas mean no data, not clean GNSS. An inference of jamming or spoofing, not a detection.`,
|
||||
blurb: `Share of ADS-B aircraft reporting low navigation accuracy over the last ${provenance.window.minutes} minutes, around the views you visit. An aircraft counts when it reports NIC < ${GNSS_NIC_THRESHOLD} or NACp < ${GNSS_NACP_THRESHOLD} or GPS loss: a GEV threshold borrowed from US ADS-B Out minima, not a published interference test. Cells use gpsjam.org's formula and 2% / 10% bands. Cells with fewer than ${GNSS_MIN_AIRCRAFT} aircraft are not drawn, so blank areas mean no data, not clean GNSS. Medium and high cells are navigation-integrity anomalies. Suspected jamming or spoofing is one possible cause, not a detection: avionics faults also report low values.`,
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
@@ -192,6 +199,7 @@ export function createGnssInterferenceLayer({
|
||||
lastUpdate: _lastUpdate,
|
||||
error: _lastError,
|
||||
stale: _stale,
|
||||
provenance,
|
||||
// Load-bearing: without an explicit boolean, layerFeedState reads the
|
||||
// adsb.lol source name as a flights fallback (src/data/feedState.js).
|
||||
fallback: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createGnssInterferenceLayer } from './index.js';
|
||||
import { createGnssIntegrityLayer } from './index.js';
|
||||
|
||||
const row = (hex, lat, degraded) => ({
|
||||
hex,
|
||||
@@ -29,7 +29,7 @@ function harness(
|
||||
};
|
||||
let clock = 1_000;
|
||||
const anchors = [];
|
||||
const layer = createGnssInterferenceLayer({
|
||||
const layer = createGnssIntegrityLayer({
|
||||
source: { getSnapshot },
|
||||
viewAnchor: () => {
|
||||
anchors.push(anchor);
|
||||
@@ -49,7 +49,7 @@ function harness(
|
||||
}
|
||||
|
||||
test('the layer requires a snapshot source and cannot be initialized twice', () => {
|
||||
assert.throws(() => createGnssInterferenceLayer({}), TypeError);
|
||||
assert.throws(() => createGnssIntegrityLayer({}), TypeError);
|
||||
const { layer } = harness(async () => ({ rows: [] }));
|
||||
assert.throws(() => layer.init({ dataSources: { add() {} } }), /already/);
|
||||
});
|
||||
@@ -91,6 +91,46 @@ test('an enabled refresh bins the snapshot into coloured ground cells', async ()
|
||||
assert.match(legend[0].blurb, /fewer than 3 aircraft are not drawn/);
|
||||
});
|
||||
|
||||
test('the visible layer leads with low navigation accuracy and exposes its provenance', async () => {
|
||||
const { layer } = harness(async () => ({ rows: [], stale: false }));
|
||||
assert.equal(layer.id, 'gnss-interference', 'the share-link id is unchanged');
|
||||
assert.equal(layer.name, 'GNSS Integrity');
|
||||
assert.doesNotMatch(layer.name, /interference|jamming/i);
|
||||
const { legend } = layer.getRowControls();
|
||||
assert.deepEqual(
|
||||
legend.map(({ label }) => label),
|
||||
['Over 10% low accuracy', '2–10% low accuracy', 'Under 2% low accuracy'],
|
||||
);
|
||||
const { blurb } = legend[0];
|
||||
assert.match(
|
||||
blurb,
|
||||
/^Share of ADS-B aircraft reporting low navigation accuracy/,
|
||||
);
|
||||
assert.match(blurb, /NIC < 7 or NACp < 8/);
|
||||
assert.match(blurb, /GEV threshold/);
|
||||
assert.match(blurb, /not a published interference test/);
|
||||
assert.match(blurb, /gpsjam\.org's formula and 2% \/ 10% bands/);
|
||||
assert.match(blurb, /navigation-integrity anomalies/);
|
||||
// Interference appears only as the secondary, hedged reading.
|
||||
assert.ok(
|
||||
blurb.indexOf('navigation-integrity anomalies') <
|
||||
blurb.indexOf('Suspected jamming or spoofing'),
|
||||
);
|
||||
assert.equal(layer.provenance.classifier.id, 'gev-nic-nacp-v1');
|
||||
assert.equal(layer.provenance.classifier.definedBy, 'GEV');
|
||||
assert.equal(layer.provenance.classifier.validated, false);
|
||||
assert.equal(layer.provenance.aggregation.source, 'https://gpsjam.org/faq');
|
||||
assert.deepEqual(layer.provenance.window, {
|
||||
minutes: 30,
|
||||
scope: 'visited-view',
|
||||
cellDeg: 0.5,
|
||||
minAircraft: 3,
|
||||
definedBy: 'GEV',
|
||||
});
|
||||
assert.equal(layer.provenance.interpretation.validated, false);
|
||||
assert.equal(layer.getStats().provenance, layer.provenance);
|
||||
});
|
||||
|
||||
test('a failed refresh keeps the last cells and reports the error', async () => {
|
||||
let fail = false;
|
||||
const { layer, sources } = harness(async () => {
|
||||
|
||||
+98
-19
@@ -1,24 +1,29 @@
|
||||
/**
|
||||
* GNSS interference inferred from ADS-B navigation integrity.
|
||||
* Low navigation accuracy reported by ADS-B aircraft, binned into cells.
|
||||
*
|
||||
* Aircraft broadcasting DO-260 version 1/2 ADS-B report how much they trust
|
||||
* their own position: NIC (integrity containment radius) and NACp (position
|
||||
* accuracy). Both collapse when the GNSS receiver is jammed or spoofed, so a
|
||||
* cluster of aircraft reporting low values in one area is a well-known public
|
||||
* proxy for interference (popularized by gpsjam.org, whose published cell
|
||||
* formula and bands this module uses, and by the readsb/tar1090 `gpsOkBefore`
|
||||
* flag). gpsjam does not publish its per-aircraft thresholds; here an aircraft
|
||||
* is degraded when it reports below the 14 CFR 91.227(c) ADS-B Out minima
|
||||
* (NIC < 7 or NACp < 8). It is an inference, not a detection:
|
||||
* avionics faults and old transponders also report low values, which is why
|
||||
* cells need several aircraft and the first degraded aircraft is discounted.
|
||||
* accuracy). A cluster of aircraft reporting low values in one area is a
|
||||
* navigation-integrity anomaly. Jamming or spoofing is one common cause, so
|
||||
* such clusters are a well-known public proxy for GNSS interference, but
|
||||
* avionics faults and old transponders also report low values: suspected
|
||||
* interference is a second, unvalidated interpretation, never the primary
|
||||
* state.
|
||||
*
|
||||
* Three separately sourced parts make up the method (see `gnssProvenance`):
|
||||
* - Per-aircraft classifier, defined by GEV: NIC < 7 or NACp < 8 (borrowed
|
||||
* from the US 14 CFR 91.227(c) ADS-B Out performance minima, which are not
|
||||
* a published interference classifier) or the readsb/tar1090 `gpsOkBefore`
|
||||
* flag. gpsjam.org does not publish its per-aircraft threshold.
|
||||
* - Cell aggregation: gpsjam.org's published formula and 2% / 10% bands.
|
||||
* - Window: a GEV-defined rolling 30 minutes over the views the user visits.
|
||||
*/
|
||||
|
||||
/** Direct 1090ES ADS-B only: MLAT, TIS-B and ADS-R positions are not self-reported. */
|
||||
const QUALIFYING_TYPES = new Set(['adsb_icao']);
|
||||
/** NIC below 7 means a containment radius of 0.2 NM or more (below the 91.227 ADS-B Out minimum). */
|
||||
/** NIC below 7: containment radius of 0.2 NM or more (below the US 91.227 ADS-B Out minimum). GEV threshold. */
|
||||
export const GNSS_NIC_THRESHOLD = 7;
|
||||
/** NACp below 8 means an estimated position uncertainty of 93 m or more (below the 91.227 minimum). */
|
||||
/** NACp below 8: position uncertainty of 93 m or more (below the US 91.227 minimum). GEV threshold. */
|
||||
export const GNSS_NACP_THRESHOLD = 8;
|
||||
/** Positions older than this are not current evidence of the local sky. */
|
||||
const MAX_POSITION_AGE_S = 60;
|
||||
@@ -28,6 +33,65 @@ export const GNSS_CELL_DEG = 0.5;
|
||||
export const GNSS_MIN_AIRCRAFT = 3;
|
||||
/** Rolling window over which aircraft seen in a cell are counted. */
|
||||
export const GNSS_WINDOW_MS = 30 * 60_000;
|
||||
/** gpsjam.org band edges, in percent of aircraft (after discounting one). */
|
||||
export const GNSS_MEDIUM_PERCENT = 2;
|
||||
export const GNSS_HIGH_PERCENT = 10;
|
||||
|
||||
/** Primary cell state: what the data shows (low navigation accuracy). */
|
||||
export const GNSS_ANOMALY_STATE = 'navigation-integrity-anomaly';
|
||||
export const GNSS_NOMINAL_STATE = 'nominal';
|
||||
/** Secondary, unvalidated reading of an anomaly cell. */
|
||||
export const GNSS_INTERPRETATION = 'suspected-gnss-interference';
|
||||
|
||||
/** The per-aircraft rule; GEV-defined and not validated against known events. */
|
||||
export const GNSS_CLASSIFIER = Object.freeze({
|
||||
id: 'gev-nic-nacp-v1',
|
||||
rule: `gpsOkBefore set || NIC < ${GNSS_NIC_THRESHOLD} || NACp < ${GNSS_NACP_THRESHOLD}`,
|
||||
basis: '14 CFR 91.227(c) ADS-B Out performance minima (US regulation)',
|
||||
definedBy: 'GEV',
|
||||
validated: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Machine-readable provenance of the cell output: which part of the method
|
||||
* comes from where, and which parts are unvalidated.
|
||||
*
|
||||
* @param {{windowMs?: number}} [options] The window the layer actually uses.
|
||||
*/
|
||||
export function gnssProvenance({ windowMs = GNSS_WINDOW_MS } = {}) {
|
||||
return Object.freeze({
|
||||
classifier: GNSS_CLASSIFIER,
|
||||
aggregation: Object.freeze({
|
||||
id: 'gpsjam-cell-bands',
|
||||
formula: '100 * max(0, bad - 1) / total',
|
||||
bands: Object.freeze([
|
||||
GNSS_MEDIUM_PERCENT / 100,
|
||||
GNSS_HIGH_PERCENT / 100,
|
||||
]),
|
||||
source: 'https://gpsjam.org/faq',
|
||||
definedBy: 'gpsjam.org',
|
||||
}),
|
||||
window: Object.freeze({
|
||||
minutes: windowMs / 60_000,
|
||||
scope: 'visited-view',
|
||||
cellDeg: GNSS_CELL_DEG,
|
||||
minAircraft: GNSS_MIN_AIRCRAFT,
|
||||
definedBy: 'GEV',
|
||||
}),
|
||||
state: Object.freeze({
|
||||
id: GNSS_ANOMALY_STATE,
|
||||
meaning: 'low navigation accuracy reported by ADS-B aircraft',
|
||||
}),
|
||||
interpretation: Object.freeze({
|
||||
id: GNSS_INTERPRETATION,
|
||||
validated: false,
|
||||
note: 'one possible cause of an anomaly cell; avionics faults also report low values',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Provenance at the default window. */
|
||||
export const GNSS_PROVENANCE = gnssProvenance();
|
||||
|
||||
const HEX_PATTERN = /^~?[0-9a-f]{6}$/;
|
||||
|
||||
@@ -118,22 +182,32 @@ export function accumulateGnssObservations(
|
||||
}
|
||||
|
||||
/**
|
||||
* Interference level of a cell, using gpsjam.org's published cell formula and
|
||||
* bands: the first degraded aircraft is discounted to limit false positives,
|
||||
* then under 2% is low, 2–10% medium and over 10% high.
|
||||
* Navigation-integrity level of a cell, using gpsjam.org's published cell
|
||||
* formula and bands: the first degraded aircraft is discounted to limit false
|
||||
* positives, then under 2% is low, 2–10% medium and over 10% high. Medium and
|
||||
* high cells are navigation-integrity anomalies.
|
||||
*/
|
||||
export function gnssInterferenceLevel(total, degraded) {
|
||||
export function gnssIntegrityLevel(total, degraded) {
|
||||
const percent = total > 0 ? (100 * Math.max(0, degraded - 1)) / total : 0;
|
||||
const level =
|
||||
percent > GNSS_HIGH_PERCENT
|
||||
? 'high'
|
||||
: percent >= GNSS_MEDIUM_PERCENT
|
||||
? 'medium'
|
||||
: 'low';
|
||||
const anomaly = level !== 'low';
|
||||
return {
|
||||
percent,
|
||||
level: percent > 10 ? 'high' : percent >= 2 ? 'medium' : 'low',
|
||||
level,
|
||||
state: anomaly ? GNSS_ANOMALY_STATE : GNSS_NOMINAL_STATE,
|
||||
interpretation: anomaly ? GNSS_INTERPRETATION : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate the observation store into displayable cells.
|
||||
*
|
||||
* @returns {{id: string, south: number, west: number, north: number, east: number, lat: number, lon: number, aircraft: number, degraded: number, percentDegraded: number, level: 'low'|'medium'|'high'}[]}
|
||||
* @returns {{id: string, south: number, west: number, north: number, east: number, lat: number, lon: number, aircraft: number, degraded: number, percentDegraded: number, level: 'low'|'medium'|'high', state: string, interpretation: string|null}[]}
|
||||
*/
|
||||
export function binGnssCells(
|
||||
store,
|
||||
@@ -152,7 +226,10 @@ export function binGnssCells(
|
||||
const [row, col] = id.split(':').map(Number);
|
||||
const south = row * cellDeg - 90;
|
||||
const west = col * cellDeg - 180;
|
||||
const { percent, level } = gnssInterferenceLevel(aircraft, degraded);
|
||||
const { percent, level, state, interpretation } = gnssIntegrityLevel(
|
||||
aircraft,
|
||||
degraded,
|
||||
);
|
||||
cells.push({
|
||||
id,
|
||||
south,
|
||||
@@ -165,6 +242,8 @@ export function binGnssCells(
|
||||
degraded,
|
||||
percentDegraded: Math.round(percent * 10) / 10,
|
||||
level,
|
||||
state,
|
||||
interpretation,
|
||||
});
|
||||
}
|
||||
return cells.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
@@ -3,8 +3,11 @@ import test from 'node:test';
|
||||
import {
|
||||
accumulateGnssObservations,
|
||||
binGnssCells,
|
||||
GNSS_CLASSIFIER,
|
||||
GNSS_PROVENANCE,
|
||||
gnssCellKey,
|
||||
gnssInterferenceLevel,
|
||||
gnssIntegrityLevel,
|
||||
gnssProvenance,
|
||||
normalizeGnssAircraft,
|
||||
} from './records.js';
|
||||
|
||||
@@ -93,14 +96,86 @@ test('cell keys are stable half-degree bins, clamped at the poles and antimeridi
|
||||
assert.equal(gnssCellKey(-0.1, -0.1), '179:359');
|
||||
});
|
||||
|
||||
test('interference level follows the gpsjam.org published cell formula and bands', () => {
|
||||
assert.deepEqual(gnssInterferenceLevel(10, 0), { percent: 0, level: 'low' });
|
||||
test('integrity level follows the gpsjam.org published cell formula and bands', () => {
|
||||
const nominal = {
|
||||
percent: 0,
|
||||
level: 'low',
|
||||
state: 'nominal',
|
||||
interpretation: null,
|
||||
};
|
||||
assert.deepEqual(gnssIntegrityLevel(10, 0), nominal);
|
||||
// One degraded aircraft is always discounted.
|
||||
assert.deepEqual(gnssInterferenceLevel(10, 1), { percent: 0, level: 'low' });
|
||||
assert.equal(gnssInterferenceLevel(50, 2).level, 'medium');
|
||||
assert.equal(gnssInterferenceLevel(10, 2).level, 'medium');
|
||||
assert.equal(gnssInterferenceLevel(10, 3).level, 'high');
|
||||
assert.equal(gnssInterferenceLevel(0, 0).level, 'low');
|
||||
assert.deepEqual(gnssIntegrityLevel(10, 1), nominal);
|
||||
assert.equal(gnssIntegrityLevel(50, 2).level, 'medium');
|
||||
assert.equal(gnssIntegrityLevel(10, 2).level, 'medium');
|
||||
assert.equal(gnssIntegrityLevel(10, 3).level, 'high');
|
||||
assert.equal(gnssIntegrityLevel(0, 0).level, 'low');
|
||||
});
|
||||
|
||||
test('the primary state is a navigation-integrity anomaly; interference is only the secondary reading', () => {
|
||||
for (const [total, bad] of [
|
||||
[50, 2],
|
||||
[10, 3],
|
||||
]) {
|
||||
const { state, interpretation } = gnssIntegrityLevel(total, bad);
|
||||
assert.equal(state, 'navigation-integrity-anomaly');
|
||||
assert.equal(interpretation, 'suspected-gnss-interference');
|
||||
}
|
||||
const store = new Map();
|
||||
accumulateGnssObservations(
|
||||
store,
|
||||
[
|
||||
{ hex: 'h1', lat: 10.1, lon: 10.1, degraded: true },
|
||||
{ hex: 'h2', lat: 10.2, lon: 10.2, degraded: true },
|
||||
{ hex: 'h3', lat: 10.3, lon: 10.3, degraded: false },
|
||||
],
|
||||
0,
|
||||
);
|
||||
const [cell] = binGnssCells(store);
|
||||
assert.equal(cell.level, 'high');
|
||||
assert.equal(cell.state, 'navigation-integrity-anomaly');
|
||||
assert.equal(cell.interpretation, 'suspected-gnss-interference');
|
||||
for (const key of Object.keys(cell))
|
||||
assert.doesNotMatch(key, /interference|jamming|spoof/i);
|
||||
});
|
||||
|
||||
test('provenance separates the GEV classifier from the gpsjam aggregation and the GEV window', () => {
|
||||
assert.deepEqual(GNSS_PROVENANCE.classifier, {
|
||||
id: 'gev-nic-nacp-v1',
|
||||
rule: 'gpsOkBefore set || NIC < 7 || NACp < 8',
|
||||
basis: '14 CFR 91.227(c) ADS-B Out performance minima (US regulation)',
|
||||
definedBy: 'GEV',
|
||||
validated: false,
|
||||
});
|
||||
assert.equal(GNSS_PROVENANCE.classifier, GNSS_CLASSIFIER);
|
||||
assert.deepEqual(GNSS_PROVENANCE.aggregation, {
|
||||
id: 'gpsjam-cell-bands',
|
||||
formula: '100 * max(0, bad - 1) / total',
|
||||
bands: [0.02, 0.1],
|
||||
source: 'https://gpsjam.org/faq',
|
||||
definedBy: 'gpsjam.org',
|
||||
});
|
||||
assert.deepEqual(GNSS_PROVENANCE.window, {
|
||||
minutes: 30,
|
||||
scope: 'visited-view',
|
||||
cellDeg: 0.5,
|
||||
minAircraft: 3,
|
||||
definedBy: 'GEV',
|
||||
});
|
||||
assert.equal(GNSS_PROVENANCE.state.id, 'navigation-integrity-anomaly');
|
||||
assert.equal(
|
||||
GNSS_PROVENANCE.interpretation.id,
|
||||
'suspected-gnss-interference',
|
||||
);
|
||||
assert.equal(GNSS_PROVENANCE.interpretation.validated, false);
|
||||
assert.ok(Object.isFrozen(GNSS_PROVENANCE));
|
||||
assert.ok(Object.isFrozen(GNSS_PROVENANCE.aggregation.bands));
|
||||
// The bands in provenance are the ones the level function applies.
|
||||
const [medium, high] = GNSS_PROVENANCE.aggregation.bands;
|
||||
assert.equal(gnssIntegrityLevel(100, 1 + 100 * medium).level, 'medium');
|
||||
assert.equal(gnssIntegrityLevel(100, 1 + 100 * high).level, 'medium');
|
||||
assert.equal(gnssIntegrityLevel(100, 2 + 100 * high).level, 'high');
|
||||
assert.equal(gnssProvenance({ windowMs: 10 * 60_000 }).window.minutes, 10);
|
||||
});
|
||||
|
||||
test('observations count each aircraft once per cell and expire after the window', () => {
|
||||
|
||||
@@ -31,6 +31,8 @@ export function createAdsbGnssSource({
|
||||
? payload.fetchedAt
|
||||
: null,
|
||||
stale: payload.stale === true,
|
||||
classifier:
|
||||
typeof payload.classifier === 'string' ? payload.classifier : null,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,7 +8,11 @@ test('the source asks the same-origin proxy for the view anchor', async () => {
|
||||
fetchImpl: async (url, options) => {
|
||||
calls.push(url);
|
||||
assert.ok(options.signal instanceof AbortSignal);
|
||||
return Response.json({ fetchedAt: 5, rows: [{ hex: 'abc123' }] });
|
||||
return Response.json({
|
||||
fetchedAt: 5,
|
||||
classifier: 'gev-nic-nacp-v1',
|
||||
rows: [{ hex: 'abc123' }],
|
||||
});
|
||||
},
|
||||
});
|
||||
const snapshot = await source.getSnapshot(
|
||||
@@ -20,6 +24,7 @@ test('the source asks the same-origin proxy for the view anchor', async () => {
|
||||
rows: [{ hex: 'abc123' }],
|
||||
fetchedAt: 5,
|
||||
stale: false,
|
||||
classifier: 'gev-nic-nacp-v1',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +35,7 @@ test('the source reports stale proxy data and rejects bad input or responses', a
|
||||
}).getSnapshot({ latitude: 1, longitude: 2 });
|
||||
assert.equal(stale.stale, true);
|
||||
assert.equal(stale.fetchedAt, null);
|
||||
assert.equal(stale.classifier, null);
|
||||
|
||||
await assert.rejects(
|
||||
createAdsbGnssSource({ fetchImpl: reply({}) }).getSnapshot({}),
|
||||
|
||||
Reference in new issue
Block a user