las-laz-js 0.4.0 → 0.6.1
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 +63 -0
- package/README.md +197 -6
- package/package.json +1 -1
- package/src/LasReader.js +6 -4
- package/src/filter/dedupe.js +59 -9
- package/src/filter/filters.js +31 -10
- package/src/index/grid.js +198 -0
- package/src/index/query.js +329 -0
- package/src/index.js +9 -2
- 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/sparse.js +97 -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,198 @@
|
|
|
1
|
+
// src/index/grid.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// The voxel neighbour index: a sorted voxel grid on flat typed
|
|
4
|
+
// arrays. Point numbers are counting-sorted by cell into `order`
|
|
5
|
+
// (stable, so a cell's points stay ascending); an open-addressing
|
|
6
|
+
// hash table (linear probing, power-of-two capacity) maps each
|
|
7
|
+
// occupied cell's key to its `start` and `length` in `order`.
|
|
8
|
+
// Coordinates are referenced, not copied: x / y / z columns, or a
|
|
9
|
+
// k = 3 positions column (Kirra's kadCloud layout) read with a
|
|
10
|
+
// stride of 3. About 6 bytes per point at 16 points per cell.
|
|
11
|
+
// The grid only prunes: queries (query.js) distance-check every
|
|
12
|
+
// candidate in Float64, so their results are exact.
|
|
13
|
+
//==========================================================
|
|
14
|
+
|
|
15
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
16
|
+
import { columnsOf, countOf } from "../ops/columns.js";
|
|
17
|
+
|
|
18
|
+
var MAX_POINTS = 4294967295;
|
|
19
|
+
var MAX_CELLS = 9007199254740992; // 2^53: keys stay exact integers
|
|
20
|
+
var EMPTY = -1;
|
|
21
|
+
|
|
22
|
+
function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The coordinates of point columns: x / y / z (stride 1), else a k = 3
|
|
26
|
+
* positions column (stride 3, y and z as offset views of it).
|
|
27
|
+
* Point i's x is coords.x[i * coords.stride].
|
|
28
|
+
* @param {object} cols - point columns.
|
|
29
|
+
* @param {string} fn - the calling function's name, for the message.
|
|
30
|
+
* @returns {{x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number>, stride: number}}
|
|
31
|
+
*/
|
|
32
|
+
export function coordsOf(cols, fn) {
|
|
33
|
+
var count = countOf(cols);
|
|
34
|
+
var k = Object.create(null);
|
|
35
|
+
columnsOf(cols).forEach(function (c) { k[c.name] = c.k; });
|
|
36
|
+
if (k.x === 1 && k.y === 1 && k.z === 1) return { x: cols.x, y: cols.y, z: cols.z, stride: 1 };
|
|
37
|
+
if (k.positions === 3 || (count === 0 && k.positions === 1)) {
|
|
38
|
+
var p = cols.positions;
|
|
39
|
+
return { x: p, y: p.subarray(1), z: p.subarray(2), stride: 3 };
|
|
40
|
+
}
|
|
41
|
+
throw bad(fn + " needs x, y, z columns or a k = 3 positions column");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Refuse a point count the index cannot number (order is a Uint32Array).
|
|
46
|
+
* @param {number} n
|
|
47
|
+
*/
|
|
48
|
+
export function checkPointCount(n) {
|
|
49
|
+
if (n > MAX_POINTS) throw new LasError(ERROR_CODES.TOO_MANY_POINTS, "the neighbour index takes at most 4,294,967,295 points, not " + n);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// murmur3's 32-bit finaliser: every input bit reaches the low bits the mask keeps
|
|
53
|
+
function fmix32(h) {
|
|
54
|
+
h ^= h >>> 16; h = Math.imul(h, 0x85EBCA6B);
|
|
55
|
+
h ^= h >>> 13; h = Math.imul(h, 0xC2B2AE35);
|
|
56
|
+
return (h ^ (h >>> 16)) >>> 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The home slot of a cell key (an integer below 2^53) in a table of mask + 1 slots.
|
|
61
|
+
* @param {number} key
|
|
62
|
+
* @param {number} mask
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
export function hashKey(key, mask) {
|
|
66
|
+
var lo = key % 4294967296, hi = (key - lo) / 4294967296;
|
|
67
|
+
return fmix32(lo ^ fmix32(hi)) & mask;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function nextPow2(n) {
|
|
71
|
+
var p = 16;
|
|
72
|
+
while (p < n) p *= 2;
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the grid over coordinates.
|
|
78
|
+
* @param {{x, y, z, stride}} coords - from coordsOf.
|
|
79
|
+
* @param {number} count - points.
|
|
80
|
+
* @param {number} [cellSize] - cell edge; omitted: about 16 points per cell.
|
|
81
|
+
* @param {boolean} [xy] - ignore Z (nz = 1).
|
|
82
|
+
* @returns {object} PointIndex
|
|
83
|
+
*/
|
|
84
|
+
export function buildGrid(coords, count, cellSize, xy) {
|
|
85
|
+
if (cellSize !== undefined && !(typeof cellSize === "number" && cellSize > 0 && isFinite(cellSize))) throw bad("cellSize must be a positive finite number, not " + String(cellSize));
|
|
86
|
+
var X = coords.x, Y = coords.y, Z = coords.z, st = coords.stride;
|
|
87
|
+
|
|
88
|
+
// 1. Bounds
|
|
89
|
+
var lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
90
|
+
for (var i = 0, o = 0; i < count; i++, o += st) {
|
|
91
|
+
var a = X[o], b = Y[o], c = xy ? 0 : Z[o];
|
|
92
|
+
if (a - a !== 0 || b - b !== 0 || c - c !== 0) throw bad("point " + i + " has a non-finite " + (a - a !== 0 ? "x" : b - b !== 0 ? "y" : "z") + " coordinate");
|
|
93
|
+
if (a < lo[0]) lo[0] = a; if (a > hi[0]) hi[0] = a;
|
|
94
|
+
if (b < lo[1]) lo[1] = b; if (b > hi[1]) hi[1] = b;
|
|
95
|
+
if (c < lo[2]) lo[2] = c; if (c > hi[2]) hi[2] = c;
|
|
96
|
+
}
|
|
97
|
+
if (count === 0) { lo = [0, 0, 0]; hi = [0, 0, 0]; }
|
|
98
|
+
var ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
|
|
99
|
+
if (cellSize === undefined) {
|
|
100
|
+
var prod = 1, d = 0;
|
|
101
|
+
ext.forEach(function (e) { if (e > 0) { prod *= e; d++; } });
|
|
102
|
+
cellSize = d === 0 ? 1 : Math.pow(16 * prod / count, 1 / d);
|
|
103
|
+
}
|
|
104
|
+
var nx = Math.floor(ext[0] / cellSize) + 1, ny = Math.floor(ext[1] / cellSize) + 1, nz = Math.floor(ext[2] / cellSize) + 1;
|
|
105
|
+
if (nx * ny * nz > MAX_CELLS) throw bad("cellSize too small for this extent: " + nx + " × " + ny + " × " + nz + " cells is more than 2^53");
|
|
106
|
+
var inv = 1 / cellSize, x0 = lo[0], y0 = lo[1], z0 = lo[2];
|
|
107
|
+
function keyOf(o) {
|
|
108
|
+
var cx = Math.floor((X[o] - x0) * inv); if (cx >= nx) cx = nx - 1;
|
|
109
|
+
var cy = Math.floor((Y[o] - y0) * inv); if (cy >= ny) cy = ny - 1;
|
|
110
|
+
var cz = 0;
|
|
111
|
+
if (!xy) { cz = Math.floor((Z[o] - z0) * inv); if (cz >= nz) cz = nz - 1; }
|
|
112
|
+
return (cx * ny + cy) * nz + cz;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Count points per cell into the hash table
|
|
116
|
+
var cap = nextPow2(2 * Math.min(nx * ny * nz, Math.ceil(count / 16)));
|
|
117
|
+
var mask = cap - 1, used = 0;
|
|
118
|
+
var keys = new Float64Array(cap).fill(EMPTY), length = new Uint32Array(cap);
|
|
119
|
+
function grow() {
|
|
120
|
+
var oldK = keys, oldL = length;
|
|
121
|
+
cap *= 2; mask = cap - 1;
|
|
122
|
+
keys = new Float64Array(cap).fill(EMPTY); length = new Uint32Array(cap);
|
|
123
|
+
for (var s = 0; s < oldK.length; s++) {
|
|
124
|
+
if (oldK[s] === EMPTY) continue;
|
|
125
|
+
var h = hashKey(oldK[s], mask);
|
|
126
|
+
while (keys[h] !== EMPTY) h = (h + 1) & mask;
|
|
127
|
+
keys[h] = oldK[s]; length[h] = oldL[s];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (i = 0, o = 0; i < count; i++, o += st) {
|
|
131
|
+
var key = keyOf(o), h = hashKey(key, mask);
|
|
132
|
+
while (keys[h] !== EMPTY && keys[h] !== key) h = (h + 1) & mask;
|
|
133
|
+
if (keys[h] === EMPTY) {
|
|
134
|
+
if (2 * (used + 1) > cap) {
|
|
135
|
+
grow();
|
|
136
|
+
h = hashKey(key, mask);
|
|
137
|
+
while (keys[h] !== EMPTY) h = (h + 1) & mask;
|
|
138
|
+
}
|
|
139
|
+
keys[h] = key; used++;
|
|
140
|
+
}
|
|
141
|
+
length[h]++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 3. Prefix sums give each cell's start; place point numbers (stable)
|
|
145
|
+
var start = new Uint32Array(cap), run = 0;
|
|
146
|
+
for (var s = 0; s < cap; s++) { start[s] = run; run += length[s]; }
|
|
147
|
+
var order = new Uint32Array(count);
|
|
148
|
+
for (i = 0, o = 0; i < count; i++, o += st) {
|
|
149
|
+
var k2 = keyOf(o), h2 = hashKey(k2, mask);
|
|
150
|
+
while (keys[h2] !== k2) h2 = (h2 + 1) & mask;
|
|
151
|
+
order[start[h2]++] = i;
|
|
152
|
+
}
|
|
153
|
+
for (s = 0; s < cap; s++) start[s] -= length[s];
|
|
154
|
+
|
|
155
|
+
return { count: count, cellSize: cellSize, min: [x0, y0, z0], nx: nx, ny: ny, nz: nz, order: order, keys: keys, start: start, length: length, mask: mask, coords: coords };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a neighbour index over point columns.
|
|
160
|
+
* @param {object} cols - point columns with x, y, z, or a k = 3 positions column.
|
|
161
|
+
* @param {{cellSize?: number}} [opts] - cellSize omitted: about 16 points per cell.
|
|
162
|
+
* @returns {object} PointIndex { count, cellSize, min, nx, ny, nz, order, keys, start, length, mask, coords }
|
|
163
|
+
*/
|
|
164
|
+
export function buildIndex(cols, opts) {
|
|
165
|
+
var coords = coordsOf(cols, "buildIndex");
|
|
166
|
+
checkPointCount(cols.count);
|
|
167
|
+
return buildGrid(coords, cols.count, opts ? opts.cellSize : undefined, false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The hash-table slot of cell (ix, iy, iz), or -1 when the cell is empty or outside the grid.
|
|
172
|
+
* @param {object} index - PointIndex.
|
|
173
|
+
* @returns {number}
|
|
174
|
+
*/
|
|
175
|
+
export function lookup(index, ix, iy, iz) {
|
|
176
|
+
if (ix < 0 || iy < 0 || iz < 0 || ix >= index.nx || iy >= index.ny || iz >= index.nz) return -1;
|
|
177
|
+
var key = (ix * index.ny + iy) * index.nz + iz, keys = index.keys, mask = index.mask;
|
|
178
|
+
var h = hashKey(key, mask);
|
|
179
|
+
while (keys[h] !== EMPTY) {
|
|
180
|
+
if (keys[h] === key) return h;
|
|
181
|
+
h = (h + 1) & mask;
|
|
182
|
+
}
|
|
183
|
+
return -1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Refuse an index that is not self-consistent.
|
|
188
|
+
* @param {object} index - PointIndex.
|
|
189
|
+
* @param {string} fn - the calling function's name, for the message.
|
|
190
|
+
*/
|
|
191
|
+
export function checkIndex(index, fn) {
|
|
192
|
+
var ok = index !== null && typeof index === "object" && index.order instanceof Uint32Array && index.order.length === index.count && index.coords;
|
|
193
|
+
if (ok && index.count > 0) {
|
|
194
|
+
var need = (index.count - 1) * index.coords.stride + 1;
|
|
195
|
+
ok = index.coords.x.length >= need && index.coords.y.length >= need && index.coords.z.length >= need;
|
|
196
|
+
}
|
|
197
|
+
if (!ok) throw bad(fn + ": index is not self-consistent (rebuild it with buildIndex)");
|
|
198
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// src/index/query.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Queries on a PointIndex (grid.js). The grid only prunes: every
|
|
4
|
+
// candidate is distance-checked in Float64 against the stored
|
|
5
|
+
// coordinates, "within" is inclusive (<=) and ties go to the lowest
|
|
6
|
+
// point number, so each answer equals a plain scan of the points.
|
|
7
|
+
// Cell ranges and cell distances are widened by a millionth of a
|
|
8
|
+
// cell so float rounding can never skip a cell holding an answer.
|
|
9
|
+
// When a query would visit more cells than the hash table has
|
|
10
|
+
// slots (a cell size far below the query's reach, or a query far
|
|
11
|
+
// outside the cloud), it walks the occupied slots instead.
|
|
12
|
+
//==========================================================
|
|
13
|
+
|
|
14
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
15
|
+
import { checkIndex, lookup as lookupCell } from "./grid.js";
|
|
16
|
+
|
|
17
|
+
var EPS = 1e-6; // in cells
|
|
18
|
+
|
|
19
|
+
function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
|
|
20
|
+
|
|
21
|
+
function fin(v) { return typeof v === "number" && isFinite(v); }
|
|
22
|
+
|
|
23
|
+
// [x, y, z] as Numbers; with xy, [x, y] or [x, y, anything]
|
|
24
|
+
function checkPoint(p, xy, name) {
|
|
25
|
+
var ok = Array.isArray(p) && (p.length === 3 || (xy && p.length === 2)) && fin(p[0]) && fin(p[1]) && (xy || fin(p[2]));
|
|
26
|
+
if (!ok) throw bad(name + " must be [x, y, z]" + (xy ? " or [x, y]" : "") + " of finite numbers");
|
|
27
|
+
return [p[0], p[1], xy ? 0 : p[2]];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function checkPositive(v, name, optional) {
|
|
31
|
+
if (optional && v === undefined) return;
|
|
32
|
+
if (!(fin(v) && v > 0)) throw bad(name + " must be a positive finite number, not " + String(v));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function dims(index) { return [index.nx, index.ny, index.nz]; }
|
|
36
|
+
|
|
37
|
+
// Inclusive cell range on one axis covering [a, b], clamped; lo > hi when empty
|
|
38
|
+
function cellRange(index, axis, a, b) {
|
|
39
|
+
var n = dims(index)[axis], inv = 1 / index.cellSize, m = index.min[axis];
|
|
40
|
+
var lo = Math.floor((a - m) * inv - EPS), hi = Math.floor((b - m) * inv + EPS);
|
|
41
|
+
return [Math.max(0, lo), Math.min(n - 1, hi)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Squared distance from q to the box of cell (ix, iy, iz), the box widened by EPS cells
|
|
45
|
+
function cellDist2(index, ix, iy, iz, q, xy) {
|
|
46
|
+
var cs = index.cellSize, g = cs * EPS, m = index.min, c = [ix, iy, iz], s = 0;
|
|
47
|
+
for (var a = 0; a < (xy ? 2 : 3); a++) {
|
|
48
|
+
var lo = m[a] + c[a] * cs - g, hi = m[a] + (c[a] + 1) * cs + g;
|
|
49
|
+
var d = lo - q[a] > 0 ? lo - q[a] : (q[a] - hi > 0 ? q[a] - hi : 0);
|
|
50
|
+
s += d * d;
|
|
51
|
+
}
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function decode(index, key) {
|
|
56
|
+
var iz = key % index.nz, t = (key - iz) / index.nz, iy = t % index.ny;
|
|
57
|
+
return [(t - iy) / index.ny, iy, iz];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Call fn(slot, ix, iy, iz) for every occupied cell in an inclusive cell box.
|
|
62
|
+
* Walks the table instead when the box has more cells than the table has slots.
|
|
63
|
+
* @param {object} index - PointIndex.
|
|
64
|
+
* @param {number[]} lo - [ix, iy, iz] low corner.
|
|
65
|
+
* @param {number[]} hi - [ix, iy, iz] high corner.
|
|
66
|
+
* @param {function} fn
|
|
67
|
+
*/
|
|
68
|
+
export function visitBox(index, lo, hi, fn) {
|
|
69
|
+
if (lo[0] > hi[0] || lo[1] > hi[1] || lo[2] > hi[2]) return;
|
|
70
|
+
var cells = (hi[0] - lo[0] + 1) * (hi[1] - lo[1] + 1) * (hi[2] - lo[2] + 1), keys = index.keys;
|
|
71
|
+
if (cells > keys.length) {
|
|
72
|
+
for (var s = 0; s < keys.length; s++) {
|
|
73
|
+
if (keys[s] < 0) continue;
|
|
74
|
+
var c = decode(index, keys[s]);
|
|
75
|
+
if (c[0] >= lo[0] && c[0] <= hi[0] && c[1] >= lo[1] && c[1] <= hi[1] && c[2] >= lo[2] && c[2] <= hi[2]) fn(s, c[0], c[1], c[2]);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (var ix = lo[0]; ix <= hi[0]; ix++) {
|
|
80
|
+
for (var iy = lo[1]; iy <= hi[1]; iy++) {
|
|
81
|
+
for (var iz = lo[2]; iz <= hi[2]; iz++) {
|
|
82
|
+
var slot = lookupCell(index, ix, iy, iz);
|
|
83
|
+
if (slot >= 0) fn(slot, ix, iy, iz);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Every point within `radius` of a point (inclusive), as a selection.
|
|
91
|
+
* @param {object} index - PointIndex from buildIndex.
|
|
92
|
+
* @param {number[]} point - [x, y, z]; with xy, [x, y] is enough.
|
|
93
|
+
* @param {number} radius - > 0.
|
|
94
|
+
* @param {{xy?: boolean}} [opts] - xy: measure in XY only.
|
|
95
|
+
* @returns {Uint32Array} ascending point numbers
|
|
96
|
+
*/
|
|
97
|
+
export function pointsInRadius(index, point, radius, opts) {
|
|
98
|
+
checkIndex(index, "pointsInRadius");
|
|
99
|
+
var xy = !!(opts && opts.xy);
|
|
100
|
+
var q = checkPoint(point, xy, "point");
|
|
101
|
+
checkPositive(radius, "radius");
|
|
102
|
+
if (index.count === 0) return new Uint32Array(0);
|
|
103
|
+
var rx = cellRange(index, 0, q[0] - radius, q[0] + radius), ry = cellRange(index, 1, q[1] - radius, q[1] + radius);
|
|
104
|
+
var rz = xy ? [0, index.nz - 1] : cellRange(index, 2, q[2] - radius, q[2] + radius);
|
|
105
|
+
var r2 = radius * radius, co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
|
|
106
|
+
var hits = [];
|
|
107
|
+
visitBox(index, [rx[0], ry[0], rz[0]], [rx[1], ry[1], rz[1]], function (s, ix, iy, iz) {
|
|
108
|
+
if (cellDist2(index, ix, iy, iz, q, xy) > r2) return;
|
|
109
|
+
for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
|
|
110
|
+
var p = order[j], o = p * st;
|
|
111
|
+
var dx = X[o] - q[0], dy = Y[o] - q[1], dz = xy ? 0 : Z[o] - q[2];
|
|
112
|
+
if (dx * dx + dy * dy + dz * dz <= r2) hits.push(p);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return Uint32Array.from(hits).sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The nearest point (lowest number on a tie), or -1 when none is within maxDistance.
|
|
120
|
+
* @param {object} index - PointIndex from buildIndex.
|
|
121
|
+
* @param {number[]} point - [x, y, z]; with xy, [x, y] is enough.
|
|
122
|
+
* @param {{maxDistance?: number, xy?: boolean}} [opts] - maxDistance omitted: unlimited.
|
|
123
|
+
* @returns {number}
|
|
124
|
+
*/
|
|
125
|
+
export function nearestPoint(index, point, opts) {
|
|
126
|
+
checkIndex(index, "nearestPoint");
|
|
127
|
+
var xy = !!(opts && opts.xy);
|
|
128
|
+
var q = checkPoint(point, xy, "point");
|
|
129
|
+
var maxD = opts ? opts.maxDistance : undefined;
|
|
130
|
+
checkPositive(maxD, "maxDistance", true);
|
|
131
|
+
if (index.count === 0) return -1;
|
|
132
|
+
var maxD2 = maxD === undefined ? Infinity : maxD * maxD;
|
|
133
|
+
var n = dims(index), cs = index.cellSize, inv = 1 / cs, m = index.min, axes = xy ? 2 : 3;
|
|
134
|
+
var c = [0, 0, 0];
|
|
135
|
+
for (var a = 0; a < axes; a++) c[a] = Math.min(n[a] - 1, Math.max(0, Math.floor((q[a] - m[a]) * inv)));
|
|
136
|
+
var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
|
|
137
|
+
var best = -1, bd = Infinity, visited = 0, budget = index.keys.length;
|
|
138
|
+
// How far the query lies outside the grid on each axis (0 inside)
|
|
139
|
+
var out = [0, 0, 0];
|
|
140
|
+
for (a = 0; a < axes; a++) {
|
|
141
|
+
var gl = m[a], gh = m[a] + n[a] * cs;
|
|
142
|
+
out[a] = Math.max(0, (q[a] < gl ? gl - q[a] : q[a] > gh ? q[a] - gh : 0) - cs * EPS);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function cell(ix, iy, iz) {
|
|
146
|
+
visited++;
|
|
147
|
+
var s = lookupCell(index, ix, iy, iz);
|
|
148
|
+
if (s < 0 || cellDist2(index, ix, iy, iz, q, xy) > Math.min(bd, maxD2)) return;
|
|
149
|
+
for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
|
|
150
|
+
var p = order[j], o = p * st;
|
|
151
|
+
var dx = X[o] - q[0], dy = Y[o] - q[1], dz = xy ? 0 : Z[o] - q[2];
|
|
152
|
+
var d = dx * dx + dy * dy + dz * dz;
|
|
153
|
+
if (d <= maxD2 && (d < bd || (d === bd && p < best))) { bd = d; best = p; }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Every grid cell at Chebyshev distance r (in the axes used) from c
|
|
157
|
+
function ring(r) {
|
|
158
|
+
var x0 = Math.max(0, c[0] - r), x1 = Math.min(n[0] - 1, c[0] + r);
|
|
159
|
+
var y0 = Math.max(0, c[1] - r), y1 = Math.min(n[1] - 1, c[1] + r);
|
|
160
|
+
var z0 = xy ? 0 : Math.max(0, c[2] - r), z1 = xy ? n[2] - 1 : Math.min(n[2] - 1, c[2] + r);
|
|
161
|
+
function column(ix, iy) { for (var iz = z0; iz <= z1; iz++) cell(ix, iy, iz); }
|
|
162
|
+
for (var ix = x0; ix <= x1; ix++) {
|
|
163
|
+
if (ix === c[0] - r || ix === c[0] + r) {
|
|
164
|
+
for (var iy = y0; iy <= y1; iy++) column(ix, iy);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (c[1] - r >= 0) column(ix, c[1] - r);
|
|
168
|
+
if (c[1] + r < n[1]) column(ix, c[1] + r);
|
|
169
|
+
if (xy) continue;
|
|
170
|
+
// interior columns: only the two z faces
|
|
171
|
+
for (iy = Math.max(y0, c[1] - r + 1); iy <= Math.min(y1, c[1] + r - 1); iy++) {
|
|
172
|
+
if (c[2] - r >= 0) cell(ix, iy, c[2] - r);
|
|
173
|
+
if (c[2] + r < n[2]) cell(ix, iy, c[2] + r);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function gap(g) { g = Math.max(0, g - cs * EPS); return g * g; }
|
|
179
|
+
|
|
180
|
+
for (var r = 0; ; r++) {
|
|
181
|
+
ring(r);
|
|
182
|
+
if (visited > budget) return nearestByScan(index, q, xy, maxD2, best, bd);
|
|
183
|
+
// Unvisited points lie beyond a face of the cells c - r .. c + r on some axis a, and
|
|
184
|
+
// inside the grid on the others: a lower bound on their squared distance
|
|
185
|
+
var bound2 = Infinity;
|
|
186
|
+
for (a = 0; a < axes; a++) {
|
|
187
|
+
var rest = 0;
|
|
188
|
+
for (var b = 0; b < axes; b++) if (b !== a) rest += out[b] * out[b];
|
|
189
|
+
if (c[a] - r > 0) bound2 = Math.min(bound2, gap(q[a] - (m[a] + (c[a] - r) * cs)) + rest);
|
|
190
|
+
if (c[a] + r < n[a] - 1) bound2 = Math.min(bound2, gap(m[a] + (c[a] + r + 1) * cs - q[a]) + rest);
|
|
191
|
+
}
|
|
192
|
+
if (bound2 === Infinity) break;
|
|
193
|
+
if (best >= 0 && bd < bound2) break;
|
|
194
|
+
if (bound2 > maxD2) break;
|
|
195
|
+
}
|
|
196
|
+
return best;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The occupied cells in order of distance, for queries a ring search would make slow
|
|
200
|
+
function nearestByScan(index, q, xy, maxD2, best, bd) {
|
|
201
|
+
var keys = index.keys, cand = [];
|
|
202
|
+
for (var s = 0; s < keys.length; s++) {
|
|
203
|
+
if (keys[s] < 0) continue;
|
|
204
|
+
var c = decode(index, keys[s]), cd = cellDist2(index, c[0], c[1], c[2], q, xy);
|
|
205
|
+
if (cd <= maxD2 && cd <= bd) cand.push([cd, s]);
|
|
206
|
+
}
|
|
207
|
+
cand.sort(function (u, v) { return u[0] - v[0]; });
|
|
208
|
+
var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
|
|
209
|
+
for (var i = 0; i < cand.length; i++) {
|
|
210
|
+
if (cand[i][0] > bd) break;
|
|
211
|
+
s = cand[i][1];
|
|
212
|
+
for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
|
|
213
|
+
var p = order[j], o = p * st;
|
|
214
|
+
var dx = X[o] - q[0], dy = Y[o] - q[1], dz = xy ? 0 : Z[o] - q[2];
|
|
215
|
+
var d = dx * dx + dy * dy + dz * dz;
|
|
216
|
+
if (d <= maxD2 && (d < bd || (d === bd && p < best))) { bd = d; best = p; }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return best;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Every point near a ray: within radius + growth · t of its axis, where t >= 0 is the
|
|
224
|
+
* point's distance along it (growth 0: a cylinder, as an orthographic view's pick ray;
|
|
225
|
+
* growth > 0: a cone, as a perspective camera's, growth = tan of the half-angle).
|
|
226
|
+
* The snap rules (front-most, priorities) are the caller's: pick from t and d.
|
|
227
|
+
* @param {object} index - PointIndex from buildIndex.
|
|
228
|
+
* @param {number[]} origin - [x, y, z].
|
|
229
|
+
* @param {number[]} direction - [x, y, z], any length > 0.
|
|
230
|
+
* @param {{radius: number, growth?: number, maxT?: number}} opts - maxT omitted: unlimited.
|
|
231
|
+
* @returns {{points: Uint32Array, t: Float64Array, d: Float64Array}} points ascending; t, d parallel
|
|
232
|
+
*/
|
|
233
|
+
export function pointsNearRay(index, origin, direction, opts) {
|
|
234
|
+
checkIndex(index, "pointsNearRay");
|
|
235
|
+
var o = checkPoint(origin, false, "origin");
|
|
236
|
+
var dir = checkPoint(direction, false, "direction");
|
|
237
|
+
var len = Math.sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
|
|
238
|
+
if (!(len > 0 && isFinite(len))) throw bad("direction must not be zero");
|
|
239
|
+
var u = [dir[0] / len, dir[1] / len, dir[2] / len];
|
|
240
|
+
opts = opts || {};
|
|
241
|
+
checkPositive(opts.radius, "radius");
|
|
242
|
+
var growth = opts.growth === undefined ? 0 : opts.growth;
|
|
243
|
+
if (!(fin(growth) && growth >= 0)) throw bad("growth must be a finite number >= 0, not " + String(growth));
|
|
244
|
+
checkPositive(opts.maxT, "maxT", true);
|
|
245
|
+
var radius = opts.radius, maxT = opts.maxT;
|
|
246
|
+
var points = [], ts = [], ds = [];
|
|
247
|
+
var co = index.coords, X = co.x, Y = co.y, Z = co.z, st = co.stride, order = index.order;
|
|
248
|
+
|
|
249
|
+
function check(s) {
|
|
250
|
+
for (var j = index.start[s], e = j + index.length[s]; j < e; j++) {
|
|
251
|
+
var p = order[j], q = p * st;
|
|
252
|
+
var vx = X[q] - o[0], vy = Y[q] - o[1], vz = Z[q] - o[2];
|
|
253
|
+
var t = vx * u[0] + vy * u[1] + vz * u[2];
|
|
254
|
+
if (t < 0 || (maxT !== undefined && t > maxT)) continue;
|
|
255
|
+
var cx = vy * u[2] - vz * u[1], cy = vz * u[0] - vx * u[2], cz = vx * u[1] - vy * u[0];
|
|
256
|
+
var d = Math.sqrt(cx * cx + cy * cy + cz * cz);
|
|
257
|
+
if (d <= radius + growth * t) { points.push(p); ts.push(t); ds.push(d); }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (index.count > 0) walk();
|
|
262
|
+
var idx = points.map(function (_, i) { return i; }).sort(function (a, b) { return points[a] - points[b]; });
|
|
263
|
+
return {
|
|
264
|
+
points: Uint32Array.from(idx, function (i) { return points[i]; }),
|
|
265
|
+
t: Float64Array.from(idx, function (i) { return ts[i]; }),
|
|
266
|
+
d: Float64Array.from(idx, function (i) { return ds[i]; })
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function walk() {
|
|
270
|
+
var cs = index.cellSize, m = index.min, n = dims(index);
|
|
271
|
+
var lo = [m[0], m[1], m[2]], hi = [m[0] + n[0] * cs, m[1] + n[1] * cs, m[2] + n[2] * cs];
|
|
272
|
+
// No point has a t beyond the far side of the grid's bounding sphere
|
|
273
|
+
var half = 0, dc = 0;
|
|
274
|
+
for (var a = 0; a < 3; a++) { var e = (hi[a] - lo[a]) / 2, w = (lo[a] + hi[a]) / 2 - o[a]; half += e * e; dc += w * w; }
|
|
275
|
+
var T = Math.sqrt(dc) + Math.sqrt(half);
|
|
276
|
+
if (maxT !== undefined && maxT < T) T = maxT;
|
|
277
|
+
var R = radius + growth * T + cs * EPS;
|
|
278
|
+
// Clip [0, T] to where the ray is within R of the grid's box (slab method)
|
|
279
|
+
var t0 = 0, t1 = T;
|
|
280
|
+
for (a = 0; a < 3; a++) {
|
|
281
|
+
if (u[a] === 0) {
|
|
282
|
+
if (o[a] < lo[a] - R || o[a] > hi[a] + R) return;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
var ta = (lo[a] - R - o[a]) / u[a], tb = (hi[a] + R - o[a]) / u[a];
|
|
286
|
+
if (ta > tb) { var sw = ta; ta = tb; tb = sw; }
|
|
287
|
+
if (ta > t0) t0 = ta;
|
|
288
|
+
if (tb < t1) t1 = tb;
|
|
289
|
+
}
|
|
290
|
+
if (t0 > t1) return;
|
|
291
|
+
// Many more cells along the way than occupied cells: test each occupied cell once
|
|
292
|
+
var across = 2 * R / cs + 1;
|
|
293
|
+
if (((t1 - t0) / cs + 1) * across * across > index.keys.length) return scanCells();
|
|
294
|
+
var seen = new Set();
|
|
295
|
+
for (var t = t0; t <= t1; t += cs) {
|
|
296
|
+
var t2 = Math.min(t + cs, t1), rr = radius + growth * t2;
|
|
297
|
+
var bl = [0, 0, 0], bh = [0, 0, 0];
|
|
298
|
+
for (a = 0; a < 3; a++) {
|
|
299
|
+
var pa = o[a] + t * u[a], pb = o[a] + t2 * u[a];
|
|
300
|
+
var r = cellRange(index, a, Math.min(pa, pb) - rr, Math.max(pa, pb) + rr);
|
|
301
|
+
bl[a] = r[0]; bh[a] = r[1];
|
|
302
|
+
}
|
|
303
|
+
visitBox(index, bl, bh, function (s) {
|
|
304
|
+
if (seen.has(s)) return;
|
|
305
|
+
seen.add(s);
|
|
306
|
+
check(s);
|
|
307
|
+
});
|
|
308
|
+
if (t2 >= t1) break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// A cell can hold an answer only if its centre is near enough: every point is
|
|
313
|
+
// within hd of the centre, so its t and d differ from the centre's by at most hd
|
|
314
|
+
function scanCells() {
|
|
315
|
+
var cs = index.cellSize, m = index.min, keys = index.keys, hd = cs * Math.sqrt(3) / 2 + cs * EPS;
|
|
316
|
+
for (var s = 0; s < keys.length; s++) {
|
|
317
|
+
if (keys[s] < 0) continue;
|
|
318
|
+
var c = decode(index, keys[s]);
|
|
319
|
+
var vx = m[0] + (c[0] + 0.5) * cs - o[0], vy = m[1] + (c[1] + 0.5) * cs - o[1], vz = m[2] + (c[2] + 0.5) * cs - o[2];
|
|
320
|
+
var tc = vx * u[0] + vy * u[1] + vz * u[2];
|
|
321
|
+
if (tc + hd < 0 || (maxT !== undefined && tc - hd > maxT)) continue;
|
|
322
|
+
var cx = vy * u[2] - vz * u[1], cy = vz * u[0] - vx * u[2], cz = vx * u[1] - vy * u[0];
|
|
323
|
+
if (Math.sqrt(cx * cx + cy * cy + cz * cz) - hd > radius + growth * Math.max(0, tc + hd)) continue;
|
|
324
|
+
check(s);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export { checkPoint, checkPositive, cellRange, cellDist2, decode };
|
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.6.1";
|
|
10
10
|
|
|
11
11
|
export { LasError, ERROR_CODES } from "./errors.js";
|
|
12
12
|
export { openLas, LasReader } from "./LasReader.js";
|
|
@@ -16,7 +16,14 @@ 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
18
|
export { makeKeep, countKept } from "./filter/filters.js";
|
|
19
|
-
export { dedupe } from "./filter/dedupe.js";
|
|
19
|
+
export { dedupe, dedupeSelection } from "./filter/dedupe.js";
|
|
20
|
+
export { buildIndex } from "./index/grid.js";
|
|
21
|
+
export { pointsInRadius, nearestPoint, pointsNearRay } from "./index/query.js";
|
|
22
|
+
export { selectBox, selectPolygon, selectClasses, selectReturns, thinSelection, unionSelections, intersectSelections, subtractSelections } from "./ops/select.js";
|
|
23
|
+
export { keepPoints, removePoints, splitPoints, mergePoints } from "./ops/apply.js";
|
|
24
|
+
export { makeTransform, transformPoints } from "./ops/transform.js";
|
|
25
|
+
export { interleavePositions } from "./ops/interleave.js";
|
|
26
|
+
export { selectSparse } from "./ops/sparse.js";
|
|
20
27
|
export { MemorySource } from "./io/MemorySource.js";
|
|
21
28
|
export { BlobSource } from "./io/BlobSource.js";
|
|
22
29
|
export { NodeFileSource } from "./io/NodeFileSource.js";
|