UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

255 lines (253 loc) 8.96 kB
import { Las } from 'copc'; import { Header } from 'copc/lib/las'; import { Box3, Float32BufferAttribute, Vector3 } from 'three'; 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 { extractAttributes, getDimensionsToRead } from './las/dimension'; import { getPerPointFilters } from './las/filter'; import { createBufferAttribute, readColor, readPosition, readScalarAttribute } from './las/readers'; import { PointCloudSourceBase } from './PointCloudSource'; /** * Inject Fetcher into copc.js to perform range requests. */ const getter = url => { return async () => { const blob = await Fetcher.blob(url); const arrayBuffer = await blob.arrayBuffer(); return new Uint8Array(arrayBuffer); }; }; let pool = null; async function decodeLazFileSync(data) { const lazPerf = await getLazPerf(); return Las.PointData.decompressFile(data, lazPerf); } function decodeLazFileUsingWorker(data) { if (pool == null) { pool = new WorkerPool({ createWorker }); } return pool.queue('DecodeLazFile', { buffer: data.buffer }, [data.buffer]).then(res => new Uint8Array(res)); } /** * 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 { isLASSource = true; type = 'LASSource'; _opCounter = new OperationCounter(); _filters = []; _options = { decimate: 1, enableWorkers: true, compressColorsToUint8: true }; // Available after initialization _header = null; _volume = null; /** The buffer that stores the entire LAS/LAZ file (in compressed form for LAZ files). */ _buffer = null; get loading() { return this._opCounter.loading; } get progress() { return this._opCounter.progress; } /** * Gets or sets the dimension filters. * @defaultValue `[]` */ get filters() { return this._filters; } set filters(v) { this._filters.length = 0; this._filters.push(...v); this.dispatchEvent({ type: 'updated' }); } constructor(options) { 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; } async initializeOnce() { 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; } async getView(include) { this._opCounter.increment(); const data = new Uint8Array(nonNull(this._buffer)); const header = nonNull(this._header); let decompressed; 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() { const { pointCount } = nonNull(this._header, 'not initialized'); const view = await this.getView(); const result = { pointCount, volume: nonNull(this._volume), attributes: extractAttributes(view.dimensions, nonNull(this._volume), this._options.compressColorsToUint8, null) }; return Promise.resolve(result); } async getHierarchy() { 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 = { depth: 0, volume, id: 'root', hasData: true, geometricError: 0, center: volume.getCenter(new Vector3()), sourceId: this.id, pointCount }; return uniqueNode; } async getNodeData(params) { 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 = undefined; this._opCounter.increment(); if (requestedAttribute != null) { this._opCounter.increment(); } let positionBuffer = undefined; let localBoundingBox = 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; 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) { // 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() { // Nothing to do } }