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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
Point operations.
|
|
6
|
+
|
|
7
|
+
- Operations on point columns (the object `readPoints` returns, or any `{ count, <name>: TypedArray }`
|
|
8
|
+
whose arrays are `count × k` long), with no dependencies. A selection is a `Uint32Array` of
|
|
9
|
+
strictly ascending point indices, checked on use (`INVALID_ARGUMENT` names the bad position).
|
|
10
|
+
- Select: `selectBox`, `selectPolygon` (even-odd; `boundary: "touching"` keeps points on the edge,
|
|
11
|
+
`"inside"` drops them; `tolerance`; `minZ` / `maxZ`), `selectClasses`, `selectReturns` (`returns`,
|
|
12
|
+
`first`, `last`, `single`, any one matches), `thinSelection` (the reader's `maxPoints` rule,
|
|
13
|
+
exported), and `unionSelections`, `intersectSelections`, `subtractSelections`.
|
|
14
|
+
- Apply: `keepPoints`, `removePoints`, `splitPoints`, `mergePoints`. Copies by default; `inPlace: true`
|
|
15
|
+
on keep and remove compacts each column inside its own buffer and returns views of it (columns that
|
|
16
|
+
share memory are refused). `mergePoints` takes the union of the columns, lists zero-filled ones
|
|
17
|
+
in `filled`, widens types (Float64 over Float32 over integers; Int32 or Uint32 with Float32 gives
|
|
18
|
+
Float64), and refuses a `k` mismatch, a column named `count` or `filled`, and signed with unsigned
|
|
19
|
+
integers unless the signed type is strictly wider.
|
|
20
|
+
- Transform: `makeTransform({ shift, rotateZ, rotate, scale, about })` builds a row-major `{ m, t }`;
|
|
21
|
+
`transformPoints(cols, T, { inPlace })` applies it in Float64 and returns a new `{ count, columns }`
|
|
22
|
+
object (a copy has new x, y, z and shares the rest; `inPlace` returns the input's arrays, overwritten).
|
|
23
|
+
- `interleavePositions(cols, { layout, into, at, origin, float32 })` packs x y (z) into a caller's
|
|
24
|
+
Float32 / Float64 buffer, shifting by `origin` in Float64 first.
|
|
25
|
+
- Reads: `index` field (each point's record number; Uint32, Float64 above 4,294,967,295 points);
|
|
26
|
+
`polygon` / `boundary` / `tolerance` crop while reading, so only the region is decoded and held;
|
|
27
|
+
`transform` applies in the fixed order record → world → T → minus `localOrigin` → Float32.
|
|
28
|
+
Filters apply in file coordinates; the result's bounds and `dedupe` are in output coordinates,
|
|
29
|
+
and the worker's elevation colour range follows the transform.
|
|
30
|
+
- Changed: an omitted (or `null`) `bounds` limit is now open. Before, a missing limit kept nothing.
|
|
31
|
+
`polygon: null` means no polygon. `selectBox` and the `minZ` / `maxZ` of `selectPolygon` read a
|
|
32
|
+
`null` bound as open too.
|
|
33
|
+
- `dedupe` moves every column (k values per point, such as an interleaved `positions` or `rgb`) with
|
|
34
|
+
the kept points; other properties pass through. `transformPoints` with `inPlace` refuses x, y or z
|
|
35
|
+
that share memory. `mergePoints` ignores a zero-point input when widening types.
|
|
36
|
+
- Demo: an Edit tab (polygon, box and class selections; keep, delete, split, transform, crop on
|
|
37
|
+
read) and LAZ export.
|
|
38
|
+
- Docs: README "Point operations".
|
|
39
|
+
|
|
3
40
|
## 0.4.0 (2026-10-22)
|
|
4
41
|
|
|
5
42
|
Writes LAZ.
|
package/README.md
CHANGED
|
@@ -7,8 +7,11 @@ runs in the browser and in Node — and built for files far larger than memory.
|
|
|
7
7
|
**Status: early (0.x).** It works and is tested, but the API may still change before 1.0.
|
|
8
8
|
|
|
9
9
|
- Working: `openLas` reads the header, VLRs / EVLRs and coordinate system of LAS 1.0–1.4;
|
|
10
|
-
`readPoints` and `chunks` decode every point format (0–10) a window at a time, with filters
|
|
11
|
-
|
|
10
|
+
`readPoints` and `chunks` decode every point format (0–10) a window at a time, with filters,
|
|
11
|
+
thinning, a polygon crop, a coordinate transform and the original point index; `colourize`
|
|
12
|
+
colours points; `writeLas` and `createLasWriter` write LAS 1.0–1.4.
|
|
13
|
+
- Point operations on columns: select by box, polygon, class or return; keep, remove, split and
|
|
14
|
+
merge; transform coordinates; interleave positions for a renderer. See Point operations.
|
|
12
15
|
- LAZ: point formats 0–3 and 6–8, read exactly like LAS — the same calls, the same options,
|
|
13
16
|
decoded chunk by chunk in plain JavaScript (no WebAssembly) — and written with
|
|
14
17
|
`compress: true`, byte for byte what PDAL (LAZperf) writes (bar one lossless exception, see
|
|
@@ -35,8 +38,9 @@ Drop a LAS or LAZ file onto the page. It is read locally (never uploaded), decod
|
|
|
35
38
|
library's Worker (LAZ on every core) behind a progress bar, then shown once in a Three.js viewer;
|
|
36
39
|
colour changes recolour the points already read, without reading the file again. The side panel shows the header, VLRs, coordinate
|
|
37
40
|
system and LAZ compression; reads with class, return, bounds, thinning and dedupe filters;
|
|
38
|
-
colours by RGB, intensity, elevation or class; lists point statistics;
|
|
39
|
-
|
|
41
|
+
colours by RGB, intensity, elevation or class; lists point statistics; selects points by polygon,
|
|
42
|
+
box or class and keeps, deletes, splits, transforms or crops them (the Edit tab); and saves the
|
|
43
|
+
points back out as LAS or LAZ. To run it locally: `npm run demo`, then open http://localhost:8080/.
|
|
40
44
|
|
|
41
45
|
## Reading
|
|
42
46
|
|
|
@@ -56,7 +60,11 @@ var pts = await las.readPoints({
|
|
|
56
60
|
fields: ["x", "y", "z", "classification", "red", "green", "blue"],
|
|
57
61
|
classes: [2], // keep only these classification codes
|
|
58
62
|
returns: [1], // keep only these return numbers
|
|
59
|
-
bounds: { minX, maxX, minY, maxY, minZ, maxZ },
|
|
63
|
+
bounds: { minX, maxX, minY, maxY, minZ, maxZ }, // any limit may be left out: an omitted limit is open
|
|
64
|
+
polygon: [x0, y0, x1, y1, x2, y2], // crop: keep points inside it (file coordinates, XY, even-odd); null is none
|
|
65
|
+
boundary: "touching", // polygon edge: "touching" keeps points on it (default), "inside" drops them
|
|
66
|
+
tolerance: 0, // file units: within this of a polygon edge counts as on it
|
|
67
|
+
transform: T, // { m, t } from makeTransform, applied to world coordinates (see Point operations)
|
|
60
68
|
maxPoints: 5000000, // even thinning; 0 keeps every point
|
|
61
69
|
dedupe: 0.01, // drop points within 1 cm (XY; dedupeZ: true for 3D)
|
|
62
70
|
localOrigin: { x: 478000, y: 6772000 }, float32: true, // small Float32 coordinates for the GPU
|
|
@@ -64,7 +72,7 @@ var pts = await las.readPoints({
|
|
|
64
72
|
signal: abortController.signal
|
|
65
73
|
});
|
|
66
74
|
pts.count; pts.x; pts.y; pts.z; // one typed array per field
|
|
67
|
-
pts.bounds; pts.stats; // world bounds, per-class counts
|
|
75
|
+
pts.bounds; pts.stats; // world bounds (after any transform), per-class counts
|
|
68
76
|
```
|
|
69
77
|
|
|
70
78
|
Fields: `x y z intensity returnNumber numberOfReturns scanDirection edgeOfFlightLine
|
|
@@ -72,10 +80,141 @@ classification classificationFlags scannerChannel scanAngle userData pointSource
|
|
|
72
80
|
red green blue nir`. Fields a point format does not hold are left out. `scanAngle` is in
|
|
73
81
|
degrees for every format.
|
|
74
82
|
|
|
83
|
+
`index` is one more field, for every format: the column is present for the points the filters and
|
|
84
|
+
`maxPoints` keep, and each value is that record's position in the file (0-based; it is not
|
|
85
|
+
renumbered after filtering). It is a `Uint32Array`, or a `Float64Array` for a file of more than
|
|
86
|
+
4,294,967,295 points. It is an ordinary column, so keep, remove, split and merge carry it, and it
|
|
87
|
+
tells you which records of the file are left.
|
|
88
|
+
|
|
75
89
|
Filters run on the raw record before it is decoded, so a filtered-out point costs a byte read.
|
|
90
|
+
`polygon` is a crop on read: only the points inside it are decoded and held, so a site region of
|
|
91
|
+
a large file never costs the whole file in memory. It needs at least 3 vertices (a flat list of
|
|
92
|
+
x, y pairs, implicitly closed) and combines with the other filters and `maxPoints`. A LAZ crop
|
|
93
|
+
saves memory and field decoding but still decompresses every chunk (there is no spatial index), so
|
|
94
|
+
its time is close to a full read.
|
|
76
95
|
`maxPoints` keeps exactly that many of the points that pass the filters, spread evenly over
|
|
77
96
|
the whole file; with filters set it first counts the passing points, which reads the file twice.
|
|
78
97
|
|
|
98
|
+
In `bounds`, a limit that is left out (or `null`) is open; before 0.5.0 a missing limit kept
|
|
99
|
+
nothing. `polygon: null` means no polygon. `boundary` and `tolerance` belong to `polygon`: a point
|
|
100
|
+
within `tolerance` of an edge or vertex is on the boundary, kept by `"touching"` and dropped by
|
|
101
|
+
`"inside"`. They are not `dedupe`'s tolerance, which is the value of the `dedupe` option.
|
|
102
|
+
|
|
103
|
+
`transform` moves the points while they are decoded, in a fixed order: record → world (scale and
|
|
104
|
+
offset) → `transform` (Float64) → minus `localOrigin` → Float32 (when `float32` and `localOrigin`
|
|
105
|
+
are set). Raw scanner coordinates never pass through Float32 at full magnitude. The filters
|
|
106
|
+
(`classes`, `returns`, `bounds`, `polygon`) apply in **file** coordinates, before the transform;
|
|
107
|
+
the result's `bounds`, its stats and `dedupe` are in the output coordinates. To crop a scan in
|
|
108
|
+
site coordinates, read it transformed and use `selectPolygon`. The Worker's elevation colour
|
|
109
|
+
range follows the transform.
|
|
110
|
+
|
|
111
|
+
## Point operations
|
|
112
|
+
|
|
113
|
+
Zero-dependency operations on **point columns**: the object `readPoints` returns, or any object
|
|
114
|
+
of the same shape, `{ count, <name>: TypedArray, ... }`. A property is a column when it is a typed
|
|
115
|
+
array whose length is `count × k` for a whole number `k` of 1 or more (`k` is 1 for `x` or
|
|
116
|
+
`classification`, 3 for an interleaved `positions` or `rgb`). Operations carry every column, `k`
|
|
117
|
+
values per point, and nothing else: `bounds`, `stats` and the like are not copied, so rebuild them
|
|
118
|
+
from the result. A **selection** is a `Uint32Array` of point indices, strictly ascending, each
|
|
119
|
+
below `count`. Everything that takes one checks it in one pass and throws `INVALID_ARGUMENT`
|
|
120
|
+
naming the first bad position; everything that makes one returns a valid one.
|
|
121
|
+
|
|
122
|
+
### Select
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import { selectBox, selectPolygon, selectClasses, selectReturns, thinSelection,
|
|
126
|
+
unionSelections, intersectSelections, subtractSelections } from "las-laz-js";
|
|
127
|
+
|
|
128
|
+
var inBox = selectBox(pts, { minX: 477010, maxX: 477050, minZ: 295 }); // inclusive; a limit left out is open
|
|
129
|
+
var inSite = selectPolygon(pts, poly, { boundary: "inside", tolerance: 0.001 }); // poly: [x0, y0, x1, y1, ...]
|
|
130
|
+
var ground = selectClasses(pts, [2]);
|
|
131
|
+
var last = selectReturns(pts, { last: true, single: true }); // returns: [..], first, last, single; any one matches
|
|
132
|
+
var thin = thinSelection(pts.count, 1000); // exactly min(count, 1000) indices, spread evenly
|
|
133
|
+
var wanted = intersectSelections(inSite, ground); // also unionSelections, subtractSelections
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`selectPolygon` takes a flat `[x0, y0, x1, y1, ...]` list of at least 3 vertices, closed for you,
|
|
137
|
+
in the columns' own coordinates; the interior is the even-odd rule, so a concave polygon works.
|
|
138
|
+
Points on the boundary are kept by `boundary: "touching"` (the default) and dropped by `"inside"`,
|
|
139
|
+
so the two differ by exactly the boundary points, and `splitPoints` by one polygon loses and
|
|
140
|
+
duplicates nothing. Coordinates are not exact in binary, so a point mathematically on an edge may not
|
|
141
|
+
count as on it at `tolerance: 0`; use a small tolerance (for example 1e-6 of the units) to catch
|
|
142
|
+
boundary points. `minZ` / `maxZ` add a height limit. `thinSelection` is the reader's
|
|
143
|
+
`maxPoints` rule: `keepPoints(all, thinSelection(n, t))` gives the points
|
|
144
|
+
`readPoints({ maxPoints: t })` does.
|
|
145
|
+
|
|
146
|
+
### Keep, remove, split, merge
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { keepPoints, removePoints, splitPoints, mergePoints } from "las-laz-js";
|
|
150
|
+
|
|
151
|
+
var kept = keepPoints(pts, wanted); // new columns of the same types
|
|
152
|
+
var without = removePoints(pts, wanted);
|
|
153
|
+
var [inside, outside] = splitPoints(pts, inSite); // always copies; every point lands in one result
|
|
154
|
+
var both = mergePoints([inside, outside]); // concatenated in order; both.filled is []
|
|
155
|
+
|
|
156
|
+
var crop = keepPoints(big, selectPolygon(big, poly), { inPlace: true }); // no second copy
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`inPlace: true` (for `keepPoints` and `removePoints`) slides the kept values down inside each
|
|
160
|
+
column's own buffer and returns views of that memory: the input is overwritten, and a big cloud is
|
|
161
|
+
cropped without a second copy. Columns that share memory (the same array twice, or overlapping
|
|
162
|
+
views) are refused, since compacting one would corrupt the other.
|
|
163
|
+
|
|
164
|
+
`mergePoints` takes the union of the column names; a column missing from an input is zero for
|
|
165
|
+
that input's points, and its name is listed in `result.filled`. A column named `count` or
|
|
166
|
+
`filled` is refused. A column's `k` must agree across inputs. The result's type is the widest:
|
|
167
|
+
Float64 over Float32 over the integer types; an Int32 or Uint32 with a Float32 gives Float64
|
|
168
|
+
(Float32 holds integers exactly only to 2^24). Signed with unsigned integers merge only when the
|
|
169
|
+
signed type is strictly wider (Int16 with Uint8 is Int16; Int8 with Uint8, or Int32 with Uint32,
|
|
170
|
+
is `INVALID_ARGUMENT`). An input with no points is ignored when the type is chosen, so an empty
|
|
171
|
+
Int8 column does not block a Uint16 one; a column only an empty input has keeps its type.
|
|
172
|
+
Coordinates must already share one frame.
|
|
173
|
+
|
|
174
|
+
### Transform
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { makeTransform, transformPoints } from "las-laz-js";
|
|
178
|
+
|
|
179
|
+
// p' = scale · R · (p − about) + about + shift; rotateZ is degrees anticlockwise about +Z
|
|
180
|
+
var T = makeTransform({ rotateZ: 90, about: [477050, 6770015, 0], shift: [10, 0, 0] });
|
|
181
|
+
// T = { m: [0, -1, 0, 1, 0, 0, 0, 0, 1], t: [7247075, 6292965, 0] } row-major: p' = m·p + t
|
|
182
|
+
|
|
183
|
+
var moved = transformPoints(pts, T); // { count, columns }: new x, y, z; the rest shared
|
|
184
|
+
var same = transformPoints(pts, T, { inPlace: true }); // the same x, y, z arrays, overwritten
|
|
185
|
+
|
|
186
|
+
var local = await las.readPoints({ // or while reading
|
|
187
|
+
fields: ["x", "y", "z"], transform: T,
|
|
188
|
+
localOrigin: { x: 477050, y: 6770015 }, float32: true
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A transform is a plain object, `{ m, t }`, with `m` nine numbers (a row-major 3×3) and `t` three.
|
|
193
|
+
Any affine map fits, so a fitted registration can be passed the same way; it is not Three.js's
|
|
194
|
+
column-major `Matrix4`. `makeTransform` takes `shift`, `about`, `scale` and either `rotateZ` or
|
|
195
|
+
`rotate` (a row-major 3×3; not both). `transformPoints` needs `x`, `y` and `z`, computes in
|
|
196
|
+
Float64 and stores in the columns' own types. It returns a new `{ count, columns }` object either
|
|
197
|
+
way: with `inPlace` the arrays in it are the input's, overwritten (and `x`, `y` and `z` must not
|
|
198
|
+
share memory, or it throws `INVALID_ARGUMENT`). On read, the order is the one
|
|
199
|
+
above.
|
|
200
|
+
|
|
201
|
+
### Interleave for a renderer
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
import { interleavePositions } from "las-laz-js";
|
|
205
|
+
|
|
206
|
+
var positions = interleavePositions(pts, { origin: { x: 477000, y: 6770000 } }); // Float32Array, x y z per point
|
|
207
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
208
|
+
|
|
209
|
+
// or into a buffer you own, starting at point 'at'; layout "xy" writes two values per point
|
|
210
|
+
interleavePositions(pts, { into: buffer, at: 50000, layout: "xyz", origin: { x: 477000, y: 6770000, z: 300 } });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The origin is subtracted in Float64 before storing, so world coordinates keep their decimals in a
|
|
214
|
+
Float32 buffer; an axis left out of `origin` is not shifted ({ x, y } keeps world Z). `float32`
|
|
215
|
+
defaults to true and picks the type of a new array; `into` must be a Float32Array or Float64Array
|
|
216
|
+
long enough, else `INVALID_ARGUMENT`.
|
|
217
|
+
|
|
79
218
|
## LAZ
|
|
80
219
|
|
|
81
220
|
A LAZ file is opened and read with the same calls as LAS. Each LASzip chunk (50,000 points in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "las-laz-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Zero-dependency JavaScript reader and writer for ASPRS LAS point clouds, and reader and writer for LAZ (LASzip), for the browser and Node, built for files larger than memory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/LasReader.js
CHANGED
|
@@ -25,6 +25,7 @@ import { createChunkDecoder } from "./laz/decompressChunk.js";
|
|
|
25
25
|
import { createLazPool, defaultThreads } from "./laz/pool.js";
|
|
26
26
|
import { makeKeep, countKept } from "./filter/filters.js";
|
|
27
27
|
import { dedupe } from "./filter/dedupe.js";
|
|
28
|
+
import { checkTransform } from "./ops/transform.js";
|
|
28
29
|
import { openScratchSyncWriter } from "./io/ScratchBinaryFile.js";
|
|
29
30
|
|
|
30
31
|
var DEFAULT_WINDOW = 64 * 1024 * 1024;
|
|
@@ -179,7 +180,7 @@ LasReader.prototype._plan = async function () {
|
|
|
179
180
|
|
|
180
181
|
/**
|
|
181
182
|
* Iterate the selected points window by window.
|
|
182
|
-
* @param {object} [opts] - fields, classes, returns, bounds, maxPoints, localOrigin, float32, onProgress, signal
|
|
183
|
+
* @param {object} [opts] - fields, classes, returns, bounds, polygon (crop; with boundary "touching" | "inside" and tolerance), maxPoints, transform ({ m, t } from makeTransform: world coordinates are transformed in Float64 before localOrigin is subtracted; filters apply in file coordinates), localOrigin, float32, onProgress, signal
|
|
183
184
|
* @returns {AsyncGenerator<object>} PointColumns per window
|
|
184
185
|
*/
|
|
185
186
|
LasReader.prototype.chunks = async function* (opts) {
|
|
@@ -192,6 +193,7 @@ LasReader.prototype.chunks = async function* (opts) {
|
|
|
192
193
|
// makes decodeRecords write straight into readPoints' result columns.
|
|
193
194
|
LasReader.prototype._scan = async function* (opts, into) {
|
|
194
195
|
var h = this.header;
|
|
196
|
+
if (opts.transform) checkTransform(opts.transform, "transform");
|
|
195
197
|
var plan = await this._plan();
|
|
196
198
|
var keep = makeKeep(opts, h);
|
|
197
199
|
var fields = resolveFields(h, opts.fields);
|
|
@@ -208,7 +210,7 @@ LasReader.prototype._scan = async function* (opts, into) {
|
|
|
208
210
|
var kept = 0, w = 0;
|
|
209
211
|
for await (var bytes of windowBytes(plan, pool, buf, opts.signal)) {
|
|
210
212
|
var win = list[w];
|
|
211
|
-
var cols = decodeRecords(bytes, h, { fields: fields, keep: keep, thin: thin, localOrigin: opts.localOrigin, float32: opts.float32, into: into ? into(win) : null });
|
|
213
|
+
var cols = decodeRecords(bytes, h, { fields: fields, keep: keep, thin: thin, localOrigin: opts.localOrigin, float32: opts.float32, transform: opts.transform, firstIndex: win.firstRecord, into: into ? into(win) : null });
|
|
212
214
|
kept += cols.count;
|
|
213
215
|
w++;
|
|
214
216
|
if (typeof opts.onProgress === "function") opts.onProgress(w / list.length, "read " + kept + " points");
|
|
@@ -257,7 +259,7 @@ LasReader.prototype._countPassing = async function (plan, pool, buf, keep, signa
|
|
|
257
259
|
|
|
258
260
|
/**
|
|
259
261
|
* Read the selected points into one set of columns.
|
|
260
|
-
* @param {object} [opts] - chunks() options plus dedupe (tolerance) and dedupeZ
|
|
262
|
+
* @param {object} [opts] - chunks() options plus dedupe (tolerance) and dedupeZ; dedupe runs on the output coordinates (after transform and localOrigin)
|
|
261
263
|
* @returns {Promise<object>} PointColumns plus bounds and stats
|
|
262
264
|
*/
|
|
263
265
|
LasReader.prototype.readPoints = async function (opts) {
|
|
@@ -273,7 +275,7 @@ LasReader.prototype.readPoints = async function (opts) {
|
|
|
273
275
|
var cap = !filtered ? (opts.maxPoints > 0 ? Math.min(opts.maxPoints, h.pointCount) : h.pointCount)
|
|
274
276
|
: (opts.maxPoints > 0 ? Math.min(opts.maxPoints, h.pointCount) : 0);
|
|
275
277
|
var acc = {}, n = 0;
|
|
276
|
-
fields.forEach(function (f) { acc[f] = new (columnType(f, opts))(cap || 1024); });
|
|
278
|
+
fields.forEach(function (f) { acc[f] = new (columnType(f, opts, h))(cap || 1024); });
|
|
277
279
|
function into(win) {
|
|
278
280
|
// A known cap is never exceeded, so it never grows; filters alone grow by doubling
|
|
279
281
|
if (!cap && n + win.count > acc[fields[0]].length) {
|
package/src/filter/dedupe.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// tolerance-sized cells, neighbours checked), ported from point
|
|
6
6
|
// objects to columns. XY by default (one Z per XY location, as a
|
|
7
7
|
// 2.5D surface wants); { z: true } compares in 3D.
|
|
8
|
-
// Every column follows the kept points.
|
|
8
|
+
// Every column follows the kept points (k values per point).
|
|
9
9
|
//==========================================================
|
|
10
10
|
|
|
11
11
|
import { LasError, ERROR_CODES } from "../errors.js";
|
|
12
|
+
import { columnsOf, gather, sameType } from "../ops/columns.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @param {object} cols - PointColumns ({ count, x, y, z?, ... }).
|
|
@@ -69,13 +70,21 @@ export function dedupe(cols, tolerance, opts) {
|
|
|
69
70
|
kept[nKept++] = i;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// Every column (k values per point) follows the kept points; anything else passes through
|
|
74
|
+
var sel = kept.subarray(0, nKept);
|
|
75
|
+
var ks = Object.create(null); // a column may be named like an Object.prototype member
|
|
76
|
+
columnsOf(cols).forEach(function (c) { ks[c.name] = c.k; });
|
|
72
77
|
var out = { count: nKept };
|
|
73
78
|
Object.keys(cols).forEach(function (name) {
|
|
79
|
+
if (name === "count") return;
|
|
74
80
|
var col = cols[name];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
var v = col;
|
|
82
|
+
if (Object.prototype.hasOwnProperty.call(ks, name)) {
|
|
83
|
+
v = sameType(col, nKept * ks[name]);
|
|
84
|
+
gather(col, v, ks[name], sel);
|
|
85
|
+
}
|
|
86
|
+
// defineProperty so a column named "__proto__" stays a column
|
|
87
|
+
Object.defineProperty(out, name, { value: v, enumerable: true, writable: true, configurable: true });
|
|
79
88
|
});
|
|
80
89
|
return out;
|
|
81
90
|
}
|
package/src/filter/filters.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//==========================================================
|
|
3
3
|
// Record filters that run on the RAW record, before decoding, so
|
|
4
4
|
// rejected points cost one or two byte reads (Kirra's classFilter
|
|
5
|
-
// trick, generalised): classification codes, return numbers
|
|
6
|
-
// 3D box. Filters combine with AND.
|
|
5
|
+
// trick, generalised): classification codes, return numbers, a
|
|
6
|
+
// 3D box and a polygon (crop on read). Filters combine with AND.
|
|
7
7
|
// maxPoints thinning is NOT Kirra's floor-stride-and-cap rule, which
|
|
8
8
|
// kept only the start of the file and, run before the filters,
|
|
9
9
|
// could leave nothing: decodeRecords thins the records that PASS,
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
//==========================================================
|
|
12
12
|
|
|
13
13
|
import { POINT_FORMATS } from "../format/PointFormats.js";
|
|
14
|
+
import { makePolygonTest } from "../ops/polygon.js";
|
|
14
15
|
|
|
15
16
|
var LE = true;
|
|
16
17
|
|
|
18
|
+
function limit(v, open) { return v === undefined || v === null ? open : v; }
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Build a keep(view, recordOffset) test for decodeRecords.
|
|
19
|
-
* @param {object} [filter] - { classes?: number[], returns?: number[], bounds?: {minX,maxX,minY,maxY,minZ,maxZ}
|
|
22
|
+
* @param {object} [filter] - { classes?: number[], returns?: number[], bounds?: {minX,maxX,minY,maxY,minZ,maxZ} (an omitted or null limit is open),
|
|
23
|
+
* polygon?: number[] (null or omitted: no crop) [x0, y0, x1, y1, ...] with boundary ("touching" | "inside") and tolerance as selectPolygon, in the file's world coordinates }
|
|
20
24
|
* @param {object} header - from parseHeader.
|
|
21
25
|
* @returns {function(DataView, number): boolean|null} null when there is nothing to filter
|
|
22
26
|
*/
|
|
@@ -38,15 +42,32 @@ export function makeKeep(filter, header) {
|
|
|
38
42
|
? function (v, at) { return ret[v.getUint8(at + 14) & 15] === 1; } // bits-ok
|
|
39
43
|
: function (v, at) { return ret[v.getUint8(at + 14) & 7] === 1; }); // bits-ok
|
|
40
44
|
}
|
|
45
|
+
var sc = header.scale, of = header.offset;
|
|
41
46
|
if (filter.bounds) {
|
|
42
|
-
|
|
47
|
+
// an omitted limit is open
|
|
48
|
+
var b = filter.bounds;
|
|
49
|
+
var minX = limit(b.minX, -Infinity), maxX = limit(b.maxX, Infinity);
|
|
50
|
+
var minY = limit(b.minY, -Infinity), maxY = limit(b.maxY, Infinity);
|
|
51
|
+
var minZ = limit(b.minZ, -Infinity), maxZ = limit(b.maxZ, Infinity);
|
|
52
|
+
tests.push(function (v, at) {
|
|
53
|
+
var x = v.getInt32(at, LE) * sc.x + of.x;
|
|
54
|
+
if (x < minX || x > maxX) return false;
|
|
55
|
+
var y = v.getInt32(at + 4, LE) * sc.y + of.y;
|
|
56
|
+
if (y < minY || y > maxY) return false;
|
|
57
|
+
var z = v.getInt32(at + 8, LE) * sc.z + of.z;
|
|
58
|
+
return z >= minZ && z <= maxZ;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (filter.polygon !== undefined && filter.polygon !== null) {
|
|
62
|
+
// the file's world x, y of the raw record, as the bounds test computes them; boundary and tolerance as selectPolygon
|
|
63
|
+
var poly = makePolygonTest(filter.polygon, { boundary: filter.boundary, tolerance: filter.tolerance });
|
|
64
|
+
var inPoly = poly.test, pminX = poly.minX, pmaxX = poly.maxX, pminY = poly.minY, pmaxY = poly.maxY;
|
|
43
65
|
tests.push(function (v, at) {
|
|
44
|
-
var x = v.getInt32(at, LE) *
|
|
45
|
-
if (x <
|
|
46
|
-
var y = v.getInt32(at + 4, LE) *
|
|
47
|
-
if (y <
|
|
48
|
-
|
|
49
|
-
return z >= b.minZ && z <= b.maxZ;
|
|
66
|
+
var x = v.getInt32(at, LE) * sc.x + of.x;
|
|
67
|
+
if (x < pminX || x > pmaxX) return false; // y is not decoded for a point outside the box in x
|
|
68
|
+
var y = v.getInt32(at + 4, LE) * sc.y + of.y;
|
|
69
|
+
if (y < pminY || y > pmaxY) return false;
|
|
70
|
+
return inPoly(x, y);
|
|
50
71
|
});
|
|
51
72
|
}
|
|
52
73
|
if (!tests.length) return null;
|
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.5.0";
|
|
10
10
|
|
|
11
11
|
export { LasError, ERROR_CODES } from "./errors.js";
|
|
12
12
|
export { openLas, LasReader } from "./LasReader.js";
|
|
@@ -17,6 +17,10 @@ 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
19
|
export { dedupe } from "./filter/dedupe.js";
|
|
20
|
+
export { selectBox, selectPolygon, selectClasses, selectReturns, thinSelection, unionSelections, intersectSelections, subtractSelections } from "./ops/select.js";
|
|
21
|
+
export { keepPoints, removePoints, splitPoints, mergePoints } from "./ops/apply.js";
|
|
22
|
+
export { makeTransform, transformPoints } from "./ops/transform.js";
|
|
23
|
+
export { interleavePositions } from "./ops/interleave.js";
|
|
20
24
|
export { MemorySource } from "./io/MemorySource.js";
|
|
21
25
|
export { BlobSource } from "./io/BlobSource.js";
|
|
22
26
|
export { NodeFileSource } from "./io/NodeFileSource.js";
|
package/src/ops/apply.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/ops/apply.js
|
|
2
|
+
//==========================================================
|
|
3
|
+
// Apply a selection to point columns: keepPoints, removePoints,
|
|
4
|
+
// splitPoints, and join clouds with mergePoints. Each carries every
|
|
5
|
+
// column, k values per point, and nothing else. Copy by default;
|
|
6
|
+
// { inPlace: true } slides the kept values down inside each column's
|
|
7
|
+
// own buffer and returns subarray views of it.
|
|
8
|
+
//==========================================================
|
|
9
|
+
|
|
10
|
+
import { LasError, ERROR_CODES } from "../errors.js";
|
|
11
|
+
import { columnsOf, checkSelection, countOf, widestType, sameType, gather, checkNoSharedMemory } from "./columns.js";
|
|
12
|
+
|
|
13
|
+
function bad(message) { return new LasError(ERROR_CODES.INVALID_ARGUMENT, message); }
|
|
14
|
+
|
|
15
|
+
// dst gets every point of src not in sel, in order. dst may be src: the
|
|
16
|
+
// write position never passes the read position.
|
|
17
|
+
function gatherComplement(src, dst, k, count, sel) {
|
|
18
|
+
var p = 0;
|
|
19
|
+
var w = 0;
|
|
20
|
+
for (var i = 0; i < count; i++) {
|
|
21
|
+
if (p < sel.length && sel[p] === i) { p++; continue; }
|
|
22
|
+
var from = i * k;
|
|
23
|
+
for (var j = 0; j < k; j++) dst[w + j] = src[from + j];
|
|
24
|
+
w += k;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A column on the output; defineProperty so a column named "__proto__" stays a column.
|
|
29
|
+
function setColumn(out, name, a) {
|
|
30
|
+
Object.defineProperty(out, name, { value: a, enumerable: true, writable: true, configurable: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Shared body of keep / remove / split: validate once, then one pass per column.
|
|
34
|
+
function pick(cols, sel, keep, inPlace) {
|
|
35
|
+
var count = countOf(cols);
|
|
36
|
+
var s = checkSelection(sel, count);
|
|
37
|
+
var n = keep ? s.length : count - s.length;
|
|
38
|
+
var out = { count: n };
|
|
39
|
+
var list = columnsOf(cols);
|
|
40
|
+
if (inPlace) checkNoSharedMemory(cols, list);
|
|
41
|
+
list.forEach(function (c) {
|
|
42
|
+
var src = cols[c.name];
|
|
43
|
+
var dst = inPlace ? src : sameType(src, n * c.k);
|
|
44
|
+
if (keep) gather(src, dst, c.k, s);
|
|
45
|
+
else gatherComplement(src, dst, c.k, count, s);
|
|
46
|
+
setColumn(out, c.name, inPlace ? src.subarray(0, n * c.k) : dst);
|
|
47
|
+
});
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The selected points of every column, in order.
|
|
53
|
+
* @param {object} cols - point columns { count, <name>: TypedArray }.
|
|
54
|
+
* @param {Uint32Array|number[]} sel - strictly ascending indices below cols.count.
|
|
55
|
+
* @param {{inPlace?: boolean}} [opts] - inPlace: slide the kept values down inside each column's own buffer and return subarray views of it (the input is overwritten; two columns sharing memory, the same array twice or overlapping views, throw INVALID_ARGUMENT, while distinct non-overlapping views of one buffer are fine). Default: new columns of the same types.
|
|
56
|
+
* @returns {object} { count, <every column> } and nothing else.
|
|
57
|
+
*/
|
|
58
|
+
export function keepPoints(cols, sel, opts) {
|
|
59
|
+
return pick(cols, sel, true, !!(opts && opts.inPlace));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every point except the selected ones, in order.
|
|
64
|
+
* @param {object} cols - point columns { count, <name>: TypedArray }.
|
|
65
|
+
* @param {Uint32Array|number[]} sel - strictly ascending indices below cols.count.
|
|
66
|
+
* @param {{inPlace?: boolean}} [opts] - inPlace as keepPoints.
|
|
67
|
+
* @returns {object} { count, <every column> } and nothing else.
|
|
68
|
+
*/
|
|
69
|
+
export function removePoints(cols, sel, opts) {
|
|
70
|
+
return pick(cols, sel, false, !!(opts && opts.inPlace));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Split into the selected points and the rest. Always copies; the input
|
|
75
|
+
* is untouched and every point lands in exactly one result.
|
|
76
|
+
* @param {object} cols - point columns { count, <name>: TypedArray }.
|
|
77
|
+
* @param {Uint32Array|number[]} sel - strictly ascending indices below cols.count.
|
|
78
|
+
* @returns {object[]} [inside, outside], each { count, <every column> }.
|
|
79
|
+
*/
|
|
80
|
+
export function splitPoints(cols, sel) {
|
|
81
|
+
var count = countOf(cols);
|
|
82
|
+
var s = checkSelection(sel, count);
|
|
83
|
+
return [pick(cols, s, true, false), pick(cols, s, false, false)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Concatenate point columns in order. The column set is the union by name
|
|
88
|
+
* in first-appearance order. A column missing from an input with points is
|
|
89
|
+
* 0 for those points and its name is listed in `filled`. A column's k must
|
|
90
|
+
* agree across inputs (a zero-point input has no k to disagree with); its
|
|
91
|
+
* type is the widest across the inputs that have points (a zero-point input
|
|
92
|
+
* is ignored, unless no input with points has the column), and merging signed with unsigned
|
|
93
|
+
* integers that would wrap throws. Coordinates must already share a frame.
|
|
94
|
+
* @param {object[]} list - point-column objects.
|
|
95
|
+
* @returns {object} { count, <every column>, filled: string[] }.
|
|
96
|
+
*/
|
|
97
|
+
export function mergePoints(list) {
|
|
98
|
+
if (!Array.isArray(list)) throw bad("list must be an Array of point columns");
|
|
99
|
+
var total = 0;
|
|
100
|
+
var info = Object.create(null);
|
|
101
|
+
var names = [];
|
|
102
|
+
var perInput = list.map(function (cols, li) {
|
|
103
|
+
var n, entries;
|
|
104
|
+
try {
|
|
105
|
+
n = countOf(cols);
|
|
106
|
+
entries = columnsOf(cols);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw bad("list[" + li + "]: " + err.message);
|
|
109
|
+
}
|
|
110
|
+
var found = Object.create(null);
|
|
111
|
+
entries.forEach(function (c) {
|
|
112
|
+
if (c.name === "count" || c.name === "filled") throw bad("column \"" + c.name + "\" in list[" + li + "] is a reserved name; rename it before merging");
|
|
113
|
+
found[c.name] = true;
|
|
114
|
+
var e = info[c.name];
|
|
115
|
+
if (!e) { e = info[c.name] = { k: 0, types: [], emptyTypes: [] }; names.push(c.name); }
|
|
116
|
+
if (n > 0) {
|
|
117
|
+
if (e.k !== 0 && e.k !== c.k) throw bad("column \"" + c.name + "\" has k = " + e.k + " in an earlier input but k = " + c.k + " in list[" + li + "]");
|
|
118
|
+
e.k = c.k;
|
|
119
|
+
}
|
|
120
|
+
(n > 0 ? e.types : e.emptyTypes).push(globalThis[Object.prototype.toString.call(cols[c.name]).slice(8, -1)]);
|
|
121
|
+
});
|
|
122
|
+
total += n;
|
|
123
|
+
return { n: n, found: found };
|
|
124
|
+
});
|
|
125
|
+
var out = { count: total };
|
|
126
|
+
var filled = [];
|
|
127
|
+
names.forEach(function (name) {
|
|
128
|
+
var e = info[name];
|
|
129
|
+
var k = e.k || 1;
|
|
130
|
+
var Type = widestType(e.types.length > 0 ? e.types : e.emptyTypes, name);
|
|
131
|
+
var dst = new Type(total * k);
|
|
132
|
+
var at = 0;
|
|
133
|
+
perInput.forEach(function (inp, i) {
|
|
134
|
+
if (inp.n > 0) {
|
|
135
|
+
if (inp.found[name]) dst.set(list[i][name], at * k);
|
|
136
|
+
else if (filled.indexOf(name) < 0) filled.push(name);
|
|
137
|
+
}
|
|
138
|
+
at += inp.n;
|
|
139
|
+
});
|
|
140
|
+
setColumn(out, name, dst);
|
|
141
|
+
});
|
|
142
|
+
out.filled = names.filter(function (name) { return filled.indexOf(name) >= 0; });
|
|
143
|
+
return out;
|
|
144
|
+
}
|