UNPKG

@jbrowse/plugin-linear-genome-view

Version:

JBrowse 2 linear genome view

433 lines (432 loc) 16.7 kB
import { lazy } from 'react'; import { ConfigurationReference, getConf } from '@jbrowse/core/configuration'; import { BaseDisplay } from '@jbrowse/core/pluggableElementTypes/models'; import { getContainingTrack, getContainingView, getSession, isFeature, isSelectionContainer, isSessionModelWithWidgets, } from '@jbrowse/core/util'; import CompositeMap from '@jbrowse/core/util/compositeMap'; import { getParentRenderProps, getRpcSessionId, } from '@jbrowse/core/util/tracks'; import { addDisposer, flow, isAlive, types } from '@jbrowse/mobx-state-tree'; import CenterFocusStrongIcon from '@mui/icons-material/CenterFocusStrong'; import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import { autorun } from 'mobx'; import { calculateSvgLegendWidth } from "./calculateSvgLegendWidth.js"; import { deduplicateFeatureLabels } from "./components/util.js"; import FeatureDensityMixin from "./models/FeatureDensityMixin.js"; import TrackHeightMixin from "./models/TrackHeightMixin.js"; import configSchema from "./models/configSchema.js"; import BlockState from "./models/serverSideRenderedBlock.js"; import { fetchFeatureByIdRpc, findSubfeatureById, getTranscripts, hasIntrons, } from "./util.js"; const Tooltip = lazy(() => import("./components/Tooltip.js")); const CollapseIntronsDialog = lazy(() => import("./components/CollapseIntronsDialog/CollapseIntronsDialog.js")); function stateModelFactory() { return types .compose('BaseLinearDisplay', BaseDisplay, TrackHeightMixin(), FeatureDensityMixin(), types.model({ blockState: types.map(BlockState), configuration: ConfigurationReference(configSchema), showLegend: types.maybe(types.boolean), showTooltips: types.maybe(types.boolean), })) .volatile(() => ({ mouseoverExtraInformation: undefined, featureIdUnderMouse: undefined, subfeatureIdUnderMouse: undefined, contextMenuFeature: undefined, })) .views(self => ({ get DisplayMessageComponent() { return undefined; }, get blockType() { return 'staticBlocks'; }, get blockDefinitions() { const view = getContainingView(self); if (!view.initialized) { throw new Error('view not initialized yet'); } return view[this.blockType]; }, })) .views(self => ({ get renderDelay() { return 50; }, get TooltipComponent() { return Tooltip; }, legendItems(_theme) { return []; }, svgLegendWidth(theme) { return self.showLegend ? calculateSvgLegendWidth(this.legendItems(theme)) : 0; }, get selectedFeatureId() { if (isAlive(self)) { const { selection } = getSession(self); if (isFeature(selection)) { return selection.id(); } } return undefined; }, get featureWidgetType() { return { type: 'BaseFeatureWidget', id: 'baseFeature', }; }, })) .views(self => ({ get showTooltipsEnabled() { return self.showTooltips ?? true; }, get features() { const featureMaps = []; for (const block of self.blockState.values()) { if (block.features) { featureMaps.push(block.features); } } return new CompositeMap(featureMaps); }, get featureUnderMouse() { const feat = self.featureIdUnderMouse; return feat ? this.features.get(feat) : undefined; }, getFeatureById(featureId, parentFeatureId) { const feature = this.features.get(featureId); if (feature) { return feature; } if (parentFeatureId) { const parent = this.features.get(parentFeatureId); if (parent) { return findSubfeatureById(parent, featureId); } } return undefined; }, get layoutFeatures() { const featureMaps = []; for (const block of self.blockState.values()) { if (block.layout?.getRectangles) { featureMaps.push(block.layout.getRectangles()); } } return new CompositeMap(featureMaps); }, getFeatureOverlapping(blockKey, x, y) { return self.blockState.get(blockKey)?.layout?.getByCoord(x, y); }, getFeatureByID(blockKey, id) { return self.blockState.get(blockKey)?.layout?.getByID(id); }, searchFeatureByID(id) { for (const block of self.blockState.values()) { const val = block.layout?.getByID(id); if (val) { return val; } } return undefined; }, get floatingLabelData() { const view = getContainingView(self); const { assemblyManager } = getSession(self); const assemblyName = view.assemblyNames[0]; const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined; return deduplicateFeatureLabels(this.layoutFeatures, view, assembly, view.bpPerPx); }, })) .actions(self => ({ addBlock(key, block) { const blockInstance = BlockState.create({ key, region: block.toRegion(), }); blockInstance.setCachedDisplay(self); self.blockState.set(key, blockInstance); }, deleteBlock(key) { self.blockState.delete(key); }, selectFeature(feature) { const session = getSession(self); if (isSelectionContainer(session)) { session.setSelection(feature); } if (isSessionModelWithWidgets(session)) { const { rpcManager } = session; const sessionId = getRpcSessionId(self); const track = getContainingTrack(self); const view = getContainingView(self); const adapterConfig = getConf(track, 'adapter'); const { type, id } = self.featureWidgetType; rpcManager .call(sessionId, 'CoreGetMetadata', { adapterConfig }) .then(descriptions => { if (isAlive(self)) { session.showWidget(session.addWidget(type, id, { featureData: feature.toJSON(), view, track, descriptions, })); } }) .catch((e) => { console.error(e); getSession(self).notifyError(`${e}`, e); }); } }, navToFeature(feature) { const view = getContainingView(self); view.navTo({ refName: feature.get('refName'), start: feature.get('start'), end: feature.get('end'), }); }, clearFeatureSelection() { getSession(self).clearSelection(); }, setFeatureIdUnderMouse(feature) { self.featureIdUnderMouse = feature; }, setSubfeatureIdUnderMouse(subfeatureId) { self.subfeatureIdUnderMouse = subfeatureId; }, setContextMenuFeature(feature) { self.contextMenuFeature = feature; }, setMouseoverExtraInformation(extra) { self.mouseoverExtraInformation = extra; }, setShowLegend(s) { self.showLegend = s; }, setShowTooltips(arg) { self.showTooltips = arg; }, })) .actions(self => { const { reload: superReload } = self; return { async reload() { self.setError(); self.setCurrStatsBpPerPx(0); self.clearFeatureDensityStats(); for (const val of self.blockState.values()) { val.doReload(); } superReload(); }, }; }) .actions(self => ({ selectFeatureById: flow(function* (featureId, parentFeatureId, topLevelFeatureId) { const feature = self.getFeatureById(featureId, parentFeatureId); if (feature) { self.selectFeature(feature); return; } const rpcParentId = topLevelFeatureId && topLevelFeatureId !== featureId ? topLevelFeatureId : parentFeatureId; try { const session = getSession(self); const f = yield fetchFeatureByIdRpc({ rpcManager: session.rpcManager, sessionId: getRpcSessionId(self), trackId: getContainingTrack(self).id, rendererType: self.rendererTypeName, featureId, parentFeatureId: rpcParentId, }); if (f && isAlive(self)) { self.selectFeature(f); } } catch (e) { console.error(e); getSession(self).notifyError(`${e}`, e); } }), setContextMenuFeatureById: flow(function* (featureId, parentFeatureId, topLevelFeatureId) { const feature = self.getFeatureById(featureId, parentFeatureId); if (feature) { self.setContextMenuFeature(feature); return; } const rpcParentId = topLevelFeatureId && topLevelFeatureId !== featureId ? topLevelFeatureId : parentFeatureId; try { const session = getSession(self); const f = yield fetchFeatureByIdRpc({ rpcManager: session.rpcManager, sessionId: getRpcSessionId(self), trackId: getContainingTrack(self).id, rendererType: self.rendererTypeName, featureId, parentFeatureId: rpcParentId, }); if (f && isAlive(self)) { self.setContextMenuFeature(f); } } catch (e) { console.error(e); getSession(self).notifyError(`${e}`, e); } }), })) .views(self => ({ trackMenuItems() { return []; }, contextMenuItems() { const feat = self.contextMenuFeature; const transcripts = getTranscripts(feat); return feat ? [ { label: 'Open feature details', icon: MenuOpenIcon, onClick: () => { self.selectFeature(feat); }, }, { label: 'Zoom to feature', icon: CenterFocusStrongIcon, onClick: () => { self.navToFeature(feat); }, }, { label: 'Copy info to clipboard', icon: ContentCopyIcon, onClick: async () => { const { uniqueId, ...rest } = feat.toJSON(); const session = getSession(self); const { default: copy } = await import('copy-to-clipboard'); copy(JSON.stringify(rest, null, 4)); session.notify('Copied to clipboard', 'success'); }, }, ...(hasIntrons(transcripts) ? [ { label: 'Collapse introns', icon: CloseFullscreenIcon, onClick: () => { const view = getContainingView(self); const { assemblyManager } = getSession(self); const assembly = assemblyManager.get(view.assemblyNames[0]); if (assembly) { getSession(self).queueDialog(handleClose => [ CollapseIntronsDialog, { view, transcripts, handleClose, assembly, }, ]); } }, }, ] : []), ] : []; }, renderingProps() { return { displayModel: self, onMouseMove(_, featureId) { self.setFeatureIdUnderMouse(featureId); }, onMouseLeave(_) { self.setFeatureIdUnderMouse(undefined); }, onContextMenu(_) { self.setContextMenuFeature(undefined); self.clearFeatureSelection(); }, onFeatureClick(_, featureId) { if (featureId) { self.selectFeatureById(featureId).catch((e) => { console.error(e); getSession(self).notifyError(`${e}`, e); }); } }, }; }, renderProps() { return { ...getParentRenderProps(self), notReady: !self.featureDensityStatsReady, rpcDriverName: self.effectiveRpcDriverName, }; }, })) .actions(self => ({ async renderSvg(opts) { const { renderBaseLinearDisplaySvg } = await import("./renderSvg.js"); return renderBaseLinearDisplaySvg(self, opts); }, afterAttach() { addDisposer(self, autorun(function blockDefinitionsAutorun() { try { if (!isAlive(self) || self.isMinimized) { return; } const view = getContainingView(self); if (!view.initialized) { return; } const contentBlocks = self.blockDefinitions.contentBlocks; const newKeys = new Set(contentBlocks.map(b => b.key)); for (const block of contentBlocks) { if (!self.blockState.has(block.key)) { self.addBlock(block.key, block); } } for (const key of self.blockState.keys()) { if (!newKeys.has(key)) { self.deleteBlock(key); } } } catch (e) { } }, { name: 'BaseLinearDisplayBlockDefinitions', delay: 60, })); }, })) .preProcessSnapshot(snap => { if (!snap) { return snap; } const { height, ...rest } = snap; return { heightPreConfig: height, ...rest }; }) .postProcessSnapshot(snap => { const r = snap; const { blockState, ...rest } = r; return rest; }); } export const BaseLinearDisplay = stateModelFactory(); export {} from "./components/FloatingLegend.js";