Browse Source

add structure export control to molstar UI

Yana Rose 4 years ago
parent
commit
b01bd3b9fd

+ 68 - 0
src/viewer/helpers/export.ts

@@ -0,0 +1,68 @@
+import { PluginContext } from 'molstar/lib/mol-plugin/context';
+import { StateSelection } from 'molstar/lib/mol-state';
+import { PluginStateObject } from 'molstar/lib/mol-plugin-state/objects';
+import { StructureSelection, Structure } from 'molstar/lib/mol-model/structure';
+import { CifExportContext, encode_mmCIF_categories } from 'molstar/lib/mol-model/structure/export/mmcif';
+import { utf8ByteCount, utf8Write } from 'molstar/lib/mol-io/common/utf8';
+import { zip } from 'molstar/lib/mol-util/zip/zip';
+import { getFormattedTime } from 'molstar/lib/mol-util/date';
+import { download } from 'molstar/lib/mol-util/download';
+import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property';
+import { CifWriter } from 'molstar/lib/mol-io/writer/cif';
+
+type encode_mmCIF_categories_Params = {
+    skipCategoryNames?: Set<string>,
+    exportCtx?: CifExportContext,
+    copyAllCategories?: boolean,
+    customProperties?: CustomPropertyDescriptor[]
+}
+
+function export_Params(): encode_mmCIF_categories_Params {
+    const skipCategories: Set<string> = new Set();
+    skipCategories.add('pdbx_struct_assembly').add('pdbx_struct_assembly_gen').add('pdbx_struct_oper_list');
+    const params: encode_mmCIF_categories_Params = {
+        skipCategoryNames: skipCategories
+    };
+    return params;
+}
+
+function to_mmCIF(name: string, structure: Structure, asBinary = false) {
+    const enc = CifWriter.createEncoder({ binary: asBinary });
+    enc.startDataBlock(name);
+    encode_mmCIF_categories(enc, structure, export_Params());
+    return enc.getData();
+}
+
+function extract_structure_data_from_state(plugin: PluginContext): { [k: string]: Structure } {
+    const content: { [k: string]: Structure } = {};
+    const cells = plugin.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure));
+    for (const c of cells) {
+        const children = plugin.state.data.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure, c.transform.ref))
+            .filter(child => (child !== c && !child.transform.transformer.definition.isDecorator))
+            .map(child => child.obj!.data);
+        const sele = StructureSelection.Sequence(c.obj!.data, children);
+        const structure = StructureSelection.unionStructure(sele);
+        const name = structure.model.entryId;
+        content[name] = structure;
+    }
+    return content;
+}
+
+export function encodeStructureData(plugin: PluginContext): { [k: string]: Uint8Array } {
+    const content: { [k: string]: Uint8Array } = {};
+    const structures = extract_structure_data_from_state(plugin);
+    for (const [key, structure] of Object.entries(structures)) {
+        const filename = `${key}.cif`;
+        const str = to_mmCIF(filename, structure, false) as string;
+        const data = new Uint8Array(utf8ByteCount(str));
+        utf8Write(data, 0, str);
+        content[filename] = data;
+    }
+    return content;
+}
+
+export function downloadAsZipFile(content: { [k: string]: Uint8Array }) {
+    const filename = `mol-star_download_${getFormattedTime()}.zip`;
+    const buf = zip(content)
+    download(new Blob([buf]), filename);
+}

+ 3 - 0
src/viewer/index.html

