From cb0c05188c465853a0dcc737a4599e16e64ba19b Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Fri, 8 Jun 2018 11:24:29 +0200
Subject: [PATCH] Basic sequence view with loci event raising

---
 README.md                                     | 21 ++++-
 src/apps/viewer/index.tsx                     |  9 ++
 src/mol-app/context/context.ts                |  8 +-
 .../controller/visualization/sequence-view.ts | 21 +++++
 src/mol-app/event/basic.ts                    |  5 ++
 .../skin/components/sequence-view.scss        |  8 ++
 src/mol-app/skin/layout/common.scss           |  2 +-
 src/mol-app/skin/ui.scss                      |  3 +-
 .../ui/visualization/sequence-view.tsx        | 84 +++++++++++++++++++
 src/mol-model/structure/model.ts              |  3 +-
 .../structure/model/formats/mmcif/sequence.ts |  8 +-
 .../structure/model/properties/sequence.ts    |  6 +-
 src/mol-model/structure/query/selection.ts    | 13 ++-
 src/mol-view/stage.ts                         |  5 +-
 14 files changed, 185 insertions(+), 11 deletions(-)
 create mode 100644 src/mol-app/controller/visualization/sequence-view.ts
 create mode 100644 src/mol-app/skin/components/sequence-view.scss
 create mode 100644 src/mol-app/ui/visualization/sequence-view.tsx

diff --git a/README.md b/README.md
index 0ac60a18d..c7a0e683f 100644
--- a/README.md
+++ b/README.md
@@ -51,17 +51,34 @@ This project builds on experience from previous solutions:
     npm run watch-extra
 
 ### Build/watch mol-viewer
-Build:
+**Build**
 
     npm run build
     npm run build-viewer
 
-Watch:
+**Watch**
 
     npm run watch
     npm run watch-extra
     npm run watch-viewer
 
+**Run**
+
+If not installed previously:
+
+    npm install -g http-server
+
+...or a similar solution.
+
+From the root of the project:
+
+    http-server -p PORT-NUMBER
+
+and navigate to `build/viewer`
+
+
+
+
 ## Contributing
 Just open an issue or make a pull request. All contributions are welcome.
 
diff --git a/src/apps/viewer/index.tsx b/src/apps/viewer/index.tsx
index f828e597a..b4d3823d0 100644
--- a/src/apps/viewer/index.tsx
+++ b/src/apps/viewer/index.tsx
@@ -22,6 +22,7 @@ import { EntityTree } from 'mol-app/ui/entity/tree';
 import { EntityTreeController } from 'mol-app/controller/entity/tree';
 import { TransformListController } from 'mol-app/controller/transform/list';
 import { TransformList } from 'mol-app/ui/transform/list';
+import { SequenceView } from 'mol-app/ui/visualization/sequence-view';
 
 const elm = document.getElementById('app')
 if (!elm) throw new Error('Can not find element with id "app".')
@@ -45,6 +46,14 @@ targets[LayoutRegion.Bottom].components.push({
     isStatic: true
 });
 
