Procházet zdrojové kódy

wip: volume streaming

David Sehnal před 6 roky
rodič
revize
5a22b99f3d

+ 126 - 0
src/mol-plugin/behavior/dynamic/volume.ts

@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import CIF from 'mol-io/reader/cif';
+import { Box3D } from 'mol-math/geometry';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { VolumeData } from 'mol-model/volume';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { IsoValueParam } from 'mol-repr/volume/isosurface';
+import { Color } from 'mol-util/color';
+import { ColorNames } from 'mol-util/color/tables';
+import { LRUCache } from 'mol-util/lru-cache';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginBehavior } from '../behavior';
+
+export namespace VolumeStreamingBehavior {
+    function channelParam(label: string) { return PD.Group({ color: PD.Color(Color(ColorNames.teal)), isoValue: IsoValueParam }, { label }); }
+
+    export const Params = {
+        id: PD.Text(''),
+        levels: PD.MappedStatic('em', {
+            'em': channelParam('EM'),
+            'x-ray': PD.Group({
+                '2fo-fc': channelParam('2Fo-Fc'),
+                'fo-fc(+ve)': channelParam('Fo-Fc(+ve)'),
+                'fo-fc(-ve)': channelParam('Fo-Fc(-ve)'),
+            })
+        }),
+        box: PD.MappedStatic('static-box', {
+            'static-box': PD.Group({
+                bottomLeft: PD.Vec3(Vec3.create(0, 0, 0)),
+                topRight: PD.Vec3(Vec3.create(1, 1, 1))
+            }, { description: 'Static box defined by cartesian coords.' }),
+            // 'around-selection': PD.Group({ radius: PD.Numeric(5, { min: 0, max: 10 }) }),
+        }),
+        detailLevel: PD.Numeric(3, { min: 0, max: 7 }),
+        serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer'),
+    }
+    export type Params = PD.Values<typeof Params>
+
+    export type ChannelData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
+    export type LevelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
+
+    export class Behavior implements PluginBehavior<Params> {
+        private cache = LRUCache.create<ChannelData>(25);
+
+        currentData: ChannelData = { }
+
+        private async queryData(box?: Box3D) {
+            let url = `${this.params.serverUrl}/${this.params.levels.name}/${this.params.id}`
+
+            if (box) {
+                const { min: a, max: b } = box;
+                url += `/box`
+                    + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
+                    + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
+            } else {
+                url += `/cell`;
+            }
+            url += `?detail=${this.params.detailLevel}`;
+
+            let data = LRUCache.get(this.cache, url);
+            if (data) {
+                return data;
+            }
+
+            const cif = await this.ctx.runTask(this.ctx.fetch(url, 'binary'));
+            data = await this.parseCif(cif as Uint8Array);
+            if (!data) {
+                return;
+            }
+
+            LRUCache.set(this.cache, url, data);
+            return data;
+        }
+
+        private async parseCif(data: Uint8Array): Promise<ChannelData | undefined> {
+            const parsed = await this.ctx.runTask(CIF.parseBinary(data));
+            if (parsed.isError) {
+                this.ctx.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
+                return;
+            }
+            if (parsed.result.blocks.length < 2) {
+                this.ctx.log.error('VolumeStreaming: Invalid data.');
+                return;
+            }
+
+            const ret: ChannelData = { };
+            for (let i = 1; i < parsed.result.blocks.length; i++) {
+                const block = parsed.result.blocks[i];
+
+                const densityServerCif = CIF.schema.densityServer(block);
+                const volume = this.ctx.runTask(await volumeFromDensityServerData(densityServerCif));
+                (ret as any)[block.header as any] = volume;
+            }
+            return ret;
+        }
+
+        register(ref: string): void {
+            this.update(this.params);
+        }
+
+        async update(params: Params): Promise<boolean> {
+            this.params = params;
+
+            const box: Box3D = Box3D.create(params.box.params.bottomLeft, params.box.params.topRight);
+            const data = await this.queryData(box);
+            this.currentData = data || { };
+
+            return true;
+        }
+
+        unregister(): void {
+        }
+
+        constructor(public ctx: PluginContext, public params: Params) {
+        }
+    }
+
+    export class Obj extends PluginStateObject.CreateBehavior<Behavior>({ name: 'Volume Streaming' }) { }
+}

