/** * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal */ import { PluginBehavior } from 'mol-plugin/behavior'; import { PluginStateObject } from 'mol-plugin/state/objects'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { VolumeServerInfo, VolumeServerHeader } from './model'; import { createIsoValueParam } from 'mol-repr/volume/isosurface'; import { VolumeIsoValue, VolumeData } from 'mol-model/volume'; import { Color } from 'mol-util/color'; import { Vec3 } from 'mol-math/linear-algebra'; import { PluginContext } from 'mol-plugin/context'; import { LRUCache } from 'mol-util/lru-cache'; import CIF from 'mol-io/reader/cif'; import { Box3D } from 'mol-math/geometry'; import { urlCombine } from 'mol-util/url'; import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server'; import { StructureElement } from 'mol-model/structure'; import { Loci } from 'mol-model/loci'; import { CreateVolumeStreamingBehavior } from './transformers'; export class VolumeStreaming extends PluginStateObject.CreateBehavior({ name: 'Volume Streaming' }) { } export namespace VolumeStreaming { function channelParam(label: string, color: Color, defaultValue: VolumeIsoValue, stats: VolumeData['dataStats']) { return PD.Group({ isoValue: createIsoValueParam(defaultValue, stats), color: PD.Color(color), opacity: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 }) }, { label, isExpanded: true }); } const fakeSampling: VolumeServerHeader.Sampling = { byteOffset: 0, rate: 1, sampleCount: [1, 1, 1], valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }] }; export function createParams(data?: VolumeServerInfo.Data) { // fake the info const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) }; return { view: PD.MappedStatic('selection-box', { 'box': PD.Group({ bottomLeft: PD.Vec3(Vec3.create(-22.4, -33.4, -21.6)), topRight: PD.Vec3(Vec3.create(-7.1, -10, -0.9)), }, { description: 'Static box defined by cartesian coords.', isFlat: true }), 'selection-box': PD.Group({ radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }), bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }), topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }), }, { description: 'Box around last-interacted element.', isFlat: true }), 'cell': PD.Group({}), // 'auto': PD.Group({ }), // based on camera distance/active selection/whatever, show whole structure or slice. }, { options: [['box', 'Bounded Box'], ['selection-box', 'Selection'], ['cell', 'Whole Structure']] }), detailLevel: PD.Select(Math.min(1, info.header.availablePrecisions.length - 1), info.header.availablePrecisions.map((p, i) => [i, `${i + 1} (${Math.pow(p.maxVoxels, 1 / 3) | 0}^3)`] as [number, string])), channels: info.kind === 'em' ? PD.Group({ 'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || VolumeIsoValue.relative(1), info.header.sampling[0].valuesInfo[0]) }, { isFlat: true }) : PD.Group({ '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), VolumeIsoValue.relative(1.5), info.header.sampling[0].valuesInfo[0]), '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 }) }; } type RT = typeof createParams extends (...args: any[]) => (infer T) ? T : never export type Params = RT extends PD.Params ? PD.Values : {} type ChannelsInfo = { [name in ChannelType]?: { isoValue: VolumeIsoValue, color: Color, opacity: number } } type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData } export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)' export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']] interface ChannelInfo { data: VolumeData, color: Color, isoValue: VolumeIsoValue.Relative, opacity: number } export type Channels = { [name in ChannelType]?: ChannelInfo } export class Behavior extends PluginBehavior.WithSubscribers { private cache = LRUCache.create(25); private params: Params = {} as any; // private ref: string = ''; channels: Channels = {} private async queryData(box?: Box3D) { let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId}`); 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.plugin.runTask(this.plugin.fetch({ url, type: '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 { const parsed = await this.plugin.runTask(CIF.parseBinary(data)); if (parsed.isError) { this.plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString()); return; } if (parsed.result.blocks.length < 2) { this.plugin.log.error('VolumeStreaming: Invalid data.'); return; } const ret: ChannelsData = {}; for (let i = 1; i < parsed.result.blocks.length; i++) { const block = parsed.result.blocks[i]; const densityServerCif = CIF.schema.densityServer(block); const volume = await this.plugin.runTask(await volumeFromDensityServerData(densityServerCif)); (ret as any)[block.header as any] = volume; } return ret; } register(ref: string): void { // this.ref = ref; this.subscribeObservable(this.plugin.events.canvas3d.click, ({ current }) => { if (this.params.view.name !== 'selection-box') return; if (!StructureElement.isLoci(current.loci)) return; // TODO: check if it's the related structure const eR = this.params.view.params.radius; const sphere = Loci.getBoundingSphere(current.loci)!; const r = Vec3.create(sphere.radius + eR, sphere.radius + eR, sphere.radius + eR); const box = Box3D.create(Vec3.sub(Vec3.zero(), sphere.center, r), Vec3.add(Vec3.zero(), sphere.center, r)); const update = this.plugin.state.dataState.build().to(ref) .update(CreateVolumeStreamingBehavior, old => ({ ...old, view: { name: 'selection-box' as 'selection-box', params: { radius: eR, bottomLeft: box.min, topRight: box.max } } })); this.plugin.runTask(this.plugin.state.dataState.updateTree(update)); }); } async update(params: Params) { this.params = params; let box: Box3D | undefined = void 0, emptyData = false; switch (params.view.name) { case 'box': box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight); break; case 'selection-box': box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight); emptyData = Box3D.volume(box) < 0.0001; break; case 'cell': box = this.info.kind === 'x-ray' ? this.info.structure.boundary.box : void 0; break; } const data = emptyData ? { } : await this.queryData(box); if (!data) return false; const info = 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]); this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]); this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]); } else { this.channels['em'] = this.createChannel(data['EM'] || VolumeData.One, info['em'], this.info.header.sampling[0].valuesInfo[0]); } return true; } private createChannel(data: VolumeData, info: ChannelsInfo['em'], stats: VolumeData['dataStats']): ChannelInfo { const i = info!; return { data, color: i.color, opacity: i.opacity, isoValue: i.isoValue.kind === 'relative' ? i.isoValue : VolumeIsoValue.toRelative(i.isoValue, stats) }; } constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) { super(plugin); } } }