@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.
Files changed (54) hide show
  1. package/README.md +45 -15
  2. package/dist/builder-entry.cjs +561 -0
  3. package/dist/builder-entry.cjs.map +6 -0
  4. package/dist/builder-entry.d.ts +4 -0
  5. package/dist/builder-entry.d.ts.map +1 -0
  6. package/dist/builder-entry.js +5 -0
  7. package/dist/builder-entry.js.map +1 -0
  8. package/dist/builder.d.ts +12 -1
  9. package/dist/builder.d.ts.map +1 -1
  10. package/dist/builder.js +112 -10
  11. package/dist/builder.js.map +1 -1
  12. package/dist/codecs.d.ts.map +1 -1
  13. package/dist/codecs.js +205 -7
  14. package/dist/codecs.js.map +1 -1
  15. package/dist/index.cjs +904 -120
  16. package/dist/index.cjs.map +2 -2
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/kernels.d.ts +8 -3
  22. package/dist/kernels.d.ts.map +1 -1
  23. package/dist/kernels.js +610 -107
  24. package/dist/kernels.js.map +1 -1
  25. package/dist/layout.d.ts.map +1 -1
  26. package/dist/layout.js +28 -5
  27. package/dist/layout.js.map +1 -1
  28. package/dist/tessellation-entry.cjs +427 -0
  29. package/dist/tessellation-entry.cjs.map +6 -0
  30. package/dist/tessellation-entry.d.ts +4 -0
  31. package/dist/tessellation-entry.d.ts.map +1 -0
  32. package/dist/tessellation-entry.js +5 -0
  33. package/dist/tessellation-entry.js.map +1 -0
  34. package/dist/types.d.ts +37 -3
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/types.js.map +1 -1
  37. package/dist/wkb-entry.cjs +1005 -0
  38. package/dist/wkb-entry.cjs.map +6 -0
  39. package/dist/wkb-entry.d.ts +3 -0
  40. package/dist/wkb-entry.d.ts.map +1 -0
  41. package/dist/wkb-entry.js +6 -0
  42. package/dist/wkb-entry.js.map +1 -0
  43. package/dist/worker.cjs +4 -0
  44. package/dist/worker.cjs.map +1 -1
  45. package/package.json +21 -6
  46. package/src/builder-entry.ts +18 -0
  47. package/src/builder.ts +133 -19
  48. package/src/codecs.ts +251 -9
  49. package/src/index.ts +1 -0
  50. package/src/kernels.ts +811 -133
  51. package/src/layout.ts +38 -13
  52. package/src/tessellation-entry.ts +7 -0
  53. package/src/types.ts +23 -4
  54. 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
