UNPKG

@jbrowse/plugin-linear-genome-view

Version:

JBrowse 2 linear genome view

287 lines (286 loc) 10.6 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 CenterFocusStrongIcon from '@mui/icons-material/CenterFocusStrong'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import copy from 'copy-to-clipboard'; import { autorun } from 'mobx'; import { addDisposer, isAlive, types } from 'mobx-state-tree'; import FeatureDensityMixin from './FeatureDensityMixin'; import TrackHeightMixin from './TrackHeightMixin'; import configSchema from './configSchema'; import BlockState from './serverSideRenderedBlock'; const Tooltip = lazy(() => import('../components/Tooltip')); function stateModelFactory() { return types .compose('BaseLinearDisplay', BaseDisplay, TrackHeightMixin(), FeatureDensityMixin(), types.model({ blockState: types.map(BlockState), configuration: ConfigurationReference(configSchema), })) .volatile(() => ({ featureIdUnderMouse: 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; }, get selectedFeatureId() { if (isAlive(self)) { const { selection } = getSession(self); if (isFeature(selection)) { return selection.id(); } } return undefined; }, copyInfoToClipboard(feature) { const { uniqueId, ...rest } = feature.toJSON(); const session = getSession(self); copy(JSON.stringify(rest, null, 4)); session.notify('Copied to clipboard', 'success'); }, })) .views(self => ({ 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; }, getFeatureOverlapping(blockKey, x, y) { var _a, _b; return (_b = (_a = self.blockState.get(blockKey)) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.getByCoord(x, y); }, getFeatureByID(blockKey, id) { var _a, _b; return (_b = (_a = self.blockState.get(blockKey)) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.getByID(id); }, searchFeatureByID(id) { var _a; let ret; for (const block of self.blockState.values()) { const val = (_a = block.layout) === null || _a === void 0 ? void 0 : _a.getByID(id); if (val) { ret = val; } } return ret; }, })) .actions(self => ({ addBlock(key, block) { self.blockState.set(key, BlockState.create({ key, region: block.toRegion(), })); }, deleteBlock(key) { self.blockState.delete(key); }, selectFeature(feature) { const session = getSession(self); if (isSessionModelWithWidgets(session)) { const { rpcManager } = session; const sessionId = getRpcSessionId(self); const track = getContainingTrack(self); const view = getContainingView(self); const adapterConfig = getConf(track, 'adapter'); (async () => { try { const descriptions = await rpcManager.call(sessionId, 'CoreGetMetadata', { adapterConfig, }); session.showWidget(session.addWidget('BaseFeatureWidget', 'baseFeature', { featureData: feature.toJSON(), view, track, descriptions, })); } catch (e) { console.error(e); getSession(e).notifyError(`${e}`, e); } })(); } if (isSelectionContainer(session)) { session.setSelection(feature); } }, 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; }, setContextMenuFeature(feature) { self.contextMenuFeature = feature; }, })) .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(); }, }; }) .views(self => ({ trackMenuItems() { return []; }, contextMenuItems() { const feat = self.contextMenuFeature; 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: () => { self.copyInfoToClipboard(feat); }, }, ] : []; }, renderProps() { return { ...getParentRenderProps(self), notReady: !self.featureDensityStatsReady, rpcDriverName: self.rpcDriverName, displayModel: self, onFeatureClick(_, featureId) { const f = featureId || self.featureIdUnderMouse; if (!f) { self.clearFeatureSelection(); } else { const feature = self.features.get(f); if (feature) { self.selectFeature(feature); } } }, onClick() { self.clearFeatureSelection(); }, onFeatureContextMenu(_, featureId) { const f = featureId || self.featureIdUnderMouse; if (!f) { self.clearFeatureSelection(); } else { self.setContextMenuFeature(self.features.get(f)); } }, onMouseMove(_, featureId) { self.setFeatureIdUnderMouse(featureId); }, onMouseLeave(_) { self.setFeatureIdUnderMouse(undefined); }, onContextMenu() { self.setContextMenuFeature(undefined); self.clearFeatureSelection(); }, }; }, })) .actions(self => ({ async renderSvg(opts) { const { renderBaseLinearDisplaySvg } = await import('./renderSvg'); return renderBaseLinearDisplaySvg(self, opts); }, afterAttach() { addDisposer(self, autorun(() => { const blocksPresent = {}; const view = getContainingView(self); if (!view.initialized) { return; } for (const block of self.blockDefinitions.contentBlocks) { blocksPresent[block.key] = true; if (!self.blockState.has(block.key)) { self.addBlock(block.key, block); } } for (const key of self.blockState.keys()) { if (!blocksPresent[key]) { self.deleteBlock(key); } } })); }, })) .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();