@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
122 lines (96 loc) • 3.56 kB
text/typescript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import type { BaseMessageMap, SuccessResponse } from '../utils/WorkerPool';
import { createErrorResponse, type Message } from '../utils/WorkerPool';
/**
* Utility functions and worker to process mapbox encoded terrain.
*/
export interface DecodeMapboxTerrainResult {
min: number;
max: number;
width: number;
height: number;
/**
* An array buffer that can be turned into a Float32Array.
*/
data: ArrayBuffer;
}
function getPixels(image: ImageBitmap): Uint8ClampedArray {
const canvas = new OffscreenCanvas(image.width, image.height);
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext('2d', { willReadFrequently: true });
if (!context) {
console.error('could not acquire 2D context on canvas');
throw new Error('could not acquire 2D context on canvas');
}
context.drawImage(image, 0, 0);
return context.getImageData(0, 0, image.width, image.height).data;
}
export async function decodeMapboxTerrainImage(
blob: Blob,
noData?: number,
): Promise<DecodeMapboxTerrainResult> {
const image = await createImageBitmap(blob);
const pixelData = getPixels(image);
return {
...decodeMapboxTerrainBuffer(pixelData, noData),
width: image.width,
height: image.height,
};
}
function decodeMapboxTerrainBuffer(
pixelData: Uint8ClampedArray,
noData?: number,
): Pick<DecodeMapboxTerrainResult, 'data' | 'min' | 'max'> {
const stride = pixelData.length % 3 === 0 ? 3 : 4;
const length = pixelData.length / stride;
const array = new Float32Array(length * 2); // To store the alpha component
let k = 0;
let min = +Infinity;
let max = -Infinity;
for (let i = 0; i < pixelData.length; i += stride) {
const r = pixelData[i + 0];
const g = pixelData[i + 1];
const b = pixelData[i + 2];
const elevation = -10000 + (r * 256 * 256 + g * 256 + b) * 0.1;
array[k * 2 + 0] = elevation;
if (noData != null && noData === elevation) {
array[k * 2 + 1] = 0;
} else {
array[k * 2 + 1] = 1;
min = Math.min(min, elevation);
max = Math.max(max, elevation);
}
k += 1;
}
return { data: array.buffer, min, max };
}
// Web worker implementation
export type DecodeMapboxTerrainMessage = Message<{ buffer: ArrayBuffer; noData?: number }>;
export type MessageType = 'DecodeMapboxTerrainMessage';
export interface MessageMap extends BaseMessageMap<MessageType> {
DecodeMapboxTerrainMessage: {
payload: DecodeMapboxTerrainMessage['payload'];
response: DecodeMapboxTerrainResult;
};
}
onmessage = async function onmessage(ev: MessageEvent<DecodeMapboxTerrainMessage>): Promise<void> {
const message = ev.data;
try {
if (message.type === 'DecodeMapboxTerrainMessage') {
const blob = new Blob([message.payload.buffer], { type: 'image/png' });
const result = await decodeMapboxTerrainImage(blob, message.payload.noData);
const response: SuccessResponse<DecodeMapboxTerrainResult> = {
requestId: message.id,
payload: result,
};
this.postMessage(response, { transfer: [response.payload.data] });
}
} catch (err) {
this.postMessage(createErrorResponse(message.id, err));
}
};