@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
255 lines (253 loc) • 8.96 kB
JavaScript
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
}
}