behavior.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  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. import { distinctUntilChanged, filter, map, Observable, throttleTime } from 'rxjs';
  27. import { arrayEqual } from '../../../../mol-util';
  28. import { Camera } from '../../../../mol-canvas3d/camera';
  29. import { PluginCommand } from '../../../command';
  30. export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
  31. export namespace VolumeStreaming {
  32. export const RootTag = 'volume-streaming-info';
  33. export interface ChannelParams {
  34. isoValue: Volume.IsoValue,
  35. color: Color,
  36. wireframe: boolean,
  37. opacity: number
  38. }
  39. function channelParam(label: string, color: Color, defaultValue: Volume.IsoValue, stats: Grid['stats'], defaults: Partial<ChannelParams> = {}) {
  40. return PD.Group<ChannelParams>({
  41. isoValue: Volume.createIsoValueParam(defaults.isoValue ?? defaultValue, stats),
  42. color: PD.Color(defaults.color ?? color),
  43. wireframe: PD.Boolean(defaults.wireframe ?? false),
  44. opacity: PD.Numeric(defaults.opacity ?? 0.3, { min: 0, max: 1, step: 0.01 })
  45. }, { label, isExpanded: true });
  46. }
  47. const fakeSampling: VolumeServerHeader.Sampling = {
  48. byteOffset: 0,
  49. rate: 1,
  50. sampleCount: [1, 1, 1],
  51. valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
  52. };
  53. export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = {}) {
  54. const { data, defaultView, channelParams } = options;
  55. const map = new Map<string, VolumeServerInfo.EntryData>();
  56. if (data) data.entries.forEach(d => map.set(d.dataId, d));
  57. const names = data ? data.entries.map(d => [d.dataId, d.dataId] as [string, string]) : [];
  58. const defaultKey = data ? data.entries[0].dataId : '';
  59. return {
  60. entry: PD.Mapped<EntryParams>(defaultKey, names, name => PD.Group(createEntryParams({ entryData: map.get(name)!, defaultView, structure: data && data.structure, channelParams }))),
  61. };
  62. }
  63. export type EntryParamDefinition = ReturnType<typeof createEntryParams>
  64. export type EntryParams = PD.Values<EntryParamDefinition>
  65. export function createEntryParams(options: { entryData?: VolumeServerInfo.EntryData, defaultView?: ViewTypes, structure?: Structure, channelParams?: DefaultChannelParams }) {
  66. const { entryData, defaultView, structure, channelParams = {} } = options;
  67. // fake the info
  68. const info = entryData || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: Volume.IsoValue.relative(0) };
  69. const box = (structure && structure.boundary.box) || Box3D();
  70. return {
  71. view: PD.MappedStatic(defaultView || (info.kind === 'em' ? 'cell' : 'selection-box'), {
  72. 'off': PD.Group<{}>({}),
  73. 'box': PD.Group({
  74. bottomLeft: PD.Vec3(box.min),
  75. topRight: PD.Vec3(box.max),
  76. }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
  77. 'selection-box': PD.Group({
  78. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  79. bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
  80. topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
  81. }, { description: 'Box around focused element.', isFlat: true }),
  82. 'camera-target': PD.Group({
  83. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  84. // Minimal detail level for the inside of the zoomed region (real detail can be higher, depending on the region size)
  85. dynamicDetailLevel: createDetailParams(info.header.availablePrecisions, 0, { label: 'Dynamic Detail' }), // TODO Adam choose appropriate default value
  86. bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
  87. topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
  88. }, { description: 'Box around camera target.', isFlat: true }),
  89. 'cell': PD.Group<{}>({}),
  90. // Show selection-box if available and cell otherwise.
  91. 'auto': PD.Group({
  92. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  93. selectionDetailLevel: createDetailParams(info.header.availablePrecisions, 6, { label: 'Selection Detail' }),
  94. isSelection: PD.Boolean(false, { isHidden: true }),
  95. bottomLeft: PD.Vec3(box.min, {}, { isHidden: true }),
  96. topRight: PD.Vec3(box.max, {}, { isHidden: true }),
  97. }, { description: 'Box around focused element.', isFlat: true })
  98. }, { 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.' }),
  99. detailLevel: createDetailParams(info.header.availablePrecisions, 3),
  100. channels: info.kind === 'em'
  101. ? PD.Group({
  102. 'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || Volume.IsoValue.relative(1), info.header.sampling[0].valuesInfo[0], channelParams['em'])
  103. }, { isFlat: true })
  104. : PD.Group({
  105. '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), Volume.IsoValue.relative(1.5), info.header.sampling[0].valuesInfo[0], channelParams['2fo-fc']),
  106. 'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), Volume.IsoValue.relative(3), info.header.sampling[0].valuesInfo[1], channelParams['fo-fc(+ve)']),
  107. 'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), Volume.IsoValue.relative(-3), info.header.sampling[0].valuesInfo[1], channelParams['fo-fc(-ve)']),
  108. }, { isFlat: true }),
  109. };
  110. }
  111. function createDetailParams(availablePrecisions: VolumeServerHeader.DetailLevel[], preferredPrecision: number, info?: PD.Info) {
  112. return PD.Select<number>(Math.min(preferredPrecision, availablePrecisions.length - 1),
  113. availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]),
  114. {
  115. 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).',
  116. ...info
  117. }
  118. );
  119. }
  120. export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['camera-target', 'Around Camera'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
  121. export type ViewTypes = 'off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'
  122. export type ParamDefinition = ReturnType<typeof createParams>
  123. export type Params = PD.Values<ParamDefinition>
  124. type ChannelsInfo = { [name in ChannelType]?: { isoValue: Volume.IsoValue, color: Color, wireframe: boolean, opacity: number } }
  125. type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: Volume }
  126. export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
  127. export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']];
  128. export interface ChannelInfo {
  129. data: Volume,
  130. color: Color,
  131. wireframe: boolean,
  132. isoValue: Volume.IsoValue.Relative,
  133. opacity: number
  134. }
  135. export type Channels = { [name in ChannelType]?: ChannelInfo }
  136. export type DefaultChannelParams = { [name in ChannelType]?: Partial<ChannelParams> }
  137. export class Behavior extends PluginBehavior.WithSubscribers<Params> {
  138. private cache = LRUCache.create<{ data: ChannelsData, asset: Asset.Wrapper }>(25);
  139. public params: Params = {} as any;
  140. private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
  141. private ref: string = '';
  142. public infoMap: Map<string, VolumeServerInfo.EntryData>;
  143. private updateQueue: MonoQueue;
  144. private cameraTargetObservable = this.plugin.canvas3d!.didDraw!.pipe(
  145. throttleTime(500, undefined, { 'leading': true, 'trailing': true }),
  146. map(() => this.plugin.canvas3d?.camera.getSnapshot()),
  147. distinctUntilChanged((a, b) => this.isCameraTargetSame(a, b)),
  148. filter(a => a !== undefined),
  149. ) as Observable<Camera.Snapshot>;
  150. private cameraTargetSubscription?: PluginCommand.Subscription = undefined;
  151. channels: Channels = {};
  152. public get info() {
  153. return this.infoMap.get(this.params.entry.name)!;
  154. }
  155. private async queryData(box?: Box3D) {
  156. let url = urlCombine(this.data.serverUrl, `${this.info.kind}/${this.info.dataId.toLowerCase()}`);
  157. if (box) {
  158. const { min: a, max: b } = box;
  159. url += `/box`
  160. + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
  161. + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
  162. } else {
  163. url += `/cell`;
  164. }
  165. let detail = this.params.entry.params.detailLevel;
  166. if (this.params.entry.params.view.name === 'auto' && this.params.entry.params.view.params.isSelection) {
  167. detail = this.params.entry.params.view.params.selectionDetailLevel;
  168. }
  169. if (this.params.entry.params.view.name === 'camera-target' && box) {
  170. detail = this.decideDetail(box, this.params.entry.params.view.params.dynamicDetailLevel);
  171. }
  172. url += `?detail=${detail}`;
  173. const entry = LRUCache.get(this.cache, url);
  174. if (entry) return entry.data;
  175. const urlAsset = Asset.getUrlAsset(this.plugin.managers.asset, url);
  176. const asset = await this.plugin.runTask(this.plugin.managers.asset.resolve(urlAsset, 'binary'));
  177. const data = await this.parseCif(asset.data);
  178. if (!data) return;
  179. const removed = LRUCache.set(this.cache, url, { data, asset });
  180. if (removed) removed.asset.dispose();
  181. return data;
  182. }
  183. private async parseCif(data: Uint8Array): Promise<ChannelsData | undefined> {
  184. const parsed = await this.plugin.runTask(CIF.parseBinary(data));
  185. if (parsed.isError) {
  186. this.plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
  187. return;
  188. }
  189. if (parsed.result.blocks.length < 2) {
  190. this.plugin.log.error('VolumeStreaming: Invalid data.');
  191. return;
  192. }
  193. const ret: ChannelsData = {};
  194. for (let i = 1; i < parsed.result.blocks.length; i++) {
  195. const block = parsed.result.blocks[i];
  196. const densityServerCif = CIF.schema.densityServer(block);
  197. const volume = await this.plugin.runTask(volumeFromDensityServerData(densityServerCif));
  198. (ret as any)[block.header as any] = volume;
  199. }
  200. return ret;
  201. }
  202. private async updateSelectionBoxParams(box: Box3D) {
  203. if (this.params.entry.params.view.name !== 'selection-box') return;
  204. const state = this.plugin.state.data;
  205. const newParams: Params = {
  206. ...this.params,
  207. entry: {
  208. name: this.params.entry.name,
  209. params: {
  210. ...this.params.entry.params,
  211. view: {
  212. name: 'selection-box' as const,
  213. params: {
  214. radius: this.params.entry.params.view.params.radius,
  215. bottomLeft: box.min,
  216. topRight: box.max
  217. }
  218. }
  219. }
  220. }
  221. };
  222. const update = state.build().to(this.ref).update(newParams);
  223. // const task = state.updateTree(update, { doNotUpdateCurrent: true });
  224. // const promise = this.plugin.runTask(task);
  225. // setTimeout(() => this.plugin.managers.task.requestAbort(task.id), 200);
  226. // await promise;
  227. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
  228. }
  229. private async updateCameraTargetParams(box: Box3D | undefined) {
  230. if (this.params.entry.params.view.name !== 'camera-target') return;
  231. const state = this.plugin.state.data;
  232. const newParams: Params = {
  233. ...this.params,
  234. entry: {
  235. name: this.params.entry.name,
  236. params: {
  237. ...this.params.entry.params,
  238. view: {
  239. name: 'camera-target' as const,
  240. params: {
  241. radius: this.params.entry.params.view.params.radius,
  242. dynamicDetailLevel: this.params.entry.params.view.params.dynamicDetailLevel,
  243. bottomLeft: box?.min || Vec3.zero(),
  244. topRight: box?.max || Vec3.zero()
  245. }
  246. }
  247. }
  248. }
  249. };
  250. const update = state.build().to(this.ref).update(newParams);
  251. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
  252. }
  253. private async updateAutoParams(box: Box3D | undefined, isSelection: boolean) {
  254. if (this.params.entry.params.view.name !== 'auto') return;
  255. const state = this.plugin.state.data;
  256. const newParams: Params = {
  257. ...this.params,
  258. entry: {
  259. name: this.params.entry.name,
  260. params: {
  261. ...this.params.entry.params,
  262. view: {
  263. name: 'auto' as const,
  264. params: {
  265. radius: this.params.entry.params.view.params.radius,
  266. selectionDetailLevel: this.params.entry.params.view.params.selectionDetailLevel,
  267. isSelection,
  268. bottomLeft: box?.min || Vec3.zero(),
  269. topRight: box?.max || Vec3.zero()
  270. }
  271. }
  272. }
  273. }
  274. };
  275. const update = state.build().to(this.ref).update(newParams);
  276. await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
  277. // TODO QUESTION is there a reason for this much code repetition? updateSelectionBoxParams vs updateAutoParams (and now also updateCameraTargetParams)
  278. }
  279. private getStructureRoot() {
  280. return this.plugin.state.data.select(StateSelection.Generators.byRef(this.ref).rootOfType(PluginStateObject.Molecule.Structure))[0];
  281. }
  282. register(ref: string): void {
  283. this.ref = ref;
  284. this.subscribeObservable(this.plugin.state.events.object.removed, o => {
  285. if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(this.lastLoci)) return;
  286. if (this.lastLoci.structure === o.obj.data) {
  287. this.lastLoci = EmptyLoci;
  288. }
  289. });
  290. this.subscribeObservable(this.plugin.state.events.object.updated, o => {
  291. if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(this.lastLoci)) return;
  292. if (this.lastLoci.structure === o.oldObj.data) {
  293. this.lastLoci = EmptyLoci;
  294. }
  295. });
  296. this.subscribeObservable(this.plugin.managers.structure.focus.behaviors.current, (entry) => {
  297. if (!this.plugin.state.data.tree.children.has(this.ref)) return;
  298. const loci = entry ? entry.loci : EmptyLoci;
  299. switch (this.params.entry.params.view.name) {
  300. case 'auto':
  301. this.updateAuto(loci);
  302. break;
  303. case 'selection-box':
  304. this.updateSelectionBox(loci);
  305. break;
  306. default:
  307. this.lastLoci = loci;
  308. break;
  309. }
  310. });
  311. }
  312. unregister() {
  313. let entry = this.cache.entries.first;
  314. while (entry) {
  315. entry.value.data.asset.dispose();
  316. entry = entry.next;
  317. }
  318. }
  319. private isCameraTargetSame(a?: Camera.Snapshot, b?: Camera.Snapshot): boolean {
  320. if (!a || !b) return false;
  321. const targetSame = arrayEqual(a.target, b.target);
  322. const sqDistA = (a.target[0] - a.position[0]) ** 2 + (a.target[1] - a.position[1]) ** 2 + (a.target[2] - a.position[2]) ** 2;
  323. const sqDistB = (b.target[0] - b.position[0]) ** 2 + (b.target[1] - b.position[1]) ** 2 + (b.target[2] - b.position[2]) ** 2;
  324. const distanceSame = Math.abs(sqDistA - sqDistB) / sqDistA < 1e-3;
  325. return targetSame && distanceSame;
  326. }
  327. private cameraTargetDistance(snapshot: Camera.Snapshot): number {
  328. return Vec3.distance(snapshot.target, snapshot.position);
  329. }
  330. private _invTransform: Mat4 = Mat4();
  331. private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
  332. if (Loci.isEmpty(loci) || isEmptyLoci(loci)) {
  333. return Box3D();
  334. }
  335. const parent = this.plugin.helpers.substructureParent.get(loci.structure, true);
  336. if (!parent) return Box3D();
  337. const root = this.getStructureRoot();
  338. if (!root || root.obj?.data !== parent.obj?.data) return Box3D();
  339. const transform = GlobalModelTransformInfo.get(root.obj?.data.models[0]!);
  340. if (transform) Mat4.invert(this._invTransform, transform);
  341. const extendedLoci = StructureElement.Loci.extendToWholeResidues(loci);
  342. const box = StructureElement.Loci.getBoundary(extendedLoci, transform && !Number.isNaN(this._invTransform[0]) ? this._invTransform : void 0).box;
  343. if (StructureElement.Loci.size(extendedLoci) === 1) {
  344. Box3D.expand(box, box, Vec3.create(1, 1, 1));
  345. }
  346. return box;
  347. }
  348. private updateAuto(loci: StructureElement.Loci | EmptyLoci) {
  349. this.updateQueue.enqueue(async () => {
  350. // if (Loci.areEqual(this.lastLoci, loci)) {
  351. // this.lastLoci = EmptyLoci;
  352. // this.updateSelectionBoxParams(Box3D.empty());
  353. // return;
  354. // }
  355. this.lastLoci = loci;
  356. if (isEmptyLoci(loci)) {
  357. this.updateAutoParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
  358. return;
  359. }
  360. const box = this.getBoxFromLoci(loci);
  361. await this.updateAutoParams(box, true);
  362. });
  363. }
  364. private updateSelectionBox(loci: StructureElement.Loci | EmptyLoci) {
  365. this.updateQueue.enqueue(async () => {
  366. if (Loci.areEqual(this.lastLoci, loci)) {
  367. this.lastLoci = EmptyLoci;
  368. this.updateSelectionBoxParams(Box3D());
  369. return;
  370. }
  371. this.lastLoci = loci;
  372. if (isEmptyLoci(loci)) {
  373. this.updateSelectionBoxParams(Box3D());
  374. return;
  375. }
  376. const box = this.getBoxFromLoci(loci);
  377. await this.updateSelectionBoxParams(box);
  378. });
  379. }
  380. private updateCameraTarget(snapshot: Camera.Snapshot) {
  381. this.updateQueue.enqueue(async () => {
  382. const box = this.boxFromCameraTarget(snapshot, true);
  383. await this.updateCameraTargetParams(box);
  384. });
  385. }
  386. private boxFromCameraTarget(snapshot: Camera.Snapshot, boundByBoundarySize: boolean): Box3D {
  387. // TODO QUESTION: what exactly is e.radius and e.radiusMax?
  388. const target = snapshot.target;
  389. const distance = this.cameraTargetDistance(snapshot);
  390. const top = Math.tan(0.5 * snapshot.fov) * distance;
  391. let radius = top;
  392. const viewport = this.plugin.canvas3d?.camera.viewport;
  393. if (viewport && viewport.width > viewport.height) {
  394. radius *= viewport.width / viewport.height;
  395. }
  396. radius *= 0.5; // debug? // TODO Adam remove?
  397. let radiusX, radiusY, radiusZ;
  398. if (boundByBoundarySize) {
  399. let bBoxSize = Vec3.zero();
  400. Box3D.size(bBoxSize, this.data.structure.boundary.box);
  401. radiusX = Math.min(radius, 0.5 * bBoxSize[0]);
  402. radiusY = Math.min(radius, 0.5 * bBoxSize[1]);
  403. radiusZ = Math.min(radius, 0.5 * bBoxSize[2]);
  404. } else {
  405. radiusX = radiusY = radiusZ = radius;
  406. }
  407. return Box3D.create(
  408. Vec3.create(target[0] - radiusX, target[1] - radiusY, target[2] - radiusZ),
  409. Vec3.create(target[0] + radiusX, target[1] + radiusY, target[2] + radiusZ)
  410. );
  411. }
  412. private decideDetail(box: Box3D, baseDetail: number): number {
  413. const cellVolume = this.info.kind === 'x-ray'
  414. ? Box3D.volume(this.data.structure.boundary.box)
  415. : this.info.header.spacegroup.size.reduce((a, b) => a * b, 1);
  416. const boxVolume = Box3D.volume(box);
  417. let ratio = boxVolume / cellVolume;
  418. const maxDetail = this.info.header.availablePrecisions.length - 1;
  419. let detail = baseDetail;
  420. while (ratio <= 0.5 && detail < maxDetail) {
  421. ratio *= 2;
  422. detail += 1;
  423. }
  424. console.log(`decided dynamic detail: ${detail}, (baseDetail: ${baseDetail}, box/cell volume ratio: ${boxVolume/cellVolume})`);
  425. return detail;
  426. }
  427. async update(params: Params) {
  428. 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';
  429. this.params = params;
  430. let box: Box3D | undefined = void 0, emptyData = false;
  431. if (params.entry.params.view.name !== 'camera-target' && this.cameraTargetSubscription) {
  432. this.cameraTargetSubscription.unsubscribe();
  433. this.cameraTargetSubscription = undefined;
  434. }
  435. switch (params.entry.params.view.name) {
  436. case 'off':
  437. emptyData = true;
  438. break;
  439. case 'box':
  440. box = Box3D.create(params.entry.params.view.params.bottomLeft, params.entry.params.view.params.topRight);
  441. emptyData = Box3D.volume(box) < 0.0001;
  442. break;
  443. case 'selection-box': {
  444. if (switchedToSelection) {
  445. box = this.getBoxFromLoci(this.lastLoci) || Box3D();
  446. } else {
  447. box = Box3D.create(Vec3.clone(params.entry.params.view.params.bottomLeft), Vec3.clone(params.entry.params.view.params.topRight));
  448. }
  449. const r = params.entry.params.view.params.radius;
  450. emptyData = Box3D.volume(box) < 0.0001;
  451. Box3D.expand(box, box, Vec3.create(r, r, r));
  452. break;
  453. }
  454. case 'camera-target':
  455. if (!this.cameraTargetSubscription) {
  456. this.cameraTargetSubscription = this.subscribeObservable(this.cameraTargetObservable, (e) => this.updateCameraTarget(e));
  457. }
  458. // TODO QUESTION why should I subscribe here in `update`, when all other views subscribe in `register`?
  459. box = this.boxFromCameraTarget(this.plugin.canvas3d!.camera.getSnapshot(), true);
  460. // console.log('boundary', this.data.structure.boundary.box);
  461. break;
  462. case 'cell':
  463. box = this.info.kind === 'x-ray'
  464. ? this.data.structure.boundary.box
  465. : void 0;
  466. break;
  467. case 'auto':
  468. box = params.entry.params.view.params.isSelection || this.info.kind === 'x-ray'
  469. ? Box3D.create(Vec3.clone(params.entry.params.view.params.bottomLeft), Vec3.clone(params.entry.params.view.params.topRight))
  470. : void 0;
  471. if (box) {
  472. emptyData = Box3D.volume(box) < 0.0001;
  473. if (params.entry.params.view.params.isSelection) {
  474. const r = params.entry.params.view.params.radius;
  475. Box3D.expand(box, box, Vec3.create(r, r, r));
  476. }
  477. }
  478. break;
  479. }
  480. const data = emptyData ? {} : await this.queryData(box);
  481. if (!data) return false;
  482. const info = params.entry.params.channels as ChannelsInfo;
  483. if (this.info.kind === 'x-ray') {
  484. this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || Volume.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
  485. this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || Volume.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]);
  486. this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || Volume.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]);
  487. } else {
  488. this.channels['em'] = this.createChannel(data['EM'] || Volume.One, info['em'], this.info.header.sampling[0].valuesInfo[0]);
  489. }
  490. return true;
  491. }
  492. private createChannel(data: Volume, info: ChannelsInfo['em'], stats: Grid['stats']): ChannelInfo {
  493. const i = info!;
  494. return {
  495. data,
  496. color: i.color,
  497. wireframe: i.wireframe,
  498. opacity: i.opacity,
  499. isoValue: i.isoValue.kind === 'relative' ? i.isoValue : Volume.IsoValue.toRelative(i.isoValue, stats)
  500. };
  501. }
  502. getDescription() {
  503. if (this.params.entry.params.view.name === 'selection-box') return 'Selection';
  504. if (this.params.entry.params.view.name === 'camera-target') return 'Camera';
  505. if (this.params.entry.params.view.name === 'box') return 'Static Box';
  506. if (this.params.entry.params.view.name === 'cell') return 'Cell';
  507. return '';
  508. }
  509. constructor(public plugin: PluginContext, public data: VolumeServerInfo.Data) {
  510. super(plugin, {} as any);
  511. this.infoMap = new Map<string, VolumeServerInfo.EntryData>();
  512. this.data.entries.forEach(info => this.infoMap.set(info.dataId, info));
  513. this.updateQueue = new MonoQueue();
  514. }
  515. }
  516. }
  517. /** Job queue that allows at most one running and one pending job.
  518. * A newly enqueued job will cancel any other pending jobs. */
  519. class MonoQueue {
  520. private isRunning: boolean;
  521. private queue: { id: number, func: () => any }[];
  522. private counter: number;
  523. private log: boolean;
  524. constructor(log: boolean = false) {
  525. this.isRunning = false;
  526. this.queue = [];
  527. this.counter = 0;
  528. this.log = log;
  529. }
  530. enqueue(job: () => any) {
  531. if (this.log) console.log('MonoQueue enqueue', this.counter);
  532. this.queue[0] = { id: this.counter, func: job };
  533. this.counter++;
  534. this.run(); // do not await
  535. }
  536. private async run() {
  537. if (this.isRunning) return;
  538. const job = this.queue.pop();
  539. if (!job) return;
  540. this.isRunning = true;
  541. try {
  542. if (this.log) console.log('MonoQueue run', job.id);
  543. await job.func();
  544. if (this.log) console.log('MonoQueue complete', job.id);
  545. } finally {
  546. this.isRunning = false;
  547. this.run();
  548. }
  549. }
  550. }