@loaders.gl/las
Version:
Framework-independent loader for the LAS and LAZ formats
403 lines (402 loc) • 12 kB
JavaScript
import getModule from "./libs/laz-perf.js";
let Module = null;
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)]
};
}
};
/**
* 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) {
let start = 32 * 3 + 35;
const o = {
pointsOffset: readAs(arraybuffer, Uint32Array, 32 * 3),
pointsFormatId: readAs(arraybuffer, Uint8Array, 32 * 3 + 8),
pointsStructSize: readAs(arraybuffer, Uint16Array, 32 * 3 + 8 + 1),
pointsCount: readAs(arraybuffer, Uint32Array, 32 * 3 + 11),
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]];
return o;
}
// LAS Loader
// Loads uncompressed files
//
class LASLoader {
arraybuffer;
readOffset = 0;
header = {
pointsOffset: 0,
pointsFormatId: 0,
pointsStructSize: 0,
pointsCount: 0,
scale: [0, 0, 0],
offset: [0, 0, 0],
maxs: [0],
mins: [0],
totalToRead: 0,
totalRead: 0,
versionAsString: '',
isCompressed: true
};
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 new ArrayBuffer, count, hasMoreData
*/
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;
instance = null; // LASZip instance
header = null;
constructor(arraybuffer) {
this.arraybuffer = arraybuffer;
if (!Module) {
// Avoid executing laz-perf on import
Module = getModule();
}
}
/**
* Opens the file
* @returns boolean
*/
open() {
try {
const { arraybuffer } = this;
this.instance = new Module.LASZip();
const abInt = new Uint8Array(arraybuffer);
const buf = Module._malloc(arraybuffer.byteLength);
this.instance.arraybuffer = arraybuffer;
this.instance.buf = buf;
Module.HEAPU8.set(abInt, buf);
this.instance.open(buf, arraybuffer.byteLength);
this.instance.readOffset = 0;
return true;
}
catch (error) {
throw new Error(`Failed to open file: ${error.message}`);
}
}
getHeader() {
if (!this.instance) {
throw new Error('You need to open the file before trying to read header');
}
try {
const header = parseLASHeader(this.instance.arraybuffer);
header.pointsFormatId &= 0x3f;
this.header = header;
return header;
}
catch (error) {
throw new Error(`Failed to get header: ${error.message}`);
}
}
/**
* @param count
* @param offset
* @param skip
* @returns Data
*/
readData(count, offset, 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 - instance.readOffset);
const bufferSize = Math.ceil(pointsToRead / skip);
let pointsRead = 0;
const thisBuf = new Uint8Array(bufferSize * header.pointsStructSize);
const bufRead = Module._malloc(header.pointsStructSize);
for (let i = 0; i < pointsToRead; i++) {
instance.getPoint(bufRead);
if (i % skip === 0) {
const a = new Uint8Array(Module.HEAPU8.buffer, bufRead, header.pointsStructSize);
thisBuf.set(a, pointsRead * header.pointsStructSize);
pointsRead++;
}
instance.readOffset++;
}
Module._free(bufRead);
return {
buffer: thisBuf.buffer,
count: pointsRead,
hasMoreData: instance.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) {
Module._free(this.instance.buf);
this.instance.delete();
this.instance = 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;
scale;
offset;
mins;
maxs;
constructor(buffer, len, header) {
this.arrayb = buffer;
this.decoder = POINT_FORMAT_READERS[header.pointsFormatId];
this.pointsCount = len;
this.pointSize = header.pointsStructSize;
this.scale = header.scale;
this.offset = header.offset;
this.mins = header.mins;
this.maxs = header.maxs;
}
/**
* 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;
if (this.determineVersion() > 13) {
throw new Error('Only file versions <= 1.3 are supported at this time');
}
this.determineFormat();
if (POINT_FORMAT_READERS[this.formatId] === undefined) {
throw new Error('The point format ID is not supported');
}
this.loader = this.isCompressed
? new LAZLoader(this.arraybuffer)
: new LASLoader(this.arraybuffer);
}
/**
* Determines format in parameters of LASHeaer
*/
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 Int8Array(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 Data
*/
readData(count, start, skip) {
return this.loader.readData(count, start, skip);
}
/**
* Closes the file
*/
close() {
if (this.loader.close()) {
this.isOpen = false;
}
}
/**
*/
getUnpacker() {
return LASDecoder;
}
}
export const LASModuleWasLoaded = false;
/* eslint no-use-before-define: 2 */