ソースを参照

added preset manager, updated molstar

Alexander Rose 5 年 前
コミット
3bed62e55a

+ 83 - 70
package-lock.json

@@ -43,9 +43,9 @@
             "dev": true
         },
         "@types/node": {
-            "version": "13.7.5",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.5.tgz",
-            "integrity": "sha512-PfSBCTQhAQg6QBP4UhXgrZ/wQ3pjfwBr4sA7Aul+pC9XwGgm9ezrJF7OiC/I4Kf+7VPu/5ThKngAruqxyctZfA==",
+            "version": "13.7.7",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.7.tgz",
+            "integrity": "sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg==",
             "dev": true
         },
         "@types/node-fetch": {
@@ -109,16 +109,6 @@
                 "tsutils": "^3.17.1"
             }
         },
-        "@typescript-eslint/eslint-plugin-tslint": {
-            "version": "2.21.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-2.21.0.tgz",
-            "integrity": "sha512-wjNoZmAuJU1zmUJHnbxLfT82qabMeFxN+UMIulpBrL/lD8gR84cl0nKh+7f3zDxA5VViCazP1BuEl/fpj5dWAA==",
-            "dev": true,
-            "requires": {
-                "@typescript-eslint/experimental-utils": "2.21.0",
-                "lodash": "^4.17.15"
-            }
-        },
         "@typescript-eslint/experimental-utils": {
             "version": "2.21.0",
             "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.21.0.tgz",
@@ -4202,7 +4192,7 @@
             }
         },
         "molstar": {
-            "version": "0.5.2",
+            "version": "0.5.4",
             "dev": true,
             "requires": {
                 "@types/argparse": "^1.0.38",
@@ -4210,9 +4200,9 @@
                 "@types/compression": "1.7.0",
                 "@types/express": "^4.17.2",
                 "@types/jest": "^25.1.3",
-                "@types/node": "^13.7.4",
-                "@types/node-fetch": "^2.5.4",
-                "@types/react": "^16.9.22",
+                "@types/node": "^13.7.7",
+                "@types/node-fetch": "^2.5.5",
+                "@types/react": "^16.9.23",
                 "@types/react-dom": "^16.9.5",
                 "@types/swagger-ui-dist": "3.0.5",
                 "argparse": "^1.0.10",
@@ -4221,10 +4211,11 @@
                 "cors": "^2.8.5",
                 "express": "^4.17.1",
                 "graphql": "^14.6.0",
+                "immer": "^5.3.6",
                 "immutable": "^3.8.2",
                 "node-fetch": "^2.6.0",
-                "react": "^16.12.0",
-                "react-dom": "^16.12.0",
+                "react": "^16.13.0",
+                "react-dom": "^16.13.0",
                 "rxjs": "^6.5.4",
                 "swagger-ui-dist": "^3.25.0",
                 "util.promisify": "^1.0.1",
@@ -7310,16 +7301,31 @@
                     }
                 },
                 "@types/node": {
-                    "version": "13.7.4",
-                    "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.4.tgz",
-                    "integrity": "sha512-oVeL12C6gQS/GAExndigSaLxTrKpQPxewx9bOcwfvJiJge4rr7wNaph4J+ns5hrmIV2as5qxqN8YKthn9qh0jw=="
+                    "version": "13.7.5",
+                    "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.5.tgz",
+                    "integrity": "sha512-PfSBCTQhAQg6QBP4UhXgrZ/wQ3pjfwBr4sA7Aul+pC9XwGgm9ezrJF7OiC/I4Kf+7VPu/5ThKngAruqxyctZfA=="
                 },
                 "@types/node-fetch": {
-                    "version": "2.5.4",
-                    "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz",
-                    "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==",
+                    "version": "2.5.5",
+                    "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.5.tgz",
+                    "integrity": "sha512-IWwjsyYjGw+em3xTvWVQi5MgYKbRs0du57klfTaZkv/B24AEQ/p/IopNeqIYNy3EsfHOpg8ieQSDomPcsYMHpA==",
+                    "dev": true,
                     "requires": {
-                        "@types/node": "*"
+                        "@types/node": "*",
+                        "form-data": "^3.0.0"
+                    },
+                    "dependencies": {
+                        "form-data": {
+                            "version": "3.0.0",
+                            "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
+                            "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
+                            "dev": true,
+                            "requires": {
+                                "asynckit": "^0.4.0",
+                                "combined-stream": "^1.0.8",
+                                "mime-types": "^2.1.12"
+                            }
+                        }
                     }
                 },
                 "@types/parse-json": {
@@ -7339,9 +7345,9 @@
                     "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
                 },
                 "@types/react": {
-                    "version": "16.9.22",
-                    "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.22.tgz",
-                    "integrity": "sha512-7OSt4EGiLvy0h5R7X+r0c7S739TCU/LvWbkNOrm10lUwNHe7XPz5OLhLOSZeCkqO9JSCly1NkYJ7ODTUqVnHJQ==",
+                    "version": "16.9.23",
+                    "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.23.tgz",
+                    "integrity": "sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw==",
                     "dev": true,
                     "requires": {
                         "@types/prop-types": "*",
@@ -7397,11 +7403,11 @@
                     "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
                 },
                 "@typescript-eslint/eslint-plugin": {
-                    "version": "2.20.0",
-                    "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.20.0.tgz",
-                    "integrity": "sha512-cimIdVDV3MakiGJqMXw51Xci6oEDEoPkvh8ggJe2IIzcc0fYqAxOXN6Vbeanahz6dLZq64W+40iUEc9g32FLDQ==",
+                    "version": "2.21.0",
+                    "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.21.0.tgz",
+                    "integrity": "sha512-b5jjjDMxzcjh/Sbjuo7WyhrQmVJg0WipTHQgXh5Xwx10uYm6nPWqN1WGOsaNq4HR3Zh4wUx4IRQdDkCHwyewyw==",
                     "requires": {
-                        "@typescript-eslint/experimental-utils": "2.20.0",
+                        "@typescript-eslint/experimental-utils": "2.21.0",
                         "eslint-utils": "^1.4.3",
                         "functional-red-black-tree": "^1.0.1",
                         "regexpp": "^3.0.0",
@@ -7415,40 +7421,31 @@
                         }
                     }
                 },
