basic.ts 17 KB

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