mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
refactor(satellites): remove the unused owner, operator, COSPAR and orbital-view groundwork; the UCS pack keeps only the purpose the classes read
This commit is contained in:
1 parent
7dcf671b2e
commit
3bb742ae7e
13 files changed
+148
-617
No files matched your search
@@ -150,7 +150,7 @@ export const DATA_CREDITS = [
|
||||
{
|
||||
key: 'ucs-satellite-database',
|
||||
html:
|
||||
'Satellite operators and purposes: ' +
|
||||
'Satellite purposes: ' +
|
||||
'<a href="https://www.ucsusa.org/satellite_database" target="_blank" rel="noopener">UCS Satellite Database</a> ' +
|
||||
'(UCS-Satellite-Database 5-1-2023), Union of Concerned Scientists',
|
||||
},
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# UCS Satellite Database pack
|
||||
|
||||
Offline operator, owner-country and purpose records for active satellites,
|
||||
keyed by NORAD catalog number. The satellites layer joins them to CelesTrak's
|
||||
positions to file each satellite under a class (stations, imaging, telecom,
|
||||
probes) and to name its operator and owner country
|
||||
Offline purpose records for active satellites, keyed by NORAD catalog number.
|
||||
The satellites layer joins them to CelesTrak's positions to file each
|
||||
satellite under a class (stations, imaging, telecom, probes)
|
||||
(`src/data/ucsSatellites.js`, `src/layers/satellites/classes.js`).
|
||||
|
||||
| File | Source dataset | Records |
|
||||
@@ -24,7 +23,7 @@ the version of the Database that was used, which is indicated by the name of
|
||||
the Excel file, and a link to or URL for the webpage
|
||||
www.ucsusa.org/satellite_database."
|
||||
|
||||
**Acknowledgement:** "Satellite operators and purposes: UCS Satellite Database
|
||||
**Acknowledgement:** "Satellite purposes: UCS Satellite Database
|
||||
(UCS-Satellite-Database 5-1-2023), https://www.ucsusa.org/satellite_database".
|
||||
It is also listed in the app's Data attribution popover and in
|
||||
DATA_SOURCES.md.
|
||||
@@ -34,18 +33,13 @@ DATA_SOURCES.md.
|
||||
|
||||
**Curation:**
|
||||
|
||||
- Kept columns: Operator/Owner, Country of Operator/Owner, Purpose, Detailed
|
||||
Purpose, Users, Class of Orbit (upper-cased, which folds the source's one
|
||||
`LEo`). Everything else (masses, contractors, launch data, sources) is left
|
||||
out.
|
||||
- Kept column: Purpose, the one the classes read. Everything else
|
||||
(operators, owners, orbits, masses, contractors, launch data, sources) is
|
||||
left out.
|
||||
- Repeated text is stored once in `strings`; each record is
|
||||
`[noradId, operator, operatorCountry, purpose, detailedPurpose, users,
|
||||
orbitClass]` with string-table indexes, `0` meaning empty.
|
||||
- The 3,996 Starlink rows are dropped: every Starlink object is classed and
|
||||
credited to SpaceX by name, so they would only add size.
|
||||
- Rows without a readable NORAD number are dropped and counted apart from
|
||||
duplicate NORAD numbers (the first row of a duplicate is kept); both counts
|
||||
are 0 in this release.
|
||||
- Owner countries stay as UCS writes them ("USA", "USA/Japan", "ESA",
|
||||
"Multinational"); `src/layers/satellites/classes.js` normalises them, together with
|
||||
CelesTrak SATCAT owner codes, in one table.
|
||||
`[noradId, purpose]` with a string-table index, `0` meaning empty.
|
||||
- The 3,996 Starlink rows are dropped: every Starlink object is classed by
|
||||
name, so they would only add size.
|
||||
- Rows without a readable, positive NORAD number are dropped and counted apart
|
||||
from duplicate NORAD numbers (the first row of a duplicate is kept); both
|
||||
counts are 0 in this release.
|
||||
@@ -1 +1 @@
|
||||
{"meta":{"source":"Union of Concerned Scientists Satellite Database","version":"UCS-Satellite-Database 5-1-2023","url":"https://www.ucsusa.org/satellite_database","dataCurrentTo":"2023-05-01","license":"Free, unrestricted use; acknowledgement requested (UCS database manual, section 3)","curation":{"sourceRows":7560,"starlinkRowsDropped":3996,"invalidNoradDropped":0,"duplicateNoradDropped":0,"columns":{"operator":"Operator/Owner","operatorCountry":"Country of Operator/Owner","purpose":"Purpose","detailedPurpose":"Detailed Purpose","users":"Users","orbitClass":"Class of Orbit"}},"recordCount":3564},"fields":["noradId","operator","operatorCountry","purpose","detailedPurpose","users","orbitClass"],"strings":{"operator":["","Hera Systems","AAC Clyde Space","Aalto University","University of Aalborg","Asia Broadcast Satellite Ltd.","Asher Space Research Institute at Technion/Israeli Aircraft Industries","Austrian Space Forum (OEWF)/Spire","National Reconnaissance Office (NRO)","US Air Force","Department of Homeland Security","European Space Agency (ESA)","Aerospace Corporation","EUTELSAT S.A.","Aurora Insight","Center for Atmospheric Sciences, Hampton University/NASA","Norwegian Coastal Admnistration","AISTech","Al Yah Satellite Communications Co. (YAHSAT)","Al-Farabi Kazakh National University","Algerian Space Agency (ASAL)","Astro Live Experiences","University of Brasilia","US Air Force Institute of Technology","Algerian Space Agency (ASAL)/UK Space Agency","Hispamar (subsidiary of Hispasat - Spain)","National Institute for Space Research (INPE)","SES S.A.","Moscow State University","Space-Communication Ltd.","AMSAT-NA","Agency for Defense Development","ANDESITE - Boston University","Centre National d'Etudes Spatiales (CNES)","AngoSat","Telesat Canada Ltd. (BCE, Inc.)","Aprize Satellite, Argentina","APT Satellite Holdings Ltd.","Arab Satellite Communications Org. (ASCO)","University of South Florida, Institute of Applied Engineering (IAE).","Norwegian Defence Research Establishment","Aoyama Gakuin University in Tokyo","Naval Physical and Oceanographic Laboratory","Planetary Resources","ArSat","Aerospacelab","Istanbul Technical University","Asia Satellite Telecommunications Co. Ltd.","1Worldspace","Japan Space Systems","Chiba Institute of Technology","Astranis","Astrocast","Telemetry Tracking and Command Network (ISTRAC) Centre","DirecTV, Inc.","PointView Tech","Centre National d'Etudes Spatiales (CNES)/Agenzia Spaziale Italiana (ASI)","Microspace Rapid Pvt Ltd.","Azercosmos","Bangladesh Telecommunication Regulatory Commission (BTRC)","Tianyi Research Institute","Ministry of Defense","Space Development Agency","Central European Institute of Technology","Technical University Berlin","Chinese Ministry of National Defense","21st Century Aerospace Technology (21AT)","Beijing Landview Mapping Information Technology Co. Ltd (BLMIT)","Changsha Tianyi space science and Technology Research Institute Co., Ltd. (Spacety)","Belintersat","Institute of Space Sensor Technology and Planetary Exploration","Norwegian Defence Research Establishment (FFI)/","German Aerospace Center (DLR)","Salish Kootenai College","National Academy of Sciences","BlackSky Global","NanoAvionics","AST SpaceMobile","Embratel Star One","SpaceQuest, Ltd.","Bank Rakyat Indonesia (BRI)","University of Toronto, Institute for Aerospace Studies","BRITE Consortium","UnseenLabs","AstroForge","Broadcasting Satellite System Corp.","Broadcasting Satellite System Corp./Sky Perfect JSAT Corporation","Defence Science and Technology Group/University of New South Wales","China Academy of Space Technology (CAST)","Satellogic S.A.","Bulsatcom","Capital Technology University","Centre National d'Etudes Spatiales (CNES)/NASA","University of Louisiana at Lafayette","Capella Space","Indian Space Research Organization (ISRO)","Korea Aerospace Research Institute (KARI)","Canadian Space Agency","China National Space Administration (CNSA)/National Institute for Space Research (Brazil)","Surrey Satellite Technology Ltd.","Earth-i/Surrey Satellite Technology Ltd.","Canon Electronics","Air Force Research Laboratory","Fleet Space Technologies","Beijing Future Navigation Technology Co. Ltd.","NASA Goddard Space Flight Center","Ministry of Armed Forces","Defense Innovation Unit/Cesium Astro","China National Space Administration (CNSA)/National Centre for Space Studies (CNES)","TELNET Holding","National Aeronautics and Space Administration (NASA) Goddard Space Flight Center","Spacety Aerospace Company","Shanghai Engineering Center for Microsatellites","Spacety Co. Ltd.","Nanjing University","Epic Aerospace","University of Florida","Chinese Academy of Sciences","GeoOptics Inc.","GeoOptics, Inc.","Ciel Satellite Group","University of California-Berkeley/Imperial College-London","Laboratory for Atmospheric and Space Physics, University of Colorado, Boulder","Utah State University","Unknown US agency","National Aeronautics and Space Administration (NASA)/Colorado State University","DepartmeLine truncated
|
||||
{"meta":{"source":"Union of Concerned Scientists Satellite Database","version":"UCS-Satellite-Database 5-1-2023","url":"https://www.ucsusa.org/satellite_database","dataCurrentTo":"2023-05-01","license":"Free, unrestricted use; acknowledgement requested (UCS database manual, section 3)","curation":{"sourceRows":7560,"starlinkRowsDropped":3996,"invalidNoradDropped":0,"duplicateNoradDropped":0,"columns":{"purpose":"Purpose"}},"recordCount":3564},"fields":["noradId","purpose"],"strings":{"purpose":["","Earth Observation","Technology Development","Communications","Earth Science","Space Science","Space Science/Technology Demonstration","Communications/Technology Development","Communications/Maritime Tracking","Technology Demonstration","Unknown","Navigation/Global Positioning","Earth Observation/Technology Development","Earth Observation/Communications","Earth/Space Observation","Educational","Earth Observation/Earth Science","Platform","Earth Observation/Space Science","Earth Observation/Navigation","Communications/Navigation","Space Observation","Surveillance","Navigation/Regional Positioning","Space Science/Technology Development","Mission Extension Technology","Earth Science/Earth Observation","Earth Observation/Communications/Space Science","Meteorological","Technology Development/Educational","Satellite Positioning"]},"records":[[7530,3],[19548,3],[20253,3],[20580,5],[20776,3],[21639,3],[22049,5],[22314,3],[22491,1],[22787,3],[22988,3],[23223,1],[23467,3],[23553,3],[23613,3],[23628,3],[23855,1],[23893,3],[24674,3],[24753,1],[24812,3],[24876,11],[24916,3],[24920,1],[24936,3],[25019,3],[25034,1],[25071,3],[25112,3],[25113,3],[25114,3],[25115,3],[25116,3],[25117,3],[25118,3],[25119,3],[25153,3],[25159,3],[25258,3],[25336,1],[25338,1],[25414,3],[25415,3],[25416,3],[25418,3],[25419,3],[25420,3],[25462,3],[25475,3],[25477,3],[25481,3],[25504,1],[25544,5],[25639,3],[25652,3],[25682,1],[25757,4],[25867,5],[25924,3],[25933,11],[25967,3],[25980,3],[25981,3],[25982,3],[25984,3],[25985,3],[25989,5],[25991,1],[25994,1],[26038,3],[26052,3],[26107,3],[26356,1],[26360,11],[26369,3],[26388,3],[26390,3],[26407,11],[26410,5],[26411,5],[26451,3],[26463,5],[26464,5],[26469,3],[26473,1],[26483,3],[26554,3],[26575,3],[26580,3],[26590,3],[26605,11],[26608,3],[26624,3],[26626,3],[26638,3],[26690,11],[26694,3],[26702,1],[26715,3],[26719,3],[26766,3],[26824,3],[26853,3],[26880,1],[26900,3],[26958,9],[26959,12],[26998,5],[27004,2],[27056,3],[27057,3],[27168,3],[27378,3],[27380,3],[27389,3],[27399,3],[27400,3],[27414,3],[27424,1],[27426,3],[27434,1],[27438,3],[27445,3],[27460,3],[27464,3],[27465,3],[27509,1],[27513,3],[27528,3],[27540,5],[27566,3],[27603,3],[27606,3],[27607,3],[27612,3],[27640,1],[27651,5],[27663,11],[27691,3],[27704,11],[27711,3],[27715,3],[27811,3],[27825,3],[27831,3],[27843,5],[27848,2],[27854,3],[27858,5],[27868,3],[27869,3],[27875,3],[27937,1],[27939,2],[27954,3],[28051,1],[28054,1],[28058,3],[28089,3],[28117,3],[28158,1],[28184,3],[28187,3],[28190,11],[28220,1],[28252,3],[28358,3],[28366,7],[28369,2],[28370,2],[28371,1],[28372,7],[28376,1],[28378,3],[28384,3],[28413,5],[28414,5],[28419,3],[28420,3],[28446,3],[28470,1],[28474,11],[28485,5],[28492,1],[28493,3],[28526,3],[28537,1],[28541,1],[28542,3],[28626,3],[28628,3],[28646,1],[28649,1],[28654,1],[28659,3],[28702,3],[28707,3],[28737,5],[28786,3],[28790,3],[28810,2],[28868,3],[28874,11],[28884,3],[28885,3],[28888,1],[28890,1],[28899,3],[28903,3],[28908,3],[28912,1],[28924,3],[28935,3],[28945,3],[28946,3],[29045,3],[29047,1],[29048,1],[29050,1],[29051,1],[29052,1],[29055,3],[29079,1],[29107,1],[29108,4],[29155,1],[29162,3],[29236,3],[29249,1],[29252,2],[29268,12],[29270,3],[29272,3],[29273,3],[29349,3],[29479,5],[29486,11],[29494,3],[29495,3],[29505,5],[29506,5],[29520,3],[29522,1],[29526,3],[29601,11],[29643,3],[29644,3],[29658,1],[29670,11],[29671,11],[29709,12],[29710,1],[30580,5],[30776,2],[30794,3],[30797,5],[30798,5],[31102,3],[31118,1],[31119,3],[31121,3],[31124,3],[31125,3],[31127,3],[31304,1],[31306,3],[31307,3],[31571,3],[31573,3],[31574,3],[31576,3],[31598,1],[31601,1],[31698,1],[31701,1],[31708,1],[31789,2],[31792,1],[31797,1],[31800,3],[31862,3],[32018,3],[32019,3],[32060,1],[32252,3],[32253,3],[32258,3],[32260,11],[32263,3],[32264,3],[32265,3],[32275,11],[32276,11],[32283,1],[32293,3],[32294,3],[32299,3],[32376,1],[32378,3],[32382,1],[32384,11],[32388,3],[32393,11],[32395,11],[32404,3],[32476,1],[32478,3],[32487,3],[32706,1],[32711,11],[32729,3],[32750,1],[32763,3],[32767,3],[32768,3],[32779,3],[32783,1],[32789,2],[32790,2],[32791,2],[32794,3],[32951,3],[32954,3],[32955,3],[32956,3],[33051,3],[33053,5],[33055,3],[33056,3],[33153,3],[33154,3],[33207,3],[33244,1],[33274,3],[33275,3],[33278,3],[33320,1],[33321,1],[33331,1],[33373,3],[33376,3],[33396,1],[33401,5],[33408,5],[33409,5],[33412,1],[33433,1],[33434,1],[33436,3],[33446,1],[33453,3],[33459,3],[33460,3],[33490,1],[33492,1],[33496,2],[33499,2],[33591,1],[33595,3],[33749,3],[33750,3],[33751,2],[337Line truncated
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file The bundled UCS Satellite Database pack: operator, owner country and
|
||||
* purpose for active satellites, keyed by NORAD catalog number.
|
||||
* @file The bundled UCS Satellite Database pack: the purpose of each active
|
||||
* satellite, keyed by NORAD catalog number.
|
||||
*
|
||||
* Data: `local_data/ucs_satellites/ucs_satellites.json`, built by
|
||||
* `scripts/build-ucs-satellites.py` from UCS-Satellite-Database 5-1-2023
|
||||
@@ -20,11 +20,8 @@ const PACK_URL = new URL(
|
||||
);
|
||||
|
||||
/**
|
||||
* One UCS row: what UCS says about a satellite's operator and purpose.
|
||||
* @typedef {{noradId: number, operator: string|null,
|
||||
* operatorCountry: string|null, purpose: string|null,
|
||||
* detailedPurpose: string|null, users: string|null,
|
||||
* orbitClass: string|null}} UcsSatellite
|
||||
* One UCS row: what UCS says a satellite is for.
|
||||
* @typedef {{noradId: number, purpose: string|null}} UcsSatellite
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,15 +22,7 @@ test('the bundled UCS pack names its release, licence and curation', () => {
|
||||
pack.meta.curation.invalidNoradDropped +
|
||||
pack.meta.curation.duplicateNoradDropped,
|
||||
);
|
||||
assert.deepEqual(pack.fields, [
|
||||
'noradId',
|
||||
'operator',
|
||||
'operatorCountry',
|
||||
'purpose',
|
||||
'detailedPurpose',
|
||||
'users',
|
||||
'orbitClass',
|
||||
]);
|
||||
assert.deepEqual(pack.fields, ['noradId', 'purpose']);
|
||||
// Small enough to load with the layer.
|
||||
assert.ok(readFileSync(PACK_URL).length < 200_000);
|
||||
});
|
||||
@@ -41,36 +33,26 @@ test('the pack loads into records keyed by NORAD number', async () => {
|
||||
const iss = records.get(25544);
|
||||
assert.ok(iss, 'the ISS is in UCS');
|
||||
assert.equal(iss.noradId, 25544);
|
||||
assert.ok(iss.operator);
|
||||
assert.ok(iss.purpose);
|
||||
for (const record of records.values()) {
|
||||
assert.doesNotMatch(
|
||||
`${record.operator ?? ''} ${record.purpose ?? ''}`,
|
||||
/starlink/i,
|
||||
'Starlink rows are dropped',
|
||||
);
|
||||
if (record.orbitClass) assert.match(record.orbitClass, /^(LEO|MEO|GEO|ELLIPTICAL)$/);
|
||||
}
|
||||
});
|
||||
|
||||
test('indexing reads the string tables and turns empty strings into null', () => {
|
||||
const records = indexUcsPack({
|
||||
fields: ['noradId', 'operator', 'purpose'],
|
||||
strings: { operator: ['', 'ESA'], purpose: ['', 'Earth Observation'] },
|
||||
fields: ['noradId', 'purpose'],
|
||||
strings: { purpose: ['', 'Earth Observation'] },
|
||||
records: [
|
||||
[39634, 1, 1],
|
||||
[40000, 0, 1],
|
||||
['bad', 1, 1],
|
||||
[-5, 1, 1],
|
||||
[39634, 1],
|
||||
[40000, 0],
|
||||
['bad', 1],
|
||||
[-5, 1],
|
||||
],
|
||||
});
|
||||
assert.deepEqual([...records.keys()], [39634, 40000]);
|
||||
assert.deepEqual(records.get(39634), {
|
||||
noradId: 39634,
|
||||
operator: 'ESA',
|
||||
purpose: 'Earth Observation',
|
||||
});
|
||||
assert.equal(records.get(40000).operator, null);
|
||||
assert.equal(records.get(40000).purpose, null);
|
||||
});
|
||||
|
||||
test('a pack of the wrong shape throws, so the loader tries again', () => {
|
||||
|
||||
@@ -124,8 +124,6 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
satrec,
|
||||
group: 'dense',
|
||||
launchYear: tleLineLaunchYear(entry.line1),
|
||||
line1: entry.line1,
|
||||
line2: entry.line2,
|
||||
// The dense feed is CelesTrak's Starlink group.
|
||||
satelliteClass: 'starlink',
|
||||
ucs: null,
|
||||
|
||||
@@ -210,13 +210,6 @@ export function classifySatellite({
|
||||
return 'probes';
|
||||
}
|
||||
|
||||
/** Whether a class may be ridden in the Cockpit's orbital view. */
|
||||
export function satelliteClassEnterable(satelliteClass) {
|
||||
return (
|
||||
SATELLITE_CLASS_IDS.includes(satelliteClass) && satelliteClass !== 'debris'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a partial class-visibility request into the current choice.
|
||||
* Unknown keys and non-boolean values are ignored.
|
||||
@@ -236,443 +229,6 @@ export function mergeClassVisibility(
|
||||
return next;
|
||||
}
|
||||
|
||||
// ISO 3166-1 alpha-2 code → short English name, for every owner below.
|
||||
const COUNTRY_NAMES = Object.freeze({
|
||||
AE: 'United Arab Emirates',
|
||||
AM: 'Armenia',
|
||||
AO: 'Angola',
|
||||
AR: 'Argentina',
|
||||
AT: 'Austria',
|
||||
AU: 'Australia',
|
||||
AZ: 'Azerbaijan',
|
||||
BD: 'Bangladesh',
|
||||
BE: 'Belgium',
|
||||
BG: 'Bulgaria',
|
||||
BH: 'Bahrain',
|
||||
BM: 'Bermuda',
|
||||
BO: 'Bolivia',
|
||||
BR: 'Brazil',
|
||||
BT: 'Bhutan',
|
||||
BW: 'Botswana',
|
||||
BY: 'Belarus',
|
||||
CA: 'Canada',
|
||||
CH: 'Switzerland',
|
||||
CL: 'Chile',
|
||||
CN: 'China',
|
||||
CO: 'Colombia',
|
||||
CR: 'Costa Rica',
|
||||
CZ: 'Czechia',
|
||||
DE: 'Germany',
|
||||
DJ: 'Djibouti',
|
||||
DK: 'Denmark',
|
||||
DZ: 'Algeria',
|
||||
EC: 'Ecuador',
|
||||
EE: 'Estonia',
|
||||
EG: 'Egypt',
|
||||
ES: 'Spain',
|
||||
ET: 'Ethiopia',
|
||||
FI: 'Finland',
|
||||
FR: 'France',
|
||||
GB: 'United Kingdom',
|
||||
GH: 'Ghana',
|
||||
GR: 'Greece',
|
||||
GT: 'Guatemala',
|
||||
HR: 'Croatia',
|
||||
HU: 'Hungary',
|
||||
ID: 'Indonesia',
|
||||
IE: 'Ireland',
|
||||
IL: 'Israel',
|
||||
IN: 'India',
|
||||
IQ: 'Iraq',
|
||||
IR: 'Iran',
|
||||
IT: 'Italy',
|
||||
JO: 'Jordan',
|
||||
JP: 'Japan',
|
||||
KE: 'Kenya',
|
||||
KP: 'North Korea',
|
||||
KR: 'South Korea',
|
||||
KW: 'Kuwait',
|
||||
KZ: 'Kazakhstan',
|
||||
LA: 'Laos',
|
||||
LK: 'Sri Lanka',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
MA: 'Morocco',
|
||||
MC: 'Monaco',
|
||||
MD: 'Moldova',
|
||||
ME: 'Montenegro',
|
||||
MM: 'Myanmar',
|
||||
MN: 'Mongolia',
|
||||
MU: 'Mauritius',
|
||||
MX: 'Mexico',
|
||||
MY: 'Malaysia',
|
||||
NG: 'Nigeria',
|
||||
NL: 'Netherlands',
|
||||
NO: 'Norway',
|
||||
NP: 'Nepal',
|
||||
NZ: 'New Zealand',
|
||||
PE: 'Peru',
|
||||
PH: 'Philippines',
|
||||
PK: 'Pakistan',
|
||||
PL: 'Poland',
|
||||
PT: 'Portugal',
|
||||
PY: 'Paraguay',
|
||||
QA: 'Qatar',
|
||||
RO: 'Romania',
|
||||
RU: 'Russia',
|
||||
RW: 'Rwanda',
|
||||
SA: 'Saudi Arabia',
|
||||
SB: 'Solomon Islands',
|
||||
SD: 'Sudan',
|
||||
SE: 'Sweden',
|
||||
SG: 'Singapore',
|
||||
SI: 'Slovenia',
|
||||
SN: 'Senegal',
|
||||
TH: 'Thailand',
|
||||
TM: 'Turkmenistan',
|
||||
TN: 'Tunisia',
|
||||
TR: 'Türkiye',
|
||||
TW: 'Taiwan',
|
||||
UA: 'Ukraine',
|
||||
US: 'United States',
|
||||
UY: 'Uruguay',
|
||||
VA: 'Vatican City',
|
||||
VE: 'Venezuela',
|
||||
VN: 'Vietnam',
|
||||
ZA: 'South Africa',
|
||||
ZW: 'Zimbabwe',
|
||||
});
|
||||
|
||||
// UCS "Country of Operator/Owner" → ISO code. Joint owners ("USA/Japan") and
|
||||
// organisations are not countries and resolve through ORGANISATIONS or as a
|
||||
// joint label.
|
||||
const UCS_COUNTRIES = Object.freeze({
|
||||
Algeria: 'DZ',
|
||||
Angola: 'AO',
|
||||
Argentina: 'AR',
|
||||
Australia: 'AU',
|
||||
Austria: 'AT',
|
||||
Azerbaijan: 'AZ',
|
||||
Bangladesh: 'BD',
|
||||
Belarus: 'BY',
|
||||
Belgium: 'BE',
|
||||
Bolivia: 'BO',
|
||||
Brazil: 'BR',
|
||||
Bulgaria: 'BG',
|
||||
Canada: 'CA',
|
||||
Chile: 'CL',
|
||||
China: 'CN',
|
||||
Colombia: 'CO',
|
||||
'Czech Republic': 'CZ',
|
||||
Denmark: 'DK',
|
||||
Ecuador: 'EC',
|
||||
Egypt: 'EG',
|
||||
Estonia: 'EE',
|
||||
Ethiopia: 'ET',
|
||||
Finland: 'FI',
|
||||
France: 'FR',
|
||||
Germany: 'DE',
|
||||
Greece: 'GR',
|
||||
Hungary: 'HU',
|
||||
India: 'IN',
|
||||
Indonesia: 'ID',
|
||||
Iran: 'IR',
|
||||
Iraq: 'IQ',
|
||||
Israel: 'IL',
|
||||
Italy: 'IT',
|
||||
Japan: 'JP',
|
||||
Jordan: 'JO',
|
||||
Kazakhstan: 'KZ',
|
||||
Kenya: 'KE',
|
||||
Kuwait: 'KW',
|
||||
Laos: 'LA',
|
||||
Lithuania: 'LT',
|
||||
Luxembourg: 'LU',
|
||||
Malaysia: 'MY',
|
||||
Mexico: 'MX',
|
||||
Monaco: 'MC',
|
||||
Morocco: 'MA',
|
||||
Nepal: 'NP',
|
||||
Netherlands: 'NL',
|
||||
'New Zealand': 'NZ',
|
||||
Nigeria: 'NG',
|
||||
Norway: 'NO',
|
||||
Pakistan: 'PK',
|
||||
Peru: 'PE',
|
||||
Poland: 'PL',
|
||||
Russia: 'RU',
|
||||
'Saudi Arabia': 'SA',
|
||||
Sinapore: 'SG',
|
||||
Singapore: 'SG',
|
||||
Slovenia: 'SI',
|
||||
'South Africa': 'ZA',
|
||||
'South Korea': 'KR',
|
||||
Spain: 'ES',
|
||||
Sudan: 'SD',
|
||||
Sweden: 'SE',
|
||||
Switzerland: 'CH',
|
||||
Taiwan: 'TW',
|
||||
Thailand: 'TH',
|
||||
Tunisia: 'TN',
|
||||
Turkey: 'TR',
|
||||
UK: 'GB',
|
||||
USA: 'US',
|
||||
Ukraine: 'UA',
|
||||
'United Arab Emirates': 'AE',
|
||||
'United Kingdom': 'GB',
|
||||
Venezuela: 'VE',
|
||||
Vietnam: 'VN',
|
||||
});
|
||||
|
||||
// CelesTrak SATCAT OWNER code → ISO code (https://celestrak.org/satcat/sources.php).
|
||||
// CIS (former USSR) is filed under Russia, its continuing state.
|
||||
const SATCAT_COUNTRIES = Object.freeze({
|
||||
ALG: 'DZ',
|
||||
ANG: 'AO',
|
||||
ARGN: 'AR',
|
||||
ARM: 'AM',
|
||||
ASRA: 'AT',
|
||||
AUS: 'AU',
|
||||
AZER: 'AZ',
|
||||
BEL: 'BE',
|
||||
BELA: 'BY',
|
||||
BERM: 'BM',
|
||||
BGD: 'BD',
|
||||
BHR: 'BH',
|
||||
BHUT: 'BT',
|
||||
BOL: 'BO',
|
||||
BRAZ: 'BR',
|
||||
BUL: 'BG',
|
||||
BWA: 'BW',
|
||||
CA: 'CA',
|
||||
CHLE: 'CL',
|
||||
CIS: 'RU',
|
||||
COL: 'CO',
|
||||
CRI: 'CR',
|
||||
CZCH: 'CZ',
|
||||
DEN: 'DK',
|
||||
DJI: 'DJ',
|
||||
ECU: 'EC',
|
||||
EGYP: 'EG',
|
||||
EST: 'EE',
|
||||
ETH: 'ET',
|
||||
FIN: 'FI',
|
||||
FR: 'FR',
|
||||
GER: 'DE',
|
||||
GHA: 'GH',
|
||||
GREC: 'GR',
|
||||
GUAT: 'GT',
|
||||
HRV: 'HR',
|
||||
HUN: 'HU',
|
||||
IND: 'IN',
|
||||
INDO: 'ID',
|
||||
IRAN: 'IR',
|
||||
IRAQ: 'IQ',
|
||||
IRL: 'IE',
|
||||
ISRA: 'IL',
|
||||
ISRO: 'IN',
|
||||
IT: 'IT',
|
||||
JPN: 'JP',
|
||||
KAZ: 'KZ',
|
||||
KEN: 'KE',
|
||||
LAOS: 'LA',
|
||||
LKA: 'LK',
|
||||
LTU: 'LT',
|
||||
LUXE: 'LU',
|
||||
MA: 'MA',
|
||||
MALA: 'MY',
|
||||
MCO: 'MC',
|
||||
MDA: 'MD',
|
||||
MEX: 'MX',
|
||||
MMR: 'MM',
|
||||
MNE: 'ME',
|
||||
MNG: 'MN',
|
||||
MUS: 'MU',
|
||||
NETH: 'NL',
|
||||
NIG: 'NG',
|
||||
NKOR: 'KP',
|
||||
NOR: 'NO',
|
||||
NPL: 'NP',
|
||||
NZ: 'NZ',
|
||||
PAKI: 'PK',
|
||||
PERU: 'PE',
|
||||
POL: 'PL',
|
||||
POR: 'PT',
|
||||
PRC: 'CN',
|
||||
PRY: 'PY',
|
||||
QAT: 'QA',
|
||||
ROC: 'TW',
|
||||
ROM: 'RO',
|
||||
RP: 'PH',
|
||||
RWA: 'RW',
|
||||
SAFR: 'ZA',
|
||||
SAUD: 'SA',
|
||||
SDN: 'SD',
|
||||
SEN: 'SN',
|
||||
SING: 'SG',
|
||||
SKOR: 'KR',
|
||||
SLB: 'SB',
|
||||
SPN: 'ES',
|
||||
SVN: 'SI',
|
||||
SWED: 'SE',
|
||||
SWTZ: 'CH',
|
||||
THAI: 'TH',
|
||||
TUN: 'TN',
|
||||
TURK: 'TR',
|
||||
UAE: 'AE',
|
||||
UK: 'GB',
|
||||
UKR: 'UA',
|
||||
URY: 'UY',
|
||||
US: 'US',
|
||||
VAT: 'VA',
|
||||
VENZ: 'VE',
|
||||
VTNM: 'VN',
|
||||
ZWE: 'ZW',
|
||||
});
|
||||
|
||||
// Organisations, by SATCAT code and by UCS name: no flag, a short label.
|
||||
const ORGANISATIONS = Object.freeze({
|
||||
AB: 'Arabsat',
|
||||
ABS: 'ABS',
|
||||
AC: 'AsiaSat',
|
||||
ESA: 'ESA',
|
||||
ESRO: 'ESRO',
|
||||
EUME: 'EUMETSAT',
|
||||
EUTE: 'Eutelsat',
|
||||
GLOB: 'Globalstar',
|
||||
IM: 'Inmarsat',
|
||||
IRID: 'Iridium',
|
||||
ISS: 'ISS partners',
|
||||
ITSO: 'Intelsat',
|
||||
NATO: 'NATO',
|
||||
NICO: 'New ICO',
|
||||
O3B: 'O3b',
|
||||
ORB: 'ORBCOMM',
|
||||
RASC: 'RascomStar-QAF',
|
||||
SEAL: 'Sea Launch',
|
||||
SES: 'SES',
|
||||
Multinational: 'Multinational',
|
||||
});
|
||||
|
||||
// SATCAT codes for joint ownership, as their parts.
|
||||
const SATCAT_JOINT = Object.freeze({
|
||||
CHBZ: ['CN', 'BR'],
|
||||
CHTU: ['CN', 'TR'],
|
||||
FGER: ['FR', 'DE'],
|
||||
FRIT: ['FR', 'IT'],
|
||||
GRSA: ['GR', 'SA'],
|
||||
PRES: ['CN', 'ESA'],
|
||||
SGJP: ['SG', 'JP'],
|
||||
STCT: ['SG', 'TW'],
|
||||
TMMC: ['TM', 'MC'],
|
||||
USBZ: ['US', 'BR'],
|
||||
});
|
||||
|
||||
function country(code) {
|
||||
return COUNTRY_NAMES[code]
|
||||
? { countryCode: code, label: COUNTRY_NAMES[code] }
|
||||
: null;
|
||||
}
|
||||
|
||||
function joint(parts) {
|
||||
const labels = parts.map(
|
||||
(part) => COUNTRY_NAMES[part] || ORGANISATIONS[part] || part,
|
||||
);
|
||||
return { countryCode: null, label: labels.join(' / ') };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a UCS owner country ("USA", "China/France", "ESA",
|
||||
* "Multinational") to `{ countryCode, label }`.
|
||||
* @param {string|null|undefined} text
|
||||
* @returns {{countryCode: string|null, label: string}|null}
|
||||
*/
|
||||
export function ownerFromUcs(text) {
|
||||
const value = String(text || '').trim();
|
||||
if (!value) return null;
|
||||
if (UCS_COUNTRIES[value]) return country(UCS_COUNTRIES[value]);
|
||||
if (ORGANISATIONS[value])
|
||||
return { countryCode: null, label: ORGANISATIONS[value] };
|
||||
if (value.includes('/'))
|
||||
return joint(
|
||||
value
|
||||
.split('/')
|
||||
.map((part) => part.trim())
|
||||
.map((part) => UCS_COUNTRIES[part] || part),
|
||||
);
|
||||
return { countryCode: null, label: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a CelesTrak SATCAT OWNER code ("US", "PRC", "CIS", "ITSO") to
|
||||
* `{ countryCode, label }`; null for TBD, UNK and blanks.
|
||||
* @param {string|null|undefined} code
|
||||
* @returns {{countryCode: string|null, label: string}|null}
|
||||
*/
|
||||
export function ownerFromSatcat(code) {
|
||||
const value = String(code || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (!value || value === 'TBD' || value === 'UNK') return null;
|
||||
if (SATCAT_COUNTRIES[value]) return country(SATCAT_COUNTRIES[value]);
|
||||
if (ORGANISATIONS[value])
|
||||
return { countryCode: null, label: ORGANISATIONS[value] };
|
||||
if (SATCAT_JOINT[value]) return joint(SATCAT_JOINT[value]);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Who owns a satellite: UCS names the operator's country, SATCAT the
|
||||
* registering owner; UCS wins where both exist. Starlink without either is
|
||||
* SpaceX's, so United States.
|
||||
* @returns {{countryCode: string|null, label: string}|null}
|
||||
*/
|
||||
export function satelliteOwner({
|
||||
ucs = null,
|
||||
satcat = null,
|
||||
satelliteClass,
|
||||
} = {}) {
|
||||
return (
|
||||
ownerFromUcs(ucs?.operatorCountry) ||
|
||||
ownerFromSatcat(satcat?.owner) ||
|
||||
(satelliteClass === 'starlink' ? country('US') : null)
|
||||
);
|
||||
}
|
||||
|
||||
/** Who operates a satellite: UCS's operator, or SpaceX for Starlink. */
|
||||
export function satelliteOperator({ ucs = null, satelliteClass } = {}) {
|
||||
if (ucs?.operator) return ucs.operator;
|
||||
return satelliteClass === 'starlink' ? 'SpaceX' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The international designator (COSPAR id) from TLE line 1, columns 10–17:
|
||||
* `98067A` → `1998-067A`. Two-digit years below 57 are 20xx.
|
||||
* @param {string|null|undefined} line1
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function cosparIdFromTleLine1(line1) {
|
||||
const field = String(line1 || '')
|
||||
.slice(9, 17)
|
||||
.trim();
|
||||
const match = /^(\d{2})(\d{3})([A-Z]{1,3})$/.exec(field);
|
||||
if (!match) return null;
|
||||
const yy = Number(match[1]);
|
||||
return `${yy < 57 ? 2000 + yy : 1900 + yy}-${match[2]}${match[3]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The TLE's epoch as epoch milliseconds, from satellite.js's Julian date.
|
||||
* @param {{jdsatepoch?: number, jdsatepochF?: number}|null|undefined} satrec
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function tleEpochMs(satrec) {
|
||||
const jd = Number(satrec?.jdsatepoch) + (Number(satrec?.jdsatepochF) || 0);
|
||||
return Number.isFinite(jd) && jd > 0
|
||||
? Math.round((jd - 2440587.5) * 86_400_000)
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Split one RFC 4180 CSV line (quoted fields may hold commas). */
|
||||
function csvFields(line) {
|
||||
const fields = [];
|
||||
@@ -700,7 +256,7 @@ function csvFields(line) {
|
||||
* Parse CelesTrak SATCAT CSV into records keyed by NORAD number.
|
||||
* @param {string} text `records.php?…&FORMAT=CSV` body.
|
||||
* @returns {Map<number, {objectType: string, opsStatus: string,
|
||||
* owner: string, cosparId: string|null, launchDate: string|null}>}
|
||||
* launchDate: string|null}>}
|
||||
*/
|
||||
export function parseSatcatCsv(text) {
|
||||
const records = new Map();
|
||||
@@ -714,8 +270,6 @@ export function parseSatcatCsv(text) {
|
||||
norad: at('NORAD_CAT_ID'),
|
||||
type: at('OBJECT_TYPE'),
|
||||
status: at('OPS_STATUS_CODE'),
|
||||
owner: at('OWNER'),
|
||||
id: at('OBJECT_ID'),
|
||||
launch: at('LAUNCH_DATE'),
|
||||
};
|
||||
if (col.norad < 0 || col.type < 0) return records;
|
||||
@@ -727,8 +281,6 @@ export function parseSatcatCsv(text) {
|
||||
records.set(noradId, {
|
||||
objectType: text(col.type).toUpperCase(),
|
||||
opsStatus: text(col.status),
|
||||
owner: text(col.owner),
|
||||
cosparId: text(col.id) || null,
|
||||
launchDate: text(col.launch) || null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { twoline2satrec } from 'satellite.js';
|
||||
import {
|
||||
DEFAULT_SATELLITE_CLASSES,
|
||||
PRESUMED_DEAD_AFTER_YEARS,
|
||||
SATELLITE_CLASS_DISPLAY,
|
||||
SATELLITE_CLASS_IDS,
|
||||
classifySatellite,
|
||||
cosparIdFromTleLine1,
|
||||
mergeClassVisibility,
|
||||
ownerFromSatcat,
|
||||
ownerFromUcs,
|
||||
parseSatcatCsv,
|
||||
satelliteClassEnterable,
|
||||
satelliteOperator,
|
||||
satelliteOwner,
|
||||
tleEpochMs,
|
||||
} from './classes.js';
|
||||
|
||||
const NOW = 2026;
|
||||
const classify = (subject) => classifySatellite({ nowYear: NOW, ...subject });
|
||||
|
||||
test('six classes, with stations and imaging on by default and the rest off', () => {
|
||||
test('six classes, every one but Starlink on by default', () => {
|
||||
assert.deepEqual(SATELLITE_CLASS_IDS, [
|
||||
'stations',
|
||||
'imaging',
|
||||
@@ -44,16 +36,28 @@ test('six classes, with stations and imaging on by default and the rest off', ()
|
||||
test('SATCAT object type and status file debris first, overruling UCS', () => {
|
||||
const imaging = { purpose: 'Earth Observation' };
|
||||
for (const objectType of ['R/B', 'DEB', 'UNK'])
|
||||
assert.equal(classify({ name: 'X', satcat: { objectType, opsStatus: '' } }), 'debris', objectType);
|
||||
assert.equal(
|
||||
classify({ name: 'X', satcat: { objectType, opsStatus: '' } }),
|
||||
'debris',
|
||||
objectType,
|
||||
);
|
||||
// A satellite UCS listed in 2023 that SATCAT now calls dead or decayed.
|
||||
for (const opsStatus of ['-', 'D'])
|
||||
assert.equal(
|
||||
classify({ name: 'OLDSAT', ucs: imaging, satcat: { objectType: 'PAY', opsStatus } }),
|
||||
classify({
|
||||
name: 'OLDSAT',
|
||||
ucs: imaging,
|
||||
satcat: { objectType: 'PAY', opsStatus },
|
||||
}),
|
||||
'debris',
|
||||
opsStatus,
|
||||
);
|
||||
assert.equal(
|
||||
classify({ name: 'OLDSAT', ucs: imaging, satcat: { objectType: 'PAY', opsStatus: '+' } }),
|
||||
classify({
|
||||
name: 'OLDSAT',
|
||||
ucs: imaging,
|
||||
satcat: { objectType: 'PAY', opsStatus: '+' },
|
||||
}),
|
||||
'imaging',
|
||||
);
|
||||
// Debris riding in the stations group (CelesTrak lists FREGAT DEB there).
|
||||
@@ -63,15 +67,33 @@ test('SATCAT object type and status file debris first, overruling UCS', () => {
|
||||
|
||||
test('the ISS is a station whatever else says', () => {
|
||||
assert.equal(
|
||||
classify({ noradId: 25544, name: 'ISS (ZARYA)', group: 'visual', satcat: { objectType: 'DEB' } }),
|
||||
classify({
|
||||
noradId: 25544,
|
||||
name: 'ISS (ZARYA)',
|
||||
group: 'visual',
|
||||
satcat: { objectType: 'DEB' },
|
||||
}),
|
||||
'stations',
|
||||
);
|
||||
});
|
||||
|
||||
test('stations: crewed stations and visiting vehicles by name', () => {
|
||||
assert.equal(classify({ name: 'CSS (TIANHE)', group: 'stations' }), 'stations');
|
||||
assert.equal(classify({ name: 'ISS (NAUKA)', group: 'stations' }), 'stations');
|
||||
for (const name of ['SHENZHOU-21', 'TIANZHOU-9', 'SOYUZ-MS 28', 'PROGRESS-MS 31', 'CREW DRAGON 11', 'CYGNUS NG-23'])
|
||||
assert.equal(
|
||||
classify({ name: 'CSS (TIANHE)', group: 'stations' }),
|
||||
'stations',
|
||||
);
|
||||
assert.equal(
|
||||
classify({ name: 'ISS (NAUKA)', group: 'stations' }),
|
||||
'stations',
|
||||
);
|
||||
for (const name of [
|
||||
'SHENZHOU-21',
|
||||
'TIANZHOU-9',
|
||||
'SOYUZ-MS 28',
|
||||
'PROGRESS-MS 31',
|
||||
'CREW DRAGON 11',
|
||||
'CYGNUS NG-23',
|
||||
])
|
||||
assert.equal(classify({ name, group: 'visual' }), 'stations', name);
|
||||
});
|
||||
|
||||
@@ -91,7 +113,11 @@ test('UCS purpose decides imaging, telecom and probes', () => {
|
||||
['Unknown', 'probes'],
|
||||
];
|
||||
for (const [purpose, expected] of table)
|
||||
assert.equal(classify({ name: 'UNNAMED-1', group: 'visual', ucs: { purpose } }), expected, purpose);
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'visual', ucs: { purpose } }),
|
||||
expected,
|
||||
purpose,
|
||||
);
|
||||
});
|
||||
|
||||
test('satellites launched after the UCS snapshot are classed by name', () => {
|
||||
@@ -110,20 +136,42 @@ test('satellites launched after the UCS snapshot are classed by name', () => {
|
||||
['STARLINK-34012', 'starlink'],
|
||||
];
|
||||
for (const [name, expected] of table)
|
||||
assert.equal(classify({ name, group: 'visual', launchYear: 2025 }), expected, name);
|
||||
assert.equal(
|
||||
classify({ name, group: 'visual', launchYear: 2025 }),
|
||||
expected,
|
||||
name,
|
||||
);
|
||||
});
|
||||
|
||||
test('the group the TLE came in is the last resort', () => {
|
||||
assert.equal(classify({ name: 'UNNAMED-1', group: 'resource', launchYear: 2024 }), 'imaging');
|
||||
assert.equal(classify({ name: 'UNNAMED-1', group: 'geo', launchYear: 2024 }), 'telecom');
|
||||
assert.equal(classify({ name: 'UNNAMED-1', group: 'science', launchYear: 2024 }), 'probes');
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'resource', launchYear: 2024 }),
|
||||
'imaging',
|
||||
);
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'geo', launchYear: 2024 }),
|
||||
'telecom',
|
||||
);
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'science', launchYear: 2024 }),
|
||||
'probes',
|
||||
);
|
||||
assert.equal(classify({ name: 'STARLINK-1', group: 'dense' }), 'starlink');
|
||||
assert.equal(classify({ name: 'UNNAMED-1', group: 'visual', launchYear: 2024 }), 'probes');
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'visual', launchYear: 2024 }),
|
||||
'probes',
|
||||
);
|
||||
});
|
||||
|
||||
test('CubeSats deployed from the ISS are not stations', () => {
|
||||
// They ride in CelesTrak's stations group under the ISS's 1998 designator.
|
||||
for (const name of ['DUPLEX', 'KNACKSAT-2', 'GXIBA-1', 'LEOPARD', 'HMU-SAT2']) {
|
||||
for (const name of [
|
||||
'DUPLEX',
|
||||
'KNACKSAT-2',
|
||||
'GXIBA-1',
|
||||
'LEOPARD',
|
||||
'HMU-SAT2',
|
||||
]) {
|
||||
assert.equal(
|
||||
classify({ name, group: 'stations', launchYear: 1998 }),
|
||||
'probes',
|
||||
@@ -141,7 +189,11 @@ test('CubeSats deployed from the ISS are not stations', () => {
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'stations', ucs: { purpose: 'Earth Observation' } }),
|
||||
classify({
|
||||
name: 'UNNAMED-1',
|
||||
group: 'stations',
|
||||
ucs: { purpose: 'Earth Observation' },
|
||||
}),
|
||||
'imaging',
|
||||
);
|
||||
});
|
||||
@@ -154,89 +206,53 @@ test('an unknown old payload is presumed dead; a recognisable one is not', () =>
|
||||
'without a SATCAT record (a failed read) nothing is presumed',
|
||||
);
|
||||
assert.equal(
|
||||
classify({ name: 'ATLAS CENTAUR 2', group: 'visual', launchYear: 1963, satcat: { objectType: 'PAY', opsStatus: '' } }),
|
||||
classify({
|
||||
name: 'ATLAS CENTAUR 2',
|
||||
group: 'visual',
|
||||
launchYear: 1963,
|
||||
satcat: { objectType: 'PAY', opsStatus: '' },
|
||||
}),
|
||||
'debris',
|
||||
'a blank SATCAT status is no evidence of life',
|
||||
);
|
||||
assert.equal(classify({ name: 'UNNAMED-1', group: 'visual', launchYear: old + 1 }), 'probes');
|
||||
assert.equal(classify({ name: 'NOAA 15', group: 'weather', launchYear: 1998 }), 'imaging');
|
||||
assert.equal(
|
||||
classify({ name: 'UNNAMED-1', group: 'visual', launchYear: 1990, satcat: { objectType: 'PAY', opsStatus: '+' } }),
|
||||
classify({ name: 'UNNAMED-1', group: 'visual', launchYear: old + 1 }),
|
||||
'probes',
|
||||
);
|
||||
assert.equal(
|
||||
classify({ name: 'NOAA 15', group: 'weather', launchYear: 1998 }),
|
||||
'imaging',
|
||||
);
|
||||
assert.equal(
|
||||
classify({
|
||||
name: 'UNNAMED-1',
|
||||
group: 'visual',
|
||||
launchYear: 1990,
|
||||
satcat: { objectType: 'PAY', opsStatus: '+' },
|
||||
}),
|
||||
'probes',
|
||||
'SATCAT says it works',
|
||||
);
|
||||
});
|
||||
|
||||
test('only debris is refused the Cockpit', () => {
|
||||
for (const id of SATELLITE_CLASS_IDS)
|
||||
assert.equal(satelliteClassEnterable(id), id !== 'debris', id);
|
||||
assert.equal(satelliteClassEnterable('nonsense'), false);
|
||||
});
|
||||
|
||||
test('class visibility merges booleans and ignores everything else', () => {
|
||||
const next = mergeClassVisibility({ telecom: true, imaging: false, debris: 'yes', bogus: true });
|
||||
assert.deepEqual(next, { ...DEFAULT_SATELLITE_CLASSES, telecom: true, imaging: false });
|
||||
assert.deepEqual(mergeClassVisibility(null), { ...DEFAULT_SATELLITE_CLASSES });
|
||||
const next = mergeClassVisibility({
|
||||
telecom: true,
|
||||
imaging: false,
|
||||
debris: 'yes',
|
||||
bogus: true,
|
||||
});
|
||||
assert.deepEqual(next, {
|
||||
...DEFAULT_SATELLITE_CLASSES,
|
||||
telecom: true,
|
||||
imaging: false,
|
||||
});
|
||||
assert.deepEqual(mergeClassVisibility(null), {
|
||||
...DEFAULT_SATELLITE_CLASSES,
|
||||
});
|
||||
assert.equal(Object.hasOwn(next, 'bogus'), false);
|
||||
});
|
||||
|
||||
test('UCS owner countries normalise to ISO codes, organisations and joint owners', () => {
|
||||
assert.deepEqual(ownerFromUcs('USA'), { countryCode: 'US', label: 'United States' });
|
||||
assert.deepEqual(ownerFromUcs('United Kingdom'), { countryCode: 'GB', label: 'United Kingdom' });
|
||||
assert.deepEqual(ownerFromUcs('Sinapore'), { countryCode: 'SG', label: 'Singapore' }, 'UCS typo');
|
||||
assert.deepEqual(ownerFromUcs('Turkey'), { countryCode: 'TR', label: 'Türkiye' });
|
||||
assert.deepEqual(ownerFromUcs('ESA'), { countryCode: null, label: 'ESA' });
|
||||
assert.deepEqual(ownerFromUcs('Multinational'), { countryCode: null, label: 'Multinational' });
|
||||
assert.deepEqual(ownerFromUcs('China/France'), { countryCode: null, label: 'China / France' });
|
||||
assert.deepEqual(ownerFromUcs('United Kingdom/ESA'), { countryCode: null, label: 'United Kingdom / ESA' });
|
||||
assert.equal(ownerFromUcs(''), null);
|
||||
assert.equal(ownerFromUcs(null), null);
|
||||
});
|
||||
|
||||
test('SATCAT owner codes normalise the same way', () => {
|
||||
assert.deepEqual(ownerFromSatcat('US'), { countryCode: 'US', label: 'United States' });
|
||||
assert.deepEqual(ownerFromSatcat('PRC'), { countryCode: 'CN', label: 'China' });
|
||||
assert.deepEqual(ownerFromSatcat('CIS'), { countryCode: 'RU', label: 'Russia' }, 'former USSR');
|
||||
assert.deepEqual(ownerFromSatcat('ISRO'), { countryCode: 'IN', label: 'India' });
|
||||
assert.deepEqual(ownerFromSatcat('itso'), { countryCode: null, label: 'Intelsat' });
|
||||
assert.deepEqual(ownerFromSatcat('EUME'), { countryCode: null, label: 'EUMETSAT' });
|
||||
assert.deepEqual(ownerFromSatcat('PRES'), { countryCode: null, label: 'China / ESA' });
|
||||
assert.deepEqual(ownerFromSatcat('TMMC'), { countryCode: null, label: 'Turkmenistan / Monaco' });
|
||||
for (const code of ['TBD', 'UNK', '', null, 'ZZZZ']) assert.equal(ownerFromSatcat(code), null, String(code));
|
||||
});
|
||||
|
||||
test('owner prefers UCS, falls back to SATCAT, and Starlink is American', () => {
|
||||
assert.deepEqual(
|
||||
satelliteOwner({ ucs: { operatorCountry: 'ESA' }, satcat: { owner: 'FR' } }),
|
||||
{ countryCode: null, label: 'ESA' },
|
||||
);
|
||||
assert.deepEqual(satelliteOwner({ satcat: { owner: 'FR' } }), { countryCode: 'FR', label: 'France' });
|
||||
assert.deepEqual(satelliteOwner({ satelliteClass: 'starlink' }), { countryCode: 'US', label: 'United States' });
|
||||
assert.equal(satelliteOwner({}), null);
|
||||
assert.equal(satelliteOperator({ ucs: { operator: 'Planet Labs, Inc.' } }), 'Planet Labs, Inc.');
|
||||
assert.equal(satelliteOperator({ satelliteClass: 'starlink' }), 'SpaceX');
|
||||
assert.equal(satelliteOperator({ satelliteClass: 'imaging' }), null);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
test('COSPAR ids come from TLE line 1, across the 1957 century pivot', () => {
|
||||
assert.equal(cosparIdFromTleLine1(ISS_LINE1), '1998-067A');
|
||||
assert.equal(cosparIdFromTleLine1(ISS_LINE1.replace('98067A ', '24123BC ')), '2024-123BC');
|
||||
assert.equal(cosparIdFromTleLine1(ISS_LINE1.replace('98067A ', '57001A ')), '1957-001A');
|
||||
assert.equal(cosparIdFromTleLine1(ISS_LINE1.replace('98067A ', ' ')), null);
|
||||
assert.equal(cosparIdFromTleLine1(null), null);
|
||||
});
|
||||
|
||||
test('the TLE epoch is read as epoch milliseconds', () => {
|
||||
const satrec = twoline2satrec(ISS_LINE1, ISS_LINE2);
|
||||
// Day 001.5 of 2024: 1 January 2024, 12:00 UTC.
|
||||
assert.ok(Math.abs(tleEpochMs(satrec) - Date.UTC(2024, 0, 1, 12)) < 1000);
|
||||
assert.equal(tleEpochMs(null), null);
|
||||
assert.equal(tleEpochMs({}), null);
|
||||
});
|
||||
|
||||
test('SATCAT CSV parses, quoted commas included, keyed by NORAD id', () => {
|
||||
const csv = [
|
||||
'OBJECT_NAME,OBJECT_ID,NORAD_CAT_ID,OBJECT_TYPE,OPS_STATUS_CODE,OWNER,LAUNCH_DATE',
|
||||
@@ -250,8 +266,6 @@ test('SATCAT CSV parses, quoted commas included, keyed by NORAD id', () => {
|
||||
assert.deepEqual(records.get(25544), {
|
||||
objectType: 'PAY',
|
||||
opsStatus: '+',
|
||||
owner: 'ISS',
|
||||
cosparId: '1998-067A',
|
||||
launchDate: '1998-11-20',
|
||||
});
|
||||
assert.equal(records.get(733).objectType, 'R/B');
|
||||
|
||||
@@ -308,8 +308,6 @@ export function createIngestion({
|
||||
groups,
|
||||
// satellite.js does not keep the designator the year match reads.
|
||||
launchYear,
|
||||
line1: entry.line1,
|
||||
line2: entry.line2,
|
||||
satelliteClass: classifySatellite({
|
||||
noradId,
|
||||
name: entry.name,
|
||||
|
||||
@@ -14,7 +14,7 @@ export function createState({ services }) {
|
||||
|
||||
state._overlayHost = state.DEFAULT_OVERLAY_HOST;
|
||||
|
||||
// Satellite catalog: { noradId → { name, satrec, group, line1, line2,
|
||||
// Satellite catalog: { noradId → { name, satrec, group, launchYear,
|
||||
// satelliteClass, ucs, satcat } } — class, UCS and SATCAT records from
|
||||
// src/layers/satellites/classes.js and the sources the update read.
|
||||
|
||||
|
||||
Reference in new issue
Block a user