diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 11282871cf9674cafdbd026e3553e3f972fd3f7c..59385d880d2d13189889c9f268c02cb0bb079b8b 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/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)) } } } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts index 213f0b1a9911dd4c57818d31113eb9ec9b4180ce..71a0fac83b6405862e96bfa91e1905e7ed925a06 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts +++ b/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 } } diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts index 0dd7284e7599ece28cd25786efda08053e1d6cce..505956c1969d371aaadce8cbcebea2ab85530ead 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts +++ b/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; diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts index 2e1c5aa4963a47ff49e16f360b34809e802bb144..9ad445c750b1b194f9acfba30b70a3627f587d5a 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts +++ b/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 } \ No newline at end of file diff --git a/src/mol-plugin/ui/custom/volume.tsx b/src/mol-plugin/ui/custom/volume.tsx index 2273f16dedc980fc5e62db57fce45b8a8a5997aa..e4292185e7753c9b33a88c3851de28aae362b91c 100644 --- a/src/mol-plugin/ui/custom/volume.tsx +++ b/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} /> </>