UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

193 lines (186 loc) 7.53 kB
import { LazPerf } from 'laz-perf'; import { Las } from 'copc'; /** * @typedef {Object} Header - Partial LAS header. * @property {number} header.pointDataRecordFormat - Type of point data * records contained by the buffer. * @property {number} header.pointDataRecordLength - Size (in bytes) of the * point data records. If the specified size is larger than implied by the * point data record format (see above) the remaining bytes are user-specfic * "extra bytes". Those are described by an Extra Bytes VLR. * @property {number[]} header.scale - Scale factors (an array `[xScale, * yScale, zScale]`) multiplied to the X, Y, Z point record values. * @property {number[]} header.offset - Offsets (an array `[xOffset, * xOffset, zOffset]`) added to the scaled X, Y, Z point record values. */ function defaultColorEncoding(header) { return header.majorVersion === 1 && header.minorVersion <= 2 ? 8 : 16; } /** * @classdesc * Loader for LAS and LAZ (LASZip) point clouds. It uses the copc.js library and * the laz-perf decoder under the hood. * * The laz-perf web assembly module is lazily fetched at runtime when a parsing * request is initiated. Location of laz-perf wasm defaults to the unpkg * repository. */ class LASLoader { constructor() { this._wasmPath = 'https://cdn.jsdelivr.net/npm/laz-perf@0.0.6/lib'; this._wasmPromise = null; } _initDecoder() { if (this._wasmPromise) { return this._wasmPromise; } this._wasmPromise = LazPerf.create({ locateFile: file => `${this._wasmPath}/${file}` }); return this._wasmPromise; } _parseView(view, options) { const colorDepth = options.colorDepth ?? 16; const getPosition = ['X', 'Y', 'Z'].map(view.getter); const getIntensity = view.getter('Intensity'); const getReturnNumber = view.getter('ReturnNumber'); const getNumberOfReturns = view.getter('NumberOfReturns'); const getClassification = view.getter('Classification'); const getPointSourceID = view.getter('PointSourceId'); const getColor = view.dimensions.Red ? ['Red', 'Green', 'Blue'].map(view.getter) : undefined; const getScanAngle = view.getter('ScanAngle'); const positions = new Float32Array(view.pointCount * 3); const intensities = new Uint16Array(view.pointCount); const returnNumbers = new Uint8Array(view.pointCount); const numberOfReturns = new Uint8Array(view.pointCount); const classifications = new Uint8Array(view.pointCount); const pointSourceIDs = new Uint16Array(view.pointCount); const colors = getColor ? new Uint8Array(view.pointCount * 4) : undefined; /* As described by the LAS spec, Scan Angle is encoded: - as signed char in a valid range from -90 to +90 (degrees) prior to the LAS 1.4 Point Data Record Formats (PDRF) 6 - as a signed short in a valid range from -30 000 to +30 000. Those values represents scan angles from -180 to +180 degrees with an increment of 0.006 for PDRF >= 6. The copc.js library does the degree convertion and stores it as a `Float32`. */ const scanAngles = new Float32Array(view.pointCount); // For precision we take the first point that will be use as origin for a local referentiel. const origin = getPosition.map(f => f(0)).map(val => Math.floor(val)); for (let i = 0; i < view.pointCount; i++) { // `getPosition` apply scale and offset transform to the X, Y, Z // values. See https://github.com/connormanning/copc.js/blob/master/src/las/extractor.ts. const [x, y, z] = getPosition.map(f => f(i)); positions[i * 3] = x - origin[0]; positions[i * 3 + 1] = y - origin[1]; positions[i * 3 + 2] = z - origin[2]; intensities[i] = getIntensity(i); returnNumbers[i] = getReturnNumber(i); numberOfReturns[i] = getNumberOfReturns(i); if (getColor) { // Note that we do not infer color depth as it is expensive // (i.e. traverse the whole view to check if there exists a red, // green or blue value > 255). let [r, g, b] = getColor.map(f => f(i)); if (colorDepth === 16) { r /= 256; g /= 256; b /= 256; } colors[i * 4] = r; colors[i * 4 + 1] = g; colors[i * 4 + 2] = b; colors[i * 4 + 3] = 255; } classifications[i] = getClassification(i); pointSourceIDs[i] = getPointSourceID(i); scanAngles[i] = getScanAngle(i); } return { position: positions, intensity: intensities, returnNumber: returnNumbers, numberOfReturns, classification: classifications, pointSourceID: pointSourceIDs, color: colors, scanAngle: scanAngles, origin }; } /** * Set LazPerf decoder path. * @param {string} path - path to `laz-perf.wasm` folder. */ set lazPerf(path) { this._wasmPath = path; this._wasmPromise = null; } /** * Parses a LAS or LAZ (LASZip) chunk. Note that this function is * **CPU-bound** and shall be parallelised in a dedicated worker. * @param {Uint8Array} data - File chunk data. * @param {Object} options - Parsing options. * @param {Header} options.header - Partial LAS header. * @param {number} options.pointCount - Number of points encoded in this * data chunk. * @param {Las.ExtraBytes[]} [options.eb] - Extra bytes LAS VLRs * headers. * @param {8 | 16} [options.colorDepth] - Color depth encoding (in bits). * Either 8 or 16 bits. Defaults to 8 bits for LAS 1.2 and 16 bits for later * versions (as mandatory by the specification). */ async parseChunk(data, options) { const { header, eb, pointCount } = options; const { pointDataRecordFormat, pointDataRecordLength } = header; const colorDepth = options.colorDepth ?? defaultColorEncoding(header); const bytes = new Uint8Array(data); const pointData = await Las.PointData.decompressChunk(bytes, { pointCount, pointDataRecordFormat, pointDataRecordLength }, this._initDecoder()); const view = Las.View.create(pointData, header, eb); const attributes = this._parseView(view, { colorDepth }); return { attributes }; } /** * Parses a LAS or LAZ (LASZip) file. Note that this function is * **CPU-bound** and shall be parallelised in a dedicated worker. * @param {ArrayBuffer} data - Binary data to parse. * @param {Object} [options] - Parsing options. * @param {8 | 16} [options.colorDepth] - Color depth encoding (in bits). * Either 8 or 16 bits. Defaults to 8 bits for LAS 1.2 and 16 bits for later * versions (as mandatory by the specification) */ async parseFile(data) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; const bytes = new Uint8Array(data); const pointData = await Las.PointData.decompressFile(bytes, this._initDecoder()); const header = Las.Header.parse(bytes); const colorDepth = options.colorDepth ?? defaultColorEncoding(header); const getter = async (begin, end) => bytes.slice(begin, end); const vlrs = await Las.Vlr.walk(getter, header); const ebVlr = Las.Vlr.find(vlrs, 'LASF_Spec', 4); const eb = ebVlr && Las.ExtraBytes.parse(await Las.Vlr.fetch(getter, ebVlr)); const view = Las.View.create(pointData, header, eb); const attributes = this._parseView(view, { colorDepth }); return { header, attributes }; } } export default LASLoader;