mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(satellites): decode Alpha-5 catalog numbers in the shared TLE module
Catalog numbers above 99999 arrive in a TLE as Alpha-5: a letter A-Z, skipping I and O, stands for 10-33 in the ten-thousands place, so 100000 is A0000. satellite.js keeps the field as the string satrec.satnum, and the three catalog builders converted it with Number(), which gives NaN; the Set and Map used to deduplicate and store treat NaN as equal to NaN, so the first such satellite was stored under NaN and every later one was dropped as a duplicate (#751). The satellite tools read the same field with parseInt and reported no number, so such a satellite could not be found by it. The decoder lives in src/sources/tle.js, beside parseTleText, which the layer and the tools both import already: noradIdFromSatnum() for the three builders, and tleCatalogNumber() decodes through the same function. Every other field reads as it did before: Number() in the layer, parseInt or null in the tools.
This commit is contained in:
1 parent
79b9de07d8
commit
ac62542641
5 files changed
+100
-6
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { twoline2satrec } from 'satellite.js';
|
||||
import { noradIdFromSatnum } from '../../sources/tle.js';
|
||||
import {
|
||||
DENSE_REFRESH_FRAMES,
|
||||
DENSE_GROUP_PATH,
|
||||
@@ -113,7 +114,7 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
const entry = entries[i];
|
||||
const satrec = twoline2satrec(entry.line1, entry.line2);
|
||||
if (!satrec || satrec.error !== 0) continue;
|
||||
const noradId = Number(satrec.satnum);
|
||||
const noradId = noradIdFromSatnum(satrec.satnum);
|
||||
if (layerState._catalog.has(noradId)) continue; // core catalog keeps priority
|
||||
const pos = parts.orbits.propagatePosition(satrec, now);
|
||||
if (!pos) continue;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { twoline2satrec } from 'satellite.js';
|
||||
import * as Cesium from 'cesium';
|
||||
import { noradIdFromSatnum } from '../../sources/tle.js';
|
||||
import { CATALOG_GROUPS, ISS_NORAD, POINT_STYLES } from './policy.js';
|
||||
|
||||
export function createIngestion({
|
||||
@@ -108,7 +109,7 @@ export function createIngestion({
|
||||
const satrec = twoline2satrec(entry.line1, entry.line2);
|
||||
if (!satrec || satrec.error !== 0) continue;
|
||||
|
||||
const noradId = Number(satrec.satnum);
|
||||
const noradId = noradIdFromSatnum(satrec.satnum);
|
||||
if (seen.has(noradId)) continue;
|
||||
seen.add(noradId);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
twoline2satrec,
|
||||
} from 'satellite.js';
|
||||
import { findNextSatellitePass } from '../../data/satellitePass.js';
|
||||
import { parseTleText } from '../../sources/tle.js';
|
||||
import { noradIdFromSatnum, parseTleText } from '../../sources/tle.js';
|
||||
import { ORBIT_PATH_STEPS, ISS_NORAD } from './policy.js';
|
||||
|
||||
export function createOrbits({ state: layerState, services, parts, source }) {
|
||||
@@ -284,7 +284,7 @@ export function createOrbits({ state: layerState, services, parts, source }) {
|
||||
const current = propagatePosition(satrec, referenceDate);
|
||||
if (!current) return null;
|
||||
return {
|
||||
noradId: Number(satrec.satnum),
|
||||
noradId: noradIdFromSatnum(satrec.satnum),
|
||||
name: String(name || '').trim(),
|
||||
current,
|
||||
periodSec: orbitalPeriodSeconds(satrec),
|
||||
|
||||
+43
-1
@@ -21,8 +21,50 @@ export function parseTleText(text) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The catalog number an Alpha-5 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".
|
||||
*/
|
||||
function alpha5CatalogNumber(field) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* NORAD catalog number of a satrec as a number.
|
||||
*
|
||||
* 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.
|
||||
* @param {string} satnum
|
||||
* @returns {number}
|
||||
*/
|
||||
export function noradIdFromSatnum(satnum) {
|
||||
const field = String(satnum ?? '').trim();
|
||||
return alpha5CatalogNumber(field) ?? Number(field);
|
||||
}
|
||||
|
||||
/** The NORAD catalog number from TLE line 1, or null. */
|
||||
export function tleCatalogNumber(line1) {
|
||||
const number = Number.parseInt(String(line1).slice(2, 7), 10);
|
||||
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;
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { twoline2satrec } from 'satellite.js';
|
||||
import { composeCatalog, coreTools } from '../index.js';
|
||||
import { parseTleText, tleCatalogNumber } from '../../sources/tle.js';
|
||||
import {
|
||||
noradIdFromSatnum,
|
||||
parseTleText,
|
||||
tleCatalogNumber,
|
||||
} from '../../sources/tle.js';
|
||||
|
||||
// Element sets published for 1 January 2024.
|
||||
const TLE = `ISS (ZARYA)
|
||||
@@ -51,6 +56,51 @@ for (const missingLine of [1, 2]) {
|
||||
});
|
||||
}
|
||||
|
||||
test('Alpha-5 catalog numbers decode, skipping I and O', () => {
|
||||
for (const [field, id] of [
|
||||
['A0000', 100000],
|
||||
['A0404', 100404],
|
||||
['H9999', 179999],
|
||||
['J0000', 180000],
|
||||
['N9999', 229999],
|
||||
['P0000', 230000],
|
||||
['T0449', 270449],
|
||||
['Z9999', 339999],
|
||||
]) {
|
||||
assert.equal(noradIdFromSatnum(field), id, field);
|
||||
assert.equal(tleCatalogNumber(`1 ${field}U`), id, field);
|
||||
}
|
||||
});
|
||||
|
||||
test('every other 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);
|
||||
}
|
||||
});
|
||||
|
||||
test('a satellite numbered above 99999 keeps its number and is found by it', async () => {
|
||||
// The ISS set with its catalog field in Alpha-5: A0000 is 100000.
|
||||
const text = TLE.replaceAll('25544', 'A0000');
|
||||
const [iss] = parseTleText(text);
|
||||
const { satnum } = twoline2satrec(iss.line1, iss.line2);
|
||||
assert.ok(Number.isNaN(Number(satnum))); // what the catalog builders used
|
||||
assert.equal(noradIdFromSatnum(satnum), 100000);
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { satellites: satellites(text), clock: CLOCK },
|
||||
});
|
||||
const result = await catalog.call('next_satellite_pass', {
|
||||
location: { lat: 40.7, lon: -74 },
|
||||
satellite: '100000',
|
||||
});
|
||||
assert.equal(result.data.satellite, 'ISS (ZARYA)');
|
||||
assert.equal(result.data.norad, 100000);
|
||||
});
|
||||
|
||||
test('the next pass defaults to the ISS and reports times, peak and direction', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
|
||||
Reference in new issue
Block a user