mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Adds an "S2 GhostMaps" layer (Events group) that renders S2 Underground's GhostMaps KMZ exports (Common Intelligence Picture, Kinetic 30d, Border Crisis, Iran CIP/Kinetic, Yemen FLOT, latest incident drop). - server/providers/ghostMaps.js: watcher polls the repo's git smart-HTTP ref advertisement (default every 15s, GHOSTMAPS_POLL_SECONDS), fetches the tree only on a new head, caches commit-pinned files by blob sha and pushes catalog updates to browsers over SSE (/api/ghostmaps/events). - src/layers/ghostMaps: KML reader, ArcGIS popup-table parsing, per-product data sources, product chips, horizon-culled pins, report cards. - Unit tests, QA gate (scripts/qa-ghostmaps.mjs), credits (CC BY-NC-SA 4.0), i18n, voice manifest, layer-state token 5. Co-Authored-By: Claude Opus 5.5 <[email protected]>
82 lines
2.6 KiB
JavaScript
82 lines
2.6 KiB
JavaScript
import {
|
|
readResponseBytesCapped,
|
|
readResponseJsonCapped,
|
|
} from '../../sources/httpBody.js';
|
|
import { GHOSTMAPS_MAX_FILE_BYTES } from './catalog.js';
|
|
|
|
// A portable export (package.json): no i18n here. Failures stay English data;
|
|
// the Layers panel shows them translated.
|
|
const msg = (text) => text;
|
|
|
|
/**
|
|
* Same-origin GhostMaps source: the catalog, one product's KMZ bytes, and a
|
|
* push subscription that fires whenever the watcher sees a new commit.
|
|
*/
|
|
export function createGhostMapsSource({
|
|
fetchImpl = (...args) => globalThis.fetch(...args),
|
|
EventSourceImpl = globalThis.EventSource,
|
|
} = {}) {
|
|
return {
|
|
async getCatalog({ signal } = {}) {
|
|
const response = await fetchImpl('/api/ghostmaps/catalog', { signal });
|
|
if (!response.ok)
|
|
throw new Error(
|
|
`GhostMaps catalog unavailable (HTTP ${response.status})`,
|
|
);
|
|
const catalog = await readResponseJsonCapped(
|
|
response,
|
|
1024 * 1024,
|
|
signal,
|
|
);
|
|
if (!Array.isArray(catalog?.products))
|
|
throw new Error(msg('Malformed GhostMaps catalog'));
|
|
return catalog;
|
|
},
|
|
|
|
async getFile(id, version, { signal } = {}) {
|
|
const response = await fetchImpl(
|
|
`/api/ghostmaps/file/${encodeURIComponent(id)}?v=${encodeURIComponent(version)}`,
|
|
{ signal },
|
|
);
|
|
if (!response.ok)
|
|
throw new Error(`GhostMaps file unavailable (HTTP ${response.status})`);
|
|
return readResponseBytesCapped(
|
|
response,
|
|
GHOSTMAPS_MAX_FILE_BYTES,
|
|
signal,
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Call `onCatalog(catalog)` for every catalog the server pushes, and
|
|
* `onState('open' | 'connecting' | 'closed')` as the stream changes. The
|
|
* browser's EventSource reconnects by itself. Returns an unsubscribe.
|
|
*/
|
|
subscribe(onCatalog, onState = () => {}) {
|
|
if (typeof EventSourceImpl !== 'function') {
|
|
onState('closed');
|
|
return () => {};
|
|
}
|
|
const events = new EventSourceImpl('/api/ghostmaps/events');
|
|
const states = ['connecting', 'open', 'closed'];
|
|
const report = () => onState(states[events.readyState] ?? 'connecting');
|
|
events.onopen = report;
|
|
events.onerror = report;
|
|
const listener = (event) => {
|
|
try {
|
|
const catalog = JSON.parse(event.data);
|
|
if (Array.isArray(catalog?.products)) onCatalog(catalog);
|
|
} catch {
|
|
/* ignore a malformed frame; the next one replaces it */
|
|
}
|
|
};
|
|
events.addEventListener('catalog', listener);
|
|
report();
|
|
return () => {
|
|
events.removeEventListener('catalog', listener);
|
|
events.close();
|
|
};
|
|
},
|
|
};
|
|
}
|