index.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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 * as ReactDOM from 'react-dom';
  7. import { Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
  8. import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
  9. import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/model-index';
  10. import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
  11. import { PluginStateObject, PluginStateObject as PSO } from '../../mol-plugin-state/objects';
  12. import { StateTransforms } from '../../mol-plugin-state/transforms';
  13. import { CreateVolumeStreamingInfo, InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
  14. import { PluginCommands } from '../../mol-plugin/commands';
  15. import { PluginContext } from '../../mol-plugin/context';
  16. import { PluginState } from '../../mol-plugin/state';
  17. import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
  18. import { StateBuilder, StateObject, StateSelection } from '../../mol-state';
  19. import { Asset } from '../../mol-util/assets';
  20. import { Color } from '../../mol-util/color';
  21. import { ColorNames } from '../../mol-util/color/names';
  22. import { getFormattedTime } from '../../mol-util/date';
  23. import { download } from '../../mol-util/download';
  24. import { RxEventHelper } from '../../mol-util/rx-event-helper';
  25. import { EvolutionaryConservation } from './annotation';
  26. import { createProteopediaCustomTheme } from './coloring';
  27. import { LoadParams, ModelInfo, RepresentationStyle, StateElements, SupportedFormats } from './helpers';
  28. import './index.html';
  29. import { volumeStreamingControls } from './ui/controls';
  30. require('../../mol-plugin-ui/skin/light.scss');
  31. class MolStarProteopediaWrapper {
  32. static VERSION_MAJOR = 5;
  33. static VERSION_MINOR = 5;
  34. private _ev = RxEventHelper.create();
  35. readonly events = {
  36. modelInfo: this._ev<ModelInfo>()
  37. };
  38. plugin: PluginContext;
  39. init(target: string | HTMLElement, options?: {
  40. customColorList?: number[]
  41. }) {
  42. this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
  43. ...DefaultPluginSpec,
  44. animations: [
  45. AnimateModelIndex
  46. ],
  47. layout: {
  48. initial: {
  49. isExpanded: false,
  50. showControls: false
  51. }
  52. },
  53. components: {
  54. remoteState: 'none'
  55. }
  56. });
  57. const customColoring = createProteopediaCustomTheme((options && options.customColorList) || []);
  58. this.plugin.representation.structure.themes.colorThemeRegistry.add(customColoring);
  59. this.plugin.representation.structure.themes.colorThemeRegistry.add(EvolutionaryConservation.colorThemeProvider!);
  60. this.plugin.managers.lociLabels.addProvider(EvolutionaryConservation.labelProvider!);
  61. this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider, true);
  62. }
  63. get state() {
  64. return this.plugin.state.data;
  65. }
  66. private download(b: StateBuilder.To<PSO.Root>, url: string, isBinary: boolean) {
  67. return b.apply(StateTransforms.Data.Download, { url: Asset.Url(url), isBinary });
  68. }
  69. private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats) {
  70. const parsed = format === 'cif'
  71. ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
  72. : b.apply(StateTransforms.Model.TrajectoryFromPDB);
  73. return parsed
  74. .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: StateElements.Model });
  75. }
  76. private structure(assemblyId: string) {
  77. const model = this.state.build().to(StateElements.Model);
  78. const props = {
  79. type: assemblyId ? {
  80. name: 'assembly' as const,
  81. params: { id: assemblyId }
  82. } : {
  83. name: 'model' as const,
  84. params: { }
  85. }
  86. };
  87. const s = model
  88. .apply(StateTransforms.Model.StructureFromModel, props, { ref: StateElements.Assembly });
  89. s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence });
  90. s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: StateElements.Het });
  91. s.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: StateElements.Water });
  92. return s;
  93. }
  94. private visual(_style?: RepresentationStyle, partial?: boolean) {
  95. const structure = this.getObj<PluginStateObject.Molecule.Structure>(StateElements.Assembly);
  96. if (!structure) return;
  97. const style = _style || { };
  98. const update = this.state.build();
  99. if (!partial || (partial && style.sequence)) {
  100. const root = update.to(StateElements.Sequence);
  101. if (style.sequence && style.sequence.hide) {
  102. root.delete(StateElements.SequenceVisual);
  103. } else {
  104. root.applyOrUpdate(StateElements.SequenceVisual, StateTransforms.Representation.StructureRepresentation3D,
  105. createStructureRepresentationParams(this.plugin, structure, {
  106. type: (style.sequence && style.sequence.kind) || 'cartoon',
  107. color: (style.sequence && style.sequence.coloring) || 'unit-index'
  108. }));
  109. }
  110. }
  111. if (!partial || (partial && style.hetGroups)) {
  112. const root = update.to(StateElements.Het);
  113. if (style.hetGroups && style.hetGroups.hide) {
  114. root.delete(StateElements.HetVisual);
  115. } else {
  116. if (style.hetGroups && style.hetGroups.hide) {
  117. root.delete(StateElements.HetVisual);
  118. } else {
  119. root.applyOrUpdate(StateElements.HetVisual, StateTransforms.Representation.StructureRepresentation3D,
  120. createStructureRepresentationParams(this.plugin, structure, {
  121. type: (style.hetGroups && style.hetGroups.kind) || 'ball-and-stick',
  122. color: style.hetGroups && style.hetGroups.coloring
  123. }));
  124. }
  125. }
  126. }
  127. if (!partial || (partial && style.snfg3d)) {
  128. const root = update.to(StateElements.Het);
  129. if (style.hetGroups && style.hetGroups.hide) {
  130. root.delete(StateElements.HetVisual);
  131. } else {
  132. if (style.snfg3d && style.snfg3d.hide) {
  133. root.delete(StateElements.Het3DSNFG);
  134. } else {
  135. root.applyOrUpdate(StateElements.Het3DSNFG, StateTransforms.Representation.StructureRepresentation3D,
  136. createStructureRepresentationParams(this.plugin, structure, { type: 'carbohydrate' }));
  137. }
  138. }
  139. }
  140. if (!partial || (partial && style.water)) {
  141. const root = update.to(StateElements.Water);
  142. if (style.water && style.water.hide) {
  143. root.delete(StateElements.WaterVisual);
  144. } else {
  145. root.applyOrUpdate(StateElements.WaterVisual, StateTransforms.Representation.StructureRepresentation3D,
  146. createStructureRepresentationParams(this.plugin, structure, {
  147. type: (style.water && style.water.kind) || 'ball-and-stick',
  148. typeParams: { alpha: 0.51 },
  149. color: style.water && style.water.coloring
  150. }));
  151. }
  152. }
  153. return update;
  154. }
  155. private getObj<T extends StateObject>(ref: string): T['data'] {
  156. const state = this.state;
  157. const cell = state.select(ref)[0];
  158. if (!cell || !cell.obj) return void 0;
  159. return (cell.obj as T).data;
  160. }
  161. private async doInfo(checkPreferredAssembly: boolean) {
  162. const model = this.getObj<PluginStateObject.Molecule.Model>('model');
  163. if (!model) return;
  164. const info = await ModelInfo.get(this.plugin, model, checkPreferredAssembly);
  165. this.events.modelInfo.next(info);
  166. return info;
  167. }
  168. private applyState(tree: StateBuilder) {
  169. return PluginCommands.State.Update(this.plugin, { state: this.plugin.state.data, tree });
  170. }
  171. private emptyLoadedParams: LoadParams = { url: '', format: 'cif', isBinary: false, assemblyId: '' };
  172. private loadedParams: LoadParams = { url: '', format: 'cif', isBinary: false, assemblyId: '' };
  173. async load({ url, format = 'cif', assemblyId = '', isBinary = false, representationStyle }: LoadParams) {
  174. let loadType: 'full' | 'update' = 'full';
  175. const state = this.plugin.state.data;
  176. if (this.loadedParams.url !== url || this.loadedParams.format !== format) {
  177. loadType = 'full';
  178. } else if (this.loadedParams.url === url) {
  179. if (state.select(StateElements.Assembly).length > 0) loadType = 'update';
  180. }
  181. if (loadType === 'full') {
  182. await PluginCommands.State.RemoveObject(this.plugin, { state, ref: state.tree.root.ref });
  183. const modelTree = this.model(this.download(state.build().toRoot(), url, isBinary), format);
  184. await this.applyState(modelTree);
  185. const info = await this.doInfo(true);
  186. const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId;
  187. const structureTree = this.structure(asmId);
  188. await this.applyState(structureTree);
  189. } else {
  190. const tree = state.build();
  191. const info = await this.doInfo(true);
  192. const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId;
  193. const props = {
  194. type: assemblyId ? {
  195. name: 'assembly' as const,
  196. params: { id: asmId }
  197. } : {
  198. name: 'model' as const,
  199. params: { }
  200. }
  201. };
  202. tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props }));
  203. await this.applyState(tree);
  204. }
  205. await this.updateStyle(representationStyle);
  206. this.loadedParams = { url, format, assemblyId };
  207. }
  208. async updateStyle(style?: RepresentationStyle, partial?: boolean) {
  209. const tree = this.visual(style, partial);
  210. if (!tree) return;
  211. await PluginCommands.State.Update(this.plugin, { state: this.plugin.state.data, tree });
  212. }
  213. setBackground(color: number) {
  214. if (!this.plugin.canvas3d) return;
  215. const renderer = this.plugin.canvas3d.props.renderer;
  216. PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: Color(color) } } });
  217. }
  218. toggleSpin() {
  219. if (!this.plugin.canvas3d) return;
  220. const trackball = this.plugin.canvas3d.props.trackball;
  221. PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
  222. }
  223. viewport = {
  224. setSettings: (settings?: Canvas3DProps) => {
  225. PluginCommands.Canvas3D.SetSettings(this.plugin, {
  226. settings: settings || DefaultCanvas3DParams
  227. });
  228. }
  229. };
  230. camera = {
  231. toggleSpin: () => this.toggleSpin(),
  232. resetPosition: () => PluginCommands.Camera.Reset(this.plugin, { })
  233. }
  234. animate = {
  235. modelIndex: {
  236. maxFPS: 8,
  237. onceForward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }); },
  238. onceBackward: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }); },
  239. palindrome: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }); },
  240. loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }); },
  241. stop: () => this.plugin.managers.animation.stop()
  242. }
  243. }
  244. coloring = {
  245. evolutionaryConservation: async (params?: { sequence?: boolean, het?: boolean, keepStyle?: boolean }) => {
  246. if (!params || !params.keepStyle) {
  247. await this.updateStyle({ sequence: { kind: 'spacefill' } }, true);
  248. }
  249. const state = this.state;
  250. const tree = state.build();
  251. const colorTheme = { name: EvolutionaryConservation.propertyProvider.descriptor.name, params: this.plugin.representation.structure.themes.colorThemeRegistry.get(EvolutionaryConservation.propertyProvider.descriptor.name).defaultValues };
  252. if (!params || !!params.sequence) {
  253. tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
  254. }
  255. if (params && !!params.het) {
  256. tree.to(StateElements.HetVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
  257. }
  258. await PluginCommands.State.Update(this.plugin, { state, tree });
  259. }
  260. }
  261. private experimentalDataElement?: Element = void 0;
  262. experimentalData = {
  263. init: async (parent: Element) => {
  264. const asm = this.state.select(StateElements.Assembly)[0].obj!;
  265. const params = InitVolumeStreaming.createDefaultParams(asm, this.plugin);
  266. params.options.behaviorRef = StateElements.VolumeStreaming;
  267. params.defaultView = 'box';
  268. params.options.channelParams['fo-fc(+ve)'] = { wireframe: true };
  269. params.options.channelParams['fo-fc(-ve)'] = { wireframe: true };
  270. await this.plugin.runTask(this.state.applyAction(InitVolumeStreaming, params, StateElements.Assembly));
  271. this.experimentalDataElement = parent;
  272. volumeStreamingControls(this.plugin, parent);
  273. },
  274. remove: () => {
  275. const r = this.state.select(StateSelection.Generators.ofTransformer(CreateVolumeStreamingInfo))[0];
  276. if (!r) return;
  277. PluginCommands.State.RemoveObject(this.plugin, { state: this.state, ref: r.transform.ref });
  278. if (this.experimentalDataElement) {
  279. ReactDOM.unmountComponentAtNode(this.experimentalDataElement);
  280. this.experimentalDataElement = void 0;
  281. }
  282. }
  283. }
  284. hetGroups = {
  285. reset: () => {
  286. const update = this.state.build().delete(StateElements.HetGroupFocusGroup);
  287. PluginCommands.State.Update(this.plugin, { state: this.state, tree: update });
  288. PluginCommands.Camera.Reset(this.plugin, { });
  289. },
  290. focusFirst: async (compId: string, options?: { hideLabels: boolean, doNotLabelWaters: boolean }) => {
  291. if (!this.state.transforms.has(StateElements.Assembly)) return;
  292. await PluginCommands.Camera.Reset(this.plugin, { });
  293. const update = this.state.build();
  294. update.delete(StateElements.HetGroupFocusGroup);
  295. const core = MS.struct.filter.first([
  296. MS.struct.generator.atomGroups({
  297. 'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), compId]),
  298. 'group-by': MS.core.str.concat([MS.struct.atomProperty.core.operatorName(), MS.struct.atomProperty.macromolecular.residueKey()])
  299. })
  300. ]);
  301. const surroundings = MS.struct.modifier.includeSurroundings({ 0: core, radius: 5, 'as-whole-residues': true });
  302. const group = update.to(StateElements.Assembly).group(StateTransforms.Misc.CreateGroup, { label: compId }, { ref: StateElements.HetGroupFocusGroup });
  303. const asm = this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure;
  304. const coreSel = group.apply(StateTransforms.Model.StructureSelectionFromExpression, { label: 'Core', expression: core }, { ref: StateElements.HetGroupFocus });
  305. coreSel.apply(StateTransforms.Representation.StructureRepresentation3D, createStructureRepresentationParams(this.plugin, asm.data, {
  306. type: 'ball-and-stick'
  307. }));
  308. coreSel.apply(StateTransforms.Representation.StructureRepresentation3D, createStructureRepresentationParams(this.plugin, asm.data, {
  309. type: 'label',
  310. typeParams: { level: 'element' }
  311. }), { tags: ['proteopedia-labels'] });
  312. group.apply(StateTransforms.Model.StructureSelectionFromExpression, { label: 'Surroundings', expression: surroundings })
  313. .apply(StateTransforms.Representation.StructureRepresentation3D, createStructureRepresentationParams(this.plugin, asm.data, {
  314. type: 'ball-and-stick',
  315. color: 'uniform', colorParams: { value: ColorNames.gray },
  316. size: 'uniform', sizeParams: { value: 0.33 }
  317. }));
  318. if (!options?.hideLabels) {
  319. // Labels
  320. const waters = MS.struct.generator.atomGroups({
  321. 'entity-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.entityType(), 'water']),
  322. });
  323. const exclude = options?.doNotLabelWaters ? MS.struct.combinator.merge([core, waters]) : core;
  324. const onlySurroundings = MS.struct.modifier.exceptBy({ 0: surroundings, by: exclude });
  325. group.apply(StateTransforms.Model.StructureSelectionFromExpression, { label: 'Surroundings (only)', expression: onlySurroundings })
  326. .apply(StateTransforms.Representation.StructureRepresentation3D, createStructureRepresentationParams(this.plugin, asm.data, {
  327. type: 'label',
  328. typeParams: { level: 'residue' }
  329. }), { tags: ['proteopedia-labels'] }); // the tag can later be used to toggle the labels
  330. }
  331. await PluginCommands.State.Update(this.plugin, { state: this.state, tree: update });
  332. const focus = (this.state.select(StateElements.HetGroupFocus)[0].obj as PluginStateObject.Molecule.Structure).data;
  333. const sphere = focus.boundary.sphere;
  334. const radius = Math.max(sphere.radius, 5);
  335. const snapshot = this.plugin.canvas3d!.camera.getFocus(sphere.center, radius);
  336. PluginCommands.Camera.SetSnapshot(this.plugin, { snapshot, durationMs: 250 });
  337. }
  338. }
  339. snapshot = {
  340. get: (params?: PluginState.SnapshotParams) => {
  341. return this.plugin.state.getSnapshot(params);
  342. },
  343. set: (snapshot: PluginState.Snapshot) => {
  344. return this.plugin.state.setSnapshot(snapshot);
  345. },
  346. download: async (type: 'molj' | 'molx' = 'molj', params?: PluginState.SnapshotParams) => {
  347. const data = await this.plugin.managers.snapshot.serialize({ type, params });
  348. download(data, `mol-star_state_${getFormattedTime()}.${type}`);
  349. },
  350. fetch: async (url: string, type: 'molj' | 'molx' = 'molj') => {
  351. try {
  352. const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
  353. this.loadedParams = { ...this.emptyLoadedParams };
  354. return await this.plugin.managers.snapshot.open(new File([data], `state.${type}`));
  355. } catch (e) {
  356. console.log(e);
  357. }
  358. }
  359. }
  360. }
  361. (window as any).MolStarProteopediaWrapper = MolStarProteopediaWrapper;