las-laz-js 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/LICENSE-APACHE +51 -0
- package/NOTICE +14 -0
- package/README.md +71 -27
- package/package.json +5 -3
- package/src/LasReader.js +134 -42
- package/src/colour/colourize.js +35 -3
- package/src/filter/dedupe.js +31 -14
- package/src/filter/filters.js +14 -10
- package/src/format/LasVlr.js +8 -5
- package/src/index.js +3 -3
- package/src/io/NodeFileSource.js +24 -2
- package/src/laz/ArithmeticDecoder.js +136 -0
- package/src/laz/IntegerDecompressor.js +77 -0
- package/src/laz/LazVlr.js +65 -0
- package/src/laz/chunkTable.js +109 -0
- package/src/laz/decompressChunk.js +115 -0
- package/src/laz/models.js +102 -0
- package/src/laz/v2/byte.js +33 -0
- package/src/laz/v2/gpstime11.js +118 -0
- package/src/laz/v2/point10.js +139 -0
- package/src/laz/v2/rgb12.js +79 -0
- package/src/laz/v3/layered.js +145 -0
- package/src/laz/v3/point14.js +266 -0
- package/src/read/decodeRecords.js +32 -14
- package/src/read/windows.js +21 -0
- package/src/workers/workerProtocol.js +10 -7
- package/src/write/writeLas.js +46 -13
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/laz/chunkTable.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Where each compressed chunk lives. A chunked LAZ file starts its
|
|
4
|
+
// point data with an i64 offset to the chunk table (written last, so
|
|
5
|
+
// a streaming writer leaves -1 there and repeats the offset in the
|
|
6
|
+
// file's final 8 bytes). The table is u32 version, u32 chunk count,
|
|
7
|
+
// then each chunk's byte count (and, for variable-size chunks, its
|
|
8
|
+
// point count), arithmetic-coded as integers predicted from the
|
|
9
|
+
// previous chunk. Chunk 0 starts right after the 8-byte offset.
|
|
10
|
+
// A point-wise (unchunked) file is one chunk running to the end.
|
|
11
|
+
//
|
|
12
|
+
// From laz-rs src/laszip/chunk_table.rs (https://github.com/laz-rs/laz-rs),
|
|
13
|
+
// itself from LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
|
|
14
|
+
// rapidlasso; (c) 2019-2023 Thomas Montaigu.
|
|
15
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
16
|
+
// NOTICE). Changes: written
|
|
17
|
+
// in JavaScript over an async byte source, Brent Buffham 2026.
|
|
18
|
+
//==========================================================
|
|
19
|
+
|
|
20
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
21
|
+
import { ArithmeticDecoder } from "./ArithmeticDecoder.js";
|
|
22
|
+
import { IntegerDecompressor } from "./IntegerDecompressor.js";
|
|
23
|
+
import { COMPRESSOR } from "./LazVlr.js";
|
|
24
|
+
|
|
25
|
+
// Signed i64 at `at` as a Number (an offset; > 2^53 is refused)
|
|
26
|
+
function i64(bytes, at) {
|
|
27
|
+
var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
28
|
+
var lo = v.getUint32(at, true), hi = v.getInt32(at + 4, true);
|
|
29
|
+
if (hi > 2097151 || hi < -2097152) throw new LasError(ERROR_CODES.BAD_HEADER, "LAZ chunk table offset is out of range", at);
|
|
30
|
+
return hi * 4294967296 + lo;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decode the chunk table body.
|
|
35
|
+
* @param {Uint8Array} bytes - from the table's start to the end of the file
|
|
36
|
+
* @param {boolean} withPointCounts - variable-size chunks
|
|
37
|
+
* @param {number} [base] - file offset of bytes[0], for error offsets
|
|
38
|
+
* @returns {Array<{pointCount:number, byteCount:number}>}
|
|
39
|
+
*/
|
|
40
|
+
export function decodeChunkTable(bytes, withPointCounts, base) {
|
|
41
|
+
if (bytes.length < 8) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table is cut short");
|
|
42
|
+
var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
43
|
+
var n = v.getUint32(4, true);
|
|
44
|
+
var out = [];
|
|
45
|
+
if (!n) return out;
|
|
46
|
+
var dec = new ArithmeticDecoder(bytes, 8, bytes.length, base);
|
|
47
|
+
dec.readInitBytes();
|
|
48
|
+
var ic = new IntegerDecompressor(32, 2);
|
|
49
|
+
var prevPoints = 0, prevBytes = 0;
|
|
50
|
+
for (var i = 0; i < n; i++) {
|
|
51
|
+
var points = withPointCounts ? ic.decompress(dec, prevPoints, 0) : 0;
|
|
52
|
+
var size = ic.decompress(dec, prevBytes, 1);
|
|
53
|
+
// coded as i32 predictions of u32 counts: the i32 is the prediction, the u32 the count
|
|
54
|
+
prevPoints = points; prevBytes = size;
|
|
55
|
+
out.push({ pointCount: points < 0 ? points + 4294967296 : points, byteCount: size < 0 ? size + 4294967296 : size });
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every chunk of a LAZ file: its first record, point count and byte range.
|
|
62
|
+
* @param {{size:number, read:function(number, number): Promise<Uint8Array>}} src
|
|
63
|
+
* @param {object} header - parseHeader (offsetToPointData, pointCount, evlrStart)
|
|
64
|
+
* @param {object} vlr - parseLazVlr
|
|
65
|
+
* @returns {Promise<Array<{firstRecord:number, count:number, start:number, end:number}>>}
|
|
66
|
+
*/
|
|
67
|
+
export async function readChunks(src, header, vlr) {
|
|
68
|
+
var dataStart = header.offsetToPointData;
|
|
69
|
+
var total = header.pointCount;
|
|
70
|
+
if (vlr.compressor === COMPRESSOR.POINTWISE) {
|
|
71
|
+
var stop = header.evlrStart > dataStart && header.evlrStart <= src.size ? header.evlrStart : src.size;
|
|
72
|
+
return total ? [{ firstRecord: 0, count: total, start: dataStart, end: stop }] : [];
|
|
73
|
+
}
|
|
74
|
+
if (vlr.compressor !== COMPRESSOR.POINTWISE_CHUNKED && vlr.compressor !== COMPRESSOR.LAYERED_CHUNKED) {
|
|
75
|
+
throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ compressor " + vlr.compressor + " is not one LASzip defines (1-3)");
|
|
76
|
+
}
|
|
77
|
+
if (dataStart + 8 > src.size) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ point data is cut short", src.size);
|
|
78
|
+
var tableAt = i64(await src.read(dataStart, dataStart + 8), 0);
|
|
79
|
+
if (tableAt <= dataStart) {
|
|
80
|
+
tableAt = i64(await src.read(src.size - 8, src.size), 0); // streaming writers repeat it last
|
|
81
|
+
if (tableAt <= dataStart || tableAt >= src.size) {
|
|
82
|
+
throw new LasError(ERROR_CODES.UNSUPPORTED, "this LAZ file has no chunk table (an unfinished write?); it cannot be read in windows");
|
|
83
|
+
}
|
|
84
|
+
} else if (tableAt >= src.size) {
|
|
85
|
+
throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table should be at byte " + tableAt + " but the file is " + src.size + " bytes (cut short?)", src.size);
|
|
86
|
+
}
|
|
87
|
+
// Read only the table: EVLRs (which can be huge) may follow it. Each coded entry
|
|
88
|
+
// is two integers of at most ~6 bytes each; 24 bytes per chunk is a safe ceiling.
|
|
89
|
+
var head = await src.read(tableAt, Math.min(src.size, tableAt + 8));
|
|
90
|
+
var nChunks = head.length === 8 ? new DataView(head.buffer, head.byteOffset, 8).getUint32(4, true) : 0;
|
|
91
|
+
var table = decodeChunkTable(await src.read(tableAt, Math.min(src.size, tableAt + 8 + nChunks * 24 + 16)), vlr.variableChunks, tableAt);
|
|
92
|
+
var chunks = [], at = dataStart + 8, first = 0;
|
|
93
|
+
for (var i = 0; i < table.length && first < total; i++) {
|
|
94
|
+
var count = vlr.variableChunks ? table[i].pointCount : Math.min(vlr.chunkSize, total - first);
|
|
95
|
+
var end = at + table[i].byteCount;
|
|
96
|
+
if (end > tableAt) throw new LasError(ERROR_CODES.BAD_HEADER, "LAZ chunk " + i + " runs into the chunk table", at);
|
|
97
|
+
chunks.push({ firstRecord: first, count: count, start: at, end: end });
|
|
98
|
+
first += count;
|
|
99
|
+
at = end;
|
|
100
|
+
}
|
|
101
|
+
if (vlr.variableChunks) {
|
|
102
|
+
// counts come from the table: they must add up to the header's, or a corrupt count
|
|
103
|
+
// would decode past the header's points (or size a huge buffer)
|
|
104
|
+
var listed = table.reduce(function (s, e) { return s + e.pointCount; }, 0);
|
|
105
|
+
if (listed !== total) throw new LasError(ERROR_CODES.BAD_HEADER, "the LAZ chunk table lists " + listed + " points but the header says " + total, tableAt);
|
|
106
|
+
}
|
|
107
|
+
if (first < total) throw new LasError(ERROR_CODES.TRUNCATED, "the LAZ chunk table covers " + first + " of " + total + " points", tableAt);
|
|
108
|
+
return chunks;
|
|
109
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/laz/decompressChunk.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// One LAZ chunk -> the raw LAS records it holds, written into the
|
|
4
|
+
// caller's buffer exactly as a LAS file stores them, so the rest of
|
|
5
|
+
// the library (decodeRecords, filters, columns) reads LAZ and LAS the
|
|
6
|
+
// same way. Each chunk is independent: fresh item decompressors, the
|
|
7
|
+
// first record stored raw, then one arithmetic stream for the rest.
|
|
8
|
+
//
|
|
9
|
+
// Item dispatch after laz-rs src/record.rs and src/laszip/details.rs
|
|
10
|
+
// (https://github.com/laz-rs/laz-rs), itself from LASzip by rapidlasso.
|
|
11
|
+
// (c) 2007-2015 martin isenburg, rapidlasso; (c) 2019-2023 Thomas Montaigu.
|
|
12
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
13
|
+
// NOTICE). Changes: written in JavaScript over byte buffers, Brent Buffham 2026.
|
|
14
|
+
//==========================================================
|
|
15
|
+
|
|
16
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
17
|
+
import { ArithmeticDecoder } from "./ArithmeticDecoder.js";
|
|
18
|
+
import { ITEM, COMPRESSOR } from "./LazVlr.js";
|
|
19
|
+
import { Point10Decompressor } from "./v2/point10.js";
|
|
20
|
+
import { GpsTime11Decompressor } from "./v2/gpstime11.js";
|
|
21
|
+
import { Rgb12Decompressor } from "./v2/rgb12.js";
|
|
22
|
+
import { ByteDecompressor } from "./v2/byte.js";
|
|
23
|
+
import { Point14Decompressor } from "./v3/point14.js";
|
|
24
|
+
import { Rgb14Decompressor, Nir14Decompressor, Byte14Decompressor } from "./v3/layered.js";
|
|
25
|
+
|
|
26
|
+
var NAMES = { 0: "Byte", 6: "Point10", 7: "GpsTime11", 8: "RGB12", 9: "WavePacket13", 10: "Point14", 11: "RGB14", 12: "RGBNIR14", 13: "WavePacket14", 14: "Byte14" };
|
|
27
|
+
|
|
28
|
+
function pointwiseItem(it) {
|
|
29
|
+
if (it.version !== 2 && !(it.type === ITEM.BYTE && it.version === 1)) {
|
|
30
|
+
throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " version " + it.version + " is not supported (version 2 is; version 1 is from LASzip before 2011)");
|
|
31
|
+
}
|
|
32
|
+
switch (it.type) {
|
|
33
|
+
case ITEM.POINT10: return function () { return new Point10Decompressor(); };
|
|
34
|
+
case ITEM.GPSTIME11: return function () { return new GpsTime11Decompressor(); };
|
|
35
|
+
case ITEM.RGB12: return function () { return new Rgb12Decompressor(); };
|
|
36
|
+
case ITEM.BYTE: return function () { return new ByteDecompressor(it.size); };
|
|
37
|
+
default: throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " is not supported (wave packets are not read)");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Layered (LAS 1.4) items, versions 3 and 4: each makes one or more field decoders
|
|
42
|
+
function layeredItem(it) {
|
|
43
|
+
if (it.version !== 3 && it.version !== 4) {
|
|
44
|
+
throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " version " + it.version + " is not supported in a layered file (versions 3 and 4 are)");
|
|
45
|
+
}
|
|
46
|
+
var v = it.version;
|
|
47
|
+
switch (it.type) {
|
|
48
|
+
case ITEM.POINT14: return function () { return [new Point14Decompressor(v)]; };
|
|
49
|
+
case ITEM.RGB14: return function () { return [new Rgb14Decompressor(v)]; };
|
|
50
|
+
case ITEM.RGBNIR14: return function () { return [new Rgb14Decompressor(v), new Nir14Decompressor(v)]; };
|
|
51
|
+
case ITEM.BYTE14: return function () { return [new Byte14Decompressor(it.size, v)]; };
|
|
52
|
+
default: throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ item " + (NAMES[it.type] || it.type) + " is not supported (wave packets are not read)");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cutShortAt(base) { return function (at) { throw new LasError(ERROR_CODES.TRUNCATED, "a LAZ chunk is cut short", base + at); }; }
|
|
57
|
+
|
|
58
|
+
// A layered chunk: the first record raw, a u32 point count, every field's
|
|
59
|
+
// layer sizes, then the layers; each later point is decoded field by field,
|
|
60
|
+
// Point14 passing the scanner channel on to the fields after it.
|
|
61
|
+
function layeredChunk(makers, recordLength, layerWords) {
|
|
62
|
+
return function decodeChunk(bytes, s, e, count, view, o, base) {
|
|
63
|
+
if (!count) return;
|
|
64
|
+
base = base || 0;
|
|
65
|
+
var cutShort = cutShortAt(base);
|
|
66
|
+
var fields = [];
|
|
67
|
+
makers.forEach(function (make) { fields.push.apply(fields, make()); });
|
|
68
|
+
var ctx = { v: 0 }, p = s, k;
|
|
69
|
+
if (p + recordLength > e) cutShort(p);
|
|
70
|
+
for (k = 0; k < fields.length; k++) { fields[k].first(bytes, p, view, o, ctx); p += fields[k].size; o += fields[k].size; }
|
|
71
|
+
if (count === 1) return;
|
|
72
|
+
if (p + 4 + layerWords * 4 > e) cutShort(p);
|
|
73
|
+
var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
74
|
+
p += 4; // the chunk's point count (the table already has it)
|
|
75
|
+
for (k = 0; k < fields.length; k++) p = fields[k].readLayerSizes(dv, p);
|
|
76
|
+
for (k = 0; k < fields.length; k++) p = fields[k].readLayers(bytes, p, e, cutShort, base);
|
|
77
|
+
for (var i = 1; i < count; i++) {
|
|
78
|
+
ctx.v = 0;
|
|
79
|
+
for (k = 0; k < fields.length; k++) { fields[k].next(view, o, ctx); o += fields[k].size; }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check a LASzip VLR against the file and build the chunk decoder.
|
|
86
|
+
* @param {object} vlr - parseLazVlr
|
|
87
|
+
* @param {number} recordLength - header record length
|
|
88
|
+
* @returns {function(Uint8Array, number, number, number, DataView, number): void}
|
|
89
|
+
* decode(bytes, start, end, count, outView, outOffset, base) — one chunk's records into outView;
|
|
90
|
+
* base is the file offset of bytes[0], for error offsets
|
|
91
|
+
*/
|
|
92
|
+
export function createChunkDecoder(vlr, recordLength) {
|
|
93
|
+
var size = vlr.items.reduce(function (s, it) { return s + it.size; }, 0);
|
|
94
|
+
if (size !== recordLength) throw new LasError(ERROR_CODES.BAD_VLR, "the LASzip items make " + size + "-byte records but the header says " + recordLength);
|
|
95
|
+
if (vlr.coder !== 0) throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ coder " + vlr.coder + " (only the arithmetic coder, 0, exists)");
|
|
96
|
+
if (vlr.compressor === COMPRESSOR.LAYERED_CHUNKED) {
|
|
97
|
+
var words = vlr.items.reduce(function (s, it) { return s + (it.type === ITEM.POINT14 ? 9 : it.type === ITEM.RGBNIR14 ? 2 : it.type === ITEM.BYTE14 ? it.size : 1); }, 0);
|
|
98
|
+
return layeredChunk(vlr.items.map(layeredItem), recordLength, words);
|
|
99
|
+
}
|
|
100
|
+
var makers = vlr.items.map(pointwiseItem);
|
|
101
|
+
return function decodeChunk(bytes, s, e, count, view, o, base) {
|
|
102
|
+
if (!count) return;
|
|
103
|
+
base = base || 0;
|
|
104
|
+
var items = makers.map(function (make) { return make(); });
|
|
105
|
+
var k, p = s;
|
|
106
|
+
if (p + recordLength > e) throw new LasError(ERROR_CODES.TRUNCATED, "a LAZ chunk is shorter than one record", base + p);
|
|
107
|
+
for (k = 0; k < items.length; k++) { items[k].first(bytes, p, view, o); p += items[k].size; o += items[k].size; }
|
|
108
|
+
if (count === 1) return;
|
|
109
|
+
var dec = new ArithmeticDecoder(bytes, p, e, base);
|
|
110
|
+
dec.readInitBytes();
|
|
111
|
+
for (var i = 1; i < count; i++) {
|
|
112
|
+
for (k = 0; k < items.length; k++) { items[k].next(dec, view, o); o += items[k].size; }
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/laz/models.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Adaptive probability models for the LASzip arithmetic coder: a
|
|
4
|
+
// multi-symbol model (with a decoder look-up table above 16 symbols)
|
|
5
|
+
// and a binary model. Both re-estimate their probabilities on a
|
|
6
|
+
// growing update cycle, exactly as the encoder did, so encoder and
|
|
7
|
+
// decoder stay in step symbol for symbol.
|
|
8
|
+
//
|
|
9
|
+
// Ported from laz-rs src/models.rs (https://github.com/laz-rs/laz-rs),
|
|
10
|
+
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
11
|
+
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
12
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
13
|
+
// NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026.
|
|
14
|
+
// All values are u32 held in Numbers; every product stays below 2^32.
|
|
15
|
+
//==========================================================
|
|
16
|
+
|
|
17
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
18
|
+
|
|
19
|
+
export var DM_LENGTH_SHIFT = 15;
|
|
20
|
+
var DM_MAX_COUNT = 32768; // 1 << DM_LENGTH_SHIFT
|
|
21
|
+
export var BM_LENGTH_SHIFT = 13;
|
|
22
|
+
var BM_MAX_COUNT = 8192; // 1 << BM_LENGTH_SHIFT
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adaptive model over `symbols` symbols (2..2048).
|
|
26
|
+
* @param {number} symbols
|
|
27
|
+
* @param {Uint32Array|number[]} [initTable] - starting counts (default 1 each)
|
|
28
|
+
*/
|
|
29
|
+
export function ArithmeticModel(symbols, initTable) {
|
|
30
|
+
if (!(symbols >= 2 && symbols <= 2048)) throw new LasError(ERROR_CODES.UNSUPPORTED, "LAZ model with " + symbols + " symbols (2-2048)");
|
|
31
|
+
this.symbols = symbols;
|
|
32
|
+
this.lastSymbol = symbols - 1;
|
|
33
|
+
this.tableSize = 0;
|
|
34
|
+
this.tableShift = 0;
|
|
35
|
+
this.decoderTable = null;
|
|
36
|
+
if (symbols > 16) {
|
|
37
|
+
var tableBits = 3;
|
|
38
|
+
while (symbols > Math.pow(2, tableBits + 2)) tableBits++;
|
|
39
|
+
this.tableSize = Math.pow(2, tableBits);
|
|
40
|
+
this.tableShift = DM_LENGTH_SHIFT - tableBits;
|
|
41
|
+
this.decoderTable = new Uint32Array(this.tableSize + 2);
|
|
42
|
+
}
|
|
43
|
+
this.distribution = new Uint32Array(symbols);
|
|
44
|
+
this.symbolCount = new Uint32Array(symbols);
|
|
45
|
+
for (var k = 0; k < symbols; k++) this.symbolCount[k] = initTable ? initTable[k] : 1;
|
|
46
|
+
this.totalCount = 0;
|
|
47
|
+
this.updateCycle = symbols;
|
|
48
|
+
this.symbolsUntilUpdate = 0;
|
|
49
|
+
this.update();
|
|
50
|
+
this.symbolsUntilUpdate = this.updateCycle = (symbols + 6) >>> 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Re-estimate the distribution from the counts (and rebuild the decoder table). */
|
|
54
|
+
ArithmeticModel.prototype.update = function () {
|
|
55
|
+
var counts = this.symbolCount, dist = this.distribution, n = this.symbols, k;
|
|
56
|
+
this.totalCount += this.updateCycle;
|
|
57
|
+
if (this.totalCount > DM_MAX_COUNT) {
|
|
58
|
+
this.totalCount = 0;
|
|
59
|
+
for (k = 0; k < n; k++) { counts[k] = (counts[k] + 1) >>> 1; this.totalCount += counts[k]; }
|
|
60
|
+
}
|
|
61
|
+
var sum = 0, scale = Math.floor(0x80000000 / this.totalCount);
|
|
62
|
+
if (!this.decoderTable) {
|
|
63
|
+
for (k = 0; k < n; k++) { dist[k] = (scale * sum) >>> 16; sum += counts[k]; } // >> (31 - DM_LENGTH_SHIFT)
|
|
64
|
+
} else {
|
|
65
|
+
var table = this.decoderTable, shift = this.tableShift, s = 0;
|
|
66
|
+
for (k = 0; k < n; k++) {
|
|
67
|
+
dist[k] = (scale * sum) >>> 16;
|
|
68
|
+
sum += counts[k];
|
|
69
|
+
var w = dist[k] >>> shift;
|
|
70
|
+
while (s < w) { s++; table[s] = k - 1; }
|
|
71
|
+
}
|
|
72
|
+
table[0] = 0;
|
|
73
|
+
while (s <= this.tableSize) { s++; table[s] = n - 1; }
|
|
74
|
+
}
|
|
75
|
+
this.updateCycle = (5 * this.updateCycle) >>> 2;
|
|
76
|
+
var maxCycle = (n + 6) * 8;
|
|
77
|
+
if (this.updateCycle > maxCycle) this.updateCycle = maxCycle;
|
|
78
|
+
this.symbolsUntilUpdate = this.updateCycle;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Adaptive binary model. */
|
|
82
|
+
export function ArithmeticBitModel() {
|
|
83
|
+
this.bit0Count = 1;
|
|
84
|
+
this.bitCount = 2;
|
|
85
|
+
this.bit0Prob = 4096; // 1 << (BM_LENGTH_SHIFT - 1)
|
|
86
|
+
this.bitsUntilUpdate = 4;
|
|
87
|
+
this.updateCycle = 4;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ArithmeticBitModel.prototype.update = function () {
|
|
91
|
+
this.bitCount += this.updateCycle;
|
|
92
|
+
if (this.bitCount > BM_MAX_COUNT) {
|
|
93
|
+
this.bitCount = (this.bitCount + 1) >>> 1;
|
|
94
|
+
this.bit0Count = (this.bit0Count + 1) >>> 1;
|
|
95
|
+
if (this.bit0Count === this.bitCount) this.bitCount++;
|
|
96
|
+
}
|
|
97
|
+
var scale = Math.floor(0x80000000 / this.bitCount);
|
|
98
|
+
this.bit0Prob = (this.bit0Count * scale) >>> 18; // >> (31 - BM_LENGTH_SHIFT)
|
|
99
|
+
this.updateCycle = (5 * this.updateCycle) >>> 2;
|
|
100
|
+
if (this.updateCycle > 64) this.updateCycle = 64;
|
|
101
|
+
this.bitsUntilUpdate = this.updateCycle;
|
|
102
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/laz/v2/byte.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// LASzip Byte item, versions 1 and 2 (the same coder): extra bytes
|
|
4
|
+
// after the standard fields, each coded as a byte difference from the
|
|
5
|
+
// same byte of the last point, one model per byte.
|
|
6
|
+
//
|
|
7
|
+
// Ported from laz-rs src/las/extra_bytes.rs, module v1 (https://github.com/laz-rs/laz-rs),
|
|
8
|
+
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
9
|
+
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
10
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
11
|
+
// NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026.
|
|
12
|
+
//==========================================================
|
|
13
|
+
|
|
14
|
+
import { ArithmeticModel } from "../models.js";
|
|
15
|
+
|
|
16
|
+
/** Byte v2 decompressor for `count` extra bytes: one per chunk. */
|
|
17
|
+
export function ByteDecompressor(count) {
|
|
18
|
+
this.size = count;
|
|
19
|
+
this.last = new Uint8Array(count);
|
|
20
|
+
this.models = [];
|
|
21
|
+
for (var i = 0; i < count; i++) this.models.push(new ArithmeticModel(256));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ByteDecompressor.prototype.first = function (src, s, view, o) {
|
|
25
|
+
for (var i = 0; i < this.size; i++) { this.last[i] = src[s + i]; view.setUint8(o + i, src[s + i]); }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
ByteDecompressor.prototype.next = function (dec, view, o) {
|
|
29
|
+
for (var i = 0; i < this.size; i++) {
|
|
30
|
+
this.last[i] = (this.last[i] + dec.decodeSymbol(this.models[i])) & 255;
|
|
31
|
+
view.setUint8(o + i, this.last[i]);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/laz/v2/gpstime11.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// LASzip GpsTime11 item, version 2: the 8-byte GPS time, coded as its
|
|
4
|
+
// raw 64-bit pattern. Up to four time sequences are tracked; each
|
|
5
|
+
// point is "same time", a multiple of the last difference (with a
|
|
6
|
+
// correction), a jump to another tracked sequence, or a full new time.
|
|
7
|
+
// The 64-bit value is held as two 32-bit halves (hi signed, lo
|
|
8
|
+
// unsigned) with explicit carry, never as a float.
|
|
9
|
+
//
|
|
10
|
+
// Ported from laz-rs src/las/gps.rs, module v2 (https://github.com/laz-rs/laz-rs),
|
|
11
|
+
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
12
|
+
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
13
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
14
|
+
// NOTICE). Changes: translated from Rust to JavaScript, i64 as two u32
|
|
15
|
+
// halves, Brent Buffham 2026.
|
|
16
|
+
//==========================================================
|
|
17
|
+
|
|
18
|
+
import { ArithmeticModel } from "../models.js";
|
|
19
|
+
import { IntegerDecompressor } from "../IntegerDecompressor.js";
|
|
20
|
+
|
|
21
|
+
var LE = true;
|
|
22
|
+
var MULTI = 500, MULTI_MINUS = -10;
|
|
23
|
+
var MULTI_UNCHANGED = MULTI - MULTI_MINUS + 1; // 511
|
|
24
|
+
var MULTI_CODE_FULL = MULTI - MULTI_MINUS + 2; // 512
|
|
25
|
+
var MULTI_TOTAL = MULTI - MULTI_MINUS + 6; // 516
|
|
26
|
+
|
|
27
|
+
/** GpsTime11 v2 decompressor: one per chunk. */
|
|
28
|
+
export function GpsTime11Decompressor() {
|
|
29
|
+
this.size = 8;
|
|
30
|
+
this.multiModel = new ArithmeticModel(MULTI_TOTAL);
|
|
31
|
+
this.zeroDiffModel = new ArithmeticModel(6);
|
|
32
|
+
this.ic = new IntegerDecompressor(32, 9);
|
|
33
|
+
this.last = 0;
|
|
34
|
+
this.nextSeq = 0;
|
|
35
|
+
this.hi = new Int32Array(4);
|
|
36
|
+
this.lo = new Float64Array(4); // u32 values, held exactly
|
|
37
|
+
this.diffs = new Int32Array(4);
|
|
38
|
+
this.extremes = new Int32Array(4);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
GpsTime11Decompressor.prototype._add = function (i, d) {
|
|
42
|
+
var lo = this.lo[i] + d;
|
|
43
|
+
if (lo >= 4294967296) { lo -= 4294967296; this.hi[i] = (this.hi[i] + 1) | 0; }
|
|
44
|
+
else if (lo < 0) { lo += 4294967296; this.hi[i] = (this.hi[i] - 1) | 0; }
|
|
45
|
+
this.lo[i] = lo;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
GpsTime11Decompressor.prototype._full = function (dec) {
|
|
49
|
+
this.nextSeq = (this.nextSeq + 1) & 3;
|
|
50
|
+
this.hi[this.nextSeq] = this.ic.decompress(dec, this.hi[this.last], 8);
|
|
51
|
+
this.lo[this.nextSeq] = dec.readInt();
|
|
52
|
+
this.last = this.nextSeq;
|
|
53
|
+
this.diffs[this.last] = 0;
|
|
54
|
+
this.extremes[this.last] = 0;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// After three extreme differences in a row, the latest becomes the new reference
|
|
58
|
+
GpsTime11Decompressor.prototype._extreme = function (d) {
|
|
59
|
+
if (++this.extremes[this.last] > 3) { this.diffs[this.last] = d; this.extremes[this.last] = 0; }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
GpsTime11Decompressor.prototype.first = function (src, s, view, o) {
|
|
63
|
+
for (var i = 0; i < 8; i++) view.setUint8(o + i, src[s + i]);
|
|
64
|
+
this.lo[0] = view.getUint32(o, LE);
|
|
65
|
+
this.hi[0] = view.getInt32(o + 4, LE);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
GpsTime11Decompressor.prototype.next = function (dec, view, o) {
|
|
69
|
+
for (;;) {
|
|
70
|
+
var L = this.last, multi, d;
|
|
71
|
+
if (this.diffs[L] === 0) {
|
|
72
|
+
multi = dec.decodeSymbol(this.zeroDiffModel);
|
|
73
|
+
if (multi === 1) {
|
|
74
|
+
this.diffs[L] = this.ic.decompress(dec, 0, 0);
|
|
75
|
+
this._add(L, this.diffs[L]);
|
|
76
|
+
this.extremes[L] = 0;
|
|
77
|
+
} else if (multi === 2) {
|
|
78
|
+
this._full(dec);
|
|
79
|
+
} else if (multi > 2) {
|
|
80
|
+
this.last = (L + multi - 2) & 3;
|
|
81
|
+
continue; // laz-rs recurses: decode again from the new sequence
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
multi = dec.decodeSymbol(this.multiModel);
|
|
85
|
+
if (multi === 1) {
|
|
86
|
+
this._add(L, this.ic.decompress(dec, this.diffs[L], 1));
|
|
87
|
+
this.extremes[L] = 0;
|
|
88
|
+
} else if (multi < MULTI_UNCHANGED) {
|
|
89
|
+
if (multi === 0) {
|
|
90
|
+
d = this.ic.decompress(dec, 0, 7);
|
|
91
|
+
this._extreme(d);
|
|
92
|
+
} else if (multi < MULTI) {
|
|
93
|
+
d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), multi < 10 ? 2 : 3);
|
|
94
|
+
} else if (multi === MULTI) {
|
|
95
|
+
d = this.ic.decompress(dec, Math.imul(MULTI, this.diffs[L]), 4);
|
|
96
|
+
this._extreme(d);
|
|
97
|
+
} else {
|
|
98
|
+
multi = MULTI - multi;
|
|
99
|
+
if (multi > MULTI_MINUS) {
|
|
100
|
+
d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), 5);
|
|
101
|
+
} else {
|
|
102
|
+
d = this.ic.decompress(dec, Math.imul(MULTI_MINUS, this.diffs[L]), 6);
|
|
103
|
+
this._extreme(d);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
this._add(L, d);
|
|
107
|
+
} else if (multi === MULTI_CODE_FULL) {
|
|
108
|
+
this._full(dec);
|
|
109
|
+
} else if (multi > MULTI_CODE_FULL) {
|
|
110
|
+
this.last = (L + multi - MULTI_CODE_FULL) & 3;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
view.setUint32(o, this.lo[this.last], LE);
|
|
117
|
+
view.setInt32(o + 4, this.hi[this.last], LE);
|
|
118
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// src/laz/v2/point10.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// LASzip Point10 item, version 2: the 20 bytes every legacy record
|
|
4
|
+
// (formats 0-5) starts with. Each point codes which attributes changed
|
|
5
|
+
// since the last one; X and Y are corrections to a streaming median
|
|
6
|
+
// of the recent differences, Z to the last height at the same return
|
|
7
|
+
// level, all through integer decompressors whose contexts follow the
|
|
8
|
+
// return structure.
|
|
9
|
+
//
|
|
10
|
+
// Ported from laz-rs src/las/point0.rs, module v2, and the median and
|
|
11
|
+
// return tables of src/las/utils.rs (https://github.com/laz-rs/laz-rs),
|
|
12
|
+
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
13
|
+
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
14
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
15
|
+
// NOTICE). Changes: translated from Rust to JavaScript, working on raw
|
|
16
|
+
// record bytes, Brent Buffham 2026.
|
|
17
|
+
//==========================================================
|
|
18
|
+
|
|
19
|
+
import { ArithmeticModel } from "../models.js";
|
|
20
|
+
import { IntegerDecompressor } from "../IntegerDecompressor.js";
|
|
21
|
+
|
|
22
|
+
var LE = true;
|
|
23
|
+
|
|
24
|
+
/** NUMBER_RETURN_MAP[numberOfReturns][returnNumber] (LASzip). */
|
|
25
|
+
export var NUMBER_RETURN_MAP = [
|
|
26
|
+
[15, 14, 13, 12, 11, 10, 9, 8], [14, 0, 1, 3, 6, 10, 10, 9], [13, 1, 2, 4, 7, 11, 11, 10], [12, 3, 4, 5, 8, 12, 12, 11],
|
|
27
|
+
[11, 6, 7, 8, 9, 13, 13, 12], [10, 10, 11, 12, 13, 14, 14, 13], [9, 10, 11, 12, 13, 14, 15, 14], [8, 9, 10, 11, 12, 13, 14, 15]
|
|
28
|
+
];
|
|
29
|
+
/** NUMBER_RETURN_LEVEL[numberOfReturns][returnNumber] (LASzip). */
|
|
30
|
+
export var NUMBER_RETURN_LEVEL = [
|
|
31
|
+
[0, 1, 2, 3, 4, 5, 6, 7], [1, 0, 1, 2, 3, 4, 5, 6], [2, 1, 0, 1, 2, 3, 4, 5], [3, 2, 1, 0, 1, 2, 3, 4],
|
|
32
|
+
[4, 3, 2, 1, 0, 1, 2, 3], [5, 4, 3, 2, 1, 0, 1, 2], [6, 5, 4, 3, 2, 1, 0, 1], [7, 6, 5, 4, 3, 2, 1, 0]
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Median of the last five values, updated in O(1) (LASzip StreamingMedian5). */
|
|
36
|
+
export function StreamingMedian() { this.v = new Int32Array(5); this.high = true; }
|
|
37
|
+
StreamingMedian.prototype.get = function () { return this.v[2]; };
|
|
38
|
+
StreamingMedian.prototype.add = function (x) {
|
|
39
|
+
var v = this.v;
|
|
40
|
+
if (this.high) {
|
|
41
|
+
if (x < v[2]) {
|
|
42
|
+
v[4] = v[3]; v[3] = v[2];
|
|
43
|
+
if (x < v[0]) { v[2] = v[1]; v[1] = v[0]; v[0] = x; }
|
|
44
|
+
else if (x < v[1]) { v[2] = v[1]; v[1] = x; }
|
|
45
|
+
else v[2] = x;
|
|
46
|
+
} else {
|
|
47
|
+
if (x < v[3]) { v[4] = v[3]; v[3] = x; } else v[4] = x;
|
|
48
|
+
this.high = false;
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
if (v[2] < x) {
|
|
52
|
+
v[0] = v[1]; v[1] = v[2];
|
|
53
|
+
if (v[4] < x) { v[2] = v[3]; v[3] = v[4]; v[4] = x; }
|
|
54
|
+
else if (v[3] < x) { v[2] = v[3]; v[3] = x; }
|
|
55
|
+
else v[2] = x;
|
|
56
|
+
} else {
|
|
57
|
+
if (v[1] < x) { v[0] = v[1]; v[1] = x; } else v[0] = x;
|
|
58
|
+
this.high = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function models(n, symbols) { var out = []; for (var i = 0; i < n; i++) out.push(new ArithmeticModel(symbols)); return out; }
|
|
64
|
+
function medians() { var out = []; for (var i = 0; i < 16; i++) out.push(new StreamingMedian()); return out; }
|
|
65
|
+
|
|
66
|
+
/** Point10 v2 decompressor: one per chunk. */
|
|
67
|
+
export function Point10Decompressor() {
|
|
68
|
+
this.size = 20;
|
|
69
|
+
this.lastIntensity = new Uint16Array(16);
|
|
70
|
+
this.xMedian = medians();
|
|
71
|
+
this.yMedian = medians();
|
|
72
|
+
this.lastHeight = new Int32Array(8);
|
|
73
|
+
this.changedValues = new ArithmeticModel(64);
|
|
74
|
+
this.scanAngleRank = models(2, 256);
|
|
75
|
+
this.bitByte = models(256, 256);
|
|
76
|
+
this.classification = models(256, 256);
|
|
77
|
+
this.userData = models(256, 256);
|
|
78
|
+
this.icIntensity = new IntegerDecompressor(16, 4);
|
|
79
|
+
this.icPointSourceId = new IntegerDecompressor(16, 1);
|
|
80
|
+
this.icDx = new IntegerDecompressor(32, 2);
|
|
81
|
+
this.icDy = new IntegerDecompressor(32, 22);
|
|
82
|
+
this.icZ = new IntegerDecompressor(32, 20);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The first point of a chunk is stored raw. */
|
|
86
|
+
Point10Decompressor.prototype.first = function (src, s, view, o) {
|
|
87
|
+
for (var i = 0; i < 20; i++) view.setUint8(o + i, src[s + i]);
|
|
88
|
+
this.x = view.getInt32(o, LE); this.y = view.getInt32(o + 4, LE); this.z = view.getInt32(o + 8, LE);
|
|
89
|
+
this.intensity = 0; // laz-rs: the first point's intensity is not a prediction
|
|
90
|
+
this.bits = view.getUint8(o + 14);
|
|
91
|
+
this.cls = view.getUint8(o + 15);
|
|
92
|
+
this.angle = view.getInt8(o + 16);
|
|
93
|
+
this.user = view.getUint8(o + 17);
|
|
94
|
+
this.psid = view.getUint16(o + 18, LE);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
Point10Decompressor.prototype.next = function (dec, view, o) {
|
|
98
|
+
var changed = dec.decodeSymbol(this.changedValues);
|
|
99
|
+
var r, n, m, l;
|
|
100
|
+
if (changed !== 0) {
|
|
101
|
+
if (changed & 32) this.bits = dec.decodeSymbol(this.bitByte[this.bits]);
|
|
102
|
+
r = this.bits & 7; n = (this.bits >>> 3) & 7;
|
|
103
|
+
m = NUMBER_RETURN_MAP[n][r]; l = NUMBER_RETURN_LEVEL[n][r];
|
|
104
|
+
if (changed & 16) {
|
|
105
|
+
this.intensity = this.icIntensity.decompress(dec, this.lastIntensity[m], m < 3 ? m : 3);
|
|
106
|
+
this.lastIntensity[m] = this.intensity;
|
|
107
|
+
} else {
|
|
108
|
+
this.intensity = this.lastIntensity[m];
|
|
109
|
+
}
|
|
110
|
+
if (changed & 8) this.cls = dec.decodeSymbol(this.classification[this.cls]);
|
|
111
|
+
if (changed & 4) {
|
|
112
|
+
var d = dec.decodeSymbol(this.scanAngleRank[(this.bits >>> 6) & 1]);
|
|
113
|
+
this.angle = ((this.angle + d) << 24) >> 24; // i8 wrapping_add
|
|
114
|
+
}
|
|
115
|
+
if (changed & 2) this.user = dec.decodeSymbol(this.userData[this.user]);
|
|
116
|
+
if (changed & 1) this.psid = this.icPointSourceId.decompress(dec, this.psid, 0);
|
|
117
|
+
} else {
|
|
118
|
+
r = this.bits & 7; n = (this.bits >>> 3) & 7;
|
|
119
|
+
m = NUMBER_RETURN_MAP[n][r]; l = NUMBER_RETURN_LEVEL[n][r];
|
|
120
|
+
}
|
|
121
|
+
var single = n === 1 ? 1 : 0;
|
|
122
|
+
var diff = this.icDx.decompress(dec, this.xMedian[m].get(), single);
|
|
123
|
+
this.x = (this.x + diff) | 0;
|
|
124
|
+
this.xMedian[m].add(diff);
|
|
125
|
+
var kx = this.icDx.k;
|
|
126
|
+
diff = this.icDy.decompress(dec, this.yMedian[m].get(), single + (kx < 20 ? kx & 0xFFFFFFFE : 20));
|
|
127
|
+
this.y = (this.y + diff) | 0;
|
|
128
|
+
this.yMedian[m].add(diff);
|
|
129
|
+
var kz = Math.floor((this.icDx.k + this.icDy.k) / 2);
|
|
130
|
+
this.z = this.icZ.decompress(dec, this.lastHeight[l], single + (kz < 18 ? kz & 0xFFFFFFFE : 18));
|
|
131
|
+
this.lastHeight[l] = this.z;
|
|
132
|
+
view.setInt32(o, this.x, LE); view.setInt32(o + 4, this.y, LE); view.setInt32(o + 8, this.z, LE);
|
|
133
|
+
view.setUint16(o + 12, this.intensity, LE);
|
|
134
|
+
view.setUint8(o + 14, this.bits);
|
|
135
|
+
view.setUint8(o + 15, this.cls);
|
|
136
|
+
view.setInt8(o + 16, this.angle);
|
|
137
|
+
view.setUint8(o + 17, this.user);
|
|
138
|
+
view.setUint16(o + 18, this.psid, LE);
|
|
139
|
+
};
|