@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
203 lines (202 loc) • 6.47 kB
JavaScript
import { findInsertionPoint, insertInterval, isRangeClear, } from "./intervalUtils.js";
export default class PileupLayout {
featureHeight;
spacing;
rowHeight;
padding;
maxRows;
rows = [];
rowMaxEnd = [];
rectangles = new Map();
lastLeft = -Infinity;
lastRow = 0;
maxHeightReached = false;
constructor(options = {}) {
this.featureHeight = options.featureHeight ?? 7;
this.spacing = options.spacing ?? 0;
this.rowHeight = this.featureHeight + this.spacing;
this.padding = options.padding ?? 1;
this.maxRows = options.maxHeight
? Math.floor(options.maxHeight / this.rowHeight)
: 100000;
}
addRect(id, left, right, height, data, serializableData) {
const existing = this.rectangles.get(id);
if (existing) {
if (existing.top === null) {
return null;
}
return existing.top * this.rowHeight;
}
let startRow = 0;
if (left === this.lastLeft) {
startRow = this.lastRow + 1;
}
const paddedRight = right + this.padding;
const row = this.findFreeRow(left, paddedRight, startRow);
if (row === null) {
const rect = {
id,
l: left,
r: right,
top: null,
h: 1,
originalHeight: this.featureHeight,
data,
serializableData,
};
this.rectangles.set(id, rect);
this.maxHeightReached = true;
return null;
}
this.addToRow(row, left, paddedRight);
const rect = {
id,
l: left,
r: right,
top: row,
h: 1,
originalHeight: this.featureHeight,
data,
serializableData,
};
this.rectangles.set(id, rect);
this.lastLeft = left;
this.lastRow = row;
return row * this.rowHeight;
}
findFreeRow(left, right, startRow) {
const rows = this.rows;
const rowMaxEnd = this.rowMaxEnd;
for (let row = startRow; row < this.maxRows; row++) {
const intervals = rows[row];
if (!intervals) {
return row;
}
if (left >= rowMaxEnd[row]) {
return row;
}
if (isRangeClear(intervals, left, right)) {
return row;
}
}
return null;
}
addToRow(rowIdx, left, right) {
let intervals = this.rows[rowIdx];
if (!intervals) {
intervals = [];
this.rows[rowIdx] = intervals;
this.rowMaxEnd[rowIdx] = 0;
}
const len = intervals.length;
if (len === 0 || left >= intervals[len - 2]) {
intervals.push(left, right);
}
else {
const idx = findInsertionPoint(intervals, left);
insertInterval(intervals, idx, left, right);
}
if (right > this.rowMaxEnd[rowIdx]) {
this.rowMaxEnd[rowIdx] = right;
}
}
collides(_rect, _top) {
return false;
}
addRectToBitmap(_rect) {
}
getTotalHeight() {
let maxRow = 0;
for (let i = this.rows.length - 1; i >= 0; i--) {
if (this.rows[i]) {
maxRow = i + 1;
break;
}
}
return maxRow * this.rowHeight;
}
getRectangles() {
const result = new Map();
const rowHeight = this.rowHeight;
const featureHeight = this.featureHeight;
for (const [id, rect] of this.rectangles) {
if (rect.top !== null) {
const top = rect.top * rowHeight;
result.set(id, [rect.l, top, rect.r, top + featureHeight]);
}
}
return result;
}
discardRange(left, right) {
for (let rowIdx = 0; rowIdx < this.rows.length; rowIdx++) {
const intervals = this.rows[rowIdx];
if (!intervals) {
continue;
}
const newIntervals = [];
let maxEnd = 0;
for (let i = 0; i < intervals.length; i += 2) {
const start = intervals[i];
const end = intervals[i + 1];
if (end <= left || start >= right) {
newIntervals.push(start, end);
if (end > maxEnd) {
maxEnd = end;
}
}
else if (start < left && end > left && end <= right) {
newIntervals.push(start, left);
if (left > maxEnd) {
maxEnd = left;
}
}
else if (start >= left && start < right && end > right) {
newIntervals.push(right, end);
if (end > maxEnd) {
maxEnd = end;
}
}
else if (start < left && end > right) {
newIntervals.push(start, left, right, end);
if (end > maxEnd) {
maxEnd = end;
}
}
}
this.rows[rowIdx] = newIntervals;
this.rowMaxEnd[rowIdx] = maxEnd;
}
this.lastLeft = -Infinity;
this.lastRow = 0;
}
getDataByID(id) {
return this.rectangles.get(id)?.data;
}
serializeRegion(region) {
const { start: x1, end: x2 } = region;
const regionRectangles = {};
for (const [id, rect] of this.rectangles.entries()) {
if (rect.top === null) {
continue;
}
const { l, r, top } = rect;
if (x2 >= l && r >= x1) {
const topPx = top * this.rowHeight;
regionRectangles[id] = [l, topPx, r, topPx + this.featureHeight];
}
}
return {
rectangles: regionRectangles,
totalHeight: this.getTotalHeight(),
maxHeightReached: this.maxHeightReached,
};
}
toJSON() {
return {
rectangles: Object.fromEntries(this.getRectangles()),
totalHeight: this.getTotalHeight(),
maxHeightReached: this.maxHeightReached,
};
}
}