diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index eefc0064172ec38a4f5543f43d55c65cdeec891a..d7e521824c97734cdc7ac07a1bb8c2d4c6ef73b8 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -10,7 +10,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { PluginCommands } from './command'; import { PluginSpec } from './spec'; -import { DownloadAtomicStructure, CreateComplexRepresentation } from './state/actions/basic'; +import { DownloadAtomicStructure, CreateComplexRepresentation, OpenAtomicStructure } from './state/actions/basic'; import { StateTransforms } from './state/transforms'; import { PluginBehaviors } from './behavior'; import { LogEntry } from 'mol-util/log-entry'; @@ -23,6 +23,7 @@ function getParam(name: string, regex: string): string { const DefaultSpec: PluginSpec = { actions: [ PluginSpec.Action(DownloadAtomicStructure), + PluginSpec.Action(OpenAtomicStructure), PluginSpec.Action(CreateComplexRepresentation), PluginSpec.Action(StateTransforms.Data.Download), PluginSpec.Action(StateTransforms.Data.ParseCif), diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index 77f5f4c4cb4e528aebefcea449bfbf80bfe8512a..f10374d3193f8bec997197dbbcd6b12cc8db12ca 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/src/mol-plugin/state/actions/basic.ts @@ -12,10 +12,14 @@ import { StateSelection } from 'mol-state/state/selection'; import { CartoonParams } from 'mol-repr/structure/representation/cartoon'; import { BallAndStickParams } from 'mol-repr/structure/representation/ball-and-stick'; import { Download } from '../transforms/data'; +import { StateTree } from 'mol-state'; +import { StateTreeBuilder } from 'mol-state/tree/builder'; + +// TODO: "structure parser provider" export { DownloadAtomicStructure } namespace DownloadAtomicStructure { - export type Sources = 'pdbe-updated' | 'rcsb' | 'bcif-static' | 'url' | 'file' + export type Sources = 'pdbe-updated' | 'rcsb' | 'bcif-static' | 'url' export type Source = ObtainStructureHelpers.MapParams<Sources, typeof ObtainStructureHelpers.ControlMap> export interface Params { source: Source @@ -26,15 +30,13 @@ namespace ObtainStructureHelpers { ['pdbe-updated', 'PDBe Updated'], ['rcsb', 'RCSB'], ['bcif-static', 'BinaryCIF (static PDBe Updated)'], - ['url', 'URL'], - ['file', 'File'] + ['url', 'URL'] ]; export const ControlMap = { 'pdbe-updated': PD.Text('1cbs', { label: 'Id' }), 'rcsb': PD.Text('1tqn', { label: 'Id' }), 'bcif-static': PD.Text('1tqn', { label: 'Id' }), - 'url': PD.Group({ url: PD.Text(''), isBinary: PD.Boolean(false) }, { isExpanded: true }), - 'file': PD.File({ accept: '.cif,.bcif' }) + 'url': PD.Group({ url: PD.Text(''), isBinary: PD.Boolean(false) }, { isExpanded: true }) } export function getControls(key: string) { return (ControlMap as any)[key]; } @@ -46,7 +48,7 @@ namespace ObtainStructureHelpers { case 'pdbe-updated': return { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params}` }; case 'rcsb': return { url: `https://files.rcsb.org/download/${src.params.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params}` }; case 'bcif-static': return { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params}` }; - default: throw new Error(`${src.name} not supported.`); + default: throw new Error(`${(src as any).name} not supported.`); } } } @@ -73,24 +75,42 @@ const DownloadAtomicStructure = StateAction.create<PluginStateObject.Root, void, const url = ObtainStructureHelpers.getUrl(params.source); - const root = b.toRoot() - .apply(StateTransforms.Data.Download, url) - .apply(StateTransforms.Data.ParseCif) - .apply(StateTransforms.Model.TrajectoryFromMmCif, {}) - .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) - .apply(StateTransforms.Model.StructureAssemblyFromModel); - - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'sequence' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'cartoon', params: PD.getDefaultValues(CartoonParams) } }); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'ligands' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: PD.getDefaultValues(BallAndStickParams) } }); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: { ...PD.getDefaultValues(BallAndStickParams), alpha: 0.51 } } }); + const data = b.toRoot().apply(StateTransforms.Data.Download, url); + return state.update(atomicStructureTree(data)); + } +}); - return state.update(root.getTree()); +export const OpenAtomicStructure = StateAction.create<PluginStateObject.Root, void, { file: File }>({ + from: [PluginStateObject.Root], + display: { + name: 'Open Structure', + description: 'Load a structure from file and create its default Assembly and visual' + }, + params: () => ({ file: PD.File({ accept: '.cif,.bcif' }) }), + apply({ params, state }) { + const b = state.build(); + const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); + return state.update(atomicStructureTree(data)); } }); +function atomicStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>): StateTree { + const root = b + .apply(StateTransforms.Data.ParseCif) + .apply(StateTransforms.Model.TrajectoryFromMmCif, {}) + .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) + .apply(StateTransforms.Model.StructureAssemblyFromModel); + + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'sequence' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'cartoon', params: PD.getDefaultValues(CartoonParams) } }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'ligands' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: PD.getDefaultValues(BallAndStickParams) } }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: { ...PD.getDefaultValues(BallAndStickParams), alpha: 0.51 } } }); + + return root.getTree(); +} + export const CreateComplexRepresentation = StateAction.create<PluginStateObject.Molecule.Structure, void, {}>({ from: [PluginStateObject.Molecule.Structure], display: { diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index d663997c5f6be5ebaac944e2f23bfc051059b150..cb7169b557f979fe3aa53493077f454ec2611104 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -11,6 +11,7 @@ import CIF from 'mol-io/reader/cif' import { PluginContext } from 'mol-plugin/context'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Transformer } from 'mol-state'; +import { readFromFile } from 'mol-util/data-source'; export { Download } namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } } @@ -45,6 +46,39 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B } }); +export { ReadFile } +namespace ReadFile { export interface Params { file: File, isBinary?: boolean, label?: string } } +const ReadFile = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, ReadFile.Params>({ + name: 'read-file', + display: { + name: 'Read File', + description: 'Read string or binary data from the specified file' + }, + from: [SO.Root], + to: [SO.Data.String, SO.Data.Binary], + params: () => ({ + file: PD.File(), + label: PD.Text('', { isOptional: true }), + isBinary: PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)', isOptional: true }) + }), + apply({ params: p }, globalCtx: PluginContext) { + return Task.create('Open File', async ctx => { + const data = await readFromFile(p.file, p.isBinary ? 'binary' : 'string').runInContext(ctx); + return p.isBinary + ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.file.name }) + : new SO.Data.String(data as string, { label: p.label ? p.label : p.file.name }); + }); + }, + update({ oldParams, newParams, b }) { + if (oldParams.label !== newParams.label) { + (b.label as string) = newParams.label || oldParams.file.name; + return Transformer.UpdateResult.Updated; + } + return Transformer.UpdateResult.Unchanged; + }, + isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' }) +}); + export { ParseCif } namespace ParseCif { export interface Params { } } const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, ParseCif.Params>({