diff --git a/packages/core/package.json b/packages/core/package.json index ea4fd5d13aa..518fd37e0f6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -53,6 +53,7 @@ "load-script2": "^2.0.5", "object.fromentries": "^2.0.0", "pako": "^1.0.10", + "rbush": "^3.0.1", "react-error-boundary": "^3.0.0", "react-intersection-observer": "^8.31.0", "react-measure": "^2.3.0", diff --git a/packages/core/util/layouts/GranularRectLayout.ts b/packages/core/util/layouts/GranularRectLayout.ts index f59b5a299ec..4fa7ca5eaa6 100644 --- a/packages/core/util/layouts/GranularRectLayout.ts +++ b/packages/core/util/layouts/GranularRectLayout.ts @@ -1,4 +1,3 @@ -import { ObservableMap } from 'mobx' import { objectFromEntries } from '../index' import { RectTuple, @@ -32,13 +31,13 @@ interface RowState { min: number max: number offset: number - bits: (Record | boolean | undefined)[] + bits: (Record | string | undefined)[] } // a single row in the layout class LayoutRow { private padding: number - private allFilled?: Record | boolean + private allFilled?: Record | string private widthLimit: number @@ -61,11 +60,11 @@ class LayoutRow { // console.log(`r${this.rowNumber}: ${msg}`) // } - setAllFilled(data: Record | boolean): void { + setAllFilled(data: Record | string): void { this.allFilled = data } - getItemAt(x: number): Record | boolean | undefined { + getItemAt(x: number): Record | string | undefined { if (this.allFilled) { return this.allFilled } @@ -129,7 +128,7 @@ class LayoutRow { // this.log(`initialize ${this.rowState.min} - ${this.rowState.max} (${this.rowState.bits.length})`) } - addRect(rect: Rectangle, data: Record | boolean): void { + addRect(rect: Rectangle, data: Record | string): void { const left = rect.l const right = rect.r + this.padding // only padding on the right if (!this.rowState) { @@ -319,7 +318,7 @@ export default class GranularRectLayout implements BaseLayout { private bitmap: LayoutRow[] - private rectangles: ObservableMap> + private rectangles: Map> public maxHeightReached: boolean @@ -329,6 +328,12 @@ export default class GranularRectLayout implements BaseLayout { private pTotalHeight: number + /* + * + * pitchX - layout grid pitch in the X direction + * pitchY - layout grid pitch in the Y direction + * maxHeight - maximum layout height, default Infinity (no max) + */ constructor({ pitchX = 10, pitchY = 10, @@ -336,11 +341,8 @@ export default class GranularRectLayout implements BaseLayout { hardRowLimit = 3000, displayMode = 'normal', }: { - /** layout grid pitch in the X direction */ pitchX?: number - /** layout grid pitch in the Y direction */ pitchY?: number - /** maximum layout height, default Infinity (no max) */ maxHeight?: number displayMode?: string hardRowLimit?: number @@ -358,7 +360,7 @@ export default class GranularRectLayout implements BaseLayout { } this.bitmap = [] - this.rectangles = new ObservableMap() + this.rectangles = new Map() this.maxHeight = Math.ceil(maxHeight / this.pitchY) this.pTotalHeight = 0 // total height, in units of bitmap squares (px/pitchY) } @@ -375,14 +377,14 @@ export default class GranularRectLayout implements BaseLayout { data?: Record, ): number | null { // if we have already laid it out, return its layout - // console.log(`${this.id} add ${id}`) const storedRec = this.rectangles.get(id) if (storedRec) { if (storedRec.top === null) { return null } - // add it to the bitmap again, since that bitmap range may have been discarded + // add it to the bitmap again, since that bitmap range may have been + // discarded this.addRectToBitmap(storedRec) return storedRec.top * this.pitchY } @@ -391,13 +393,11 @@ export default class GranularRectLayout implements BaseLayout { const pRight = Math.floor(right / this.pitchX) const pHeight = Math.ceil(height / this.pitchY) - // const midX = Math.floor((pLeft + pRight) / 2) const rectangle: Rectangle = { id, l: pLeft, r: pRight, top: null, - // mX: midX, h: pHeight, originalHeight: height, data, @@ -423,7 +423,6 @@ export default class GranularRectLayout implements BaseLayout { this.addRectToBitmap(rectangle) this.rectangles.set(id, rectangle) this.pTotalHeight = Math.max(this.pTotalHeight || 0, top + pHeight) - // console.log(`G2 ${data.get('name')} ${top}`) return top * this.pitchY } @@ -467,7 +466,7 @@ export default class GranularRectLayout implements BaseLayout { return } - const data = rect.data || true + const data = rect.data || rect.id const { bitmap } = this const yEnd = rect.top + rect.h if (rect.r - rect.l > maxFeaturePitchWidth) { @@ -508,7 +507,7 @@ export default class GranularRectLayout implements BaseLayout { return this.rectangles.has(id) } - getByCoord(x: number, y: number): Record | boolean | undefined { + getByCoord(x: number, y: number): Record | string | undefined { const pY = Math.floor(y / this.pitchY) const row = this.bitmap[pY] if (!row) { @@ -518,11 +517,13 @@ export default class GranularRectLayout implements BaseLayout { return row.getItemAt(pX) } - getByID(id: string): (Record | boolean) | undefined { + getByID(id: string): RectTuple | undefined { const r = this.rectangles.get(id) if (r) { - return r.data || true + const t = (r.top as number) * this.pitchX + return [r.l * this.pitchX, t, r.r * this.pitchX, t + r.originalHeight] } + return undefined } diff --git a/packages/core/util/layouts/PrecomputedLayout.ts b/packages/core/util/layouts/PrecomputedLayout.ts index e9c0a0161f5..9b87fa889fe 100644 --- a/packages/core/util/layouts/PrecomputedLayout.ts +++ b/packages/core/util/layouts/PrecomputedLayout.ts @@ -5,6 +5,15 @@ import { BaseLayout, Rectangle, } from './BaseLayout' +import RBush from 'rbush' + +export interface Layout { + minX: number + minY: number + maxX: number + maxY: number + name: string +} export default class PrecomputedLayout implements BaseLayout { private rectangles: Map @@ -13,11 +22,23 @@ export default class PrecomputedLayout implements BaseLayout { public maxHeightReached: boolean + private rbush: RBush + constructor({ rectangles, totalHeight, maxHeightReached }: SerializedLayout) { this.rectangles = new Map(Object.entries(rectangles)) // rectangles is of the form "featureId": [leftPx, topPx, rightPx, bottomPx] this.totalHeight = totalHeight this.maxHeightReached = maxHeightReached + this.rbush = new RBush() + for (const [key, layout] of Object.entries(rectangles)) { + this.rbush.insert({ + minX: layout[0], + minY: layout[1], + maxX: layout[2], + maxY: layout[3], + name: key, + }) + } } addRect(id: string) { @@ -44,6 +65,15 @@ export default class PrecomputedLayout implements BaseLayout { throw new Error('Method not implemented.') } + getByCoord(x: number, y: number) { + const rect = { minX: x, minY: y, maxX: x + 1, maxY: y + 1 } + return this.rbush.collides(rect) ? this.rbush.search(rect)[0].name : [] + } + + getByID(id: string) { + return this.rectangles.get(id) + } + addRectToBitmap(_rect: Rectangle, _data: Record): void { throw new Error('Method not implemented.') } diff --git a/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx b/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx index 03144153663..72fd60d2e85 100644 --- a/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx +++ b/plugins/alignments/src/LinearAlignmentsDisplay/models/model.tsx @@ -65,8 +65,8 @@ const stateModelFactory = ( } }, - get layoutFeatures() { - return self.PileupDisplay.layoutFeatures + getFeatureByID(id: string) { + return self.PileupDisplay.getFeatureByID(id) }, get features() { diff --git a/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx b/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx index 2b4c38deef4..69058d7fb7d 100644 --- a/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx +++ b/plugins/alignments/src/PileupRenderer/components/PileupRendering.tsx @@ -151,12 +151,11 @@ function PileupRendering(props: { const px = region.reversed ? width - offsetX : offsetX const clientBp = region.start + bpPerPx * px - const feats = displayModel.getFeatureOverlapping( + const featIdUnderMouse = displayModel.getFeatureOverlapping( blockKey, clientBp, offsetY, ) - const featIdUnderMouse = feats.length ? feats[0].name : undefined if (onMouseMove) { onMouseMove(event, featIdUnderMouse) diff --git a/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx b/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx index 298294ba288..8e247620700 100644 --- a/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx +++ b/plugins/breakpoint-split-view/src/BreakpointSplitView.test.tsx @@ -23,13 +23,19 @@ const getView = () => { type: 'FakeTrack', configuration: types.frozen(), displays: types.array( - types.model('FakeDisplay', { - type: 'FakeDisplay', - displayId: 'FakeDisplay', - configuration: types.frozen(), - features: types.frozen(), - layoutFeatures: types.frozen(), - }), + types + .model('FakeDisplay', { + type: 'FakeDisplay', + displayId: 'FakeDisplay', + configuration: types.frozen(), + layoutFeatures: types.frozen(), + features: types.frozen(), + }) + .views(self => ({ + getFeatureByID(id: string) { + return self.layoutFeatures[id] + }, + })), ), }) .actions(self => ({ @@ -37,11 +43,9 @@ const getView = () => { self.displays[0].features = new Map( Object.entries(self.displays[0].features), ) - self.displays[0].layoutFeatures = new Map( - Object.entries(self.displays[0].layoutFeatures), - ) }, })) + stubManager.addViewType( () => new ViewType({ diff --git a/plugins/breakpoint-split-view/src/model.test.tsx b/plugins/breakpoint-split-view/src/model.test.tsx index 79824513615..1259af07cd8 100644 --- a/plugins/breakpoint-split-view/src/model.test.tsx +++ b/plugins/breakpoint-split-view/src/model.test.tsx @@ -23,13 +23,19 @@ const getView = () => { type: 'FakeTrack', configuration: types.frozen(), displays: types.array( - types.model('FakeDisplay', { - type: 'FakeDisplay', - displayId: 'FakeDisplay', - configuration: types.frozen(), - features: types.frozen(), - layoutFeatures: types.frozen(), - }), + types + .model('FakeDisplay', { + type: 'FakeDisplay', + displayId: 'FakeDisplay', + configuration: types.frozen(), + features: types.frozen(), + layoutFeatures: types.frozen(), + }) + .views(self => ({ + getFeatureByID(id: string) { + return self.layoutFeatures[id] + }, + })), ), }) .actions(self => ({ @@ -37,9 +43,6 @@ const getView = () => { self.displays[0].features = new Map( Object.entries(self.displays[0].features), ) - self.displays[0].layoutFeatures = new Map( - Object.entries(self.displays[0].layoutFeatures), - ) }, })) stubManager.addViewType( diff --git a/plugins/breakpoint-split-view/src/model.ts b/plugins/breakpoint-split-view/src/model.ts index 4c996943651..d8cadab18ea 100644 --- a/plugins/breakpoint-split-view/src/model.ts +++ b/plugins/breakpoint-split-view/src/model.ts @@ -229,8 +229,9 @@ export default function stateModelFactory(pluginManager: any) { // use reverse to search the second track first const tracks = this.getMatchedTracks(trackConfigId) - const calc = (track: any, feat: Feature) => - track.displays[0].layoutFeatures.get(feat.id()) + const calc = (track: any, feat: Feature) => { + return track.displays[0].getFeatureByID(feat.id()) + } return features.map(c => c.map(feature => { diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/BreakpointSplitRenderer.ts b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/BreakpointSplitRenderer.ts deleted file mode 100644 index 4a8c240adc5..00000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/BreakpointSplitRenderer.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import ComparativeServerSideRendererType, { - RenderArgsDeserialized as ComparativeRenderArgsDeserialized, -} from '@jbrowse/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType' -import { Feature } from '@jbrowse/core/util/simpleFeature' -import { readConfObject } from '@jbrowse/core/configuration' -import { - createCanvas, - createImageBitmap, -} from '@jbrowse/core/util/offscreenCanvasPonyfill' -import { - overlayYPos, - interstitialYPos, - getPxFromCoordinate, - LayoutRecord, - ReducedLinearGenomeView, -} from '../util' - -const [LEFT, , RIGHT] = [0, 1, 2, 3] - -export interface RenderArgsDeserialized - extends ComparativeRenderArgsDeserialized { - height: number - width: number - middle: boolean - highResolutionScaling: number - linkedTrack: string - views: ReducedLinearGenomeView[] -} - -export interface RenderArgsDeserializedWithImageData - extends RenderArgsDeserialized { - imageData: any -} - -// faster than localeCompare by 5x or so -const strcmp = new Intl.Collator(undefined, { - numeric: true, - sensitivity: 'base', -}).compare - -function instantiateTrackLayoutFeatures(views: ReducedLinearGenomeView[]) { - views.forEach(view => { - view.tracks.forEach(track => { - if (track.layoutFeatures) { - // @ts-ignore - track.layoutFeatures = new Map(track.layoutFeatures) - } - }) - }) - return views - // return views.map(view => { - // console.log(view) - // // view.tracks = [] - // console.log(view.tracks) - // return stateModelFactory(pluginManager).create(view) - // }) -} - -function* generateLayoutMatches( - views: ReducedLinearGenomeView[], - trackId: string, - middle: boolean, -) { - const feats = views - .map((view, index) => { - const track = views[index].tracks.find(t => t.configuration === trackId) - return view.features.map(feature => { - const layout = - track && !middle - ? // prettier-ignore - // @ts-ignore - track.layoutFeatures.get(String(feature.id())) - : ([feature.get('start'), 0, feature.get('end'), 0] as LayoutRecord) - return { - feature, - level: index, - refName: feature.get('refName'), - layout, - } - }) - }) - .flat() - .sort((a, b) => strcmp(a.feature.get('name'), b.feature.get('name'))) - - let currEmit = [feats[0]] - let currFeat: { feature: Feature; level: number } = feats[0] - - for (let i = 1; i < feats.length; i++) { - if (currFeat.feature.get('name') !== feats[i].feature.get('name')) { - yield currEmit - currFeat = feats[i] - currEmit = [feats[i]] - } else { - currEmit.push(feats[i]) - } - } -} - -function drawArrow( - context: CanvasRenderingContext2D, - fromx: number, - fromy: number, - tox: number, - toy: number, -) { - const headlen = 6 // length of head in pixels - const dx = tox - fromx - const dy = toy - fromy - const angle = Math.atan2(dy, dx) - context.beginPath() - context.moveTo(tox, toy) - context.lineTo( - tox - headlen * Math.cos(angle - Math.PI / 8), - toy - headlen * Math.sin(angle - Math.PI / 6), - ) - context.lineTo( - tox - headlen * Math.cos(angle + Math.PI / 8), - toy - headlen * Math.sin(angle + Math.PI / 8), - ) - context.closePath() - context.fill() -} - -export default class BreakpointSplitRenderer extends ComparativeServerSideRendererType { - async makeImageData(props: RenderArgsDeserialized) { - const { - highResolutionScaling: scale = 1, - width, - height, - middle, - linkedTrack, - config, - } = props - - const trackId = linkedTrack - const canvas = createCanvas(Math.ceil(width * scale), height * scale) - const ctx = canvas.getContext('2d') - ctx.scale(scale, scale) - ctx.strokeStyle = readConfObject(config, 'color') - ctx.fillStyle = readConfObject(config, 'color') - const drawMode = readConfObject(config, 'drawMode') - const views = instantiateTrackLayoutFeatures(props.views) - - for (const chunk of generateLayoutMatches(views, trackId, middle)) { - // we follow a path in the list of chunks, not from top to bottom, just in series - // following x1,y1 -> x2,y2 - chunk.sort((a, b) => a.feature.get('clipPos') - b.feature.get('clipPos')) - for (let i = 0; i < chunk.length - 1; i += 1) { - const { layout: c1, feature: f1, level: level1, refName: ref1 } = chunk[ - i - ] - const { layout: c2, feature: f2, level: level2, refName: ref2 } = chunk[ - i + 1 - ] - const v1 = views[level1] - const v2 = views[level2] - - if (!c1 || !c2) { - console.warn('received null layout for a overlay feature') - continue - } - - // possible TODO - // restore refName mapping for alternative refNames - - // disable rendering connections in a single level - // if (!showIntraviewLinks && level1 === level2) { - // continue - // } - - const x1 = getPxFromCoordinate( - v1, - ref1, - c1[f1.get('strand') === -1 ? LEFT : RIGHT], - ) - const x2 = getPxFromCoordinate( - v2, - ref2, - c2[f2.get('strand') === -1 ? RIGHT : LEFT], - ) - - // const tracks = views.map(v => v.getTrack(trackConfigId)) - - const y1 = middle - ? interstitialYPos(level1 < level2, height) - : overlayYPos(trackId, level1, views, c1, level1 < level2) - const y2 = middle - ? interstitialYPos(level2 < level1, height) - : overlayYPos(trackId, level2, views, c2, level2 < level1) - - // possible todo: use totalCurveHeight to possibly make alternative squiggle if the S is too small - // - - if (drawMode === 'spline') { - ctx.beginPath() - ctx.moveTo(x1, y1) - ctx.bezierCurveTo( - x1 + 200 * f1.get('strand'), - y1, - x2 - 200 * f2.get('strand'), - y2, - x2, - y2, - ) - ctx.stroke() - drawArrow(ctx, x2 - 10 * f2.get('strand'), y2, x2, y2) - } else { - ctx.beginPath() - ctx.moveTo(x1 + 10 * f1.get('strand') * -1, y1) - ctx.lineTo(x1, y1) - ctx.lineTo(x2 - 10 * f2.get('strand') * -1, y2) - ctx.lineTo(x2, y2) - ctx.stroke() - // drawArrow( - // ctx, - // x2 - 10 * f2.get('strand') * flipMultipliers[level2], - // y2, - // x2, - // y2, - // ) - } - } - } - - const imageData = await createImageBitmap(canvas) - return { imageData } - } - - async render(renderProps: RenderArgsDeserialized) { - const { height, width } = renderProps - const { imageData } = await this.makeImageData(renderProps) - - const results = await super.render({ ...renderProps, imageData }) - - return { - ...results, - imageData, - height, - width, - } - } -} diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/__image_snapshots__/linear-synteny-renderer-test-ts-test-rendering-a-simple-synteny-from-fake-data-1-snap.png b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/__image_snapshots__/linear-synteny-renderer-test-ts-test-rendering-a-simple-synteny-from-fake-data-1-snap.png deleted file mode 100644 index b2757f16d03..00000000000 Binary files a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/__image_snapshots__/linear-synteny-renderer-test-ts-test-rendering-a-simple-synteny-from-fake-data-1-snap.png and /dev/null differ diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/components/BreakpointSplitRendering.tsx b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/components/BreakpointSplitRendering.tsx deleted file mode 100644 index d4e16163a3a..00000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/components/BreakpointSplitRendering.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useRef, useMemo, useEffect } from 'react' -import { getParent, isStateTreeNode } from 'mobx-state-tree' -import { observer, PropTypes } from 'mobx-react' -import { ImageBitmapType } from '@jbrowse/core/util/offscreenCanvasPonyfill' -import { RenderArgsDeserializedWithImageData } from '../BreakpointSplitRenderer' - -/** - * A block whose content is rendered outside of the main thread and hydrated by this - * component. - */ -function BreakpointSplitRendering(props: RenderArgsDeserializedWithImageData) { - const { - displayModel = {}, - width, - height, - highResolutionScaling = 1, - imageData, - } = props - const voffs = useMemo(() => { - const ret = [0, 0] - if (displayModel && isStateTreeNode(displayModel)) { - // @ts-ignore - const { viewOffsets } = displayModel - const { views } = getParent(displayModel, 3) - for (let i = 0; i < views.length; i++) { - ret.push(views[i].offsetPx - viewOffsets[i]) - } - } - return ret - }, [displayModel]) - - const featureCanvas = useRef(null) - - useEffect(() => { - if (!imageData) { - return - } - const canvas = featureCanvas.current - if (!canvas) { - return - } - const context = canvas.getContext('2d') - if (!context) { - return - } - context.clearRect(0, 0, width, height) - context.resetTransform() - // see https://en.wikipedia.org/wiki/Transformation_matrix#/media/File:2D_affine_transformation_matrix.svg - context.transform(1, 0, -(voffs[1] - voffs[0]) / height, 1, -voffs[0], 0) - if (imageData.commands) { - imageData.commands.forEach( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (command: { style: string; type: string; args: any[] }) => { - if (command.type === 'strokeStyle') { - context.strokeStyle = command.style - } else if (command.type === 'fillStyle') { - context.fillStyle = command.style - } else if (command.type === 'font') { - context.font = command.style - } else { - // @ts-ignore - context[command.type](...command.args) - } - }, - ) - } else if (imageData instanceof ImageBitmapType) { - context.drawImage(imageData, 0, 0) - } else if (imageData.dataURL) { - const img = new Image() - img.onload = () => context.drawImage(img, 0, 0) - img.src = imageData.dataURL - } - }, [height, imageData, voffs, width]) - - return ( - - ) -} - -BreakpointSplitRendering.propTypes = { - displayModel: PropTypes.observableObject, -} - -export default observer(BreakpointSplitRendering) diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/configSchema.js b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/configSchema.js deleted file mode 100644 index 586dab88a94..00000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/configSchema.js +++ /dev/null @@ -1,20 +0,0 @@ -import { ConfigurationSchema } from '@jbrowse/core/configuration' -import { types } from 'mobx-state-tree' - -export default ConfigurationSchema( - 'BreakpointSplitRenderer', - { - drawMode: { - type: 'stringEnum', - description: 'drawing mode', - model: types.enumeration('Rendering', ['spline', 'bracket']), - defaultValue: 'spline', - }, - color: { - type: 'color', - description: 'the color of each feature in a synteny', - defaultValue: 'black', - }, - }, - { explicitlyTyped: true }, -) diff --git a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/index.js b/plugins/linear-comparative-view/src/BreakpointSplitRenderer/index.js deleted file mode 100644 index dd088617e98..00000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitRenderer/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { default as ReactComponent } from './components/BreakpointSplitRendering' -export { default as configSchema } from './configSchema' -export { default } from './BreakpointSplitRenderer' diff --git a/plugins/linear-comparative-view/src/BreakpointSplitView/index.ts b/plugins/linear-comparative-view/src/BreakpointSplitView/index.ts deleted file mode 100644 index e50ff18c32b..00000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitView/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Feature } from '@jbrowse/core/util/simpleFeature' -import PluginManager from '@jbrowse/core/PluginManager' - -import { IAnyStateTreeNode } from 'mobx-state-tree' -import modelF from './model' -import componentF from '../LinearComparativeView/components/LinearComparativeView' - -export default ({ lib, load }: PluginManager) => { - const ViewType = lib['@jbrowse/core/pluggableElementTypes/ViewType'] - const { getSession } = lib['@jbrowse/core/util'] - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ReactComponent = load(componentF) as any - const stateModel = load(modelF) - - class BreakpointSplitViewType extends ViewType { - snapshotFromBreakendFeature( - feature: Feature, - view: IAnyStateTreeNode & { - displayedRegions: [ - { assemblyName: string; start: number; end: number; refName: string }, - ] - }, - ) { - const breakendSpecification = (feature.get('ALT') || [])[0] - const startPos = feature.get('start') - let endPos - const bpPerPx = 10 - - // TODO: Figure this out for multiple assembly names - const { assemblyName } = view.displayedRegions[0] - const assembly = getSession(view).assemblyManager.get(assemblyName) - if (!assembly) { - throw new Error('assembly not yet loaded') - } - const { getCanonicalRefName } = assembly - const featureRefName = getCanonicalRefName(feature.get('refName')) - - const topRegion = view.displayedRegions.find( - (f: { refName: string }) => f.refName === String(featureRefName), - ) - - let mateRefName: string | undefined - let startMod = 0 - let endMod = 0 - - if (breakendSpecification) { - // a VCF breakend feature - if (breakendSpecification === '') { - const INFO = feature.get('INFO') || [] - endPos = INFO.END[0] - 1 - mateRefName = getCanonicalRefName(INFO.CHR2[0]) - } else if (breakendSpecification.MatePosition) { - const matePosition = breakendSpecification.MatePosition.split(':') - endPos = parseInt(matePosition[1], 10) - 1 - mateRefName = getCanonicalRefName(matePosition[0]) - if (breakendSpecification.Join === 'left') { - startMod = -1 - } - if (breakendSpecification.MateDirection === 'left') { - endMod = -1 - } - } - - // if (breakendSpecification.Join === 'left') { - // marker -1, else 0 - - // if (breakendSpecification.MateDirection === 'left') { - // marker -1, else 0 - } else if (feature.get('mate')) { - // a generic 'mate' feature - const mate = feature.get('mate') - mateRefName = getCanonicalRefName(mate.refName) - endPos = mate.start - } - - if (!mateRefName) { - console.warn( - `unable to resolve mate refName ${mateRefName} in reference genome`, - ) - return undefined - } - - const bottomRegion = view.displayedRegions.find( - f => f.refName === String(mateRefName), - ) - - if (!topRegion || !bottomRegion) { - console.warn( - `unable to find the refName for the top or bottom of the breakpoint view`, - ) - return undefined - } - - const topMarkedRegion = [{ ...topRegion }, { ...topRegion }] - const bottomMarkedRegion = [{ ...bottomRegion }, { ...bottomRegion }] - topMarkedRegion[0].end = startPos + startMod - topMarkedRegion[1].start = startPos + startMod - bottomMarkedRegion[0].end = endPos + endMod - bottomMarkedRegion[1].start = endPos + endMod - const snapshot = { - type: 'BreakpointSplitView', - views: [ - { - type: 'LinearGenomeView', - displayedRegions: topMarkedRegion, - hideHeader: true, - bpPerPx, - offsetPx: (topRegion.start + feature.get('start')) / bpPerPx, - }, - { - type: 'LinearGenomeView', - displayedRegions: bottomMarkedRegion, - hideHeader: true, - bpPerPx, - offsetPx: (bottomRegion.start + endPos) / bpPerPx, - }, - ], - displayName: `${ - feature.get('name') || feature.get('id') || 'breakend' - } split detail`, - } - return snapshot - } - } - - return new BreakpointSplitViewType({ - name: 'BreakpointSplitView', - stateModel, - ReactComponent, - }) -} diff --git a/plugins/linear-comparative-view/src/BreakpointSplitView/model.ts b/plugins/linear-comparative-view/src/BreakpointSplitView/model.ts deleted file mode 100644 index 0a7077fa594..00000000000 --- a/plugins/linear-comparative-view/src/BreakpointSplitView/model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { types, Instance } from 'mobx-state-tree' -import baseModel from '../LinearComparativeView/model' - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default function stateModelFactory(pluginManager: any) { - return types.compose( - baseModel(pluginManager), - types.model('BreakpointSplitView', { - type: types.literal('BreakpointSplitView'), - }), - ) -} -export type BreakpointSplitView = ReturnType -export type BreakpointSplitViewModel = Instance diff --git a/plugins/linear-comparative-view/src/util.ts b/plugins/linear-comparative-view/src/util.ts index dc330cc5402..9b2b7e21e55 100644 --- a/plugins/linear-comparative-view/src/util.ts +++ b/plugins/linear-comparative-view/src/util.ts @@ -18,7 +18,6 @@ export interface ReducedLinearGenomeView { scrollTop: number height: number configuration: string - layoutFeatures: [string, LayoutRecord][] skip: number }[] } diff --git a/plugins/linear-genome-view/package.json b/plugins/linear-genome-view/package.json index fec6a219158..c6e5c8356a6 100644 --- a/plugins/linear-genome-view/package.json +++ b/plugins/linear-genome-view/package.json @@ -43,7 +43,6 @@ "is-object": "^1.0.1", "json-stable-stringify": "^1.0.1", "normalize-wheel": "^1.0.1", - "rbush": "^3.0.1", "react-sizeme": "^2.6.7" }, "peerDependencies": { diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx index 62511e25e97..b5107655592 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx @@ -18,7 +18,6 @@ import Typography from '@material-ui/core/Typography' import MenuOpenIcon from '@material-ui/icons/MenuOpen' import { autorun } from 'mobx' import { addDisposer, Instance, isAlive, types, getEnv } from 'mobx-state-tree' -import RBush from 'rbush' import React from 'react' import { Tooltip } from '../components/BaseLinearDisplay' import BlockState, { renderBlockData } from './serverSideRenderedBlock' @@ -117,8 +116,6 @@ export const BaseLinearDisplay = types }, })) .views(self => { - let stale = false // used to make rtree refresh, the mobx reactivity fails for some reason - let rbush: Record> = {} return { /** * a CompositeMap of `featureId -> feature obj` that @@ -131,7 +128,6 @@ export const BaseLinearDisplay = types featureMaps.push(block.features) } } - stale = true return new CompositeMap(featureMaps) }, @@ -155,53 +151,22 @@ export const BaseLinearDisplay = types layoutMaps.set(block.key, block.layout.getRectangles()) } } - stale = true return layoutMaps }, - /** - * a CompositeMap of `featureId -> feature obj` that - * just looks in all the block data for that feature - * - * when you are not using the rtree you can use this - * method because it still provides a stable reference - * of a featureId to a layout record (when using the - * rtree, you cross contaminate the coordinates) - */ - get layoutFeatures() { - const layoutMaps = [] - for (const block of self.blockState.values()) { - if (block && block.layout && block.layout.rectangles) { - layoutMaps.push(block.layout.getRectangles()) - } - } - stale = true // make rtree refresh - return new CompositeMap(layoutMaps) + + getFeatureOverlapping(blockKey: string, x: number, y: number) { + return self.blockState.get(blockKey)?.layout?.getByCoord(x, y) }, - get rtree() { - if (stale) { - rbush = {} as Record> - for (const [blockKey, layoutFeatures] of this.blockLayoutFeatures) { - rbush[blockKey] = new RBush() - const r = rbush[blockKey] - for (const [key, layout] of layoutFeatures) { - r.insert({ - minX: layout[0], - minY: layout[1], - maxX: layout[2], - maxY: layout[3], - name: key, - }) - } + getFeatureByID(id: string) { + let ret + self.blockState.forEach(block => { + const val = block?.layout?.getByID(id) + if (val) { + ret = val } - stale = false - } - return rbush - }, - getFeatureOverlapping(blockKey: string, x: number, y: number) { - const rect = { minX: x, minY: y, maxX: x + 1, maxY: y + 1 } - const rtree = this.rtree[blockKey] - return rtree && rtree.collides(rect) ? rtree.search(rect) : [] + }) + return ret }, } }) diff --git a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js index 2ce313be067..4822f03a5a9 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js +++ b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.js @@ -273,14 +273,11 @@ function SvgFeatureRendering(props) { const px = region.reversed ? width - offsetX : offsetX const clientBp = region.start + bpPerPx * px - const feats = displayModel.getFeatureOverlapping( + const featureIdCurrentlyUnderMouse = displayModel.getFeatureOverlapping( blockKey, clientBp, offsetY, ) - const featureIdCurrentlyUnderMouse = feats.length - ? feats[0].name - : undefined if (onMouseMove) { onMouseMove(event, featureIdCurrentlyUnderMouse) diff --git a/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap b/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap index 5ddb28c417b..641370571a1 100644 --- a/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap +++ b/products/jbrowse-web/src/tests/__snapshots__/BreakpointSplitView.test.js.snap @@ -7,12 +7,12 @@ exports[`breakpoint split view open a split view 1`] = ` stroke="#333" > diff --git a/test_data/volvox/config_main_thread.json b/test_data/volvox/config_main_thread.json index 0c9723c7422..7c111e3c947 100644 --- a/test_data/volvox/config_main_thread.json +++ b/test_data/volvox/config_main_thread.json @@ -36,5 +36,60 @@ } } } + ], + "tracks": [ + { + "type": "AlignmentsTrack", + "trackId": "volvox_bam_pileup", + "name": "volvox-sorted.bam (contigA LinearPileupDisplay)", + "category": ["Integration test"], + "assemblyNames": ["volvox"], + "adapter": { + "type": "BamAdapter", + "bamLocation": { + "uri": "volvox-sorted-altname.bam" + }, + "index": { + "location": { + "uri": "volvox-sorted-altname.bam.bai" + } + } + }, + "displays": [ + { + "type": "LinearPileupDisplay", + "displayId": "volvox_bam_pileup_pileup" + } + ] + }, + { + "type": "AlignmentsTrack", + "trackId": "volvox_alignments", + "name": "volvox-sorted.bam (ctgA, svg)", + "category": ["Integration test"], + "assemblyNames": ["volvox", "volvox2"], + "adapter": { + "type": "BamAdapter", + "bamLocation": { + "uri": "volvox-sorted.bam" + }, + "index": { + "location": { + "uri": "volvox-sorted.bam.bai" + } + } + }, + "displays": [ + { + "type": "LinearAlignmentsDisplay", + "displayId": "volvox_alignments_alignments", + "pileupDisplay": { + "type": "LinearPileupDisplay", + "displayId": "volvox_bam_altname_alignments_pileup", + "defaultRendering": "svg" + } + } + ] + } ] }