@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
261 lines (225 loc) • 8.37 kB
text/typescript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import * as copc from 'copc';
import type { BaseMessageMap, Message, SuccessResponse } from '../../utils/WorkerPool';
import type { PointCloudAttribute } from '../PointCloudSource';
import type { DimensionFilter } from './filter';
import { createErrorResponse } from '../../utils/WorkerPool';
import { getLazPerf, setLazPerfWasmBinary } from './config';
import { getPerPointFilters } from './filter';
import { readColor, readPosition, readScalarAttribute } from './readers';
export interface Metadata {
pointCount: number;
pointDataRecordFormat: number;
pointDataRecordLength: number;
}
async function decompressChunk(chunk: ArrayBufferLike, metadata: Metadata): Promise<copc.Binary> {
const lazPerf = await getLazPerf();
return copc.Las.PointData.decompressChunk(new Uint8Array(chunk), metadata, lazPerf);
}
async function decompressFile(chunk: ArrayBufferLike): Promise<copc.Binary> {
const lazPerf = await getLazPerf();
return copc.Las.PointData.decompressFile(new Uint8Array(chunk), lazPerf);
}
export type BoundingBox = [number, number, number, number, number, number];
export type MessageType = 'DecodeLazChunk' | 'DecodeLazFile' | 'ReadView' | 'SetWasmBinary';
interface TypedMessage<K extends MessageType, T> extends Message<T> {
type: K;
}
type DecodeLazChunkMessage = TypedMessage<
'DecodeLazChunk',
{ buffer: ArrayBufferLike; metadata: Metadata }
>;
export interface ReadViewResult {
position?: {
buffer: ArrayBuffer;
localBoundingBox: BoundingBox;
};
attributes: ArrayBuffer[];
}
type ReadViewMessage = TypedMessage<
'ReadView',
{
buffer: ArrayBufferLike;
metadata: Metadata;
header: copc.Las.Extractor.PartialHeader;
origin: { x: number; y: number; z: number };
eb?: copc.Las.ExtraBytes[];
position: boolean;
stride?: number;
include?: string[];
attributes: PointCloudAttribute[];
filters?: DimensionFilter[];
compressColors: boolean;
}
>;
type DecodeLazFileMessage = TypedMessage<'DecodeLazFile', { buffer: ArrayBufferLike }>;
type DecodeLazChunkResponse = SuccessResponse<ArrayBufferLike>;
type DecodeLazFileResponse = SuccessResponse<ArrayBufferLike>;
type ReadViewResponse = SuccessResponse<ReadViewResult>;
export interface SetWasmBinaryMessage {
type: 'SetWasmBinary';
buffer: ArrayBuffer;
}
type Messages =
| DecodeLazFileMessage
| DecodeLazChunkMessage
| SetWasmBinaryMessage
| ReadViewMessage;
export interface MessageMap extends BaseMessageMap<MessageType> {
DecodeLazChunk: {
payload: DecodeLazChunkMessage['payload'];
response: DecodeLazFileResponse['payload'];
};
DecodeLazFile: {
payload: DecodeLazFileMessage['payload'];
response: DecodeLazChunkResponse['payload'];
};
ReadView: {
payload: ReadViewMessage['payload'];
response: ReadViewResponse['payload'];
};
}
export interface LazWorker extends Worker {
postMessage(message: SetWasmBinaryMessage, options?: StructuredSerializeOptions): void;
postMessage(message: SetWasmBinaryMessage, transfer: Transferable[]): void;
}
function processDecodeChunkMessage(msg: DecodeLazChunkMessage): void {
decompressChunk(msg.payload.buffer, msg.payload.metadata)
.then(buf => {
const response: DecodeLazChunkResponse = {
requestId: msg.id,
payload: buf.buffer,
};
postMessage(response, { transfer: [buf.buffer] });
})
.catch(err => {
postMessage(createErrorResponse(msg.id, err));
});
}
function processDecodeFileMessage(msg: DecodeLazFileMessage): void {
decompressFile(msg.payload.buffer)
.then(buf => {
const response: DecodeLazFileResponse = {
requestId: msg.id,
payload: buf.buffer,
};
postMessage(response, { transfer: [buf.buffer] });
})
.catch(err => {
console.error(err);
postMessage(createErrorResponse(msg.id, err));
});
}
export function createLasView(
buffer: copc.Binary,
header: copc.Las.Extractor.PartialHeader,
eb?: copc.Las.ExtraBytes[],
include?: string[],
): copc.View {
const view = copc.Las.View.create(buffer, header, eb, include);
if (eb) {
const newDimensions: copc.Dimension.Map = {};
for (const [name, dimension] of Object.entries(view.dimensions)) {
let actualDimension = dimension;
const extrabyte = eb.find(extra => extra.name === name);
if (extrabyte) {
if (extrabyte.type === 'signed' || extrabyte.type === 'unsigned') {
if (
(typeof extrabyte.scale === 'number' && extrabyte.scale !== 1) ||
(typeof extrabyte.offset === 'number' && extrabyte.offset !== 0)
) {
// If a LAS ExtraByte is a scaled integer, then we need to store its scaled value in a float,
// otherwise we might lose precision.
actualDimension = { type: 'float', size: 4 };
}
}
}
newDimensions[name] = actualDimension;
}
view.dimensions = newDimensions;
}
return view;
}
export function readView(options: {
view: copc.View;
origin: { x: number; y: number; z: number };
stride?: number;
position: boolean;
attributes: PointCloudAttribute[];
filters?: DimensionFilter[];
compressColors: boolean;
}): ReadViewResult {
const { view, filters, origin, attributes, compressColors } = options;
const stride = options.stride ?? 1;
const perPointFilters = getPerPointFilters(filters ?? [], view);
let position: ReadViewResult['position'] | undefined = undefined;
if (options.position) {
const data = readPosition(view, origin, stride, perPointFilters);
const localBoundingBox: BoundingBox = [
data.localBoundingBox.min.x,
data.localBoundingBox.min.y,
data.localBoundingBox.min.z,
data.localBoundingBox.max.x,
data.localBoundingBox.max.y,
data.localBoundingBox.max.z,
];
position = {
buffer: data.buffer,
localBoundingBox,
};
}
const attributesBuffers = attributes.map(attribute => {
switch (attribute.interpretation) {
case 'color':
return readColor(view, stride, compressColors, perPointFilters);
break;
case 'classification':
case 'unknown':
return readScalarAttribute(view, attribute, stride, perPointFilters);
break;
}
});
return { position, attributes: attributesBuffers };
}
function processReadViewMessage(msg: ReadViewMessage): void {
const { buffer, metadata, header, eb, include } = msg.payload;
decompressChunk(buffer, metadata)
.then(bin => {
const view = createLasView(bin, header, eb, include);
const payload = readView({ ...msg.payload, view });
const response: ReadViewResponse = {
requestId: msg.id,
payload,
};
const transfer: Transferable[] = [...payload.attributes];
if (payload.position) {
transfer.push(payload.position.buffer);
}
postMessage(response, { transfer });
})
.catch(err => {
console.error(err);
postMessage(createErrorResponse(msg.id, err));
});
}
onmessage = (event: MessageEvent<Messages>): void => {
const message = event.data;
switch (message.type) {
case 'DecodeLazChunk':
processDecodeChunkMessage(message);
break;
case 'DecodeLazFile':
processDecodeFileMessage(message);
break;
case 'ReadView':
processReadViewMessage(message);
break;
case 'SetWasmBinary':
setLazPerfWasmBinary(message.buffer);
break;
}
};