mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
fix(satellites): classes read every group a TLE came in, and current SATCAT
Deduplication kept only the first group's tag, so a recent imaging satellite in both visual and resource, with no UCS record or known name, fell to Probes. The catalog entry now keeps every group, and the last-resort class is the first of them that names one. SATCAT groups merged in group order, so a stale cached copy read later overwrote a current record of the same object: a satellite SATCAT now calls dead stayed in Imaging. A stale copy no longer overrules a current read, from the same merge or an earlier one.
This commit is contained in:
1 parent
c84cfcadeb
commit
8bd16b0be9
3 files changed
+87
-9
No files matched your search
@@ -147,10 +147,14 @@ function launchYearOf(launchDate) {
|
|||||||
/**
|
/**
|
||||||
* The class a satellite belongs to.
|
* The class a satellite belongs to.
|
||||||
* @param {{noradId?: number, name?: string, group?: string|null,
|
* @param {{noradId?: number, name?: string, group?: string|null,
|
||||||
|
* groups?: string[]|null,
|
||||||
* ucs?: {purpose?: string|null}|null,
|
* ucs?: {purpose?: string|null}|null,
|
||||||
* satcat?: {objectType?: string, opsStatus?: string,
|
* satcat?: {objectType?: string, opsStatus?: string,
|
||||||
* launchDate?: string|null}|null,
|
* launchDate?: string|null}|null,
|
||||||
* launchYear?: number|null, nowYear?: number}} subject `launchYear` is
|
* launchYear?: number|null, nowYear?: number}} subject `groups` is every
|
||||||
|
* CelesTrak group the TLE came in (`group` is the one it is shown under):
|
||||||
|
* the first of them that names a class gives the last-resort class, so a
|
||||||
|
* satellite in both `visual` and `resource` is still imaging. `launchYear` is
|
||||||
* the TLE designator's; SATCAT's launch date wins where it has one (an
|
* the TLE designator's; SATCAT's launch date wins where it has one (an
|
||||||
* object deployed from the ISS carries the station's 1998 designator).
|
* object deployed from the ISS carries the station's 1998 designator).
|
||||||
* @returns {'stations'|'imaging'|'telecom'|'starlink'|'probes'|'debris'}
|
* @returns {'stations'|'imaging'|'telecom'|'starlink'|'probes'|'debris'}
|
||||||
@@ -161,6 +165,7 @@ export function classifySatellite({
|
|||||||
group = null,
|
group = null,
|
||||||
ucs = null,
|
ucs = null,
|
||||||
satcat = null,
|
satcat = null,
|
||||||
|
groups = null,
|
||||||
launchYear = null,
|
launchYear = null,
|
||||||
nowYear = new Date().getUTCFullYear(),
|
nowYear = new Date().getUTCFullYear(),
|
||||||
} = {}) {
|
} = {}) {
|
||||||
@@ -199,7 +204,10 @@ export function classifySatellite({
|
|||||||
)
|
)
|
||||||
return 'debris';
|
return 'debris';
|
||||||
|
|
||||||
return GROUP_CLASS[group] || 'probes';
|
const memberships = groups?.length ? groups : [group];
|
||||||
|
for (const membership of memberships)
|
||||||
|
if (GROUP_CLASS[membership]) return GROUP_CLASS[membership];
|
||||||
|
return 'probes';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether a class may be ridden in the Cockpit's orbital view. */
|
/** Whether a class may be ridden in the Cockpit's orbital view. */
|
||||||
|
|||||||
@@ -72,8 +72,16 @@ export function createIngestion({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (group.stale && satcatState === 'current') satcatState = 'stale';
|
if (group.stale && satcatState === 'current') satcatState = 'stale';
|
||||||
for (const [noradId, record] of group.records)
|
for (const [noradId, record] of group.records) {
|
||||||
layerState._satcat.set(noradId, record);
|
// A group served from the proxy's last copy never overrules a
|
||||||
|
// current read of the same object, from this merge or an earlier one.
|
||||||
|
const held = layerState._satcat.get(noradId);
|
||||||
|
if (group.stale && held && !held.stale) continue;
|
||||||
|
layerState._satcat.set(
|
||||||
|
noradId,
|
||||||
|
group.stale ? { ...record, stale: true } : record,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!groups.length) return;
|
if (!groups.length) return;
|
||||||
const changed = layerState._satcatState !== satcatState;
|
const changed = layerState._satcatState !== satcatState;
|
||||||
@@ -100,6 +108,7 @@ export function createIngestion({
|
|||||||
noradId,
|
noradId,
|
||||||
name: sat.name,
|
name: sat.name,
|
||||||
group: sat.group,
|
group: sat.group,
|
||||||
|
groups: sat.groups,
|
||||||
ucs: sat.ucs,
|
ucs: sat.ucs,
|
||||||
satcat,
|
satcat,
|
||||||
launchYear: sat.launchYear,
|
launchYear: sat.launchYear,
|
||||||
@@ -259,6 +268,18 @@ export function createIngestion({
|
|||||||
allEntries.push({ ...e, group: r.tag, stale: r.stale });
|
allEntries.push({ ...e, group: r.tag, stale: r.stale });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every group each TLE came in, keyed by its catalog-number column:
|
||||||
|
// the shown tag is the first, but classification reads them all.
|
||||||
|
const groupsByNumber = new Map();
|
||||||
|
for (const entry of allEntries) {
|
||||||
|
const number = String(entry.line1 || '')
|
||||||
|
.slice(2, 7)
|
||||||
|
.trim();
|
||||||
|
const groups = groupsByNumber.get(number);
|
||||||
|
if (!groups) groupsByNumber.set(number, [entry.group]);
|
||||||
|
else if (!groups.includes(entry.group)) groups.push(entry.group);
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicate by NORAD ID — first (most specific) group tag wins
|
// Deduplicate by NORAD ID — first (most specific) group tag wins
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
let coreStale = false;
|
let coreStale = false;
|
||||||
@@ -275,10 +296,16 @@ export function createIngestion({
|
|||||||
const launchYear = tleLineLaunchYear(entry.line1);
|
const launchYear = tleLineLaunchYear(entry.line1);
|
||||||
const ucs = layerState._ucs?.get(noradId) || null;
|
const ucs = layerState._ucs?.get(noradId) || null;
|
||||||
const satcat = layerState._satcat.get(noradId) || null;
|
const satcat = layerState._satcat.get(noradId) || null;
|
||||||
|
const groups = groupsByNumber.get(
|
||||||
|
String(entry.line1 || '')
|
||||||
|
.slice(2, 7)
|
||||||
|
.trim(),
|
||||||
|
) || [entry.group];
|
||||||
layerState._catalog.set(noradId, {
|
layerState._catalog.set(noradId, {
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
satrec,
|
satrec,
|
||||||
group: entry.group,
|
group: entry.group,
|
||||||
|
groups,
|
||||||
// satellite.js does not keep the designator the year match reads.
|
// satellite.js does not keep the designator the year match reads.
|
||||||
launchYear,
|
launchYear,
|
||||||
line1: entry.line1,
|
line1: entry.line1,
|
||||||
@@ -287,6 +314,7 @@ export function createIngestion({
|
|||||||
noradId,
|
noradId,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
group: entry.group,
|
group: entry.group,
|
||||||
|
groups,
|
||||||
ucs,
|
ucs,
|
||||||
satcat,
|
satcat,
|
||||||
launchYear,
|
launchYear,
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ function ingestionFor(catalogText, { source: sourceExtras = {} } = {}) {
|
|||||||
catalog: {},
|
catalog: {},
|
||||||
};
|
};
|
||||||
const source = {
|
const source = {
|
||||||
async readGroup() {
|
async readGroup(path) {
|
||||||
return { ok: true, text: catalogText };
|
const text =
|
||||||
|
typeof catalogText === 'function' ? catalogText(path) : catalogText;
|
||||||
|
return { ok: true, text };
|
||||||
},
|
},
|
||||||
...sourceExtras,
|
...sourceExtras,
|
||||||
};
|
};
|
||||||
@@ -141,9 +143,10 @@ test('a hanging SATCAT read neither holds the catalog nor is lost when it lands'
|
|||||||
const satcatAnswer = new Promise((resolve) => {
|
const satcatAnswer = new Promise((resolve) => {
|
||||||
answer = resolve;
|
answer = resolve;
|
||||||
});
|
});
|
||||||
const { state, update } = ingestionFor(tleSet('OLD STAGE', '40115'), {
|
const { state, update } = ingestionFor(
|
||||||
source: { readSatcat: () => satcatAnswer },
|
(path) => (path === 'stations' ? tleSet('OLD STAGE', '40115') : ''),
|
||||||
});
|
{ source: { readSatcat: () => satcatAnswer } },
|
||||||
|
);
|
||||||
state._enabled = true;
|
state._enabled = true;
|
||||||
const settle = async () => {
|
const settle = async () => {
|
||||||
for (let turn = 0; turn < 20; turn += 1)
|
for (let turn = 0; turn < 20; turn += 1)
|
||||||
@@ -170,6 +173,45 @@ test('a hanging SATCAT read neither holds the catalog nor is lost when it lands'
|
|||||||
assert.equal(state._satcatState, 'stale', 'its staleness is kept');
|
assert.equal(state._satcatState, 'stale', 'its staleness is kept');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a satellite in visual and resource is imaging, whichever group shows it', async (t) => {
|
||||||
|
t.mock.method(console, 'log', () => {});
|
||||||
|
t.mock.method(console, 'warn', () => {});
|
||||||
|
// A name no pattern knows, no UCS record and no SATCAT: only its groups.
|
||||||
|
const { state, update } = ingestionFor((path) =>
|
||||||
|
path === 'visual' || path === 'resource' ? tleSet('ZZ TESTSAT', '50001') : '',
|
||||||
|
);
|
||||||
|
await update();
|
||||||
|
const sat = state._catalog.get(50001);
|
||||||
|
assert.equal(sat.group, 'visual', 'shown under the first group');
|
||||||
|
assert.deepEqual(sat.groups, ['visual', 'resource']);
|
||||||
|
assert.equal(sat.satelliteClass, 'imaging');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stale SATCAT copy never overrules a current read of the same object', async (t) => {
|
||||||
|
t.mock.method(console, 'log', () => {});
|
||||||
|
t.mock.method(console, 'warn', () => {});
|
||||||
|
const header =
|
||||||
|
'OBJECT_NAME,OBJECT_ID,NORAD_CAT_ID,OBJECT_TYPE,OPS_STATUS_CODE,OWNER\n';
|
||||||
|
const { state, update } = ingestionFor(tleSet('ZZ TESTSAT', '50002'), {
|
||||||
|
source: {
|
||||||
|
// visual is current and says it died; resource, a cached copy, still
|
||||||
|
// says it works.
|
||||||
|
readSatcat: async (path) =>
|
||||||
|
path === 'visual'
|
||||||
|
? { ok: true, text: `${header}ZZ TESTSAT,2015-001A,50002,PAY,-,US` }
|
||||||
|
: path === 'resource'
|
||||||
|
? {
|
||||||
|
ok: true,
|
||||||
|
stale: true,
|
||||||
|
text: `${header}ZZ TESTSAT,2015-001A,50002,PAY,+,US`,
|
||||||
|
}
|
||||||
|
: { ok: true, text: header },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await update();
|
||||||
|
assert.equal(state._catalog.get(50002).satelliteClass, 'debris');
|
||||||
|
});
|
||||||
|
|
||||||
test('a failed SATCAT read is told on the layer row, not only the console', async (t) => {
|
test('a failed SATCAT read is told on the layer row, not only the console', async (t) => {
|
||||||
t.mock.method(console, 'log', () => {});
|
t.mock.method(console, 'log', () => {});
|
||||||
t.mock.method(console, 'warn', () => {});
|
t.mock.method(console, 'warn', () => {});
|
||||||
|
|||||||
Reference in new issue
Block a user