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} </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