mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Merge branch 'main' into contrib/gods-next-31-gbfs-unknown-counts-20261013
This commit is contained in:
91 files changed
+8631
-122
No files matched your search
@@ -7,10 +7,12 @@ import {
|
||||
finite,
|
||||
} from './contract.js';
|
||||
|
||||
const openSkyId = (row) =>
|
||||
Array.isArray(row) ? cleanText(row[0]).toLowerCase() : '';
|
||||
|
||||
/** Normalize the OpenSky state-vector route, including its regional fallback. */
|
||||
export function normalizeOpenSkyAircraft(row) {
|
||||
if (!Array.isArray(row)) return null;
|
||||
const id = cleanText(row[0]).toLowerCase();
|
||||
const id = openSkyId(row);
|
||||
if (!id || !coordinates(row[6], row[5])) return null;
|
||||
return {
|
||||
id,
|
||||
@@ -77,6 +79,16 @@ export function normalizeReadsbAircraft(row, snapshotTimeMs) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenSky allows null longitude/latitude for a contact that is still
|
||||
* broadcasting. Strict nulls also reject rows truncated before the position.
|
||||
*/
|
||||
function openSkyUnpositionedId(row) {
|
||||
return Array.isArray(row) && row[5] === null && row[6] === null
|
||||
? openSkyId(row)
|
||||
: '';
|
||||
}
|
||||
|
||||
export function openSkySnapshot(
|
||||
payload,
|
||||
{
|
||||
@@ -96,6 +108,7 @@ export function openSkySnapshot(
|
||||
return record;
|
||||
},
|
||||
'OpenSky',
|
||||
openSkyUnpositionedId,
|
||||
);
|
||||
const observedAtMs = epoch(payload?.time, 1000);
|
||||
const ageMs = observedAtMs == null ? null : Math.max(0, now - observedAtMs);
|
||||
|
||||
@@ -53,17 +53,30 @@ export function coordinates(latitude, longitude) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Admit a snapshot atomically; an all-invalid nonempty feed is unavailable. */
|
||||
export function admitRecords(rows, normalize, label) {
|
||||
/**
|
||||
* Admit a snapshot atomically; an all-invalid nonempty feed is unavailable.
|
||||
* `unpositionedId(row)` names a rejected row that is a valid contact without
|
||||
* a position: it is returned in `retainIds` and does not make the snapshot
|
||||
* incomplete.
|
||||
*/
|
||||
export function admitRecords(rows, normalize, label, unpositionedId) {
|
||||
if (!Array.isArray(rows))
|
||||
throw new LiveSourceError('malformed', `Malformed ${label} response`);
|
||||
const records = [];
|
||||
const ids = new Set();
|
||||
const retainIds = new Set();
|
||||
let unpositioned = 0;
|
||||
for (const row of rows) {
|
||||
const record = normalize(row);
|
||||
if (record && !ids.has(record.id)) {
|
||||
records.push(record);
|
||||
ids.add(record.id);
|
||||
} else if (!record) {
|
||||
const id = unpositionedId?.(row);
|
||||
if (id) {
|
||||
retainIds.add(id);
|
||||
unpositioned += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rows.length && !records.length) {
|
||||
@@ -71,7 +84,8 @@ export function admitRecords(rows, normalize, label) {
|
||||
}
|
||||
return {
|
||||
records,
|
||||
complete: records.length === rows.length,
|
||||
retainIds,
|
||||
complete: records.length + unpositioned === rows.length,
|
||||
rejectedCount: rows.length - records.length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,6 +67,50 @@ test('mixed-invalid and duplicate admissions are explicitly incomplete; all-inva
|
||||
assert.equal(openSkySnapshot({ states: [] }).complete, true);
|
||||
});
|
||||
|
||||
test('position-less OpenSky rows are seen, not missing; malformed rows stay incomplete', () => {
|
||||
// OpenSky's spec allows null longitude/latitude; every live /states/all
|
||||
// carries such rows.
|
||||
const unpositioned = ['DEF456', ...aircraft.slice(1)];
|
||||
unpositioned[5] = null;
|
||||
unpositioned[6] = null;
|
||||
const snapshot = openSkySnapshot({ states: [aircraft, unpositioned] });
|
||||
assert.equal(snapshot.records.length, 1);
|
||||
assert.equal(snapshot.complete, true);
|
||||
assert.deepEqual([...snapshot.retainIds], ['def456']);
|
||||
assert.equal(snapshot.rejectedCount, 1);
|
||||
const malformed = [...aircraft];
|
||||
malformed[5] = 181;
|
||||
const garbled = openSkySnapshot({
|
||||
states: [aircraft, unpositioned, malformed],
|
||||
});
|
||||
assert.equal(garbled.complete, false);
|
||||
assert.deepEqual([...garbled.retainIds], ['def456']);
|
||||
// No usable id, only half a position, a truncated row, or no row at all.
|
||||
for (const row of [
|
||||
[' ', ...unpositioned.slice(1)],
|
||||
Object.assign([...unpositioned], { 5: -97 }),
|
||||
['DEF456'],
|
||||
null,
|
||||
])
|
||||
assert.equal(openSkySnapshot({ states: [aircraft, row] }).complete, false);
|
||||
// Repeated position-less rows stay complete.
|
||||
const repeated = openSkySnapshot({
|
||||
states: [aircraft, unpositioned, unpositioned],
|
||||
});
|
||||
assert.equal(repeated.complete, true);
|
||||
// A feed with no positions at all still cannot prove absence.
|
||||
assert.throws(() => openSkySnapshot({ states: [unpositioned] }), {
|
||||
code: 'malformed',
|
||||
});
|
||||
// Military readsb admission keeps its existing semantics.
|
||||
const military = readsbSnapshot(
|
||||
{ ac: [{ hex: 'abc123', lat: 30, lon: -97 }, { hex: 'def456' }] },
|
||||
{ observedAtMs: null },
|
||||
);
|
||||
assert.equal(military.complete, false);
|
||||
assert.equal(military.retainIds.size, 0);
|
||||
});
|
||||
|
||||
test('readsb units and position/contact ages are normalized once against the source epoch', () => {
|
||||
const row = normalizeReadsbAircraft(
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createUsgsEarthquakeSource } from '../layers/earthquakes/source.js';
|
||||
import { createEcccAirQualitySource } from '../layers/airQuality/source.js';
|
||||
import { createWfigsPerimeterSource } from '../layers/perimeters/source.js';
|
||||
import { createBundledCableSource } from '../layers/submarineCables/bundledSource.js';
|
||||
import { createAdsbGnssSource } from '../layers/gnss/source.js';
|
||||
@@ -7,6 +8,7 @@ import { createAdsbGnssSource } from '../layers/gnss/source.js';
|
||||
export function createReferenceSources() {
|
||||
return {
|
||||
earthquakes: createUsgsEarthquakeSource(),
|
||||
'air-quality': createEcccAirQualitySource(),
|
||||
'fire-perimeters': createWfigsPerimeterSource(),
|
||||
cables: createBundledCableSource(),
|
||||
'gnss-integrity': createAdsbGnssSource(),
|
||||
|
||||
@@ -14,15 +14,18 @@ test('reference factories retain compatibility without starting acquisition or s
|
||||
const second = createReferenceSources();
|
||||
assert.deepEqual(Object.keys(first), [
|
||||
'earthquakes',
|
||||
'air-quality',
|
||||
'fire-perimeters',
|
||||
'cables',
|
||||
'gnss-integrity',
|
||||
]);
|
||||
assert.notEqual(first.earthquakes, second.earthquakes);
|
||||
assert.notEqual(first['air-quality'], second['air-quality']);
|
||||
assert.notEqual(first['fire-perimeters'], second['fire-perimeters']);
|
||||
assert.notEqual(first.cables, second.cables);
|
||||
assert.notEqual(first['gnss-integrity'], second['gnss-integrity']);
|
||||
assert.equal(typeof first.earthquakes.getSnapshot, 'function');
|
||||
assert.equal(typeof first['air-quality'].getSnapshot, 'function');
|
||||
assert.equal(typeof first['fire-perimeters'].getSnapshot, 'function');
|
||||
assert.equal(typeof first.cables.fetch, 'function');
|
||||
assert.equal(typeof first['gnss-integrity'].getSnapshot, 'function');
|
||||
|
||||
@@ -328,8 +328,12 @@ export function createTransitHistory({
|
||||
t > now + 10000
|
||||
)
|
||||
continue;
|
||||
const byVehicle = feed.vehicleMode?.(record.id);
|
||||
const mode =
|
||||
feed.routeMode?.(record.routeId) || feed.defaultMode || 'unknown';
|
||||
(byVehicle !== 'unknown' && byVehicle) ||
|
||||
feed.routeMode?.(record.routeId) ||
|
||||
feed.defaultMode ||
|
||||
'unknown';
|
||||
const context = contextOf(record, mode);
|
||||
if (!context) continue;
|
||||
const key = `${feed.id}\u0000${record.id}`;
|
||||
|
||||
Reference in new issue
Block a user