mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
- i18n scanner: lastError sink, no false sinks on defaults and ternaries - shared src/i18n/marker.js for modules that only mark strings - wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js - drop review IDs, change history and stale references from comments and docs Co-Authored-By: Claude Opus 5.5 <[email protected]>
217 lines
8.5 KiB
JavaScript
217 lines
8.5 KiB
JavaScript
/**
|
|
* Satellite classification — CelesTrak source group → operator-legible class.
|
|
*
|
|
* The catalog already tags every satellite with the CelesTrak group it was
|
|
* ingested from (`CATALOG_GROUPS` in satellites.js), so classification is a
|
|
* pure lookup: no extra fetch, no heuristics, no per-frame work. This module is
|
|
* the single source of truth for the class a satellite belongs to, the label
|
|
* shown on its card, and the point color it renders in — the render path, the
|
|
* tracked card, and the row legend all read the same table, so a palette edit
|
|
* can never leave one surface disagreeing with another.
|
|
*
|
|
* Deliberately Cesium-free: colors are CSS hex strings that satellites.js
|
|
* converts once at module load. That keeps this file unit-testable and lets the
|
|
* legend swatches reuse the exact same strings the points are drawn with.
|
|
*
|
|
* Class order below is the legend order: crewed first (the objects people look
|
|
* for), then the constellations users can actually name, then the grab-bag.
|
|
*/
|
|
|
|
import { t, msg } from '../i18n/index.js';
|
|
|
|
/**
|
|
* Class registry: the point color of each class. The card and legend text is
|
|
* in CLASS_DISPLAY_KEYS.
|
|
* @type {Readonly<Record<string, { color: string }>>}
|
|
*/
|
|
export const SATELLITE_CLASSES = Object.freeze({
|
|
station: Object.freeze({
|
|
// Warm white — "crewed", and the brightest class in the sky. Deliberately
|
|
// NOT amber: 40-48deg amber is the app's known-military convention
|
|
// (flights.js MIL_TINT / militaryFlights.js MIL_ICON_COLOR) and must not
|
|
// leak into another domain. Slightly warm so it also separates from the
|
|
// pure white the civilian air fleet draws in.
|
|
color: '#fff6e5',
|
|
}),
|
|
nav: Object.freeze({
|
|
// Cyan — the strongest accent goes to the class users actually name
|
|
// ("how many GPS satellites are up?"). Takes over the slot the catch-all
|
|
// VISUAL group used to occupy, so this adds no new hue to the app.
|
|
color: '#4fd8ff',
|
|
}),
|
|
geo: Object.freeze({
|
|
// Violet, unchanged. Already the app's "space" semantic (the detection
|
|
// overlay paints the SAT tier #bda4ff), and the geostationary belt draws
|
|
// as one clean equatorial ring at globe scale.
|
|
color: '#c89bff',
|
|
}),
|
|
visual: Object.freeze({
|
|
// Muted blue-gray. This is the catch-all bucket, so it gives up the bright
|
|
// cyan it used to hold and recedes behind the three classes that mean
|
|
// something. Still well clear of the dense shell by luminance.
|
|
color: '#9fb3c4',
|
|
}),
|
|
comms: Object.freeze({
|
|
// Dim slate — thousands of these appear in DENSE mode. They must read as
|
|
// texture behind the core catalog, never compete with it. Rec.601 luma
|
|
// ~0.40 against VISUAL's ~0.69 keeps the two separable even when the
|
|
// NVG/FLIR shaders collapse the scene to a single channel.
|
|
color: '#54697f',
|
|
}),
|
|
});
|
|
|
|
/** Legend/report order for the classes above. */
|
|
export const SATELLITE_CLASS_ORDER = Object.freeze([
|
|
'station',
|
|
'nav',
|
|
'geo',
|
|
'visual',
|
|
'comms',
|
|
]);
|
|
|
|
// What the card and legend show for each class.
|
|
const CLASS_DISPLAY_KEYS = Object.freeze({
|
|
station: Object.freeze({
|
|
label: msg('STATION'),
|
|
blurb: msg('Crewed stations and their visiting vehicles'),
|
|
}),
|
|
nav: Object.freeze({
|
|
label: msg('NAV'),
|
|
blurb: msg('GNSS navigation — GPS, GLONASS, Galileo'),
|
|
}),
|
|
geo: Object.freeze({
|
|
label: msg('GEO'),
|
|
blurb: msg(
|
|
'Geostationary belt — comms and weather, fixed over the equator',
|
|
),
|
|
}),
|
|
visual: Object.freeze({
|
|
label: msg('VISUAL'),
|
|
blurb: msg('Brightest naked-eye objects — CelesTrak visual group'),
|
|
}),
|
|
comms: Object.freeze({
|
|
label: msg('COMMS'),
|
|
blurb: msg('Broadband constellation shell — shown only in DENSE mode'),
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* CelesTrak group tag → { class, subtype }. Subtype names the specific
|
|
* constellation so the card can read "NAV · GPS" rather than a bare "NAV".
|
|
* Keys must stay in sync with CATALOG_GROUPS + the 'dense' tag in satellites.js.
|
|
*/
|
|
const GROUP_CLASS = Object.freeze({
|
|
stations: Object.freeze({ klass: 'station', subtype: null }),
|
|
visual: Object.freeze({ klass: 'visual', subtype: null }),
|
|
'gps-ops': Object.freeze({ klass: 'nav', subtype: 'GPS' }),
|
|
glonass: Object.freeze({ klass: 'nav', subtype: 'GLONASS' }),
|
|
galileo: Object.freeze({ klass: 'nav', subtype: 'GALILEO' }),
|
|
geo: Object.freeze({ klass: 'geo', subtype: null }),
|
|
dense: Object.freeze({ klass: 'comms', subtype: 'STARLINK' }),
|
|
});
|
|
|
|
/** Unknown groups fall back to the neutral bucket rather than vanishing. */
|
|
const FALLBACK = Object.freeze({ klass: 'visual', subtype: null });
|
|
|
|
/**
|
|
* The ISS is always a STATION, whichever group ingested it. CelesTrak lists it
|
|
* in `visual` as well as `stations`, and a catalog rebuild survives a partial
|
|
* group outage — so a dropped stations feed would otherwise file the ISS under
|
|
* VISUAL in both its label and the legend tally.
|
|
*/
|
|
const ISS_CLASS = Object.freeze({ klass: 'station', subtype: 'ISS' });
|
|
|
|
/**
|
|
* Resolve a satellite to its class key and constellation subtype. This is the
|
|
* ONLY place the ISS special case lives; the label and the legend tally both
|
|
* route through here so they can never disagree about what the ISS is.
|
|
* @param {string|undefined|null} group CelesTrak group tag (see GROUP_CLASS).
|
|
* @param {{ isIss?: boolean }} [options] Whether this satellite is the ISS.
|
|
* @returns {{ klass: string, subtype: string|null }} Class key and subtype.
|
|
*/
|
|
export function satelliteClassOf(group, { isIss = false } = {}) {
|
|
if (isIss) return ISS_CLASS;
|
|
return GROUP_CLASS[group] || FALLBACK;
|
|
}
|
|
|
|
/**
|
|
* Point color for a catalog group, as a CSS hex string.
|
|
* @param {string|undefined|null} group CelesTrak group tag.
|
|
* @returns {string} CSS hex color.
|
|
*/
|
|
export function satelliteClassColor(group) {
|
|
return SATELLITE_CLASSES[satelliteClassOf(group).klass].color;
|
|
}
|
|
|
|
/**
|
|
* Operator-facing class label for a satellite — the text field shown on the
|
|
* tracked card and the detection overlay ("NAV · GPS", "GEO", "STATION · ISS").
|
|
* The ISS is the one object named individually: it is the reason most people
|
|
* open this layer, and its dot is already styled apart from its class.
|
|
*
|
|
* The ISS is always a STATION regardless of which group ingested it — see
|
|
* satelliteClassOf, which owns that rule for every consumer.
|
|
* @param {string|undefined|null} group CelesTrak group tag.
|
|
* @param {{ isIss?: boolean, localize?: boolean }} [options] isIss: the ISS;
|
|
* localize: false keeps the English label (data for the AI tools).
|
|
* @returns {string} Display label.
|
|
*/
|
|
export function satelliteClassLabel(
|
|
group,
|
|
{ isIss = false, localize = true } = {},
|
|
) {
|
|
const { klass, subtype } = satelliteClassOf(group, { isIss });
|
|
const label = CLASS_DISPLAY_KEYS[klass].label;
|
|
const base = localize ? t(label) : label;
|
|
return subtype ? `${base} · ${subtype}` : base;
|
|
}
|
|
|
|
/**
|
|
* Tally satellites into a class → count record.
|
|
* Split from the legend builder so the counting rule stays independent of how
|
|
* the legend is presented. Entries may be bare group tags, or `{ group, isIss }`
|
|
* descriptors when the caller can identify the ISS — the tally classifies
|
|
* through satelliteClassOf, so the legend and the card always agree.
|
|
* @param {Iterable<string|undefined|null|{group?: string, isIss?: boolean}>} entries
|
|
* One entry per satellite.
|
|
* @returns {Record<string, number>} Class key → count.
|
|
*/
|
|
export function tallySatelliteClasses(entries) {
|
|
const counts = Object.create(null);
|
|
for (const entry of entries || []) {
|
|
const descriptor =
|
|
entry && typeof entry === 'object' ? entry : { group: entry };
|
|
const { klass } = satelliteClassOf(descriptor.group, {
|
|
isIss: descriptor.isIss,
|
|
});
|
|
counts[klass] = (counts[klass] || 0) + 1;
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
/**
|
|
* Build the layer-row legend from a class tally.
|
|
* Classes with no members are omitted so the legend never advertises a class
|
|
* that is not on screen (COMMS only appears once DENSE is on).
|
|
* @param {Record<string, number>} counts Class key → count (see tallySatelliteClasses).
|
|
* @returns {Array<{ klass: string, label: string, color: string, blurb: string, count: number }>}
|
|
* Present classes in legend order.
|
|
*/
|
|
export function satelliteClassLegend(counts) {
|
|
const result = [];
|
|
for (const klass of SATELLITE_CLASS_ORDER) {
|
|
const count = counts?.[klass];
|
|
if (!(count > 0)) continue;
|
|
const spec = SATELLITE_CLASSES[klass];
|
|
const display = CLASS_DISPLAY_KEYS[klass];
|
|
result.push({
|
|
klass,
|
|
label: t(display.label),
|
|
color: spec.color,
|
|
blurb: t(display.blurb),
|
|
count,
|
|
});
|
|
}
|
|
return result;
|
|
}
|