From 5e0188c2e047954352ab73b5b4a745e53b3d3689 Mon Sep 17 00:00:00 2001 From: Ib Green Date: Tue, 15 Oct 2024 22:00:09 -0400 Subject: [PATCH] feat(arrow): Add Polygon column support (GeoArrow compatible) (#2280) --- modules/arrow/package.json | 1 + modules/arrow/src/arrow/arrow-utils.ts | 15 + .../src/attribute-utils/attribute-utils.ts | 63 ++++ modules/arrow/src/geoarrow/earcut.ts | 60 +++ .../arrow/src/geoarrow/geoarrow-transform.ts | 55 +++ modules/arrow/src/geoarrow/geoarrow-types.ts | 49 +++ modules/arrow/src/geoarrow/geoarrow.ts | 345 ++++++++++++++++++ modules/arrow/src/index.ts | 40 ++ .../test/arrow/arrow-column-info.spec.ts | 29 ++ modules/arrow/test/geoarrow/geoarrow.spec.ts | 31 ++ modules/arrow/test/index.ts | 4 +- modules/arrow/test/test-utils.ts | 17 + yarn.lock | 5 +- 13 files changed, 711 insertions(+), 3 deletions(-) create mode 100644 modules/arrow/src/arrow/arrow-utils.ts create mode 100644 modules/arrow/src/attribute-utils/attribute-utils.ts create mode 100644 modules/arrow/src/geoarrow/earcut.ts create mode 100644 modules/arrow/src/geoarrow/geoarrow-transform.ts create mode 100644 modules/arrow/src/geoarrow/geoarrow-types.ts create mode 100644 modules/arrow/src/geoarrow/geoarrow.ts create mode 100644 modules/arrow/test/arrow/arrow-column-info.spec.ts create mode 100644 modules/arrow/test/geoarrow/geoarrow.spec.ts create mode 100644 modules/arrow/test/test-utils.ts diff --git a/modules/arrow/package.json b/modules/arrow/package.json index b4666d10f5..a6c6c35301 100644 --- a/modules/arrow/package.json +++ b/modules/arrow/package.json @@ -43,6 +43,7 @@ "dependencies": { "@luma.gl/core": "9.2.0-alpha.0", "@math.gl/polygon": "^4.1.0", + "@math.gl/types": "^4.1.0", "apache-arrow": "^17.0.0" }, "gitHead": "c636c34b8f1581eed163e94543a8eb1f4382ba8e" diff --git a/modules/arrow/src/arrow/arrow-utils.ts b/modules/arrow/src/arrow/arrow-utils.ts new file mode 100644 index 0000000000..6ec3fb939f --- /dev/null +++ b/modules/arrow/src/arrow/arrow-utils.ts @@ -0,0 +1,15 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import * as arrow from 'apache-arrow'; + +/** Count number of nested top level Arrow Lists */ +export function getArrowListNestingLevel(data: arrow.Data): number { + let nestingLevel = 0; + if (arrow.DataType.isList(data.type)) { + nestingLevel += 1; + data = data.children[0]; + } + return nestingLevel; +} diff --git a/modules/arrow/src/attribute-utils/attribute-utils.ts b/modules/arrow/src/attribute-utils/attribute-utils.ts new file mode 100644 index 0000000000..fcd7fb3801 --- /dev/null +++ b/modules/arrow/src/attribute-utils/attribute-utils.ts @@ -0,0 +1,63 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {TypedArray} from '@math.gl/types'; + +/** + * Expand an array from "one element per geometry" to "one element per coordinate" + * + * @param input: the input array to expand + * @param size : the number of nested elements in the input array per geometry. So for example, for RGB data this would be 3, for RGBA this would be 4. For radius, this would be 1. + * @param geomOffsets : an offsets array mapping from the geometry to the coordinate indexes. So in the case of a LineStringArray, this is retrieved directly from the GeoArrow storage. In the case of a PolygonArray, this comes from the resolved indexes that need to be given to the SolidPolygonLayer anyways. + * + * @return values expanded to be per-coordinate + */ +export function expandArrayToCoords( + input: T, + size: number, + geomOffsets: Int32Array +): T { + const numCoords = geomOffsets[geomOffsets.length - 1]; + // @ts-expect-error + const outputArray: T = new input.constructor(numCoords * size); + + // geomIdx is an index into the geomOffsets array + // geomIdx is also the geometry/table index + for (let geomIdx = 0; geomIdx < geomOffsets.length - 1; geomIdx++) { + // geomOffsets maps from the geometry index to the coord index + // So here we get the range of coords that this geometry covers + const lastCoordIdx = geomOffsets[geomIdx]; + const nextCoordIdx = geomOffsets[geomIdx + 1]; + + // Iterate over this range of coord indices + for (let coordIdx = lastCoordIdx; coordIdx < nextCoordIdx; coordIdx++) { + // Iterate over size + for (let i = 0; i < size; i++) { + // Copy from the geometry index in `input` to the coord index in + // `output` + outputArray[coordIdx * size + i] = input[geomIdx * size + i]; + } + } + } + + return outputArray; +} + +/** + * Invert offsets so that lookup can go in the opposite direction + */ +export function invertOffsets(offsets: Int32Array): Uint32Array { + const largestOffset = offsets[offsets.length - 1]; + + const invertedOffsets = new Uint32Array(largestOffset); + for (let arrayIdx = 0; arrayIdx < offsets.length - 1; arrayIdx++) { + const thisOffset = offsets[arrayIdx]; + const nextOffset = offsets[arrayIdx + 1]; + for (let offset = thisOffset; offset < nextOffset; offset++) { + invertedOffsets[offset] = arrayIdx; + } + } + + return invertedOffsets; +} diff --git a/modules/arrow/src/geoarrow/earcut.ts b/modules/arrow/src/geoarrow/earcut.ts new file mode 100644 index 0000000000..cc581a48e9 --- /dev/null +++ b/modules/arrow/src/geoarrow/earcut.ts @@ -0,0 +1,60 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {earcut} from '@math.gl/polygon'; +import * as arrow from 'apache-arrow'; +import {ArrowPolygon} from './geoarrow-types'; +import {getLineStringChild, getPointChild, getPolygonChild} from './geoarrow'; + +export function earcutPolygonArray(data: arrow.Data): Uint32Array { + const trianglesResults: number[][] = []; + let outputSize = 0; + for (let geometryIndex = 0; geometryIndex < data.length; geometryIndex++) { + const triangles = earcutSinglePolygon(data, geometryIndex); + trianglesResults.push(triangles); + outputSize += triangles.length; + } + + const outputArray = new Uint32Array(outputSize); + let idx = 0; + for (const triangles of trianglesResults) { + for (const value of triangles) { + outputArray[idx] = value; + idx += 1; + } + } + + return outputArray; +} + +function earcutSinglePolygon(data: arrow.Data, geometryIndex: number): number[] { + const geometryOffsets = data.valueOffsets; + const rings = getPolygonChild(data); + const ringOffsets = rings.valueOffsets; + + const coords = getLineStringChild(rings); + const dim = coords.type.listSize; + const flatCoords = getPointChild(coords); + + const ringBegin = geometryOffsets[geometryIndex]; + const ringEnd = geometryOffsets[geometryIndex + 1]; + + const coordsBegin = ringOffsets[ringBegin]; + const coordsEnd = ringOffsets[ringEnd]; + + const slicedFlatCoords = flatCoords.values.subarray(coordsBegin * dim, coordsEnd * dim); + + const initialCoordIndex = ringOffsets[ringBegin]; + const holeIndices = []; + for (let holeRingIdx = ringBegin + 1; holeRingIdx < ringEnd; holeRingIdx++) { + holeIndices.push(ringOffsets[holeRingIdx] - initialCoordIndex); + } + const triangles = earcut(slicedFlatCoords, holeIndices, dim); + + for (let i = 0; i < triangles.length; i++) { + triangles[i] += initialCoordIndex; + } + + return triangles; +} diff --git a/modules/arrow/src/geoarrow/geoarrow-transform.ts b/modules/arrow/src/geoarrow/geoarrow-transform.ts new file mode 100644 index 0000000000..8d473e0bc4 --- /dev/null +++ b/modules/arrow/src/geoarrow/geoarrow-transform.ts @@ -0,0 +1,55 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import * as arrow from 'apache-arrow'; +import {ArrowMultiLineString, ArrowPolygon, ArrowMultiPolygon} from './geoarrow-types'; +import {getMultiLineStringChild, getPolygonChild, getMultiPolygonChild} from './geoarrow'; + +export function getMultiLineStringResolvedOffsets( + data: arrow.Data +): Int32Array { + const geomOffsets = data.valueOffsets; + const lineStringData = getMultiLineStringChild(data); + const ringOffsets = lineStringData.valueOffsets; + + const resolvedRingOffsets = new Int32Array(geomOffsets.length); + for (let i = 0; i < resolvedRingOffsets.length; ++i) { + // Perform the lookup into the ringIndices array using the geomOffsets + // array + resolvedRingOffsets[i] = ringOffsets[geomOffsets[i]]; + } + + return resolvedRingOffsets; +} + +export function getPolygonResolvedOffsets(data: arrow.Data): Int32Array { + const geomOffsets = data.valueOffsets; + const ringData = getPolygonChild(data); + const ringOffsets = ringData.valueOffsets; + + const resolvedRingOffsets = new Int32Array(geomOffsets.length); + for (let i = 0; i < resolvedRingOffsets.length; ++i) { + // Perform the lookup into the ringIndices array using the geomOffsets + // array + resolvedRingOffsets[i] = ringOffsets[geomOffsets[i]]; + } + + return resolvedRingOffsets; +} + +export function getMultiPolygonResolvedOffsets(data: arrow.Data): Int32Array { + const polygonData = getMultiPolygonChild(data); + const ringData = getPolygonChild(polygonData); + + const geomOffsets = data.valueOffsets; + const polygonOffsets = polygonData.valueOffsets; + const ringOffsets = ringData.valueOffsets; + + const resolvedRingOffsets = new Int32Array(geomOffsets.length); + for (let i = 0; i < resolvedRingOffsets.length; ++i) { + resolvedRingOffsets[i] = ringOffsets[polygonOffsets[geomOffsets[i]]]; + } + + return resolvedRingOffsets; +} diff --git a/modules/arrow/src/geoarrow/geoarrow-types.ts b/modules/arrow/src/geoarrow/geoarrow-types.ts new file mode 100644 index 0000000000..ba92d2d9a0 --- /dev/null +++ b/modules/arrow/src/geoarrow/geoarrow-types.ts @@ -0,0 +1,49 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import * as arrow from 'apache-arrow'; + +/** + * Enum holding GeoArrow extension type names + */ +export enum EXTENSION_NAME { + POINT = 'geoarrow.point', + LINESTRING = 'geoarrow.linestring', + POLYGON = 'geoarrow.polygon', + MULTIPOINT = 'geoarrow.multipoint', + MULTILINESTRING = 'geoarrow.multilinestring', + MULTIPOLYGON = 'geoarrow.multipolygon' +} + +export type ArrowInterleavedCoord = arrow.FixedSizeList; +export type ArrowSeparatedCoord = arrow.Struct<{ + x: arrow.Float64; + y: arrow.Float64; +}>; +// TODO: support separated coords +export type ArrowCoord = ArrowInterleavedCoord; // | SeparatedCoord; +export type ArrowPoint = ArrowCoord; +export type ArrowLineString = arrow.List; +export type ArrowPolygon = arrow.List>; +export type ArrowMultiPoint = arrow.List; +export type ArrowMultiLineString = arrow.List>; +export type ArrowMultiPolygon = arrow.List>>; + +// export type PointVector = arrow.Vector; +// export type LineStringVector = arrow.Vector; +// export type PolygonVector = arrow.Vector; +// export type MultiPointVector = arrow.Vector; +// export type MultiLineStringVector = arrow.Vector; +// export type MultiPolygonVector = arrow.Vector; + +// export type PointData = arrow.Data; +// export type LineStringData = arrow.Data; +// export type PolygonData = arrow.Data; +// export type MultiPointData = arrow.Data; +// export type MultiLineStringData = arrow.Data; +// export type MultiPolygonData = arrow.Data; + +export type GeoArrowPickingInfo = { + object: arrow.StructRowProxy; +}; diff --git a/modules/arrow/src/geoarrow/geoarrow.ts b/modules/arrow/src/geoarrow/geoarrow.ts new file mode 100644 index 0000000000..e39c303cdb --- /dev/null +++ b/modules/arrow/src/geoarrow/geoarrow.ts @@ -0,0 +1,345 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import * as arrow from 'apache-arrow'; +import { + ArrowPoint, + ArrowMultiPoint, + ArrowLineString, + ArrowMultiLineString, + ArrowPolygon, + ArrowMultiPolygon +} from './geoarrow-types'; + +function assert(condition: boolean, message?: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +export function findGeometryColumnIndex( + schema: arrow.Schema, + extensionName: string, + geometryColumnName?: string | null +): number | null { + const index = schema.fields.findIndex( + field => + field.name === geometryColumnName || + field.metadata.get('ARROW:extension:name') === extensionName + ); + return index !== -1 ? index : null; +} + +/** + * Returns `true` if the input is a reference to a column in the table + */ +export function isColumnReference(input: any): input is string { + return typeof input === 'string'; +} + +function isDataInterleavedCoords( + data: arrow.Data +): data is arrow.Data> { + // TODO: also check 2 or 3d? Float64? + return data.type instanceof arrow.FixedSizeList; +} + +function isDataSeparatedCoords( + data: arrow.Data +): data is arrow.Data> { + // TODO: also check child names? Float64? + return data.type instanceof arrow.Struct; +} + +/** + * Convert geoarrow Struct coordinates to FixedSizeList coords + * + * The GeoArrow spec allows for either separated or interleaved coords, but at + * this time deck.gl only supports interleaved. + */ +// TODO: this hasn't been tested yet +export function convertStructToFixedSizeList( + coords: + | arrow.Data> + | arrow.Data> +): arrow.Data> { + if (isDataInterleavedCoords(coords)) { + return coords; + } + if (isDataSeparatedCoords(coords)) { + // TODO: support 3d + const interleavedCoords = new Float64Array(coords.length * 2); + const [xChild, yChild] = coords.children; + for (let i = 0; i < coords.length; i++) { + interleavedCoords[i * 2] = xChild.values[i]; + interleavedCoords[i * 2 + 1] = yChild.values[i]; + } + + const childDataType = new arrow.Float64(); + const dataType = new arrow.FixedSizeList(2, new arrow.Field('coords', childDataType)); + + const interleavedCoordsData = arrow.makeData({ + type: childDataType, + length: interleavedCoords.length + }); + + const data = arrow.makeData({ + type: dataType, + length: coords.length, + nullCount: coords.nullCount, + nullBitmap: coords.nullBitmap, + child: interleavedCoordsData + }); + return data; + } + + throw new Error('data'); +} + +/** + * Get a geometry vector with the specified extension type name from the table. + */ +export function getGeometryVector( + table: arrow.Table, + geoarrowTypeName: string +): arrow.Vector | null { + const geometryColumnIdx = findGeometryColumnIndex(table.schema, geoarrowTypeName); + + if (geometryColumnIdx === null) { + return null; + // throw new Error(`No column found with extension type ${geoarrowTypeName}`); + } + + return table.getChildAt(geometryColumnIdx); +} + +/** + * Provide validation for accessors provided + * + * - Assert that all vectors have the same number of chunks as the main table + * - Assert that all chunks in each vector have the same number of rows as the + * relevant batch in the main table. + * + */ +export function validateVectorAccessors(table: arrow.Table, vectorAccessors: arrow.Vector[]) { + // Check the same number of chunks as the table's batches + for (const vectorAccessor of vectorAccessors) { + assert(table.batches.length === vectorAccessor.data.length); + } + + // Check that each table batch/vector data has the same number of rows + for (const vectorAccessor of vectorAccessors) { + for (let i = 0; i < table.batches.length; i++) { + assert(table.batches[i].numRows === vectorAccessor.data[i].length); + } + } +} + +export function validateColorVector(vector: arrow.Vector) { + // Assert the color vector is a FixedSizeList + assert(arrow.DataType.isFixedSizeList(vector.type)); + + // Assert it has 3 or 4 values + assert(vector.type.listSize === 3 || vector.type.listSize === 4); + + // Assert the child type is an integer + assert(arrow.DataType.isInt(vector.type.children[0])); + + // Assert the child type is a Uint8 + // @ts-ignore + // Property 'type' does not exist on type 'Int_'. Did you mean 'TType'? + assert(vector.type.children[0].type.bitWidth === 8); +} + +export function isPointVector(vector: arrow.Vector): vector is arrow.Vector { + // Check FixedSizeList + if (!arrow.DataType.isFixedSizeList(vector.type)) { + return false; + } + + // Check list size of 2 or 3 + if (vector.type.listSize !== 2 && vector.type.listSize !== 3) { + return false; + } + + // Check child of FixedSizeList is floating type + if (!arrow.DataType.isFloat(vector.type.children[0])) { + return false; + } + + return true; +} + +export function isLineStringVector(vector: arrow.Vector): vector is arrow.Vector { + // Check the outer vector is a List + if (!arrow.DataType.isList(vector.type)) { + return false; + } + + // Check the child is a point vector + if (!isPointVector(vector.getChildAt(0)!)) { + return false; + } + + return true; +} + +export function isPolygonVector(vector: arrow.Vector): vector is arrow.Vector { + // Check the outer vector is a List + if (!arrow.DataType.isList(vector.type)) { + return false; + } + + // Check the child is a linestring vector + if (!isLineStringVector(vector.getChildAt(0)!)) { + return false; + } + + return true; +} + +export function isMultiPointVector(vector: arrow.Vector): vector is arrow.Vector { + // Check the outer vector is a List + if (!arrow.DataType.isList(vector.type)) { + return false; + } + + // Check the child is a point vector + if (!isPointVector(vector.getChildAt(0)!)) { + return false; + } + + return true; +} + +export function isMultiLineStringVector( + vector: arrow.Vector +): vector is arrow.Vector { + // Check the outer vector is a List + if (!arrow.DataType.isList(vector.type)) { + return false; + } + + // Check the child is a linestring vector + if (!isLineStringVector(vector.getChildAt(0)!)) { + return false; + } + + return true; +} + +export function isMultiPolygonVector( + vector: arrow.Vector +): vector is arrow.Vector { + // Check the outer vector is a List + if (!arrow.DataType.isList(vector.type)) { + return false; + } + + // Check the child is a polygon vector + if (!isPolygonVector(vector.getChildAt(0)!)) { + return false; + } + + return true; +} + +export function validatePointType(type: arrow.DataType): type is ArrowPoint { + // Assert the point vector is a FixedSizeList + // TODO: support struct + assert(arrow.DataType.isFixedSizeList(type)); + + // Assert it has 2 or 3 values + assert(type.listSize === 2 || type.listSize === 3); + + // Assert the child type is a float + assert(arrow.DataType.isFloat(type.children[0])); + + return true; +} + +export function validateLineStringType(type: arrow.DataType): type is ArrowLineString { + // Assert the outer vector is a List + assert(arrow.DataType.isList(type)); + + // Assert its inner vector is a point layout + validatePointType(type.children[0].type); + + return true; +} + +export function validatePolygonType(type: arrow.DataType): type is ArrowPolygon { + // Assert the outer vector is a List + assert(arrow.DataType.isList(type)); + + // Assert its inner vector is a linestring layout + validateLineStringType(type.children[0].type); + + return true; +} + +// Note: this is the same as validateLineStringType +export function validateMultiPointType(type: arrow.DataType): type is ArrowMultiPoint { + // Assert the outer vector is a List + assert(arrow.DataType.isList(type)); + + // Assert its inner vector is a point layout + validatePointType(type.children[0].type); + + return true; +} + +export function validateMultiLineStringType(type: arrow.DataType): type is ArrowPolygon { + // Assert the outer vector is a List + assert(arrow.DataType.isList(type)); + + // Assert its inner vector is a linestring layout + validateLineStringType(type.children[0].type); + + return true; +} + +export function validateMultiPolygonType(type: arrow.DataType): type is ArrowMultiPolygon { + // Assert the outer vector is a List + assert(arrow.DataType.isList(type)); + + // Assert its inner vector is a linestring layout + validatePolygonType(type.children[0].type); + + return true; +} + +export function getPointChild(data: arrow.Data): arrow.Data { + // @ts-expect-error + return data.children[0]; +} + +export function getLineStringChild(data: arrow.Data): arrow.Data { + // @ts-expect-error + return data.children[0]; +} + +export function getPolygonChild(data: arrow.Data): arrow.Data { + // @ts-expect-error + return data.children[0]; +} + +export function getMultiPointChild(data: arrow.Data): arrow.Data { + // @ts-expect-error + return data.children[0]; +} + +export function getMultiLineStringChild( + data: arrow.Data +): arrow.Data { + // @ts-expect-error + return data.children[0]; +} + +export function getMultiPolygonChild( + data: arrow.Data +): arrow.Data { + // @ts-expect-error + return data.children[0]; +} diff --git a/modules/arrow/src/index.ts b/modules/arrow/src/index.ts index b18d67c622..af578f8fa2 100644 --- a/modules/arrow/src/index.ts +++ b/modules/arrow/src/index.ts @@ -14,3 +14,43 @@ export {getArrowPaths, getArrowDataByPath, getArrowVectorByPath} from './arrow/a export {getArrowColumnInfo} from './arrow/arrow-column-info'; export {analyzeArrowTable} from './arrow/analyze-arrow-table'; + +export {getArrowListNestingLevel} from './arrow/arrow-utils'; + +// GEOARROW + +export { + findGeometryColumnIndex, + isColumnReference, + getGeometryVector, + validateVectorAccessors, + validateColorVector, + isPointVector, + isLineStringVector, + isPolygonVector, + isMultiPointVector, + isMultiLineStringVector, + isMultiPolygonVector, + validatePointType, + validateLineStringType, + validatePolygonType, + validateMultiPointType, + validateMultiLineStringType, + validateMultiPolygonType, + getPointChild, + getLineStringChild, + getPolygonChild, + getMultiPointChild, + getMultiLineStringChild, + getMultiPolygonChild +} from './geoarrow/geoarrow'; + +export { + getMultiLineStringResolvedOffsets, + getPolygonResolvedOffsets, + getMultiPolygonResolvedOffsets +} from './geoarrow/geoarrow-transform'; + +export {expandArrayToCoords, invertOffsets} from './attribute-utils/attribute-utils'; + +// assignAccessor, diff --git a/modules/arrow/test/arrow/arrow-column-info.spec.ts b/modules/arrow/test/arrow/arrow-column-info.spec.ts new file mode 100644 index 0000000000..72258a0e80 --- /dev/null +++ b/modules/arrow/test/arrow/arrow-column-info.spec.ts @@ -0,0 +1,29 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +// import test from 'tape-promise/tape'; +// import * as arrow from 'apache-arrow'; +// import {getArrowDataByPath} from '@luma.gl/arrow'; +// import { expandArrayToCoords } from "../src/utils.js"; +// import { arraysEqual } from "./utils.js"; + +// test('getArrowDataByPath', async t => { +// const nestedTable = makeNestedArrowTable(); +// t.ok(nestedTable, 'nestedTable created'); + +// const ageData = getArrowDataByPath(nestedTable, 'age'); +// t.equal(ageData, 'extracted age from struct'); + +// const deeplyNestedTable = makeDeeplyNestedArrowTable('data'); +// t.ok(deeplyNestedTable, 'deeplyNestedTable created'); + +// const heightData = getArrowDataByPath(deeplyNestedTable, 'data.age'); +// t.equal( +// heightData.type.typeId, +// new arrow.Float32().typeId, +// 'extracted age from deeply nested struct' +// ); + +// t.end(); +// }); diff --git a/modules/arrow/test/geoarrow/geoarrow.spec.ts b/modules/arrow/test/geoarrow/geoarrow.spec.ts new file mode 100644 index 0000000000..91ded421ba --- /dev/null +++ b/modules/arrow/test/geoarrow/geoarrow.spec.ts @@ -0,0 +1,31 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import test from 'tape-promise/tape'; +import {arraysEqual} from '../test-utils'; +import {expandArrayToCoords} from '@luma.gl/arrow'; + +test('linestring vertex expansion#1', t => { + const input = new Uint8Array([1, 2, 3, 4]); + const size = 1; + const geomOffsets = new Int32Array([0, 5, 8, 12]); + const expanded = expandArrayToCoords(input, size, geomOffsets); + const expected = new Uint8Array([1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3]); + t.ok(arraysEqual(expanded, expected), 'expands correctly (size = 1)'); + + t.end(); +}); + +test('linestring vertex expansion#3', t => { + const input = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8]); + const size = 3; + const geomOffsets = new Int32Array([0, 2, 5, 9]); + const expanded = expandArrayToCoords(input, size, geomOffsets); + const expected = new Uint8Array([ + 0, 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, 3, 4, 5, 6, 7, 8, 6, 7, 8, 6, 7, 8, 6, 7, 8 + ]); + t.ok(arraysEqual(expanded, expected), 'expands correctly (size = 3)'); + + t.end(); +}); diff --git a/modules/arrow/test/index.ts b/modules/arrow/test/index.ts index 7b851e44c5..35406d7e5a 100644 --- a/modules/arrow/test/index.ts +++ b/modules/arrow/test/index.ts @@ -3,5 +3,7 @@ // Copyright (c) vis.gl contributors import './arrow/arrow-paths.spec'; -import './arrow/get-arrow-data.spec'; +import './arrow/arrow-column-info.spec'; import './arrow/analyze-arrow-table.spec'; + +import './geoarrow/geoarrow.spec'; diff --git a/modules/arrow/test/test-utils.ts b/modules/arrow/test/test-utils.ts new file mode 100644 index 0000000000..c49bf0cc19 --- /dev/null +++ b/modules/arrow/test/test-utils.ts @@ -0,0 +1,17 @@ +// luma.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export function arraysEqual(arr1: ArrayLike, arr2: ArrayLike): boolean { + if (arr1.length !== arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) { + return false; + } + } + + return true; +} diff --git a/yarn.lock b/yarn.lock index 51dc1cfbf1..8fd7bd392a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -992,6 +992,7 @@ __metadata: dependencies: "@luma.gl/core": "npm:9.2.0-alpha.0" "@math.gl/polygon": "npm:^4.1.0" + "@math.gl/types": "npm:^4.1.0" apache-arrow: "npm:^17.0.0" languageName: unknown linkType: soft @@ -10496,11 +10497,11 @@ __metadata: "typescript@patch:typescript@npm%3A^5.5.0#optional!builtin": version: 5.6.2 - resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin::version=5.6.2&hash=8c6c40" + resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin::version=5.6.2&hash=74658d" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/94eb47e130d3edd964b76da85975601dcb3604b0c848a36f63ac448d0104e93819d94c8bdf6b07c00120f2ce9c05256b8b6092d23cf5cf1c6fa911159e4d572f + checksum: 10c0/e6c1662e4852e22fe4bbdca471dca3e3edc74f6f1df043135c44a18a7902037023ccb0abdfb754595ca9028df8920f2f8492c00fc3cbb4309079aae8b7de71cd languageName: node linkType: hard