index.ts 16 KB

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