From a3094b4d19155d5daf197d31b621e4634b49f234 Mon Sep 17 00:00:00 2001
From: Alexander Rose <alex.rose@rcsb.org>
Date: Wed, 19 Jun 2019 17:03:20 -0700
Subject: [PATCH] wip, per-chain sequence widget

---
 src/mol-plugin/ui/sequence.tsx                |  47 ++++----
 src/mol-plugin/ui/sequence/polymer.ts         | 111 ++++++++++++++++++
 src/mol-plugin/ui/sequence/polymer.tsx        |  40 -------
 src/mol-plugin/ui/sequence/residue.tsx        |   4 +-
 .../ui/sequence/{base.tsx => sequence.tsx}    |  51 ++++----
 src/mol-plugin/ui/sequence/util.ts            |  83 +++----------
 6 files changed, 177 insertions(+), 159 deletions(-)
 create mode 100644 src/mol-plugin/ui/sequence/polymer.ts
 delete mode 100644 src/mol-plugin/ui/sequence/polymer.tsx
 rename src/mol-plugin/ui/sequence/{base.tsx => sequence.tsx} (61%)

diff --git a/src/mol-plugin/ui/sequence.tsx b/src/mol-plugin/ui/sequence.tsx
index 40f176411..520d828dc 100644
--- a/src/mol-plugin/ui/sequence.tsx
+++ b/src/mol-plugin/ui/sequence.tsx
@@ -9,19 +9,29 @@ import * as React from 'react'
 import { PluginUIComponent } from './base';
 import { StateTreeSpine } from '../../mol-state/tree/spine';
 import { PluginStateObject as SO } from '../state/objects';
+import { Sequence } from './sequence/sequence';
+import { Structure } from '../../mol-model/structure';
+import { SequenceWrapper } from './sequence/util';
+import { PolymerSequenceWrapper } from './sequence/polymer';
+import { StructureElementSelectionManager } from '../util/structure-element-selection';
 import { MarkerAction } from '../../mol-util/marker-action';
-import { PolymerSequence } from './sequence/polymer';
-import { StructureSeq, markResidue } from './sequence/util';
 
