sequence.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /**
  2. * Copyright (c) 2018-2020 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 { MarkerAction } from '../../mol-util/marker-action';
  10. import { ButtonsType, ModifiersKeys, getButtons, getModifiers, getButton } from '../../mol-util/input/input-observer';
  11. import { SequenceWrapper } from './wrapper';
  12. import { StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
  13. import { Subject } from 'rxjs';
  14. import { debounceTime } from 'rxjs/operators';
  15. import { OrderedSet } from '../../mol-data/int';
  16. import { Representation } from '../../mol-repr/representation';
  17. type SequenceProps = {
  18. sequenceWrapper: SequenceWrapper.Any,
  19. sequenceNumberPeriod?: number,
  20. hideSequenceNumbers?: boolean,
  21. }
  22. /** Note, if this is changed, the CSS for `msp-sequence-number` needs adjustment too */
  23. const MaxSequenceNumberSize = 5;
  24. // TODO: this is somewhat inefficient and should be done using a canvas.
  25. export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
  26. private parentDiv = React.createRef<HTMLDivElement>();
  27. private lastMouseOverSeqIdx = -1;
  28. private highlightQueue = new Subject<{ seqIdx: number, buttons: number, button: number, modifiers: ModifiersKeys }>();
  29. private lociHighlightProvider = (loci: Representation.Loci, action: MarkerAction) => {
  30. const changed = this.props.sequenceWrapper.markResidue(loci.loci, action);
  31. if (changed) this.updateMarker();
  32. };
  33. private lociSelectionProvider = (loci: Representation.Loci, action: MarkerAction) => {
  34. const changed = this.props.sequenceWrapper.markResidue(loci.loci, action);
  35. if (changed) this.updateMarker();
  36. };
  37. private get sequenceNumberPeriod() {
  38. if (this.props.sequenceNumberPeriod !== undefined) {
  39. return this.props.sequenceNumberPeriod as number;
  40. }
  41. if (this.props.sequenceWrapper.length > 10) return 10;
  42. const lastSeqNum = this.getSequenceNumber(this.props.sequenceWrapper.length - 1);
  43. if (lastSeqNum.length > 1) return 5;
  44. return 1;
  45. }
  46. componentDidMount() {
  47. this.plugin.managers.interactivity.lociHighlights.addProvider(this.lociHighlightProvider);
  48. this.plugin.managers.interactivity.lociSelects.addProvider(this.lociSelectionProvider);
  49. this.subscribe(debounceTime<{ seqIdx: number, buttons: number, button: number, modifiers: ModifiersKeys }>(15)(this.highlightQueue), (e) => {
  50. const loci = this.getLoci(e.seqIdx < 0 ? void 0 : e.seqIdx);
  51. this.hover(loci, e.buttons, e.button, e.modifiers);
  52. });
  53. // this.updateMarker()
  54. }
  55. componentWillUnmount() {
  56. super.componentWillUnmount();
  57. this.plugin.managers.interactivity.lociHighlights.removeProvider(this.lociHighlightProvider);
  58. this.plugin.managers.interactivity.lociSelects.removeProvider(this.lociSelectionProvider);
  59. }
  60. getLoci(seqIdx: number | undefined) {
  61. if (seqIdx !== undefined) {
  62. const loci = this.props.sequenceWrapper.getLoci(seqIdx);
  63. if (!StructureElement.Loci.isEmpty(loci)) return loci;
  64. }
  65. }
  66. getSeqIdx(e: React.MouseEvent) {
  67. let seqIdx: number | undefined = undefined;
  68. const el = e.target as HTMLElement;
  69. if (el && el.getAttribute) {
  70. seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : undefined;
  71. }
  72. return seqIdx;
  73. }
  74. hover(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
  75. const ev = { current: Representation.Loci.Empty, buttons, button, modifiers };
  76. if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
  77. ev.current = { loci };
  78. }
  79. this.plugin.behaviors.interaction.hover.next(ev);
  80. }
  81. click(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
  82. const ev = { current: Representation.Loci.Empty, buttons, button, modifiers };
  83. if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
  84. ev.current = { loci };
  85. }
  86. this.plugin.behaviors.interaction.click.next(ev);
  87. }
  88. contextMenu = (e: React.MouseEvent) => {
  89. e.preventDefault();
  90. };
  91. private mouseDownLoci: StructureElement.Loci | undefined = undefined;
  92. mouseDown = (e: React.MouseEvent) => {
  93. e.stopPropagation();
  94. const seqIdx = this.getSeqIdx(e);
  95. const loci = this.getLoci(seqIdx);
  96. const buttons = getButtons(e.nativeEvent);
  97. const button = getButton(e.nativeEvent);
  98. const modifiers = getModifiers(e.nativeEvent);
  99. this.click(loci, buttons, button, modifiers);
  100. this.mouseDownLoci = loci;
  101. };
  102. mouseUp = (e: React.MouseEvent) => {
  103. e.stopPropagation();
  104. // ignore mouse-up events without a bound loci
  105. if (this.mouseDownLoci === undefined) return;
  106. const seqIdx = this.getSeqIdx(e);
  107. const loci = this.getLoci(seqIdx);
  108. if (loci && !StructureElement.Loci.areEqual(this.mouseDownLoci, loci)) {
  109. const buttons = getButtons(e.nativeEvent);
  110. const button = getButton(e.nativeEvent);
  111. const modifiers = getModifiers(e.nativeEvent);
  112. const ref = this.mouseDownLoci.elements[0];
  113. const ext = loci.elements[0];
  114. const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices));
  115. const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices));
  116. const range = StructureElement.Loci(loci.structure, [{
  117. unit: ref.unit,
  118. indices: OrderedSet.ofRange(min as StructureElement.UnitIndex, max as StructureElement.UnitIndex)
  119. }]);
  120. this.click(StructureElement.Loci.subtract(range, this.mouseDownLoci), buttons, button, modifiers);
  121. }
  122. this.mouseDownLoci = undefined;
  123. };
  124. private getBackgroundColor(marker: number) {
  125. // TODO: make marker color configurable
  126. if (typeof marker === 'undefined') console.error('unexpected marker value');
  127. return marker === 0
  128. ? ''
  129. : marker % 2 === 0
  130. ? 'rgb(51, 255, 25)' // selected
  131. : 'rgb(255, 102, 153)'; // highlighted
  132. }
  133. private getResidueClass(seqIdx: number, label: string) {
  134. return label.length > 1
  135. ? this.props.sequenceWrapper.residueClass(seqIdx) + (seqIdx === 0 ? ' msp-sequence-residue-long-begin' : ' msp-sequence-residue-long')
  136. : this.props.sequenceWrapper.residueClass(seqIdx);
  137. }
  138. private residue(seqIdx: number, label: string, marker: number) {
  139. return <span key={seqIdx} data-seqid={seqIdx} style={{ backgroundColor: this.getBackgroundColor(marker) }} className={this.getResidueClass(seqIdx, label)}>{`\u200b${label}\u200b`}</span>;
  140. }
  141. private getSequenceNumberClass(seqIdx: number, seqNum: string, label: string) {
  142. const classList = ['msp-sequence-number'];
  143. if (seqNum.startsWith('-')) {
  144. if (label.length > 1 && seqIdx > 0) classList.push('msp-sequence-number-long-negative');
  145. else classList.push('msp-sequence-number-negative');
  146. } else {
  147. if (label.length > 1 && seqIdx > 0) classList.push('msp-sequence-number-long');
  148. }
  149. return classList.join(' ');
  150. }
  151. private location = StructureElement.Location.create(void 0);
  152. private getSequenceNumber(seqIdx: number) {
  153. let seqNum = '';
  154. const loci = this.props.sequenceWrapper.getLoci(seqIdx);
  155. const l = StructureElement.Loci.getFirstLocation(loci, this.location);
  156. if (l) {
  157. if (Unit.isAtomic(l.unit)) {
  158. const seqId = StructureProperties.residue.auth_seq_id(l);
  159. const insCode = StructureProperties.residue.pdbx_PDB_ins_code(l);
  160. seqNum = `${seqId}${insCode ? insCode : ''}`;
  161. } else if (Unit.isCoarse(l.unit)) {
  162. seqNum = `${seqIdx + 1}`;
  163. }
  164. }
  165. return seqNum;
  166. }
  167. private padSeqNum(n: string) {
  168. if (n.length < MaxSequenceNumberSize) return n + new Array(MaxSequenceNumberSize - n.length + 1).join('\u00A0');
  169. return n;
  170. }
  171. private getSequenceNumberSpan(seqIdx: number, label: string) {
  172. const seqNum = this.getSequenceNumber(seqIdx);
  173. return <span key={`marker-${seqIdx}`} className={this.getSequenceNumberClass(seqIdx, seqNum, label)}>{this.padSeqNum(seqNum)}</span>;
  174. }
  175. private updateMarker() {
  176. if (!this.parentDiv.current) return;
  177. const xs = this.parentDiv.current.children;
  178. const { markerArray } = this.props.sequenceWrapper;
  179. const hasNumbers = !this.props.hideSequenceNumbers, period = this.sequenceNumberPeriod;
  180. // let first: HTMLSpanElement | undefined;
  181. let o = 0;
  182. for (let i = 0, il = markerArray.length; i < il; i++) {
  183. if (hasNumbers && i % period === 0 && i < il) o++;
  184. // o + 1 to account for help icon
  185. const span = xs[o] as HTMLSpanElement;
  186. if (!span) return;
  187. o++;
  188. // if (!first && markerArray[i] > 0) {
  189. // first = span;
  190. // }
  191. const backgroundColor = this.getBackgroundColor(markerArray[i]);
  192. if (span.style.backgroundColor !== backgroundColor) span.style.backgroundColor = backgroundColor;
  193. }
  194. // if (first) {
  195. // first.scrollIntoView({ block: 'nearest' });
  196. // }
  197. }
  198. mouseMove = (e: React.MouseEvent) => {
  199. e.stopPropagation();
  200. const buttons = getButtons(e.nativeEvent);
  201. const button = getButton(e.nativeEvent);
  202. const modifiers = getModifiers(e.nativeEvent);
  203. const el = e.target as HTMLElement;
  204. if (!el || !el.getAttribute) {
  205. if (this.lastMouseOverSeqIdx === -1) return;
  206. this.lastMouseOverSeqIdx = -1;
  207. this.highlightQueue.next({ seqIdx: -1, buttons, button, modifiers });
  208. return;
  209. }
  210. const seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : -1;
  211. if (this.lastMouseOverSeqIdx === seqIdx) {
  212. return;
  213. } else {
  214. this.lastMouseOverSeqIdx = seqIdx;
  215. if (this.mouseDownLoci !== undefined) {
  216. const loci = this.getLoci(seqIdx);
  217. this.hover(loci, ButtonsType.Flag.None, ButtonsType.Flag.None, { ...modifiers, shift: true });
  218. } else {
  219. this.highlightQueue.next({ seqIdx, buttons, button, modifiers });
  220. }
  221. }
  222. };
  223. mouseLeave = (e: React.MouseEvent) => {
  224. e.stopPropagation();
  225. this.mouseDownLoci = undefined;
  226. if (this.lastMouseOverSeqIdx === -1) return;
  227. this.lastMouseOverSeqIdx = -1;
  228. const buttons = getButtons(e.nativeEvent);
  229. const button = getButton(e.nativeEvent);
  230. const modifiers = getModifiers(e.nativeEvent);
  231. this.highlightQueue.next({ seqIdx: -1, buttons, button, modifiers });
  232. };
  233. render() {
  234. const sw = this.props.sequenceWrapper;
  235. const elems: JSX.Element[] = [];
  236. const hasNumbers = !this.props.hideSequenceNumbers, period = this.sequenceNumberPeriod;
  237. for (let i = 0, il = sw.length; i < il; ++i) {
  238. const label = sw.residueLabel(i);
  239. // add sequence number before name so the html element do not get separated by a line-break
  240. if (hasNumbers && i % period === 0 && i < il) {
  241. elems[elems.length] = this.getSequenceNumberSpan(i, label);
  242. }
  243. elems[elems.length] = this.residue(i, label, sw.markerArray[i]);
  244. }
  245. // calling .updateMarker here is neccesary to ensure existing
  246. // residue spans are updated as react won't update them
  247. this.updateMarker();
  248. return <div
  249. className='msp-sequence-wrapper'
  250. onContextMenu={this.contextMenu}
  251. onMouseDown={this.mouseDown}
  252. onMouseUp={this.mouseUp}
  253. onMouseMove={this.mouseMove}
  254. onMouseLeave={this.mouseLeave}
  255. ref={this.parentDiv}
  256. >
  257. {elems}
  258. </div>;
  259. }
  260. }