@thi.ng/morton
Version:
Z-order curve / Morton encoding, decoding & range extraction for arbitrary dimensions
290 lines (289 loc) • 7.47 kB
JavaScript
import { MASKS } from "@thi.ng/binary/constants";
import { assert } from "@thi.ng/errors/assert";
const ZERO = BigInt(0);
const ONE = BigInt(1);
const MINUS_ONE = BigInt(-1);
class ZCurve {
/**
* Encodes a single nD point component as partial Z index.
*
* @param x - component value
* @param bits - bits per component
* @param dims - number of dimensions
* @param offset - bit offset (for curr dimension)
* @param out - existing partial Z result
*/
static encodeComponent(x, bits, dims, offset, out = ZERO) {
for (let j = bits; j-- > 0; ) {
if (x >>> j & 1) {
out |= ONE << BigInt(j * dims + offset);
}
}
return out;
}
/**
* Decodes a single nD component from given Z index.
*
* @param z - Z index
* @param bits - bits per component
* @param dims - number of dimensions
* @param offset - bit offset (for curr dimension)
*/
static decodeComponent(z, bits, dims, offset) {
let res = 0;
for (let j = bits; j-- > 0; ) {
if (z >> BigInt(j * dims + offset) & ONE) {
res |= 1 << j;
}
}
return res >>> 0;
}
dim;
bits;
order;
masks;
wipeMasks;
/**
* @param dim - dimensions
* @param bits - number of bits per component
* @param order - component ordering
*/
constructor(dim, bits, order) {
assert(dim >= 2, `unsupported dimensions`);
assert(bits >= 1 && bits <= 32, `unsupported bits per component`);
this.dim = dim;
this.bits = bits;
if (!order) {
order = [];
for (let i = 0; i < dim; i++) order[i] = i;
}
this.order = order;
this.initMasks();
}
/**
* Encodes given nD point as Z index.
*
* @param p - point to encode
*/
encode(p) {
let res = ZERO;
const { dim, bits, order } = this;
for (let i = dim; i-- > 0; ) {
res = ZCurve.encodeComponent(p[i], bits, dim, order[i], res);
}
return res;
}
/**
* Decodes given Z index into point coordinates.
*
* @param z - Z index
* @param out - optional result array
*/
decode(z, out = []) {
const { dim, bits, order } = this;
for (let i = dim; i-- > 0; ) {
out[i] = ZCurve.decodeComponent(z, bits, dim, order[i]);
}
return out;
}
/**
* Decomposes given Z index into individual bit patterns, one per
* dimension.
*
* @remarks
*
*
* @param z -
* @param out -
*/
split(z, out = []) {
for (let i = this.dim; i-- > 0; ) {
out[i] = z & this.masks[i];
}
return out;
}
merge(zparts) {
let res = ZERO;
for (let i = zparts.length; i-- > 0; ) {
res |= zparts[i];
}
return res;
}
/**
* Yields iterator of Z-curve indices in given nD bounding box.
*
* @remarks
* Uses {@link ZCurve.pointInBox} and {@link ZCurve.bigMin} to efficiently
* skip Z index sub-ranges outside the box.
*
* References:
* - https://en.wikipedia.org/wiki/Z-order_curve#Use_with_one-dimensional_data_structures_for_range_searching
* - https://aws.amazon.com/blogs/database/z-order-indexing-for-multifaceted-queries-in-amazon-dynamodb-part-1/
* - https://aws.amazon.com/blogs/database/z-order-indexing-for-multifaceted-queries-in-amazon-dynamodb-part-2/
*
* @param rmin - bbox min point
* @param rmax - bbox max point
*/
*range(rmin, rmax) {
const zmin = this.encode(rmin);
const zmax = this.encode(rmax);
const p = new Array(this.dim);
let xd = zmin;
while (xd !== MINUS_ONE) {
this.decode(xd, p);
if (this.pointInBox(p, rmin, rmax)) {
yield xd;
xd++;
} else {
xd = this.bigMin(xd, zmin, zmax);
}
}
}
/**
* Computes the next valid Z index in bbox defined by `zmin` / `zmax` and
* greater than `zcurr`. Returns -1 if no further indices are in the box.
*
* @remarks
* Partially based on:
* https://github.com/statgen/LDServer/blob/develop/core/src/Morton.cpp#L38
*
* @param zcurr -
* @param zmin -
* @param zmax -
*/
bigMin(zcurr, zmin, zmax) {
const dim = this.dim;
let bigmin = MINUS_ONE;
let bitPos = dim * this.bits - 1;
let mask = ONE << BigInt(bitPos);
do {
const zminBit = zmin & mask;
const zmaxBit = zmax & mask;
const currBit = zcurr & mask;
const bitMask = 1 << (bitPos / dim | 0);
if (!currBit) {
if (!zminBit && zmaxBit) {
bigmin = this.loadBits(bitMask, bitPos, zmin);
zmax = this.loadBits(bitMask - 1, bitPos, zmax);
} else if (zminBit) {
if (zmaxBit) {
return zmin;
} else {
throw new Error("illegal BIGMIN state");
}
}
} else {
if (!zminBit) {
if (zmaxBit) {
zmin = this.loadBits(bitMask, bitPos, zmin);
} else {
return bigmin;
}
} else if (!zmaxBit) {
throw new Error("illegal BIGMIN state");
}
}
bitPos--;
mask >>= ONE;
} while (mask);
return bigmin;
}
pointInBox(p, rmin, rmax) {
for (let i = this.dim; i-- > 0; ) {
const x = p[i];
if (x < rmin[i] || x > rmax[i]) return false;
}
return true;
}
initMasks() {
const { bits, dim, order } = this;
this.masks = [];
for (let i = dim; i-- > 0; ) {
this.masks[i] = ZCurve.encodeComponent(
MASKS[bits],
bits,
dim,
order[i]
);
}
this.wipeMasks = [];
const fullMask = (ONE << BigInt(dim * bits)) - ONE;
for (let i = dim * bits; i-- > 0; ) {
this.wipeMasks[i] = ZCurve.encodeComponent(
MASKS[bits] >>> bits - ((i / dim | 0) + 1),
bits,
dim,
i % dim
) ^ fullMask;
}
}
loadBits(mask, bitPos, z) {
const dim = this.dim;
return z & this.wipeMasks[bitPos] | ZCurve.encodeComponent(mask, this.bits, dim, bitPos % dim);
}
}
class ZCurve2 extends ZCurve {
constructor(bits, order) {
super(2, bits, order);
}
encode(p) {
const { dim, bits, order } = this;
return ZCurve.encodeComponent(
p[1],
bits,
dim,
order[1],
ZCurve.encodeComponent(p[0], bits, dim, order[0])
);
}
decode(z, out = []) {
const { dim, bits, order } = this;
out[0] = ZCurve.decodeComponent(z, bits, dim, order[0]);
out[1] = ZCurve.decodeComponent(z, bits, dim, order[1]);
return out;
}
pointInBox(p, rmin, rmax) {
const x = p[0];
const y = p[1];
return x >= rmin[0] && x <= rmax[0] && y >= rmin[1] && y <= rmax[1];
}
}
class ZCurve3 extends ZCurve {
constructor(bits, order) {
super(3, bits, order);
}
encode(p) {
const { dim, bits, order } = this;
return ZCurve.encodeComponent(
p[2],
bits,
dim,
order[2],
ZCurve.encodeComponent(
p[1],
bits,
dim,
order[1],
ZCurve.encodeComponent(p[0], bits, dim, order[0])
)
);
}
decode(z, out = []) {
const { dim, bits, order } = this;
out[0] = ZCurve.decodeComponent(z, bits, dim, order[0]);
out[1] = ZCurve.decodeComponent(z, bits, dim, order[1]);
out[2] = ZCurve.decodeComponent(z, bits, dim, order[2]);
return out;
}
pointInBox(p, rmin, rmax) {
const x = p[0];
const y = p[1];
const z = p[2];
return x >= rmin[0] && x <= rmax[0] && y >= rmin[1] && y <= rmax[1] && z >= rmin[2] && z <= rmax[2];
}
}
export {
ZCurve,
ZCurve2,
ZCurve3
};