mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge meirm/i18n-spanish: upstream main (geocode proxy, radio mirror, CMR, terrain, tools)
Both sides had merged the same main features (cockpit emergencies, GNSS integrity, geo file import) with their own i18n. Conflicts keep this branch's reviewed version; the 7 catalog entries only the other side used are pruned. Newer main commits come in unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
54f7513ef6
38 files changed
+1139
-118
No files matched your search
@@ -27,7 +27,8 @@ const CMR_SEARCH_AFTER = 'CMR-Search-After';
|
||||
/**
|
||||
* Read one product's granules across CMR pages, sending back each response's
|
||||
* `CMR-Search-After` cursor until a short page, a missing cursor or the
|
||||
* record cap ends the walk. `truncated` is measured against CMR's `hits`.
|
||||
* record cap ends the walk. `truncated` means CMR's `hits` exceed the rows
|
||||
* read, or the cap dropped valid granules from the last page read.
|
||||
* @param {{ product: string, url: string, fetchImpl: typeof fetch, signal?: AbortSignal, maxRecords?: number }} request
|
||||
* @returns {Promise<{ granules: Array<object>, truncated: boolean }>}
|
||||
*/
|
||||
@@ -40,6 +41,8 @@ export async function fetchCmrPages({
|
||||
}) {
|
||||
const granules = [];
|
||||
let hits = 0;
|
||||
let readCount = 0;
|
||||
let capped = 0;
|
||||
let cursor = null;
|
||||
for (;;) {
|
||||
const init = { signal };
|
||||
@@ -47,19 +50,27 @@ export async function fetchCmrPages({
|
||||
const response = await fetchImpl(url, init);
|
||||
if (!response?.ok)
|
||||
throw new Error(`HTTP ${response?.status ?? 'error'} from CMR`);
|
||||
const page = parseCmrUmm(await response.json(), product);
|
||||
granules.push(...page.granules);
|
||||
const json = await response.json();
|
||||
const rawCount = Array.isArray(json?.items) ? json.items.length : 0;
|
||||
const page = parseCmrUmm(json, product);
|
||||
readCount += rawCount;
|
||||
const admitted = page.granules.slice(
|
||||
0,
|
||||
Math.max(0, maxRecords - granules.length),
|
||||
);
|
||||
granules.push(...admitted);
|
||||
capped += page.granules.length - admitted.length;
|
||||
hits = Math.max(hits, page.hits);
|
||||
cursor = response.headers?.get?.(CMR_SEARCH_AFTER) ?? null;
|
||||
if (
|
||||
!cursor ||
|
||||
page.granules.length < CMR_PAGE_SIZE ||
|
||||
granules.length >= maxRecords ||
|
||||
granules.length >= hits
|
||||
rawCount < CMR_PAGE_SIZE ||
|
||||
readCount >= maxRecords ||
|
||||
readCount >= hits
|
||||
)
|
||||
break;
|
||||
}
|
||||
return { granules, truncated: hits > granules.length };
|
||||
return { granules, truncated: hits > readCount || capped > 0 };
|
||||
}
|
||||
|
||||
function finiteOrNull(value) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { fetchCmrPages } from './catalog.js';
|
||||
const item = (id) => ({
|
||||
meta: { 'concept-id': id },
|
||||
umm: {
|
||||
TemporalExtent: {
|
||||
RangeDateTime: { BeginningDateTime: '2026-09-18T12:00:00Z' },
|
||||
},
|
||||
},
|
||||
});
|
||||
test('discarded rows do not turn a full CMR page into a final page', async () => {
|
||||
let calls = 0;
|
||||
const result = await fetchCmrPages({
|
||||
product: 'S30',
|
||||
url: 'fixture',
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
const items =
|
||||
calls === 1
|
||||
? Array.from({ length: 200 }, (_, i) =>
|
||||
i === 0 ? {} : item(String(i)),
|
||||
)
|
||||
: [item('last')];
|
||||
return Response.json(
|
||||
{ hits: 201, items },
|
||||
{ headers: { 'CMR-Search-After': `cursor-${calls}` } },
|
||||
);
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(result.granules.length, 200);
|
||||
assert.equal(result.granules.at(-1).id, 'last');
|
||||
assert.equal(result.truncated, false);
|
||||
});
|
||||
test('a non-page-multiple limit never returns more admitted records than its budget', async () => {
|
||||
let calls = 0;
|
||||
const result = await fetchCmrPages({
|
||||
product: 'S30',
|
||||
url: 'fixture',
|
||||
maxRecords: 250,
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
return Response.json(
|
||||
{
|
||||
hits: 1000,
|
||||
items: Array.from({ length: 200 }, (_, i) => item(`${calls}:${i}`)),
|
||||
},
|
||||
{ headers: { 'CMR-Search-After': `cursor-${calls}` } },
|
||||
);
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(result.granules.length, 250);
|
||||
assert.equal(result.truncated, true);
|
||||
});
|
||||
test('valid records dropped by the cap mark the result truncated even when every hit was read', async () => {
|
||||
let calls = 0;
|
||||
const result = await fetchCmrPages({
|
||||
product: 'S30',
|
||||
url: 'fixture',
|
||||
maxRecords: 250,
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
return Response.json(
|
||||
{
|
||||
hits: 400,
|
||||
items: Array.from({ length: 200 }, (_, i) => item(`${calls}:${i}`)),
|
||||
},
|
||||
{ headers: { 'CMR-Search-After': `cursor-${calls}` } },
|
||||
);
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(result.granules.length, 250);
|
||||
assert.equal(result.truncated, true);
|
||||
});
|
||||
test('malformed rows discarded under the cap leave a fully examined result untruncated', async () => {
|
||||
let calls = 0;
|
||||
const result = await fetchCmrPages({
|
||||
product: 'S30',
|
||||
url: 'fixture',
|
||||
maxRecords: 250,
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
const items = Array.from({ length: 200 }, (_, i) =>
|
||||
calls === 2 && i >= 50 ? {} : item(`${calls}:${i}`),
|
||||
);
|
||||
return Response.json(
|
||||
{ hits: 400, items },
|
||||
{ headers: { 'CMR-Search-After': `cursor-${calls}` } },
|
||||
);
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(result.granules.length, 250);
|
||||
assert.equal(result.truncated, false);
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
import { searchHls } from './catalog.js';
|
||||
import { gibsTemplate, wvsSnapshotUrl } from './model.js';
|
||||
|
||||
/** NASA-compatible imagery acquisition; transport and URL builders are replaceable. */
|
||||
/**
|
||||
* NASA-compatible imagery acquisition; transport and URL builders are replaceable.
|
||||
* Thumbnail responses must expose Data-Present: true for image admission, or
|
||||
* false for confirmed empty coverage, including injected getThumbnail sources.
|
||||
*/
|
||||
export function createRecentImagerySource({
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
search = searchHls,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createThumbnailLoader } from './thumbnails.js';
|
||||
test('missing or malformed presence evidence cannot prove thumbnail pixels', async () => {
|
||||
for (const header of [null, 'unknown', '', 'true', 'false']) {
|
||||
let decodes = 0;
|
||||
const loader = createThumbnailLoader({
|
||||
requestThumbnail: async () => ({
|
||||
ok: true,
|
||||
headers: { get: (key) => (key === 'Data-Present' ? header : null) },
|
||||
blob: async () => {
|
||||
decodes++;
|
||||
return new Blob(['fixture']);
|
||||
},
|
||||
}),
|
||||
createObjectUrl: () => 'blob:fixture',
|
||||
});
|
||||
loader.request(
|
||||
{ key: 'fixture', product: 'S30', day: '2026-09-18' },
|
||||
{ west: 0, east: 1, south: 0, north: 1 },
|
||||
);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(
|
||||
loader.get('fixture').status,
|
||||
header === 'true' ? 'present' : header === 'false' ? 'empty' : 'error',
|
||||
);
|
||||
assert.equal(decodes, header === 'true' ? 1 : 0);
|
||||
loader.destroy();
|
||||
}
|
||||
});
|
||||
@@ -100,12 +100,12 @@ export function createThumbnailLoader({
|
||||
cancelled = true;
|
||||
} else if (response?.ok) {
|
||||
const present = String(
|
||||
response.headers?.get?.('Data-Present') ?? 'true',
|
||||
response.headers?.get?.('Data-Present') ?? '',
|
||||
).toLowerCase();
|
||||
acquisitionTime = response.headers?.get?.('Acquisition-Time') || null;
|
||||
if (present === 'false') {
|
||||
status = 'empty';
|
||||
} else {
|
||||
} else if (present === 'true') {
|
||||
const blob = await response.blob();
|
||||
if (controller.signal.aborted) cancelled = true;
|
||||
else {
|
||||
|
||||
Reference in new issue
Block a user