feat: add a recent satellite imagery tool

Add get_recent_imagery. It searches the recent Harmonized Landsat and
Sentinel-2 catalog for an area, picks the image the layer would show
with the layer's own ranking, and returns it with its date, sensor and
cloud cover, falling back to the VIIRS daily overview.
This commit is contained in:
Sameh Khamis committed 2026-10-01 13:25:00 -07:00
1 parent a3aa058569
commit 9be90fff44
8 files changed
+251 -4

No files matched your search

+26
View File
@@ -16,6 +16,8 @@ import { createInstallationSource } from '../layers/installations/source.js';
import { createLaunchSource } from '../layers/launches/source.js';
import { createWfigsPerimeterSource } from '../layers/perimeters/source.js';
import { createRadioSource } from '../layers/radio/source.js';
import { searchHls } from '../layers/recentImagery/catalog.js';
import { rankLatest, wvsSnapshotUrl } from '../layers/recentImagery/model.js';
import { createSatelliteSource } from '../layers/satellites/source.js';
import { createTrafficSource } from '../layers/traffic/source.js';
import { createTransitSource } from '../layers/transit/source.js';
@@ -63,6 +65,7 @@ export function createToolServices({ fetchImpl, appUrl }) {
weather: requests.weather,
weatherMaps: createWeatherSource({ fetchImpl }),
wind: createWindSource({ fetchImpl }),
imagery: createImageryService({ fetchImpl }),
regional: requests.regional,
terrain: requests.terrain,
summary: requests.summary,
@@ -76,3 +79,26 @@ export function createToolServices({ fetchImpl, appUrl }) {
places: createGeocodePlaceService({ fetchImpl }),
};
}
/** Recent keyless satellite imagery: the clearest recent day and its image. */
function createImageryService({ fetchImpl }) {
return {
async latest({ box, signal }) {
const result = await searchHls({ box, fetchImpl, signal });
return {
...rankLatest(result.candidates, { truncated: result.truncated }),
errors: result.errors,
};
},
async getSnapshot({ product, day, box, width, height, signal }) {
const response = await fetchImpl(
wvsSnapshotUrl({ product, day, box, width, height }),
{ signal },
);
if (!response.ok) throw new Error(`Imagery HTTP ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
const type = response.headers.get('content-type') || '';
return { contentType: type.split(';')[0].trim().toLowerCase(), bytes };
},
};
}