Skip to content
Snippets Groups Projects
Select Git revision
  • 7692b59c7c63cbd8b00e23c24c52388ce496bcae
  • master default protected
  • rednatco-v2
  • base-pairs-ladder
  • rednatco
  • test
  • ntc-tube-uniform-color
  • ntc-tube-missing-atoms
  • restore-vertex-array-per-program
  • watlas2
  • dnatco_new
  • cleanup-old-nodejs
  • webmmb
  • fix_auth_seq_id
  • update_deps
  • ext_dev
  • ntc_balls
  • nci-2
  • plugin
  • bugfix-0.4.5
  • nci
  • v0.5.0-dev.1
  • v0.4.5
  • v0.4.4
  • v0.4.3
  • v0.4.2
  • v0.4.1
  • v0.4.0
  • v0.3.12
  • v0.3.11
  • v0.3.10
  • v0.3.9
  • v0.3.8
  • v0.3.7
  • v0.3.6
  • v0.3.5
  • v0.3.4
  • v0.3.3
  • v0.3.2
  • v0.3.1
  • v0.3.0
41 results

sequence.tsx

Blame
  • sequence.tsx 12.21 KiB
    /**
     * Copyright (c) 2018-2020 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>
     */
    
    import * as React from 'react';
    import { Subject } from 'rxjs';
    import { throttleTime } from 'rxjs/operators';
    import { OrderedSet } from '../../mol-data/int';
    import { StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
    import { Representation } from '../../mol-repr/representation';
    import { ButtonsType, getButton, getButtons, getModifiers, ModifiersKeys } from '../../mol-util/input/input-observer';
    import { MarkerAction } from '../../mol-util/marker-action';
    import { PluginUIComponent } from '../base';
    import { SequenceWrapper } from './wrapper';
    
    type SequenceProps = {
        sequenceWrapper: SequenceWrapper.Any,
        sequenceNumberPeriod?: number,
        hideSequenceNumbers?: boolean,
    }
    
    /** Note, if this is changed, the CSS for `msp-sequence-number` needs adjustment too */
    const MaxSequenceNumberSize = 5;
    
    // TODO: this is somewhat inefficient and should be done using a canvas.
    export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
        private parentDiv = React.createRef<HTMLDivElement>();
        private lastMouseOverSeqIdx = -1;
        private highlightQueue = new Subject<{ seqIdx: number, buttons: number, button: number, modifiers: ModifiersKeys }>();
    
        private lociHighlightProvider = (loci: Representation.Loci, action: MarkerAction) => {
            const changed = this.props.sequenceWrapper.markResidue(loci.loci, action);
            if (changed) this.updateMarker();
        };
    
        private lociSelectionProvider = (loci: Representation.Loci, action: MarkerAction) => {
            const changed = this.props.sequenceWrapper.markResidue(loci.loci, action);
            if (changed) this.updateMarker();
        };
    
        private get sequenceNumberPeriod() {
            if (this.props.sequenceNumberPeriod !== undefined) {
                return this.props.sequenceNumberPeriod as number;
            }
            if (this.props.sequenceWrapper.length > 10) return 10;
            const lastSeqNum = this.getSequenceNumber(this.props.sequenceWrapper.length - 1);
            if (lastSeqNum.length > 1) return 5;
            return 1;
        }
    
        componentDidMount() {
            this.plugin.managers.interactivity.lociHighlights.addProvider(this.lociHighlightProvider);
            this.plugin.managers.interactivity.lociSelects.addProvider(this.lociSelectionProvider);
    
            this.subscribe(this.highlightQueue.pipe(throttleTime(3 * 16.666, void 0, { leading: true, trailing: true })), (e) => {
                const loci = this.getLoci(e.seqIdx < 0 ? void 0 : e.seqIdx);
                this.hover(loci, e.buttons, e.button, e.modifiers);
            });
        }
    
        componentWillUnmount() {
            super.componentWillUnmount();
    
            this.plugin.managers.interactivity.lociHighlights.removeProvider(this.lociHighlightProvider);
            this.plugin.managers.interactivity.lociSelects.removeProvider(this.lociSelectionProvider);
        }
    
        getLoci(seqIdx: number | undefined) {
            if (seqIdx !== undefined) {
                const loci = this.props.sequenceWrapper.getLoci(seqIdx);
                if (!StructureElement.Loci.isEmpty(loci)) return loci;
            }
        }
    
        getSeqIdx(e: React.MouseEvent) {
            let seqIdx: number | undefined = undefined;
            const el = e.target as HTMLElement;
            if (el && el.getAttribute) {
                seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : undefined;
            }
            return seqIdx;
        }
    
        hover(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
            const ev = { current: Representation.Loci.Empty, buttons, button, modifiers };
            if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
                ev.current = { loci };
            }
            this.plugin.behaviors.interaction.hover.next(ev);
        }
    
        click(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
            const ev = { current: Representation.Loci.Empty, buttons, button, modifiers };
            if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
                ev.current = { loci };
            }
            this.plugin.behaviors.interaction.click.next(ev);
        }
    
        contextMenu = (e: React.MouseEvent) => {
            e.preventDefault();
        };
    
        private mouseDownLoci: StructureElement.Loci | undefined = undefined;
    
        mouseDown = (e: React.MouseEvent) => {
            e.stopPropagation();
    
            const seqIdx = this.getSeqIdx(e);
            const loci = this.getLoci(seqIdx);
            const buttons = getButtons(e.nativeEvent);
            const button = getButton(e.nativeEvent);
            const modifiers = getModifiers(e.nativeEvent);
    
            this.click(loci, buttons, button, modifiers);
            this.mouseDownLoci = loci;
        };
    
        mouseUp = (e: React.MouseEvent) => {
            e.stopPropagation();
    
            // ignore mouse-up events without a bound loci
            if (this.mouseDownLoci === undefined) return;
    
            const seqIdx = this.getSeqIdx(e);
            const loci = this.getLoci(seqIdx);
    
            if (loci && !StructureElement.Loci.areEqual(this.mouseDownLoci, loci)) {
                const buttons = getButtons(e.nativeEvent);
                const button = getButton(e.nativeEvent);
                const modifiers = getModifiers(e.nativeEvent);
    
                const ref = this.mouseDownLoci.elements[0];
                const ext = loci.elements[0];
                const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices));
                const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices));
    
                const range = StructureElement.Loci(loci.structure, [{
                    unit: ref.unit,
                    indices: OrderedSet.ofRange(min as StructureElement.UnitIndex, max as StructureElement.UnitIndex)
                }]);
    
                this.click(StructureElement.Loci.subtract(range, this.mouseDownLoci), buttons, button, modifiers);
            }
            this.mouseDownLoci = undefined;
        };
    
        private getBackgroundColor(marker: number) {
            // TODO: make marker color configurable
            if (typeof marker === 'undefined') console.error('unexpected marker value');
            return marker === 0
                ? ''
                : marker % 2 === 0
                    ? 'rgb(51, 255, 25)' // selected
                    : 'rgb(255, 102, 153)'; // highlighted
        }
    
        private getResidueClass(seqIdx: number, label: string) {
            return label.length > 1
                ? this.props.sequenceWrapper.residueClass(seqIdx) + (seqIdx === 0 ? ' msp-sequence-residue-long-begin' : ' msp-sequence-residue-long')
                : this.props.sequenceWrapper.residueClass(seqIdx);
        }
    
        private residue(seqIdx: number, label: string, marker: number) {
            return <span key={seqIdx} data-seqid={seqIdx} style={{ backgroundColor: this.getBackgroundColor(marker) }} className={this.getResidueClass(seqIdx, label)}>{`\u200b${label}\u200b`}</span>;
        }
    
        private getSequenceNumberClass(seqIdx: number, seqNum: string, label: string) {
            const classList = ['msp-sequence-number'];
            if (seqNum.startsWith('-')) {
                if (label.length > 1 && seqIdx > 0) classList.push('msp-sequence-number-long-negative');
                else classList.push('msp-sequence-number-negative');
            } else {
                if (label.length > 1 && seqIdx > 0) classList.push('msp-sequence-number-long');
            }
            return classList.join(' ');
        }
    
        private location = StructureElement.Location.create(void 0);
        private getSequenceNumber(seqIdx: number) {
            let seqNum = '';
            const loci = this.props.sequenceWrapper.getLoci(seqIdx);
            const l = StructureElement.Loci.getFirstLocation(loci, this.location);
            if (l) {
                if (Unit.isAtomic(l.unit)) {
                    const seqId = StructureProperties.residue.auth_seq_id(l);
                    const insCode = StructureProperties.residue.pdbx_PDB_ins_code(l);
                    seqNum = `${seqId}${insCode ? insCode : ''}`;
                } else if (Unit.isCoarse(l.unit)) {
                    seqNum = `${seqIdx + 1}`;
                }
            }
            return seqNum;
        }
    
        private padSeqNum(n: string) {
            if (n.length < MaxSequenceNumberSize) return n + new Array(MaxSequenceNumberSize - n.length + 1).join('\u00A0');
            return n;
        }
        private getSequenceNumberSpan(seqIdx: number, label: string) {
            const seqNum = this.getSequenceNumber(seqIdx);
            return <span key={`marker-${seqIdx}`} className={this.getSequenceNumberClass(seqIdx, seqNum, label)}>{this.padSeqNum(seqNum)}</span>;
        }
    
        private updateMarker() {
            if (!this.parentDiv.current) return;
            const xs = this.parentDiv.current.children;
            const { markerArray } = this.props.sequenceWrapper;
            const hasNumbers = !this.props.hideSequenceNumbers, period = this.sequenceNumberPeriod;
    
            // let first: HTMLSpanElement | undefined;
    
            let o = 0;
            for (let i = 0, il = markerArray.length; i < il; i++) {
                if (hasNumbers && i % period === 0 && i < il) o++;
                // o + 1 to account for help icon
                const span = xs[o] as HTMLSpanElement;
                if (!span) return;
                o++;
    
                // if (!first && markerArray[i] > 0) {
                //     first = span;
                // }
    
                const backgroundColor = this.getBackgroundColor(markerArray[i]);
                if (span.style.backgroundColor !== backgroundColor) span.style.backgroundColor = backgroundColor;
            }
    
            // if (first) {
            //     first.scrollIntoView({ block: 'nearest' });
            // }
        }
    
        mouseMove = (e: React.MouseEvent) => {
            e.stopPropagation();
    
            const buttons = getButtons(e.nativeEvent);
            const button = getButton(e.nativeEvent);
            const modifiers = getModifiers(e.nativeEvent);
    
            const el = e.target as HTMLElement;
            if (!el || !el.getAttribute) {
                if (this.lastMouseOverSeqIdx === -1) return;
                this.lastMouseOverSeqIdx = -1;
                this.highlightQueue.next({ seqIdx: -1, buttons, button, modifiers });
                return;
            }
            const seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : -1;
            if (this.lastMouseOverSeqIdx === seqIdx) {
                return;
            } else {
                this.lastMouseOverSeqIdx = seqIdx;
                if (this.mouseDownLoci !== undefined) {
                    const loci = this.getLoci(seqIdx);
                    this.hover(loci, ButtonsType.Flag.None, ButtonsType.Flag.None, { ...modifiers, shift: true });
                } else {
                    this.highlightQueue.next({ seqIdx, buttons, button, modifiers });
                }
            }
        };
    
        mouseLeave = (e: React.MouseEvent) => {
            e.stopPropagation();
            this.mouseDownLoci = undefined;
    
            if (this.lastMouseOverSeqIdx === -1) return;
            this.lastMouseOverSeqIdx = -1;
            const buttons = getButtons(e.nativeEvent);
            const button = getButton(e.nativeEvent);
            const modifiers = getModifiers(e.nativeEvent);
            this.highlightQueue.next({ seqIdx: -1, buttons, button, modifiers });
        };
    
        render() {
            const sw = this.props.sequenceWrapper;
    
            const elems: JSX.Element[] = [];
    
            const hasNumbers = !this.props.hideSequenceNumbers, period = this.sequenceNumberPeriod;
            for (let i = 0, il = sw.length; i < il; ++i) {
                const label = sw.residueLabel(i);
                // add sequence number before name so the html element do not get separated by a line-break
                if (hasNumbers && i % period === 0 && i < il) {
                    elems[elems.length] = this.getSequenceNumberSpan(i, label);
                }
                elems[elems.length] = this.residue(i, label, sw.markerArray[i]);
            }
    
            // calling .updateMarker here is neccesary to ensure existing
            // residue spans are updated as react won't update them
            this.updateMarker();
    
            return <div
                className='msp-sequence-wrapper'
                onContextMenu={this.contextMenu}
                onMouseDown={this.mouseDown}
                onMouseUp={this.mouseUp}
                onMouseMove={this.mouseMove}
                onMouseLeave={this.mouseLeave}
                ref={this.parentDiv}
            >
                {elems}
            </div>;
        }
    }