Ver Fonte

add Zenodo import extension

Alexander Rose há 3 anos atrás
pai
commit
a1448131d8
4 ficheiros alterados com 302 adições e 0 exclusões
  1. 1 0
      CHANGELOG.md
  2. 2 0
      src/apps/viewer/app.ts
  3. 30 0
      src/extensions/zenodo/index.ts
  4. 269 0
      src/extensions/zenodo/ui.tsx

+ 1 - 0
CHANGELOG.md

@@ -8,6 +8,7 @@ Note that since we don't clearly distinguish between a public and private interf
 
 - Fix handling of mmcif with empty ``label_*`` fields
 - Add LoadTrajectory action
+- Add Zenodo import extension (load structures, trajectories, and volumes)
 
 ## [v3.3.1] - 2022-02-27
 

+ 2 - 0
src/apps/viewer/app.ts

@@ -17,6 +17,7 @@ import { ModelExport } from '../../extensions/model-export';
 import { Mp4Export } from '../../extensions/mp4-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
+import { ZenodoImport } from '../../extensions/zenodo';
 import { Volume } from '../../mol-model/volume';
 import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
 import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
@@ -63,6 +64,7 @@ const Extensions = {
     'mp4-export': PluginSpec.Behavior(Mp4Export),
     'geo-export': PluginSpec.Behavior(GeometryExport),
     'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
+    'zenodo-import': PluginSpec.Behavior(ZenodoImport),
 };
 
 const DefaultViewerOptions = {

+ 30 - 0
src/extensions/zenodo/index.ts

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
+import { ZenodoImportUI } from './ui';
+
+export const ZenodoImport = PluginBehavior.create<{ }>({
+    name: 'extension-zenodo-import',
+    category: 'misc',
+    display: {
+        name: 'Zenodo Export'
+    },
+    ctor: class extends PluginBehavior.Handler<{ }> {
+        register(): void {
+            this.ctx.customStructureControls.set('zenodo-import', ZenodoImportUI as any);
+        }
+
+        update() {
+            return false;
+        }
+
+        unregister() {
+            this.ctx.customStructureControls.delete('zenodo-import');
+        }
+    },
+    params: () => ({ })
+});

+ 269 - 0
src/extensions/zenodo/ui.tsx

@@ -0,0 +1,269 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { DownloadStructure, LoadTrajectory } from '../../mol-plugin-state/actions/structure';
+import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
+import { TrajectoryFormatCategory } from '../../mol-plugin-state/formats/trajectory';
+import { VolumeFormatCategory } from '../../mol-plugin-state/formats/volume';
+import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
+import { Button } from '../../mol-plugin-ui/controls/common';
+import { OpenInBrowserSvg } from '../../mol-plugin-ui/controls/icons';
+import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
+import { PluginContext } from '../../mol-plugin/context';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+
+type ZenodoFile = {
+    bucket: string
+    checksum: string
+    key: string
+    links: {
+        [key: string]: string
+        self: string
+    }
+    size: number
+    type: string
+}
+
+type ZenodoRecord = {
+    id: number
+    conceptdoi: string
+    conceptrecid: string
+    created: string
+    doi: string
+    files: ZenodoFile[]
+    revision: number
+    updated: string
+    metadata: {
+        title: string
+    }
+}
+
+interface State {
+    busy?: boolean
+    recordValues: PD.Values<typeof ZenodoImportParams>
+    importValues?: PD.Values<ImportParams>
+    importParams?: ImportParams
+    record?: ZenodoRecord
+    files?: ZenodoFile[]
+}
+
+const ZenodoImportParams = {
+    record: PD.Text('847637', { description: 'Zenodo ID.' })
+};
+
+function createImportParams(files: ZenodoFile[], plugin: PluginContext) {
+    const modelOpts: [string, string][] = [];
+    const topologyOpts: [string, string][] = [];
+    const coordinatesOpts: [string, string][] = [];
+    const volumeOpts: [string, string][] = [];
+
+    const structureExts = new Map<string, { format: string, isBinary: boolean }>();
+    const volumeExts = new Map<string, { format: string, isBinary: boolean }>();
+    for (const { provider: { category, binaryExtensions, stringExtensions }, name } of plugin.dataFormats.list) {
+        if (category === TrajectoryFormatCategory) {
+            if (binaryExtensions) for (const e of binaryExtensions) structureExts.set(e, { format: name, isBinary: true });
+            if (stringExtensions) for (const e of stringExtensions) structureExts.set(e, { format: name, isBinary: false });
+        } else if (category === VolumeFormatCategory) {
+            if (binaryExtensions) for (const e of binaryExtensions) volumeExts.set(e, { format: name, isBinary: true });
+            if (stringExtensions) for (const e of stringExtensions) volumeExts.set(e, { format: name, isBinary: false });
+        }
+    }
+
+    for (const file of files) {
+        if (structureExts.has(file.type)) {
+            const { format, isBinary } = structureExts.get(file.type)!;
+            modelOpts.push([`${file.links.self}|${format}|${isBinary}`, file.key]);
+            topologyOpts.push([`${file.links.self}|${format}|${isBinary}`, file.key]);
+        } else if (volumeExts.has(file.type)) {
+            const { format, isBinary } = volumeExts.get(file.type)!;
+            volumeOpts.push([`${file.links.self}|${format}|${isBinary}`, file.key]);
+        } else if (file.type === 'psf') {
+            topologyOpts.push([`${file.links.self}|${file.type}|false`, file.key]);
+        } else if (file.type === 'xtc' || file.type === 'dcd') {
+            coordinatesOpts.push([`${file.links.self}|${file.type}|true`, file.key]);
+        }
+    }
+
+    const params: PD.Params = {};
+    let defaultType = '';
+
+    if (modelOpts.length) {
+        defaultType = 'structure';
+        params.structure = PD.Select(modelOpts[0][0], modelOpts);
+    }
+
+    if (modelOpts.length && topologyOpts.length) {
+        if (!defaultType) defaultType = 'trajectory';
+        params.trajectory = PD.Group({
+            topology: PD.Select(topologyOpts[0][0], topologyOpts),
+            coordinates: PD.Select(coordinatesOpts[0][0], coordinatesOpts),
+        }, { isFlat: true });
+    }
+
+    if (volumeOpts.length) {
+        if (!defaultType) defaultType = 'volume';
+        params.volume = PD.Select(volumeOpts[0][0], volumeOpts);
+    }
+
+    return {
+        type: PD.MappedStatic(defaultType, Object.keys(params).length ? params : { '': PD.EmptyGroup() })
+    };
+}
+type ImportParams = ReturnType<typeof createImportParams>
+
+export class ZenodoImportUI extends CollapsableControls<{}, State> {
+    protected defaultState(): State & CollapsableState {
+        return {
+            header: 'Zenodo Import',
+            isCollapsed: true,
+            brand: { accent: 'cyan', svg: OpenInBrowserSvg },
+            recordValues: PD.getDefaultValues(ZenodoImportParams),
+            importValues: undefined,
+            importParams: undefined,
+            record: undefined,
+            files: undefined,
+        };
+    }
+
+    private recordParamsOnChange = (values: any) => {
+        this.setState({ recordValues: values });
+    };
+
+    private importParamsOnChange = (values: any) => {
+        this.setState({ importValues: values });
+    };
+
+    private loadRecord = async () => {
+        try {
+            this.setState({ busy: true });
+            const record: ZenodoRecord = await this.plugin.runTask(this.plugin.fetch({ url: `https://zenodo.org/api/records/${this.state.recordValues.record}`, type: 'json' }));
+            const importParams = createImportParams(record.files, this.plugin);
+            this.setState({
+                record,
+                files: record.files,
+                busy: false,
+                importValues: PD.getDefaultValues(importParams),
+                importParams
+            });
+        } catch (e) {
+            console.error(e);
+            this.plugin.log.error(`Failed to load Zenodo record '${this.state.recordValues.record}'`);
+            this.setState({ busy: false });
+        }
+    };
+
+    private loadFile = async (values: PD.Values<ImportParams>) => {
+        try {
+            this.setState({ busy: true });
+
+            const t = values.type;
+            if (t.name === 'structure') {
+                const defaultParams = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
+
+                const [url, format, isBinary] = t.params.split('|');
+
+                await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
+                    source: {
+                        name: 'url',
+                        params: {
+                            url,
+                            format: format as any,
+                            isBinary: isBinary === 'true',
+                            options: defaultParams.source.params.options,
+                        }
+                    }
+                }));
+            } else if (t.name === 'trajectory') {
+                const [topologyUrl, topologyFormat, topologyIsBinary] = t.params.topology.split('|');
+                const [coordinatesUrl, coordinatesFormat, coordinatesIsBinary] = t.params.coordinates.split('|');
+
+                await this.plugin.runTask(this.plugin.state.data.applyAction(LoadTrajectory, {
+                    source: {
+                        name: 'url',
+                        params: {
+                            model: {
+                                url: topologyUrl,
+                                format: topologyFormat as any,
+                                isBinary: topologyIsBinary === 'true',
+                            },
+                            coordinates: {
+                                url: coordinatesUrl,
+                                format: coordinatesFormat as any,
+                                isBinary: coordinatesIsBinary === 'true',
+                            },
+                        }
+                    }
+                }));
+            } else if (t.name === 'volume') {
+                const [url, format, isBinary] = t.params.split('|');
+
+                await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
+                    source: {
+                        name: 'url',
+                        params: {
+                            url,
+                            format: format as any,
+                            isBinary: isBinary === 'true',
+                        }
+                    }
+                }));
+            }
+        } catch (e) {
+            console.error(e);
+            this.plugin.log.error(`Failed to load Zenodo file`);
+        } finally {
+            this.setState({ busy: false });
+        }
+    };
+
+    private clearRecord = () => {
+        this.setState({
+            importValues: undefined,
+            importParams: undefined,
+            record: undefined,
+            files: undefined
+        });
+    };
+
+    private renderLoadRecord() {
+        return <div style={{ marginBottom: 10 }}>
+            <ParameterControls params={ZenodoImportParams} values={this.state.recordValues} onChangeValues={this.recordParamsOnChange} isDisabled={this.state.busy} />
+            <Button onClick={this.loadRecord} style={{ marginTop: 1 }} disabled={this.state.busy}>
+                Load Record
+            </Button>
+        </div>;
+    }
+
+    private renderRecordInfo(record: ZenodoRecord) {
+        return <div style={{ marginBottom: 10 }}>
+            <div className='msp-help-text'>
+                <div>{`${record.metadata.title} (${record.id})`}</div>
+            </div>
+            <Button onClick={this.clearRecord} style={{ marginTop: 1 }} disabled={this.state.busy}>
+                Clear
+            </Button>
+        </div>;
+    }
+
+    private renderImportFile(params: ImportParams, values: PD.Values<ImportParams>) {
+        return values.type.name ? <div style={{ marginBottom: 10 }}>
+            <ParameterControls params={params} values={this.state.importValues} onChangeValues={this.importParamsOnChange} isDisabled={this.state.busy} />
+            <Button onClick={() => this.loadFile(values)} style={{ marginTop: 1 }} disabled={this.state.busy}>
+                Import File
+            </Button>
+        </div> : <div className='msp-help-text' style={{ marginBottom: 10 }}>
+            <div>No supported files</div>
+        </div>;
+    }
+
+    protected renderControls(): JSX.Element | null {
+        return <>
+            {!this.state.record ? this.renderLoadRecord() : null}
+            {this.state.record ? this.renderRecordInfo(this.state.record) : null}
+            {this.state.importParams && this.state.importValues ? this.renderImportFile(this.state.importParams, this.state.importValues) : null}
+        </>;
+    }
+}