+ 5 - 5
src/mol-plugin/state/actions/volume.ts

@@ -81,7 +81,7 @@ const Ccp4Provider: DataFormatProvider<any> = {
     getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
         return Task.create('CCP4/MRC/BRIX default builder', async taskCtx => {
             const tree = data.apply(StateTransforms.Data.ParseCcp4)
-                .apply(StateTransforms.Model.VolumeFromCcp4)
+                .apply(StateTransforms.Volume.VolumeFromCcp4)
                 .apply(StateTransforms.Representation.VolumeRepresentation3D)
             await state.updateTree(tree).runInContext(taskCtx)
         })
@@ -98,7 +98,7 @@ const Dsn6Provider: DataFormatProvider<any> = {
     getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
         return Task.create('DSN6/BRIX default builder', async taskCtx => {
             const tree = data.apply(StateTransforms.Data.ParseDsn6)
-                .apply(StateTransforms.Model.VolumeFromDsn6)
+                .apply(StateTransforms.Volume.VolumeFromDsn6)
                 .apply(StateTransforms.Representation.VolumeRepresentation3D)
             await state.updateTree(tree).runInContext(taskCtx)
         })
@@ -121,14 +121,14 @@ const DscifProvider: DataFormatProvider<any> = {
             let tree: StateBuilder.To<any>
             if (blocks.length === 1) {
                 tree = b
-                    .apply(StateTransforms.Model.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
                     .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.teal }))
             } else if (blocks.length === 2) {
                 tree = b
-                    .apply(StateTransforms.Model.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
                     .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.blue }))
                 const vol = tree.to(cifBuilder.ref)
-                    .apply(StateTransforms.Model.VolumeFromDensityServerCif, { blockHeader: blocks[1].header })
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[1].header })
                 const posParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(3), alpha: 0.3 }, 'uniform', { value: ColorNames.green })
                 tree = vol.apply(StateTransforms.Representation.VolumeRepresentation3D, posParams)
                 const negParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(-3), alpha: 0.3 }, 'uniform', { value: ColorNames.red })

+ 2 - 0
src/mol-plugin/state/transforms.ts

@@ -6,10 +6,12 @@
 
 import * as Data from './transforms/data'
 import * as Model from './transforms/model'
+import * as Volume from './transforms/volume'
 import * as Representation from './transforms/representation'
 
 export const StateTransforms = {
     Data,
     Model,
+    Volume,
     Representation
 }

+ 21 - 105
src/mol-plugin/state/transforms/model.ts

@@ -5,28 +5,31 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { PluginStateTransform } from '../objects';
-import { PluginStateObject as SO } from '../objects';
-import { Task, RuntimeContext } from 'mol-task';
-import { Model, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import Expression from 'mol-script/language/expression';
-import { compile } from 'mol-script/runtime/query/compiler';
-import { MolScriptBuilder } from 'mol-script/language/builder';
-import { StateObject } from 'mol-state';
-import { PluginContext } from 'mol-plugin/context';
-import { stringToWords } from 'mol-util/string';
-import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4';
+import { parsePDB } from 'mol-io/reader/pdb/parser';
 import { Vec3 } from 'mol-math/linear-algebra';
-import CIF from 'mol-io/reader/cif';
-import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6';
-import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
 import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
-import { parsePDB } from 'mol-io/reader/pdb/parser';
 import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
+import { Model, ModelSymmetry, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureSymmetry } from 'mol-model/structure';
 import { Assembly } from 'mol-model/structure/model/properties/symmetry';
+import { PluginContext } from 'mol-plugin/context';
+import { MolScriptBuilder } from 'mol-script/language/builder';
+import Expression from 'mol-script/language/expression';
+import { compile } from 'mol-script/runtime/query/compiler';
+import { StateObject } from 'mol-state';
+import { RuntimeContext, Task } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { stringToWords } from 'mol-util/string';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 
-export { TrajectoryFromMmCif }
+export { TrajectoryFromMmCif };
+export { TrajectoryFromPDB };
+export { ModelFromTrajectory };
+export { StructureFromModel };
+export { StructureAssemblyFromModel };
+export { StructureSymmetryFromModel };
+export { StructureSelection };
+export { StructureComplexElement };
+export { CustomModelProperties };
 type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
 const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-mmcif',
@@ -60,7 +63,6 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
 });
 
 
