behavior.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
  8. import { PluginStateObject } from '../../../state/objects';
  9. import { VolumeIsoValue, VolumeData } from '../../../../mol-model/volume';
  10. import { createIsoValueParam } from '../../../../mol-repr/volume/isosurface';
  11. import { VolumeServerHeader, VolumeServerInfo } from './model';
  12. import { Box3D } from '../../../../mol-math/geometry';
  13. import { Vec3 } from '../../../../mol-math/linear-algebra';
  14. import { Color } from '../../../../mol-util/color';
  15. import { PluginBehavior } from '../../behavior';
  16. import { LRUCache } from '../../../../mol-util/lru-cache';
  17. import { urlCombine } from '../../../../mol-util/url';
  18. import { CIF } from '../../../../mol-io/reader/cif';
  19. import { volumeFromDensityServerData } from '../../../../mol-model-formats/volume/density-server';
  20. import { PluginCommands } from '../../../command';
  21. import { StateSelection } from '../../../../mol-state';
  22. import { Representation } from '../../../../mol-repr/representation';
  23. import { ButtonsType, ModifiersKeys } from '../../../../mol-util/input/input-observer';
  24. import { StructureElement, Link, Structure } from '../../../../mol-model/structure';
  25. import { PluginContext } from '../../../context';
  26. import { Binding } from '../../../../mol-util/binding';
  27. import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
  28. const B = ButtonsType
  29. const M = ModifiersKeys
  30. const Trigger = Binding.Trigger
  31. export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
  32. export namespace VolumeStreaming {
  33. function channelParam(label: string, color: Color, defaultValue: VolumeIsoValue, stats: VolumeData['dataStats']) {
  34. return PD.Group({
  35. isoValue: createIsoValueParam(defaultValue, stats),
  36. color: PD.Color(color),
  37. wireframe: PD.Boolean(false),
  38. opacity: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 })
  39. }, { label, isExpanded: true });
  40. }
  41. const fakeSampling: VolumeServerHeader.Sampling = {
  42. byteOffset: 0,
  43. rate: 1,
  44. sampleCount: [1, 1, 1],
  45. valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
  46. };
  47. export const DefaultBindings = {
  48. clickVolumeAroundOnly: Binding(Trigger(B.Flag.Secondary, M.create()), 'Show the volume around only the clicked element using ${trigger}.'),
  49. }
  50. export function createParams(data?: VolumeServerInfo.Data, defaultView?: ViewTypes, binding?: typeof DefaultBindings) {
  51. // fake the info
  52. const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) };
  53. const box = (data && data.structure.boundary.box) || Box3D.empty();
  54. return {
  55. view: PD.MappedStatic(defaultView || (info.kind === 'em' ? 'cell' : 'selection-box'), {
  56. 'off': PD.Group({}),
  57. 'box': PD.Group({
  58. bottomLeft: PD.Vec3(box.min),
  59. topRight: PD.Vec3(box.max),
  60. }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
  61. 'selection-box': PD.Group({
  62. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }),
  63. bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
  64. topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
  65. }, { description: 'Box around last-interacted element.', isFlat: true }),
  66. 'cell': PD.Group({}),
  67. // 'auto': PD.Group({ }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
  68. }, { options: ViewTypeOptions as any }),
  69. detailLevel: PD.Select<number>(Math.min(3, info.header.availablePrecisions.length - 1),
  70. info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string])),
  71. channels: info.kind === 'em'
  72. ? PD.Group({
  73. 'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || VolumeIsoValue.relative(1), info.header.sampling[0].valuesInfo[0])
  74. }, { isFlat: true })
  75. : PD.Group({
  76. '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), VolumeIsoValue.relative(1.5), info.header.sampling[0].valuesInfo[0]),
  77. 'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), VolumeIsoValue.relative(3), info.header.sampling[0].valuesInfo[1]),
  78. 'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), VolumeIsoValue.relative(-3), info.header.sampling[0].valuesInfo[1]),
  79. }, { isFlat: true }),
  80. bindings: PD.Value(binding || DefaultBindings, { isHidden: true }),
  81. };
  82. }
  83. export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Surroundings'], ['cell', 'Whole Structure']];
  84. export type ViewTypes = 'off' | 'box' | 'selection-box' | 'cell'
  85. export type ParamDefinition = typeof createParams extends (...args: any[]) => (infer T) ? T : never
  86. export type Params = ParamDefinition extends PD.Params ? PD.Values<ParamDefinition> : {}
  87. type CT = typeof channelParam extends (...args: any[]) => (infer T) ? T : never
  88. export type ChannelParams = CT extends PD.Group<infer T> ? T : {}
  89. type ChannelsInfo = { [name in ChannelType]?: { isoValue: VolumeIsoValue, color: Color, wireframe: boolean, opacity: number } }
  90. type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
  91. export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
  92. export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']]
  93. export interface ChannelInfo {
  94. data: VolumeData,
  95. color: Color,
  96. wireframe: boolean,
  97. isoValue: VolumeIsoValue.Relative,
  98. opacity: number
  99. }
  100. export type Channels = { [name in ChannelType]?: ChannelInfo }
  101. export class Behavior extends PluginBehavior.WithSubscribers<Params> {
  102. private cache = LRUCache.create<ChannelsData>(25);
  103. public params: Params = {} as any;
  104. private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
  105. private ref: string = '';
  106. channels: Channels = {}
  107. private async queryData(box?: Box3D) {
  108. let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId.toLowerCase()}`);
  109. if (box) {
  110. const { min: a, max: b } = box;
  111. url += `/box`
  112. + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
  113. + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
  114. } else {
  115. url += `/cell`;
  116. }
  117. url += `?detail=${this.params.detailLevel}`;
  118. let data = LRUCache.get(this.cache, url);
  119. if (data) {
  120. return data;
  121. }
  122. const cif = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
  123. data = await this.parseCif(cif as Uint8Array);
  124. if (!data) {
  125. return;
  126. }
  127. LRUCache.set(this.cache, url, data);
  128. return data;
  129. }
  130. private async parseCif(data: Uint8Array): Promise<ChannelsData | undefined> {
  131. const parsed = await this.plugin.runTask(CIF.parseBinary(data));
  132. if (parsed.isError) {
  133. this.plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
  134. return;
  135. }
  136. if (parsed.result.blocks.length < 2) {
  137. this.plugin.log.error('VolumeStreaming: Invalid data.');
  138. return;
  139. }
  140. const ret: ChannelsData = {};
  141. for (let i = 1; i < parsed.result.blocks.length; i++) {
  142. const block = parsed.result.blocks[i];
  143. const densityServerCif = CIF.schema.densityServer(block);
  144. const volume = await this.plugin.runTask(await volumeFromDensityServerData(densityServerCif));
  145. (ret as any)[block.header as any] = volume;
  146. }
  147. return ret;
  148. }
  149. private updateDynamicBox(box: Box3D) {
  150. if (this.params.view.name !== 'selection-box') return;
  151. const state = this.plugin.state.dataState;
  152. const newParams: Params = {
  153. ...this.params,
  154. view: {
  155. name: 'selection-box' as 'selection-box',
  156. params: {
  157. radius: this.params.view.params.radius,
  158. bottomLeft: box.min,
  159. topRight: box.max
  160. }
  161. }
  162. };
  163. const update = state.build().to(this.ref).update(newParams);
  164. PluginCommands.State.Update.dispatch(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
  165. }
  166. private getStructureRoot() {
  167. return this.plugin.state.dataState.select(StateSelection.Generators.byRef(this.ref).rootOfType([PluginStateObject.Molecule.Structure]))[0];
  168. }
  169. register(ref: string): void {
  170. this.ref = ref;
  171. this.subscribeObservable(this.plugin.events.state.object.removed, o => {
  172. if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(this.lastLoci)) return;
  173. if (this.lastLoci.structure === o.obj.data) {
  174. this.lastLoci = EmptyLoci;
  175. }
  176. });
  177. this.subscribeObservable(this.plugin.events.state.object.updated, o => {
  178. if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(this.lastLoci)) return;
  179. if (this.lastLoci.structure === o.oldObj.data) {
  180. this.lastLoci = EmptyLoci;
  181. }
  182. });
  183. this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => {
  184. if (!Binding.match(this.params.bindings.clickVolumeAroundOnly || DefaultBindings.clickVolumeAroundOnly, buttons, modifiers)) return;
  185. if (this.params.view.name !== 'selection-box') {
  186. this.lastLoci = this.getNormalizedLoci(current.loci);
  187. } else {
  188. this.updateInteraction(current);
  189. }
  190. });
  191. }
  192. private getNormalizedLoci(loci: Loci): StructureElement.Loci | EmptyLoci {
  193. if (StructureElement.Loci.is(loci)) {
  194. return loci;
  195. } else if (Link.isLoci(loci)) {
  196. return Link.toStructureElementLoci(loci);
  197. } else if (Structure.isLoci(loci)) {
  198. return Structure.toStructureElementLoci(loci);
  199. } else {
  200. return EmptyLoci;
  201. }
  202. }
  203. private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
  204. if (isEmptyLoci(loci) || StructureElement.Loci.isEmpty(loci)) {
  205. return Box3D.empty();
  206. }
  207. const parent = this.plugin.helpers.substructureParent.get(loci.structure);
  208. if (!parent) return Box3D.empty();
  209. const root = this.getStructureRoot();
  210. if (!root || !root.obj || root.obj !== parent.obj) return Box3D.empty();
  211. const extendedLoci = StructureElement.Loci.extendToWholeResidues(loci)
  212. const box = StructureElement.Loci.getBoundary(extendedLoci).box
  213. if (StructureElement.Loci.size(extendedLoci) === 1) {
  214. Box3D.expand(box, box, Vec3.create(1, 1, 1))
  215. }
  216. return box;
  217. }
  218. private updateInteraction(current: Representation.Loci) {
  219. const loci = this.getNormalizedLoci(current.loci)
  220. if (Loci.areEqual(this.lastLoci, loci)) {
  221. this.lastLoci = EmptyLoci;
  222. this.updateDynamicBox(Box3D.empty());
  223. return;
  224. }
  225. this.lastLoci = loci;
  226. if (isEmptyLoci(loci)) {
  227. this.updateDynamicBox(Box3D.empty());
  228. return;
  229. }
  230. const box = this.getBoxFromLoci(loci);
  231. this.updateDynamicBox(box);
  232. }
  233. async update(params: Params) {
  234. const switchedToSelection = params.view.name === 'selection-box' && this.params && this.params.view && this.params.view.name !== 'selection-box';
  235. this.params = params;
  236. let box: Box3D | undefined = void 0, emptyData = false;
  237. switch (params.view.name) {
  238. case 'off':
  239. emptyData = true;
  240. break;
  241. case 'box':
  242. box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight);
  243. emptyData = Box3D.volume(box) < 0.0001;
  244. break;
  245. case 'selection-box': {
  246. if (switchedToSelection) {
  247. box = this.getBoxFromLoci(this.lastLoci) || Box3D.empty();
  248. } else {
  249. box = Box3D.create(Vec3.clone(params.view.params.bottomLeft), Vec3.clone(params.view.params.topRight));
  250. }
  251. const r = params.view.params.radius;
  252. emptyData = Box3D.volume(box) < 0.0001;
  253. Box3D.expand(box, box, Vec3.create(r, r, r));
  254. break;
  255. }
  256. case 'cell':
  257. box = this.info.kind === 'x-ray'
  258. ? this.info.structure.boundary.box
  259. : void 0;
  260. break;
  261. }
  262. const data = emptyData ? {} : await this.queryData(box);
  263. if (!data) return false;
  264. const info = params.channels as ChannelsInfo;
  265. if (this.info.kind === 'x-ray') {
  266. this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || VolumeData.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
  267. this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]);
  268. this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]);
  269. } else {
  270. this.channels['em'] = this.createChannel(data['EM'] || VolumeData.One, info['em'], this.info.header.sampling[0].valuesInfo[0]);
  271. }
  272. return true;
  273. }
  274. private createChannel(data: VolumeData, info: ChannelsInfo['em'], stats: VolumeData['dataStats']): ChannelInfo {
  275. const i = info!;
  276. return {
  277. data,
  278. color: i.color,
  279. wireframe: i.wireframe,
  280. opacity: i.opacity,
  281. isoValue: i.isoValue.kind === 'relative' ? i.isoValue : VolumeIsoValue.toRelative(i.isoValue, stats)
  282. };
  283. }
  284. getDescription() {
  285. if (this.params.view.name === 'selection-box') return 'Selection';
  286. if (this.params.view.name === 'box') return 'Static Box';
  287. if (this.params.view.name === 'cell') return 'Cell';
  288. return '';
  289. }
  290. constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) {
  291. super(plugin, {} as any);
  292. }
  293. }
  294. }