index.ts 18 KB

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