From c43c94ba5f70294c0d00c4996be2057037e34c38 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 13:18:01 -0700 Subject: [PATCH] feat: read weather image frames through the weather source Add getImage() to the weather source. It reads one frame through the bounded image route the layer already uses and returns its content type and bytes, so code outside the layer can show the current map. --- src/layers/weather/source.js | 14 +++++++++++ src/layers/weather/source.test.mjs | 37 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/layers/weather/source.js b/src/layers/weather/source.js index ec92cd5a2..65bcd4a51 100644 --- a/src/layers/weather/source.js +++ b/src/layers/weather/source.js @@ -132,5 +132,19 @@ export function createWeatherSource({ signal?.removeEventListener('abort', abort); } }, + /** Read one image frame through the bounded image route. */ + async getImage({ product, time, size, bbox = null, signal } = {}) { + signal?.throwIfAborted(); + const response = await fetchImpl( + weatherImageUrl(product, time, size, bbox), + { signal, redirect: 'error' }, + ); + if (!response.ok) + throw new Error(`Weather image HTTP ${response.status}`); + const bytes = new Uint8Array(await response.arrayBuffer()); + signal?.throwIfAborted(); + const type = response.headers.get('content-type') || ''; + return { contentType: type.split(';')[0].trim().toLowerCase(), bytes }; + }, }; } diff --git a/src/layers/weather/source.test.mjs b/src/layers/weather/source.test.mjs index 8913d462e..83ab24e76 100644 --- a/src/layers/weather/source.test.mjs +++ b/src/layers/weather/source.test.mjs @@ -162,3 +162,40 @@ test('detail-window image URLs carry the bbox and omit the default detail size', /size/, ); }); + +test('image frames are read through the bounded image route', async () => { + const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]); + const requested = []; + const source = createWeatherSource({ + fetchImpl: async (url, init) => { + requested.push({ url, redirect: init.redirect }); + return new Response(png, { headers: { 'Content-Type': 'image/PNG' } }); + }, + }); + const frame = await source.getImage({ + product: 'radar', + time, + size: { width: 1024, height: 512 }, + bbox: { west: -100, south: 30, east: -96, north: 32 }, + }); + assert.equal(frame.contentType, 'image/png'); + assert.deepEqual([...frame.bytes], [...png]); + assert.deepEqual(requested, [ + { + url: weatherImageUrl( + 'radar', + time, + { width: 1024, height: 512 }, + { west: -100, south: 30, east: -96, north: 32 }, + ), + redirect: 'error', + }, + ]); + const failing = createWeatherSource({ + fetchImpl: async () => new Response(null, { status: 409 }), + }); + await assert.rejects( + failing.getImage({ product: 'radar', time }), + /Weather image HTTP 409/, + ); +});