@math.gl/wkb 5.0.0-alpha.2
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/LICENSE +140 -0
- package/README.md +232 -0
- package/dist/index.cjs +947 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +34 -0
- package/dist/types.js.map +1 -0
- package/dist/wkb-builder.d.ts +87 -0
- package/dist/wkb-builder.d.ts.map +1 -0
- package/dist/wkb-builder.js +259 -0
- package/dist/wkb-builder.js.map +1 -0
- package/dist/wkb-reader.d.ts +74 -0
- package/dist/wkb-reader.d.ts.map +1 -0
- package/dist/wkb-reader.js +249 -0
- package/dist/wkb-reader.js.map +1 -0
- package/dist/wkb.d.ts +21 -0
- package/dist/wkb.d.ts.map +1 -0
- package/dist/wkb.js +204 -0
- package/dist/wkb.js.map +1 -0
- package/dist/wkt.d.ts +6 -0
- package/dist/wkt.d.ts.map +1 -0
- package/dist/wkt.js +224 -0
- package/dist/wkt.js.map +1 -0
- package/package.json +37 -0
- package/src/index.ts +37 -0
- package/src/types.ts +54 -0
- package/src/wkb-builder.ts +367 -0
- package/src/wkb-reader.ts +400 -0
- package/src/wkb.ts +253 -0
- package/src/wkt.ts +233 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {WellKnownDimension} from './types';
|
|
6
|
+
import type {WKBGeometryType} from './wkb-reader';
|
|
7
|
+
|
|
8
|
+
/** Function that writes one WKB geometry into a builder. */
|
|
9
|
+
export type WKBGeometryWriter = (builder: WKBBuilder) => void;
|
|
10
|
+
|
|
11
|
+
/** Transforms one complete coordinate before it is written. */
|
|
12
|
+
export type WKBCoordinateTransform = (
|
|
13
|
+
coordinate: readonly number[],
|
|
14
|
+
dimension: WellKnownDimension
|
|
15
|
+
) => readonly number[];
|
|
16
|
+
|
|
17
|
+
/** Options shared by WKB measuring and writing modes. */
|
|
18
|
+
export type WKBBuilderBaseOptions = Readonly<{
|
|
19
|
+
/** Coordinate dimension written by every geometry header. Defaults to `xy`. */
|
|
20
|
+
dimension?: WellKnownDimension;
|
|
21
|
+
/** Byte order written by every geometry header. Defaults to little endian. */
|
|
22
|
+
byteOrder?: 'little-endian' | 'big-endian';
|
|
23
|
+
/** Optional EWKB spatial reference identifier. */
|
|
24
|
+
srid?: number;
|
|
25
|
+
/** Optional coordinate transform applied during writing and measurement. */
|
|
26
|
+
transform?: WKBCoordinateTransform;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
/** Options for a builder that only measures byte length. */
|
|
30
|
+
export type WKBBuilderMeasureOptions = WKBBuilderBaseOptions & Readonly<{mode: 'measure'}>;
|
|
31
|
+
|
|
32
|
+
/** Options for a builder that writes into caller-owned storage. */
|
|
33
|
+
export type WKBBuilderWriteOptions = WKBBuilderBaseOptions &
|
|
34
|
+
Readonly<{
|
|
35
|
+
mode: 'write';
|
|
36
|
+
target: ArrayBufferLike | ArrayBufferView;
|
|
37
|
+
/** Starting byte offset within `target`. Defaults to zero. */
|
|
38
|
+
byteOffset?: number;
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
/** Constructor options for {@link WKBBuilder}. */
|
|
42
|
+
export type WKBBuilderOptions = WKBBuilderMeasureOptions | WKBBuilderWriteOptions;
|
|
43
|
+
|
|
44
|
+
/** Plain offset, value, and validity buffers suitable for Arrow Binary adapters. */
|
|
45
|
+
export type WKBGeometryArray = Readonly<{
|
|
46
|
+
valueOffsets: Int32Array;
|
|
47
|
+
values: Uint8Array;
|
|
48
|
+
nullBitmap?: Uint8Array;
|
|
49
|
+
nullCount: number;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
const GEOMETRY_TYPE_CODES: Readonly<Record<WKBGeometryType, number>> = {
|
|
53
|
+
Point: 1,
|
|
54
|
+
LineString: 2,
|
|
55
|
+
Polygon: 3,
|
|
56
|
+
MultiPoint: 4,
|
|
57
|
+
MultiLineString: 5,
|
|
58
|
+
MultiPolygon: 6,
|
|
59
|
+
GeometryCollection: 7
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Incremental WKB writer with matching measure and caller-buffer write modes.
|
|
64
|
+
*
|
|
65
|
+
* Callers run the same event sequence once in `measure` mode and once in `write`
|
|
66
|
+
* mode. No Arrow or GeoJSON objects are created by the builder.
|
|
67
|
+
*/
|
|
68
|
+
export class WKBBuilder {
|
|
69
|
+
readonly mode: 'measure' | 'write';
|
|
70
|
+
readonly dimension: WellKnownDimension;
|
|
71
|
+
readonly littleEndian: boolean;
|
|
72
|
+
readonly srid?: number;
|
|
73
|
+
readonly transform?: WKBCoordinateTransform;
|
|
74
|
+
|
|
75
|
+
private readonly dataView: DataView | null;
|
|
76
|
+
private readonly startByteOffset: number;
|
|
77
|
+
private readonly endByteOffset: number;
|
|
78
|
+
private byteOffset: number;
|
|
79
|
+
|
|
80
|
+
constructor(options: WKBBuilderOptions) {
|
|
81
|
+
this.mode = options.mode;
|
|
82
|
+
this.dimension = options.dimension ?? 'xy';
|
|
83
|
+
this.littleEndian = options.byteOrder !== 'big-endian';
|
|
84
|
+
this.srid = options.srid;
|
|
85
|
+
this.transform = options.transform;
|
|
86
|
+
validateSrid(this.srid);
|
|
87
|
+
|
|
88
|
+
if (options.mode === 'write') {
|
|
89
|
+
const dataView = getTargetDataView(options.target);
|
|
90
|
+
const byteOffset = options.byteOffset ?? 0;
|
|
91
|
+
if (!Number.isSafeInteger(byteOffset) || byteOffset < 0 || byteOffset > dataView.byteLength) {
|
|
92
|
+
throw new Error('WKBBuilder byteOffset is outside the target');
|
|
93
|
+
}
|
|
94
|
+
this.dataView = dataView;
|
|
95
|
+
this.startByteOffset = byteOffset;
|
|
96
|
+
this.endByteOffset = dataView.byteLength;
|
|
97
|
+
this.byteOffset = byteOffset;
|
|
98
|
+
} else {
|
|
99
|
+
this.dataView = null;
|
|
100
|
+
this.startByteOffset = 0;
|
|
101
|
+
this.endByteOffset = Number.POSITIVE_INFINITY;
|
|
102
|
+
this.byteOffset = 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Begins a geometry and writes its count field when required. */
|
|
107
|
+
beginGeometry(type: WKBGeometryType, count?: number): void {
|
|
108
|
+
switch (type) {
|
|
109
|
+
case 'Point':
|
|
110
|
+
this.beginPoint();
|
|
111
|
+
break;
|
|
112
|
+
case 'LineString':
|
|
113
|
+
this.beginLineString(count ?? 0);
|
|
114
|
+
break;
|
|
115
|
+
case 'Polygon':
|
|
116
|
+
this.beginPolygon(count ?? 0);
|
|
117
|
+
break;
|
|
118
|
+
case 'MultiPoint':
|
|
119
|
+
this.beginMultiPoint(count ?? 0);
|
|
120
|
+
break;
|
|
121
|
+
case 'MultiLineString':
|
|
122
|
+
this.beginMultiLineString(count ?? 0);
|
|
123
|
+
break;
|
|
124
|
+
case 'MultiPolygon':
|
|
125
|
+
this.beginMultiPolygon(count ?? 0);
|
|
126
|
+
break;
|
|
127
|
+
case 'GeometryCollection':
|
|
128
|
+
this.writeHeader('GeometryCollection');
|
|
129
|
+
this.writeUint32(count ?? 0);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Begins one point geometry. */
|
|
135
|
+
beginPoint(): void {
|
|
136
|
+
this.writeHeader('Point');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Begins one linestring geometry. */
|
|
140
|
+
beginLineString(pointCount: number): void {
|
|
141
|
+
this.writeHeader('LineString');
|
|
142
|
+
this.writeUint32(pointCount);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Begins one polygon geometry. */
|
|
146
|
+
beginPolygon(ringCount: number): void {
|
|
147
|
+
this.writeHeader('Polygon');
|
|
148
|
+
this.writeUint32(ringCount);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Begins one linear ring inside a polygon. */
|
|
152
|
+
beginLinearRing(pointCount: number): void {
|
|
153
|
+
this.writeUint32(pointCount);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Begins one multipoint geometry. */
|
|
157
|
+
beginMultiPoint(pointCount: number): void {
|
|
158
|
+
this.writeHeader('MultiPoint');
|
|
159
|
+
this.writeUint32(pointCount);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Begins one multilinestring geometry. */
|
|
163
|
+
beginMultiLineString(lineCount: number): void {
|
|
164
|
+
this.writeHeader('MultiLineString');
|
|
165
|
+
this.writeUint32(lineCount);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Begins one multipolygon geometry. */
|
|
169
|
+
beginMultiPolygon(polygonCount: number): void {
|
|
170
|
+
this.writeHeader('MultiPolygon');
|
|
171
|
+
this.writeUint32(polygonCount);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Writes one coordinate using the builder's semantic dimension. */
|
|
175
|
+
writeCoordinate(x: number, y: number, z?: number, m?: number): void {
|
|
176
|
+
let coordinate = makeCoordinate(this.dimension, x, y, z, m);
|
|
177
|
+
if (this.transform) coordinate = this.transform(coordinate, this.dimension);
|
|
178
|
+
const coordinateSize = getDimensionSize(this.dimension);
|
|
179
|
+
for (let index = 0; index < coordinateSize; index++) {
|
|
180
|
+
this.writeFloat64(coordinate[index] ?? Number.NaN);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Returns the number of bytes measured or written. */
|
|
185
|
+
finishGeometry(): number {
|
|
186
|
+
return this.byteOffset - this.startByteOffset;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Measures geometry callbacks and returns contiguous Binary offsets. */
|
|
190
|
+
static measureGeometryArray(
|
|
191
|
+
geometryWriters: readonly (WKBGeometryWriter | null | undefined)[],
|
|
192
|
+
options: WKBBuilderBaseOptions = {}
|
|
193
|
+
): Int32Array {
|
|
194
|
+
const valueOffsets = new Int32Array(geometryWriters.length + 1);
|
|
195
|
+
for (let geometryIndex = 0; geometryIndex < geometryWriters.length; geometryIndex++) {
|
|
196
|
+
const geometryWriter = geometryWriters[geometryIndex];
|
|
197
|
+
const byteLength = geometryWriter ? measureGeometry(geometryWriter, options) : 0;
|
|
198
|
+
const nextOffset = valueOffsets[geometryIndex] + byteLength;
|
|
199
|
+
if (nextOffset > 0x7fffffff) throw new Error('WKB geometry array exceeds Int32 offsets');
|
|
200
|
+
valueOffsets[geometryIndex + 1] = nextOffset;
|
|
201
|
+
}
|
|
202
|
+
return valueOffsets;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Writes geometry callbacks into an existing contiguous values buffer. */
|
|
206
|
+
static writeGeometryArray(
|
|
207
|
+
geometryWriters: readonly (WKBGeometryWriter | null | undefined)[],
|
|
208
|
+
valueOffsets: Int32Array,
|
|
209
|
+
values: Uint8Array,
|
|
210
|
+
options: WKBBuilderBaseOptions = {}
|
|
211
|
+
): Uint8Array {
|
|
212
|
+
if (valueOffsets.length !== geometryWriters.length + 1) {
|
|
213
|
+
throw new Error('WKB valueOffsets length must equal geometry count plus one');
|
|
214
|
+
}
|
|
215
|
+
if (valueOffsets[valueOffsets.length - 1] > values.byteLength) {
|
|
216
|
+
throw new Error('WKB values buffer is smaller than its final offset');
|
|
217
|
+
}
|
|
218
|
+
for (let geometryIndex = 0; geometryIndex < geometryWriters.length; geometryIndex++) {
|
|
219
|
+
const geometryWriter = geometryWriters[geometryIndex];
|
|
220
|
+
if (!geometryWriter) continue;
|
|
221
|
+
const builder = new WKBBuilder({
|
|
222
|
+
mode: 'write',
|
|
223
|
+
target: values,
|
|
224
|
+
byteOffset: valueOffsets[geometryIndex],
|
|
225
|
+
...options
|
|
226
|
+
});
|
|
227
|
+
geometryWriter(builder);
|
|
228
|
+
const byteLength = builder.finishGeometry();
|
|
229
|
+
if (valueOffsets[geometryIndex] + byteLength !== valueOffsets[geometryIndex + 1]) {
|
|
230
|
+
throw new Error('WKB measure and write passes produced different byte lengths');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return values;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Builds plain offsets, values, and validity buffers in two passes. */
|
|
237
|
+
static buildGeometryArray(
|
|
238
|
+
geometryWriters: readonly (WKBGeometryWriter | null | undefined)[],
|
|
239
|
+
options: WKBBuilderBaseOptions = {}
|
|
240
|
+
): WKBGeometryArray {
|
|
241
|
+
const valueOffsets = WKBBuilder.measureGeometryArray(geometryWriters, options);
|
|
242
|
+
const values = new Uint8Array(valueOffsets[valueOffsets.length - 1]);
|
|
243
|
+
WKBBuilder.writeGeometryArray(geometryWriters, valueOffsets, values, options);
|
|
244
|
+
const {nullBitmap, nullCount} = makeNullBitmap(geometryWriters);
|
|
245
|
+
return {
|
|
246
|
+
valueOffsets,
|
|
247
|
+
values,
|
|
248
|
+
...(nullCount > 0 ? {nullBitmap} : {}),
|
|
249
|
+
nullCount
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private writeHeader(geometryType: WKBGeometryType): void {
|
|
254
|
+
this.writeUint8(this.littleEndian ? 1 : 0);
|
|
255
|
+
this.writeUint32(getWKBTypeCode(geometryType, this.dimension, this.srid !== undefined));
|
|
256
|
+
if (this.srid !== undefined) this.writeUint32(this.srid);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private writeUint8(value: number): void {
|
|
260
|
+
this.ensureSize(1);
|
|
261
|
+
this.dataView?.setUint8(this.byteOffset, value);
|
|
262
|
+
this.byteOffset++;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private writeUint32(value: number): void {
|
|
266
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) {
|
|
267
|
+
throw new Error('WKB count must be an unsigned 32-bit integer');
|
|
268
|
+
}
|
|
269
|
+
this.ensureSize(4);
|
|
270
|
+
this.dataView?.setUint32(this.byteOffset, value, this.littleEndian);
|
|
271
|
+
this.byteOffset += 4;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private writeFloat64(value: number): void {
|
|
275
|
+
this.ensureSize(8);
|
|
276
|
+
this.dataView?.setFloat64(this.byteOffset, value, this.littleEndian);
|
|
277
|
+
this.byteOffset += 8;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private ensureSize(byteLength: number): void {
|
|
281
|
+
if (this.byteOffset + byteLength > this.endByteOffset) {
|
|
282
|
+
throw new Error('WKBBuilder target buffer overflow');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function measureGeometry(
|
|
288
|
+
geometryWriter: WKBGeometryWriter,
|
|
289
|
+
options: WKBBuilderBaseOptions
|
|
290
|
+
): number {
|
|
291
|
+
const builder = new WKBBuilder({mode: 'measure', ...options});
|
|
292
|
+
geometryWriter(builder);
|
|
293
|
+
return builder.finishGeometry();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getTargetDataView(target: ArrayBufferLike | ArrayBufferView): DataView {
|
|
297
|
+
return ArrayBuffer.isView(target)
|
|
298
|
+
? new DataView(target.buffer, target.byteOffset, target.byteLength)
|
|
299
|
+
: new DataView(target);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getWKBTypeCode(
|
|
303
|
+
geometryType: WKBGeometryType,
|
|
304
|
+
dimension: WellKnownDimension,
|
|
305
|
+
hasSrid: boolean
|
|
306
|
+
): number {
|
|
307
|
+
const geometryCode = GEOMETRY_TYPE_CODES[geometryType];
|
|
308
|
+
if (hasSrid) {
|
|
309
|
+
const dimensionFlags =
|
|
310
|
+
dimension === 'xyz'
|
|
311
|
+
? 0x80000000
|
|
312
|
+
: dimension === 'xym'
|
|
313
|
+
? 0x40000000
|
|
314
|
+
: dimension === 'xyzm'
|
|
315
|
+
? 0xc0000000
|
|
316
|
+
: 0;
|
|
317
|
+
return (geometryCode | dimensionFlags | 0x20000000) >>> 0;
|
|
318
|
+
}
|
|
319
|
+
const dimensionOffset =
|
|
320
|
+
dimension === 'xyz' ? 1000 : dimension === 'xym' ? 2000 : dimension === 'xyzm' ? 3000 : 0;
|
|
321
|
+
return geometryCode + dimensionOffset;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function makeCoordinate(
|
|
325
|
+
dimension: WellKnownDimension,
|
|
326
|
+
x: number,
|
|
327
|
+
y: number,
|
|
328
|
+
z?: number,
|
|
329
|
+
m?: number
|
|
330
|
+
): readonly number[] {
|
|
331
|
+
switch (dimension) {
|
|
332
|
+
case 'xy':
|
|
333
|
+
return [x, y];
|
|
334
|
+
case 'xyz':
|
|
335
|
+
return [x, y, z ?? Number.NaN];
|
|
336
|
+
case 'xym':
|
|
337
|
+
return [x, y, m ?? Number.NaN];
|
|
338
|
+
case 'xyzm':
|
|
339
|
+
return [x, y, z ?? Number.NaN, m ?? Number.NaN];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function getDimensionSize(dimension: WellKnownDimension): 2 | 3 | 4 {
|
|
344
|
+
return dimension === 'xy' ? 2 : dimension === 'xyzm' ? 4 : 3;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function validateSrid(srid: number | undefined): void {
|
|
348
|
+
if (srid !== undefined && (!Number.isSafeInteger(srid) || srid < 0 || srid > 0xffffffff)) {
|
|
349
|
+
throw new Error('WKBBuilder srid must be an unsigned 32-bit integer');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function makeNullBitmap(geometryWriters: readonly (WKBGeometryWriter | null | undefined)[]): {
|
|
354
|
+
nullBitmap: Uint8Array;
|
|
355
|
+
nullCount: number;
|
|
356
|
+
} {
|
|
357
|
+
const nullBitmap = new Uint8Array(Math.ceil(geometryWriters.length / 8));
|
|
358
|
+
let nullCount = 0;
|
|
359
|
+
for (let geometryIndex = 0; geometryIndex < geometryWriters.length; geometryIndex++) {
|
|
360
|
+
if (geometryWriters[geometryIndex]) {
|
|
361
|
+
nullBitmap[geometryIndex >> 3] |= 1 << (geometryIndex & 7);
|
|
362
|
+
} else {
|
|
363
|
+
nullCount++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return {nullBitmap, nullCount};
|
|
367
|
+
}
|