behavior.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { PluginBehavior } from 'mol-plugin/behavior';
  7. import { PluginStateObject } from 'mol-plugin/state/objects';
  8. import { ParamDefinition as PD } from 'mol-util/param-definition';
  9. import { VolumeServerInfo, VolumeServerHeader } from './model';
  10. import { createIsoValueParam } from 'mol-repr/volume/isosurface';
  11. import { VolumeIsoValue, VolumeData } from 'mol-model/volume';
  12. import { Color } from 'mol-util/color';
  13. import { Vec3 } from 'mol-math/linear-algebra';
  14. import { PluginContext } from 'mol-plugin/context';
  15. import { LRUCache } from 'mol-util/lru-cache';
  16. import CIF from 'mol-io/reader/cif';
  17. import { Box3D } from 'mol-math/geometry';
  18. import { urlCombine } from 'mol-util/url';
  19. import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
  20. import { StructureElement } from 'mol-model/structure';
  21. import { Loci } from 'mol-model/loci';
  22. import { CreateVolumeStreamingBehavior } from './transformers';
  23. export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
  24. export namespace VolumeStreaming {
  25. function channelParam(label: string, color: Color, defaultValue: VolumeIsoValue, stats: VolumeData['dataStats']) {
  26. return PD.Group({
  27. isoValue: createIsoValueParam(defaultValue, stats),
  28. color: PD.Color(color),
  29. opacity: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 })
  30. }, { label, isExpanded: true });
  31. }
  32. const fakeSampling: VolumeServerHeader.Sampling = {
  33. byteOffset: 0,
  34. rate: 1,
  35. sampleCount: [1, 1, 1],
  36. valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
  37. };
  38. export function createParams(data?: VolumeServerInfo.Data) {
  39. // fake the info
  40. const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) };
  41. return {
  42. view: PD.MappedStatic('selection-box', {
  43. 'box': PD.Group({
  44. bottomLeft: PD.Vec3(Vec3.create(-22.4, -33.4, -21.6)),
  45. topRight: PD.Vec3(Vec3.create(-7.1, -10, -0.9)),
  46. }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
  47. 'selection-box': PD.Group({
  48. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }),
  49. bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
  50. topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
  51. }, { description: 'Box around last-interacted element.', isFlat: true }),
  52. 'cell': PD.Group({}),
  53. // 'auto': PD.Group({ }), // based on camera distance/active selection/whatever, show whole structure or slice.
  54. }, { options: [['box', 'Bounded Box'], ['selection-box', 'Selection'], ['cell', 'Whole Structure']] }),
  55. detailLevel: PD.Select<number>(Math.min(1, info.header.availablePrecisions.length - 1),
  56. info.header.availablePrecisions.map((p, i) => [i, `${i + 1} (${Math.pow(p.maxVoxels, 1 / 3) | 0}^3)`] as [number, string])),
  57. channels: info.kind === 'em'
  58. ? PD.Group({
  59. 'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || VolumeIsoValue.relative(1), info.header.sampling[0].valuesInfo[0])
  60. }, { isFlat: true })
  61. : PD.Group({
  62. '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), VolumeIsoValue.relative(1.5), info.header.sampling[0].valuesInfo[0]),
  63. 'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), VolumeIsoValue.relative(3), info.header.sampling[0].valuesInfo[1]),
  64. 'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), VolumeIsoValue.relative(-3), info.header.sampling[0].valuesInfo[1]),
  65. }, { isFlat: true })
  66. };
  67. }
  68. type RT = typeof createParams extends (...args: any[]) => (infer T) ? T : never
  69. export type Params = RT extends PD.Params ? PD.Values<RT> : {}
  70. type ChannelsInfo = { [name in ChannelType]?: { isoValue: VolumeIsoValue, color: Color, opacity: number } }
  71. type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
  72. export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
  73. export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']]
  74. interface ChannelInfo {
  75. data: VolumeData,
  76. color: Color,
  77. isoValue: VolumeIsoValue.Relative,
  78. opacity: number
  79. }
  80. export type Channels = { [name in ChannelType]?: ChannelInfo }
  81. export class Behavior extends PluginBehavior.WithSubscribers<Params> {
  82. private cache = LRUCache.create<ChannelsData>(25);
  83. private params: Params = {} as any;
  84. // private ref: string = '';
  85. channels: Channels = {}
  86. private async queryData(box?: Box3D) {
  87. let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId}`);
  88. if (box) {
  89. const { min: a, max: b } = box;
  90. url += `/box`
  91. + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
  92. + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
  93. } else {
  94. url += `/cell`;
  95. }
  96. url += `?detail=${this.params.detailLevel}`;
  97. let data = LRUCache.get(this.cache, url);
  98. if (data) {
  99. return data;
  100. }
  101. const cif = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
  102. data = await this.parseCif(cif as Uint8Array);
  103. if (!data) {
  104. return;
  105. }
  106. LRUCache.set(this.cache, url, data);
  107. return data;
  108. }
  109. private async parseCif(data: Uint8Array): Promise<ChannelsData | undefined> {
  110. const parsed = await this.plugin.runTask(CIF.parseBinary(data));
  111. if (parsed.isError) {
  112. this.plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
  113. return;
  114. }
  115. if (parsed.result.blocks.length < 2) {
  116. this.plugin.log.error('VolumeStreaming: Invalid data.');
  117. return;
  118. }
  119. const ret: ChannelsData = {};
  120. for (let i = 1; i < parsed.result.blocks.length; i++) {
  121. const block = parsed.result.blocks[i];
  122. const densityServerCif = CIF.schema.densityServer(block);
  123. const volume = await this.plugin.runTask(await volumeFromDensityServerData(densityServerCif));
  124. (ret as any)[block.header as any] = volume;
  125. }
  126. return ret;
  127. }
  128. register(ref: string): void {
  129. // this.ref = ref;
  130. this.subscribeObservable(this.plugin.events.canvas3d.click, ({ current }) => {
  131. if (this.params.view.name !== 'selection-box') return;
  132. if (!StructureElement.isLoci(current.loci)) return;
  133. // TODO: check if it's the related structure
  134. const eR = this.params.view.params.radius;
  135. const sphere = Loci.getBoundingSphere(current.loci)!;
  136. const r = Vec3.create(sphere.radius + eR, sphere.radius + eR, sphere.radius + eR);
  137. const box = Box3D.create(Vec3.sub(Vec3.zero(), sphere.center, r), Vec3.add(Vec3.zero(), sphere.center, r));
  138. const update = this.plugin.state.dataState.build().to(ref)
  139. .update(CreateVolumeStreamingBehavior, old => ({
  140. ...old,
  141. view: {
  142. name: 'selection-box' as 'selection-box',
  143. params: {
  144. radius: eR,
  145. bottomLeft: box.min,
  146. topRight: box.max
  147. }
  148. }
  149. }));
  150. this.plugin.runTask(this.plugin.state.dataState.updateTree(update));
  151. });
  152. }
  153. async update(params: Params) {
  154. this.params = params;
  155. let box: Box3D | undefined = void 0, emptyData = false;
  156. switch (params.view.name) {
  157. case 'box':
  158. box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight);
  159. break;
  160. case 'selection-box':
  161. box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight);
  162. emptyData = Box3D.volume(box) < 0.0001;
  163. break;
  164. case 'cell':
  165. box = this.info.kind === 'x-ray'
  166. ? this.info.structure.boundary.box
  167. : void 0;
  168. break;
  169. }
  170. const data = emptyData ? { } : await this.queryData(box);
  171. if (!data) return false;
  172. const info = params.channels as ChannelsInfo;
  173. if (this.info.kind === 'x-ray') {
  174. this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || VolumeData.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
  175. this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]);
  176. this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]);
  177. } else {
  178. this.channels['em'] = this.createChannel(data['EM'] || VolumeData.One, info['em'], this.info.header.sampling[0].valuesInfo[0]);
  179. }
  180. return true;
  181. }
  182. private createChannel(data: VolumeData, info: ChannelsInfo['em'], stats: VolumeData['dataStats']): ChannelInfo {
  183. const i = info!;
  184. return {
  185. data,
  186. color: i.color,
  187. opacity: i.opacity,
  188. isoValue: i.isoValue.kind === 'relative' ? i.isoValue : VolumeIsoValue.toRelative(i.isoValue, stats)
  189. };
  190. }
  191. constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) {
  192. super(plugin);
  193. }
  194. }
  195. }