+targets[LayoutRegion.Top].components.push({
+    key: 'molstar-sequence-view',
+    controller: ctx.components.sequenceView,
+    region: LayoutRegion.Top,
+    view: SequenceView,
+    isStatic: true
+});
+
 targets[LayoutRegion.Main].components.push({
     key: 'molstar-background-jobs',
     controller: new JobsController(ctx, 'Background'),
diff --git a/src/mol-app/context/context.ts b/src/mol-app/context/context.ts
index 92a317aad..5c8fee555 100644
--- a/src/mol-app/context/context.ts
+++ b/src/mol-app/context/context.ts
@@ -15,6 +15,7 @@ import { Stage } from 'mol-view/stage';
 import { AnyTransform } from 'mol-view/state/transform';
 import { BehaviorSubject } from 'rxjs';
 import { AnyEntity } from 'mol-view/state/entity';
+import { SequenceViewController } from '../controller/visualization/sequence-view';
 
 export class Settings {
     private settings = new Map<string, any>();
@@ -35,11 +36,16 @@ export class Context {
     logger = new Logger(this);
     performance = new PerformanceMonitor();
 
-    stage = new Stage();
+    stage = new Stage(this);
     viewport = new ViewportController(this);
     layout: LayoutController;
     settings = new Settings();
 
+    // TODO: this is a temporary solution
+    components = {
+        sequenceView: new SequenceViewController(this)
+    };
+
     currentEntity = new BehaviorSubject(undefined) as BehaviorSubject<AnyEntity | undefined>
     currentTransforms = new BehaviorSubject([] as AnyTransform[])
 
diff --git a/src/mol-app/controller/visualization/sequence-view.ts b/src/mol-app/controller/visualization/sequence-view.ts
new file mode 100644
index 000000000..17423f980
--- /dev/null
+++ b/src/mol-app/controller/visualization/sequence-view.ts
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { shallowClone } from 'mol-util';
+import { Context } from '../../context/context'
+import { Controller } from '../controller';
+import { Structure } from 'mol-model/structure';
+
+export const DefaultSequenceViewState = {
+    structure: void 0 as (Structure | undefined)
+}
+export type SequenceViewState = typeof DefaultSequenceViewState
+
+export class SequenceViewController extends Controller<SequenceViewState> {
+    constructor(context: Context) {
+        super(context, shallowClone(DefaultSequenceViewState));
+    }
+}
\ No newline at end of file
diff --git a/src/mol-app/event/basic.ts b/src/mol-app/event/basic.ts
index 99cf366f9..b87ea5c14 100644
--- a/src/mol-app/event/basic.ts
+++ b/src/mol-app/event/basic.ts
@@ -11,6 +11,7 @@ import { Dispatcher } from '../service/dispatcher'
 import { LayoutState } from '../controller/layout';
 import { ViewportOptions } from '../controller/visualization/viewport';
 import { Job } from '../service/job';
+import { Element } from 'mol-model/structure'
 
 const Lane = Dispatcher.Lane;
 
@@ -31,3 +32,7 @@ export namespace LayoutEvents {
     export const SetState = Event.create<Partial<LayoutState>>('lm.cmd.Layout.SetState', Lane.Slow);
     export const SetViewportOptions = Event.create<ViewportOptions>('bs.cmd.Layout.SetViewportOptions', Lane.Slow);
 }
+
+export namespace InteractivityEvents {
+    export const HighlightElementLoci = Event.create<Element.Loci | undefined>('bs.Interactivity.HighlightElementLoci', Lane.Slow);
+}
diff --git a/src/mol-app/skin/components/sequence-view.scss b/src/mol-app/skin/components/sequence-view.scss
new file mode 100644
index 000000000..2be5ea580
--- /dev/null
+++ b/src/mol-app/skin/components/sequence-view.scss
@@ -0,0 +1,8 @@
+.molstar-sequence-view-wrap {
+    position: absolute;
+    right: 0;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    overflow: hidden;
+}
\ No newline at end of file
diff --git a/src/mol-app/skin/layout/common.scss b/src/mol-app/skin/layout/common.scss
index b84b69214..219de13b1 100644
--- a/src/mol-app/skin/layout/common.scss
+++ b/src/mol-app/skin/layout/common.scss
@@ -23,7 +23,7 @@
     overflow: hidden;
 }
 
-.molstar-layout-main, .molstar-layout-bottom {
+.molstar-layout-main, .molstar-layout-bottom, .molstar-layout-top {
     .molstar-layout-static {
         left: 0;
         right: 0;
diff --git a/src/mol-app/skin/ui.scss b/src/mol-app/skin/ui.scss
index 41e78fc74..cd572232a 100644
--- a/src/mol-app/skin/ui.scss
+++ b/src/mol-app/skin/ui.scss
@@ -35,4 +35,5 @@
 @import 'components/misc';
 @import 'components/panel';
 @import 'components/slider';
-@import 'components/viewport';
\ No newline at end of file
+@import 'components/viewport';
+@import 'components/sequence-view';
\ No newline at end of file
diff --git a/src/mol-app/ui/visualization/sequence-view.tsx b/src/mol-app/ui/visualization/sequence-view.tsx
new file mode 100644
index 000000000..4105dea2e
--- /dev/null
+++ b/src/mol-app/ui/visualization/sequence-view.tsx
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react'
+import { View } from '../view';
+import { SequenceViewController } from '../../controller/visualization/sequence-view';
+import { Structure, StructureSequence, Queries, Selection } from 'mol-model/structure';
+import { Context } from '../../context/context';
+import { InteractivityEvents } from '../../event/basic';
+import { SyncRuntimeContext } from 'mol-task/execution/synchronous';
+
+export class SequenceView extends View<SequenceViewController, {}, {}> {
+    render() {
+        const s = this.controller.latestState.structure;
+        if (!s) return <div className='molstar-sequence-view-wrap'>No structure available.</div>;
+
+        const seqs = Structure.getModels(s)[0].sequence.sequences;
+        return <div className='molstar-sequence-view-wrap'>
+            {seqs.map((seq, i) => <EntitySequence key={i} ctx={this.controller.context} seq={seq} structure={s} /> )}
+        </div>;
+    }
+}
+
+function createQuery(entityId: string, label_seq_id: number) {
+    return Queries.generators.atoms({
+        entityTest: l => Queries.props.entity.id(l) === entityId,
+        residueTest: l => Queries.props.residue.label_seq_id(l) === label_seq_id
+    });
+}
+
+// TODO: this is really ineffective and should be done using a canvas.
+class EntitySequence extends React.Component<{ ctx: Context, seq: StructureSequence.Entity, structure: Structure }> {
+
+    async raiseInteractityEvent(seqId?: number) {
+        if (typeof seqId === 'undefined') {
+            InteractivityEvents.HighlightElementLoci.dispatch(this.props.ctx, void 0);
+            return;
+        }
+
+        const query = createQuery(this.props.seq.entityId, seqId);
+        const loci = Selection.toLoci(await query(this.props.structure, SyncRuntimeContext));
+        InteractivityEvents.HighlightElementLoci.dispatch(this.props.ctx, loci);
+    }
+
+
+    render() {
+        const { ctx, seq } = this.props;
+        const { offset, sequence } = seq.sequence;
+
+        const elems: JSX.Element[] = [];
+        for (let i = 0, _i = sequence.length; i < _i; i++) {
+            elems[elems.length] = <ResidueView ctx={ctx} seqId={offset + i} letter={sequence[i]} parent={this} key={i} />;
+        }
+
+        return <div style={{ wordWrap: 'break-word' }}>
+            <span style={{ fontWeight: 'bold' }}>{this.props.seq.entityId}:{offset}&nbsp;</span>
+            {elems}
+        </div>;
+    }
+}
+
+class ResidueView extends React.Component<{ ctx: Context, seqId: number, letter: string, parent: EntitySequence }, { isHighlighted: boolean }> {
+    state = { isHighlighted: false }
+
+    mouseEnter = () => {
+        this.setState({ isHighlighted: true });
+        this.props.parent.raiseInteractityEvent(this.props.seqId);
+    }
+
+    mouseLeave = () => {
+        this.setState({ isHighlighted: false });
+        this.props.parent.raiseInteractityEvent();
+    }
+
+    render() {
+        return <span onMouseEnter={this.mouseEnter} onMouseLeave={this.mouseLeave}
+            style={{ cursor: 'pointer', backgroundColor: this.state.isHighlighted ? 'yellow' : void 0 }}>
+            {this.props.letter}
+        </span>;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/model.ts b/src/mol-model/structure/model.ts
index 1704ab092..0551a4c20 100644
--- a/src/mol-model/structure/model.ts
+++ b/src/mol-model/structure/model.ts
@@ -8,5 +8,6 @@ import Model from './model/model'
 import * as Types from './model/types'
 import Format from './model/format'
 import { ModelSymmetry } from './model/properties/symmetry'
+import StructureSequence from './model/properties/sequence'
 
-export { Model, Types, Format, ModelSymmetry }
\ No newline at end of file
+export { Model, Types, Format, ModelSymmetry, StructureSequence }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/mmcif/sequence.ts b/src/mol-model/structure/model/formats/mmcif/sequence.ts
index 06f0c888d..67e617ef3 100644
--- a/src/mol-model/structure/model/formats/mmcif/sequence.ts
+++ b/src/mol-model/structure/model/formats/mmcif/sequence.ts
@@ -27,6 +27,7 @@ export function getSequence(cif: mmCIF, entities: Entities, hierarchy: AtomicHie
     const { entity_id, num, mon_id } = cif.entity_poly_seq;
 
     const byEntityKey: StructureSequence['byEntityKey'] = {};
+    const sequences: StructureSequence.Entity[] = [];
     const count = entity_id.rowCount;
 
     let i = 0;
@@ -38,14 +39,17 @@ export function getSequence(cif: mmCIF, entities: Entities, hierarchy: AtomicHie
         const id = entity_id.value(start);
         const _compId = Column.window(mon_id, start, i);
         const _num = Column.window(num, start, i);
+        const entityKey = entities.getEntityIndex(id);
 
-        byEntityKey[entities.getEntityIndex(id)] = {
+        byEntityKey[entityKey] = {
             entityId: id,
             compId: _compId,
             num: _num,
             sequence: Sequence.ofResidueNames(_compId, _num)
         };
+
+        sequences.push(byEntityKey[entityKey]);
     }
 
-    return { byEntityKey };
+    return { byEntityKey, sequences };
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/sequence.ts b/src/mol-model/structure/model/properties/sequence.ts
index a2f53c56d..3cd6e601a 100644
--- a/src/mol-model/structure/model/properties/sequence.ts
+++ b/src/mol-model/structure/model/properties/sequence.ts
@@ -10,6 +10,7 @@ import { Entities } from './common';
 import { Sequence } from '../../../sequence';
 
 interface StructureSequence {
+    readonly sequences: ReadonlyArray<StructureSequence.Entity>,
     readonly byEntityKey: { [key: number]: StructureSequence.Entity }
 }
 
@@ -27,6 +28,7 @@ namespace StructureSequence {
         const { chainSegments, residueSegments } = hierarchy
 
         const byEntityKey: StructureSequence['byEntityKey'] = { };
+        const sequences: StructureSequence.Entity[] = [];
 
         for (let cI = 0, _cI = hierarchy.chains._rowCount; cI < _cI; cI++) {
             const entityKey = hierarchy.entityKey[cI];
@@ -52,9 +54,11 @@ namespace StructureSequence {
                 num,
                 sequence: Sequence.ofResidueNames(compId, num)
             };
+
+            sequences.push(byEntityKey[entityKey]);
         }
 
-        return { byEntityKey }
+        return { byEntityKey, sequences };
     }
 }
 
diff --git a/src/mol-model/structure/query/selection.ts b/src/mol-model/structure/query/selection.ts
index a1fc34f2a..0aaa01922 100644
--- a/src/mol-model/structure/query/selection.ts
+++ b/src/mol-model/structure/query/selection.ts
@@ -5,8 +5,9 @@
  */
 
 import { HashSet } from 'mol-data/generic'
-import { Structure } from '../structure'
+import { Structure, Element, Unit } from '../structure'
 import { structureUnion } from './utils/structure';
+import { SortedArray } from 'mol-data/int';
 
 // A selection is a pair of a Structure and a sequence of unique AtomSets
 type Selection = Selection.Singletons | Selection.Sequence
@@ -34,6 +35,16 @@ namespace Selection {
         return structureUnion(sel.source, sel.structures);
     }
 
+    export function toLoci(sel: Selection): Element.Loci {
+        const loci: { unit: Unit, indices: SortedArray }[] = [];
+
+        for (const unit of unionStructure(sel).units) {
+            loci[loci.length] = { unit, indices: SortedArray.indicesOf(sel.source.unitMap.get(unit.id).elements, unit.elements) }
+        }
+
+        return Element.Loci(loci);
+    }
+
     export interface Builder {
         add(structure: Structure): void,
         getSelection(): Selection
diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts
index af44bda4b..ae1757971 100644
--- a/src/mol-view/stage.ts
+++ b/src/mol-view/stage.ts
@@ -10,6 +10,7 @@ import { Progress } from 'mol-task';
 import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBond } from './state/transform';
 import { UrlEntity } from './state/entity';
 import { SpacefillProps } from 'mol-geo/representation/structure/spacefill';
+import { Context } from 'mol-app/context/context';
 
 // export const ColorTheme = {
 //     'atom-index': {},
@@ -30,7 +31,7 @@ export class Stage {
     viewer: Viewer
     ctx = new StateContext(Progress.format)
 
-    constructor() {
+    constructor(public globalContext: Context) {
 
     }
 
@@ -48,6 +49,8 @@ export class Stage {
         const structureEntity = await ModelToStructure.apply(this.ctx, modelEntity)
         StructureToSpacefill.apply(this.ctx, structureEntity, spacefillProps)
         StructureToBond.apply(this.ctx, structureEntity, spacefillProps) // TODO props
+
+        this.globalContext.components.sequenceView.setState({ structure: structureEntity.value });
     }
 
     loadPdbid (pdbid: string) {
-- 
GitLab