@jbrowse/plugin-linear-genome-view
Version:
JBrowse 2 linear genome view
433 lines (432 loc) • 16.7 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 { 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";