@@ -57,6 +57,7 @@
             // create an instance of the plugin
             var viewer = new rcsbMolstar.Viewer('viewer', {
                 showImportControls: !pdbId,
+                showExportControls: !pdbId,
                 showSessionControls: !pdbId,
                 layoutShowLog: !pdbId,
                 layoutShowControls: !isEmbedded,
@@ -83,8 +84,10 @@
 
             Superposed
             <button style="padding: 3px;" onclick="superposed()">3PQR | 1U19</button>
+
         </div>
         <script>
+
             function loadExample(index) {
                 var e = examples[index]
                 viewer.loadPdbId(e.id, e.props)

+ 8 - 0
src/viewer/index.ts

@@ -29,6 +29,7 @@ import { BuiltInTrajectoryFormat } from 'molstar/lib/mol-plugin-state/formats/tr
 import { ObjectKeys } from 'molstar/lib/mol-util/type-helpers';
 import { PluginLayoutControlsDisplay } from 'molstar/lib/mol-plugin/layout';
 import { SuperposeColorThemeProvider } from './helpers/superpose/color';
+import { encodeStructureData, downloadAsZipFile } from './helpers/export';
 require('./skin/rcsb.scss')
 
 /** package version, filled in at bundle build time */
@@ -47,6 +48,7 @@ const Extensions = {
 
 const DefaultViewerProps = {
     showImportControls: false,
+    showExportControls: false,
     showSessionControls: false,
     modelUrlProviders: [
         (pdbId: string) => ({
@@ -140,6 +142,7 @@ export class Viewer {
 
         (this.plugin.customState as ViewerState) = {
             showImportControls: o.showImportControls,
+            showExportControls: o.showExportControls,
             showSessionControls: o.showSessionControls,
             modelLoader: new ModelLoader(this.plugin),
             collapsed: new BehaviorSubject<CollapsedState>({
@@ -216,4 +219,9 @@ export class Viewer {
     handleResize() {
         this.plugin.layout.events.updated.next();
     }
+
+    async exportLoadedStructures() {
+        const content = encodeStructureData(this.plugin);
+        downloadAsZipFile(content);
+    }
 }

+ 1 - 0
src/viewer/types.ts

@@ -50,6 +50,7 @@ export type VisibilityState = {
 }
 export interface ViewerState {
     showImportControls: boolean
+    showExportControls: boolean
     showSessionControls: boolean
     modelLoader: ModelLoader
     collapsed: BehaviorSubject<CollapsedState>

+ 2 - 0
src/viewer/ui/controls.tsx

@@ -9,6 +9,7 @@ import { PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
 import { ViewerState } from '../types';
 import { CustomStructureControls } from 'molstar/lib/mol-plugin-ui/controls';
 import { ImportControls } from './import';
+import { ExportControls } from './export';
 import { StructureSourceControls } from 'molstar/lib/mol-plugin-ui/structure/source';
 import { StructureMeasurementsControls } from 'molstar/lib/mol-plugin-ui/structure/measurements';
 import { StructureSuperpositionControls } from 'molstar/lib/mol-plugin-ui/structure/superposition';
@@ -45,6 +46,7 @@ export class ControlsWrapper extends PluginUIComponent {
     render() {
         return <div className='msp-scrollable-container'>
             {ViewerState(this.plugin).showImportControls && <ImportControls />}
+            {ViewerState(this.plugin).showExportControls && <ExportControls />}
             {ViewerState(this.plugin).showSessionControls && <SessionControls />}
             <StructureTools />
         </div>;

+ 43 - 0
src/viewer/ui/export.tsx

@@ -0,0 +1,43 @@
+import React = require('react');
+import { CollapsableControls, CollapsableState, PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
+import { Button } from 'molstar/lib/mol-plugin-ui/controls/common';
+import { GetAppSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
+import { encodeStructureData, downloadAsZipFile } from '../helpers/export';
+
+export class ExportControls extends CollapsableControls {
+
+    protected defaultState(): CollapsableState {
+        return {
+            header: 'Export',
+            isCollapsed: true,
+            brand: { accent:  'gray' as const, svg: ExportOutlinedSvg }
+        }
+    }
+    protected renderControls(): JSX.Element | null {
+        return <div className={'msp-control-offset'} style={{ paddingTop: '1px' }}>
+            <CoordinatesExportControls />
+        </div>
+    }
+}
+
+class CoordinatesExportControls extends PluginUIComponent<{ onAction?: () => void }> {
+
+    download = () => {
+        this.props.onAction?.();
+        const content = encodeStructureData(this.plugin);
+        downloadAsZipFile(content);
+    }
+
+    render() {
+        return <>
+            <div className='msp-flex-row'>
+                <Button icon={GetAppSvg} onClick={this.download} title='Save structures as mmCIF files'>
+                    Structures
+                </Button>
+            </div>
+        </>;
+    }
+}
+
+function ExportOutlinedSvg() { return _ExportOutlined; }
+const _ExportOutlined = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M19 12v7H5v-7H3v9h18v-9h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2v9.67z" /></svg>;