index.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /**
  2. * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author Joan Segura <joan.segura@rcsb.org>
  6. */
  7. import { BehaviorSubject } from 'rxjs';
  8. import { Plugin } from 'molstar/lib/mol-plugin-ui/plugin';
  9. import { PluginContext } from 'molstar/lib/mol-plugin/context';
  10. import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
  11. import { ViewerState as ViewerState, CollapsedState, ModelUrlProvider } from './types';
  12. import { PluginSpec } from 'molstar/lib/mol-plugin/spec';
  13. import { ColorNames } from 'molstar/lib/mol-util/color/names';
  14. import * as React from 'react';
  15. import * as ReactDOM from 'react-dom';
  16. import { ModelLoader } from './helpers/model';
  17. import { PresetProps } from './helpers/preset';
  18. import { ControlsWrapper } from './ui/controls';
  19. import { PluginConfig } from 'molstar/lib/mol-plugin/config';
  20. import { RCSBAssemblySymmetry } from 'molstar/lib/extensions/rcsb/assembly-symmetry/behavior';
  21. import { RCSBValidationReport } from 'molstar/lib/extensions/rcsb/validation-report/behavior';
  22. import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
  23. import { PluginState } from 'molstar/lib/mol-plugin/state';
  24. import { BuiltInTrajectoryFormat } from 'molstar/lib/mol-plugin-state/formats/trajectory';
  25. import { ObjectKeys } from 'molstar/lib/mol-util/type-helpers';
  26. import { PluginLayoutControlsDisplay } from 'molstar/lib/mol-plugin/layout';
  27. import { SuperposeColorThemeProvider } from './helpers/superpose/color';
  28. import { encodeStructureData, downloadAsZipFile } from './helpers/export';
  29. import { ViewerMethods } from './helpers/viewer';
  30. import { StructureRef } from 'molstar/lib/mol-plugin-state/manager/structure/hierarchy-state';
  31. import { StructureRepresentationRegistry } from 'molstar/lib/mol-repr/structure/registry';
  32. import { Mp4Export } from 'molstar/lib/extensions/mp4-export';
  33. import { DefaultPluginUISpec, PluginUISpec } from 'molstar/lib/mol-plugin-ui/spec';
  34. import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
  35. import { ANVILMembraneOrientation, MembraneOrientationPreset } from 'molstar/lib/extensions/anvil/behavior';
  36. import { MembraneOrientationRepresentationProvider } from 'molstar/lib/extensions/anvil/representation';
  37. import { MotifAlignmentRequest, alignMotifs } from './helpers/superpose/pecos-integration';
  38. /** package version, filled in at bundle build time */
  39. declare const __RCSB_MOLSTAR_VERSION__: string;
  40. export const RCSB_MOLSTAR_VERSION = typeof __RCSB_MOLSTAR_VERSION__ != 'undefined' ? __RCSB_MOLSTAR_VERSION__ : 'none';
  41. /** unix time stamp, to be filled in at bundle build time */
  42. declare const __BUILD_TIMESTAMP__: number;
  43. export const BUILD_TIMESTAMP = typeof __BUILD_TIMESTAMP__ != 'undefined' ? __BUILD_TIMESTAMP__ : 'none';
  44. export const BUILD_DATE = new Date(BUILD_TIMESTAMP);
  45. const Extensions = {
  46. 'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
  47. 'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
  48. 'mp4-export': PluginSpec.Behavior(Mp4Export),
  49. 'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation)
  50. };
  51. const DefaultViewerProps = {
  52. showImportControls: false,
  53. showExportControls: false,
  54. showSessionControls: false,
  55. showStructureSourceControls: true,
  56. showSuperpositionControls: true,
  57. showMembraneOrientationPreset: false,
  58. /**
  59. * Needed when running outside of sierra. If set to true, the strucmotif UI will use an absolute URL to sierra-prod.
  60. * Otherwise, the link will be relative on the current host.
  61. */
  62. detachedFromSierra: false,
  63. modelUrlProviders: [
  64. (pdbId: string) => ({
  65. url: `https://models.rcsb.org/${pdbId.toLowerCase()}.bcif`,
  66. format: 'mmcif',
  67. isBinary: true
  68. }),
  69. (pdbId: string) => ({
  70. url: `https://files.rcsb.org/download/${pdbId.toLowerCase()}.cif`,
  71. format: 'mmcif',
  72. isBinary: false
  73. })
  74. ] as ModelUrlProvider[],
  75. extensions: ObjectKeys(Extensions),
  76. layoutIsExpanded: false,
  77. layoutShowControls: true,
  78. layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
  79. layoutShowSequence: true,
  80. layoutShowLog: false,
  81. viewportShowExpand: true,
  82. viewportShowSelectionMode: true,
  83. volumeStreamingServer: 'https://maps.rcsb.org/',
  84. backgroundColor: ColorNames.white,
  85. showWelcomeToast: true
  86. };
  87. export type ViewerProps = typeof DefaultViewerProps
  88. export class Viewer {
  89. private readonly plugin: PluginUIContext;
  90. private readonly modelUrlProviders: ModelUrlProvider[];
  91. private get customState() {
  92. return this.plugin.customState as ViewerState;
  93. }
  94. constructor(elementOrId: string | HTMLElement, props: Partial<ViewerProps> = {}) {
  95. const element = typeof elementOrId === 'string' ? document.getElementById(elementOrId)! : elementOrId;
  96. if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
  97. const o = { ...DefaultViewerProps, ...props };
  98. const defaultSpec = DefaultPluginUISpec();
  99. const spec: PluginUISpec = {
  100. ...defaultSpec,
  101. actions: defaultSpec.actions,
  102. behaviors: [
  103. ...defaultSpec.behaviors,
  104. ...o.extensions.map(e => Extensions[e]),
  105. ],
  106. animations: [...defaultSpec.animations || []],
  107. layout: {
  108. initial: {
  109. isExpanded: o.layoutIsExpanded,
  110. showControls: o.layoutShowControls,
  111. controlsDisplay: o.layoutControlsDisplay,
  112. },
  113. },
  114. components: {
  115. ...defaultSpec.components,
  116. controls: {
  117. ...defaultSpec.components?.controls,
  118. top: o.layoutShowSequence ? undefined : 'none',
  119. bottom: o.layoutShowLog ? undefined : 'none',
  120. left: 'none',
  121. right: ControlsWrapper,
  122. },
  123. remoteState: 'none',
  124. },
  125. config: [
  126. [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
  127. [PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
  128. [PluginConfig.Viewport.ShowAnimation, false],
  129. [PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
  130. [PluginConfig.Download.DefaultPdbProvider, 'rcsb'],
  131. [PluginConfig.Download.DefaultEmdbProvider, 'rcsb']
  132. ]
  133. };
  134. this.plugin = new PluginUIContext(spec);
  135. this.modelUrlProviders = o.modelUrlProviders;
  136. (this.plugin.customState as ViewerState) = {
  137. showImportControls: o.showImportControls,
  138. showExportControls: o.showExportControls,
  139. showSessionControls: o.showSessionControls,
  140. showStructureSourceControls: o.showStructureSourceControls,
  141. showSuperpositionControls: o.showSuperpositionControls,
  142. modelLoader: new ModelLoader(this.plugin),
  143. collapsed: new BehaviorSubject<CollapsedState>({
  144. selection: true,
  145. strucmotifSubmit: true,
  146. measurements: true,
  147. superposition: true,
  148. component: false,
  149. volume: true,
  150. custom: true
  151. }),
  152. detachedFromSierra: o.detachedFromSierra
  153. };
  154. this.plugin.init()
  155. .then(async () => {
  156. // hide 'Membrane Orientation' preset from UI - has to happen 'before' react render, apparently
  157. if (!o.showMembraneOrientationPreset) {
  158. this.plugin.builders.structure.representation.unregisterPreset(MembraneOrientationPreset);
  159. this.plugin.representation.structure.registry.remove(MembraneOrientationRepresentationProvider);
  160. }
  161. ReactDOM.render(React.createElement(Plugin, { plugin: this.plugin }), element);
  162. const renderer = this.plugin.canvas3d!.props.renderer;
  163. await PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: o.backgroundColor } } });
  164. this.plugin.representation.structure.themes.colorThemeRegistry.add(SuperposeColorThemeProvider);
  165. if (o.showWelcomeToast) {
  166. await PluginCommands.Toast.Show(this.plugin, {
  167. title: 'Welcome',
  168. message: `RCSB PDB Mol* Viewer ${RCSB_MOLSTAR_VERSION} [${BUILD_DATE.toLocaleString()}]`,
  169. key: 'toast-welcome',
  170. timeoutMs: 5000
  171. });
  172. }
  173. this.prevExpanded = this.plugin.layout.state.isExpanded;
  174. this.plugin.layout.events.updated.subscribe(() => this.toggleControls());
  175. });
  176. }
  177. private prevExpanded: boolean;
  178. private toggleControls(): void {
  179. const currExpanded = this.plugin.layout.state.isExpanded;
  180. const expandedChanged = (this.prevExpanded !== currExpanded);
  181. if (!expandedChanged) return;
  182. if (currExpanded && !this.plugin.layout.state.showControls) {
  183. this.plugin.layout.setProps({showControls: true});
  184. } else if (!currExpanded && this.plugin.layout.state.showControls) {
  185. this.plugin.layout.setProps({showControls: false});
  186. }
  187. this.prevExpanded = this.plugin.layout.state.isExpanded;
  188. }
  189. //
  190. resetCamera(durationMs?: number) {
  191. this.plugin.managers.camera.reset(undefined, durationMs);
  192. }
  193. clear() {
  194. const state = this.plugin.state.data;
  195. return PluginCommands.State.RemoveObject(this.plugin, { state, ref: state.tree.root.ref });
  196. }
  197. async loadPdbId(pdbId: string, props?: PresetProps, matrix?: Mat4) {
  198. for (const provider of this.modelUrlProviders) {
  199. try {
  200. const p = provider(pdbId);
  201. await this.customState.modelLoader.load({ fileOrUrl: p.url, format: p.format, isBinary: p.isBinary }, props, matrix);
  202. break;
  203. } catch (e) {
  204. console.warn(`loading '${pdbId}' failed with '${e}', trying next model-loader-provider`);
  205. }
  206. }
  207. }
  208. async loadPdbIds(args: { pdbId: string, props?: PresetProps, matrix?: Mat4 }[]) {
  209. for (const { pdbId, props, matrix } of args) {
  210. await this.loadPdbId(pdbId, props, matrix);
  211. }
  212. this.resetCamera(0);
  213. }
  214. async alignMotifs(request: MotifAlignmentRequest) {
  215. const { query, hits } = request;
  216. await this.loadPdbId(query.entry_id,
  217. {
  218. kind: 'motif',
  219. label: query.entry_id,
  220. targets: query.residue_ids
  221. });
  222. for (const hit of hits) {
  223. const { rmsd, matrix } = await alignMotifs(query, hit);
  224. await this.loadPdbId(hit.entry_id, {
  225. kind: 'motif',
  226. assemblyId: hit.assembly_id,
  227. label: `${hit.entry_id} #${hit.id}: ${rmsd.toFixed(2)} RMSD`,
  228. targets: hit.residue_ids
  229. }, matrix);
  230. this.resetCamera(0);
  231. }
  232. }
  233. loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat, isBinary: boolean, props?: PresetProps, matrix?: Mat4) {
  234. return this.customState.modelLoader.load({ fileOrUrl: url, format, isBinary }, props, matrix);
  235. }
  236. loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {
  237. return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
  238. }
  239. async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, isBinary: boolean, props?: PresetProps & { dataLabel?: string }, matrix?: Mat4) {
  240. return this.customState.modelLoader.parse({ data, format, isBinary }, props, matrix);
  241. }
  242. handleResize() {
  243. this.plugin.layout.events.updated.next(void 0);
  244. }
  245. exportLoadedStructures() {
  246. const content = encodeStructureData(this.plugin);
  247. return downloadAsZipFile(this.plugin, content);
  248. }
  249. pluginCall(f: (plugin: PluginContext) => void){
  250. f(this.plugin);
  251. }
  252. public getPlugin(): PluginContext {
  253. return this.plugin;
  254. }
  255. public setFocus(modelId: string, asymId: string, begin: number, end: number): void;
  256. public setFocus(...args: any[]): void{
  257. if(args.length === 4)
  258. ViewerMethods.setFocusFromRange(this.plugin, args[0], args[1], args[2], args[3]);
  259. if(args.length === 2)
  260. ViewerMethods.setFocusFromSet(this.plugin, args[0], args[1]);
  261. }
  262. public clearFocus(): void {
  263. this.plugin.managers.structure.focus.clear();
  264. }
  265. public select(selection: Array<{modelId: string; asymId: string; position: number;}>, mode: 'select'|'hover', modifier: 'add'|'set'): void;
  266. public select(selection: Array<{modelId: string; asymId: string; begin: number; end: number;}>, mode: 'select'|'hover', modifier: 'add'|'set'): void;
  267. public select(modelId: string, asymId: string, position: number, mode: 'select'|'hover', modifier: 'add'|'set'): void;
  268. public select(modelId: string, asymId: string, begin: number, end: number, mode: 'select'|'hover', modifier: 'add'|'set'): void;
  269. public select(...args: any[]){
  270. if(args.length === 3 && (args[0] as Array<{modelId: string; asymId: string; position: number;}>).length > 0 && typeof (args[0] as Array<{modelId: string; asymId: string; position: number;}>)[0].position === 'number'){
  271. if(args[2] === 'set')
  272. this.clearSelection('select');
  273. (args[0] as Array<{modelId: string; asymId: string; position: number;}>).forEach(r=>{
  274. ViewerMethods.selectSegment(this.plugin, r.modelId, r.asymId, r.position, r.position, args[1], 'add');
  275. });
  276. }else if(args.length === 3 && (args[0] as Array<{modelId: string; asymId: string; begin: number; end: number;}>).length > 0 && typeof (args[0] as Array<{modelId: string; asymId: string; begin: number; end: number;}>)[0].begin === 'number'){
  277. ViewerMethods.selectMultipleSegments(this.plugin, args[0], args[1], args[2]);
  278. }else if(args.length === 5){
  279. ViewerMethods.selectSegment(this.plugin, args[0], args[1], args[2], args[2], args[3], args[4]);
  280. }else if(args.length === 6){
  281. ViewerMethods.selectSegment(this.plugin, args[0], args[1], args[2], args[3], args[4], args[5]);
  282. }
  283. }
  284. public clearSelection(mode: 'select'|'hover', options?: {modelId: string; labelAsymId: string;}): void {
  285. ViewerMethods.clearSelection(this.plugin, mode, options);
  286. }
  287. public async createComponent(componentLabel: string, modelId: string, asymId: string, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>;
  288. public async createComponent(componentLabel: string, modelId: string, residues: Array<{asymId: string; position: number;}>, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>;
  289. public async createComponent(componentLabel: string, modelId: string, residues: Array<{asymId: string; begin: number; end: number;}>, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>;
  290. public async createComponent(componentLabel: string, modelId: string, asymId: string, begin: number, end: number, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>;
  291. public async createComponent(...args: any[]): Promise<void>{
  292. const structureRef: StructureRef | undefined = ViewerMethods.getStructureRefWithModelId(this.plugin.managers.structure.hierarchy.current.structures, args[1]);
  293. if(structureRef == null)
  294. throw 'createComponent error: model not found';
  295. if (args.length === 4 && typeof args[2] === 'string') {
  296. await ViewerMethods.createComponentFromChain(this.plugin, args[0], structureRef, args[2], args[3]);
  297. } else if (args.length === 4 && args[2] instanceof Array && args[2].length > 0 && typeof args[2][0].position === 'number') {
  298. await ViewerMethods.createComponentFromSet(this.plugin, args[0], structureRef, args[2], args[3]);
  299. } else if (args.length === 4 && args[2] instanceof Array && args[2].length > 0 && typeof args[2][0].begin === 'number') {
  300. await ViewerMethods.createComponentFromMultipleRange(this.plugin, args[0], structureRef, args[2], args[3]);
  301. }else if (args.length === 6) {
  302. await ViewerMethods.createComponentFromRange(this.plugin, args[0], structureRef, args[2], args[3], args[4], args[5]);
  303. }
  304. }
  305. public removeComponent(componentLabel: string): void{
  306. ViewerMethods.removeComponent(this.plugin, componentLabel);
  307. }
  308. }