Bladeren bron

viewer: jolecule state loader

David Sehnal 6 jaren geleden
bovenliggende
commit
868f3d43fa

+ 162 - 0
src/apps/viewer/extensions/jolecule.ts

@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateTree, StateBuilder, StateAction } from 'mol-state';
+import { StateTransforms } from 'mol-plugin/state/transforms';
+import { createModelTree, complexRepresentation } from 'mol-plugin/state/actions/structure';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { ParamDefinition } from 'mol-util/param-definition';
+import { PluginCommands } from 'mol-plugin/command';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots';
+import { MolScriptBuilder as MS } from 'mol-script/language/builder';
+import { Text } from 'mol-geo/geometry/text/text';
+import { UUID } from 'mol-util';
+import { ColorNames } from 'mol-util/color/tables';
+import { Camera } from 'mol-canvas3d/camera';
+import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
+
+export const CreateJoleculeState = StateAction.build({
+    display: { name: 'Jolecule State' },
+    params: { id: ParamDefinition.Text('1mbo') },
+    from: PluginStateObject.Root
+})(async ({ ref, state, params }, plugin: PluginContext) => {
+    try {
+        const id = params.id.trim().toLowerCase();
+        const data = await plugin.runTask(plugin.fetch({ url: `https://jolecule.appspot.com/pdb/${id}.views.json`, type: 'json' })) as JoleculeSnapshot[];
+
+        data.sort((a, b) => a.order - b.order);
+
+        await PluginCommands.State.RemoveObject.dispatch(plugin, { state, ref }, true);
+        plugin.state.snapshots.clear();
+
+        const template = createTemplate(plugin, state.tree, id);
+        const snapshots = data.map((e, idx) => buildSnapshot(plugin, template, { e, idx, len: data.length }));
+        for (const s of snapshots) {
+            plugin.state.snapshots.add(s);
+        }
+
+        PluginCommands.State.Snapshots.Apply.dispatch(plugin, { id: snapshots[0].snapshot.id }, true);
+    } catch (e) {
+        plugin.log.error(`Jolecule Failed: ${e}`);
+    }
+});
+
+interface JoleculeSnapshot {
+    order: number,
+    distances: { i_atom1: number, i_atom2: number }[],
+    labels: { i_atom: number, text: string }[],
+    camera: { up: Vec3, pos: Vec3, in: Vec3, slab: { z_front: number, z_back: number, zoom: number } },
+    selected: number[],
+    text: string
+}
+
+function createTemplate(plugin: PluginContext, tree: StateTree, id: string) {
+    const b = new StateBuilder.Root(tree);
+    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { props: { isGhost: true }});
+    const model = createModelTree(data, 'cif');
+    const structure = model.apply(StateTransforms.Model.StructureFromModel, {}, { ref: 'structure' });
+    complexRepresentation(plugin, structure, { hideWater: true });
+    return b.getTree();
+}
+
+const labelOptions = {
+    ...ParamDefinition.getDefaultValues(Text.Params),
+    sizeFactor: 1.5,
+    offsetX: 1,
+    offsetY: 1,
+    offsetZ: 10,
+    background: true,
+    backgroundMargin: 0.2,
+    backgroundColor: ColorNames.snow,
+    backgroundOpacity: 0.9
+}
+
+// const distanceLabelOptions = {
+//     ...ParamDefinition.getDefaultValues(Text.Params),
+//     sizeFactor: 1,
+//     offsetX: 0,
+//     offsetY: 0,
+//     offsetZ: 10,
+//     background: true,
+//     backgroundMargin: 0.2,
+//     backgroundColor: ColorNames.snow,
+//     backgroundOpacity: 0.9
+// }
+
+function buildSnapshot(plugin: PluginContext, template: StateTree, params: { e: JoleculeSnapshot, idx: number, len: number }): PluginStateSnapshotManager.Entry {
+    const b = new StateBuilder.Root(template);
+
+    let i = 0;
+    for (const l of params.e.labels) {
+        b.to('structure')
+            .apply(StateTransforms.Model.StructureSelection, { query: createQuery([l.i_atom]), label: `Label ${++i}` })
+            .apply(StateTransforms.Representation.StructureLabels3D, {
+                target: { name: 'static-text', params: { value: l.text || '' } },
+                options: labelOptions
+            });
+    }
+    if (params.e.selected && params.e.selected.length > 0) {
+        b.to('structure')
+            .apply(StateTransforms.Model.StructureSelection, { query: createQuery(params.e.selected), label: `Selected` })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'ball-and-stick'));
+    }
+    // TODO
+    // for (const l of params.e.distances) {
+    //     b.to('structure')
+    //         .apply(StateTransforms.Model.StructureSelection, { query: createQuery([l.i_atom1, l.i_atom2]), label: `Distance ${++i}` })
+    //         .apply(StateTransforms.Representation.StructureLabels3D, {
+    //             target: { name: 'static-text', params: { value: l. || '' } },
+    //             options: labelOptions
+    //         });
+    // }
+    return PluginStateSnapshotManager.Entry({
+        id: UUID.create22(),
+        data: { tree: StateTree.toJSON(b.getTree()) },
+        camera: {
+            current: getCameraSnapshot(params.e.camera),
+            transitionStyle: 'animate',
+            transitionDurationInMs: 350
+        }
+    }, {
+        name:  params.e.text
+    });
+}
+
+function getCameraSnapshot(e: JoleculeSnapshot['camera']): Camera.Snapshot {
+    const direction = Vec3.sub(Vec3.zero(), e.pos, e.in);
+    Vec3.normalize(direction, direction);
+    const up = Vec3.sub(Vec3.zero(), e.pos, e.up);
+    Vec3.normalize(up, up);
+
+    const s: Camera.Snapshot = {
+        mode: 'perspective',
+        position: Vec3.scaleAndAdd(Vec3.zero(), e.pos, direction, e.slab.zoom),
+        target: e.pos,
+        direction,
+        up,
+        near: e.slab.zoom + e.slab.z_front,
+        far: e.slab.zoom + e.slab.z_back,
+        fogNear: e.slab.zoom + e.slab.z_front,
+        fogFar: e.slab.zoom + e.slab.z_back,
+        fov: Math.PI / 4,
+        zoom: 1
+    };
+    return s;
+}
+
+function createQuery(atomIndices: number[]) {
+    if (atomIndices.length === 0) return MS.struct.generator.empty();
+
+    return MS.struct.generator.atomGroups({
+        'atom-test': atomIndices.length === 1
+            ? MS.core.rel.eq([MS.struct.atomProperty.core.sourceIndex(), atomIndices[0]])
+            : MS.core.set.has([MS.set.apply(null, atomIndices), MS.struct.atomProperty.core.sourceIndex()]),
+        'group-by': 0
+    });
+}

