@math.gl/geoarrow 5.0.0-alpha.2 → 5.0.0-alpha.3
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/README.md +45 -15
- package/dist/builder-entry.cjs +561 -0
- package/dist/builder-entry.cjs.map +6 -0
- package/dist/builder-entry.d.ts +4 -0
- package/dist/builder-entry.d.ts.map +1 -0
- package/dist/builder-entry.js +5 -0
- package/dist/builder-entry.js.map +1 -0
- package/dist/builder.d.ts +12 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +112 -10
- package/dist/builder.js.map +1 -1
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +205 -7
- package/dist/codecs.js.map +1 -1
- package/dist/index.cjs +904 -120
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kernels.d.ts +8 -3
- package/dist/kernels.d.ts.map +1 -1
- package/dist/kernels.js +610 -107
- package/dist/kernels.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +28 -5
- package/dist/layout.js.map +1 -1
- package/dist/tessellation-entry.cjs +427 -0
- package/dist/tessellation-entry.cjs.map +6 -0
- package/dist/tessellation-entry.d.ts +4 -0
- package/dist/tessellation-entry.d.ts.map +1 -0
- package/dist/tessellation-entry.js +5 -0
- package/dist/tessellation-entry.js.map +1 -0
- package/dist/types.d.ts +37 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wkb-entry.cjs +1005 -0
- package/dist/wkb-entry.cjs.map +6 -0
- package/dist/wkb-entry.d.ts +3 -0
- package/dist/wkb-entry.d.ts.map +1 -0
- package/dist/wkb-entry.js +6 -0
- package/dist/wkb-entry.js.map +1 -0
- package/dist/worker.cjs +4 -0
- package/dist/worker.cjs.map +1 -1
- package/package.json +21 -6
- package/src/builder-entry.ts +18 -0
- package/src/builder.ts +133 -19
- package/src/codecs.ts +251 -9
- package/src/index.ts +1 -0
- package/src/kernels.ts +811 -133
- package/src/layout.ts +38 -13
- package/src/tessellation-entry.ts +7 -0
- package/src/types.ts +23 -4
- package/src/wkb-entry.ts +11 -0
package/src/kernels.ts
CHANGED
|
@@ -18,8 +18,9 @@ import {
|
|
|
18
18
|
getGeoArrowRowCount,
|
|
19
19
|
getGeoArrowVertexCount,
|
|
20
20
|
isGeoArrowValueValid,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
visitGeoArrowCoordinates,
|
|
22
|
+
getListRange,
|
|
23
|
+
materializeGeometryRow
|
|
23
24
|
} from './layout';
|
|
24
25
|
|
|
25
26
|
/** Resource limits applied before potentially expensive materialization or conversion. */
|
|
@@ -35,14 +36,17 @@ export type GeoArrowResourceLimitOptions = Readonly<{
|
|
|
35
36
|
export type MapGeoArrowCoordinatesOptions = Readonly<{
|
|
36
37
|
dimension?: GeoArrowDimension;
|
|
37
38
|
coordinateLayout?: GeoArrowCoordinateLayout;
|
|
39
|
+
coordinateType?: 'preserve' | 'float32' | 'float64';
|
|
38
40
|
limits?: GeoArrowResourceLimitOptions;
|
|
39
41
|
}>;
|
|
40
42
|
|
|
41
43
|
/** Conversion options for native physical layouts. */
|
|
42
44
|
export type ConvertGeoArrowColumnOptions = Readonly<{
|
|
43
|
-
encoding?: GeoArrowEncoding;
|
|
45
|
+
encoding?: GeoArrowEncoding | 'native';
|
|
44
46
|
dimension?: GeoArrowDimension;
|
|
45
|
-
coordinateLayout?: GeoArrowCoordinateLayout;
|
|
47
|
+
coordinateLayout?: GeoArrowCoordinateLayout | 'preserve';
|
|
48
|
+
coordinateType?: 'preserve' | 'float32' | 'float64';
|
|
49
|
+
offsetType?: 'preserve' | 'int32' | 'int64';
|
|
46
50
|
limits?: GeoArrowResourceLimitOptions;
|
|
47
51
|
}>;
|
|
48
52
|
|
|
@@ -76,6 +80,54 @@ export function getGeoArrowBounds(column: GeoArrowColumn): GeoArrowBounds | null
|
|
|
76
80
|
return Number.isFinite(minimumX) ? [minimumX, minimumY, maximumX, maximumY] : null;
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** Computes exact XY bounds for each logical row without materializing geometry values. */
|
|
84
|
+
export function getGeoArrowRowBounds(column: GeoArrowColumn): readonly (GeoArrowBounds | null)[] {
|
|
85
|
+
const rowCount = getGeoArrowRowCount(column);
|
|
86
|
+
if (column.encoding === 'geoarrow.box') {
|
|
87
|
+
const result: Array<GeoArrowBounds | null> = [];
|
|
88
|
+
for (const chunk of column.chunks) {
|
|
89
|
+
for (let rowIndex = 0; rowIndex < chunk.length; rowIndex++) {
|
|
90
|
+
if (chunk.kind !== 'struct' || !isGeoArrowValueValid(chunk.validity, rowIndex)) {
|
|
91
|
+
result.push(null);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const index = (chunk.offset || 0) + rowIndex;
|
|
95
|
+
const xmin = readPrimitiveNumber(chunk.children['xmin'], index);
|
|
96
|
+
const ymin = readPrimitiveNumber(chunk.children['ymin'], index);
|
|
97
|
+
const xmax = readPrimitiveNumber(chunk.children['xmax'], index);
|
|
98
|
+
const ymax = readPrimitiveNumber(chunk.children['ymax'], index);
|
|
99
|
+
result.push(
|
|
100
|
+
[xmin, ymin, xmax, ymax].every(Number.isFinite) ? [xmin, ymin, xmax, ymax] : null
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
const minimumX = new Float64Array(rowCount).fill(Number.POSITIVE_INFINITY);
|
|
107
|
+
const minimumY = new Float64Array(rowCount).fill(Number.POSITIVE_INFINITY);
|
|
108
|
+
const maximumX = new Float64Array(rowCount).fill(Number.NEGATIVE_INFINITY);
|
|
109
|
+
const maximumY = new Float64Array(rowCount).fill(Number.NEGATIVE_INFINITY);
|
|
110
|
+
visitGeoArrowCoordinates(column, (coordinate, sourceRowIndex) => {
|
|
111
|
+
const [x, y] = coordinate;
|
|
112
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
113
|
+
minimumX[sourceRowIndex] = Math.min(minimumX[sourceRowIndex], x);
|
|
114
|
+
minimumY[sourceRowIndex] = Math.min(minimumY[sourceRowIndex], y);
|
|
115
|
+
maximumX[sourceRowIndex] = Math.max(maximumX[sourceRowIndex], x);
|
|
116
|
+
maximumY[sourceRowIndex] = Math.max(maximumY[sourceRowIndex], y);
|
|
117
|
+
}
|
|
118
|
+
return coordinate;
|
|
119
|
+
});
|
|
120
|
+
const bounds: Array<GeoArrowBounds | null> = [];
|
|
121
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
122
|
+
bounds.push(
|
|
123
|
+
Number.isFinite(minimumX[rowIndex])
|
|
124
|
+
? [minimumX[rowIndex], minimumY[rowIndex], maximumX[rowIndex], maximumY[rowIndex]]
|
|
125
|
+
: null
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return bounds;
|
|
129
|
+
}
|
|
130
|
+
|
|
79
131
|
function getGeoArrowBoxBounds(column: GeoArrowColumn): GeoArrowBounds | null {
|
|
80
132
|
let minimumX = Number.POSITIVE_INFINITY;
|
|
81
133
|
let minimumY = Number.POSITIVE_INFINITY;
|
|
@@ -118,23 +170,33 @@ function readPrimitiveNumber(array: GeoArrowArray, index: number): number {
|
|
|
118
170
|
export function mapGeoArrowCoordinates(
|
|
119
171
|
column: GeoArrowColumn,
|
|
120
172
|
mapper: GeoArrowCoordinateMapper,
|
|
121
|
-
options: MapGeoArrowCoordinatesOptions = {}
|
|
173
|
+
options: MapGeoArrowCoordinatesOptions = {},
|
|
174
|
+
sourceDimension?: GeoArrowDimension
|
|
122
175
|
): GeoArrowColumn {
|
|
123
176
|
assertGeoArrowResourceLimits(column, options.limits);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
177
|
+
if (column.encoding === 'geoarrow.wkb' || column.encoding === 'geoarrow.wkt') {
|
|
178
|
+
throw new Error('Coordinate mapping requires native GeoArrow storage');
|
|
179
|
+
}
|
|
180
|
+
const dimension = options.dimension || column.dimension;
|
|
181
|
+
const layout = options.coordinateLayout || column.coordinateLayout || 'interleaved';
|
|
182
|
+
let rowOffset = 0;
|
|
183
|
+
const chunks = column.chunks.map(chunk => {
|
|
184
|
+
const mapped = mapArrayCoordinates(
|
|
185
|
+
chunk,
|
|
186
|
+
column.encoding,
|
|
187
|
+
rowOffset,
|
|
188
|
+
dimension,
|
|
189
|
+
layout,
|
|
190
|
+
mapper,
|
|
191
|
+
options.coordinateType || 'preserve',
|
|
192
|
+
options.dimension !== undefined,
|
|
193
|
+
options.coordinateLayout !== undefined,
|
|
194
|
+
sourceDimension
|
|
195
|
+
);
|
|
196
|
+
rowOffset += chunk.length;
|
|
131
197
|
return mapped;
|
|
132
198
|
});
|
|
133
|
-
|
|
134
|
-
dimension: options.dimension || column.dimension,
|
|
135
|
-
coordinateLayout: options.coordinateLayout || column.coordinateLayout || 'interleaved'
|
|
136
|
-
});
|
|
137
|
-
return copyColumnMetadata(column, result);
|
|
199
|
+
return {...column, dimension, coordinateLayout: layout, chunks};
|
|
138
200
|
}
|
|
139
201
|
|
|
140
202
|
/**
|
|
@@ -198,13 +260,21 @@ export function convertGeoArrowColumn(
|
|
|
198
260
|
column: GeoArrowColumn,
|
|
199
261
|
options: ConvertGeoArrowColumnOptions = {}
|
|
200
262
|
): GeoArrowColumn {
|
|
201
|
-
const encoding =
|
|
263
|
+
const encoding =
|
|
264
|
+
options.encoding === 'native' || !options.encoding ? column.encoding : options.encoding;
|
|
202
265
|
const dimension = options.dimension || column.dimension;
|
|
203
|
-
const coordinateLayout =
|
|
266
|
+
const coordinateLayout =
|
|
267
|
+
options.coordinateLayout === 'preserve' || !options.coordinateLayout
|
|
268
|
+
? column.coordinateLayout
|
|
269
|
+
: options.coordinateLayout;
|
|
270
|
+
const requestedOffsetType = options.offsetType || 'preserve';
|
|
271
|
+
const currentOffsetType = getColumnOffsetType(column);
|
|
204
272
|
if (
|
|
205
273
|
encoding === column.encoding &&
|
|
206
274
|
dimension === column.dimension &&
|
|
207
|
-
coordinateLayout === column.coordinateLayout
|
|
275
|
+
coordinateLayout === column.coordinateLayout &&
|
|
276
|
+
(requestedOffsetType === 'preserve' || requestedOffsetType === currentOffsetType) &&
|
|
277
|
+
(!options.coordinateType || options.coordinateType === 'preserve')
|
|
208
278
|
) {
|
|
209
279
|
return column;
|
|
210
280
|
}
|
|
@@ -212,18 +282,40 @@ export function convertGeoArrowColumn(
|
|
|
212
282
|
throw new Error('Use encodeGeoArrowWKB or encodeGeoArrowWKT for serialized output');
|
|
213
283
|
}
|
|
214
284
|
assertGeoArrowResourceLimits(column, options.limits);
|
|
215
|
-
const
|
|
216
|
-
|
|
285
|
+
const mapped = mapGeoArrowCoordinates(
|
|
286
|
+
column,
|
|
287
|
+
coordinate => coordinate,
|
|
288
|
+
{
|
|
289
|
+
dimension,
|
|
290
|
+
coordinateLayout: coordinateLayout || undefined,
|
|
291
|
+
coordinateType: options.coordinateType,
|
|
292
|
+
limits: options.limits
|
|
293
|
+
},
|
|
294
|
+
column.dimension
|
|
217
295
|
);
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
296
|
+
const offsetConverted =
|
|
297
|
+
requestedOffsetType === 'preserve'
|
|
298
|
+
? mapped
|
|
299
|
+
: {
|
|
300
|
+
...mapped,
|
|
301
|
+
chunks: mapped.chunks.map(chunk => convertArrayOffsets(chunk, requestedOffsetType))
|
|
302
|
+
};
|
|
303
|
+
if (encoding === column.encoding) return offsetConverted;
|
|
304
|
+
if (encoding === 'geoarrow.geometry') return promoteToDenseUnion(offsetConverted);
|
|
305
|
+
if (column.encoding === 'geoarrow.geometry') return demoteDenseUnion(offsetConverted, encoding);
|
|
306
|
+
throw new Error(`Rows cannot be represented as ${encoding}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resizeCoordinate(
|
|
310
|
+
coordinate: readonly number[],
|
|
311
|
+
sourceDimension: GeoArrowDimension,
|
|
312
|
+
targetDimension: GeoArrowDimension
|
|
313
|
+
): number[] {
|
|
314
|
+
const sourceNames = getDimensionNames(sourceDimension);
|
|
315
|
+
return getDimensionNames(targetDimension).map(name => {
|
|
316
|
+
const sourceIndex = sourceNames.indexOf(name);
|
|
317
|
+
return sourceIndex >= 0 ? (coordinate[sourceIndex] ?? 0) : 0;
|
|
222
318
|
});
|
|
223
|
-
if (result.encoding !== encoding && encoding !== 'geoarrow.geometry') {
|
|
224
|
-
throw new Error(`Rows cannot be represented as ${encoding}`);
|
|
225
|
-
}
|
|
226
|
-
return copyColumnMetadata(column, result);
|
|
227
319
|
}
|
|
228
320
|
|
|
229
321
|
/** Rewinds polygon rings without changing non-polygon rows. */
|
|
@@ -232,20 +324,25 @@ export function rewindGeoArrow(
|
|
|
232
324
|
options: RewindGeoArrowOptions = {}
|
|
233
325
|
): GeoArrowColumn {
|
|
234
326
|
assertGeoArrowResourceLimits(column, options.limits);
|
|
327
|
+
if (column.encoding === 'geoarrow.wkb' || column.encoding === 'geoarrow.wkt') {
|
|
328
|
+
throw new Error('Rewinding requires native GeoArrow storage');
|
|
329
|
+
}
|
|
235
330
|
const outerClockwise = (options.outer || 'counter-clockwise') === 'clockwise';
|
|
236
331
|
let changed = false;
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
332
|
+
const chunks = column.chunks.map(chunk => {
|
|
333
|
+
const clone = mapArrayCoordinates(
|
|
334
|
+
chunk,
|
|
335
|
+
column.encoding,
|
|
336
|
+
0,
|
|
337
|
+
column.dimension,
|
|
338
|
+
column.coordinateLayout || 'interleaved',
|
|
339
|
+
coordinate => coordinate,
|
|
340
|
+
'preserve'
|
|
341
|
+
);
|
|
342
|
+
changed ||= rewindArrayRings(clone, column.encoding, outerClockwise);
|
|
343
|
+
return clone;
|
|
247
344
|
});
|
|
248
|
-
return
|
|
345
|
+
return changed ? {...column, chunks} : column;
|
|
249
346
|
}
|
|
250
347
|
|
|
251
348
|
/** Throws when a column exceeds configured work or output limits. */
|
|
@@ -278,118 +375,345 @@ export function assertGeoArrowResourceLimits(
|
|
|
278
375
|
}
|
|
279
376
|
}
|
|
280
377
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
378
|
+
/** Directly clones a physical array while rewriting only coordinate leaves. */
|
|
379
|
+
function mapArrayCoordinates(
|
|
380
|
+
array: GeoArrowArray,
|
|
381
|
+
encoding: GeoArrowEncoding,
|
|
382
|
+
rowOffset: number,
|
|
383
|
+
targetDimension: GeoArrowDimension,
|
|
384
|
+
targetLayout: GeoArrowCoordinateLayout,
|
|
385
|
+
mapper: GeoArrowCoordinateMapper,
|
|
386
|
+
coordinateType: 'preserve' | 'float32' | 'float64',
|
|
387
|
+
forceDimension = false,
|
|
388
|
+
forceLayout = false,
|
|
389
|
+
sourceDimension?: GeoArrowDimension,
|
|
390
|
+
rowIndicesOverride?: readonly number[]
|
|
391
|
+
): GeoArrowArray {
|
|
392
|
+
if (encoding === 'geoarrow.geometry' && array.kind === 'dense-union') {
|
|
393
|
+
const childRowIndices = collectUnionRowIndices(array, rowOffset);
|
|
394
|
+
return {
|
|
395
|
+
...array,
|
|
396
|
+
children: array.children.map((child, childIndex) => {
|
|
397
|
+
const childEncoding = child.encoding || getEncodingFromChildName(child.name);
|
|
398
|
+
const childDimension = forceDimension
|
|
399
|
+
? targetDimension
|
|
400
|
+
: child.dimension || targetDimension;
|
|
401
|
+
const childLayout = forceLayout ? targetLayout : child.coordinateLayout || targetLayout;
|
|
402
|
+
return {
|
|
403
|
+
...child,
|
|
404
|
+
data: mapArrayCoordinates(
|
|
405
|
+
child.data,
|
|
406
|
+
childEncoding,
|
|
407
|
+
rowOffset,
|
|
408
|
+
childDimension,
|
|
409
|
+
childLayout,
|
|
410
|
+
mapper,
|
|
411
|
+
coordinateType,
|
|
412
|
+
forceDimension,
|
|
413
|
+
forceLayout,
|
|
414
|
+
childDimension,
|
|
415
|
+
childRowIndices[childIndex]
|
|
416
|
+
),
|
|
417
|
+
dimension: childDimension,
|
|
418
|
+
coordinateLayout: childLayout,
|
|
419
|
+
encoding: childEncoding
|
|
420
|
+
};
|
|
421
|
+
})
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (encoding === 'geoarrow.geometrycollection' && array.kind === 'list') {
|
|
286
425
|
return {
|
|
287
|
-
...
|
|
288
|
-
|
|
426
|
+
...array,
|
|
427
|
+
child: mapArrayCoordinates(
|
|
428
|
+
array.child,
|
|
429
|
+
'geoarrow.geometry',
|
|
430
|
+
rowOffset,
|
|
431
|
+
targetDimension,
|
|
432
|
+
targetLayout,
|
|
433
|
+
mapper,
|
|
434
|
+
coordinateType,
|
|
435
|
+
forceDimension,
|
|
436
|
+
forceLayout,
|
|
437
|
+
sourceDimension,
|
|
438
|
+
rowIndicesOverride
|
|
439
|
+
)
|
|
289
440
|
};
|
|
290
441
|
}
|
|
442
|
+
const depth = getEncodingDepth(encoding);
|
|
443
|
+
const rowIndices = rowIndicesOverride || collectLeafRowIndices(array, depth, rowOffset);
|
|
444
|
+
return mapNestedArray(
|
|
445
|
+
array,
|
|
446
|
+
depth,
|
|
447
|
+
rowOffset,
|
|
448
|
+
targetDimension,
|
|
449
|
+
targetLayout,
|
|
450
|
+
mapper,
|
|
451
|
+
coordinateType,
|
|
452
|
+
rowIndices,
|
|
453
|
+
sourceDimension
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function mapNestedArray(
|
|
458
|
+
array: GeoArrowArray,
|
|
459
|
+
depth: number,
|
|
460
|
+
rowOffset: number,
|
|
461
|
+
targetDimension: GeoArrowDimension,
|
|
462
|
+
targetLayout: GeoArrowCoordinateLayout,
|
|
463
|
+
mapper: GeoArrowCoordinateMapper,
|
|
464
|
+
coordinateType: 'preserve' | 'float32' | 'float64',
|
|
465
|
+
rowIndices?: readonly number[],
|
|
466
|
+
sourceDimension?: GeoArrowDimension
|
|
467
|
+
): GeoArrowArray {
|
|
468
|
+
if (depth === 0) {
|
|
469
|
+
return mapCoordinateLeaf(
|
|
470
|
+
array,
|
|
471
|
+
rowOffset,
|
|
472
|
+
targetDimension,
|
|
473
|
+
targetLayout,
|
|
474
|
+
mapper,
|
|
475
|
+
coordinateType,
|
|
476
|
+
rowIndices,
|
|
477
|
+
sourceDimension
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (array.kind !== 'list') return array;
|
|
291
481
|
return {
|
|
292
|
-
...
|
|
293
|
-
|
|
294
|
-
|
|
482
|
+
...array,
|
|
483
|
+
child: mapNestedArray(
|
|
484
|
+
array.child,
|
|
485
|
+
depth - 1,
|
|
486
|
+
rowOffset,
|
|
487
|
+
targetDimension,
|
|
488
|
+
targetLayout,
|
|
489
|
+
mapper,
|
|
490
|
+
coordinateType,
|
|
491
|
+
rowIndices,
|
|
492
|
+
sourceDimension
|
|
493
|
+
)
|
|
494
|
+
};
|
|
295
495
|
}
|
|
296
496
|
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
497
|
+
function mapCoordinateLeaf(
|
|
498
|
+
array: GeoArrowArray,
|
|
499
|
+
rowOffset: number,
|
|
500
|
+
targetDimension: GeoArrowDimension,
|
|
501
|
+
targetLayout: GeoArrowCoordinateLayout,
|
|
502
|
+
mapper: GeoArrowCoordinateMapper,
|
|
503
|
+
coordinateType: 'preserve' | 'float32' | 'float64',
|
|
504
|
+
rowIndices?: readonly number[],
|
|
505
|
+
sourceDimension?: GeoArrowDimension
|
|
506
|
+
): GeoArrowArray {
|
|
507
|
+
const count = array.length;
|
|
508
|
+
const targetSize = getGeoArrowDimensionSize(targetDimension);
|
|
509
|
+
const sourceValues = new Array<number[]>(count);
|
|
510
|
+
for (let index = 0; index < count; index++) {
|
|
511
|
+
const coordinate = readCoordinateAt(array, index);
|
|
512
|
+
const mapped = coordinate
|
|
513
|
+
? mapper(
|
|
514
|
+
sourceDimension
|
|
515
|
+
? resizeCoordinate(coordinate, sourceDimension, targetDimension)
|
|
516
|
+
: coordinate,
|
|
517
|
+
rowIndices?.[index] ?? rowOffset
|
|
518
|
+
)
|
|
519
|
+
: new Array(targetSize).fill(0);
|
|
520
|
+
if (mapped.length !== targetSize) {
|
|
521
|
+
throw new Error(`Coordinate mapper returned ${mapped.length} values; expected ${targetSize}`);
|
|
522
|
+
}
|
|
523
|
+
sourceValues[index] = [...mapped];
|
|
524
|
+
}
|
|
525
|
+
const FloatArray = resolveCoordinateConstructor(array, coordinateType);
|
|
526
|
+
return makeCoordinateLeaf(
|
|
527
|
+
sourceValues,
|
|
528
|
+
targetDimension,
|
|
529
|
+
targetLayout,
|
|
530
|
+
FloatArray,
|
|
531
|
+
array.validity
|
|
532
|
+
);
|
|
304
533
|
}
|
|
305
534
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
? {...geometry, geometries}
|
|
314
|
-
: geometry;
|
|
535
|
+
/** Fills one row-index entry per coordinate in a concrete nested array. */
|
|
536
|
+
function collectLeafRowIndices(array: GeoArrowArray, depth: number, rowOffset: number): number[] {
|
|
537
|
+
const output = new Array<number>(getLeafLength(array, depth)).fill(rowOffset);
|
|
538
|
+
for (let rowIndex = 0; rowIndex < array.length; rowIndex++) {
|
|
539
|
+
if (isGeoArrowValueValid(array.validity, rowIndex)) {
|
|
540
|
+
assignNestedRowIndices(array, rowIndex, depth, rowOffset + rowIndex, output);
|
|
541
|
+
}
|
|
315
542
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
543
|
+
return output;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function assignNestedRowIndices(
|
|
547
|
+
array: GeoArrowArray,
|
|
548
|
+
index: number,
|
|
549
|
+
depth: number,
|
|
550
|
+
rowIndex: number,
|
|
551
|
+
output: number[]
|
|
552
|
+
): void {
|
|
553
|
+
if (depth === 0) {
|
|
554
|
+
if (index >= 0 && index < output.length) output[index] = rowIndex;
|
|
555
|
+
return;
|
|
319
556
|
}
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
: {...geometry, coordinates};
|
|
557
|
+
if (array.kind !== 'list') return;
|
|
558
|
+
const [first, last] = getListRange(array, index);
|
|
559
|
+
for (let childIndex = first; childIndex < last; childIndex++) {
|
|
560
|
+
assignNestedRowIndices(array.child, childIndex, depth - 1, rowIndex, output);
|
|
325
561
|
}
|
|
326
|
-
return geometry;
|
|
327
562
|
}
|
|
328
563
|
|
|
329
|
-
function
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const result = rings.map((ring, ringIndex) => {
|
|
335
|
-
const shouldBeClockwise = ringIndex === 0 ? outerClockwise : !outerClockwise;
|
|
336
|
-
const isClockwise = getRingSignedArea(ring) < 0;
|
|
337
|
-
if (isClockwise === shouldBeClockwise) return ring;
|
|
338
|
-
changed = true;
|
|
339
|
-
return [...ring].reverse();
|
|
564
|
+
function collectUnionRowIndices(union: GeoArrowArray, rowOffset: number): number[][] {
|
|
565
|
+
if (union.kind !== 'dense-union') return [];
|
|
566
|
+
const output = union.children.map(child => {
|
|
567
|
+
const encoding = child.encoding || getEncodingFromChildName(child.name);
|
|
568
|
+
return new Array<number>(getLeafLength(child.data, getEncodingDepth(encoding))).fill(rowOffset);
|
|
340
569
|
});
|
|
341
|
-
|
|
570
|
+
for (let rowIndex = 0; rowIndex < union.length; rowIndex++) {
|
|
571
|
+
if (!isGeoArrowValueValid(union.validity, rowIndex)) continue;
|
|
572
|
+
const physical = (union.offset || 0) + rowIndex;
|
|
573
|
+
const childIndex = union.children.findIndex(child => child.typeId === union.typeIds[physical]);
|
|
574
|
+
if (childIndex < 0) continue;
|
|
575
|
+
const child = union.children[childIndex];
|
|
576
|
+
assignUnionRowIndices(
|
|
577
|
+
child.data,
|
|
578
|
+
child.encoding || getEncodingFromChildName(child.name),
|
|
579
|
+
union.valueOffsets[physical],
|
|
580
|
+
rowOffset + rowIndex,
|
|
581
|
+
output[childIndex]
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
return output;
|
|
342
585
|
}
|
|
343
586
|
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
587
|
+
function assignUnionRowIndices(
|
|
588
|
+
array: GeoArrowArray,
|
|
589
|
+
encoding: GeoArrowEncoding,
|
|
590
|
+
index: number,
|
|
591
|
+
rowIndex: number,
|
|
592
|
+
output: number[]
|
|
593
|
+
): void {
|
|
594
|
+
if (encoding === 'geoarrow.geometry' && array.kind === 'dense-union') {
|
|
595
|
+
const physical = (array.offset || 0) + index;
|
|
596
|
+
const childIndex = array.children.findIndex(child => child.typeId === array.typeIds[physical]);
|
|
597
|
+
if (childIndex >= 0) {
|
|
598
|
+
const child = array.children[childIndex];
|
|
599
|
+
assignUnionRowIndices(
|
|
600
|
+
child.data,
|
|
601
|
+
child.encoding || getEncodingFromChildName(child.name),
|
|
602
|
+
array.valueOffsets[physical],
|
|
603
|
+
rowIndex,
|
|
604
|
+
output
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
return;
|
|
350
608
|
}
|
|
351
|
-
|
|
609
|
+
if (encoding === 'geoarrow.geometrycollection' && array.kind === 'list') {
|
|
610
|
+
const [first, last] = getListRange(array, index);
|
|
611
|
+
if (array.child.kind === 'dense-union') {
|
|
612
|
+
for (let childIndex = first; childIndex < last; childIndex++) {
|
|
613
|
+
assignUnionRowIndices(array.child, 'geoarrow.geometry', childIndex, rowIndex, output);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
assignNestedRowIndices(array, index, getEncodingDepth(encoding), rowIndex, output);
|
|
352
619
|
}
|
|
353
620
|
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
621
|
+
function getLeafLength(array: GeoArrowArray, depth: number): number {
|
|
622
|
+
let current = array;
|
|
623
|
+
for (let level = 0; level < depth; level++) {
|
|
624
|
+
if (current.kind !== 'list') return 0;
|
|
625
|
+
current = current.child;
|
|
626
|
+
}
|
|
627
|
+
return current.length;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function resolveCoordinateConstructor(
|
|
631
|
+
array: GeoArrowArray,
|
|
632
|
+
coordinateType: 'preserve' | 'float32' | 'float64'
|
|
633
|
+
): Float32ArrayConstructor | Float64ArrayConstructor {
|
|
634
|
+
if (coordinateType === 'float32') return Float32Array;
|
|
635
|
+
if (coordinateType === 'float64') return Float64Array;
|
|
636
|
+
if (array.kind === 'fixed-size-list' && array.child.kind === 'primitive') {
|
|
637
|
+
return array.child.values instanceof Float32Array ? Float32Array : Float64Array;
|
|
638
|
+
}
|
|
639
|
+
if (array.kind === 'struct') {
|
|
640
|
+
const child = array.children['x'];
|
|
641
|
+
if (child?.kind === 'primitive' && child.values instanceof Float32Array) return Float32Array;
|
|
642
|
+
}
|
|
643
|
+
return Float64Array;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function makeCoordinateLeaf(
|
|
647
|
+
coordinates: readonly (readonly number[])[],
|
|
648
|
+
dimension: GeoArrowDimension,
|
|
649
|
+
layout: GeoArrowCoordinateLayout,
|
|
650
|
+
FloatArray: Float32ArrayConstructor | Float64ArrayConstructor,
|
|
651
|
+
validity: GeoArrowArray['validity']
|
|
652
|
+
): GeoArrowArray {
|
|
653
|
+
const size = getGeoArrowDimensionSize(dimension);
|
|
654
|
+
if (layout === 'interleaved') {
|
|
655
|
+
const values = new FloatArray(coordinates.length * size);
|
|
656
|
+
for (let index = 0; index < coordinates.length; index++) {
|
|
657
|
+
values.set(coordinates[index], index * size);
|
|
366
658
|
}
|
|
367
|
-
|
|
659
|
+
return {
|
|
660
|
+
kind: 'fixed-size-list',
|
|
661
|
+
length: coordinates.length,
|
|
662
|
+
size,
|
|
663
|
+
child: {kind: 'primitive', length: values.length, values},
|
|
664
|
+
validity
|
|
665
|
+
};
|
|
368
666
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
667
|
+
const children: Record<string, GeoArrowArray> = {
|
|
668
|
+
x: {kind: 'primitive', length: coordinates.length, values: new FloatArray(coordinates.length)},
|
|
669
|
+
y: {kind: 'primitive', length: coordinates.length, values: new FloatArray(coordinates.length)}
|
|
670
|
+
};
|
|
671
|
+
const names = getDimensionNames(dimension);
|
|
672
|
+
for (let component = 2; component < names.length; component++) {
|
|
673
|
+
children[names[component]] = {
|
|
674
|
+
kind: 'primitive',
|
|
675
|
+
length: coordinates.length,
|
|
676
|
+
values: new FloatArray(coordinates.length)
|
|
677
|
+
};
|
|
374
678
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
679
|
+
for (let index = 0; index < coordinates.length; index++) {
|
|
680
|
+
const coordinate = coordinates[index];
|
|
681
|
+
for (let component = 0; component < names.length; component++) {
|
|
682
|
+
const child = children[names[component]];
|
|
683
|
+
if (child.kind === 'primitive') child.values[index] = coordinate[component];
|
|
684
|
+
}
|
|
378
685
|
}
|
|
379
|
-
return {
|
|
686
|
+
return {kind: 'struct', length: coordinates.length, children, validity};
|
|
380
687
|
}
|
|
381
688
|
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
689
|
+
function readCoordinateAt(array: GeoArrowArray, index: number): number[] | null {
|
|
690
|
+
if (!isGeoArrowValueValid(array.validity, index)) return null;
|
|
691
|
+
if (array.kind === 'fixed-size-list') {
|
|
692
|
+
const logicalIndex = (array.offset || 0) + index;
|
|
693
|
+
if (array.child.kind !== 'primitive') return null;
|
|
694
|
+
const values: number[] = [];
|
|
695
|
+
for (let component = 0; component < array.size; component++) {
|
|
696
|
+
const scalarIndex = logicalIndex * array.size + component;
|
|
697
|
+
values.push(
|
|
698
|
+
Number(
|
|
699
|
+
array.child.values[(array.child.offset || 0) + scalarIndex * (array.child.stride || 1)]
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
return values;
|
|
704
|
+
}
|
|
705
|
+
if (array.kind === 'struct') {
|
|
706
|
+
const logicalIndex = (array.offset || 0) + index;
|
|
707
|
+
const values: number[] = [];
|
|
708
|
+
for (const name of ['x', 'y', 'z', 'm'] as const) {
|
|
709
|
+
const child = array.children[name];
|
|
710
|
+
if (child?.kind === 'primitive') {
|
|
711
|
+
values.push(Number(child.values[(child.offset || 0) + logicalIndex * (child.stride || 1)]));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return values.length >= 2 ? values : null;
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
393
717
|
}
|
|
394
718
|
|
|
395
719
|
function getDimensionNames(dimension: GeoArrowDimension): Array<'x' | 'y' | 'z' | 'm'> {
|
|
@@ -405,15 +729,369 @@ function getDimensionNames(dimension: GeoArrowDimension): Array<'x' | 'y' | 'z'
|
|
|
405
729
|
}
|
|
406
730
|
}
|
|
407
731
|
|
|
408
|
-
function
|
|
732
|
+
function getEncodingFromChildName(name: string): GeoArrowEncoding {
|
|
733
|
+
const normalized = name.replace(/[^a-z]/gi, '').toLowerCase();
|
|
734
|
+
const encoding = `geoarrow.${normalized}` as GeoArrowEncoding;
|
|
735
|
+
if (
|
|
736
|
+
encoding === 'geoarrow.point' ||
|
|
737
|
+
encoding === 'geoarrow.linestring' ||
|
|
738
|
+
encoding === 'geoarrow.polygon' ||
|
|
739
|
+
encoding === 'geoarrow.multipoint' ||
|
|
740
|
+
encoding === 'geoarrow.multilinestring' ||
|
|
741
|
+
encoding === 'geoarrow.multipolygon' ||
|
|
742
|
+
encoding === 'geoarrow.geometrycollection'
|
|
743
|
+
) {
|
|
744
|
+
return encoding;
|
|
745
|
+
}
|
|
746
|
+
throw new Error(`Unknown GeoArrow dense-union child ${name}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function getEncodingDepth(encoding: GeoArrowEncoding): 0 | 1 | 2 | 3 {
|
|
750
|
+
switch (encoding) {
|
|
751
|
+
case 'geoarrow.point':
|
|
752
|
+
return 0;
|
|
753
|
+
case 'geoarrow.linestring':
|
|
754
|
+
case 'geoarrow.multipoint':
|
|
755
|
+
return 1;
|
|
756
|
+
case 'geoarrow.polygon':
|
|
757
|
+
case 'geoarrow.multilinestring':
|
|
758
|
+
return 2;
|
|
759
|
+
case 'geoarrow.multipolygon':
|
|
760
|
+
return 3;
|
|
761
|
+
default:
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function promoteToDenseUnion(column: GeoArrowColumn): GeoArrowColumn {
|
|
767
|
+
if (column.encoding === 'geoarrow.geometry') return column;
|
|
768
|
+
const chunks = column.chunks.map(chunk => {
|
|
769
|
+
const typeId = getCanonicalTypeId(column.encoding, column.dimension);
|
|
770
|
+
const typeIds = new Int8Array(chunk.length);
|
|
771
|
+
typeIds.fill(typeId);
|
|
772
|
+
const valueOffsets = new Int32Array(chunk.length);
|
|
773
|
+
for (let index = 0; index < chunk.length; index++) valueOffsets[index] = index;
|
|
774
|
+
return {
|
|
775
|
+
kind: 'dense-union' as const,
|
|
776
|
+
length: chunk.length,
|
|
777
|
+
typeIds,
|
|
778
|
+
valueOffsets,
|
|
779
|
+
validity: chunk.validity,
|
|
780
|
+
children: [
|
|
781
|
+
{
|
|
782
|
+
name: getGeoArrowGeometryTypeName(column.encoding),
|
|
783
|
+
typeId,
|
|
784
|
+
encoding: column.encoding,
|
|
785
|
+
dimension: column.dimension,
|
|
786
|
+
coordinateLayout: column.coordinateLayout,
|
|
787
|
+
data: chunk
|
|
788
|
+
}
|
|
789
|
+
]
|
|
790
|
+
};
|
|
791
|
+
});
|
|
792
|
+
return {...column, encoding: 'geoarrow.geometry', chunks};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function demoteDenseUnion(column: GeoArrowColumn, encoding: GeoArrowEncoding): GeoArrowColumn {
|
|
796
|
+
if (encoding === 'geoarrow.geometry' || encoding === 'geoarrow.geometrycollection') return column;
|
|
797
|
+
const rows: Array<GeoArrowGeometryValue | null> = [];
|
|
798
|
+
for (const chunk of column.chunks) {
|
|
799
|
+
if (chunk.kind !== 'dense-union') throw new Error('Expected dense-union storage');
|
|
800
|
+
for (let index = 0; index < chunk.length; index++) {
|
|
801
|
+
if (!isGeoArrowValueValid(chunk.validity, index)) {
|
|
802
|
+
rows.push(null);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const physical = (chunk.offset || 0) + index;
|
|
806
|
+
const child = chunk.children.find(candidate => candidate.typeId === chunk.typeIds[physical]);
|
|
807
|
+
const childEncoding = child && (child.encoding || getEncodingFromChildName(child.name));
|
|
808
|
+
if (!child || childEncoding !== encoding) {
|
|
809
|
+
throw new Error(`Rows cannot be represented as ${encoding}`);
|
|
810
|
+
}
|
|
811
|
+
const geometry = materializeGeometryRow(
|
|
812
|
+
child.data,
|
|
813
|
+
chunk.valueOffsets[physical],
|
|
814
|
+
childEncoding
|
|
815
|
+
);
|
|
816
|
+
rows.push(
|
|
817
|
+
geometry
|
|
818
|
+
? resizeGeometryValue(geometry, child.dimension || column.dimension, column.dimension)
|
|
819
|
+
: null
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
const built = makeGeoArrowColumnFromGeometryRows(rows, {
|
|
824
|
+
dimension: column.dimension,
|
|
825
|
+
coordinateLayout: column.coordinateLayout || 'interleaved',
|
|
826
|
+
offsetType: getColumnOffsetType(column)
|
|
827
|
+
});
|
|
409
828
|
return {
|
|
410
|
-
...
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
829
|
+
...built,
|
|
830
|
+
encoding: encoding as GeoArrowColumn['encoding'],
|
|
831
|
+
spatialReference: column.spatialReference,
|
|
832
|
+
edges: column.edges,
|
|
833
|
+
metadata: column.metadata
|
|
414
834
|
};
|
|
415
835
|
}
|
|
416
836
|
|
|
837
|
+
function resizeGeometryValue(
|
|
838
|
+
geometry: import('./types').GeoArrowGeometryValue,
|
|
839
|
+
sourceDimension: GeoArrowDimension,
|
|
840
|
+
targetDimension: GeoArrowDimension
|
|
841
|
+
): import('./types').GeoArrowGeometryValue {
|
|
842
|
+
const map = (value: unknown): unknown => {
|
|
843
|
+
if (Array.isArray(value) && (value.length === 0 || typeof value[0] === 'number')) {
|
|
844
|
+
return resizeCoordinate(value as number[], sourceDimension, targetDimension);
|
|
845
|
+
}
|
|
846
|
+
return Array.isArray(value) ? value.map(map) : value;
|
|
847
|
+
};
|
|
848
|
+
if (geometry.type === 'GeometryCollection') {
|
|
849
|
+
return {
|
|
850
|
+
type: geometry.type,
|
|
851
|
+
geometries: geometry.geometries.map(child =>
|
|
852
|
+
resizeGeometryValue(child, sourceDimension, targetDimension)
|
|
853
|
+
)
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
...geometry,
|
|
858
|
+
coordinates: map(geometry.coordinates)
|
|
859
|
+
} as import('./types').GeoArrowGeometryValue;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function getCanonicalTypeId(encoding: GeoArrowEncoding, dimension: GeoArrowDimension): number {
|
|
863
|
+
const families = [
|
|
864
|
+
'point',
|
|
865
|
+
'linestring',
|
|
866
|
+
'polygon',
|
|
867
|
+
'multipoint',
|
|
868
|
+
'multilinestring',
|
|
869
|
+
'multipolygon',
|
|
870
|
+
'geometrycollection'
|
|
871
|
+
];
|
|
872
|
+
const dimensions = ['xy', 'xyz', 'xym', 'xyzm'];
|
|
873
|
+
const family = encoding.replace('geoarrow.', '');
|
|
874
|
+
const familyIndex = families.indexOf(family);
|
|
875
|
+
const dimensionIndex = dimensions.indexOf(dimension);
|
|
876
|
+
return familyIndex < 0 || dimensionIndex < 0 ? 1 : familyIndex * 4 + dimensionIndex + 1;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function getColumnOffsetType(column: GeoArrowColumn): 'int32' | 'int64' {
|
|
880
|
+
for (const chunk of column.chunks) {
|
|
881
|
+
const found = findOffsetType(chunk);
|
|
882
|
+
if (found) return found;
|
|
883
|
+
}
|
|
884
|
+
return 'int32';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function findOffsetType(array: GeoArrowArray): 'int32' | 'int64' | null {
|
|
888
|
+
switch (array.kind) {
|
|
889
|
+
case 'list':
|
|
890
|
+
case 'serialized':
|
|
891
|
+
return array.offsets instanceof BigInt64Array ? 'int64' : 'int32';
|
|
892
|
+
case 'fixed-size-list':
|
|
893
|
+
return findOffsetType(array.child);
|
|
894
|
+
case 'struct': {
|
|
895
|
+
for (const child of Object.values(array.children)) {
|
|
896
|
+
const found = findOffsetType(child);
|
|
897
|
+
if (found) return found;
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
case 'dense-union':
|
|
902
|
+
for (const child of array.children) {
|
|
903
|
+
const found = findOffsetType(child.data);
|
|
904
|
+
if (found) return found;
|
|
905
|
+
}
|
|
906
|
+
return null;
|
|
907
|
+
default:
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function convertArrayOffsets(array: GeoArrowArray, offsetType: 'int32' | 'int64'): GeoArrowArray {
|
|
913
|
+
const toOffsets = (
|
|
914
|
+
offsets: Int32Array | BigInt64Array,
|
|
915
|
+
offsetBase: number | bigint | undefined
|
|
916
|
+
): {offsets: Int32Array | BigInt64Array; offsetBase?: number | bigint} => {
|
|
917
|
+
if (offsetType === 'int64') {
|
|
918
|
+
const result = new BigInt64Array(offsets.length);
|
|
919
|
+
for (let index = 0; index < offsets.length; index++) result[index] = BigInt(offsets[index]);
|
|
920
|
+
return {offsets: result, ...(offsetBase === undefined ? {} : {offsetBase})};
|
|
921
|
+
}
|
|
922
|
+
const result = new Int32Array(offsets.length);
|
|
923
|
+
const base = BigInt(offsetBase ?? 0);
|
|
924
|
+
for (let index = 0; index < offsets.length; index++) {
|
|
925
|
+
const value = BigInt(offsets[index]) - base;
|
|
926
|
+
if (value < -2147483648n || value > 2147483647n) {
|
|
927
|
+
throw new Error('GeoArrow offset cannot be represented as Int32');
|
|
928
|
+
}
|
|
929
|
+
result[index] = Number(value);
|
|
930
|
+
}
|
|
931
|
+
return {offsets: result, offsetBase: 0};
|
|
932
|
+
};
|
|
933
|
+
switch (array.kind) {
|
|
934
|
+
case 'list':
|
|
935
|
+
if (array.offsets instanceof (offsetType === 'int64' ? BigInt64Array : Int32Array)) {
|
|
936
|
+
return {...array, child: convertArrayOffsets(array.child, offsetType)};
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
...array,
|
|
940
|
+
...toOffsets(array.offsets, array.offsetBase),
|
|
941
|
+
child: convertArrayOffsets(array.child, offsetType)
|
|
942
|
+
} as GeoArrowArray;
|
|
943
|
+
case 'serialized':
|
|
944
|
+
if (array.offsets instanceof (offsetType === 'int64' ? BigInt64Array : Int32Array))
|
|
945
|
+
return array;
|
|
946
|
+
return {...array, ...toOffsets(array.offsets, array.offsetBase)} as GeoArrowArray;
|
|
947
|
+
case 'fixed-size-list':
|
|
948
|
+
return {...array, child: convertArrayOffsets(array.child, offsetType)};
|
|
949
|
+
case 'struct':
|
|
950
|
+
return {
|
|
951
|
+
...array,
|
|
952
|
+
children: Object.fromEntries(
|
|
953
|
+
Object.entries(array.children).map(([name, child]) => [
|
|
954
|
+
name,
|
|
955
|
+
convertArrayOffsets(child, offsetType)
|
|
956
|
+
])
|
|
957
|
+
)
|
|
958
|
+
};
|
|
959
|
+
case 'dense-union':
|
|
960
|
+
return {
|
|
961
|
+
...array,
|
|
962
|
+
children: array.children.map(child => ({
|
|
963
|
+
...child,
|
|
964
|
+
data: convertArrayOffsets(child.data, offsetType)
|
|
965
|
+
}))
|
|
966
|
+
};
|
|
967
|
+
default:
|
|
968
|
+
return array;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function getGeoArrowGeometryTypeName(encoding: GeoArrowEncoding): string {
|
|
973
|
+
return encoding.replace('geoarrow.', '').replace(/^./, character => character.toUpperCase());
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function rewindArrayRings(
|
|
977
|
+
array: GeoArrowArray,
|
|
978
|
+
encoding: GeoArrowEncoding,
|
|
979
|
+
outerClockwise: boolean
|
|
980
|
+
): boolean {
|
|
981
|
+
if (encoding === 'geoarrow.geometry' && array.kind === 'dense-union') {
|
|
982
|
+
let changed = false;
|
|
983
|
+
for (const child of array.children) {
|
|
984
|
+
if (
|
|
985
|
+
rewindArrayRings(
|
|
986
|
+
child.data,
|
|
987
|
+
child.encoding || getEncodingFromChildName(child.name),
|
|
988
|
+
outerClockwise
|
|
989
|
+
)
|
|
990
|
+
)
|
|
991
|
+
changed = true;
|
|
992
|
+
}
|
|
993
|
+
return changed;
|
|
994
|
+
}
|
|
995
|
+
if (encoding === 'geoarrow.geometrycollection' && array.kind === 'list') {
|
|
996
|
+
if (array.child.kind !== 'dense-union') return false;
|
|
997
|
+
let changed = false;
|
|
998
|
+
for (const child of array.child.children) {
|
|
999
|
+
if (
|
|
1000
|
+
rewindArrayRings(
|
|
1001
|
+
child.data,
|
|
1002
|
+
child.encoding || getEncodingFromChildName(child.name),
|
|
1003
|
+
outerClockwise
|
|
1004
|
+
)
|
|
1005
|
+
)
|
|
1006
|
+
changed = true;
|
|
1007
|
+
}
|
|
1008
|
+
return changed;
|
|
1009
|
+
}
|
|
1010
|
+
if (encoding !== 'geoarrow.polygon' && encoding !== 'geoarrow.multipolygon') return false;
|
|
1011
|
+
if (array.kind !== 'list' || array.child.kind !== 'list') return false;
|
|
1012
|
+
let changed = false;
|
|
1013
|
+
for (let rowIndex = 0; rowIndex < array.length; rowIndex++) {
|
|
1014
|
+
if (!isGeoArrowValueValid(array.validity, rowIndex)) continue;
|
|
1015
|
+
const [, rowEnd] = getListRange(array, rowIndex);
|
|
1016
|
+
const rowStart = getListRange(array, rowIndex)[0];
|
|
1017
|
+
if (encoding === 'geoarrow.polygon') {
|
|
1018
|
+
const rings = array.child;
|
|
1019
|
+
if (rewindRingGroup(rings, rowStart, rowEnd, outerClockwise)) changed = true;
|
|
1020
|
+
} else {
|
|
1021
|
+
const polygons = array.child;
|
|
1022
|
+
for (let polygonIndex = rowStart; polygonIndex < rowEnd; polygonIndex++) {
|
|
1023
|
+
const [ringStart, ringEnd] = getListRange(polygons, polygonIndex);
|
|
1024
|
+
if (rewindRingGroup(polygons.child, ringStart, ringEnd, outerClockwise)) changed = true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return changed;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function rewindRingGroup(
|
|
1032
|
+
rings: GeoArrowArray,
|
|
1033
|
+
firstRing: number,
|
|
1034
|
+
lastRing: number,
|
|
1035
|
+
outerClockwise: boolean
|
|
1036
|
+
): boolean {
|
|
1037
|
+
if (rings.kind !== 'list') return false;
|
|
1038
|
+
const leaf = rings.child;
|
|
1039
|
+
if (leaf.kind !== 'fixed-size-list' && leaf.kind !== 'struct') return false;
|
|
1040
|
+
let changed = false;
|
|
1041
|
+
for (let ringIndex = firstRing; ringIndex < lastRing; ringIndex++) {
|
|
1042
|
+
const [first, last] = getListRange(rings, ringIndex);
|
|
1043
|
+
const area = getRingArea(leaf, first, last);
|
|
1044
|
+
if (!Number.isFinite(area) || area === 0) continue;
|
|
1045
|
+
const shouldClockwise = ringIndex === firstRing ? outerClockwise : !outerClockwise;
|
|
1046
|
+
if (area < 0 !== shouldClockwise) {
|
|
1047
|
+
reverseCoordinateRange(leaf, first, last);
|
|
1048
|
+
changed = true;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return changed;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function getRingArea(leaf: GeoArrowArray, first: number, last: number): number {
|
|
1055
|
+
let area = 0;
|
|
1056
|
+
for (let index = first; index < last; index++) {
|
|
1057
|
+
const current = readCoordinateAt(leaf, index);
|
|
1058
|
+
const next = readCoordinateAt(leaf, index + 1 < last ? index + 1 : first);
|
|
1059
|
+
if (!current || !next) return Number.NaN;
|
|
1060
|
+
area += current[0] * next[1] - next[0] * current[1];
|
|
1061
|
+
}
|
|
1062
|
+
return area / 2;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function reverseCoordinateRange(leaf: GeoArrowArray, first: number, last: number): void {
|
|
1066
|
+
const values: number[][] = [];
|
|
1067
|
+
for (let index = first; index < last; index++) values.push(readCoordinateAt(leaf, index)!);
|
|
1068
|
+
values.reverse();
|
|
1069
|
+
for (let index = first; index < last; index++)
|
|
1070
|
+
writeCoordinateAt(leaf, index, values[index - first]);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function writeCoordinateAt(
|
|
1074
|
+
array: GeoArrowArray,
|
|
1075
|
+
index: number,
|
|
1076
|
+
coordinate: readonly number[]
|
|
1077
|
+
): void {
|
|
1078
|
+
if (array.kind === 'fixed-size-list' && array.child.kind === 'primitive') {
|
|
1079
|
+
const logical = (array.offset || 0) + index;
|
|
1080
|
+
for (let component = 0; component < array.size; component++) {
|
|
1081
|
+
const scalar = (array.child.offset || 0) + logical * array.size + component;
|
|
1082
|
+
array.child.values[scalar] = coordinate[component];
|
|
1083
|
+
}
|
|
1084
|
+
} else if (array.kind === 'struct') {
|
|
1085
|
+
const logical = (array.offset || 0) + index;
|
|
1086
|
+
const names = ['x', 'y', 'z', 'm'] as const;
|
|
1087
|
+
for (let component = 0; component < names.length; component++) {
|
|
1088
|
+
const child = array.children[names[component]];
|
|
1089
|
+
if (child?.kind === 'primitive')
|
|
1090
|
+
child.values[(child.offset || 0) + logical * (child.stride || 1)] = coordinate[component];
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
417
1095
|
function collectCoordinateLeaves(column: GeoArrowColumn): Array<Float32Array | Float64Array> {
|
|
418
1096
|
const leaves: Array<Float32Array | Float64Array> = [];
|
|
419
1097
|
for (const chunk of column.chunks) collectArrayCoordinateLeaves(chunk, leaves);
|