basic.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { PluginContext } from 'mol-plugin/context';
  8. import { StateTree, Transformer, StateObject, State } from 'mol-state';
  9. import { StateAction } from 'mol-state/action';
  10. import { StateSelection } from 'mol-state/state/selection';
  11. import { StateTreeBuilder } from 'mol-state/tree/builder';
  12. import { ParamDefinition as PD } from 'mol-util/param-definition';
  13. import { PluginStateObject } from '../objects';
  14. import { StateTransforms } from '../transforms';
  15. import { Download } from '../transforms/data';
  16. import { StructureRepresentation3DHelpers, VolumeRepresentation3DHelpers } from '../transforms/representation';
  17. import { getFileInfo, FileInfo } from 'mol-util/file-info';
  18. import { Task } from 'mol-task';
  19. import { ColorNames } from 'mol-util/color/tables';
  20. import { VolumeIsoValue } from 'mol-model/volume';
  21. // TODO: "structure/volume parser provider"
  22. export { DownloadStructure };
  23. type DownloadStructure = typeof DownloadStructure
  24. const DownloadStructure = StateAction.build({
  25. from: PluginStateObject.Root,
  26. display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
  27. params: {
  28. source: PD.MappedStatic('bcif-static', {
  29. 'pdbe-updated': PD.Group({
  30. id: PD.Text('1cbs', { label: 'Id' }),
  31. supportProps: PD.Boolean(false)
  32. }, { isFlat: true }),
  33. 'rcsb': PD.Group({
  34. id: PD.Text('1tqn', { label: 'Id' }),
  35. supportProps: PD.Boolean(false)
  36. }, { isFlat: true }),
  37. 'bcif-static': PD.Group({
  38. id: PD.Text('1tqn', { label: 'Id' }),
  39. supportProps: PD.Boolean(false)
  40. }, { isFlat: true }),
  41. 'url': PD.Group({
  42. url: PD.Text(''),
  43. format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]),
  44. isBinary: PD.Boolean(false),
  45. supportProps: PD.Boolean(false)
  46. }, { isFlat: true })
  47. }, {
  48. options: [
  49. ['pdbe-updated', 'PDBe Updated'],
  50. ['rcsb', 'RCSB'],
  51. ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
  52. ['url', 'URL']
  53. ]
  54. })
  55. }
  56. })(({ params, state }, ctx: PluginContext) => {
  57. const b = state.build();
  58. const src = params.source;
  59. let downloadParams: Transformer.Params<Download>;
  60. switch (src.name) {
  61. case 'url':
  62. downloadParams = { url: src.params.url, isBinary: src.params.isBinary };
  63. break;
  64. case 'pdbe-updated':
  65. downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
  66. break;
  67. case 'rcsb':
  68. downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
  69. break;
  70. case 'bcif-static':
  71. downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
  72. break;
  73. default: throw new Error(`${(src as any).name} not supported.`);
  74. }
  75. const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams);
  76. const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
  77. return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps));
  78. });
  79. export const OpenStructure = StateAction.build({
  80. display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' },
  81. from: PluginStateObject.Root,
  82. params: { file: PD.File({ accept: '.cif,.bcif' }) }
  83. })(({ params, state }, ctx: PluginContext) => {
  84. const b = state.build();
  85. const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
  86. const traj = createModelTree(data, 'cif');
  87. return state.updateTree(createStructureTree(ctx, traj, false));
  88. });
  89. function createModelTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') {
  90. const parsed = format === 'cif'
  91. ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
  92. : b.apply(StateTransforms.Model.TrajectoryFromPDB);
  93. return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
  94. }
  95. function createStructureTree(ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean): StateTree {
  96. let root = b;
  97. if (supportProps) {
  98. root = root.apply(StateTransforms.Model.CustomModelProperties);
  99. }
  100. const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
  101. complexRepresentation(ctx, structure);
  102. return root.getTree();
  103. }
  104. function complexRepresentation(ctx: PluginContext, root: StateTreeBuilder.To<PluginStateObject.Molecule.Structure>) {
  105. root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
  106. .apply(StateTransforms.Representation.StructureRepresentation3D,
  107. StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
  108. root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
  109. .apply(StateTransforms.Representation.StructureRepresentation3D,
  110. StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
  111. root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
  112. .apply(StateTransforms.Representation.StructureRepresentation3D,
  113. StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
  114. root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
  115. .apply(StateTransforms.Representation.StructureRepresentation3D,
  116. StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
  117. }
  118. export const CreateComplexRepresentation = StateAction.build({
  119. display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
  120. from: PluginStateObject.Molecule.Structure
  121. })(({ ref, state }, ctx: PluginContext) => {
  122. const root = state.build().to(ref);
  123. complexRepresentation(ctx, root);
  124. return state.updateTree(root.getTree());
  125. });
  126. export const UpdateTrajectory = StateAction.build({
  127. display: { name: 'Update Trajectory' },
  128. params: {
  129. action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
  130. by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
  131. }
  132. })(({ params, state }) => {
  133. const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
  134. .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
  135. const update = state.build();
  136. if (params.action === 'reset') {
  137. for (const m of models) {
  138. update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
  139. () => ({ modelIndex: 0 }));
  140. }
  141. } else {
  142. for (const m of models) {
  143. const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
  144. if (!parent || !parent.obj) continue;
  145. const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
  146. update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
  147. old => {
  148. let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
  149. if (modelIndex < 0) modelIndex += traj.data.length;
  150. return { modelIndex };
  151. });
  152. }
  153. }
  154. return state.updateTree(update);
  155. });
  156. //
  157. export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> {
  158. private _list: { name: string, provider: DataFormatProvider<D> }[] = []
  159. private _map = new Map<string, DataFormatProvider<D>>()
  160. get default() { return this._list[0]; }
  161. get types(): [string, string][] {
  162. return this._list.map(e => [e.name, e.provider.label] as [string, string]);
  163. }
  164. constructor() {
  165. this.add('ccp4', Ccp4Provider)
  166. this.add('dsn6', Dsn6Provider)
  167. this.add('dscif', DscifProvider)
  168. };
  169. add(name: string, provider: DataFormatProvider<D>) {
  170. this._list.push({ name, provider })
  171. this._map.set(name, provider)
  172. }
  173. remove(name: string) {
  174. this._list.splice(this._list.findIndex(e => e.name === name), 1)
  175. this._map.delete(name)
  176. }
  177. auto(info: FileInfo, dataStateObject: D) {
  178. for (let i = 0, il = this.list.length; i < il; ++i) {
  179. const { provider } = this._list[i]
  180. if (provider.isApplicable(info, dataStateObject.data)) return provider
  181. }
  182. throw new Error('no compatible data format provider available')
  183. }
  184. get(name: string): DataFormatProvider<D> {
  185. if (this._map.has(name)) {
  186. return this._map.get(name)!
  187. } else {
  188. throw new Error(`unknown data format name '${name}'`)
  189. }
  190. }
  191. get list() {
  192. return this._list
  193. }
  194. }
  195. interface DataFormatProvider<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
  196. label: string
  197. description: string
  198. fileExtensions: string[]
  199. isApplicable(info: FileInfo, data: string | Uint8Array): boolean
  200. getDefaultBuilder(ctx: PluginContext, data: StateTreeBuilder.To<D>, state?: State): Task<void>
  201. }
  202. const Ccp4Provider: DataFormatProvider<any> = {
  203. label: 'CCP4/MRC/BRIX',
  204. description: 'CCP4/MRC/BRIX',
  205. fileExtensions: ['ccp4', 'mrc', 'map'],
  206. isApplicable: (info: FileInfo, data: Uint8Array) => {
  207. return info.ext === 'ccp4' || info.ext === 'mrc' || info.ext === 'map'
  208. },
  209. getDefaultBuilder: (ctx: PluginContext, data: StateTreeBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
  210. return Task.create('CCP4/MRC/BRIX default builder', async taskCtx => {
  211. const tree = data.apply(StateTransforms.Data.ParseCcp4)
  212. .apply(StateTransforms.Model.VolumeFromCcp4)
  213. .apply(StateTransforms.Representation.VolumeRepresentation3D)
  214. await state.updateTree(tree).runInContext(taskCtx)
  215. })
  216. }
  217. }
  218. const Dsn6Provider: DataFormatProvider<any> = {
  219. label: 'DSN6/BRIX',
  220. description: 'DSN6/BRIX',
  221. fileExtensions: ['dsn6', 'brix'],
  222. isApplicable: (info: FileInfo, data: Uint8Array) => {
  223. return info.ext === 'dsn6' || info.ext === 'brix'
  224. },
  225. getDefaultBuilder: (ctx: PluginContext, data: StateTreeBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
  226. return Task.create('DSN6/BRIX default builder', async taskCtx => {
  227. const tree = data.apply(StateTransforms.Data.ParseDsn6)
  228. .apply(StateTransforms.Model.VolumeFromDsn6)
  229. .apply(StateTransforms.Representation.VolumeRepresentation3D)
  230. await state.updateTree(tree).runInContext(taskCtx)
  231. })
  232. }
  233. }
  234. const DscifProvider: DataFormatProvider<any> = {
  235. label: 'DensityServer CIF',
  236. description: 'DensityServer CIF',
  237. fileExtensions: ['cif'],
  238. isApplicable: (info: FileInfo, data: Uint8Array) => {
  239. return info.ext === 'cif'
  240. },
  241. getDefaultBuilder: (ctx: PluginContext, data: StateTreeBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
  242. return Task.create('DensityServer CIF default builder', async taskCtx => {
  243. const cifBuilder = data.apply(StateTransforms.Data.ParseCif)
  244. const cifStateObject = await state.updateTree(cifBuilder).runInContext(taskCtx)
  245. const b = state.build().to(cifBuilder.ref);
  246. const blocks = cifStateObject.data.blocks.slice(1); // zero block contains query meta-data
  247. let tree: StateTreeBuilder.To<any>
  248. if (blocks.length === 1) {
  249. tree = b
  250. .apply(StateTransforms.Model.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
  251. .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.teal }))
  252. } else if (blocks.length === 2) {
  253. tree = b
  254. .apply(StateTransforms.Model.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
  255. .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.blue }))
  256. const vol = tree.to(cifBuilder.ref)
  257. .apply(StateTransforms.Model.VolumeFromDensityServerCif, { blockHeader: blocks[1].header })
  258. const posParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(3), alpha: 0.3 }, 'uniform', { value: ColorNames.green })
  259. tree = vol.apply(StateTransforms.Representation.VolumeRepresentation3D, posParams)
  260. const negParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(-3), alpha: 0.3 }, 'uniform', { value: ColorNames.red })
  261. tree = tree.to(vol.ref).apply(StateTransforms.Representation.VolumeRepresentation3D, negParams)
  262. } else {
  263. throw new Error('unknown number of blocks')
  264. }
  265. await state.updateTree(tree).runInContext(taskCtx);
  266. })
  267. }
  268. }
  269. //
  270. function getDataFormatExtensionsOptions(dataFormatRegistry: DataFormatRegistry<any, any>) {
  271. const extensions: string[] = []
  272. const options: [string, string][] = [['auto', 'Automatic']]
  273. dataFormatRegistry.list.forEach(({ name, provider }) => {
  274. extensions.push(...provider.fileExtensions)
  275. options.push([ name, provider.label ])
  276. })
  277. return { extensions, options }
  278. }
  279. export const OpenVolume = StateAction.build({
  280. display: { name: 'Open Volume', description: 'Load a volume from file and create its default visual' },
  281. from: PluginStateObject.Root,
  282. params: (a, ctx: PluginContext) => {
  283. const { extensions, options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry)
  284. return {
  285. file: PD.File({ accept: extensions.map(e => `.${e}`).join(',')}),
  286. format: PD.Select('auto', options),
  287. isBinary: PD.Boolean(true), // TOOD should take selected format into account
  288. }
  289. }
  290. })(({ params, state }, ctx: PluginContext) => Task.create('Open Volume', async taskCtx => {
  291. const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: params.isBinary });
  292. const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
  293. // Alternative for more complex states where the builder is not a simple StateTreeBuilder.To<>:
  294. /*
  295. const dataRef = dataTree.ref;
  296. await state.updateTree(dataTree).runInContext(taskCtx);
  297. const dataCell = state.select(dataRef)[0];
  298. */
  299. const provider = params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(params.file), dataStateObject) : ctx.dataFormat.registry.get(params.format)
  300. const b = state.build().to(data.ref);
  301. // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
  302. await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
  303. }));
  304. export { DownloadDensity };
  305. type DownloadDensity = typeof DownloadDensity
  306. const DownloadDensity = StateAction.build({
  307. from: PluginStateObject.Root,
  308. display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' },
  309. params: (a, ctx: PluginContext) => {
  310. const { options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry)
  311. return {
  312. source: PD.MappedStatic('rcsb', {
  313. 'pdbe': PD.Group({
  314. id: PD.Text('1tqn', { label: 'Id' }),
  315. type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
  316. }, { isFlat: true }),
  317. 'pdbe-emd-ds': PD.Group({
  318. id: PD.Text('emd-8004', { label: 'Id' }),
  319. detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
  320. }, { isFlat: true }),
  321. 'pdbe-xray-ds': PD.Group({
  322. id: PD.Text('1tqn', { label: 'Id' }),
  323. detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
  324. }, { isFlat: true }),
  325. 'rcsb': PD.Group({
  326. id: PD.Text('1tqn', { label: 'Id' }),
  327. type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
  328. }, { isFlat: true }),
  329. 'url': PD.Group({
  330. url: PD.Text(''),
  331. isBinary: PD.Boolean(false),
  332. format: PD.Select('auto', options),
  333. }, { isFlat: true })
  334. }, {
  335. options: [
  336. ['pdbe', 'PDBe X-ray maps'],
  337. ['pdbe-emd-ds', 'PDBe EMD Density Server'],
  338. ['pdbe-xray-ds', 'PDBe X-ray Density Server'],
  339. ['rcsb', 'RCSB X-ray maps'],
  340. ['url', 'URL']
  341. ]
  342. })
  343. }
  344. }
  345. })(({ params, state }, ctx: PluginContext) => Task.create('Download Density', async taskCtx => {
  346. const src = params.source;
  347. let downloadParams: Transformer.Params<Download>;
  348. let provider: DataFormatProvider<any>
  349. switch (src.name) {
  350. case 'url':
  351. downloadParams = src.params;
  352. break;
  353. case 'pdbe':
  354. downloadParams = {
  355. url: src.params.type === '2fofc'
  356. ? `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}.ccp4`
  357. : `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}_diff.ccp4`,
  358. isBinary: true,
  359. label: `PDBe X-ray map: ${src.params.id}`
  360. };
  361. break;
  362. case 'pdbe-emd-ds':
  363. downloadParams = {
  364. url: `https://www.ebi.ac.uk/pdbe/densities/emd/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`,
  365. isBinary: true,
  366. label: `PDBe EMD Density Server: ${src.params.id}`
  367. };
  368. break;
  369. case 'pdbe-xray-ds':
  370. downloadParams = {
  371. url: `https://www.ebi.ac.uk/pdbe/densities/x-ray/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`,
  372. isBinary: true,
  373. label: `PDBe X-ray Density Server: ${src.params.id}`
  374. };
  375. break;
  376. case 'rcsb':
  377. downloadParams = {
  378. url: src.params.type === '2fofc'
  379. ? `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_2fofc.dsn6`
  380. : `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_fofc.dsn6`,
  381. isBinary: true,
  382. label: `RCSB X-ray map: ${src.params.id}`
  383. };
  384. break;
  385. default: throw new Error(`${(src as any).name} not supported.`);
  386. }
  387. const data = state.build().toRoot().apply(StateTransforms.Data.Download, downloadParams);
  388. const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
  389. switch (src.name) {
  390. case 'url':
  391. downloadParams = src.params;
  392. provider = src.params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(downloadParams.url), dataStateObject) : ctx.dataFormat.registry.get(src.params.format)
  393. break;
  394. case 'pdbe':
  395. provider = ctx.dataFormat.registry.get('ccp4')
  396. break;
  397. case 'pdbe-emd-ds':
  398. case 'pdbe-xray-ds':
  399. provider = ctx.dataFormat.registry.get('dscif')
  400. break;
  401. case 'rcsb':
  402. provider = ctx.dataFormat.registry.get('dsn6')
  403. break;
  404. default: throw new Error(`${(src as any).name} not supported.`);
  405. }
  406. const b = state.build().to(data.ref);
  407. await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
  408. }));