Browse Source

support for streaming multiple em volumes, limit to associate maps

Alexander Rose 5 years ago
parent
commit
d8f1aa5bc9

+ 52 - 25
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -55,13 +55,27 @@ export namespace VolumeStreaming {
     }
 
     export function createParams(data?: VolumeServerInfo.Data, defaultView?: ViewTypes, binding?: typeof DefaultBindings) {
+        const map = new Map<string, VolumeServerInfo.EntryData>()
+        if (data) data.entries.forEach(d => map.set(d.dataId, d))
+        const names = data ? data.entries.map(d => [d.dataId, d.dataId] as [string, string]) : []
+        const defaultKey = data ? data.entries[0].dataId : ''
+        return {
+            entry: PD.Mapped<EntryParams>(defaultKey, names, name => PD.Group(createEntryParams(map.get(name)!, defaultView, data && data.structure))),
+            bindings: PD.Value(binding || DefaultBindings, { isHidden: true }),
+        }
+    }
+
+    export type EntryParamDefinition = typeof createEntryParams extends (...args: any[]) => (infer T) ? T : never
+    export type EntryParams = EntryParamDefinition extends PD.Params ? PD.Values<EntryParamDefinition> : {}
+
+    export function createEntryParams(entryData?: VolumeServerInfo.EntryData, defaultView?: ViewTypes, structure?: Structure) {
         // fake the info
-        const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) };
-        const box = (data && data.structure.boundary.box) || Box3D.empty();
+        const info = entryData || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) };
+        const box = (structure && structure.boundary.box) || Box3D.empty();
 
         return {
             view: PD.MappedStatic(defaultView || (info.kind === 'em' ? 'cell' : 'selection-box'), {
-                'off': PD.Group({}),
+                'off': PD.Group<{}>({}),
                 'box': PD.Group({
                     bottomLeft: PD.Vec3(box.min),
                     topRight: PD.Vec3(box.max),
@@ -85,7 +99,6 @@ export namespace VolumeStreaming {
                     'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), VolumeIsoValue.relative(3), info.header.sampling[0].valuesInfo[1]),
                     'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), VolumeIsoValue.relative(-3), info.header.sampling[0].valuesInfo[1]),
                 }, { isFlat: true }),
-            bindings: PD.Value(binding || DefaultBindings, { isHidden: true }),
         };
     }
 
@@ -118,11 +131,16 @@ export namespace VolumeStreaming {
         public params: Params = {} as any;
         private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
         private ref: string = '';
+        public infoMap: Map<string, VolumeServerInfo.EntryData>
 
         channels: Channels = {}
 
+        public get info () {
+            return this.infoMap.get(this.params.entry.name)!
+        }
+
         private async queryData(box?: Box3D) {
-            let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId.toLowerCase()}`);
+            let url = urlCombine(this.data.serverUrl, `${this.info.kind}/${this.info.dataId.toLowerCase()}`);
 
             if (box) {
                 const { min: a, max: b } = box;
@@ -132,7 +150,7 @@ export namespace VolumeStreaming {
             } else {
                 url += `/cell`;
             }
-            url += `?detail=${this.params.detailLevel}`;
+            url += `?detail=${this.params.entry.params.detailLevel}`;
 
             let data = LRUCache.get(this.cache, url);
             if (data) {
@@ -165,24 +183,30 @@ export namespace VolumeStreaming {
                 const block = parsed.result.blocks[i];
 
                 const densityServerCif = CIF.schema.densityServer(block);
-                const volume = await this.plugin.runTask(await volumeFromDensityServerData(densityServerCif));
+                const volume = await this.plugin.runTask(volumeFromDensityServerData(densityServerCif));
                 (ret as any)[block.header as any] = volume;
             }
             return ret;
         }
 
         private updateDynamicBox(box: Box3D) {
-            if (this.params.view.name !== 'selection-box') return;
+            if (this.params.entry.params.view.name !== 'selection-box') return;
 
             const state = this.plugin.state.dataState;
             const newParams: Params = {
                 ...this.params,
-                view: {
-                    name: 'selection-box' as 'selection-box',
+                entry: {
+                    name: this.params.entry.name,
                     params: {
-                        radius: this.params.view.params.radius,
-                        bottomLeft: box.min,
-                        topRight: box.max
+                        ...this.params.entry.params,
+                        view: {
+                            name: 'selection-box' as 'selection-box',
+                            params: {
+                                radius: this.params.entry.params.view.params.radius,
+                                bottomLeft: box.min,
+                                topRight: box.max
+                            }
+                        }
                     }
                 }
             };
@@ -214,7 +238,7 @@ export namespace VolumeStreaming {
 
             this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
                 if (!Binding.match((this.params.bindings && this.params.bindings.clickVolumeAroundOnly) || DefaultBindings.clickVolumeAroundOnly, buttons, modifiers)) return;
-                if (this.params.view.name !== 'selection-box') {
+                if (this.params.entry.params.view.name !== 'selection-box') {
                     this.lastLoci = this.getNormalizedLoci(current.loci);
                 } else {
                     this.updateInteraction(current);
@@ -272,34 +296,34 @@ export namespace VolumeStreaming {
         }
 
         async update(params: Params) {
-            const switchedToSelection = params.view.name === 'selection-box' && this.params && this.params.view && this.params.view.name !== 'selection-box';
+            const switchedToSelection = params.entry.params.view.name === 'selection-box' && this.params && this.params.entry && this.params.entry.params && this.params.entry.params.view && this.params.entry.params.view.name !== 'selection-box';
 
             this.params = params;
 
             let box: Box3D | undefined = void 0, emptyData = false;
 
-            switch (params.view.name) {
+            switch (params.entry.params.view.name) {
                 case 'off':
                     emptyData = true;
                     break;
                 case 'box':
-                    box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight);
+                    box = Box3D.create(params.entry.params.view.params.bottomLeft, params.entry.params.view.params.topRight);
                     emptyData = Box3D.volume(box) < 0.0001;
                     break;
                 case 'selection-box': {
                     if (switchedToSelection) {
                         box = this.getBoxFromLoci(this.lastLoci) || Box3D.empty();
                     } else {
-                        box = Box3D.create(Vec3.clone(params.view.params.bottomLeft), Vec3.clone(params.view.params.topRight));
+                        box = Box3D.create(Vec3.clone(params.entry.params.view.params.bottomLeft), Vec3.clone(params.entry.params.view.params.topRight));
                     }
-                    const r = params.view.params.radius;
+                    const r = params.entry.params.view.params.radius;
                     emptyData = Box3D.volume(box) < 0.0001;
                     Box3D.expand(box, box, Vec3.create(r, r, r));
                     break;
                 }
                 case 'cell':
                     box = this.info.kind === 'x-ray'
-                        ? this.info.structure.boundary.box
+                        ? this.data.structure.boundary.box
                         : void 0;
                     break;
             }
@@ -308,7 +332,7 @@ export namespace VolumeStreaming {
 
             if (!data) return false;
 
-            const info = params.channels as ChannelsInfo;
+            const info = params.entry.params.channels as ChannelsInfo;
 
             if (this.info.kind === 'x-ray') {
                 this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || VolumeData.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
@@ -333,14 +357,17 @@ export namespace VolumeStreaming {
         }
 
         getDescription() {
-            if (this.params.view.name === 'selection-box') return 'Selection';
-            if (this.params.view.name === 'box') return 'Static Box';
-            if (this.params.view.name === 'cell') return 'Cell';
+            if (this.params.entry.params.view.name === 'selection-box') return 'Selection';
+            if (this.params.entry.params.view.name === 'box') return 'Static Box';
+            if (this.params.entry.params.view.name === 'cell') return 'Cell';
             return '';
         }
 
-        constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) {
+        constructor(public plugin: PluginContext, public data: VolumeServerInfo.Data) {
             super(plugin, {} as any);
+
+            this.infoMap = new Map<string, VolumeServerInfo.EntryData>()
+            this.data.entries.forEach(info => this.infoMap.set(info.dataId, info))
         }
     }
 }

+ 6 - 2
src/mol-plugin/behavior/dynamic/volume-streaming/model.ts

@@ -2,6 +2,7 @@
  * Copyright (c) 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 { PluginStateObject } from '../../../state/objects';
@@ -12,13 +13,16 @@ export class VolumeServerInfo extends PluginStateObject.Create<VolumeServerInfo.
 
 export namespace VolumeServerInfo {
     export type Kind = 'x-ray' | 'em'
-    export interface Data  {
-        serverUrl: string,
+    export interface EntryData  {
         kind: Kind,
         // for em, the EMDB access code, for x-ray, the PDB id
         dataId: string,
         header: VolumeServerHeader,
         emDefaultContourLevel?: VolumeIsoValue,
+    }
+    export interface Data {
+        serverUrl: string,
+        entries: EntryData[],
         structure: Structure
     }
 }

+ 91 - 41
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -14,7 +14,7 @@ import { urlCombine } from '../../../../mol-util/url';
 import { createIsoValueParam } from '../../../../mol-repr/volume/isosurface';
 import { VolumeIsoValue } from '../../../../mol-model/volume';
 import { StateAction, StateObject, StateTransformer } from '../../../../mol-state';
-import { getStreamingMethod, getId, getContourLevel, getEmdbId } from './util';
+import { getStreamingMethod, getIds, getContourLevel, getEmdbIds } from './util';
 import { VolumeStreaming } from './behavior';
 import { VolumeRepresentation3DHelpers } from '../../../../mol-plugin/state/transforms/representation';
 import { BuiltInVolumeRepresentations } from '../../../../mol-repr/volume/registry';
@@ -22,15 +22,24 @@ import { createTheme } from '../../../../mol-theme/theme';
 import { Box3D } from '../../../../mol-math/geometry';
 import { Vec3 } from '../../../../mol-math/linear-algebra';
 
+function addEntry(entries: InfoEntryProps[], method: VolumeServerInfo.Kind, dataId: string, emDefaultContourLevel: number) {
+    entries.push({
+        source: method === 'em'
+            ? { name: 'em', params: { isoValue: VolumeIsoValue.absolute(emDefaultContourLevel || 0) } }
+            : { name: 'x-ray', params: { } },
+        dataId
+    })
+}
+
 export const InitVolumeStreaming = StateAction.build({
     display: { name: 'Volume Streaming' },
     from: SO.Molecule.Structure,
     params(a) {
         const method = getStreamingMethod(a && a.data);
-        const id = getId(a && a.data);
+        const ids = getIds(method, a && a.data);
         return {
             method: PD.Select<VolumeServerInfo.Kind>(method, [['em', 'EM'], ['x-ray', 'X-Ray']]),
-            id: PD.Text(id),
+            entries: PD.ObjectList({ id: PD.Text(ids[0] || '') }, ({ id }) => id, { defaultValue: ids.map(id => ({ id })) }),
             serverUrl: PD.Text('https://ds.litemol.org'),
             defaultView: PD.Select<VolumeStreaming.ViewTypes>(method === 'em' ? 'cell' : 'selection-box', VolumeStreaming.ViewTypeOptions as any),
             behaviorRef: PD.Text('', { isHidden: true }),
@@ -40,23 +49,35 @@ export const InitVolumeStreaming = StateAction.build({
     },
     isApplicable: (a) => a.data.models.length === 1
 })(({ ref, state, params }, plugin: PluginContext) => Task.create('Volume Streaming', async taskCtx => {
-    let dataId = params.id.toLowerCase(), emDefaultContourLevel: number | undefined;
-    if (params.method === 'em') {
-        await taskCtx.update('Getting EMDB info...');
-        if (!dataId.toUpperCase().startsWith('EMD')) {
-            dataId = await getEmdbId(plugin, taskCtx, dataId)
+    const entries: InfoEntryProps[] = []
+
+    for (let i = 0, il = params.entries.length; i < il; ++i) {
+        let dataId = params.entries[i].id.toLowerCase()
+        let emDefaultContourLevel: number | undefined;
+
+        if (params.method === 'em') {
+            // if pdb ids are given for method 'em', get corresponding emd ids
+            // and continue the loop
+            if (!dataId.toUpperCase().startsWith('EMD')) {
+                await taskCtx.update('Getting EMDB info...');
+                const emdbIds = await getEmdbIds(plugin, taskCtx, dataId)
+                for (let j = 0, jl = emdbIds.length; j < jl; ++j) {
+                    const emdbId = emdbIds[j]
+                    const contourLevel = await getContourLevel(params.emContourProvider, plugin, taskCtx, emdbId)
+                    addEntry(entries, params.method, emdbId, contourLevel || 0)
+                }
+                continue;
+            }
+            emDefaultContourLevel = await getContourLevel(params.emContourProvider, plugin, taskCtx, dataId);
         }
-        const contourLevel = await getContourLevel(params.emContourProvider, plugin, taskCtx, dataId);
-        emDefaultContourLevel = contourLevel || 0;
+
+        addEntry(entries, params.method, dataId, emDefaultContourLevel || 0)
     }
 
     const infoTree = state.build().to(ref)
         .apply(CreateVolumeStreamingInfo, {
             serverUrl: params.serverUrl,
-            source: params.method === 'em'
-                ? { name: 'em', params: { isoValue: VolumeIsoValue.absolute(emDefaultContourLevel || 0) } }
-                : { name: 'x-ray', params: { } },
-            dataId
+            entries
         });
 
     const infoObj = await state.updateTree(infoTree).runInContext(taskCtx);
@@ -78,26 +99,43 @@ export const InitVolumeStreaming = StateAction.build({
 export const BoxifyVolumeStreaming = StateAction.build({
     display: { name: 'Boxify Volume Streaming', description: 'Make the current box permanent.' },
     from: VolumeStreaming,
-    isApplicable: (a) => a.data.params.view.name === 'selection-box'
+    isApplicable: (a) => a.data.params.entry.params.view.name === 'selection-box'
 })(({ a, ref, state }, plugin: PluginContext) => {
     const params = a.data.params;
-    if (params.view.name !== 'selection-box') return;
-    const box = Box3D.create(Vec3.clone(params.view.params.bottomLeft), Vec3.clone(params.view.params.topRight));
-    const r = params.view.params.radius;
+    if (params.entry.params.view.name !== 'selection-box') return;
+    const box = Box3D.create(Vec3.clone(params.entry.params.view.params.bottomLeft), Vec3.clone(params.entry.params.view.params.topRight));
+    const r = params.entry.params.view.params.radius;
     Box3D.expand(box, box, Vec3.create(r, r, r));
     const newParams: VolumeStreaming.Params = {
         ...params,
-        view: {
-            name: 'box' as 'box',
+        entry: {
+            name: params.entry.name,
             params: {
-                bottomLeft: box.min,
-                topRight: box.max
+                ...params.entry.params,
+                view: {
+                    name: 'box' as 'box',
+                    params: {
+                        bottomLeft: box.min,
+                        topRight: box.max
+                    }
+                }
             }
         }
     };
     return state.updateTree(state.build().to(ref).update(newParams));
 });
 
+const InfoEntryParams = {
+    dataId: PD.Text(''),
+    source: PD.MappedStatic('x-ray', {
+        'em': PD.Group({
+            isoValue: createIsoValueParam(VolumeIsoValue.relative(1))
+        }),
+        'x-ray': PD.Group({ })
+    })
+}
+type InfoEntryProps = PD.Values<typeof InfoEntryParams>
+
 export { CreateVolumeStreamingInfo }
 type CreateVolumeStreamingInfo = typeof CreateVolumeStreamingInfo
 const CreateVolumeStreamingInfo = PluginStateTransform.BuiltIn({
@@ -108,30 +146,34 @@ const CreateVolumeStreamingInfo = PluginStateTransform.BuiltIn({
     params(a) {
         return {
             serverUrl: PD.Text('https://ds.litemol.org'),
-            source: PD.MappedStatic('x-ray', {
-                'em': PD.Group({
-                    isoValue: createIsoValueParam(VolumeIsoValue.relative(1))
-                }),
-                'x-ray': PD.Group({ })
+            entries: PD.ObjectList<InfoEntryProps>(InfoEntryParams, ({ dataId }) => dataId, {
+                defaultValue: [{ dataId: '', source: { name: 'x-ray', params: {} } }]
             }),
-            dataId: PD.Text('')
         };
     }
 })({
     apply: ({ a, params }, plugin: PluginContext) => Task.create('', async taskCtx => {
-        const dataId = params.dataId;
-        const emDefaultContourLevel = params.source.name === 'em' ? params.source.params.isoValue : VolumeIsoValue.relative(1);
-        await taskCtx.update('Getting server header...');
-        const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${params.source.name}/${dataId.toLocaleLowerCase()}`), type: 'json' }).runInContext(taskCtx);
+        const entries: VolumeServerInfo.EntryData[] = []
+        for (let i = 0, il = params.entries.length; i < il; ++i) {
+            const e = params.entries[i]
+            const dataId = e.dataId;
+            const emDefaultContourLevel = e.source.name === 'em' ? e.source.params.isoValue : VolumeIsoValue.relative(1);
+            await taskCtx.update('Getting server header...');
+            const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${e.source.name}/${dataId.toLocaleLowerCase()}`), type: 'json' }).runInContext(taskCtx);
+            entries.push({
+                dataId,
+                kind: e.source.name,
+                header,
+                emDefaultContourLevel
+            })
+        }
+
         const data: VolumeServerInfo.Data = {
             serverUrl: params.serverUrl,
-            dataId,
-            kind: params.source.name,
-            header,
-            emDefaultContourLevel,
+            entries,
             structure: a.data
         };
-        return new VolumeServerInfo(data, { label: `Volume Server: ${dataId}` });
+        return new VolumeServerInfo(data, { label: 'Volume Server', description: `${entries.map(e => e.dataId). join(', ')}` });
     })
 });
 
@@ -147,17 +189,25 @@ const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({
     }
 })({
     canAutoUpdate: ({ oldParams, newParams }) => {
-        return oldParams.view === newParams.view
-            || newParams.view.name === 'selection-box'
-            || newParams.view.name === 'off';
+        return oldParams.entry.params.view === newParams.entry.params.view
+            || newParams.entry.params.view.name === 'selection-box'
+            || newParams.entry.params.view.name === 'off';
     },
     apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {
         const behavior = new VolumeStreaming.Behavior(plugin, a.data);
         await behavior.update(params);
         return new VolumeStreaming(behavior, { label: 'Volume Streaming', description: behavior.getDescription() });
     }),
-    update({ b, newParams }) {
+    update({ a, b, oldParams, newParams }) {
         return Task.create('Update Volume Streaming', async _ => {
+            if (oldParams.entry.name !== newParams.entry.name) {
+                if ('em' in newParams.entry.params.channels) {
+                    const { emDefaultContourLevel } = b.data.infoMap.get(newParams.entry.name)!
+                    if (emDefaultContourLevel) {
+                        newParams.entry.params.channels['em'].isoValue = emDefaultContourLevel
+                    }
+                }
+            }
             const ret = await b.data.update(newParams) ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
             b.description = b.data.getDescription();
             return ret;

+ 29 - 15
src/mol-plugin/behavior/dynamic/volume-streaming/util.ts

@@ -5,7 +5,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Structure } from '../../../../mol-model/structure';
+import { Structure, Model } from '../../../../mol-model/structure';
 import { VolumeServerInfo } from './model';
 import { PluginContext } from '../../../../mol-plugin/context';
 import { RuntimeContext } from '../../../../mol-task';
@@ -25,20 +25,34 @@ export function getStreamingMethod(s?: Structure, defaultKind: VolumeServerInfo.
     return 'x-ray';
 }
 
-export function getId(s?: Structure): string {
-    if (!s) return ''
+/** Returns EMD ID when available, otherwise falls back to PDB ID */
+export function getEmIds(model: Model): string[] {
+    const ids: string[] = []
+    if (model.sourceData.kind !== 'mmCIF') return [ model.entryId ]
 
-    const model = s.models[0]
-    if (model.sourceData.kind !== 'mmCIF') return ''
+    const { db_id, db_name, content_type } = model.sourceData.data.pdbx_database_related
+    if (!db_name.isDefined) return [ model.entryId ]
 
-    const d = model.sourceData.data
-    for (let i = 0, il = d.pdbx_database_related._rowCount; i < il; ++i) {
-        if (d.pdbx_database_related.db_name.value(i).toUpperCase() === 'EMDB') {
-            return d.pdbx_database_related.db_id.value(i)
+    for (let i = 0, il = db_name.rowCount; i < il; ++i) {
+        if (db_name.value(i).toUpperCase() === 'EMDB' && content_type.value(i) === 'associated EM volume') {
+            ids.push(db_id.value(i))
         }
     }
 
-    return s.models.length > 0 ? s.models[0].entryId : ''
+    return ids
+}
+
+export function getXrayIds(model: Model): string[] {
+    return [ model.entryId ]
+}
+
+export function getIds(method: VolumeServerInfo.Kind, s?: Structure): string[] {
+    if (!s || !s.models.length) return []
+    const model = s.models[0]
+    switch (method) {
+        case 'em': return getEmIds(model)
+        case 'x-ray': return getXrayIds(model)
+    }
 }
 
 export async function getContourLevel(provider: 'wwpdb' | 'pdbe', plugin: PluginContext, taskCtx: RuntimeContext, emdbId: string) {
@@ -71,21 +85,21 @@ export async function getContourLevelPdbe(plugin: PluginContext, taskCtx: Runtim
     return contourLevel;
 }
 
-export async function getEmdbId(plugin: PluginContext, taskCtx: RuntimeContext, pdbId: string) {
+export async function getEmdbIds(plugin: PluginContext, taskCtx: RuntimeContext, pdbId: string) {
     // TODO: parametrize to a differnt URL? in plugin settings perhaps
     const summary = await plugin.fetch({ url: `https://www.ebi.ac.uk/pdbe/api/pdb/entry/summary/${pdbId}`, type: 'json' }).runInContext(taskCtx);
 
     const summaryEntry = summary && summary[pdbId];
-    let emdbId: string;
+    let emdbIds: string[] = [];
     if (summaryEntry && summaryEntry[0] && summaryEntry[0].related_structures) {
-        const emdb = summaryEntry[0].related_structures.filter((s: any) => s.resource === 'EMDB');
+        const emdb = summaryEntry[0].related_structures.filter((s: any) => s.resource === 'EMDB' && s.relationship === 'associated EM volume');
         if (!emdb.length) {
             throw new Error(`No related EMDB entry found for '${pdbId}'.`);
         }
-        emdbId = emdb[0].accession;
+        emdbIds.push(...emdb.map((e: { accession: string }) => e.accession));
     } else {
         throw new Error(`No related EMDB entry found for '${pdbId}'.`);
     }
 
-    return emdbId
+    return emdbIds
 }

