@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
424 lines (423 loc) • 14.7 kB
JavaScript
import FlatQueue from "../flatqueue/index.js";
const ARRAY_TYPES = [
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
];
const VERSION = 3;
export default class Flatbush {
numItems;
nodeSize;
byteOffset;
ArrayType;
IndexArrayType;
data;
minX;
minY;
maxX;
maxY;
_levelBounds;
_boxes;
_indices;
_pos;
_queue;
static from(data, byteOffset = 0) {
if (byteOffset % 8 !== 0) {
throw new Error('byteOffset must be 8-byte aligned.');
}
if (data.byteLength === 0) {
throw new Error('Flatbush data buffer is detached (byteLength=0). ' +
'This usually means the buffer was transferred and is no longer usable.');
}
const [magic, versionAndType] = new Uint8Array(data, byteOffset + 0, 2);
if (magic !== 0xfb) {
throw new Error('Data does not appear to be in a Flatbush format.');
}
const version = versionAndType >> 4;
if (version !== VERSION) {
throw new Error(`Got v${version} data when expected v${VERSION}.`);
}
const ArrayType = ARRAY_TYPES[versionAndType & 0x0f];
if (!ArrayType) {
throw new Error('Unrecognized array type.');
}
const [nodeSize] = new Uint16Array(data, byteOffset + 2, 1);
const [numItems] = new Uint32Array(data, byteOffset + 4, 1);
return new Flatbush(numItems, nodeSize, ArrayType, undefined, data, byteOffset);
}
constructor(numItems, nodeSize = 16, ArrayType = Float64Array, ArrayBufferType = ArrayBuffer, data, byteOffset = 0) {
if (isNaN(numItems) || numItems <= 0) {
throw new Error(`Unexpected numItems value: ${numItems}.`);
}
this.numItems = +numItems;
this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
this.byteOffset = byteOffset;
let n = numItems;
let numNodes = n;
this._levelBounds = [n * 4];
do {
n = Math.ceil(n / this.nodeSize);
numNodes += n;
this._levelBounds.push(numNodes * 4);
} while (n !== 1);
this.ArrayType = ArrayType;
this.IndexArrayType = numNodes < 16384 ? Uint16Array : Uint32Array;
const arrayTypeIndex = ARRAY_TYPES.indexOf(ArrayType);
const nodesByteSize = numNodes * 4 * ArrayType.BYTES_PER_ELEMENT;
if (arrayTypeIndex === -1) {
throw new Error(`Unexpected typed array class: ${ArrayType}.`);
}
if (data) {
this.data = data;
this._boxes = new ArrayType(data, byteOffset + 8, numNodes * 4);
this._indices = new this.IndexArrayType(data, byteOffset + 8 + nodesByteSize, numNodes);
this._pos = numNodes * 4;
this.minX = this._boxes[this._pos - 4];
this.minY = this._boxes[this._pos - 3];
this.maxX = this._boxes[this._pos - 2];
this.maxY = this._boxes[this._pos - 1];
}
else {
const newData = (this.data = new ArrayBufferType(8 + nodesByteSize + numNodes * this.IndexArrayType.BYTES_PER_ELEMENT));
this._boxes = new ArrayType(newData, 8, numNodes * 4);
this._indices = new this.IndexArrayType(newData, 8 + nodesByteSize, numNodes);
this._pos = 0;
this.minX = Infinity;
this.minY = Infinity;
this.maxX = -Infinity;
this.maxY = -Infinity;
new Uint8Array(newData, 0, 2).set([0xfb, (VERSION << 4) + arrayTypeIndex]);
new Uint16Array(newData, 2, 1)[0] = nodeSize;
new Uint32Array(newData, 4, 1)[0] = numItems;
}
this._queue = new FlatQueue();
}
add(minX, minY, maxX = minX, maxY = minY) {
const index = this._pos >> 2;
const boxes = this._boxes;
this._indices[index] = index;
boxes[this._pos++] = minX;
boxes[this._pos++] = minY;
boxes[this._pos++] = maxX;
boxes[this._pos++] = maxY;
if (minX < this.minX) {
this.minX = minX;
}
if (minY < this.minY) {
this.minY = minY;
}
if (maxX > this.maxX) {
this.maxX = maxX;
}
if (maxY > this.maxY) {
this.maxY = maxY;
}
return index;
}
finish() {
if (this._pos >> 2 !== this.numItems) {
throw new Error(`Added ${this._pos >> 2} items when expected ${this.numItems}.`);
}
const boxes = this._boxes;
if (this.numItems <= this.nodeSize) {
boxes[this._pos++] = this.minX;
boxes[this._pos++] = this.minY;
boxes[this._pos++] = this.maxX;
boxes[this._pos++] = this.maxY;
return;
}
const width = this.maxX - this.minX || 1;
const height = this.maxY - this.minY || 1;
const hilbertValues = new Uint32Array(this.numItems);
const hilbertMax = (1 << 16) - 1;
const scaleX = hilbertMax / width;
const scaleY = hilbertMax / height;
const offsetX = this.minX;
const offsetY = this.minY;
for (let i = 0, pos = 0; i < this.numItems; i++) {
const minX = boxes[pos++];
const minY = boxes[pos++];
const maxX = boxes[pos++];
const maxY = boxes[pos++];
const x = Math.floor(((minX + maxX) * 0.5 - offsetX) * scaleX);
const y = Math.floor(((minY + maxY) * 0.5 - offsetY) * scaleY);
hilbertValues[i] = hilbert(x, y);
}
sort(hilbertValues, boxes, this._indices, 0, this.numItems - 1, this.nodeSize);
for (let i = 0, pos = 0; i < this._levelBounds.length - 1; i++) {
const end = this._levelBounds[i];
while (pos < end) {
const nodeIndex = pos;
let nodeMinX = boxes[pos++];
let nodeMinY = boxes[pos++];
let nodeMaxX = boxes[pos++];
let nodeMaxY = boxes[pos++];
for (let j = 1; j < this.nodeSize && pos < end; j++) {
const x0 = boxes[pos++];
const y0 = boxes[pos++];
const x1 = boxes[pos++];
const y1 = boxes[pos++];
if (x0 < nodeMinX) {
nodeMinX = x0;
}
if (y0 < nodeMinY) {
nodeMinY = y0;
}
if (x1 > nodeMaxX) {
nodeMaxX = x1;
}
if (y1 > nodeMaxY) {
nodeMaxY = y1;
}
}
this._indices[this._pos >> 2] = nodeIndex;
boxes[this._pos++] = nodeMinX;
boxes[this._pos++] = nodeMinY;
boxes[this._pos++] = nodeMaxX;
boxes[this._pos++] = nodeMaxY;
}
}
}
search(minX, minY, maxX, maxY, filterFn) {
if (this._pos !== this._boxes.length) {
throw new Error('Data not yet indexed - call index.finish().');
}
let nodeIndex = this._boxes.length - 4;
const queue = [];
const results = [];
while (nodeIndex !== undefined) {
const end = Math.min(nodeIndex + this.nodeSize * 4, upperBound(nodeIndex, this._levelBounds));
for (let pos = nodeIndex; pos < end; pos += 4) {
const x0 = this._boxes[pos];
if (maxX < x0) {
continue;
}
const y0 = this._boxes[pos + 1];
if (maxY < y0) {
continue;
}
const x1 = this._boxes[pos + 2];
if (minX > x1) {
continue;
}
const y1 = this._boxes[pos + 3];
if (minY > y1) {
continue;
}
const index = this._indices[pos >> 2] | 0;
if (nodeIndex >= this.numItems * 4) {
queue.push(index);
}
else if (filterFn === undefined || filterFn(index, x0, y0, x1, y1)) {
results.push(index);
}
}
nodeIndex = queue.pop();
}
return results;
}
neighbors(x, y, maxResults = Infinity, maxDistance = Infinity, filterFn) {
if (this._pos !== this._boxes.length) {
throw new Error('Data not yet indexed - call index.finish().');
}
let nodeIndex = this._boxes.length - 4;
const q = this._queue;
const results = [];
const maxDistSquared = maxDistance * maxDistance;
outer: while (nodeIndex !== undefined) {
const end = Math.min(nodeIndex + this.nodeSize * 4, upperBound(nodeIndex, this._levelBounds));
for (let pos = nodeIndex; pos < end; pos += 4) {
const index = this._indices[pos >> 2] | 0;
const minX = this._boxes[pos];
const minY = this._boxes[pos + 1];
const maxX = this._boxes[pos + 2];
const maxY = this._boxes[pos + 3];
const dx = x < minX ? minX - x : x > maxX ? x - maxX : 0;
const dy = y < minY ? minY - y : y > maxY ? y - maxY : 0;
const dist = dx * dx + dy * dy;
if (dist > maxDistSquared) {
continue;
}
if (nodeIndex >= this.numItems * 4) {
q.push(index << 1, dist);
}
else if (filterFn === undefined || filterFn(index)) {
q.push((index << 1) + 1, dist);
}
}
while (q.length && q.peek() & 1) {
const dist = q.peekValue();
if (dist > maxDistSquared) {
break outer;
}
results.push(q.pop() >> 1);
if (results.length === maxResults) {
break outer;
}
}
nodeIndex = q.length ? q.pop() >> 1 : undefined;
}
q.clear();
return results;
}
}
function upperBound(value, arr) {
let i = 0;
let j = arr.length - 1;
while (i < j) {
const m = (i + j) >> 1;
if (arr[m] > value) {
j = m;
}
else {
i = m + 1;
}
}
return arr[i];
}
function sort(values, boxes, indices, left, right, nodeSize) {
const stack = [left, right];
while (stack.length > 0) {
const r = stack.pop();
const l = stack.pop();
if (Math.floor(l / nodeSize) >= Math.floor(r / nodeSize)) {
continue;
}
if (r - l < 10) {
insertionSort(values, boxes, indices, l, r);
continue;
}
const start = values[l];
const mid = values[(l + r) >> 1];
const end = values[r];
let pivot = end;
const x = Math.max(start, mid);
if (end > x) {
pivot = x;
}
else if (x === start) {
pivot = Math.max(mid, end);
}
else if (x === mid) {
pivot = Math.max(start, end);
}
let i = l - 1;
let j = r + 1;
while (true) {
do {
i++;
} while (values[i] < pivot);
do {
j--;
} while (values[j] > pivot);
if (i >= j) {
break;
}
swap(values, boxes, indices, i, j);
}
stack.push(l, j, j + 1, r);
}
}
function insertionSort(values, boxes, indices, left, right) {
for (let i = left + 1; i <= right; i++) {
const tempVal = values[i];
const tempIdx = indices[i];
const k = i * 4;
const tempA = boxes[k];
const tempB = boxes[k + 1];
const tempC = boxes[k + 2];
const tempD = boxes[k + 3];
let j = i - 1;
while (j >= left && values[j] > tempVal) {
values[j + 1] = values[j];
indices[j + 1] = indices[j];
const src = j * 4;
const dst = (j + 1) * 4;
boxes[dst] = boxes[src];
boxes[dst + 1] = boxes[src + 1];
boxes[dst + 2] = boxes[src + 2];
boxes[dst + 3] = boxes[src + 3];
j--;
}
values[j + 1] = tempVal;
indices[j + 1] = tempIdx;
const dst = (j + 1) * 4;
boxes[dst] = tempA;
boxes[dst + 1] = tempB;
boxes[dst + 2] = tempC;
boxes[dst + 3] = tempD;
}
}
function swap(values, boxes, indices, i, j) {
const temp = values[i];
values[i] = values[j];
values[j] = temp;
const k = 4 * i;
const m = 4 * j;
const a = boxes[k];
const b = boxes[k + 1];
const c = boxes[k + 2];
const d = boxes[k + 3];
boxes[k] = boxes[m];
boxes[k + 1] = boxes[m + 1];
boxes[k + 2] = boxes[m + 2];
boxes[k + 3] = boxes[m + 3];
boxes[m] = a;
boxes[m + 1] = b;
boxes[m + 2] = c;
boxes[m + 3] = d;
const e = indices[i];
indices[i] = indices[j];
indices[j] = e;
}
function hilbert(x, y) {
let a = x ^ y;
let b = 0xffff ^ a;
let c = 0xffff ^ (x | y);
let d = x & (y ^ 0xffff);
let A = a | (b >> 1);
let B = (a >> 1) ^ a;
let C = (c >> 1) ^ (b & (d >> 1)) ^ c;
let D = (a & (c >> 1)) ^ (d >> 1) ^ d;
a = A;
b = B;
c = C;
d = D;
A = (a & (a >> 2)) ^ (b & (b >> 2));
B = (a & (b >> 2)) ^ (b & ((a ^ b) >> 2));
C ^= (a & (c >> 2)) ^ (b & (d >> 2));
D ^= (b & (c >> 2)) ^ ((a ^ b) & (d >> 2));
a = A;
b = B;
c = C;
d = D;
A = (a & (a >> 4)) ^ (b & (b >> 4));
B = (a & (b >> 4)) ^ (b & ((a ^ b) >> 4));
C ^= (a & (c >> 4)) ^ (b & (d >> 4));
D ^= (b & (c >> 4)) ^ ((a ^ b) & (d >> 4));
a = A;
b = B;
c = C;
d = D;
C ^= (a & (c >> 8)) ^ (b & (d >> 8));
D ^= (b & (c >> 8)) ^ ((a ^ b) & (d >> 8));
a = C ^ (C >> 1);
b = D ^ (D >> 1);
let i0 = x ^ y;
let i1 = b | (0xffff ^ (i0 | a));
i0 = (i0 | (i0 << 8)) & 0x00ff00ff;
i0 = (i0 | (i0 << 4)) & 0x0f0f0f0f;
i0 = (i0 | (i0 << 2)) & 0x33333333;
i0 = (i0 | (i0 << 1)) & 0x55555555;
i1 = (i1 | (i1 << 8)) & 0x00ff00ff;
i1 = (i1 | (i1 << 4)) & 0x0f0f0f0f;
i1 = (i1 | (i1 << 2)) & 0x33333333;
i1 = (i1 | (i1 << 1)) & 0x55555555;
return ((i1 << 1) | i0) >>> 0;
}