Browse Source

refactored viewer app to make it usable for simple embedded use-cases

Alexander Rose 5 years ago
parent
commit
7c18e5eb86

+ 65 - 0
src/apps/viewer/embedded.html

@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+        <link rel="icon" href="./favicon.ico" type="image/x-icon">
+        <title>Embedded Mol* Viewer</title>
+        <style>
+            #app {
+                position: absolute;
+                left: 100px;
+                top: 100px;
+                width: 800px;
+                height: 600px;
+            }
+        </style>
+        <link rel="stylesheet" type="text/css" href="molstar.css" />
+    </head>
+    <body>
+        <div id="app"></div>
+        <script type="text/javascript" src="./molstar.js"></script>
+        <script type="text/javascript">
+            var viewer = new molstar.Viewer('app', {
+                extensions: [],
+
+                layoutIsExpanded: false,
+                layoutShowControls: false,
+                layoutShowRemoteState: false,
+                layoutShowSequence: true,
+                layoutShowLog: false,
+                layoutShowLeftPanel: true,
+
+                viewportShowExpand: true,
+                viewportShowSelectionMode: false,
+                viewportShowAnimation: false,
+
+                pdbProvider: 'rcsb',
+                emdbProvider: 'rcsb',
+            });
+            viewer.loadPdb('7bv2');
+            viewer.loadEmdb('EMD-30210');
+
+            // TODO add Volume.customProperty and load suggested isoValue via custom property
+            var sub = viewer.plugin.managers.volume.hierarchy.behaviors.selection.subscribe(function (value) {
+                if (value.volume?.representations[0]) {
+                    var ref = value.volume.representations[0].cell;
+                    var tree = viewer.plugin.state.data.build().to(ref).update({
+                        type: {
+                            name: 'isosurface',
+                            params: {
+                                isoValue: {
+                                    kind: 'relative',
+                                    relativeValue: 6
+                                }
+                            }
+                        },
+                        colorTheme: ref.transform.params?.colorTheme
+                    });
+                    viewer.plugin.runTask(viewer.plugin.state.data.updateTree(tree));
+                    if (typeof sub !== 'undefined') sub.unsubscribe();
+                }
+            });
+        </script>
+    </body>
+</html>

+ 35 - 2
src/apps/viewer/index.html

@@ -34,10 +34,43 @@
                 height: 600px;
             }
         </style>
-        <link rel="stylesheet" type="text/css" href="app.css" />
+        <link rel="stylesheet" type="text/css" href="molstar.css" />
     </head>
     <body>
         <div id="app"></div>
-        <script type="text/javascript" src="./index.js"></script>
+        <script type="text/javascript" src="./molstar.js"></script>
+        <script type="text/javascript">
+            function getParam(name, regex) {
+                var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
+                return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
+            }
+
+            var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
+            var viewer = new molstar.Viewer('app', {
+                layoutShowControls: !hideControls,
+                viewportShowExpand: false,
+            });
+
+            var snapshotId = getParam('snapshot-id', '[^&]+').trim();
+            if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
+
+            var snapshotUrl = getParam('snapshot-url', '[^&]+').trim();
+            var snapshotUrlType = getParam('snapshot-url-type', '[^&]+').toLowerCase().trim();
+            if (snapshotUrl && snapshotUrlType) viewer.loadSnapshotFromUrl(snapshotUrl, snapshotUrlType);
+
+            var structureUrl = getParam('structure-url', '[^&]+').trim();
+            var structureUrlFormat = getParam('structure-url-format', '[a-z]+').toLowerCase().trim();
+            var structureUrlIsBinary = getParam('structure-url-is-binary', '[^&]+').trim() === '1';
+            if (structureUrl) viewer.loadStructureFromUrl(structureUrl, structureUrlFormat, structureUrlIsBinary);
+
+            var pdb = getParam('pdb', '[^&]+').trim();
+            if (pdb) viewer.loadPdb(pdb);
+
+            var pdbDev = getParam('pdb-dev', '[^&]+').trim();
+            if (pdbDev) viewer.loadPdbDev(pdbDev);
+
+            var emdb = getParam('emdb', '[^&]+').trim();
+            if (emdb) viewer.loadEmdb(emdb);
+        </script>
     </body>
 </html>

+ 134 - 64
src/apps/viewer/index.ts

@@ -8,84 +8,110 @@
 import '../../mol-util/polyfill';
 import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
 import './index.html';
+import './embedded.html';
 import './favicon.ico';
 import { PluginContext } from '../../mol-plugin/context';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginSpec } from '../../mol-plugin/spec';
