import { readResponseBytesCapped } from '../../sources/httpBody.js'; import { msg } from '../../i18n/index.js'; import { weatherImageUrl, weatherPixelSize, drawWeatherPixels, closeWeatherPixels, } from './source.js'; import { applyInfraredAlpha } from './infraredAlpha.js'; export const MAX_MOSAIC_BYTES = 4 * 1024 * 1024; /** Apply the display transfer once to a decoded image; `flipY` draws a * bottom-up image back upright. */ export function processInfraredImage( image, mode, createCanvas, { flipY = false } = {}, ) { const canvas = createCanvas(); canvas.width = image.width; canvas.height = image.height; const context = canvas.getContext('2d'); if (flipY) context.setTransform(1, 0, 0, -1, 0, image.height); context.drawImage(image, 0, 0); if (flipY) context.setTransform(1, 0, 0, 1, 0, 0); applyInfraredTransfer(context, canvas, mode); return canvas; } function applyInfraredTransfer(context, canvas, mode) { const pixels = context.getImageData(0, 0, canvas.width, canvas.height); // In place: a copy of a 4096×2048 image is 33 MB, made and written back. applyInfraredAlpha(pixels.data, mode); context.putImageData(pixels, 0, 0); } /** Draw pixels a frame source supplied into a new canvas, with the display * transfer for infrared products, and release them. They must be `size`. */ function drawSuppliedImage(pixels, product, mode, size, signal, createCanvas) { try { signal.throwIfAborted(); const { width, height } = weatherPixelSize(pixels); if (width !== size.width || height !== size.height) throw new Error('Invalid weather image dimensions'); const texture = createCanvas(); texture.width = width; texture.height = height; const context = texture.getContext('2d'); drawWeatherPixels(context, pixels); if (product === 'clouds' || product === 'clouds-regional') applyInfraredTransfer(context, texture, mode); return texture; } finally { closeWeatherPixels(pixels); } } /** Decode locally, releasing the object URL on success, failure or cancellation. */ export function decodeInfraredImage(blob, signal) { signal.throwIfAborted(); if (typeof globalThis.createImageBitmap === 'function') return globalThis.createImageBitmap(blob); return new Promise((resolve, reject) => { const image = new Image(); const url = URL.createObjectURL(blob); const cleanup = () => { signal.removeEventListener('abort', abort); image.onload = image.onerror = null; URL.revokeObjectURL(url); }; const abort = () => { cleanup(); image.src = ''; reject(signal.reason); }; image.onload = () => { cleanup(); resolve(image); }; image.onerror = () => { cleanup(); reject(new Error(msg('Infrared decode failed'))); }; signal.addEventListener('abort', abort, { once: true }); image.src = url; }); } /** Fetch one capped whole-extent frame, or a `bbox` detail window of it, and draw * it into a canvas. Infrared frames get the display transfer; a canvas also keeps * Cesium's texture row order, which an ImageBitmap upload would not. The decoded * image is always released. With a `frameSource` that has `getImage`, the host * supplies the decoded pixels instead and nothing is fetched or decoded here. */ export async function acquireWeatherImage( product, time, { signal, mode, size, bbox = null, maxBytes, createCanvas, fetchImpl, decodeImage = decodeInfraredImage, now = () => performance.now(), onFetched = () => {}, frameSource = null, }, ) { if (frameSource?.getImage) { signal.throwIfAborted(); const started = now(); const pixels = await frameSource.getImage({ product, time, size, bbox, signal, }); onFetched(); const texture = drawSuppliedImage( pixels, product, mode, size, signal, createCanvas, ); return { texture, decodeMs: now() - started }; } const response = await fetchImpl(weatherImageUrl(product, time, size, bbox), { signal, }); if (!response.ok) throw new Error(`Weather HTTP ${response.status}`); const bytes = await readResponseBytesCapped(response, maxBytes); signal.throwIfAborted(); onFetched(); const started = now(); const image = await decodeImage( new Blob([bytes], { type: 'image/png' }), signal, ); try { signal.throwIfAborted(); if (image.width !== size.width || image.height !== size.height) throw new Error('Invalid weather image dimensions'); let texture; if (product === 'clouds' || product === 'clouds-regional') texture = processInfraredImage(image, mode, createCanvas); else { texture = createCanvas(); texture.width = image.width; texture.height = image.height; texture.getContext('2d').drawImage(image, 0, 0); } return { texture, decodeMs: now() - started }; } finally { image.close?.(); } } /** Acquire one capped global mosaic before adding any imagery layer. */ export function acquireInfraredMosaic(time, options) { return acquireWeatherImage('clouds', time, { ...options, size: { width: 2048, height: 1024 }, maxBytes: MAX_MOSAIC_BYTES, }); }