diff --git a/ts/package-lock.json b/ts/package-lock.json index 3148bc809..43e317356 100644 --- a/ts/package-lock.json +++ b/ts/package-lock.json @@ -25,8 +25,8 @@ "earcut": "^3.0.2", "eslint": "^10.0.3", "pbf": "^4.0.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.62.0", "vitest": "^4.0.1" } }, @@ -968,20 +968,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", + "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/type-utils": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -991,9 +991,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/parser": "^8.62.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1007,16 +1007,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", + "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3" }, "engines": { @@ -1028,18 +1028,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", + "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", + "@typescript-eslint/tsconfig-utils": "^8.62.0", + "@typescript-eslint/types": "^8.62.0", "debug": "^4.4.3" }, "engines": { @@ -1050,18 +1050,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", + "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1072,9 +1072,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", + "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", "dev": true, "license": "MIT", "engines": { @@ -1085,21 +1085,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", + "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1110,13 +1110,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", + "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", "dev": true, "license": "MIT", "engines": { @@ -1128,21 +1128,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", + "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.62.0", + "@typescript-eslint/tsconfig-utils": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1152,20 +1152,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", + "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1176,17 +1176,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", + "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2739,9 +2739,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -2773,9 +2773,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2787,16 +2787,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", + "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "@typescript-eslint/eslint-plugin": "8.62.0", + "@typescript-eslint/parser": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2807,7 +2807,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { diff --git a/ts/package.json b/ts/package.json index 81fd2f1db..88c121905 100644 --- a/ts/package.json +++ b/ts/package.json @@ -41,8 +41,8 @@ "earcut": "^3.0.2", "eslint": "^10.0.3", "pbf": "^4.0.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.62.0", "vitest": "^4.0.1" }, "dependencies": { diff --git a/ts/src/decoding/decodingTestUtils.ts b/ts/src/decoding/decodingTestUtils.ts index 1f321fd38..8b9e39b22 100644 --- a/ts/src/decoding/decodingTestUtils.ts +++ b/ts/src/decoding/decodingTestUtils.ts @@ -5,7 +5,7 @@ import { DictionaryType } from "../metadata/tile/dictionaryType"; import { LengthType } from "../metadata/tile/lengthType"; import { OffsetType } from "../metadata/tile/offsetType"; import IntWrapper from "./intWrapper"; -import { type Column, type Field, ComplexType, ScalarType } from "../metadata/tileset/tilesetMetadata"; +import { type Column, type Field, ColumnScope, ComplexType, ScalarType } from "../metadata/tileset/tilesetMetadata"; import { encodeBooleanRle, encodeStrings, createStringLengths } from "../encoding/encodingUtils"; import { encodeVarintInt32Value, encodeVarintInt32 } from "../encoding/integerEncodingUtils"; import type { RleEncodedStreamMetadata, StreamMetadata } from "../metadata/tile/streamMetadataDecoder"; @@ -74,6 +74,7 @@ export function createColumnMetadataForStruct( return { name: columnName, nullable: false, + columnScope: ColumnScope.FEATURE, complexType: { physicalType: ComplexType.STRUCT, children, diff --git a/ts/src/decoding/decodingUtils.ts b/ts/src/decoding/decodingUtils.ts index 267784ddb..d47231d56 100644 --- a/ts/src/decoding/decodingUtils.ts +++ b/ts/src/decoding/decodingUtils.ts @@ -111,7 +111,7 @@ export function decodeString(buf: Uint8Array, pos: number, end: number): string return readUtf8(buf, pos, end); } -function readUtf8(buf, pos, end): string { +function readUtf8(buf: Uint8Array, pos: number, end: number): string { let str = ""; let i = pos; @@ -190,7 +190,7 @@ export function getVectorTypeBooleanStream( : VectorType.FLAT; } -function bitCount(number): number { +function bitCount(number: number): number { //TODO: refactor to get rid of special case handling return number === 0 ? 1 : Math.floor(Math.log2(number) + 1); } diff --git a/ts/src/decoding/geometryDecoder.spec.ts b/ts/src/decoding/geometryDecoder.spec.ts new file mode 100644 index 000000000..bc4cbf8f4 --- /dev/null +++ b/ts/src/decoding/geometryDecoder.spec.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import IntWrapper from "./intWrapper"; +import { decodeGeometryColumn } from "./geometryDecoder"; +import { createStream, concatenateBuffers } from "./decodingTestUtils"; +import { encodeVarintInt32 } from "../encoding/integerEncodingUtils"; +import { PhysicalStreamType } from "../metadata/tile/physicalStreamType"; +import { PhysicalLevelTechnique } from "../metadata/tile/physicalLevelTechnique"; +import { DictionaryType } from "../metadata/tile/dictionaryType"; +import { OffsetType } from "../metadata/tile/offsetType"; + +// A single-value geometry-type stream decodes to VectorType.CONST, a multi-value one to FLAT. +const constGeometryType = createStream(PhysicalStreamType.DATA, encodeVarintInt32(new Uint32Array([3])), { + technique: PhysicalLevelTechnique.VARINT, + count: 1, +}); +const flatGeometryType = createStream(PhysicalStreamType.DATA, encodeVarintInt32(new Uint32Array([1, 1])), { + technique: PhysicalLevelTechnique.VARINT, + count: 2, +}); +const vertexStream = createStream(PhysicalStreamType.DATA, encodeVarintInt32(new Uint32Array([0, 0])), { + logical: { dictionaryType: DictionaryType.VERTEX }, + technique: PhysicalLevelTechnique.VARINT, + count: 2, +}); +const indexStream = createStream(PhysicalStreamType.OFFSET, encodeVarintInt32(new Uint32Array([0, 1])), { + logical: { offsetType: OffsetType.INDEX }, + technique: PhysicalLevelTechnique.VARINT, + count: 2, +}); + +describe("decodeGeometryColumn invariants", () => { + it("throws when a single-geometry-type column is missing its vertex buffer", () => { + expect(() => decodeGeometryColumn(constGeometryType, 1, new IntWrapper(0), 1)).toThrow( + "Geometry column is missing its vertex buffer.", + ); + }); + + it("throws when a mixed-geometry-type column is missing its vertex buffer", () => { + expect(() => decodeGeometryColumn(flatGeometryType, 1, new IntWrapper(0), 2)).toThrow( + "Geometry column is missing its vertex buffer.", + ); + }); + + it("throws when a single-geometry-type tessellated column is missing its triangle offsets", () => { + const column = concatenateBuffers(constGeometryType, vertexStream, indexStream); + expect(() => decodeGeometryColumn(column, 3, new IntWrapper(0), 1)).toThrow( + "Tessellated geometry is missing its triangle offsets.", + ); + }); + + it("throws when a mixed-geometry-type tessellated column is missing its triangle offsets", () => { + const column = concatenateBuffers(flatGeometryType, vertexStream, indexStream); + expect(() => decodeGeometryColumn(column, 3, new IntWrapper(0), 2)).toThrow( + "Tessellated geometry is missing its triangle offsets.", + ); + }); +}); diff --git a/ts/src/decoding/geometryDecoder.ts b/ts/src/decoding/geometryDecoder.ts index 0e02a2a1e..a27d8133d 100644 --- a/ts/src/decoding/geometryDecoder.ts +++ b/ts/src/decoding/geometryDecoder.ts @@ -96,7 +96,14 @@ export function decodeGeometryColumn( } } + if (vertexBuffer === undefined) { + throw new Error("Geometry column is missing its vertex buffer."); + } + if (indexBuffer) { + if (triangleOffsets === undefined) { + throw new Error("Tessellated geometry is missing its triangle offsets."); + } if (geometryOffsets !== undefined || partOffsets !== undefined) { /* Case when the indices of a Polygon outline are encoded in the tile */ const topologyVector = { geometryOffsets, partOffsets, ringOffsets }; @@ -208,13 +215,19 @@ export function decodeGeometryColumn( partOffsets = decodeRootLengthStream(geometryTypeVector, partLengths, 0); } - if (indexBuffer && !partOffsets) { - /* Case when the indices of a Polygon outline are not encoded in the data so no - * topology data are present in the tile */ - return createFlatGpuVector(geometryTypeVector, triangleOffsets, indexBuffer, vertexBuffer); + if (vertexBuffer === undefined) { + throw new Error("Geometry column is missing its vertex buffer."); } if (indexBuffer) { + if (triangleOffsets === undefined) { + throw new Error("Tessellated geometry is missing its triangle offsets."); + } + if (!partOffsets) { + /* Case when the indices of a Polygon outline are not encoded in the data so no + * topology data are present in the tile */ + return createFlatGpuVector(geometryTypeVector, triangleOffsets, indexBuffer, vertexBuffer); + } /* Case when the indices of a Polygon outline are encoded in the tile */ return createFlatGpuVector(geometryTypeVector, triangleOffsets, indexBuffer, vertexBuffer, { geometryOffsets, diff --git a/ts/src/decoding/integerDecodingUtils.ts b/ts/src/decoding/integerDecodingUtils.ts index 569466aa8..8a68dfa15 100644 --- a/ts/src/decoding/integerDecodingUtils.ts +++ b/ts/src/decoding/integerDecodingUtils.ts @@ -118,7 +118,7 @@ function decodeVarintFloat64Value(buf: Uint8Array, offset: IntWrapper): number { return decodeVarintRemainder(val, buf, offset); } -function decodeVarintRemainder(l, buf, offset) { +function decodeVarintRemainder(l: number, buf: Uint8Array, offset: IntWrapper): number { let h; let b; b = buf[offset.get()]; diff --git a/ts/src/decoding/integerStreamDecoder.ts b/ts/src/decoding/integerStreamDecoder.ts index 00f19ab12..63c2e14d1 100644 --- a/ts/src/decoding/integerStreamDecoder.ts +++ b/ts/src/decoding/integerStreamDecoder.ts @@ -261,7 +261,7 @@ function decodeSignedInt32( decodedValues = new Int32Array(values); break; case LogicalLevelTechnique.COMPONENTWISE_DELTA: - if (scalingData && !nullabilityBuffer) { + if (scalingData?.scale !== undefined && !nullabilityBuffer) { return decodeComponentwiseDeltaVec2Scaled(values, scalingData.scale, scalingData.min, scalingData.max); } decodedValues = decodeComponentwiseDeltaVec2(values); @@ -310,7 +310,7 @@ function decodeUnsignedInt32( decodedValues = values; break; case LogicalLevelTechnique.COMPONENTWISE_DELTA: - if (scalingData && !nullabilityBuffer) { + if (scalingData?.scale !== undefined && !nullabilityBuffer) { decodedValues = decodeUnsignedComponentwiseDeltaVec2Scaled( values, scalingData.scale, diff --git a/ts/src/decoding/propertyDecoder.ts b/ts/src/decoding/propertyDecoder.ts index e8814ec5c..c3307a5b5 100644 --- a/ts/src/decoding/propertyDecoder.ts +++ b/ts/src/decoding/propertyDecoder.ts @@ -36,7 +36,7 @@ export function decodePropertyColumn( numStreams: number, numFeatures: number, propertyColumnNames?: Set, -): Vector | Vector[] { +): Vector | Vector[] | null { if (columnMetadata.type === "scalarType") { if (propertyColumnNames && !propertyColumnNames.has(columnMetadata.name)) { skipColumn(numStreams, data, offset); @@ -68,7 +68,7 @@ function decodeScalarPropertyColumn( column: ScalarColumn, columnMetadata: Column, ) { - let nullabilityBuffer: BitVector = null; + let nullabilityBuffer: BitVector | undefined; if (numStreams === 0) { return null; } @@ -91,7 +91,7 @@ function decodeScalarPropertyColumn( case ScalarType.STRING: { // In embedded format: numStreams includes nullability stream if column is nullable const stringDataStreams = columnMetadata.nullable ? numStreams - 1 : numStreams; - return decodeString(columnMetadata.name, data, offset, stringDataStreams, nullabilityBuffer); + return decodeString(columnMetadata.name, data, offset, stringDataStreams, nullabilityBuffer) ?? null; } case ScalarType.BOOLEAN: return decodeBooleanColumn(data, offset, columnMetadata, numFeatures, sizeOrNullabilityBuffer); diff --git a/ts/src/decoding/stringDecoder.spec.ts b/ts/src/decoding/stringDecoder.spec.ts index 5283b923b..910e16878 100644 --- a/ts/src/decoding/stringDecoder.spec.ts +++ b/ts/src/decoding/stringDecoder.spec.ts @@ -13,9 +13,10 @@ import { import { StringFlatVector } from "../vector/flat/stringFlatVector"; import { StringDictionaryVector } from "../vector/dictionary/stringDictionaryVector"; import { StringFsstDictionaryVector } from "../vector/fsst-dictionary/stringFsstDictionaryVector"; -import { ScalarType } from "../metadata/tileset/tilesetMetadata"; +import { type Column, ColumnScope, ScalarType } from "../metadata/tileset/tilesetMetadata"; import { PhysicalStreamType } from "../metadata/tile/physicalStreamType"; import { LengthType } from "../metadata/tile/lengthType"; +import { DictionaryType } from "../metadata/tile/dictionaryType"; describe("decodeString - Plain String Decoder", () => { it("should decode plain strings with simple ASCII values", () => { @@ -175,20 +176,20 @@ describe("decodeString - FSST Dictionary Decoder (Basic Coverage)", () => { }); describe("decodeString - Empty Column Edge Cases", () => { - it("should handle empty column with numStreams = 0 (returns null)", () => { + it("should handle empty column with numStreams = 0 (returns undefined)", () => { const fullStream = new Uint8Array([]); const offset = new IntWrapper(0); const result = decodeString("testColumn", fullStream, offset, 0); - expect(result).toBeNull(); + expect(result).toBeUndefined(); }); - it("should handle column with all zero-length streams (returns null)", () => { + it("should handle column with all zero-length streams (returns undefined)", () => { const emptyStream = createStream(PhysicalStreamType.LENGTH, new Uint8Array([]), { logical: { lengthType: LengthType.VAR_BINARY }, }); const offset = new IntWrapper(0); const result = decodeString("testColumn", emptyStream, offset, 1); - expect(result).toBeNull(); + expect(result).toBeUndefined(); }); it("should handle single value plain string column", () => { @@ -210,6 +211,28 @@ describe("decodeString - Empty Column Edge Cases", () => { }); }); +describe("decodeString - incomplete column streams throw", () => { + it("throws when an FSST symbol table is present but its companion streams are missing", () => { + const symbolTableOnly = createStream(PhysicalStreamType.DATA, new Uint8Array([1, 2, 3]), { + logical: { dictionaryType: DictionaryType.FSST }, + }); + const offset = new IntWrapper(0); + expect(() => decodeString("fsstColumn", symbolTableOnly, offset, 1)).toThrow( + 'Incomplete FSST dictionary string column "fsstColumn"', + ); + }); + + it("throws when a dictionary stream is present but offsets/lengths are missing", () => { + const dictionaryDataOnly = createStream(PhysicalStreamType.DATA, new Uint8Array([1, 2, 3]), { + logical: { dictionaryType: DictionaryType.SINGLE }, + }); + const offset = new IntWrapper(0); + expect(() => decodeString("dictColumn", dictionaryDataOnly, offset, 1)).toThrow( + 'Incomplete dictionary string column "dictColumn"', + ); + }); +}); + describe("decodeString - Integration Tests", () => { it("should correctly track offset through multiple streams", () => { const strings = ["hello", "world"]; @@ -503,6 +526,46 @@ describe("decodeSharedDictionary", () => { }).toThrow("Currently only scalar string fields are implemented for a struct."); }); + it("should throw when the column is not a complex (struct) column", () => { + const { lengthStream, dataStream } = encodeSharedDictionary(["value"]); + const complete = concatenateBuffers(lengthStream, dataStream); + const scalarColumn: Column = { + name: "notAStruct", + nullable: false, + columnScope: ColumnScope.FEATURE, + type: "scalarType", + scalarType: { longID: false, type: "physicalType", physicalType: ScalarType.STRING }, + }; + + expect(() => { + decodeSharedDictionary(complete, new IntWrapper(0), scalarColumn); + }).toThrow("Shared dictionary column notAStruct must be a complex (struct) column."); + }); + + it("should throw when the shared dictionary offsets are missing", () => { + // Only the dictionary data stream is present, so the dictionary offset buffer is never decoded. + const { dataStream } = encodeSharedDictionary(["value"]); + const columnMetadata = createColumnMetadataForStruct("incomplete:", [{ name: "field1" }]); + + expect(() => { + decodeSharedDictionary(dataStream, new IntWrapper(0), columnMetadata); + }).toThrow('Incomplete shared dictionary for column "incomplete:"'); + }); + + it("should throw when an FSST shared dictionary is missing its symbol offsets", () => { + // Provide the dictionary length + data and the FSST symbol table, but omit the symbol length stream + // so the symbol offset buffer is never decoded. + const { lengthStream, dataStream, symbolDataStream } = encodeSharedDictionary(["value"], { useFsst: true }); + if (!symbolDataStream) throw new Error("test setup: expected an FSST symbol data stream"); + const fieldStreams = encodeStructField([0], [true]); + const complete = concatenateBuffers(lengthStream, symbolDataStream, dataStream, fieldStreams); + const columnMetadata = createColumnMetadataForStruct("fsst:", [{ name: "value" }]); + + expect(() => { + decodeSharedDictionary(complete, new IntWrapper(0), columnMetadata); + }).toThrow('Incomplete shared FSST dictionary for column "fsst:value"'); + }); + it("should throw error for mismatched nullability and numStreams", () => { const dictionaryStrings = ["value"]; const { lengthStream, dataStream } = encodeSharedDictionary(dictionaryStrings); diff --git a/ts/src/decoding/stringDecoder.ts b/ts/src/decoding/stringDecoder.ts index 987d6e555..a6a915fb7 100644 --- a/ts/src/decoding/stringDecoder.ts +++ b/ts/src/decoding/stringDecoder.ts @@ -19,15 +19,15 @@ export function decodeString( offset: IntWrapper, numStreams: number, bitVector?: BitVector, -): Vector { - let dictionaryLengthStream: Uint32Array = null; - let offsetStream: Uint32Array = null; - let dictionaryStream: Uint8Array = null; - let symbolLengthStream: Uint32Array = null; - let symbolTableStream: Uint8Array = null; - let nullabilityBuffer: BitVector = bitVector ?? null; - let plainLengthStream: Uint32Array = null; - let plainDataStream: Uint8Array = null; +): Vector | undefined { + let dictionaryLengthStream: Uint32Array | undefined; + let offsetStream: Uint32Array | undefined; + let dictionaryStream: Uint8Array | undefined; + let symbolLengthStream: Uint32Array | undefined; + let symbolTableStream: Uint8Array | undefined; + let nullabilityBuffer: BitVector | undefined = bitVector; + let plainLengthStream: Uint32Array | undefined; + let plainDataStream: Uint8Array | undefined; for (let i = 0; i < numStreams; i++) { const streamMetadata = decodeStreamMetadata(data, offset); @@ -88,15 +88,18 @@ export function decodeString( function decodeFsstDictionaryVector( name: string, - symbolTableStream: Uint8Array | null, - offsetStream: Uint32Array | null, - dictionaryLengthStream: Uint32Array | null, - dictionaryStream: Uint8Array | null, - symbolLengthStream: Uint32Array | null, - nullabilityBuffer: BitVector | null, -): Vector | null { + symbolTableStream: Uint8Array | undefined, + offsetStream: Uint32Array | undefined, + dictionaryLengthStream: Uint32Array | undefined, + dictionaryStream: Uint8Array | undefined, + symbolLengthStream: Uint32Array | undefined, + nullabilityBuffer: BitVector | undefined, +): Vector | undefined { if (!symbolTableStream) { - return null; + return undefined; + } + if (!offsetStream || !dictionaryLengthStream || !dictionaryStream || !symbolLengthStream) { + throw new Error(`Incomplete FSST dictionary string column "${name}"`); } return new StringFsstDictionaryVector( name, @@ -111,13 +114,16 @@ function decodeFsstDictionaryVector( function decodeDictionaryVector( name: string, - dictionaryStream: Uint8Array | null, - offsetStream: Uint32Array | null, - dictionaryLengthStream: Uint32Array | null, - nullabilityBuffer: BitVector | null, -): Vector | null { + dictionaryStream: Uint8Array | undefined, + offsetStream: Uint32Array | undefined, + dictionaryLengthStream: Uint32Array | undefined, + nullabilityBuffer: BitVector | undefined, +): Vector | undefined { if (!dictionaryStream) { - return null; + return undefined; + } + if (!offsetStream || !dictionaryLengthStream) { + throw new Error(`Incomplete dictionary string column "${name}"`); } return nullabilityBuffer ? new StringDictionaryVector(name, offsetStream, dictionaryLengthStream, dictionaryStream, nullabilityBuffer) @@ -126,13 +132,13 @@ function decodeDictionaryVector( function decodePlainStringVector( name: string, - plainLengthStream: Uint32Array | null, - plainDataStream: Uint8Array | null, - offsetStream: Uint32Array | null, - nullabilityBuffer: BitVector | null, -): Vector | null { + plainLengthStream: Uint32Array | undefined, + plainDataStream: Uint8Array | undefined, + offsetStream: Uint32Array | undefined, + nullabilityBuffer: BitVector | undefined, +): Vector | undefined { if (!plainLengthStream || !plainDataStream) { - return null; + return undefined; } if (offsetStream) { @@ -171,10 +177,10 @@ export function decodeSharedDictionary( column: Column, propertyColumnNames?: Set, ): Vector[] { - let dictionaryOffsetBuffer: Uint32Array = null; - let dictionaryBuffer: Uint8Array = null; - let symbolOffsetBuffer: Uint32Array = null; - let symbolTableBuffer: Uint8Array = null; + let dictionaryOffsetBuffer: Uint32Array | undefined; + let dictionaryBuffer: Uint8Array | undefined; + let symbolOffsetBuffer: Uint32Array | undefined; + let symbolTableBuffer: Uint8Array | undefined; let dictionaryStreamDecoded = false; while (!dictionaryStreamDecoded) { @@ -202,6 +208,12 @@ export function decodeSharedDictionary( } } + if (column.type !== "complexType") { + throw new Error(`Shared dictionary column ${column.name} must be a complex (struct) column.`); + } + if (!dictionaryOffsetBuffer || !dictionaryBuffer) { + throw new Error(`Incomplete shared dictionary for column "${column.name}"`); + } const childFields = column.complexType.children; const stringDictionaryVectors = []; let i = 0; @@ -250,23 +262,28 @@ export function decodeSharedDictionary( presentStreamBitVector, ); - stringDictionaryVectors[i++] = symbolTableBuffer - ? new StringFsstDictionaryVector( - columnName, - offsetStream, - dictionaryOffsetBuffer, - dictionaryBuffer, - symbolOffsetBuffer, - symbolTableBuffer, - presentStreamBitVector, - ) - : new StringDictionaryVector( - columnName, - offsetStream, - dictionaryOffsetBuffer, - dictionaryBuffer, - presentStreamBitVector, - ); + if (symbolTableBuffer) { + if (!symbolOffsetBuffer) { + throw new Error(`Incomplete shared FSST dictionary for column "${columnName}"`); + } + stringDictionaryVectors[i++] = new StringFsstDictionaryVector( + columnName, + offsetStream, + dictionaryOffsetBuffer, + dictionaryBuffer, + symbolOffsetBuffer, + symbolTableBuffer, + presentStreamBitVector, + ); + } else { + stringDictionaryVectors[i++] = new StringDictionaryVector( + columnName, + offsetStream, + dictionaryOffsetBuffer, + dictionaryBuffer, + presentStreamBitVector, + ); + } } return stringDictionaryVectors; diff --git a/ts/src/decoding/unpackNullableUtils.ts b/ts/src/decoding/unpackNullableUtils.ts index d90ed5f68..8ebd4d79b 100644 --- a/ts/src/decoding/unpackNullableUtils.ts +++ b/ts/src/decoding/unpackNullableUtils.ts @@ -46,7 +46,7 @@ export function unpackNullable( let counter = 0; for (let i = 0; i < size; i++) { // If position has a value, take from data stream; otherwise use default - result[i] = presentBits.get(i) ? dataStream[counter++] : (defaultValue as any); + result[i] = presentBits.get(i) ? dataStream[counter++] : defaultValue; } return result; diff --git a/ts/src/encoding/constGeometryVectorEncoder.ts b/ts/src/encoding/constGeometryVectorEncoder.ts index 6b0ca1751..e639046ba 100644 --- a/ts/src/encoding/constGeometryVectorEncoder.ts +++ b/ts/src/encoding/constGeometryVectorEncoder.ts @@ -4,7 +4,7 @@ import { VertexBufferType } from "../vector/geometry/vertexBufferType"; import { encodeZOrderCurve } from "./zOrderCurveEncoder"; import type { GeometryVector, MortonSettings } from "../vector/geometry/geometryVector"; -export const DEFAULT_MORTON_SETTINGS: MortonSettings = { numBits: 16, coordinateShift: 0 } as MortonSettings; +export const DEFAULT_MORTON_SETTINGS: MortonSettings = { numBits: 16, coordinateShift: 0 }; export function encode(x: number, y: number): number { return encodeZOrderCurve(x, y, DEFAULT_MORTON_SETTINGS.numBits, DEFAULT_MORTON_SETTINGS.coordinateShift); diff --git a/ts/src/encoding/fastPforEncoder.ts b/ts/src/encoding/fastPforEncoder.ts index 47d550dda..a34337fb8 100644 --- a/ts/src/encoding/fastPforEncoder.ts +++ b/ts/src/encoding/fastPforEncoder.ts @@ -50,7 +50,7 @@ function ensureUint8Capacity(buffer: Uint8Array, requiredLength: number): Uint8A * Use one workspace per concurrent encode call. */ export type FastPforEncoderWorkspace = { - dataToBePacked: Array; + dataToBePacked: Uint32Array[]; dataPointers: Int32Array; byteContainer: Uint8Array; bitWidthFrequencies: Int32Array; @@ -99,8 +99,10 @@ export function fastPack32( } export function createFastPforEncoderWorkspace(): FastPforEncoderWorkspace { - const dataToBePacked: Array = new Array(BIT_WIDTH_SLOTS); - for (let k = 1; k < BIT_WIDTH_SLOTS; k++) { + // Slot 0 (bit width 0) is never indexed, but allocating it keeps the array free of `undefined` + // holes so callers can index without null checks. + const dataToBePacked: Uint32Array[] = new Array(BIT_WIDTH_SLOTS); + for (let k = 0; k < BIT_WIDTH_SLOTS; k++) { dataToBePacked[k] = new Uint32Array(INITIAL_PACKED_BUFFER_SIZE_WORDS); } @@ -161,7 +163,7 @@ function writeByte(workspace: FastPforEncoderWorkspace, byteContainerPos: number } function ensureExceptionValuesCapacity( - dataToBePacked: Array, + dataToBePacked: Uint32Array[], dataPointers: Int32Array, exceptionBitWidth: number, exceptionCount: number, @@ -170,11 +172,11 @@ function ensureExceptionValuesCapacity( const needed = dataPointers[exceptionBitWidth] + exceptionCount; const currentExceptionValues = dataToBePacked[exceptionBitWidth]; - if (!currentExceptionValues || needed >= currentExceptionValues.length) { + if (needed >= currentExceptionValues.length) { let newSize = 2 * needed; newSize = roundUpToMultipleOf32(newSize); const next = new Uint32Array(newSize); - if (currentExceptionValues) next.set(currentExceptionValues); + next.set(currentExceptionValues); dataToBePacked[exceptionBitWidth] = next; } } diff --git a/ts/src/encoding/integerStreamEncoder.ts b/ts/src/encoding/integerStreamEncoder.ts index a7c0d17cd..0fa0531eb 100644 --- a/ts/src/encoding/integerStreamEncoder.ts +++ b/ts/src/encoding/integerStreamEncoder.ts @@ -86,7 +86,7 @@ function encodeSignedInt32( data = new Uint32Array(values); return { data }; case LogicalLevelTechnique.COMPONENTWISE_DELTA: - if (scalingData && !bitVector) { + if (scalingData?.scale !== undefined && !bitVector) { const data = encodeComponentwiseDeltaVec2Scaled(values, scalingData.scale); return { data }; } @@ -127,7 +127,7 @@ function encodeUnsignedInt32( data = values; return { data }; case LogicalLevelTechnique.COMPONENTWISE_DELTA: - if (scalingData && !bitVector) { + if (scalingData?.scale !== undefined && !bitVector) { const data = encodeComponentwiseDeltaVec2Scaled(new Int32Array(values), scalingData.scale); return { data }; } diff --git a/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.spec.ts b/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.spec.ts index c48f99ddf..1179f385c 100644 --- a/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.spec.ts +++ b/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.spec.ts @@ -228,6 +228,19 @@ describe("embeddedTilesetMetadataDecoder", () => { }).toThrow("Missing layer name"); }); + it("should throw for an unsupported column type code", () => { + const buffer = concatenateBuffers( + encodeFieldName("layer"), + encodeTypeCode(4096), + encodeChildCount(1), + encodeTypeCode(5), // 5 is not a valid column type code + ); + + expect(() => { + decodeEmbeddedTileSetMetadata(buffer, new IntWrapper(0)); + }).toThrow("Unsupported column type code 5"); + }); + it("should decode logical ID metadata with implicit id column name", () => { const typeCode = 3; const buffer = concatenateBuffers( diff --git a/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.ts b/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.ts index 3b1a3f48a..8b61a5f7e 100644 --- a/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.ts +++ b/ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.ts @@ -29,13 +29,10 @@ function decodeString(src: Uint8Array, offset: IntWrapper): string { * Used when decoding Field metadata which has the same format as Column. */ function columnToField(column: Column): Field { - return { - name: column.name, - nullable: column.nullable, - scalarField: column.scalarType, - complexField: column.complexType, - type: column.type === "scalarType" ? "scalarField" : "complexField", - }; + const base = { name: column.name, nullable: column.nullable }; + return column.type === "scalarType" + ? { ...base, type: "scalarField", scalarField: column.scalarType } + : { ...base, type: "complexField", complexField: column.complexType }; } /** @@ -44,21 +41,20 @@ function columnToField(column: Column): Field { export function decodeField(src: Uint8Array, offset: IntWrapper): Field { const typeCode = decodeVarintInt32(src, offset, 1)[0] >>> 0; - if (typeCode < 10 || typeCode > 30) { + const base = typeCode >= 10 ? decodeColumnType(typeCode) : null; + if (!base) { throw new Error(`Unsupported field type code ${typeCode}. Supported: ${SUPPORTED_FIELD_TYPES}`); } - const column = decodeColumnType(typeCode); + // Field type codes (10-30) always carry an explicit name. + const column: Column = { ...base, name: decodeString(src, offset) }; - if (columnTypeHasName(typeCode)) { - column.name = decodeString(src, offset); - } - - if (columnTypeHasChildren(typeCode)) { + if (column.type === "complexType" && columnTypeHasChildren(typeCode)) { + const complexCol = column.complexType; const childCount = decodeVarintInt32(src, offset, 1)[0] >>> 0; - column.complexType.children = new Array(childCount); + complexCol.children = new Array(childCount); for (let i = 0; i < childCount; i++) { - column.complexType.children[i] = decodeField(src, offset); + complexCol.children[i] = decodeField(src, offset); } } @@ -70,24 +66,25 @@ export function decodeField(src: Uint8Array, offset: IntWrapper): Field { */ function decodeColumn(src: Uint8Array, offset: IntWrapper): Column { const typeCode = decodeVarintInt32(src, offset, 1)[0] >>> 0; - const column = decodeColumnType(typeCode); + const base = decodeColumnType(typeCode); - if (!column) { + if (!base) { throw new Error(`Unsupported column type code ${typeCode}. Supported: ${SUPPORTED_COLUMN_TYPES}`); } + let name: string; if (columnTypeHasName(typeCode)) { - column.name = decodeString(src, offset); - } else { + name = decodeString(src, offset); + } else if (typeCode <= 3) { // ID and GEOMETRY columns have implicit names - if (typeCode >= 0 && typeCode <= 3) { - column.name = "id"; - } else if (typeCode === 4) { - column.name = "geometry"; - } + name = "id"; + } else { + name = "geometry"; } - if (columnTypeHasChildren(typeCode)) { + const column: Column = { ...base, name }; + + if (column.type === "complexType" && columnTypeHasChildren(typeCode)) { // Only STRUCT (typeCode 30) has children const childCount = decodeVarintInt32(src, offset, 1)[0] >>> 0; const complexCol = column.complexType; diff --git a/ts/src/metadata/tileset/tilesetMetadata.ts b/ts/src/metadata/tileset/tilesetMetadata.ts index 5fd7345b1..7ddf9cd87 100644 --- a/ts/src/metadata/tileset/tilesetMetadata.ts +++ b/ts/src/metadata/tileset/tilesetMetadata.ts @@ -32,62 +32,65 @@ export const LogicalComplexType = { } as const; export interface TileSetMetadata { - version?: number | null; + version?: number; featureTables: FeatureTableSchema[]; - name?: string | null; - description?: string | null; - attribution?: string | null; - minZoom?: number | null; - maxZoom?: number | null; + name?: string; + description?: string; + attribution?: string; + minZoom?: number; + maxZoom?: number; bounds: number[]; center: number[]; } export interface FeatureTableSchema { - name?: string | null; + name: string; columns: Column[]; } -export interface Column { - name?: string | null; - nullable?: boolean | null; - columnScope?: number | null; - scalarType?: ScalarColumn | null; - complexType?: ComplexColumn | null; - type?: "scalarType" | "complexType"; -} +export type Column = { + name: string; + nullable: boolean; + columnScope: number; +} & ( + | { type: "scalarType"; scalarType: ScalarColumn; complexType?: undefined } + | { type: "complexType"; complexType: ComplexColumn; scalarType?: undefined } +); + +/** `Omit` that distributes over the union members of {@link Column}, preserving the `type` discriminant. */ +export type ColumnWithoutName = Column extends infer C ? (C extends Column ? Omit : never) : never; export interface ScalarColumn { - longID?: boolean | null; - physicalType?: number | null; - logicalType?: number | null; + longID: boolean; + physicalType?: number; + logicalType?: number; type?: "physicalType" | "logicalType"; } export interface ComplexColumn { - physicalType?: number | null; - logicalType?: number | null; + physicalType?: number; + logicalType?: number; children: Field[]; type?: "physicalType" | "logicalType"; } -export interface Field { - name?: string | null; - nullable?: boolean | null; - scalarField?: ScalarField | null; - complexField?: ComplexField | null; - type?: "scalarField" | "complexField"; -} +export type Field = { + name?: string; + nullable?: boolean; +} & ( + | { type: "scalarField"; scalarField: ScalarField; complexField?: undefined } + | { type: "complexField"; complexField: ComplexField; scalarField?: undefined } +); export interface ScalarField { - physicalType?: number | null; - logicalType?: number | null; + physicalType?: number; + logicalType?: number; type?: "physicalType" | "logicalType"; } export interface ComplexField { - physicalType?: number | null; - logicalType?: number | null; + physicalType?: number; + logicalType?: number; children: Field[]; type?: "physicalType" | "logicalType"; } diff --git a/ts/src/metadata/tileset/typeMap.spec.ts b/ts/src/metadata/tileset/typeMap.spec.ts index a4c892366..d1dbe1f44 100644 --- a/ts/src/metadata/tileset/typeMap.spec.ts +++ b/ts/src/metadata/tileset/typeMap.spec.ts @@ -27,6 +27,55 @@ describe("typeMap helpers", () => { } }); + it("should decode the GEOMETRY type code as a non-nullable complex column", () => { + expect(decodeColumnType(4)).toMatchObject({ + nullable: false, + type: "complexType", + complexType: { type: "physicalType", physicalType: ComplexType.GEOMETRY }, + }); + }); + + it("should decode the STRUCT type code as a non-nullable complex column", () => { + expect(decodeColumnType(30)).toMatchObject({ + nullable: false, + type: "complexType", + complexType: { type: "physicalType", physicalType: ComplexType.STRUCT }, + }); + }); + + it("should decode scalar type codes with the nullable flag in the low bit", () => { + const cases: Array<{ even: number; physicalType: number }> = [ + { even: 10, physicalType: ScalarType.BOOLEAN }, + { even: 12, physicalType: ScalarType.INT_8 }, + { even: 14, physicalType: ScalarType.UINT_8 }, + { even: 16, physicalType: ScalarType.INT_32 }, + { even: 18, physicalType: ScalarType.UINT_32 }, + { even: 20, physicalType: ScalarType.INT_64 }, + { even: 22, physicalType: ScalarType.UINT_64 }, + { even: 24, physicalType: ScalarType.FLOAT }, + { even: 26, physicalType: ScalarType.DOUBLE }, + { even: 28, physicalType: ScalarType.STRING }, + ]; + + for (const { even, physicalType } of cases) { + expect(decodeColumnType(even)).toMatchObject({ + nullable: false, + type: "scalarType", + scalarType: { type: "physicalType", physicalType }, + }); + expect(decodeColumnType(even + 1)).toMatchObject({ + nullable: true, + type: "scalarType", + scalarType: { type: "physicalType", physicalType }, + }); + } + }); + + it("should return null for unsupported type codes", () => { + expect(decodeColumnType(5)).toBeNull(); + expect(decodeColumnType(99)).toBeNull(); + }); + it("should return false for physical scalar columns even when column is named id", () => { const physicalIdNamedColumn = { name: "id", diff --git a/ts/src/metadata/tileset/typeMap.ts b/ts/src/metadata/tileset/typeMap.ts index 62efbb8eb..629fc3d6b 100644 --- a/ts/src/metadata/tileset/typeMap.ts +++ b/ts/src/metadata/tileset/typeMap.ts @@ -1,10 +1,9 @@ import { type Column, + type ColumnWithoutName, ColumnScope, - type ComplexColumn, ComplexType, LogicalScalarType, - type ScalarColumn, ScalarType, } from "./tilesetMetadata"; @@ -27,47 +26,46 @@ import { * ID columns are kept as logical types so they remain distinguishable * from feature properties that may also be named "id". */ -export function decodeColumnType(typeCode: number): Column | null { +export function decodeColumnType(typeCode: number): ColumnWithoutName | null { switch (typeCode) { case 0: case 1: case 2: - case 3: { - const column = {} as Column; - column.nullable = (typeCode & 1) !== 0; - column.columnScope = ColumnScope.FEATURE; - const scalarCol = {} as ScalarColumn; - scalarCol.type = "logicalType"; - scalarCol.logicalType = LogicalScalarType.ID; - scalarCol.longID = (typeCode & 2) !== 0; - column.scalarType = scalarCol; - column.type = "scalarType"; - return column; - } - case 4: { + case 3: + return { + nullable: (typeCode & 1) !== 0, + columnScope: ColumnScope.FEATURE, + type: "scalarType", + scalarType: { + longID: (typeCode & 2) !== 0, + type: "logicalType", + logicalType: LogicalScalarType.ID, + }, + }; + case 4: // GEOMETRY (non-nullable, no children) - const column = {} as Column; - column.nullable = false; - column.columnScope = ColumnScope.FEATURE; - const complexCol = {} as ComplexColumn; - complexCol.type = "physicalType"; - complexCol.physicalType = ComplexType.GEOMETRY; - column.type = "complexType"; - column.complexType = complexCol; - return column; - } - case 30: { + return { + nullable: false, + columnScope: ColumnScope.FEATURE, + type: "complexType", + complexType: { + type: "physicalType", + physicalType: ComplexType.GEOMETRY, + children: [], + }, + }; + case 30: // STRUCT (non-nullable with children) - const column = {} as Column; - column.nullable = false; - column.columnScope = ColumnScope.FEATURE; - const complexCol = {} as ComplexColumn; - complexCol.type = "physicalType"; - complexCol.physicalType = ComplexType.STRUCT; - column.type = "complexType"; - column.complexType = complexCol; - return column; - } + return { + nullable: false, + columnScope: ColumnScope.FEATURE, + type: "complexType", + complexType: { + type: "physicalType", + physicalType: ComplexType.STRUCT, + children: [], + }, + }; default: return mapScalarType(typeCode); } @@ -160,61 +158,62 @@ export function isGeometryColumn(column: Column): boolean { * Type codes 10-29 encode scalar types with nullable flag. * Even codes are non-nullable, odd codes are nullable. */ -function mapScalarType(typeCode: number): Column | null { - let scalarType: number | null; +function mapScalarType(typeCode: number): ColumnWithoutName | null { + let physicalType: number; switch (typeCode) { case 10: case 11: - scalarType = ScalarType.BOOLEAN; + physicalType = ScalarType.BOOLEAN; break; case 12: case 13: - scalarType = ScalarType.INT_8; + physicalType = ScalarType.INT_8; break; case 14: case 15: - scalarType = ScalarType.UINT_8; + physicalType = ScalarType.UINT_8; break; case 16: case 17: - scalarType = ScalarType.INT_32; + physicalType = ScalarType.INT_32; break; case 18: case 19: - scalarType = ScalarType.UINT_32; + physicalType = ScalarType.UINT_32; break; case 20: case 21: - scalarType = ScalarType.INT_64; + physicalType = ScalarType.INT_64; break; case 22: case 23: - scalarType = ScalarType.UINT_64; + physicalType = ScalarType.UINT_64; break; case 24: case 25: - scalarType = ScalarType.FLOAT; + physicalType = ScalarType.FLOAT; break; case 26: case 27: - scalarType = ScalarType.DOUBLE; + physicalType = ScalarType.DOUBLE; break; case 28: case 29: - scalarType = ScalarType.STRING; + physicalType = ScalarType.STRING; break; default: return null; } - const column = {} as Column; - column.nullable = (typeCode & 1) !== 0; - column.columnScope = ColumnScope.FEATURE; - const scalarCol = {} as ScalarColumn; - scalarCol.type = "physicalType"; - scalarCol.physicalType = scalarType; - column.type = "scalarType"; - column.scalarType = scalarCol; - return column; + return { + nullable: (typeCode & 1) !== 0, + columnScope: ColumnScope.FEATURE, + type: "scalarType", + scalarType: { + longID: false, + type: "physicalType", + physicalType, + }, + }; } diff --git a/ts/src/mltDecoder.ts b/ts/src/mltDecoder.ts index dd3afd410..52b012d84 100644 --- a/ts/src/mltDecoder.ts +++ b/ts/src/mltDecoder.ts @@ -149,10 +149,14 @@ export default function decodeTile( } } + if (geometryVector === null) { + throw new Error(`Feature table "${featureTableMetadata.name}" is missing its geometry column.`); + } + const featureTable = new FeatureTable( featureTableMetadata.name, geometryVector, - idVector, + idVector ?? undefined, propertyVectors, extent, ); diff --git a/ts/src/vector/featureTable.spec.ts b/ts/src/vector/featureTable.spec.ts new file mode 100644 index 000000000..7feb028c0 --- /dev/null +++ b/ts/src/vector/featureTable.spec.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import FeatureTable from "./featureTable"; +import type Vector from "./vector"; +import { encodePointGeometryVector } from "../encoding/constGeometryVectorEncoder"; + +const geometryVector = encodePointGeometryVector(1, 2); +const propertyVector = (name: string): Vector => ({ name }) as unknown as Vector; + +describe("FeatureTable", () => { + it("throws when constructed without a layer name", () => { + expect(() => new FeatureTable("", geometryVector)).toThrow("Missing layer name"); + }); + + it("returns an empty array when no property vectors are provided", () => { + const table = new FeatureTable("layer", geometryVector); + expect(table.propertyVectors).toEqual([]); + }); + + it("looks up property vectors by name and returns undefined for unknown names", () => { + const a = propertyVector("a"); + const b = propertyVector("b"); + const table = new FeatureTable("layer", geometryVector, undefined, [a, b]); + + expect(table.getPropertyVector("a")).toBe(a); + expect(table.getPropertyVector("b")).toBe(b); + expect(table.getPropertyVector("missing")).toBeUndefined(); + }); +}); diff --git a/ts/src/vector/featureTable.ts b/ts/src/vector/featureTable.ts index 6586b33dd..163fcd4cb 100644 --- a/ts/src/vector/featureTable.ts +++ b/ts/src/vector/featureTable.ts @@ -8,13 +8,13 @@ import { Int32ConstVector } from "./constant/int32ConstVector"; import type { GpuVector } from "./geometry/gpuVector"; export interface Feature { - id: number | bigint; + id: number | bigint | undefined; geometry: Geometry; properties: { [key: string]: unknown }; } export default class FeatureTable { - private propertyVectorsMap: Map; + private propertyVectorsMap?: Map; constructor( private readonly _name: string, @@ -32,7 +32,7 @@ export default class FeatureTable { return this._name; } - get idVector(): IdVector { + get idVector(): IdVector | undefined { return this._idVector; } @@ -41,12 +41,12 @@ export default class FeatureTable { } get propertyVectors(): Vector[] { - return this._propertyVectors; + return this._propertyVectors ?? []; } - getPropertyVector(name: string): Vector { + getPropertyVector(name: string): Vector | undefined { if (!this.propertyVectorsMap) { - this.propertyVectorsMap = new Map(this._propertyVectors.map((vector) => [vector.name, vector])); + this.propertyVectorsMap = new Map(this.propertyVectors.map((vector) => [vector.name, vector])); } return this.propertyVectorsMap.get(name); @@ -68,10 +68,12 @@ export default class FeatureTable { const geometries = this.geometryVector.getGeometries(); for (let i = 0; i < this.numFeatures; i++) { - let id; + let id: number | bigint | undefined; if (this.idVector) { const idValue = this.idVector.getValue(i); - id = this.containsMaxSafeIntegerValues(this.idVector) && idValue !== null ? Number(idValue) : idValue; + if (idValue !== null) { + id = this.containsMaxSafeIntegerValues(this.idVector) ? Number(idValue) : idValue; + } } const geometry = { coordinates: geometries[i], diff --git a/ts/src/vector/filter/flatSelectionVector.ts b/ts/src/vector/filter/flatSelectionVector.ts index cbbeaa7c9..15e6bf801 100644 --- a/ts/src/vector/filter/flatSelectionVector.ts +++ b/ts/src/vector/filter/flatSelectionVector.ts @@ -5,18 +5,18 @@ import type { SelectionVector } from "./selectionVector"; * Stores indices explicitly, suitable for irregular patterns and frequent modifications. */ export class FlatSelectionVector implements SelectionVector { + private _limit: number; + /** * @param _selectionVector - * @param _limit In write mode the limit of a Buffer is the limit of how much data you can write into the buffer. + * @param limit In write mode the limit of a Buffer is the limit of how much data you can write into the buffer. * In write mode the limit is equal to the capacity of the Buffer. */ constructor( private _selectionVector: number[], - private _limit?: number, + limit?: number, ) { - if (!this._limit) { - this._limit = this._selectionVector.length; - } + this._limit = limit || this._selectionVector.length; } /** @inheritdoc */ @@ -51,12 +51,12 @@ export class FlatSelectionVector implements SelectionVector { } /** @inheritdoc */ - get capacity() { + get capacity(): number { return this._selectionVector.length; } /** @inheritdoc */ - get limit() { + get limit(): number { return this._limit; } } diff --git a/ts/src/vector/filter/selectionVector.ts b/ts/src/vector/filter/selectionVector.ts index 7956e9a5c..2f74b60d0 100644 --- a/ts/src/vector/filter/selectionVector.ts +++ b/ts/src/vector/filter/selectionVector.ts @@ -6,7 +6,7 @@ export interface SelectionVector { /* Index of the first element that should not be read or written. * It's not the last index that can be accessed, but rather the index that marks the end of * the valid data in the buffer */ - get limit(); + get limit(): number; /* Total size of the buffer */ - get capacity(); + get capacity(): number; } diff --git a/ts/src/vector/fsst-dictionary/stringFsstDictionaryVector.ts b/ts/src/vector/fsst-dictionary/stringFsstDictionaryVector.ts index 7bccd1b0f..f80a3e14f 100644 --- a/ts/src/vector/fsst-dictionary/stringFsstDictionaryVector.ts +++ b/ts/src/vector/fsst-dictionary/stringFsstDictionaryVector.ts @@ -5,8 +5,8 @@ import { decodeString } from "../../decoding/decodingUtils"; export class StringFsstDictionaryVector extends VariableSizeVector { // TODO: extend from StringVector - private symbolLengthBuffer: Uint32Array; - private decodedDictionary: Uint8Array; + private symbolLengthBuffer?: Uint32Array; + private decodedDictionary?: Uint8Array; constructor( name: string, @@ -15,7 +15,7 @@ export class StringFsstDictionaryVector extends VariableSizeVector { it("decodes a point with a non-zero coordinateShift", () => { const x = 50; const y = 80; - const settings: MortonSettings = { numBits: 16, coordinateShift: 100 } as MortonSettings; + const settings: MortonSettings = { numBits: 16, coordinateShift: 100 }; const code = encodeZOrderCurve(x, y, settings.numBits, settings.coordinateShift); const gv = new ConstGeometryVector( @@ -648,3 +649,105 @@ describe("Edge cases", () => { expect(result[0]).toEqual([[new Point(3, 9)]]); }); }); + +describe("missing topology offsets and morton settings throw", () => { + // Build a minimal GeometryVector stand-in so we can omit individual topology + // arrays / morton settings and exercise the invariant guards in convertGeometryVector. + function fakeVector(opts: { + geometryType: number; + containsPolygon?: boolean; + topologyVector?: TopologyVector; + vertexBufferType?: VertexBufferType; + vertexOffsets?: Uint32Array; + vertexBuffer?: Int32Array; + mortonSettings?: MortonSettings; + }): GeometryVector { + return { + numGeometries: 1, + topologyVector: opts.topologyVector ?? {}, + vertexBufferType: opts.vertexBufferType ?? VertexBufferType.VEC_2, + vertexOffsets: opts.vertexOffsets, + vertexBuffer: opts.vertexBuffer ?? new Int32Array([0, 0]), + mortonSettings: opts.mortonSettings, + geometryType: () => opts.geometryType, + containsPolygonGeometry: () => opts.containsPolygon ?? false, + } as unknown as GeometryVector; + } + + it("throws for a Morton-encoded Point without morton settings", () => { + const gv = fakeVector({ + geometryType: GEOMETRY_TYPE.POINT, + vertexBufferType: VertexBufferType.MORTON, + vertexOffsets: new Uint32Array([0]), + vertexBuffer: new Int32Array([0]), + }); + expect(() => convertGeometryVector(gv)).toThrow("Morton-encoded geometry vector is missing morton settings."); + }); + + it("throws for a MultiPoint without geometry offsets", () => { + const gv = fakeVector({ geometryType: GEOMETRY_TYPE.MULTIPOINT }); + expect(() => convertGeometryVector(gv)).toThrow("MultiPoint geometry is missing its geometry offsets."); + }); + + it("throws for a polygon-outline LineString without ring offsets", () => { + const gv = fakeVector({ + geometryType: GEOMETRY_TYPE.LINESTRING, + containsPolygon: true, + topologyVector: { partOffsets: new Uint32Array([0, 2]) }, + }); + expect(() => convertGeometryVector(gv)).toThrow("LineString geometry is missing its ring offsets."); + }); + + it("throws for a LineString without part offsets", () => { + const gv = fakeVector({ geometryType: GEOMETRY_TYPE.LINESTRING }); + expect(() => convertGeometryVector(gv)).toThrow("LineString geometry is missing its part offsets."); + }); + + it("throws for a Polygon without part or ring offsets", () => { + const gv = fakeVector({ + geometryType: GEOMETRY_TYPE.POLYGON, + topologyVector: { ringOffsets: new Uint32Array([0, 4]) }, + }); + expect(() => convertGeometryVector(gv)).toThrow("Polygon geometry is missing its part or ring offsets."); + }); + + it("throws for a MultiLineString without geometry offsets", () => { + const gv = fakeVector({ geometryType: GEOMETRY_TYPE.MULTILINESTRING }); + expect(() => convertGeometryVector(gv)).toThrow("MultiLineString geometry is missing its geometry offsets."); + }); + + it("throws for a polygon-outline MultiLineString without ring offsets", () => { + const gv = fakeVector({ + geometryType: GEOMETRY_TYPE.MULTILINESTRING, + containsPolygon: true, + topologyVector: { geometryOffsets: new Uint32Array([0, 1]) }, + }); + expect(() => convertGeometryVector(gv)).toThrow("MultiLineString geometry is missing its ring offsets."); + }); + + it("throws for a MultiLineString without part offsets", () => { + const gv = fakeVector({ + geometryType: GEOMETRY_TYPE.MULTILINESTRING, + topologyVector: { geometryOffsets: new Uint32Array([0, 1]) }, + }); + expect(() => convertGeometryVector(gv)).toThrow("MultiLineString geometry is missing its part offsets."); + }); + + it("throws for a MultiPolygon without geometry, part, or ring offsets", () => { + const gv = fakeVector({ geometryType: GEOMETRY_TYPE.MULTIPOLYGON }); + expect(() => convertGeometryVector(gv)).toThrow( + "MultiPolygon geometry is missing its geometry, part, or ring offsets.", + ); + }); + + it("throws for a Morton dictionary-encoded LineString without morton settings", () => { + const gv = fakeVector({ + geometryType: GEOMETRY_TYPE.LINESTRING, + vertexBufferType: VertexBufferType.MORTON, + topologyVector: { partOffsets: new Uint32Array([0, 1]) }, + vertexOffsets: new Uint32Array([0]), + vertexBuffer: new Int32Array([0]), + }); + expect(() => convertGeometryVector(gv)).toThrow("Morton-encoded geometry vector is missing morton settings."); + }); +}); diff --git a/ts/src/vector/geometry/geometryVectorConverter.ts b/ts/src/vector/geometry/geometryVectorConverter.ts index 72eb942f1..be7c17fcc 100644 --- a/ts/src/vector/geometry/geometryVectorConverter.ts +++ b/ts/src/vector/geometry/geometryVectorConverter.ts @@ -15,9 +15,9 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat const mortonSettings = geometryVector.mortonSettings; const topologyVector = geometryVector.topologyVector; - const geometryOffsets = topologyVector.geometryOffsets; - const partOffsets = topologyVector.partOffsets; - const ringOffsets = topologyVector.ringOffsets; + // These topology arrays are present only for the geometry types that need them (e.g. multi-geometries + // carry geometryOffsets, polygons carry part/ring offsets); each branch below guards what it indexes. + const { geometryOffsets, partOffsets, ringOffsets } = topologyVector; const vertexOffsets = geometryVector.vertexOffsets; const nonOffset = !vertexOffsets || vertexOffsets.length === 0; @@ -37,6 +37,9 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat } else if (geometryVector.vertexBufferType === VertexBufferType.MORTON) { const offset = vertexOffsets[vertexOffsetsOffset++]; const mortonCode = vertexBuffer[offset]; + if (!mortonSettings) { + throw new Error("Morton-encoded geometry vector is missing morton settings."); + } const vertex = decodeZOrderCurve( mortonCode, mortonSettings.numBits, @@ -57,6 +60,9 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat break; case GEOMETRY_TYPE.MULTIPOINT: { + if (!geometryOffsets) { + throw new Error("MultiPoint geometry is missing its geometry offsets."); + } const numPoints = geometryOffsets[geometryOffsetsCounter] - geometryOffsets[geometryOffsetsCounter - 1]; geometryOffsetsCounter++; @@ -85,9 +91,15 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat { let numVertices: number; if (containsPolygon) { + if (!ringOffsets) { + throw new Error("LineString geometry is missing its ring offsets."); + } numVertices = ringOffsets[ringOffsetsCounter] - ringOffsets[ringOffsetsCounter - 1]; ringOffsetsCounter++; } else { + if (!partOffsets) { + throw new Error("LineString geometry is missing its part offsets."); + } numVertices = partOffsets[partOffsetCounter] - partOffsets[partOffsetCounter - 1]; } partOffsetCounter++; @@ -116,6 +128,9 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat break; case GEOMETRY_TYPE.POLYGON: { + if (!partOffsets || !ringOffsets) { + throw new Error("Polygon geometry is missing its part or ring offsets."); + } const numRings = partOffsets[partOffsetCounter] - partOffsets[partOffsetCounter - 1]; partOffsetCounter++; const rings: CoordinatesArray = new Array(numRings - 1); @@ -164,6 +179,9 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat break; case GEOMETRY_TYPE.MULTILINESTRING: { + if (!geometryOffsets) { + throw new Error("MultiLineString geometry is missing its geometry offsets."); + } const numLineStrings = geometryOffsets[geometryOffsetsCounter] - geometryOffsets[geometryOffsetsCounter - 1]; geometryOffsetsCounter++; @@ -171,9 +189,15 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat for (let j = 0; j < numLineStrings; j++) { let numVertices: number; if (containsPolygon) { + if (!ringOffsets) { + throw new Error("MultiLineString geometry is missing its ring offsets."); + } numVertices = ringOffsets[ringOffsetsCounter] - ringOffsets[ringOffsetsCounter - 1]; ringOffsetsCounter++; } else { + if (!partOffsets) { + throw new Error("MultiLineString geometry is missing its part offsets."); + } numVertices = partOffsets[partOffsetCounter] - partOffsets[partOffsetCounter - 1]; } partOffsetCounter++; @@ -199,6 +223,9 @@ export function convertGeometryVector(geometryVector: GeometryVector): Coordinat break; case GEOMETRY_TYPE.MULTIPOLYGON: { + if (!geometryOffsets || !partOffsets || !ringOffsets) { + throw new Error("MultiPolygon geometry is missing its geometry, part, or ring offsets."); + } const numPolygons = geometryOffsets[geometryOffsetsCounter] - geometryOffsets[geometryOffsetsCounter - 1]; geometryOffsetsCounter++; @@ -265,9 +292,12 @@ function decodeDictionaryEncodedLineStringOrRing( vertexOffset: number, numVertices: number, closeLineString: boolean, - mortonSettings: MortonSettings, + mortonSettings: MortonSettings | undefined, ): Point[] { if (vertexBufferType === VertexBufferType.MORTON) { + if (!mortonSettings) { + throw new Error("Morton-encoded geometry vector is missing morton settings."); + } return decodeMortonDictionaryEncodedLineString( vertexBuffer, vertexOffsets, diff --git a/ts/src/vector/geometry/gpuVector.spec.ts b/ts/src/vector/geometry/gpuVector.spec.ts new file mode 100644 index 000000000..034a96ab4 --- /dev/null +++ b/ts/src/vector/geometry/gpuVector.spec.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from "vitest"; +import { ConstGpuVector } from "./constGpuVector"; +import { GEOMETRY_TYPE } from "./geometryType"; +import type { TopologyVector } from "./topologyVector"; + +function gpuVector(geometryType: number, topologyVector?: TopologyVector): ConstGpuVector { + return new ConstGpuVector( + 1, + geometryType, + new Uint32Array([0]), // triangleOffsets + new Uint32Array([0]), // indexBuffer + new Int32Array([0, 0]), // vertexBuffer + topologyVector, + ); +} + +describe("GpuVector.getGeometries invariants", () => { + it("throws without topology information", () => { + expect(() => gpuVector(GEOMETRY_TYPE.POLYGON).getGeometries()).toThrow( + "Cannot convert GpuVector to coordinates without topology information", + ); + }); + + it("throws when part or ring offsets are missing", () => { + expect(() => gpuVector(GEOMETRY_TYPE.POLYGON, {}).getGeometries()).toThrow( + "Cannot convert GpuVector to coordinates without part and ring offsets", + ); + }); + + it("throws for a MultiPolygon without geometry offsets", () => { + const topology: TopologyVector = { + partOffsets: new Uint32Array([0, 1]), + ringOffsets: new Uint32Array([0, 1]), + }; + expect(() => gpuVector(GEOMETRY_TYPE.MULTIPOLYGON, topology).getGeometries()).toThrow( + "Cannot convert MultiPolygon GpuVector to coordinates without geometry offsets", + ); + }); +}); + +describe("GpuVector accessors", () => { + it("exposes the buffers and topology passed to the constructor", () => { + const triangleOffsets = new Uint32Array([0, 1]); + const indexBuffer = new Uint32Array([2, 3]); + const vertexBuffer = new Int32Array([4, 5]); + const topology: TopologyVector = { partOffsets: new Uint32Array([0, 1]) }; + const vector = new ConstGpuVector( + 1, + GEOMETRY_TYPE.POLYGON, + triangleOffsets, + indexBuffer, + vertexBuffer, + topology, + ); + + expect(vector.triangleOffsets).toBe(triangleOffsets); + expect(vector.indexBuffer).toBe(indexBuffer); + expect(vector.vertexBuffer).toBe(vertexBuffer); + expect(vector.topologyVector).toBe(topology); + }); +}); + +describe("GpuVector iterator", () => { + it("throws because it is not implemented yet", () => { + expect(() => [...gpuVector(GEOMETRY_TYPE.POLYGON)]).toThrow("Iterator on a GpuVector is not implemented yet."); + }); +}); diff --git a/ts/src/vector/geometry/gpuVector.ts b/ts/src/vector/geometry/gpuVector.ts index f331f1109..3ba6e7423 100644 --- a/ts/src/vector/geometry/gpuVector.ts +++ b/ts/src/vector/geometry/gpuVector.ts @@ -43,10 +43,10 @@ export abstract class GpuVector implements Iterable { } const geometries: CoordinatesArray[] = new Array(this.numGeometries); - const topology = this._topologyVector; - const partOffsets = topology.partOffsets; - const ringOffsets = topology.ringOffsets; - const geometryOffsets = topology.geometryOffsets; + const { partOffsets, ringOffsets, geometryOffsets } = this._topologyVector; + if (!partOffsets || !ringOffsets) { + throw new Error("Cannot convert GpuVector to coordinates without part and ring offsets"); + } // Use counters to track position in offset arrays (like Java implementation) let vertexBufferOffset = 0; @@ -89,6 +89,11 @@ export abstract class GpuVector implements Iterable { break; case GEOMETRY_TYPE.MULTIPOLYGON: { + if (!geometryOffsets) { + throw new Error( + "Cannot convert MultiPolygon GpuVector to coordinates without geometry offsets", + ); + } // Get number of polygons in this multipolygon const numPolygons = geometryOffsets[geometryOffsetsCounter] - geometryOffsets[geometryOffsetsCounter - 1]; @@ -139,7 +144,6 @@ export abstract class GpuVector implements Iterable { yield geometries[index++]; }*/ - //throw new Error("Iterator on a GpuVector is not implemented yet."); - return null; + throw new Error("Iterator on a GpuVector is not implemented yet."); } } diff --git a/ts/src/vector/vector.ts b/ts/src/vector/vector.ts index ce4561c65..15b7357a9 100644 --- a/ts/src/vector/vector.ts +++ b/ts/src/vector/vector.ts @@ -1,7 +1,7 @@ import type BitVector from "./flat/bitVector"; export default abstract class Vector { - protected nullabilityBuffer: BitVector | null; + protected nullabilityBuffer: BitVector | null = null; protected _size: number; constructor( diff --git a/ts/tsconfig.json b/ts/tsconfig.json index 578ff79ac..af90124ef 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "target": "ES2020", "outDir": "./dist", + "rootDir": "./src", + "strict": true, "resolveJsonModule": true, "module": "es2022", "declaration": true,