@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/dist/kernels.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
  import { getGeoArrowDimensionSize } from "./types.js";
5
5
  import { makeGeoArrowColumnFromGeometryRows } from "./builder.js";
6
- import { getGeoArrowRowCount, getGeoArrowVertexCount, isGeoArrowValueValid, materializeGeoArrowRows, visitGeoArrowCoordinates } from "./layout.js";
6
+ import { getGeoArrowRowCount, getGeoArrowVertexCount, isGeoArrowValueValid, visitGeoArrowCoordinates, getListRange, materializeGeometryRow } from "./layout.js";
7
7
  /** Returns XY bounds, or null when the column contains no finite coordinates. */
8
8
  export function getGeoArrowBounds(column) {
9
9
  if (column.encoding === 'geoarrow.box')
@@ -25,6 +25,49 @@ export function getGeoArrowBounds(column) {
25
25
  });
26
26
  return Number.isFinite(minimumX) ? [minimumX, minimumY, maximumX, maximumY] : null;
27
27
  }
28
+ /** Computes exact XY bounds for each logical row without materializing geometry values. */
29
+ export function getGeoArrowRowBounds(column) {
30
+ const rowCount = getGeoArrowRowCount(column);
31
+ if (column.encoding === 'geoarrow.box') {
32
+ const result = [];
33
+ for (const chunk of column.chunks) {
34
+ for (let rowIndex = 0; rowIndex < chunk.length; rowIndex++) {
35
+ if (chunk.kind !== 'struct' || !isGeoArrowValueValid(chunk.validity, rowIndex)) {
36
+ result.push(null);
37
+ continue;
38
+ }
39
+ const index = (chunk.offset || 0) + rowIndex;
40
+ const xmin = readPrimitiveNumber(chunk.children['xmin'], index);
41
+ const ymin = readPrimitiveNumber(chunk.children['ymin'], index);
42
+ const xmax = readPrimitiveNumber(chunk.children['xmax'], index);
43
+ const ymax = readPrimitiveNumber(chunk.children['ymax'], index);
44
+ result.push([xmin, ymin, xmax, ymax].every(Number.isFinite) ? [xmin, ymin, xmax, ymax] : null);
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ const minimumX = new Float64Array(rowCount).fill(Number.POSITIVE_INFINITY);
50
+ const minimumY = new Float64Array(rowCount).fill(Number.POSITIVE_INFINITY);
51
+ const maximumX = new Float64Array(rowCount).fill(Number.NEGATIVE_INFINITY);
52
+ const maximumY = new Float64Array(rowCount).fill(Number.NEGATIVE_INFINITY);
53
+ visitGeoArrowCoordinates(column, (coordinate, sourceRowIndex) => {
54
+ const [x, y] = coordinate;
55
+ if (Number.isFinite(x) && Number.isFinite(y)) {
56
+ minimumX[sourceRowIndex] = Math.min(minimumX[sourceRowIndex], x);
57
+ minimumY[sourceRowIndex] = Math.min(minimumY[sourceRowIndex], y);
58
+ maximumX[sourceRowIndex] = Math.max(maximumX[sourceRowIndex], x);
59
+ maximumY[sourceRowIndex] = Math.max(maximumY[sourceRowIndex], y);
60
+ }
61
+ return coordinate;
62
+ });
63
+ const bounds = [];
64
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
65
+ bounds.push(Number.isFinite(minimumX[rowIndex])
66
+ ? [minimumX[rowIndex], minimumY[rowIndex], maximumX[rowIndex], maximumY[rowIndex]]
67
+ : null);
68
+ }
69
+ return bounds;
70
+ }
28
71
  function getGeoArrowBoxBounds(column) {
29
72
  let minimumX = Number.POSITIVE_INFINITY;
30
73
  let minimumY = Number.POSITIVE_INFINITY;
@@ -66,22 +109,20 @@ function readPrimitiveNumber(array, index) {
66
109
  return Number(array.values[valueIndex]);
67
110
  }
68
111
  /** Maps every coordinate into newly allocated physical buffers while preserving column semantics. */
69
- export function mapGeoArrowCoordinates(column, mapper, options = {}) {
112
+ export function mapGeoArrowCoordinates(column, mapper, options = {}, sourceDimension) {
70
113
  assertGeoArrowResourceLimits(column, options.limits);
71
- const rows = materializeGeoArrowRows(column);
72
- let rowIndex = 0;
73
- const mappedRows = rows.map(row => {
74
- const mapped = row
75
- ? mapGeometryCoordinates(row, coordinate => mapper(coordinate, rowIndex))
76
- : null;
77
- rowIndex++;
114
+ if (column.encoding === 'geoarrow.wkb' || column.encoding === 'geoarrow.wkt') {
115
+ throw new Error('Coordinate mapping requires native GeoArrow storage');
116
+ }
117
+ const dimension = options.dimension || column.dimension;
118
+ const layout = options.coordinateLayout || column.coordinateLayout || 'interleaved';
119
+ let rowOffset = 0;
120
+ const chunks = column.chunks.map(chunk => {
121
+ const mapped = mapArrayCoordinates(chunk, column.encoding, rowOffset, dimension, layout, mapper, options.coordinateType || 'preserve', options.dimension !== undefined, options.coordinateLayout !== undefined, sourceDimension);
122
+ rowOffset += chunk.length;
78
123
  return mapped;
79
124
  });
80
- const result = makeGeoArrowColumnFromGeometryRows(mappedRows, {
81
- dimension: options.dimension || column.dimension,
82
- coordinateLayout: options.coordinateLayout || column.coordinateLayout || 'interleaved'
83
- });
84
- return copyColumnMetadata(column, result);
125
+ return { ...column, dimension, coordinateLayout: layout, chunks };
85
126
  }
86
127
  /**
87
128
  * Maps coordinates and copies the result into caller-provided destination buffers.
@@ -137,48 +178,65 @@ export function normalizeGeoArrowUnion(column) {
137
178
  }
138
179
  /** Converts between concrete/mixed encodings, dimensions, and coordinate layouts. */
139
180
  export function convertGeoArrowColumn(column, options = {}) {
140
- const encoding = options.encoding || column.encoding;
181
+ const encoding = options.encoding === 'native' || !options.encoding ? column.encoding : options.encoding;
141
182
  const dimension = options.dimension || column.dimension;
142
- const coordinateLayout = options.coordinateLayout || column.coordinateLayout;
183
+ const coordinateLayout = options.coordinateLayout === 'preserve' || !options.coordinateLayout
184
+ ? column.coordinateLayout
185
+ : options.coordinateLayout;
186
+ const requestedOffsetType = options.offsetType || 'preserve';
187
+ const currentOffsetType = getColumnOffsetType(column);
143
188
  if (encoding === column.encoding &&
144
189
  dimension === column.dimension &&
145
- coordinateLayout === column.coordinateLayout) {
190
+ coordinateLayout === column.coordinateLayout &&
191
+ (requestedOffsetType === 'preserve' || requestedOffsetType === currentOffsetType) &&
192
+ (!options.coordinateType || options.coordinateType === 'preserve')) {
146
193
  return column;
147
194
  }
148
195
  if (encoding === 'geoarrow.wkb' || encoding === 'geoarrow.wkt') {
149
196
  throw new Error('Use encodeGeoArrowWKB or encodeGeoArrowWKT for serialized output');
150
197
  }
151
198
  assertGeoArrowResourceLimits(column, options.limits);
152
- const rows = materializeGeoArrowRows(column).map(row => row ? convertGeometryFamily(row, encoding, column.dimension, dimension) : null);
153
- const result = makeGeoArrowColumnFromGeometryRows(rows, {
154
- encoding: encoding === 'geoarrow.geometry' ? encoding : undefined,
199
+ const mapped = mapGeoArrowCoordinates(column, coordinate => coordinate, {
155
200
  dimension,
156
- coordinateLayout: coordinateLayout || 'interleaved'
201
+ coordinateLayout: coordinateLayout || undefined,
202
+ coordinateType: options.coordinateType,
203
+ limits: options.limits
204
+ }, column.dimension);
205
+ const offsetConverted = requestedOffsetType === 'preserve'
206
+ ? mapped
207
+ : {
208
+ ...mapped,
209
+ chunks: mapped.chunks.map(chunk => convertArrayOffsets(chunk, requestedOffsetType))
210
+ };
211
+ if (encoding === column.encoding)
212
+ return offsetConverted;
213
+ if (encoding === 'geoarrow.geometry')
214
+ return promoteToDenseUnion(offsetConverted);
215
+ if (column.encoding === 'geoarrow.geometry')
216
+ return demoteDenseUnion(offsetConverted, encoding);
217
+ throw new Error(`Rows cannot be represented as ${encoding}`);
218
+ }
219
+ function resizeCoordinate(coordinate, sourceDimension, targetDimension) {
220
+ const sourceNames = getDimensionNames(sourceDimension);
221
+ return getDimensionNames(targetDimension).map(name => {
222
+ const sourceIndex = sourceNames.indexOf(name);
223
+ return sourceIndex >= 0 ? (coordinate[sourceIndex] ?? 0) : 0;
157
224
  });
158
- if (result.encoding !== encoding && encoding !== 'geoarrow.geometry') {
159
- throw new Error(`Rows cannot be represented as ${encoding}`);
160
- }
161
- return copyColumnMetadata(column, result);
162
225
  }
163
226
  /** Rewinds polygon rings without changing non-polygon rows. */
164
227
  export function rewindGeoArrow(column, options = {}) {
165
228
  assertGeoArrowResourceLimits(column, options.limits);
229
+ if (column.encoding === 'geoarrow.wkb' || column.encoding === 'geoarrow.wkt') {
230
+ throw new Error('Rewinding requires native GeoArrow storage');
231
+ }
166
232
  const outerClockwise = (options.outer || 'counter-clockwise') === 'clockwise';
167
233
  let changed = false;
168
- const rows = materializeGeoArrowRows(column).map(row => {
169
- if (!row)
170
- return null;
171
- const rewound = rewindGeometry(row, outerClockwise);
172
- changed || (changed = rewound !== row);
173
- return rewound;
174
- });
175
- if (!changed)
176
- return column;
177
- const result = makeGeoArrowColumnFromGeometryRows(rows, {
178
- dimension: column.dimension,
179
- coordinateLayout: column.coordinateLayout || 'interleaved'
234
+ const chunks = column.chunks.map(chunk => {
235
+ const clone = mapArrayCoordinates(chunk, column.encoding, 0, column.dimension, column.coordinateLayout || 'interleaved', coordinate => coordinate, 'preserve');
236
+ changed || (changed = rewindArrayRings(clone, column.encoding, outerClockwise));
237
+ return clone;
180
238
  });
181
- return copyColumnMetadata(column, result);
239
+ return changed ? { ...column, chunks } : column;
182
240
  }
183
241
  /** Throws when a column exceeds configured work or output limits. */
184
242
  export function assertGeoArrowResourceLimits(column, options = {}) {
@@ -206,90 +264,218 @@ export function assertGeoArrowResourceLimits(column, options = {}) {
206
264
  }
207
265
  }
208
266
  }
209
- function mapGeometryCoordinates(geometry, mapper) {
210
- if (geometry.type === 'GeometryCollection') {
267
+ /** Directly clones a physical array while rewriting only coordinate leaves. */
268
+ function mapArrayCoordinates(array, encoding, rowOffset, targetDimension, targetLayout, mapper, coordinateType, forceDimension = false, forceLayout = false, sourceDimension, rowIndicesOverride) {
269
+ if (encoding === 'geoarrow.geometry' && array.kind === 'dense-union') {
270
+ const childRowIndices = collectUnionRowIndices(array, rowOffset);
271
+ return {
272
+ ...array,
273
+ children: array.children.map((child, childIndex) => {
274
+ const childEncoding = child.encoding || getEncodingFromChildName(child.name);
275
+ const childDimension = forceDimension
276
+ ? targetDimension
277
+ : child.dimension || targetDimension;
278
+ const childLayout = forceLayout ? targetLayout : child.coordinateLayout || targetLayout;
279
+ return {
280
+ ...child,
281
+ data: mapArrayCoordinates(child.data, childEncoding, rowOffset, childDimension, childLayout, mapper, coordinateType, forceDimension, forceLayout, childDimension, childRowIndices[childIndex]),
282
+ dimension: childDimension,
283
+ coordinateLayout: childLayout,
284
+ encoding: childEncoding
285
+ };
286
+ })
287
+ };
288
+ }
289
+ if (encoding === 'geoarrow.geometrycollection' && array.kind === 'list') {
211
290
  return {
212
- ...geometry,
213
- geometries: geometry.geometries.map(child => mapGeometryCoordinates(child, mapper))
291
+ ...array,
292
+ child: mapArrayCoordinates(array.child, 'geoarrow.geometry', rowOffset, targetDimension, targetLayout, mapper, coordinateType, forceDimension, forceLayout, sourceDimension, rowIndicesOverride)
214
293
  };
215
294
  }
295
+ const depth = getEncodingDepth(encoding);
296
+ const rowIndices = rowIndicesOverride || collectLeafRowIndices(array, depth, rowOffset);
297
+ return mapNestedArray(array, depth, rowOffset, targetDimension, targetLayout, mapper, coordinateType, rowIndices, sourceDimension);
298
+ }
299
+ function mapNestedArray(array, depth, rowOffset, targetDimension, targetLayout, mapper, coordinateType, rowIndices, sourceDimension) {
300
+ if (depth === 0) {
301
+ return mapCoordinateLeaf(array, rowOffset, targetDimension, targetLayout, mapper, coordinateType, rowIndices, sourceDimension);
302
+ }
303
+ if (array.kind !== 'list')
304
+ return array;
216
305
  return {
217
- ...geometry,
218
- coordinates: mapCoordinateNesting(geometry.coordinates, mapper)
306
+ ...array,
307
+ child: mapNestedArray(array.child, depth - 1, rowOffset, targetDimension, targetLayout, mapper, coordinateType, rowIndices, sourceDimension)
219
308
  };
220
309
  }
221
- function mapCoordinateNesting(value, mapper) {
222
- if (value.length === 0)
223
- return [];
224
- if (typeof value[0] === 'number')
225
- return [...mapper(value)];
226
- return value.map(child => mapCoordinateNesting(child, mapper));
310
+ function mapCoordinateLeaf(array, rowOffset, targetDimension, targetLayout, mapper, coordinateType, rowIndices, sourceDimension) {
311
+ const count = array.length;
312
+ const targetSize = getGeoArrowDimensionSize(targetDimension);
313
+ const sourceValues = new Array(count);
314
+ for (let index = 0; index < count; index++) {
315
+ const coordinate = readCoordinateAt(array, index);
316
+ const mapped = coordinate
317
+ ? mapper(sourceDimension
318
+ ? resizeCoordinate(coordinate, sourceDimension, targetDimension)
319
+ : coordinate, rowIndices?.[index] ?? rowOffset)
320
+ : new Array(targetSize).fill(0);
321
+ if (mapped.length !== targetSize) {
322
+ throw new Error(`Coordinate mapper returned ${mapped.length} values; expected ${targetSize}`);
323
+ }
324
+ sourceValues[index] = [...mapped];
325
+ }
326
+ const FloatArray = resolveCoordinateConstructor(array, coordinateType);
327
+ return makeCoordinateLeaf(sourceValues, targetDimension, targetLayout, FloatArray, array.validity);
227
328
  }
228
- function rewindGeometry(geometry, outerClockwise) {
229
- if (geometry.type === 'GeometryCollection') {
230
- const geometries = geometry.geometries.map(child => rewindGeometry(child, outerClockwise));
231
- return geometries.some((child, index) => child !== geometry.geometries[index])
232
- ? { ...geometry, geometries }
233
- : geometry;
329
+ /** Fills one row-index entry per coordinate in a concrete nested array. */
330
+ function collectLeafRowIndices(array, depth, rowOffset) {
331
+ const output = new Array(getLeafLength(array, depth)).fill(rowOffset);
332
+ for (let rowIndex = 0; rowIndex < array.length; rowIndex++) {
333
+ if (isGeoArrowValueValid(array.validity, rowIndex)) {
334
+ assignNestedRowIndices(array, rowIndex, depth, rowOffset + rowIndex, output);
335
+ }
234
336
  }
235
- if (geometry.type === 'Polygon') {
236
- const coordinates = rewindPolygon(geometry.coordinates, outerClockwise);
237
- return coordinates === geometry.coordinates ? geometry : { ...geometry, coordinates };
337
+ return output;
338
+ }
339
+ function assignNestedRowIndices(array, index, depth, rowIndex, output) {
340
+ if (depth === 0) {
341
+ if (index >= 0 && index < output.length)
342
+ output[index] = rowIndex;
343
+ return;
238
344
  }
239
- if (geometry.type === 'MultiPolygon') {
240
- const coordinates = geometry.coordinates.map(polygon => rewindPolygon(polygon, outerClockwise));
241
- return coordinates.every((polygon, index) => polygon === geometry.coordinates[index])
242
- ? geometry
243
- : { ...geometry, coordinates };
345
+ if (array.kind !== 'list')
346
+ return;
347
+ const [first, last] = getListRange(array, index);
348
+ for (let childIndex = first; childIndex < last; childIndex++) {
349
+ assignNestedRowIndices(array.child, childIndex, depth - 1, rowIndex, output);
244
350
  }
245
- return geometry;
246
351
  }
247
- function rewindPolygon(rings, outerClockwise) {
248
- let changed = false;
249
- const result = rings.map((ring, ringIndex) => {
250
- const shouldBeClockwise = ringIndex === 0 ? outerClockwise : !outerClockwise;
251
- const isClockwise = getRingSignedArea(ring) < 0;
252
- if (isClockwise === shouldBeClockwise)
253
- return ring;
254
- changed = true;
255
- return [...ring].reverse();
352
+ function collectUnionRowIndices(union, rowOffset) {
353
+ if (union.kind !== 'dense-union')
354
+ return [];
355
+ const output = union.children.map(child => {
356
+ const encoding = child.encoding || getEncodingFromChildName(child.name);
357
+ return new Array(getLeafLength(child.data, getEncodingDepth(encoding))).fill(rowOffset);
256
358
  });
257
- return changed ? result : rings;
359
+ for (let rowIndex = 0; rowIndex < union.length; rowIndex++) {
360
+ if (!isGeoArrowValueValid(union.validity, rowIndex))
361
+ continue;
362
+ const physical = (union.offset || 0) + rowIndex;
363
+ const childIndex = union.children.findIndex(child => child.typeId === union.typeIds[physical]);
364
+ if (childIndex < 0)
365
+ continue;
366
+ const child = union.children[childIndex];
367
+ assignUnionRowIndices(child.data, child.encoding || getEncodingFromChildName(child.name), union.valueOffsets[physical], rowOffset + rowIndex, output[childIndex]);
368
+ }
369
+ return output;
258
370
  }
259
- function getRingSignedArea(ring) {
260
- let area = 0;
261
- for (let index = 0; index < ring.length; index++) {
262
- const current = ring[index];
263
- const next = ring[(index + 1) % ring.length];
264
- area += current[0] * next[1] - next[0] * current[1];
371
+ function assignUnionRowIndices(array, encoding, index, rowIndex, output) {
372
+ if (encoding === 'geoarrow.geometry' && array.kind === 'dense-union') {
373
+ const physical = (array.offset || 0) + index;
374
+ const childIndex = array.children.findIndex(child => child.typeId === array.typeIds[physical]);
375
+ if (childIndex >= 0) {
376
+ const child = array.children[childIndex];
377
+ assignUnionRowIndices(child.data, child.encoding || getEncodingFromChildName(child.name), array.valueOffsets[physical], rowIndex, output);
378
+ }
379
+ return;
265
380
  }
266
- return area / 2;
381
+ if (encoding === 'geoarrow.geometrycollection' && array.kind === 'list') {
382
+ const [first, last] = getListRange(array, index);
383
+ if (array.child.kind === 'dense-union') {
384
+ for (let childIndex = first; childIndex < last; childIndex++) {
385
+ assignUnionRowIndices(array.child, 'geoarrow.geometry', childIndex, rowIndex, output);
386
+ }
387
+ }
388
+ return;
389
+ }
390
+ assignNestedRowIndices(array, index, getEncodingDepth(encoding), rowIndex, output);
267
391
  }
268
- function convertGeometryFamily(geometry, encoding, sourceDimension, targetDimension) {
269
- if (geometry.type === 'GeometryCollection') {
270
- const geometries = geometry.geometries.map(child => convertGeometryFamily(child, 'geoarrow.geometry', sourceDimension, targetDimension));
271
- if (encoding === 'geoarrow.geometry' || encoding === 'geoarrow.geometrycollection') {
272
- return { ...geometry, geometries };
392
+ function getLeafLength(array, depth) {
393
+ let current = array;
394
+ for (let level = 0; level < depth; level++) {
395
+ if (current.kind !== 'list')
396
+ return 0;
397
+ current = current.child;
398
+ }
399
+ return current.length;
400
+ }
401
+ function resolveCoordinateConstructor(array, coordinateType) {
402
+ if (coordinateType === 'float32')
403
+ return Float32Array;
404
+ if (coordinateType === 'float64')
405
+ return Float64Array;
406
+ if (array.kind === 'fixed-size-list' && array.child.kind === 'primitive') {
407
+ return array.child.values instanceof Float32Array ? Float32Array : Float64Array;
408
+ }
409
+ if (array.kind === 'struct') {
410
+ const child = array.children['x'];
411
+ if (child?.kind === 'primitive' && child.values instanceof Float32Array)
412
+ return Float32Array;
413
+ }
414
+ return Float64Array;
415
+ }
416
+ function makeCoordinateLeaf(coordinates, dimension, layout, FloatArray, validity) {
417
+ const size = getGeoArrowDimensionSize(dimension);
418
+ if (layout === 'interleaved') {
419
+ const values = new FloatArray(coordinates.length * size);
420
+ for (let index = 0; index < coordinates.length; index++) {
421
+ values.set(coordinates[index], index * size);
273
422
  }
274
- throw new Error(`${geometry.type} cannot be represented as ${encoding}`);
423
+ return {
424
+ kind: 'fixed-size-list',
425
+ length: coordinates.length,
426
+ size,
427
+ child: { kind: 'primitive', length: values.length, values },
428
+ validity
429
+ };
275
430
  }
276
- const coordinates = mapCoordinateNesting(geometry.coordinates, coordinate => resizeCoordinate(coordinate, sourceDimension, targetDimension));
277
- if (encoding === 'geoarrow.geometry') {
278
- return { ...geometry, coordinates };
431
+ const children = {
432
+ x: { kind: 'primitive', length: coordinates.length, values: new FloatArray(coordinates.length) },
433
+ y: { kind: 'primitive', length: coordinates.length, values: new FloatArray(coordinates.length) }
434
+ };
435
+ const names = getDimensionNames(dimension);
436
+ for (let component = 2; component < names.length; component++) {
437
+ children[names[component]] = {
438
+ kind: 'primitive',
439
+ length: coordinates.length,
440
+ values: new FloatArray(coordinates.length)
441
+ };
279
442
  }
280
- const target = encoding.replace('geoarrow.', '');
281
- if (geometry.type.toLowerCase() !== target) {
282
- throw new Error(`${geometry.type} cannot be represented as ${encoding}`);
443
+ for (let index = 0; index < coordinates.length; index++) {
444
+ const coordinate = coordinates[index];
445
+ for (let component = 0; component < names.length; component++) {
446
+ const child = children[names[component]];
447
+ if (child.kind === 'primitive')
448
+ child.values[index] = coordinate[component];
449
+ }
283
450
  }
284
- return { ...geometry, coordinates };
451
+ return { kind: 'struct', length: coordinates.length, children, validity };
285
452
  }
286
- function resizeCoordinate(coordinate, sourceDimension, targetDimension) {
287
- const sourceNames = getDimensionNames(sourceDimension);
288
- const targetNames = getDimensionNames(targetDimension);
289
- return targetNames.map(name => {
290
- const sourceIndex = sourceNames.indexOf(name);
291
- return sourceIndex >= 0 ? (coordinate[sourceIndex] ?? 0) : 0;
292
- });
453
+ function readCoordinateAt(array, index) {
454
+ if (!isGeoArrowValueValid(array.validity, index))
455
+ return null;
456
+ if (array.kind === 'fixed-size-list') {
457
+ const logicalIndex = (array.offset || 0) + index;
458
+ if (array.child.kind !== 'primitive')
459
+ return null;
460
+ const values = [];
461
+ for (let component = 0; component < array.size; component++) {
462
+ const scalarIndex = logicalIndex * array.size + component;
463
+ values.push(Number(array.child.values[(array.child.offset || 0) + scalarIndex * (array.child.stride || 1)]));
464
+ }
465
+ return values;
466
+ }
467
+ if (array.kind === 'struct') {
468
+ const logicalIndex = (array.offset || 0) + index;
469
+ const values = [];
470
+ for (const name of ['x', 'y', 'z', 'm']) {
471
+ const child = array.children[name];
472
+ if (child?.kind === 'primitive') {
473
+ values.push(Number(child.values[(child.offset || 0) + logicalIndex * (child.stride || 1)]));
474
+ }
475
+ }
476
+ return values.length >= 2 ? values : null;
477
+ }
478
+ return null;
293
479
  }
294
480
  function getDimensionNames(dimension) {
295
481
  switch (dimension) {
@@ -303,13 +489,330 @@ function getDimensionNames(dimension) {
303
489
  return ['x', 'y', 'z', 'm'];
304
490
  }
305
491
  }
306
- function copyColumnMetadata(source, target) {
492
+ function getEncodingFromChildName(name) {
493
+ const normalized = name.replace(/[^a-z]/gi, '').toLowerCase();
494
+ const encoding = `geoarrow.${normalized}`;
495
+ if (encoding === 'geoarrow.point' ||
496
+ encoding === 'geoarrow.linestring' ||
497
+ encoding === 'geoarrow.polygon' ||
498
+ encoding === 'geoarrow.multipoint' ||
499
+ encoding === 'geoarrow.multilinestring' ||
500
+ encoding === 'geoarrow.multipolygon' ||
501
+ encoding === 'geoarrow.geometrycollection') {
502
+ return encoding;
503
+ }
504
+ throw new Error(`Unknown GeoArrow dense-union child ${name}`);
505
+ }
506
+ function getEncodingDepth(encoding) {
507
+ switch (encoding) {
508
+ case 'geoarrow.point':
509
+ return 0;
510
+ case 'geoarrow.linestring':
511
+ case 'geoarrow.multipoint':
512
+ return 1;
513
+ case 'geoarrow.polygon':
514
+ case 'geoarrow.multilinestring':
515
+ return 2;
516
+ case 'geoarrow.multipolygon':
517
+ return 3;
518
+ default:
519
+ return 0;
520
+ }
521
+ }
522
+ function promoteToDenseUnion(column) {
523
+ if (column.encoding === 'geoarrow.geometry')
524
+ return column;
525
+ const chunks = column.chunks.map(chunk => {
526
+ const typeId = getCanonicalTypeId(column.encoding, column.dimension);
527
+ const typeIds = new Int8Array(chunk.length);
528
+ typeIds.fill(typeId);
529
+ const valueOffsets = new Int32Array(chunk.length);
530
+ for (let index = 0; index < chunk.length; index++)
531
+ valueOffsets[index] = index;
532
+ return {
533
+ kind: 'dense-union',
534
+ length: chunk.length,
535
+ typeIds,
536
+ valueOffsets,
537
+ validity: chunk.validity,
538
+ children: [
539
+ {
540
+ name: getGeoArrowGeometryTypeName(column.encoding),
541
+ typeId,
542
+ encoding: column.encoding,
543
+ dimension: column.dimension,
544
+ coordinateLayout: column.coordinateLayout,
545
+ data: chunk
546
+ }
547
+ ]
548
+ };
549
+ });
550
+ return { ...column, encoding: 'geoarrow.geometry', chunks };
551
+ }
552
+ function demoteDenseUnion(column, encoding) {
553
+ if (encoding === 'geoarrow.geometry' || encoding === 'geoarrow.geometrycollection')
554
+ return column;
555
+ const rows = [];
556
+ for (const chunk of column.chunks) {
557
+ if (chunk.kind !== 'dense-union')
558
+ throw new Error('Expected dense-union storage');
559
+ for (let index = 0; index < chunk.length; index++) {
560
+ if (!isGeoArrowValueValid(chunk.validity, index)) {
561
+ rows.push(null);
562
+ continue;
563
+ }
564
+ const physical = (chunk.offset || 0) + index;
565
+ const child = chunk.children.find(candidate => candidate.typeId === chunk.typeIds[physical]);
566
+ const childEncoding = child && (child.encoding || getEncodingFromChildName(child.name));
567
+ if (!child || childEncoding !== encoding) {
568
+ throw new Error(`Rows cannot be represented as ${encoding}`);
569
+ }
570
+ const geometry = materializeGeometryRow(child.data, chunk.valueOffsets[physical], childEncoding);
571
+ rows.push(geometry
572
+ ? resizeGeometryValue(geometry, child.dimension || column.dimension, column.dimension)
573
+ : null);
574
+ }
575
+ }
576
+ const built = makeGeoArrowColumnFromGeometryRows(rows, {
577
+ dimension: column.dimension,
578
+ coordinateLayout: column.coordinateLayout || 'interleaved',
579
+ offsetType: getColumnOffsetType(column)
580
+ });
581
+ return {
582
+ ...built,
583
+ encoding: encoding,
584
+ spatialReference: column.spatialReference,
585
+ edges: column.edges,
586
+ metadata: column.metadata
587
+ };
588
+ }
589
+ function resizeGeometryValue(geometry, sourceDimension, targetDimension) {
590
+ const map = (value) => {
591
+ if (Array.isArray(value) && (value.length === 0 || typeof value[0] === 'number')) {
592
+ return resizeCoordinate(value, sourceDimension, targetDimension);
593
+ }
594
+ return Array.isArray(value) ? value.map(map) : value;
595
+ };
596
+ if (geometry.type === 'GeometryCollection') {
597
+ return {
598
+ type: geometry.type,
599
+ geometries: geometry.geometries.map(child => resizeGeometryValue(child, sourceDimension, targetDimension))
600
+ };
601
+ }
307
602
  return {
308
- ...target,
309
- spatialReference: source.spatialReference,
310
- edges: source.edges,
311
- metadata: source.metadata
603
+ ...geometry,
604
+ coordinates: map(geometry.coordinates)
605
+ };
606
+ }
607
+ function getCanonicalTypeId(encoding, dimension) {
608
+ const families = [
609
+ 'point',
610
+ 'linestring',
611
+ 'polygon',
612
+ 'multipoint',
613
+ 'multilinestring',
614
+ 'multipolygon',
615
+ 'geometrycollection'
616
+ ];
617
+ const dimensions = ['xy', 'xyz', 'xym', 'xyzm'];
618
+ const family = encoding.replace('geoarrow.', '');
619
+ const familyIndex = families.indexOf(family);
620
+ const dimensionIndex = dimensions.indexOf(dimension);
621
+ return familyIndex < 0 || dimensionIndex < 0 ? 1 : familyIndex * 4 + dimensionIndex + 1;
622
+ }
623
+ function getColumnOffsetType(column) {
624
+ for (const chunk of column.chunks) {
625
+ const found = findOffsetType(chunk);
626
+ if (found)
627
+ return found;
628
+ }
629
+ return 'int32';
630
+ }
631
+ function findOffsetType(array) {
632
+ switch (array.kind) {
633
+ case 'list':
634
+ case 'serialized':
635
+ return array.offsets instanceof BigInt64Array ? 'int64' : 'int32';
636
+ case 'fixed-size-list':
637
+ return findOffsetType(array.child);
638
+ case 'struct': {
639
+ for (const child of Object.values(array.children)) {
640
+ const found = findOffsetType(child);
641
+ if (found)
642
+ return found;
643
+ }
644
+ return null;
645
+ }
646
+ case 'dense-union':
647
+ for (const child of array.children) {
648
+ const found = findOffsetType(child.data);
649
+ if (found)
650
+ return found;
651
+ }
652
+ return null;
653
+ default:
654
+ return null;
655
+ }
656
+ }
657
+ function convertArrayOffsets(array, offsetType) {
658
+ const toOffsets = (offsets, offsetBase) => {
659
+ if (offsetType === 'int64') {
660
+ const result = new BigInt64Array(offsets.length);
661
+ for (let index = 0; index < offsets.length; index++)
662
+ result[index] = BigInt(offsets[index]);
663
+ return { offsets: result, ...(offsetBase === undefined ? {} : { offsetBase }) };
664
+ }
665
+ const result = new Int32Array(offsets.length);
666
+ const base = BigInt(offsetBase ?? 0);
667
+ for (let index = 0; index < offsets.length; index++) {
668
+ const value = BigInt(offsets[index]) - base;
669
+ if (value < -2147483648n || value > 2147483647n) {
670
+ throw new Error('GeoArrow offset cannot be represented as Int32');
671
+ }
672
+ result[index] = Number(value);
673
+ }
674
+ return { offsets: result, offsetBase: 0 };
312
675
  };
676
+ switch (array.kind) {
677
+ case 'list':
678
+ if (array.offsets instanceof (offsetType === 'int64' ? BigInt64Array : Int32Array)) {
679
+ return { ...array, child: convertArrayOffsets(array.child, offsetType) };
680
+ }
681
+ return {
682
+ ...array,
683
+ ...toOffsets(array.offsets, array.offsetBase),
684
+ child: convertArrayOffsets(array.child, offsetType)
685
+ };
686
+ case 'serialized':
687
+ if (array.offsets instanceof (offsetType === 'int64' ? BigInt64Array : Int32Array))
688
+ return array;
689
+ return { ...array, ...toOffsets(array.offsets, array.offsetBase) };
690
+ case 'fixed-size-list':
691
+ return { ...array, child: convertArrayOffsets(array.child, offsetType) };
692
+ case 'struct':
693
+ return {
694
+ ...array,
695
+ children: Object.fromEntries(Object.entries(array.children).map(([name, child]) => [
696
+ name,
697
+ convertArrayOffsets(child, offsetType)
698
+ ]))
699
+ };
700
+ case 'dense-union':
701
+ return {
702
+ ...array,
703
+ children: array.children.map(child => ({
704
+ ...child,
705
+ data: convertArrayOffsets(child.data, offsetType)
706
+ }))
707
+ };
708
+ default:
709
+ return array;
710
+ }
711
+ }
712
+ function getGeoArrowGeometryTypeName(encoding) {
713
+ return encoding.replace('geoarrow.', '').replace(/^./, character => character.toUpperCase());
714
+ }
715
+ function rewindArrayRings(array, encoding, outerClockwise) {
716
+ if (encoding === 'geoarrow.geometry' && array.kind === 'dense-union') {
717
+ let changed = false;
718
+ for (const child of array.children) {
719
+ if (rewindArrayRings(child.data, child.encoding || getEncodingFromChildName(child.name), outerClockwise))
720
+ changed = true;
721
+ }
722
+ return changed;
723
+ }
724
+ if (encoding === 'geoarrow.geometrycollection' && array.kind === 'list') {
725
+ if (array.child.kind !== 'dense-union')
726
+ return false;
727
+ let changed = false;
728
+ for (const child of array.child.children) {
729
+ if (rewindArrayRings(child.data, child.encoding || getEncodingFromChildName(child.name), outerClockwise))
730
+ changed = true;
731
+ }
732
+ return changed;
733
+ }
734
+ if (encoding !== 'geoarrow.polygon' && encoding !== 'geoarrow.multipolygon')
735
+ return false;
736
+ if (array.kind !== 'list' || array.child.kind !== 'list')
737
+ return false;
738
+ let changed = false;
739
+ for (let rowIndex = 0; rowIndex < array.length; rowIndex++) {
740
+ if (!isGeoArrowValueValid(array.validity, rowIndex))
741
+ continue;
742
+ const [, rowEnd] = getListRange(array, rowIndex);
743
+ const rowStart = getListRange(array, rowIndex)[0];
744
+ if (encoding === 'geoarrow.polygon') {
745
+ const rings = array.child;
746
+ if (rewindRingGroup(rings, rowStart, rowEnd, outerClockwise))
747
+ changed = true;
748
+ }
749
+ else {
750
+ const polygons = array.child;
751
+ for (let polygonIndex = rowStart; polygonIndex < rowEnd; polygonIndex++) {
752
+ const [ringStart, ringEnd] = getListRange(polygons, polygonIndex);
753
+ if (rewindRingGroup(polygons.child, ringStart, ringEnd, outerClockwise))
754
+ changed = true;
755
+ }
756
+ }
757
+ }
758
+ return changed;
759
+ }
760
+ function rewindRingGroup(rings, firstRing, lastRing, outerClockwise) {
761
+ if (rings.kind !== 'list')
762
+ return false;
763
+ const leaf = rings.child;
764
+ if (leaf.kind !== 'fixed-size-list' && leaf.kind !== 'struct')
765
+ return false;
766
+ let changed = false;
767
+ for (let ringIndex = firstRing; ringIndex < lastRing; ringIndex++) {
768
+ const [first, last] = getListRange(rings, ringIndex);
769
+ const area = getRingArea(leaf, first, last);
770
+ if (!Number.isFinite(area) || area === 0)
771
+ continue;
772
+ const shouldClockwise = ringIndex === firstRing ? outerClockwise : !outerClockwise;
773
+ if (area < 0 !== shouldClockwise) {
774
+ reverseCoordinateRange(leaf, first, last);
775
+ changed = true;
776
+ }
777
+ }
778
+ return changed;
779
+ }
780
+ function getRingArea(leaf, first, last) {
781
+ let area = 0;
782
+ for (let index = first; index < last; index++) {
783
+ const current = readCoordinateAt(leaf, index);
784
+ const next = readCoordinateAt(leaf, index + 1 < last ? index + 1 : first);
785
+ if (!current || !next)
786
+ return Number.NaN;
787
+ area += current[0] * next[1] - next[0] * current[1];
788
+ }
789
+ return area / 2;
790
+ }
791
+ function reverseCoordinateRange(leaf, first, last) {
792
+ const values = [];
793
+ for (let index = first; index < last; index++)
794
+ values.push(readCoordinateAt(leaf, index));
795
+ values.reverse();
796
+ for (let index = first; index < last; index++)
797
+ writeCoordinateAt(leaf, index, values[index - first]);
798
+ }
799
+ function writeCoordinateAt(array, index, coordinate) {
800
+ if (array.kind === 'fixed-size-list' && array.child.kind === 'primitive') {
801
+ const logical = (array.offset || 0) + index;
802
+ for (let component = 0; component < array.size; component++) {
803
+ const scalar = (array.child.offset || 0) + logical * array.size + component;
804
+ array.child.values[scalar] = coordinate[component];
805
+ }
806
+ }
807
+ else if (array.kind === 'struct') {
808
+ const logical = (array.offset || 0) + index;
809
+ const names = ['x', 'y', 'z', 'm'];
810
+ for (let component = 0; component < names.length; component++) {
811
+ const child = array.children[names[component]];
812
+ if (child?.kind === 'primitive')
813
+ child.values[(child.offset || 0) + logical * (child.stride || 1)] = coordinate[component];
814
+ }
815
+ }
313
816
  }
314
817
  function collectCoordinateLeaves(column) {
315
818
  const leaves = [];