-                "@typescript-eslint/eslint-plugin-tslint": {
-                    "version": "2.20.0",
-                    "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-2.20.0.tgz",
-                    "integrity": "sha512-nvQqHXNtTg56eeLgl8BbTqw0+PILjgtthB2MEJ279NqfSMjTzUr7dkt/JIuGbxi9netT7u3iQaTE4nuGbGTTpQ==",
-                    "requires": {
-                        "@typescript-eslint/experimental-utils": "2.20.0",
-                        "lodash": "^4.17.15"
-                    }
-                },
                 "@typescript-eslint/experimental-utils": {
-                    "version": "2.20.0",
-                    "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.20.0.tgz",
-                    "integrity": "sha512-fEBy9xYrwG9hfBLFEwGW2lKwDRTmYzH3DwTmYbT+SMycmxAoPl0eGretnBFj/s+NfYBG63w/5c3lsvqqz5mYag==",
+                    "version": "2.21.0",
+                    "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.21.0.tgz",
+                    "integrity": "sha512-olKw9JP/XUkav4lq0I7S1mhGgONJF9rHNhKFn9wJlpfRVjNo3PPjSvybxEldvCXnvD+WAshSzqH5cEjPp9CsBA==",
                     "requires": {
                         "@types/json-schema": "^7.0.3",
-                        "@typescript-eslint/typescript-estree": "2.20.0",
+                        "@typescript-eslint/typescript-estree": "2.21.0",
                         "eslint-scope": "^5.0.0"
                     }
                 },
                 "@typescript-eslint/parser": {
-                    "version": "2.20.0",
-                    "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.20.0.tgz",
-                    "integrity": "sha512-o8qsKaosLh2qhMZiHNtaHKTHyCHc3Triq6aMnwnWj7budm3xAY9owSZzV1uon5T9cWmJRJGzTFa90aex4m77Lw==",
+                    "version": "2.21.0",
+                    "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.21.0.tgz",
+                    "integrity": "sha512-VrmbdrrrvvI6cPPOG7uOgGUFXNYTiSbnRq8ZMyuGa4+qmXJXVLEEz78hKuqupvkpwJQNk1Ucz1TenrRP90gmBg==",
                     "requires": {
                         "@types/eslint-visitor-keys": "^1.0.0",
-                        "@typescript-eslint/experimental-utils": "2.20.0",
-                        "@typescript-eslint/typescript-estree": "2.20.0",
+                        "@typescript-eslint/experimental-utils": "2.21.0",
+                        "@typescript-eslint/typescript-estree": "2.21.0",
                         "eslint-visitor-keys": "^1.1.0"
                     }
                 },
                 "@typescript-eslint/typescript-estree": {
-                    "version": "2.20.0",
-                    "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.20.0.tgz",
-                    "integrity": "sha512-WlFk8QtI8pPaE7JGQGxU7nGcnk1ccKAJkhbVookv94ZcAef3m6oCE/jEDL6dGte3JcD7reKrA0o55XhBRiVT3A==",
+                    "version": "2.21.0",
+                    "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.21.0.tgz",
+                    "integrity": "sha512-NC/nogZNb9IK2MEFQqyDBAciOT8Lp8O3KgAfvHx2Skx6WBo+KmDqlU3R9KxHONaijfTIKtojRe3SZQyMjr3wBw==",
                     "requires": {
                         "debug": "^4.1.1",
                         "eslint-visitor-keys": "^1.1.0",
@@ -7828,6 +7825,16 @@
                         "core-js": "^3.0.1",
                         "node-fetch": "^2.2.0",
                         "sha.js": "^2.4.11"
+                    },
+                    "dependencies": {
+                        "@types/node-fetch": {
+                            "version": "2.5.4",
+                            "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz",
+                            "integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==",
+                            "requires": {
+                                "@types/node": "*"
+                            }
+                        }
                     }
                 },
                 "apollo-graphql": {
@@ -12413,6 +12420,12 @@
                     "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
                     "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
                 },