-import { DownloadStructure } from '../../mol-plugin-state/actions/structure';
+import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
 import { PluginConfig } from '../../mol-plugin/config';
 import { CellPack } from '../../extensions/cellpack';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { Asset } from '../../mol-util/assets';
+import { ObjectKeys } from '../../mol-util/type-helpers';
+import { PluginState } from '../../mol-plugin/state';
+import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
+import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 require('mol-plugin-ui/skin/light.scss');
 
-function getParam(name: string, regex: string): string {
-    let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
-    return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
-}
+const Extensions = {
+    'cellpack': PluginSpec.Behavior(CellPack),
+    'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
+    'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
+    'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport)
+};
 
-const hideControls = getParam('hide-controls', `[^&]+`) === '1';
+const DefaultViewerOptions = {
+    extensions: ObjectKeys(Extensions),
+    layoutIsExpanded: true,
+    layoutShowControls: true,
+    layoutShowRemoteState: true,
+    layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
+    layoutShowSequence: true,
+    layoutShowLog: true,
+    layoutShowLeftPanel: true,
 
-function init() {
-    const spec: PluginSpec = {
-        actions: [...DefaultPluginSpec.actions],
-        behaviors: [
-            ...DefaultPluginSpec.behaviors,
-            PluginSpec.Behavior(CellPack),
-            PluginSpec.Behavior(PDBeStructureQualityReport),
-            PluginSpec.Behavior(RCSBAssemblySymmetry),
-            PluginSpec.Behavior(RCSBValidationReport),
-        ],
-        animations: [...DefaultPluginSpec.animations || []],
-        customParamEditors: DefaultPluginSpec.customParamEditors,
-        layout: {
-            initial: {
-                isExpanded: true,
-                showControls: !hideControls
+    viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
+    viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
+    viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
+    pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
+    volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
+    pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
+    emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
+};
+type ViewerOptions = typeof DefaultViewerOptions;
+
+export class Viewer {
+    plugin: PluginContext
+
+    constructor(elementId: string, options: Partial<ViewerOptions> = {}) {
+        const o = { ...DefaultViewerOptions, ...options };
+
+        const spec: PluginSpec = {
+            actions: [...DefaultPluginSpec.actions],
+            behaviors: [
+                ...DefaultPluginSpec.behaviors,
+                ...o.extensions.map(e => Extensions[e]),
+            ],
+            animations: [...DefaultPluginSpec.animations || []],
+            customParamEditors: DefaultPluginSpec.customParamEditors,
+            layout: {
+                initial: {
+                    isExpanded: o.layoutIsExpanded,
+                    showControls: o.layoutShowControls,
+                    controlsDisplay: o.layoutControlsDisplay,
+                },
+                controls: {
+                    ...DefaultPluginSpec.layout && DefaultPluginSpec.layout.controls,
+                    top: o.layoutShowSequence ? undefined : 'none',
+                    bottom: o.layoutShowLog ? undefined : 'none',
+                    left: o.layoutShowLeftPanel ? undefined : 'none',
+                }
             },
-            controls: {
-                ...DefaultPluginSpec.layout && DefaultPluginSpec.layout.controls
-            }
-        },
-        config: DefaultPluginSpec.config
-    };
-    spec.config?.set(PluginConfig.Viewport.ShowExpand, false);
-    const plugin = createPlugin(document.getElementById('app')!, spec);
-    trySetSnapshot(plugin);
-    tryLoadFromUrl(plugin);
-}
+            components: {
+                ...DefaultPluginSpec.components,
+                remoteState: o.layoutShowRemoteState ? 'default' : 'none',
+            },
+            config: DefaultPluginSpec.config
+        };
 
-async function trySetSnapshot(ctx: PluginContext) {
-    try {
-        const snapshotUrl = getParam('snapshot-url', `[^&]+`);
-        const snapshotId = getParam('snapshot-id', `[^&]+`);
-        if (!snapshotUrl && !snapshotId) return;
-        // TODO parametrize the server
-        const url = snapshotId
-            ? `https://webchem.ncbr.muni.cz/molstar-state/get/${snapshotId}`
-            : snapshotUrl;
-        await PluginCommands.State.Snapshots.Fetch(ctx, { url });
-    } catch (e) {
-        ctx.log.error('Failed to load snapshot.');
-        console.warn('Failed to load snapshot', e);
-    }
-}
+        spec.config?.set(PluginConfig.Viewport.ShowExpand, o.viewportShowExpand);
+        spec.config?.set(PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode);
+        spec.config?.set(PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation);
+        spec.config?.set(PluginConfig.State.DefaultServer, o.pluginStateServer);
+        spec.config?.set(PluginConfig.State.CurrentServer, o.pluginStateServer);
+        spec.config?.set(PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer);
+        spec.config?.set(PluginConfig.Download.DefaultPdbProvider, o.pdbProvider);
+        spec.config?.set(PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider);
 
-async function tryLoadFromUrl(ctx: PluginContext) {
-    const url = getParam('loadFromURL', '[^&]+').trim();
-    try {
-        if (!url) return;
+        const element = document.getElementById(elementId);
+        if (!element) throw new Error(`Could not get element with id '${elementId}'`);
+        this.plugin = createPlugin(element, spec);
+    }
 
-        let format = 'cif', isBinary = false;
-        switch (getParam('loadFromURLFormat', '[a-z]+').toLocaleLowerCase().trim()) {
-            case 'pdb': format = 'pdb'; break;
-            case 'mmbcif': isBinary = true; break;
-        }
+    async setRemoteSnapshot(id: string) {
+        const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
+        await PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
+    }
 
-        const params = DownloadStructure.createDefaultParams(void 0 as any, ctx);
+    async loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {
+        await PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
+    }
 
-        return ctx.runTask(ctx.state.data.applyAction(DownloadStructure, {
+    async loadStructureFromUrl(url: string, format = 'cif', isBinary = false) {
+        const params = DownloadStructure.createDefaultParams(undefined, this.plugin);
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
             source: {
                 name: 'url',
                 params: {
@@ -96,10 +122,54 @@ async function tryLoadFromUrl(ctx: PluginContext) {
                 }
             }
         }));
-    } catch (e) {
-        ctx.log.error(`Failed to load from URL (${url})`);
-        console.warn(`Failed to load from URL (${url})`, e);
     }
-}
 
-init();
+    async loadPdb(pdb: string) {
+        const params = DownloadStructure.createDefaultParams(undefined, this.plugin);
+        const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!;
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
+            source: {
+                name: 'pdb' as const,
+                params: {
+                    provider: {
+                        id: pdb,
+                        server: {
+                            name: provider,
+                            params: PdbDownloadProvider[provider].defaultValue as any
+                        }
+                    },
+                    options: params.source.params.options,
+                }
+            }
+        }));
+    }
+
+    async loadPdbDev(pdbDev: string) {
+        const params = DownloadStructure.createDefaultParams(undefined, this.plugin);
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
+            source: {
+                name: 'pdb-dev' as const,
+                params: {
+                    id: pdbDev,
+                    options: params.source.params.options,
+                }
+            }
+        }));
+    }
+
+    async loadEmdb(emdb: string) {
+        const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!;
+        return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
+            source: {
+                name: 'pdb-emd-ds' as const,
+                params: {
+                    provider: {
+                        id: emdb,
+                        server: provider,
+                    },
+                    detail: 3,
+                }
+            }
+        }));
+    }
+}