-export { TrajectoryFromPDB }
 type TrajectoryFromPDB = typeof TrajectoryFromPDB
 const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-pdb',
@@ -80,7 +82,6 @@ const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
 });
 
 
-export { ModelFromTrajectory }
 const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
 type ModelFromTrajectory = typeof ModelFromTrajectory
 const ModelFromTrajectory = PluginStateTransform.BuiltIn({
@@ -106,7 +107,6 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureFromModel }
 type StructureFromModel = typeof StructureFromModel
 const StructureFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-from-model',
@@ -125,7 +125,6 @@ function structureDesc(s: Structure) {
     return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`;
 }
 
-export { StructureAssemblyFromModel }
 type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
 const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-assembly-from-model',
@@ -178,7 +177,6 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureSymmetryFromModel }
 type StructureSymmetryFromModel = typeof StructureSymmetryFromModel
 const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-symmetry-from-model',
@@ -204,7 +202,6 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureSelection }
 type StructureSelection = typeof StructureSelection
 const StructureSelection = PluginStateTransform.BuiltIn({
     name: 'structure-selection',
@@ -226,7 +223,6 @@ const StructureSelection = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureComplexElement }
 namespace StructureComplexElement {
     export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
 }
@@ -259,7 +255,6 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
     }
 });
 
-export { CustomModelProperties }
 type CustomModelProperties = typeof CustomModelProperties
 const CustomModelProperties = PluginStateTransform.BuiltIn({
     name: 'custom-model-properties',
@@ -283,83 +278,4 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon
         const p = ctx.customModelProperties.get(name);
         await p.attach(model).runInContext(taskCtx);
     }
-}
-
-//
-
-export { VolumeFromCcp4 }
-type VolumeFromCcp4 = typeof VolumeFromCcp4
-const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
-    name: 'volume-from-ccp4',
-    display: { name: 'Volume from CCP4/MRC/MAP', description: 'Create Volume from CCP4/MRC/MAP data' },
-    from: SO.Format.Ccp4,
-    to: SO.Volume.Data,
-    params(a) {
-        return {
-            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
-        };
-    }
-})({
-    apply({ a, params }) {
-        return Task.create('Create volume from CCP4/MRC/MAP', async ctx => {
-            const volume = await volumeFromCcp4(a.data, params).runInContext(ctx)
-            const props = { label: 'Volume' };
-            return new SO.Volume.Data(volume, props);
-        });
-    }
-});
-
-export { VolumeFromDsn6 }
-type VolumeFromDsn6 = typeof VolumeFromDsn6
-const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
-    name: 'volume-from-dsn6',
-    display: { name: 'Volume from DSN6/BRIX', description: 'Create Volume from DSN6/BRIX data' },
-    from: SO.Format.Dsn6,
-    to: SO.Volume.Data,
-    params(a) {
-        return {
-            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
-        };
-    }
-})({
-    apply({ a, params }) {
-        return Task.create('Create volume from DSN6/BRIX', async ctx => {
-            const volume = await volumeFromDsn6(a.data, params).runInContext(ctx)
-            const props = { label: 'Volume' };
-            return new SO.Volume.Data(volume, props);
-        });
-    }
-});
-
-export { VolumeFromDensityServerCif }
-type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif
-const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
-    name: 'volume-from-density-server-cif',
-    display: { name: 'Volume from density-server CIF', description: 'Identify and create all separate models in the specified CIF data block' },
-    from: SO.Format.Cif,
-    to: SO.Volume.Data,
-    params(a) {
-        if (!a) {
-            return {
-                blockHeader: PD.makeOptional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
-            };
-        }
-        const blocks = a.data.blocks.slice(1); // zero block contains query meta-data
-        return {
-            blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
-        };
-    }
-})({
-    isApplicable: a => a.data.blocks.length > 0,
-    apply({ a, params }) {
-        return Task.create('Parse density-server CIF', async ctx => {
-            const header = params.blockHeader || a.data.blocks[1].header; // zero block contains query meta-data
-            const block = a.data.blocks.find(b => b.header === header);
-            if (!block) throw new Error(`Data block '${[header]}' not found.`);
-            const densityServerCif = CIF.schema.densityServer(block)
-            const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx)
-            const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` };
-            return new SO.Volume.Data(volume, props);
-        });
-    }
-});
+}

