ui.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { DownloadFile } from '../../mol-plugin-state/actions/file';
  7. import { DownloadStructure, LoadTrajectory } from '../../mol-plugin-state/actions/structure';
  8. import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
  9. import { CoordinatesFormatCategory } from '../../mol-plugin-state/formats/coordinates';
  10. import { TopologyFormatCategory } from '../../mol-plugin-state/formats/topology';
  11. import { TrajectoryFormatCategory } from '../../mol-plugin-state/formats/trajectory';
  12. import { VolumeFormatCategory } from '../../mol-plugin-state/formats/volume';
  13. import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
  14. import { Button } from '../../mol-plugin-ui/controls/common';
  15. import { OpenInBrowserSvg } from '../../mol-plugin-ui/controls/icons';
  16. import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
  17. import { PluginContext } from '../../mol-plugin/context';
  18. import { formatBytes } from '../../mol-util';
  19. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  20. type ZenodoFile = {
  21. bucket: string
  22. checksum: string
  23. key: string
  24. links: {
  25. [key: string]: string
  26. self: string
  27. }
  28. size: number
  29. type: string
  30. }
  31. type ZenodoRecord = {
  32. id: number
  33. conceptdoi: string
  34. conceptrecid: string
  35. created: string
  36. doi: string
  37. files: ZenodoFile[]
  38. revision: number
  39. updated: string
  40. metadata: {
  41. title: string
  42. }
  43. }
  44. interface State {
  45. busy?: boolean
  46. recordValues: PD.Values<typeof ZenodoImportParams>
  47. importValues?: PD.Values<ImportParams>
  48. importParams?: ImportParams
  49. record?: ZenodoRecord
  50. files?: ZenodoFile[]
  51. }
  52. const ZenodoImportParams = {
  53. record: PD.Text('', { description: 'Zenodo ID.' })
  54. };
  55. function createImportParams(files: ZenodoFile[], plugin: PluginContext) {
  56. const modelOpts: [string, string][] = [];
  57. const topologyOpts: [string, string][] = [];
  58. const coordinatesOpts: [string, string][] = [];
  59. const volumeOpts: [string, string][] = [];
  60. const compressedOpts: [string, string][] = [];
  61. const structureExts = new Map<string, { format: string, isBinary: boolean }>();
  62. const coordinatesExts = new Map<string, { format: string, isBinary: boolean }>();
  63. const topologyExts = new Map<string, { format: string, isBinary: boolean }>();
  64. const volumeExts = new Map<string, { format: string, isBinary: boolean }>();
  65. for (const { provider: { category, binaryExtensions, stringExtensions }, name } of plugin.dataFormats.list) {
  66. if (category === TrajectoryFormatCategory) {
  67. if (binaryExtensions) for (const e of binaryExtensions) structureExts.set(e, { format: name, isBinary: true });
  68. if (stringExtensions) for (const e of stringExtensions) structureExts.set(e, { format: name, isBinary: false });
  69. } else if (category === VolumeFormatCategory) {
  70. if (binaryExtensions) for (const e of binaryExtensions) volumeExts.set(e, { format: name, isBinary: true });
  71. if (stringExtensions) for (const e of stringExtensions) volumeExts.set(e, { format: name, isBinary: false });
  72. } else if (category === CoordinatesFormatCategory) {
  73. if (binaryExtensions) for (const e of binaryExtensions) coordinatesExts.set(e, { format: name, isBinary: true });
  74. if (stringExtensions) for (const e of stringExtensions) coordinatesExts.set(e, { format: name, isBinary: false });
  75. } else if (category === TopologyFormatCategory) {
  76. if (binaryExtensions) for (const e of binaryExtensions) topologyExts.set(e, { format: name, isBinary: true });
  77. if (stringExtensions) for (const e of stringExtensions) topologyExts.set(e, { format: name, isBinary: false });
  78. }
  79. }
  80. for (const file of files) {
  81. const label = `${file.key} (${formatBytes(file.size)})`;
  82. if (structureExts.has(file.type)) {
  83. const { format, isBinary } = structureExts.get(file.type)!;
  84. modelOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
  85. topologyOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
  86. } else if (volumeExts.has(file.type)) {
  87. const { format, isBinary } = volumeExts.get(file.type)!;
  88. volumeOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
  89. } else if (topologyExts.has(file.type)) {
  90. const { format, isBinary } = topologyExts.get(file.type)!;
  91. topologyOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
  92. } else if (coordinatesExts.has(file.type)) {
  93. const { format, isBinary } = coordinatesExts.get(file.type)!;
  94. coordinatesOpts.push([`${file.links.self}|${format}|${isBinary}`, label]);
  95. } else if (file.type === 'zip') {
  96. compressedOpts.push([`${file.links.self}|${file.type}|true`, label]);
  97. }
  98. }
  99. const params: PD.Params = {};
  100. let defaultType = '';
  101. if (modelOpts.length) {
  102. defaultType = 'structure';
  103. params.structure = PD.Select(modelOpts[0][0], modelOpts);
  104. }
  105. if (topologyOpts.length && coordinatesOpts.length) {
  106. if (!defaultType) defaultType = 'trajectory';
  107. params.trajectory = PD.Group({
  108. topology: PD.Select(topologyOpts[0][0], topologyOpts),
  109. coordinates: PD.Select(coordinatesOpts[0][0], coordinatesOpts),
  110. }, { isFlat: true });
  111. }
  112. if (volumeOpts.length) {
  113. if (!defaultType) defaultType = 'volume';
  114. params.volume = PD.Select(volumeOpts[0][0], volumeOpts);
  115. }
  116. if (compressedOpts.length) {
  117. if (!defaultType) defaultType = 'compressed';
  118. params.compressed = PD.Select(compressedOpts[0][0], compressedOpts);
  119. }
  120. return {
  121. type: PD.MappedStatic(defaultType, Object.keys(params).length ? params : { '': PD.EmptyGroup() })
  122. };
  123. }
  124. type ImportParams = ReturnType<typeof createImportParams>
  125. export class ZenodoImportUI extends CollapsableControls<{}, State> {
  126. protected defaultState(): State & CollapsableState {
  127. return {
  128. header: 'Zenodo Import',
  129. isCollapsed: true,
  130. brand: { accent: 'cyan', svg: OpenInBrowserSvg },
  131. recordValues: PD.getDefaultValues(ZenodoImportParams),
  132. importValues: undefined,
  133. importParams: undefined,
  134. record: undefined,
  135. files: undefined,
  136. };
  137. }
  138. private recordParamsOnChange = (values: any) => {
  139. this.setState({ recordValues: values });
  140. };
  141. private importParamsOnChange = (values: any) => {
  142. this.setState({ importValues: values });
  143. };
  144. private loadRecord = async () => {
  145. try {
  146. this.setState({ busy: true });
  147. const record: ZenodoRecord = await this.plugin.runTask(this.plugin.fetch({ url: `https://zenodo.org/api/records/${this.state.recordValues.record}`, type: 'json' }));
  148. const importParams = createImportParams(record.files, this.plugin);
  149. this.setState({
  150. record,
  151. files: record.files,
  152. busy: false,
  153. importValues: PD.getDefaultValues(importParams),
  154. importParams
  155. });
  156. } catch (e) {
  157. console.error(e);
  158. this.plugin.log.error(`Failed to load Zenodo record '${this.state.recordValues.record}'`);
  159. this.setState({ busy: false });
  160. }
  161. };
  162. private loadFile = async (values: PD.Values<ImportParams>) => {
  163. try {
  164. this.setState({ busy: true });
  165. const t = values.type;
  166. if (t.name === 'structure') {
  167. const defaultParams = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
  168. const [url, format, isBinary] = t.params.split('|');
  169. await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
  170. source: {
  171. name: 'url',
  172. params: {
  173. url,
  174. format: format as any,
  175. isBinary: isBinary === 'true',
  176. options: defaultParams.source.params.options,
  177. }
  178. }
  179. }));
  180. } else if (t.name === 'trajectory') {
  181. const [topologyUrl, topologyFormat, topologyIsBinary] = t.params.topology.split('|');
  182. const [coordinatesUrl, coordinatesFormat, coordinatesIsBinary] = t.params.coordinates.split('|');
  183. await this.plugin.runTask(this.plugin.state.data.applyAction(LoadTrajectory, {
  184. source: {
  185. name: 'url',
  186. params: {
  187. model: {
  188. url: topologyUrl,
  189. format: topologyFormat as any,
  190. isBinary: topologyIsBinary === 'true',
  191. },
  192. coordinates: {
  193. url: coordinatesUrl,
  194. format: coordinatesFormat as any,
  195. isBinary: coordinatesIsBinary === 'true',
  196. },
  197. }
  198. }
  199. }));
  200. } else if (t.name === 'volume') {
  201. const [url, format, isBinary] = t.params.split('|');
  202. await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
  203. source: {
  204. name: 'url',
  205. params: {
  206. url,
  207. format: format as any,
  208. isBinary: isBinary === 'true',
  209. }
  210. }
  211. }));
  212. } else if (t.name === 'compressed') {
  213. const [url, format, isBinary] = t.params.split('|');
  214. await this.plugin.runTask(this.plugin.state.data.applyAction(DownloadFile, {
  215. url,
  216. format: format as any,
  217. isBinary: isBinary === 'true',
  218. visuals: true
  219. }));
  220. }
  221. } catch (e) {
  222. console.error(e);
  223. this.plugin.log.error(`Failed to load Zenodo file`);
  224. } finally {
  225. this.setState({ busy: false });
  226. }
  227. };
  228. private clearRecord = () => {
  229. this.setState({
  230. importValues: undefined,
  231. importParams: undefined,
  232. record: undefined,
  233. files: undefined
  234. });
  235. };
  236. private renderLoadRecord() {
  237. return <div style={{ marginBottom: 10 }}>
  238. <ParameterControls params={ZenodoImportParams} values={this.state.recordValues} onChangeValues={this.recordParamsOnChange} isDisabled={this.state.busy} />
  239. <Button onClick={this.loadRecord} style={{ marginTop: 1 }} disabled={this.state.busy || !this.state.recordValues.record}>
  240. Load Record
  241. </Button>
  242. </div>;
  243. }
  244. private renderRecordInfo(record: ZenodoRecord) {
  245. return <div style={{ marginBottom: 10 }}>
  246. <div className='msp-help-text'>
  247. <div>Record {`${record.id}`}: <i>{`${record.metadata.title}`}</i></div>
  248. </div>
  249. <Button onClick={this.clearRecord} style={{ marginTop: 1 }} disabled={this.state.busy}>
  250. Clear
  251. </Button>
  252. </div>;
  253. }
  254. private renderImportFile(params: ImportParams, values: PD.Values<ImportParams>) {
  255. return values.type.name ? <div style={{ marginBottom: 10 }}>
  256. <ParameterControls params={params} values={this.state.importValues} onChangeValues={this.importParamsOnChange} isDisabled={this.state.busy} />
  257. <Button onClick={() => this.loadFile(values)} style={{ marginTop: 1 }} disabled={this.state.busy}>
  258. Import File
  259. </Button>
  260. </div> : <div className='msp-help-text' style={{ marginBottom: 10 }}>
  261. <div>No supported files</div>
  262. </div>;
  263. }
  264. protected renderControls(): JSX.Element | null {
  265. return <>
  266. {!this.state.record ? this.renderLoadRecord() : null}
  267. {this.state.record ? this.renderRecordInfo(this.state.record) : null}
  268. {this.state.importParams && this.state.importValues ? this.renderImportFile(this.state.importParams, this.state.importValues) : null}
  269. </>;
  270. }
  271. }