- materializeGeoArrowRows,
22
- visitGeoArrowCoordinates
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
- const rows = materializeGeoArrowRows(column);
125
- let rowIndex = 0;
126
- const mappedRows = rows.map(row => {
127
- const mapped = row
128
- ? mapGeometryCoordinates(row, coordinate => mapper(coordinate, rowIndex))
129
- : null;
130
- rowIndex++;
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
- const result = makeGeoArrowColumnFromGeometryRows(mappedRows, {
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 = options.encoding || column.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 = options.coordinateLayout || column.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 rows = materializeGeoArrowRows(column).map(row =>
216
- row ? convertGeometryFamily(row, encoding, column.dimension, dimension) : null
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 result = makeGeoArrowColumnFromGeometryRows(rows, {
219
- encoding: encoding === 'geoarrow.geometry' ? encoding : undefined,
220
- dimension,
221
- coordinateLayout: coordinateLayout || 'interleaved'
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 rows = materializeGeoArrowRows(column).map(row => {
238
- if (!row) return null;
239
- const rewound = rewindGeometry(row, outerClockwise);
240
- changed ||= rewound !== row;
241
- return rewound;
242
- });
243
- if (!changed) return column;
244
- const result = makeGeoArrowColumnFromGeometryRows(rows, {
245
- dimension: column.dimension,
246
- coordinateLayout: column.coordinateLayout || 'interleaved'
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 copyColumnMetadata(column, result);
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
- function mapGeometryCoordinates(
282
- geometry: GeoArrowGeometryValue,
283
- mapper: (coordinate: readonly number[]) => readonly number[]
284
- ): GeoArrowGeometryValue {
285
- if (geometry.type === 'GeometryCollection') {
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
- ...geometry,
288
- geometries: geometry.geometries.map(child => mapGeometryCoordinates(child, mapper))
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
- ...geometry,
293
- coordinates: mapCoordinateNesting(geometry.coordinates, mapper)
294
- } as GeoArrowGeometryValue;
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 mapCoordinateNesting(
298
- value: readonly unknown[],
299
- mapper: (coordinate: readonly number[]) => readonly number[]
300
- ): unknown {
301
- if (value.length === 0) return [];
302
- if (typeof value[0] === 'number') return [...mapper(value as readonly number[])];
303
- return value.map(child => mapCoordinateNesting(child as readonly unknown[], mapper));
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
- function rewindGeometry(
307
- geometry: GeoArrowGeometryValue,
308
- outerClockwise: boolean
309
- ): GeoArrowGeometryValue {
310
- if (geometry.type === 'GeometryCollection') {
311
- const geometries = geometry.geometries.map(child => rewindGeometry(child, outerClockwise));
312
- return geometries.some((child, index) => child !== geometry.geometries[index])
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
- if (geometry.type === 'Polygon') {
317
- const coordinates = rewindPolygon(geometry.coordinates, outerClockwise);
318
- return coordinates === geometry.coordinates ? geometry : {...geometry, coordinates};
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 (geometry.type === 'MultiPolygon') {
321
- const coordinates = geometry.coordinates.map(polygon => rewindPolygon(polygon, outerClockwise));
322
- return coordinates.every((polygon, index) => polygon === geometry.coordinates[index])
323
- ? geometry
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 rewindPolygon(
330
- rings: readonly (readonly (readonly number[])[])[],
331
- outerClockwise: boolean
332
- ): readonly (readonly (readonly number[])[])[] {
333
- let changed = false;
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
- return changed ? result : rings;
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 getRingSignedArea(ring: readonly (readonly number[])[]): number {
345
- let area = 0;
346
- for (let index = 0; index < ring.length; index++) {
347
- const current = ring[index];
348
- const next = ring[(index + 1) % ring.length];
349
- area += current[0] * next[1] - next[0] * current[1];
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
- return area / 2;
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 convertGeometryFamily(
355
- geometry: GeoArrowGeometryValue,
356
- encoding: GeoArrowEncoding,
357
- sourceDimension: GeoArrowDimension,
358
- targetDimension: GeoArrowDimension
359
- ): GeoArrowGeometryValue {
360
- if (geometry.type === 'GeometryCollection') {
361
- const geometries = geometry.geometries.map(child =>
362
- convertGeometryFamily(child, 'geoarrow.geometry', sourceDimension, targetDimension)
363
- );
364
- if (encoding === 'geoarrow.geometry' || encoding === 'geoarrow.geometrycollection') {
365
- return {...geometry, geometries};
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
- throw new Error(`${geometry.type} cannot be represented as ${encoding}`);
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 coordinates = mapCoordinateNesting(geometry.coordinates, coordinate =>
370
- resizeCoordinate(coordinate, sourceDimension, targetDimension)
371
- );
372
- if (encoding === 'geoarrow.geometry') {
373
- return {...geometry, coordinates} as GeoArrowGeometryValue;
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
- const target = encoding.replace('geoarrow.', '');
376
- if (geometry.type.toLowerCase() !== target) {
377
- throw new Error(`${geometry.type} cannot be represented as ${encoding}`);
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 {...geometry, coordinates} as GeoArrowGeometryValue;
686
+ return {kind: 'struct', length: coordinates.length, children, validity};
380
687
  }
381
688
 
382
- function resizeCoordinate(
383
- coordinate: readonly number[],
384
- sourceDimension: GeoArrowDimension,
385
- targetDimension: GeoArrowDimension
386
- ): number[] {
387
- const sourceNames = getDimensionNames(sourceDimension);
388
- const targetNames = getDimensionNames(targetDimension);
389
- return targetNames.map(name => {
390
- const sourceIndex = sourceNames.indexOf(name);
391
- return sourceIndex >= 0 ? (coordinate[sourceIndex] ?? 0) : 0;
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 copyColumnMetadata(source: GeoArrowColumn, target: GeoArrowColumn): GeoArrowColumn {
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
- ...target,
411
- spatialReference: source.spatialReference,
412
- edges: source.edges,
413
- metadata: source.metadata
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);