mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
fix(imagery): require explicit Data-Present proof before decoding thumbnails
Rebased onto current main with review follow-ups folded in. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
0265ad4c19
commit
3962ac52f7
5 files changed
+41
-3
No files matched your search
@@ -33,6 +33,8 @@
|
|||||||
|
|
||||||
- Director timeline seeks cross the dateline along the short longitude arc.
|
- Director timeline seeks cross the dateline along the short longitude arc.
|
||||||
|
|
||||||
|
- Recent Imagery thumbnails require explicit presence evidence before reporting available pixels.
|
||||||
|
|
||||||
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
||||||
clearing obsolete contacts while retaining stale or incomplete snapshots.
|
clearing obsolete contacts while retaining stale or incomplete snapshots.
|
||||||
|
|
||||||
|
|||||||
@@ -4449,6 +4449,8 @@ are omitted rather than framing the wrong part of the globe.
|
|||||||
|
|
||||||
### Recent Imagery (September 2026)
|
### Recent Imagery (September 2026)
|
||||||
|
|
||||||
|
Thumbnail decoding requires explicit `Data-Present: true` evidence; false remains empty, while absent or malformed evidence reports an error. Injected `getThumbnail` sources must return an OK Response with readable `Data-Present: true` to admit image bytes, or `false` for confirmed empty coverage. Cross-origin sources must expose that header to browser JavaScript.
|
||||||
|
|
||||||
- The layer sits in the Data Layers panel's Cameras group beside Cameras (CCTV); Mapped ALPR Cameras moved to the Infrastructure group to make room.
|
- The layer sits in the Data Layers panel's Cameras group beside Cameras (CCTV); Mapped ALPR Cameras moved to the Infrastructure group to make room.
|
||||||
- DATA ▸ **Recent Imagery** (`recent-imagery`, `src/layers/recentImagery/`) lists the last 30 UTC days of imagery for a selected box and drapes the days you pick: HLS S30 (Sentinel-2) and L30 (Landsat 8/9) at 30 m, and the VIIRS NOAA-21 daily overview at 250 m. Keyless and browser-direct: NASA CMR for which days have HLS granules in the box (`catalog.js`, both collections concurrently, paged with `CMR-Search-After` up to 2,000 records each, `truncated` against CMR `hits`), GIBS WMTS for tiles, Worldview Snapshots for thumbnails and exports. Worldview's `Data-Present` header is the only availability signal: GIBS answers an empty tile with HTTP 200, and the JPEG overview would drape black, so a day confirmed empty never drapes.
|
- DATA ▸ **Recent Imagery** (`recent-imagery`, `src/layers/recentImagery/`) lists the last 30 UTC days of imagery for a selected box and drapes the days you pick: HLS S30 (Sentinel-2) and L30 (Landsat 8/9) at 30 m, and the VIIRS NOAA-21 daily overview at 250 m. Keyless and browser-direct: NASA CMR for which days have HLS granules in the box (`catalog.js`, both collections concurrently, paged with `CMR-Search-After` up to 2,000 records each, `truncated` against CMR `hits`), GIBS WMTS for tiles, Worldview Snapshots for thumbnails and exports. Worldview's `Data-Present` header is the only availability signal: GIBS answers an empty tile with HTTP 200, and the JPEG overview would drape black, so a day confirmed empty never drapes.
|
||||||
- **Box.** SELECT BOX (`src/ui/imageryBoxTool.js`) claims the pointer like Draw, borrows the stock click actions and freezes the camera only mid-drag; USE VIEW takes the camera rectangle; `boxFromPinAt` grows a 10 km box. Boxes must be ≤ 1,000 km a side, on one side of the dateline and inside the Web-Mercator limit (`validateBox`). A refusal (`boxError`, including the tool's own via `reportBoxRefusal`) stays at the top of the panel until a box succeeds; a view over the cap is refused with its size, never shrunk. An oversized box or view (`Box is … km wide · limit 1,000 km`, `View is … wide · …`) sets the hint to `Zoom in or draw a smaller box` and enables ZOOM IN: `zoomToFit()` flies top-down (1.2 s) to the refused box's centre, or the ground under the canvas centre for a view, at the height whose view is about 400 km wide for the canvas aspect and the camera's vertical FOV (`fitViewHeightM`, clamped 5–400 km), and returns that height.
|
- **Box.** SELECT BOX (`src/ui/imageryBoxTool.js`) claims the pointer like Draw, borrows the stock click actions and freezes the camera only mid-drag; USE VIEW takes the camera rectangle; `boxFromPinAt` grows a 10 km box. Boxes must be ≤ 1,000 km a side, on one side of the dateline and inside the Web-Mercator limit (`validateBox`). A refusal (`boxError`, including the tool's own via `reportBoxRefusal`) stays at the top of the panel until a box succeeds; a view over the cap is refused with its size, never shrunk. An oversized box or view (`Box is … km wide · limit 1,000 km`, `View is … wide · …`) sets the hint to `Zoom in or draw a smaller box` and enables ZOOM IN: `zoomToFit()` flies top-down (1.2 s) to the refused box's centre, or the ground under the canvas centre for a view, at the height whose view is about 400 km wide for the canvas aspect and the camera's vertical FOV (`fitViewHeightM`, clamped 5–400 km), and returns that height.
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { searchHls } from './catalog.js';
|
import { searchHls } from './catalog.js';
|
||||||
import { gibsTemplate, wvsSnapshotUrl } from './model.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({
|
export function createRecentImagerySource({
|
||||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||||
search = searchHls,
|
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;
|
cancelled = true;
|
||||||
} else if (response?.ok) {
|
} else if (response?.ok) {
|
||||||
const present = String(
|
const present = String(
|
||||||
response.headers?.get?.('Data-Present') ?? 'true',
|
response.headers?.get?.('Data-Present') ?? '',
|
||||||
).toLowerCase();
|
).toLowerCase();
|
||||||
acquisitionTime = response.headers?.get?.('Acquisition-Time') || null;
|
acquisitionTime = response.headers?.get?.('Acquisition-Time') || null;
|
||||||
if (present === 'false') {
|
if (present === 'false') {
|
||||||
status = 'empty';
|
status = 'empty';
|
||||||
} else {
|
} else if (present === 'true') {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
if (controller.signal.aborted) cancelled = true;
|
if (controller.signal.aborted) cancelled = true;
|
||||||
else {
|
else {
|
||||||
|
|||||||
Reference in new issue
Block a user