@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,400 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {WellKnownDimension, WellKnownGeometry} from './types';
|
|
6
|
+
import {getWellKnownDimensionSize} from './types';
|
|
7
|
+
|
|
8
|
+
/** Geometry family encoded by a WKB header. */
|
|
9
|
+
export type WKBGeometryType = WellKnownGeometry['type'];
|
|
10
|
+
|
|
11
|
+
/** WKB dialect identified from one geometry header. */
|
|
12
|
+
export type WKBDialect = 'wkb' | 'iso-wkb' | 'ewkb';
|
|
13
|
+
|
|
14
|
+
/** Immutable facts decoded from one WKB geometry header. */
|
|
15
|
+
export type WKBHeader = Readonly<{
|
|
16
|
+
geometryType: WKBGeometryType;
|
|
17
|
+
dimension: WellKnownDimension;
|
|
18
|
+
dialect: WKBDialect;
|
|
19
|
+
littleEndian: boolean;
|
|
20
|
+
/** Offset of the endian byte, relative to the supplied input view. */
|
|
21
|
+
byteOffset: number;
|
|
22
|
+
/** Offset of the geometry body, relative to the supplied input view. */
|
|
23
|
+
bodyByteOffset: number;
|
|
24
|
+
/** Number of bytes occupied by the header. */
|
|
25
|
+
byteLength: number;
|
|
26
|
+
/** EWKB spatial reference identifier, when present. */
|
|
27
|
+
srid?: number;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
/** Defensive limits shared by WKB traversal and scanning. */
|
|
31
|
+
export type WKBTraversalOptions = Readonly<{
|
|
32
|
+
/** Maximum recursive geometry nesting. Defaults to 64. */
|
|
33
|
+
maximumDepth?: number;
|
|
34
|
+
/** Maximum total declared list elements. Defaults to 100 million. */
|
|
35
|
+
maximumElements?: number;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
/** Direct callbacks invoked while traversing WKB bytes without materializing geometry rows. */
|
|
39
|
+
export type WKBVisitor = Readonly<{
|
|
40
|
+
/** Called at the start of each geometry. `count` is points, rings, or child geometries. */
|
|
41
|
+
geometry?: (header: WKBHeader, count: number | undefined, depth: number) => void;
|
|
42
|
+
/** Called at the start of each polygon ring. */
|
|
43
|
+
ring?: (pointCount: number, ringIndex: number, depth: number) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called for each coordinate without allocating a coordinate array.
|
|
46
|
+
* Missing Z or M ordinates are passed as `undefined`.
|
|
47
|
+
*/
|
|
48
|
+
coordinate?: (
|
|
49
|
+
x: number,
|
|
50
|
+
y: number,
|
|
51
|
+
z: number | undefined,
|
|
52
|
+
m: number | undefined,
|
|
53
|
+
dimension: WellKnownDimension,
|
|
54
|
+
byteOffset: number,
|
|
55
|
+
depth: number
|
|
56
|
+
) => void;
|
|
57
|
+
}>;
|
|
58
|
+
|
|
59
|
+
/** Finite coordinate bounds collected by {@link scanWKB}. */
|
|
60
|
+
export type WKBBounds = Readonly<{
|
|
61
|
+
xmin: number;
|
|
62
|
+
ymin: number;
|
|
63
|
+
xmax: number;
|
|
64
|
+
ymax: number;
|
|
65
|
+
zmin?: number;
|
|
66
|
+
zmax?: number;
|
|
67
|
+
mmin?: number;
|
|
68
|
+
mmax?: number;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
/** Per-family geometry counts collected by {@link scanWKB}. */
|
|
72
|
+
export type WKBGeometryCounts = Readonly<Record<WKBGeometryType, number>>;
|
|
73
|
+
|
|
74
|
+
/** Structural statistics collected without materializing geometry rows. */
|
|
75
|
+
export type WKBScanResult = Readonly<{
|
|
76
|
+
header: WKBHeader;
|
|
77
|
+
byteLength: number;
|
|
78
|
+
coordinateCount: number;
|
|
79
|
+
ringCount: number;
|
|
80
|
+
geometryCount: number;
|
|
81
|
+
maximumDepth: number;
|
|
82
|
+
geometryTypes: readonly WKBGeometryType[];
|
|
83
|
+
geometryCounts: WKBGeometryCounts;
|
|
84
|
+
bounds?: WKBBounds;
|
|
85
|
+
}>;
|
|
86
|
+
|
|
87
|
+
type MutableBounds = {
|
|
88
|
+
xmin?: number;
|
|
89
|
+
ymin?: number;
|
|
90
|
+
xmax?: number;
|
|
91
|
+
ymax?: number;
|
|
92
|
+
zmin?: number;
|
|
93
|
+
zmax?: number;
|
|
94
|
+
mmin?: number;
|
|
95
|
+
mmax?: number;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type TraversalState = {
|
|
99
|
+
maximumDepth: number;
|
|
100
|
+
maximumElements: number;
|
|
101
|
+
elementCount: number;
|
|
102
|
+
visitor: WKBVisitor;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const GEOMETRY_TYPES: readonly WKBGeometryType[] = [
|
|
106
|
+
'Point',
|
|
107
|
+
'LineString',
|
|
108
|
+
'Polygon',
|
|
109
|
+
'MultiPoint',
|
|
110
|
+
'MultiLineString',
|
|
111
|
+
'MultiPolygon',
|
|
112
|
+
'GeometryCollection'
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** Inspects one WKB/ISO WKB/EWKB header without reading its coordinate payload. */
|
|
116
|
+
export function inspectWKBHeader(
|
|
117
|
+
input: ArrayBufferLike | ArrayBufferView,
|
|
118
|
+
byteOffset = 0
|
|
119
|
+
): WKBHeader {
|
|
120
|
+
return inspectWKBHeaderView(getDataView(input), byteOffset);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Traverses exactly one WKB geometry without constructing coordinate or geometry objects.
|
|
125
|
+
* Returns the number of bytes consumed.
|
|
126
|
+
*/
|
|
127
|
+
export function visitWKB(
|
|
128
|
+
input: ArrayBufferLike | ArrayBufferView,
|
|
129
|
+
visitor: WKBVisitor,
|
|
130
|
+
options: WKBTraversalOptions = {}
|
|
131
|
+
): number {
|
|
132
|
+
const view = getDataView(input);
|
|
133
|
+
const state: TraversalState = {
|
|
134
|
+
maximumDepth: validateLimit(options.maximumDepth, 64, 'maximumDepth'),
|
|
135
|
+
maximumElements: validateLimit(options.maximumElements, 100_000_000, 'maximumElements'),
|
|
136
|
+
elementCount: 0,
|
|
137
|
+
visitor
|
|
138
|
+
};
|
|
139
|
+
const byteLength = visitGeometry(view, 0, state, 0).byteOffset;
|
|
140
|
+
if (byteLength !== view.byteLength) throw new Error('WKB contains trailing bytes');
|
|
141
|
+
return byteLength;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Collects geometry counts and XYZM bounds without materializing geometry rows. */
|
|
145
|
+
export function scanWKB(
|
|
146
|
+
input: ArrayBufferLike | ArrayBufferView,
|
|
147
|
+
options: WKBTraversalOptions = {}
|
|
148
|
+
): WKBScanResult {
|
|
149
|
+
const header = inspectWKBHeader(input);
|
|
150
|
+
const geometryCounts: Record<WKBGeometryType, number> = {
|
|
151
|
+
Point: 0,
|
|
152
|
+
LineString: 0,
|
|
153
|
+
Polygon: 0,
|
|
154
|
+
MultiPoint: 0,
|
|
155
|
+
MultiLineString: 0,
|
|
156
|
+
MultiPolygon: 0,
|
|
157
|
+
GeometryCollection: 0
|
|
158
|
+
};
|
|
159
|
+
const geometryTypes = new Set<WKBGeometryType>();
|
|
160
|
+
const bounds: MutableBounds = {};
|
|
161
|
+
let coordinateCount = 0;
|
|
162
|
+
let ringCount = 0;
|
|
163
|
+
let geometryCount = 0;
|
|
164
|
+
let maximumDepth = 0;
|
|
165
|
+
|
|
166
|
+
const byteLength = visitWKB(
|
|
167
|
+
input,
|
|
168
|
+
{
|
|
169
|
+
geometry: (geometryHeader, _count, depth) => {
|
|
170
|
+
geometryCounts[geometryHeader.geometryType]++;
|
|
171
|
+
geometryTypes.add(geometryHeader.geometryType);
|
|
172
|
+
geometryCount++;
|
|
173
|
+
maximumDepth = Math.max(maximumDepth, depth);
|
|
174
|
+
},
|
|
175
|
+
ring: () => ringCount++,
|
|
176
|
+
coordinate: (x, y, z, m) => {
|
|
177
|
+
coordinateCount++;
|
|
178
|
+
updateBounds(bounds, 'x', x);
|
|
179
|
+
updateBounds(bounds, 'y', y);
|
|
180
|
+
if (z !== undefined) updateBounds(bounds, 'z', z);
|
|
181
|
+
if (m !== undefined) updateBounds(bounds, 'm', m);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
options
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const concreteBounds = makeBounds(bounds);
|
|
188
|
+
return {
|
|
189
|
+
header,
|
|
190
|
+
byteLength,
|
|
191
|
+
coordinateCount,
|
|
192
|
+
ringCount,
|
|
193
|
+
geometryCount,
|
|
194
|
+
maximumDepth,
|
|
195
|
+
geometryTypes: GEOMETRY_TYPES.filter(type => geometryTypes.has(type)),
|
|
196
|
+
geometryCounts,
|
|
197
|
+
...(concreteBounds ? {bounds: concreteBounds} : {})
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function visitGeometry(
|
|
202
|
+
view: DataView,
|
|
203
|
+
byteOffset: number,
|
|
204
|
+
state: TraversalState,
|
|
205
|
+
depth: number,
|
|
206
|
+
expectedType?: WKBGeometryType
|
|
207
|
+
): {byteOffset: number; header: WKBHeader} {
|
|
208
|
+
if (depth > state.maximumDepth) throw new Error('WKB geometry nesting exceeds maximumDepth');
|
|
209
|
+
const header = inspectWKBHeaderView(view, byteOffset);
|
|
210
|
+
if (expectedType && header.geometryType !== expectedType) {
|
|
211
|
+
throw new Error(`WKB ${expectedType} collection contains a ${header.geometryType} child`);
|
|
212
|
+
}
|
|
213
|
+
byteOffset = header.bodyByteOffset;
|
|
214
|
+
|
|
215
|
+
switch (header.geometryType) {
|
|
216
|
+
case 'Point':
|
|
217
|
+
state.visitor.geometry?.(header, undefined, depth);
|
|
218
|
+
byteOffset = visitCoordinate(view, byteOffset, header, state.visitor, depth);
|
|
219
|
+
break;
|
|
220
|
+
case 'LineString': {
|
|
221
|
+
const pointCount = readCount(view, byteOffset, header.littleEndian, state);
|
|
222
|
+
byteOffset += 4;
|
|
223
|
+
state.visitor.geometry?.(header, pointCount, depth);
|
|
224
|
+
byteOffset = visitCoordinateSequence(view, byteOffset, pointCount, header, state, depth);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'Polygon': {
|
|
228
|
+
const ringCount = readCount(view, byteOffset, header.littleEndian, state);
|
|
229
|
+
byteOffset += 4;
|
|
230
|
+
state.visitor.geometry?.(header, ringCount, depth);
|
|
231
|
+
for (let ringIndex = 0; ringIndex < ringCount; ringIndex++) {
|
|
232
|
+
const pointCount = readCount(view, byteOffset, header.littleEndian, state);
|
|
233
|
+
byteOffset += 4;
|
|
234
|
+
state.visitor.ring?.(pointCount, ringIndex, depth);
|
|
235
|
+
byteOffset = visitCoordinateSequence(view, byteOffset, pointCount, header, state, depth);
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'MultiPoint':
|
|
240
|
+
case 'MultiLineString':
|
|
241
|
+
case 'MultiPolygon':
|
|
242
|
+
case 'GeometryCollection': {
|
|
243
|
+
const childCount = readCount(view, byteOffset, header.littleEndian, state);
|
|
244
|
+
byteOffset += 4;
|
|
245
|
+
state.visitor.geometry?.(header, childCount, depth);
|
|
246
|
+
const childType =
|
|
247
|
+
header.geometryType === 'MultiPoint'
|
|
248
|
+
? 'Point'
|
|
249
|
+
: header.geometryType === 'MultiLineString'
|
|
250
|
+
? 'LineString'
|
|
251
|
+
: header.geometryType === 'MultiPolygon'
|
|
252
|
+
? 'Polygon'
|
|
253
|
+
: undefined;
|
|
254
|
+
for (let childIndex = 0; childIndex < childCount; childIndex++) {
|
|
255
|
+
byteOffset = visitGeometry(view, byteOffset, state, depth + 1, childType).byteOffset;
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {byteOffset, header};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function visitCoordinateSequence(
|
|
264
|
+
view: DataView,
|
|
265
|
+
byteOffset: number,
|
|
266
|
+
pointCount: number,
|
|
267
|
+
header: WKBHeader,
|
|
268
|
+
state: TraversalState,
|
|
269
|
+
depth: number
|
|
270
|
+
): number {
|
|
271
|
+
for (let pointIndex = 0; pointIndex < pointCount; pointIndex++) {
|
|
272
|
+
byteOffset = visitCoordinate(view, byteOffset, header, state.visitor, depth);
|
|
273
|
+
}
|
|
274
|
+
return byteOffset;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function visitCoordinate(
|
|
278
|
+
view: DataView,
|
|
279
|
+
byteOffset: number,
|
|
280
|
+
header: WKBHeader,
|
|
281
|
+
visitor: WKBVisitor,
|
|
282
|
+
depth: number
|
|
283
|
+
): number {
|
|
284
|
+
const coordinateByteLength = getWellKnownDimensionSize(header.dimension) * 8;
|
|
285
|
+
assertRemaining(view, byteOffset, coordinateByteLength);
|
|
286
|
+
const x = view.getFloat64(byteOffset, header.littleEndian);
|
|
287
|
+
const y = view.getFloat64(byteOffset + 8, header.littleEndian);
|
|
288
|
+
const third =
|
|
289
|
+
header.dimension === 'xy' ? undefined : view.getFloat64(byteOffset + 16, header.littleEndian);
|
|
290
|
+
const fourth =
|
|
291
|
+
header.dimension === 'xyzm' ? view.getFloat64(byteOffset + 24, header.littleEndian) : undefined;
|
|
292
|
+
const z = header.dimension === 'xyz' || header.dimension === 'xyzm' ? third : undefined;
|
|
293
|
+
const m = header.dimension === 'xym' ? third : fourth;
|
|
294
|
+
visitor.coordinate?.(x, y, z, m, header.dimension, byteOffset, depth);
|
|
295
|
+
return byteOffset + coordinateByteLength;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function inspectWKBHeaderView(view: DataView, byteOffset: number): WKBHeader {
|
|
299
|
+
const startByteOffset = byteOffset;
|
|
300
|
+
assertRemaining(view, byteOffset, 5);
|
|
301
|
+
const byteOrder = view.getUint8(byteOffset++);
|
|
302
|
+
if (byteOrder !== 0 && byteOrder !== 1) throw new Error('Invalid WKB byte order');
|
|
303
|
+
const littleEndian = byteOrder === 1;
|
|
304
|
+
const typeCode = view.getUint32(byteOffset, littleEndian);
|
|
305
|
+
byteOffset += 4;
|
|
306
|
+
|
|
307
|
+
const hasZ = Boolean(typeCode & 0x80000000);
|
|
308
|
+
const hasM = Boolean(typeCode & 0x40000000);
|
|
309
|
+
const hasSrid = Boolean(typeCode & 0x20000000);
|
|
310
|
+
let geometryCode = typeCode & 0x1fffffff;
|
|
311
|
+
let dimension: WellKnownDimension;
|
|
312
|
+
if (geometryCode >= 3000 && geometryCode < 4000) {
|
|
313
|
+
dimension = 'xyzm';
|
|
314
|
+
geometryCode -= 3000;
|
|
315
|
+
} else if (geometryCode >= 2000 && geometryCode < 3000) {
|
|
316
|
+
dimension = 'xym';
|
|
317
|
+
geometryCode -= 2000;
|
|
318
|
+
} else if (geometryCode >= 1000 && geometryCode < 2000) {
|
|
319
|
+
dimension = 'xyz';
|
|
320
|
+
geometryCode -= 1000;
|
|
321
|
+
} else {
|
|
322
|
+
dimension = hasZ && hasM ? 'xyzm' : hasZ ? 'xyz' : hasM ? 'xym' : 'xy';
|
|
323
|
+
}
|
|
324
|
+
const geometryType = GEOMETRY_TYPES[geometryCode - 1];
|
|
325
|
+
if (!geometryType) throw new Error(`Unsupported WKB geometry type ${geometryCode}`);
|
|
326
|
+
|
|
327
|
+
let srid: number | undefined;
|
|
328
|
+
if (hasSrid) {
|
|
329
|
+
assertRemaining(view, byteOffset, 4);
|
|
330
|
+
srid = view.getUint32(byteOffset, littleEndian);
|
|
331
|
+
byteOffset += 4;
|
|
332
|
+
}
|
|
333
|
+
const dialect: WKBDialect =
|
|
334
|
+
hasZ || hasM || hasSrid ? 'ewkb' : dimension === 'xy' ? 'wkb' : 'iso-wkb';
|
|
335
|
+
return {
|
|
336
|
+
geometryType,
|
|
337
|
+
dimension,
|
|
338
|
+
dialect,
|
|
339
|
+
littleEndian,
|
|
340
|
+
byteOffset: startByteOffset,
|
|
341
|
+
bodyByteOffset: byteOffset,
|
|
342
|
+
byteLength: byteOffset - startByteOffset,
|
|
343
|
+
...(srid === undefined ? {} : {srid})
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function readCount(
|
|
348
|
+
view: DataView,
|
|
349
|
+
byteOffset: number,
|
|
350
|
+
littleEndian: boolean,
|
|
351
|
+
state: TraversalState
|
|
352
|
+
): number {
|
|
353
|
+
assertRemaining(view, byteOffset, 4);
|
|
354
|
+
const count = view.getUint32(byteOffset, littleEndian);
|
|
355
|
+
state.elementCount += count;
|
|
356
|
+
if (state.elementCount > state.maximumElements) {
|
|
357
|
+
throw new Error('WKB element count exceeds maximumElements');
|
|
358
|
+
}
|
|
359
|
+
return count;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function updateBounds(bounds: MutableBounds, axis: 'x' | 'y' | 'z' | 'm', value: number): void {
|
|
363
|
+
if (!Number.isFinite(value)) return;
|
|
364
|
+
const minimum = `${axis}min` as keyof MutableBounds;
|
|
365
|
+
const maximum = `${axis}max` as keyof MutableBounds;
|
|
366
|
+
bounds[minimum] = bounds[minimum] === undefined ? value : Math.min(bounds[minimum]!, value);
|
|
367
|
+
bounds[maximum] = bounds[maximum] === undefined ? value : Math.max(bounds[maximum]!, value);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function makeBounds(bounds: MutableBounds): WKBBounds | undefined {
|
|
371
|
+
if (
|
|
372
|
+
bounds.xmin === undefined ||
|
|
373
|
+
bounds.ymin === undefined ||
|
|
374
|
+
bounds.xmax === undefined ||
|
|
375
|
+
bounds.ymax === undefined
|
|
376
|
+
) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
return bounds as WKBBounds;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function getDataView(input: ArrayBufferLike | ArrayBufferView): DataView {
|
|
383
|
+
return ArrayBuffer.isView(input)
|
|
384
|
+
? new DataView(input.buffer, input.byteOffset, input.byteLength)
|
|
385
|
+
: new DataView(input);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function assertRemaining(view: DataView, byteOffset: number, byteLength: number): void {
|
|
389
|
+
if (byteOffset < 0 || byteOffset + byteLength > view.byteLength) {
|
|
390
|
+
throw new Error('Unexpected end of WKB');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function validateLimit(value: number | undefined, fallback: number, name: string): number {
|
|
395
|
+
const limit = value ?? fallback;
|
|
396
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
397
|
+
throw new Error(`${name} must be a non-negative safe integer`);
|
|
398
|
+
}
|
|
399
|
+
return limit;
|
|
400
|
+
}
|
package/src/wkb.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {WellKnownDimension, WellKnownGeometry} from './types';
|
|
6
|
+
import {getWellKnownDimensionSize, inferWellKnownGeometryDimension} from './types';
|
|
7
|
+
import {inspectWKBHeader} from './wkb-reader';
|
|
8
|
+
|
|
9
|
+
/** Defensive limits applied before WKB allocates geometry arrays. */
|
|
10
|
+
export type WKBParseOptions = Readonly<{
|
|
11
|
+
/** Maximum recursive geometry nesting. Defaults to 64. */
|
|
12
|
+
maximumDepth?: number;
|
|
13
|
+
/** Maximum total declared child elements. Defaults to 100 million. */
|
|
14
|
+
maximumElements?: number;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/** One completely parsed WKB value and metadata declared by its root header. */
|
|
18
|
+
export type WKBParseResult = Readonly<{
|
|
19
|
+
geometry: WellKnownGeometry;
|
|
20
|
+
byteLength: number;
|
|
21
|
+
dimension: WellKnownDimension;
|
|
22
|
+
/** EWKB SRID when the root header carries one. */
|
|
23
|
+
srid?: number;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
type WKBReadResult = {
|
|
27
|
+
geometry: WellKnownGeometry;
|
|
28
|
+
offset: number;
|
|
29
|
+
dimension: WellKnownDimension;
|
|
30
|
+
srid?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type WKBReadState = {
|
|
34
|
+
maximumDepth: number;
|
|
35
|
+
maximumElements: number;
|
|
36
|
+
elementCount: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Parses exactly one little- or big-endian ISO WKB or EWKB geometry. */
|
|
40
|
+
export function parseWKB(bytes: Uint8Array, options: WKBParseOptions = {}): WKBParseResult {
|
|
41
|
+
const state: WKBReadState = {
|
|
42
|
+
maximumDepth: validateLimit(options.maximumDepth, 64, 'maximumDepth'),
|
|
43
|
+
maximumElements: validateLimit(options.maximumElements, 100_000_000, 'maximumElements'),
|
|
44
|
+
elementCount: 0
|
|
45
|
+
};
|
|
46
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
47
|
+
const result = readWKBGeometry(view, 0, state, 0);
|
|
48
|
+
if (result.offset !== bytes.byteLength) throw new Error('WKB contains trailing bytes');
|
|
49
|
+
return {
|
|
50
|
+
geometry: result.geometry,
|
|
51
|
+
byteLength: result.offset,
|
|
52
|
+
dimension: result.dimension,
|
|
53
|
+
...(result.srid === undefined ? {} : {srid: result.srid})
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Writes one geometry as little-endian ISO WKB. */
|
|
58
|
+
export function writeWKB(
|
|
59
|
+
geometry: WellKnownGeometry,
|
|
60
|
+
dimension: WellKnownDimension = inferWellKnownGeometryDimension(geometry)
|
|
61
|
+
): Uint8Array {
|
|
62
|
+
const bytes: number[] = [];
|
|
63
|
+
writeWKBGeometry(bytes, geometry, dimension);
|
|
64
|
+
return Uint8Array.from(bytes);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readWKBGeometry(
|
|
68
|
+
view: DataView,
|
|
69
|
+
startOffset: number,
|
|
70
|
+
state: WKBReadState,
|
|
71
|
+
depth: number
|
|
72
|
+
): WKBReadResult {
|
|
73
|
+
if (depth > state.maximumDepth) throw new Error('WKB geometry nesting exceeds maximumDepth');
|
|
74
|
+
const header = inspectWKBHeader(view, startOffset);
|
|
75
|
+
let offset = header.bodyByteOffset;
|
|
76
|
+
const {dimension, geometryType, littleEndian, srid} = header;
|
|
77
|
+
const dimensions = getWellKnownDimensionSize(dimension);
|
|
78
|
+
const readCoordinate = (): number[] => {
|
|
79
|
+
assertRemaining(view, offset, dimensions * 8);
|
|
80
|
+
const coordinate = new Array<number>(dimensions);
|
|
81
|
+
for (let index = 0; index < dimensions; index++) {
|
|
82
|
+
coordinate[index] = view.getFloat64(offset, littleEndian);
|
|
83
|
+
offset += 8;
|
|
84
|
+
}
|
|
85
|
+
return coordinate;
|
|
86
|
+
};
|
|
87
|
+
const readCount = (): number => {
|
|
88
|
+
assertRemaining(view, offset, 4);
|
|
89
|
+
const count = view.getUint32(offset, littleEndian);
|
|
90
|
+
offset += 4;
|
|
91
|
+
state.elementCount += count;
|
|
92
|
+
if (state.elementCount > state.maximumElements) {
|
|
93
|
+
throw new Error('WKB element count exceeds maximumElements');
|
|
94
|
+
}
|
|
95
|
+
return count;
|
|
96
|
+
};
|
|
97
|
+
const readCoordinates = (): number[][] => {
|
|
98
|
+
const count = readCount();
|
|
99
|
+
return Array.from({length: count}, readCoordinate);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let geometry: WellKnownGeometry;
|
|
103
|
+
switch (geometryType) {
|
|
104
|
+
case 'Point':
|
|
105
|
+
geometry = {type: 'Point', coordinates: readCoordinate()};
|
|
106
|
+
break;
|
|
107
|
+
case 'LineString':
|
|
108
|
+
geometry = {type: 'LineString', coordinates: readCoordinates()};
|
|
109
|
+
break;
|
|
110
|
+
case 'Polygon':
|
|
111
|
+
geometry = {type: 'Polygon', coordinates: Array.from({length: readCount()}, readCoordinates)};
|
|
112
|
+
break;
|
|
113
|
+
case 'MultiPoint':
|
|
114
|
+
case 'MultiLineString':
|
|
115
|
+
case 'MultiPolygon':
|
|
116
|
+
case 'GeometryCollection': {
|
|
117
|
+
const children: WellKnownGeometry[] = [];
|
|
118
|
+
for (let index = 0, count = readCount(); index < count; index++) {
|
|
119
|
+
const child = readWKBGeometry(view, offset, state, depth + 1);
|
|
120
|
+
children.push(child.geometry);
|
|
121
|
+
offset = child.offset;
|
|
122
|
+
}
|
|
123
|
+
if (geometryType === 'MultiPoint') {
|
|
124
|
+
geometry = {
|
|
125
|
+
type: 'MultiPoint',
|
|
126
|
+
coordinates: children.map(assertPoint).map(point => point.coordinates)
|
|
127
|
+
};
|
|
128
|
+
} else if (geometryType === 'MultiLineString') {
|
|
129
|
+
geometry = {
|
|
130
|
+
type: 'MultiLineString',
|
|
131
|
+
coordinates: children.map(assertLineString).map(line => line.coordinates)
|
|
132
|
+
};
|
|
133
|
+
} else if (geometryType === 'MultiPolygon') {
|
|
134
|
+
geometry = {
|
|
135
|
+
type: 'MultiPolygon',
|
|
136
|
+
coordinates: children.map(assertPolygon).map(polygon => polygon.coordinates)
|
|
137
|
+
};
|
|
138
|
+
} else {
|
|
139
|
+
geometry = {type: 'GeometryCollection', geometries: children};
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {geometry, offset, dimension, ...(srid === undefined ? {} : {srid})};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeWKBGeometry(
|
|
148
|
+
bytes: number[],
|
|
149
|
+
geometry: WellKnownGeometry,
|
|
150
|
+
dimension: WellKnownDimension
|
|
151
|
+
): void {
|
|
152
|
+
bytes.push(1);
|
|
153
|
+
const size = getWellKnownDimensionSize(dimension);
|
|
154
|
+
const dimensionOffset =
|
|
155
|
+
dimension === 'xyz' ? 1000 : dimension === 'xym' ? 2000 : dimension === 'xyzm' ? 3000 : 0;
|
|
156
|
+
writeUint32(bytes, getWKBType(geometry.type) + dimensionOffset);
|
|
157
|
+
const writeCoordinate = (coordinate: readonly number[]): void => {
|
|
158
|
+
for (let index = 0; index < size; index++) writeFloat64(bytes, coordinate[index] ?? 0);
|
|
159
|
+
};
|
|
160
|
+
const writeCoordinates = (coordinates: readonly (readonly number[])[]): void => {
|
|
161
|
+
writeUint32(bytes, coordinates.length);
|
|
162
|
+
for (const coordinate of coordinates) writeCoordinate(coordinate);
|
|
163
|
+
};
|
|
164
|
+
switch (geometry.type) {
|
|
165
|
+
case 'Point':
|
|
166
|
+
writeCoordinate(geometry.coordinates);
|
|
167
|
+
break;
|
|
168
|
+
case 'LineString':
|
|
169
|
+
writeCoordinates(geometry.coordinates);
|
|
170
|
+
break;
|
|
171
|
+
case 'Polygon':
|
|
172
|
+
writeUint32(bytes, geometry.coordinates.length);
|
|
173
|
+
for (const ring of geometry.coordinates) writeCoordinates(ring);
|
|
174
|
+
break;
|
|
175
|
+
case 'MultiPoint':
|
|
176
|
+
writeUint32(bytes, geometry.coordinates.length);
|
|
177
|
+
for (const coordinate of geometry.coordinates) {
|
|
178
|
+
writeWKBGeometry(bytes, {type: 'Point', coordinates: coordinate}, dimension);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
case 'MultiLineString':
|
|
182
|
+
writeUint32(bytes, geometry.coordinates.length);
|
|
183
|
+
for (const coordinates of geometry.coordinates) {
|
|
184
|
+
writeWKBGeometry(bytes, {type: 'LineString', coordinates}, dimension);
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
case 'MultiPolygon':
|
|
188
|
+
writeUint32(bytes, geometry.coordinates.length);
|
|
189
|
+
for (const coordinates of geometry.coordinates) {
|
|
190
|
+
writeWKBGeometry(bytes, {type: 'Polygon', coordinates}, dimension);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case 'GeometryCollection':
|
|
194
|
+
writeUint32(bytes, geometry.geometries.length);
|
|
195
|
+
for (const child of geometry.geometries) writeWKBGeometry(bytes, child, dimension);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getWKBType(type: WellKnownGeometry['type']): number {
|
|
201
|
+
return (
|
|
202
|
+
[
|
|
203
|
+
'Point',
|
|
204
|
+
'LineString',
|
|
205
|
+
'Polygon',
|
|
206
|
+
'MultiPoint',
|
|
207
|
+
'MultiLineString',
|
|
208
|
+
'MultiPolygon',
|
|
209
|
+
'GeometryCollection'
|
|
210
|
+
].indexOf(type) + 1
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function writeUint32(bytes: number[], value: number): void {
|
|
215
|
+
bytes.push(value & 255, (value >>> 8) & 255, (value >>> 16) & 255, (value >>> 24) & 255);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeFloat64(bytes: number[], value: number): void {
|
|
219
|
+
const buffer = new ArrayBuffer(8);
|
|
220
|
+
new DataView(buffer).setFloat64(0, value, true);
|
|
221
|
+
bytes.push(...new Uint8Array(buffer));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function assertRemaining(view: DataView, offset: number, length: number): void {
|
|
225
|
+
if (offset + length > view.byteLength) throw new Error('Unexpected end of WKB');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function assertPoint(value: WellKnownGeometry): Extract<WellKnownGeometry, {type: 'Point'}> {
|
|
229
|
+
if (value.type !== 'Point') throw new Error('WKB MultiPoint contains a non-Point child');
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function assertLineString(
|
|
234
|
+
value: WellKnownGeometry
|
|
235
|
+
): Extract<WellKnownGeometry, {type: 'LineString'}> {
|
|
236
|
+
if (value.type !== 'LineString') {
|
|
237
|
+
throw new Error('WKB MultiLineString contains a non-LineString child');
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function assertPolygon(value: WellKnownGeometry): Extract<WellKnownGeometry, {type: 'Polygon'}> {
|
|
243
|
+
if (value.type !== 'Polygon') throw new Error('WKB MultiPolygon contains a non-Polygon child');
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function validateLimit(value: number | undefined, fallback: number, name: string): number {
|
|
248
|
+
const limit = value ?? fallback;
|
|
249
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
250
|
+
throw new Error(`${name} must be a non-negative safe integer`);
|
|
251
|
+
}
|
|
252
|
+
return limit;
|
|
253
|
+
}
|