index.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. */
  6. import { BehaviorSubject } from 'rxjs';
  7. import { DefaultPluginSpec } from 'molstar/lib/mol-plugin';
  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 {Structure} from 'molstar/lib/mol-model/structure/structure';
  30. import {Script} from 'molstar/lib/mol-script/script';
  31. import {MolScriptBuilder} from 'molstar/lib/mol-script/language/builder';
  32. import {SetUtils} from 'molstar/lib/mol-util/set';
  33. import {Loci} from 'molstar/lib/mol-model/loci';
  34. import {StructureSelection} from 'molstar/lib/mol-model/structure/query';
  35. import {StructureRef} from 'molstar/lib/mol-plugin-state/manager/structure/hierarchy-state';
  36. import {StructureSelectionQuery} from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query';
  37. import {StructureRepresentationRegistry} from 'molstar/lib/mol-repr/structure/registry';
  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. };
  49. const DefaultViewerProps = {
  50. showImportControls: false,
  51. showExportControls: false,
  52. showSessionControls: false,
  53. showStructureSourceControls: true,
  54. showSuperpositionControls: true,
  55. modelUrlProviders: [
  56. (pdbId: string) => ({
  57. url: `//models.rcsb.org/${pdbId.toLowerCase()}.bcif`,
  58. format: 'mmcif',
  59. isBinary: true
  60. }),
  61. (pdbId: string) => ({
  62. url: `//files.rcsb.org/download/${pdbId.toLowerCase()}.cif`,
  63. format: 'mmcif',
  64. isBinary: false
  65. })
  66. ] as ModelUrlProvider[],
  67. extensions: ObjectKeys(Extensions),
  68. layoutIsExpanded: false,
  69. layoutShowControls: true,
  70. layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
  71. layoutShowSequence: true,
  72. layoutShowLog: false,
  73. viewportShowExpand: true,
  74. viewportShowSelectionMode: true,
  75. volumeStreamingServer: '//maps.rcsb.org/',
  76. backgroundColor: ColorNames.white,
  77. showWelcomeToast: true
  78. };
  79. export type ViewerProps = typeof DefaultViewerProps
  80. export class Viewer {
  81. private readonly plugin: PluginContext;
  82. private readonly modelUrlProviders: ModelUrlProvider[];
  83. private get customState() {
  84. return this.plugin.customState as ViewerState;
  85. }
  86. constructor(target: string | HTMLElement, props: Partial<ViewerProps> = {}) {
  87. target = typeof target === 'string' ? document.getElementById(target)! : target;
  88. const o = { ...DefaultViewerProps, ...props };
  89. const spec: PluginSpec = {
  90. actions: [...DefaultPluginSpec.actions],
  91. behaviors: [
  92. ...DefaultPluginSpec.behaviors,
  93. ...o.extensions.map(e => Extensions[e]),
  94. ],
  95. animations: [...DefaultPluginSpec.animations || []],
  96. customParamEditors: DefaultPluginSpec.customParamEditors,
  97. layout: {
  98. initial: {
  99. isExpanded: o.layoutIsExpanded,
  100. showControls: o.layoutShowControls,
  101. controlsDisplay: o.layoutControlsDisplay,
  102. },
  103. controls: {
  104. ...DefaultPluginSpec.layout && DefaultPluginSpec.layout.controls,
  105. top: o.layoutShowSequence ? undefined : 'none',
  106. bottom: o.layoutShowLog ? undefined : 'none',
  107. // left: 'none',
  108. right: ControlsWrapper,
  109. }
  110. },
  111. components: {
  112. ...DefaultPluginSpec.components,
  113. remoteState: 'none',
  114. },
  115. config: [
  116. [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
  117. [PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
  118. [PluginConfig.Viewport.ShowAnimation, false],
  119. [PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
  120. [PluginConfig.Download.DefaultPdbProvider, 'rcsb'],
  121. [PluginConfig.Download.DefaultEmdbProvider, 'rcsb']
  122. ]
  123. };
  124. this.plugin = new PluginContext(spec);
  125. this.modelUrlProviders = o.modelUrlProviders;
  126. (this.plugin.customState as ViewerState) = {
  127. showImportControls: o.showImportControls,
  128. showExportControls: o.showExportControls,
  129. showSessionControls: o.showSessionControls,
  130. showStructureSourceControls: o.showStructureSourceControls,
  131. showSuperpositionControls: o.showSuperpositionControls,
  132. modelLoader: new ModelLoader(this.plugin),
  133. collapsed: new BehaviorSubject<CollapsedState>({
  134. selection: true,
  135. strucmotifSubmit: true,
  136. measurements: true,
  137. superposition: true,
  138. component: false,
  139. volume: true,
  140. custom: true,
  141. })
  142. };
  143. this.plugin.init();
  144. ReactDOM.render(React.createElement(Plugin, { plugin: this.plugin }), target);
  145. const renderer = this.plugin.canvas3d!.props.renderer;
  146. PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: o.backgroundColor } } });
  147. this.plugin.representation.structure.themes.colorThemeRegistry.add(SuperposeColorThemeProvider);
  148. // this.plugin.builders.structure.representation.registerPreset(RcsbSuperpositionRepresentationPreset);
  149. // TODO Check why this.plugin.canvas3d can be null
  150. // this.plugin.canvas3d can be null. The value is not assigned until React Plugin component is mounted
  151. // Next wait Promise guarantees that its value is defined
  152. const wait: Promise<void> = new Promise<void>((resolve, reject)=>{
  153. const recursive: () => void = () => {
  154. if(this.plugin.canvas3d != null){
  155. resolve();
  156. }else{
  157. setTimeout(()=>{
  158. recursive();
  159. }, 100);
  160. }
  161. };
  162. recursive();
  163. });
  164. wait.then(result=>{
  165. const renderer = this.plugin.canvas3d!.props.renderer;
  166. PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: o.backgroundColor } } });
  167. });
  168. if (o.showWelcomeToast) {
  169. PluginCommands.Toast.Show(this.plugin, {
  170. title: 'Welcome',
  171. message: `RCSB PDB Mol* Viewer ${RCSB_MOLSTAR_VERSION} [${BUILD_DATE.toLocaleString()}]`,
  172. key: 'toast-welcome',
  173. timeoutMs: 5000
  174. });
  175. }
  176. this.prevExpanded = this.plugin.layout.state.isExpanded;
  177. this.plugin.layout.events.updated.subscribe(() => this.toggleControls());
  178. }
  179. private prevExpanded: boolean;
  180. private toggleControls(): void {
  181. const currExpanded = this.plugin.layout.state.isExpanded;
  182. const expanedChanged = (this.prevExpanded !== currExpanded);
  183. if (!expanedChanged) return;
  184. if (currExpanded && !this.plugin.layout.state.showControls) {
  185. this.plugin.layout.setProps({showControls: true});
  186. } else if (!currExpanded && this.plugin.layout.state.showControls) {
  187. this.plugin.layout.setProps({showControls: false});
  188. }
  189. this.prevExpanded = this.plugin.layout.state.isExpanded;
  190. }
  191. //
  192. resetCamera(durationMs?: number) {
  193. this.plugin.managers.camera.reset(undefined, durationMs);
  194. }
  195. clear() {
  196. const state = this.plugin.state.data;
  197. return PluginCommands.State.RemoveObject(this.plugin, { state, ref: state.tree.root.ref });
  198. }
  199. async loadPdbId(pdbId: string, props?: PresetProps, matrix?: Mat4) {
  200. for (const provider of this.modelUrlProviders) {
  201. try {
  202. const p = provider(pdbId);
  203. await this.customState.modelLoader.load({ fileOrUrl: p.url, format: p.format, isBinary: p.isBinary }, props, matrix);
  204. break;
  205. } catch (e) {
  206. console.warn(`loading '${pdbId}' failed with '${e}', trying next model-loader-provider`);
  207. }
  208. }
  209. }
  210. async loadPdbIds(args: { pdbId: string, props?: PresetProps, matrix?: Mat4 }[]) {
  211. for (const { pdbId, props, matrix } of args) {
  212. await this.loadPdbId(pdbId, props, matrix);
  213. }
  214. this.resetCamera(0);
  215. }
  216. loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat, isBinary: boolean, props?: PresetProps, matrix?: Mat4) {
  217. return this.customState.modelLoader.load({ fileOrUrl: url, format, isBinary }, props, matrix);
  218. }
  219. loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {
  220. return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
  221. }
  222. async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, isBinary: boolean, props?: PresetProps & { dataLabel?: string }, matrix?: Mat4) {
  223. return this.customState.modelLoader.parse({ data, format, isBinary }, props, matrix);
  224. }
  225. handleResize() {
  226. this.plugin.layout.events.updated.next();
  227. }
  228. async exportLoadedStructures() {
  229. const content = encodeStructureData(this.plugin);
  230. downloadAsZipFile(content);
  231. }
  232. pluginCall(f: (plugin: PluginContext) => void){
  233. f(this.plugin);
  234. }
  235. public getPlugin(): PluginContext {
  236. return this.plugin;
  237. }
  238. public select(selection: Array<{modelId: string; asymId: string; position: number;}>, mode: 'select'|'hover', modifier: 'add'|'set'): void;
  239. public select(modelId: string, asymId: string, position: number, mode: 'select'|'hover', modifier: 'add'|'set'): void;
  240. public select(modelId: string, asymId: string, begin: number, end: number, mode: 'select'|'hover', modifier: 'add'|'set'): void;
  241. public select(...args: any[]){
  242. if(args.length === 3){
  243. if(args[2] === 'set')
  244. this.clearSelection('select');
  245. (args[0] as Array<{modelId: string; asymId: string; position: number;}>).forEach(r=>{
  246. this.selectSegment(r.modelId, r.asymId, r.position, r.position, args[1], 'add');
  247. });
  248. }else if(args.length === 5){
  249. this.selectSegment(args[0], args[1], args[2], args[2], args[3], args[4]);
  250. }else if(args.length === 6){
  251. this.selectSegment(args[0], args[1], args[2], args[3], args[4], args[5]);
  252. }
  253. }
  254. private selectSegment(modelId: string, asymId: string, begin: number, end: number, mode: 'select'|'hover', modifier: 'add'|'set'): void {
  255. const data: Structure | undefined = getStructureWithModelId(this.plugin.managers.structure.hierarchy.current.structures, modelId);
  256. if (data == null) return;
  257. const seq_id: Array<number> = new Array<number>();
  258. for(let n = begin; n <= end; n++){
  259. seq_id.push(n);
  260. }
  261. const sel: StructureSelection = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
  262. 'chain-test': Q.core.rel.eq([asymId, MolScriptBuilder.ammp('label_asym_id')]),
  263. 'residue-test': Q.core.set.has([MolScriptBuilder.set(...SetUtils.toArray(new Set(seq_id))), MolScriptBuilder.ammp('label_seq_id')])
  264. }), data);
  265. const loci: Loci = StructureSelection.toLociWithSourceUnits(sel);
  266. if(mode == null || mode === 'select')
  267. this.plugin.managers.structure.selection.fromLoci(modifier, loci);
  268. else if(mode === 'hover')
  269. this.plugin.managers.interactivity.lociHighlights.highlight({ loci });
  270. }
  271. public clearSelection(mode: 'select'|'hover', options?: {modelId: string; labelAsymId: string;}): void {
  272. if(mode == null || mode === 'select') {
  273. if(options == null){
  274. this.plugin.managers.interactivity.lociSelects.deselectAll();
  275. }else{
  276. const data: Structure | undefined = getStructureWithModelId(this.plugin.managers.structure.hierarchy.current.structures, options.modelId);
  277. if (data == null) return;
  278. const sel: StructureSelection = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
  279. 'chain-test': Q.core.rel.eq([options.labelAsymId, MolScriptBuilder.ammp('label_asym_id')])
  280. }), data);
  281. const loci: Loci = StructureSelection.toLociWithSourceUnits(sel);
  282. this.plugin.managers.interactivity.lociSelects.deselect({loci});
  283. }
  284. }else if(mode === 'hover') {
  285. this.plugin.managers.interactivity.lociHighlights.clearHighlights();
  286. }
  287. }
  288. public createComponent(componentId: string, modelId: string, asymId: string, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>;
  289. public createComponent(componentId: string, modelId: string, residues: Array<{asymId: string, position: number}>, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>;
  290. public createComponent(componentId: string, modelId: string, asymId: string, begin: number, end: number, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>;
  291. public createComponent(...args: any[]): Promise<void>{
  292. if(args.length === 4 && typeof args[2] === 'string'){
  293. return this.createComponentFromChain(args[0], args[1], args[2], args[3]);
  294. }else if(args.length === 4 && args[2] instanceof Array){
  295. return this.createComponentFromSet(args[0], args[1], args[2], args[3]);
  296. }else if(args.length === 6 ){
  297. return this.createComponentFromRange(args[0], args[1], args[2], args[3], args[4], args[5]);
  298. }
  299. throw 'createComponent error: wrong arguments';
  300. }
  301. private async createComponentFromChain(componentId: string, modelId: string, asymId: string, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>{
  302. const structureRef: StructureRef | undefined = getStructureRefWithModelId(this.plugin.managers.structure.hierarchy.current.structures, modelId);
  303. if(structureRef == null)
  304. return;
  305. await this.plugin.managers.structure.component.add({
  306. selection: StructureSelectionQuery(
  307. 'innerQuery_' + Math.random().toString(36).substr(2),
  308. MolScriptBuilder.struct.generator.atomGroups({
  309. 'chain-test': MolScriptBuilder.core.rel.eq([asymId, MolScriptBuilder.ammp('label_asym_id')])
  310. })
  311. ),
  312. options: { checkExisting: false, label: componentId },
  313. representation: representationType,
  314. }, [structureRef]);
  315. }
  316. private async createComponentFromSet(componentId: string, modelId: string, residues: Array<{asymId: string, position: number}>, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>{
  317. const structureRef: StructureRef | undefined = getStructureRefWithModelId(this.plugin.managers.structure.hierarchy.current.structures, modelId);
  318. if(structureRef == null)
  319. return;
  320. await this.plugin.managers.structure.component.add({
  321. selection: StructureSelectionQuery(
  322. 'innerQuery_' + Math.random().toString(36).substr(2),
  323. MolScriptBuilder.struct.combinator.merge(
  324. residues.map(r=>MolScriptBuilder.struct.generator.atomGroups({
  325. 'chain-test': MolScriptBuilder.core.rel.eq([r.asymId, MolScriptBuilder.ammp('label_asym_id')]),
  326. 'residue-test': MolScriptBuilder.core.rel.eq([r.position, MolScriptBuilder.ammp('label_seq_id')])
  327. }))
  328. )
  329. ),
  330. options: { checkExisting: false, label: componentId },
  331. representation: representationType,
  332. }, [structureRef]);
  333. }
  334. private async createComponentFromRange(componentId: string, modelId: string, asymId: string, begin: number, end: number, representationType: StructureRepresentationRegistry.BuiltIn): Promise<void>{
  335. const structureRef: StructureRef | undefined = getStructureRefWithModelId(this.plugin.managers.structure.hierarchy.current.structures, modelId);
  336. if(structureRef == null)
  337. return;
  338. const seq_id: Array<number> = new Array<number>();
  339. for(let n = begin; n <= end; n++){
  340. seq_id.push(n);
  341. }
  342. await this.plugin.managers.structure.component.add({
  343. selection: StructureSelectionQuery(
  344. 'innerQuery_' + Math.random().toString(36).substr(2),
  345. MolScriptBuilder.struct.generator.atomGroups({
  346. 'chain-test': MolScriptBuilder.core.rel.eq([asymId, MolScriptBuilder.ammp('label_asym_id')]),
  347. 'residue-test': MolScriptBuilder.core.set.has([MolScriptBuilder.set(...SetUtils.toArray(new Set(seq_id))), MolScriptBuilder.ammp('label_seq_id')])
  348. })
  349. ),
  350. options: { checkExisting: false, label: componentId },
  351. representation: representationType,
  352. }, [structureRef]);
  353. }
  354. public removeComponent(componentId: string): void{
  355. this.plugin.managers.structure.hierarchy.currentComponentGroups.forEach(c=>{
  356. for(const comp of c){
  357. if(comp.cell.obj?.label === componentId) {
  358. this.plugin.managers.structure.hierarchy.remove(c);
  359. break;
  360. }
  361. }
  362. });
  363. }
  364. }
  365. function getStructureRefWithModelId(structures: StructureRef[], modelId: string): StructureRef|undefined{
  366. for(const structure of structures){
  367. if(!structure.cell?.obj?.data?.units)
  368. continue;
  369. const unit = structure.cell.obj.data.units[0];
  370. const id: string = unit.model.id;
  371. if(id === modelId)
  372. return structure;
  373. }
  374. }
  375. function getStructureWithModelId(structures: StructureRef[], modelId: string): Structure|undefined{
  376. const structureRef: StructureRef | undefined = getStructureRefWithModelId(structures, modelId);
  377. if(structureRef != null)
  378. return structureRef.cell?.obj?.data;
  379. }