Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WKB parse tests #37

Open
wants to merge 1 commit into
base: kyle/wkb
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"apache-arrow": ">=15"
},
"devDependencies": {
"@loaders.gl/core": "^4.3.3",
"@loaders.gl/schema": "^4.3.3",
"@loaders.gl/wkt": "^4.3.3",
"@rollup/plugin-node-resolve": "^15.2.3",
Expand Down
135 changes: 135 additions & 0 deletions tests/io/parse-test-cases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import type {Geometry, BinaryGeometry} from '@loaders.gl/schema';

export interface TestCase {
/** Geometry in WKT */
wkt: string;

/** Geometry in WKB, stored in base64 */
wkb: string;

/** Geometry in EWKB, stored in base64 */
ewkb: string;

/** Geometry in WKB XDR (big endian), stored in base64 */
wkbXdr: string;

/** Geometry in EWKB XDR (big endian), stored in base64 */
ewkbXdr: string;

/** Geometry in Tiny WKB, stored in base64 */
twkb: string;

/** GeoJSON-formatted geometry */
geoJSON: Geometry;

/** Geometry in EWKB, stored in base64, without coordinate system identifier */
ewkbNoSrid: string;

/** Geometry in EWKB (big endian), stored in base64, without coordinate system identifier */
ewkbXdrNoSrid: string;

/** Equivalent data in loaders.gl binary format (but stored as "normal" JS arrays) */
binary: BinaryGeometry;
}

interface ParsedTestCase {
/** Geometry in WKT */
wkt: string;

/** Geometry in WKB, stored in hex */
wkbHex: string;

/** Geometry in WKB */
wkb: ArrayBuffer;

/** Geometry in EWKB */
ewkb: ArrayBuffer;

/** Geometry in WKB XDR (big endian) */
wkbXdr: ArrayBuffer;

/** Geometry in WKB, stored in hex */
wkbHexXdr: string;

/** Geometry in EWKB XDR (big endian) */
ewkbXdr: ArrayBuffer;

/** Geometry in Tiny WKB */
twkb: ArrayBuffer;

/** GeoJSON-formatted geometry */
geoJSON: Geometry;

/** Geometry in EWKB, without coordinate system identifier */
ewkbNoSrid: ArrayBuffer;

/** Geometry in EWKB (big endian), without coordinate system identifier */
ewkbXdrNoSrid: ArrayBuffer;

/** Equivalent data in loaders.gl binary format*/
binary: BinaryGeometry;
}

/**
* Convert a hex string to an ArrayBuffer.
*
* @param hexString - hex representation of bytes
* @return Parsed bytes
*/
export default function hexStringToArrayBuffer(hexString: string): ArrayBuffer {
// remove the leading 0x
hexString = hexString.replace(/^0x/, '');

// split the string into pairs of octets
const pairs = hexString.match(/[\dA-F]{2}/gi);

// convert the octets to integers
const integers = pairs ? pairs.map((s) => parseInt(s, 16)) : [];
return new Uint8Array(integers).buffer;
}

export function parseTestCases(
testCases: Record<string, TestCase>
): Record<string, ParsedTestCase> {
const parsedTestCases: Record<string, ParsedTestCase> = {};

for (const [key, value] of Object.entries(testCases)) {
const {wkt, wkb, ewkb, wkbXdr, ewkbXdr, twkb, geoJSON, ewkbNoSrid, ewkbXdrNoSrid, binary} =
value;

// Convert binary arrays into typedArray
if (binary && binary.positions) {
binary.positions.value = new Float64Array(binary.positions.value);
}
if (binary && binary.type === 'LineString') {
binary.pathIndices.value = new Uint32Array(binary.pathIndices.value);
}
if (binary && binary.type === 'Polygon') {
binary.polygonIndices.value = new Uint32Array(binary.polygonIndices.value);
binary.primitivePolygonIndices.value = new Uint32Array(binary.primitivePolygonIndices.value);
}

const parsedTestCase: ParsedTestCase = {
wkt,
geoJSON,
wkbHex: wkb,
wkbHexXdr: wkbXdr,
wkb: hexStringToArrayBuffer(wkb),
ewkb: hexStringToArrayBuffer(ewkb),
twkb: hexStringToArrayBuffer(twkb),
wkbXdr: hexStringToArrayBuffer(wkbXdr),
ewkbXdr: hexStringToArrayBuffer(ewkbXdr),
ewkbNoSrid: hexStringToArrayBuffer(ewkbNoSrid),
ewkbXdrNoSrid: hexStringToArrayBuffer(ewkbXdrNoSrid),
binary
};

parsedTestCases[key] = parsedTestCase;
}

return parsedTestCases;
}
89 changes: 89 additions & 0 deletions tests/io/wkb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { makeData, Binary } from "apache-arrow";
import { describe, expect, it } from "vitest";
import { parseTestCases } from "./parse-test-cases";
import { fetchFile } from "@loaders.gl/core";
import { parseWkb, WKBType } from "../../src/io/wkb";
import { isLineStringData, isPolygonData, isPointData } from "../../src/data";

//Test cases from Loaders.gl https://github.com/visgl/loaders.gl
const WKB_2D_TEST_CASES =
"https://raw.githubusercontent.com/visgl/loaders.gl/d74df445180408fd772567ca6a6bbb8d42aa50be/modules/gis/test/data/wkt/wkb-testdata2d.json";

describe("parse point wkb", (t) => {
it("should parse point wkb as valid geoarrow", async () => {
const response = await fetchFile(WKB_2D_TEST_CASES);
const testCases = parseTestCases(await response.json());
const linePointTypes = ["point"];
const testCasesPoint = Object.fromEntries(
Object.entries(testCases).filter(([key, val]) =>
linePointTypes.includes(key),
),
);

for (const [title, testCase] of Object.entries(testCasesPoint)) {
const data = new Uint8Array(testCase.wkb);
const offsets = new Uint32Array([0, data.length]);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These offsets are incorrect. I'm having trouble getting the binary length of each polygon

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "binary length of each polygon"? The number of bytes after being serialized to WKB?

const polygonData = makeData({
type: new Binary(),
data: data,
valueOffsets: offsets,
});
const parsedPointData = parseWkb(polygonData, WKBType.Point, 2);
expect(isPointData(parsedPointData)).toBe(true);
}
});
});

describe("parse linestring wkb", (t) => {
it("should parse linestring wkb as valid geoarrow", async () => {
const response = await fetchFile(WKB_2D_TEST_CASES);
const testCases = parseTestCases(await response.json());
const lineStringTypes = ["lineString"];
const testCasesLineString = Object.fromEntries(
Object.entries(testCases).filter(([key, val]) =>
lineStringTypes.includes(key),
),
);

for (const [title, testCase] of Object.entries(testCasesLineString)) {
const data = new Uint8Array(testCase.wkb);
const offsets = new Uint32Array([0, data.length]);
const polygonData = makeData({
type: new Binary(),
data: data,
valueOffsets: offsets,
});
const parsedLineStringData = parseWkb(polygonData, WKBType.LineString, 2);
expect(isLineStringData(parsedLineStringData)).toBe(true);
}
});
});

describe("parse polygon wkb", (t) => {
it("should parse polygon wkb as valid geoarrow", async () => {
const response = await fetchFile(WKB_2D_TEST_CASES);
const testCases = parseTestCases(await response.json());
const polygonTypes = [
"polygon",
"polygonWithOneInteriorRing",
"polygonWithTwoInteriorRings",
];
const testCasesPolygon = Object.fromEntries(
Object.entries(testCases).filter(([key, val]) =>
polygonTypes.includes(key),
),
);

for (const [title, testCase] of Object.entries(testCasesPolygon)) {
const data = new Uint8Array(testCase.wkb);
const offsets = new Uint32Array([0, data.length]);
const polygonData = makeData({
type: new Binary(),
data: data,
valueOffsets: offsets,
});
const parsedPolygonData = parseWkb(polygonData, WKBType.Polygon, 2);
expect(isPolygonData(parsedPolygonData)).toBe(true);
}
});
});
Loading