@math.gl/geoarrow 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 +311 -0
- package/dist/builder.d.ts +90 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +428 -0
- package/dist/builder.js.map +1 -0
- package/dist/codecs.d.ts +10 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +122 -0
- package/dist/codecs.js.map +1 -0
- package/dist/index.cjs +1651 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/kernels.d.ts +51 -0
- package/dist/kernels.d.ts.map +1 -0
- package/dist/kernels.js +359 -0
- package/dist/kernels.js.map +1 -0
- package/dist/layout.d.ts +52 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +669 -0
- package/dist/layout.js.map +1 -0
- package/dist/tessellate.d.ts +31 -0
- package/dist/tessellate.d.ts.map +1 -0
- package/dist/tessellate.js +102 -0
- package/dist/tessellate.js.map +1 -0
- package/dist/types.d.ts +113 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +41 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.cjs +73 -0
- package/dist/worker.cjs.map +6 -0
- package/dist/worker.d.ts +11 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +10 -0
- package/dist/worker.js.map +1 -0
- package/package.json +48 -0
- package/src/builder.ts +569 -0
- package/src/codecs.ts +155 -0
- package/src/index.ts +93 -0
- package/src/kernels.ts +463 -0
- package/src/layout.ts +833 -0
- package/src/tessellate.ts +133 -0
- package/src/types.ts +200 -0
- package/src/worker.ts +20 -0
package/dist/layout.js
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
import { getGeoArrowDimensionSize, getGeoArrowEncodingForGeometry, getGeoArrowGeometryType } from "./types.js";
|
|
5
|
+
/** Returns whether one logical value is valid at a nesting level. */
|
|
6
|
+
export function isGeoArrowValueValid(validity, index) {
|
|
7
|
+
if (!validity)
|
|
8
|
+
return true;
|
|
9
|
+
const bitIndex = (validity.bitOffset || 0) + index;
|
|
10
|
+
return Boolean(validity.values[bitIndex >> 3] & (1 << (bitIndex & 7)));
|
|
11
|
+
}
|
|
12
|
+
/** Returns the total number of logical rows across chunks. */
|
|
13
|
+
export function getGeoArrowRowCount(column) {
|
|
14
|
+
return column.chunks.reduce((count, chunk) => count + chunk.length, 0);
|
|
15
|
+
}
|
|
16
|
+
/** Visits coordinates without materializing row geometry objects. */
|
|
17
|
+
export function visitGeoArrowCoordinates(column, visitor) {
|
|
18
|
+
let rowOffset = 0;
|
|
19
|
+
for (const chunk of column.chunks) {
|
|
20
|
+
visitChunkCoordinates(chunk, column.encoding, rowOffset, visitor);
|
|
21
|
+
rowOffset += chunk.length;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Returns a zero-copy logical slice of a column, preserving chunk boundaries. */
|
|
25
|
+
export function sliceGeoArrowColumn(column, begin = 0, end = getGeoArrowRowCount(column)) {
|
|
26
|
+
const rowCount = getGeoArrowRowCount(column);
|
|
27
|
+
const first = normalizeSliceIndex(begin, rowCount);
|
|
28
|
+
const last = Math.max(first, normalizeSliceIndex(end, rowCount));
|
|
29
|
+
if (first === 0 && last === rowCount)
|
|
30
|
+
return column;
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let chunkStart = 0;
|
|
33
|
+
for (const chunk of column.chunks) {
|
|
34
|
+
const chunkEnd = chunkStart + chunk.length;
|
|
35
|
+
const localFirst = Math.max(0, first - chunkStart);
|
|
36
|
+
const localLast = Math.min(chunk.length, last - chunkStart);
|
|
37
|
+
if (localLast > localFirst) {
|
|
38
|
+
chunks.push(sliceGeoArrowArray(chunk, localFirst, localLast));
|
|
39
|
+
}
|
|
40
|
+
chunkStart = chunkEnd;
|
|
41
|
+
if (chunkStart >= last)
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
return { ...column, chunks };
|
|
45
|
+
}
|
|
46
|
+
/** Returns a zero-copy logical slice of one physical array. */
|
|
47
|
+
export function sliceGeoArrowArray(array, begin, end) {
|
|
48
|
+
if (begin === 0 && end === array.length)
|
|
49
|
+
return array;
|
|
50
|
+
const length = end - begin;
|
|
51
|
+
const validity = array.validity
|
|
52
|
+
? { ...array.validity, bitOffset: (array.validity.bitOffset || 0) + begin }
|
|
53
|
+
: undefined;
|
|
54
|
+
switch (array.kind) {
|
|
55
|
+
case 'primitive':
|
|
56
|
+
return {
|
|
57
|
+
...array,
|
|
58
|
+
length,
|
|
59
|
+
offset: (array.offset || 0) + begin * (array.stride || 1),
|
|
60
|
+
validity
|
|
61
|
+
};
|
|
62
|
+
case 'fixed-size-list':
|
|
63
|
+
return { ...array, length, offset: (array.offset || 0) + begin, validity };
|
|
64
|
+
case 'list':
|
|
65
|
+
case 'serialized':
|
|
66
|
+
return { ...array, length, offset: (array.offset || 0) + begin, validity };
|
|
67
|
+
case 'struct':
|
|
68
|
+
return { ...array, length, offset: (array.offset || 0) + begin, validity };
|
|
69
|
+
case 'dense-union':
|
|
70
|
+
return { ...array, length, offset: (array.offset || 0) + begin, validity };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Validates physical bounds, list offsets, union dispatch, and declared coordinate layout. */
|
|
74
|
+
export function validateGeoArrowColumn(column) {
|
|
75
|
+
const issues = [];
|
|
76
|
+
for (let chunkIndex = 0; chunkIndex < column.chunks.length; chunkIndex++) {
|
|
77
|
+
validateArray(column.chunks[chunkIndex], `chunks[${chunkIndex}]`, issues);
|
|
78
|
+
}
|
|
79
|
+
validateColumnLayout(column, issues);
|
|
80
|
+
return { valid: issues.length === 0, issues };
|
|
81
|
+
}
|
|
82
|
+
/** Inspects a column without decoding serialized geometry or creating row objects. */
|
|
83
|
+
export function inspectGeoArrowColumn(column) {
|
|
84
|
+
const validation = validateGeoArrowColumn(column);
|
|
85
|
+
const storageKinds = [];
|
|
86
|
+
let nullCount = 0;
|
|
87
|
+
for (const chunk of column.chunks) {
|
|
88
|
+
collectStorageKinds(chunk, storageKinds);
|
|
89
|
+
for (let rowIndex = 0; rowIndex < chunk.length; rowIndex++) {
|
|
90
|
+
if (!isGeoArrowValueValid(chunk.validity, rowIndex))
|
|
91
|
+
nullCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
encoding: column.encoding,
|
|
96
|
+
rowCount: getGeoArrowRowCount(column),
|
|
97
|
+
chunkCount: column.chunks.length,
|
|
98
|
+
nullCount,
|
|
99
|
+
coordinateCount: getGeoArrowVertexCount(column),
|
|
100
|
+
storageKinds,
|
|
101
|
+
valid: validation.valid,
|
|
102
|
+
issues: validation.issues
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Counts coordinate tuples without materializing geometry objects. */
|
|
106
|
+
export function getGeoArrowVertexCount(column) {
|
|
107
|
+
if (column.encoding === 'geoarrow.wkb' ||
|
|
108
|
+
column.encoding === 'geoarrow.wkt' ||
|
|
109
|
+
column.encoding === 'geoarrow.box') {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
let count = 0;
|
|
113
|
+
visitGeoArrowCoordinates(column, coordinate => {
|
|
114
|
+
count++;
|
|
115
|
+
return coordinate;
|
|
116
|
+
});
|
|
117
|
+
return count;
|
|
118
|
+
}
|
|
119
|
+
/** Collects unique transferable ArrayBuffers borrowed by a column. */
|
|
120
|
+
export function getGeoArrowTransferList(column) {
|
|
121
|
+
const buffers = new Set();
|
|
122
|
+
for (const chunk of column.chunks)
|
|
123
|
+
collectArrayBuffers(chunk, buffers);
|
|
124
|
+
return [...buffers];
|
|
125
|
+
}
|
|
126
|
+
/** Materializes column rows for codecs and structural algorithms. */
|
|
127
|
+
export function materializeGeoArrowRows(column) {
|
|
128
|
+
if (column.encoding === 'geoarrow.wkb' || column.encoding === 'geoarrow.wkt') {
|
|
129
|
+
throw new Error('Serialized GeoArrow columns must be decoded before materialization');
|
|
130
|
+
}
|
|
131
|
+
const rows = [];
|
|
132
|
+
for (const chunk of column.chunks) {
|
|
133
|
+
for (let rowIndex = 0; rowIndex < chunk.length; rowIndex++) {
|
|
134
|
+
rows.push(materializeGeometryRow(chunk, rowIndex, column.encoding));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return rows;
|
|
138
|
+
}
|
|
139
|
+
/** Materializes one row from a physical array. */
|
|
140
|
+
export function materializeGeometryRow(array, rowIndex, encoding) {
|
|
141
|
+
if (!isGeoArrowValueValid(array.validity, rowIndex))
|
|
142
|
+
return null;
|
|
143
|
+
if (encoding === 'geoarrow.geometry') {
|
|
144
|
+
if (array.kind !== 'dense-union')
|
|
145
|
+
return null;
|
|
146
|
+
return materializeUnionRow(array, rowIndex);
|
|
147
|
+
}
|
|
148
|
+
if (encoding === 'geoarrow.geometrycollection') {
|
|
149
|
+
if (array.kind !== 'list')
|
|
150
|
+
return null;
|
|
151
|
+
const [first, last] = getListRange(array, rowIndex);
|
|
152
|
+
const geometries = [];
|
|
153
|
+
for (let index = first; index < last; index++) {
|
|
154
|
+
if (array.child.kind !== 'dense-union')
|
|
155
|
+
continue;
|
|
156
|
+
const geometry = materializeUnionRow(array.child, index);
|
|
157
|
+
if (geometry)
|
|
158
|
+
geometries.push(geometry);
|
|
159
|
+
}
|
|
160
|
+
return { type: 'GeometryCollection', geometries };
|
|
161
|
+
}
|
|
162
|
+
const geometryType = getGeoArrowGeometryType(encoding);
|
|
163
|
+
if (!geometryType || geometryType === 'GeometryCollection')
|
|
164
|
+
return null;
|
|
165
|
+
const depth = getEncodingDepth(encoding);
|
|
166
|
+
const coordinates = readNestedCoordinates(array, rowIndex, depth);
|
|
167
|
+
if (!coordinates)
|
|
168
|
+
return null;
|
|
169
|
+
return { type: geometryType, coordinates };
|
|
170
|
+
}
|
|
171
|
+
/** Returns a safe numeric offset. */
|
|
172
|
+
export function getGeoArrowOffset(offsets, index) {
|
|
173
|
+
const value = offsets[index];
|
|
174
|
+
const numericValue = typeof value === 'bigint' ? Number(value) : value;
|
|
175
|
+
if (!Number.isSafeInteger(numericValue)) {
|
|
176
|
+
throw new Error(`GeoArrow offset ${String(value)} exceeds JavaScript's safe integer range`);
|
|
177
|
+
}
|
|
178
|
+
return numericValue;
|
|
179
|
+
}
|
|
180
|
+
/** Returns the child range for one list row. */
|
|
181
|
+
export function getListRange(list, rowIndex) {
|
|
182
|
+
const offsetIndex = (list.offset || 0) + rowIndex;
|
|
183
|
+
const baseValue = list.offsetBase ?? 0;
|
|
184
|
+
const base = typeof baseValue === 'bigint' ? Number(baseValue) : baseValue;
|
|
185
|
+
return [
|
|
186
|
+
getGeoArrowOffset(list.offsets, offsetIndex) - base,
|
|
187
|
+
getGeoArrowOffset(list.offsets, offsetIndex + 1) - base
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
function visitChunkCoordinates(array, encoding, rowOffset, visitor) {
|
|
191
|
+
for (let rowIndex = 0; rowIndex < array.length; rowIndex++) {
|
|
192
|
+
if (!isGeoArrowValueValid(array.validity, rowIndex))
|
|
193
|
+
continue;
|
|
194
|
+
if (encoding === 'geoarrow.geometry' && array.kind === 'dense-union') {
|
|
195
|
+
visitUnionRowCoordinates(array, rowIndex, rowOffset + rowIndex, visitor);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (encoding === 'geoarrow.geometrycollection' && array.kind === 'list') {
|
|
199
|
+
const [first, last] = getListRange(array, rowIndex);
|
|
200
|
+
if (array.child.kind === 'dense-union') {
|
|
201
|
+
for (let index = first; index < last; index++) {
|
|
202
|
+
visitUnionRowCoordinates(array.child, index, rowOffset + rowIndex, visitor);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const depth = getEncodingDepth(encoding);
|
|
208
|
+
visitNestedCoordinates(array, rowIndex, depth, rowOffset + rowIndex, visitor);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function visitUnionRowCoordinates(union, rowIndex, sourceRowIndex, visitor) {
|
|
212
|
+
if (!isGeoArrowValueValid(union.validity, rowIndex))
|
|
213
|
+
return;
|
|
214
|
+
const physicalIndex = (union.offset || 0) + rowIndex;
|
|
215
|
+
const typeId = union.typeIds[physicalIndex];
|
|
216
|
+
const valueOffset = union.valueOffsets[physicalIndex];
|
|
217
|
+
const child = union.children.find(candidate => candidate.typeId === typeId);
|
|
218
|
+
if (!child)
|
|
219
|
+
return;
|
|
220
|
+
const encoding = getEncodingFromChildName(child.name);
|
|
221
|
+
if (encoding === 'geoarrow.geometrycollection' && child.data.kind === 'list') {
|
|
222
|
+
const [first, last] = getListRange(child.data, valueOffset);
|
|
223
|
+
if (child.data.child.kind === 'dense-union') {
|
|
224
|
+
for (let index = first; index < last; index++) {
|
|
225
|
+
visitUnionRowCoordinates(child.data.child, index, sourceRowIndex, visitor);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
visitNestedCoordinates(child.data, valueOffset, getEncodingDepth(encoding), sourceRowIndex, visitor);
|
|
231
|
+
}
|
|
232
|
+
function visitNestedCoordinates(array, index, depth, rowIndex, visitor) {
|
|
233
|
+
if (depth === 0) {
|
|
234
|
+
const coordinate = readCoordinate(array, index);
|
|
235
|
+
if (coordinate)
|
|
236
|
+
visitor(coordinate, rowIndex);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (array.kind !== 'list' || !isGeoArrowValueValid(array.validity, index))
|
|
240
|
+
return;
|
|
241
|
+
const [first, last] = getListRange(array, index);
|
|
242
|
+
for (let childIndex = first; childIndex < last; childIndex++) {
|
|
243
|
+
visitNestedCoordinates(array.child, childIndex, depth - 1, rowIndex, visitor);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function readNestedCoordinates(array, index, depth) {
|
|
247
|
+
if (depth === 0)
|
|
248
|
+
return readCoordinate(array, index);
|
|
249
|
+
if (array.kind !== 'list' || !isGeoArrowValueValid(array.validity, index))
|
|
250
|
+
return null;
|
|
251
|
+
const [first, last] = getListRange(array, index);
|
|
252
|
+
const values = [];
|
|
253
|
+
for (let childIndex = first; childIndex < last; childIndex++) {
|
|
254
|
+
const value = readNestedCoordinates(array.child, childIndex, depth - 1);
|
|
255
|
+
if (value !== null)
|
|
256
|
+
values.push(value);
|
|
257
|
+
}
|
|
258
|
+
return values;
|
|
259
|
+
}
|
|
260
|
+
function readCoordinate(array, index) {
|
|
261
|
+
if (!isGeoArrowValueValid(array.validity, index))
|
|
262
|
+
return null;
|
|
263
|
+
if (array.kind === 'fixed-size-list') {
|
|
264
|
+
const listIndex = (array.offset || 0) + index;
|
|
265
|
+
const coordinate = [];
|
|
266
|
+
for (let component = 0; component < array.size; component++) {
|
|
267
|
+
coordinate.push(readPrimitive(array.child, listIndex * array.size + component));
|
|
268
|
+
}
|
|
269
|
+
return coordinate;
|
|
270
|
+
}
|
|
271
|
+
if (array.kind === 'struct') {
|
|
272
|
+
const structIndex = (array.offset || 0) + index;
|
|
273
|
+
const coordinate = [];
|
|
274
|
+
for (const name of ['x', 'y', 'z', 'm']) {
|
|
275
|
+
const child = array.children[name];
|
|
276
|
+
if (child)
|
|
277
|
+
coordinate.push(readPrimitive(child, structIndex));
|
|
278
|
+
}
|
|
279
|
+
return coordinate.length >= 2 ? coordinate : null;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function readPrimitive(array, index) {
|
|
284
|
+
if (array.kind !== 'primitive')
|
|
285
|
+
throw new Error('GeoArrow coordinate child must be primitive');
|
|
286
|
+
const valueIndex = (array.offset || 0) + index * (array.stride || 1);
|
|
287
|
+
return Number(array.values[valueIndex]);
|
|
288
|
+
}
|
|
289
|
+
function materializeUnionRow(union, rowIndex) {
|
|
290
|
+
if (!isGeoArrowValueValid(union.validity, rowIndex))
|
|
291
|
+
return null;
|
|
292
|
+
const physicalIndex = (union.offset || 0) + rowIndex;
|
|
293
|
+
const typeId = union.typeIds[physicalIndex];
|
|
294
|
+
const valueOffset = union.valueOffsets[physicalIndex];
|
|
295
|
+
const child = union.children.find(candidate => candidate.typeId === typeId);
|
|
296
|
+
if (!child || valueOffset < 0 || valueOffset >= child.data.length)
|
|
297
|
+
return null;
|
|
298
|
+
return materializeGeometryRow(child.data, valueOffset, getEncodingFromChildName(child.name));
|
|
299
|
+
}
|
|
300
|
+
function getEncodingFromChildName(name) {
|
|
301
|
+
const normalized = name.replace(/[^a-z]/gi, '').toLowerCase();
|
|
302
|
+
switch (normalized) {
|
|
303
|
+
case 'point':
|
|
304
|
+
return 'geoarrow.point';
|
|
305
|
+
case 'linestring':
|
|
306
|
+
return 'geoarrow.linestring';
|
|
307
|
+
case 'polygon':
|
|
308
|
+
return 'geoarrow.polygon';
|
|
309
|
+
case 'multipoint':
|
|
310
|
+
return 'geoarrow.multipoint';
|
|
311
|
+
case 'multilinestring':
|
|
312
|
+
return 'geoarrow.multilinestring';
|
|
313
|
+
case 'multipolygon':
|
|
314
|
+
return 'geoarrow.multipolygon';
|
|
315
|
+
case 'geometrycollection':
|
|
316
|
+
return 'geoarrow.geometrycollection';
|
|
317
|
+
default:
|
|
318
|
+
throw new Error(`Unknown GeoArrow dense-union child ${name}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function getEncodingDepth(encoding) {
|
|
322
|
+
switch (encoding) {
|
|
323
|
+
case 'geoarrow.point':
|
|
324
|
+
return 0;
|
|
325
|
+
case 'geoarrow.linestring':
|
|
326
|
+
case 'geoarrow.multipoint':
|
|
327
|
+
return 1;
|
|
328
|
+
case 'geoarrow.polygon':
|
|
329
|
+
case 'geoarrow.multilinestring':
|
|
330
|
+
return 2;
|
|
331
|
+
case 'geoarrow.multipolygon':
|
|
332
|
+
return 3;
|
|
333
|
+
default:
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function validateArray(array, path, issues) {
|
|
338
|
+
if (!Number.isSafeInteger(array.length) || array.length < 0) {
|
|
339
|
+
issues.push({
|
|
340
|
+
code: 'invalid-length',
|
|
341
|
+
path,
|
|
342
|
+
message: 'Length must be a non-negative safe integer.'
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
validateValidity(array.validity, array.length, path, issues);
|
|
347
|
+
switch (array.kind) {
|
|
348
|
+
case 'primitive': {
|
|
349
|
+
const stride = array.stride || 1;
|
|
350
|
+
const offset = array.offset || 0;
|
|
351
|
+
if (!Number.isSafeInteger(stride) || stride < 1) {
|
|
352
|
+
issues.push({ code: 'invalid-stride', path, message: 'Primitive stride must be positive.' });
|
|
353
|
+
}
|
|
354
|
+
const requiredLength = array.length === 0 ? offset : offset + (array.length - 1) * stride + 1;
|
|
355
|
+
if (offset < 0 || requiredLength > array.values.length) {
|
|
356
|
+
issues.push({
|
|
357
|
+
code: 'invalid-length',
|
|
358
|
+
path,
|
|
359
|
+
message: 'Primitive values do not cover the logical range.'
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case 'fixed-size-list': {
|
|
365
|
+
const offset = array.offset ?? 0;
|
|
366
|
+
const validOffset = Number.isSafeInteger(offset) && offset >= 0;
|
|
367
|
+
if (!validOffset) {
|
|
368
|
+
issues.push({
|
|
369
|
+
code: 'invalid-offset',
|
|
370
|
+
path,
|
|
371
|
+
message: 'Fixed-size list offset must be a non-negative safe integer.'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (!Number.isSafeInteger(array.size) || array.size < 1) {
|
|
375
|
+
issues.push({
|
|
376
|
+
code: 'invalid-length',
|
|
377
|
+
path,
|
|
378
|
+
message: 'Fixed-size list size must be positive.'
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const childEnd = (offset + array.length) * array.size;
|
|
382
|
+
if (validOffset && childEnd > array.child.length) {
|
|
383
|
+
issues.push({ code: 'invalid-child', path, message: 'Fixed-size list child is too short.' });
|
|
384
|
+
}
|
|
385
|
+
validateArray(array.child, `${path}.child`, issues);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
case 'list':
|
|
389
|
+
validateList(array, path, issues);
|
|
390
|
+
validateArray(array.child, `${path}.child`, issues);
|
|
391
|
+
break;
|
|
392
|
+
case 'struct':
|
|
393
|
+
if (Object.keys(array.children).length === 0) {
|
|
394
|
+
issues.push({ code: 'invalid-child', path, message: 'Struct must contain children.' });
|
|
395
|
+
}
|
|
396
|
+
for (const [name, child] of Object.entries(array.children)) {
|
|
397
|
+
if ((array.offset || 0) + array.length > child.length) {
|
|
398
|
+
issues.push({
|
|
399
|
+
code: 'invalid-child',
|
|
400
|
+
path: `${path}.${name}`,
|
|
401
|
+
message: 'Struct child is too short.'
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
validateArray(child, `${path}.${name}`, issues);
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
case 'dense-union': {
|
|
408
|
+
const end = (array.offset || 0) + array.length;
|
|
409
|
+
if (end > array.typeIds.length || end > array.valueOffsets.length) {
|
|
410
|
+
issues.push({
|
|
411
|
+
code: 'invalid-union',
|
|
412
|
+
path,
|
|
413
|
+
message: 'Dense-union dispatch buffers are too short.'
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const childIds = new Set();
|
|
417
|
+
for (const child of array.children) {
|
|
418
|
+
if (childIds.has(child.typeId)) {
|
|
419
|
+
issues.push({
|
|
420
|
+
code: 'invalid-union',
|
|
421
|
+
path,
|
|
422
|
+
message: `Duplicate dense-union type ID ${child.typeId}.`
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
childIds.add(child.typeId);
|
|
426
|
+
validateArray(child.data, `${path}.${child.name}`, issues);
|
|
427
|
+
}
|
|
428
|
+
for (let index = array.offset || 0; index < end; index++) {
|
|
429
|
+
if (!isGeoArrowValueValid(array.validity, index - (array.offset || 0)))
|
|
430
|
+
continue;
|
|
431
|
+
const child = array.children.find(candidate => candidate.typeId === array.typeIds[index]);
|
|
432
|
+
const valueOffset = array.valueOffsets[index];
|
|
433
|
+
if (!child || valueOffset < 0 || valueOffset >= child.data.length) {
|
|
434
|
+
issues.push({
|
|
435
|
+
code: 'invalid-union',
|
|
436
|
+
path: `${path}[${index}]`,
|
|
437
|
+
message: 'Dense-union row references an invalid child value.'
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case 'serialized': {
|
|
444
|
+
validateOffsets(array.offsets, array.offset || 0, array.length, array.values.length, array.offsetBase, path, issues);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function validateList(list, path, issues) {
|
|
450
|
+
validateOffsets(list.offsets, list.offset || 0, list.length, list.child.length, list.offsetBase, path, issues);
|
|
451
|
+
}
|
|
452
|
+
function validateOffsets(offsets, offset, length, childLength, offsetBase, path, issues) {
|
|
453
|
+
if (offset < 0 || offset + length >= offsets.length) {
|
|
454
|
+
issues.push({ code: 'invalid-offset', path, message: 'Offset buffer is too short.' });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const base = typeof offsetBase === 'bigint' ? Number(offsetBase) : offsetBase || 0;
|
|
458
|
+
let previous;
|
|
459
|
+
try {
|
|
460
|
+
previous = getGeoArrowOffset(offsets, offset) - base;
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
issues.push({ code: 'unsafe-offset', path, message: error.message });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
for (let index = 1; index <= length; index++) {
|
|
467
|
+
let current;
|
|
468
|
+
try {
|
|
469
|
+
current = getGeoArrowOffset(offsets, offset + index) - base;
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
issues.push({ code: 'unsafe-offset', path, message: error.message });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (current < previous || current < 0 || current > childLength) {
|
|
476
|
+
issues.push({
|
|
477
|
+
code: 'invalid-offset',
|
|
478
|
+
path,
|
|
479
|
+
message: 'Offsets must be monotonic and within child storage.'
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
previous = current;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function validateValidity(validity, length, path, issues) {
|
|
487
|
+
if (!validity)
|
|
488
|
+
return;
|
|
489
|
+
const bitOffset = validity.bitOffset || 0;
|
|
490
|
+
if (bitOffset < 0 || bitOffset + length > validity.values.length * 8) {
|
|
491
|
+
issues.push({ code: 'invalid-validity', path, message: 'Validity bitmap is too short.' });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function validateColumnLayout(column, issues) {
|
|
495
|
+
if (column.encoding === 'geoarrow.wkb' || column.encoding === 'geoarrow.wkt') {
|
|
496
|
+
for (const [index, chunk] of column.chunks.entries()) {
|
|
497
|
+
const expectedEncoding = column.encoding === 'geoarrow.wkb' ? 'binary' : 'utf8';
|
|
498
|
+
if (chunk.kind !== 'serialized' || chunk.encoding !== expectedEncoding) {
|
|
499
|
+
issues.push({
|
|
500
|
+
code: 'invalid-layout',
|
|
501
|
+
path: `chunks[${index}]`,
|
|
502
|
+
message: `Serialized encoding requires ${expectedEncoding} storage.`
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (column.encoding === 'geoarrow.box') {
|
|
509
|
+
for (const [index, chunk] of column.chunks.entries()) {
|
|
510
|
+
if (chunk.kind !== 'struct') {
|
|
511
|
+
addInvalidLayout(`chunks[${index}]`, 'Box encoding requires struct storage.', issues);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const requiredNames = column.dimension === 'xy'
|
|
515
|
+
? ['xmin', 'ymin', 'xmax', 'ymax']
|
|
516
|
+
: column.dimension === 'xyz'
|
|
517
|
+
? ['xmin', 'ymin', 'zmin', 'xmax', 'ymax', 'zmax']
|
|
518
|
+
: column.dimension === 'xym'
|
|
519
|
+
? ['xmin', 'ymin', 'mmin', 'xmax', 'ymax', 'mmax']
|
|
520
|
+
: ['xmin', 'ymin', 'zmin', 'mmin', 'xmax', 'ymax', 'zmax', 'mmax'];
|
|
521
|
+
for (const name of requiredNames) {
|
|
522
|
+
if (chunk.children[name]?.kind !== 'primitive') {
|
|
523
|
+
addInvalidLayout(`chunks[${index}].${name}`, `Box storage requires primitive ${name}.`, issues);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (!column.coordinateLayout) {
|
|
530
|
+
addInvalidLayout('coordinateLayout', 'Native geometry requires a coordinate layout.', issues);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const [index, chunk] of column.chunks.entries()) {
|
|
534
|
+
validateGeometryLayout(chunk, column.encoding, column, `chunks[${index}]`, issues);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function validateGeometryLayout(array, encoding, column, path, issues) {
|
|
538
|
+
if (encoding === 'geoarrow.geometry') {
|
|
539
|
+
if (array.kind !== 'dense-union') {
|
|
540
|
+
addInvalidLayout(path, 'Mixed geometry encoding requires dense-union storage.', issues);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
validateUnionLayouts(array, column, path, issues);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (encoding === 'geoarrow.geometrycollection') {
|
|
547
|
+
if (array.kind !== 'list' || array.child.kind !== 'dense-union') {
|
|
548
|
+
addInvalidLayout(path, 'GeometryCollection requires list-of-dense-union storage.', issues);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
validateUnionLayouts(array.child, column, `${path}.child`, issues);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
let coordinateArray = array;
|
|
555
|
+
const depth = getEncodingDepth(encoding);
|
|
556
|
+
for (let level = 0; level < depth; level++) {
|
|
557
|
+
if (coordinateArray.kind !== 'list') {
|
|
558
|
+
addInvalidLayout(path, `${encoding} requires ${depth} variable-list levels.`, issues);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
coordinateArray = coordinateArray.child;
|
|
562
|
+
}
|
|
563
|
+
validateCoordinateLayout(coordinateArray, column, path, issues);
|
|
564
|
+
}
|
|
565
|
+
function validateUnionLayouts(union, column, path, issues) {
|
|
566
|
+
for (const child of union.children) {
|
|
567
|
+
let encoding;
|
|
568
|
+
try {
|
|
569
|
+
encoding = getEncodingFromChildName(child.name);
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
addInvalidLayout(`${path}.${child.name}`, error.message, issues);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
validateGeometryLayout(child.data, encoding, column, `${path}.${child.name}`, issues);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function validateCoordinateLayout(array, column, path, issues) {
|
|
579
|
+
const dimensionSize = getGeoArrowDimensionSize(column.dimension);
|
|
580
|
+
if (column.coordinateLayout === 'interleaved') {
|
|
581
|
+
if (array.kind !== 'fixed-size-list' ||
|
|
582
|
+
array.size !== dimensionSize ||
|
|
583
|
+
array.child.kind !== 'primitive') {
|
|
584
|
+
addInvalidLayout(path, `Interleaved ${column.dimension} coordinates require fixed-size-list<${dimensionSize}> primitive storage.`, issues);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (array.kind !== 'struct') {
|
|
589
|
+
addInvalidLayout(path, `Separated ${column.dimension} coordinates require struct storage.`, issues);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const requiredNames = column.dimension === 'xy'
|
|
593
|
+
? ['x', 'y']
|
|
594
|
+
: column.dimension === 'xyz'
|
|
595
|
+
? ['x', 'y', 'z']
|
|
596
|
+
: column.dimension === 'xym'
|
|
597
|
+
? ['x', 'y', 'm']
|
|
598
|
+
: ['x', 'y', 'z', 'm'];
|
|
599
|
+
for (const name of requiredNames) {
|
|
600
|
+
if (array.children[name]?.kind !== 'primitive') {
|
|
601
|
+
addInvalidLayout(`${path}.${name}`, `Separated coordinates require primitive ${name}.`, issues);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function addInvalidLayout(path, message, issues) {
|
|
606
|
+
issues.push({ code: 'invalid-layout', path, message });
|
|
607
|
+
}
|
|
608
|
+
function collectStorageKinds(array, kinds) {
|
|
609
|
+
kinds.push(array.kind);
|
|
610
|
+
switch (array.kind) {
|
|
611
|
+
case 'fixed-size-list':
|
|
612
|
+
case 'list':
|
|
613
|
+
collectStorageKinds(array.child, kinds);
|
|
614
|
+
break;
|
|
615
|
+
case 'struct':
|
|
616
|
+
for (const child of Object.values(array.children))
|
|
617
|
+
collectStorageKinds(child, kinds);
|
|
618
|
+
break;
|
|
619
|
+
case 'dense-union':
|
|
620
|
+
for (const child of array.children)
|
|
621
|
+
collectStorageKinds(child.data, kinds);
|
|
622
|
+
break;
|
|
623
|
+
default:
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function collectArrayBuffers(array, buffers) {
|
|
628
|
+
if (array.validity)
|
|
629
|
+
addBuffer(array.validity.values.buffer, buffers);
|
|
630
|
+
switch (array.kind) {
|
|
631
|
+
case 'primitive':
|
|
632
|
+
addBuffer(array.values.buffer, buffers);
|
|
633
|
+
break;
|
|
634
|
+
case 'fixed-size-list':
|
|
635
|
+
collectArrayBuffers(array.child, buffers);
|
|
636
|
+
break;
|
|
637
|
+
case 'list':
|
|
638
|
+
addBuffer(array.offsets.buffer, buffers);
|
|
639
|
+
collectArrayBuffers(array.child, buffers);
|
|
640
|
+
break;
|
|
641
|
+
case 'struct':
|
|
642
|
+
for (const child of Object.values(array.children))
|
|
643
|
+
collectArrayBuffers(child, buffers);
|
|
644
|
+
break;
|
|
645
|
+
case 'dense-union':
|
|
646
|
+
addBuffer(array.typeIds.buffer, buffers);
|
|
647
|
+
addBuffer(array.valueOffsets.buffer, buffers);
|
|
648
|
+
for (const child of array.children)
|
|
649
|
+
collectArrayBuffers(child.data, buffers);
|
|
650
|
+
break;
|
|
651
|
+
case 'serialized':
|
|
652
|
+
addBuffer(array.offsets.buffer, buffers);
|
|
653
|
+
addBuffer(array.values.buffer, buffers);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function addBuffer(buffer, buffers) {
|
|
658
|
+
if (buffer instanceof ArrayBuffer)
|
|
659
|
+
buffers.add(buffer);
|
|
660
|
+
}
|
|
661
|
+
function normalizeSliceIndex(index, length) {
|
|
662
|
+
const normalized = index < 0 ? Math.max(0, length + index) : Math.min(length, index);
|
|
663
|
+
return Math.max(0, Math.trunc(normalized));
|
|
664
|
+
}
|
|
665
|
+
/** Returns a concrete encoding for a geometry value. */
|
|
666
|
+
export function getEncodingForGeometryValue(geometry) {
|
|
667
|
+
return getGeoArrowEncodingForGeometry(geometry.type);
|
|
668
|
+
}
|
|
669
|
+
//# sourceMappingURL=layout.js.map
|