+ 171 - 0
src/mol-plugin/state/transforms/volume.ts

@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2018-2019 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 CIF from 'mol-io/reader/cif';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6';
+import { Task } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+import { VolumeStreamingBehavior as VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume';
+import { PluginContext } from 'mol-plugin/context';
+import { StateTransformer } from 'mol-state';
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
+import { BuiltInVolumeRepresentations } from 'mol-repr/volume/registry';
+import { createTheme } from 'mol-theme/theme';
+
+export { VolumeFromCcp4 };
+export { VolumeFromDsn6 };
+export { VolumeFromDensityServerCif };
+type VolumeFromCcp4 = typeof VolumeFromCcp4
+const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
+    name: 'volume-from-ccp4',
+    display: { name: 'Volume from CCP4/MRC/MAP', description: 'Create Volume from CCP4/MRC/MAP data' },
+    from: SO.Format.Ccp4,
+    to: SO.Volume.Data,
+    params(a) {
+        return {
+            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create volume from CCP4/MRC/MAP', async ctx => {
+            const volume = await volumeFromCcp4(a.data, params).runInContext(ctx)
+            const props = { label: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+type VolumeFromDsn6 = typeof VolumeFromDsn6
+const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
+    name: 'volume-from-dsn6',
+    display: { name: 'Volume from DSN6/BRIX', description: 'Create Volume from DSN6/BRIX data' },
+    from: SO.Format.Dsn6,
+    to: SO.Volume.Data,
+    params(a) {
+        return {
+            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create volume from DSN6/BRIX', async ctx => {
+            const volume = await volumeFromDsn6(a.data, params).runInContext(ctx)
+            const props = { label: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif
+const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
+    name: 'volume-from-density-server-cif',
+    display: { name: 'Volume from density-server CIF', description: 'Identify and create all separate models in the specified CIF data block' },
+    from: SO.Format.Cif,
+    to: SO.Volume.Data,
+    params(a) {
+        if (!a) {
+            return {
+                blockHeader: PD.makeOptional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
+            };
+        }
+        const blocks = a.data.blocks.slice(1); // zero block contains query meta-data
+        return {
+            blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
+        };
+    }
+})({
+    isApplicable: a => a.data.blocks.length > 0,
+    apply({ a, params }) {
+        return Task.create('Parse density-server CIF', async ctx => {
+            const header = params.blockHeader || a.data.blocks[1].header; // zero block contains query meta-data
+            const block = a.data.blocks.find(b => b.header === header);
+            if (!block) throw new Error(`Data block '${[header]}' not found.`);
+            const densityServerCif = CIF.schema.densityServer(block)
+            const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx)
+            const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+export { VolumeStreamingBehavior }
+type VolumeStreamingBehavior = typeof VolumeStreamingBehavior
+const VolumeStreamingBehavior = PluginStateTransform.BuiltIn({
+    name: 'volume-streaming-behavior',
+    display: { name: 'Volume Streaming Behavior', description: 'Create Volume Streaming behavior.' },
+    from: SO.Molecule.Model,
+    to: VolumeStreaming.Obj,
+    params: VolumeStreaming.Params
+})({
+    apply({ a, params }, plugin: PluginContext) {
+        const behavior = new VolumeStreaming.Behavior(plugin, params);
+        return new VolumeStreaming.Obj(behavior, { label: 'Volume Streaming' });
+    },
+    update({ b, newParams }) {
+        return Task.create('Update Volume Streaming', async _ => {
+            await b.data.update(newParams);
+            return StateTransformer.UpdateResult.Updated;
+        });
+    }
+});
+
+export { VolumeStreamingVisual }
+type VolumeStreamingVisual = typeof VolumeStreamingVisual
+const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
+    name: 'volume-streaming-visual',
+    display: { name: 'Volume Streaming Visual' },
+    from: VolumeStreaming.Obj,
+    to: SO.Volume.Representation3D,
+    params: {
+        channel: PD.Select<keyof VolumeStreaming.ChannelData>('EM', [['EM', 'EM'], ['FO-FC', 'Fo-Fc'], ['2FO-FC', '2Fo-Fc']], { isHidden: true }),
+        level: PD.Text<VolumeStreaming.LevelType>('em')
+    }
+})({
+    apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
+        const { data, props, theme } = createVolumeProps(a.data, params.channel, params.level)
+
+        const repr = BuiltInVolumeRepresentations.isosurface.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, BuiltInVolumeRepresentations.isosurface.getParams);
+        repr.setTheme(theme);
+
+        await repr.createOrUpdate(props, data).runInContext(ctx);
+        return new SO.Volume.Representation3D(repr, { label: params.level });
+    }),
+    update: ({ a, b, oldParams, newParams }) => Task.create('Volume Representation', async ctx => {
+        // TODO : check if params have changed
+        const { data, props, theme } = createVolumeProps(a.data, newParams.channel, newParams.level);
+        b.data.setTheme(theme);
+        await b.data.createOrUpdate(props, data).runInContext(ctx);
+        return StateTransformer.UpdateResult.Updated;
+    })
+});
+
+function createVolumeProps(streaming: VolumeStreaming.Behavior, channel: keyof VolumeStreaming.ChannelData, level: VolumeStreaming.LevelType) {
+    const data = streaming.currentData[channel] || VolumeData.Empty;
+    const { themeCtx } = streaming.ctx.volumeRepresentation;
+
+    const props = PD.getDefaultValues(BuiltInVolumeRepresentations.isosurface.getParams(themeCtx, data));
+    let isoValue: VolumeIsoValue;
+
+    if (level === 'em' && streaming.params.levels.name === 'em') {
+        isoValue = streaming.params.levels.params.isoValue;
+    } else if (level !== 'em' && streaming.params.levels.name === 'x-ray') {
+        isoValue = streaming.params.levels.params[level].isoValue;
+    } else {
+        throw new Error(`Unsupported iso level ${level}.`);
+    }
+
+    props.isoValue = isoValue;
+
+    const theme = createTheme(streaming.ctx.volumeRepresentation.themeCtx, { volume: data }, props);
+
+    return { data, props, theme };
+}

+ 1 - 1
src/mol-repr/volume/isosurface.ts

@@ -19,7 +19,7 @@ import { VisualContext } from 'mol-repr/visual';
 import { NullLocation } from 'mol-model/location';
 import { Lines } from 'mol-geo/geometry/lines/lines';
 
-const IsoValueParam = PD.Conditioned(
+export const IsoValueParam = PD.Conditioned(
     VolumeIsoValue.relative(2),
     {
         'absolute': PD.Converted(

+ 53 - 0
src/mol-util/lru-cache.ts

@@ -0,0 +1,53 @@
+
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol.
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { LinkedList } from 'mol-data/generic';
+
+export { LRUCache }
+
+interface LRUCache<T> {
+    entries: LinkedList<LRUCache.Entry<T>>,
+    capacity: number
+}
+
+namespace LRUCache {
+    export interface Entry<T> {
+        key: string,
+        data: T
+    }
+
+    function entry<T>(key: string, data: T): Entry<T> {
+        return { key, data };
+    }
+
+    export function create<T>(capacity: number): LRUCache<T> {
+        return {
+            entries: LinkedList<Entry<T>>(),
+            capacity: Math.max(1, capacity)
+        };
+    }
+
+    export function get<T>(cache: LRUCache<T>, key: string) {
+        for (let e = cache.entries.first; e; e = e.next) {
+            if (e.value.key === key) {
+                cache.entries.remove(e);
+                cache.entries.addLast(e.value);
+                return e.value.data;
+            }
+        }
+        return void 0;
+    }
+
+    export function set<T>(cache: LRUCache<T>, key: string, data: T): T {
+        if (cache.entries.count >= cache.capacity) {
+            cache.entries.remove(cache.entries.first!);
+        }
+        cache.entries.addLast(entry(key, data));
+        return data;
+    }
+}