+ 8 - 3
src/apps/viewer/index.ts

@@ -8,6 +8,8 @@ import { createPlugin, DefaultPluginSpec } from 'mol-plugin';
 import './index.html'
 import { PluginContext } from 'mol-plugin/context';
 import { PluginCommands } from 'mol-plugin/command';
+import { PluginSpec } from 'mol-plugin/spec';
+import { CreateJoleculeState } from './extensions/jolecule';
 require('mol-plugin/skin/light.scss')
 
 function getParam(name: string, regex: string): string {
@@ -18,15 +20,18 @@ function getParam(name: string, regex: string): string {
 const hideControls = getParam('hide-controls', `[^&]+`) === '1';
 
 function init() {
-    const plugin = createPlugin(document.getElementById('app')!, {
-        ...DefaultPluginSpec,
+    const spec: PluginSpec = {
+        actions: [...DefaultPluginSpec.actions, PluginSpec.Action(CreateJoleculeState)],
+        behaviors: [...DefaultPluginSpec.behaviors],
+        animations: [...DefaultPluginSpec.animations || []],
         layout: {
             initial: {
                 isExpanded: true,
                 showControls: !hideControls
             }
         }
-    });
+    };
+    const plugin = createPlugin(document.getElementById('app')!, spec);
     trySetSnapshot(plugin);
 }
 

+ 10 - 10
src/mol-plugin/command/base.ts

@@ -13,7 +13,7 @@ export { PluginCommand }
 
 interface PluginCommand<T = unknown> {
     readonly id: UUID,
-    dispatch(ctx: PluginContext, params: T): Promise<void>,
+    dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void>,
     subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription,
     params: { isImmediate: boolean }
 }
@@ -24,8 +24,8 @@ function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginC
 }
 
 class Impl<T> implements PluginCommand<T> {
-    dispatch(ctx: PluginContext, params: T): Promise<void> {
-        return ctx.commands.dispatch(this, params)
+    dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void> {
+        return ctx.commands.dispatch(this, params, isChild);
     }
     subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription {
         return ctx.commands.subscribe(this, action);
@@ -43,7 +43,7 @@ namespace PluginCommand {
     }
 
     export type Action<T> = (params: T) => unknown | Promise<unknown>
-    type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void }
+    type Instance = { cmd: PluginCommand<any>, params: any, isChild: boolean, resolve: () => void, reject: (e: any) => void }
 
     export class Manager {
         private subs = new Map<string, Action<any>[]>();
@@ -84,7 +84,7 @@ namespace PluginCommand {
 
 
         /** Resolves after all actions have completed */
-        dispatch<T>(cmd: PluginCommand<T>, params: T) {
+        dispatch<T>(cmd: PluginCommand<T>, params: T, isChild = false) {
             return new Promise<void>((resolve, reject) => {
                 if (this.disposing) {
                     reject('disposed');
@@ -97,12 +97,12 @@ namespace PluginCommand {
                     return;
                 }
 
-                const instance: Instance = { cmd, params, resolve, reject };
+                const instance: Instance = { cmd, params, resolve, reject, isChild };
 
-                if (cmd.params.isImmediate) {
+                if (cmd.params.isImmediate || isChild) {
                     this.resolve(instance);
                 } else {
-                    this.queue.addLast({ cmd, params, resolve, reject });
+                    this.queue.addLast(instance);
                     this.next();
                 }
             });
@@ -127,7 +127,7 @@ namespace PluginCommand {
             }
 
             try {
-                if (!instance.cmd.params.isImmediate) this.executing = true;
+                if (!instance.cmd.params.isImmediate && !instance.isChild) this.executing = true;
                 // TODO: should actions be called "asynchronously" ("setImmediate") instead?
                 for (const a of actions) {
                     await a(instance.params);
@@ -136,7 +136,7 @@ namespace PluginCommand {
             } catch (e) {
                 instance.reject(e);
             } finally {
-                if (!instance.cmd.params.isImmediate) {
+                if (!instance.cmd.params.isImmediate && !instance.isChild) {
                     this.executing = false;
                     if (!this.disposing) this.next();
                 }

+ 38 - 27
src/mol-plugin/state/actions/structure.ts

@@ -101,13 +101,13 @@ const DownloadStructure = StateAction.build({
                 })
             }, { isFlat: true })
         }, {
-            options: [
-                ['pdbe-updated', 'PDBe Updated'],
-                ['rcsb', 'RCSB'],
-                ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
-                ['url', 'URL']
-            ]
-        })
+                options: [
+                    ['pdbe-updated', 'PDBe Updated'],
+                    ['rcsb', 'RCSB'],
+                    ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
+                    ['url', 'URL']
+                ]
+            })
     }
 })(({ params, state }, ctx: PluginContext) => {
     const b = state.build();
@@ -143,7 +143,7 @@ const DownloadStructure = StateAction.build({
         createStructureTree(ctx, traj, supportProps);
     } else {
         for (const download of downloadParams) {
-            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true }});
+            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true } });
             const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
             createStructureTree(ctx, traj, supportProps)
         }
@@ -172,18 +172,18 @@ function createSingleTrajectoryModel(sources: StateTransformer.Params<Download>[
         .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
 }
 
-function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' | 'gro' = 'cif') {
+export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' | 'gro' = 'cif') {
     let parsed: StateBuilder.To<PluginStateObject.Molecule.Trajectory>
     switch (format) {
         case 'cif':
-            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true }})
-                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true }})
+            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true } })
+                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true } })
             break
         case 'pdb':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true }});
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true } });
             break
         case 'gro':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true }});
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true } });
             break
         default:
             throw new Error('unsupported format')
