@loaders.gl/wkt
Version:
Loader and Writer for the WKT (Well Known Text) Format
254 lines (250 loc) • 9.59 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
// Forked from https://github.com/cschwarz/wkx under MIT license, Copyright (c) 2013 Christian Schwarz
import { BinaryReader } from "./utils/binary-reader.js";
import { WKBGeometryType } from "./parse-wkb-header.js";
/**
* Check if an array buffer might be a TWKB array buffer
* @param arrayBuffer The array buffer to check
* @returns false if this is definitely not a TWKB array buffer, true if it might be a TWKB array buffer
*/
export function isTWKB(arrayBuffer) {
const binaryReader = new BinaryReader(arrayBuffer);
const type = binaryReader.readUInt8();
const geometryType = type & 0x0f;
// Only geometry types 1 to 7 (point to geometry collection are currently defined)
if (geometryType < 1 || geometryType > 7) {
return false;
}
return true;
}
export function parseTWKBGeometry(arrayBuffer) {
const binaryReader = new BinaryReader(arrayBuffer);
const context = parseTWKBHeader(binaryReader);
if (context.hasSizeAttribute) {
binaryReader.readVarInt();
}
if (context.hasBoundingBox) {
let dimensions = 2;
if (context.hasZ) {
dimensions++;
}
if (context.hasM) {
dimensions++;
}
// TODO why are we throwing away these datums?
for (let i = 0; i < dimensions; i++) {
binaryReader.readVarInt();
binaryReader.readVarInt();
}
}
return parseGeometry(binaryReader, context, context.geometryType);
}
function parseTWKBHeader(binaryReader) {
const type = binaryReader.readUInt8();
const metadataHeader = binaryReader.readUInt8();
const geometryType = type & 0x0f;
const precision = zigZagDecode(type >> 4);
const hasExtendedPrecision = Boolean((metadataHeader >> 3) & 1);
let hasZ = false;
let hasM = false;
let zPrecision = 0;
let zPrecisionFactor = 1;
let mPrecision = 0;
let mPrecisionFactor = 1;
if (hasExtendedPrecision) {
const extendedPrecision = binaryReader.readUInt8();
hasZ = (extendedPrecision & 0x01) === 0x01;
hasM = (extendedPrecision & 0x02) === 0x02;
zPrecision = zigZagDecode((extendedPrecision & 0x1c) >> 2);
zPrecisionFactor = Math.pow(10, zPrecision);
mPrecision = zigZagDecode((extendedPrecision & 0xe0) >> 5);
mPrecisionFactor = Math.pow(10, mPrecision);
}
return {
geometryType,
precision,
precisionFactor: Math.pow(10, precision),
hasBoundingBox: Boolean((metadataHeader >> 0) & 1),
hasSizeAttribute: Boolean((metadataHeader >> 1) & 1),
hasIdList: Boolean((metadataHeader >> 2) & 1),
hasExtendedPrecision,
isEmpty: Boolean((metadataHeader >> 4) & 1),
hasZ,
hasM,
zPrecision,
zPrecisionFactor,
mPrecision,
mPrecisionFactor
};
}
function parseGeometry(binaryReader, context, geometryType) {
switch (geometryType) {
case WKBGeometryType.Point:
return parsePoint(binaryReader, context);
case WKBGeometryType.LineString:
return parseLineString(binaryReader, context);
case WKBGeometryType.Polygon:
return parsePolygon(binaryReader, context);
case WKBGeometryType.MultiPoint:
return parseMultiPoint(binaryReader, context);
case WKBGeometryType.MultiLineString:
return parseMultiLineString(binaryReader, context);
case WKBGeometryType.MultiPolygon:
return parseMultiPolygon(binaryReader, context);
case WKBGeometryType.GeometryCollection:
return parseGeometryCollection(binaryReader, context);
default:
throw new Error(`GeometryType ${geometryType} not supported`);
}
}
// GEOMETRIES
function parsePoint(reader, context) {
if (context.isEmpty) {
return { type: 'Point', coordinates: [] };
}
return { type: 'Point', coordinates: readFirstPoint(reader, context) };
}
function parseLineString(reader, context) {
if (context.isEmpty) {
return { type: 'LineString', coordinates: [] };
}
const pointCount = reader.readVarInt();
const previousPoint = makePreviousPoint(context);
const points = [];
for (let i = 0; i < pointCount; i++) {
points.push(parseNextPoint(reader, context, previousPoint));
}
return { type: 'LineString', coordinates: points };
}
function parsePolygon(reader, context) {
if (context.isEmpty) {
return { type: 'Polygon', coordinates: [] };
}
const ringCount = reader.readVarInt();
const previousPoint = makePreviousPoint(context);
const exteriorRingLength = reader.readVarInt();
const exteriorRing = [];
for (let i = 0; i < exteriorRingLength; i++) {
exteriorRing.push(parseNextPoint(reader, context, previousPoint));
}
const polygon = [exteriorRing];
for (let i = 1; i < ringCount; i++) {
const interiorRingCount = reader.readVarInt();
const interiorRing = [];
for (let j = 0; j < interiorRingCount; j++) {
interiorRing.push(parseNextPoint(reader, context, previousPoint));
}
polygon.push(interiorRing);
}
return { type: 'Polygon', coordinates: polygon };
}
function parseMultiPoint(reader, context) {
if (context.isEmpty) {
return { type: 'MultiPoint', coordinates: [] };
}
const previousPoint = makePreviousPoint(context);
const pointCount = reader.readVarInt();
const coordinates = [];
for (let i = 0; i < pointCount; i++) {
coordinates.push(parseNextPoint(reader, context, previousPoint));
}
return { type: 'MultiPoint', coordinates };
}
function parseMultiLineString(reader, context) {
if (context.isEmpty) {
return { type: 'MultiLineString', coordinates: [] };
}
const previousPoint = makePreviousPoint(context);
const lineStringCount = reader.readVarInt();
const coordinates = [];
for (let i = 0; i < lineStringCount; i++) {
const pointCount = reader.readVarInt();
const lineString = [];
for (let j = 0; j < pointCount; j++) {
lineString.push(parseNextPoint(reader, context, previousPoint));
}
coordinates.push(lineString);
}
return { type: 'MultiLineString', coordinates };
}
function parseMultiPolygon(reader, context) {
if (context.isEmpty) {
return { type: 'MultiPolygon', coordinates: [] };
}
const previousPoint = makePreviousPoint(context);
const polygonCount = reader.readVarInt();
const polygons = [];
for (let i = 0; i < polygonCount; i++) {
const ringCount = reader.readVarInt();
const exteriorPointCount = reader.readVarInt();
const exteriorRing = [];
for (let j = 0; j < exteriorPointCount; j++) {
exteriorRing.push(parseNextPoint(reader, context, previousPoint));
}
const polygon = exteriorRing ? [exteriorRing] : [];
for (let j = 1; j < ringCount; j++) {
const interiorRing = [];
const interiorRingLength = reader.readVarInt();
for (let k = 0; k < interiorRingLength; k++) {
interiorRing.push(parseNextPoint(reader, context, previousPoint));
}
polygon.push(interiorRing);
}
polygons.push(polygon);
}
return { type: 'MultiPolygon', coordinates: polygons };
}
/** Geometry collection not yet supported */
function parseGeometryCollection(reader, context) {
return { type: 'GeometryCollection', geometries: [] };
/**
if (context.isEmpty) {
return {type: 'GeometryCollection', geometries: []};
}
const geometryCount = reader.readVarInt();
const geometries: Geometry[] = new Array(geometryCount);
for (let i = 0; i < geometryCount; i++) {
const geometry = parseGeometry(reader, context, geometryType);
geometries.push(geometry);
}
return {type: 'GeometryCollection', geometries: []};
*/
}
// HELPERS
/**
* Maps negative values to positive values while going back and
forth (0 = 0, -1 = 1, 1 = 2, -2 = 3, 2 = 4, -3 = 5, 3 = 6 ...)
*/
function zigZagDecode(value) {
return (value >> 1) ^ -(value & 1);
}
function makePointCoordinates(x, y, z, m) {
return (z !== undefined ? (m !== undefined ? [x, y, z, m] : [x, y, z]) : [x, y]);
}
function makePreviousPoint(context) {
return makePointCoordinates(0, 0, context.hasZ ? 0 : undefined, context.hasM ? 0 : undefined);
}
function readFirstPoint(reader, context) {
const x = zigZagDecode(reader.readVarInt()) / context.precisionFactor;
const y = zigZagDecode(reader.readVarInt()) / context.precisionFactor;
const z = context.hasZ ? zigZagDecode(reader.readVarInt()) / context.zPrecisionFactor : undefined;
const m = context.hasM ? zigZagDecode(reader.readVarInt()) / context.mPrecisionFactor : undefined;
return makePointCoordinates(x, y, z, m);
}
/**
* Modifies previousPoint
*/
function parseNextPoint(reader, context, previousPoint) {
previousPoint[0] += zigZagDecode(reader.readVarInt()) / context.precisionFactor;
previousPoint[1] += zigZagDecode(reader.readVarInt()) / context.precisionFactor;
if (context.hasZ) {
previousPoint[2] += zigZagDecode(reader.readVarInt()) / context.zPrecisionFactor;
}
if (context.hasM) {
previousPoint[3] += zigZagDecode(reader.readVarInt()) / context.mPrecisionFactor;
}
// Copy the point
return previousPoint.slice();
}