UNPKG

@jbrowse/core

Version:

JBrowse 2 core libraries used by plugins

203 lines (202 loc) 6.47 kB
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, }; } }