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:
Sameh Khamis committed 2026-11-08 15:16:01 -08:00
1 parent 7dcf671b2e
commit 3bb742ae7e
13 files changed
+148 -617

No files matched your search

+2 -2
View File
@@ -229,7 +229,7 @@ The [Bhote Koshi event pack](public/events/bhote-koshi-2026/README.md), under `p
| **Québec CCTV source catalog** (1,120 locations) | `public/quebec-cameras.json` | Mixed: MTQ locations CC BY 4.0; Montréal camera nodes ODbL 1.0; scenic records retain per-record provenance. No video files are bundled or cached. | Location data is per-record; video remains hosted by Québec 511 and its platform terms apply; scenic source pages may have separate terms | MTQ / Données Québec CC BY 4.0; © OpenStreetMap contributors (ODbL); scenic operator and position source shown in the selected-camera panel |
| **World camera image subset** (2,230 locations) | `config/cctv_sources.world-image-cameras.json` | Mixed: ODOT TripCheck public images (ODOT permits use with credit) and MLIT site content under terms compatible with CC BY 4.0. Frames are not bundled or persisted; the server holds a bounded in-memory cache. | Per-source terms apply; NZTA Journey Planner web-cache rows are excluded; Vegagerðin rows are served by the Iceland pack instead | Oregon Department of Transportation (Camera courtesy of ODOT) and Japan Ministry of Land, Infrastructure, Transport and Tourism (出典:国土交通省 川の防災情報) |
| **TeleGeography Submarine Cable Map** (712 cables + 1,917 landing points) | `telegeography_submarine_cables/` | **CC BY-NC-SA 3.0** | ❌ **NonCommercial — remove for commercial use** | "© TeleGeography — submarinecablemap.com" |
| **UCS Satellite Database** UCS-Satellite-Database 5-1-2023 (3,564 records; operator, owner country, purpose) | `ucs_satellites/` | **Free, unrestricted use** (UCS database manual, section 3) | ✅ (acknowledgement requested) | "UCS Satellite Database (UCS-Satellite-Database 5-1-2023), https://www.ucsusa.org/satellite_database" |
| **UCS Satellite Database** UCS-Satellite-Database 5-1-2023 (3,564 records; purpose) | `ucs_satellites/` | **Free, unrestricted use** (UCS database manual, section 3) | ✅ (acknowledgement requested) | "UCS Satellite Database (UCS-Satellite-Database 5-1-2023), https://www.ucsusa.org/satellite_database" |
| **Natural Earth physical regions** (1,046 land + 292 marine named polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
| **DataSF Analysis Neighborhoods** (41 SF neighborhood polygons) | `neighborhoods/` | **PDDL 1.0** (public domain) | ✅ (no restrictions) | "City & County of San Francisco — DataSF" (courtesy — not legally required) |
| **Natural Earth countries, states and provinces** (260 admin-0/map units + 4,587 admin-1 polygons) | `natural_earth/` | **Public domain** | ✅ (no restrictions) | "Made with Natural Earth" (courtesy credit — not legally required) |
@@ -296,7 +296,7 @@ The former bundled 2026-05-25 snapshot was removed 2026-07-16.
### UCS Satellite Database (`ucs_satellites/`)
Operator, owner country and purpose for active satellites, keyed by NORAD number, from the Union of Concerned Scientists Satellite Database release **UCS-Satellite-Database 5-1-2023** (data current through 2023-05-01; https://www.ucsusa.org/satellite_database). The satellites layer joins it to CelesTrak positions and SATCAT records to file each satellite as a station, imaging, telecom, Starlink, probe or debris object, and to name its operator and owner. Section 3 of the UCS database manual: "The Database is free and its use is unrestricted. We request that its use be acknowledged and referenced in written materials. References should include 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." The app acknowledges it in the Data attribution popover. UCS has paused updates since this release; Starlink rows are dropped (Starlink is classed by name). Build: `scripts/build-ucs-satellites.py`; curation in the folder README.
The purpose of each active satellite, keyed by NORAD number, from the Union of Concerned Scientists Satellite Database release **UCS-Satellite-Database 5-1-2023** (data current through 2023-05-01; https://www.ucsusa.org/satellite_database). The satellites layer joins it to CelesTrak positions and SATCAT records to file each satellite as a station, imaging, telecom, Starlink, probe or debris object. Section 3 of the UCS database manual: "The Database is free and its use is unrestricted. We request that its use be acknowledged and referenced in written materials. References should include 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." The app acknowledges it in the Data attribution popover. UCS has paused updates since this release; Starlink rows are dropped (Starlink is classed by name). Build: `scripts/build-ucs-satellites.py`; curation in the folder README.
### Natural Earth physical regions (`natural_earth/`)
+1 -1
View File
@@ -5458,7 +5458,7 @@ silently demoting every later lookup for the session.
- **Satellite classes** (`src/layers/satellites/classes.js`): every satellite is filed under one of six classes, each with its own point colour, legend swatch and toggle: STATIONS (warm white), IMAGING (cyan), TELECOM (violet; communications and navigation, so GPS, GLONASS, Galileo and BeiDou read as one family), STARLINK (dim slate), PROBES (muted blue-gray) and DEBRIS (receding brown-gray). That module is the single source of truth for class, label and colour, so the dot, the card and the legend swatch cannot disagree. Two deliberate palette rules: no class may sit in the 40–48° amber band, which is the app-wide known-military convention; and STARLINK stays far below PROBES in Rec.601 luminance so the dense shell stays separable when NVG/FLIR collapse the scene to one channel. The ISS keeps its long-standing red hero dot rather than the STATIONS colour — it carries a permanent name label and its card still reads `STATIONS · ISS`; it is a station whichever group ingested it, so a stations-feed outage cannot refile it. `src/data/satelliteClass.js` keeps the group-based labels beneath the classes: the constellation subtype and a group's own label for a satellite with no class.
- Class is also a **text field**, not just a color: `TELECOM · GPS` / `STARLINK` / `STATIONS · ISS` leads the tracked card's detail block and replaces the raw CelesTrak tag on the detection-overlay label. Because that canvas composites above the post-FX chain, the class stays readable in NVG/FLIR after the in-scene dot colors are flattened.
- The satellites row in DATA LAYERS carries per-layer sub-controls (`DataLayerManager._syncRowControls`, the first consumer of the optional `getRowControls()` layer hook): one chip per class (the STARLINK chip exposes the existing `catalog` param), a swatch legend with live per-class counts, and an info line while SATCAT is stale or unavailable (`Classes may lag: SATCAT is a cached copy` / `Classes may lag: SATCAT unavailable`). Default is the sparse core catalog. The chips are stateless — each declares the params to apply and the manager owns the write — so the Space Missions capture/restore path over the same param stays authoritative. Controls stay hidden while the layer is off. An explicit CORE or DENSE choice participates in versioned local and share-link state; temporary Space Missions overrides do not.
- **Class toggles** (`src/layers/satellites/classes.js`): the layer row carries one chip per class — STATIONS, IMAGING, TELECOM (communications and navigation), PROBES (science, technology development and demonstration, unknown purpose) and DEBRIS (rocket bodies, fragments, unknown objects and dead payloads) on by default, so the layer shows what it did before classes; STARLINK off, as DENSE was. Points of a hidden class stay built and are only hidden, so a toggle needs no reload; hidden classes are left out of the detection overlay and the voice/framing position lists, and the legend keys only the classes on screen. The class comes, strongest evidence first, from CelesTrak SATCAT (object type and operational status, fetched per group through `/api/celestrak-satcat/<group>`, cached a day), the bundled UCS Satellite Database pack (UCS-Satellite-Database 5-1-2023: operator, owner country and purpose; `src/data/local_data/ucs_satellites/`), name patterns for satellites launched after May 2023, and finally the CelesTrak group. Only crewed stations and the craft that visit them, by name, are STATIONS: the CubeSats CelesTrak's stations group also carries after their deployment from the ISS are classed like any other satellite. A payload whose SATCAT record gives no status, with no UCS record and no recognisable name, that launched 20 or more years ago (SATCAT's launch date, else the TLE designator's year) is presumed dead and filed as debris; without a SATCAT record (a failed read) nothing is presumed. To cover imaging launched after the UCS snapshot the core catalog also reads CelesTrak's `resource`, `weather`, `planet`, `sar` and `science` groups. Retired stations and the shuttles have re-entered and have no orbit to show. An explicit class choice is kept in local and share-link state as the `classes` option, one letter per class shown (`sitpd`: stations, imaging, telecom, probes, debris; `0`: none; `src/data/satelliteClassMask.js`), and the layer takes those letters back through `setParams({classes})`; STARLINK maps to the existing `catalog: 'dense'` param instead.
- **Class toggles** (`src/layers/satellites/classes.js`): the layer row carries one chip per class — STATIONS, IMAGING, TELECOM (communications and navigation), PROBES (science, technology development and demonstration, unknown purpose) and DEBRIS (rocket bodies, fragments, unknown objects and dead payloads) on by default, so the layer shows what it did before classes; STARLINK off, as DENSE was. Points of a hidden class stay built and are only hidden, so a toggle needs no reload; hidden classes are left out of the detection overlay, the voice/framing position lists and analyst answers (the tracked satellite still answers), and the legend keys only the classes on screen. The class comes, strongest evidence first, from CelesTrak SATCAT (object type and operational status, fetched per group through `/api/celestrak-satcat/<group>`, cached a day), the bundled UCS Satellite Database pack (UCS-Satellite-Database 5-1-2023: each satellite's purpose; `src/data/local_data/ucs_satellites/`), name patterns for satellites launched after May 2023, and finally the CelesTrak group. Only crewed stations and the craft that visit them, by name, are STATIONS: the CubeSats CelesTrak's stations group also carries after their deployment from the ISS are classed like any other satellite. A payload whose SATCAT record gives no status, with no UCS record and no recognisable name, that launched 20 or more years ago (SATCAT's launch date, else the TLE designator's year) is presumed dead and filed as debris; without a SATCAT record (a failed read) nothing is presumed. To cover imaging launched after the UCS snapshot the core catalog also reads CelesTrak's `resource`, `weather`, `planet`, `sar` and `science` groups. Retired stations and the shuttles have re-entered and have no orbit to show. An explicit class choice is kept in local and share-link state as the `classes` option, one letter per class shown (`sitpd`: stations, imaging, telecom, probes, debris; `0`: none; `src/data/satelliteClassMask.js`), and the layer takes those letters back through `setParams({classes})`; STARLINK maps to the existing `catalog: 'dense'` param instead.
- **The STARLINK chip (formerly DENSE) reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `STARLINK ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `STARLINK ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale.
- **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert.
- The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe.
+6 -10
View File
@@ -2,9 +2,9 @@
'''Maintainer-only UCS Satellite Database pack build (openpyxl).
Reads the UCS Excel release and writes the compact pack the satellites layer
loads: one row per satellite, keyed by NORAD number, with string-table indexes
for the repeated text columns. Starlink rows are left out: the layer files
every STARLINK object under its own class and operator without them.
loads: one row per satellite, keyed by NORAD number, with a string-table index
for its purpose, the one column the layer's classes read. Starlink rows are
left out: the layer files every STARLINK object under its own class by name.
Usage: python scripts/build-ucs-satellites.py "<path>/UCS-Satellite-Database 5-1-2023.xlsx"
'''
@@ -26,12 +26,7 @@ URL = 'https://www.ucsusa.org/satellite_database'
# Output column -> spreadsheet header.
COLUMNS = {
'operator': 'Operator/Owner',
'operatorCountry': 'Country of Operator/Owner',
'purpose': 'Purpose',
'detailedPurpose': 'Detailed Purpose',
'users': 'Users',
'orbitClass': 'Class of Orbit',
}
STARLINK = re.compile(r'\bstarlink\b', re.I)
@@ -65,11 +60,12 @@ def build(xlsx: Path) -> dict:
except (TypeError, ValueError):
invalid_norad += 1
continue
if norad <= 0:
invalid_norad += 1
continue
record = [norad]
for key, column in COLUMNS.items():
text = clean(row[header[column]])
if key == 'orbitClass':
text = text.upper()
if text not in index[key]:
index[key][text] = len(tables[key])
tables[key].append(text)
+1 -1
View File
@@ -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',
},
+13 -19
View File
@@ -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
+4 -7
View File
@@ -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
*/
/**
+8 -26
View File
@@ -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', () => {
-2
View File
@@ -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,
+1 -449
View File
@@ -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,
});
}
+110 -96
View File
@@ -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');
-2
View File
@@ -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,
+1 -1
View File
@@ -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.