+                "immer": {
+                    "version": "5.3.6",
+                    "resolved": "https://registry.npmjs.org/immer/-/immer-5.3.6.tgz",
+                    "integrity": "sha512-pqWQ6ozVfNOUDjrLfm4Pt7q4Q12cGw2HUZgry4Q5+Myxu9nmHRkWBpI0J4+MK0AxbdFtdMTwEGVl7Vd+vEiK+A==",
+                    "dev": true
+                },
                 "immutable": {
                     "version": "3.8.2",
                     "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
@@ -21748,9 +21761,9 @@
             }
         },
         "react": {
-            "version": "16.12.0",
-            "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
-            "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
+            "version": "16.13.0",
+            "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz",
+            "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==",
             "dev": true,
             "requires": {
                 "loose-envify": "^1.1.0",
@@ -21759,21 +21772,21 @@
             }
         },
         "react-dom": {
-            "version": "16.12.0",
-            "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
-            "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==",
+            "version": "16.13.0",
+            "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.0.tgz",
+            "integrity": "sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg==",
             "dev": true,
             "requires": {
                 "loose-envify": "^1.1.0",
                 "object-assign": "^4.1.1",
                 "prop-types": "^15.6.2",
-                "scheduler": "^0.18.0"
+                "scheduler": "^0.19.0"
             }
         },
         "react-is": {
-            "version": "16.12.0",
-            "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
-            "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==",
+            "version": "16.13.0",
+            "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz",
+            "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==",
             "dev": true
         },
         "read-pkg": {
@@ -22320,9 +22333,9 @@
             }
         },
         "scheduler": {
-            "version": "0.18.0",
-            "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz",
-            "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==",
+            "version": "0.19.0",
+            "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.0.tgz",
+            "integrity": "sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==",
             "dev": true,
             "requires": {
                 "loose-envify": "^1.1.0",
@@ -23184,9 +23197,9 @@
             "dev": true
         },
         "typescript": {
-            "version": "3.8.2",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz",
-            "integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==",
+            "version": "3.8.3",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
+            "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
             "dev": true
         },
         "union-value": {

+ 6 - 7
package.json

@@ -12,7 +12,7 @@
     },
     "scripts": {
         "test": "npm run lint",
-        "lint": "tslint src/**/*.ts",
+        "lint": "eslint src/**/*.ts",
         "build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
         "build-tsc": "tsc",
         "build-extra": "cpx \"src/**/*.{scss,woff,woff2,ttf,otf,eot,svg,html,ico}\" build/src/",
@@ -37,12 +37,11 @@
     "license": "MIT",
     "devDependencies": {
         "@types/argparse": "^1.0.38",
-        "@types/node": "^13.7.5",
+        "@types/node": "^13.7.7",
         "@types/node-fetch": "^2.5.5",
         "@types/react": "^16.9.23",
         "@types/react-dom": "^16.9.5",
         "@typescript-eslint/eslint-plugin": "^2.21.0",
-        "@typescript-eslint/eslint-plugin-tslint": "^2.21.0",
         "@typescript-eslint/parser": "^2.21.0",
         "concurrently": "^5.1.0",
         "cpx2": "^2.0.0",
@@ -51,17 +50,17 @@
         "extra-watch-webpack-plugin": "^1.0.3",
         "file-loader": "^5.1.0",
         "mini-css-extract-plugin": "^0.9.0",
-        "molstar": "^0.5.2",
+        "molstar": "^0.5.4",
         "node-fetch": "^2.6.0",
         "node-sass": "^4.13.1",
         "raw-loader": "^4.0.0",
-        "react": "^16.12.0",
-        "react-dom": "^16.12.0",
+        "react": "^16.13.0",
+        "react-dom": "^16.13.0",
         "resolve-url-loader": "^3.1.1",
         "rxjs": "^6.5.4",
         "sass-loader": "^8.0.2",
         "style-loader": "^1.1.3",
-        "typescript": "3.8.2",
+        "typescript": "3.8.3",
         "webpack": "^4.41.6",
         "webpack-cli": "^3.3.11"
     },

+ 5 - 1
src/structure-viewer/helpers/model.ts

@@ -12,6 +12,10 @@ import { PluginContext } from 'molstar/lib/mol-plugin/context';
 import { PluginStateObject as PSO } from 'molstar/lib/mol-plugin/state/objects';
 
 export class ModelLoader {
+    get customState() {
+        return this.plugin.customState as StructureViewerState
+    }
+
     download(b: StateBuilder.To<PSO.Root>, url: string, isBinary: boolean) {
         return b.apply(StateTransforms.Data.Download, { url, isBinary })
     }
@@ -43,7 +47,7 @@ export class ModelLoader {
     }
 
     async init(assemblyId = 'deposited') {
-        await (this.plugin.customState as StructureViewerState).structureView.setAssembly(assemblyId)
+        await this.customState.structureView.setAssembly(assemblyId)
     }
 
     async applyState(tree: StateBuilder) {

+ 245 - 0
src/structure-viewer/helpers/preset.ts

@@ -0,0 +1,245 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureViewerState } from '../types';
+import { getStructureSize, StructureSize } from './util';
+import { PluginContext } from 'molstar/lib/mol-plugin/context';
+import { Structure } from 'molstar/lib/mol-model/structure';
+import { Loci, EmptyLoci } from 'molstar/lib/mol-model/loci';
+import { Axes3D } from 'molstar/lib/mol-math/geometry';
+import { Vec3 } from 'molstar/lib/mol-math/linear-algebra';
+import { ValidationReport } from 'molstar/lib/mol-model-props/rcsb/validation-report';
+import { StructureSelectionQueries as SSQ } from 'molstar/lib/mol-plugin/util/structure-selection-helper';
+import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
+import { AssemblySymmetry } from 'molstar/lib/mol-model-props/rcsb/assembly-symmetry';
+
+type Target = {
+    readonly auth_seq_id?: number
+    readonly label_seq_id?: number
+    readonly label_comp_id?: number
+    readonly label_asym_id?: number
+    readonly pdbx_struct_oper_list_ids?: string[]
+}
+
+function targetToLoci(target: Target, structure: Structure): Loci {
+    return EmptyLoci
+}
+
+type ValidationProps = {
+    kind: 'validation'
+    colorTheme?: string
+    showClashes?: boolean
+    modelIndex?: number
+}
+
+type AssemblyProps = {
+    kind: 'assembly'
+    assemblyId: string
+    modelIndex?: number
+}
+
+type StandardProps = {
+    kind: 'standard'
+}
+
+type SymmetryProps = {
+    kind: 'symmetry'
+    assemblyId?: string
+    symmetryIndex?: number
+}
+
+type FeatureProps = {
+    kind: 'feature'
+    assemblyId: string
+    target: Target
+}
+
+export type PresetProps = ValidationProps | AssemblyProps | StandardProps | SymmetryProps | FeatureProps
+
+export class PresetManager {
+    get customState() {
+        return this.plugin.customState as StructureViewerState
+    }
+
+    async apply(props?: PresetProps) {
+        if (!props) props = { kind: 'assembly', assemblyId: 'deposited' }
+        switch (props.kind) {
+            case 'assembly':
+                return this.assembly(props.assemblyId, props.modelIndex)
+            case 'feature':
+                return this.feature(props.target, props.assemblyId)
+            case 'standard':
+                return this.standard()
+            case 'symmetry':
+                return this.symmetry(props.symmetryIndex, props.assemblyId)
+            case 'validation':
+                return this.validation(props.colorTheme, props.showClashes, props.modelIndex)
+        }
+    }
+
+    async default() {
+        const assembly = this.customState.structureView.getAssembly()
+        if (!assembly || assembly.data.isEmpty) return
+
+        const r = this.plugin.helpers.structureRepresentation
+        const size = getStructureSize(assembly.data)
+
+        if (size === StructureSize.Gigantic) {
+            await r.clearExcept(['gaussian-surface'])
+            await r.setFromExpression('only', 'gaussian-surface', SSQ.trace.expression, {
+                repr: {
+                    radiusOffset: 1,
+                    smoothness: 0.5,
+                    visuals: ['structure-gaussian-surface-mesh']
+                }
+            })
+        } else if(size === StructureSize.Huge) {
+            await r.clearExcept(['gaussian-surface'])
+            await r.setFromExpression('add', 'gaussian-surface', SSQ.polymer.expression, {
+                repr: {
+                    smoothness: 0.5
+                },
+            })
+        } else if(size === StructureSize.Large) {
+            await r.clearExcept(['cartoon'])
+            await r.setFromExpression('only', 'cartoon', SSQ.polymer.expression)
+        } else if(size === StructureSize.Medium) {
+            await r.clearExcept(['cartoon', 'carbohydrate', 'ball-and-stick'])
+            await r.setFromExpression('only', 'cartoon', SSQ.polymer.expression)
+            await r.setFromExpression('only', 'carbohydrate', SSQ.branchedPlusConnected.expression)
+            await r.setFromExpression('only', 'ball-and-stick', MS.struct.modifier.union([
+                MS.struct.combinator.merge([
+                    SSQ.ligandPlusConnected.expression,
+                    SSQ.branchedConnectedOnly.expression,
+                    SSQ.disulfideBridges.expression,
+                    SSQ.nonStandardPolymer.expression,
+                    // SSQ.water.expression
+                ])
+            ]))
+        } else if(size === StructureSize.Small) {
+            await r.clearExcept(['ball-and-stick'])
+            await r.setFromExpression('only', 'ball-and-stick', MS.struct.modifier.union([
+                MS.struct.modifier.exceptBy({
+                    0: MS.struct.generator.all(),
+                    by: SSQ.water.expression
+                })
+            ]))
+        }
+    }
+
+    async standard() {
+        await this.customState.structureView.setSymmetry(-1)
+        await this.default()
+        this.focus()
+    }
+
+    async assembly(assemblyId: string, modelIndex?: number) {
+        if (modelIndex !== undefined) {
+            await this.customState.structureView.setModel(modelIndex)
+        }
+        await this.customState.structureView.setAssembly(assemblyId)
+        await this.default()
+        this.focus()
+    }
+
+    async model(modelIndex: number) {
+        await this.customState.structureView.setModel(modelIndex)
+        await this.default()
+        this.focus()
+    }
+
+    async feature(target: Target, assemblyId?: string, modelIndex?: number) {
+        if (modelIndex !== undefined) {
+            await this.customState.structureView.setModel(modelIndex)
+        }
+        if (assemblyId !== undefined) {
+            await this.customState.structureView.setAssembly(assemblyId)
+        }
+        const assembly = this.customState.structureView.getAssembly()
+        if (!assembly || assembly.data.isEmpty) return
+
+        const loci = targetToLoci(target, assembly.data)
+        // TODO show target and surrounding residues in detail if small
+        this.focus(loci)
+    }
+
+    async symmetry(symmetryIndex?: number, assemblyId?: string) {
+        if (assemblyId !== undefined) {
+            await this.customState.structureView.setAssembly(assemblyId)
+            await this.default()
+        }
+
+        const assembly = this.customState.structureView.getAssembly()
+        if (!assembly || assembly.data.isEmpty) return
+
+        const r = this.plugin.helpers.structureRepresentation
+
+        await this.customState.structureView.setSymmetry(symmetryIndex || 0)
+        r.eachRepresentation((repr, type, update) => {
+            if (type !== ValidationReport.Tag.Clashes) {
+                r.setRepresentationParams(repr, type, update, { color: AssemblySymmetry.Tag.Cluster })
+            }
+        })
+
+        // TODO focus on symmetry axes
+        this.focus()
+    }
+
+    async validation(colorTheme?: string, showClashes?: boolean, modelIndex?: number) {
+        if (modelIndex !== undefined) {
+            this.customState.structureView.setModel(modelIndex)
+            await this.default()
+        }
+
+        const assembly = this.customState.structureView.getAssembly()
+        if (!assembly || assembly.data.isEmpty) return
+
+        const r = this.plugin.helpers.structureRepresentation
+
+        if (showClashes === undefined) {
+            showClashes = getStructureSize(assembly.data) <= StructureSize.Medium
+        }
+
+        await this.customState.structureView.attachValidationReport()
+        if (showClashes) {
+            await r.setFromExpression('only', ValidationReport.Tag.Clashes, SSQ.all.expression)
+            await r.setFromExpression('add', 'ball-and-stick', SSQ.hasClash.expression)
+        } else {
+            await r.setFromExpression('remove', ValidationReport.Tag.Clashes, SSQ.all.expression)
+        }
+
+        if (colorTheme === undefined) colorTheme = ValidationReport.Tag.GeometryQuality
+        r.eachRepresentation((repr, type, update) => {
+            if (type !== ValidationReport.Tag.Clashes) {
+                r.setRepresentationParams(repr, type, update, { color: colorTheme })
+            }
+        })
+
+        this.focus()
+    }
+
+    focus(loci?: Loci) {
+        if (!loci) {
+            const assembly = this.customState.structureView.getAssembly()
+            if (!assembly || assembly.data.isEmpty) return
+
+            loci = Structure.toStructureElementLoci(assembly.data)
+        }
+
+        const principalAxes = Loci.getPrincipalAxes(loci)
+        if (!principalAxes) return
+
+        const extraRadius = 4, minRadius = 8, durationMs = 250
+        const { origin, dirA, dirC } = principalAxes.boxAxes
+        const axesRadius = Math.max(...Axes3D.size(Vec3(), principalAxes.boxAxes)) / 2
+        const radius = Math.max(axesRadius + extraRadius, minRadius)
+        this.plugin.canvas3d!.camera.focus(origin, radius, radius, durationMs, dirA, dirC);
+    }
+
+    constructor(private plugin: PluginContext) {
+
+    }
+}

+ 42 - 29
src/structure-viewer/helpers/structure.ts

@@ -4,53 +4,47 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { StateElements, AssemblyNames, StructureViewerState } from '../types';
+import { StateElements, AssemblyNames, StructureViewerState, ModelNames } from '../types';
 import { PluginCommands } from 'molstar/lib/mol-plugin/command';
 import { StateBuilder, State, StateSelection } from 'molstar/lib/mol-state';
 import { StateTransforms } from 'molstar/lib/mol-plugin/state/transforms';
 import { Vec3 } from 'molstar/lib/mol-math/linear-algebra';
 import { PluginContext } from 'molstar/lib/mol-plugin/context';
 import { PluginStateObject as PSO } from 'molstar/lib/mol-plugin/state/objects';
-import { Structure, StructureElement } from 'molstar/lib/mol-model/structure';
 import { AssemblySymmetryProvider } from 'molstar/lib/mol-model-props/rcsb/assembly-symmetry';
 import { Task } from 'molstar/lib/mol-task';
 import { AssemblySymmetry3D } from 'molstar/lib/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry';
+import { ValidationReportProvider } from 'molstar/lib/mol-model-props/rcsb/validation-report';
 
 export class StructureView {
+    get customState() {
+        return this.plugin.customState as StructureViewerState
+    }
+
     async applyState(tree: StateBuilder) {
         await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
     }
 
     get experimentalData () {
-        return (this.plugin.customState as StructureViewerState).volumeData
+        return this.customState.volumeData
     }
 
-    private findTrajectoryRef() {
+    findTrajectoryRef() {
         const trajectories = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Trajectory))
         return trajectories.length > 0 ? trajectories[0].transform.ref : ''
     }
 
-    private getAssembly() {
+    getAssembly() {
         const trajectoryRef = this.findTrajectoryRef()
         if (!trajectoryRef || !this.plugin.state.dataState.transforms.has(trajectoryRef)) return
         const assemblies = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure, trajectoryRef))
         return assemblies.length > 0 ? assemblies[0].obj : undefined
     }
 
-    async preset() {
-        await this.plugin.helpers.structureRepresentation.preset()
-
-        const assembly = this.getAssembly()
-        if (!assembly || assembly.data.isEmpty) return
-
-        const extraRadius = 4, minRadius = 8, durationMs = 250
-
-        const radius = Math.max(assembly.data.lookup3d.boundary.sphere.radius + extraRadius, minRadius);
-        const loci = Structure.toStructureElementLoci(assembly.data)
-        const principalAxes = StructureElement.Loci.getPrincipalAxes(loci)
-        const { origin, dirA, dirC } = principalAxes.boxAxes
-
-        this.plugin.canvas3d!.camera.focus(origin, radius, radius, durationMs, dirA, dirC);
+    getModel() {
+        const trajectoryRef = this.findTrajectoryRef()
+        const models = this.plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Model, trajectoryRef))
+        return models.length > 0 ? models[0].obj : undefined
     }
 
     private ensureModelUnitcell(tree: StateBuilder.Root, state: State) {
@@ -62,7 +56,7 @@ export class StructureView {
         }
     }
 
-    private async attachSymmetry() {
+    async attachAssemblySymmetry() {
         const assembly = this.getAssembly()
         if (!assembly || assembly.data.isEmpty) return
 
@@ -71,6 +65,15 @@ export class StructureView {
         }))
     }
 