+ 94 - 57
src/mol-plugin/ui/custom/volume.tsx

@@ -61,14 +61,20 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
     }
 
     changeIso = (name: string, value: number, isRelative: boolean) => {
-        const old = this.props.params;
+        const old = this.props.params as VolumeStreaming.Params
         this.newParams({
             ...old,
-            channels: {
-                ...old.channels,
-                [name]: {
-                    ...old.channels[name],
-                    isoValue: isRelative ? VolumeIsoValue.relative(value) : VolumeIsoValue.absolute(value)
+            entry: {
+                name: old.entry.name,
+                params: {
+                    ...old.entry.params,
+                    channels: {
+                        ...old.entry.params.channels,
+                        [name]: {
+                            ...(old.entry.params.channels as any)[name],
+                            isoValue: isRelative ? VolumeIsoValue.relative(value) : VolumeIsoValue.absolute(value)
+                        }
+                    }
                 }
             }
         });
@@ -78,11 +84,17 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
         const old = this.props.params;
         this.newParams({
             ...old,
-            channels: {
-                ...old.channels,
-                [name]: {
-                    ...old.channels[name],
-                    [param]: value
+            entry: {
+                name: old.entry.name,
+                params: {
+                    ...old.entry.params,
+                    channels: {
+                        ...old.entry.params.channels,
+                        [name]: {
+                            ...(old.entry.params.channels as any)[name],
+                            [param]: value
+                        }
+                    }
                 }
             }
         });
@@ -94,41 +106,62 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
             : VolumeIsoValue.toAbsolute(channel.isoValue, stats) }
     }
 
-    changeOption: ParamOnChange = ({ value }) => {
-        const b = (this.props.b as VolumeStreaming).data;
-        const isEM = b.info.kind === 'em';
+    changeOption: ParamOnChange = ({ name, value }) => {
+        const old = this.props.params as VolumeStreaming.Params
 
-        const isRelative = value.params.isRelative;
-        const sampling = b.info.header.sampling[0];
-        const old = this.props.params as VolumeStreaming.Params, oldChannels = old.channels as any;
-
-        const oldView = old.view.name === value.name
-            ? old.view.params
-            : (this.props.info.params as VolumeStreaming.ParamDefinition).view.map(value.name).defaultValue;
-
-        const viewParams = { ...oldView };
-        if (value.name === 'selection-box') {
-            viewParams.radius = value.params.radius;
-        } else if (value.name === 'box') {
-            viewParams.bottomLeft = value.params.bottomLeft;
-            viewParams.topRight = value.params.topRight;
-        }
+        if (name === 'entry') {
+            this.newParams({
+                ...old,
+                entry: {
+                    name: value,
+                    params: old.entry.params,
+                }
+            });
+        } else {
+            const b = (this.props.b as VolumeStreaming).data;
+            const isEM = b.info.kind === 'em';
+
+            const isRelative = value.params.isRelative;
+            const sampling = b.info.header.sampling[0];
+            const oldChannels = old.entry.params.channels as any;
+
+            const oldView = old.entry.params.view.name === value.name
+                ? old.entry.params.view.params
+                : (((this.props.info.params as VolumeStreaming.ParamDefinition)
+                    .entry.map(old.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>)
+                        .params as VolumeStreaming.EntryParamDefinition)
+                            .view.map(value.name).defaultValue;
+
+            const viewParams = { ...oldView };
+            if (value.name === 'selection-box') {
+                viewParams.radius = value.params.radius;
+            } else if (value.name === 'box') {
+                viewParams.bottomLeft = value.params.bottomLeft;
+                viewParams.topRight = value.params.topRight;
+            }
 
-        this.newParams({
-            ...old,
-            view: {
-                name: value.name,
-                params: viewParams
-            },
-            detailLevel: value.params.detailLevel,
-            channels: isEM
-                ? { em: this.convert(oldChannels.em, sampling.valuesInfo[0], isRelative) }
-                : {
-                    '2fo-fc': this.convert(oldChannels['2fo-fc'], sampling.valuesInfo[0], isRelative),
-                    'fo-fc(+ve)': this.convert(oldChannels['fo-fc(+ve)'], sampling.valuesInfo[1], isRelative),
-                    'fo-fc(-ve)': this.convert(oldChannels['fo-fc(-ve)'], sampling.valuesInfo[1], isRelative)
+            this.newParams({
+                ...old,
+                entry: {
+                    name: old.entry.name,
+                    params: {
+                        ...old.entry.params,
+                        view: {
+                            name: value.name,
+                            params: viewParams
+                        },
+                        detailLevel: value.params.detailLevel,
+                        channels: isEM
+                            ? { em: this.convert(oldChannels.em, sampling.valuesInfo[0], isRelative) }
+                            : {
+                                '2fo-fc': this.convert(oldChannels['2fo-fc'], sampling.valuesInfo[0], isRelative),
+                                'fo-fc(+ve)': this.convert(oldChannels['fo-fc(+ve)'], sampling.valuesInfo[1], isRelative),
+                                'fo-fc(-ve)': this.convert(oldChannels['fo-fc(-ve)'], sampling.valuesInfo[1], isRelative)
+                            }
+                    }
                 }
-        });
+            });
+        }
     };
 
     render() {
@@ -139,50 +172,54 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
         const pivot = isEM ? 'em' : '2fo-fc';
 
         const params = this.props.params as VolumeStreaming.Params;
-        const isRelative = ((params.channels as any)[pivot].isoValue as VolumeIsoValue).kind === 'relative';
+        const detailLevel = ((this.props.info.params as VolumeStreaming.ParamDefinition)
+            .entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>).params.detailLevel
+        const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as VolumeIsoValue).kind === 'relative';
 
         const sampling = b.info.header.sampling[0];
 
         // TODO: factor common things out
         const OptionsParams = {
-            view: PD.MappedStatic(params.view.name, {
+            entry: PD.Select(params.entry.name, b.data.entries.map(info => [info.dataId, info.dataId] as [string, string])),
+            view: PD.MappedStatic(params.entry.params.view.name, {
                 'off': PD.Group({}, { description: 'Display off.' }),
                 'box': PD.Group({
                     bottomLeft: PD.Vec3(Vec3.zero()),
                     topRight: PD.Vec3(Vec3.zero()),
-                    detailLevel: this.props.info.params.detailLevel,
+                    detailLevel,
                     isRelative: PD.Boolean(isRelative, { description: 'Use relative or absolute iso values.' })
                 }, { description: 'Static box defined by cartesian coords.' }),
                 'selection-box': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }),
-                    detailLevel: this.props.info.params.detailLevel,
+                    detailLevel,
                     isRelative: PD.Boolean(isRelative, { description: 'Use relative or absolute iso values.' })
                 }, { description: 'Box around last-interacted element.' }),
                 'cell': PD.Group({
-                    detailLevel: this.props.info.params.detailLevel,
+                    detailLevel,
                     isRelative: PD.Boolean(isRelative, { description: 'Use relative or absolute iso values.' })
                 }, { description: 'Box around the structure\'s bounding box.' }),
                 // 'auto': PD.Group({  }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
             }, { options: [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Surroundings'], ['cell', 'Whole Structure']] })
         };
         const options = {
+            entry: params.entry.name,
             view: {
-                name: params.view.name,
+                name: params.entry.params.view.name,
                 params: {
-                    detailLevel: params.detailLevel,
-                    radius: (params.view.params as any).radius,
-                    bottomLeft: (params.view.params as any).bottomLeft,
-                    topRight: (params.view.params as any).topRight,
+                    detailLevel: params.entry.params.detailLevel,
+                    radius: (params.entry.params.view.params as any).radius,
+                    bottomLeft: (params.entry.params.view.params as any).bottomLeft,
+                    topRight: (params.entry.params.view.params as any).topRight,
                     isRelative
                 }
             }
         };
 
         return <>
-            {!isEM && <Channel label='2Fo-Fc' name='2fo-fc' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
-            {!isEM && <Channel label='Fo-Fc(+ve)' name='fo-fc(+ve)' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
-            {!isEM && <Channel label='Fo-Fc(-ve)' name='fo-fc(-ve)' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
-            {isEM && <Channel label='EM' name='em' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
+            {!isEM && <Channel label='2Fo-Fc' name='2fo-fc' channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
+            {!isEM && <Channel label='Fo-Fc(+ve)' name='fo-fc(+ve)' channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
+            {!isEM && <Channel label='Fo-Fc(-ve)' name='fo-fc(-ve)' channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
+            {isEM && <Channel label='EM' name='em' channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
 
             <ParameterControls onChange={this.changeOption} params={OptionsParams} values={options} onEnter={this.props.events.onEnter} />
         </>