@jbrowse/plugin-linear-genome-view
Version:
JBrowse 2 linear genome view
287 lines (286 loc) • 10.6 kB
JavaScript
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();