From 4779432e48ae8ba64bfdcd7ec2d2fac23fe30f2b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 22:53:35 -0800 Subject: [PATCH] Read a satellite's launch year from one TLE parser The catalog stored the designator on satrec.intldesg for internationalDesignatorYear, while the active-TLE lookup parsed the same columns with tleLineLaunchYear. Each catalog entry now keeps launchYear from tleLineLaunchYear, which moves to src/sources/tle.js beside the other TLE field readers, and internationalDesignatorYear goes. A blank designator now reads as an unknown year in both lookups; the active-TLE lookup read it as 2000. --- src/layers/satellites/catalog.js | 5 ++--- src/layers/satellites/ingestion.js | 6 +++--- src/layers/satellites/ingestion.test.mjs | 23 ++++++++++++++++++++++- src/layers/satellites/orbits.js | 22 ++++++---------------- src/sources/tle.js | 13 +++++++++++++ 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/layers/satellites/catalog.js b/src/layers/satellites/catalog.js index 008b2a19d..f4a347af2 100644 --- a/src/layers/satellites/catalog.js +++ b/src/layers/satellites/catalog.js @@ -1,6 +1,6 @@ import * as Cesium from 'cesium'; import { twoline2satrec } from 'satellite.js'; -import { noradIdFromSatnum } from '../../sources/tle.js'; +import { noradIdFromSatnum, tleLineLaunchYear } from '../../sources/tle.js'; import { DENSE_REFRESH_FRAMES, DENSE_GROUP_PATH, @@ -114,8 +114,6 @@ 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; - // satellite.js omits the designator the launch-year match reads. - satrec.intldesg = entry.line1.slice(9, 17).trim(); const noradId = noradIdFromSatnum(satrec.satnum); if (layerState._catalog.has(noradId)) continue; // core catalog keeps priority const pos = parts.orbits.propagatePosition(satrec, now); @@ -125,6 +123,7 @@ export function createCatalog({ state: layerState, services, parts, source }) { name: entry.name, satrec, group: 'dense', + launchYear: tleLineLaunchYear(entry.line1), }); const point = layerState._pointCollection.add({ position: Cesium.Cartesian3.fromDegrees( diff --git a/src/layers/satellites/ingestion.js b/src/layers/satellites/ingestion.js index 168a5903b..d2cc16e15 100644 --- a/src/layers/satellites/ingestion.js +++ b/src/layers/satellites/ingestion.js @@ -1,7 +1,7 @@ import { twoline2satrec } from 'satellite.js'; import * as Cesium from 'cesium'; import { msg } from '../../i18n/index.js'; -import { noradIdFromSatnum } from '../../sources/tle.js'; +import { noradIdFromSatnum, tleLineLaunchYear } from '../../sources/tle.js'; import { CATALOG_GROUPS, ISS_NORAD, POINT_STYLES } from './policy.js'; export function createIngestion({ @@ -119,8 +119,6 @@ export function createIngestion({ for (const entry of allEntries) { const satrec = twoline2satrec(entry.line1, entry.line2); if (!satrec || satrec.error !== 0) continue; - // satellite.js omits the designator the launch-year match reads. - satrec.intldesg = entry.line1.slice(9, 17).trim(); const noradId = noradIdFromSatnum(satrec.satnum); if (seen.has(noradId)) continue; @@ -131,6 +129,8 @@ export function createIngestion({ name: entry.name, satrec, group: entry.group, + // satellite.js does not keep the designator the year match reads. + launchYear: tleLineLaunchYear(entry.line1), }); coreStale ||= entry.stale; diff --git a/src/layers/satellites/ingestion.test.mjs b/src/layers/satellites/ingestion.test.mjs index 7605b318c..beea1a518 100644 --- a/src/layers/satellites/ingestion.test.mjs +++ b/src/layers/satellites/ingestion.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { parseTleText } from '../../sources/tle.js'; +import { parseTleText, tleLineLaunchYear } from '../../sources/tle.js'; import { createIngestion } from './ingestion.js'; import { createOrbits } from './orbits.js'; @@ -98,3 +98,24 @@ test('a mission payload does not match a loaded satellite from another launch ye assert.equal(track('2026-06-29T02:25:00Z')?.noradId, 69728); assert.equal(track('2030-06-29T02:25:00Z'), null); }); + +test('a satellite with a blank designator keeps matching by name', async (t) => { + t.mock.method(console, 'log', () => {}); + t.mock.method(console, 'warn', () => {}); + const { state, update } = ingestionFor( + [ + 'SXM-11', + '1 69728U 26208.65166678 .00000015 00000+0 00000+0 0 9990', + '2 69728 0.0784 264.9826 0001797 240.4588 273.9506 1.00271527 440', + ].join('\n'), + ); + await update(); + const { getSatelliteOrbitTrack } = createOrbits({ state }); + assert.equal(tleLineLaunchYear('1 69728U 26208.65166678'), null); + assert.equal( + getSatelliteOrbitTrack('Sirius SXM-11', { + launchTime: '2030-06-29T02:25:00Z', + })?.noradId, + 69728, + ); +}); diff --git a/src/layers/satellites/orbits.js b/src/layers/satellites/orbits.js index 0e4461e85..a2bfbd939 100644 --- a/src/layers/satellites/orbits.js +++ b/src/layers/satellites/orbits.js @@ -8,7 +8,11 @@ import { twoline2satrec, } from 'satellite.js'; import { findNextSatellitePass } from '../../data/satellitePass.js'; -import { noradIdFromSatnum, parseTleText } from '../../sources/tle.js'; +import { + noradIdFromSatnum, + parseTleText, + tleLineLaunchYear, +} from '../../sources/tle.js'; import { ORBIT_PATH_STEPS, ISS_NORAD } from './policy.js'; export function createOrbits({ state: layerState, services, parts, source }) { @@ -258,13 +262,6 @@ export function createOrbits({ state: layerState, services, parts, source }) { return score; } - function internationalDesignatorYear(satrec) { - const match = String(satrec?.intldesg || '').match(/^(\d{2})/); - if (!match) return null; - const shortYear = Number(match[1]); - return shortYear >= 57 ? 1900 + shortYear : 2000 + shortYear; - } - function lookupTleEntries(tleText) { if (tleText !== layerState._lookupTleText) { layerState._lookupTleText = tleText; @@ -273,12 +270,6 @@ export function createOrbits({ state: layerState, services, parts, source }) { return layerState._lookupTleEntries; } - function tleLineLaunchYear(line1) { - const shortYear = Number(String(line1 || '').slice(9, 11)); - if (!Number.isFinite(shortYear)) return null; - return shortYear >= 57 ? 1900 + shortYear : 2000 + shortYear; - } - function orbitTrackFromRecord(name, satrec) { const referenceDate = new Date(); const current = propagatePosition(satrec, referenceDate); @@ -350,7 +341,7 @@ export function createOrbits({ state: layerState, services, parts, source }) { : null; let bestScore = 0; for (const [id, sat] of layerState._catalog) { - const designatorYear = internationalDesignatorYear(sat.satrec); + const designatorYear = sat.launchYear ?? null; if ( launchYear !== null && designatorYear !== null && @@ -379,7 +370,6 @@ export function createOrbits({ state: layerState, services, parts, source }) { getNextSatellitePass, resolveSatelliteForPass, scoreSatelliteNameMatch, - internationalDesignatorYear, lookupTleEntries, tleLineLaunchYear, orbitTrackFromRecord, diff --git a/src/sources/tle.js b/src/sources/tle.js index 61c409a2b..b34280787 100644 --- a/src/sources/tle.js +++ b/src/sources/tle.js @@ -72,3 +72,16 @@ export function tleCatalogNumber(line) { const field = String(line).slice(2, 7); return decodeCatalogField(field.replace(/^ +/, '')); } + +/** + * Launch year from TLE line 1's international designator (columns 10-11); + * null when the designator is blank. + * @param {string} line1 + * @returns {number|null} + */ +export function tleLineLaunchYear(line1) { + const digits = String(line1 || '').slice(9, 11); + if (!/^\d{2}$/.test(digits)) return null; + const shortYear = Number(digits); + return shortYear >= 57 ? 1900 + shortYear : 2000 + shortYear; +}