mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Decode catalog numbers strictly and add the changelog fragment
Route tleCatalogNumber() and noradIdFromSatnum() through one decoder that accepts only plain digits or a valid Alpha-5 field. parseInt and Number() read '2554A', '255 4', '-1234', '0x1F0', '1.5' and blank as numbers; they now return null/NaN. Digit satnums of any length still decode, so OMM satrecs numbered past 99999 keep working. Add a layer test that several Alpha-5 satellites survive ingestion dedupe, the changelog fragment for #767, and note in CURRENT-STATE that this is Alpha-5 TLE compatibility and malformed fields are rejected.
This commit is contained in:
1 parent
6ae854c121
commit
7ec84ac316
5 files changed
+147
-34
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseTleText } from '../../sources/tle.js';
|
||||
import { createIngestion } from './ingestion.js';
|
||||
|
||||
// The ISS set (1 January 2024) with its catalog field swapped per satellite.
|
||||
const ISS_LINE1 =
|
||||
'1 25544U 98067A 24001.50000000 .00016717 00000-0 30370-3 0 9993';
|
||||
const ISS_LINE2 =
|
||||
'2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.50377579432323';
|
||||
|
||||
function tleSet(name, field) {
|
||||
return [
|
||||
name,
|
||||
ISS_LINE1.replace('25544', field),
|
||||
ISS_LINE2.replace('25544', field),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function ingestionFor(catalogText) {
|
||||
const state = {
|
||||
_trackingRefreshEpoch: 0,
|
||||
_activeUpdateControllers: new Set(),
|
||||
_pointCollection: {
|
||||
removeAll() {},
|
||||
add: (point) => point,
|
||||
},
|
||||
_points: new Map(),
|
||||
_orbitPaths: new Map(),
|
||||
_catalog: new Map(),
|
||||
_detectionObjects: new Map(),
|
||||
_denseLoadToken: 0,
|
||||
_catalogRevision: 0,
|
||||
_params: { catalog: 'core', showOrbits: false },
|
||||
};
|
||||
const parts = {
|
||||
orbits: {
|
||||
parseTLE: parseTleText,
|
||||
propagatePosition: () => ({ longitude: 0, latitude: 0, altitude: 4e5 }),
|
||||
},
|
||||
labels: { _syncIssOverlay() {} },
|
||||
controls: { _pointStyleFor: () => ({}) },
|
||||
rendering: { _showOrbitPath() {} },
|
||||
tracking: {
|
||||
async _reconcileTrackedSubjectContext() {},
|
||||
_applyPendingTrackingRestore() {},
|
||||
},
|
||||
catalog: {},
|
||||
};
|
||||
const source = {
|
||||
async readGroup() {
|
||||
return { ok: true, text: catalogText };
|
||||
},
|
||||
};
|
||||
const { methods } = createIngestion({ state, services: {}, parts, source });
|
||||
return { state, update: () => methods.update({ scene: {} }) };
|
||||
}
|
||||
|
||||
test('every Alpha-5 satellite keeps its own catalog entry through dedupe', async (t) => {
|
||||
t.mock.method(console, 'log', () => {});
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
const { state, update } = ingestionFor(
|
||||
[
|
||||
tleSet('FIRST', 'A0000'),
|
||||
tleSet('SECOND', 'B0001'),
|
||||
tleSet('THIRD', 'Z9999'),
|
||||
].join('\n'),
|
||||
);
|
||||
await update();
|
||||
assert.deepEqual(
|
||||
[...state._catalog].map(([id, entry]) => [id, entry.name]),
|
||||
[
|
||||
[100000, 'FIRST'],
|
||||
[110001, 'SECOND'],
|
||||
[339999, 'THIRD'],
|
||||
],
|
||||
);
|
||||
assert.deepEqual([...state._points.keys()], [100000, 110001, 339999]);
|
||||
assert.equal(state._count, 3);
|
||||
});
|
||||
+25
-28
@@ -21,50 +21,47 @@ export function parseTleText(text) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Alpha-5: a letter A–Z, skipping I and O, then exactly four digits.
|
||||
const ALPHA5 = /^[A-HJ-NP-Z]\d{4}$/;
|
||||
const DIGITS = /^\d+$/;
|
||||
|
||||
/**
|
||||
* The catalog number an Alpha-5 field stands for, or null for any other field.
|
||||
* The catalog number a catalog field stands for, or null for any other field.
|
||||
*
|
||||
* Since the catalog passed 99999 (July 2026) a TLE writes the numbers above it
|
||||
* in Alpha-5: a letter A–Z, skipping I and O, stands for 10–33 in the
|
||||
* ten-thousands place, so 100000 is "A0000" and 339999 is "Z9999".
|
||||
* A field is either plain digits or Alpha-5, the TLE spelling of numbers above
|
||||
* 99999: the letter stands for 10–33 in the ten-thousands place, skipping I
|
||||
* and O, so 100000 is "A0000" and 339999 is "Z9999". Anything else (a sign, a
|
||||
* decimal point, hex, an embedded space, a lowercase or trailing letter) is
|
||||
* not a catalog number.
|
||||
* @param {string} field the field with any leading spaces already removed
|
||||
* @returns {number | null}
|
||||
*/
|
||||
function alpha5CatalogNumber(field) {
|
||||
function decodeCatalogField(field) {
|
||||
if (DIGITS.test(field)) return Number(field);
|
||||
if (!ALPHA5.test(field)) return null;
|
||||
const letter = field[0];
|
||||
if (
|
||||
letter >= 'A' &&
|
||||
letter <= 'Z' &&
|
||||
letter !== 'I' &&
|
||||
letter !== 'O' &&
|
||||
/^\d{4}$/.test(field.slice(1))
|
||||
) {
|
||||
let tens = letter.charCodeAt(0) - 65 + 10;
|
||||
if (letter > 'I') tens -= 1;
|
||||
if (letter > 'O') tens -= 1;
|
||||
return tens * 10000 + Number(field.slice(1));
|
||||
}
|
||||
return null;
|
||||
let tens = letter.charCodeAt(0) - 65 + 10;
|
||||
if (letter > 'I') tens -= 1;
|
||||
if (letter > 'O') tens -= 1;
|
||||
return tens * 10000 + Number(field.slice(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* NORAD catalog number of a satrec as a number.
|
||||
* NORAD catalog number of a satrec as a number, or NaN.
|
||||
*
|
||||
* satellite.js keeps the TLE's five-character field verbatim in
|
||||
* `satrec.satnum`, and `Number("A0000")` is NaN, so every Alpha-5 satellite
|
||||
* would share one NaN key. An Alpha-5 field decodes to its number; a
|
||||
* five-digit field is unchanged; anything else stays NaN.
|
||||
* would share one NaN key. Digits of any length (an OMM satrec carries the
|
||||
* number unpadded) and Alpha-5 decode; anything else, blank included, is NaN.
|
||||
* @param {string} satnum
|
||||
* @returns {number}
|
||||
*/
|
||||
export function noradIdFromSatnum(satnum) {
|
||||
const field = String(satnum ?? '').trim();
|
||||
return alpha5CatalogNumber(field) ?? Number(field);
|
||||
return decodeCatalogField(String(satnum ?? '').trim()) ?? Number.NaN;
|
||||
}
|
||||
|
||||
/** The NORAD catalog number from TLE line 1, or null. */
|
||||
/** The NORAD catalog number from TLE line 1 (columns 3–7), or null. */
|
||||
export function tleCatalogNumber(line1) {
|
||||
const field = String(line1).slice(2, 7);
|
||||
const alpha5 = alpha5CatalogNumber(field);
|
||||
if (alpha5 !== null) return alpha5;
|
||||
const number = Number.parseInt(field, 10);
|
||||
return Number.isInteger(number) ? number : null;
|
||||
return decodeCatalogField(field.replace(/^ +/, ''));
|
||||
}
|
||||
@@ -72,14 +72,39 @@ test('Alpha-5 catalog numbers decode, skipping I and O', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('every other catalog field reads as it did before', () => {
|
||||
test('a digit catalog field reads as it did before', () => {
|
||||
assert.equal(noradIdFromSatnum('25544'), 25544);
|
||||
assert.equal(noradIdFromSatnum('00404'), 404);
|
||||
assert.equal(noradIdFromSatnum(''), 0); // as Number('') did
|
||||
for (const field of ['I0000', 'O0000', 'a0404', 'A00']) {
|
||||
assert.ok(Number.isNaN(noradIdFromSatnum(field)), field);
|
||||
assert.equal(tleCatalogNumber(`1 ${field}U`), null, field);
|
||||
assert.equal(noradIdFromSatnum(' 404'), 404);
|
||||
assert.equal(tleCatalogNumber('1 25544U'), 25544);
|
||||
assert.equal(tleCatalogNumber('1 404U'), 404);
|
||||
// An OMM satrec carries the number unpadded, past five digits.
|
||||
assert.equal(noradIdFromSatnum('100000'), 100000);
|
||||
});
|
||||
|
||||
test('a malformed catalog field is no number', () => {
|
||||
for (const field of [
|
||||
'I0000',
|
||||
'O0000',
|
||||
'a0404',
|
||||
'A00',
|
||||
'2554A',
|
||||
'255 4',
|
||||
'-1234',
|
||||
'0x1F0',
|
||||
'1.5',
|
||||
'1e3',
|
||||
'',
|
||||
' ',
|
||||
]) {
|
||||
assert.ok(Number.isNaN(noradIdFromSatnum(field)), JSON.stringify(field));
|
||||
assert.equal(
|
||||
tleCatalogNumber(`1 ${field.padEnd(5)}U`),
|
||||
null,
|
||||
JSON.stringify(field),
|
||||
);
|
||||
}
|
||||
assert.ok(Number.isNaN(noradIdFromSatnum(undefined)));
|
||||
});
|
||||
|
||||
test('a satellite numbered above 99999 keeps its number and is found by it', async () => {
|
||||
|
||||
Reference in new issue
Block a user