From 46af7d03bfad18ca6905a26825fd5cc106497f29 Mon Sep 17 00:00:00 2001 From: midlik Date: Wed, 14 Feb 2024 17:22:27 +0000 Subject: [PATCH] MVSX (#1041) * MVSX format provider * MVS: Integration examples * MVS: drag-and-drop support for MVSX * MVS: support for URL param mvs-format=mvsx * MVS: docs for MVSX * MVS: mvs-render supports MVSX * Update README --- CHANGELOG.md | 2 + docs/extensions/mvs/README.md | 38 ++++-- docs/extensions/mvs/annotations.md | 2 + docs/extensions/mvs/integration-examples.html | 112 ++++++++++++++++++ examples/mvs/1h9t.mvsx | Bin 0 -> 1350 bytes src/apps/viewer/app.ts | 33 +++++- src/cli/mvs/mvs-render.ts | 26 +++- src/extensions/mvs/behavior.ts | 32 +++-- src/extensions/mvs/components/formats.ts | 105 ++++++++++++++-- src/mol-util/zip/zip.ts | 3 + 10 files changed, 316 insertions(+), 37 deletions(-) create mode 100644 docs/extensions/mvs/integration-examples.html create mode 100644 examples/mvs/1h9t.mvsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5154a00f4..cdb6b5942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +MolViewSpec extension: support for MVSX file format + ## [v4.0.0] - 2023-02-04 - Add Mesoscale Explorer app for investigating large systems diff --git a/docs/extensions/mvs/README.md b/docs/extensions/mvs/README.md index d08dea6ca..a773700a2 100644 --- a/docs/extensions/mvs/README.md +++ b/docs/extensions/mvs/README.md @@ -44,6 +44,8 @@ A complete list of supported node types and their parameters is described by the ### Encoding +#### MVSJ + A MolViewSpec tree can be encoded and stored in `.mvsj` format, which is basically a JSON representation of the tree with additional metadata: ```json @@ -68,6 +70,24 @@ A MolViewSpec tree can be encoded and stored in `.mvsj` format, which is basical ``` Complete file: [1cbs.mvsj](../../../examples/mvs/1cbs.mvsj) +#### MVSX + +The MolViewSpec tree can also be stored in a `.mvsx` format. This is simply a ZIP archive containing: +- main file `index.mvsj` (contains the MolViewSpec tree encoded as MVSJ), +- any number of other files, such as MVS annotations or structure files. + +The advantage of this format is that the main file can reference other files in the archive using relative URIs. Thus the view description along with all necessary data can be stored as a single MVSX file. + +It is important that the `index.mvsj` be at the top level of the archive, not in a subdirectory ( +``` +$ ls example/ +annotations-1h9t.cif index.mvsj +$ zip -r example.mvsx example/ # Wrong, won't create a valid MVSX file +$ cd example/; zip -r ../example.mvsx * # Correct +``` + +Example: [1ht9.mvsx](../../../examples/mvs/1h9t.mvsx) + ## MolViewSpec extension functionality @@ -75,20 +95,24 @@ Mol* MolViewSpec extension provides functionality for building, validating, and ### Graphical user interface -- **Drag&drop support:** The easiest way to load a MVS view into Mol* Viewer is to drag a `.mvsj` file and drop it in a browser window with Mol* Viewer. +- **Drag&drop support:** The easiest way to load a MVS view into Mol* Viewer is to drag a `.mvsj` or `.mvsx` file and drop it in a browser window with Mol* Viewer. -- **Load via menu:** Another way to load a MVS view is to use "Download File" or "Open Files" action, available in the "Home" tab in the left panel. For these actions, the "Format" parameter must be set to "MVSJ" (in the "Miscellaneous" category) or "Auto". +- **Load via menu:** Another way to load a MVS view is to use "Download File" or "Open Files" action, available in the "Home" tab in the left panel. For these actions, the "Format" parameter must be set to "MVSJ" or "MVSX" (in the "Miscellaneous" category) or "Auto". - **URL parameters:** Mol* Viewer supports `mvs-url`, `mvs-data`, and `mvs-format` URL parameters to specify a MVS view to be loaded when the viewer is initialized. - `mvs-url` specifies the address from which the MVS view should be retrieved. - - `mvs-data` specifies the MVS view data directly. Keep in mind that some characters must be escaped to be used in the URL. Also beware that URLs longer than 2000 character may not work in all browsers. - - `mvs-format` specifies the format of the MVS view data (from `mvs-url` or `mvs-data`). The only allowed (and default) value is `mvsj`, as this is currently the only supported format. + - `mvs-data` specifies the MVS view data directly. Keep in mind that some characters must be escaped to be used in the URL. Also beware that URLs longer than 2000 character may not work in all browsers. Because of these limitations, the preferred method it to host the data somewhere and use `mvs-url` instead. + - `mvs-format` specifies the format of the MVS view data from `mvs-url` or `mvs-data`. Allowed values are `mvsj` and `mvsx` (default is `mvsj`). Examples of URL parameter usage: - - https://molstar.org/viewer?mvs-format=mvsj&mvs-url=https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj + - https://molstar.org/viewer/?mvs-format=mvsj&mvs-url=https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj - - https://molstar.org/viewer?mvs-format=mvsj&mvs-data=%7B%22metadata%22%3A%7B%22title%22%3A%22Example%20MolViewSpec%20-%201cbs%20with%20labelled%20protein%20and%20ligand%22%2C%22version%22%3A%221%22%2C%22timestamp%22%3A%222023-11-24T10%3A38%3A17.483%22%7D%2C%22root%22%3A%7B%22kind%22%3A%22root%22%2C%22children%22%3A%5B%7B%22kind%22%3A%22download%22%2C%22params%22%3A%7B%22url%22%3A%22https%3A//www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22parse%22%2C%22params%22%3A%7B%22format%22%3A%22bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22structure%22%2C%22params%22%3A%7B%22type%22%3A%22model%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22polymer%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22cartoon%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22green%22%7D%7D%2C%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22selector%22%3A%7B%22label_asym_id%22%3A%22A%22%2C%22beg_label_seq_id%22%3A1%2C%22end_label_seq_id%22%3A50%7D%2C%22color%22%3A%22%236688ff%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Protein%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22ligand%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22ball_and_stick%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22%23cc3399%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Retinoic%20Acid%22%7D%7D%5D%7D%5D%7D%5D%7D%5D%7D%2C%7B%22kind%22%3A%22canvas%22%2C%22params%22%3A%7B%22background_color%22%3A%22%23ffffee%22%7D%7D%2C%7B%22kind%22%3A%22camera%22%2C%22params%22%3A%7B%22target%22%3A%5B17%2C21%2C27%5D%2C%22position%22%3A%5B41%2C34%2C69%5D%2C%22up%22%3A%5B-0.129%2C0.966%2C-0.224%5D%7D%7D%5D%7D%7D + - https://molstar.org/viewer/?mvs-format=mvsx&mvs-url=https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1h9t.mvsx + + - https://molstar.org/viewer/?mvs-format=mvsj&mvs-data=%7B%22metadata%22%3A%7B%22title%22%3A%22Example%20MolViewSpec%20-%201cbs%20with%20labelled%20protein%20and%20ligand%22%2C%22version%22%3A%221%22%2C%22timestamp%22%3A%222023-11-24T10%3A38%3A17.483%22%7D%2C%22root%22%3A%7B%22kind%22%3A%22root%22%2C%22children%22%3A%5B%7B%22kind%22%3A%22download%22%2C%22params%22%3A%7B%22url%22%3A%22https%3A//www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22parse%22%2C%22params%22%3A%7B%22format%22%3A%22bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22structure%22%2C%22params%22%3A%7B%22type%22%3A%22model%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22polymer%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22cartoon%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22green%22%7D%7D%2C%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22selector%22%3A%7B%22label_asym_id%22%3A%22A%22%2C%22beg_label_seq_id%22%3A1%2C%22end_label_seq_id%22%3A50%7D%2C%22color%22%3A%22%236688ff%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Protein%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22ligand%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22ball_and_stick%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22%23cc3399%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Retinoic%20Acid%22%7D%7D%5D%7D%5D%7D%5D%7D%5D%7D%2C%7B%22kind%22%3A%22canvas%22%2C%22params%22%3A%7B%22background_color%22%3A%22%23ffffee%22%7D%7D%2C%7B%22kind%22%3A%22camera%22%2C%22params%22%3A%7B%22target%22%3A%5B17%2C21%2C27%5D%2C%22position%22%3A%5B41%2C34%2C69%5D%2C%22up%22%3A%5B-0.129%2C0.966%2C-0.224%5D%7D%7D%5D%7D%7D + + - https://molstar.org/viewer/?mvs-format=mvsx&mvs-data=base64,UEsDBBQAAAAIADSFPlhDx8RXYwEAAGwFAAAUABwAYW5ub3RhdGlvbnMtMWg5dC5jaWZVVAkAA8MmuWVuvbtldXgLAAEE9gEAAAQUAAAAlZJNj4IwEIbv/RVNPPSkacGPckT8TDabPe6emkq72ARbF9iD/34LFBV1FSbh7YTh4Z1hELzgjOyDgnGtTcELZXQOQGrMkQEWm8PRaKmLfJTynUwZz08HpkSrck5BCD8yU0ilwfycRXDxHoJFpUu4NqkAm/pYwWifmkwJCdaXdHtJmy6uOrtt47q0kwmry7n8uatKLZ5UY2M9Hzi1byWZ+T2WbAhtBPaiPoRoIMR0ijGyqZ1yuFNaKJ0geI5RBdAxhJ5PSgBjSmsgjE/pMDIhbFEj+wFbDnHcAP85zO8dKH3mEFWHEzQgBGPOnQP8vHp347BoA6KKBvh6ACzbwLeNeoZy/XfPW2DTF1i1AYyFqIHmH0I3wLovsO0LlJuezJygTDZ9ozeVcC2aLcBcFSVLKmA6c9IRIB5x8hrwamDipDMQOOkKzKiT14BfAh72S+nY0rzf0OMa6D60A7oPXQF9ZhiDP1BLAwQUAAAACACagEFYhgn8CJECAAAbFQAACgAcAGluZGV4Lm12c2pVVAkAAyTBu2UlwbtldXgLAAEE9gEAAAQUAAAA7VjBjtowEL3vV0S5tiSBFnXhttpKPbWq1KqXqoqMPWwsHDuynaVoxb/XNhAIiUOEtK26m5zIzLzJG8+8gfB0EwShFEKH8+DJfDZ3K8qJudtZ3+5sOKOMSODG/tNZgn10DUHEmjOByB7lfAWSKFdV9r21lMwCMq0LNY/j9XodwYJGCEflKi7IAmLgWm5GS8pAxYe88Tib6bQsCNJAIkyXYZVze/LIFrJ1wjXShqCCE8Z+1s6zFDJH9rTCPK8xOGPRxaTJpsZIaVliXcpzVt3MnFdvCnDcBAEWNiK2LQk7OLbzrHHFIi8EN81Kl1LkaSlpC+nLxF2EBZuUUYw4FxppKrga2Za7VrenrXWkMwyboXkQcpNylEONuurITYGRJsIPWDCBV2kGiIC0EDexJ/X4kQpnkCP3mAxRfonTI2IlKE/T9pFfpdBgUnkifrXaW2bEJbswJ/bydfZ0qUAhQZkjdMfhLbLPvLiow8BjJLUQ3lK9ZbkkPUqzVxeRmiSYkJfkUKF6lekir5NHBe8nkyq8IZc+Q3x82JluzIlcBl2tnSrDUUNmyCgpIZWIP4B/Kuy17fD6fe3i8SHaolsn8nWv22fYgR+/3L2G/bdAjBmtkFRpilfDGvTC/+oaHPZRhRj20S7yk2CkS/b3mZENJd4ZeUlLS5VyiXCHHIZtNWwrT/Q124qhBbD/b1P1en2s/+J2lT7vW2qPGWnr5jV9M292TNNi6Ny/7VzDdq7LesSp9+g5WHcWe+cmIsxBI4I0Ov4P+QhS2a8bwziJxod/IgkoLGmh9547HsBvlBcMgjXVWfD5x7egeRyhpjkobeIsZpJM3o+SySgZfx+P59Pb+btZNPtwO5lO3yTJPEnskWxvtn8AUEsBAh4DFAAAAAgANIU+WEPHxFdjAQAAbAUAABQAGAAAAAAAAQAAAKSBAAAAAGFubm90YXRpb25zLTFoOXQuY2lmVVQFAAPDJrlldXgLAAEE9gEAAAQUAAAAUEsBAh4DFAAAAAgAmoBBWIYJ/AiRAgAAGxUAAAoAGAAAAAAAAQAAAKSBsQEAAGluZGV4Lm12c2pVVAUAAyTBu2V1eAsAAQT2AQAABBQAAABQSwUGAAAAAAIAAgCqAAAAhgQAAAAA ### Programming interface @@ -126,7 +150,7 @@ const mvsData2: MVSData = builder.getState(); await loadMVS(this.plugin, mvsData2, { replaceExisting: false }); ``` -When using the pre-built Mol* plugin bundle, `MVSData` and `loadMVS` are exposed as `molstar.PluginExtensions.mvs.MVSData` and `molstar.PluginExtensions.mvs.loadMVS`. Furthermore, the `molstar.Viewer` class has `loadMvsFromUrl` and `loadMvsData` methods, providing the same functionality as `mvs-url` and `mvs-data` URL parameters. +When using the pre-built Mol* plugin bundle, `MVSData` and `loadMVS` are exposed as `molstar.PluginExtensions.mvs.MVSData` and `molstar.PluginExtensions.mvs.loadMVS`. Furthermore, the `molstar.Viewer` class has `loadMvsFromUrl` and `loadMvsData` methods, providing the same functionality as `mvs-url` and `mvs-data` URL parameters. See the [integration examples](./integration-examples.html) page for a demonstration. ### Command-line utilities diff --git a/docs/extensions/mvs/annotations.md b/docs/extensions/mvs/annotations.md index 50e411536..93659a3aa 100644 --- a/docs/extensions/mvs/annotations.md +++ b/docs/extensions/mvs/annotations.md @@ -101,6 +101,8 @@ assuming that the JSON annotation file shown in the previous section is availabl The `uri` parameter can also hold a URI reference (relative URI). In such cases, this URI reference is relative to the URI of the MVS file itself (e.g. if the MVS file is available from `https://example.org/spanish/inquisition/expectations.mvsj`, then the relative URI `./annotations.json` is equivalent to `https://example.org/spanish/inquisition/annotations.json`). This is however not applicable in all cases (e.g. the MVS tree can be constructed ad-hoc within a web application, therefore it has no URI; or the MVS file is loaded from a local disk using drag&drop, therefore the relative location is not accessible by the browser). + A special case is when the MVS tree is saved in MVSX format. An MVSX file is a ZIP archive containing the MVS tree in `index.mvsj` and possibly other files. In this case, the relative URIs will resolve to the files within the archive (e.g. `./annotations.json` points to the file `annotations.json` stored in the MSVX archive). + ### From source The MVS annotations can in fact be stored within the same mmCIF file from which the structure coordinates are loaded. To reference these annotations, we can use `color_from_source`, `label_from_source`, `tooltip_from_source`, and `component_from_source` nodes. Example: diff --git a/docs/extensions/mvs/integration-examples.html b/docs/extensions/mvs/integration-examples.html new file mode 100644 index 000000000..0e99d609b --- /dev/null +++ b/docs/extensions/mvs/integration-examples.html @@ -0,0 +1,112 @@ + + + + + + + + + + + +

Integration of Mol* with MolViewSpec Extension

+

+ This page demonstrates several methods to integrate Mol* Viewer in a web page and use MolViewSpec functionality. + See the source HTML to see the actual code. +

+ + +

Method 1: Get MVS view from a server and pass to the viewer

+

+ The recommended method is to serve the MVS view files by your server (either as static files or generated by the + server on-demand) and call the loadMvsFromUrl method to retrieve and load them. + This example uses a MVS view file from the address specified in the sourceUrl variable. + If the MVS view file contains relative references, they will be resolved as relative to sourceUrl. +

+ +
+ + + +

+ A variation of this method uses molstar.PluginExtensions.mvs.loadMVS instead of + loadMvsFromUrl and allows replacing the MVS view after it has been loaded. +

+ +
+ + + + + +

Method 2: Construct MVS view on frontend and pass to the viewer

+

+ Another option is to utilize the MVS builder provided by the extension to build the view on frontend and then + pass it to the viewer. This example builds the view in plain JavaScript, directly in a <script> tag in + HTML. However, for a better developer experience consider writing the code in TypeScript. + If the built MVS view contains relative references, they will be resolved as relative to the URL of this HTML + page. +

+ +
+ + + +

+ Again, there is variation with using molstar.PluginExtensions.mvs.loadMVS instead of + loadMvsData. +

+ + + + \ No newline at end of file diff --git a/examples/mvs/1h9t.mvsx b/examples/mvs/1h9t.mvsx new file mode 100644 index 0000000000000000000000000000000000000000..64c63706af779a182c2cd8e1726665951c593f14 GIT binary patch literal 1350 zcmWIWW@Zs#U|`^2Fln`ma6Wz{JeiS!A%~TLL4-kuAu%s6za+6FGe56b*D%AfL@zlr zEi{Caf%&l7&eXiUyHiUmxEUB(z5%s>O_@5$x4+3ipzZx%QD2)cOEM4k7rFfLF`I4s zZr&`f-8WdG#cwqKuj3XGEfrOJ__pQY!?}q9dmi+>X>KZ9>wAzpwS>&!$(vDt?& zW~}V4neu6_kK}oO_GK=si~Jlp?2UpwmLB{w$F<<2T$Afk?;U0<8D{^AIG8bimJcWA zwdp0*p0oAVS?$h!ylcy3&ef-P{@Jpm;Q#g+*4-HJ2Pj2k6G${Y{3P`uE8)=+V*!ho zTxV8HzTObeB=^nx<&8^w?q7H;^}=oo^Dh2a#@aNAX8Qnmgw1MjjA-Nh!!ePGfk9f7 zfq@GcVVQX;sTF#;WyM+GXi_=2J5?2k(W2>X#KFE>1_FEEhr8_DrqU=@`f%GVFWarF zuB@8As;kE~M@Pnb)7+&QNB3Vh6X57{ZrQSS*X%hb&g?lUeSS~lhZ13yg(6PvpC6p8 zF)PsaDS?$cY-PDK~$cb_bG>??R!32%zn8=hvSlilF#(Ssm0ZwPKa$PJ!E>R zZ9$XpvAS#LHj5a!f7^1%X1$?B^2MV|PA)s?wL8^`CJ8eSQy33l(y+| zT3c-Uyr%G|l0J+1S+D6qPck(#xnG_>65GN$drDxBg}79v)()wg({_h#U%s|#gXtBdWU%)W3Zy{Z_KlLD6xha>?@Hj0w}91T3tY`ci5AWOvs0>XWthC{DL?uBo{(Gw)1i3|s8lxmt$< zozqTeXev(auy3f>db9QIhhMdCAAMB3wJ2KsQ|bEK={FkX_utvu_IJBOkL213A(>s_ zho!z8`u|Ew=38K^#J7ujpC%_P@&5LG=0slAdy=VfeD3v6I;kO9FW>+C^6j6yBTar6SkLskuX57H^UcJZ{B89N0p5&Ea?H5O zAqiml#J~V7V;Gh+f>_u~C00nOgqGBiO~%YH$R=+DT7aB=faZa+4~BWHY#=u<0pThj J-Npjq0RZ-*Ht+xd literal 0 HcmV?d00001 diff --git a/src/apps/viewer/app.ts b/src/apps/viewer/app.ts index 9f0ba1274..a5163b1e5 100644 --- a/src/apps/viewer/app.ts +++ b/src/apps/viewer/app.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal * @author Alexander Rose @@ -15,6 +15,7 @@ import { QualityAssessment } from '../../extensions/model-archive/quality-assess import { ModelExport } from '../../extensions/model-export'; import { Mp4Export } from '../../extensions/mp4-export'; import { MolViewSpec } from '../../extensions/mvs/behavior'; +import { loadMVSX } from '../../extensions/mvs/components/formats'; import { loadMVS } from '../../extensions/mvs/load'; import { MVSData } from '../../extensions/mvs/mvs-data'; import { PDBeStructureQualityReport } from '../../extensions/pdbe'; @@ -50,6 +51,7 @@ import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout'; import { PluginSpec } from '../../mol-plugin/spec'; import { PluginState } from '../../mol-plugin/state'; import { StateObjectRef, StateObjectSelector } from '../../mol-state'; +import { Task } from '../../mol-task'; import { Asset } from '../../mol-util/assets'; import { Color } from '../../mol-util/color'; import '../../mol-util/polyfill'; @@ -468,25 +470,46 @@ export class Viewer { return { model, coords, preset }; } - async loadMvsFromUrl(url: string, format: 'mvsj') { + async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx') { if (format === 'mvsj') { const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' })); const mvsData = MVSData.fromMVSJ(data); await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: url }); + } else if (format === 'mvsx') { + const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' })); + await this.plugin.runTask(Task.create('Load MVSX file', async ctx => { + const parsed = await loadMVSX(this.plugin, ctx, data); + await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl }); + })); } else { throw new Error(`Unknown MolViewSpec format: ${format}`); } - // We might add more formats in the future } - async loadMvsData(data: string, format: 'mvsj') { + /** Load MolViewSpec from `data`. + * If `format` is 'mvsj', `data` must be a string or a Uint8Array containing a UTF8-encoded string. + * If `format` is 'mvsx', `data` must be a Uint8Array or a string containing base64-encoded binary data prefixed with 'base64,'. */ + async loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx') { + if (typeof data === 'string' && data.startsWith('base64')) { + data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array + } if (format === 'mvsj') { + if (typeof data !== 'string') { + data = new TextDecoder().decode(data); // Decode Uint8Array to string using UTF8 + } const mvsData = MVSData.fromMVSJ(data); await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: undefined }); + } else if (format === 'mvsx') { + if (typeof data === 'string') { + throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'."); + } + await this.plugin.runTask(Task.create('Load MVSX file', async ctx => { + const parsed = await loadMVSX(this.plugin, ctx, data as Uint8Array); + await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl }); + })); } else { throw new Error(`Unknown MolViewSpec format: ${format}`); } - // We might add more formats in the future } handleResize() { diff --git a/src/cli/mvs/mvs-render.ts b/src/cli/mvs/mvs-render.ts index 018817314..e7582ccf9 100644 --- a/src/cli/mvs/mvs-render.ts +++ b/src/cli/mvs/mvs-render.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik * @@ -17,19 +17,21 @@ import path from 'path'; import pngjs from 'pngjs'; import { Canvas3DParams } from '../../mol-canvas3d/canvas3d'; +import { setCanvasModule } from '../../mol-geo/geometry/text/font-atlas'; import { PluginContext } from '../../mol-plugin/context'; import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context'; import { DefaultPluginSpec, PluginSpec } from '../../mol-plugin/spec'; import { ExternalModules, defaultCanvas3DParams } from '../../mol-plugin/util/headless-screenshot'; +import { Task } from '../../mol-task'; import { setFSModule } from '../../mol-util/data-source'; import { onelinerJsonString } from '../../mol-util/json'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; // MolViewSpec must be imported after HeadlessPluginContext import { MolViewSpec } from '../../extensions/mvs/behavior'; +import { loadMVSX } from '../../extensions/mvs/components/formats'; import { loadMVS } from '../../extensions/mvs/load'; import { MVSData } from '../../extensions/mvs/mvs-data'; -import { setCanvasModule } from '../../mol-geo/geometry/text/font-atlas'; setFSModule(fs); @@ -48,7 +50,7 @@ interface Args { /** Return parsed command line arguments for `main` */ function parseArguments(): Args { const parser = new ArgumentParser({ description: 'Command-line application for rendering images from MolViewSpec files' }); - parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj format' }); + parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj or .mvsx format. File format is inferred from the file extension.' }); parser.add_argument('-o', '--output', { required: true, nargs: '+', help: 'File path(s) for output files (one output path for each input file). Output format is inferred from the file extension (.png or .jpg)' }); parser.add_argument('-s', '--size', { help: `Output image resolution, {width}x{height}. Default: ${DEFAULT_SIZE}.`, default: DEFAULT_SIZE }); parser.add_argument('-m', '--molj', { action: 'store_true', help: `Save Mol* state (.molj) in addition to rendered images (use the same output file paths but with .molj extension)` }); @@ -75,10 +77,22 @@ async function main(args: Args): Promise { const output = args.output[i]; console.log(`Processing ${input} -> ${output}`); - const data = fs.readFileSync(input, { encoding: 'utf8' }); - const mvsData = MVSData.fromMVSJ(data); + let mvsData: MVSData; + let sourceUrl: string | undefined; + if (input.toLowerCase().endsWith('.mvsj')) { + const data = fs.readFileSync(input, { encoding: 'utf8' }); + mvsData = MVSData.fromMVSJ(data); + sourceUrl = `file://${path.resolve(input)}`; + } else if (input.toLowerCase().endsWith('.mvsx')) { + const data = fs.readFileSync(input); + const mvsx = await plugin.runTask(Task.create('Load MVSX', async ctx => loadMVSX(plugin, ctx, data))); + mvsData = mvsx.mvsData; + sourceUrl = mvsx.sourceUrl; + } else { + throw new Error(`Input file name must end with .mvsj or .mvsx: ${input}`); + } + await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: true, sourceUrl: sourceUrl }); - await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: true, sourceUrl: `file://${path.resolve(input)}` }); fs.mkdirSync(path.dirname(output), { recursive: true }); if (args.molj) { await plugin.saveStateSnapshot(withExtension(output, '.molj')); diff --git a/src/extensions/mvs/behavior.ts b/src/extensions/mvs/behavior.ts index dcddbf403..217ff760d 100644 --- a/src/extensions/mvs/behavior.ts +++ b/src/extensions/mvs/behavior.ts @@ -13,6 +13,7 @@ import { PluginBehavior } from '../../mol-plugin/behavior/behavior'; import { PluginContext } from '../../mol-plugin/context'; import { StructureRepresentationProvider } from '../../mol-repr/structure/representation'; import { StateAction } from '../../mol-state'; +import { Task } from '../../mol-task'; import { ColorTheme } from '../../mol-theme/color'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { MVSAnnotationColorThemeProvider } from './components/annotation-color-theme'; @@ -21,7 +22,7 @@ import { MVSAnnotationsProvider } from './components/annotation-prop'; import { MVSAnnotationTooltipsLabelProvider, MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop'; import { CustomLabelRepresentationProvider } from './components/custom-label/representation'; import { CustomTooltipsLabelProvider, CustomTooltipsProvider } from './components/custom-tooltips-prop'; -import { LoadMvsData, MVSJFormatProvider } from './components/formats'; +import { LoadMvsData, MVSJFormatProvider, MVSXFormatProvider, loadMVSX } from './components/formats'; import { IsMVSModelProvider } from './components/is-mvs-model-prop'; import { makeMultilayerColorThemeProvider } from './components/multilayer-color-theme'; import { loadMVS } from './load'; @@ -76,6 +77,7 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({ ], dataFormats: [ { name: 'MVSJ', provider: MVSJFormatProvider }, + { name: 'MVSX', provider: MVSXFormatProvider }, ], actions: [ LoadMvsData, @@ -158,18 +160,32 @@ interface DragAndDropHandler { handle: PluginDragAndDropHandler, } -/** DragAndDropHandler handler for `.mvsj` files */ +/** DragAndDropHandler handler for `.mvsj` and `.mvsx` files */ const MVSDragAndDropHandler: DragAndDropHandler = { - name: 'mvs-mvsj', - /** Load .mvsj files. Delete previous plugin state before loading. - * If multiple files are provided, merge their MVS data into one state. */ + name: 'mvs-mvsj-mvsx', + /** Load .mvsj and .mvsx files. Delete previous plugin state before loading. + * If multiple files are provided, merge their MVS data into one state. + * Return `true` if at least one file has been loaded. */ async handle(files: File[], plugin: PluginContext): Promise { let applied = false; for (const file of files) { if (file.name.toLowerCase().endsWith('.mvsj')) { - const data = await file.text(); - const mvsData = MVSData.fromMVSJ(data); - await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: !applied, sourceUrl: undefined }); + const task = Task.create('Load MVSJ file', async ctx => { + const data = await file.text(); + const mvsData = MVSData.fromMVSJ(data); + await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: !applied, sourceUrl: undefined }); + }); + await plugin.runTask(task); + applied = true; + } + if (file.name.toLowerCase().endsWith('.mvsx')) { + const task = Task.create('Load MVSX file', async ctx => { + const buffer = await file.arrayBuffer(); + const array = new Uint8Array(buffer); + const parsed = await loadMVSX(plugin, ctx, array); + await loadMVS(plugin, parsed.mvsData, { sanityChecks: true, replaceExisting: !applied, sourceUrl: parsed.sourceUrl }); + }); + await plugin.runTask(task); applied = true; } } diff --git a/src/extensions/mvs/components/formats.ts b/src/extensions/mvs/components/formats.ts index 9b79f182f..3f1973f29 100644 --- a/src/extensions/mvs/components/formats.ts +++ b/src/extensions/mvs/components/formats.ts @@ -1,17 +1,19 @@ /** - * Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik */ +import { hashFnv32a } from '../../../mol-data/util'; import { DataFormatProvider } from '../../../mol-plugin-state/formats/provider'; import { PluginStateObject as SO } from '../../../mol-plugin-state/objects'; import { Download } from '../../../mol-plugin-state/transforms/data'; import { PluginContext } from '../../../mol-plugin/context'; import { StateAction, StateObjectRef } from '../../../mol-state'; -import { Task } from '../../../mol-task'; -import { Asset } from '../../../mol-util/assets'; +import { RuntimeContext, Task } from '../../../mol-task'; +import { Asset, AssetManager } from '../../../mol-util/assets'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { unzip } from '../../../mol-util/zip/zip'; import { loadMVS } from '../load'; import { MVSData } from '../mvs-data'; import { MVSTransform } from './annotation-structure-component'; @@ -23,7 +25,7 @@ export class Mvs extends SO.Create<{ mvsData: MVSData, sourceUrl?: string }>({ n /** Transformer for parsing data in MVSJ format */ export const ParseMVSJ = MVSTransform({ name: 'mvs-parse-mvsj', - display: { name: 'MVS Annotation Component', description: 'A molecular structure component defined by MVS annotation data.' }, + display: { name: 'MolViewSpec from MVSJ', description: 'Create MolViewSpec view from MVSJ data' }, from: SO.Data.String, to: Mvs, })({ @@ -34,16 +36,27 @@ export const ParseMVSJ = MVSTransform({ }, }); -/** If the PluginStateObject `pso` comes from a Download transform, try to get its `url` parameter. */ -function tryGetDownloadUrl(pso: SO.Data.String, plugin: PluginContext): string | undefined { - const theCell = plugin.state.data.selectQ(q => q.ofTransformer(Download)).find(cell => cell.obj === pso); - const urlParam = theCell?.transform.params?.url; - return urlParam ? Asset.getUrl(urlParam) : undefined; -} +/** Transformer for parsing data in MVSX format (= zipped MVSJ + referenced files like structures and annotations) */ +export const ParseMVSX = MVSTransform({ + name: 'mvs-parse-mvsx', + display: { name: 'MolViewSpec from MVSX', description: 'Create MolViewSpec view from MVSX data' }, + from: SO.Data.Binary, + to: Mvs, + params: { + mainFilePath: PD.Text('index.mvsj'), + }, +})({ + apply({ a, params }, plugin: PluginContext) { + return Task.create('Parse MVSX file', async ctx => { + const data = await loadMVSX(plugin, ctx, a.data, params.mainFilePath); + return new Mvs(data); + }); + }, +}); /** Params for the `LoadMvsData` action */ -const LoadMvsDataParams = { +export const LoadMvsDataParams = { replaceExisting: PD.Boolean(false, { description: 'If true, the loaded MVS view will replace the current state; if false, the MVS view will be added to the current state.' }), }; @@ -75,3 +88,73 @@ export const MVSJFormatProvider: DataFormatProvider<{}, StateObjectRef, any return await plugin.state.data.applyAction(LoadMvsData, params, ref).run(); }, }); + +/** Data format provider for MVSX format. + * If Visuals:On, it will load the parsed MVS view; + * otherwise it will just create a plugin state object with parsed data. */ +export const MVSXFormatProvider: DataFormatProvider<{}, StateObjectRef, any> = DataFormatProvider({ + label: 'MVSX', + description: 'MVSX', + category: 'Miscellaneous', + binaryExtensions: ['mvsx'], + parse: async (plugin, data) => { + return plugin.state.data.build().to(data).apply(ParseMVSX).commit(); + }, + visuals: MVSJFormatProvider.visuals, +}); + + +/** Parse binary data `data` as MVSX archive, + * add all contained files to `plugin`'s asset manager, + * and parse the main file in the archive as MVSJ. + * Return parsed MVS data and `sourceUrl` for resolution of relative URIs. */ +export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array, mainFilePath: string = 'index.mvsj'): Promise<{ mvsData: MVSData, sourceUrl: string }> { + const archiveId = `ni,fnv1a;${hashFnv32a(data)}`; + let files: { [path: string]: Uint8Array }; + try { + files = await unzip(runtimeCtx, data) as typeof files; + } catch (err) { + plugin.log.error('Invalid MVSX file'); + throw err; + } + for (const path in files) { + const url = arcpUri(archiveId, path); + ensureUrlAsset(plugin.managers.asset, url, files[path]); + } + const mainFile = files[mainFilePath]; + if (!mainFile) throw new Error(`File ${mainFilePath} not found in the MVSX archive`); + const mvsData = MVSData.fromMVSJ(decodeUtf8(mainFile)); + const sourceUrl = arcpUri(archiveId, mainFilePath); + return { mvsData, sourceUrl }; +} + +/** If the PluginStateObject `pso` comes from a Download transform, try to get its `url` parameter. */ +function tryGetDownloadUrl(pso: SO.Data.String, plugin: PluginContext): string | undefined { + const theCell = plugin.state.data.selectQ(q => q.ofTransformer(Download)).find(cell => cell.obj === pso); + const urlParam = theCell?.transform.params?.url; + return urlParam ? Asset.getUrl(urlParam) : undefined; +} + +/** Return a URI referencing a file within an archive, using ARCP scheme (https://arxiv.org/pdf/1809.06935.pdf). + * `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,fnv1a;938511930') + * `path` corresponds to the path to a file within the archive */ +function arcpUri(archiveId: string, path: string): string { + return new URL(path, `arcp://${archiveId}/`).href; +} + +/** Add a URL asset to asset manager. + * Skip if an asset with the same URL already exists. */ +function ensureUrlAsset(manager: AssetManager, url: string, data: Uint8Array) { + const asset = Asset.getUrlAsset(manager, url); + if (!manager.has(asset)) { + const filename = url.split('/').pop() ?? 'file'; + manager.set(asset, new File([data], filename)); + } +} + +/** Decode bytes to text using UTF-8 encoding */ +function decodeUtf8(bytes: Uint8Array): string { + _decoder ??= new TextDecoder(); + return _decoder.decode(bytes); +} +let _decoder: TextDecoder | undefined; diff --git a/src/mol-util/zip/zip.ts b/src/mol-util/zip/zip.ts index ea838f5c3..90f139552 100644 --- a/src/mol-util/zip/zip.ts +++ b/src/mol-util/zip/zip.ts @@ -22,6 +22,9 @@ export function Unzip(buf: ArrayBuffer, onlyNames = false) { export async function unzip(runtime: RuntimeContext, buf: ArrayBuffer, onlyNames = false) { const out: { [k: string]: Uint8Array | { size: number, csize: number } } = Object.create(null); const data = new Uint8Array(buf); + if (readUshort(data, 0) !== 0x4b50) { + throw new Error('Invalid ZIP file. A valid ZIP file must start with two magic bytes \\x50\\x4b ("PK" in ASCII).'); + } let eocd = data.length - 4; while (readUint(data, eocd) !== 0x06054b50) eocd--;