volume.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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 { PluginUIComponent } from '../base';
  7. import { StateTransformParameters } from '../state/common';
  8. import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
  9. import { ExpandableControlRow, IconButton } from '../controls/common';
  10. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  11. import { ParameterControls, ParamOnChange } from '../controls/parameters';
  12. import { Slider } from '../controls/slider';
  13. import { Volume, Grid } from '../../mol-model/volume';
  14. import { Vec3 } from '../../mol-math/linear-algebra';
  15. import { ColorNames } from '../../mol-util/color/names';
  16. import { toPrecision } from '../../mol-util/number';
  17. import { StateSelection, StateObjectCell } from '../../mol-state';
  18. import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
  19. import { VisibilityOutlinedSvg, VisibilityOffOutlinedSvg } from '../controls/icons';
  20. const ChannelParams = {
  21. color: PD.Color(ColorNames.black, { description: 'Display color of the volume.' }),
  22. wireframe: PD.Boolean(false, { description: 'Control display of the volume as a wireframe.' }),
  23. opacity: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 }, { description: 'Opacity of the volume.' })
  24. };
  25. type ChannelParams = PD.Values<typeof ChannelParams>
  26. const Bounds = new Map<VolumeStreaming.ChannelType, [number, number]>([
  27. ['em', [-5, 5]],
  28. ['2fo-fc', [0, 3]],
  29. ['fo-fc(+ve)', [1, 5]],
  30. ['fo-fc(-ve)', [-5, -1]],
  31. ]);
  32. class Channel extends PluginUIComponent<{
  33. label: string,
  34. name: VolumeStreaming.ChannelType,
  35. channels: { [k: string]: VolumeStreaming.ChannelParams },
  36. isRelative: boolean,
  37. params: StateTransformParameters.Props,
  38. stats: Grid['stats'],
  39. changeIso: (name: string, value: number, isRelative: boolean) => void,
  40. changeParams: (name: string, param: string, value: any) => void,
  41. bCell: StateObjectCell,
  42. isDisabled?: boolean,
  43. isUnbounded?: boolean
  44. }> {
  45. private ref = StateSelection.findTagInSubtree(this.plugin.state.data.tree, this.props.bCell!.transform.ref, this.props.name);
  46. componentDidUpdate() {
  47. this.ref = StateSelection.findTagInSubtree(this.plugin.state.data.tree, this.props.bCell!.transform.ref, this.props.name);
  48. }
  49. componentDidMount() {
  50. this.subscribe(this.plugin.state.data.events.cell.stateUpdated, e => {
  51. if (this.ref === e.ref) this.forceUpdate();
  52. });
  53. }
  54. getVisible = () => {
  55. const state = this.plugin.state.data;
  56. const ref = this.ref;
  57. if (!ref) return false;
  58. return !state.cells.get(ref)!.state.isHidden;
  59. };
  60. toggleVisible = () => {
  61. const state = this.plugin.state.data;
  62. const ref = this.ref;
  63. if (!ref) return;
  64. setSubtreeVisibility(state, ref, !state.cells.get(ref)!.state.isHidden);
  65. };
  66. render() {
  67. const props = this.props;
  68. const { isRelative, stats } = props;
  69. const channel = props.channels[props.name]!;
  70. const { min, max, mean, sigma } = stats;
  71. const value = Math.round(100 * (channel.isoValue.kind === 'relative' ? channel.isoValue.relativeValue : channel.isoValue.absoluteValue)) / 100;
  72. let relMin = (min - mean) / sigma;
  73. let relMax = (max - mean) / sigma;
  74. if (!this.props.isUnbounded) {
  75. const bounds = Bounds.get(this.props.name)!;
  76. if (this.props.name === 'em') {
  77. relMin = Math.max(bounds[0], relMin);
  78. relMax = Math.min(bounds[1], relMax);
  79. } else {
  80. relMin = bounds[0];
  81. relMax = bounds[1];
  82. }
  83. }
  84. const vMin = mean + sigma * relMin, vMax = mean + sigma * relMax;
  85. const step = toPrecision(isRelative ? Math.round(((vMax - vMin) / sigma)) / 100 : sigma / 100, 2);
  86. const ctrlMin = isRelative ? relMin : vMin;
  87. const ctrlMax = isRelative ? relMax : vMax;
  88. return <ExpandableControlRow
  89. label={props.label + (props.isRelative ? ' \u03C3' : '')}
  90. colorStripe={channel.color}
  91. pivot={<div className='msp-volume-channel-inline-controls'>
  92. <Slider value={value} min={ctrlMin} max={ctrlMax} step={step}
  93. onChange={v => props.changeIso(props.name, v, isRelative)} onChangeImmediate={v => props.changeIso(props.name, v, isRelative)} disabled={props.params.isDisabled} onEnter={props.params.events.onEnter} />
  94. <IconButton svg={this.getVisible() ? VisibilityOutlinedSvg : VisibilityOffOutlinedSvg} onClick={this.toggleVisible} toggleState={false} disabled={props.params.isDisabled} />
  95. </div>}
  96. controls={<ParameterControls onChange={({ name, value }) => props.changeParams(props.name, name, value)} params={ChannelParams} values={channel} onEnter={props.params.events.onEnter} isDisabled={props.params.isDisabled} />}
  97. />;
  98. }
  99. }
  100. export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransformParameters.Props> {
  101. private areInitial(params: any) {
  102. return PD.areEqual(this.props.info.params, params, this.props.info.initialValues);
  103. }
  104. private newParams(params: VolumeStreaming.Params) {
  105. this.props.events.onChange(params, this.areInitial(params));
  106. }
  107. changeIso = (name: string, value: number, isRelative: boolean) => {
  108. const old = this.props.params as VolumeStreaming.Params;
  109. this.newParams({
  110. ...old,
  111. entry: {
  112. name: old.entry.name,
  113. params: {
  114. ...old.entry.params,
  115. channels: {
  116. ...old.entry.params.channels,
  117. [name]: {
  118. ...(old.entry.params.channels as any)[name],
  119. isoValue: isRelative ? Volume.IsoValue.relative(value) : Volume.IsoValue.absolute(value)
  120. }
  121. }
  122. }
  123. }
  124. });
  125. };
  126. changeParams = (name: string, param: string, value: any) => {
  127. const old = this.props.params;
  128. this.newParams({
  129. ...old,
  130. entry: {
  131. name: old.entry.name,
  132. params: {
  133. ...old.entry.params,
  134. channels: {
  135. ...old.entry.params.channels,
  136. [name]: {
  137. ...(old.entry.params.channels as any)[name],
  138. [param]: value
  139. }
  140. }
  141. }
  142. }
  143. });
  144. };
  145. convert(channel: any, stats: Grid['stats'], isRelative: boolean) {
  146. return {
  147. ...channel, isoValue: isRelative
  148. ? Volume.IsoValue.toRelative(channel.isoValue, stats)
  149. : Volume.IsoValue.toAbsolute(channel.isoValue, stats)
  150. };
  151. }
  152. changeOption: ParamOnChange = ({ name, value }) => {
  153. const old = this.props.params as VolumeStreaming.Params;
  154. if (name === 'entry') {
  155. this.newParams({
  156. ...old,
  157. entry: {
  158. name: value,
  159. params: old.entry.params,
  160. }
  161. });
  162. } else {
  163. const b = (this.props.b as VolumeStreaming).data;
  164. const isEM = b.info.kind === 'em';
  165. const isRelative = value.params.isRelative;
  166. const sampling = b.info.header.sampling[0];
  167. const oldChannels = old.entry.params.channels as any;
  168. const oldView = old.entry.params.view.name === value.name
  169. ? old.entry.params.view.params
  170. : (((this.props.info.params as VolumeStreaming.ParamDefinition)
  171. .entry.map(old.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>)
  172. .params as VolumeStreaming.EntryParamDefinition)
  173. .view.map(value.name).defaultValue;
  174. const viewParams = { ...oldView };
  175. if (value.name === 'selection-box') {
  176. viewParams.radius = value.params.radius;
  177. } else if (value.name === 'camera-target') {
  178. viewParams.radius = value.params.radius;
  179. viewParams.dynamicDetailLevel = value.params.dynamicDetailLevel;
  180. } else if (value.name === 'box') {
  181. viewParams.bottomLeft = value.params.bottomLeft;
  182. viewParams.topRight = value.params.topRight;
  183. } else if (value.name === 'auto') {
  184. viewParams.radius = value.params.radius;
  185. viewParams.selectionDetailLevel = value.params.selectionDetailLevel;
  186. }
  187. viewParams.isUnbounded = !!value.params.isUnbounded;
  188. this.newParams({
  189. ...old,
  190. entry: {
  191. name: old.entry.name,
  192. params: {
  193. ...old.entry.params,
  194. view: {
  195. name: value.name,
  196. params: viewParams
  197. },
  198. detailLevel: value.params.detailLevel,
  199. channels: isEM
  200. ? { em: this.convert(oldChannels.em, sampling.valuesInfo[0], isRelative) }
  201. : {
  202. '2fo-fc': this.convert(oldChannels['2fo-fc'], sampling.valuesInfo[0], isRelative),
  203. 'fo-fc(+ve)': this.convert(oldChannels['fo-fc(+ve)'], sampling.valuesInfo[1], isRelative),
  204. 'fo-fc(-ve)': this.convert(oldChannels['fo-fc(-ve)'], sampling.valuesInfo[1], isRelative)
  205. }
  206. }
  207. }
  208. });
  209. }
  210. };
  211. render() {
  212. if (!this.props.b) return null;
  213. const b = (this.props.b as VolumeStreaming).data;
  214. const isEM = b.info.kind === 'em';
  215. const pivot = isEM ? 'em' : '2fo-fc';
  216. const params = this.props.params as VolumeStreaming.Params;
  217. const entry = (this.props.info.params as VolumeStreaming.ParamDefinition)
  218. .entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>;
  219. const detailLevel = entry.params.detailLevel;
  220. // const dynamicDetailLevel = (params.entry.params.view.params as any).dynamicDetailLevel;
  221. // const selectionDetailLevel = (params.entry.params.view.params as any).selectionDetailLevel;
  222. // console.log('params:', params);
  223. // console.log('entry.params:', entry.params);
  224. // console.log('entry:', (this.props.info.params as VolumeStreaming.ParamDefinition).entry);
  225. // console.log('detailLevel:', detailLevel);
  226. // console.log('dynamicDetailLevel:', dynamicDetailLevel);
  227. // console.log('selectionDetailLevel:', selectionDetailLevel);
  228. // TODO Adam: somehow try to get the correct mdfkn dynamicDetailLevel value
  229. const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
  230. const sampling = b.info.header.sampling[0];
  231. const isRelativeParam = PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' });
  232. const isUnbounded = !!(params.entry.params.view.params as any).isUnbounded;
  233. const isUnboundedParam = PD.Boolean(isUnbounded, { description: 'Show full/limited range of iso-values for more fine-grained control.', label: 'Unbounded' });
  234. const isOff = params.entry.params.view.name === 'off';
  235. // TODO: factor common things out, cache
  236. const OptionsParams = {
  237. entry: PD.Select(params.entry.name, b.data.entries.map(info => [info.dataId, info.dataId] as [string, string]), { isHidden: isOff, description: 'Which entry with volume data to display.' }),
  238. view: PD.MappedStatic(params.entry.params.view.name, {
  239. 'off': PD.Group({
  240. isRelative: PD.Boolean(isRelative, { isHidden: true }),
  241. isUnbounded: PD.Boolean(isUnbounded, { isHidden: true }),
  242. }, { description: 'Display off.' }),
  243. 'box': PD.Group({
  244. bottomLeft: PD.Vec3(Vec3.zero()),
  245. topRight: PD.Vec3(Vec3.zero()),
  246. detailLevel,
  247. isRelative: isRelativeParam,
  248. isUnbounded: isUnboundedParam,
  249. }, { description: 'Static box defined by cartesian coords.' }),
  250. 'selection-box': PD.Group({
  251. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  252. detailLevel,
  253. isRelative: isRelativeParam,
  254. isUnbounded: isUnboundedParam,
  255. }, { description: 'Box around focused element.' }),
  256. 'camera-target': PD.Group({
  257. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  258. detailLevel,
  259. dynamicDetailLevel: { ...detailLevel },
  260. isRelative: isRelativeParam,
  261. isUnbounded: isUnboundedParam,
  262. }, { description: 'Box around camera target.' }),
  263. 'cell': PD.Group({
  264. detailLevel,
  265. isRelative: isRelativeParam,
  266. isUnbounded: isUnboundedParam,
  267. }, { description: 'Box around the structure\'s bounding box.' }),
  268. 'auto': PD.Group({
  269. radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
  270. detailLevel,
  271. selectionDetailLevel: { ...detailLevel, label: 'Selection Detail' },
  272. isRelative: isRelativeParam,
  273. isUnbounded: isUnboundedParam,
  274. }, { description: 'Box around focused element.' }),
  275. }, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Around Camera" shows the volume around the point the camera is targeting. "Whole Structure" shows the volume for the whole structure.' })
  276. };
  277. const options = {
  278. entry: params.entry.name,
  279. view: {
  280. name: params.entry.params.view.name,
  281. params: {
  282. detailLevel: params.entry.params.detailLevel,
  283. radius: (params.entry.params.view.params as any).radius,
  284. bottomLeft: (params.entry.params.view.params as any).bottomLeft,
  285. topRight: (params.entry.params.view.params as any).topRight,
  286. selectionDetailLevel: (params.entry.params.view.params as any).selectionDetailLevel,
  287. dynamicDetailLevel: (params.entry.params.view.params as any).dynamicDetailLevel,
  288. isRelative,
  289. isUnbounded
  290. }
  291. }
  292. };
  293. if (isOff) {
  294. return <ParameterControls onChange={this.changeOption} params={OptionsParams} values={options} onEnter={this.props.events.onEnter} isDisabled={this.props.isDisabled} />;
  295. }
  296. return <>
  297. {!isEM && <Channel label='2Fo-Fc' name='2fo-fc' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} isUnbounded={isUnbounded} />}
  298. {!isEM && <Channel label='Fo-Fc(+ve)' name='fo-fc(+ve)' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} isUnbounded={isUnbounded} />}
  299. {!isEM && <Channel label='Fo-Fc(-ve)' name='fo-fc(-ve)' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} isUnbounded={isUnbounded} />}
  300. {isEM && <Channel label='EM' name='em' bCell={this.props.bCell!} channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} isUnbounded={isUnbounded} />}
  301. <ParameterControls onChange={this.changeOption} params={OptionsParams} values={options} onEnter={this.props.events.onEnter} isDisabled={this.props.isDisabled} />
  302. </>;
  303. }
  304. }