molstar
Version:
A comprehensive macromolecular library.
370 lines (369 loc) • 19 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Ventura Rivera <venturaxrivera@gmail.com>
*/
import * as React from 'react';
import { PluginUIComponent } from './base';
import { PluginStateObject as PSO } from '../mol-plugin-state/objects';
import { Sequence } from './sequence/sequence';
import { Structure, StructureElement, StructureProperties as SP, Unit } from '../mol-model/structure';
import { PolymerSequenceWrapper } from './sequence/polymer';
import { MarkerAction } from '../mol-util/marker-action';
import { PureSelectControl } from './controls/parameters';
import { ParamDefinition as PD } from '../mol-util/param-definition';
import { HeteroSequenceWrapper } from './sequence/hetero';
import { StateSelection } from '../mol-state';
import { ChainSequenceWrapper } from './sequence/chain';
import { ElementSequenceWrapper } from './sequence/element';
import { elementLabel } from '../mol-theme/label';
import { Icon, HelpOutlineSvg } from './controls/icons';
import { arrayEqual } from '../mol-util/array';
const MaxDisplaySequenceLength = 5000;
// TODO: add virtualized Select controls (at best with a search box)?
const MaxSelectOptionsCount = 1000;
const MaxSequenceWrappersCount = 30;
export function opKey(l) {
const ids = SP.unit.pdbx_struct_oper_list_ids(l);
const ncs = SP.unit.struct_ncs_oper_id(l);
const hkl = SP.unit.hkl(l);
const spgrOp = SP.unit.spgrOp(l);
return `${ids.sort().join(',')}|${ncs}|${hkl}|${spgrOp}`;
}
export function splitModelEntityId(modelEntityId) {
const [modelIdx, entityId] = modelEntityId.split('|');
return [parseInt(modelIdx), entityId];
}
export function getSequenceWrapper(state, structureSelection) {
const { structure, modelEntityId, chainGroupId, operatorKey } = state;
const l = StructureElement.Location.create(structure);
const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
const units = [];
for (const unit of structure.units) {
StructureElement.Location.set(l, structure, unit, unit.elements[0]);
if (structure.getModelIndex(unit.model) !== modelIdx)
continue;
if (SP.entity.id(l) !== entityId)
continue;
if (unit.chainGroupId !== chainGroupId)
continue;
if (opKey(l) !== operatorKey)
continue;
units.push(unit);
}
if (units.length > 0) {
const data = { structure, units };
const unit = units[0];
let sw;
if (unit.polymerElements.length) {
const l = StructureElement.Location.create(structure, unit, unit.elements[0]);
const entitySeq = unit.model.sequence.byEntityKey[SP.entity.key(l)];
// check if entity sequence is available
if (entitySeq && entitySeq.sequence.length <= MaxDisplaySequenceLength) {
sw = new PolymerSequenceWrapper(data);
}
else {
const polymerElementCount = units.reduce((a, v) => a + v.polymerElements.length, 0);
if (Unit.isAtomic(unit) || polymerElementCount > MaxDisplaySequenceLength) {
sw = new ChainSequenceWrapper(data);
}
else {
sw = new ElementSequenceWrapper(data);
}
}
}
else if (Unit.isAtomic(unit)) {
const residueCount = units.reduce((a, v) => a + v.residueCount, 0);
if (residueCount > MaxDisplaySequenceLength) {
sw = new ChainSequenceWrapper(data);
}
else {
sw = new HeteroSequenceWrapper(data);
}
}
else {
console.warn('should not happen, expecting coarse units to be polymeric');
sw = new ChainSequenceWrapper(data);
}
sw.markResidue(structureSelection.getLoci(structure), MarkerAction.Select);
return sw;
}
else {
return 'No sequence available';
}
}
export function getModelEntityOptions(structure, polymersOnly = false) {
const options = [];
const l = StructureElement.Location.create(structure);
const seen = new Set();
for (const unit of structure.units) {
StructureElement.Location.set(l, structure, unit, unit.elements[0]);
const id = SP.entity.id(l);
const modelIdx = structure.getModelIndex(unit.model);
const key = `${modelIdx}|${id}`;
if (seen.has(key))
continue;
if (polymersOnly && SP.entity.type(l) !== 'polymer')
continue;
let description = SP.entity.pdbx_description(l).join(', ');
if (structure.models.length) {
if (structure.representativeModel) { // indicates model trajectory
description += ` (Model ${structure.models[modelIdx].modelNum})`;
}
else if (description.startsWith('Polymer ')) { // indicates generic entity name
description += ` (${structure.models[modelIdx].entry})`;
}
}
const label = `${id}: ${description}`;
options.push([key, label]);
seen.add(key);
if (options.length > MaxSelectOptionsCount) {
return [['', 'Too many entities']];
}
}
if (options.length === 0)
options.push(['', 'No entities']);
return options;
}
export function getChainOptions(structure, modelEntityId) {
const options = [];
const l = StructureElement.Location.create(structure);
const seen = new Set();
const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
for (const unit of structure.units) {
StructureElement.Location.set(l, structure, unit, unit.elements[0]);
if (structure.getModelIndex(unit.model) !== modelIdx)
continue;
if (SP.entity.id(l) !== entityId)
continue;
const id = unit.chainGroupId;
if (seen.has(id))
continue;
// TODO handle special case
// - more than one chain in a unit
const label = elementLabel(l, { granularity: 'chain', hidePrefix: true, htmlStyling: false });
options.push([id, label]);
seen.add(id);
if (options.length > MaxSelectOptionsCount) {
return [[-1, 'Too many chains']];
}
}
if (options.length === 0)
options.push([-1, 'No chains']);
return options;
}
export function getOperatorOptions(structure, modelEntityId, chainGroupId) {
const options = [];
const l = StructureElement.Location.create(structure);
const seen = new Set();
const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
for (const unit of structure.units) {
StructureElement.Location.set(l, structure, unit, unit.elements[0]);
if (structure.getModelIndex(unit.model) !== modelIdx)
continue;
if (SP.entity.id(l) !== entityId)
continue;
if (unit.chainGroupId !== chainGroupId)
continue;
const id = opKey(l);
if (seen.has(id))
continue;
const label = unit.conformation.operator.name;
options.push([id, label]);
seen.add(id);
if (options.length > MaxSelectOptionsCount) {
return [['', 'Too many operators']];
}
}
if (options.length === 0)
options.push(['', 'No operators']);
return options;
}
export function getStructureOptions(state) {
var _a;
const options = [];
const all = [];
const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure));
for (const s of structures) {
if (!((_a = s.obj) === null || _a === void 0 ? void 0 : _a.data))
continue;
all.push(s.obj.data);
options.push([s.transform.ref, s.obj.data.label]);
}
if (options.length === 0)
options.push(['', 'No structure']);
return { options, all };
}
const SequenceViewModeParam = PD.Select('single', [['single', 'Chain'], ['polymers', 'Polymers'], ['all', 'Everything']]);
export class SequenceView extends PluginUIComponent {
constructor() {
super(...arguments);
this.state = { structureOptions: { options: [], all: [] }, structure: Structure.Empty, structureRef: '', modelEntityId: '', chainGroupId: -1, operatorKey: '', mode: 'single', sequenceViewModeParam: SequenceViewModeParam };
this.setParamProps = (p) => {
const state = { ...this.state };
switch (p.name) {
case 'mode':
state.mode = p.value;
if (this.state.mode === state.mode)
return;
if (state.mode === 'all' || state.mode === 'polymers') {
break;
}
case 'structure':
if (p.name === 'structure')
state.structureRef = p.value;
state.structure = this.getStructure(state.structureRef);
state.modelEntityId = getModelEntityOptions(state.structure)[0][0];
state.chainGroupId = getChainOptions(state.structure, state.modelEntityId)[0][0];
state.operatorKey = getOperatorOptions(state.structure, state.modelEntityId, state.chainGroupId)[0][0];
break;
case 'entity':
state.modelEntityId = p.value;
state.chainGroupId = getChainOptions(state.structure, state.modelEntityId)[0][0];
state.operatorKey = getOperatorOptions(state.structure, state.modelEntityId, state.chainGroupId)[0][0];
break;
case 'chain':
state.chainGroupId = p.value;
state.operatorKey = getOperatorOptions(state.structure, state.modelEntityId, state.chainGroupId)[0][0];
break;
case 'operator':
state.operatorKey = p.value;
break;
}
this.setState(state);
};
}
componentDidMount() {
var _a, _b;
if (this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)).length > 0)
this.setState(this.getInitialState());
this.subscribe(this.plugin.state.events.object.updated, ({ ref, obj }) => {
if (ref === this.state.structureRef && obj && obj.type === PSO.Molecule.Structure.type && obj.data !== this.state.structure) {
this.sync();
}
});
this.subscribe(this.plugin.state.events.object.created, ({ obj }) => {
if (obj && obj.type === PSO.Molecule.Structure.type) {
this.sync();
}
});
this.subscribe(this.plugin.state.events.object.removed, ({ obj }) => {
if (obj && obj.type === PSO.Molecule.Structure.type) {
this.sync();
}
});
const modeOptions = (_b = (_a = this.plugin.spec.components) === null || _a === void 0 ? void 0 : _a.sequenceViewer) === null || _b === void 0 ? void 0 : _b.modeOptions;
if (modeOptions) {
const modeSet = new Set(modeOptions);
const sequenceViewModeParam = {
...SequenceViewModeParam,
options: SequenceViewModeParam.options.filter(([firstItem]) => modeSet.has(firstItem)),
};
this.setState({ sequenceViewModeParam: sequenceViewModeParam });
}
}
sync() {
const structureOptions = getStructureOptions(this.plugin.state.data);
if (arrayEqual(structureOptions.all, this.state.structureOptions.all))
return;
this.setState(this.getInitialState());
}
getStructure(ref) {
const state = this.plugin.state.data;
const cell = state.select(ref)[0];
if (!ref || !cell || !cell.obj)
return Structure.Empty;
return cell.obj.data;
}
getSequenceWrapper(params) {
return {
wrapper: getSequenceWrapper(this.state, this.plugin.managers.structure.selection),
label: `${PD.optionLabel(params.chain, this.state.chainGroupId)} | ${PD.optionLabel(params.entity, this.state.modelEntityId)}`
};
}
getSequenceWrappers(params) {
if (this.state.mode === 'single')
return [this.getSequenceWrapper(params)];
const structure = this.getStructure(this.state.structureRef);
const wrappers = [];
for (const [modelEntityId, eLabel] of getModelEntityOptions(structure, this.state.mode === 'polymers')) {
for (const [chainGroupId, cLabel] of getChainOptions(structure, modelEntityId)) {
for (const [operatorKey] of getOperatorOptions(structure, modelEntityId, chainGroupId)) {
wrappers.push({
wrapper: getSequenceWrapper({
structure,
modelEntityId,
chainGroupId,
operatorKey
}, this.plugin.managers.structure.selection),
label: `${cLabel} | ${eLabel}`
});
if (wrappers.length > MaxSequenceWrappersCount)
return [];
}
}
}
return wrappers;
}
getInitialState() {
var _a, _b, _c, _d;
const structureOptions = getStructureOptions(this.plugin.state.data);
const structureRef = structureOptions.options[0][0];
const structure = this.getStructure(structureRef);
let modelEntityId = getModelEntityOptions(structure)[0][0];
let chainGroupId = getChainOptions(structure, modelEntityId)[0][0];
let operatorKey = getOperatorOptions(structure, modelEntityId, chainGroupId)[0][0];
if (this.state.structure && this.state.structure === structure) {
modelEntityId = this.state.modelEntityId;
chainGroupId = this.state.chainGroupId;
operatorKey = this.state.operatorKey;
}
const defaultMode = (_b = (_a = this.plugin.spec.components) === null || _a === void 0 ? void 0 : _a.sequenceViewer) === null || _b === void 0 ? void 0 : _b.defaultMode;
const initialMode = (_d = (_c = this.props.defaultMode) !== null && _c !== void 0 ? _c : defaultMode) !== null && _d !== void 0 ? _d : 'single';
return { structureOptions, structure, structureRef, modelEntityId, chainGroupId, operatorKey, mode: initialMode, sequenceViewModeParam: this.state.sequenceViewModeParam };
}
get params() {
const { structureOptions, structure, modelEntityId, chainGroupId } = this.state;
const entityOptions = getModelEntityOptions(structure);
const chainOptions = getChainOptions(structure, modelEntityId);
const operatorOptions = getOperatorOptions(structure, modelEntityId, chainGroupId);
return {
structure: PD.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true }),
entity: PD.Select(entityOptions[0][0], entityOptions, { shortLabel: true }),
chain: PD.Select(chainOptions[0][0], chainOptions, { shortLabel: true, twoColumns: true, label: 'Chain' }),
operator: PD.Select(operatorOptions[0][0], operatorOptions, { shortLabel: true, twoColumns: true }),
mode: this.state.sequenceViewModeParam,
};
}
get values() {
return {
structure: this.state.structureRef,
entity: this.state.modelEntityId,
chain: this.state.chainGroupId,
operator: this.state.operatorKey,
mode: this.state.mode
};
}
render() {
if (this.getStructure(this.state.structureRef) === Structure.Empty) {
return _jsx("div", { className: 'msp-sequence', children: _jsxs("div", { className: 'msp-sequence-select', children: [_jsx(Icon, { svg: HelpOutlineSvg, style: { cursor: 'help', position: 'absolute', right: 0, top: 0 }, title: 'Shows a sequence of one or more chains. Use the controls to alter selection.' }), _jsx("span", { children: "Sequence" }), _jsx("span", { style: { fontWeight: 'normal' }, children: "No structure available" })] }) });
}
const params = this.params;
const values = this.values;
const sequenceWrappers = this.getSequenceWrappers(params);
return _jsxs("div", { className: 'msp-sequence', children: [_jsxs("div", { className: 'msp-sequence-select', children: [_jsx(Icon, { svg: HelpOutlineSvg, style: { cursor: 'help', position: 'absolute', right: 0, top: 0 }, title: 'This shows a single sequence. Use the controls to show a different sequence. \nUse Ctrl or Cmd key to add a sequence range to focus; use Shift key to extend last focused/selected range.' }), _jsx("span", { children: "Sequence of" }), _jsx(PureSelectControl, { title: `[Structure] ${PD.optionLabel(params.structure, values.structure)}`, param: params.structure, name: 'structure', value: values.structure, onChange: this.setParamProps }), _jsx(PureSelectControl, { title: `[Mode]`, param: this.state.sequenceViewModeParam, name: 'mode', value: values.mode, onChange: this.setParamProps }), values.mode === 'single' && _jsx(PureSelectControl, { title: `[Entity] ${PD.optionLabel(params.entity, values.entity)}`, param: params.entity, name: 'entity', value: values.entity, onChange: this.setParamProps }), values.mode === 'single' && _jsx(PureSelectControl, { title: `[Chain] ${PD.optionLabel(params.chain, values.chain)}`, param: params.chain, name: 'chain', value: values.chain, onChange: this.setParamProps }), params.operator.options.length > 1 && _jsx(_Fragment, { children: _jsx(PureSelectControl, { title: `[Instance] ${PD.optionLabel(params.operator, values.operator)}`, param: params.operator, name: 'operator', value: values.operator, onChange: this.setParamProps }) })] }), _jsx(NonEmptySequenceWrapper, { children: sequenceWrappers.map((s, i) => {
const elem = typeof s.wrapper === 'string'
? _jsx("div", { className: 'msp-sequence-wrapper', children: s.wrapper }, i)
: _jsx(Sequence, { sequenceWrapper: s.wrapper }, i);
if (values.mode === 'single')
return elem;
return _jsxs(React.Fragment, { children: [_jsx("div", { className: 'msp-sequence-chain-label', children: s.label }), elem] }, i);
}) })] });
}
}
function NonEmptySequenceWrapper({ children }) {
return _jsx("div", { className: 'msp-sequence-wrapper-non-empty', children: children });
}