UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

357 lines (293 loc) 11.9 kB
import type { Binary, View } from 'copc'; import { Las } from 'copc'; import { Header } from 'copc/lib/las'; import type { BufferAttribute } from 'three'; import { Box3, Float32BufferAttribute, Vector3 } from 'three'; import type { GetMemoryUsageContext } from '../core/MemoryUsage'; import OperationCounter from '../core/OperationCounter'; import { defer } from '../core/RequestQueue'; import Fetcher from '../utils/Fetcher'; import { nonNull } from '../utils/tsutils'; import WorkerPool from '../utils/WorkerPool'; import { getLazPerf } from './las/config'; import createWorker from './las/createWorker'; import type { DimensionName } from './las/dimension'; import { extractAttributes, getDimensionsToRead } from './las/dimension'; import { getPerPointFilters, type DimensionFilter } from './las/filter'; import { createBufferAttribute, readColor, readPosition, readScalarAttribute } from './las/readers'; import type { MessageMap, MessageType } from './las/worker'; import type { GetNodeDataOptions, PointCloudMetadata, PointCloudNode, PointCloudNodeData, } from './PointCloudSource'; import { PointCloudSourceBase } from './PointCloudSource'; export type Getter = () => Promise<Uint8Array>; /** * Inject Fetcher into copc.js to perform range requests. */ const getter: (url: string) => Getter = url => { return async () => { const blob = await Fetcher.blob(url); const arrayBuffer = await blob.arrayBuffer(); return new Uint8Array(arrayBuffer); }; }; let pool: WorkerPool<MessageType, MessageMap> | null = null; async function decodeLazFileSync(data: Uint8Array): Promise<Uint8Array> { const lazPerf = await getLazPerf(); return Las.PointData.decompressFile(data, lazPerf); } function decodeLazFileUsingWorker(data: Uint8Array): Promise<Uint8Array> { if (pool == null) { pool = new WorkerPool({ createWorker }); } return pool .queue('DecodeLazFile', { buffer: data.buffer }, [data.buffer]) .then(res => new Uint8Array(res)); } export type LASSourceOptions = { /** * The URL to the remote LAS file, or a function to retrieve the remote file. */ url: string | Getter; /** * If true, colors are compressed to 8-bit (instead of 16-bit). * @defaultValue true */ compressColorsTo8Bit?: boolean; /** * If specified, will keep every Nth point. For example, a decimation value of 10 will keep * one point out of ten, and discard the 9 other points. Useful to reduce memory usage. * @defaultValue 1 */ decimate?: number; /** * Enable web workers to perform CPU intensive tasks. * @defaultValue true */ enableWorkers?: boolean; /** * The filters to use. */ filters?: Readonly<DimensionFilter[]>; }; type PerfOptions = { decimate: number; enableWorkers: boolean; compressColorsToUint8: boolean; }; /** * A source that reads from a LAS or LAZ file. * * **Note**: if you wish to read Cloud Optimized Point Cloud (COPC) LAZ files, use the COPCSource * instead. * * LAZ decompression is done in background threads using workers. If you wish to disable workers * (for a noticeable cost in performance), you can set {@link LASSourceOptions.enableWorkers} to * `false` in constructor options. * * Note: this source uses the **laz-perf** package to perform decoding of point cloud data. This * package uses WebAssembly. If you wish to override the path to the required .wasm file, use * {@link sources.las.config.setLazPerfPath | setLazPerfPath()} before using this source. * The default path is {@link sources.las.config.DEFAULT_LAZPERF_PATH | DEFAULT_LAZPERF_PATH}. * * ### Supported LAS version * * This source supports LAS 1.2 and 1.4 only. * * ### Decimation * * This source supports decimation. By passing the {@link LASSourceOptions.decimate} argument to * a value other than 1, every Nth point will be kept and other points will be discarded during * read operations. * * ### Dimensions filtering * * This source supports filtering over dimensions (also known as attributes). By providing filters * in the form of callback functions to apply to various dimensions, it is possible to eliminate * points during reads. For example, it is possible to remove unwanted classifications such as noise * from the output points. * * Note that dimension filtering is independent from the selected attribute. In other words, it is * possible to select the dimension `"Intensity"`, while filtering on dimensions `"Classification"` * and `"ReturnNumber"` for example. * * For example, if we wish to remove all points that have the dimension "High noise" (dimension 18 * in the ASPRS classification list), as well as removing all points whose intensity is lower than * 1000: * * ```ts * const source = new LASSource(...); * * source.filters = [ * { dimension: 'Classification', filter: (val) => val !== 18 }, * { dimension: 'Intensity', filter: (val) => val >= 1000 }, * ]; * ``` */ export default class LASSource extends PointCloudSourceBase { readonly isLASSource = true as const; readonly type = 'LASSource' as const; private readonly _getter: Getter; private readonly _opCounter = new OperationCounter(); private readonly _filters: DimensionFilter[] = []; private readonly _options: PerfOptions = { decimate: 1, enableWorkers: true, compressColorsToUint8: true, }; // Available after initialization private _header: Header | null = null; private _volume: Box3 | null = null; /** The buffer that stores the entire LAS/LAZ file (in compressed form for LAZ files). */ private _buffer: ArrayBuffer | null = null; get loading(): boolean { return this._opCounter.loading; } get progress() { return this._opCounter.progress; } /** * Gets or sets the dimension filters. * @defaultValue `[]` */ get filters(): Readonly<DimensionFilter[]> { return this._filters; } set filters(v: Readonly<DimensionFilter[]>) { this._filters.length = 0; this._filters.push(...v); this.dispatchEvent({ type: 'updated' }); } constructor(options: LASSourceOptions) { super(); this._opCounter.addEventListener('changed', () => this.dispatchEvent({ type: 'progress' })); this._options.compressColorsToUint8 = options.compressColorsTo8Bit ?? this._options.compressColorsToUint8; this._options.decimate = options.decimate ?? 1; if (this._options.decimate < 1) { throw new Error('decimate should be at least 1'); } this._options.enableWorkers = options.enableWorkers ?? true; if (options.filters != null && options.filters.length > 0) { this._filters.push(...options.filters); } this._getter = typeof options.url === 'string' ? getter(options.url) : options.url; } protected async initializeOnce(): Promise<this> { this._opCounter.increment(); this._buffer = await this._getter().finally(() => this._opCounter.decrement()); this._header = Header.parse(new Uint8Array(this._buffer)); const { min, max } = this._header; this._volume = new Box3().set( new Vector3(min[0], min[1], min[2]), new Vector3(max[0], max[1], max[2]), ); return this; } private async getView(include?: DimensionName[]): Promise<View> { this._opCounter.increment(); const data = new Uint8Array(nonNull(this._buffer)); const header = nonNull(this._header); let decompressed: Binary; if (this._options.enableWorkers === false) { decompressed = await decodeLazFileSync(data).finally(() => this._opCounter.decrement()); } else { decompressed = await decodeLazFileUsingWorker(data).finally(() => this._opCounter.decrement(), ); } const view = Las.View.create(decompressed, header, undefined, include); return view; } async getMetadata(): Promise<PointCloudMetadata> { const { pointCount } = nonNull(this._header, 'not initialized'); const view = await this.getView(); const result: PointCloudMetadata = { pointCount, volume: nonNull(this._volume), attributes: extractAttributes( view.dimensions, nonNull(this._volume), this._options.compressColorsToUint8, null, ), }; return Promise.resolve(result); } async getHierarchy(): Promise<PointCloudNode> { const { min, max, pointCount } = nonNull(this._header, 'not initialized'); const volume = new Box3().set( new Vector3(min[0], min[1], min[2]), new Vector3(max[0], max[1], max[2]), ); const uniqueNode: PointCloudNode = { depth: 0, volume, id: 'root', hasData: true, geometricError: 0, center: volume.getCenter(new Vector3()), sourceId: this.id, pointCount, }; return uniqueNode; } async getNodeData(params: GetNodeDataOptions): Promise<PointCloudNodeData> { const dimensions = getDimensionsToRead(params.attribute, params.position, this._filters); const view = await this.getView(dimensions); const { min } = nonNull(this._volume); const origin = min.clone(); const stride = this._options.decimate ?? 1; const signal = params.signal; const filters = getPerPointFilters(this._filters, view); const requestedAttribute = params.attribute; const compressColors = this._options.compressColorsToUint8; let attribute: BufferAttribute | undefined = undefined; this._opCounter.increment(); if (requestedAttribute != null) { this._opCounter.increment(); } let positionBuffer: BufferAttribute | undefined = undefined; let localBoundingBox: Box3 | undefined = undefined; if (params.position) { const result = await defer(() => readPosition(view, origin, stride, filters)).finally( () => this._opCounter.decrement(), ); positionBuffer = new Float32BufferAttribute(new Float32Array(result.buffer), 3); localBoundingBox = result.localBoundingBox; } if (requestedAttribute != null) { let action: () => ArrayBuffer; switch (requestedAttribute.interpretation) { case 'color': action = () => readColor(view, stride, compressColors, filters); break; default: action = () => readScalarAttribute(view, requestedAttribute, stride, filters); break; } const buffer = await defer(action, signal).finally(() => this._opCounter.decrement()); attribute = createBufferAttribute(buffer, requestedAttribute, compressColors); } return Promise.resolve({ origin, pointCount: positionBuffer?.count ?? attribute?.count, localBoundingBox, position: positionBuffer, attribute, }); } getMemoryUsage(context: GetMemoryUsageContext): void { // We have to store the whole file in memory, since there is no guarantee that the // remote server supports range requests (which is a requirement for COPC files for example) if (this._buffer != null) { context.objects.set(this.id, { cpuMemory: this._buffer.byteLength, gpuMemory: 0 }); } } dispose(): void { // Nothing to do } }