mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
- Status pieces translate in one place (src/ui/statusText.js): a Local
ADS-B error with a single piece now translates, and the analyst card
splits empty-layer notes into pieces (Transit, Local ADS-B).
- Translate the ALPR retry chip text, the ECCC error on the Air quality
card, "Upstream response too large" and the "entry {n}" feed label.
- The unmarked-text check also sees error/loadingLabel/statusMessage/
status assignments, `status:` properties and showToast?.( /
this._showToast( calls; the scene import preview marks its pack
statuses, data and model-facing text carry i18n-ignore with a reason.
- Tests check the real code: the HUD locality formatter moves to
src/hudLocalityFormat.js, the unsaved-keys test pins the teardown.
- One copy each: city names come from src/cityPois.js, the GNSS HTTP
template and wind "field unavailable" texts; dead fallbacks removed.
- Leftovers of the reverted vessel-label change and the reformatting of
detectionHost.test.mjs / keySetup.test.mjs are back to main.
Catalogs: es/ru 2617 messages each, complete.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
143 lines
4.1 KiB
JavaScript
143 lines
4.1 KiB
JavaScript
/**
|
|
* Browser live-source contract. Coordinates are WGS84 degrees; lengths and
|
|
* velocities are metres and metres/second; times are Unix milliseconds.
|
|
* Barometric altitude and ellipsoid altitude are separate, nullable observations.
|
|
* A missing timestamp is unknown, never the time the response was received.
|
|
*
|
|
* Sources expose getSnapshot(query, { signal }) and optionally
|
|
* getTrack(reference, { signal }). References are opaque to consumers.
|
|
* Snapshots describe coverage and completeness independently of freshness.
|
|
* Records contain observation data only, with no scene objects or transport data.
|
|
*/
|
|
// Messages are English data (AI tools read them too); the Layers panel shows
|
|
// them translated by their shape (translateFeedError in src/ui/statusText.js),
|
|
// so a new or reworded message is changed there as well.
|
|
export class LiveSourceError extends Error {
|
|
constructor(
|
|
code,
|
|
message,
|
|
{ status = null, retryAfterMs = 20000, source = null } = {},
|
|
) {
|
|
super(message);
|
|
this.name = 'LiveSourceError';
|
|
this.code = code;
|
|
this.status = status;
|
|
this.retryAfterMs = retryAfterMs;
|
|
this.source = source;
|
|
}
|
|
}
|
|
|
|
export function finite(value) {
|
|
if (value == null || value === '' || typeof value === 'boolean') return null;
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|
|
|
|
export function epoch(value, scale = 1) {
|
|
const number = finite(value);
|
|
const milliseconds = number == null ? null : number * scale;
|
|
return milliseconds != null &&
|
|
milliseconds > 0 &&
|
|
milliseconds <= 8640000000000000
|
|
? milliseconds
|
|
: null;
|
|
}
|
|
|
|
export function cleanText(value) {
|
|
return typeof value === 'string' ? value.trim() : '';
|
|
}
|
|
|
|
export function coordinates(latitude, longitude) {
|
|
return (
|
|
Number.isFinite(latitude) &&
|
|
Math.abs(latitude) <= 90 &&
|
|
Number.isFinite(longitude) &&
|
|
Math.abs(longitude) <= 180
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Admit a snapshot atomically; an all-invalid nonempty feed is unavailable.
|
|
* `unpositionedId(row)` names a rejected row that is a valid contact without
|
|
* a position: it is returned in `retainIds` and does not make the snapshot
|
|
* incomplete.
|
|
*/
|
|
export function admitRecords(rows, normalize, label, unpositionedId) {
|
|
if (!Array.isArray(rows))
|
|
throw new LiveSourceError('malformed', `Malformed ${label} response`);
|
|
const records = [];
|
|
const ids = new Set();
|
|
const retainIds = new Set();
|
|
let unpositioned = 0;
|
|
for (const row of rows) {
|
|
const record = normalize(row);
|
|
if (record && !ids.has(record.id)) {
|
|
records.push(record);
|
|
ids.add(record.id);
|
|
} else if (!record) {
|
|
const id = unpositionedId?.(row);
|
|
if (id) {
|
|
retainIds.add(id);
|
|
unpositioned += 1;
|
|
}
|
|
}
|
|
}
|
|
if (rows.length && !records.length) {
|
|
throw new LiveSourceError('malformed', `Malformed ${label} aircraft rows`);
|
|
}
|
|
return {
|
|
records,
|
|
retainIds,
|
|
complete: records.length + unpositioned === rows.length,
|
|
rejectedCount: rows.length - records.length,
|
|
};
|
|
}
|
|
|
|
/** Cancellation is checked after body parsing even when a transport ignores it. */
|
|
export async function readResponse(
|
|
fetchImpl,
|
|
url,
|
|
{ signal, ...init } = {},
|
|
source = 'Live source',
|
|
) {
|
|
signal?.throwIfAborted();
|
|
let response;
|
|
try {
|
|
response = await fetchImpl(url, { ...init, signal });
|
|
signal?.throwIfAborted();
|
|
} catch (error) {
|
|
signal?.throwIfAborted();
|
|
if (error?.name === 'AbortError') throw error;
|
|
throw new LiveSourceError('unavailable', `${source} network error`, {
|
|
source,
|
|
});
|
|
}
|
|
let payload = null;
|
|
try {
|
|
payload = await response.json();
|
|
} catch {
|
|
/* classified below */
|
|
}
|
|
signal?.throwIfAborted();
|
|
return { response, payload };
|
|
}
|
|
|
|
export function httpError(response, source) {
|
|
const status = response.status;
|
|
const code =
|
|
status === 429
|
|
? 'limited'
|
|
: status === 401 || status === 403
|
|
? 'denied'
|
|
: 'unavailable';
|
|
return new LiveSourceError(
|
|
code,
|
|
status === 429 ? `${source} rate limited` : `${source} HTTP ${status}`,
|
|
{
|
|
status,
|
|
source,
|
|
retryAfterMs: code === 'limited' || code === 'denied' ? 45000 : 20000,
|
|
},
|
|
);
|
|
}
|