+    async attachValidationReport() {
+        const model = this.getModel()
+        if (!model) return
+
+        await this.plugin.runTask(Task.create('Validation Report', async runtime => {
+            await ValidationReportProvider.attach({ fetch: this.plugin.fetch, runtime }, model.data)
+        }))
+    }
+
     async setAssembly(id: string) {
         const state = this.plugin.state.dataState;
         const tree = state.build();
@@ -114,23 +117,28 @@ export class StructureView {
                     props, { ref: StateElements.Assembly, tags: [ AssemblyNames.CrystalContacts ] }
                 )
         } else {
+            const props = {
+                type: {
+                    name: 'assembly' as const,
+                    params: { id }
+                }
+            }
             tree.delete(StateElements.ModelUnitcell)
             tree.delete(StateElements.Assembly)
                 .to(StateElements.Model).apply(
-                    StateTransforms.Model.StructureAssemblyFromModel,
-                    { id }, { ref: StateElements.Assembly }
+                    StateTransforms.Model.StructureFromModel,
+                    props, { ref: StateElements.Assembly }
                 )
         }
         await this.applyState(tree)
-        await this.attachSymmetry()
-        await this.preset()
+        await this.attachAssemblySymmetry()
         await this.experimentalData.init()
     }
 
     async setModel(modelIndex: number) {
         const state = this.plugin.state.dataState;
         const tree = state.build();
-        if (modelIndex === -1) {
+        if (modelIndex === ModelNames.All) {
             tree.delete(StateElements.Model)
                 .to(StateElements.Trajectory).apply(
                     StateTransforms.Model.StructureFromTrajectory,
@@ -143,20 +151,25 @@ export class StructureView {
                     props => ({ ...props, modelIndex })
                 )
             } else {
+                const props = {
+                    type: {
+                        name: 'assembly' as const,
+                        params: { id: AssemblyNames.Deposited }
+                    }
+                }
                 tree.delete(StateElements.Assembly)
                     .to(StateElements.Trajectory).apply(
                         StateTransforms.Model.ModelFromTrajectory,
                         { modelIndex }, { ref: StateElements.Model }
                     )
                     .apply(
-                        StateTransforms.Model.StructureAssemblyFromModel,
-                        { id: AssemblyNames.Deposited }, { ref: StateElements.Assembly }
+                        StateTransforms.Model.StructureFromModel,
+                        props, { ref: StateElements.Assembly }
                     )
             }
         }
         await this.applyState(tree)
-        await this.attachSymmetry()
-        await this.preset()
+        await this.attachAssemblySymmetry()
     }
 
     async setSymmetry(symmetryIndex: number) {

+ 9 - 1
src/structure-viewer/index.html

@@ -69,10 +69,18 @@
                     id: '1CRN',
                     info: 'small: only polymer'
                 },
+                {
+                    id: '6JI7',
+                    info: 'small: NMR structure with RCI'
+                },
                 {
                     id: '3PQR',
                     info: 'medium: polymer, carbs, ligands'
                 },
+                {
+                    id: '4HHB',
+                    info: 'medium: classic, lots of validation geometry problems'
+                },
                 {
                     id: '1A6D',
                     info: 'medium: dihedral symmetry (D8)'
@@ -199,7 +207,7 @@
                 },
                 {
                     id: '1GRM',
-                    info: 'REFINEMENT OF THE SPATIAL STRUCTURE OF THE GRAMICIDIN A TRANSMEMBRANE ION-CHANNEL. Single-starnded helix from beta-sheet.'
+                    info: 'REFINEMENT OF THE SPATIAL STRUCTURE OF THE GRAMICIDIN A TRANSMEMBRANE ION-CHANNEL. Single-starnded helix from beta-sheet. NMR structure.'
                 }
             ];
 