-function getStructureSeqKey(structureSeq: StructureSeq) {
-    const { structure, seq } = structureSeq
-    const strucHash = structure.parent ? structure.parent.hashCode : structure.hashCode
-    return `${strucHash}|${seq.entityId}`
+function getSequenceWrappersForStructure(structure: Structure, structureSelection: StructureElementSelectionManager) {
+    const sequenceWrappers: SequenceWrapper.Any[] = []
+
+    structure.units.forEach(unit => {
+        if (unit.polymerElements.length === 0) return
+
+        const sw = new PolymerSequenceWrapper({ structure, unit })
+        sw.markResidue(structureSelection.get(structure), MarkerAction.Select)
+        sequenceWrappers.push(sw)
+    })
+
+    return sequenceWrappers
 }
 
 export class SequenceView extends PluginUIComponent<{ }, { }> {
     private spine: StateTreeSpine.Impl
-    private markerArrays = new Map<string, Uint8Array>()
 
     componentDidMount() {
         this.spine = new StateTreeSpine.Impl(this.plugin.state.dataState.cells);
@@ -39,20 +49,6 @@ export class SequenceView extends PluginUIComponent<{ }, { }> {
         });
     }
 
-    private getMarkerArray(structureSeq: StructureSeq): Uint8Array {
-        const { structure, seq } = structureSeq
-        const key = getStructureSeqKey(structureSeq)
-        let markerArray = this.markerArrays.get(key)
-        if (!markerArray) {
-            markerArray = new Uint8Array(seq.sequence.sequence.length)
-            this.markerArrays.set(key, markerArray)
-        }
-        const loci = this.plugin.helpers.structureSelection.get(structure)
-        markerArray.fill(0)
-        markResidue(loci, structureSeq, markerArray, MarkerAction.Select)
-        return markerArray
-    }
-
     private getStructure() {
         const so = this.spine && this.spine.getRootOfType(SO.Molecule.Structure)
         return so && so.data
@@ -64,12 +60,11 @@ export class SequenceView extends PluginUIComponent<{ }, { }> {
             <div className='msp-sequence-entity'>No structure available</div>
         </div>;
 
-        const seqs = structure.models[0].sequence.sequences;
+        const { structureSelection } = this.plugin.helpers
+        const sequenceWrappers = getSequenceWrappersForStructure(structure, structureSelection)
         return <div className='msp-sequence'>
-            {seqs.map((seq, i) => {
-                const structureSeq = { structure, seq }
-                const markerArray = this.getMarkerArray(structureSeq)
-                return <PolymerSequence key={i} structureSeq={structureSeq} markerArray={markerArray} />
+            {sequenceWrappers.map((sequenceWrapper, i) => {
+                return <Sequence key={i} sequenceWrapper={sequenceWrapper} />
             })}
         </div>;
     }
diff --git a/src/mol-plugin/ui/sequence/polymer.ts b/src/mol-plugin/ui/sequence/polymer.ts
new file mode 100644
index 000000000..77516a239
--- /dev/null
+++ b/src/mol-plugin/ui/sequence/polymer.ts
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureSelection, StructureQuery, Structure, Queries, StructureProperties as SP, StructureElement, Unit } from '../../../mol-model/structure';
+import { SequenceWrapper } from './util';
+import { OrderedSet, Interval } from '../../../mol-data/int';
+import { Loci } from '../../../mol-model/loci';
+
+export type StructureUnit = { structure: Structure, unit: Unit }
+
+export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> {
+    private readonly location = StructureElement.create()
+
+    private entityId: string
+    private label_asym_id: string
+
+    eachResidue(loci: Loci, apply: (interval: Interval) => boolean) {
+        let changed = false
+        const { structure } = this.data
+        if (!StructureElement.isLoci(loci)) return false
+        if (!Structure.areParentsEquivalent(loci.structure, structure)) return false
+
+        const { location, entityId, label_asym_id } = this
+        for (const e of loci.elements) {
+            let rIprev = -1
+            location.unit = e.unit
+
+            const { index: residueIndex } = e.unit.model.atomicHierarchy.residueAtomSegments
+
+            OrderedSet.forEach(e.indices, v => {
+                location.element = e.unit.elements[v]
+                const rI = residueIndex[location.element]
+                // avoid checking for the same residue multiple times
+                if (rI !== rIprev) {
+                    if (SP.entity.id(location) !== entityId) return
+                    if (SP.chain.label_asym_id(location) !== label_asym_id) return
+
+                    if (apply(getSeqIdInterval(location))) changed = true
+                    rIprev = rI
+                }
+            })
+        }
+        return changed
+    }
+
+    getLoci(seqId: number) {
+        const query = createResidueQuery(this.entityId, seqId, this.label_asym_id);
+        return StructureSelection.toLoci2(StructureQuery.run(query, this.data.structure));
+    }
+
+    constructor(readonly data: StructureUnit) {
+        super()
+
+        const l = this.location
+        l.unit = data.unit
+        l.element = data.unit.elements[0]
+
+        this.entityId = SP.entity.id(l)
+        this.label_asym_id = SP.chain.label_asym_id(l)
+
+        this.label = `${this.label_asym_id}|${this.entityId}`
+        this.sequence = data.unit.model.sequence.byEntityKey[SP.entity.key(l)].sequence
+        this.markerArray = new Uint8Array(this.sequence.sequence.length)
+    }
+}
+
+function createResidueQuery(entityId: string, label_seq_id: number, label_asym_id: string) {
+    return Queries.generators.atoms({
+        entityTest: ctx => {
+            return SP.entity.id(ctx.element) === entityId
+        },
+        chainTest: ctx => {
+            return SP.chain.label_asym_id(ctx.element) === label_asym_id
+        },
+        residueTest: ctx => {
+            if (ctx.element.unit.kind === Unit.Kind.Atomic) {
+                return SP.residue.label_seq_id(ctx.element) === label_seq_id
+            } else {
+                return (
+                    SP.coarse.seq_id_begin(ctx.element) <= label_seq_id &&
+                    SP.coarse.seq_id_end(ctx.element) >= label_seq_id
+                )
+            }
+        }
+    });
+}
+
+/** Zero-indexed */
+function getSeqIdInterval(location: StructureElement): Interval {
+    const { unit, element } = location
+    const { model } = unit
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            const residueIndex = model.atomicHierarchy.residueAtomSegments.index[element]
+            const seqId = model.atomicHierarchy.residues.label_seq_id.value(residueIndex)
+            return Interval.ofSingleton(seqId - 1)
+        case Unit.Kind.Spheres:
+            return Interval.ofRange(
+                model.coarseHierarchy.spheres.seq_id_begin.value(element) - 1,
+                model.coarseHierarchy.spheres.seq_id_end.value(element) - 1
+            )
+        case Unit.Kind.Gaussians:
+            return Interval.ofRange(
+                model.coarseHierarchy.gaussians.seq_id_begin.value(element) - 1,
+                model.coarseHierarchy.gaussians.seq_id_end.value(element) - 1
+            )
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/ui/sequence/polymer.tsx b/src/mol-plugin/ui/sequence/polymer.tsx
deleted file mode 100644
index 2e1f5e5ce..000000000
--- a/src/mol-plugin/ui/sequence/polymer.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2018-2019 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 { StructureSelection, StructureQuery } from '../../../mol-model/structure';
-import { createResidueQuery } from './util';
-import { Residue } from './residue';
-import { BaseSequence } from './base';
-
-export class PolymerSequence extends BaseSequence {
-    getLoci(seqId: number) {
-        const { structure, seq } = this.props.structureSeq
-        const query = createResidueQuery(seq.entityId, seqId);
-        return StructureSelection.toLoci2(StructureQuery.run(query, structure));
-    }
-
-    render() {
-        const { markerData } = this.state;
-        const { seq } = this.props.structureSeq;
-        const { offset, sequence } = seq.sequence;
-
-        const elems: JSX.Element[] = [];
-        for (let i = 0, _i = sequence.length; i < _i; i++) {
-            elems[elems.length] = <Residue seqId={offset + i + 1} letter={sequence[i]} parent={this} marker={markerData.value[i]} key={i} />;
-        }
-
-        return <div
-            className='msp-sequence-entity'
-            onContextMenu={this.contextMenu}
-            onMouseDown={this.mouseDown}
-        >
-            <span style={{ fontWeight: 'bold' }}>{seq.entityId}:{offset}&nbsp;</span>
-            {elems}
-        </div>;
-    }
-}
diff --git a/src/mol-plugin/ui/sequence/residue.tsx b/src/mol-plugin/ui/sequence/residue.tsx
index d27f47768..d5329a90b 100644
--- a/src/mol-plugin/ui/sequence/residue.tsx
+++ b/src/mol-plugin/ui/sequence/residue.tsx
@@ -8,9 +8,9 @@
 import * as React from 'react'
 import { PurePluginUIComponent } from '../base';
 import { getButtons, getModifiers } from '../../../mol-util/input/input-observer';
-import { BaseSequence } from './base';
+import { Sequence } from './sequence';
 
-export class Residue extends PurePluginUIComponent<{ seqId: number, letter: string, parent: BaseSequence, marker: number }> {
+export class Residue extends PurePluginUIComponent<{ seqId: number, letter: string, parent: Sequence<any>, marker: number }> {
 
     mouseEnter = (e: React.MouseEvent) => {
         const modifiers = getModifiers(e.nativeEvent)
diff --git a/src/mol-plugin/ui/sequence/base.tsx b/src/mol-plugin/ui/sequence/sequence.tsx
similarity index 61%
rename from src/mol-plugin/ui/sequence/base.tsx
rename to src/mol-plugin/ui/sequence/sequence.tsx
index b10a549f8..b392a335b 100644
--- a/src/mol-plugin/ui/sequence/base.tsx
+++ b/src/mol-plugin/ui/sequence/sequence.tsx
@@ -6,39 +6,44 @@
  */
 
 import * as React from 'react'
-import { StructureSelection, StructureQuery } from '../../../mol-model/structure';
 import { PluginUIComponent } from '../base';
 import { Interactivity } from '../../util/interactivity';
 import { MarkerAction } from '../../../mol-util/marker-action';
 import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../../mol-util/input/input-observer';
 import { ValueBox } from '../../../mol-util';
-import { createResidueQuery, markResidue, StructureSeq } from './util';
 import { Residue } from './residue';
+import { SequenceWrapper } from './util';
 
-type BaseSequenceProps = { structureSeq: StructureSeq, markerArray: Uint8Array }
-type BaseSequenceState = { markerData: ValueBox<Uint8Array> }
+type SequenceProps = { sequenceWrapper: SequenceWrapper.Any }
+type SequenceState = { markerData: ValueBox<Uint8Array> }
+
+function getState(markerData: ValueBox<Uint8Array>) {
+    return { markerData: ValueBox.withValue(markerData, markerData.value) }
+}
 
 // TODO: this is really inefficient and should be done using a canvas.
-export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSequenceState> {
+export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, SequenceState> {
     state = {
-        markerData: ValueBox.create(new Uint8Array(this.props.markerArray))
+        markerData: ValueBox.create(this.props.sequenceWrapper.markerArray)
+    }
+
+    private setMarkerData(markerData: ValueBox<Uint8Array>) {
+        this.setState(getState(markerData))
     }
 
     private lociHighlightProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
-        const { markerData } = this.state;
-        const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action)
-        if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) })
+        const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
+        if (changed) this.setMarkerData(this.state.markerData)
     }
 
     private lociSelectionProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
-        const { markerData } = this.state;
-        const changed = markResidue(loci.loci, this.props.structureSeq, markerData.value, action)
-        if (changed) this.setState({ markerData: ValueBox.withValue(markerData, markerData.value) })
+        const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
+        if (changed) this.setMarkerData(this.state.markerData)
     }
 
-    static getDerivedStateFromProps(nextProps: BaseSequenceProps, prevState: BaseSequenceState): BaseSequenceState | null {
-        if (prevState.markerData.value !== nextProps.markerArray) {
-            return { markerData: ValueBox.create(nextProps.markerArray) }
+    static getDerivedStateFromProps(nextProps: SequenceProps, prevState: SequenceState): SequenceState | null {
+        if (prevState.markerData.value !== nextProps.sequenceWrapper.markerArray) {
+            return getState(ValueBox.create(nextProps.sequenceWrapper.markerArray))
         }
         return null
     }
@@ -53,16 +58,10 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque
         this.plugin.interactivity.lociSelections.removeProvider(this.lociSelectionProvider)
     }
 
-    getLoci(seqId: number) {
-        const { structure, seq } = this.props.structureSeq
-        const query = createResidueQuery(seq.entityId, seqId);
-        return StructureSelection.toLoci2(StructureQuery.run(query, structure));
-    }
-
     highlight(seqId?: number, modifiers?: ModifiersKeys) {
         const ev = { current: Interactivity.Loci.Empty, modifiers }
         if (seqId !== undefined) {
-            const loci = this.getLoci(seqId);
+            const loci = this.props.sequenceWrapper.getLoci(seqId);
             if (loci.elements.length > 0) ev.current = { loci };
         }
         this.plugin.behaviors.interaction.highlight.next(ev)
@@ -71,7 +70,7 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque
     click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
         const ev = { current: Interactivity.Loci.Empty, buttons, modifiers }
         if (seqId !== undefined) {
-            const loci = this.getLoci(seqId);
+            const loci = this.props.sequenceWrapper.getLoci(seqId);
             if (loci.elements.length > 0) ev.current = { loci };
         }
         this.plugin.behaviors.interaction.click.next(ev)
@@ -89,8 +88,8 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque
 
     render() {
         const { markerData } = this.state;
-        const { seq } = this.props.structureSeq;
-        const { offset, sequence } = seq.sequence;
+        const { label } = this.props.sequenceWrapper
+        const { offset, sequence } = this.props.sequenceWrapper.sequence;
 
         const elems: JSX.Element[] = [];
         for (let i = 0, _i = sequence.length; i < _i; i++) {
@@ -102,7 +101,7 @@ export class BaseSequence extends PluginUIComponent<BaseSequenceProps, BaseSeque
             onContextMenu={this.contextMenu}
             onMouseDown={this.mouseDown}
         >
-            <span style={{ fontWeight: 'bold' }}>{seq.entityId}:{offset}&nbsp;</span>
+            <span style={{ fontWeight: 'bold' }}>{label}:{offset}&nbsp;</span>
             {elems}
         </div>;
     }
diff --git a/src/mol-plugin/ui/sequence/util.ts b/src/mol-plugin/ui/sequence/util.ts
index 59ceb9751..68b4e8d56 100644
--- a/src/mol-plugin/ui/sequence/util.ts
+++ b/src/mol-plugin/ui/sequence/util.ts
@@ -1,79 +1,32 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019 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 { Structure, StructureSequence, Queries, StructureProperties as SP, StructureElement, Unit } from '../../../mol-model/structure';
-import { OrderedSet, Interval } from '../../../mol-data/int';
+import { Interval } from '../../../mol-data/int';
 import { Loci } from '../../../mol-model/loci';
-import { applyMarkerAction, MarkerAction } from '../../../mol-util/marker-action';
+import { MarkerAction, applyMarkerAction } from '../../../mol-util/marker-action';
+import { StructureElement } from '../../../mol-model/structure';
+import { Sequence } from '../../../mol-model/sequence';
 
-export function createResidueQuery(entityId: string, label_seq_id: number) {
-    return Queries.generators.atoms({
-        entityTest: ctx => {
-            return SP.entity.id(ctx.element) === entityId
-        },
-        residueTest: ctx => {
-            if (ctx.element.unit.kind === Unit.Kind.Atomic) {
-                return SP.residue.label_seq_id(ctx.element) === label_seq_id
-            } else {
-                return (
-                    SP.coarse.seq_id_begin(ctx.element) <= label_seq_id &&
-                    SP.coarse.seq_id_end(ctx.element) >= label_seq_id
-                )
-            }
-        }
-    });
-}
-
-/** Zero-indexed */
-export function getSeqIdInterval(location: StructureElement): Interval {
-    const { unit, element } = location
-    const { model } = unit
-    switch (unit.kind) {
-        case Unit.Kind.Atomic:
-            const residueIndex = model.atomicHierarchy.residueAtomSegments.index[element]
-            const seqId = model.atomicHierarchy.residues.label_seq_id.value(residueIndex)
-            return Interval.ofSingleton(seqId - 1)
-        case Unit.Kind.Spheres:
-            return Interval.ofRange(
-                model.coarseHierarchy.spheres.seq_id_begin.value(element) - 1,
-                model.coarseHierarchy.spheres.seq_id_end.value(element) - 1
-            )
-        case Unit.Kind.Gaussians:
-            return Interval.ofRange(
-                model.coarseHierarchy.gaussians.seq_id_begin.value(element) - 1,
-                model.coarseHierarchy.gaussians.seq_id_end.value(element) - 1
-            )
-    }
-}
+export { SequenceWrapper }
 
-export type StructureSeq = { structure: Structure, seq: StructureSequence.Entity }
+abstract class SequenceWrapper<D> {
+    label: string
+    data: D
+    markerArray: Uint8Array
+    sequence: Sequence
+    abstract eachResidue(loci: Loci, apply: (interval: Interval) => boolean): boolean
+    abstract getLoci(seqId: number): StructureElement.Loci
 
-export function eachResidue(loci: Loci, structureSeq: StructureSeq, apply: (interval: Interval) => boolean) {
-    let changed = false
-    const { structure, seq } = structureSeq
-    if (!StructureElement.isLoci(loci)) return false
-    if (!Structure.areParentsEquivalent(loci.structure, structure)) return false
-    const l = StructureElement.create()
-    for (const e of loci.elements) {
-        l.unit = e.unit
-        OrderedSet.forEach(e.indices, v => {
-            l.element = e.unit.elements[v]
-            const entityId = SP.entity.id(l)
-            if (entityId === seq.entityId) {
-                if (apply(getSeqIdInterval(l))) changed = true
-            }
+    markResidue(loci: Loci, action: MarkerAction) {
+        return this.eachResidue(loci, (i: Interval) => {
+            return applyMarkerAction(this.markerArray, i, action)
         })
     }
-    return changed
 }
 
-export function markResidue(loci: Loci, structureSeq: StructureSeq, array: Uint8Array, action: MarkerAction) {
-    const { structure, seq } = structureSeq
-    return eachResidue(loci, { structure , seq }, (i: Interval) => {
-        return applyMarkerAction(array, i, action)
-    })
+namespace SequenceWrapper {
+    export type Any = SequenceWrapper<any>
 }
\ No newline at end of file
-- 
GitLab