las-laz-js 0.3.0 → 0.4.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 +46 -0
- package/NOTICE +1 -1
- package/README.md +56 -14
- package/package.json +2 -2
- package/src/index.js +1 -1
- package/src/laz/ArithmeticEncoder.js +123 -0
- package/src/laz/IntegerCompressor.js +76 -0
- package/src/laz/LazVlr.js +63 -1
- package/src/laz/chunkTable.js +139 -109
- package/src/laz/chunkWorker.js +51 -18
- package/src/laz/compressChunk.js +107 -0
- package/src/laz/decompressChunk.js +40 -10
- package/src/laz/pool.js +77 -34
- package/src/laz/v2/byte.js +23 -7
- package/src/laz/v2/gpstime11.js +165 -25
- package/src/laz/v2/point10.js +91 -22
- package/src/laz/v2/rgb12.js +62 -7
- package/src/laz/v3/layered.js +138 -40
- package/src/laz/v3/point14.js +231 -33
- package/src/write/encodeRecords.js +3 -2
- package/src/write/writeLas.js +259 -6
package/src/laz/pool.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// src/laz/pool.js
|
|
2
2
|
//==========================================================
|
|
3
|
-
// A pool of workers that decode LAZ chunks in parallel
|
|
4
|
-
// independent, so N cores
|
|
5
|
-
// Workers or Node worker_threads, running
|
|
6
|
-
// queue and go to the next free worker;
|
|
7
|
-
// buffers
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// A pool of workers that decode and compress LAZ chunks in parallel
|
|
4
|
+
// (chunks are independent, so N cores do about N times the work).
|
|
5
|
+
// Browser module Workers or Node worker_threads, running
|
|
6
|
+
// src/laz/chunkWorker.js. Tasks queue and go to the next free worker;
|
|
7
|
+
// their buffers go there, and the results come back, transferred.
|
|
8
|
+
// close() terminates every worker and fails what is still queued, so
|
|
9
|
+
// nothing outlives a read or a write. When workers cannot start (no
|
|
10
|
+
// Worker, a blocked or failing script), createLazPool returns null and
|
|
11
|
+
// the reader or writer works on its own thread.
|
|
11
12
|
//
|
|
12
13
|
// Uses the chunk decoder translated from laz-rs (https://github.com/laz-rs/laz-rs),
|
|
13
14
|
// itself a port of LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
|
|
@@ -23,7 +24,7 @@ var NODE_OS = ["node", "os"].join(":");
|
|
|
23
24
|
var MAX_THREADS = 16;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* How many
|
|
27
|
+
* How many LAZ workers to use by default: one fewer than the cores, at most 16.
|
|
27
28
|
* @returns {Promise<number>}
|
|
28
29
|
*/
|
|
29
30
|
export async function defaultThreads() {
|
|
@@ -38,33 +39,57 @@ export async function defaultThreads() {
|
|
|
38
39
|
return Math.max(1, Math.min(MAX_THREADS, cores - 1));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
//
|
|
42
|
-
|
|
42
|
+
// Node worker options: the parent's flags are inherited, except --input-type
|
|
43
|
+
// (node --input-type=module -e passes it on, and a worker has no string input
|
|
44
|
+
// for it). Only then is execArgv given: an explicit one is checked flag by
|
|
45
|
+
// flag, and some parents (the test runner) hold flags a worker may not take.
|
|
46
|
+
function nodeWorkerOptions(argv) {
|
|
47
|
+
var isInputType = function (a, i) { return /^--input-type(=|$)/.test(a) || argv[i - 1] === "--input-type"; };
|
|
48
|
+
if (!argv.some(isInputType)) return undefined;
|
|
49
|
+
return { execArgv: argv.filter(function (a, i) { return !isInputType(a, i); }) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// One worker, browser or Node, behind the same small interface. Resolves once
|
|
53
|
+
// the worker says it is ready; rejects if it fails or exits first (a blocked
|
|
54
|
+
// or broken script), so a pool that cannot start is known before any task.
|
|
55
|
+
async function spawn(onMessage, onFail, nodeUrl) {
|
|
56
|
+
var started = false, ready, failed;
|
|
57
|
+
var up = new Promise(function (res, rej) { ready = res; failed = rej; });
|
|
58
|
+
function message(m) { if (m && m.ready) { started = true; ready(); } else onMessage(m); }
|
|
59
|
+
function error(e) { if (started) onFail(e); else failed(e); }
|
|
60
|
+
var handle;
|
|
43
61
|
if (typeof Worker === "function") {
|
|
44
62
|
// written inline so bundlers (Vite, webpack 5) see the worker and emit it
|
|
45
63
|
var w = new Worker(new URL("./chunkWorker.js", import.meta.url), { type: "module" });
|
|
46
|
-
w.onmessage = function (e) {
|
|
47
|
-
w.onerror = function (e) { if (e && e.preventDefault) e.preventDefault();
|
|
48
|
-
|
|
64
|
+
w.onmessage = function (e) { message(e.data); };
|
|
65
|
+
w.onerror = function (e) { if (e && e.preventDefault) e.preventDefault(); error(new Error(e && e.message ? e.message : "LAZ worker failed")); };
|
|
66
|
+
handle = { post: function (m, t) { w.postMessage(m, t); }, close: function () { w.terminate(); } };
|
|
67
|
+
} else {
|
|
68
|
+
var wt = await import(/* webpackIgnore: true */ /* @vite-ignore */ NODE_WORKER_THREADS);
|
|
69
|
+
var nw = new wt.Worker(nodeUrl || new URL("./chunkWorker.js", import.meta.url), nodeWorkerOptions(process.execArgv));
|
|
70
|
+
nw.on("message", message);
|
|
71
|
+
nw.on("error", error);
|
|
72
|
+
nw.on("exit", function (code) { if (!started) failed(new Error("LAZ worker exited (" + code + ") before it started")); });
|
|
73
|
+
// Node runs while a worker has a task and not while it is idle, so a forgotten
|
|
74
|
+
// pool never keeps it running (unref after the "message" listener, which refs again)
|
|
75
|
+
handle = { post: function (m, t) { nw.postMessage(m, t); }, close: function () { return nw.terminate(); }, hold: function () { nw.ref(); }, release: function () { nw.unref(); } };
|
|
49
76
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
nw.on("message", onMessage);
|
|
54
|
-
nw.on("error", onFail);
|
|
55
|
-
return { post: function (m, t) { nw.postMessage(m, t); }, close: function () { return nw.terminate(); } };
|
|
77
|
+
try { await up; } catch (e) { handle.close(); throw e; }
|
|
78
|
+
if (handle.release) handle.release();
|
|
79
|
+
return handle;
|
|
56
80
|
}
|
|
57
81
|
|
|
58
82
|
/**
|
|
59
83
|
* @param {number} n - workers to start (2 or more)
|
|
60
|
-
* @
|
|
84
|
+
* @param {URL} [nodeUrl] - a Node worker entry in place of chunkWorker.js (tests)
|
|
85
|
+
* @returns {Promise<{size:number, decode:function(object): Promise<Uint8Array>, compress:function(object): Promise<{bytes:Uint8Array, sizes:number[]}>, close:function(): void}|null>}
|
|
61
86
|
*/
|
|
62
|
-
export async function createLazPool(n) {
|
|
87
|
+
export async function createLazPool(n, nodeUrl) {
|
|
63
88
|
var workers = [], idle = [], queue = [], waiting = new Map(), nextId = 1, closed = false;
|
|
64
89
|
|
|
65
90
|
function fail(err) {
|
|
66
91
|
// a worker that dies takes the pool with it: every waiting task fails
|
|
67
|
-
var e = err instanceof LasError ? err : new LasError(ERROR_CODES.UNSUPPORTED, "a LAZ
|
|
92
|
+
var e = err instanceof LasError ? err : new LasError(ERROR_CODES.UNSUPPORTED, "a LAZ worker failed: " + (err && err.message ? err.message : String(err)));
|
|
68
93
|
waiting.forEach(function (job) { job.reject(e); });
|
|
69
94
|
waiting.clear();
|
|
70
95
|
queue.forEach(function (job) { job.reject(e); });
|
|
@@ -76,7 +101,8 @@ export async function createLazPool(n) {
|
|
|
76
101
|
var w = idle.pop(), job = queue.shift();
|
|
77
102
|
job.worker = w;
|
|
78
103
|
waiting.set(job.msg.id, job);
|
|
79
|
-
w.
|
|
104
|
+
if (w.hold) w.hold();
|
|
105
|
+
w.post(job.msg, job.transfer);
|
|
80
106
|
}
|
|
81
107
|
}
|
|
82
108
|
|
|
@@ -84,20 +110,32 @@ export async function createLazPool(n) {
|
|
|
84
110
|
var job = waiting.get(m.id);
|
|
85
111
|
if (!job) return;
|
|
86
112
|
waiting.delete(m.id);
|
|
113
|
+
if (job.worker.release) job.worker.release();
|
|
87
114
|
idle.push(job.worker);
|
|
88
115
|
if (m.error) job.reject(new LasError(m.error.code || ERROR_CODES.UNSUPPORTED, m.error.message, m.error.offset));
|
|
89
|
-
else job.resolve(
|
|
116
|
+
else job.resolve(job.result(m));
|
|
90
117
|
pump();
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
var starts = [];
|
|
121
|
+
for (var i = 0; i < n; i++) starts.push(spawn(onMessage, fail, nodeUrl));
|
|
122
|
+
var settled = await Promise.allSettled(starts);
|
|
123
|
+
workers = settled.filter(function (s) { return s.status === "fulfilled"; }).map(function (s) { return s.value; });
|
|
124
|
+
if (workers.length < n) {
|
|
125
|
+
// any worker that cannot start means the reader decodes on its own thread
|
|
96
126
|
workers.forEach(function (w) { w.close(); });
|
|
97
127
|
return null;
|
|
98
128
|
}
|
|
99
129
|
idle = workers.slice();
|
|
100
130
|
|
|
131
|
+
function run(msg, transfer, result) {
|
|
132
|
+
if (closed) return Promise.reject(new LasError(ERROR_CODES.ABORTED, "the LAZ pool is closed"));
|
|
133
|
+
return new Promise(function (resolve, reject) {
|
|
134
|
+
queue.push({ msg: Object.assign({ id: nextId++ }, msg), transfer: transfer, result: result, resolve: resolve, reject: reject });
|
|
135
|
+
pump();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
return {
|
|
102
140
|
size: n,
|
|
103
141
|
/**
|
|
@@ -106,16 +144,21 @@ export async function createLazPool(n) {
|
|
|
106
144
|
* @returns {Promise<Uint8Array>} the chunks' raw LAS records, in order
|
|
107
145
|
*/
|
|
108
146
|
decode: function (task) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
147
|
+
return run(task, [task.packed], function (m) { return new Uint8Array(m.records); });
|
|
148
|
+
},
|
|
149
|
+
/**
|
|
150
|
+
* Compress whole chunks from their own copy of the raw records (transferred).
|
|
151
|
+
* @param {{vlr:object, recordLength:number, records:ArrayBuffer, counts:number[]}} task
|
|
152
|
+
* counts: each chunk's points, the chunks back to back from the start of records
|
|
153
|
+
* @returns {Promise<{bytes:Uint8Array, sizes:number[]}>} the chunks, back to back, and each one's length
|
|
154
|
+
*/
|
|
155
|
+
compress: function (task) {
|
|
156
|
+
return run(Object.assign({ op: "compress" }, task), [task.records], function (m) { return { bytes: new Uint8Array(m.chunks), sizes: m.sizes }; });
|
|
114
157
|
},
|
|
115
158
|
close: function () {
|
|
116
159
|
if (closed) return;
|
|
117
160
|
closed = true;
|
|
118
|
-
fail(new LasError(ERROR_CODES.ABORTED, "the
|
|
161
|
+
fail(new LasError(ERROR_CODES.ABORTED, "the LAZ pool was closed"));
|
|
119
162
|
workers.forEach(function (w) { w.close(); });
|
|
120
163
|
}
|
|
121
164
|
};
|
package/src/laz/v2/byte.js
CHANGED
|
@@ -8,19 +8,23 @@
|
|
|
8
8
|
// itself a port of LASzip by rapidlasso. (c) 2007-2014 martin isenburg,
|
|
9
9
|
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
10
10
|
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
11
|
-
// NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026
|
|
11
|
+
// NOTICE). Changes: translated from Rust to JavaScript, Brent Buffham 2026;
|
|
12
|
+
// the compressor (ByteCompressor) added beside the decompressor.
|
|
12
13
|
//==========================================================
|
|
13
14
|
|
|
14
15
|
import { ArithmeticModel } from "../models.js";
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
for (var i = 0; i < count; i++)
|
|
17
|
+
// The state and models both halves start a chunk with: the last bytes and one model per byte
|
|
18
|
+
function initByte(self, count) {
|
|
19
|
+
self.size = count;
|
|
20
|
+
self.last = new Uint8Array(count);
|
|
21
|
+
self.models = [];
|
|
22
|
+
for (var i = 0; i < count; i++) self.models.push(new ArithmeticModel(256));
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** Byte v2 decompressor for `count` extra bytes: one per chunk. */
|
|
26
|
+
export function ByteDecompressor(count) { initByte(this, count); }
|
|
27
|
+
|
|
24
28
|
ByteDecompressor.prototype.first = function (src, s, view, o) {
|
|
25
29
|
for (var i = 0; i < this.size; i++) { this.last[i] = src[s + i]; view.setUint8(o + i, src[s + i]); }
|
|
26
30
|
};
|
|
@@ -31,3 +35,15 @@ ByteDecompressor.prototype.next = function (dec, view, o) {
|
|
|
31
35
|
view.setUint8(o + i, this.last[i]);
|
|
32
36
|
}
|
|
33
37
|
};
|
|
38
|
+
|
|
39
|
+
/** Byte v2 compressor for `count` extra bytes: one per chunk, the mirror of ByteDecompressor. */
|
|
40
|
+
export function ByteCompressor(count) { initByte(this, count); }
|
|
41
|
+
|
|
42
|
+
ByteCompressor.prototype.first = function (src, s) { this.last.set(src.subarray(s, s + this.size)); };
|
|
43
|
+
|
|
44
|
+
ByteCompressor.prototype.next = function (enc, src, s) {
|
|
45
|
+
for (var i = 0; i < this.size; i++) {
|
|
46
|
+
enc.encodeSymbol(this.models[i], (src[s + i] - this.last[i]) & 255); // u8 wrapping_sub
|
|
47
|
+
this.last[i] = src[s + i];
|
|
48
|
+
}
|
|
49
|
+
};
|
package/src/laz/v2/gpstime11.js
CHANGED
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
13
13
|
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
14
14
|
// NOTICE). Changes: translated from Rust to JavaScript, i64 as two u32
|
|
15
|
-
// halves, Brent Buffham 2026
|
|
15
|
+
// halves, Brent Buffham 2026; the compressor (GpsTime11Compressor) added
|
|
16
|
+
// beside the decompressor, rounding its multiplier as LAZperf does; its
|
|
17
|
+
// multiplier and 64-bit difference shared with the Point14 compressor.
|
|
16
18
|
//==========================================================
|
|
17
19
|
|
|
18
20
|
import { ArithmeticModel } from "../models.js";
|
|
19
21
|
import { IntegerDecompressor } from "../IntegerDecompressor.js";
|
|
22
|
+
import { IntegerCompressor } from "../IntegerCompressor.js";
|
|
20
23
|
|
|
21
24
|
var LE = true;
|
|
22
25
|
var MULTI = 500, MULTI_MINUS = -10;
|
|
@@ -24,18 +27,39 @@ var MULTI_UNCHANGED = MULTI - MULTI_MINUS + 1; // 511
|
|
|
24
27
|
var MULTI_CODE_FULL = MULTI - MULTI_MINUS + 2; // 512
|
|
25
28
|
var MULTI_TOTAL = MULTI - MULTI_MINUS + 6; // 516
|
|
26
29
|
|
|
30
|
+
// The state and models both halves start a chunk with; IC is the integer coder
|
|
31
|
+
// (IntegerCompressor or IntegerDecompressor)
|
|
32
|
+
function initGps(self, IC) {
|
|
33
|
+
self.size = 8;
|
|
34
|
+
self.multiModel = new ArithmeticModel(MULTI_TOTAL);
|
|
35
|
+
self.zeroDiffModel = new ArithmeticModel(6);
|
|
36
|
+
self.ic = new IC(32, 9);
|
|
37
|
+
self.last = 0;
|
|
38
|
+
self.nextSeq = 0;
|
|
39
|
+
self.hi = new Int32Array(4);
|
|
40
|
+
self.lo = new Float64Array(4); // u32 values, held exactly
|
|
41
|
+
self.diffs = new Int32Array(4);
|
|
42
|
+
self.extremes = new Int32Array(4);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A full time (hi, lo) starts the next of the four sequences, with no difference yet
|
|
46
|
+
function startSequence(self, hi, lo) {
|
|
47
|
+
self.nextSeq = (self.nextSeq + 1) & 3;
|
|
48
|
+
self.hi[self.nextSeq] = hi;
|
|
49
|
+
self.lo[self.nextSeq] = lo;
|
|
50
|
+
self.last = self.nextSeq;
|
|
51
|
+
self.diffs[self.last] = 0;
|
|
52
|
+
self.extremes[self.last] = 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// After three extreme differences in a row, the latest becomes the new reference
|
|
56
|
+
function extreme(self, d) {
|
|
57
|
+
if (++self.extremes[self.last] > 3) { self.diffs[self.last] = d; self.extremes[self.last] = 0; }
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
/** GpsTime11 v2 decompressor: one per chunk. */
|
|
28
61
|
export function GpsTime11Decompressor() {
|
|
29
|
-
this
|
|
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);
|
|
62
|
+
initGps(this, IntegerDecompressor);
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
GpsTime11Decompressor.prototype._add = function (i, d) {
|
|
@@ -46,17 +70,8 @@ GpsTime11Decompressor.prototype._add = function (i, d) {
|
|
|
46
70
|
};
|
|
47
71
|
|
|
48
72
|
GpsTime11Decompressor.prototype._full = function (dec) {
|
|
49
|
-
|
|
50
|
-
this
|
|
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; }
|
|
73
|
+
var hi = this.ic.decompress(dec, this.hi[this.last], 8);
|
|
74
|
+
startSequence(this, hi, dec.readInt());
|
|
60
75
|
};
|
|
61
76
|
|
|
62
77
|
GpsTime11Decompressor.prototype.first = function (src, s, view, o) {
|
|
@@ -88,19 +103,19 @@ GpsTime11Decompressor.prototype.next = function (dec, view, o) {
|
|
|
88
103
|
} else if (multi < MULTI_UNCHANGED) {
|
|
89
104
|
if (multi === 0) {
|
|
90
105
|
d = this.ic.decompress(dec, 0, 7);
|
|
91
|
-
this
|
|
106
|
+
extreme(this, d);
|
|
92
107
|
} else if (multi < MULTI) {
|
|
93
108
|
d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), multi < 10 ? 2 : 3);
|
|
94
109
|
} else if (multi === MULTI) {
|
|
95
110
|
d = this.ic.decompress(dec, Math.imul(MULTI, this.diffs[L]), 4);
|
|
96
|
-
this
|
|
111
|
+
extreme(this, d);
|
|
97
112
|
} else {
|
|
98
113
|
multi = MULTI - multi;
|
|
99
114
|
if (multi > MULTI_MINUS) {
|
|
100
115
|
d = this.ic.decompress(dec, Math.imul(multi, this.diffs[L]), 5);
|
|
101
116
|
} else {
|
|
102
117
|
d = this.ic.decompress(dec, Math.imul(MULTI_MINUS, this.diffs[L]), 6);
|
|
103
|
-
this
|
|
118
|
+
extreme(this, d);
|
|
104
119
|
}
|
|
105
120
|
}
|
|
106
121
|
this._add(L, d);
|
|
@@ -116,3 +131,128 @@ GpsTime11Decompressor.prototype.next = function (dec, view, o) {
|
|
|
116
131
|
view.setUint32(o, this.lo[this.last], LE);
|
|
117
132
|
view.setInt32(o + 4, this.hi[this.last], LE);
|
|
118
133
|
};
|
|
134
|
+
|
|
135
|
+
// The multiplier between this and the last difference, as LAZperf rounds it:
|
|
136
|
+
// std::round of the f32 quotient (half away from zero), then (int). LASzip and
|
|
137
|
+
// laz-rs add 0.5f in f32 instead, which differs only within an f32 step of a
|
|
138
|
+
// half; the fixtures' oracle is LAZperf. The one quotient past i32 (2^31, a last
|
|
139
|
+
// difference of +-1) converts as x86 does, to -2^31. Either choice decodes the same.
|
|
140
|
+
export function quantizeMulti(curr, last) {
|
|
141
|
+
var q = Math.fround(Math.fround(curr) / Math.fround(last));
|
|
142
|
+
var m = q >= 0 ? Math.floor(q + 0.5) : -Math.floor(0.5 - q);
|
|
143
|
+
return m > 2147483647 ? -2147483648 : m;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A DataView over the caller's records, made once per buffer
|
|
147
|
+
function viewOf(item, src) {
|
|
148
|
+
if (item._src !== src) { item._src = src; item._view = new DataView(src.buffer, src.byteOffset, src.byteLength); }
|
|
149
|
+
return item._view;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** GpsTime11 v2 compressor: one per chunk, the mirror of GpsTime11Decompressor. */
|
|
153
|
+
export function GpsTime11Compressor() {
|
|
154
|
+
initGps(this, IntegerCompressor);
|
|
155
|
+
this._src = null;
|
|
156
|
+
this._view = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
// (hi, lo) - (hi0, lo0) as an i32, or null when the 64-bit difference does not fit
|
|
161
|
+
// (hi signed, lo unsigned; shared with the Point14 compressor's GPS time)
|
|
162
|
+
export function diff32(hi, lo, hi0, lo0) {
|
|
163
|
+
var dl = lo - lo0, dh = hi - hi0;
|
|
164
|
+
if (dl < 0) { dl += 4294967296; dh -= 1; }
|
|
165
|
+
dh = dh | 0; // the high word of the i64 wrapping_sub
|
|
166
|
+
if (dh === 0 && dl < 2147483648) return dl;
|
|
167
|
+
if (dh === -1 && dl >= 2147483648) return dl - 4294967296;
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// (hi, lo) minus sequence i as an i32, or null when the 64-bit difference does not fit
|
|
172
|
+
GpsTime11Compressor.prototype._diff = function (i, hi, lo) { return diff32(hi, lo, this.hi[i], this.lo[i]); };
|
|
173
|
+
|
|
174
|
+
// The first other sequence (1-3 on from the last) this time is within i32 of, or 0
|
|
175
|
+
GpsTime11Compressor.prototype._other = function (hi, lo) {
|
|
176
|
+
for (var i = 1; i < 4; i++) if (this._diff((this.last + i) & 3, hi, lo) !== null) return i;
|
|
177
|
+
return 0;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
GpsTime11Compressor.prototype._full = function (enc, hi, lo) {
|
|
181
|
+
this.ic.compress(enc, this.hi[this.last], hi, 8);
|
|
182
|
+
enc.writeInt(lo);
|
|
183
|
+
startSequence(this, hi, lo);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
GpsTime11Compressor.prototype.first = function (src, s) {
|
|
187
|
+
var v = viewOf(this, src);
|
|
188
|
+
this.lo[0] = v.getUint32(s, LE);
|
|
189
|
+
this.hi[0] = v.getInt32(s + 4, LE);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
GpsTime11Compressor.prototype.next = function (enc, src, s) {
|
|
193
|
+
var v = viewOf(this, src);
|
|
194
|
+
var lo = v.getUint32(s, LE), hi = v.getInt32(s + 4, LE);
|
|
195
|
+
for (;;) {
|
|
196
|
+
var L = this.last, d, j, multi;
|
|
197
|
+
if (hi === this.hi[L] && lo === this.lo[L]) {
|
|
198
|
+
if (this.diffs[L] === 0) enc.encodeSymbol(this.zeroDiffModel, 0);
|
|
199
|
+
else enc.encodeSymbol(this.multiModel, MULTI_UNCHANGED);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
d = this._diff(L, hi, lo);
|
|
203
|
+
if (this.diffs[L] === 0) {
|
|
204
|
+
if (d !== null) {
|
|
205
|
+
enc.encodeSymbol(this.zeroDiffModel, 1);
|
|
206
|
+
this.ic.compress(enc, 0, d, 0);
|
|
207
|
+
this.diffs[L] = d;
|
|
208
|
+
this.extremes[L] = 0;
|
|
209
|
+
} else if ((j = this._other(hi, lo))) {
|
|
210
|
+
enc.encodeSymbol(this.zeroDiffModel, j + 2);
|
|
211
|
+
this.last = (L + j) & 3;
|
|
212
|
+
continue; // laz-rs recurses: code again against the new sequence
|
|
213
|
+
} else {
|
|
214
|
+
enc.encodeSymbol(this.zeroDiffModel, 2);
|
|
215
|
+
this._full(enc, hi, lo);
|
|
216
|
+
}
|
|
217
|
+
} else if (d !== null) {
|
|
218
|
+
multi = quantizeMulti(d, this.diffs[L]);
|
|
219
|
+
if (multi === 1) {
|
|
220
|
+
enc.encodeSymbol(this.multiModel, 1);
|
|
221
|
+
this.ic.compress(enc, this.diffs[L], d, 1);
|
|
222
|
+
this.extremes[L] = 0;
|
|
223
|
+
} else if (multi > 0) {
|
|
224
|
+
if (multi < MULTI) {
|
|
225
|
+
enc.encodeSymbol(this.multiModel, multi);
|
|
226
|
+
this.ic.compress(enc, Math.imul(multi, this.diffs[L]), d, multi < 10 ? 2 : 3);
|
|
227
|
+
} else {
|
|
228
|
+
enc.encodeSymbol(this.multiModel, MULTI);
|
|
229
|
+
this.ic.compress(enc, Math.imul(MULTI, this.diffs[L]), d, 4);
|
|
230
|
+
extreme(this, d);
|
|
231
|
+
}
|
|
232
|
+
} else if (multi < 0) {
|
|
233
|
+
if (multi > MULTI_MINUS) {
|
|
234
|
+
enc.encodeSymbol(this.multiModel, MULTI - multi);
|
|
235
|
+
this.ic.compress(enc, Math.imul(multi, this.diffs[L]), d, 5);
|
|
236
|
+
} else {
|
|
237
|
+
enc.encodeSymbol(this.multiModel, MULTI - MULTI_MINUS);
|
|
238
|
+
this.ic.compress(enc, Math.imul(MULTI_MINUS, this.diffs[L]), d, 6);
|
|
239
|
+
extreme(this, d);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
enc.encodeSymbol(this.multiModel, 0);
|
|
243
|
+
this.ic.compress(enc, 0, d, 7);
|
|
244
|
+
extreme(this, d);
|
|
245
|
+
}
|
|
246
|
+
} else if ((j = this._other(hi, lo))) {
|
|
247
|
+
enc.encodeSymbol(this.multiModel, MULTI_CODE_FULL + j);
|
|
248
|
+
this.last = (L + j) & 3;
|
|
249
|
+
continue;
|
|
250
|
+
} else {
|
|
251
|
+
enc.encodeSymbol(this.multiModel, MULTI_CODE_FULL);
|
|
252
|
+
this._full(enc, hi, lo);
|
|
253
|
+
}
|
|
254
|
+
this.hi[this.last] = hi;
|
|
255
|
+
this.lo[this.last] = lo;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
};
|
package/src/laz/v2/point10.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// (formats 0-5) starts with. Each point codes which attributes changed
|
|
5
5
|
// since the last one; X and Y are corrections to a streaming median
|
|
6
6
|
// of the recent differences, Z to the last height at the same return
|
|
7
|
-
// level, all through integer
|
|
8
|
-
//
|
|
7
|
+
// level, all through integer coders whose contexts follow the return
|
|
8
|
+
// structure. The compressor and decompressor below make the same model
|
|
9
|
+
// choices from the same state, so they stay in step point for point.
|
|
9
10
|
//
|
|
10
11
|
// Ported from laz-rs src/las/point0.rs, module v2, and the median and
|
|
11
12
|
// return tables of src/las/utils.rs (https://github.com/laz-rs/laz-rs),
|
|
@@ -13,11 +14,13 @@
|
|
|
13
14
|
// rapidlasso; (c) 2014 Uday Verma, Hobu, Inc.; (c) 2019 Thomas Montaigu.
|
|
14
15
|
// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE and
|
|
15
16
|
// NOTICE). Changes: translated from Rust to JavaScript, working on raw
|
|
16
|
-
// record bytes, Brent Buffham 2026
|
|
17
|
+
// record bytes, Brent Buffham 2026; the compressor (Point10Compressor)
|
|
18
|
+
// added beside the decompressor, sharing its models, median and tables.
|
|
17
19
|
//==========================================================
|
|
18
20
|
|
|
19
21
|
import { ArithmeticModel } from "../models.js";
|
|
20
22
|
import { IntegerDecompressor } from "../IntegerDecompressor.js";
|
|
23
|
+
import { IntegerCompressor } from "../IntegerCompressor.js";
|
|
21
24
|
|
|
22
25
|
var LE = true;
|
|
23
26
|
|
|
@@ -66,23 +69,34 @@ function models(n, symbols) { var out = []; for (var i = 0; i < n; i++) out.push
|
|
|
66
69
|
function lazyModel(table, i) { return table[i] || (table[i] = new ArithmeticModel(256)); }
|
|
67
70
|
function medians() { var out = []; for (var i = 0; i < 16; i++) out.push(new StreamingMedian()); return out; }
|
|
68
71
|
|
|
72
|
+
// The Y and Z coder contexts, from the bit lengths of the X (and Y) corrections just coded
|
|
73
|
+
// (Point14 uses them too)
|
|
74
|
+
export function yContext(single, icDx) { var k = icDx.k; return single + (k < 20 ? k & 0xFFFFFFFE : 20); }
|
|
75
|
+
export function zContext(single, icDx, icDy) { var k = Math.floor((icDx.k + icDy.k) / 2); return single + (k < 18 ? k & 0xFFFFFFFE : 18); }
|
|
76
|
+
|
|
77
|
+
// The state and models both halves start a chunk with; IC is the integer coder
|
|
78
|
+
// (IntegerCompressor or IntegerDecompressor), so each side gets the same widths and contexts
|
|
79
|
+
function initPoint10(self, IC) {
|
|
80
|
+
self.size = 20;
|
|
81
|
+
self.lastIntensity = new Uint16Array(16);
|
|
82
|
+
self.xMedian = medians();
|
|
83
|
+
self.yMedian = medians();
|
|
84
|
+
self.lastHeight = new Int32Array(8);
|
|
85
|
+
self.changedValues = new ArithmeticModel(64);
|
|
86
|
+
self.scanAngleRank = models(2, 256);
|
|
87
|
+
self.bitByte = new Array(256);
|
|
88
|
+
self.classification = new Array(256);
|
|
89
|
+
self.userData = new Array(256);
|
|
90
|
+
self.icIntensity = new IC(16, 4);
|
|
91
|
+
self.icPointSourceId = new IC(16, 1);
|
|
92
|
+
self.icDx = new IC(32, 2);
|
|
93
|
+
self.icDy = new IC(32, 22);
|
|
94
|
+
self.icZ = new IC(32, 20);
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
/** Point10 v2 decompressor: one per chunk. */
|
|
70
98
|
export function Point10Decompressor() {
|
|
71
|
-
this
|
|
72
|
-
this.lastIntensity = new Uint16Array(16);
|
|
73
|
-
this.xMedian = medians();
|
|
74
|
-
this.yMedian = medians();
|
|
75
|
-
this.lastHeight = new Int32Array(8);
|
|
76
|
-
this.changedValues = new ArithmeticModel(64);
|
|
77
|
-
this.scanAngleRank = models(2, 256);
|
|
78
|
-
this.bitByte = new Array(256);
|
|
79
|
-
this.classification = new Array(256);
|
|
80
|
-
this.userData = new Array(256);
|
|
81
|
-
this.icIntensity = new IntegerDecompressor(16, 4);
|
|
82
|
-
this.icPointSourceId = new IntegerDecompressor(16, 1);
|
|
83
|
-
this.icDx = new IntegerDecompressor(32, 2);
|
|
84
|
-
this.icDy = new IntegerDecompressor(32, 22);
|
|
85
|
-
this.icZ = new IntegerDecompressor(32, 20);
|
|
99
|
+
initPoint10(this, IntegerDecompressor);
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
/** The first point of a chunk is stored raw. */
|
|
@@ -125,12 +139,10 @@ Point10Decompressor.prototype.next = function (dec, view, o) {
|
|
|
125
139
|
var diff = this.icDx.decompress(dec, this.xMedian[m].get(), single);
|
|
126
140
|
this.x = (this.x + diff) | 0;
|
|
127
141
|
this.xMedian[m].add(diff);
|
|
128
|
-
|
|
129
|
-
diff = this.icDy.decompress(dec, this.yMedian[m].get(), single + (kx < 20 ? kx & 0xFFFFFFFE : 20));
|
|
142
|
+
diff = this.icDy.decompress(dec, this.yMedian[m].get(), yContext(single, this.icDx));
|
|
130
143
|
this.y = (this.y + diff) | 0;
|
|
131
144
|
this.yMedian[m].add(diff);
|
|
132
|
-
|
|
133
|
-
this.z = this.icZ.decompress(dec, this.lastHeight[l], single + (kz < 18 ? kz & 0xFFFFFFFE : 18));
|
|
145
|
+
this.z = this.icZ.decompress(dec, this.lastHeight[l], zContext(single, this.icDx, this.icDy));
|
|
134
146
|
this.lastHeight[l] = this.z;
|
|
135
147
|
view.setInt32(o, this.x, LE); view.setInt32(o + 4, this.y, LE); view.setInt32(o + 8, this.z, LE);
|
|
136
148
|
view.setUint16(o + 12, this.intensity, LE);
|
|
@@ -140,3 +152,60 @@ Point10Decompressor.prototype.next = function (dec, view, o) {
|
|
|
140
152
|
view.setUint8(o + 17, this.user);
|
|
141
153
|
view.setUint16(o + 18, this.psid, LE);
|
|
142
154
|
};
|
|
155
|
+
|
|
156
|
+
// A DataView over the caller's records, made once per buffer
|
|
157
|
+
function viewOf(item, src) {
|
|
158
|
+
if (item._src !== src) { item._src = src; item._view = new DataView(src.buffer, src.byteOffset, src.byteLength); }
|
|
159
|
+
return item._view;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Point10 v2 compressor: one per chunk, the mirror of Point10Decompressor. */
|
|
163
|
+
export function Point10Compressor() {
|
|
164
|
+
initPoint10(this, IntegerCompressor);
|
|
165
|
+
this._src = null;
|
|
166
|
+
this._view = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The first point of a chunk is stored raw; it only sets the state. */
|
|
170
|
+
Point10Compressor.prototype.first = function (src, s) {
|
|
171
|
+
var v = viewOf(this, src);
|
|
172
|
+
this.x = v.getInt32(s, LE); this.y = v.getInt32(s + 4, LE); this.z = v.getInt32(s + 8, LE);
|
|
173
|
+
this.bits = v.getUint8(s + 14);
|
|
174
|
+
this.cls = v.getUint8(s + 15);
|
|
175
|
+
this.angle = v.getInt8(s + 16);
|
|
176
|
+
this.user = v.getUint8(s + 17);
|
|
177
|
+
this.psid = v.getUint16(s + 18, LE);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
Point10Compressor.prototype.next = function (enc, src, s) {
|
|
181
|
+
var v = viewOf(this, src);
|
|
182
|
+
var x = v.getInt32(s, LE), y = v.getInt32(s + 4, LE), z = v.getInt32(s + 8, LE);
|
|
183
|
+
var intensity = v.getUint16(s + 12, LE), bits = v.getUint8(s + 14), cls = v.getUint8(s + 15);
|
|
184
|
+
var angle = v.getInt8(s + 16), user = v.getUint8(s + 17), psid = v.getUint16(s + 18, LE);
|
|
185
|
+
var r = bits & 7, n = (bits >>> 3) & 7;
|
|
186
|
+
var m = NUMBER_RETURN_MAP[n][r], l = NUMBER_RETURN_LEVEL[n][r];
|
|
187
|
+
// return number, number of returns, scan direction and edge fill the bit byte, so "any changed" is the byte changed
|
|
188
|
+
var changed = (bits !== this.bits ? 32 : 0) | (intensity !== this.lastIntensity[m] ? 16 : 0) | (cls !== this.cls ? 8 : 0) |
|
|
189
|
+
(angle !== this.angle ? 4 : 0) | (user !== this.user ? 2 : 0) | (psid !== this.psid ? 1 : 0);
|
|
190
|
+
enc.encodeSymbol(this.changedValues, changed);
|
|
191
|
+
if (changed & 32) enc.encodeSymbol(lazyModel(this.bitByte, this.bits), bits);
|
|
192
|
+
if (changed & 16) {
|
|
193
|
+
this.icIntensity.compress(enc, this.lastIntensity[m], intensity, m < 3 ? m : 3);
|
|
194
|
+
this.lastIntensity[m] = intensity;
|
|
195
|
+
}
|
|
196
|
+
if (changed & 8) enc.encodeSymbol(lazyModel(this.classification, this.cls), cls);
|
|
197
|
+
if (changed & 4) enc.encodeSymbol(this.scanAngleRank[(bits >>> 6) & 1], (angle - this.angle) & 255); // u8 wrapping_sub
|
|
198
|
+
if (changed & 2) enc.encodeSymbol(lazyModel(this.userData, this.user), user);
|
|
199
|
+
if (changed & 1) this.icPointSourceId.compress(enc, this.psid, psid, 0);
|
|
200
|
+
var single = n === 1 ? 1 : 0;
|
|
201
|
+
var diff = (x - this.x) | 0; // i32 wrapping_sub
|
|
202
|
+
this.icDx.compress(enc, this.xMedian[m].get(), diff, single);
|
|
203
|
+
this.xMedian[m].add(diff);
|
|
204
|
+
diff = (y - this.y) | 0;
|
|
205
|
+
this.icDy.compress(enc, this.yMedian[m].get(), diff, yContext(single, this.icDx));
|
|
206
|
+
this.yMedian[m].add(diff);
|
|
207
|
+
this.icZ.compress(enc, this.lastHeight[l], z, zContext(single, this.icDx, this.icDy));
|
|
208
|
+
this.lastHeight[l] = z;
|
|
209
|
+
this.x = x; this.y = y; this.z = z;
|
|
210
|
+
this.bits = bits; this.cls = cls; this.angle = angle; this.user = user; this.psid = psid;
|
|
211
|
+
};
|