diff --git a/src/examples/docking-viewer/index.html b/src/examples/docking-viewer/index.html new file mode 100644 index 0000000000000000000000000000000000000000..24e6504181db342b753449065b662b0377c1f575 --- /dev/null +++ b/src/examples/docking-viewer/index.html @@ -0,0 +1,50 @@ +<!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"> + <title>Mol* Docking 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="./index.js"></script> + <script type="text/javascript"> + var viewer = new DockingViewer('app', { + layoutIsExpanded: false, + layoutShowControls: false, + layoutShowRemoteState: false, + layoutShowSequence: true, + layoutShowLog: false, + layoutShowLeftPanel: true, + + viewportShowExpand: true, + viewportShowControls: false, + viewportShowSettings: false, + viewportShowSelectionMode: false, + viewportShowAnimation: false, + }); + + function getParam(name, regex) { + var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i'); + return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || ''); + } + var pdbqt = getParam('pdbqt', '[^&]+').trim(); + var mol2 = getParam('mol2', '[^&]+').trim(); + + viewer.loadStructuresFromUrlsAndMerge([ + { url: pdbqt, format: 'pdbqt' }, + { url: mol2, format: 'mol2' } + ]); + </script> + </body> +</html> \ No newline at end of file diff --git a/src/examples/docking-viewer/index.ts b/src/examples/docking-viewer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..96f25a55215da3067318c0ef9d4eb4a61ff43561 --- /dev/null +++ b/src/examples/docking-viewer/index.ts @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import '../../mol-util/polyfill'; +import { createPlugin, DefaultPluginSpec } from '../../mol-plugin'; +import './index.html'; +import { PluginContext } from '../../mol-plugin/context'; +import { PluginCommands } from '../../mol-plugin/commands'; +import { PluginSpec } from '../../mol-plugin/spec'; +import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure'; +import { PluginConfig } from '../../mol-plugin/config'; +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'; +import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory'; +import { Structure } from '../../mol-model/structure'; +import { PluginStateTransform, PluginStateObject as PSO } from '../../mol-plugin-state/objects'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { Task } from '../../mol-task'; +import { StateObject } from '../../mol-state'; +import { ViewportComponent, StructurePreset } from './viewport'; +import { PluginBehaviors } from '../../mol-plugin/behavior'; +import { ColorNames } from '../../mol-util/color/names'; + +require('mol-plugin-ui/skin/light.scss'); + +export { PLUGIN_VERSION as version } from '../../mol-plugin/version'; +export { setProductionMode, setDebugMode } from '../../mol-util/debug'; + +const DefaultViewerOptions = { + extensions: ObjectKeys({}), + layoutIsExpanded: true, + layoutShowControls: true, + layoutShowRemoteState: true, + layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay, + layoutShowSequence: true, + layoutShowLog: true, + layoutShowLeftPanel: true, + + viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue, + viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue, + viewportShowSettings: PluginConfig.Viewport.ShowSettings.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; + +class Viewer { + plugin: PluginContext + + constructor(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) { + const o = { ...DefaultViewerOptions, ...options }; + + const spec: PluginSpec = { + actions: [...DefaultPluginSpec.actions], + behaviors: [ + PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci, { mark: false }), + PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), + PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci), + + PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo), + PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions), + PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure), + ], + 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', + } + }, + components: { + ...DefaultPluginSpec.components, + remoteState: o.layoutShowRemoteState ? 'default' : 'none', + viewport: { + view: ViewportComponent + } + }, + config: [ + [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand], + [PluginConfig.Viewport.ShowControls, o.viewportShowControls], + [PluginConfig.Viewport.ShowSettings, o.viewportShowSettings], + [PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode], + [PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation], + [PluginConfig.State.DefaultServer, o.pluginStateServer], + [PluginConfig.State.CurrentServer, o.pluginStateServer], + [PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer], + [PluginConfig.Download.DefaultPdbProvider, o.pdbProvider], + [PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider] + ] + }; + + const element = typeof elementOrId === 'string' + ? document.getElementById(elementOrId) + : elementOrId; + if (!element) throw new Error(`Could not get element with id '${elementOrId}'`); + this.plugin = createPlugin(element, spec); + + PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { + renderer: { + ...this.plugin.canvas3d!.props.renderer, + backgroundColor: ColorNames.white, + }, + camera: { + ...this.plugin.canvas3d!.props.camera, + helper: { axes: { name: 'off', params: {} } } + } + } }); + } + + setRemoteSnapshot(id: string) { + const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`; + return PluginCommands.State.Snapshots.Fetch(this.plugin, { url }); + } + + loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) { + return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type }); + } + + loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false) { + const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); + return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { + source: { + name: 'url', + params: { + url: Asset.Url(url), + format: format as any, + isBinary, + options: params.source.params.options, + } + } + })); + } + + async loadStructuresFromUrlsAndMerge(sources: { url: string, format: BuiltInTrajectoryFormat, isBinary?: boolean }[]) { + const structures: { ref: string }[] = []; + for (const { url, format, isBinary } of sources) { + const data = await this.plugin.builders.data.download({ url, isBinary }); + const trajectory = await this.plugin.builders.structure.parseTrajectory(data, format); + const model = await this.plugin.builders.structure.createModel(trajectory); + const modelProperties = await this.plugin.builders.structure.insertModelProperties(model); + const structure = await this.plugin.builders.structure.createStructure(modelProperties || model); + const structureProperties = await this.plugin.builders.structure.insertStructureProperties(structure); + + structures.push({ ref: structureProperties?.ref || structure.ref }); + } + const dependsOn = structures.map(({ ref }) => ref); + const data = this.plugin.state.data.build().toRoot().apply(MergeStructures, { structures }, { dependsOn }); + const structure = await data.commit(); + const structureProperties = await this.plugin.builders.structure.insertStructureProperties(structure); + await this.plugin.builders.structure.representation.applyPreset(structureProperties || structure, StructurePreset); + } + + async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, options?: { dataLabel?: string }) { + const _data = await this.plugin.builders.data.rawData({ data, label: options?.dataLabel }); + const trajectory = await this.plugin.builders.structure.parseTrajectory(_data, format); + await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default'); + } + + loadPdb(pdb: string) { + const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, 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, + } + } + })); + } + + loadPdbDev(pdbDev: string) { + const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin); + return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, { + source: { + name: 'pdb-dev' as const, + params: { + provider: { + id: pdbDev, + encoding: 'bcif', + }, + options: params.source.params.options, + } + } + })); + } + + 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, + } + } + })); + } +} + +type MergeStructures = typeof MergeStructures +const MergeStructures = PluginStateTransform.BuiltIn({ + name: 'merge-structures', + display: { name: 'Merge Structures', description: 'Merge Structure' }, + from: PSO.Root, + to: PSO.Molecule.Structure, + params: { + structures: PD.ObjectList({ + ref: PD.Text('') + }, ({ ref }) => ref, { isHidden: true }) + } +})({ + apply({ params, dependencies }) { + return Task.create('Merge Structures', async ctx => { + if (params.structures.length === 0) return StateObject.Null; + + const first = dependencies![params.structures[0].ref].data as Structure; + const builder = Structure.Builder({ masterModel: first.models[0] }); + for (const { ref } of params.structures) { + const s = dependencies![ref].data as Structure; + for (const unit of s.units) { + // TODO invariantId + builder.addUnit(unit.kind, unit.model, unit.conformation.operator, unit.elements, unit.traits); + } + } + + const structure = builder.getStructure(); + return new PSO.Molecule.Structure(structure, { label: 'Merged Structure' }); + }); + } +}); + +(window as any).DockingViewer = Viewer; \ No newline at end of file diff --git a/src/examples/docking-viewer/viewport.tsx b/src/examples/docking-viewer/viewport.tsx new file mode 100644 index 0000000000000000000000000000000000000000..669f971c79666d1442f65070185c65cc83f82185 --- /dev/null +++ b/src/examples/docking-viewer/viewport.tsx @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react'; +import { PluginUIComponent } from '../../mol-plugin-ui/base'; +import { Viewport, ViewportControls } from '../../mol-plugin-ui/viewport'; +import { BackgroundTaskProgress } from '../../mol-plugin-ui/task'; +import { LociLabels } from '../../mol-plugin-ui/controls'; +import { Toasts } from '../../mol-plugin-ui/toast'; +import { Button } from '../../mol-plugin-ui/controls/common'; +import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../mol-plugin-state/builder/structure/representation-preset'; +import { StateObjectRef } from '../../mol-state'; +import { StructureSelectionQueries, StructureSelectionQuery } from '../../mol-plugin-state/helpers/structure-selection-query'; +import { MolScriptBuilder as MS } from '../../mol-script/language/builder'; +import { InteractionsRepresentationProvider } from '../../mol-model-props/computed/representations/interactions'; +import { InteractionTypeColorThemeProvider } from '../../mol-model-props/computed/themes/interaction-type'; +import { compile } from '../../mol-script/runtime/query/compiler'; +import { StructureSelection, QueryContext, Structure } from '../../mol-model/structure'; +import { PluginCommands } from '../../mol-plugin/commands'; +import { PluginContext } from '../../mol-plugin/context'; + +function shinyStyle(plugin: PluginContext) { + return PluginCommands.Canvas3D.SetSettings(plugin, { settings: { + renderer: { + ...plugin.canvas3d!.props.renderer, + style: { name: 'plastic', params: {} }, + }, + postprocessing: { + ...plugin.canvas3d!.props.postprocessing, + occlusion: { name: 'off', params: {} }, + outline: { name: 'off', params: {} } + } + } }); +} + +function occlusionStyle(plugin: PluginContext) { + return PluginCommands.Canvas3D.SetSettings(plugin, { settings: { + renderer: { + ...plugin.canvas3d!.props.renderer, + style: { name: 'flat', params: {} } + }, + postprocessing: { + ...plugin.canvas3d!.props.postprocessing, + occlusion: { name: 'on', params: { + kernelSize: 8, + bias: 0.8, + radius: 64 + } }, + outline: { name: 'on', params: { + scale: 1.0, + threshold: 0.8 + } } + } + } }); +} + +const ligandPlusSurroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Ligand plus Ligand itself', MS.struct.modifier.union([ + MS.struct.modifier.includeSurroundings({ + 0: StructureSelectionQueries.ligand.expression, + radius: 5, + 'as-whole-residues': true + }) +])); + +const ligandSurroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Ligand', MS.struct.modifier.union([ + MS.struct.modifier.exceptBy({ + 0: ligandPlusSurroundings.expression, + by: StructureSelectionQueries.ligand.expression + }) +])); + +const PresetParams = { + ...StructureRepresentationPresetProvider.CommonParams, +}; + +export const StructurePreset = StructureRepresentationPresetProvider({ + id: 'preset-structure', + display: { name: 'Structure' }, + params: () => PresetParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + if (!structureCell) return {}; + + const components = { + ligand: await presetStaticComponent(plugin, structureCell, 'ligand'), + polymer: await presetStaticComponent(plugin, structureCell, 'polymer'), + }; + + const { update, builder, typeParams, color } = StructureRepresentationPresetProvider.reprBuilder(plugin, params); + const representations = { + ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color }, { tag: 'ligand' }), + polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams }, color }, { tag: 'polymer' }), + }; + + await update.commit({ revertOnError: true }); + await shinyStyle(plugin); + plugin.managers.interactivity.setProps({ granularity: 'residue' }); + + return { components, representations }; + } +}); + +export const IllustrativePreset = StructureRepresentationPresetProvider({ + id: 'preset-illustrative', + display: { name: 'Illustrative' }, + params: () => PresetParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + if (!structureCell) return {}; + + const components = { + all: await presetStaticComponent(plugin, structureCell, 'all') + }; + + const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params); + const representations = { + all: builder.buildRepresentation(update, components.all, { type: 'spacefill', typeParams: { ...typeParams }, color: 'illustrative' }, { tag: 'all' }), + }; + + await update.commit({ revertOnError: true }); + await occlusionStyle(plugin); + plugin.managers.interactivity.setProps({ granularity: 'residue' }); + + return { components, representations }; + } +}); + +const PocketPreset = StructureRepresentationPresetProvider({ + id: 'preset-pocket', + display: { name: 'Pocket' }, + params: () => PresetParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + const structure = structureCell?.obj?.data; + if (!structureCell || !structure) return {}; + + const components = { + ligand: await presetStaticComponent(plugin, structureCell, 'ligand'), + surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `selection`), + }; + + const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params); + const representations = { + ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'partial-charge' }, { tag: 'ligand' }), + surroundings: builder.buildRepresentation(update, components.surroundings, { type: 'molecular-surface', typeParams: { ...typeParams, includeParent: true, quality: 'custom', resolution: 0.2, doubleSided: true }, color: 'partial-charge' }, { tag: 'surroundings' }), + }; + + await update.commit({ revertOnError: true }); + await shinyStyle(plugin); + plugin.managers.interactivity.setProps({ granularity: 'element' }); + + const compiled = compile<StructureSelection>(StructureSelectionQueries.ligand.expression); + const result = compiled(new QueryContext(structure)); + const selection = StructureSelection.unionStructure(result); + plugin.managers.camera.focusLoci(Structure.toStructureElementLoci(selection)); + + return { components, representations }; + } +}); + +const InteractionsPreset = StructureRepresentationPresetProvider({ + id: 'preset-interactions', + display: { name: 'Interactions' }, + params: () => PresetParams, + async apply(ref, params, plugin) { + const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref); + const structure = structureCell?.obj?.data; + if (!structureCell || !structure) return {}; + + const components = { + ligand: await presetStaticComponent(plugin, structureCell, 'ligand'), + selection: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `selection`) + }; + + const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params); + const representations = { + ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'partial-charge' }, { tag: 'ligand' }), + ballAndStick: builder.buildRepresentation(update, components.selection, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'partial-charge' }, { tag: 'ball-and-stick' }), + interactions: builder.buildRepresentation(update, components.selection, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }), + }; + + await update.commit({ revertOnError: true }); + await shinyStyle(plugin); + plugin.managers.interactivity.setProps({ granularity: 'element' }); + + const compiled = compile<StructureSelection>(StructureSelectionQueries.ligand.expression); + const result = compiled(new QueryContext(structure)); + const selection = StructureSelection.unionStructure(result); + plugin.managers.camera.focusLoci(Structure.toStructureElementLoci(selection)); + + return { components, representations }; + } +}); + +export class ViewportComponent extends PluginUIComponent { + structurePreset = () => { + this.plugin.managers.structure.component.applyPreset( + this.plugin.managers.structure.hierarchy.selection.structures, + StructurePreset + ); + } + + illustrativePreset = () => { + this.plugin.managers.structure.component.applyPreset( + this.plugin.managers.structure.hierarchy.selection.structures, + IllustrativePreset + ); + } + + pocketPreset = () => { + this.plugin.managers.structure.component.applyPreset( + this.plugin.managers.structure.hierarchy.selection.structures, + PocketPreset + ); + } + + interactionsPreset = () => { + this.plugin.managers.structure.component.applyPreset( + this.plugin.managers.structure.hierarchy.selection.structures, + InteractionsPreset + ); + } + + render() { + const VPControls = this.plugin.spec.components?.viewport?.controls || ViewportControls; + + return <> + <Viewport /> + <div className='msp-viewport-top-left-controls'> + <div style={{ marginBottom: '4px' }}> + <Button onClick={this.structurePreset} >Structure</Button> + </div> + <div style={{ marginBottom: '4px' }}> + <Button onClick={this.illustrativePreset}>Illustrative</Button> + </div> + <div style={{ marginBottom: '4px' }}> + <Button onClick={this.pocketPreset}>Pocket</Button> + </div> + <div style={{ marginBottom: '4px' }}> + <Button onClick={this.interactionsPreset}>Interactions</Button> + </div> + </div> + <VPControls /> + <BackgroundTaskProgress /> + <div className='msp-highlight-toast-wrapper'> + <LociLabels /> + <Toasts /> + </div> + </>; + } +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 16765f511da4e4b459381fecb4f8b4e5c975fadf..f4cdb9f476a93b24a76405c2f722c92cc6b226cb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,6 @@ const { createApp, createExample, createBrowserTest } = require('./webpack.config.common.js'); -const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting']; +const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting', 'docking-viewer']; const tests = [ 'font-atlas', 'marching-cubes',