basic.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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 } 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, FileInput } 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. const VolumeFormats = { 'ccp4': '', 'mrc': '', 'map': '', 'dsn6': '', 'brix': '' }
  156. type VolumeFormat = keyof typeof VolumeFormats
  157. function getVolumeData(format: VolumeFormat, b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>) {
  158. switch (format) {
  159. case 'ccp4': case 'mrc': case 'map':
  160. return b.apply(StateTransforms.Data.ParseCcp4).apply(StateTransforms.Model.VolumeFromCcp4);
  161. case 'dsn6': case 'brix':
  162. return b.apply(StateTransforms.Data.ParseDsn6).apply(StateTransforms.Model.VolumeFromDsn6);
  163. }
  164. }
  165. function createVolumeTree(format: VolumeFormat, ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>): StateTree {
  166. return getVolumeData(format, b)
  167. .apply(StateTransforms.Representation.VolumeRepresentation3D)
  168. // the parameters will be used automatically by the reconciler and the IsoValue object
  169. // will get the correct Stats object instead of the empty one
  170. // VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface'))
  171. .getTree();
  172. }
  173. function getFileFormat(format: VolumeFormat | 'auto', file: FileInput, data?: Uint8Array): VolumeFormat {
  174. if (format === 'auto') {
  175. const fileFormat = getFileInfo(file).ext
  176. if (fileFormat in VolumeFormats) {
  177. return fileFormat as VolumeFormat
  178. } else {
  179. throw new Error('unsupported format')
  180. }
  181. } else {
  182. return format
  183. }
  184. }
  185. export const OpenVolume = StateAction.build({
  186. display: { name: 'Open Volume', description: 'Load a volume from file and create its default visual' },
  187. from: PluginStateObject.Root,
  188. params: {
  189. file: PD.File({ accept: '.ccp4,.mrc,.map,.dsn6,.brix'}),
  190. format: PD.Select('auto', [
  191. ['auto', 'Automatic'], ['ccp4', 'CCP4'], ['mrc', 'MRC'], ['map', 'MAP'], ['dsn6', 'DSN6'], ['brix', 'BRIX']
  192. ]),
  193. }
  194. })(({ params, state }, ctx: PluginContext) => Task.create('Open Volume', async taskCtx => {
  195. const dataTree = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: true });
  196. const volumeData = await state.updateTree(dataTree).runInContext(taskCtx);
  197. // Alternative for more complex states where the builder is not a simple StateTreeBuilder.To<>:
  198. /*
  199. const dataRef = dataTree.ref;
  200. await state.updateTree(dataTree).runInContext(taskCtx);
  201. const dataCell = state.select(dataRef)[0];
  202. */
  203. const format = getFileFormat(params.format, params.file, volumeData.data as Uint8Array);
  204. const volumeTree = state.build().to(dataTree.ref);
  205. // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
  206. await state.updateTree(createVolumeTree(format, ctx, volumeTree)).runInContext(taskCtx);
  207. }));
  208. export { DownloadDensity };
  209. type DownloadDensity = typeof DownloadDensity
  210. const DownloadDensity = StateAction.build({
  211. from: PluginStateObject.Root,
  212. display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' },
  213. params: {
  214. source: PD.MappedStatic('rcsb', {
  215. 'pdbe': PD.Group({
  216. id: PD.Text('1tqn', { label: 'Id' }),
  217. type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
  218. }, { isFlat: true }),
  219. 'rcsb': PD.Group({
  220. id: PD.Text('1tqn', { label: 'Id' }),
  221. type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
  222. }, { isFlat: true }),
  223. 'url': PD.Group({
  224. url: PD.Text(''),
  225. format: PD.Select('auto', [
  226. ['auto', 'Automatic'], ['ccp4', 'CCP4'], ['mrc', 'MRC'], ['map', 'MAP'], ['dsn6', 'DSN6'], ['brix', 'BRIX']
  227. ]),
  228. }, { isFlat: true })
  229. }, {
  230. options: [
  231. ['pdbe', 'PDBe X-ray maps'],
  232. ['rcsb', 'RCSB X-ray maps'],
  233. ['url', 'URL']
  234. ]
  235. })
  236. }
  237. })(({ params, state }, ctx: PluginContext) => {
  238. const b = state.build();
  239. const src = params.source;
  240. let downloadParams: Transformer.Params<Download>;
  241. let format: VolumeFormat
  242. switch (src.name) {
  243. case 'url':
  244. downloadParams = src.params;
  245. format = getFileFormat(src.params.format, src.params.url)
  246. break;
  247. case 'pdbe':
  248. downloadParams = {
  249. url: src.params.type === '2fofc'
  250. ? `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}.ccp4`
  251. : `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}_diff.ccp4`,
  252. isBinary: true,
  253. label: `PDBe X-ray map: ${src.params.id}`
  254. };
  255. format = 'ccp4'
  256. break;
  257. case 'rcsb':
  258. downloadParams = {
  259. url: src.params.type === '2fofc'
  260. ? `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_2fofc.dsn6`
  261. : `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_fofc.dsn6`,
  262. isBinary: true,
  263. label: `RCSB X-ray map: ${src.params.id}`
  264. };
  265. format = 'dsn6'
  266. break;
  267. default: throw new Error(`${(src as any).name} not supported.`);
  268. }
  269. const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams);
  270. return state.updateTree(createVolumeTree(format, ctx, data));
  271. });