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
package/src/colour/colourize.js
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
// so one hot return does not wash the rest out.
|
|
15
15
|
// classification: conventional viewer colours. ASPRS defines the
|
|
16
16
|
// codes, not colours; these follow common LiDAR-viewer practice.
|
|
17
|
+
//
|
|
18
|
+
// createChunkColourizer colours a STREAM of chunks with one set of
|
|
19
|
+
// rules, so neighbouring windows match: elevation over the file's
|
|
20
|
+
// header Z range, intensity over the first chunk's percentiles, and
|
|
21
|
+
// the 8-bit decision made once (it only ever moves to 16-bit, when a
|
|
22
|
+
// later chunk holds a value above 255).
|
|
17
23
|
//==========================================================
|
|
18
24
|
|
|
19
25
|
import { LasError, ERROR_CODES } from "../errors.js";
|
|
@@ -81,9 +87,15 @@ function rampColours(values, n, range, ramp) {
|
|
|
81
87
|
return out;
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
function maxRgb(points) {
|
|
91
|
+
var n = points.count;
|
|
92
|
+
return Math.max(rangeOf(need(points, "red"), n)[1], rangeOf(need(points, "green"), n)[1], rangeOf(need(points, "blue"), n)[1]);
|
|
93
|
+
}
|
|
94
|
+
|
|
84
95
|
/**
|
|
85
96
|
* @param {object} points - PointColumns.
|
|
86
|
-
* @param {{by:string, ramp?:string, range?:number[]}} opts
|
|
97
|
+
* @param {{by:string, ramp?:string, range?:number[], eightBit?:boolean}} opts - eightBit
|
|
98
|
+
* overrides the 8-bit colour detection
|
|
87
99
|
* @returns {Uint8Array} r, g, b per point
|
|
88
100
|
*/
|
|
89
101
|
export function colourize(points, opts) {
|
|
@@ -96,8 +108,7 @@ export function colourize(points, opts) {
|
|
|
96
108
|
switch (opts.by) {
|
|
97
109
|
case "rgb": {
|
|
98
110
|
var r = need(points, "red"), g = need(points, "green"), b = need(points, "blue");
|
|
99
|
-
var
|
|
100
|
-
var eightBit = max <= 255;
|
|
111
|
+
var eightBit = typeof opts.eightBit === "boolean" ? opts.eightBit : maxRgb(points) <= 255;
|
|
101
112
|
out = new Uint8Array(n * 3);
|
|
102
113
|
for (i = 0; i < n; i++) {
|
|
103
114
|
out[i * 3] = eightBit ? r[i] : Math.min(255, Math.round(r[i] / 256));
|
|
@@ -127,3 +138,24 @@ export function colourize(points, opts) {
|
|
|
127
138
|
throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "colourize by \"" + opts.by + "\" is not a mode (rgb, intensity, elevation, classification)");
|
|
128
139
|
}
|
|
129
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Colour a stream of chunks (reader.chunks) consistently from window to window.
|
|
144
|
+
* @param {{by:string, ramp?:string, range?:number[]}} opts - as colourize
|
|
145
|
+
* @param {{minZ:number, maxZ:number}} [bounds] - the file's header bounds (elevation range)
|
|
146
|
+
* @param {{z?:number}} [localOrigin] - the origin the chunks were read with
|
|
147
|
+
* @returns {function(object): Uint8Array} colours one chunk
|
|
148
|
+
*/
|
|
149
|
+
export function createChunkColourizer(opts, bounds, localOrigin) {
|
|
150
|
+
opts = opts || {};
|
|
151
|
+
var range = opts.range || null, eightBit = null;
|
|
152
|
+
if (!range && opts.by === "elevation" && bounds && bounds.maxZ >= bounds.minZ) {
|
|
153
|
+
var oz = localOrigin && isFinite(localOrigin.z) ? localOrigin.z : 0;
|
|
154
|
+
range = [bounds.minZ - oz, bounds.maxZ - oz];
|
|
155
|
+
}
|
|
156
|
+
return function (points) {
|
|
157
|
+
if (opts.by === "intensity" && !range && points.count) range = percentiles(need(points, "intensity"), points.count);
|
|
158
|
+
if (opts.by === "rgb" && points.count && eightBit !== false) eightBit = maxRgb(points) <= 255;
|
|
159
|
+
return colourize(points, Object.assign({}, opts, { range: range || undefined, eightBit: eightBit === null ? undefined : eightBit }));
|
|
160
|
+
};
|
|
161
|
+
}
|
package/src/filter/dedupe.js
CHANGED
|
@@ -20,35 +20,52 @@ export function dedupe(cols, tolerance, opts) {
|
|
|
20
20
|
if (!(tolerance > 0)) throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "dedupe tolerance must be > 0");
|
|
21
21
|
var use3d = !!(opts && opts.z && cols.z);
|
|
22
22
|
var tol2 = tolerance * tolerance;
|
|
23
|
-
var
|
|
24
|
-
var kept = new Uint32Array(cols.count);
|
|
25
|
-
var nKept = 0;
|
|
23
|
+
var n = cols.count;
|
|
26
24
|
var x = cols.x, y = cols.y, z = cols.z;
|
|
27
25
|
var dzRange = use3d ? 1 : 0;
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// Cell indices, shifted so every neighbour is >= 0
|
|
28
|
+
var lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
29
|
+
for (var p = 0; p < n; p++) {
|
|
30
|
+
var a0 = Math.floor(x[p] / tolerance), a1 = Math.floor(y[p] / tolerance), a2 = use3d ? Math.floor(z[p] / tolerance) : 0;
|
|
31
|
+
if (a0 < lo[0]) lo[0] = a0; if (a0 > hi[0]) hi[0] = a0;
|
|
32
|
+
if (a1 < lo[1]) lo[1] = a1; if (a1 > hi[1]) hi[1] = a1;
|
|
33
|
+
if (a2 < lo[2]) lo[2] = a2; if (a2 > hi[2]) hi[2] = a2;
|
|
34
|
+
}
|
|
35
|
+
var SY = hi[1] - lo[1] + 3, SZ = hi[2] - lo[2] + 3;
|
|
36
|
+
// A dense numeric key when the cell count fits 2^53; else a hashed one —
|
|
37
|
+
// two cells may then share a key, which only adds candidates: every
|
|
38
|
+
// candidate is still distance-checked, so the result is exact either way
|
|
39
|
+
var dense = (hi[0] - lo[0] + 3) * SY * SZ <= 9007199254740992;
|
|
40
|
+
function key(i, j, k) { return dense ? (i * SY + j) * SZ + k : (i * 73856093 + j * 19349663 + k * 83492791) % 9007199254740881; }
|
|
41
|
+
|
|
42
|
+
// Each cell is a linked list through next[] (Map: key -> most recent point)
|
|
43
|
+
var head = new Map();
|
|
44
|
+
var next = new Int32Array(n);
|
|
45
|
+
var kept = new Uint32Array(n);
|
|
46
|
+
var nKept = 0;
|
|
47
|
+
for (var i = 0; i < n; i++) {
|
|
48
|
+
var cx = Math.floor(x[i] / tolerance) - lo[0] + 1, cy = Math.floor(y[i] / tolerance) - lo[1] + 1;
|
|
49
|
+
var cz = use3d ? Math.floor(z[i] / tolerance) - lo[2] + 1 : 1;
|
|
32
50
|
var dup = false;
|
|
33
51
|
for (var dx = -1; dx <= 1 && !dup; dx++) {
|
|
34
52
|
for (var dy = -1; dy <= 1 && !dup; dy++) {
|
|
35
53
|
for (var dz = -dzRange; dz <= dzRange && !dup; dz++) {
|
|
36
|
-
var
|
|
37
|
-
|
|
38
|
-
for (var j = 0; j < cell.length; j++) {
|
|
39
|
-
var k = cell[j];
|
|
54
|
+
var k = head.get(key(cx + dx, cy + dy, cz + dz));
|
|
55
|
+
while (k !== undefined && k >= 0) {
|
|
40
56
|
var ex = x[k] - x[i], ey = y[k] - y[i];
|
|
41
57
|
var ez = use3d ? z[k] - z[i] : 0;
|
|
42
58
|
if (ex * ex + ey * ey + ez * ez <= tol2) { dup = true; break; }
|
|
59
|
+
k = next[k];
|
|
43
60
|
}
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
}
|
|
47
64
|
if (dup) continue;
|
|
48
|
-
var
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
var own = key(cx, cy, cz);
|
|
66
|
+
var prev = head.get(own);
|
|
67
|
+
next[i] = prev === undefined ? -1 : prev;
|
|
68
|
+
head.set(own, i);
|
|
52
69
|
kept[nKept++] = i;
|
|
53
70
|
}
|
|
54
71
|
|
package/src/filter/filters.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// rejected points cost one or two byte reads (Kirra's classFilter
|
|
5
5
|
// trick, generalised): classification codes, return numbers and a
|
|
6
6
|
// 3D box. Filters combine with AND.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// maxPoints thinning is NOT Kirra's floor-stride-and-cap rule, which
|
|
8
|
+
// kept only the start of the file and, run before the filters,
|
|
9
|
+
// could leave nothing: decodeRecords thins the records that PASS,
|
|
10
|
+
// evenly over the whole file (countKept supplies their number).
|
|
10
11
|
//==========================================================
|
|
11
12
|
|
|
12
13
|
import { POINT_FORMATS } from "../format/PointFormats.js";
|
|
@@ -57,12 +58,15 @@ export function makeKeep(filter, header) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @param {
|
|
62
|
-
* @param {number}
|
|
63
|
-
* @
|
|
61
|
+
* Count the records in a window of whole records that pass keep.
|
|
62
|
+
* @param {Uint8Array} bytes
|
|
63
|
+
* @param {number} recordLength
|
|
64
|
+
* @param {function(DataView, number): boolean} keep
|
|
65
|
+
* @returns {number}
|
|
64
66
|
*/
|
|
65
|
-
export function
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
export function countKept(bytes, recordLength, keep) {
|
|
68
|
+
var view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
69
|
+
var n = Math.floor(bytes.length / recordLength), k = 0;
|
|
70
|
+
for (var i = 0; i < n; i++) if (keep(view, i * recordLength)) k++;
|
|
71
|
+
return k;
|
|
68
72
|
}
|
package/src/format/LasVlr.js
CHANGED
|
@@ -16,7 +16,7 @@ var LE = true;
|
|
|
16
16
|
* @param {number} start - offset of the first record in src.
|
|
17
17
|
* @param {number} count
|
|
18
18
|
* @param {number} [limit] - records must end at or before this offset (default: end of src).
|
|
19
|
-
* @returns {Array<{userId:string, recordId:number, description:string, start:number, bytes:Uint8Array}>}
|
|
19
|
+
* @returns {Array<{userId:string, recordId:number, description:string, start:number, length:number, bytes:Uint8Array}>}
|
|
20
20
|
*/
|
|
21
21
|
export function parseVlrs(src, start, count, limit) {
|
|
22
22
|
var end = typeof limit === "number" ? limit : src.length();
|
|
@@ -31,6 +31,7 @@ export function parseVlrs(src, start, count, limit) {
|
|
|
31
31
|
recordId: src.getUint16(pos + 18, LE),
|
|
32
32
|
description: readAscii(src, pos + 22, 32),
|
|
33
33
|
start: pos + 54,
|
|
34
|
+
length: len,
|
|
34
35
|
bytes: new Uint8Array(src.slice(pos + 54, pos + 54 + len))
|
|
35
36
|
});
|
|
36
37
|
pos += 54 + len;
|
|
@@ -43,21 +44,23 @@ export function parseVlrs(src, start, count, limit) {
|
|
|
43
44
|
* @param {object} src - sync source holding the records (offsets in src).
|
|
44
45
|
* @param {number} start
|
|
45
46
|
* @param {number} count
|
|
46
|
-
* @
|
|
47
|
+
* @param {boolean} [headersOnly] - src holds only the headers: no bytes (null), no length check
|
|
48
|
+
* @returns {Array<{userId:string, recordId:number, description:string, start:number, length:number, bytes:Uint8Array|null}>}
|
|
47
49
|
*/
|
|
48
|
-
export function parseEvlrs(src, start, count) {
|
|
50
|
+
export function parseEvlrs(src, start, count, headersOnly) {
|
|
49
51
|
var out = [];
|
|
50
52
|
var pos = start;
|
|
51
53
|
for (var i = 0; i < count; i++) {
|
|
52
54
|
if (pos + 60 > src.length()) throw new LasError(ERROR_CODES.BAD_VLR, "EVLR " + (i + 1) + " starts past the end of the file", pos);
|
|
53
55
|
var len = readU64(src, pos + 20, LE);
|
|
54
|
-
if (len === null || pos + 60 + len > src.length()) throw new LasError(ERROR_CODES.BAD_VLR, "EVLR " + (i + 1) + " runs past the end of the file", pos);
|
|
56
|
+
if (len === null || (!headersOnly && pos + 60 + len > src.length())) throw new LasError(ERROR_CODES.BAD_VLR, "EVLR " + (i + 1) + " runs past the end of the file", pos);
|
|
55
57
|
out.push({
|
|
56
58
|
userId: readAscii(src, pos + 2, 16),
|
|
57
59
|
recordId: src.getUint16(pos + 18, LE),
|
|
58
60
|
description: readAscii(src, pos + 28, 32),
|
|
59
61
|
start: pos + 60,
|
|
60
|
-
|
|
62
|
+
length: len,
|
|
63
|
+
bytes: headersOnly ? null : new Uint8Array(src.slice(pos + 60, pos + 60 + len))
|
|
61
64
|
});
|
|
62
65
|
pos += 60 + len;
|
|
63
66
|
}
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//==========================================================
|
|
7
7
|
|
|
8
8
|
/** Library version, matches package.json. */
|
|
9
|
-
export var VERSION = "0.
|
|
9
|
+
export var VERSION = "0.2.0";
|
|
10
10
|
|
|
11
11
|
export { LasError, ERROR_CODES } from "./errors.js";
|
|
12
12
|
export { openLas, LasReader } from "./LasReader.js";
|
|
@@ -15,13 +15,13 @@ export { parseVlrs, parseEvlrs } from "./format/LasVlr.js";
|
|
|
15
15
|
export { parseCrs } from "./format/LasCrs.js";
|
|
16
16
|
export { POINT_FORMATS, formatHasRgb } from "./format/PointFormats.js";
|
|
17
17
|
export { decodeRecords, DEFAULT_FIELDS, FIELD_TYPES } from "./read/decodeRecords.js";
|
|
18
|
-
export { makeKeep,
|
|
18
|
+
export { makeKeep, countKept } from "./filter/filters.js";
|
|
19
19
|
export { dedupe } from "./filter/dedupe.js";
|
|
20
20
|
export { MemorySource } from "./io/MemorySource.js";
|
|
21
21
|
export { BlobSource } from "./io/BlobSource.js";
|
|
22
22
|
export { NodeFileSource } from "./io/NodeFileSource.js";
|
|
23
23
|
export { SCRATCH_DIR, openScratchSyncWriter, openScratchReader, removeScratchFile, sweepStaleScratchFiles, listScratchFiles, getScratchFile } from "./io/ScratchBinaryFile.js";
|
|
24
|
-
export { colourize, RAMPS, CLASS_COLOURS } from "./colour/colourize.js";
|
|
24
|
+
export { colourize, createChunkColourizer, RAMPS, CLASS_COLOURS } from "./colour/colourize.js";
|
|
25
25
|
export { writeLas, createLasWriter } from "./write/writeLas.js";
|
|
26
26
|
export { chooseScaleOffset } from "./write/scaleOffset.js";
|
|
27
27
|
export { createWorkerHandler } from "./workers/workerProtocol.js";
|
package/src/io/NodeFileSource.js
CHANGED
|
@@ -26,11 +26,15 @@ export function NodeFileSource(handle, size) {
|
|
|
26
26
|
* @param {string} path
|
|
27
27
|
* @returns {Promise<NodeFileSource>}
|
|
28
28
|
*/
|
|
29
|
+
var NODE_FS = ["node", "fs/promises"].join(":");
|
|
30
|
+
|
|
29
31
|
NodeFileSource.open = async function (path) {
|
|
30
32
|
if (typeof path !== "string" || !path) {
|
|
31
33
|
throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "NodeFileSource needs a file path");
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
// A non-literal specifier, so browser bundlers (esbuild, webpack, Vite)
|
|
36
|
+
// leave it alone instead of failing to resolve a Node built-in
|
|
37
|
+
var fs = await import(/* webpackIgnore: true */ /* @vite-ignore */ NODE_FS);
|
|
34
38
|
var handle = await fs.open(path, "r");
|
|
35
39
|
var stat = await handle.stat();
|
|
36
40
|
return new NodeFileSource(handle, stat.size);
|
|
@@ -49,7 +53,25 @@ NodeFileSource.prototype.read = async function (start, end) {
|
|
|
49
53
|
if (end > this.size) {
|
|
50
54
|
throw new LasError(ERROR_CODES.TRUNCATED, "read past end of " + this.size + "-byte file", end);
|
|
51
55
|
}
|
|
52
|
-
|
|
56
|
+
return this.readInto(start, end, new Uint8Array(end - start));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read bytes [start, end) into the front of target (reused by the reader
|
|
61
|
+
* from window to window, so a big read leaves no per-window garbage).
|
|
62
|
+
* @param {number} start
|
|
63
|
+
* @param {number} end
|
|
64
|
+
* @param {Uint8Array} target - at least end - start bytes
|
|
65
|
+
* @returns {Promise<Uint8Array>} the filled view of target
|
|
66
|
+
*/
|
|
67
|
+
NodeFileSource.prototype.readInto = async function (start, end, target) {
|
|
68
|
+
if (start < 0 || end < start) {
|
|
69
|
+
throw new LasError(ERROR_CODES.INVALID_ARGUMENT, "bad read range " + start + ".." + end);
|
|
70
|
+
}
|
|
71
|
+
if (end > this.size) {
|
|
72
|
+
throw new LasError(ERROR_CODES.TRUNCATED, "read past end of " + this.size + "-byte file", end);
|
|
73
|
+
}
|
|
74
|
+
var out = target.subarray(0, end - start);
|
|
53
75
|
var done = 0;
|
|
54
76
|
while (done < out.length) {
|
|
55
77
|
var r = await this._handle.read(out, done, out.length - done, start + done);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/laz/ArithmeticDecoder.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// The LASzip arithmetic decoder, over a byte range of one Uint8Array
|
|
4
|
+
// (a LAZ chunk, read whole). Running past the range end is TRUNCATED:
|
|
5
|
+
// a well-formed stream never needs a byte beyond its own end, so a
|
|
6
|
+
// short or corrupt chunk fails loudly instead of decoding garbage.
|
|
7
|
+
//
|
|
8
|
+
// Ported from laz-rs src/decoders.rs (https://github.com/laz-rs/laz-rs),
|
|
9
|
+
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
10
|
+
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
11
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
12
|
+
// NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026;
|
|
13
|
+
// the coder interval laz-rs calls `length` is `range` here.
|
|
14
|
+
// value and range are u32 in Numbers (>>> 0 after every shift); every
|
|
15
|
+
// product of a probability and the shifted range stays below 2^32.
|
|
16
|
+
//==========================================================
|
|
17
|
+
|
|
18
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
19
|
+
|
|
20
|
+
var AC_MAX_LENGTH = 0xFFFFFFFF;
|
|
21
|
+
var AC_MIN_LENGTH = 0x01000000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Uint8Array} bytes
|
|
25
|
+
* @param {number} [start] - first byte of the stream (default 0)
|
|
26
|
+
* @param {number} [end] - one past the last byte (default bytes.length)
|
|
27
|
+
* @param {number} [base] - file offset of bytes[0], so errors report file offsets (default 0)
|
|
28
|
+
*/
|
|
29
|
+
export function ArithmeticDecoder(bytes, start, end, base) {
|
|
30
|
+
this.bytes = bytes;
|
|
31
|
+
this.base = base || 0;
|
|
32
|
+
this.p = start || 0;
|
|
33
|
+
this.end = end === undefined ? bytes.length : end;
|
|
34
|
+
this.value = 0;
|
|
35
|
+
this.range = AC_MAX_LENGTH;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ArithmeticDecoder.prototype._next = function () {
|
|
39
|
+
if (this.p >= this.end) throw new LasError(ERROR_CODES.TRUNCATED, "compressed LAZ data ended early", this.base + this.p);
|
|
40
|
+
return this.bytes[this.p++];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Read the 4 big-endian bytes that start the stream. */
|
|
44
|
+
ArithmeticDecoder.prototype.readInitBytes = function () {
|
|
45
|
+
this.value = this._next() * 16777216 + this._next() * 65536 + this._next() * 256 + this._next();
|
|
46
|
+
this.range = AC_MAX_LENGTH;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
ArithmeticDecoder.prototype._renorm = function () {
|
|
50
|
+
do {
|
|
51
|
+
this.value = ((this.value << 8) | this._next()) >>> 0;
|
|
52
|
+
this.range = (this.range << 8) >>> 0;
|
|
53
|
+
} while (this.range < AC_MIN_LENGTH);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Decode one bit with an adaptive binary model. */
|
|
57
|
+
ArithmeticDecoder.prototype.decodeBit = function (m) {
|
|
58
|
+
var x = m.bit0Prob * (this.range >>> 13); // BM_LENGTH_SHIFT
|
|
59
|
+
var sym;
|
|
60
|
+
if (this.value < x) {
|
|
61
|
+
sym = 0;
|
|
62
|
+
this.range = x;
|
|
63
|
+
m.bit0Count++;
|
|
64
|
+
} else {
|
|
65
|
+
sym = 1;
|
|
66
|
+
this.value -= x;
|
|
67
|
+
this.range -= x;
|
|
68
|
+
}
|
|
69
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
70
|
+
if (--m.bitsUntilUpdate === 0) m.update();
|
|
71
|
+
return sym;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Decode one symbol with an adaptive multi-symbol model. */
|
|
75
|
+
ArithmeticDecoder.prototype.decodeSymbol = function (m) {
|
|
76
|
+
var dist = m.distribution, sym, n, x, k, y = this.range;
|
|
77
|
+
if (m.decoderTable) {
|
|
78
|
+
this.range = this.range >>> 15; // DM_LENGTH_SHIFT
|
|
79
|
+
if (this.range === 0) throw new LasError(ERROR_CODES.TRUNCATED, "corrupt LAZ stream (zero interval)", this.base + this.p);
|
|
80
|
+
var dv = Math.floor(this.value / this.range);
|
|
81
|
+
var t = dv >>> m.tableShift;
|
|
82
|
+
sym = m.decoderTable[t];
|
|
83
|
+
n = m.decoderTable[t + 1] + 1;
|
|
84
|
+
while (n > sym + 1) {
|
|
85
|
+
k = (sym + n) >>> 1;
|
|
86
|
+
if (dist[k] > dv) n = k; else sym = k;
|
|
87
|
+
}
|
|
88
|
+
x = dist[sym] * this.range;
|
|
89
|
+
if (sym !== m.lastSymbol) y = dist[sym + 1] * this.range;
|
|
90
|
+
} else {
|
|
91
|
+
x = 0;
|
|
92
|
+
sym = 0;
|
|
93
|
+
this.range = this.range >>> 15;
|
|
94
|
+
n = m.symbols;
|
|
95
|
+
k = n >>> 1;
|
|
96
|
+
do {
|
|
97
|
+
var z = this.range * dist[k];
|
|
98
|
+
if (z > this.value) { n = k; y = z; } else { sym = k; x = z; }
|
|
99
|
+
k = (sym + n) >>> 1;
|
|
100
|
+
} while (k !== sym);
|
|
101
|
+
}
|
|
102
|
+
this.value -= x;
|
|
103
|
+
this.range = y - x;
|
|
104
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
105
|
+
m.symbolCount[sym]++;
|
|
106
|
+
if (--m.symbolsUntilUpdate === 0) m.update();
|
|
107
|
+
return sym;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** `bits` raw bits (1-32), as an unsigned Number. */
|
|
111
|
+
ArithmeticDecoder.prototype.readBits = function (bits) {
|
|
112
|
+
if (bits > 19) {
|
|
113
|
+
var low = this.readShort();
|
|
114
|
+
return this.readBits(bits - 16) * 65536 + low;
|
|
115
|
+
}
|
|
116
|
+
this.range = this.range >>> bits;
|
|
117
|
+
var sym = Math.floor(this.value / this.range);
|
|
118
|
+
this.value -= this.range * sym;
|
|
119
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
120
|
+
return sym;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** 16 raw bits. */
|
|
124
|
+
ArithmeticDecoder.prototype.readShort = function () {
|
|
125
|
+
this.range = this.range >>> 16;
|
|
126
|
+
var sym = Math.floor(this.value / this.range);
|
|
127
|
+
this.value -= this.range * sym;
|
|
128
|
+
if (this.range < AC_MIN_LENGTH) this._renorm();
|
|
129
|
+
return sym;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** 32 raw bits (low 16 first), as an unsigned Number. */
|
|
133
|
+
ArithmeticDecoder.prototype.readInt = function () {
|
|
134
|
+
var low = this.readShort();
|
|
135
|
+
return this.readShort() * 65536 + low;
|
|
136
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/laz/IntegerDecompressor.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// LASzip's integer decompressor: an integer is coded as a correction
|
|
4
|
+
// to a prediction — first k, the bit length of the correction (one
|
|
5
|
+
// model per context), then the correction's bits (modelled up to
|
|
6
|
+
// bitsHigh bits, raw above). The result wraps into the integer's
|
|
7
|
+
// range like the encoder's did (i32 wrap for 32-bit integers).
|
|
8
|
+
//
|
|
9
|
+
// Ported from laz-rs src/decompressors.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
|
+
//==========================================================
|
|
15
|
+
|
|
16
|
+
import { ArithmeticModel, ArithmeticBitModel } from "./models.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {number} [bits] - integer width (default 16; 32 wraps as i32)
|
|
20
|
+
* @param {number} [contexts] - number of k models (default 1)
|
|
21
|
+
* @param {number} [bitsHigh] - correction bits that are modelled (default 8)
|
|
22
|
+
* (laz-rs also takes an explicit range; no LAZ item uses one, so it is left out)
|
|
23
|
+
*/
|
|
24
|
+
export function IntegerDecompressor(bits, contexts, bitsHigh) {
|
|
25
|
+
bits = bits === undefined ? 16 : bits;
|
|
26
|
+
this.contexts = contexts === undefined ? 1 : contexts;
|
|
27
|
+
this.bitsHigh = bitsHigh === undefined ? 8 : bitsHigh;
|
|
28
|
+
if (bits && bits < 32) {
|
|
29
|
+
this.corrBits = bits;
|
|
30
|
+
this.corrRange = Math.pow(2, bits);
|
|
31
|
+
this.corrMin = -Math.floor(this.corrRange / 2);
|
|
32
|
+
} else {
|
|
33
|
+
this.corrBits = 32;
|
|
34
|
+
this.corrRange = 0;
|
|
35
|
+
this.corrMin = -2147483648;
|
|
36
|
+
}
|
|
37
|
+
// laz-rs compares and adds `corr_range as i32`
|
|
38
|
+
this.corrRangeI32 = this.corrRange > 2147483647 ? this.corrRange - 4294967296 : this.corrRange;
|
|
39
|
+
this.k = 0;
|
|
40
|
+
this.mBits = [];
|
|
41
|
+
for (var c = 0; c < this.contexts; c++) this.mBits.push(new ArithmeticModel(this.corrBits + 1));
|
|
42
|
+
this.mCorrector0 = new ArithmeticBitModel();
|
|
43
|
+
this.mCorrector = [];
|
|
44
|
+
for (var i = 1; i <= this.corrBits; i++) this.mCorrector.push(new ArithmeticModel(Math.pow(2, i <= this.bitsHigh ? i : this.bitsHigh)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import("./ArithmeticDecoder.js").ArithmeticDecoder} dec
|
|
49
|
+
* @param {number} pred - the prediction (i32)
|
|
50
|
+
* @param {number} context
|
|
51
|
+
* @returns {number} the decoded integer (i32; within [0, range) for narrower integers)
|
|
52
|
+
*/
|
|
53
|
+
IntegerDecompressor.prototype.decompress = function (dec, pred, context) {
|
|
54
|
+
var k = dec.decodeSymbol(this.mBits[context]);
|
|
55
|
+
var c;
|
|
56
|
+
this.k = k;
|
|
57
|
+
if (k !== 0) {
|
|
58
|
+
if (k < 32) {
|
|
59
|
+
if (k <= this.bitsHigh) {
|
|
60
|
+
c = dec.decodeSymbol(this.mCorrector[k - 1]);
|
|
61
|
+
} else {
|
|
62
|
+
var k1 = k - this.bitsHigh;
|
|
63
|
+
c = dec.decodeSymbol(this.mCorrector[k - 1]) * Math.pow(2, k1) + dec.readBits(k1);
|
|
64
|
+
}
|
|
65
|
+
if (c >= Math.pow(2, k - 1)) c += 1;
|
|
66
|
+
else c -= Math.pow(2, k) - 1;
|
|
67
|
+
} else {
|
|
68
|
+
c = this.corrMin;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
c = dec.decodeBit(this.mCorrector0);
|
|
72
|
+
}
|
|
73
|
+
var real = (pred + c) | 0; // i32 wrapping_add
|
|
74
|
+
if (real < 0) real = (real + this.corrRangeI32) | 0;
|
|
75
|
+
else if (real >= this.corrRangeI32) real = (real - this.corrRangeI32) | 0;
|
|
76
|
+
return real;
|
|
77
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/laz/LazVlr.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// The LASzip VLR ("laszip encoded", record 22204): how the points are
|
|
4
|
+
// compressed — the compressor (point-wise, point-wise chunked, layered
|
|
5
|
+
// chunked), the chunk size (0 / 0xFFFFFFFF = variable, the table then
|
|
6
|
+
// carries each chunk's point count) and the items that make up one
|
|
7
|
+
// record (Point10, GpsTime, RGB12, Byte … or the 1.4 Point14 family),
|
|
8
|
+
// each with its size and coder version.
|
|
9
|
+
//
|
|
10
|
+
// Layout from laz-rs src/laszip/vlr.rs (https://github.com/laz-rs/laz-rs),
|
|
11
|
+
// itself from LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
|
|
12
|
+
// rapidlasso; (c) 2019-2023 Thomas Montaigu.
|
|
13
|
+
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
14
|
+
// NOTICE). Changes: written
|
|
15
|
+
// in JavaScript from that layout, Brent Buffham 2026.
|
|
16
|
+
//==========================================================
|
|
17
|
+
|
|
18
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
19
|
+
|
|
20
|
+
/** LASzip compressor kinds. */
|
|
21
|
+
export var COMPRESSOR = Object.freeze({ NONE: 0, POINTWISE: 1, POINTWISE_CHUNKED: 2, LAYERED_CHUNKED: 3 });
|
|
22
|
+
|
|
23
|
+
/** LASzip item types. */
|
|
24
|
+
export var ITEM = Object.freeze({ BYTE: 0, POINT10: 6, GPSTIME11: 7, RGB12: 8, WAVEPACKET13: 9, POINT14: 10, RGB14: 11, RGBNIR14: 12, WAVEPACKET14: 13, BYTE14: 14 });
|
|
25
|
+
|
|
26
|
+
/** User id and record id of the LASzip VLR. */
|
|
27
|
+
export var LASZIP_USER_ID = "laszip encoded";
|
|
28
|
+
export var LASZIP_RECORD_ID = 22204;
|
|
29
|
+
|
|
30
|
+
var VARIABLE = 0xFFFFFFFF;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {Uint8Array} bytes - the VLR's payload
|
|
34
|
+
* @returns {{compressor:number, coder:number, version:{major:number, minor:number, revision:number},
|
|
35
|
+
* options:number, chunkSize:number, variableChunks:boolean, items:Array<{type:number, size:number, version:number}>}}
|
|
36
|
+
*/
|
|
37
|
+
export function parseLazVlr(bytes) {
|
|
38
|
+
if (!bytes || bytes.length < 34) throw new LasError(ERROR_CODES.BAD_VLR, "the LASzip VLR is " + (bytes ? bytes.length : 0) + " bytes (needs 34 or more)");
|
|
39
|
+
var v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
40
|
+
var n = v.getUint16(32, true);
|
|
41
|
+
if (bytes.length < 34 + n * 6) throw new LasError(ERROR_CODES.BAD_VLR, "the LASzip VLR lists " + n + " items but holds only " + bytes.length + " bytes");
|
|
42
|
+
var chunk = v.getUint32(12, true);
|
|
43
|
+
var items = [];
|
|
44
|
+
for (var i = 0; i < n; i++) items.push({ type: v.getUint16(34 + i * 6, true), size: v.getUint16(36 + i * 6, true), version: v.getUint16(38 + i * 6, true) });
|
|
45
|
+
return {
|
|
46
|
+
compressor: v.getUint16(0, true),
|
|
47
|
+
coder: v.getUint16(2, true),
|
|
48
|
+
version: { major: v.getUint8(4), minor: v.getUint8(5), revision: v.getUint16(6, true) },
|
|
49
|
+
options: v.getUint32(8, true),
|
|
50
|
+
chunkSize: chunk === 0 ? VARIABLE : chunk,
|
|
51
|
+
variableChunks: chunk === 0 || chunk === VARIABLE,
|
|
52
|
+
items: items
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The LASzip VLR among a file's VLRs, or null.
|
|
58
|
+
* @param {Array<{userId:string, recordId:number, bytes:Uint8Array}>} vlrs
|
|
59
|
+
*/
|
|
60
|
+
export function findLazVlr(vlrs) {
|
|
61
|
+
for (var i = 0; i < vlrs.length; i++) {
|
|
62
|
+
if (vlrs[i].recordId === LASZIP_RECORD_ID && vlrs[i].userId === LASZIP_USER_ID) return parseLazVlr(vlrs[i].bytes);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|