UNPKG

@jbrowse/core

Version:

JBrowse 2 core libraries used by plugins

344 lines (343 loc) 12.2 kB
"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;