+ 6 - 6
src/examples/basic-wrapper/index.html

@@ -41,7 +41,7 @@
                 display: block;
             }
         </style>
-        <link rel="stylesheet" type="text/css" href="app.css" />
+        <link rel="stylesheet" type="text/css" href="molstar.css" />
         <script type="text/javascript" src="./index.js"></script>
     </head>
     <body>
@@ -55,13 +55,13 @@
             </select>
         </div>
         <div id="app"></div>
-        <script>      
+        <script>
             function $(id) { return document.getElementById(id); }
-        
+
             var pdbId = '1grm', assemblyId= '1';
             var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif';
             var format = 'mmcif';
-            
+
             $('url').value = url;
             $('url').onchange = function (e) { url = e.target.value; }
             $('assemblyId').value = assemblyId;
@@ -86,7 +86,7 @@
 
             addHeader('Camera');
             addControl('Toggle Spin', () => BasicMolStarWrapper.toggleSpin());
-            
+
             addSeparator();
 
             addHeader('Animation');
@@ -115,7 +115,7 @@
             addControl('Static Superposition', () => BasicMolStarWrapper.tests.staticSuperposition());
             addControl('Dynamic Superposition', () => BasicMolStarWrapper.tests.dynamicSuperposition());
             addControl('Validation Tooltip', () => BasicMolStarWrapper.tests.toggleValidationTooltip());
-            
+
             addControl('Show Toasts', () => BasicMolStarWrapper.tests.showToasts());
             addControl('Hide Toasts', () => BasicMolStarWrapper.tests.hideToasts());
 

+ 3 - 3
src/examples/lighting/index.html

@@ -38,16 +38,16 @@
                 display: block;
             }
         </style>
-        <link rel="stylesheet" type="text/css" href="app.css" />
+        <link rel="stylesheet" type="text/css" href="molstar.css" />
         <script type="text/javascript" src="./index.js"></script>
     </head>
     <body>
         <div id='controls'></div>
         <div id="app"></div>
