diff --git a/src/examples/proteopedia-wrapper/annotation.ts b/src/examples/proteopedia-wrapper/annotation.ts new file mode 100644 index 0000000000000000000000000000000000000000..54c89117c7f78c5209364fab76dcd1d35f3f7599 --- /dev/null +++ b/src/examples/proteopedia-wrapper/annotation.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { CustomElementProperty } from 'mol-model-props/common/custom-element-property'; +import { Model, ElementIndex } from 'mol-model/structure'; +import { Color } from 'mol-util/color'; + +export const StripedResidues = CustomElementProperty.create<number>({ + isStatic: true, + name: 'basic-wrapper-residue-striping', + display: 'Residue Stripes', + getData(model: Model) { + const map = new Map<ElementIndex, number>(); + const residueIndex = model.atomicHierarchy.residueAtomSegments.index; + for (let i = 0, _i = model.atomicHierarchy.atoms._rowCount; i < _i; i++) { + map.set(i as ElementIndex, residueIndex[i] % 2); + } + return map; + }, + coloring: { + getColor(e) { return e === 0 ? Color(0xff0000) : Color(0x0000ff) }, + defaultColor: Color(0x777777) + }, + format(e) { + return e === 0 ? 'Odd stripe' : 'Even stripe' + } +}) \ No newline at end of file diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..01b89bc62ce9a2f824304a0ef169c19ee86129b0 --- /dev/null +++ b/src/examples/proteopedia-wrapper/helpers.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { ResidueIndex } from 'mol-model/structure'; +import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry'; +import { BuiltInColorThemeName } from 'mol-theme/color'; + +export interface StructureInfo { + ligands: { name: string, indices: ResidueIndex[] }[], + assemblies: { id: string, description: string, isPreferred: boolean }[] +} + +export type SupportedFormats = 'cif' | 'pdb' +export interface LoadParams { + url: string, + format?: SupportedFormats, + assemblyId?: string, + representationStyle?: RepresentationStyle +} + +export interface RepresentationStyle { + sequence?: RepresentationStyle.Entry, + hetGroups?: RepresentationStyle.Entry, + water?: RepresentationStyle.Entry +} + +export namespace RepresentationStyle { + export type Entry = { kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName } +} \ No newline at end of file diff --git a/src/examples/proteopedia-wrapper/index.html b/src/examples/proteopedia-wrapper/index.html new file mode 100644 index 0000000000000000000000000000000000000000..8e2219d8eba2f84474ef99187c02d084d68a321f --- /dev/null +++ b/src/examples/proteopedia-wrapper/index.html @@ -0,0 +1,131 @@ +<!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* Proteopedia Wrapper</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + #app { + position: absolute; + left: 160px; + top: 100px; + width: 600px; + height: 600px; + border: 1px solid #ccc; + } + + #controls { + position: absolute; + width: 130px; + top: 10px; + left: 10px; + } + + #controls > button { + display: block; + width: 100%; + text-align: left; + } + + #controls > hr { + margin: 5px 0; + } + + #controls > input, #controls > select { + width: 100%; + display: block; + } + </style> + <link rel="stylesheet" type="text/css" href="app.css" /> + <script type="text/javascript" src="./index.js"></script> + </head> + <body> + <div id='controls'> + <h3>Source</h3> + <input type='text' id='url' placeholder='url' /> + <input type='text' id='assemblyId' placeholder='assembly id' /> + <select id='format'> + <option value='cif' selected>CIF</option> + <option value='pdb'>PDB</option> + </select> + </div> + <div id="app"></div> + <script> + // create an instance of the plugin + var MolStarProteopediaWrapper = new MolStarProteopediaWrapper(); + + 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 = 'cif'; + + $('url').value = url; + $('url').onchange = function (e) { url = e.target.value; } + $('assemblyId').value = assemblyId; + $('assemblyId').onchange = function (e) { assemblyId = e.target.value; } + $('format').value = format; + $('format').onchange = function (e) { format = e.target.value; } + + // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent'; + // var format = 'pdb'; + // var assemblyId = 'deposited'; + + MolStarProteopediaWrapper.init('app' /** or document.getElementById('app') */); + MolStarProteopediaWrapper.setBackground(0xffffff); + MolStarProteopediaWrapper.load({ url: url, format: format, assemblyId: assemblyId }); + MolStarProteopediaWrapper.toggleSpin(); + + addControl('Load Asym Unit', () => MolStarProteopediaWrapper.load({ url: url, format: format })); + addControl('Load Assembly', () => MolStarProteopediaWrapper.load({ url: url, format: format, assemblyId: assemblyId })); + + addSeparator(); + + addHeader('Camera'); + addControl('Toggle Spin', () => MolStarProteopediaWrapper.toggleSpin()); + + addSeparator(); + + addHeader('Animation'); + + // adjust this number to make the animation faster or slower + // requires to "restart" the animation if changed + MolStarProteopediaWrapper.animate.modelIndex.maxFPS = 30; + + addControl('Play To End', () => MolStarProteopediaWrapper.animate.modelIndex.onceForward()); + addControl('Play To Start', () => MolStarProteopediaWrapper.animate.modelIndex.onceBackward()); + addControl('Play Palindrome', () => MolStarProteopediaWrapper.animate.modelIndex.palindrome()); + addControl('Play Loop', () => MolStarProteopediaWrapper.animate.modelIndex.loop()); + addControl('Stop', () => MolStarProteopediaWrapper.animate.modelIndex.stop()); + + addHeader('Misc'); + + addControl('Apply Stripes', () => MolStarProteopediaWrapper.coloring.applyStripes()); + + //////////////////////////////////////////////////////// + + function addControl(label, action) { + var btn = document.createElement('button'); + btn.onclick = action; + btn.innerText = label; + $('controls').appendChild(btn); + } + + function addSeparator() { + var hr = document.createElement('hr'); + $('controls').appendChild(hr); + } + + function addHeader(header) { + var h = document.createElement('h3'); + h.innerText = header; + $('controls').appendChild(h); + } + </script> + </body> +</html> \ No newline at end of file diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5370a843a1b1717048a5b5db903b99b071cd75e4 --- /dev/null +++ b/src/examples/proteopedia-wrapper/index.ts @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { createPlugin, DefaultPluginSpec } from 'mol-plugin'; +import './index.html' +import { PluginContext } from 'mol-plugin/context'; +import { PluginCommands } from 'mol-plugin/command'; +import { StateTransforms } from 'mol-plugin/state/transforms'; +import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation'; +import { Color } from 'mol-util/color'; +import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/objects'; +import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in'; +import { StateBuilder } from 'mol-state'; +import { StripedResidues } from './annotation'; +import { LoadParams, SupportedFormats, RepresentationStyle } from './helpers'; +require('mol-plugin/skin/light.scss') + +class MolStarProteopediaWrapper { + static VERSION_MAJOR = 1; + + plugin: PluginContext; + + init(target: string | HTMLElement) { + this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, { + ...DefaultPluginSpec, + initialLayout: { + isExpanded: false, + showControls: false + } + }); + + this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(StripedResidues.Descriptor.name, StripedResidues.colorTheme!); + this.plugin.lociLabels.addProvider(StripedResidues.labelProvider); + this.plugin.customModelProperties.register(StripedResidues.propertyProvider); + } + + get state() { + return this.plugin.state.dataState; + } + + private download(b: StateBuilder.To<PSO.Root>, url: string) { + return b.apply(StateTransforms.Data.Download, { url, isBinary: false }) + } + + private parse(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) { + const parsed = format === 'cif' + ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) + : b.apply(StateTransforms.Model.TrajectoryFromPDB); + + return parsed + .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) + .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', props: { isGhost: false } }) + .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); + } + + private visual(ref: string, style?: RepresentationStyle) { + const state = this.state; + const cell = state.select(ref)[0]; + if (!cell || !cell.obj) return void 0; + const structure = (cell.obj as PluginStateObject.Molecule.Structure).data; + + const root = state.build().to(ref); + + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: 'sequence' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style && style.sequence && style.sequence.kind) || 'cartoon', + (style && style.sequence && style.sequence.coloring) || 'unit-index', structure), + { ref: 'sequence-visual' }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: 'het' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style && style.hetGroups && style.hetGroups.kind) || 'ball-and-stick', + (style && style.hetGroups && style.hetGroups.coloring), structure), + { ref: 'het-visual' }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: 'water' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style && style.water && style.water.kind) || 'ball-and-stick', + (style && style.water && style.water.coloring), structure, { alpha: 0.51 }), + { ref: 'water-visual' }); + + return root; + } + + private loadedParams: LoadParams = { url: '', format: 'cif', assemblyId: '' }; + async load({ url, format = 'cif', assemblyId = '', representationStyle }: LoadParams) { + let loadType: 'full' | 'update' = 'full'; + + const state = this.plugin.state.dataState; + + if (this.loadedParams.url !== url || this.loadedParams.format !== format) { + loadType = 'full'; + } else if (this.loadedParams.url === url) { + if (state.select('asm').length > 0) loadType = 'update'; + } + + let tree: StateBuilder.Root; + if (loadType === 'full') { + await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref }); + tree = state.build(); + this.parse(this.download(tree.toRoot(), url), format, assemblyId); + } else { + tree = state.build(); + tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); + } + + await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); + await this.updateStyle(representationStyle); + + this.loadedParams = { url, format, assemblyId }; + PluginCommands.Camera.Reset.dispatch(this.plugin, { }); + } + + async updateStyle(style?: RepresentationStyle) { + const tree = this.visual('asm', style); + if (!tree) return; + await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); + } + + setBackground(color: number) { + PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } }); + } + + toggleSpin() { + const trackball = this.plugin.canvas3d.props.trackball; + const spinning = trackball.spin; + PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } }); + if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { }); + } + + animate = { + modelIndex: { + maxFPS: 8, + onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) }, + onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) }, + palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) }, + loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) }, + stop: () => this.plugin.state.animation.stop() + } + } + + coloring = { + applyStripes: async () => { + await this.updateStyle({ sequence: { kind: 'molecular-surface' } }); + + const state = this.state; + + const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D)); + const tree = state.build(); + const colorTheme = { name: StripedResidues.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.Descriptor.name).defaultValues }; + + for (const v of visuals) { + tree.to(v.transform.ref).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme })); + } + + await PluginCommands.State.Update.dispatch(this.plugin, { state, tree }); + } + } +} + +(window as any).MolStarProteopediaWrapper = MolStarProteopediaWrapper; \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index 25af4e857307005d0aeae0607c18f0896e067b30..389308f6b2b2f955da1c8d98650afbd9c6d85547 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -37,6 +37,21 @@ export namespace StructureRepresentation3DHelpers { }) } + export function getDefaultParamsWithTheme(ctx: PluginContext, reprName: BuiltInStructureRepresentationsName, colorName: BuiltInColorThemeName | undefined, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> { + const type = ctx.structureRepresentation.registry.get(reprName); + + const themeDataCtx = { structure }; + const color = colorName || type.defaultColorTheme; + const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(color).getParams(themeDataCtx); + const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).getParams(themeDataCtx) + const structureDefaultParams = PD.getDefaultValues(type.getParams(ctx.structureRepresentation.themeCtx, structure)) + return ({ + type: { name: reprName, params: structureParams ? { ...structureDefaultParams, ...structureParams } : structureDefaultParams }, + colorTheme: { name: color, params: PD.getDefaultValues(colorParams) }, + sizeTheme: { name: type.defaultSizeTheme, params: PD.getDefaultValues(sizeParams) } + }) + } + export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> { const type = ctx.structureRepresentation.registry.get(name); const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues; diff --git a/webpack.config.js b/webpack.config.js index e3d711da35516237d7e9a559daa50ef8fa032e1f..3c8b6502aa99479f79962a3eaa96b98737e28a1d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -62,6 +62,17 @@ const sharedConfig = { } } + +function createEntry(src, outFolder, outFilename, isNode) { + return { + node: isNode ? void 0 : { fs: 'empty' }, // TODO find better solution? Currently used in file-handle.ts + target: isNode ? 'node' : void 0, + entry: path.resolve(__dirname, `build/src/${src}.js`), + output: { filename: `${outFilename}.js`, path: path.resolve(__dirname, `build/${outFolder}`) }, + ...sharedConfig + } +} + function createEntryPoint(name, dir, out) { return { node: { fs: 'empty' }, // TODO find better solution? Currently used in file-handle.ts @@ -87,6 +98,7 @@ function createNodeApp(name) { return createNodeEntryPoint('index', `apps/${name module.exports = [ createApp('viewer'), createApp('basic-wrapper'), + createEntry('examples/proteopedia-wrapper/index', 'examples/proteopedia-wrapper', 'index'), createNodeApp('state-docs'), createNodeEntryPoint('preprocess', 'servers/model', 'model-server'), createApp('model-server-query'),