diff --git a/README.md b/README.md index 0ac60a18d27686a1c9e0550cdd91e84b653c6de9..c7a0e683f1167095371c524669fdecb88be37d88 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 f828e597af544499d74c9c85afc8dea6e4bd7fd7..b4d3823d0bc0d8f2fe473fc73cf2e33b5b4ba2f6 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 92a317aad2dc583dcf9f1f9051a6295c26536596..5c8fee555e120226afad3fc1ecf5e58c0ea95655 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 0000000000000000000000000000000000000000..17423f9806beef2b22a3eb3c4a4117eed3bf1534 --- /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 99cf366f9c83e66e05af729e3224d74ec7f252de..b87ea5c14eec10f8c2a7ff7161cd569221611bde 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 0000000000000000000000000000000000000000..2be5ea58067f9e28e888a0a5cc6ef7aa015dface --- /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 b84b69214b3468b2d165104c9a20c6ff0cf542c3..219de13b1086c2c6ecd7d3ac1d27b5007b1e97c8 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 41e78fc7465ee379df02eb3565bcac082c6c4584..cd572232a6843ab134fca6b60e97568d8a4730ee 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 0000000000000000000000000000000000000000..4105dea2ed580bf99d89cc0ecd6c309db2150876 --- /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 1704ab09282eb09ec1e30a442532e52dc80def63..0551a4c2098ebf6cb5ec74367fb37905d55b0500 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 06f0c888d9af880650668dfe69776c0308aae9ce..67e617ef3c172f9d525982fb250ecd23bc07f866 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 a2f53c56df7c790bbbacadc67e1d28a4f983a6ac..3cd6e601a704acb142ee34472deecbb2c54a7d84 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 a1fc34f2a0e6f741a667aa97c2af2fcd7ec7a9c2..0aaa01922dc0f82ca9626e9ba9b0d258125ec742 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 af44bda4bf3e7f202db99f95f6f971f8ef7b347b..ae1757971fa9f1815be9363065b42e8d7e53e1d7 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) {