Browse Source

mol-plugin-state: TrajectoryFormatProvider

David Sehnal 5 years ago
parent
commit
943f5feb59

+ 6 - 5
src/mol-plugin-state/actions/structure.ts

@@ -20,6 +20,7 @@ import { Download, ParsePsf } from '../transforms/data';
 import { CoordinatesFromDcd, CustomModelProperties, CustomStructureProperties, TopologyFromPsf, TrajectoryFromModelAndCoordinates } from '../transforms/model';
 import { DataFormatProvider, guessCifVariant } from './data-format';
 import { TrajectoryFormat } from '../builder/structure';
+import { BuildInTrajectoryFormat } from '../formats/trajectory';
 
 export const MmcifProvider: DataFormatProvider<PluginStateObject.Data.String | PluginStateObject.Data.Binary> = {
     label: 'mmCIF',
@@ -34,7 +35,7 @@ export const MmcifProvider: DataFormatProvider<PluginStateObject.Data.String | P
     },
     getDefaultBuilder: (ctx: PluginContext, data, options) => {
         return Task.create('mmCIF default builder', async taskCtx => {
-            const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'cif' });
+            const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'mmcif' });
             if (options.visuals) {
                 await ctx.builders.representation.structurePreset(structure, 'auto');
             }
@@ -52,7 +53,7 @@ export const PdbProvider: DataFormatProvider<any> = {
     },
     getDefaultBuilder: (ctx: PluginContext, data, options) => {
         return Task.create('PDB default builder', async () => {
-            const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'cif' });
+            const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'pdb' });
             if (options.visuals) {
                 await ctx.builders.representation.structurePreset(structure, 'auto');
             }