-        <script>      
+        <script>
             LightingDemo.init('app')
             LightingDemo.load({ url: 'https://files.rcsb.org/download/1M07.cif', assemblyId: '1' })
-            
+
             addHeader('Example PDB IDs');
             addControl('1M07', () => LightingDemo.load({ url: 'https://files.rcsb.org/download/1M07.cif', assemblyId: '1' }));
             addControl('6HY0', () => LightingDemo.load({ url: 'https://files.rcsb.org/download/6HY0.cif', assemblyId: '1' }));

+ 6 - 6
src/examples/proteopedia-wrapper/index.html

@@ -48,7 +48,7 @@
                 width: 300px;
             }
         </style>
-        <link rel="stylesheet" type="text/css" href="app.css" />
+        <link rel="stylesheet" type="text/css" href="molstar.css" />
         <script type="text/javascript" src="./index.js"></script>
     </head>
     <body>
@@ -65,7 +65,7 @@
         <div id="app"></div>
         <div id="volume-streaming-wrapper"></div>
         <script>
-            // it might be a good idea to define these colors in a separate script file 
+            // it might be a good idea to define these colors in a separate script file
             var CustomColors = [0x00ff00, 0x0000ff];
 
             // create an instance of the plugin
@@ -74,11 +74,11 @@
             console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR, MolStarProteopediaWrapper.VERSION_MINOR);
 
             function $(id) { return document.getElementById(id); }
-        
+
             var pdbId = '1cbs', assemblyId= 'preferred', isBinary = true;
             var url = 'https://www.ebi.ac.uk/pdbe/entry-files/download/' + pdbId + '.bcif'
             var format = 'cif';
-            
+
             $('url').value = url;
             $('url').onchange = function (e) { url = e.target.value; }
             $('assemblyId').value = assemblyId;
@@ -144,7 +144,7 @@
             // Same as "wheel icon" and Viewport options
             // addControl('Clip', () => PluginWrapper.viewport.setSettings({ clip: [33, 66] }));
             // addControl('Reset Clip', () => PluginWrapper.viewport.setSettings({ clip: [1, 100] }));
-            
+
             addSeparator();
 
             addHeader('Animation');
@@ -177,7 +177,7 @@
             addControl('Init', () => PluginWrapper.experimentalData.init($('volume-streaming-wrapper')));
             addControl('Remove', () => PluginWrapper.experimentalData.remove());
 
-            addSeparator(); 
+            addSeparator();
             addHeader('State');
 
             var snapshot;

+ 4 - 4
webpack.config.common.js

@@ -43,7 +43,7 @@ const sharedConfig = {
             // __VERSION_TIMESTAMP__: webpack.DefinePlugin.runtimeValue(() => `${new Date().valueOf()}`, true),
             'process.env.DEBUG': JSON.stringify(process.env.DEBUG)
         }),
-        new MiniCssExtractPlugin({ filename: 'app.css' }),
+        new MiniCssExtractPlugin({ filename: 'molstar.css',  }),
         new VersionFile({
             extras: { timestamp: `${new Date().valueOf()}` },
             packageFile: path.resolve(__dirname, 'package.json'),
@@ -73,11 +73,11 @@ function createEntry(src, outFolder, outFilename, isNode) {
     }
 }
 
-function createEntryPoint(name, dir, out) {
+function createEntryPoint(name, dir, out, library) {
     return {
         node: { fs: 'empty' }, // TODO find better solution? Currently used in file-handle.ts
         entry: path.resolve(__dirname, `lib/${dir}/${name}.js`),
-        output: { filename: `${name}.js`, path: path.resolve(__dirname, `build/${out}`) },
+        output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd' },
         ...sharedConfig
     }
 }
@@ -97,7 +97,7 @@ function createNodeEntryPoint(name, dir, out) {
     }
 }
 
-function createApp(name) { return createEntryPoint('index', `apps/${name}`, name) }
+function createApp(name, library) { return createEntryPoint('index', `apps/${name}`, name, library) }
 function createExample(name) { return createEntry(`examples/${name}/index`, `examples/${name}`, 'index') }
 function createBrowserTest(name) { return createEntryPoint(name, 'tests/browser', 'tests') }
 function createNodeApp(name) { return createNodeEntryPoint('index', `apps/${name}`, name) }

+ 1 - 1
webpack.config.viewer.js

@@ -1,5 +1,5 @@
 const common = require('./webpack.config.common.js');
 const createApp = common.createApp;
 module.exports = [
-    createApp('viewer')
+    createApp('viewer', 'molstar')
 ]