+ 13 - 13
src/structure-viewer/index.ts

@@ -23,6 +23,7 @@ import ReactDOM = require('react-dom');
 import React = require('react');
 import { ModelLoader } from './helpers/model';
 import { VolumeData } from './helpers/volume';
+import { PresetManager, PresetProps } from './helpers/preset';
 require('./skin/rcsb.scss')
 
 /** package version, filled in at bundle build time */
@@ -45,6 +46,10 @@ export class StructureViewer {
     private readonly plugin: PluginContext;
     private readonly props: Readonly<StructureViewerProps>
 
+    private get customState() {
+        return this.plugin.customState as StructureViewerState
+    }
+
     constructor(target: string | HTMLElement, props: Partial<StructureViewerProps> = {}) {
         target = typeof target === 'string' ? document.getElementById(target)! : target
 
@@ -88,9 +93,11 @@ export class StructureViewer {
 
         (this.plugin.customState as StructureViewerState) = {
             props: this.props,
+
             modelLoader: new ModelLoader(this.plugin),
+            presetManager: new PresetManager(this.plugin),
             structureView: new StructureView(this.plugin),
-            volumeData: new VolumeData(this.plugin)
+            volumeData: new VolumeData(this.plugin),
         }
 
         ReactDOM.render(React.createElement(Plugin, { plugin: this.plugin }), target)
@@ -106,20 +113,13 @@ export class StructureViewer {
         })
     }
 
-    async loadPdbId(pdbId: string, assemblyId = 'deposited') {
+    async loadPdbId(pdbId: string, props?: PresetProps) {
         const p = this.props.modelUrlProvider(pdbId)
-        return (this.plugin.customState as StructureViewerState).modelLoader.load({
-            fileOrUrl: p.url,
-            format: p.format,
-            assemblyId,
-        })
+        await this.customState.modelLoader.load({ fileOrUrl: p.url, format: p.format })
+        await this.customState.presetManager.apply(props)
     }
 
-    async loadUrl(url: string, assemblyId = 'deposited') {
-        return (this.plugin.customState as StructureViewerState).modelLoader.load({
-            fileOrUrl: url,
-            format: 'cif',
-            assemblyId,
-        })
+    async loadUrl(url: string, props?: PresetProps) {
+        await this.customState.modelLoader.load({ fileOrUrl: url, format: 'cif', })
     }
 }

+ 7 - 0
src/structure-viewer/types.ts

@@ -7,6 +7,7 @@
 import { StructureView } from './helpers/structure';
 import { ModelLoader } from './helpers/model';
 import { VolumeData } from './helpers/volume';
+import { PresetManager } from './helpers/preset';
 
 export interface StructureViewerProps {
     volumeServerUrl: string,
@@ -54,9 +55,15 @@ export enum AssemblyNames {
     CrystalContacts = 'crystal-contacts',
 }
 
+export enum ModelNames {
+    All = -1,
+}
+
 export interface StructureViewerState {
     props: StructureViewerProps
+
     modelLoader: ModelLoader
+    presetManager: PresetManager
     structureView: StructureView
     volumeData: VolumeData
 }

+ 7 - 3
src/structure-viewer/ui/controls.tsx

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -20,16 +20,20 @@ import { HelpContent } from './help';
 import { OpenFile } from './open';
 
 export class ControlsWrapper extends PluginUIComponent {
+    get customState() {
+        return this.plugin.customState as StructureViewerState
+    }
+
     componentDidMount() {
         this.subscribe(this.plugin.state.behavior.currentObject, () => this.forceUpdate());
         this.subscribe(this.plugin.events.state.object.updated, () => this.forceUpdate());
     }
 
     render() {
-        const { showOpenFileControls } = (this.plugin.customState as StructureViewerState).props
+        const { showOpenFileControls } = this.customState.props
         return <div className='msp-scrollable-container msp-right-controls' style={{ paddingTop: '0px' }}>
             {showOpenFileControls && <OpenFile initiallyCollapsed={false} />}
-            <StructureControls  />
+            <StructureControls />
             <StructureSelectionControls header='Manage Selection' initiallyCollapsed={true} />
             <StructureRepresentationControls header='Change Representation' initiallyCollapsed={true} />
             <TransformUpdaterControl nodeRef={StateElements.VolumeStreaming} header={{ name: 'Density Controls', description: '' }} initiallyCollapsed={true} />

+ 68 - 9
src/structure-viewer/ui/structure.tsx

@@ -15,6 +15,10 @@ import { StateTransforms } from 'molstar/lib/mol-plugin/state/transforms';
 import { stringToWords } from 'molstar/lib/mol-util/string';
 import { ModelSymmetry } from 'molstar/lib/mol-model-formats/structure/property/symmetry';
 import { AssemblySymmetryProvider } from 'molstar/lib/mol-model-props/rcsb/assembly-symmetry'
+import { ActionMenu } from 'molstar/lib/mol-plugin-ui/controls/action-menu';
+import { PresetProps } from '../helpers/preset';
+import { ValidationReport } from 'molstar/lib/mol-model-props/rcsb/validation-report';
+import { modelFromCrystallography, modelHasMap, modelHasSymmetry, modelFromNmr, getStructureSize, StructureSize } from '../helpers/util';
 
 interface StructureControlsState extends CollapsableState {
     trajectoryRef: string
@@ -22,12 +26,12 @@ interface StructureControlsState extends CollapsableState {
 }
 
 export class StructureControls<P, S extends StructureControlsState> extends CollapsableControls<P, S> {
-    constructor(props: P, context?: any) {
-        super(props, context);
+    get customState() {
+        return this.plugin.customState as StructureViewerState
     }
 
-    get structureView () {
-        return (this.plugin.customState as StructureViewerState).structureView
+    constructor(props: P, context?: any) {
+        super(props, context);
     }
 
     async setColorTheme(theme: { [k: string]: string }) {
@@ -53,7 +57,7 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
                 )
             }
         })
-        await this.structureView.applyState(tree)
+        await this.customState.structureView.applyState(tree)
     }
 
     async syncSymmetryIndex() {
@@ -73,17 +77,17 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
                 )
             }
         })
-        await this.structureView.applyState(tree)
+        await this.customState.structureView.applyState(tree)
     }
 
     onChange = async (p: { param: PD.Base<any>, name: string, value: any }) => {
         // console.log('onChange', p.name, p.value)
         if (p.name === 'assembly') {
-            await this.structureView.setAssembly(p.value)
+            await this.customState.presetManager.assembly(p.value)
         } else if (p.name === 'model') {
-            await this.structureView.setModel(p.value)
+            await this.customState.presetManager.model(p.value)
         } else if (p.name === 'symmetry') {
-            await this.structureView.setSymmetry(p.value)
+            await this.customState.structureView.setSymmetry(p.value)
             await this.syncSymmetryIndex()
         } else if (p.name === 'colorThemes') {
             await this.setColorTheme(p.value)
@@ -304,6 +308,54 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
         return this.plugin.state.dataState.transforms.get(StateElements.AssemblySymmetry)
     }
 
+    private actionMenu = new ActionMenu();
+
+    private applyPreset = (props: PresetProps) => {
+        this.customState.presetManager.apply(props)
+    }
+
+    private getPresets = () => {
+        const model = this.getModel()
+        const assembly = this.getAssembly()
+
+        const showClashes = assembly && getStructureSize(assembly.data) <= StructureSize.Medium
+
+        const validationItems = [
+            'Validation Report',
+            ActionMenu.Item(`Geometry Quality Coloring${showClashes ? ' & Clashes' : ''}`, {
+                kind: 'validation',
+                colorTheme: ValidationReport.Tag.GeometryQuality,
+                showClashes
+            }),
+        ]
+
+        if (model && modelHasMap(model.data)) {
+            validationItems.push(ActionMenu.Item('Density Fit Coloring', {
+                kind: 'validation',
+                colorTheme: ValidationReport.Tag.DensityFit,
+                showClashes: false
+            }))
+        }
+
+        if (model && modelFromNmr(model.data)) {
+            validationItems.push(ActionMenu.Item('Random Coil Index Coloring', {
+                kind: 'validation',
+                colorTheme: ValidationReport.Tag.RandomCoilIndex,
+                showClashes: false
+            }))
+        }
+
+        return [
+            ActionMenu.Item('Standard', {
+                kind: 'standard'
+            }),
+            ActionMenu.Item('Assembly Symmetry', {
+                kind: 'symmetry'
+            }),
+            validationItems
+        ] as unknown as ActionMenu.Spec
+    }
+
     defaultState() {
         return {
             isCollapsed: false,
@@ -319,6 +371,13 @@ export class StructureControls<P, S extends StructureControlsState> extends Coll
         if (!this.getTrajectory() || !this.getAssembly()) return null
 
         return <div>
+            <div>
+                <div className='msp-control-row'>
+                    <ActionMenu.Toggle menu={this.actionMenu} items={this.getPresets()} label='Apply Preset' onSelect={this.applyPreset} disabled={this.state.isDisabled} />
+                </div>
+                <ActionMenu.Options menu={this.actionMenu} />
+            </div>
+
             <ParameterControls params={this.getParams()} values={this.values} onChange={this.onChange} isDisabled={this.state.isDisabled} />
         </div>
     }