sequence.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. /**
  2. * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. */
  7. import * as React from 'react';
  8. import { PluginUIComponent } from './base';
  9. import { PluginStateObject as PSO } from '../mol-plugin-state/objects';
  10. import { Sequence } from './sequence/sequence';
  11. import { Structure, StructureElement, StructureProperties as SP, Unit } from '../mol-model/structure';
  12. import { SequenceWrapper } from './sequence/wrapper';
  13. import { PolymerSequenceWrapper } from './sequence/polymer';
  14. import { MarkerAction } from '../mol-util/marker-action';
  15. import { PureSelectControl } from './controls/parameters';
  16. import { ParamDefinition as PD } from '../mol-util/param-definition';
  17. import { HeteroSequenceWrapper } from './sequence/hetero';
  18. import { State, StateSelection } from '../mol-state';
  19. import { ChainSequenceWrapper } from './sequence/chain';
  20. import { ElementSequenceWrapper } from './sequence/element';
  21. import { elementLabel } from '../mol-theme/label';
  22. import { Icon, HelpOutlineSvg } from './controls/icons';
  23. import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
  24. import { arrayEqual } from '../mol-util/array';
  25. const MaxDisplaySequenceLength = 5000;
  26. // TODO: add virtualized Select controls (at best with a search box)?
  27. const MaxSelectOptionsCount = 1000;
  28. const MaxSequenceWrappersCount = 30;
  29. function opKey(l: StructureElement.Location) {
  30. const ids = SP.unit.pdbx_struct_oper_list_ids(l);
  31. const ncs = SP.unit.struct_ncs_oper_id(l);
  32. const hkl = SP.unit.hkl(l);
  33. const spgrOp = SP.unit.spgrOp(l);
  34. return `${ids.sort().join(',')}|${ncs}|${hkl}|${spgrOp}`;
  35. }
  36. function splitModelEntityId(modelEntityId: string) {
  37. const [modelIdx, entityId] = modelEntityId.split('|');
  38. return [parseInt(modelIdx), entityId];
  39. }
  40. function getSequenceWrapper(state: { structure: Structure, modelEntityId: string, chainGroupId: number, operatorKey: string }, structureSelection: StructureSelectionManager): SequenceWrapper.Any | string {
  41. const { structure, modelEntityId, chainGroupId, operatorKey } = state;
  42. const l = StructureElement.Location.create(structure);
  43. const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
  44. const units: Unit[] = [];
  45. for (const unit of structure.units) {
  46. StructureElement.Location.set(l, structure, unit, unit.elements[0]);
  47. if (structure.getModelIndex(unit.model) !== modelIdx) continue;
  48. if (SP.entity.id(l) !== entityId) continue;
  49. if (unit.chainGroupId !== chainGroupId) continue;
  50. if (opKey(l) !== operatorKey) continue;
  51. units.push(unit);
  52. }
  53. if (units.length > 0) {
  54. const data = { structure, units };
  55. const unit = units[0];
  56. let sw: SequenceWrapper<any>;
  57. if (unit.polymerElements.length) {
  58. const l = StructureElement.Location.create(structure, unit, unit.elements[0]);
  59. const entitySeq = unit.model.sequence.byEntityKey[SP.entity.key(l)];
  60. // check if entity sequence is available
  61. if (entitySeq && entitySeq.sequence.length <= MaxDisplaySequenceLength) {
  62. sw = new PolymerSequenceWrapper(data);
  63. } else {
  64. const polymerElementCount = units.reduce((a, v) => a + v.polymerElements.length, 0);
  65. if (Unit.isAtomic(unit) || polymerElementCount > MaxDisplaySequenceLength) {
  66. sw = new ChainSequenceWrapper(data);
  67. } else {
  68. sw = new ElementSequenceWrapper(data);
  69. }
  70. }
  71. } else if (Unit.isAtomic(unit)) {
  72. const residueCount = units.reduce((a, v) => a + (v as Unit.Atomic).residueCount, 0);
  73. if (residueCount > MaxDisplaySequenceLength) {
  74. sw = new ChainSequenceWrapper(data);
  75. } else {
  76. sw = new HeteroSequenceWrapper(data);
  77. }
  78. } else {
  79. console.warn('should not happen, expecting coarse units to be polymeric');
  80. sw = new ChainSequenceWrapper(data);
  81. }
  82. sw.markResidue(structureSelection.getLoci(structure), MarkerAction.Select);
  83. return sw;
  84. } else {
  85. return 'No sequence available';
  86. }
  87. }
  88. function getModelEntityOptions(structure: Structure, polymersOnly = false): [string, string][] {
  89. const options: [string, string][] = [];
  90. const l = StructureElement.Location.create(structure);
  91. const seen = new Set<string>();
  92. for (const unit of structure.units) {
  93. StructureElement.Location.set(l, structure, unit, unit.elements[0]);
  94. const id = SP.entity.id(l);
  95. const modelIdx = structure.getModelIndex(unit.model);
  96. const key = `${modelIdx}|${id}`;
  97. if (seen.has(key)) continue;
  98. if (polymersOnly && SP.entity.type(l) !== 'polymer') continue;
  99. let description = SP.entity.pdbx_description(l).join(', ');
  100. if (structure.models.length) {
  101. if (structure.representativeModel) { // indicates model trajectory
  102. description += ` (Model ${structure.models[modelIdx].modelNum})`;
  103. } else if (description.startsWith('Polymer ')) { // indicates generic entity name
  104. description += ` (${structure.models[modelIdx].entry})`;
  105. }
  106. }
  107. const label = `${id}: ${description}`;
  108. options.push([key, label]);
  109. seen.add(key);
  110. if (options.length > MaxSelectOptionsCount) {
  111. return [['', 'Too many entities']];
  112. }
  113. }
  114. if (options.length === 0) options.push(['', 'No entities']);
  115. return options;
  116. }
  117. function getChainOptions(structure: Structure, modelEntityId: string): [number, string][] {
  118. const options: [number, string][] = [];
  119. const l = StructureElement.Location.create(structure);
  120. const seen = new Set<number>();
  121. const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
  122. for (const unit of structure.units) {
  123. StructureElement.Location.set(l, structure, unit, unit.elements[0]);
  124. if (structure.getModelIndex(unit.model) !== modelIdx) continue;
  125. if (SP.entity.id(l) !== entityId) continue;
  126. const id = unit.chainGroupId;
  127. if (seen.has(id)) continue;
  128. // TODO handle special case
  129. // - more than one chain in a unit
  130. const label = elementLabel(l, { granularity: 'chain', hidePrefix: true, htmlStyling: false });
  131. options.push([id, label]);
  132. seen.add(id);
  133. if (options.length > MaxSelectOptionsCount) {
  134. return [[-1, 'Too many chains']];
  135. }
  136. }
  137. if (options.length === 0) options.push([-1, 'No chains']);
  138. return options;
  139. }
  140. function getOperatorOptions(structure: Structure, modelEntityId: string, chainGroupId: number): [string, string][] {
  141. const options: [string, string][] = [];
  142. const l = StructureElement.Location.create(structure);
  143. const seen = new Set<string>();
  144. const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
  145. for (const unit of structure.units) {
  146. StructureElement.Location.set(l, structure, unit, unit.elements[0]);
  147. if (structure.getModelIndex(unit.model) !== modelIdx) continue;
  148. if (SP.entity.id(l) !== entityId) continue;
  149. if (unit.chainGroupId !== chainGroupId) continue;
  150. const id = opKey(l);
  151. if (seen.has(id)) continue;
  152. const label = unit.conformation.operator.name;
  153. options.push([id, label]);
  154. seen.add(id);
  155. if (options.length > MaxSelectOptionsCount) {
  156. return [['', 'Too many operators']];
  157. }
  158. }
  159. if (options.length === 0) options.push(['', 'No operators']);
  160. return options;
  161. }
  162. function getStructureOptions(state: State) {
  163. const options: [string, string][] = [];
  164. const all: Structure[] = [];
  165. const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure));
  166. for (const s of structures) {
  167. if (!s.obj?.data) continue;
  168. all.push(s.obj.data);
  169. options.push([s.transform.ref, s.obj!.data.label]);
  170. }
  171. if (options.length === 0) options.push(['', 'No structure']);
  172. return { options, all };
  173. }
  174. export type SequenceViewMode = 'single' | 'polymers' | 'all'
  175. const SequenceViewModeParam = PD.Select<SequenceViewMode>('single', [['single', 'Chain'], ['polymers', 'Polymers'], ['all', 'Everything']]);
  176. type SequenceViewState = {
  177. structureOptions: { options: [string, string][], all: Structure[] },
  178. structure: Structure,
  179. structureRef: string,
  180. modelEntityId: string,
  181. chainGroupId: number,
  182. operatorKey: string,
  183. mode: SequenceViewMode
  184. }
  185. export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceViewMode }, SequenceViewState> {
  186. state: SequenceViewState = { structureOptions: { options: [], all: [] }, structure: Structure.Empty, structureRef: '', modelEntityId: '', chainGroupId: -1, operatorKey: '', mode: 'single' };
  187. componentDidMount() {
  188. if (this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)).length > 0) this.setState(this.getInitialState());
  189. this.subscribe(this.plugin.state.events.object.updated, ({ ref, obj }) => {
  190. if (ref === this.state.structureRef && obj && obj.type === PSO.Molecule.Structure.type && obj.data !== this.state.structure) {
  191. this.sync();
  192. }
  193. });
  194. this.subscribe(this.plugin.state.events.object.created, ({ obj }) => {
  195. if (obj && obj.type === PSO.Molecule.Structure.type) {
  196. this.sync();
  197. }
  198. });
  199. this.subscribe(this.plugin.state.events.object.removed, ({ obj }) => {
  200. if (obj && obj.type === PSO.Molecule.Structure.type && obj.data === this.state.structure) {
  201. this.sync();
  202. }
  203. });
  204. }
  205. private sync() {
  206. const structureOptions = getStructureOptions(this.plugin.state.data);
  207. if (arrayEqual(structureOptions.all, this.state.structureOptions.all)) return;
  208. this.setState(this.getInitialState());
  209. }
  210. private getStructure(ref: string) {
  211. const state = this.plugin.state.data;
  212. const cell = state.select(ref)[0];
  213. if (!ref || !cell || !cell.obj) return Structure.Empty;
  214. return (cell.obj as PSO.Molecule.Structure).data;
  215. }
  216. private getSequenceWrapper(params: SequenceView['params']) {
  217. return {
  218. wrapper: getSequenceWrapper(this.state, this.plugin.managers.structure.selection),
  219. label: `${PD.optionLabel(params.chain, this.state.chainGroupId)} | ${PD.optionLabel(params.entity, this.state.modelEntityId)}`
  220. };
  221. }
  222. private getSequenceWrappers(params: SequenceView['params']) {
  223. if (this.state.mode === 'single') return [this.getSequenceWrapper(params)];
  224. const structure = this.getStructure(this.state.structureRef);
  225. const wrappers: { wrapper: (string | SequenceWrapper.Any), label: string }[] = [];
  226. for (const [modelEntityId, eLabel] of getModelEntityOptions(structure, this.state.mode === 'polymers')) {
  227. for (const [chainGroupId, cLabel] of getChainOptions(structure, modelEntityId)) {
  228. for (const [operatorKey] of getOperatorOptions(structure, modelEntityId, chainGroupId)) {
  229. wrappers.push({
  230. wrapper: getSequenceWrapper({
  231. structure,
  232. modelEntityId,
  233. chainGroupId,
  234. operatorKey
  235. }, this.plugin.managers.structure.selection),
  236. label: `${cLabel} | ${eLabel}`
  237. });
  238. if (wrappers.length > MaxSequenceWrappersCount) return [];
  239. }
  240. }
  241. }
  242. return wrappers;
  243. }
  244. private getInitialState(): SequenceViewState {
  245. const structureOptions = getStructureOptions(this.plugin.state.data);
  246. const structureRef = structureOptions.options[0][0];
  247. const structure = this.getStructure(structureRef);
  248. let modelEntityId = getModelEntityOptions(structure)[0][0];
  249. let chainGroupId = getChainOptions(structure, modelEntityId)[0][0];
  250. let operatorKey = getOperatorOptions(structure, modelEntityId, chainGroupId)[0][0];
  251. if (this.state.structure && this.state.structure === structure) {
  252. modelEntityId = this.state.modelEntityId;
  253. chainGroupId = this.state.chainGroupId;
  254. operatorKey = this.state.operatorKey;
  255. }
  256. return { structureOptions, structure, structureRef, modelEntityId, chainGroupId, operatorKey, mode: this.props.defaultMode ?? 'single' };
  257. }
  258. private get params() {
  259. const { structureOptions, structure, modelEntityId, chainGroupId } = this.state;
  260. const entityOptions = getModelEntityOptions(structure);
  261. const chainOptions = getChainOptions(structure, modelEntityId);
  262. const operatorOptions = getOperatorOptions(structure, modelEntityId, chainGroupId);
  263. return {
  264. structure: PD.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true }),
  265. entity: PD.Select(entityOptions[0][0], entityOptions, { shortLabel: true }),
  266. chain: PD.Select(chainOptions[0][0], chainOptions, { shortLabel: true, twoColumns: true, label: 'Chain' }),
  267. operator: PD.Select(operatorOptions[0][0], operatorOptions, { shortLabel: true, twoColumns: true }),
  268. mode: SequenceViewModeParam
  269. };
  270. }
  271. private get values(): PD.Values<SequenceView['params']> {
  272. return {
  273. structure: this.state.structureRef,
  274. entity: this.state.modelEntityId,
  275. chain: this.state.chainGroupId,
  276. operator: this.state.operatorKey,
  277. mode: this.state.mode
  278. };
  279. }
  280. private setParamProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
  281. const state = { ...this.state };
  282. switch (p.name) {
  283. case 'mode':
  284. state.mode = p.value;
  285. if (this.state.mode === state.mode) return;
  286. if (state.mode === 'all' || state.mode === 'polymers') {
  287. break;
  288. }
  289. case 'structure':
  290. if (p.name === 'structure') state.structureRef = p.value;
  291. state.structure = this.getStructure(state.structureRef);
  292. state.modelEntityId = getModelEntityOptions(state.structure)[0][0];
  293. state.chainGroupId = getChainOptions(state.structure, state.modelEntityId)[0][0];
  294. state.operatorKey = getOperatorOptions(state.structure, state.modelEntityId, state.chainGroupId)[0][0];
  295. break;
  296. case 'entity':
  297. state.modelEntityId = p.value;
  298. state.chainGroupId = getChainOptions(state.structure, state.modelEntityId)[0][0];
  299. state.operatorKey = getOperatorOptions(state.structure, state.modelEntityId, state.chainGroupId)[0][0];
  300. break;
  301. case 'chain':
  302. state.chainGroupId = p.value;
  303. state.operatorKey = getOperatorOptions(state.structure, state.modelEntityId, state.chainGroupId)[0][0];
  304. break;
  305. case 'operator':
  306. state.operatorKey = p.value;
  307. break;
  308. }
  309. this.setState(state);
  310. };
  311. render() {
  312. if (this.getStructure(this.state.structureRef) === Structure.Empty) {
  313. return <div className='msp-sequence'>
  314. <div className='msp-sequence-select'>
  315. <Icon svg={HelpOutlineSvg} style={{ cursor: 'help', position: 'absolute', right: 0, top: 0 }}
  316. title='Shows a sequence of one or more chains. Use the controls to alter selection.' />
  317. <span>Sequence</span><span style={{ fontWeight: 'normal' }}>No structure available</span>
  318. </div>
  319. </div>;
  320. }
  321. const params = this.params;
  322. const values = this.values;
  323. const sequenceWrappers = this.getSequenceWrappers(params);
  324. return <div className='msp-sequence'>
  325. <div className='msp-sequence-select'>
  326. <Icon svg={HelpOutlineSvg} style={{ cursor: 'help', position: 'absolute', right: 0, top: 0 }}
  327. title='This shows a single sequence. Use the controls to show a different sequence.' />
  328. <span>Sequence of</span>
  329. <PureSelectControl title={`[Structure] ${PD.optionLabel(params.structure, values.structure)}`} param={params.structure} name='structure' value={values.structure} onChange={this.setParamProps} />
  330. <PureSelectControl title={`[Mode]`} param={SequenceViewModeParam} name='mode' value={values.mode} onChange={this.setParamProps} />
  331. {values.mode === 'single' && <PureSelectControl title={`[Entity] ${PD.optionLabel(params.entity, values.entity)}`} param={params.entity} name='entity' value={values.entity} onChange={this.setParamProps} />}
  332. {values.mode === 'single' && <PureSelectControl title={`[Chain] ${PD.optionLabel(params.chain, values.chain)}`} param={params.chain} name='chain' value={values.chain} onChange={this.setParamProps} />}
  333. {params.operator.options.length > 1 && <>
  334. <PureSelectControl title={`[Instance] ${PD.optionLabel(params.operator, values.operator)}`} param={params.operator} name='operator' value={values.operator} onChange={this.setParamProps} />
  335. </>}
  336. </div>
  337. <NonEmptySequenceWrapper>
  338. {sequenceWrappers.map((s, i) => {
  339. const elem = typeof s.wrapper === 'string'
  340. ? <div key={i} className='msp-sequence-wrapper'>{s.wrapper}</div>
  341. : <Sequence key={i} sequenceWrapper={s.wrapper} />;
  342. if (values.mode === 'single') return elem;
  343. return <React.Fragment key={i}>
  344. <div className='msp-sequence-chain-label'>{s.label}</div>
  345. {elem}
  346. </React.Fragment>;
  347. })}
  348. </NonEmptySequenceWrapper>
  349. </div>;
  350. }
  351. }
  352. function NonEmptySequenceWrapper({ children }: { children: React.ReactNode }) {
  353. return <div className='msp-sequence-wrapper-non-empty'>
  354. {children}
  355. </div>;
  356. }