ソースを参照

lazy volume loading

dsehnal 4 年 前
コミット
9e105020e3

+ 3 - 0
CHANGELOG.md

@@ -13,6 +13,9 @@ Note that since we don't clearly distinguish between a public and private interf
 - Support for  full pausing (no draw) rendering: ``Canvas3D.pause(true)``.
 - Add `MeshBuilder.addMesh`.
 - Add `Torus` primitive.
+- Lazy volume loading support.
+- [Breaking] ``Viewer.loadVolumeFromUrl`` signature change.
+  - ``loadVolumeFromUrl(url, format, isBinary, isovalues, entryId)`` => ``loadVolumeFromUrl({ url, format, isBinary }, isovalues, { entryId, isLazy })``
 
 ## [v2.0.4] - 2021-04-20
 

+ 15 - 1
src/apps/viewer/embedded.html

@@ -21,7 +21,7 @@
         <script type="text/javascript" src="./molstar.js"></script>
         <script type="text/javascript">
             var viewer = new molstar.Viewer('app', {
-                layoutIsExpanded: false,
+                layoutIsExpanded: true,
                 layoutShowControls: false,
                 layoutShowRemoteState: false,
                 layoutShowSequence: true,
@@ -37,6 +37,20 @@
             });
             viewer.loadPdb('7bv2');
             viewer.loadEmdb('EMD-30210', { detail: 6 });
+
+            // viewer.loadVolumeFromUrl({
+            //     url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6',
+            //     format: 'dscif',
+            //     isBinary: true 
+            // }, [{
+            //     type: 'relative',
+            //     value: 1,
+            //     color: 0x3377aa
+            // }], {
+            //     entryId: 'EMD-30210',
+            //     isLazy: true
+            // });
+
             // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
         </script>
     </body>

+ 16 - 4
src/apps/viewer/index.ts

@@ -243,17 +243,29 @@ export class Viewer {
         }));
     }
 
-    async loadVolumeFromUrl(url: string, format: BuildInVolumeFormat, isBinary: boolean, isovalues: VolumeIsovalueInfo[], entryId?: string) {
+    async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string, isLazy?: boolean }) {
         const plugin = this.plugin;
 
         if (!plugin.dataFormats.get(format)) {
             throw new Error(`Unknown density format: ${format}`);
         }
 
+        if (options?.isLazy) {
+            const update = this.plugin.build();
+            update.toRoot().apply(StateTransforms.Data.LazyVolume, {
+                url,
+                format,
+                entryId: options?.entryId,
+                isBinary,
+                isovalues: isovalues.map(v => ({ alpha: 1, ...v }))
+            });
+            return update.commit();
+        }
+
         return plugin.dataTransaction(async () => {
-            const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } });
+            const data = await plugin.builders.data.download({ url, isBinary, label: options?.entryId }, { state: { isGhost: true } });
 
-            const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId });
+            const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId });
             const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
             if (!volume?.isOk) throw new Error('Failed to parse any volume.');
 
