UNPKG

@jbrowse/core

Version:

JBrowse 2 core libraries used by plugins

419 lines (418 loc) 13.6 kB
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, }; } }