@@ -174,7 +175,7 @@ const DownloadStructure = StateAction.build({
             }, { isFlat: true, label: 'SWISS-MODEL', description: 'Loads the best homology model or experimental structure' }),
             'url': PD.Group({
                 url: PD.Text(''),
-                format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']] as ['cif' | 'pdb', string][]),
+                format: PD.Select('mmcif', [['mmcif', 'CIF'], ['pdb', 'PDB']] as ['mmcif' | 'pdb', string][]),
                 isBinary: PD.Boolean(false),
                 structure: DownloadModelRepresentationOptions,
                 options: PD.Group({
@@ -185,11 +186,11 @@ const DownloadStructure = StateAction.build({
         })
     }
 })(({ params, state }, plugin: PluginContext) => Task.create('Download Structure', async ctx => {
-    // plugin.behaviors.layout.leftPanelTabName.next('data');
+    plugin.behaviors.layout.leftPanelTabName.next('data');
 
     const src = params.source;
     let downloadParams: StateTransformer.Params<Download>[];
-    let supportProps = false, asTrajectory = false, format: TrajectoryFormat = 'cif';
+    let supportProps = false, asTrajectory = false, format: BuildInTrajectoryFormat = 'mmcif';
 
     switch (src.name) {
         case 'url':

+ 9 - 50
src/mol-plugin-state/builder/structure.ts

@@ -5,11 +5,12 @@
  */
 
 import { PluginContext } from '../../mol-plugin/context';
-import { StateBuilder, StateObjectRef, StateObjectSelector, StateTransformer } from '../../mol-state';
+import { StateObjectRef, StateObjectSelector, StateTransformer } from '../../mol-state';
 import { PluginStateObject as SO } from '../objects';
 import { StateTransforms } from '../transforms';
 import { RootStructureDefinition } from '../helpers/root-structure';
 import { StructureComponentParams } from '../helpers/structure-component';
+import { BuildInTrajectoryFormat, TrajectoryFormatProvider } from '../formats/trajectory';
 
 export type TrajectoryFormat = 'pdb' | 'cif' | 'gro' | '3dg'
 
@@ -27,32 +28,11 @@ export class StructureBuilder {
         return this.plugin.state.dataState;
     }
 
-    private async parseTrajectoryData(data: StateObjectRef<SO.Data.Binary | SO.Data.String>, format: TrajectoryFormat) {
-        const state = this.dataState;
-        const root = state.build().to(data);
-        let parsed: StateBuilder.To<SO.Molecule.Trajectory>;
-        const tag = { tags: StructureBuilderTags.Trajectory };
-        switch (format) {
-            case 'cif':
-                parsed = root.apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
-                    .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, tag)
-                break
-            case 'pdb':
-                parsed = root.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, tag);
-                break
-            case 'gro':
-                parsed = root.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, tag);
-                break
-            case '3dg':
-                parsed = root.apply(StateTransforms.Model.TrajectoryFrom3DG, void 0, tag);
-                break
-            default:
-                throw new Error('unsupported format')
-        }
-
-        await this.plugin.runTask(this.dataState.updateTree(parsed, { revertOnError: true }));
-
-        return parsed.selector;
+    private async parseTrajectoryData(data: StateObjectRef<SO.Data.Binary | SO.Data.String>, format: BuildInTrajectoryFormat | TrajectoryFormatProvider) {
+        const provider = typeof format === 'string' ? this.plugin.dataFormat.trajectory.get(format) : format;
+        if (!provider) throw new Error(`'${format}' is not a supported data format.`);
+        const { trajectory } = await provider.parse(this.plugin, data, { trajectoryTags: StructureBuilderTags.Trajectory });
+        return trajectory;
     }
 
     private async parseTrajectoryBlob(data: StateObjectRef<SO.Data.Blob>, params: StateTransformer.Params<StateTransforms['Data']['ParseBlob']>) {
@@ -66,7 +46,7 @@ export class StructureBuilder {
 
     async parseStructure(params: {
         data?: StateObjectRef<SO.Data.Binary | SO.Data.String>,
-        dataFormat?: TrajectoryFormat,
+        dataFormat?: BuildInTrajectoryFormat | TrajectoryFormatProvider,
         blob?: StateObjectRef<SO.Data.Blob>
         blobParams?: StateTransformer.Params<StateTransforms['Data']['ParseBlob']>,
         model?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>,
@@ -97,11 +77,9 @@ export class StructureBuilder {
         };
     }
 
-    async parseTrajectory(data: StateObjectRef<SO.Data.Binary | SO.Data.String>, format: TrajectoryFormat): Promise<StateObjectSelector<SO.Molecule.Trajectory>>
+    async parseTrajectory(data: StateObjectRef<SO.Data.Binary | SO.Data.String>, format: BuildInTrajectoryFormat | TrajectoryFormatProvider): Promise<StateObjectSelector<SO.Molecule.Trajectory>>
     async parseTrajectory(blob: StateObjectRef<SO.Data.Blob>, params: StateTransformer.Params<StateTransforms['Data']['ParseBlob']>): Promise<StateObjectSelector<SO.Molecule.Trajectory>>
     async parseTrajectory(data: StateObjectRef, params: any) {
-        // TODO: proper format support
-        // needs to integrated to transforms directly because of blobs
         const cell = StateObjectRef.resolveAndCheck(this.dataState, data as StateObjectRef);
         if (!cell) throw new Error('Invalid data cell.');
 
@@ -114,21 +92,11 @@ export class StructureBuilder {
 
     async createModel(trajectory: StateObjectRef<SO.Molecule.Trajectory>, params?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>) {
         const state = this.dataState;
-
         const model = state.build().to(trajectory)
             .apply(StateTransforms.Model.ModelFromTrajectory, params || { modelIndex: 0 }, { tags: StructureBuilderTags.Model });
 
-        // const props = !!params?.properties 
-        //     ? model.apply(StateTransforms.Model.CustomModelProperties, typeof params?.properties !== 'boolean' ? params?.properties : void 0, { tags: StructureBuilderTags.ModelProperties, isDecorator: true })
-        //     : void 0;
-
         await this.plugin.runTask(this.dataState.updateTree(model, { revertOnError: true }));
-
         return model.selector;
-
-        // const modelSelector = model.selector, propertiesSelector = props?.selector;
-
-        // return { model: propertiesSelector || modelSelector, index: modelSelector, properties: propertiesSelector };
     }
 
     async insertModelProperties(model: StateObjectRef<SO.Molecule.Model>, params?: StateTransformer.Params<StateTransforms['Model']['CustomModelProperties']>) {
@@ -144,17 +112,8 @@ export class StructureBuilder {
         const structure = state.build().to(model)
             .apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure });        
 
-        // const props = !!params?.properties 
-        //     ? structure.apply(StateTransforms.Model.CustomStructureProperties, typeof params?.properties !== 'boolean' ? params?.properties : void 0, { tags: StructureBuilderTags.StructureProperties, isDecorator: true })
-        //     : void 0;
-
         await this.plugin.runTask(this.dataState.updateTree(structure, { revertOnError: true }));
-
         return structure.selector;
-
-        // const structureSelector = structure.selector, propertiesSelector = props?.selector;
-
-        // return { structure: propertiesSelector || structureSelector, definition: structureSelector, properties: propertiesSelector };
     }
 
     async insertStructureProperties(structure: StateObjectRef<SO.Molecule.Structure>, params?: StateTransformer.Params<StateTransforms['Model']['CustomStructureProperties']>) {

+ 116 - 0
src/mol-plugin-state/formats/registry.ts

@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import msgpackDecode from '../../mol-io/common/msgpack/decode';
+import { PluginContext } from '../../mol-plugin/context';
+import { StateObjectRef } from '../../mol-state';
+import { FileInfo } from '../../mol-util/file-info';
+import { PluginStateObject } from '../objects';
+
+export class DataFormatRegistry<Provider extends DataFormatProvider> {
+    private _list: { name: string, provider: Provider }[] = []
+    private _map = new Map<string, Provider>()
+    private _extensions: Set<string> | undefined = undefined
+    private _binaryExtensions: Set<string> | undefined = undefined
+    private _options: [string, string][] | undefined = undefined
+
+    get types(): [string, string][] {
+        return this._list.map(e => [e.name, e.provider.label] as [string, string]);
+    }
+
+    get extensions() {
+        if (this._extensions) return this._extensions
+        const extensions = new Set<string>()
+        this._list.forEach(({ provider }) => {
+            provider.stringExtensions.forEach(ext => extensions.add(ext))
+            provider.binaryExtensions.forEach(ext => extensions.add(ext))
+        })
+        this._extensions = extensions
+        return extensions
+    }
+
+    get binaryExtensions() {
+        if (this._binaryExtensions) return this._binaryExtensions
+        const binaryExtensions = new Set<string>()
+        this._list.forEach(({ provider }) => provider.binaryExtensions.forEach(ext => binaryExtensions.add(ext)))
+        this._binaryExtensions = binaryExtensions
+        return binaryExtensions
+    }
+
+    get options() {
+        if (this._options) return this._options
+        const options: [string, string][] = [['auto', 'Automatic']]
+        this._list.forEach(({ name, provider }) => options.push([ name, provider.label ]))
+        this._options = options
+        return options
+    }
+
+    constructor(buildInFormats: ReadonlyArray<readonly [string, Provider]>) {
+        for (const [id, p] of buildInFormats) this.add(id, p);
+    };
+
+    private _clear() {
+        this._extensions = undefined
+        this._binaryExtensions = undefined
+        this._options = undefined
+    }
+
+    add(name: string, provider: Provider) {
+        this._clear()
+        this._list.push({ name, provider })
+        this._map.set(name, provider)
+    }
+
+    remove(name: string) {
+        this._clear()
+        this._list.splice(this._list.findIndex(e => e.name === name), 1)
+        this._map.delete(name)
+    }
+
+    auto(info: FileInfo, dataStateObject: PluginStateObject.Data.Binary | PluginStateObject.Data.String) {
+        for (let i = 0, il = this.list.length; i < il; ++i) {
+            const { provider } = this._list[i];
+            if (provider.isApplicable(info, dataStateObject.data)) return provider;
+        }
+        return;
+    }
+
+    get(name: string): Provider | undefined {
+        if (this._map.has(name)) {
+            return this._map.get(name)!
+        } else {
+            throw new Error(`unknown data format name '${name}'`)
+        }
+    }
+
+    get list() {
+        return this._list
+    }
+}
+
+export interface DataFormatProvider<P = any, R = any> {
+    label: string
+    description: string
+    stringExtensions: string[]
+    binaryExtensions: string[]
+    isApplicable(info: FileInfo, data: string | Uint8Array): boolean
+    parse(plugin: PluginContext, data: StateObjectRef<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, params: P | undefined): Promise<R>
+}
+
+type cifVariants = 'dscif' | -1
+export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
+    if (info.ext === 'bcif') {
+        try {
+            // TODO: find a way to run msgpackDecode only once
+            //      now it is run twice, here and during file parsing
+            if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif'
+        } catch { }
+    } else if (info.ext === 'cif') {
+        if ((data as string).startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
+    }
+    return -1
+}

+ 93 - 0
src/mol-plugin-state/formats/trajectory.ts

@@ -0,0 +1,93 @@
+/**
+ * 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 { FileInfo } from '../../mol-util/file-info';
+import { StateTransforms } from '../transforms';
+import { guessCifVariant, DataFormatProvider, DataFormatRegistry } from './registry';
+import { StateTransformer, StateObjectRef } from '../../mol-state';
+import { PluginStateObject } from '../objects';
+
+export interface TrajectoryFormatProvider<P extends { trajectoryTags?: string | string[] } = { trajectoryTags?: string | string[] }, R extends { trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory> } = { trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory> }> 
+    extends DataFormatProvider<P, R> {
+}
+
+export function TrajectoryFormatRegistry() {
+    return new DataFormatRegistry<TrajectoryFormatProvider>(BuildInTrajectoryFormats);
+}
+
+export const MmcifProvider: TrajectoryFormatProvider = {
+    label: 'mmCIF',
+    description: 'mmCIF',
+    stringExtensions: ['cif', 'mmcif', 'mcif'],
+    binaryExtensions: ['bcif'],
+    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
+        if (info.ext === 'mmcif' || info.ext === 'mcif') return true
+        // assume cif/bcif files that are not DensityServer CIF are mmCIF
+        if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) !== 'dscif'
+        return false
+    },
+    parse: async (plugin, data, params) => {
+        const state = plugin.state.dataState;
+        const trajectory = state.build().to(data)
+            .apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
+            .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { tags: params?.trajectoryTags })
+        await plugin.runTask(state.updateTree(trajectory, { revertOnError: true }));
+        return { trajectory: trajectory.selector };
+    }
+}
+
+function directTrajectory(transformer: StateTransformer<PluginStateObject.Data.String | PluginStateObject.Data.Binary, PluginStateObject.Molecule.Trajectory>): TrajectoryFormatProvider['parse'] {
+    return async (plugin, data, params) => {
+        const state = plugin.state.dataState;
+        const trajectory = state.build().to(data)
+            .apply(transformer, void 0, { tags: params?.trajectoryTags })
+        await plugin.runTask(state.updateTree(trajectory, { revertOnError: true }));
+        return { trajectory: trajectory.selector };
+    }
+}
+
+export const PdbProvider: TrajectoryFormatProvider = {
+    label: 'PDB',
+    description: 'PDB',
+    stringExtensions: ['pdb', 'ent'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === 'pdb' || info.ext === 'ent'
+    },
+    parse: directTrajectory(StateTransforms.Model.TrajectoryFromPDB)
+}
+
+export const GroProvider: TrajectoryFormatProvider = {
+    label: 'GRO',
+    description: 'GRO',
+    stringExtensions: ['gro'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === 'gro'
+    },
+    parse: directTrajectory(StateTransforms.Model.TrajectoryFromGRO)
+}
+
+export const Provider3dg: TrajectoryFormatProvider = {
+    label: '3DG',
+    description: '3DG',
+    stringExtensions: ['3dg'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === '3dg'
+    },
+    parse: directTrajectory(StateTransforms.Model.TrajectoryFrom3DG)
+}
+
+export const BuildInTrajectoryFormats = [
+    ['mmcif', MmcifProvider] as const,
+    ['pdb', PdbProvider] as const,
+    ['gro', GroProvider] as const,
+    ['3dg', Provider3dg] as const,
+] as const
+
+export type BuildInTrajectoryFormat = (typeof BuildInTrajectoryFormats)[number][0]

+ 2 - 0
src/mol-plugin/context.ts

@@ -48,6 +48,7 @@ import { DataBuilder } from '../mol-plugin-state/builder/data';
 import { StructureBuilder } from '../mol-plugin-state/builder/structure';
 import { StructureHierarchyManager } from '../mol-plugin-state/manager/structure/hierarchy';
 import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
+import { TrajectoryFormatRegistry } from '../mol-plugin-state/formats/trajectory';
 
 export class PluginContext {
     private disposed = false;
@@ -117,6 +118,7 @@ export class PluginContext {
     } as const
 
     readonly dataFormat = {
+        trajectory: TrajectoryFormatRegistry(),
         registry: new DataFormatRegistry()
     } as const