@@ -203,19 +203,30 @@ function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateO
     return root;
 }
 
-function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) {
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
+export function complexRepresentation(
+    ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>,
+    params?: { hideSequence?: boolean, hideHET?: boolean, hideWater?: boolean, hideCoarse?: boolean; }
+) {
+    if (!params || !params.hideSequence) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
+    }
+    if (!params || !params.hideHET) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
+    }
+    if (!params || !params.hideWater) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
+    }
+    if (!params || !params.hideCoarse) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
+    }
 }
 
 export const CreateComplexRepresentation = StateAction.build({
@@ -296,7 +307,7 @@ export const StructureFromSelection = StateAction.build({
 
 
 export const TestBlob = StateAction.build({
-    display: { name: 'Test Blob'},
+    display: { name: 'Test Blob' },
     from: PluginStateObject.Root
 })(({ ref, state }, ctx: PluginContext) => {
 

+ 1 - 1
src/mol-plugin/ui/viewport.tsx

@@ -72,9 +72,9 @@ export class ViewportControls extends PluginUIComponent {
         return <div className={'msp-viewport-controls'}>
             <div className='msp-viewport-controls-buttons'>
                 {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}<br/>
-                {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}<br/>
                 {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}<br/>
                 {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}
+                {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}<br/>
             </div>
             {this.state.isSettingsExpanded &&
             <div className='msp-viewport-controls-scene-options'>