strucmotif.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  5. */
  6. import * as React from 'react';
  7. import {CollapsableControls, PurePluginUIComponent} from 'molstar/lib/mol-plugin-ui/base';
  8. import {Button, IconButton} from 'molstar/lib/mol-plugin-ui/controls/common';
  9. import {
  10. ArrowDownwardSvg,
  11. ArrowUpwardSvg,
  12. DeleteOutlinedSvg,
  13. HelpOutlineSvg,
  14. Icon
  15. } from 'molstar/lib/mol-plugin-ui/controls/icons';
  16. import {ActionMenu} from 'molstar/lib/mol-plugin-ui/controls/action-menu';
  17. import {StructureSelectionHistoryEntry} from 'molstar/lib/mol-plugin-state/manager/structure/selection';
  18. import {StructureElement, StructureProperties} from 'molstar/lib/mol-model/structure/structure';
  19. import {ToggleSelectionModeButton} from 'molstar/lib/mol-plugin-ui/structure/selection';
  20. import {OrderedSet} from 'molstar/lib/mol-data/int';
  21. // TODO use prod
  22. // const ADVANCED_SEARCH_URL = 'https://strucmotif-dev.rcsb.org/search?request=';
  23. const ADVANCED_SEARCH_URL = 'http://localhost:8080/search?request=';
  24. const MAX_MOTIF_SIZE = 10;
  25. export class StrucmotifSubmitControls extends CollapsableControls {
  26. protected defaultState() {
  27. return {
  28. header: 'Structural Motif Search',
  29. isCollapsed: false,
  30. brand: { accent: 'gray' as const, svg: SearchIconSvg }
  31. };
  32. }
  33. renderControls() {
  34. return <>
  35. <SubmitControls />
  36. </>;
  37. }
  38. }
  39. const _SearchIcon = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M8 5v14l11-7z' /></svg>;
  40. export function SearchIconSvg() { return _SearchIcon; }
  41. const location = StructureElement.Location.create(void 0);
  42. export class SubmitControls extends PurePluginUIComponent<{}, { isBusy: boolean }> {
  43. state = { isBusy: false }
  44. componentDidMount() {
  45. this.subscribe(this.selection.events.additionsHistoryUpdated, () => {
  46. this.forceUpdate();
  47. });
  48. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  49. this.setState({ isBusy: v });
  50. });
  51. }
  52. get selection() {
  53. return this.plugin.managers.structure.selection;
  54. }
  55. submitSearch = () => {
  56. const pdbId: Set<string> = new Set();
  57. const residueIds: { label_asym_id: string, struct_oper_id?: string, label_seq_id: number }[] = [];
  58. const loci = this.plugin.managers.structure.selection.additionsHistory;
  59. let structure;
  60. for (let l of loci) {
  61. structure = l.loci.structure;
  62. pdbId.add(structure.model.entry);
  63. // TODO ensure selection references only polymeric entities
  64. // only first element and only first index will be considered (ignoring multiple residues)
  65. const e = l.loci.elements[0];
  66. StructureElement.Location.set(location, structure, e.unit, e.unit.elements[OrderedSet.getAt(e.indices, 0)]);
  67. residueIds.push({
  68. label_asym_id: StructureProperties.chain.label_asym_id(location),
  69. // struct_oper_id: '1',
  70. label_seq_id: StructureProperties.residue.label_seq_id(location)
  71. });
  72. }
  73. if (pdbId.size > 1) {
  74. console.warn('motifs can only be extracted from a single model');
  75. return;
  76. }
  77. if (residueIds.length > MAX_MOTIF_SIZE) {
  78. console.warn(`maximum motif size is ${MAX_MOTIF_SIZE} residues`);
  79. return;
  80. }
  81. const query = {
  82. query: {
  83. type: 'group',
  84. logical_operator: 'and',
  85. nodes: [{
  86. type: 'terminal',
  87. service: 'strucmotif',
  88. parameters: {
  89. value: {
  90. data: pdbId.values().next().value as string,
  91. residue_ids: residueIds
  92. },
  93. score_cutoff: 5,
  94. exchanges: []
  95. },
  96. label: 'strucmotif',
  97. node_id: 0
  98. }],
  99. label: 'query-builder'
  100. },
  101. return_type: 'assembly',
  102. request_options: {
  103. pager: {
  104. start: 0,
  105. rows: 100
  106. },
  107. scoring_strategy: 'combined',
  108. sort: [{
  109. sort_by: 'score',
  110. direction: 'desc'
  111. }]
  112. },
  113. // TODO needed?
  114. // 'request_info': {
  115. // 'src': 'ui',
  116. // 'query_id': 'a4efda380aee3ef202dc59447a419e80'
  117. // }
  118. };
  119. // TODO figure out if Mol* can compose sierra/BioJava operator
  120. // TODO probably there should be a sierra-endpoint that handles mapping of Mol* operator ids to sierra/BioJava ones
  121. window.open(ADVANCED_SEARCH_URL + encodeURIComponent(JSON.stringify(query)), '_blank');
  122. }
  123. get actions(): ActionMenu.Items {
  124. const history = this.selection.additionsHistory;
  125. return [
  126. {
  127. kind: 'item',
  128. label: `Submit Search ${history.length < 3 ? ' (3 selections required)' : ''}`,
  129. value: this.submitSearch,
  130. disabled: history.length < 3
  131. },
  132. ];
  133. }
  134. selectAction: ActionMenu.OnSelect = item => {
  135. if (!item) return;
  136. (item?.value as any)();
  137. }
  138. highlight(loci: StructureElement.Loci) {
  139. this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci }, false);
  140. }
  141. moveHistory(e: StructureSelectionHistoryEntry, direction: 'up' | 'down') {
  142. this.plugin.managers.structure.selection.modifyHistory(e, direction, 4);
  143. }
  144. focusLoci(loci: StructureElement.Loci) {
  145. this.plugin.managers.camera.focusLoci(loci);
  146. }
  147. historyEntry(e: StructureSelectionHistoryEntry, idx: number) {
  148. const history = this.plugin.managers.structure.selection.additionsHistory;
  149. return <div className='msp-flex-row' key={e.id}>
  150. <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={this.plugin.managers.interactivity.lociHighlights.clearHighlights}>
  151. {idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} />
  152. </Button>
  153. {history.length > 1 && <IconButton svg={ArrowUpwardSvg} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} flex='20px' title={'Move up'} />}
  154. {history.length > 1 && <IconButton svg={ArrowDownwardSvg} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'down')} flex='20px' title={'Move down'} />}
  155. <IconButton svg={DeleteOutlinedSvg} small={true} className='msp-form-control' onClick={() => this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} flex title={'Remove'} />
  156. </div>;
  157. }
  158. add() {
  159. const history = this.plugin.managers.structure.selection.additionsHistory;
  160. const entries: JSX.Element[] = [];
  161. for (let i = 0, _i = Math.min(history.length, 10); i < _i; i++) {
  162. entries.push(this.historyEntry(history[i], i + 1));
  163. }
  164. return <>
  165. <ActionMenu items={this.actions} onSelect={this.selectAction} />
  166. {entries.length > 0 && <div className='msp-control-offset'>
  167. {entries}
  168. </div>}
  169. {entries.length === 0 && <div className='msp-control-offset msp-help-text'>
  170. <div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />Add one or more selections (toggle <ToggleSelectionModeButton inline /> mode)</div>
  171. </div>}
  172. </>;
  173. }
  174. render() {
  175. return <>
  176. {this.add()}
  177. </>;
  178. }
  179. }