behavior.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. /**
  2. * Copyright (c) 2019-2020 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 '../../../../mol-plugin-state/objects';
  9. import { Volume, Grid } from '../../../../mol-model/volume';
  10. import { VolumeServerHeader, VolumeServerInfo } from './model';
  11. import { Box3D } from '../../../../mol-math/geometry';
  12. import { Mat4, Vec3 } from '../../../../mol-math/linear-algebra';
  13. import { Color } from '../../../../mol-util/color';
  14. import { PluginBehavior } from '../../behavior';
  15. import { LRUCache } from '../../../../mol-util/lru-cache';
  16. import { urlCombine } from '../../../../mol-util/url';
  17. import { CIF } from '../../../../mol-io/reader/cif';
  18. import { volumeFromDensityServerData } from '../../../../mol-model-formats/volume/density-server';
  19. import { PluginCommands } from '../../../commands';
  20. import { StateSelection } from '../../../../mol-state';
  21. import { StructureElement, Structure } from '../../../../mol-model/structure';
  22. import { PluginContext } from '../../../context';
  23. import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
  24. import { Asset } from '../../../../mol-util/assets';
  25. import { GlobalModelTransformInfo } from '../../../../mol-model/structure/model/properties/global-transform';
  26. export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
  27. export namespace VolumeStreaming {
  28. export const RootTag = 'volume-streaming-info';
  29. export interface ChannelParams {
  30. isoValue: Volume.IsoValue,
  31. color: Color,
  32. wireframe: boolean,
  33. opacity: number
  34. }
  35. function channelParam(label: string, color: Color, defaultValue: Volume.IsoValue, stats: Grid['stats'], defaults: Partial<ChannelParams> = {}) {
  36. return PD.Group<ChannelParams>({
  37. isoValue: Volume.createIsoValueParam(defaults.isoValue ?? defaultValue, stats),
  38. color: PD.Color(defaults.color ?? color),
  39. wireframe: PD.Boolean(defaults.wireframe ?? false),
  40. opacity: PD.Numeric(defaults.opacity ?? 0.3, { min: 0, max: 1, step: 0.01 })
  41. }, { label, isExpanded: true });
  42. }
  43. const fakeSampling: VolumeServerHeader.Sampling = {
  44. byteOffset: 0,
  45. rate: 1,
  46. sampleCount: [1, 1, 1],
  47. valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
  48. };
  49. export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = { }) {
  50. const { data, defaultView, channelParams } = options;
  51. const map = new Map<string, VolumeServerInfo.EntryData>();
  52. if (data) data.entries.forEach(d => map.set(d.dataId, d));
  53. const names = data ? data.entries.map(d => [d.dataId, d.dataId] as [string, string]) : [];
  54. const defaultKey = data ? data.entries[0].dataId : '';
  55. return {
  56. entry: PD.Mapped<EntryParams>(defaultKey, names, name => PD.Group(createEntryParams({ entryData: map.get(name)!, defaultView, structure: data && data.structure, channelParams }))),
  57. };
  58. }
  59. export type EntryParamDefinition = ReturnType<typeof createEntryParams>
  60. export type EntryParams = PD.Values<EntryParamDefinition>
  61. export function createEntryParams(options: { entryData?: VolumeServerInfo.EntryData, defaultView?: ViewTypes, structure?: Structure, channelParams?: DefaultChannelParams }) {
  62. const { entryData, defaultView, structure, channelParams = { } } = options;
  63. // fake the info
  64. const info = entryData || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: Volume.IsoValue.relative(0) };
  65. const box = (structure && structure.boundary.box) || Box3D();
  66. return {
  67. view: PD.MappedStatic(defaultView || (info.kind === 'em' ? 'cell' : 'selection-box'), {
  68. 'off': PD.Group<{}>({}),
  69. 'box': PD.Group({
  70. bottomLeft: PD.Vec3(box.min),
  71. topRight: PD.Vec3(box.max),
  72. }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
  73. 'selection-box': PD.Group({
  74. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  75. bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
  76. topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
  77. }, { description: 'Box around focused element.', isFlat: true }),
  78. 'cell': PD.Group<{}>({}),
  79. // Show selection-box if available and cell otherwise.
  80. 'auto': PD.Group({
  81. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  82. selectionDetailLevel: PD.Select<number>(Math.min(6, info.header.availablePrecisions.length - 1),
  83. info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { label: 'Selection Detail', description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
  84. isSelection: PD.Boolean(false, { isHidden: true }),
  85. bottomLeft: PD.Vec3(box.min, {}, { isHidden: true }),
  86. topRight: PD.Vec3(box.max, {}, { isHidden: true }),
  87. }, { description: 'Box around focused element.', isFlat: true })
  88. }, { options: ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Interaction" shows the volume around the focused element/atom. "Whole Structure" shows the volume for the whole structure.' }),
  89. detailLevel: PD.Select<number>(Math.min(3, info.header.availablePrecisions.length - 1),
  90. info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
  91. channels: info.kind === 'em'
  92. ? PD.Group({
  93. 'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || Volume.IsoValue.relative(1), info.header.sampling[0].valuesInfo[0], channelParams['em'])
  94. }, { isFlat: true })
  95. : PD.Group({
  96. '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), Volume.IsoValue.relative(1.5), info.header.sampling[0].valuesInfo[0], channelParams['2fo-fc']),
  97. 'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), Volume.IsoValue.relative(3), info.header.sampling[0].valuesInfo[1], channelParams['fo-fc(+ve)']),
  98. 'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), Volume.IsoValue.relative(-3), info.header.sampling[0].valuesInfo[1], channelParams['fo-fc(-ve)']),
  99. }, { isFlat: true }),
  100. };
  101. }
  102. export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
  103. export type ViewTypes = 'off' | 'box' | 'selection-box' | 'cell' | 'auto'
  104. export type ParamDefinition = ReturnType<typeof createParams>
  105. export type Params = PD.Values<ParamDefinition>
  106. type ChannelsInfo = { [name in ChannelType]?: { isoValue: Volume.IsoValue, color: Color, wireframe: boolean, opacity: number } }
  107. type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: Volume }
  108. export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
  109. export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']];
  110. export interface ChannelInfo {
  111. data: Volume,
  112. color: Color,
  113. wireframe: boolean,
  114. isoValue: Volume.IsoValue.Relative,
  115. opacity: number
  116. }
  117. export type Channels = { [name in ChannelType]?: ChannelInfo }
  118. export type DefaultChannelParams = { [name in ChannelType]?: Partial<ChannelParams> }
  119. export class Behavior extends PluginBehavior.WithSubscribers<Params> {
  120. private cache = LRUCache.create<{ data: ChannelsData, asset: Asset.Wrapper }>(25);
  121. public params: Params = {} as any;
  122. private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
  123. private ref: string = '';
  124. public infoMap: Map<string, VolumeServerInfo.EntryData>
  125. channels: Channels = {}
  126. public get info() {
  127. return this.infoMap.get(this.params.entry.name)!;
  128. }
  129. private async queryData(box?: Box3D) {
  130. let url = urlCombine(this.data.serverUrl, `${this.info.kind}/${this.info.dataId.toLowerCase()}`);
  131. if (box) {
  132. const { min: a, max: b } = box;
  133. url += `/box`
  134. + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
  135. + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
  136. } else {
  137. url += `/cell`;
  138. }
  139. let detail = this.params.entry.params.detailLevel;
  140. if (this.params.entry.params.view.name === 'auto' && this.params.entry.params.view.params.isSelection) {
  141. detail = this.params.entry.params.view.params.selectionDetailLevel;
  142. }
  143. url += `?detail=${detail}`;
  144. const entry = LRUCache.get(this.cache, url);
  145. if (entry) return entry.data;
  146. const urlAsset = Asset.getUrlAsset(this.plugin.managers.asset, url);
  147. const asset = await this.plugin.runTask(this.plugin.managers.asset.resolve(urlAsset, 'binary'));
  148. const data = await this.parseCif(asset.data);
  149. if (!data) return;
  150. const removed = LRUCache.set(this.cache, url, { data, asset });
  151. if (removed) removed.asset.dispose();
  152. return data;
  153. }
  154. private async parseCif(data: Uint8Array): Promise<ChannelsData | undefined> {
  155. const parsed = await this.plugin.runTask(CIF.parseBinary(data));
  156. if (parsed.isError) {
  157. this.plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
  158. return;
  159. }
  160. if (parsed.result.blocks.length < 2) {
  161. this.plugin.log.error('VolumeStreaming: Invalid data.');
  162. return;
  163. }
  164. const ret: ChannelsData = {};
  165. for (let i = 1; i < parsed.result.blocks.length; i++) {
  166. const block = parsed.result.blocks[i];
  167. const densityServerCif = CIF.schema.densityServer(block);
  168. const volume = await this.plugin.runTask(volumeFromDensityServerData(densityServerCif));
  169. (ret as any)[block.header as any] = volume;
  170. }
  171. return ret;
  172. }
  173. private updateSelectionBoxParams(box: Box3D) {
  174. if (this.params.entry.params.view.name !== 'selection-box') return;
  175. const state = this.plugin.state.data;
  176. const newParams: Params = {
  177. ...this.params,
  178. entry: {
  179. name: this.params.entry.name,
  180. params: {
  181. ...this.params.entry.params,
  182. view: {
  183. name: 'selection-box' as const,
  184. params: {
  185. radius: this.params.entry.params.view.params.radius,
  186. bottomLeft: box.min,
  187. topRight: box.max
  188. }
  189. }
  190. }
  191. }
  192. };
  193. const update = state.build().to(this.ref).update(newParams);
  194. PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
  195. }
  196. private updateAutoParams(box: Box3D | undefined, isSelection: boolean) {
  197. if (this.params.entry.params.view.name !== 'auto') return;
  198. const state = this.plugin.state.data;
  199. const newParams: Params = {
  200. ...this.params,
  201. entry: {
  202. name: this.params.entry.name,
  203. params: {
  204. ...this.params.entry.params,
  205. view: {
  206. name: 'auto' as const,
  207. params: {
  208. radius: this.params.entry.params.view.params.radius,
  209. selectionDetailLevel: this.params.entry.params.view.params.selectionDetailLevel,
  210. isSelection,
  211. bottomLeft: box?.min || Vec3.zero(),
  212. topRight: box?.max || Vec3.zero()
  213. }
  214. }
  215. }
  216. }
  217. };
  218. const update = state.build().to(this.ref).update(newParams);
  219. PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
  220. }
  221. private getStructureRoot() {
  222. return this.plugin.state.data.select(StateSelection.Generators.byRef(this.ref).rootOfType(PluginStateObject.Molecule.Structure))[0];
  223. }
  224. register(ref: string): void {
  225. this.ref = ref;
  226. this.subscribeObservable(this.plugin.state.events.object.removed, o => {
  227. if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(this.lastLoci)) return;
  228. if (this.lastLoci.structure === o.obj.data) {
  229. this.lastLoci = EmptyLoci;
  230. }
  231. });
  232. this.subscribeObservable(this.plugin.state.events.object.updated, o => {
  233. if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(this.lastLoci)) return;
  234. if (this.lastLoci.structure === o.oldObj.data) {
  235. this.lastLoci = EmptyLoci;
  236. }
  237. });
  238. this.subscribeObservable(this.plugin.managers.structure.focus.behaviors.current, (entry) => {
  239. if (!this.plugin.state.data.tree.children.has(this.ref)) return;
  240. const loci = entry ? entry.loci : EmptyLoci;
  241. switch (this.params.entry.params.view.name) {
  242. case 'auto':
  243. this.updateAuto(loci);
  244. break;
  245. case 'selection-box':
  246. this.updateSelectionBox(loci);
  247. break;
  248. default:
  249. this.lastLoci = loci;
  250. break;
  251. }
  252. });
  253. }
  254. unregister() {
  255. let entry = this.cache.entries.first;
  256. while (entry) {
  257. entry.value.data.asset.dispose();
  258. entry = entry.next;
  259. }
  260. }
  261. private _invTransform: Mat4 = Mat4();
  262. private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
  263. if (Loci.isEmpty(loci) || isEmptyLoci(loci)) {
  264. return Box3D();
  265. }
  266. const parent = this.plugin.helpers.substructureParent.get(loci.structure, true);
  267. if (!parent) return Box3D();
  268. const root = this.getStructureRoot();
  269. if (!root || root.obj?.data !== parent.obj?.data) return Box3D();
  270. const transform = GlobalModelTransformInfo.get(root.obj?.data.models[0]!);
  271. if (transform) Mat4.invert(this._invTransform, transform);
  272. const extendedLoci = StructureElement.Loci.extendToWholeResidues(loci);
  273. const box = StructureElement.Loci.getBoundary(extendedLoci, transform && !Number.isNaN(this._invTransform[0]) ? this._invTransform : void 0).box;
  274. if (StructureElement.Loci.size(extendedLoci) === 1) {
  275. Box3D.expand(box, box, Vec3.create(1, 1, 1));
  276. }
  277. return box;
  278. }
  279. private updateAuto(loci: StructureElement.Loci | EmptyLoci) {
  280. // if (Loci.areEqual(this.lastLoci, loci)) {
  281. // this.lastLoci = EmptyLoci;
  282. // this.updateSelectionBoxParams(Box3D.empty());
  283. // return;
  284. // }
  285. this.lastLoci = loci;
  286. if (isEmptyLoci(loci)) {
  287. this.updateAutoParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
  288. return;
  289. }
  290. const box = this.getBoxFromLoci(loci);
  291. this.updateAutoParams(box, true);
  292. }
  293. private updateSelectionBox(loci: StructureElement.Loci | EmptyLoci) {
  294. if (Loci.areEqual(this.lastLoci, loci)) {
  295. this.lastLoci = EmptyLoci;
  296. this.updateSelectionBoxParams(Box3D());
  297. return;
  298. }
  299. this.lastLoci = loci;
  300. if (isEmptyLoci(loci)) {
  301. this.updateSelectionBoxParams(Box3D());
  302. return;
  303. }
  304. const box = this.getBoxFromLoci(loci);
  305. this.updateSelectionBoxParams(box);
  306. }
  307. async update(params: Params) {
  308. 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';
  309. this.params = params;
  310. let box: Box3D | undefined = void 0, emptyData = false;
  311. switch (params.entry.params.view.name) {
  312. case 'off':
  313. emptyData = true;
  314. break;
  315. case 'box':
  316. box = Box3D.create(params.entry.params.view.params.bottomLeft, params.entry.params.view.params.topRight);
  317. emptyData = Box3D.volume(box) < 0.0001;
  318. break;
  319. case 'selection-box': {
  320. if (switchedToSelection) {
  321. box = this.getBoxFromLoci(this.lastLoci) || Box3D();
  322. } else {
  323. box = Box3D.create(Vec3.clone(params.entry.params.view.params.bottomLeft), Vec3.clone(params.entry.params.view.params.topRight));
  324. }
  325. const r = params.entry.params.view.params.radius;
  326. emptyData = Box3D.volume(box) < 0.0001;
  327. Box3D.expand(box, box, Vec3.create(r, r, r));
  328. break;
  329. }
  330. case 'cell':
  331. box = this.info.kind === 'x-ray'
  332. ? this.data.structure.boundary.box
  333. : void 0;
  334. break;
  335. case 'auto':
  336. box = params.entry.params.view.params.isSelection || this.info.kind === 'x-ray'
  337. ? Box3D.create(Vec3.clone(params.entry.params.view.params.bottomLeft), Vec3.clone(params.entry.params.view.params.topRight))
  338. : void 0;
  339. if (box) {
  340. emptyData = Box3D.volume(box) < 0.0001;
  341. if (params.entry.params.view.params.isSelection) {
  342. const r = params.entry.params.view.params.radius;
  343. Box3D.expand(box, box, Vec3.create(r, r, r));
  344. }
  345. }
  346. break;
  347. }
  348. const data = emptyData ? {} : await this.queryData(box);
  349. if (!data) return false;
  350. const info = params.entry.params.channels as ChannelsInfo;
  351. if (this.info.kind === 'x-ray') {
  352. this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || Volume.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
  353. this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || Volume.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]);
  354. this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || Volume.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]);
  355. } else {
  356. this.channels['em'] = this.createChannel(data['EM'] || Volume.One, info['em'], this.info.header.sampling[0].valuesInfo[0]);
  357. }
  358. return true;
  359. }
  360. private createChannel(data: Volume, info: ChannelsInfo['em'], stats: Grid['stats']): ChannelInfo {
  361. const i = info!;
  362. return {
  363. data,
  364. color: i.color,
  365. wireframe: i.wireframe,
  366. opacity: i.opacity,
  367. isoValue: i.isoValue.kind === 'relative' ? i.isoValue : Volume.IsoValue.toRelative(i.isoValue, stats)
  368. };
  369. }
  370. getDescription() {
  371. if (this.params.entry.params.view.name === 'selection-box') return 'Selection';
  372. if (this.params.entry.params.view.name === 'box') return 'Static Box';
  373. if (this.params.entry.params.view.name === 'cell') return 'Cell';
  374. return '';
  375. }
  376. constructor(public plugin: PluginContext, public data: VolumeServerInfo.Data) {
  377. super(plugin, {} as any);
  378. this.infoMap = new Map<string, VolumeServerInfo.EntryData>();
  379. this.data.entries.forEach(info => this.infoMap.set(info.dataId, info));
  380. }
  381. }
  382. }