UNPKG

molstar

Version:

A comprehensive macromolecular library.

282 lines (281 loc) 14.5 kB
/** * Copyright (c) 2019-2022 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> * @author Adam Midlik <midlik@gmail.com> */ import { PluginStateObject as SO, PluginStateTransform } from '../../../../mol-plugin-state/objects'; import { VolumeServerInfo } from './model'; import { ParamDefinition as PD } from '../../../../mol-util/param-definition'; import { Task } from '../../../../mol-task'; import { urlCombine } from '../../../../mol-util/url'; import { Volume } from '../../../../mol-model/volume'; import { StateAction, StateObject, StateTransformer } from '../../../../mol-state'; import { getStreamingMethod, getIds, getContourLevel, getEmdbIds } from './util'; import { VolumeStreaming } from './behavior'; import { VolumeRepresentation3DHelpers } from '../../../../mol-plugin-state/transforms/representation'; import { VolumeRepresentationRegistry } from '../../../../mol-repr/volume/registry'; import { Theme } from '../../../../mol-theme/theme'; import { Box3D } from '../../../../mol-math/geometry'; import { Vec3 } from '../../../../mol-math/linear-algebra'; import { PluginConfig } from '../../../config'; import { Model } from '../../../../mol-model/structure'; import { GlobalModelTransformInfo } from '../../../../mol-model/structure/model/properties/global-transform'; function addEntry(entries, method, dataId, emDefaultContourLevel) { entries.push({ source: method === 'em' ? { name: 'em', params: { isoValue: Volume.IsoValue.absolute(emDefaultContourLevel || 0) } } : { name: 'x-ray', params: {} }, dataId }); } export const InitVolumeStreaming = StateAction.build({ display: { name: 'Volume Streaming' }, from: SO.Molecule.Structure, params(a, plugin) { const method = getStreamingMethod(a && a.data); const ids = getIds(method, a && a.data); return { method: PD.Select(method, [['em', 'EM'], ['x-ray', 'X-Ray']]), entries: PD.ObjectList({ id: PD.Text(ids[0] || '') }, ({ id }) => id, { defaultValue: ids.map(id => ({ id })) }), defaultView: PD.Select(method === 'em' ? 'auto' : 'selection-box', VolumeStreaming.ViewTypeOptions), options: PD.Group({ serverUrl: PD.Text(plugin.config.get(PluginConfig.VolumeStreaming.DefaultServer) || 'https://ds.litemol.org'), behaviorRef: PD.Text('', { isHidden: true }), emContourProvider: PD.Select('emdb', [['emdb', 'EMDB'], ['pdbe', 'PDBe']], { isHidden: true }), channelParams: PD.Value({}, { isHidden: true }) }) }; }, isApplicable: (a, _, plugin) => { const canStreamTest = plugin.config.get(PluginConfig.VolumeStreaming.CanStream); if (canStreamTest) return canStreamTest(a.data, plugin); return a.data.models.length === 1 && Model.probablyHasDensityMap(a.data.models[0]); } })(({ ref, state, params }, plugin) => Task.create('Volume Streaming', async (taskCtx) => { const entries = []; for (let i = 0, il = params.entries.length; i < il; ++i) { const dataId = params.entries[i].id.toLowerCase(); let emDefaultContourLevel; 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]; let contourLevel; try { contourLevel = await getContourLevel(params.options.emContourProvider, plugin, taskCtx, emdbId); } catch (e) { console.info(`Could not get map info for ${emdbId}: ${e}`); continue; } addEntry(entries, params.method, emdbId, contourLevel || 0); } continue; } try { emDefaultContourLevel = await getContourLevel(params.options.emContourProvider, plugin, taskCtx, dataId); } catch (e) { console.info(`Could not get map info for ${dataId}: ${e}`); continue; } } addEntry(entries, params.method, dataId, emDefaultContourLevel || 0); } const infoTree = state.build().to(ref) .applyOrUpdateTagged(VolumeStreaming.RootTag, CreateVolumeStreamingInfo, { serverUrl: params.options.serverUrl, entries }); await infoTree.commit(); const info = infoTree.selector; if (!info.isOk) return; // clear the children in case there were errors const children = state.tree.children.get(info.ref); if ((children === null || children === void 0 ? void 0 : children.size) > 0) await plugin.managers.structure.hierarchy.remove(children === null || children === void 0 ? void 0 : children.toArray()); const infoObj = info.cell.obj; const behTree = state.build().to(infoTree.ref).apply(CreateVolumeStreamingBehavior, PD.getDefaultValues(VolumeStreaming.createParams({ data: infoObj.data, defaultView: params.defaultView, channelParams: params.options.channelParams })), { ref: params.options.behaviorRef ? params.options.behaviorRef : void 0 }); if (params.method === 'em') { behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true }, tags: 'em' }); } else { behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true }, tags: '2fo-fc' }); behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true }, tags: 'fo-fc(+ve)' }); behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true }, tags: 'fo-fc(-ve)' }); } await state.updateTree(behTree).runInContext(taskCtx); })); export const BoxifyVolumeStreaming = StateAction.build({ display: { name: 'Boxify Volume Streaming', description: 'Make the current box permanent.' }, from: VolumeStreaming, isApplicable: (a) => a.data.params.entry.params.view.name === 'selection-box' })(({ a, ref, state }, plugin) => { const params = a.data.params; 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 = { ...params, entry: { name: params.entry.name, params: { ...params.entry.params, view: { name: '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: Volume.createIsoValueParam(Volume.IsoValue.relative(1)) }), 'x-ray': PD.Group({}) }) }; export { CreateVolumeStreamingInfo }; const CreateVolumeStreamingInfo = PluginStateTransform.BuiltIn({ name: 'create-volume-streaming-info', display: { name: 'Volume Streaming Info' }, from: SO.Molecule.Structure, to: VolumeServerInfo, params(a) { return { serverUrl: PD.Text('https://ds.litemol.org'), entries: PD.ObjectList(InfoEntryParams, ({ dataId }) => dataId, { defaultValue: [{ dataId: '', source: { name: 'x-ray', params: {} } }] }), }; } })({ apply: ({ a, params }, plugin) => Task.create('', async (taskCtx) => { const entries = []; 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 : Volume.IsoValue.relative(1); await taskCtx.update('Getting server header...'); const header = await plugin.fetch({ url: urlCombine(params.serverUrl, `${e.source.name}/${dataId.toLocaleLowerCase()}`), type: 'json' }).runInContext(taskCtx); entries.push({ dataId, kind: e.source.name, header, emDefaultContourLevel }); } const data = { serverUrl: params.serverUrl, entries, structure: a.data }; return new VolumeServerInfo(data, { label: 'Volume Server', description: `${entries.map(e => e.dataId).join(', ')}` }); }) }); export { CreateVolumeStreamingBehavior }; const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({ name: 'create-volume-streaming-behavior', display: { name: 'Volume Streaming Behavior' }, from: VolumeServerInfo, to: VolumeStreaming, params(a) { return VolumeStreaming.createParams({ data: a && a.data }); } })({ canAutoUpdate: ({ oldParams, newParams }) => { return oldParams.entry.params.view === newParams.entry.params.view || newParams.entry.params.view.name === 'selection-box' || newParams.entry.params.view.name === 'camera-target' || newParams.entry.params.view.name === 'off'; }, apply: ({ a, params }, plugin) => 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({ 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; }); } }); export { VolumeStreamingVisual }; const VolumeStreamingVisual = PluginStateTransform.BuiltIn({ name: 'create-volume-streaming-visual', display: { name: 'Volume Streaming Visual' }, from: VolumeStreaming, to: SO.Volume.Representation3D, params: { channel: PD.Select('em', VolumeStreaming.ChannelTypeOptions, { isHidden: true }) } })({ apply: ({ a, params: srcParams, spine }, plugin) => Task.create('Volume Representation', async (ctx) => { var _a, _b; const channel = a.data.channels[srcParams.channel]; if (!channel) return StateObject.Null; const params = createVolumeProps(a.data, srcParams.channel); const provider = VolumeRepresentationRegistry.BuiltIn.isosurface; const props = params.type.params || {}; const repr = provider.factory({ webgl: (_a = plugin.canvas3d) === null || _a === void 0 ? void 0 : _a.webgl, ...plugin.representation.volume.themes }, provider.getParams); repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: channel.data }, params)); const structure = (_b = spine.getAncestorOfType(SO.Molecule.Structure)) === null || _b === void 0 ? void 0 : _b.data; const transform = (structure === null || structure === void 0 ? void 0 : structure.models.length) === 0 ? void 0 : GlobalModelTransformInfo.get(structure === null || structure === void 0 ? void 0 : structure.models[0]); await repr.createOrUpdate(props, channel.data).runInContext(ctx); if (transform) repr.setState({ transform }); return new SO.Volume.Representation3D({ repr, sourceData: channel.data }, { label: `${Math.round(channel.isoValue.relativeValue * 100) / 100} σ [${srcParams.channel}]` }); }), update: ({ a, b, newParams, spine }, plugin) => Task.create('Volume Representation', async (ctx) => { // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work const channel = a.data.channels[newParams.channel]; // TODO: is this correct behavior? if (!channel) return StateTransformer.UpdateResult.Unchanged; const visible = b.data.repr.state.visible; const params = createVolumeProps(a.data, newParams.channel); const props = { ...b.data.repr.props, ...params.type.params }; b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: channel.data }, params)); await b.data.repr.createOrUpdate(props, channel.data).runInContext(ctx); b.data.repr.setState({ visible }); b.data.sourceData = channel.data; // TODO: set the transform here as well in case the structure moves? // doing this here now breaks the code for some reason... // const structure = spine.getAncestorOfType(SO.Molecule.Structure)?.data; // const transform = structure?.models.length === 0 ? void 0 : GlobalModelTransformInfo.get(structure?.models[0]!); // if (transform) b.data.repr.setState({ transform }); return StateTransformer.UpdateResult.Updated; }) }); function createVolumeProps(streaming, channelName) { const channel = streaming.channels[channelName]; return VolumeRepresentation3DHelpers.getDefaultParamsStatic(streaming.plugin, 'isosurface', { isoValue: channel.isoValue, alpha: channel.opacity, visuals: channel.wireframe ? ['wireframe'] : ['solid'] }, 'uniform', { value: channel.color }); }