@@ -261,7 +273,7 @@ export class Viewer {
             for (const iso of isovalues) {
                 repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, {
                     type: 'isosurface',
-                    typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ?  { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
+                    typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
                     color: 'uniform',
                     colorParams: { value: iso.color }
                 }));

+ 14 - 2
src/mol-plugin-state/manager/volume/hierarchy-state.ts

@@ -17,13 +17,14 @@ export function buildVolumeHierarchy(state: State, previous?: VolumeHierarchy) {
 
 export interface VolumeHierarchy {
     volumes: VolumeRef[],
+    lazyVolumes: LazyVolumeRef[],
     refs: Map<StateTransform.Ref, VolumeHierarchyRef>
     // TODO: might be needed in the future
     // decorators: Map<StateTransform.Ref, StateTransform>,
 }
 
 export function VolumeHierarchy(): VolumeHierarchy {
-    return { volumes: [], refs: new Map() };
+    return { volumes: [], lazyVolumes: [], refs: new Map() };
 }
 
 interface RefBase<K extends string = string, O extends StateObject = StateObject, T extends StateTransformer = StateTransformer> {
@@ -32,7 +33,7 @@ interface RefBase<K extends string = string, O extends StateObject = StateObject
     version: StateTransform['version']
 }
 
-export type VolumeHierarchyRef = VolumeRef | VolumeRepresentationRef
+export type VolumeHierarchyRef = VolumeRef | LazyVolumeRef | VolumeRepresentationRef
 
 export interface VolumeRef extends RefBase<'volume', SO.Volume.Data> {
     representations: VolumeRepresentationRef[]
@@ -42,6 +43,13 @@ function VolumeRef(cell: StateObjectCell<SO.Volume.Data>): VolumeRef {
     return { kind: 'volume', cell, version: cell.transform.version, representations: [] };
 }
 
+export interface LazyVolumeRef extends RefBase<'lazy-volume', SO.Volume.Lazy> {
+}
+
+function LazyVolumeRef(cell: StateObjectCell<SO.Volume.Lazy>): LazyVolumeRef {
+    return { kind: 'lazy-volume', cell, version: cell.transform.version };
+}
+
 export interface VolumeRepresentationRef extends RefBase<'volume-representation', SO.Volume.Representation3D, StateTransforms['Representation']['VolumeRepresentation3D']> {
     volume: VolumeRef
 }
@@ -95,6 +103,10 @@ const Mapping: [TestCell, ApplyRef, LeaveRef][] = [
         state.currentVolume = createOrUpdateRefList(state, cell, state.hierarchy.volumes, VolumeRef, cell);
     }, state => state.currentVolume = void 0],
 
+    [cell => SO.Volume.Lazy.is(cell.obj), (state, cell) => {
+        createOrUpdateRefList(state, cell, state.hierarchy.lazyVolumes, LazyVolumeRef, cell);
+    }, noop],
+
     [(cell, state) => {
         return !cell.state.isGhost && !!state.currentVolume && SO.Volume.Representation3D.is(cell.obj);
     }, (state, cell) => {

+ 16 - 0
src/mol-plugin-state/objects.ts

@@ -22,6 +22,8 @@ import { VolumeRepresentation } from '../mol-repr/volume/representation';
 import { StateObject, StateTransformer } from '../mol-state';
 import { CubeFile } from '../mol-io/reader/cube/parser';
 import { DxFile } from '../mol-io/reader/dx/parser';
+import { Color } from '../mol-util/color/color';
+import { Asset } from '../mol-util/assets';
 
 export type TypeClass = 'root' | 'data' | 'prop'
 
@@ -119,7 +121,21 @@ export namespace PluginStateObject {
     }
 
     export namespace Volume {
+        export interface LazyInfo {
+            url: string | Asset.Url,
+            isBinary: boolean,
+            format: string,
+            entryId?: string,
+            isovalues: {
+                type: 'absolute' | 'relative',
+                value: number,
+                color: Color,
+                alpha?: number
+            }[]
+        }
+
         export class Data extends Create<_Volume>({ name: 'Volume', typeClass: 'Object' }) { }
+        export class Lazy extends Create<LazyInfo>({ name: 'Lazy Volume', typeClass: 'Object' }) { }
         export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>, _Volume>({ name: 'Volume 3D' }) { }
     }
 

+ 30 - 1
src/mol-plugin-state/transforms/data.ts

@@ -19,6 +19,7 @@ import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 import { Asset } from '../../mol-util/assets';
 import { parseCube } from '../../mol-io/reader/cube/parser';
 import { parseDx } from '../../mol-io/reader/dx/parser';
+import { ColorNames } from '../../mol-util/color/names';
 
 export { Download };
 export { DownloadBlob };
@@ -35,6 +36,7 @@ export { ParseDx };
 export { ImportString };
 export { ImportJson };
 export { ParseJson };
+export { LazyVolume };
 
 type Download = typeof Download
 const Download = PluginStateTransform.BuiltIn({
@@ -441,4 +443,31 @@ const ParseJson = PluginStateTransform.BuiltIn({
             return new SO.Format.Json(json);
         });
     }
-});
+});
+
+type LazyVolume = typeof LazyVolume
+const LazyVolume = PluginStateTransform.BuiltIn({
+    name: 'lazy-volume',
+    display: { name: 'Lazy Volume', description: 'A placeholder for lazy loaded volume representation' },
+    from: SO.Root,
+    to: SO.Volume.Lazy,
+    params: {
+        url: PD.Url(''),
+        isBinary: PD.Boolean(false),
+        format: PD.Text('ccp4'), // TODO: use Select based on available formats
+        entryId: PD.Text(''),
+        isovalues: PD.ObjectList({
+            type: PD.Text<'absolute' | 'relative'>('relative'), // TODO: Select
+            value: PD.Numeric(0),
+            color: PD.Color(ColorNames.black),
+            alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 })
+        }, e => `${e.type} ${e.value}`)
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Lazy Volume', async ctx => {
+            return new SO.Volume.Lazy(params, { label: `${params.entryId || params.url}`, description: 'Lazy Volume' });
+        });
+    }
+});
+

+ 65 - 11
src/mol-plugin-ui/structure/volume.tsx

@@ -8,11 +8,11 @@
 import * as React from 'react';
 import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
 import { VolumeHierarchyManager } from '../../mol-plugin-state/manager/volume/hierarchy';
