sequence.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /**
  2. * Copyright (c) 2018-2019 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 { Interactivity } from '../../util/interactivity';
  10. import { MarkerAction } from '../../../mol-util/marker-action';
  11. import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../../mol-util/input/input-observer';
  12. import { SequenceWrapper } from './wrapper';
  13. import { StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
  14. import { Subject } from 'rxjs';
  15. import { debounceTime } from 'rxjs/operators';
  16. import { Color } from '../../../mol-util/color';
  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: Interactivity.Loci, action: MarkerAction) => {
  30. const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
  31. if (changed) this.updateMarker();
  32. }
  33. private lociSelectionProvider = (loci: Interactivity.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.interactivity.lociHighlights.addProvider(this.lociHighlightProvider)
  48. this.plugin.interactivity.lociSelects.addProvider(this.lociSelectionProvider)
  49. this.subscribe(debounceTime<{ seqIdx: number, buttons: number, modifiers: ModifiersKeys }>(15)(this.highlightQueue), (e) => {
  50. this.hover(e.seqIdx < 0 ? void 0 : e.seqIdx, e.buttons, e.modifiers);
  51. });
  52. // this.updateMarker()
  53. }
  54. componentWillUnmount() {
  55. this.plugin.interactivity.lociHighlights.removeProvider(this.lociHighlightProvider)
  56. this.plugin.interactivity.lociSelects.removeProvider(this.lociSelectionProvider)
  57. }
  58. hover(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
  59. const ev = { current: Interactivity.Loci.Empty, buttons, modifiers }
  60. if (seqId !== undefined) {
  61. const loci = this.props.sequenceWrapper.getLoci(seqId);
  62. if (!StructureElement.Loci.isEmpty(loci)) ev.current = { loci };
  63. }
  64. this.plugin.behaviors.interaction.hover.next(ev)
  65. }
  66. click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
  67. const ev = { current: Interactivity.Loci.Empty, buttons, modifiers }
  68. if (seqId !== undefined) {
  69. const loci = this.props.sequenceWrapper.getLoci(seqId);
  70. if (!StructureElement.Loci.isEmpty(loci)) ev.current = { loci };
  71. }
  72. this.plugin.behaviors.interaction.click.next(ev)
  73. }
  74. contextMenu = (e: React.MouseEvent) => {
  75. e.preventDefault()
  76. }
  77. mouseDown = (e: React.MouseEvent) => {
  78. e.stopPropagation();
  79. const buttons = getButtons(e.nativeEvent)
  80. const modifiers = getModifiers(e.nativeEvent)
  81. let seqIdx: number | undefined = undefined;
  82. const el = e.target as HTMLElement;
  83. if (el && el.getAttribute) {
  84. seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : undefined;
  85. }
  86. this.click(seqIdx, buttons, modifiers);
  87. }
  88. private getBackgroundColor(marker: number) {
  89. // TODO: make marker color configurable
  90. if (typeof marker === 'undefined') console.error('unexpected marker value')
  91. return marker === 0 ? '' : marker % 2 === 0 ? 'rgb(51, 255, 25)' /* selected */ : 'rgb(255, 102, 153)' /* highlighted */;
  92. }
  93. private getResidueClass(seqIdx: number, label: string) {
  94. return label.length > 1
  95. ? (seqIdx === 0 ? 'msp-sequence-residue-long-begin' : 'msp-sequence-residue-long')
  96. : void 0
  97. }
  98. private residue(seqIdx: number, label: string, marker: number, color: Color) {
  99. return <span key={seqIdx} data-seqid={seqIdx} style={{ color: Color.toStyle(color), backgroundColor: this.getBackgroundColor(marker) }} className={this.getResidueClass(seqIdx, label)}>{label}</span>;
  100. }
  101. private getSequenceNumberClass(seqIdx: number, seqNum: string, label: string) {
  102. const classList = ['msp-sequence-number']
  103. if (seqNum.startsWith('-')) {
  104. if (label.length > 1 && seqIdx > 0) classList.push('msp-sequence-number-long-negative')
  105. else classList.push('msp-sequence-number-negative')
  106. } else {
  107. if (label.length > 1 && seqIdx > 0) classList.push('msp-sequence-number-long')
  108. }
  109. return classList.join(' ')
  110. }
  111. private location = StructureElement.Location.create();
  112. private getSequenceNumber(seqIdx: number) {
  113. let seqNum = ''
  114. const loci = this.props.sequenceWrapper.getLoci(seqIdx)
  115. const l = StructureElement.Loci.getFirstLocation(loci, this.location);
  116. if (l) {
  117. if (Unit.isAtomic(l.unit)) {
  118. const seqId = StructureProperties.residue.auth_seq_id(l)
  119. const insCode = StructureProperties.residue.pdbx_PDB_ins_code(l)
  120. seqNum = `${seqId}${insCode ? insCode : ''}`
  121. } else if (Unit.isCoarse(l.unit)) {
  122. seqNum = `${seqIdx + 1}`
  123. }
  124. }
  125. return seqNum
  126. }
  127. private padSeqNum(n: string) {
  128. if (n.length < MaxSequenceNumberSize) return n + new Array(MaxSequenceNumberSize - n.length + 1).join('\u00A0');
  129. return n;
  130. }
  131. private getSequenceNumberSpan(seqIdx: number, label: string) {
  132. const seqNum = this.getSequenceNumber(seqIdx)
  133. return <span key={`marker-${seqIdx}`} className={this.getSequenceNumberClass(seqIdx, seqNum, label)}>{this.padSeqNum(seqNum)}</span>
  134. }
  135. private updateMarker() {
  136. if (!this.parentDiv.current) return;
  137. const xs = this.parentDiv.current.children;
  138. const { markerArray } = this.props.sequenceWrapper;
  139. const hasNumbers = !this.props.hideSequenceNumbers, period = this.sequenceNumberPeriod;
  140. // let first: HTMLSpanElement | undefined;
  141. let o = 0;
  142. for (let i = 0, il = markerArray.length; i < il; i++) {
  143. if (hasNumbers && i % period === 0 && i < il) o++;
  144. // o + 1 to account for help icon
  145. const span = xs[o + 1] as HTMLSpanElement;
  146. if (!span) return;
  147. o++;
  148. // if (!first && markerArray[i] > 0) {
  149. // first = span;
  150. // }
  151. const backgroundColor = this.getBackgroundColor(markerArray[i]);
  152. if (span.style.backgroundColor !== backgroundColor) span.style.backgroundColor = backgroundColor;
  153. }
  154. // if (first) {
  155. // first.scrollIntoView({ block: 'nearest' });
  156. // }
  157. }
  158. mouseMove = (e: React.MouseEvent) => {
  159. e.stopPropagation();
  160. const el = e.target as HTMLElement;
  161. if (!el || !el.getAttribute) {
  162. if (this.lastMouseOverSeqIdx === -1) return;
  163. this.lastMouseOverSeqIdx = -1;
  164. const buttons = getButtons(e.nativeEvent)
  165. const modifiers = getModifiers(e.nativeEvent)
  166. this.highlightQueue.next({ seqIdx: -1, buttons, modifiers })
  167. return;
  168. }
  169. const seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : -1;
  170. if (this.lastMouseOverSeqIdx === seqIdx) {
  171. return;
  172. } else {
  173. const buttons = getButtons(e.nativeEvent)
  174. const modifiers = getModifiers(e.nativeEvent)
  175. this.lastMouseOverSeqIdx = seqIdx;
  176. this.highlightQueue.next({ seqIdx, buttons, modifiers })
  177. }
  178. }
  179. mouseLeave = (e: React.MouseEvent) => {
  180. if (this.lastMouseOverSeqIdx === -1) return;
  181. this.lastMouseOverSeqIdx = -1;
  182. const buttons = getButtons(e.nativeEvent)
  183. const modifiers = getModifiers(e.nativeEvent)
  184. this.highlightQueue.next({ seqIdx: -1, buttons, modifiers })
  185. }
  186. render() {
  187. const sw = this.props.sequenceWrapper
  188. const elems: JSX.Element[] = [];
  189. const hasNumbers = !this.props.hideSequenceNumbers, period = this.sequenceNumberPeriod;
  190. for (let i = 0, il = sw.length; i < il; ++i) {
  191. const label = sw.residueLabel(i)
  192. // add sequence number before name so the html element do not get separated by a line-break
  193. if (hasNumbers && i % period === 0 && i < il) {
  194. elems[elems.length] = this.getSequenceNumberSpan(i, label)
  195. }
  196. elems[elems.length] = this.residue(i, label, sw.markerArray[i], sw.residueColor(i));
  197. }
  198. // calling .updateMarker here is neccesary to ensure existing
  199. // residue spans are updated as react won't update them
  200. this.updateMarker()
  201. return <div
  202. className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'
  203. onContextMenu={this.contextMenu}
  204. onMouseDown={this.mouseDown}
  205. onMouseMove={this.mouseMove}
  206. onMouseLeave={this.mouseLeave}
  207. ref={this.parentDiv}
  208. >
  209. <span className={`msp-icon msp-icon-help`} style={{ cursor: 'help' }} title='This shows a single sequence. Use the menu on the left to show a different sequence.' />
  210. {elems}
  211. </div>;
  212. }
  213. }