UNPKG

@loaders.gl/las

Version:

Framework-independent loader for the LAS and LAZ formats

446 lines 13.8 kB
// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { WasmLasZipDecompressor } from "../../libs/laz-rs-wasm/laz_rs_wasm.js"; const POINT_FORMAT_READERS = { 0: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(15) }; }, 1: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(15) }; }, 2: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(15), color: [dv.getUint16(20, true), dv.getUint16(22, true), dv.getUint16(24, true)] }; }, 3: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(15), color: [dv.getUint16(28, true), dv.getUint16(30, true), dv.getUint16(32, true)] }; }, 4: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(15) }; }, 5: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(15), color: [dv.getUint16(28, true), dv.getUint16(30, true), dv.getUint16(32, true)] }; }, 6: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(16) }; }, 7: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(16), color: [dv.getUint16(30, true), dv.getUint16(32, true), dv.getUint16(34, true)] }; }, 8: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(16), color: [dv.getUint16(30, true), dv.getUint16(32, true), dv.getUint16(34, true)] }; }, 9: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(16) }; }, 10: (dv) => { return { position: [dv.getInt32(0, true), dv.getInt32(4, true), dv.getInt32(8, true)], intensity: dv.getUint16(12, true), classification: dv.getUint8(16), color: [dv.getUint16(30, true), dv.getUint16(32, true), dv.getUint16(34, true)] }; } }; /** * Reads incoming binary data depends on the Type parameter * @param buf * @param Type * @param offset * @param count * @returns number | number[] from incoming binary data */ function readAs(buf, Type = {}, offset, count) { count = count === undefined || count === 0 ? 1 : count; const sub = buf.slice(offset, offset + Type.BYTES_PER_ELEMENT * count); const r = new Type(sub); if (count === 1) { return r[0]; } const ret = []; for (let i = 0; i < count; i++) { ret.push(r[i]); } return ret; } /** * Parsing of header's attributes * @param arraybuffer * @returns header as LASHeader */ function parseLASHeader(arraybuffer) { const ver = new Uint8Array(arraybuffer, 24, 2); const version = ver[0] * 10 + ver[1]; const versionAsString = `${ver[0]}.${ver[1]}`; const rawPointsFormatId = readAs(arraybuffer, Uint8Array, 32 * 3 + 8); const bit7 = (rawPointsFormatId & 0x80) >> 7; const bit6 = (rawPointsFormatId & 0x40) >> 6; const isCompressed = bit7 === 1 || bit6 === 1; const pointsFormatId = rawPointsFormatId & 0x3f; const o = { pointsOffset: readAs(arraybuffer, Uint32Array, 32 * 3), pointsFormatId, pointsStructSize: readAs(arraybuffer, Uint16Array, 32 * 3 + 8 + 1), pointsCount: readAs(arraybuffer, Uint32Array, 32 * 3 + 11), versionAsString, isCompressed }; let start = 32 * 3 + 35; o.scale = readAs(arraybuffer, Float64Array, start, 3); start += 24; // 8*3 o.offset = readAs(arraybuffer, Float64Array, start, 3); start += 24; const bounds = readAs(arraybuffer, Float64Array, start, 6); start += 48; // 8*6 o.maxs = [bounds[0], bounds[2], bounds[4]]; o.mins = [bounds[1], bounds[3], bounds[5]]; start += 20; // 8*20 if (version === 14) { o.pointsCount = Number(readAs(arraybuffer, BigUint64Array, start)); } const colorPointFormats = new Set([2, 3, 5, 7, 8, 10]); o.hasColor = colorPointFormats.has(pointsFormatId); // @ts-expect-error Caused when restoring lazperf return o; } // LAS Loader // Loads uncompressed files // class LASLoader { arraybuffer; readOffset = 0; header = null; constructor(arraybuffer) { this.arraybuffer = arraybuffer; } /** * @returns boolean */ open() { // Nothing needs to be done to open this return true; } /** * Parsing of incoming binary * @returns LASHeader */ getHeader() { this.header = parseLASHeader(this.arraybuffer); return this.header; } /** * Reading data * @param count * @param skip * @returns LasData */ readData(count, skip) { const { header, arraybuffer } = this; if (!header) { throw new Error('Cannot start reading data till a header request is issued'); } let { readOffset } = this; let start; if (skip <= 1) { count = Math.min(count, header.pointsCount - readOffset); start = header.pointsOffset + readOffset * header.pointsStructSize; const end = start + count * header.pointsStructSize; readOffset += count; this.readOffset = readOffset; return { buffer: arraybuffer.slice(start, end), count, hasMoreData: readOffset < header.pointsCount }; } const pointsToRead = Math.min(count * skip, header.pointsCount - readOffset); const bufferSize = Math.ceil(pointsToRead / skip); let pointsRead = 0; const buf = new Uint8Array(bufferSize * header.pointsStructSize); for (let i = 0; i < pointsToRead; i++) { if (i % skip === 0) { start = header.pointsOffset + readOffset * header.pointsStructSize; const src = new Uint8Array(arraybuffer, start, header.pointsStructSize); buf.set(src, pointsRead * header.pointsStructSize); pointsRead++; } readOffset++; } this.readOffset = readOffset; return { buffer: buf.buffer, count: pointsRead, hasMoreData: readOffset < header.pointsCount }; } /** * Method which brings data to null to close the file * @returns */ close() { // @ts-ignore Possibly null this.arraybuffer = null; return true; } } /** * LAZ Loader * Uses NaCL module to load LAZ files */ class LAZLoader { arraybuffer; readOffset = 0; instance = null; header = null; constructor(arraybuffer) { this.arraybuffer = arraybuffer; } /** * Opens the file * @returns boolean */ open() { try { const abInt = new Uint8Array(this.arraybuffer); this.instance = new WasmLasZipDecompressor(abInt); return true; } catch (error) { throw new Error(`Failed to open file: ${error.message}`); } } getHeader() { try { this.header = parseLASHeader(this.arraybuffer); return this.header; } catch (error) { throw new Error(`Failed to get header: ${error.message}`); } } /** * @param count * @param offset * @param skip * @returns LASData */ readData(count, skip) { if (!this.instance) { throw new Error('You need to open the file before trying to read stuff'); } const { header, instance } = this; if (!header) { throw new Error('You need to query header before reading, I maintain state that way, sorry :('); } try { const pointsToRead = Math.min(count * skip, header.pointsCount - this.readOffset); const bufferSize = Math.ceil(pointsToRead / skip); let pointsRead = 0; const buf = new Uint8Array(bufferSize * header.pointsStructSize); const bufRead = new Uint8Array(header.pointsStructSize); for (let i = 0; i < pointsToRead; i++) { instance.decompress_many(bufRead); if (i % skip === 0) { buf.set(bufRead, pointsRead * header.pointsStructSize); pointsRead++; } this.readOffset++; } return { buffer: buf.buffer, count: pointsRead, hasMoreData: this.readOffset < header.pointsCount }; } catch (error) { throw new Error(`Failed to read data: ${error.message}`); } } /** * Deletes the instance * @returns boolean */ close() { try { if (this.instance !== null) { this.instance.free(); this.instance = null; } // @ts-ignore Possibly null this.arraybuffer = null; return true; } catch (error) { throw new Error(`Failed to close file: ${error.message}`); } } } /** * Helper class: Decodes LAS records into points */ class LASDecoder { arrayb; decoder; pointsCount; pointSize; constructor(buffer, len, header) { this.arrayb = buffer; this.decoder = POINT_FORMAT_READERS[header.pointsFormatId]; this.pointsCount = len; this.pointSize = header.pointsStructSize; } /** * Decodes data depends on this point size * @param index * @returns New object */ getPoint(index) { if (index < 0 || index >= this.pointsCount) { throw new Error('Point index out of range'); } const dv = new DataView(this.arrayb, index * this.pointSize, this.pointSize); return this.decoder(dv); } } /** * A single consistent interface for loading LAS/LAZ files */ export class LASFile { arraybuffer; formatId = 0; loader; isCompressed = true; isOpen = false; version = 0; versionAsString = ''; constructor(arraybuffer) { this.arraybuffer = arraybuffer; this.validate(); this.loader = this.isCompressed ? new LAZLoader(this.arraybuffer) : new LASLoader(this.arraybuffer); } validate() { const signature = readAs(this.arraybuffer, Uint8Array, 0, 4); const check = String.fromCharCode(...signature); if (check !== 'LASF') { throw new Error('Invalid LAS file'); } if (this.determineVersion() > 14) { throw new Error('Only file versions <= 1.4 are supported'); } this.determineFormat(); if (POINT_FORMAT_READERS[this.formatId] === undefined) { throw new Error('The point format ID is not supported'); } } /** * Determines format in parameters of LASHeader */ determineFormat() { const formatId = readAs(this.arraybuffer, Uint8Array, 32 * 3 + 8); const bit7 = (formatId & 0x80) >> 7; const bit6 = (formatId & 0x40) >> 6; if (bit7 === 1 && bit6 === 1) { throw new Error('Old style compression not supported'); } this.formatId = formatId & 0x3f; this.isCompressed = bit7 === 1 || bit6 === 1; } /** * Determines version * @returns version */ determineVersion() { const ver = new Uint8Array(this.arraybuffer, 24, 2); this.version = ver[0] * 10 + ver[1]; this.versionAsString = `${ver[0]}.${ver[1]}`; return this.version; } /** * Reads if the file is open * @returns boolean */ open() { if (this.loader.open()) { this.isOpen = true; } } /** * Gets the header * @returns Header */ getHeader() { return this.loader.getHeader(); } /** * @param count * @param start * @param skip * @returns LASData */ readData(count, skip) { return this.loader.readData(count, skip); } /** * Closes the file */ close() { if (this.loader.close()) { this.isOpen = false; } } /** */ getUnpacker() { return LASDecoder; } } /* eslint no-use-before-define: 2 */ // export const LASModuleWasLoaded = false; //# sourceMappingURL=laslaz-decoder.js.map