@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
344 lines (343 loc) • 12.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const minSizeToBotherWith = 10000;
const maxFeaturePitchWidth = 20000;
function segmentsIntersect(x1, x2, y1, y2) {
return x2 >= y1 && y2 >= x1;
}
class LayoutRow {
constructor() {
this.padding = 1;
this.widthLimit = 1000000;
}
setAllFilled(data) {
this.allFilled = data;
}
getItemAt(x) {
var _a;
if (this.allFilled) {
return this.allFilled;
}
if (((_a = this.rowState) === null || _a === void 0 ? void 0 : _a.min) === undefined ||
x < this.rowState.min ||
x >= this.rowState.max) {
return undefined;
}
return this.rowState.bits[x - this.rowState.offset];
}
isRangeClear(left, right) {
if (this.allFilled) {
return false;
}
if (this.rowState === undefined ||
right <= this.rowState.min ||
left >= this.rowState.max) {
return true;
}
const { min, max, offset, bits } = this.rowState;
const maxX = Math.min(max, right) - offset;
let flag = true;
for (let x = Math.max(min, left) - offset; x < maxX && flag; x++) {
flag = bits[x] === undefined;
}
return flag;
}
initialize(left, right) {
const rectWidth = right - left;
return {
offset: left - rectWidth,
min: left,
max: right,
bits: Array.from({ length: 3 * rectWidth }),
};
}
addRect(rect, data) {
const left = rect.l;
const right = rect.r + this.padding;
if (!this.rowState) {
this.rowState = this.initialize(left, right);
}
let oLeft = left - this.rowState.offset;
let oRight = right - this.rowState.offset;
const currLength = this.rowState.bits.length;
if (oRight >= this.rowState.bits.length) {
const additionalLength = oRight + 1;
if (this.rowState.bits.length + additionalLength > this.widthLimit) {
console.warn('Layout width limit exceeded, discarding old layout. Please be more careful about discarding unused blocks.');
this.rowState = this.initialize(left, right);
}
else if (additionalLength > 0) {
this.rowState.bits = [
...this.rowState.bits,
...Array.from({ length: additionalLength }),
];
}
}
if (left < this.rowState.offset) {
const additionalLength = Math.min(currLength - oLeft, this.rowState.offset);
if (this.rowState.bits.length + additionalLength > this.widthLimit) {
console.warn('Layout width limit exceeded, discarding old layout. Please be more careful about discarding unused blocks.');
this.rowState = this.initialize(left, right);
}
else {
this.rowState.bits = [
...Array.from({ length: additionalLength }),
...this.rowState.bits,
];
this.rowState.offset -= additionalLength;
}
}
oRight = right - this.rowState.offset;
oLeft = left - this.rowState.offset;
const w = oRight - oLeft;
if (w > maxFeaturePitchWidth) {
console.warn(`Layout X pitch set too low, feature spans ${w} bits in a single row.`, rect, data);
}
for (let x = oLeft; x < oRight; x += 1) {
this.rowState.bits[x] = data;
}
if (left < this.rowState.min) {
this.rowState.min = left;
}
if (right > this.rowState.max) {
this.rowState.max = right;
}
}
discardRange(left, right) {
if (this.allFilled) {
return;
}
if (!this.rowState) {
return;
}
if (right <= this.rowState.min || left >= this.rowState.max) {
return;
}
if (left <= this.rowState.min && right >= this.rowState.max) {
this.rowState = undefined;
return;
}
if (right > this.rowState.min && left <= this.rowState.min) {
this.rowState.min = right;
}
if (left < this.rowState.max && right >= this.rowState.max) {
this.rowState.max = left;
}
if (this.rowState.offset < this.rowState.min - minSizeToBotherWith &&
this.rowState.bits.length >
this.rowState.max + minSizeToBotherWith - this.rowState.offset) {
const leftTrimAmount = this.rowState.min - this.rowState.offset;
const rightTrimAmount = this.rowState.bits.length -
1 -
(this.rowState.max - this.rowState.offset);
this.rowState.bits = this.rowState.bits.slice(leftTrimAmount, this.rowState.bits.length - rightTrimAmount);
this.rowState.offset += leftTrimAmount;
}
else if (this.rowState.offset < this.rowState.min - minSizeToBotherWith) {
const desiredOffset = this.rowState.min - Math.floor(minSizeToBotherWith / 2);
const trimAmount = desiredOffset - this.rowState.offset;
this.rowState.bits.splice(0, trimAmount);
this.rowState.offset += trimAmount;
}
else if (this.rowState.bits.length >
this.rowState.max - this.rowState.offset + minSizeToBotherWith) {
const desiredLength = this.rowState.max -
this.rowState.offset +
1 +
Math.floor(minSizeToBotherWith / 2);
this.rowState.bits.length = desiredLength;
}
const oLeft = Math.max(this.rowState.min, left) - this.rowState.offset;
const oRight = Math.min(right, this.rowState.max) - this.rowState.offset;
for (let x = oLeft; x >= 0 && x < oRight; x += 1) {
this.rowState.bits[x] = undefined;
}
}
}
class GranularRectLayout {
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) {
const storedRec = this.rectangles.get(id);
if (storedRec) {
if (storedRec.top === null) {
return null;
}
this.addRectToBitmap(storedRec);
return storedRec.top * this.pitchY;
}
const pLeft = Math.floor(left / this.pitchX);
const pRight = Math.floor(right / this.pitchX);
const pHeight = Math.ceil(height / this.pitchY);
const rectangle = {
id,
l: pLeft,
r: pRight,
top: null,
h: pHeight,
originalHeight: height,
data,
};
const maxTop = this.maxHeight - pHeight;
let top = 0;
if (this.displayMode !== 'collapse') {
for (; top <= maxTop; top += 1) {
if (!this.collides(rectangle, top)) {
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 * this.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;
}
autovivifyRow(bitmap, y) {
let row = bitmap[y];
if (!row) {
if (y > this.hardRowLimit) {
throw new Error(`layout hard limit (${this.hardRowLimit * this.pitchY}px) exceeded, aborting layout`);
}
row = new LayoutRow();
bitmap[y] = row;
}
return row;
}
addRectToBitmap(rect) {
if (rect.top === null) {
return;
}
const data = rect.id;
const yEnd = rect.top + rect.h;
if (rect.r - rect.l > maxFeaturePitchWidth) {
for (let y = rect.top; y < yEnd; y += 1) {
this.autovivifyRow(this.bitmap, y).setAllFilled(data);
}
}
else {
for (let y = rect.top; y < yEnd; y += 1) {
this.autovivifyRow(this.bitmap, y).addRect(rect, data);
}
}
}
discardRange(left, right) {
const pLeft = Math.floor(left / this.pitchX);
const pRight = Math.floor(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.floor(y / this.pitchY);
const row = this.bitmap[pY];
if (!row) {
return undefined;
}
const pX = Math.floor(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) {
var _a;
return (_a = this.rectangles.get(id)) === null || _a === void 0 ? void 0 : _a.data;
}
cleanup() { }
getTotalHeight() {
return this.pTotalHeight * this.pitchY;
}
get totalHeight() {
return this.getTotalHeight();
}
getRectangles() {
return new Map([...this.rectangles.entries()].map(([id, rect]) => {
const { l, r, originalHeight, top } = rect;
const t = (top || 0) * this.pitchY;
const b = t + originalHeight;
return [id, [l * this.pitchX, t, r * this.pitchX, b]];
}));
}
serializeRegion(region) {
const regionRectangles = {};
let maxHeightReached = false;
for (const [id, rect] of this.rectangles.entries()) {
const { l, r, originalHeight, top } = rect;
if (rect.top === null) {
maxHeightReached = true;
}
else {
const t = (top || 0) * this.pitchY;
const b = t + originalHeight;
const y1 = l * this.pitchX;
const y2 = r * this.pitchX;
const x1 = region.start;
const x2 = region.end;
if (segmentsIntersect(x1, x2, y1 - this.pitchX, y2 + this.pitchX)) {
regionRectangles[id] = [y1, t, y2, b];
}
}
}
return {
rectangles: regionRectangles,
containsNoTransferables: true,
totalHeight: this.getTotalHeight(),
maxHeightReached,
};
}
toJSON() {
const rectangles = Object.fromEntries(this.getRectangles());
return {
rectangles,
containsNoTransferables: true,
totalHeight: this.getTotalHeight(),
maxHeightReached: this.maxHeightReached,
};
}
}
exports.default = GranularRectLayout;