las-laz-js 0.4.0 → 0.5.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 +37 -0
- package/README.md +145 -6
- package/package.json +1 -1
- package/src/LasReader.js +6 -4
- package/src/filter/dedupe.js +14 -5
- package/src/filter/filters.js +31 -10
- package/src/index.js +5 -1
- package/src/ops/apply.js +144 -0
- package/src/ops/columns.js +209 -0
- package/src/ops/interleave.js +81 -0
- package/src/ops/polygon.js +83 -0
- package/src/ops/select.js +245 -0
- package/src/ops/transform.js +189 -0
- package/src/read/decodeRecords.js +41 -13
- package/src/workers/workerProtocol.js +8 -2
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// src/ops/columns.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Point columns and selections, the two shapes every operation in
|
|
4
|
+
// src/ops takes. Columns are the object readPoints returns (or any
|
|
5
|
+
// caller's object of the same shape): { count, <name>: TypedArray }.
|
|
6
|
+
// A property is a column when it is a typed array of length
|
|
7
|
+
// count * k, k a whole number >= 1 (k = 1 for x, classification;
|
|
8
|
+
// k = 3 for an interleaved positions or rgb). A selection is a
|
|
9
|
+
// Uint32Array of point indices, strictly ascending, each < count.
|
|
10
|
+
//==========================================================
|
|
11
|
+
|
|
12
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
13
|
+
|
|
14
|
+
var MAX_INDEX = 4294967295;
|
|
15
|
+
|
|
16
|
+
function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
|
|
17
|
+
|
|
18
|
+
function isTyped(v) { return ArrayBuffer.isView(v) && !(v instanceof DataView); }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check cols.count is a whole number >= 0 and return it.
|
|
22
|
+
* @param {object} cols - point columns.
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function countOf(cols) {
|
|
26
|
+
if (cols === null || typeof cols !== "object") throw bad("columns must be an object { count, <name>: TypedArray }");
|
|
27
|
+
var n = cols.count;
|
|
28
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0) throw bad("count " + String(n) + " is not a whole number ≥ 0");
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The columns of a point-column object: own typed-array properties of
|
|
34
|
+
* length count * k. With count 0, every typed array of length 0 is a
|
|
35
|
+
* k = 1 column. Everything else (numbers, bounds, a typed array of any
|
|
36
|
+
* other length) is skipped silently.
|
|
37
|
+
* @param {object} cols
|
|
38
|
+
* @returns {Array<{name: string, k: number}>}
|
|
39
|
+
*/
|
|
40
|
+
export function columnsOf(cols) {
|
|
41
|
+
var n = countOf(cols);
|
|
42
|
+
var out = [];
|
|
43
|
+
Object.keys(cols).forEach(function (name) {
|
|
44
|
+
var a = cols[name];
|
|
45
|
+
if (!isTyped(a)) return;
|
|
46
|
+
if (n === 0) {
|
|
47
|
+
if (a.length === 0) out.push({ name: name, k: 1 });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
var k = a.length / n;
|
|
51
|
+
if (Number.isInteger(k) && k >= 1) out.push({ name: name, k: k });
|
|
52
|
+
});
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate a selection and return it as a Uint32Array. Accepts a
|
|
58
|
+
* Uint32Array or a plain Array of whole numbers; it must be strictly
|
|
59
|
+
* ascending with every index below count.
|
|
60
|
+
* @param {Uint32Array|number[]} sel
|
|
61
|
+
* @param {number} count - points in the columns (Infinity: no upper limit).
|
|
62
|
+
* @param {string} [name] - how the message names it (default "selection").
|
|
63
|
+
* @returns {Uint32Array}
|
|
64
|
+
*/
|
|
65
|
+
export function checkSelection(sel, count, name) {
|
|
66
|
+
name = name || "selection";
|
|
67
|
+
var isU32 = sel instanceof Uint32Array;
|
|
68
|
+
if (!isU32 && !Array.isArray(sel)) throw bad(name + " must be a Uint32Array or an Array of whole numbers");
|
|
69
|
+
var prev = -1;
|
|
70
|
+
for (var i = 0; i < sel.length; i++) {
|
|
71
|
+
var v = sel[i];
|
|
72
|
+
var at = name + "[" + i + "] = " + v;
|
|
73
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0) throw bad(at + " is not a whole number ≥ 0");
|
|
74
|
+
if (v > MAX_INDEX) throw bad(at + " is not below " + (MAX_INDEX + 1));
|
|
75
|
+
if (v <= prev) throw bad(at + " is not above " + name + "[" + (i - 1) + "] = " + prev);
|
|
76
|
+
if (v >= count) throw bad(at + " is not below count " + count);
|
|
77
|
+
prev = v;
|
|
78
|
+
}
|
|
79
|
+
return isU32 ? sel : Uint32Array.from(sel);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The k = 1 column called name, or an error naming the function.
|
|
84
|
+
* @param {object} cols
|
|
85
|
+
* @param {string} name
|
|
86
|
+
* @param {string} fn - the calling function, for the message.
|
|
87
|
+
* @returns {TypedArray}
|
|
88
|
+
*/
|
|
89
|
+
export function needColumn(cols, name, fn) {
|
|
90
|
+
var n = countOf(cols);
|
|
91
|
+
var a = cols[name];
|
|
92
|
+
if (!isTyped(a) || a.length !== n) throw bad(fn + " needs a " + name + " column");
|
|
93
|
+
return a;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A new typed array of the same type as a (a Node Buffer gives a Uint8Array).
|
|
98
|
+
* Internal; not exported from the package entry.
|
|
99
|
+
* @param {TypedArray} a
|
|
100
|
+
* @param {number} length
|
|
101
|
+
* @returns {TypedArray}
|
|
102
|
+
*/
|
|
103
|
+
export function sameType(a, length) {
|
|
104
|
+
var Ctor = globalThis[Object.prototype.toString.call(a).slice(8, -1)];
|
|
105
|
+
return new Ctor(length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* dst[i] = src[sel[i]] for each i (k values each; internal, not exported from the package entry).
|
|
110
|
+
* dst may be src: i <= sel[i], so ascending writes never overwrite a value still to be read.
|
|
111
|
+
* @param {TypedArray} src
|
|
112
|
+
* @param {TypedArray} dst
|
|
113
|
+
* @param {number} k
|
|
114
|
+
* @param {Uint32Array} sel - strictly ascending.
|
|
115
|
+
*/
|
|
116
|
+
export function gather(src, dst, k, sel) {
|
|
117
|
+
var n = sel.length;
|
|
118
|
+
if (k === 1) {
|
|
119
|
+
for (var i = 0; i < n; i++) dst[i] = src[sel[i]];
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (var p = 0; p < n; p++) {
|
|
123
|
+
var from = sel[p] * k;
|
|
124
|
+
var to = p * k;
|
|
125
|
+
for (var j = 0; j < k; j++) dst[to + j] = src[from + j];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Throw INVALID_ARGUMENT when two of the named columns overlap in memory (internal; not exported
|
|
131
|
+
* from the package entry). In-place compaction writes into every column's buffer, so two columns
|
|
132
|
+
* whose byte ranges overlap would corrupt each other: refuse before any write.
|
|
133
|
+
* @param {object} cols
|
|
134
|
+
* @param {Array<{name: string}>} list - the columns to check, by name.
|
|
135
|
+
*/
|
|
136
|
+
export function checkNoSharedMemory(cols, list) {
|
|
137
|
+
for (var i = 0; i < list.length; i++) {
|
|
138
|
+
var a = cols[list[i].name];
|
|
139
|
+
if (a.byteLength === 0) continue;
|
|
140
|
+
for (var j = i + 1; j < list.length; j++) {
|
|
141
|
+
var b = cols[list[j].name];
|
|
142
|
+
if (b.byteLength === 0 || a.buffer !== b.buffer) continue;
|
|
143
|
+
if (a.byteOffset < b.byteOffset + b.byteLength && b.byteOffset < a.byteOffset + a.byteLength) {
|
|
144
|
+
throw bad("inPlace: columns \"" + list[i].name + "\" and \"" + list[j].name + "\" share memory");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
var FLOATS = { Float64Array: 8, Float32Array: 4 };
|
|
151
|
+
function has(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }
|
|
152
|
+
|
|
153
|
+
var INTS = {
|
|
154
|
+
Int8Array: [1, true], Int16Array: [2, true], Int32Array: [4, true],
|
|
155
|
+
Uint8Array: [1, false], Uint8ClampedArray: [1, false], Uint16Array: [2, false], Uint32Array: [4, false]
|
|
156
|
+
};
|
|
157
|
+
var BIGS = { BigInt64Array: true, BigUint64Array: true };
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The typed-array type a merge of columns of these types is stored in
|
|
161
|
+
* (internal; not exported from the package entry). Float64 beats Float32
|
|
162
|
+
* beats any integer type, except that Float32 with a 32-bit integer type
|
|
163
|
+
* (Int32, Uint32) gives Float64, since Float32 holds integers exactly only
|
|
164
|
+
* to 2^24. Among integers the widest wins, and mixed
|
|
165
|
+
* signedness is allowed only when the widest is signed and strictly
|
|
166
|
+
* wider than every unsigned one (Uint8 + Int16 gives Int16); Int16 +
|
|
167
|
+
* Uint16 and Int8 + Uint16 throw, because the unsigned values would wrap.
|
|
168
|
+
* Uint8ClampedArray counts as Uint8Array. A BigInt array only merges
|
|
169
|
+
* with its own type.
|
|
170
|
+
* @param {Function[]} types - typed-array constructors, at least one.
|
|
171
|
+
* @param {string} name - the column, for the message.
|
|
172
|
+
* @returns {Function}
|
|
173
|
+
*/
|
|
174
|
+
export function widestType(types, name) {
|
|
175
|
+
var label = "column \"" + name + "\"";
|
|
176
|
+
if (!Array.isArray(types) || types.length === 0) throw bad(label + " has no types to merge");
|
|
177
|
+
var names = types.map(function (t) {
|
|
178
|
+
var n = t && t.name;
|
|
179
|
+
if (typeof n !== "string" || (!has(FLOATS, n) && !has(INTS, n) && !has(BIGS, n))) throw bad(label + " has a type that is not a typed-array constructor");
|
|
180
|
+
return n === "Uint8ClampedArray" ? "Uint8Array" : n;
|
|
181
|
+
});
|
|
182
|
+
var first = names[0];
|
|
183
|
+
if (has(BIGS, first)) {
|
|
184
|
+
names.forEach(function (n) {
|
|
185
|
+
if (n !== first) throw bad(label + " cannot merge " + first + " with " + n);
|
|
186
|
+
});
|
|
187
|
+
return globalThis[first];
|
|
188
|
+
}
|
|
189
|
+
names.forEach(function (n) {
|
|
190
|
+
if (has(BIGS, n)) throw bad(label + " cannot merge " + n + " with " + first);
|
|
191
|
+
});
|
|
192
|
+
if (names.indexOf("Float64Array") >= 0) return Float64Array;
|
|
193
|
+
if (names.indexOf("Float32Array") >= 0) {
|
|
194
|
+
// Float32 holds integers exactly only to 2^24: 32-bit integers (an index column) go to Float64
|
|
195
|
+
if (names.indexOf("Int32Array") >= 0 || names.indexOf("Uint32Array") >= 0) return Float64Array;
|
|
196
|
+
return Float32Array;
|
|
197
|
+
}
|
|
198
|
+
var maxS = 0;
|
|
199
|
+
var maxU = 0;
|
|
200
|
+
names.forEach(function (n) {
|
|
201
|
+
var b = INTS[n][0];
|
|
202
|
+
if (INTS[n][1]) { if (b > maxS) maxS = b; } else if (b > maxU) maxU = b;
|
|
203
|
+
});
|
|
204
|
+
if (maxS > 0 && maxU > 0 && maxS <= maxU) {
|
|
205
|
+
throw bad(label + " cannot merge signed Int" + maxS * 8 + "Array with unsigned Uint" + maxU * 8 + "Array (values would wrap)");
|
|
206
|
+
}
|
|
207
|
+
if (maxS > 0) return globalThis["Int" + maxS * 8 + "Array"];
|
|
208
|
+
return globalThis["Uint" + maxU * 8 + "Array"];
|
|
209
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/ops/interleave.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Interleaved positions for a renderer: x, y[, z] packed per point
|
|
4
|
+
// into one typed array (Kirra uploads these to Three.js). Values
|
|
5
|
+
// are written in Float64 and shifted by an optional origin before
|
|
6
|
+
// they are stored, so large world coordinates keep their decimals
|
|
7
|
+
// in a Float32 buffer.
|
|
8
|
+
//==========================================================
|
|
9
|
+
|
|
10
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
11
|
+
import { countOf, needColumn } from "./columns.js";
|
|
12
|
+
|
|
13
|
+
function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
|
|
14
|
+
|
|
15
|
+
// The origin axis as a number to subtract, or 0 when it is absent or not finite.
|
|
16
|
+
function shiftOf(origin, axis) {
|
|
17
|
+
var v = origin[axis];
|
|
18
|
+
if (v === undefined || v === null) return 0;
|
|
19
|
+
if (typeof v !== "number") throw bad("origin." + axis + " must be a number");
|
|
20
|
+
return Number.isFinite(v) ? v : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pack point positions as interleaved values, x y z (stride 3) or x y
|
|
25
|
+
* (stride 2). Each value is shifted by the origin in Float64, then
|
|
26
|
+
* stored. An origin axis that is absent or not finite is not shifted,
|
|
27
|
+
* so { x, y } keeps world Z.
|
|
28
|
+
* @param {object} cols - point columns with x and y (and z for "xyz").
|
|
29
|
+
* @param {{layout?: "xyz"|"xy", into?: Float32Array|Float64Array, at?: number,
|
|
30
|
+
* origin?: {x?: number, y?: number, z?: number}, float32?: boolean}} [options]
|
|
31
|
+
* layout: default "xyz". into: a caller's buffer, written from element at × stride
|
|
32
|
+
* (default: a new Float32Array when float32 is true or omitted, else a Float64Array).
|
|
33
|
+
* at: whole number ≥ 0, default 0.
|
|
34
|
+
* @returns {Float32Array|Float64Array} into, or the new array.
|
|
35
|
+
* @throws {LasError} INVALID_ARGUMENT when an argument is wrong or into is too short.
|
|
36
|
+
*/
|
|
37
|
+
export function interleavePositions(cols, options) {
|
|
38
|
+
var opt = options === undefined ? {} : options;
|
|
39
|
+
if (opt === null || typeof opt !== "object") throw bad("options must be an object");
|
|
40
|
+
var n = countOf(cols);
|
|
41
|
+
var layout = opt.layout === undefined ? "xyz" : opt.layout;
|
|
42
|
+
if (layout !== "xyz" && layout !== "xy") throw bad("layout " + String(layout) + " is not \"xyz\" or \"xy\"");
|
|
43
|
+
var stride = layout === "xyz" ? 3 : 2;
|
|
44
|
+
var at = opt.at === undefined ? 0 : opt.at;
|
|
45
|
+
if (typeof at !== "number" || !Number.isInteger(at) || at < 0) throw bad("at " + String(at) + " is not a whole number ≥ 0");
|
|
46
|
+
var origin = opt.origin === undefined || opt.origin === null ? {} : opt.origin;
|
|
47
|
+
if (typeof origin !== "object") throw bad("origin must be an object { x, y, z }");
|
|
48
|
+
var sx = shiftOf(origin, "x");
|
|
49
|
+
var sy = shiftOf(origin, "y");
|
|
50
|
+
var sz = layout === "xyz" ? shiftOf(origin, "z") : 0;
|
|
51
|
+
|
|
52
|
+
var x = needColumn(cols, "x", "interleavePositions");
|
|
53
|
+
var y = needColumn(cols, "y", "interleavePositions");
|
|
54
|
+
var z = layout === "xyz" ? needColumn(cols, "z", "interleavePositions") : null;
|
|
55
|
+
|
|
56
|
+
var into = opt.into;
|
|
57
|
+
if (into === undefined) {
|
|
58
|
+
var f32 = opt.float32 === undefined ? true : opt.float32;
|
|
59
|
+
into = f32 ? new Float32Array(n * stride) : new Float64Array(n * stride);
|
|
60
|
+
} else if (!(into instanceof Float32Array) && !(into instanceof Float64Array)) {
|
|
61
|
+
throw bad("into must be a Float32Array or a Float64Array");
|
|
62
|
+
}
|
|
63
|
+
if ((at + n) * stride > into.length) throw bad("into is too short: needs " + (at + n) * stride + " elements from " + at * stride + ", has " + into.length);
|
|
64
|
+
|
|
65
|
+
var base = at * stride;
|
|
66
|
+
if (stride === 3) {
|
|
67
|
+
for (var i = 0; i < n; i++) {
|
|
68
|
+
var o = base + i * 3;
|
|
69
|
+
into[o] = x[i] - sx;
|
|
70
|
+
into[o + 1] = y[i] - sy;
|
|
71
|
+
into[o + 2] = z[i] - sz;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for (var j = 0; j < n; j++) {
|
|
75
|
+
var p = base + j * 2;
|
|
76
|
+
into[p] = x[j] - sx;
|
|
77
|
+
into[p + 1] = y[j] - sy;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return into;
|
|
81
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/ops/polygon.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// The point-in-polygon test behind selectPolygon and the reader's
|
|
4
|
+
// polygon crop (filters.js). Internal: not exported from index.js.
|
|
5
|
+
// The boundary decision comes first (a point within tolerance of an
|
|
6
|
+
// edge or vertex, measured in Float64), so an edge point never
|
|
7
|
+
// depends on crossing-test rounding; only a point clear of every
|
|
8
|
+
// edge goes to the even-odd crossing test (W. Randolph Franklin's
|
|
9
|
+
// PNPOLY).
|
|
10
|
+
//==========================================================
|
|
11
|
+
|
|
12
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
13
|
+
|
|
14
|
+
function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
|
|
15
|
+
|
|
16
|
+
// Validate a flat polygon [x0, y0, x1, y1, ...] and copy it to a Float64Array
|
|
17
|
+
function checkPolygon(polygon) {
|
|
18
|
+
if (!Array.isArray(polygon) && !(ArrayBuffer.isView(polygon) && !(polygon instanceof DataView))) throw bad("polygon must be an Array or typed array [x0, y0, x1, y1, ...]");
|
|
19
|
+
var n = polygon.length;
|
|
20
|
+
if (n % 2 !== 0) throw bad("polygon has an odd length " + n + ": it holds x, y pairs");
|
|
21
|
+
if (n < 6) throw bad("polygon needs at least 3 vertices (6 values), got " + n);
|
|
22
|
+
var p = new Float64Array(n);
|
|
23
|
+
for (var i = 0; i < n; i++) {
|
|
24
|
+
var v = polygon[i];
|
|
25
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw bad("polygon[" + i + "] = " + String(v) + " is not a finite number");
|
|
26
|
+
p[i] = v;
|
|
27
|
+
}
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a point-in-polygon test.
|
|
33
|
+
* @param {number[]|TypedArray} polygon - [x0, y0, x1, y1, ...], at least 3 vertices, implicitly closed.
|
|
34
|
+
* @param {{boundary?: string, tolerance?: number}} [opts] - boundary "touching" (default: points on the
|
|
35
|
+
* boundary are in) or "inside" (they are out); tolerance (default 0): within this distance of an edge
|
|
36
|
+
* or vertex is on the boundary.
|
|
37
|
+
* @returns {{test: function(number, number): boolean, minX: number, maxX: number, minY: number, maxY: number}}
|
|
38
|
+
* test(x, y) decides a point (false outside the box at once); the box is the polygon's grown by tolerance.
|
|
39
|
+
*/
|
|
40
|
+
export function makePolygonTest(polygon, opts) {
|
|
41
|
+
if (opts === undefined || opts === null) opts = {};
|
|
42
|
+
if (typeof opts !== "object") throw bad("options must be an object { boundary, tolerance }");
|
|
43
|
+
var p = checkPolygon(polygon);
|
|
44
|
+
var boundary = opts.boundary === undefined ? "touching" : opts.boundary;
|
|
45
|
+
if (boundary !== "touching" && boundary !== "inside") throw bad("boundary must be \"touching\" or \"inside\", got " + String(boundary));
|
|
46
|
+
var tol = opts.tolerance === undefined ? 0 : opts.tolerance;
|
|
47
|
+
if (typeof tol !== "number" || !Number.isFinite(tol) || tol < 0) throw bad("tolerance must be a finite number ≥ 0, got " + String(tol));
|
|
48
|
+
var touching = boundary === "touching", tol2 = tol * tol;
|
|
49
|
+
var n = p.length / 2;
|
|
50
|
+
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
51
|
+
for (var k = 0; k < n; k++) {
|
|
52
|
+
if (p[2 * k] < minX) minX = p[2 * k];
|
|
53
|
+
if (p[2 * k] > maxX) maxX = p[2 * k];
|
|
54
|
+
if (p[2 * k + 1] < minY) minY = p[2 * k + 1];
|
|
55
|
+
if (p[2 * k + 1] > maxY) maxY = p[2 * k + 1];
|
|
56
|
+
}
|
|
57
|
+
minX -= tol; maxX += tol; minY -= tol; maxY += tol;
|
|
58
|
+
|
|
59
|
+
// Squared distance from (px, py) to the segment (ax, ay)-(bx, by); 0 when the point is exactly on it
|
|
60
|
+
function dist2(px, py, ax, ay, bx, by) {
|
|
61
|
+
var ex = bx - ax, ey = by - ay;
|
|
62
|
+
var wx = px - ax, wy = py - ay;
|
|
63
|
+
var len2 = ex * ex + ey * ey;
|
|
64
|
+
if (len2 === 0) return wx * wx + wy * wy;
|
|
65
|
+
if (ex * wy - ey * wx === 0 && px >= Math.min(ax, bx) && px <= Math.max(ax, bx) && py >= Math.min(ay, by) && py <= Math.max(ay, by)) return 0;
|
|
66
|
+
var t = (wx * ex + wy * ey) / len2;
|
|
67
|
+
if (t < 0) t = 0; else if (t > 1) t = 1;
|
|
68
|
+
var dx = px - (ax + t * ex), dy = py - (ay + t * ey);
|
|
69
|
+
return dx * dx + dy * dy;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function test(px, py) {
|
|
73
|
+
if (!(px >= minX && px <= maxX && py >= minY && py <= maxY)) return false;
|
|
74
|
+
var inside = false;
|
|
75
|
+
for (var i = 0, j = n - 1; i < n; j = i++) {
|
|
76
|
+
var ax = p[2 * j], ay = p[2 * j + 1], bx = p[2 * i], by = p[2 * i + 1];
|
|
77
|
+
if (dist2(px, py, ax, ay, bx, by) <= tol2) return touching;
|
|
78
|
+
if ((by > py) !== (ay > py) && px < (ax - bx) * (py - by) / (ay - by) + bx) inside = !inside;
|
|
79
|
+
}
|
|
80
|
+
return inside;
|
|
81
|
+
}
|
|
82
|
+
return { test: test, minX: minX, maxX: maxX, minY: minY, maxY: maxY };
|
|
83
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// src/ops/select.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Making selections: a Uint32Array of point indices, strictly
|
|
4
|
+
// ascending, from point columns (see columns.js). Box, class and
|
|
5
|
+
// return selections mean what the reader's filters mean; thinSelection
|
|
6
|
+
// is the reader's maxPoints rule, exported, and thinStep is the one
|
|
7
|
+
// place that rule lives (decodeRecords calls it per passing record).
|
|
8
|
+
// Set operations are linear merges of two ascending selections.
|
|
9
|
+
//==========================================================
|
|
10
|
+
|
|
11
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
12
|
+
import { countOf, checkSelection, needColumn } from "./columns.js";
|
|
13
|
+
import { makePolygonTest } from "./polygon.js";
|
|
14
|
+
|
|
15
|
+
function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
|
|
16
|
+
|
|
17
|
+
function whole(v) { return typeof v === "number" && Number.isInteger(v) && v >= 0; }
|
|
18
|
+
|
|
19
|
+
// out[0..n) as a copy that does not pin the full-size buffer.
|
|
20
|
+
function trim(out, n) { return n === out.length ? out : out.slice(0, n); }
|
|
21
|
+
|
|
22
|
+
// A lookup table of 256 flags for a list of whole numbers 0..255.
|
|
23
|
+
function table(list, name) {
|
|
24
|
+
if (!Array.isArray(list)) throw bad(name + " must be an Array of whole numbers");
|
|
25
|
+
var t = new Uint8Array(256);
|
|
26
|
+
list.forEach(function (v, i) {
|
|
27
|
+
if (!whole(v) || v > 255) throw bad(name + "[" + i + "] = " + v + " is not a whole number 0 to 255");
|
|
28
|
+
t[v] = 1;
|
|
29
|
+
});
|
|
30
|
+
return t;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bound(box, key) {
|
|
34
|
+
var v = box[key];
|
|
35
|
+
if (v === undefined || v === null) return undefined;
|
|
36
|
+
if (typeof v !== "number" || v !== v) throw bad(key + " must be a number");
|
|
37
|
+
return v;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Indices of the points inside a box, bounds inclusive. An omitted
|
|
42
|
+
* (undefined) or null bound is no limit, as in the reader's bounds. Needs x and y; z only when minZ or
|
|
43
|
+
* maxZ is given.
|
|
44
|
+
* @param {object} cols - point columns.
|
|
45
|
+
* @param {{minX?:number, maxX?:number, minY?:number, maxY?:number, minZ?:number, maxZ?:number}} box
|
|
46
|
+
* @returns {Uint32Array}
|
|
47
|
+
*/
|
|
48
|
+
export function selectBox(cols, box) {
|
|
49
|
+
var n = countOf(cols);
|
|
50
|
+
if (box === null || typeof box !== "object") throw bad("box must be an object { minX, maxX, minY, maxY, minZ, maxZ }");
|
|
51
|
+
var minX = bound(box, "minX"), maxX = bound(box, "maxX");
|
|
52
|
+
var minY = bound(box, "minY"), maxY = bound(box, "maxY");
|
|
53
|
+
var minZ = bound(box, "minZ"), maxZ = bound(box, "maxZ");
|
|
54
|
+
var x = needColumn(cols, "x", "selectBox"), y = needColumn(cols, "y", "selectBox");
|
|
55
|
+
var useZ = minZ !== undefined || maxZ !== undefined;
|
|
56
|
+
var z = useZ ? needColumn(cols, "z", "selectBox") : null;
|
|
57
|
+
if (minX === undefined) minX = -Infinity;
|
|
58
|
+
if (maxX === undefined) maxX = Infinity;
|
|
59
|
+
if (minY === undefined) minY = -Infinity;
|
|
60
|
+
if (maxY === undefined) maxY = Infinity;
|
|
61
|
+
if (minZ === undefined) minZ = -Infinity;
|
|
62
|
+
if (maxZ === undefined) maxZ = Infinity;
|
|
63
|
+
var out = new Uint32Array(n), m = 0;
|
|
64
|
+
for (var i = 0; i < n; i++) {
|
|
65
|
+
var vx = x[i];
|
|
66
|
+
if (!(vx >= minX && vx <= maxX)) continue;
|
|
67
|
+
var vy = y[i];
|
|
68
|
+
if (!(vy >= minY && vy <= maxY)) continue;
|
|
69
|
+
if (useZ) {
|
|
70
|
+
var vz = z[i];
|
|
71
|
+
if (!(vz >= minZ && vz <= maxZ)) continue;
|
|
72
|
+
}
|
|
73
|
+
out[m++] = i;
|
|
74
|
+
}
|
|
75
|
+
return trim(out, m);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Indices of the points inside a polygon (even-odd rule), in the columns' own frame. A point on the
|
|
80
|
+
* boundary (within tolerance of an edge or vertex, as the 2D distance to the segment in Float64) is
|
|
81
|
+
* kept by boundary "touching" (default) and dropped by "inside", so the two differ by exactly the
|
|
82
|
+
* boundary points. The boundary is decided before the crossing test. Needs x and y; z only when a Z
|
|
83
|
+
* limit is given.
|
|
84
|
+
* @param {object} cols - point columns.
|
|
85
|
+
* @param {number[]|TypedArray} polygon - [x0, y0, x1, y1, ...], at least 3 vertices, implicitly closed.
|
|
86
|
+
* @param {{boundary?: "touching"|"inside", tolerance?: number, minZ?: number, maxZ?: number}} [opts]
|
|
87
|
+
* @returns {Uint32Array}
|
|
88
|
+
*/
|
|
89
|
+
export function selectPolygon(cols, polygon, opts) {
|
|
90
|
+
var n = countOf(cols);
|
|
91
|
+
if (opts === undefined || opts === null) opts = {};
|
|
92
|
+
if (typeof opts !== "object") throw bad("options must be an object { boundary, tolerance, minZ, maxZ }");
|
|
93
|
+
var poly = makePolygonTest(polygon, opts);
|
|
94
|
+
var minZ = bound(opts, "minZ"), maxZ = bound(opts, "maxZ");
|
|
95
|
+
var x = needColumn(cols, "x", "selectPolygon"), y = needColumn(cols, "y", "selectPolygon");
|
|
96
|
+
var useZ = minZ !== undefined || maxZ !== undefined;
|
|
97
|
+
var z = useZ ? needColumn(cols, "z", "selectPolygon") : null;
|
|
98
|
+
if (minZ === undefined) minZ = -Infinity;
|
|
99
|
+
if (maxZ === undefined) maxZ = Infinity;
|
|
100
|
+
var test = poly.test;
|
|
101
|
+
var out = new Uint32Array(n), m = 0;
|
|
102
|
+
for (var i = 0; i < n; i++) {
|
|
103
|
+
if (useZ) {
|
|
104
|
+
var vz = z[i];
|
|
105
|
+
if (!(vz >= minZ && vz <= maxZ)) continue;
|
|
106
|
+
}
|
|
107
|
+
if (test(x[i], y[i])) out[m++] = i;
|
|
108
|
+
}
|
|
109
|
+
return trim(out, m);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Indices of the points whose classification is in the list.
|
|
114
|
+
* @param {object} cols - point columns (needs classification).
|
|
115
|
+
* @param {number[]} classes
|
|
116
|
+
* @returns {Uint32Array}
|
|
117
|
+
*/
|
|
118
|
+
export function selectClasses(cols, classes) {
|
|
119
|
+
var n = countOf(cols);
|
|
120
|
+
var cls = needColumn(cols, "classification", "selectClasses");
|
|
121
|
+
var t = table(classes, "classes");
|
|
122
|
+
var out = new Uint32Array(n), m = 0;
|
|
123
|
+
for (var i = 0; i < n; i++) if (cls[i] < 256 && t[cls[i]] === 1) out[m++] = i;
|
|
124
|
+
return trim(out, m);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Indices of the points matching any of the return criteria given:
|
|
129
|
+
* returns (returnNumber in the list), first (returnNumber 1), last
|
|
130
|
+
* (returnNumber equals numberOfReturns), single (numberOfReturns 1).
|
|
131
|
+
* @param {object} cols - point columns (returnNumber; numberOfReturns for last / single).
|
|
132
|
+
* @param {{returns?:number[], first?:boolean, last?:boolean, single?:boolean}} criteria
|
|
133
|
+
* @returns {Uint32Array}
|
|
134
|
+
*/
|
|
135
|
+
export function selectReturns(cols, criteria) {
|
|
136
|
+
var n = countOf(cols);
|
|
137
|
+
if (criteria === null || typeof criteria !== "object") throw bad("criteria must be an object { returns, first, last, single }");
|
|
138
|
+
var hasList = criteria.returns !== undefined;
|
|
139
|
+
var first = !!criteria.first, last = !!criteria.last, single = !!criteria.single;
|
|
140
|
+
if (!hasList && !first && !last && !single) throw bad("selectReturns needs returns, first, last or single");
|
|
141
|
+
var t = hasList ? table(criteria.returns, "returns") : null;
|
|
142
|
+
var rn = needColumn(cols, "returnNumber", "selectReturns");
|
|
143
|
+
var nr = (last || single) ? needColumn(cols, "numberOfReturns", "selectReturns") : null;
|
|
144
|
+
var out = new Uint32Array(n), m = 0;
|
|
145
|
+
for (var i = 0; i < n; i++) {
|
|
146
|
+
var r = rn[i];
|
|
147
|
+
var hit = (hasList && r < 256 && t[r] === 1) || (first && r === 1) || (last && r === nr[i]) || (single && nr[i] === 1);
|
|
148
|
+
if (hit) out[m++] = i;
|
|
149
|
+
}
|
|
150
|
+
return trim(out, m);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* One step of the reader's even-thinning rule. thin is
|
|
155
|
+
* { total, target, acc }: acc gains target per record and the record
|
|
156
|
+
* is kept each time acc reaches total (then acc -= total). The j-th
|
|
157
|
+
* kept record is number ceil(j * total / target) - 1.
|
|
158
|
+
* @param {{total:number, target:number, acc:number}} thin - updated in place.
|
|
159
|
+
* @returns {boolean} true when this record is kept.
|
|
160
|
+
*/
|
|
161
|
+
export function thinStep(thin) {
|
|
162
|
+
thin.acc += thin.target;
|
|
163
|
+
if (thin.acc < thin.total) return false;
|
|
164
|
+
thin.acc -= thin.total;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The reader's maxPoints rule as a selection: exactly min(count, target)
|
|
170
|
+
* indices spread evenly over 0..count-1. readPoints({ maxPoints: target })
|
|
171
|
+
* keeps the same points.
|
|
172
|
+
* @param {number} count - points to choose from.
|
|
173
|
+
* @param {number} target - points to keep.
|
|
174
|
+
* @returns {Uint32Array}
|
|
175
|
+
*/
|
|
176
|
+
export function thinSelection(count, target) {
|
|
177
|
+
if (!whole(count)) throw bad("count " + String(count) + " is not a whole number ≥ 0");
|
|
178
|
+
if (!whole(target)) throw bad("target " + String(target) + " is not a whole number ≥ 0");
|
|
179
|
+
var out, i;
|
|
180
|
+
if (target >= count) {
|
|
181
|
+
out = new Uint32Array(count);
|
|
182
|
+
for (i = 0; i < count; i++) out[i] = i;
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
out = new Uint32Array(target);
|
|
186
|
+
var thin = { total: count, target: target, acc: 0 }, m = 0;
|
|
187
|
+
for (i = 0; i < count && m < target; i++) if (thinStep(thin)) out[m++] = i;
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Indices in a or in b.
|
|
193
|
+
* @param {Uint32Array|number[]} a - ascending selection.
|
|
194
|
+
* @param {Uint32Array|number[]} b - ascending selection.
|
|
195
|
+
* @returns {Uint32Array}
|
|
196
|
+
*/
|
|
197
|
+
export function unionSelections(a, b) {
|
|
198
|
+
a = checkSelection(a, Infinity, "a");
|
|
199
|
+
b = checkSelection(b, Infinity, "b");
|
|
200
|
+
var out = new Uint32Array(a.length + b.length), i = 0, j = 0, m = 0;
|
|
201
|
+
while (i < a.length && j < b.length) {
|
|
202
|
+
if (a[i] < b[j]) out[m++] = a[i++];
|
|
203
|
+
else if (a[i] > b[j]) out[m++] = b[j++];
|
|
204
|
+
else { out[m++] = a[i++]; j++; }
|
|
205
|
+
}
|
|
206
|
+
while (i < a.length) out[m++] = a[i++];
|
|
207
|
+
while (j < b.length) out[m++] = b[j++];
|
|
208
|
+
return trim(out, m);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Indices in both a and b.
|
|
213
|
+
* @param {Uint32Array|number[]} a - ascending selection.
|
|
214
|
+
* @param {Uint32Array|number[]} b - ascending selection.
|
|
215
|
+
* @returns {Uint32Array}
|
|
216
|
+
*/
|
|
217
|
+
export function intersectSelections(a, b) {
|
|
218
|
+
a = checkSelection(a, Infinity, "a");
|
|
219
|
+
b = checkSelection(b, Infinity, "b");
|
|
220
|
+
var out = new Uint32Array(Math.min(a.length, b.length)), i = 0, j = 0, m = 0;
|
|
221
|
+
while (i < a.length && j < b.length) {
|
|
222
|
+
if (a[i] < b[j]) i++;
|
|
223
|
+
else if (a[i] > b[j]) j++;
|
|
224
|
+
else { out[m++] = a[i++]; j++; }
|
|
225
|
+
}
|
|
226
|
+
return trim(out, m);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Indices in a that are not in b.
|
|
231
|
+
* @param {Uint32Array|number[]} a - ascending selection.
|
|
232
|
+
* @param {Uint32Array|number[]} b - ascending selection.
|
|
233
|
+
* @returns {Uint32Array}
|
|
234
|
+
*/
|
|
235
|
+
export function subtractSelections(a, b) {
|
|
236
|
+
a = checkSelection(a, Infinity, "a");
|
|
237
|
+
b = checkSelection(b, Infinity, "b");
|
|
238
|
+
var out = new Uint32Array(a.length), i = 0, j = 0, m = 0;
|
|
239
|
+
while (i < a.length) {
|
|
240
|
+
while (j < b.length && b[j] < a[i]) j++;
|
|
241
|
+
if (j < b.length && b[j] === a[i]) { i++; continue; }
|
|
242
|
+
out[m++] = a[i++];
|
|
243
|
+
}
|
|
244
|
+
return trim(out, m);
|
|
245
|
+
}
|