@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
419 lines (418 loc) • 13.6 kB
JavaScript
const maxFeaturePitchWidth = 20000;
class LayoutRow {
padding = 1;
allFilled;
intervals = [];
data = [];
setAllFilled(data) {
this.allFilled = data;
}
getIntervals() {
return this.intervals;
}
getItemAt(x) {
if (this.allFilled) {
return this.allFilled;
}
const intervals = this.intervals;
const len = intervals.length;
if (len === 0) {
return undefined;
}
if (len < 40) {
for (let i = 0; i < len; i += 2) {
if (x >= intervals[i] && x < intervals[i + 1]) {
return this.data[i >> 1];
}
}
return undefined;
}
let low = 0;
let high = len >> 1;
while (low < high) {
const mid = (low + high) >>> 1;
const midIdx = mid << 1;
if (intervals[midIdx + 1] <= x) {
low = mid + 1;
}
else {
high = mid;
}
}
const idx = low << 1;
if (idx < len && x >= intervals[idx] && x < intervals[idx + 1]) {
return this.data[low];
}
return undefined;
}
isRangeClear(left, right) {
if (this.allFilled) {
return false;
}
const intervals = this.intervals;
const len = intervals.length;
if (len === 0) {
return true;
}
if (len < 40) {
for (let i = 0; i < len; i += 2) {
const start = intervals[i];
const end = intervals[i + 1];
if (end > left && start < right) {
return false;
}
}
return true;
}
let low = 0;
let high = len >> 1;
while (low < high) {
const mid = (low + high) >>> 1;
const midIdx = mid << 1;
if (intervals[midIdx + 1] <= left) {
low = mid + 1;
}
else {
high = mid;
}
}
const idx = low << 1;
if (idx >= len) {
return true;
}
const start = intervals[idx];
if (start >= right) {
return true;
}
return false;
}
addRect(rect, data) {
const left = rect.l;
const right = rect.r + this.padding;
const intervals = this.intervals;
const len = intervals.length;
if (len < 40) {
let idx = len;
for (let i = 0; i < len; i += 2) {
if (left < intervals[i]) {
idx = i;
break;
}
}
intervals.splice(idx, 0, left, right);
this.data.splice(idx >> 1, 0, data);
}
else {
let low = 0;
let high = len >> 1;
while (low < high) {
const mid = (low + high) >>> 1;
const midIdx = mid << 1;
if (intervals[midIdx] < left) {
low = mid + 1;
}
else {
high = mid;
}
}
intervals.splice(low << 1, 0, left, right);
this.data.splice(low, 0, data);
}
}
discardRange(left, right) {
if (this.allFilled) {
return;
}
const intervals = this.intervals;
const data = this.data;
const oldLen = intervals.length;
const newIntervals = [];
const newData = [];
for (let i = 0; i < oldLen; i += 2) {
const start = intervals[i];
const end = intervals[i + 1];
const intervalData = data[i >> 1];
if (start >= left && end <= right) {
continue;
}
if (end <= left || start >= right) {
newIntervals.push(start, end);
newData.push(intervalData);
}
else if (start < left && end > left) {
if (end <= right) {
newIntervals.push(start, left);
newData.push(intervalData);
}
else {
newIntervals.push(start, left, right, end);
newData.push(intervalData, intervalData);
}
}
else if (start < right && end > right) {
newIntervals.push(right, end);
newData.push(intervalData);
}
}
this.intervals = newIntervals;
this.data = newData;
}
}
export default class GranularRectLayout {
pitchX;
pitchY;
hardRowLimit;
bitmap;
rectangles;
maxHeightReached;
maxHeight;
displayMode;
pTotalHeight;
constructor({ pitchX = 10, pitchY = 10, maxHeight = 10000, hardRowLimit = 10000, displayMode = 'normal', } = {}) {
this.pitchX = pitchX;
this.pitchY = pitchY;
this.hardRowLimit = hardRowLimit;
this.maxHeightReached = false;
this.displayMode = displayMode;
if (this.displayMode === 'compact') {
this.pitchY = Math.round(this.pitchY / 4) || 1;
this.pitchX = Math.round(this.pitchX / 4) || 1;
}
this.bitmap = [];
this.rectangles = new Map();
this.maxHeight = Math.ceil(maxHeight / this.pitchY);
this.pTotalHeight = 0;
}
addRect(id, left, right, height, data, serializableData, startingRow) {
const pitchX = this.pitchX;
const pitchY = this.pitchY;
const storedRec = this.rectangles.get(id);
if (storedRec) {
if (serializableData !== undefined) {
storedRec.serializableData = serializableData;
}
if (storedRec.top === null) {
return null;
}
this.addRectToBitmap(storedRec);
return storedRec.top * pitchY;
}
const pLeft = Math.trunc(left / pitchX);
const pRight = Math.trunc(right / pitchX);
const pHeight = Math.ceil(height / pitchY);
const rectangle = {
id,
l: pLeft,
r: pRight,
top: null,
h: pHeight,
originalHeight: height,
data,
serializableData,
};
const maxTop = this.maxHeight;
let top = startingRow !== undefined
? Math.min(Math.floor(startingRow / pitchY), maxTop)
: 0;
if (this.displayMode !== 'collapse') {
const bitmap = this.bitmap;
outer: for (; top <= maxTop; top += 1) {
const maxY = top + pHeight;
for (let y = top; y < maxY; y += 1) {
const row = bitmap[y];
if (!row) {
continue;
}
if (row.allFilled) {
continue outer;
}
const intervals = row.getIntervals();
const len = intervals.length;
if (len > 0) {
if (len < 40) {
for (let i = 0; i < len; i += 2) {
const start = intervals[i];
const end = intervals[i + 1];
if (end > pLeft && start < pRight) {
continue outer;
}
}
}
else {
let low = 0;
let high = len >> 1;
while (low < high) {
const mid = (low + high) >>> 1;
const midIdx = mid << 1;
if (intervals[midIdx + 1] <= pLeft) {
low = mid + 1;
}
else {
high = mid;
}
}
const idx = low << 1;
if (idx < len) {
const start = intervals[idx];
if (start < pRight) {
continue outer;
}
}
}
}
}
break;
}
if (top > maxTop) {
rectangle.top = null;
this.rectangles.set(id, rectangle);
this.maxHeightReached = true;
return null;
}
}
rectangle.top = top;
this.addRectToBitmap(rectangle);
this.rectangles.set(id, rectangle);
this.pTotalHeight = Math.max(this.pTotalHeight || 0, top + pHeight);
return top * pitchY;
}
collides(rect, top) {
const { bitmap } = this;
const maxY = top + rect.h;
for (let y = top; y < maxY; y += 1) {
const row = bitmap[y];
if (row !== undefined && !row.isRangeClear(rect.l, rect.r)) {
return true;
}
}
return false;
}
addRectToBitmap(rect) {
if (rect.top === null) {
return;
}
const data = rect.id;
const yEnd = rect.top + rect.h;
const bitmap = this.bitmap;
const hardRowLimit = this.hardRowLimit;
const pitchY = this.pitchY;
if (rect.r - rect.l > maxFeaturePitchWidth) {
for (let y = rect.top; y < yEnd; y += 1) {
let row = bitmap[y];
if (!row) {
if (y > hardRowLimit) {
throw new Error(`layout hard limit (${hardRowLimit * pitchY}px) exceeded, aborting layout`);
}
row = new LayoutRow();
bitmap[y] = row;
}
row.setAllFilled(data);
}
}
else {
for (let y = rect.top; y < yEnd; y += 1) {
let row = bitmap[y];
if (!row) {
if (y > hardRowLimit) {
throw new Error(`layout hard limit (${hardRowLimit * pitchY}px) exceeded, aborting layout`);
}
row = new LayoutRow();
bitmap[y] = row;
}
row.addRect(rect, data);
}
}
}
discardRange(left, right) {
const pLeft = Math.trunc(left / this.pitchX);
const pRight = Math.trunc(right / this.pitchX);
const { bitmap } = this;
for (const row of bitmap) {
row.discardRange(pLeft, pRight);
}
}
hasSeen(id) {
return this.rectangles.has(id);
}
getByCoord(x, y) {
const pY = Math.trunc(y / this.pitchY);
const row = this.bitmap[pY];
if (!row) {
return undefined;
}
const pX = Math.trunc(x / this.pitchX);
return row.getItemAt(pX);
}
getByID(id) {
const r = this.rectangles.get(id);
if (r) {
const t = r.top * this.pitchY;
return [
r.l * this.pitchX,
t,
r.r * this.pitchX,
t + r.originalHeight,
];
}
return undefined;
}
getDataByID(id) {
return this.rectangles.get(id)?.data;
}
getSerializableDataByID(id) {
return this.rectangles.get(id)?.serializableData;
}
cleanup() { }
getTotalHeight() {
return this.pTotalHeight * this.pitchY;
}
get totalHeight() {
return this.getTotalHeight();
}
getRectangles() {
const pitchX = this.pitchX;
const pitchY = this.pitchY;
return new Map([...this.rectangles.entries()].map(([id, rect]) => {
const { l, r, originalHeight, top, serializableData } = rect;
const t = (top || 0) * pitchY;
const b = t + originalHeight;
return [id, [l * pitchX, t, r * pitchX, b, serializableData]];
}));
}
serializeRegion(region) {
const pitchX = this.pitchX;
const pitchY = this.pitchY;
const x1 = region.start;
const x2 = region.end;
const regionRectangles = {};
let maxHeightReached = false;
for (const [id, rect] of this.rectangles.entries()) {
const { l, r, originalHeight, top } = rect;
if (top === null) {
maxHeightReached = true;
}
else {
const t = top * pitchY;
const b = t + originalHeight;
const y1 = l * pitchX;
const y2 = r * pitchX;
if (x2 >= y1 - pitchX && y2 + pitchX >= x1) {
regionRectangles[id] = [y1, t, y2, b, rect.serializableData];
}
}
}
return {
rectangles: regionRectangles,
totalHeight: this.getTotalHeight(),
maxHeightReached,
};
}
toJSON() {
const rectangles = Object.fromEntries(this.getRectangles());
return {
rectangles,
totalHeight: this.getTotalHeight(),
maxHeightReached: this.maxHeightReached,
};
}
}