Skip to content

Instantly share code, notes, and snippets.

@spoike
Created June 18, 2020 17:19
Show Gist options
  • Save spoike/38a08352e86b0ed66a6201119b522545 to your computer and use it in GitHub Desktop.
Save spoike/38a08352e86b0ed66a6201119b522545 to your computer and use it in GitHub Desktop.
Boop Scripts: Convert between WKT and WKB
POINT(2.0 4.0)
010100000000000000000000400000000000001040
LINESTRING(2.0 4.0, 1.0 2.0)
01020000000200000000000000000000400000000000001040000000000000f03f0000000000000040
POLYGON ((30 10, 40 40, 20 40))
010300000001000000030000000000000000003e4000000000000024400000000000004440000000000000444000000000000034400000000000004440
POLYGON ((30 10, 40 40), (20 40, 10 30))
010300000002000000020000000000000000003e40000000000000244000000000000044400000000000004440020000000000000000003440000000000000444000000000000024400000000000003e40
MULTIPOINT (10 40, 40 30)
010400000002000000010100000000000000000024400000000000004440010100000000000000000044400000000000003e40
MULTIPOINT ((10 40), (40 30))
010400000002000000010100000000000000000024400000000000004440010100000000000000000044400000000000003e40
MULTILINESTRING ((10 10, 20 20), (40 40, 30 30))
0105000000020000000102000000020000000000000000002440000000000000244000000000000034400000000000003440010200000002000000000000000000444000000000000044400000000000003e400000000000003e40
MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))
010600000002000000010300000001000000040000000000000000003e40000000000000344000000000008046400000000000004440000000000000244000000000000044400000000000003e400000000000003440010300000001000000050000000000000000002e4000000000000014400000000000004440000000000000244000000000000024400000000000003440000000000000144000000000000024400000000000002e400000000000001440
/**
{
"api":1,
"name":"Well-Known Binary to Text",
"description":"wkb2kwt - converts your hex encoded WKB (any endian) to WKB",
"author":"Mikael Brassman (Twitter: @spoike)",
"icon":"metamorphose",
"tags":"wkb,convert,wkt,binary,hex,wkb2wkt"
}
**/
function main(input) {
try {
input.text = input.text.replace(/0[01]0[1-6][0-9a-f]+/g, convertHex);
} catch (err) {
input.postError(err);
return;
}
}
function convertHex(hexStr) {
let pos = 0;
let output = "";
while (pos < hexStr.length) {
const littleEndian = getIsLittleEndian(hexStr, pos);
pos += 2;
const geoType = getUint32(hexStr, pos, littleEndian);
pos += 8;
switch (geoType) {
case 1: {
// POINT
const point = getPoint(hexStr, pos, littleEndian);
pos += 32;
output += `POINT (${point})`;
break;
}
case 2: {
// LINESTRING
const length = getUint32(hexStr, pos, littleEndian);
pos += 8;
const points = [];
for (let i = 0; i < length; i++) {
points.push(getPoint(hexStr, pos, littleEndian));
pos += 32;
}
output += `LINESTRING (${points.join(", ")})`;
break;
}
case 3: {
// POLYGON
const count = getUint32(hexStr, pos, littleEndian);
pos += 8;
const rings = [];
for (let i = 0; i < count; i++) {
const length = getUint32(hexStr, pos, littleEndian);
pos += 8;
const points = [];
for (let j = 0; j < length; j++) {
points.push(getPoint(hexStr, pos, littleEndian));
pos += 32;
}
rings.push(points.join(", "));
}
output += `POLYGON (${rings.map(wrapParens).join(", ")})`;
break;
}
case 4: {
// MULTIPOINT
const points = [];
const count = getUint32(hexStr, pos, littleEndian);
pos += 8;
for (let i = 0; i < count; i++) {
const innerLE = getIsLittleEndian(hexStr, pos);
pos += 2 + 8;
points.push(getPoint(hexStr, pos, innerLE));
pos += 32;
}
output += `MULTIPOINT (${points.join(", ")})`;
break;
}
case 5: {
// MULTILINESTRING
const lineStrings = [];
const count = getUint32(hexStr, pos, littleEndian);
pos += 8;
for (let i = 0; i < count; i++) {
const innerLE = getIsLittleEndian(hexStr, pos);
pos += 2 + 8;
const points = [];
const length = getUint32(hexStr, pos, littleEndian);
pos += 8;
for (let j = 0; j < length; j++) {
points.push(getPoint(hexStr, pos, innerLE));
pos += 32;
}
lineStrings.push(points.join(", "));
}
output += `MULTILINESTRING (${lineStrings.map(wrapParens).join(", ")})`;
break;
}
case 6: {
// MULTIPOLYGON
const polys = [];
const polyCount = getUint32(hexStr, pos, littleEndian);
pos += 8;
for (let i = 0; i < polyCount; i++) {
const innerLE = getIsLittleEndian(hexStr, pos);
pos += 2 + 8;
const rings = [];
const ringCount = getUint32(hexStr, pos, innerLE);
pos += 8;
for (let j = 0; j < ringCount; j++) {
const points = [];
const pointCount = getUint32(hexStr, pos, innerLE);
pos += 8;
for (let k = 0; k < pointCount; k++) {
points.push(getPoint(hexStr, pos, innerLE));
pos += 32;
}
rings.push(points.join(", "));
}
polys.push(rings.map(wrapParens).join(", "));
}
output += `MULTIPOLYGON (${polys.map(wrapParens).join(", ")})`;
break;
}
default:
throw geoType + " is not supported";
}
}
return output;
}
function wrapParens(el) {
return `(${el})`;
}
function getIsLittleEndian(str, pos) {
const byteString = str.substr(pos, 2);
if (byteString === "00") {
return false;
} else if (byteString === "01") {
return true;
}
throw byteString + " is unknown byte order";
}
function getPoint(str, pos, littleEndian) {
const numbers = [];
numbers.push(getDouble(str, pos, littleEndian));
numbers.push(getDouble(str, pos + 16, littleEndian));
return numbers.join(" ");
}
function getUint32(str, pos, littleEndian) {
const view = new DataView(new ArrayBuffer(4));
let data = str.substr(pos, 8).match(/../g);
for (let i = 0; i < data.length; i++) {
view.setUint8(i, parseInt(data[i], 16));
}
return view.getUint32(0, littleEndian);
}
function getDouble(str, pos, littleEndian) {
const view = new DataView(new ArrayBuffer(8));
let data = str.substr(pos, 16).match(/../g);
for (let i = 0; i < data.length; i++) {
view.setUint8(i, parseInt(data[i], 16));
}
return view.getFloat64(0, littleEndian);
}
/**
{
"api":1,
"name":"Well-Known Text to Binary",
"description":"wkt2wkb - converts your WKT to little endian WKB (hex encoded)",
"author":"Mikael Brassman (Twitter: @spoike)",
"icon":"metamorphose",
"tags":"wkb,convert,wkt,binary,little endian,hex,wkt2wkb"
}
**/
const re = /(?:(?:MULTI)?POINT|(?:MULTI)?LINESTRING|(?:MULTI)?POLYGON)\s*\([()0-9\s,.]+\)/g;
function main(input) {
try {
input.text = input.text.replace(re, convert);
} catch (err) {
input.postError(err);
return;
}
}
function convert(text) {
const littleEndian = true;
let tokens = [];
for (let i = 0; i < text.length; i++) {
tokenize(tokens, text[i]);
}
tokens = tokens.filter(Boolean);
let output = "";
while (tokens.length > 0) {
const token = tokens.shift();
if (tokens.shift() !== "(") {
throw token + "is missing (";
}
switch (token) {
case "POINT":
output += handlePoint(tokens, littleEndian);
break;
case "LINESTRING":
output += handleLineString(tokens, littleEndian);
break;
case "POLYGON":
output += handlePolygon(
getParensSlice(tokens, "POLYGON", false),
littleEndian
);
break;
case "MULTIPOINT":
output += handleMultipoint(tokens, littleEndian);
break;
case "MULTILINESTRING":
output += handleMultilinestring(tokens, littleEndian);
break;
case "MULTIPOLYGON":
output += handleMultipolygon(tokens, littleEndian);
break;
default:
throw "Unrecognized token " + token;
}
if (tokens.shift() !== ")") {
throw token + " is missing )";
}
}
return output;
}
function tokenize(memo, char) {
if (memo.length === 0) {
memo.push(char);
return;
}
if (/[A-Z0-9\.\-]/.test(char)) {
memo[memo.length - 1] = memo[memo.length - 1] + char;
} else if (/[()]/.test(char)) {
memo.push(char);
memo.push("");
} else {
memo.push("");
}
}
function handlePoint(arr, littleEndian) {
let out = toByteOrder(littleEndian) + toUint32(1, littleEndian);
out += handleDouble(arr.shift(), littleEndian);
out += handleDouble(arr.shift(), littleEndian);
return out;
}
function handleLineString(arr, littleEndian) {
let out = toByteOrder(littleEndian) + toUint32(2, littleEndian);
const slice = getParensSlice(arr, "LINESTRING", true);
const pairs = Math.floor(slice.length / 2);
out += toUint32(pairs, littleEndian);
for (const token of slice) {
out += handleDouble(token, littleEndian);
}
return out;
}
function handlePolygon(rings, littleEndian) {
let out = toByteOrder(littleEndian) + toUint32(3, littleEndian);
out += toUint32(rings.length, littleEndian);
for (let ring of rings) {
out += handleRing(ring, littleEndian, "POLYGON");
}
return out;
}
function handleMultipoint(arr, littleEndian) {
let out = toByteOrder(littleEndian) + toUint32(4, littleEndian);
const slice = getParensSlice(arr, "MULTIPOINT", true);
const pairs = slice.length / 2;
out += toUint32(pairs, littleEndian);
for (let i = 0; i < slice.length; i = i + 2) {
out += toByteOrder(littleEndian);
out += toUint32(1, littleEndian);
out += toDouble(slice[i], littleEndian);
out += toDouble(slice[i + 1], littleEndian);
}
return out;
}
function handleMultilinestring(arr, littleEndian) {
let out = toByteOrder(littleEndian) + toUint32(5, littleEndian);
const slices = getParensSlice(arr, "MULTILINESTRING", false);
out += toUint32(slices.length, littleEndian);
for (let slice of slices) {
const pairs = Math.floor(slice.length / 2);
out +=
toByteOrder(littleEndian) +
toUint32(2, littleEndian) +
toUint32(pairs, littleEndian);
for (let token of slice) {
out += toDouble(token, littleEndian);
}
}
return out;
}
function handleMultipolygon(arr, littleEndian) {
let out = toByteOrder(littleEndian) + toUint32(6, littleEndian);
const polygons = getParensSlice(arr, "MULTIPOLYGON", false);
out += toUint32(polygons.length, littleEndian);
for (let polygon of polygons) {
out += handlePolygon(polygon, littleEndian);
}
return out;
}
function handleRing(tokens, littleEndian) {
let out = "";
const pairs = Math.floor(tokens.length / 2);
out += toUint32(pairs, littleEndian);
for (let token of tokens) {
out += handleDouble(token, littleEndian);
}
return out;
}
function getParensSlice(arr, type, flatten) {
let slices = [];
while (arr[0] === "(") {
arr.shift(); // remove (
const innerSlice = getParensSlice(arr, type, flatten);
slices.push(flatten ? innerSlice.flat() : innerSlice);
arr.shift(); // remove )
}
let seek = arr.findIndex((token) => /^\)$/.test(token));
if (seek === -1) {
throw type + " missing matching )";
}
if (seek > 0) {
slices = slices.concat(arr.splice(0, seek));
}
return flatten ? slices.flat() : slices;
}
function handleDouble(token, littleEndian) {
const number = parseFloat(token);
if (isNaN(number)) {
throw token + " is NaN";
}
return toDouble(number, littleEndian);
}
function toByteOrder(littleEndian) {
return littleEndian ? "01" : "00";
}
function toUint32(number, littleEndian) {
const view = new DataView(new ArrayBuffer(4));
view.setUint32(0, number, littleEndian);
return asHex(view, 4);
}
function toDouble(number, littleEndian) {
const view = new DataView(new ArrayBuffer(8));
view.setFloat64(0, number, littleEndian);
return asHex(view, 8);
}
function getHex(i) {
return ("00" + i.toString(16)).slice(-2);
}
function asHex(view, length) {
return Array.apply(null, { length })
.map((_, i) => getHex(view.getUint8(i)))
.join("");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment