mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
The Satellite clouds transfer ran a power curve per pixel and wrote the result back from a full-size copy. On the 4096x2048 images the 3D Tiles host draws, that held the page for about 150 ms per image. The scale depends only on the brightest channel, so it is now worked out once per byte value and applied to the canvas pixels in place. Output bytes are unchanged. Co-Authored-By: Claude Fable 5.2 <[email protected]>
173 lines
5.2 KiB
JavaScript
173 lines
5.2 KiB
JavaScript
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,
|
||
});
|
||
}
|