-import { VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state';
+import { LazyVolumeRef, VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state';
 import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
 import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
-import { State, StateSelection, StateTransform } from '../../mol-state';
+import { State, StateObjectCell, StateObjectSelector, StateSelection, StateTransform } from '../../mol-state';
 import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
 import { Button, ExpandGroup, IconButton } from '../controls/common';
@@ -21,6 +21,9 @@ import { UpdateTransformControl } from '../state/update-transform';
 import { BindingsHelp } from '../viewport/help';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { BlurOnSvg, ErrorSvg, CheckSvg, AddSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg } from '../controls/icons';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
 
 interface VolumeStreamingControlState extends CollapsableState {
     isBusy: boolean
@@ -104,6 +107,7 @@ export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStrea
 
 interface VolumeSourceControlState extends CollapsableState {
     isBusy: boolean,
+    loadingLabel?: string,
     show?: 'hierarchy' | 'add-repr'
 }
 
@@ -120,18 +124,23 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
 
     componentDidMount() {
         this.subscribe(this.plugin.managers.volume.hierarchy.behaviors.selection, sel => {
-            this.setState({ isHidden: sel.hierarchy.volumes.length === 0 });
+            this.setState({ isHidden: sel.hierarchy.volumes.length === 0 && sel.hierarchy.lazyVolumes.length === 0 });
         });
         this.subscribe(this.plugin.behaviors.state.isBusy, v => {
             this.setState({ isBusy: v });
         });
     }
 
-    private item = (ref: VolumeRef) => {
+    private item = (ref: VolumeRef | LazyVolumeRef) => {
         const selected = this.plugin.managers.volume.hierarchy.selection;
 
         const label = ref.cell.obj?.label || 'Volume';
-        const item: ActionMenu.Item = { kind: 'item', label: label || ref.kind, selected: selected === ref, value: ref };
+        const item: ActionMenu.Item = {
+            kind: 'item',
+            label: (ref.kind === 'lazy-volume' ? 'Load ' : '') + (label || ref.kind),
+            selected: selected === ref,
+            value: ref
+        };
         return item;
     }
 
@@ -139,9 +148,15 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
         const mng = this.plugin.managers.volume.hierarchy;
         const { current } = mng;
         const ret: ActionMenu.Items = [];
-        for (let ref of current.volumes) {
+
+        for (const ref of current.volumes) {
+            ret.push(this.item(ref));
+        }
+
+        for (const ref of current.lazyVolumes) {
             ret.push(this.item(ref));
         }
+
         return ret;
     }
 
@@ -158,11 +173,13 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
     }
 
     get isEmpty() {
-        const { volumes } = this.plugin.managers.volume.hierarchy.current;
-        return volumes.length === 0;
+        const { volumes, lazyVolumes } = this.plugin.managers.volume.hierarchy.current;
+        return volumes.length === 0 && lazyVolumes.length === 0;
     }
 
     get label() {
+        if (this.state.loadingLabel) return `Loading ${this.state.loadingLabel}...`;
+
         const selected = this.plugin.managers.volume.hierarchy.selection;
         if (!selected) return 'Nothing Selected';
         return selected?.cell.obj?.label || 'Volume';
@@ -171,7 +188,45 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
     selectCurrent: ActionMenu.OnSelect = (item) => {
         this.toggleHierarchy();
         if (!item) return;
-        this.plugin.managers.volume.hierarchy.setCurrent(item.value as VolumeRef);
+
+        const current = item.value as VolumeRef | LazyVolumeRef;
+        if (current.kind === 'volume') {
+            this.plugin.managers.volume.hierarchy.setCurrent(current);
+        } else {
+            this.lazyLoad(current.cell);
+        }
+    }
+
+    private async lazyLoad(cell: StateObjectCell<PluginStateObject.Volume.Lazy>) {
+        const { url, isBinary, format, entryId, isovalues } = cell.obj!.data;
+
+        this.setState({ isBusy: true, loadingLabel: cell.obj!.label });
+
+        try {
+            const plugin = this.plugin;
+            await plugin.dataTransaction(async () => {
+                const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } });
+                const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId });
+                const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
+                if (!volume?.isOk) throw new Error('Failed to parse any volume.');
+
+                const repr = plugin.build().to(volume);
+                for (const iso of isovalues) {
+                    repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, {
+                        type: 'isosurface',
+                        typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
+                        color: 'uniform',
+                        colorParams: { value: iso.color }
+                    }));
+                }
+
+                await repr.commit();
+
+                await plugin.build().delete(cell).commit();
+            });
+        } finally {
+            this.setState({ isBusy: false, loadingLabel: void 0 });
+        }
     }
 
     selectAdd: ActionMenu.OnSelect = (item) => {
@@ -186,13 +241,12 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
     renderControls() {
         const disabled = this.state.isBusy || this.isEmpty;
         const label = this.label;
-
         const selected = this.plugin.managers.volume.hierarchy.selection;
 
         return <>
             <div className='msp-flex-row' style={{ marginTop: '1px' }}>
                 <Button noOverflow flex onClick={this.toggleHierarchy} disabled={disabled} title={label}>{label}</Button>
-                {!this.isEmpty && <IconButton svg={AddSvg} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />}
+                {!this.isEmpty && selected && <IconButton svg={AddSvg} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />}
             </div>
             {this.state.show === 'hierarchy' && <ActionMenu items={this.hierarchyItems} onSelect={this.selectCurrent} />}
             {this.state.show === 'add-repr' && <ActionMenu items={this.addActions} onSelect={this.selectAdd} />}