ソースを参照

Merge branch 'master' into plugin

David Sehnal 5 年 前
コミット
fb5eb1e19c
100 ファイル変更2045 行追加1250 行削除
  1. 71 55
      package-lock.json
  2. 10 10
      package.json
  3. 7 1
      src/apps/basic-wrapper/helpers.ts
  4. 15 2
      src/apps/basic-wrapper/index.ts
  5. 14 2
      src/apps/demos/lighting/index.ts
  6. 1 1
      src/apps/state-docs/pd-to-md.ts
  7. 0 13
      src/apps/structure-info/model.ts
  8. 1 0
      src/examples/proteopedia-wrapper/coloring.ts
  9. 4 5
      src/examples/proteopedia-wrapper/helpers.ts
  10. 14 2
      src/examples/proteopedia-wrapper/index.ts
  11. 13 5
      src/mol-canvas3d/canvas3d.ts
  12. 2 2
      src/mol-geo/geometry/base.ts
  13. 36 0
      src/mol-math/geometry/_spec/spacegroup.spec.ts
  14. 49 1
      src/mol-math/geometry/spacegroup/construction.ts
  15. 2 2
      src/mol-model-formats/structure/basic/parser.ts
  16. 35 20
      src/mol-model-formats/structure/basic/properties.ts
  17. 0 4
      src/mol-model-formats/structure/basic/schema.ts
  18. 3 3
      src/mol-model-formats/structure/basic/sequence.ts
  19. 4 1
      src/mol-model-formats/structure/common/component.ts
  20. 4 0
      src/mol-model-formats/structure/common/property.ts
  21. 0 9
      src/mol-model-formats/structure/mmcif.ts
  22. 2 2
      src/mol-model-formats/structure/pdb/to-cif.ts
  23. 0 26
      src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts
  24. 1 0
      src/mol-model-props/common/custom-element-property.ts
  25. 26 3
      src/mol-model-props/computed/accessible-surface-area.ts
  26. 26 10
      src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts
  27. 9 7
      src/mol-model-props/computed/interactions/interactions.ts
  28. 2 2
      src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
  29. 2 2
      src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
  30. 10 8
      src/mol-model-props/computed/secondary-structure.ts
  31. 6 7
      src/mol-model-props/computed/themes/accessible-surface-area.ts
  32. 2 1
      src/mol-model-props/computed/themes/interaction-type.ts
  33. 23 25
      src/mol-model-props/integrative/cross-link-restraint/color.ts
  34. 6 6
      src/mol-model-props/integrative/cross-link-restraint/format.ts
  35. 221 0
      src/mol-model-props/integrative/cross-link-restraint/property.ts
  36. 149 0
      src/mol-model-props/integrative/cross-link-restraint/representation.ts
  37. 49 0
      src/mol-model-props/integrative/pair-restraints.ts
  38. 1 1
      src/mol-model-props/pdbe/structure-quality-report.ts
  39. 3 2
      src/mol-model-props/pdbe/themes/structure-quality-report.ts
  40. 19 4
      src/mol-model-props/rcsb/assembly-symmetry.ts
  41. 6 12
      src/mol-model-props/rcsb/representations/assembly-symmetry.ts
  42. 2 2
      src/mol-model-props/rcsb/representations/validation-report-clashes.ts
  43. 7 4
      src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
  44. 3 2
      src/mol-model-props/rcsb/themes/density-fit.ts
  45. 3 2
      src/mol-model-props/rcsb/themes/geometry-quality.ts
  46. 3 2
      src/mol-model-props/rcsb/themes/random-coil-index.ts
  47. 24 2
      src/mol-model-props/rcsb/validation-report.ts
  48. 2 1
      src/mol-model/loci.ts
  49. 4 8
      src/mol-model/sequence/sequence.ts
  50. 0 64
      src/mol-model/structure/export/categories/modified-residues.ts
  51. 0 2
      src/mol-model/structure/export/mmcif.ts
  52. 3 6
      src/mol-model/structure/model/model.ts
  53. 4 1
      src/mol-model/structure/model/properties/common.ts
  54. 4 4
      src/mol-model/structure/model/properties/sequence.ts
  55. 3 3
      src/mol-model/structure/model/types.ts
  56. 18 0
      src/mol-model/structure/structure/element/loci.ts
  57. 7 16
      src/mol-model/structure/structure/properties.ts
  58. 1 9
      src/mol-model/structure/structure/structure.ts
  59. 69 17
      src/mol-model/structure/structure/symmetry.ts
  60. 0 10
      src/mol-model/structure/structure/unit/pair-restraints.ts
  61. 0 77
      src/mol-model/structure/structure/unit/pair-restraints/data.ts
  62. 0 111
      src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
  63. 0 7
      src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts
  64. 0 7
      src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts
  65. 1 0
      src/mol-plugin-ui/base.tsx
  66. 157 0
      src/mol-plugin-ui/controls/action-menu.tsx
  67. 25 13
      src/mol-plugin-ui/controls/common.tsx
  68. 191 63
      src/mol-plugin-ui/controls/parameters.tsx
  69. 1 0
      src/mol-plugin-ui/skin/base/components/controls.scss
  70. 17 0
      src/mol-plugin-ui/skin/base/components/misc.scss
  71. 31 0
      src/mol-plugin-ui/skin/base/components/temp.scss
  72. 1 18
      src/mol-plugin-ui/structure/representation.tsx
  73. 51 44
      src/mol-plugin-ui/structure/selection.tsx
  74. 55 90
      src/mol-plugin-ui/viewport/simple-settings.tsx
  75. 2 0
      src/mol-plugin/behavior/dynamic/custom-props.ts
  76. 6 0
      src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts
  77. 45 0
      src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts
  78. 6 3
      src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
  79. 23 10
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
  80. 11 2
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts
  81. 2 2
      src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
  82. 2 2
      src/mol-plugin/behavior/static/camera.ts
  83. 1 1
      src/mol-plugin/command.ts
  84. 4 1
      src/mol-plugin/index.ts
  85. 48 6
      src/mol-plugin/state/representation/model.ts
  86. 1 1
      src/mol-plugin/state/representation/structure/preset.ts
  87. 4 33
      src/mol-plugin/state/transforms/model.ts
  88. 23 20
      src/mol-plugin/state/transforms/representation.ts
  89. 1 1
      src/mol-plugin/util/structure-complex-helper.ts
  90. 15 5
      src/mol-plugin/util/structure-overpaint-helper.ts
  91. 94 90
      src/mol-plugin/util/structure-representation-helper.ts
  92. 209 55
      src/mol-plugin/util/structure-selection-helper.ts
  93. 0 2
      src/mol-repr/structure/registry.ts
  94. 0 43
      src/mol-repr/structure/representation/distance-restraint.ts
  95. 0 119
      src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
  96. 1 4
      src/mol-repr/structure/visual/nucleotide-block-mesh.ts
  97. 1 4
      src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
  98. 0 2
      src/mol-script/runtime/query/table.ts
  99. 10 3
      src/mol-theme/color.ts
  100. 1 0
      src/mol-theme/color/carbohydrate-symbol.ts

+ 71 - 55
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "0.5.1",
+  "version": "0.5.5",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -3389,16 +3389,29 @@
       }
     },
     "@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.7",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.7.tgz",
+      "integrity": "sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg=="
     },
     "@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==",
       "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==",
+          "requires": {
+            "asynckit": "^0.4.0",
+            "combined-stream": "^1.0.8",
+            "mime-types": "^2.1.12"
+          }
+        }
       }
     },
     "@types/parse-json": {
@@ -3418,9 +3431,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==",
       "requires": {
         "@types/prop-types": "*",
         "csstype": "^2.2.0"
@@ -3474,12 +3487,12 @@
       "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==",
       "dev": true,
       "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",
@@ -3494,43 +3507,33 @@
         }
       }
     },
-    "@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==",
-      "dev": true,
-      "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==",
       "dev": true,
       "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==",
       "dev": true,
       "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==",
       "dev": true,
       "requires": {
         "debug": "^4.1.1",
@@ -3957,6 +3960,17 @@
         "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==",
+          "dev": true,
+          "requires": {
+            "@types/node": "*"
+          }
+        }
       }
     },
     "apollo-graphql": {
@@ -4301,8 +4315,7 @@
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
-      "dev": true
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
     },
     "atob": {
       "version": "2.1.2",
@@ -5327,7 +5340,6 @@
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dev": true,
       "requires": {
         "delayed-stream": "~1.0.0"
       }
@@ -5992,8 +6004,7 @@
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
-      "dev": true
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
     },
     "delegates": {
       "version": "1.0.0",
@@ -8972,6 +8983,11 @@
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
       "dev": true
     },
+    "immer": {
+      "version": "5.3.6",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-5.3.6.tgz",
+      "integrity": "sha512-pqWQ6ozVfNOUDjrLfm4Pt7q4Q12cGw2HUZgry4Q5+Myxu9nmHRkWBpI0J4+MK0AxbdFtdMTwEGVl7Vd+vEiK+A=="
+    },
     "immutable": {
       "version": "3.8.2",
       "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
@@ -14666,9 +14682,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==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
@@ -14676,14 +14692,14 @@
       }
     },
     "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==",
       "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": {
@@ -15953,9 +15969,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==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1"
@@ -17215,9 +17231,9 @@
       }
     },
     "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
     },
     "ua-parser-js": {

+ 10 - 10
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "0.5.1",
+  "version": "0.5.5",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -72,9 +72,8 @@
     "@graphql-codegen/typescript-graphql-request": "^1.12.2",
     "@graphql-codegen/typescript-operations": "^1.12.2",
     "@types/cors": "^2.8.6",
-    "@typescript-eslint/eslint-plugin": "^2.20.0",
-    "@typescript-eslint/eslint-plugin-tslint": "^2.20.0",
-    "@typescript-eslint/parser": "^2.20.0",
+    "@typescript-eslint/eslint-plugin": "^2.21.0",
+    "@typescript-eslint/parser": "^2.21.0",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.2.0",
     "concurrently": "^5.1.0",
@@ -96,7 +95,7 @@
     "simple-git": "^1.131.0",
     "style-loader": "^1.1.3",
     "ts-jest": "^25.2.1",
-    "typescript": "^3.8.2",
+    "typescript": "^3.8.3",
     "webpack": "^4.41.6",
     "webpack-cli": "^3.3.11"
   },
@@ -106,9 +105,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",
@@ -117,10 +116,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",

+ 7 - 1
src/apps/basic-wrapper/helpers.ts

@@ -67,7 +67,13 @@ export namespace StateHelper {
     }
 
     export function assemble(b: StateBuilder.To<PSO.Molecule.Model>, id?: string) {
-        return b.apply(StateTransforms.Model.StructureAssemblyFromModel, { id: id || 'deposited' }, { tags: 'asm' })
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: id || 'deposited' }
+            }
+        }
+        return b.apply(StateTransforms.Model.StructureFromModel, props, { tags: 'asm' })
     }
 
     export function visual(ctx: PluginContext, visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {

+ 15 - 2
src/apps/basic-wrapper/index.ts

@@ -61,10 +61,16 @@ class BasicWrapper {
             ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
 
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: assemblyId || 'deposited' }
+            }
+        }
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
             .apply(StateTransforms.Model.CustomModelProperties, { autoAttach: [StripedResidues.propertyProvider.descriptor.name], properties: {} }, { ref: 'props', state: { isGhost: false } })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+            .apply(StateTransforms.Model.StructureFromModel, props, { ref: 'asm' });
     }
 
     private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {
@@ -101,8 +107,15 @@ class BasicWrapper {
             tree = state.build();
             this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId));
         } else {
+            const props = {
+                type: {
+                    name: 'assembly' as const,
+                    params: { id: assemblyId || 'deposited' }
+                }
+            }
+
             tree = state.build();
-            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+            tree.to('asm').update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props }));
         }
 
         await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });

+ 14 - 2
src/apps/demos/lighting/index.ts

@@ -120,9 +120,15 @@ class LightingDemo {
             ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
 
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: assemblyId || 'deposited' }
+            }
+        }
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+            .apply(StateTransforms.Model.StructureFromModel, props, { ref: 'asm' });
     }
 
     private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {
@@ -153,8 +159,14 @@ class LightingDemo {
             tree = state.build();
             this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId));
         } else {
+            const props = {
+                type: {
+                    name: 'assembly' as const,
+                    params: { id: assemblyId || 'deposited' }
+                }
+            }
             tree = state.build();
-            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+            tree.to('asm').update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props }));
         }
 
         await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });

+ 1 - 1
src/apps/state-docs/pd-to-md.ts

@@ -39,7 +39,7 @@ function paramInfo(param: PD.Any, offset: number): string {
     }
 }
 
-function oToS(options: readonly (readonly [string, string])[]) {
+function oToS(options: readonly (readonly [string, string] | readonly [string, string, string])[]) {
     return options.map(o => `'${o[0]}'`).join(', ');
 }
 

+ 0 - 13
src/apps/structure-info/model.ts

@@ -123,18 +123,6 @@ export function printSequence(model: Model) {
     console.log();
 }
 
-export function printModRes(model: Model) {
-    console.log('\nModified Residues\n=============');
-    const map = model.properties.modifiedResidues.parentId;
-    const { label_comp_id, _rowCount } = model.atomicHierarchy.residues;
-    for (let i = 0; i < _rowCount; i++) {
-        const comp_id = label_comp_id.value(i);
-        if (!map.has(comp_id)) continue;
-        console.log(`[${i}] ${map.get(comp_id)} -> ${comp_id}`);
-    }
-    console.log();
-}
-
 export function printRings(structure: Structure) {
     console.log('\nRings\n=============');
     for (const unit of structure.units) {
@@ -221,7 +209,6 @@ async function run(frame: CifFrame, args: Args) {
     if (args.rings) printRings(structure);
     if (args.intraBonds) printBonds(structure, true, false);
     if (args.interBonds) printBonds(structure, false, true);
-    if (args.mod) printModRes(models[0]);
     if (args.sec) printSecStructure(models[0]);
 }
 

+ 1 - 0
src/examples/proteopedia-wrapper/coloring.ts

@@ -96,6 +96,7 @@ export function createProteopediaCustomTheme(colors: number[]) {
 
     const ProteopediaCustomColorThemeProvider: ColorTheme.Provider<ProteopediaCustomColorThemeParams> = {
         label: 'Proteopedia Custom',
+        category: 'Custom',
         factory: ProteopediaCustomColorTheme,
         getParams: getChainIdColorThemeParams,
         defaultValues: PD.getDefaultValues(ProteopediaCustomColorThemeParams),

+ 4 - 5
src/examples/proteopedia-wrapper/helpers.ts

@@ -7,7 +7,7 @@
 import { ResidueIndex, Model } from '../../mol-model/structure';
 import { BuiltInStructureRepresentationsName } from '../../mol-repr/structure/registry';
 import { BuiltInColorThemeName } from '../../mol-theme/color';
-import { AminoAcidNames } from '../../mol-model/structure/model/types';
+import { PolymerType } from '../../mol-model/structure/model/types';
 import { PluginContext } from '../../mol-plugin/context';
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
 
@@ -54,15 +54,14 @@ export namespace ModelInfo {
         const hetMap = new Map<string, ModelInfo['hetResidues'][0]>();
 
         for (let rI = 0 as ResidueIndex; rI < residueCount; rI++) {
-            const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI);
-            if (AminoAcidNames.has(comp_id)) continue;
-            const mod_parent = model.properties.modifiedResidues.parentId.get(comp_id);
-            if (mod_parent && AminoAcidNames.has(mod_parent)) continue;
+            if (model.atomicHierarchy.derived.residue.polymerType[rI] !== PolymerType.NA) continue;
 
             const cI = chainIndex[residueOffsets[rI]];
             const eI = model.atomicHierarchy.index.getEntityFromChain(cI);
             if (model.entities.data.type.value(eI) === 'water') continue;
 
+            const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI);
+
             let lig = hetMap.get(comp_id);
             if (!lig) {
                 lig = { name: comp_id, indices: [] };

+ 14 - 2
src/examples/proteopedia-wrapper/index.ts

@@ -92,10 +92,16 @@ class MolStarProteopediaWrapper {
 
     private structure(assemblyId: string) {
         const model = this.state.build().to(StateElements.Model);
+        const props = {
+            type: {
+                name: 'assembly' as const,
+                params: { id: assemblyId || 'deposited' }
+            }
+        }
 
         const s = model
             .apply(StateTransforms.Model.CustomModelProperties, { autoAttach: [EvolutionaryConservation.propertyProvider.descriptor.name], properties: {} }, { ref: StateElements.ModelProps, state: { isGhost: false } })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly });
+            .apply(StateTransforms.Model.StructureFromModel, props, { ref: StateElements.Assembly });
 
         s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence });
         s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: StateElements.Het });
@@ -213,7 +219,13 @@ class MolStarProteopediaWrapper {
             const tree = state.build();
             const info = await this.doInfo(true);
             const asmId = (assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId;
-            tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: asmId }));
+            const props = {
+                type: {
+                    name: 'assembly' as const,
+                    params: { id: asmId || 'deposited' }
+                }
+            }
+            tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureFromModel, p => ({ ...p, ...props }));
             await this.applyState(tree);
         }
 

+ 13 - 5
src/mol-canvas3d/canvas3d.ts

@@ -77,7 +77,7 @@ interface Canvas3D {
 
     handleResize(): void
     /** Focuses camera on scene's bounding sphere, centered and zoomed. */
-    requestCameraReset(durationMs?: number): void
+    requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
     readonly camera: Camera
     readonly boundingSphere: Readonly<Sphere3D>
     downloadScreenshot(): void
@@ -191,6 +191,7 @@ namespace Canvas3D {
         let drawPending = false
         let cameraResetRequested = false
         let nextCameraResetDuration: number | undefined = void 0
+        let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0
 
         function getLoci(pickingId: PickingId) {
             let loci: Loci = EmptyLoci
@@ -290,9 +291,15 @@ namespace Canvas3D {
         function resolveCameraReset() {
             if (!cameraResetRequested) return;
             const { center, radius } = scene.boundingSphere;
-            const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration
-            camera.focus(center, radius, radius, duration);
+            if (radius > 0) {
+                const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration
+                const focus = camera.getFocus(center, radius, radius);
+                const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus;
+                camera.setState(snapshot, duration);
+            }
+
             nextCameraResetDuration = void 0;
+            nextCameraResetSnapshot = void 0;
             cameraResetRequested = false;
         }
 
@@ -388,8 +395,9 @@ namespace Canvas3D {
             getLoci,
 
             handleResize,
-            requestCameraReset: (durationMs) => {
-                nextCameraResetDuration = durationMs;
+            requestCameraReset: options => {
+                nextCameraResetDuration = options?.durationMs;
+                nextCameraResetSnapshot = options?.snapshot;
                 cameraResetRequested = true;
             },
             camera,

+ 2 - 2
src/mol-geo/geometry/base.ts

@@ -28,8 +28,8 @@ export const VisualQualityInfo = {
     'lowest': {},
 }
 export type VisualQuality = keyof typeof VisualQualityInfo
-export const VisualQualityNames = Object.keys(VisualQualityInfo)
-export const VisualQualityOptions = VisualQualityNames.map(n => [n, n] as [VisualQuality, string])
+export const VisualQualityNames = Object.keys(VisualQualityInfo) as VisualQuality[]
+export const VisualQualityOptions = PD.arrayToOptions(VisualQualityNames)
 
 //
 

+ 36 - 0
src/mol-math/geometry/_spec/spacegroup.spec.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Spacegroup, SpacegroupCell } from '../spacegroup/construction';
+import { Vec3 } from '../../linear-algebra';
+
+function getSpacegroup(name: string) {
+    const size = Vec3.create(1, 1, 1)
+    const anglesInRadians = Vec3.create(Math.PI / 2, Math.PI / 2, Math.PI / 2)
+    const cell = SpacegroupCell.create(name, size, anglesInRadians)
+    return Spacegroup.create(cell)
+}
+
+function checkOperatorsXyz(name: string, expected: string[]) {
+    const spacegroup = getSpacegroup(name)
+    for (let i = 0, il = spacegroup.operators.length; i < il; ++i) {
+        const op = spacegroup.operators[i]
+        const actual = Spacegroup.getOperatorXyz(op)
+        expect(actual).toBe(expected[i])
+    }
+}
+
+describe('Spacegroup', () => {
+    it('operators xyz', () => {
+        checkOperatorsXyz('P 1', ['X,Y,Z'])
+        checkOperatorsXyz('P -1', ['X,Y,Z', '-X,-Y,-Z'])
+        checkOperatorsXyz('P 1 21 1', ['X,Y,Z', '-X,1/2+Y,-Z'])
+        checkOperatorsXyz('P 1 21/m 1', ['X,Y,Z', '-X,1/2+Y,-Z', '-X,-Y,-Z', 'X,1/2-Y,Z'])
+        checkOperatorsXyz('P 41', ['X,Y,Z', '-X,-Y,1/2+Z', '-Y,X,1/4+Z', 'Y,-X,3/4+Z'])
+        checkOperatorsXyz('P 41 21 2', ['X,Y,Z', '-X,-Y,1/2+Z', '1/2-Y,1/2+X,1/4+Z', '1/2+Y,1/2-X,3/4+Z', '1/2-X,1/2+Y,1/4-Z', '1/2+X,1/2-Y,3/4-Z', 'Y,X,-Z', '-Y,-X,1/2-Z'])
+        checkOperatorsXyz('P 3', ['X,Y,Z', '-Y,X-Y,Z', 'Y-X,-X,Z'])
+    });
+})

+ 49 - 1
src/mol-math/geometry/spacegroup/construction.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -145,6 +145,54 @@ namespace Spacegroup {
         const r3 = TransformData[ids[2]];
         return Mat4.ofRows([r1, r2, r3, [0, 0, 0, 1]]);
     }
+
+    export function getOperatorXyz(op: Mat4) {
+        return [
+            formatElement(getRotation(op[0], op[4], op[8]), getShift(op[12])),
+            formatElement(getRotation(op[1], op[5], op[9]), getShift(op[13])),
+            formatElement(getRotation(op[2], op[6], op[10]), getShift(op[14]))
+        ].join(',')
+    }
+
+    function getRotation(x: number, y: number, z: number) {
+        let r: string[] = []
+        if (x > 0) r.push('+X')
+        else if (x < 0) r.push('-X')
+        if (y > 0) r.push('+Y')
+        else if (y < 0) r.push('-Y')
+        if (z > 0) r.push('+Z')
+        else if (z < 0) r.push('-Z')
+
+        if (r.length === 1) {
+            return r[0].charAt(0) === '+' ? r[0].substr(1) : r[0]
+        }
+        if (r.length === 2) {
+            const s0 = r[0].charAt(0)
+            const s1 = r[1].charAt(0)
+            if (s0 === '+') return `${r[0].substr(1)}${r[1]}`
+            if (s1 === '+') return `${r[1].substr(1)}${r[0]}`
+        }
+        throw new Error(`unknown rotation '${r}', ${x} ${y} ${z}`)
+    }
+
+    function getShift(s: number) {
+        switch (s) {
+            case 1/2: return '1/2'
+            case 1/4: return '1/4'
+            case 3/4: return '3/4'
+            case 1/3: return '1/3'
+            case 2/3: return '2/3'
+            case 1/6: return '1/6'
+            case 5/6: return '5/6'
+        }
+        return ''
+    }
+
+    function formatElement(rotation: string, shift: string) {
+        if (shift === '') return rotation
+        if (rotation.length > 2) return `${rotation}+${shift}`
+        return rotation.charAt(0) === '-' ? `${shift}${rotation}` : `${shift}+${rotation}`
+    }
 }
 
 export { Spacegroup, SpacegroupCell }

+ 2 - 2
src/mol-model-formats/structure/basic/parser.ts

@@ -45,7 +45,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
     }
 
     const coarse = EmptyCoarse;
-    const sequence = getSequence(data, entities, atomic.hierarchy, coarse.hierarchy, properties.modifiedResidues.parentId)
+    const sequence = getSequence(data, entities, atomic.hierarchy, coarse.hierarchy)
     const atomicRanges = getAtomicRanges(atomic.hierarchy, entities, atomic.conformation, sequence)
 
     const entry = data.entry.id.valueKind(0) === Column.ValueKind.Present
@@ -80,7 +80,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
 function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: Model['properties'], format: ModelFormat): Model {
     const atomic = getAtomicHierarchyAndConformation(ihm.atom_site, ihm.atom_site_sourceIndex, ihm.entities, properties.chemicalComponentMap);
     const coarse = getCoarse(ihm, properties);
-    const sequence = getSequence(data, ihm.entities, atomic.hierarchy, coarse.hierarchy, properties.modifiedResidues.parentId)
+    const sequence = getSequence(data, ihm.entities, atomic.hierarchy, coarse.hierarchy)
     const atomicRanges = getAtomicRanges(atomic.hierarchy, ihm.entities, atomic.conformation, sequence)
 
     const entry = data.entry.id.valueKind(0) === Column.ValueKind.Present

+ 35 - 20
src/mol-model-formats/structure/basic/properties.ts

@@ -6,30 +6,13 @@
  */
 
 import { Model } from '../../../mol-model/structure/model/model';
-import { ChemicalComponent, MissingResidue } from '../../../mol-model/structure/model/properties/common';
+import { ChemicalComponent, MissingResidue, StructAsym } from '../../../mol-model/structure/model/properties/common';
 import { getMoleculeType, MoleculeType, getDefaultChemicalComponent } from '../../../mol-model/structure/model/types';
 import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from '../../../mol-model/structure/structure/carbohydrates/constants';
 import { memoize1 } from '../../../mol-util/memoize';
 import { BasicData } from './schema';
 import { Table } from '../../../mol-data/db';
 
-function getModifiedResidueNameMap(data: BasicData): Model['properties']['modifiedResidues'] {
-    const parentId = new Map<string, string>();
-    const details = new Map<string, string>();
-
-    const c = data.pdbx_struct_mod_residue;
-    const comp_id = c.label_comp_id.isDefined ? c.label_comp_id : c.auth_comp_id;
-    const parent_id = c.parent_comp_id, details_data = c.details;
-
-    for (let i = 0; i < c._rowCount; i++) {
-        const id = comp_id.value(i);
-        parentId.set(id, parent_id.value(i));
-        details.set(id, details_data.value(i));
-    }
-
-    return { parentId, details };
-}
-
 function getMissingResidues(data: BasicData): Model['properties']['missingResidues'] {
     const map = new Map<string, MissingResidue>();
     const getKey = (model_num: number, asym_id: string, seq_id: number) => {
@@ -124,11 +107,43 @@ const getUniqueComponentNames = memoize1((data: BasicData) => {
     return uniqueNames
 })
 
+
+function getStructAsymMap(data: BasicData): Model['properties']['structAsymMap'] {
+    const map = new Map<string, StructAsym>();
+
+    const { label_asym_id, auth_asym_id, label_entity_id } = data.atom_site
+    for (let i = 0, il = label_asym_id.rowCount; i < il; ++i) {
+        const id = label_asym_id.value(i)
+        if (!map.has(id)) {
+            map.set(id, {
+                id,
+                auth_id: auth_asym_id.value(i),
+                entity_id: label_entity_id.value(i)
+            })
+        }
+    }
+
+    if (data.struct_asym._rowCount > 0) {
+        const { id, entity_id } = data.struct_asym
+        for (let i = 0, il = id.rowCount; i < il; ++i) {
+            const _id = id.value(i)
+            if (!map.has(_id)) {
+                map.set(_id, {
+                    id: _id,
+                    auth_id: '',
+                    entity_id: entity_id.value(i)
+                })
+            }
+        }
+    }
+    return map
+}
+
 export function getProperties(data: BasicData): Model['properties'] {
     return {
-        modifiedResidues: getModifiedResidueNameMap(data),
         missingResidues: getMissingResidues(data),
         chemicalComponentMap: getChemicalComponentMap(data),
-        saccharideComponentMap: getSaccharideComponentMap(data)
+        saccharideComponentMap: getSaccharideComponentMap(data),
+        structAsymMap: getStructAsymMap(data)
     }
 }

+ 0 - 4
src/mol-model-formats/structure/basic/schema.ts

@@ -8,7 +8,6 @@ import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
 import { Table } from '../../../mol-data/db';
 
 // TODO split into conformation and hierarchy parts
-// TODO extract `pdbx_struct_mod_residue` as property?
 
 export type Entry = Table<mmCIF_Schema['entry']>
 export type Struct = Table<mmCIF_Schema['struct']>
@@ -22,7 +21,6 @@ export type EntityPolySeq = Table<mmCIF_Schema['entity_poly_seq']>
 export type EntityBranch = Table<mmCIF_Schema['pdbx_entity_branch']>
 export type ChemComp = Table<mmCIF_Schema['chem_comp']>
 export type ChemCompIdentifier = Table<mmCIF_Schema['pdbx_chem_comp_identifier']>
-export type StructModResidue = Table<mmCIF_Schema['pdbx_struct_mod_residue']>
 export type AtomSite = Table<mmCIF_Schema['atom_site']>
 export type IhmSphereObjSite = Table<mmCIF_Schema['ihm_sphere_obj_site']>
 export type IhmGaussianObjSite =Table<mmCIF_Schema['ihm_gaussian_obj_site']>
@@ -41,7 +39,6 @@ export const BasicSchema = {
     pdbx_entity_branch: mmCIF_Schema.pdbx_entity_branch,
     chem_comp: mmCIF_Schema.chem_comp,
     pdbx_chem_comp_identifier: mmCIF_Schema.pdbx_chem_comp_identifier,
-    pdbx_struct_mod_residue: mmCIF_Schema.pdbx_struct_mod_residue,
     atom_site: mmCIF_Schema.atom_site,
     ihm_sphere_obj_site: mmCIF_Schema.ihm_sphere_obj_site,
     ihm_gaussian_obj_site: mmCIF_Schema.ihm_gaussian_obj_site,
@@ -61,7 +58,6 @@ export interface BasicData {
     pdbx_entity_branch: EntityBranch
     chem_comp: ChemComp
     pdbx_chem_comp_identifier: ChemCompIdentifier
-    pdbx_struct_mod_residue: StructModResidue
     atom_site: AtomSite
     ihm_sphere_obj_site: IhmSphereObjSite
     ihm_gaussian_obj_site: IhmGaussianObjSite

+ 3 - 3
src/mol-model-formats/structure/basic/sequence.ts

@@ -13,9 +13,9 @@ import { Sequence } from '../../../mol-model/sequence';
 import { CoarseHierarchy } from '../../../mol-model/structure/model/properties/coarse';
 import { BasicData } from './schema';
 
-export function getSequence(data: BasicData, entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy, modResMap: ReadonlyMap<string, string>): StructureSequence {
+export function getSequence(data: BasicData, entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy): StructureSequence {
     if (!data.entity_poly_seq || !data.entity_poly_seq._rowCount) {
-        return StructureSequence.fromHierarchy(entities, atomicHierarchy, coarseHierarchy, modResMap);
+        return StructureSequence.fromHierarchy(entities, atomicHierarchy, coarseHierarchy);
     }
 
     const { entity_id, num, mon_id } = data.entity_poly_seq;
@@ -37,7 +37,7 @@ export function getSequence(data: BasicData, entities: Entities, atomicHierarchy
 
         byEntityKey[entityKey] = {
             entityId: id,
-            sequence: Sequence.ofResidueNames(compId, seqId, modResMap)
+            sequence: Sequence.ofResidueNames(compId, seqId)
         };
 
         sequences.push(byEntityKey[entityKey]);

+ 4 - 1
src/mol-model-formats/structure/common/component.ts

@@ -6,7 +6,7 @@
 
 import { Table, Column } from '../../../mol-data/db';
 import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
-import { WaterNames } from '../../../mol-model/structure/model/types';
+import { WaterNames, PolymerNames } from '../../../mol-model/structure/model/types';
 import { SetUtils } from '../../../mol-util/set';
 import { BasicSchema } from '../basic/schema';
 
@@ -77,12 +77,14 @@ export class ComponentBuilder {
     private ids: string[] = []
     private names: string[] = []
     private types: mmCIF_Schema['chem_comp']['type']['T'][] = []
+    private mon_nstd_flags: mmCIF_Schema['chem_comp']['mon_nstd_flag']['T'][] = []
 
     private set(c: Component) {
         this.comps.set(c.id, c)
         this.ids.push(c.id)
         this.names.push(c.name)
         this.types.push(c.type)
+        this.mon_nstd_flags.push(PolymerNames.has(c.id) ? 'y' : 'n')
     }
 
     private getAtomIds(index: number) {
@@ -141,6 +143,7 @@ export class ComponentBuilder {
             id: Column.ofStringArray(this.ids),
             name: Column.ofStringArray(this.names),
             type: Column.ofStringAliasArray(this.types),
+            mon_nstd_flag: Column.ofStringAliasArray(this.mon_nstd_flags),
         }, this.ids.length)
     }
 

+ 4 - 0
src/mol-model-formats/structure/common/property.ts

@@ -14,6 +14,10 @@ class FormatRegistry<T> {
         this.map.set(kind, obtain)
     }
 
+    remove(kind: ModelFormat['kind']) {
+        this.map.delete(kind)
+    }
+
     get(kind: ModelFormat['kind']) {
         return this.map.get(kind)
     }

+ 0 - 9
src/mol-model-formats/structure/mmcif.ts

@@ -17,7 +17,6 @@ import { Table } from '../../mol-data/db';
 import { AtomSiteAnisotrop } from './property/anisotropic';
 import { ComponentBond } from './property/bonds/comp';
 import { StructConn } from './property/bonds/struct_conn';
-import { ModelCrossLinkRestraint } from './property/pair-restraints/cross-links';
 
 function modelSymmetryFromMmcif(model: Model) {
     if (!MmcifFormat.is(model.sourceData)) return;
@@ -65,14 +64,6 @@ function structConnFromMmcif(model: Model) {
 }
 StructConn.Provider.formatRegistry.add('mmCIF', structConnFromMmcif)
 
-function crossLinkRestraintFromMmcif(model: Model) {
-    if (!MmcifFormat.is(model.sourceData)) return;
-    const { ihm_cross_link_restraint } = model.sourceData.data.db;
-    if (ihm_cross_link_restraint._rowCount === 0) return;
-    return ModelCrossLinkRestraint.fromTable(ihm_cross_link_restraint, model)
-}
-ModelCrossLinkRestraint.Provider.formatRegistry.add('mmCIF', crossLinkRestraintFromMmcif)
-
 //
 
 export { MmcifFormat }

+ 2 - 2
src/mol-model-formats/structure/pdb/to-cif.ts

@@ -162,8 +162,8 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
     }
 
     const categories = {
-        entity: entityBuilder.getEntityTable(),
-        chem_comp: componentBuilder.getChemCompTable(),
+        entity: CifCategory.ofTable('entity', entityBuilder.getEntityTable()),
+        chem_comp: CifCategory.ofTable('chem_comp', componentBuilder.getChemCompTable()),
         atom_site: CifCategory.ofFields('atom_site', getAtomSite(atomSite)),
         atom_site_anisotrop: CifCategory.ofFields('atom_site_anisotrop', getAnisotropic(anisotropic))
     } as any;

+ 0 - 26
src/mol-model-formats/structure/property/pair-restraints/predicted-contacts.ts

@@ -1,26 +0,0 @@
-/**
- * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-// TODO
-// ihm_predicted_contact_restraint: {
-//     id: int,
-//     entity_id_1: str,
-//     entity_id_2: str,
-//     asym_id_1: str,
-//     asym_id_2: str,
-//     comp_id_1: str,
-//     comp_id_2: str,
-//     seq_id_1: int,
-//     seq_id_2: int,
-//     atom_id_1: str,
-//     atom_id_2: str,
-//     distance_upper_limit: float,
-//     probability: float,
-//     restraint_type: Aliased<'lower bound' | 'upper bound' | 'lower and upper bound'>(str),
-//     model_granularity: Aliased<'by-residue' | 'by-feature' | 'by-atom'>(str),
-//     dataset_list_id: int,
-//     software_id: int,
-// },

+ 1 - 0
src/mol-model-props/common/custom-element-property.ts

@@ -99,6 +99,7 @@ namespace CustomElementProperty {
 
         return {
             label: modelProperty.label,
+            category: 'Custom',
             factory: Coloring,
             getParams: () => ({}),
             defaultValues: {},

+ 26 - 3
src/mol-model-props/computed/accessible-surface-area.ts

@@ -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 Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -7,9 +7,12 @@
 
 import { ParamDefinition as PD } from '../../mol-util/param-definition'
 import { ShrakeRupleyComputationParams, AccessibleSurfaceArea } from './accessible-surface-area/shrake-rupley';
-import { Structure, CustomPropertyDescriptor } from '../../mol-model/structure';
+import { Structure, CustomPropertyDescriptor, Unit } from '../../mol-model/structure';
 import { CustomStructureProperty } from '../common/custom-structure-property';
 import { CustomProperty } from '../common/custom-property';
+import { QuerySymbolRuntime } from '../../mol-script/runtime/query/compiler';
+import { CustomPropSymbol } from '../../mol-script/language/symbol';
+import Type from '../../mol-script/language/type';
 
 export const AccessibleSurfaceAreaParams = {
     ...ShrakeRupleyComputationParams
@@ -17,13 +20,33 @@ export const AccessibleSurfaceAreaParams = {
 export type AccessibleSurfaceAreaParams = typeof AccessibleSurfaceAreaParams
 export type AccessibleSurfaceAreaProps = PD.Values<AccessibleSurfaceAreaParams>
 
+export const AccessibleSurfaceAreaSymbols = {
+    isBuried: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'accessible-surface-area.is-buried', Type.Bool),
+        ctx => {
+            if (!Unit.isAtomic(ctx.element.unit)) return false
+            const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(ctx.element.structure).value
+            if (!accessibleSurfaceArea) return false
+            return AccessibleSurfaceArea.getFlag(ctx.element, accessibleSurfaceArea) === AccessibleSurfaceArea.Flag.Buried
+        }
+    ),
+    isAccessible: QuerySymbolRuntime.Dynamic(CustomPropSymbol('computed', 'accessible-surface-area.is-accessible', Type.Bool),
+        ctx => {
+            if (!Unit.isAtomic(ctx.element.unit)) return false
+            const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(ctx.element.structure).value
+            if (!accessibleSurfaceArea) return false
+            return AccessibleSurfaceArea.getFlag(ctx.element, accessibleSurfaceArea) === AccessibleSurfaceArea.Flag.Accessible
+        }
+    ),
+}
+
 export type AccessibleSurfaceAreaValue = AccessibleSurfaceArea
 
 export const AccessibleSurfaceAreaProvider: CustomStructureProperty.Provider<AccessibleSurfaceAreaParams, AccessibleSurfaceAreaValue> = CustomStructureProperty.createProvider({
     label: 'Accessible Surface Area',
     descriptor: CustomPropertyDescriptor({
         name: 'molstar_accessible_surface_area',
-        // TODO `cifExport` and `symbol`
+        symbols: AccessibleSurfaceAreaSymbols,
+        // TODO `cifExport`
     }),
     type: 'root',
     defaultParams: AccessibleSurfaceAreaParams,

+ 26 - 10
src/mol-model-props/computed/accessible-surface-area/shrake-rupley.ts

@@ -9,7 +9,7 @@ import { Task, RuntimeContext } from '../../../mol-task';
 // import { BitFlags } from '../../../mol-util';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition'
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { Structure } from '../../../mol-model/structure';
+import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
 import { assignRadiusForHeavyAtoms } from './shrake-rupley/radii';
 import { ShrakeRupleyContext, VdWLookup, MaxAsa, DefaultMaxAsa } from './shrake-rupley/common';
 import { computeArea } from './shrake-rupley/area';
@@ -86,19 +86,35 @@ namespace AccessibleSurfaceArea {
         return points;
     }
 
-    // export namespace SolventAccessibility {
-    //     export const is: (t: number, f: Flag) => boolean = BitFlags.has
-    //     export const create: (f: Flag) => number = BitFlags.create
-    //     export const enum Flag {
-    //         _ = 0x0,
-    //         BURIED = 0x1,
-    //         ACCESSIBLE = 0x2
-    //     }
-    // }
+    export const enum Flag {
+        NA = 0x0,
+        Buried = 0x1,
+        Accessible = 0x2
+    }
 
     /** Get relative area for a given component id */
     export function normalize(compId: string, asa: number) {
         const maxAsa = MaxAsa[compId] || DefaultMaxAsa;
         return asa / maxAsa
     }
+
+    export function getValue(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) {
+        const { getSerialIndex } = location.structure.root.serialMapping
+        const { area, serialResidueIndex } = accessibleSurfaceArea
+        const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)]
+        if (rSI === -1) return -1
+        return area[rSI]
+    }
+
+    export function getNormalizedValue(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) {
+        const value = getValue(location, accessibleSurfaceArea)
+        return value === -1 ? -1 : normalize(StructureProperties.residue.label_comp_id(location), value)
+    }
+
+    export function getFlag(location: StructureElement.Location, accessibleSurfaceArea: AccessibleSurfaceArea) {
+        const value = getNormalizedValue(location, accessibleSurfaceArea)
+        return value === -1 ? Flag.NA :
+            value < 0.16 ? Flag.Buried :
+                Flag.Accessible
+    }
 }

+ 9 - 7
src/mol-model-props/computed/interactions/interactions.ts

@@ -38,8 +38,9 @@ interface Interactions {
 }
 
 namespace Interactions {
+    type StructureInteractions = { readonly structure: Structure, readonly interactions: Interactions }
+
     export interface Element {
-        structure: Structure,
         unitA: Unit
         /** Index into features of unitA */
         indexA: Features.FeatureIndex
@@ -47,11 +48,12 @@ namespace Interactions {
         /** Index into features of unitB */
         indexB: Features.FeatureIndex
     }
-    export interface Location extends DataLocation<Interactions, Element> {}
+
+    export interface Location extends DataLocation<StructureInteractions, Element> {}
 
     export function Location(interactions: Interactions, structure: Structure, unitA?: Unit, indexA?: Features.FeatureIndex, unitB?: Unit, indexB?: Features.FeatureIndex): Location {
-        return DataLocation('interactions', interactions, 
-            { structure: structure as any, unitA: unitA as any, indexA: indexA as any, unitB: unitB as any, indexB: indexB as any });
+        return DataLocation('interactions', { structure, interactions },
+            { unitA: unitA as any, indexA: indexA as any, unitB: unitB as any, indexB: indexB as any });
     }
 
     export function isLocation(x: any): x is Location {
@@ -60,7 +62,8 @@ namespace Interactions {
 
     export function areLocationsEqual(locA: Location, locB: Location) {
         return (
-            locA.data === locB.data &&
+            locA.data.structure === locB.data.structure &&
+            locA.data.interactions === locB.data.interactions &&
             locA.element.indexA === locB.element.indexA &&
             locA.element.indexB === locB.element.indexB &&
             locA.element.unitA === locB.element.unitA &&
@@ -82,10 +85,9 @@ namespace Interactions {
     }
 
     export function locationLabel(location: Location): string {
-        return _label(location.data, location.element)
+        return _label(location.data.interactions, location.element)
     }
 
-    type StructureInteractions = { readonly structure: Structure, readonly interactions: Interactions }
     export interface Loci extends DataLoci<StructureInteractions, Element> { }
 
     export function Loci(structure: Structure, interactions: Interactions, elements: ReadonlyArray<Element>): Loci {

+ 2 - 2
src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts

@@ -101,8 +101,8 @@ function getInteractionLoci(pickingId: PickingId, structure: Structure, id: numb
         const interactions = InteractionsProvider.get(structure).value!
         const c = interactions.contacts.edges[groupId]
         return Interactions.Loci(structure, interactions, [
-            { structure, unitA: c.unitA, indexA: c.indexA, unitB: c.unitB, indexB: c.indexB },
-            { structure, unitA: c.unitB, indexA: c.indexB, unitB: c.unitA, indexB: c.indexA },
+            { unitA: c.unitA, indexA: c.indexA, unitB: c.unitB, indexB: c.indexB },
+            { unitA: c.unitB, indexA: c.indexB, unitB: c.unitA, indexB: c.indexA },
         ])
     }
     return EmptyLoci

+ 2 - 2
src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts

@@ -97,8 +97,8 @@ function getInteractionLoci(pickingId: PickingId, structureGroup: StructureGroup
         const interactions = InteractionsProvider.get(structure).value!
         const { a, b } = interactions.unitsContacts.get(unit.id)
         return Interactions.Loci(structure, interactions, [
-            { structure, unitA: unit, indexA: a[groupId], unitB: unit, indexB: b[groupId] },
-            { structure, unitA: unit, indexA: b[groupId], unitB: unit, indexB: a[groupId] },
+            { unitA: unit, indexA: a[groupId], unitB: unit, indexB: b[groupId] },
+            { unitA: unit, indexA: b[groupId], unitB: unit, indexB: a[groupId] },
         ])
     }
     return EmptyLoci

+ 10 - 8
src/mol-model-props/computed/secondary-structure.ts

@@ -1,10 +1,10 @@
 /**
- * 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>
  */
 
-import { CustomPropertyDescriptor, Structure } from '../../mol-model/structure';
+import { Structure } from '../../mol-model/structure';
 import { DSSPComputationParams, DSSPComputationProps, computeUnitDSSP } from './secondary-structure/dssp';
 import { SecondaryStructure } from '../../mol-model/structure/model/properties/seconday-structure';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
@@ -13,9 +13,10 @@ import { CustomStructureProperty } from '../common/custom-structure-property';
 import { CustomProperty } from '../common/custom-property';
 import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure';
 import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { CustomPropertyDescriptor } from '../../mol-model/structure/common/custom-property';
 
 function getSecondaryStructureParams(data?: Structure) {
-    let defaultType = 'mmcif' as 'mmcif' | 'dssp'
+    let defaultType = 'model' as 'model' | 'dssp'
     if (data) {
         defaultType = 'dssp'
         for (let i = 0, il = data.models.length; i < il; ++i) {
@@ -27,7 +28,7 @@ function getSecondaryStructureParams(data?: Structure) {
                 ) {
                     // if there is any secondary structure definition given or if there is
                     // an archival model, don't calculate dssp by default
-                    defaultType = 'mmcif'
+                    defaultType = 'model'
                     break
                 }
             }
@@ -35,9 +36,9 @@ function getSecondaryStructureParams(data?: Structure) {
     }
     return {
         type: PD.MappedStatic(defaultType, {
-            'mmcif': PD.EmptyGroup({ label: 'mmCIF' }),
+            'model': PD.EmptyGroup({ label: 'Model' }),
             'dssp': PD.Group(DSSPComputationParams, { label: 'DSSP', isFlat: true })
-        }, { options: [['mmcif', 'mmCIF'], ['dssp', 'DSSP']] })
+        }, { options: [['model', 'Model'], ['dssp', 'DSSP']] })
     }
 }
 
@@ -45,6 +46,7 @@ export const SecondaryStructureParams = getSecondaryStructureParams()
 export type SecondaryStructureParams = typeof SecondaryStructureParams
 export type SecondaryStructureProps = PD.Values<SecondaryStructureParams>
 
+/** Maps `unit.id` to `SecondaryStructure` */
 export type SecondaryStructureValue = Map<number, SecondaryStructure>
 
 export const SecondaryStructureProvider: CustomStructureProperty.Provider<SecondaryStructureParams, SecondaryStructureValue> = CustomStructureProperty.createProvider({
@@ -61,7 +63,7 @@ export const SecondaryStructureProvider: CustomStructureProperty.Provider<Second
         const p = { ...PD.getDefaultValues(SecondaryStructureParams), ...props }
         switch (p.type.name) {
             case 'dssp': return await computeDssp(data, p.type.params)
-            case 'mmcif': return await computeMmcif(data)
+            case 'model': return await computeModel(data)
         }
     }
 })
@@ -80,7 +82,7 @@ async function computeDssp(structure: Structure, props: DSSPComputationProps): P
     return map
 }
 
-async function computeMmcif(structure: Structure): Promise<SecondaryStructureValue> {
+async function computeModel(structure: Structure): Promise<SecondaryStructureValue> {
     const map = new Map<number, SecondaryStructure>()
     for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
         const u = structure.unitSymmetryGroups[i].units[0]

+ 6 - 7
src/mol-model-props/computed/themes/accessible-surface-area.ts

@@ -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 Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -10,7 +10,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition'
 import { Color, ColorScale } from '../../../mol-util/color'
 import { ThemeDataContext } from '../../../mol-theme/theme'
 import { ColorTheme, LocationColor } from '../../../mol-theme/color'
-import { StructureProperties, StructureElement, Unit } from '../../../mol-model/structure'
+import { StructureElement, Unit } from '../../../mol-model/structure'
 import { AccessibleSurfaceAreaProvider } from '../accessible-surface-area'
 import { AccessibleSurfaceArea } from '../accessible-surface-area/shrake-rupley'
 import { CustomProperty } from '../../common/custom-property'
@@ -36,18 +36,16 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
         domain: [0.0, 1.0]
     })
 
-    const { label_comp_id } = StructureProperties.residue
     const accessibleSurfaceArea = ctx.structure && AccessibleSurfaceAreaProvider.get(ctx.structure)
     const contextHash = accessibleSurfaceArea?.version
 
     if (accessibleSurfaceArea?.value && ctx.structure) {
-        const { getSerialIndex } = ctx.structure.root.serialMapping
-        const { area, serialResidueIndex } = accessibleSurfaceArea.value
+        const asa = accessibleSurfaceArea.value
 
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) {
-                const rSI = serialResidueIndex[getSerialIndex(location.unit, location.element)]
-                return rSI === -1 ? DefaultColor : scale.color(AccessibleSurfaceArea.normalize(label_comp_id(location), area[rSI]))
+                const value = AccessibleSurfaceArea.getNormalizedValue(location, asa)
+                return value === -1 ? DefaultColor : scale.color(value)
             }
             return DefaultColor
         }
@@ -68,6 +66,7 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
 
 export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<AccessibleSurfaceAreaColorThemeParams> = {
     label: 'Accessible Surface Area',
+    category: ColorTheme.Category.Residue,
     factory: AccessibleSurfaceAreaColorTheme,
     getParams: getAccessibleSurfaceAreaColorThemeParams,
     defaultValues: PD.getDefaultValues(AccessibleSurfaceAreaColorThemeParams),

+ 2 - 1
src/mol-model-props/computed/themes/interaction-type.ts

@@ -78,7 +78,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
     if (interactions && interactions.value) {
         color = (location: Location) => {
             if (Interactions.isLocation(location)) {
-                const { unitsContacts, contacts } = location.data
+                const { unitsContacts, contacts } = location.data.interactions
                 const { unitA, unitB, indexA, indexB } = location.element
                 if (unitA === unitB) {
                     const links = unitsContacts.get(unitA.id)
@@ -108,6 +108,7 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
 
 export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionTypeColorThemeParams> = {
     label: 'Interaction Type',
+    category: ColorTheme.Category.Misc,
     factory: InteractionTypeColorTheme,
     getParams: getInteractionTypeColorThemeParams,
     defaultValues: PD.getDefaultValues(InteractionTypeColorThemeParams),

+ 23 - 25
src/mol-theme/color/cross-link.ts → src/mol-model-props/integrative/cross-link-restraint/color.ts

@@ -1,23 +1,23 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Bond } from '../../mol-model/structure';
-import { Color, ColorScale } from '../../mol-util/color';
-import { Location } from '../../mol-model/location';
-import { ColorTheme, LocationColor } from '../color';
-import { Vec3 } from '../../mol-math/linear-algebra';
-import { ParamDefinition as PD } from '../../mol-util/param-definition'
-import { ThemeDataContext } from '../../mol-theme/theme';
-import { ColorListName, ColorListOptionsScale } from '../../mol-util/color/lists';
+import { Color, ColorScale } from '../../../mol-util/color';
+import { Location } from '../../../mol-model/location';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition'
+import { ThemeDataContext } from '../../../mol-theme/theme';
+import { ColorListName, ColorListOptionsScale } from '../../../mol-util/color/lists';
+import { ColorTheme, LocationColor } from '../../../mol-theme/color';
+import { CustomProperty } from '../../common/custom-property';
+import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property';
 
 const DefaultColor = Color(0xCCCCCC)
-const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. `ihm_cross_link_restraint.distance_threshold`).'
+const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. modeled / `ihm_cross_link_restraint.distance_threshold`).'
 
 export const CrossLinkColorThemeParams = {
-    domain: PD.Interval([-10, 10]),
+    domain: PD.Interval([0.5, 1.5], { step: 0.01 }),
     list: PD.ColorList<ColorListName>('red-grey', ColorListOptionsScale),
 }
 export type CrossLinkColorThemeParams = typeof CrossLinkColorThemeParams
@@ -25,19 +25,13 @@ export function getCrossLinkColorThemeParams(ctx: ThemeDataContext) {
     return CrossLinkColorThemeParams // TODO return copy
 }
 
-const distVecA = Vec3.zero(), distVecB = Vec3.zero()
-function linkDistance(link: Bond.Location) {
-    link.aUnit.conformation.position(link.aUnit.elements[link.aIndex], distVecA)
-    link.bUnit.conformation.position(link.bUnit.elements[link.bIndex], distVecB)
-    return Vec3.distance(distVecA, distVecB)
-}
-
 export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<CrossLinkColorThemeParams>): ColorTheme<CrossLinkColorThemeParams> {
     let color: LocationColor
     let scale: ColorScale | undefined = undefined
 
-    if (ctx.structure) {
-        const crosslinks = ctx.structure.crossLinkRestraints
+    const crossLinkRestraints = ctx.structure && CrossLinkRestraintProvider.get(ctx.structure).value
+
+    if (crossLinkRestraints) {
         scale = ColorScale.create({
             domain: props.domain,
             listOrName: props.list
@@ -45,10 +39,10 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros
         const scaleColor = scale.color
 
         color = (location: Location): Color => {
-            if (Bond.isLocation(location)) {
-                const pairs = crosslinks.getPairs(location.aIndex, location.aUnit, location.bIndex, location.bUnit)
-                if (pairs) {
-                    return scaleColor(linkDistance(location) - pairs[0].distanceThreshold)
+            if (CrossLinkRestraint.isLocation(location)) {
+                const pair = crossLinkRestraints.pairs[location.element]
+                if (pair) {
+                    return scaleColor(CrossLinkRestraint.distance(pair) / pair.distanceThreshold)
                 }
             }
             return DefaultColor
@@ -69,8 +63,12 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros
 
 export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThemeParams> = {
     label: 'Cross Link',
+    category: ColorTheme.Category.Misc,
     factory: CrossLinkColorTheme,
     getParams: getCrossLinkColorThemeParams,
     defaultValues: PD.getDefaultValues(CrossLinkColorThemeParams),
-    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.crossLinkRestraints.count > 0
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && CrossLinkRestraint.isApplicable(ctx.structure),
+    ensureCustomProperties: (ctx: CustomProperty.Context, data: ThemeDataContext) => {
+        return data.structure ? CrossLinkRestraintProvider.attach(ctx, data.structure) : Promise.resolve()
+    }
 }

+ 6 - 6
src/mol-model-formats/structure/property/pair-restraints/cross-links.ts → src/mol-model-props/integrative/cross-link-restraint/format.ts

@@ -4,12 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../../mol-model/structure/model/model'
-import { Table } from '../../../../mol-data/db'
-import { mmCIF_Schema } from '../../../../mol-io/reader/cif/schema/mmcif';
-import { Unit, CustomPropertyDescriptor } from '../../../../mol-model/structure';
-import { ElementIndex } from '../../../../mol-model/structure/model/indexing';
-import { FormatPropertyProvider } from '../../common/property';
+import { Model } from '../../../mol-model/structure/model/model'
+import { Table } from '../../../mol-data/db'
+import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
+import { Unit, CustomPropertyDescriptor } from '../../../mol-model/structure';
+import { ElementIndex } from '../../../mol-model/structure/model/indexing';
+import { FormatPropertyProvider } from '../../../mol-model-formats/structure/common/property';
 
 export { ModelCrossLinkRestraint }
 

+ 221 - 0
src/mol-model-props/integrative/cross-link-restraint/property.ts

@@ -0,0 +1,221 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ModelCrossLinkRestraint } from './format';
+import { Unit, StructureElement, Structure, CustomPropertyDescriptor, Bond} from '../../../mol-model/structure';
+import { PairRestraints, PairRestraint } from '../pair-restraints';
+import { CustomStructureProperty } from '../../common/custom-structure-property';
+import { CustomProperty } from '../../common/custom-property';
+import { DataLocation } from '../../../mol-model/location';
+import { DataLoci } from '../../../mol-model/loci';
+import { Sphere3D } from '../../../mol-math/geometry';
+import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
+import { bondLabel } from '../../../mol-theme/label';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+
+export type CrossLinkRestraintValue = PairRestraints<CrossLinkRestraint>
+
+export const CrossLinkRestraintProvider: CustomStructureProperty.Provider<{}, CrossLinkRestraintValue> = CustomStructureProperty.createProvider({
+    label: 'Cross Link Restraint',
+    descriptor: CustomPropertyDescriptor({
+        name: 'integrative-cross-link-restraint',
+        // TODO `cifExport` and `symbol`
+    }),
+    type: 'local',
+    defaultParams: {},
+    getParams: (data: Structure) => ({}),
+    isApplicable: (data: Structure) => data.models.some(m => !!ModelCrossLinkRestraint.Provider.get(m)),
+    obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<{}>) => {
+        return extractCrossLinkRestraints(data)
+    }
+})
+
+export { CrossLinkRestraint }
+
+interface CrossLinkRestraint extends PairRestraint {
+    readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound'
+    readonly distanceThreshold: number
+    readonly psi: number
+    readonly sigma1: number
+    readonly sigma2: number
+}
+
+namespace CrossLinkRestraint {
+    export enum Tag {
+        CrossLinkRestraint = 'cross-link-restraint'
+    }
+
+    export function isApplicable(structure: Structure) {
+        return structure.models.some(m => !!ModelCrossLinkRestraint.Provider.get(m))
+    }
+
+    const distVecA = Vec3(), distVecB = Vec3()
+    export function distance(pair: CrossLinkRestraint) {
+        pair.unitA.conformation.position(pair.unitA.elements[pair.indexA], distVecA)
+        pair.unitB.conformation.position(pair.unitB.elements[pair.indexB], distVecB)
+        return Vec3.distance(distVecA, distVecB)
+    }
+
+    type StructureCrossLinkRestraints = { readonly structure: Structure, readonly crossLinkRestraints: CrossLinkRestraintValue }
+
+    export type Element = number
+    export interface Location extends DataLocation<StructureCrossLinkRestraints, Element> {}
+
+    export function Location(crossLinkRestraints: CrossLinkRestraintValue, structure: Structure, index?: number): Location {
+        return DataLocation('cross-link-restraints', { structure, crossLinkRestraints }, index as any);
+    }
+
+    export function isLocation(x: any): x is Location {
+        return !!x && x.kind === 'data-location' && x.tag === 'cross-link-restraints';
+    }
+
+    export function areLocationsEqual(locA: Location, locB: Location) {
+        return (
+            locA.data.structure === locB.data.structure &&
+            locA.data.crossLinkRestraints === locB.data.crossLinkRestraints &&
+            locA.element === locB.element
+        )
+    }
+
+    function _label(crossLinkRestraints: CrossLinkRestraintValue, element: Element): string {
+        const p = crossLinkRestraints.pairs[element]
+        return `Cross Link Restraint | Type: ${p.restraintType} | Threshold: ${p.distanceThreshold} \u212B | Psi: ${p.psi} | Sigma 1: ${p.sigma1} | Sigma 2: ${p.sigma2} | Distance: ${distance(p).toFixed(2)} \u212B`
+    }
+
+    export function locationLabel(location: Location): string {
+        return _label(location.data.crossLinkRestraints, location.element)
+    }
+
+    export interface Loci extends DataLoci<StructureCrossLinkRestraints, Element> { }
+
+    export function Loci(structure: Structure, crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>): Loci {
+        return DataLoci('cross-link-restraints', { structure, crossLinkRestraints }, elements,
+            (boundingSphere) => getBoundingSphere(crossLinkRestraints, elements, boundingSphere),
+            () => getLabel(structure, crossLinkRestraints, elements));
+    }
+
+    export function isLoci(x: any): x is Loci {
+        return !!x && x.kind === 'data-loci' && x.tag === 'interactions';
+    }
+
+    export function getBoundingSphere(crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>, boundingSphere: Sphere3D) {
+        return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => {
+            const p = crossLinkRestraints.pairs[elements[i]]
+            p.unitA.conformation.position(p.unitA.elements[p.indexA], pA)
+            p.unitB.conformation.position(p.unitB.elements[p.indexB], pB)
+        }, boundingSphere)
+    }
+
+    export function getLabel(structure: Structure, crossLinkRestraints: CrossLinkRestraintValue, elements: ReadonlyArray<Element>) {
+        const element = elements[0]
+        if (element === undefined) return ''
+        const p = crossLinkRestraints.pairs[element]
+        return [
+            _label(crossLinkRestraints, element),
+            bondLabel(Bond.Location(structure, p.unitA, p.indexA, structure, p.unitB, p.indexB))
+        ].join('</br>')
+    }
+}
+
+//
+
+function _addRestraints(map: Map<number, number>, unit: Unit, restraints: ModelCrossLinkRestraint) {
+    const { elements } = unit;
+    const elementCount = elements.length;
+    const kind = unit.kind
+
+    for (let i = 0; i < elementCount; i++) {
+        const e = elements[i];
+        restraints.getIndicesByElement(e, kind).forEach(ri => map.set(ri, i))
+    }
+}
+
+function extractInter(pairs: CrossLinkRestraint[], unitA: Unit, unitB: Unit) {
+    if (unitA.model !== unitB.model) return
+    if (unitA.model.sourceData.kind !== 'mmCIF') return
+
+    const restraints = ModelCrossLinkRestraint.Provider.get(unitA.model)
+    if (!restraints) return
+
+    const rA = new Map<number, StructureElement.UnitIndex>();
+    const rB = new Map<number, StructureElement.UnitIndex>();
+    _addRestraints(rA, unitA, restraints)
+    _addRestraints(rB, unitB, restraints)
+
+    rA.forEach((indexA, ri) => {
+        const indexB = rB.get(ri)
+        if (indexB !== undefined) {
+            pairs.push(
+                createCrossLinkRestraint(unitA, indexA, unitB, indexB, restraints, ri),
+                createCrossLinkRestraint(unitB, indexB, unitA, indexA, restraints, ri)
+            )
+        }
+    })
+}
+
+function extractIntra(pairs: CrossLinkRestraint[], unit: Unit) {
+    if (unit.model.sourceData.kind !== 'mmCIF') return
+
+    const restraints = ModelCrossLinkRestraint.Provider.get(unit.model)
+    if (!restraints) return
+
+    const { elements } = unit;
+    const elementCount = elements.length;
+    const kind = unit.kind
+
+    const r = new Map<number, StructureElement.UnitIndex[]>();
+
+    for (let i = 0; i < elementCount; i++) {
+        const e = elements[i];
+        restraints.getIndicesByElement(e, kind).forEach(ri => {
+            const il = r.get(ri)
+            if (il) il.push(i as StructureElement.UnitIndex)
+            else r.set(ri, [i as StructureElement.UnitIndex])
+        })
+    }
+
+    r.forEach((il, ri) => {
+        if (il.length < 2) return
+        const [ indexA, indexB ] = il
+        pairs.push(
+            createCrossLinkRestraint(unit, indexA, unit, indexB, restraints, ri),
+            createCrossLinkRestraint(unit, indexB, unit, indexA, restraints, ri)
+        )
+    })
+}
+
+function createCrossLinkRestraint(unitA: Unit, indexA: StructureElement.UnitIndex, unitB: Unit, indexB: StructureElement.UnitIndex, restraints: ModelCrossLinkRestraint, row: number): CrossLinkRestraint {
+    return {
+        unitA, indexA, unitB, indexB,
+
+        restraintType: restraints.data.restraint_type.value(row),
+        distanceThreshold: restraints.data.distance_threshold.value(row),
+        psi: restraints.data.psi.value(row),
+        sigma1: restraints.data.sigma_1.value(row),
+        sigma2: restraints.data.sigma_2.value(row),
+    }
+}
+
+function extractCrossLinkRestraints(structure: Structure): PairRestraints<CrossLinkRestraint> {
+    const pairs: CrossLinkRestraint[] = []
+    if (!structure.models.some(m => ModelCrossLinkRestraint.Provider.get(m))) {
+        return new PairRestraints(pairs)
+    }
+
+    const n = structure.units.length
+    for (let i = 0; i < n; ++i) {
+        const unitA = structure.units[i]
+        extractIntra(pairs, unitA)
+        for (let j = i + 1; j < n; ++j) {
+            const unitB = structure.units[j]
+            if (unitA.model === unitB.model) {
+                extractInter(pairs, unitA, unitB)
+            }
+        }
+    }
+
+    return new PairRestraints(pairs)
+}

+ 149 - 0
src/mol-model-props/integrative/cross-link-restraint/representation.ts

@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
+import { ThemeRegistryContext } from '../../../mol-theme/theme';
+import { Theme } from '../../../mol-theme/theme';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { LocationIterator } from '../../../mol-geo/util/location-iterator';
+import { PickingId } from '../../../mol-geo/geometry/picking';
+import { EmptyLoci, Loci } from '../../../mol-model/loci';
+import { Interval } from '../../../mol-data/int';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Structure, StructureElement } from '../../../mol-model/structure';
+import { VisualContext } from '../../../mol-repr/visual';
+import { createLinkCylinderMesh, LinkCylinderParams } from '../../../mol-repr/structure/visual/util/link';
+import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual';
+import { VisualUpdateState } from '../../../mol-repr/util';
+import { ComplexRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider } from '../../../mol-repr/structure/representation';
+import { UnitKind, UnitKindOptions } from '../../../mol-repr/structure/visual/util/common';
+import { CustomProperty } from '../../common/custom-property';
+import { CrossLinkRestraintProvider, CrossLinkRestraint } from './property';
+
+function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintCylinderParams>, mesh?: Mesh) {
+
+    const crossLinks = CrossLinkRestraintProvider.get(structure).value!
+    if (!crossLinks.count) return Mesh.createEmpty(mesh)
+    const { sizeFactor } = props
+
+    const location = StructureElement.Location.create(structure)
+
+    const builderProps = {
+        linkCount: crossLinks.count,
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const b = crossLinks.pairs[edgeIndex]
+            const uA = b.unitA, uB = b.unitB
+            uA.conformation.position(uA.elements[b.indexA], posA)
+            uB.conformation.position(uB.elements[b.indexB], posB)
+        },
+        radius: (edgeIndex: number) => {
+            const b = crossLinks.pairs[edgeIndex]
+            location.unit = b.unitA
+            location.element = b.unitA.elements[b.indexA]
+            return theme.size.size(location) * sizeFactor
+        },
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const CrossLinkRestraintCylinderParams = {
+    ...ComplexMeshParams,
+    ...LinkCylinderParams,
+    sizeFactor: PD.Numeric(0.5, { min: 0, max: 10, step: 0.1 }),
+}
+export type CrossLinkRestraintCylinderParams = typeof CrossLinkRestraintCylinderParams
+
+export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintCylinderParams> {
+    return ComplexMeshVisual<CrossLinkRestraintCylinderParams>({
+        defaultProps: PD.getDefaultValues(CrossLinkRestraintCylinderParams),
+        createGeometry: createCrossLinkRestraintCylinderMesh,
+        createLocationIterator: createCrossLinkRestraintIterator,
+        getLoci: getLinkLoci,
+        eachLocation: eachCrossLink,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintCylinderParams>, currentProps: PD.Values<CrossLinkRestraintCylinderParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.linkCap !== currentProps.linkCap
+            )
+        }
+    }, materialId)
+}
+
+function createCrossLinkRestraintIterator(structure: Structure): LocationIterator {
+    const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value!
+    const { pairs } = crossLinkRestraints
+    const groupCount = pairs.length
+    const instanceCount = 1
+    const location = CrossLinkRestraint.Location(crossLinkRestraints, structure)
+    const getLocation = (groupIndex: number) => {
+        location.element = groupIndex
+        return location
+    }
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
+}
+
+function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, groupId } = pickingId
+    if (id === objectId) {
+        const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value!
+        const pair = crossLinkRestraints.pairs[groupId]
+        if (pair) {
+            return CrossLinkRestraint.Loci(structure, crossLinkRestraints, [groupId])
+        }
+    }
+    return EmptyLoci
+}
+
+function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+    let changed = false
+    if (CrossLinkRestraint.isLoci(loci)) {
+        if (!Structure.areEquivalent(loci.data.structure, structure)) return false
+        const crossLinkRestraints = CrossLinkRestraintProvider.get(structure).value!
+        if (loci.data.crossLinkRestraints !== crossLinkRestraints) return false
+
+        for (const e of loci.elements) {
+            if (apply(Interval.ofSingleton(e))) changed = true
+        }
+    }
+    return changed
+}
+
+//
+
+const CrossLinkRestraintVisuals = {
+    'cross-link-restraint': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintCylinderParams>) => ComplexRepresentation('Cross-link restraint', ctx, getParams, CrossLinkRestraintVisual),
+}
+
+export const CrossLinkRestraintParams = {
+    ...CrossLinkRestraintCylinderParams,
+    unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions),
+}
+export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams
+export function getCrossLinkRestraintParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(CrossLinkRestraintParams)
+}
+
+export type CrossLinkRestraintRepresentation = StructureRepresentation<CrossLinkRestraintParams>
+export function CrossLinkRestraintRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintParams>): CrossLinkRestraintRepresentation {
+    return Representation.createMulti('CrossLinkRestraint', ctx, getParams, StructureRepresentationStateBuilder, CrossLinkRestraintVisuals as unknown as Representation.Def<Structure, CrossLinkRestraintParams>)
+}
+
+export const CrossLinkRestraintRepresentationProvider: StructureRepresentationProvider<CrossLinkRestraintParams> = {
+    label: 'Cross Link Restraint',
+    description: 'Displays cross-link restraints.',
+    factory: CrossLinkRestraintRepresentation,
+    getParams: getCrossLinkRestraintParams,
+    defaultValues: PD.getDefaultValues(CrossLinkRestraintParams),
+    defaultColorTheme: { name: CrossLinkRestraint.Tag.CrossLinkRestraint },
+    defaultSizeTheme: { name: 'uniform' },
+    isApplicable: (structure: Structure) => CrossLinkRestraint.isApplicable(structure),
+    ensureCustomProperties: (ctx: CustomProperty.Context, structure: Structure) => {
+        return CrossLinkRestraintProvider.attach(ctx, structure)
+    }
+}

+ 49 - 0
src/mol-model-props/integrative/pair-restraints.ts

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureElement, Unit } from '../../mol-model/structure';
+
+const emptyArray: number[] = []
+
+export interface PairRestraint {
+    readonly unitA: Unit,
+    readonly unitB: Unit,
+    readonly indexA: StructureElement.UnitIndex,
+    readonly indexB: StructureElement.UnitIndex,
+}
+
+function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) {
+    return `${indexA}|${unitA.id}|${indexB}|${unitB.id}`
+}
+
+export class PairRestraints<T extends PairRestraint> {
+    readonly count: number
+    private readonly pairKeyIndices: Map<string, number[]>
+
+    /** Indices into this.pairs */
+    getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> {
+        const key = getPairKey(indexA, unitA, indexB, unitB)
+        return this.pairKeyIndices.get(key) || emptyArray
+    }
+
+    getPairs(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): T[] {
+        const indices = this.getPairIndices(indexA, unitA, indexB, unitB)
+        return indices.map(idx => this.pairs[idx])
+    }
+
+    constructor(public pairs: ReadonlyArray<T>) {
+        const pairKeyIndices = new Map<string, number[]>()
+        this.pairs.forEach((p, i) => {
+            const key = getPairKey(p.indexA, p.unitA, p.indexB, p.unitB)
+            const indices = pairKeyIndices.get(key)
+            if (indices) indices.push(i)
+            else pairKeyIndices.set(key, [i])
+        })
+
+        this.count = pairs.length
+        this.pairKeyIndices = pairKeyIndices
+    }
+}

+ 1 - 1
src/mol-model-props/pdbe/structure-quality-report.ts

@@ -119,7 +119,7 @@ export type StructureQualityReportParams = typeof StructureQualityReportParams
 export type StructureQualityReportProps = PD.Values<StructureQualityReportParams>
 
 export const StructureQualityReportProvider: CustomModelProperty.Provider<StructureQualityReportParams, StructureQualityReport> = CustomModelProperty.createProvider({
-    label: 'PDBe Structure Quality Report',
+    label: 'Structure Quality Report',
     descriptor: CustomPropertyDescriptor<ReportExportContext, any>({
         name: 'pdbe_structure_quality_report',
         cifExport: {

+ 3 - 2
src/mol-model-props/pdbe/themes/structure-quality-report.ts

@@ -72,13 +72,14 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
         granularity: 'group',
         color: color,
         props: props,
-        description: 'Assigns residue colors according to the number of issues or a specific issue in the PDBe Validation Report.',
+        description: 'Assigns residue colors according to the number of quality issues or a specific quality issue. Data from wwPDB Validation Report, obtained via PDBe.',
         legend: TableLegend(ValidationColorTable)
     }
 }
 
 export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params> =  {
-    label: 'PDBe Structure Quality Report',
+    label: 'Structure Quality Report',
+    category: ColorTheme.Category.Validation,
     factory: StructureQualityReportColorTheme,
     getParams: ctx => {
         const issueTypes = StructureQualityReport.getIssueTypes(ctx.structure);

+ 19 - 4
src/mol-model-props/rcsb/assembly-symmetry.ts

@@ -15,6 +15,7 @@ import { CustomProperty } from '../common/custom-property';
 import { NonNullableArray } from '../../mol-util/type-helpers';
 import { CustomStructureProperty } from '../common/custom-structure-property';
 import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { ReadonlyVec3 } from '../../mol-math/linear-algebra/3d/vec3';
 
 const BiologicalAssemblyNames = new Set([
     'author_and_software_defined_assembly',
@@ -26,6 +27,11 @@ const BiologicalAssemblyNames = new Set([
 ])
 
 export namespace AssemblySymmetry {
+    export enum Tag {
+        Cluster = 'rcsb-assembly-symmetry-cluster',
+        Representation = 'rcsb-assembly-symmetry-3d'
+    }
+
     export const DefaultServerUrl = 'https://data-beta.rcsb.org/graphql'
 
     export function isApplicable(structure?: Structure): boolean {
@@ -37,6 +43,7 @@ export namespace AssemblySymmetry {
         const mmcif = structure.models[0].sourceData.data.db
         if (!mmcif.pdbx_struct_assembly.details.isDefined) return false
         const id = structure.units[0].conformation.operator.assembly.id
+        if (id === '' || id === 'deposited') return true
         const indices = Column.indicesOf(mmcif.pdbx_struct_assembly.id, e => e === id)
         if (indices.length !== 1) return false
         const details = mmcif.pdbx_struct_assembly.details.value(indices[0])
@@ -48,27 +55,35 @@ export namespace AssemblySymmetry {
 
         const client = new GraphQLClient(props.serverUrl, ctx.fetch)
         const variables: AssemblySymmetryQueryVariables = {
-            assembly_id: structure.units[0].conformation.operator.assembly.id,
+            assembly_id: structure.units[0].conformation.operator.assembly.id || 'deposited',
             entry_id: structure.units[0].model.entryId
         }
         const result = await client.request<AssemblySymmetryQuery>(ctx.runtime, query, variables)
 
         if (!result.assembly?.rcsb_struct_symmetry) {
-            throw new Error('missing fields')
+            console.error('expected `rcsb_struct_symmetry` field')
+            return []
         }
         return result.assembly.rcsb_struct_symmetry as AssemblySymmetryValue
     }
+
+    export type RotationAxes = ReadonlyArray<{ order: number, start: ReadonlyVec3, end: ReadonlyVec3 }>
+    export function isRotationAxes(x: AssemblySymmetryValue[0]['rotation_axes']): x is RotationAxes {
+        return !!x && x.length > 0
+    }
 }
 
 export function getSymmetrySelectParam(structure?: Structure) {
-    const param = PD.Select<number>(0, [[0, 'No Symmetries']])
+    const param = PD.Select<number>(-1, [[-1, 'No Symmetries']])
     if (structure) {
         const assemblySymmetry = AssemblySymmetryProvider.get(structure).value
         if (assemblySymmetry) {
             const options: [number, string][] = []
             for (let i = 0, il = assemblySymmetry.length; i < il; ++i) {
                 const { symbol, kind } = assemblySymmetry[i]
-                options.push([ i, `${i + 1}: ${symbol} ${kind}` ])
+                if (symbol !== 'C1') {
+                    options.push([ i, `${i + 1}: ${symbol} ${kind}` ])
+                }
             }
             if (options.length) {
                 param.options = options

+ 6 - 12
src/mol-model-props/rcsb/representations/assembly-symmetry.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { AssemblySymmetryValue, getSymmetrySelectParam, AssemblySymmetryProvider } from '../assembly-symmetry';
+import { AssemblySymmetryValue, getSymmetrySelectParam, AssemblySymmetryProvider, AssemblySymmetry } from '../assembly-symmetry';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
@@ -29,7 +29,6 @@ import { TetrahedronCage } from '../../../mol-geo/primitive/tetrahedron';
 import { IcosahedronCage } from '../../../mol-geo/primitive/icosahedron';
 import { degToRad, radToDeg } from '../../../mol-math/misc';
 import { Mutable } from '../../../mol-util/type-helpers';
-import { ReadonlyVec3 } from '../../../mol-math/linear-algebra/3d/vec3';
 import { equalEps } from '../../../mol-math/linear-algebra/3d/common';
 import { Structure } from '../../../mol-model/structure';
 import { isInteger } from '../../../mol-util/number';
@@ -87,11 +86,6 @@ export type AssemblySymmetryProps = PD.Values<AssemblySymmetryParams>
 
 //
 
-type RotationAxes = ReadonlyArray<{ order: number, start: ReadonlyVec3, end: ReadonlyVec3 }>
-function isRotationAxes(x: AssemblySymmetryValue[0]['rotation_axes']): x is RotationAxes {
-    return !!x && x.length > 0
-}
-
 function getAssemblyName(s: Structure) {
     const { id } = s.units[0].conformation.operator.assembly
     return isInteger(id) ? `Assembly ${id}` : id
@@ -122,7 +116,7 @@ function getAxesMesh(data: AssemblySymmetryValue, props: PD.Values<AxesParams>,
     const { symmetryIndex, scale } = props
 
     const { rotation_axes } = data[symmetryIndex]
-    if (!isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
+    if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
 
     const { start, end } = rotation_axes[0]
     const radius = (Vec3.distance(start, end) / 500) * scale
@@ -227,11 +221,11 @@ function getSymbolScale(symbol: string) {
     return 1
 }
 
-function setSymbolTransform(t: Mat4, symbol: string, axes: RotationAxes, size: number, structure: Structure) {
+function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.RotationAxes, size: number, structure: Structure) {
     const eye = Vec3()
     const target = Vec3()
     const up = Vec3()
-    let pair: Mutable<RotationAxes> | undefined = undefined
+    let pair: Mutable<AssemblySymmetry.RotationAxes> | undefined = undefined
 
     if (symbol.startsWith('C')) {
         pair = [axes[0]]
@@ -288,7 +282,7 @@ function getCageMesh(data: Structure, props: PD.Values<CageParams>, mesh?: Mesh)
     const { symmetryIndex, scale } = props
 
     const { rotation_axes, symbol } = assemblySymmetry[symmetryIndex]
-    if (!isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
+    if (!AssemblySymmetry.isRotationAxes(rotation_axes)) return Mesh.createEmpty(mesh)
 
     const cage = getSymbolCage(symbol)
     if (!cage) return Mesh.createEmpty(mesh)
@@ -329,5 +323,5 @@ function getCageShape(ctx: RuntimeContext, data: Structure, props: AssemblySymme
 
 export type AssemblySymmetryRepresentation = Representation<Structure, AssemblySymmetryParams>
 export function AssemblySymmetryRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, AssemblySymmetryParams>): AssemblySymmetryRepresentation {
-    return Representation.createMulti('Symmetry', ctx, getParams, Representation.StateBuilder, AssemblySymmetryVisuals as unknown as Representation.Def<Structure, AssemblySymmetryParams>)
+    return Representation.createMulti('Assembly Symmetry', ctx, getParams, Representation.StateBuilder, AssemblySymmetryVisuals as unknown as Representation.Def<Structure, AssemblySymmetryParams>)
 }

+ 2 - 2
src/mol-model-props/rcsb/representations/validation-report-clashes.ts

@@ -279,8 +279,8 @@ export function ClashesRepresentation(ctx: RepresentationContext, getParams: Rep
 }
 
 export const ClashesRepresentationProvider: StructureRepresentationProvider<ClashesParams> = {
-    label: 'RCSB Clashes',
-    description: 'Displays clashes between atoms as disks.',
+    label: 'Validation Clashes',
+    description: 'Displays clashes between atoms as disks. Data from wwPDB Validation Report, obtained via RCSB PDB.',
     factory: ClashesRepresentation,
     getParams: getClashesParams,
     defaultValues: PD.getDefaultValues(ClashesParams),

+ 7 - 4
src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts

@@ -59,11 +59,13 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
             for (let j = 0, jl = members.length; j < jl; ++j) {
                 const asymId = members[j]!.asym_id
                 const operList = [...members[j]!.pdbx_struct_oper_list_ids || []] as string[]
-                if (operList.length === 0) operList.push('1') // TODO hack assuming '1' is the id of the identity operator
                 clusterByMember.set(clusterMemberKey(asymId, operList), i)
+                if (operList.length === 0) {
+                    operList.push('1') // TODO hack assuming '1' is the id of the identity operator
+                    clusterByMember.set(clusterMemberKey(asymId, operList), i)
+                }
             }
         }
-
         const palette = getPalette(clusters.length, props)
         legend = palette.legend
 
@@ -84,13 +86,14 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
         color,
         props,
         contextHash,
-        description: 'Assigns chain colors according to assembly symmetry cluster membership.',
+        description: 'Assigns chain colors according to assembly symmetry cluster membership calculated with BioJava and obtained via RCSB PDB.',
         legend
     }
 }
 
 export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams> = {
-    label: 'RCSB Assembly Symmetry Cluster',
+    label: 'Assembly Symmetry Cluster',
+    category: ColorTheme.Category.Symmetry,
     factory: AssemblySymmetryClusterColorTheme,
     getParams: getAssemblySymmetryClusterColorThemeParams,
     defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),

+ 3 - 2
src/mol-model-props/rcsb/themes/density-fit.ts

@@ -55,13 +55,14 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
         color,
         props,
         contextHash,
-        description: 'Assigns residue colors according to the density fit using normalized Real Space R (RSRZ) for polymer residues and real space correlation coefficient (RSCC) for ligands. Colors range from poor (RSRZ = 2 or RSCC = 0.678) - to better (RSRZ = 0 or RSCC = 1.0).',
+        description: 'Assigns residue colors according to the density fit using normalized Real Space R (RSRZ) for polymer residues and real space correlation coefficient (RSCC) for ligands. Colors range from poor (RSRZ = 2 or RSCC = 0.678) - to better (RSRZ = 0 or RSCC = 1.0). Data from wwPDB Validation Report, obtained via RCSB PDB.',
         legend: scaleRsrz.legend
     }
 }
 
 export const DensityFitColorThemeProvider: ColorTheme.Provider<{}> = {
-    label: 'RCSB Density Fit',
+    label: 'Density Fit',
+    category: ColorTheme.Category.Validation,
     factory: DensityFitColorTheme,
     getParams: () => ({}),
     defaultValues: PD.getDefaultValues({}),

+ 3 - 2
src/mol-model-props/rcsb/themes/geometry-quality.ts

@@ -95,13 +95,14 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
         color,
         props,
         contextHash,
-        description: 'Assigns residue colors according to the number of (filtered) geometry issues.',
+        description: 'Assigns residue colors according to the number of (filtered) geometry issues. Data from wwPDB Validation Report, obtained via RCSB PDB.',
         legend: ColorLegend
     }
 }
 
 export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQualityColorThemeParams> = {
-    label: 'RCSB Geometry Quality',
+    label: 'Geometry Quality',
+    category: ColorTheme.Category.Validation,
     factory: GeometryQualityColorTheme,
     getParams: getGeometricQualityColorThemeParams,
     defaultValues: PD.getDefaultValues(getGeometricQualityColorThemeParams({})),

+ 3 - 2
src/mol-model-props/rcsb/themes/random-coil-index.ts

@@ -46,13 +46,14 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
         color,
         props,
         contextHash,
-        description: 'Assigns residue colors according to the Random Coil Index value.',
+        description: 'Assigns residue colors according to the Random Coil Index value. Data from wwPDB Validation Report, obtained via RCSB PDB.',
         legend: scale.legend
     }
 }
 
 export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}> = {
-    label: 'RCSB Random Coil Index',
+    label: 'Random Coil Index',
+    category: ColorTheme.Category.Validation,
     factory: RandomCoilIndexColorTheme,
     getParams: () => ({}),
     defaultValues: PD.getDefaultValues({}),

+ 24 - 2
src/mol-model-props/rcsb/validation-report.ts

@@ -19,6 +19,9 @@ import { arrayMax } from '../../mol-util/array';
 import { equalEps } from '../../mol-math/linear-algebra/3d/common';
 import { Vec3 } from '../../mol-math/linear-algebra';
 import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
+import { QuerySymbolRuntime } from '../../mol-script/runtime/query/compiler';
+import { CustomPropSymbol } from '../../mol-script/language/symbol';
+import Type from '../../mol-script/language/type';
 
 export { ValidationReport }
 
@@ -118,6 +121,25 @@ namespace ValidationReport {
             case 'server': return fetch(ctx, model, props.source.params)
         }
     }
+
+    export const symbols = {
+        hasClash: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.has-clash', Type.Bool),
+            ctx => {
+                const { unit, element } = ctx.element
+                if (!Unit.isAtomic(unit)) return 0
+                const validationReport = ValidationReportProvider.get(unit.model).value
+                return validationReport && validationReport.clashes.getVertexEdgeCount(element) > 0
+            }
+        ),
+        issueCount: QuerySymbolRuntime.Dynamic(CustomPropSymbol('rcsb', 'validation-report.issue-count', Type.Num),
+            ctx => {
+                const { unit, element } = ctx.element
+                if (!Unit.isAtomic(unit)) return 0
+                const validationReport = ValidationReportProvider.get(unit.model).value
+                return validationReport?.geometryIssues.get(unit.residueIndex[element])?.size || 0
+            }
+        ),
+    }
 }
 
 const FileSourceParams = {
@@ -140,10 +162,10 @@ export type ValidationReportParams = typeof ValidationReportParams
 export type ValidationReportProps = PD.Values<ValidationReportParams>
 
 export const ValidationReportProvider: CustomModelProperty.Provider<ValidationReportParams, ValidationReport> = CustomModelProperty.createProvider({
-    label: 'RCSB Validation Report',
+    label: 'Validation Report',
     descriptor: CustomPropertyDescriptor({
         name: 'rcsb_validation_report',
-        // TODO `cifExport` and `symbol`
+        symbols: ValidationReport.symbols
     }),
     type: 'dynamic',
     defaultParams: ValidationReportParams,

+ 2 - 1
src/mol-model/loci.ts

@@ -42,7 +42,8 @@ export function isDataLoci(x?: Loci): x is DataLoci {
     return !!x && x.kind === 'data-loci';
 }
 export function areDataLociEqual(a: DataLoci, b: DataLoci) {
-    if (a.data !== b.data || a.tag !== b.tag) return false
+    // use shallowEqual to allow simple data objects that are contructed on-the-fly
+    if (!shallowEqual(a.data, b.data) || a.tag !== b.tag) return false
     if (a.elements.length !== b.elements.length) return false
     for (let i = 0, il = a.elements.length; i < il; ++i) {
         if (!shallowEqual(a.elements[i], b.elements[i])) return false

+ 4 - 8
src/mol-model/sequence/sequence.ts

@@ -80,11 +80,11 @@ namespace Sequence {
         return code
     }
 
-    export function ofResidueNames(compId: Column<string>, seqId: Column<number>, modifiedMap?: ReadonlyMap<string, string>): Sequence {
+    export function ofResidueNames(compId: Column<string>, seqId: Column<number>): Sequence {
         if (seqId.rowCount === 0) throw new Error('cannot be empty');
 
         const kind = determineKind(compId);
-        return new ResidueNamesImpl(kind, compId, seqId, modifiedMap) as Sequence;
+        return new ResidueNamesImpl(kind, compId, seqId) as Sequence;
     }
 
     class ResidueNamesImpl<K extends Kind, Alphabet extends string> implements Base<K, Alphabet> {
@@ -154,11 +154,7 @@ namespace Sequence {
                 const code = this.codeFromName(name);
                 // in case of MICROHETEROGENEITY `sequenceArray[idx]` may already be set
                 if (!sequenceArray[idx] || sequenceArray[idx] === '-') {
-                    if (code === 'X' && this.modifiedMap && this.modifiedMap.has(name)) {
-                        sequenceArray[idx] = this.modifiedMap.get(name)!
-                    } else {
-                        sequenceArray[idx] = code;
-                    }
+                    sequenceArray[idx] = code;
                 }
                 labels[idx].push(code === 'X' ? name : code);
                 compIds[seqId].push(name);
@@ -183,7 +179,7 @@ namespace Sequence {
             this._length = count
         }
 
-        constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>, private modifiedMap?: ReadonlyMap<string, string>) {
+        constructor(public kind: K, public compId: Column<string>, public seqId: Column<number>) {
 
             this.codeFromName = codeProvider(kind)
         }

+ 0 - 64
src/mol-model/structure/export/categories/modified-residues.ts

@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2017-2018 Mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Segmentation } from '../../../../mol-data/int';
-import { CifWriter } from '../../../../mol-io/writer/cif';
-import { StructureElement, StructureProperties as P, Unit } from '../../../structure';
-import { CifExportContext } from '../mmcif';
-
-import CifField = CifWriter.Field
-import CifCategory = CifWriter.Category
-
-const pdbx_struct_mod_residue_fields: CifField<number, StructureElement.Location[]>[] = [
-    CifField.index('id'),
-    CifField.str(`label_comp_id`, (i, xs) => P.residue.label_comp_id(xs[i])),
-    CifField.int(`label_seq_id`, (i, xs) => P.residue.label_seq_id(xs[i])),
-    CifField.str(`pdbx_PDB_ins_code`, (i, xs) => P.residue.pdbx_PDB_ins_code(xs[i])),
-    CifField.str(`label_asym_id`, (i, xs) => P.chain.label_asym_id(xs[i])),
-    CifField.str(`label_entity_id`, (i, xs) => P.chain.label_entity_id(xs[i])),
-    CifField.str(`auth_comp_id`, (i, xs) => P.residue.auth_comp_id(xs[i])),
-    CifField.int(`auth_seq_id`, (i, xs) => P.residue.auth_seq_id(xs[i])),
-    CifField.str(`auth_asym_id`, (i, xs) => P.chain.auth_asym_id(xs[i])),
-    CifField.str<number, StructureElement.Location[]>('parent_comp_id', (i, xs) => xs[i].unit.model.properties.modifiedResidues.parentId.get(P.residue.label_comp_id(xs[i]))!),
-    CifField.str('details', (i, xs) => xs[i].unit.model.properties.modifiedResidues.details.get(P.residue.label_comp_id(xs[i]))!)
-];
-
-function getModifiedResidues({ structures }: CifExportContext): StructureElement.Location[] {
-    // TODO: can different models (in the same mmCIF file) have different modified residues?
-    const structure = structures[0], model = structure.model;
-    const map = model.properties.modifiedResidues.parentId;
-    if (!map.size) return [];
-
-    const ret = [];
-    const prop = P.residue.label_comp_id;
-    const loc = StructureElement.Location.create(structure);
-    for (const unit of structure.units) {
-        if (!Unit.isAtomic(unit) || !unit.conformation.operator.isIdentity) continue;
-        const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
-        loc.unit = unit;
-        while (residues.hasNext) {
-            const seg = residues.move();
-            loc.element = unit.elements[seg.start];
-            const name = prop(loc);
-            if (map.has(name)) {
-                ret[ret.length] = StructureElement.Location.clone(loc);
-            }
-        }
-    }
-    return ret;
-}
-
-export const _pdbx_struct_mod_residue: CifCategory<CifExportContext> = {
-    name: 'pdbx_struct_mod_residue',
-    instance(ctx) {
-        const residues = getModifiedResidues(ctx);
-        return {
-            fields: pdbx_struct_mod_residue_fields,
-            source: [{ data: residues, rowCount: residues.length }]
-        };
-    }
-}

+ 0 - 2
src/mol-model/structure/export/mmcif.ts

@@ -11,7 +11,6 @@ import { Structure } from '../structure'
 import { _atom_site } from './categories/atom_site';
 import CifCategory = CifWriter.Category
 import { _struct_conf, _struct_sheet_range } from './categories/secondary-structure';
-import { _pdbx_struct_mod_residue } from './categories/modified-residues';
 import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './categories/misc';
 import { Model } from '../model';
 import { getUniqueEntityIndicesFromStructures, copy_mmCif_category } from './categories/utils';
@@ -83,7 +82,6 @@ const Categories = [
     copy_mmCif_category('atom_sites'),
 
     _pdbx_nonpoly_scheme,
-    _pdbx_struct_mod_residue,
 
     // Atoms
     _atom_site

+ 3 - 6
src/mol-model/structure/model/model.ts

@@ -9,7 +9,7 @@ import UUID from '../../../mol-util/uuid';
 import StructureSequence from './properties/sequence';
 import { AtomicHierarchy, AtomicConformation, AtomicRanges } from './properties/atomic';
 import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
-import { Entities, ChemicalComponentMap, MissingResidues } from './properties/common';
+import { Entities, ChemicalComponentMap, MissingResidues, StructAsymMap } from './properties/common';
 import { CustomProperties } from '../common/custom-property';
 import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
 import { ModelFormat } from '../../../mol-model-formats/structure/format';
@@ -53,17 +53,14 @@ export interface Model extends Readonly<{
     atomicRanges: AtomicRanges,
 
     properties: {
-        /** maps modified residue name to its parent */
-        readonly modifiedResidues: Readonly<{
-            parentId: ReadonlyMap<string, string>,
-            details: ReadonlyMap<string, string>
-        }>,
         /** map that holds details about unobserved or zero occurrence residues */
         readonly missingResidues: MissingResidues,
         /** maps residue name to `ChemicalComponent` data */
         readonly chemicalComponentMap: ChemicalComponentMap
         /** maps residue name to `SaccharideComponent` data */
         readonly saccharideComponentMap: SaccharideComponentMap
+        /** maps label_asym_id name to `StructAsym` data */
+        readonly structAsymMap: StructAsymMap
     },
 
     customProperties: CustomProperties,

+ 4 - 1
src/mol-model/structure/model/properties/common.ts

@@ -29,4 +29,7 @@ export interface MissingResidues {
     has(model_num: number, asym_id: string, seq_id: number): boolean
     get(model_num: number, asym_id: string, seq_id: number): MissingResidue | undefined
     readonly size: number
-}
+}
+
+export type StructAsym = Table.Row<Pick<mmCIF_Schema['struct_asym'], 'id' | 'entity_id'> & { auth_id: Column.Schema.Str }>
+export type StructAsymMap = ReadonlyMap<string, StructAsym>

+ 4 - 4
src/mol-model/structure/model/properties/sequence.ts

@@ -37,13 +37,13 @@ namespace StructureSequence {
         return { sequences, byEntityKey }
     }
 
-    export function fromHierarchy(entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy, modResMap?: ReadonlyMap<string, string>): StructureSequence {
-        const atomic = fromAtomicHierarchy(entities, atomicHierarchy, modResMap)
+    export function fromHierarchy(entities: Entities, atomicHierarchy: AtomicHierarchy, coarseHierarchy: CoarseHierarchy): StructureSequence {
+        const atomic = fromAtomicHierarchy(entities, atomicHierarchy)
         const coarse = coarseHierarchy.isDefined ? fromCoarseHierarchy(entities, coarseHierarchy) : Empty
         return merge(atomic, coarse)
     }
 
-    export function fromAtomicHierarchy(entities: Entities, hierarchy: AtomicHierarchy, modResMap?: ReadonlyMap<string, string>): StructureSequence {
+    export function fromAtomicHierarchy(entities: Entities, hierarchy: AtomicHierarchy): StructureSequence {
         const { label_comp_id, label_seq_id } = hierarchy.residues
         const { chainAtomSegments, residueAtomSegments } = hierarchy
         const { count, offsets } = chainAtomSegments
@@ -75,7 +75,7 @@ namespace StructureSequence {
             const num = Column.window(label_seq_id, rStart, rEnd);
             byEntityKey[entityKey] = {
                 entityId: entities.data.id.value(entityKey),
-                sequence: Sequence.ofResidueNames(compId, num, modResMap)
+                sequence: Sequence.ofResidueNames(compId, num)
             };
 
             sequences.push(byEntityKey[entityKey]);

+ 3 - 3
src/mol-model/structure/model/types.ts

@@ -137,7 +137,7 @@ export const PolymerTypeAtomRoleId: { [k in PolymerType]: { [k in AtomRole]: Set
 export const ProteinBackboneAtoms = new Set([
     'CA', 'C', 'N', 'O',
     'O1', 'O2', 'OC1', 'OC2', 'OX1', 'OXT',
-    'H', 'H1', 'H2', 'H3', 'HA', 'HN',
+    'H', 'H1', 'H2', 'H3', 'HA', 'HN', 'HXT',
     'BB'
 ])
 
@@ -257,7 +257,7 @@ export const DnaBaseNames = new Set([
     'DN' // unknown DNA base from CCD
 ])
 export const PeptideBaseNames = new Set([ 'APN', 'CPN', 'TPN', 'GPN' ])
-export const PurineBaseNames = new Set([ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ])
+export const PurineBaseNames = new Set([ 'A', 'G', 'I', 'DA', 'DG', 'DI', 'APN', 'GPN' ])
 export const PyrimidineBaseNames = new Set([ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ])
 export const BaseNames = SetUtils.unionMany(RnaBaseNames, DnaBaseNames, PeptideBaseNames)
 
@@ -342,7 +342,7 @@ export function getDefaultChemicalComponent(compId: string): ChemicalComponent {
         formula_weight: 0,
         id: compId,
         name: compId,
-        mon_nstd_flag: 'n',
+        mon_nstd_flag: PolymerNames.has(compId) ? 'y' : 'n',
         pdbx_synonyms: [],
         type: getComponentType(compId)
     };

+ 18 - 0
src/mol-model/structure/structure/element/loci.ts

@@ -98,6 +98,24 @@ export namespace Loci {
         return Location.create(loci.structure, unit, element);
     }
 
+    export function firstElement(loci: Loci): Loci {
+        if (isEmpty(loci)) return loci;
+        return Loci(loci.structure, [{
+            unit: loci.elements[0].unit,
+            indices: OrderedSet.ofSingleton(OrderedSet.start(loci.elements[0].indices))
+        }])
+    }
+
+    export function firstResidue(loci: Loci): Loci {
+        if (isEmpty(loci)) return loci;
+        return extendToWholeResidues(firstElement(loci))
+    }
+
+    export function firstChain(loci: Loci): Loci {
+        if (isEmpty(loci)) return loci;
+        return extendToWholeChains(firstElement(loci))
+    }
+
     export function toStructure(loci: Loci): Structure {
         const units: Unit[] = []
         for (const e of loci.elements) {

+ 7 - 16
src/mol-model/structure/structure/properties.ts

@@ -1,14 +1,15 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import StructureElement from './element'
 import Unit from './unit'
 import { VdwRadius } from '../model/properties/atomic';
-import { ModelSecondaryStructure } from '../../../mol-model-formats/structure/property/secondary-structure';
 import { SecondaryStructureType } from '../model/types';
+import { SecondaryStructureProvider } from '../../../mol-model-props/computed/secondary-structure';
 
 function p<T>(p: StructureElement.Property<T>) { return p; }
 
@@ -96,28 +97,18 @@ const residue = {
     pdbx_PDB_ins_code: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.pdbx_PDB_ins_code.value(l.unit.residueIndex[l.element])),
 
     // Properties
-    isModified: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.modifiedResidues.parentId.has(compId(l))),
-    modifiedParentName: p(l => {
-        if (!Unit.isAtomic(l.unit)) notAtomic()
-        const id = compId(l)
-        return l.unit.model.properties.modifiedResidues.parentId.get(id) || id
-    }),
     isNonStandard: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.chemicalComponentMap.get(compId(l))!.mon_nstd_flag[0] !== 'y'),
     hasMicroheterogeneity: p(hasMicroheterogeneity),
     microheterogeneityCompIds: p(microheterogeneityCompIds),
-    // TODO implement as symbol in SecondaryStructureProvider (not ModelSecondaryStructure.Provider)
     secondary_structure_type: p(l => {
         if (!Unit.isAtomic(l.unit)) notAtomic()
-        const secondaryStructure = ModelSecondaryStructure.Provider.get(l.unit.model)
-        if (secondaryStructure) return secondaryStructure.type[l.unit.residueIndex[l.element]]
-        else return SecondaryStructureType.Flag.NA
+        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id)
+        return secStruc?.type[l.unit.residueIndex[l.element]] ?? SecondaryStructureType.Flag.NA
     }),
-    // TODO implement as symbol in SecondaryStructureProvider (not ModelSecondaryStructure.Provider)
     secondary_structure_key: p(l => {
         if (!Unit.isAtomic(l.unit)) notAtomic()
-        const secondaryStructure = ModelSecondaryStructure.Provider.get(l.unit.model)
-        if (secondaryStructure) return secondaryStructure.key[l.unit.residueIndex[l.element]]
-        else return -1
+        const secStruc = SecondaryStructureProvider.get(l.structure).value?.get(l.unit.id)
+        return secStruc?.key[l.unit.residueIndex[l.element]] ?? -1
     }),
     chem_comp_type: p(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.properties.chemicalComponentMap.get(compId(l))!.type),
 }

+ 1 - 9
src/mol-model/structure/structure/structure.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -16,7 +16,6 @@ import { StructureLookup3D } from './util/lookup3d';
 import { CoarseElements } from '../model/properties/coarse';
 import { StructureSubsetBuilder } from './util/subset-builder';
 import { InterUnitBonds, computeInterUnitBonds, Bond } from './unit/bonds';
-import { PairRestraints, CrossLinkRestraint, extractCrossLinkRestraints } from './unit/pair-restraints';
 import StructureSymmetry from './symmetry';
 import StructureProperties from './properties';
 import { ResidueIndex, ChainIndex, EntityIndex } from '../model/indexing';
@@ -41,7 +40,6 @@ class Structure {
         parent?: Structure,
         lookup3d?: StructureLookup3D,
         interUnitBonds?: InterUnitBonds,
-        crossLinkRestraints?: PairRestraints<CrossLinkRestraint>,
         unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>,
         unitSymmetryGroupsIndexMap?: IntMap<number>,
         carbohydrates?: Carbohydrates,
@@ -228,12 +226,6 @@ class Structure {
         return this._props.interUnitBonds;
     }
 
-    get crossLinkRestraints() {
-        if (this._props.crossLinkRestraints) return this._props.crossLinkRestraints;
-        this._props.crossLinkRestraints = extractCrossLinkRestraints(this);
-        return this._props.crossLinkRestraints;
-    }
-
     get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> {
         if (this._props.unitSymmetryGroups) return this._props.unitSymmetryGroups;
         this._props.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this);

+ 69 - 17
src/mol-model/structure/structure/symmetry.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -11,10 +11,11 @@ import { Spacegroup, SpacegroupCell, SymmetryOperator } from '../../../mol-math/
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { RuntimeContext, Task } from '../../../mol-task';
 import { Symmetry, Model } from '../model';
-import { QueryContext, StructureSelection } from '../query';
+import { QueryContext, StructureSelection, Queries as Q } from '../query';
 import Structure from './structure';
 import Unit from './unit';
 import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
+import StructureProperties from './properties';
 
 namespace StructureSymmetry {
     export function buildAssembly(structure: Structure, asmName: string) {
@@ -48,6 +49,40 @@ namespace StructureSymmetry {
         });
     }
 
+    export type Generators = { operators: { index: number, shift: Vec3 }[], asymIds: string[] }[]
+
+    export function buildSymmetryAssembly(structure: Structure, generators: Generators, symmetry: Symmetry) {
+        return Task.create('Build Symmetry Assembly', async ctx => {
+            const models = structure.models;
+            if (models.length !== 1) throw new Error('Can only build symmetry assemblies from structures based on 1 model.');
+
+            const modelCenter = Vec3()
+            const assembler = Structure.Builder({ label: structure.label });
+
+            const queryCtx = new QueryContext(structure);
+
+            for (const g of generators) {
+                const selector = getSelector(g.asymIds);
+                const selection = selector(queryCtx);
+                if (StructureSelection.structureCount(selection) === 0) {
+                    continue;
+                }
+                const { units } = StructureSelection.unionStructure(selection);
+
+                for (const { index, shift: [i, j, k] } of g.operators) {
+                    const operators = getOperatorsForIndex(symmetry, index, i, j, k, modelCenter)
+                    for (const unit of units) {
+                        for (const op of operators) {
+                            assembler.addWithOperator(unit, op);
+                        }
+                    }
+                }
+            }
+
+            return assembler.getStructure();
+        });
+    }
+
     export function builderSymmetryMates(structure: Structure, radius: number) {
         return Task.create('Find Symmetry Mates', ctx => findMatesRadius(ctx, structure, radius));
     }
@@ -96,7 +131,35 @@ namespace StructureSymmetry {
     }
 }
 
-function getOperators(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCenter: Vec3) {
+function getSelector(asymIds: string[]) {
+    return Q.generators.atoms({ chainTest: Q.pred.and(
+        Q.pred.eq(ctx => StructureProperties.unit.operator_name(ctx.element), SymmetryOperator.DefaultName),
+        Q.pred.inSet(ctx => StructureProperties.chain.label_asym_id(ctx.element), asymIds)
+    )});
+}
+
+function getOperatorsForIndex(symmetry: Symmetry, index: number, i: number, j: number, k: number, modelCenter: Vec3) {
+    const { spacegroup, ncsOperators } = symmetry;
+    const operators: SymmetryOperator[] = []
+
+    const { toFractional } = spacegroup.cell
+    const ref = Vec3.transformMat4(Vec3(), modelCenter, toFractional)
+
+    const symOp = Spacegroup.getSymmetryOperatorRef(spacegroup, index, i, j, k, ref)
+    if (ncsOperators && ncsOperators.length) {
+        for (let u = 0, ul = ncsOperators.length; u < ul; ++u) {
+            const ncsOp = ncsOperators![u]
+            const matrix = Mat4.mul(Mat4(), symOp.matrix, ncsOp.matrix)
+            const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl, symOp.spgrOp);
+            operators.push(operator)
+        }
+    } else {
+        operators.push(symOp)
+    }
+    return operators
+}
+
+function getOperatorsForRange(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCenter: Vec3) {
     const { spacegroup, ncsOperators } = symmetry;
     const ncsCount = (ncsOperators && ncsOperators.length) || 0
     const operators: SymmetryOperator[] = [];
@@ -117,18 +180,7 @@ function getOperators(symmetry: Symmetry, ijkMin: Vec3, ijkMax: Vec3, modelCente
                 for (let k = ijkMin[2]; k <= ijkMax[2]; k++) {
                     // check if we have added identity as the 1st operator.
                     if (!ncsCount && op === 0 && i === 0 && j === 0 && k === 0) continue;
-
-                    const symOp = Spacegroup.getSymmetryOperatorRef(spacegroup, op, i, j, k, ref)
-                    if (ncsCount) {
-                        for (let u = 0; u < ncsCount; ++u) {
-                            const ncsOp = ncsOperators![u]
-                            const matrix = Mat4.mul(Mat4.zero(), symOp.matrix, ncsOp.matrix)
-                            const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl, symOp.spgrOp);
-                            operators[operators.length] = operator;
-                        }
-                    } else {
-                        operators[operators.length] = symOp;
-                    }
+                    operators.push(...getOperatorsForIndex(symmetry, op, i, j, k, ref))
                 }
             }
         }
@@ -142,7 +194,7 @@ function getOperatorsCached333(symmetry: Symmetry, ref: Vec3) {
     }
     symmetry._operators_333 = {
         ref: Vec3.clone(ref),
-        operators: getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3), ref)
+        operators: getOperatorsForRange(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3), ref)
     };
     return symmetry._operators_333.operators;
 }
@@ -181,7 +233,7 @@ async function findSymmetryRange(ctx: RuntimeContext, structure: Structure, ijkM
     if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
 
     const modelCenter = Model.getCenter(models[0])
-    const operators = getOperators(symmetry, ijkMin, ijkMax, modelCenter);
+    const operators = getOperatorsForRange(symmetry, ijkMin, ijkMax, modelCenter);
     return assembleOperators(structure, operators);
 }
 

+ 0 - 10
src/mol-model/structure/structure/unit/pair-restraints.ts

@@ -1,10 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-export * from './pair-restraints/data'
-export * from './pair-restraints/extract-cross-links'
-// export * from './pair-restraints/extract-predicted-contacts'
-// export * from './pair-restraints/extract-distance-restraints'

+ 0 - 77
src/mol-model/structure/structure/unit/pair-restraints/data.ts

@@ -1,77 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import Unit from '../../unit';
-import { StructureElement } from '../../../structure';
-
-const emptyArray: number[] = []
-
-interface PairRestraint {
-    readonly unitA: Unit,
-    readonly unitB: Unit,
-    readonly indexA: StructureElement.UnitIndex,
-    readonly indexB: StructureElement.UnitIndex,
-}
-
-function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) {
-    return `${indexA}|${unitA.id}|${indexB}|${unitB.id}`
-}
-
-export class PairRestraints<T extends PairRestraint> {
-    readonly count: number
-    private readonly pairKeyIndices: Map<string, number[]>
-
-    /** Indices into this.pairs */
-    getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> {
-        const key = getPairKey(indexA, unitA, indexB, unitB)
-        const indices = this.pairKeyIndices.get(key)
-        return indices !== undefined ? indices : emptyArray
-    }
-
-    getPairs(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): T[] | undefined {
-        const indices = this.getPairIndices(indexA, unitA, indexB, unitB)
-        return indices.length ? indices.map(idx => this.pairs[idx]) : undefined
-    }
-
-    constructor(public pairs: ReadonlyArray<T>) {
-        const pairKeyIndices = new Map<string, number[]>()
-        this.pairs.forEach((p, i) => {
-            const key = getPairKey(p.indexA, p.unitA, p.indexB, p.unitB)
-            const indices = pairKeyIndices.get(key)
-            if (indices) indices.push(i)
-            else pairKeyIndices.set(key, [i])
-        })
-
-        this.count = pairs.length
-        this.pairKeyIndices = pairKeyIndices
-    }
-}
-
-export interface CrossLinkRestraint extends PairRestraint {
-    readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound'
-    readonly distanceThreshold: number
-    readonly psi: number
-    readonly sigma1: number
-    readonly sigma2: number
-}
-
-export interface PredictedContactRestraint extends PairRestraint {
-    readonly distance_lower_limit: number
-    readonly distance_upper_limit: number
-    readonly probability: number
-    readonly restraint_type: 'lower bound' | 'upper bound' | 'lower and upper bound'
-    readonly model_granularity: 'by-residue' | 'by-feature' | 'by-atom'
-}
-
-export interface DistanceRestraint extends PairRestraint {
-    readonly upper_limit: number
-    readonly upper_limit_esd: number
-    readonly lower_limit: number
-    readonly lower_limit_esd: number
-    readonly probability: number
-    readonly restraint_type: 'lower bound' | 'upper bound' | 'lower and upper bound'
-    readonly granularity: 'by-residue' | 'by-atom'
-}

+ 0 - 111
src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts

@@ -1,111 +0,0 @@
-/**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import Unit from '../../unit';
-import Structure from '../../structure';
-import { PairRestraints, CrossLinkRestraint } from './data';
-import { StructureElement } from '../../../structure';
-import { ModelCrossLinkRestraint } from '../../../../../mol-model-formats/structure/property/pair-restraints/cross-links';
-
-function _addRestraints(map: Map<number, number>, unit: Unit, restraints: ModelCrossLinkRestraint) {
-    const { elements } = unit;
-    const elementCount = elements.length;
-    const kind = unit.kind
-
-    for (let i = 0; i < elementCount; i++) {
-        const e = elements[i];
-        restraints.getIndicesByElement(e, kind).forEach(ri => map.set(ri, i))
-    }
-}
-
-function extractInter(pairs: CrossLinkRestraint[], unitA: Unit, unitB: Unit) {
-    if (unitA.model !== unitB.model) return
-    if (unitA.model.sourceData.kind !== 'mmCIF') return
-
-    const restraints = ModelCrossLinkRestraint.Provider.get(unitA.model)
-    if (!restraints) return
-
-    const rA = new Map<number, StructureElement.UnitIndex>();
-    const rB = new Map<number, StructureElement.UnitIndex>();
-    _addRestraints(rA, unitA, restraints)
-    _addRestraints(rB, unitB, restraints)
-
-    rA.forEach((indexA, ri) => {
-        const indexB = rB.get(ri)
-        if (indexB !== undefined) {
-            pairs.push(
-                createCrossLinkRestraint(unitA, indexA, unitB, indexB, restraints, ri),
-                createCrossLinkRestraint(unitB, indexB, unitA, indexA, restraints, ri)
-            )
-        }
-    })
-}
-
-function extractIntra(pairs: CrossLinkRestraint[], unit: Unit) {
-    if (unit.model.sourceData.kind !== 'mmCIF') return
-
-    const restraints = ModelCrossLinkRestraint.Provider.get(unit.model)
-    if (!restraints) return
-
-    const { elements } = unit;
-    const elementCount = elements.length;
-    const kind = unit.kind
-
-    const r = new Map<number, StructureElement.UnitIndex[]>();
-
-    for (let i = 0; i < elementCount; i++) {
-        const e = elements[i];
-        restraints.getIndicesByElement(e, kind).forEach(ri => {
-            const il = r.get(ri)
-            if (il) il.push(i as StructureElement.UnitIndex)
-            else r.set(ri, [i as StructureElement.UnitIndex])
-        })
-    }
-
-    r.forEach((il, ri) => {
-        if (il.length < 2) return
-        const [ indexA, indexB ] = il
-        pairs.push(
-            createCrossLinkRestraint(unit, indexA, unit, indexB, restraints, ri),
-            createCrossLinkRestraint(unit, indexB, unit, indexA, restraints, ri)
-        )
-    })
-}
-
-function createCrossLinkRestraint(unitA: Unit, indexA: StructureElement.UnitIndex, unitB: Unit, indexB: StructureElement.UnitIndex, restraints: ModelCrossLinkRestraint, row: number): CrossLinkRestraint {
-    return {
-        unitA, indexA, unitB, indexB,
-
-        restraintType: restraints.data.restraint_type.value(row),
-        distanceThreshold: restraints.data.distance_threshold.value(row),
-        psi: restraints.data.psi.value(row),
-        sigma1: restraints.data.sigma_1.value(row),
-        sigma2: restraints.data.sigma_2.value(row),
-    }
-}
-
-function extractCrossLinkRestraints(structure: Structure): PairRestraints<CrossLinkRestraint> {
-    const pairs: CrossLinkRestraint[] = []
-    if (!structure.models.some(m => ModelCrossLinkRestraint.Provider.get(m))) {
-        return new PairRestraints(pairs)
-    }
-
-    const n = structure.units.length
-    for (let i = 0; i < n; ++i) {
-        const unitA = structure.units[i]
-        extractIntra(pairs, unitA)
-        for (let j = i + 1; j < n; ++j) {
-            const unitB = structure.units[j]
-            if (unitA.model === unitB.model) {
-                extractInter(pairs, unitA, unitB)
-            }
-        }
-    }
-
-    return new PairRestraints(pairs)
-}
-
-export { extractCrossLinkRestraints };

+ 0 - 7
src/mol-model/structure/structure/unit/pair-restraints/extract-distance-restraints.ts

@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-// TODO extract from `_ma_distance_restraints`

+ 0 - 7
src/mol-model/structure/structure/unit/pair-restraints/extract-predicted-contacts.ts

@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-// TODO extract from `ihm_predicted_contact_restraint`

+ 1 - 0
src/mol-plugin-ui/base.tsx

@@ -25,6 +25,7 @@ export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.C
     componentWillUnmount() {
         if (!this.subs) return;
         for (const s of this.subs) s.unsubscribe();
+        this.subs = [];
     }
 
     protected init?(): void;

+ 157 - 0
src/mol-plugin-ui/controls/action-menu.tsx

@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react'
+import { Icon } from './common';
+import { ParamDefinition } from '../../mol-util/param-definition';
+
+export class ActionMenu extends React.PureComponent<ActionMenu.Props> {
+    hide = () => this.props.onSelect(void 0)
+
+    render() {
+        const cmd = this.props;
+
+        return <div className='msp-action-menu-options' style={{ marginTop: '1px' }}>
+            {cmd.header && <div className='msp-control-group-header' style={{ position: 'relative' }}>
+                <button className='msp-btn msp-btn-block' onClick={this.hide}>
+                    <Icon name='off' style={{ position: 'absolute', right: '2px', top: 0 }} />
+                    <b>{cmd.header}</b>
+                </button>
+            </div>}
+            <Section items={cmd.items} onSelect={cmd.onSelect} current={cmd.current} />
+        </div>
+    }
+}
+
+export namespace ActionMenu {
+    export type Props = { items: Items, onSelect: OnSelect, header?: string, current?: Item | undefined }
+
+    export type OnSelect = (item: Item | undefined) => void
+
+    export type Items = string | Item | [Items]
+    export type Item = { label: string, icon?: string, value: unknown }
+
+    export function Item(label: string, value: unknown): Item
+    export function Item(label: string, icon: string, value: unknown): Item
+    export function Item(label: string, iconOrValue: any, value?: unknown): Item {
+        if (value) return { label, icon: iconOrValue, value };
+        return { label, value: iconOrValue };
+    }
+
+    export function createItems<T>(xs: ArrayLike<T>, options?: { label?: (t: T) => string, value?: (t: T) => any, category?: (t: T) => string | undefined }) {
+        const { label, value, category } = options || { };
+        let cats: Map<string, (ActionMenu.Item | string)[]> | undefined = void 0;
+        const items: (ActionMenu.Item | (ActionMenu.Item | string)[] | string)[] = [];
+        for (let i = 0; i < xs.length; i++) {
+            const x = xs[i];
+
+            const catName = category?.(x);
+            const l = label ? label(x) : '' + x;
+            const v = value ? value(x) : x;
+
+            if (!!catName) {
+                if (!cats) cats = new Map<string, (ActionMenu.Item | string)[]>();
+
+                let cat = cats.get(catName);
+                if (!cat) {
+                    cat = [catName];
+                    cats.set(catName, cat);
+                    items.push(cat);
+                }
+                cat.push(ActionMenu.Item(l, v));
+            } else {
+                items.push(ActionMenu.Item(l, v));
+            }
+        }
+        return items as ActionMenu.Items;
+    }
+    
+    type Opt = ParamDefinition.Select<any>['options'][0];
+    const _selectOptions = { value: (o: Opt) => o[0], label: (o: Opt) => o[1], category: (o: Opt) => o[2] };
+
+    export function createItemsFromSelectParam(param: ParamDefinition.Select<any>) {
+        return createItems(param.options, _selectOptions);
+    }
+
+    export function findItem(items: Items, value: any): Item | undefined {
+        if (typeof items === 'string') return;
+        if (isItem(items)) return items.value === value ? items : void 0;
+        for (const s of items) {
+            const found = findItem(s, value);
+            if (found) return found;
+        }
+    }
+
+    export function getFirstItem(items: Items): Item | undefined {
+        if (typeof items === 'string') return;
+        if (isItem(items)) return items;
+        for (const s of items) {
+            const found = getFirstItem(s);
+            if (found) return found;
+        }
+    }
+}
+
+type SectionProps = { header?: string, items: ActionMenu.Items, onSelect: ActionMenu.OnSelect, current: ActionMenu.Item | undefined }
+type SectionState = { items: ActionMenu.Items, current: ActionMenu.Item | undefined, isExpanded: boolean }
+
+class Section extends React.PureComponent<SectionProps, SectionState> {
+    state = {
+        items: this.props.items,
+        current: this.props.current,
+        isExpanded: !!this.props.current && !!ActionMenu.findItem(this.props.items, this.props.current.value)
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    static getDerivedStateFromProps(props: SectionProps, state: SectionState) {
+        if (props.items === state.items && props.current === state.current) return null;
+        return { items: props.items, current: props.current, isExpanded: props.current && !!ActionMenu.findItem(props.items, props.current.value) }
+    }
+
+    render() {
+        const { header, items, onSelect, current } = this.props;
+
+        if (typeof items === 'string') return null;
+        if (isItem(items)) return <Action item={items} onSelect={onSelect} current={current} />
+
+        const hasCurrent = header && current && !!ActionMenu.findItem(items, current.value)
+
+        return <div>
+            {header && <div className='msp-control-group-header' style={{ marginTop: '1px' }}>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                    <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
+                    {hasCurrent ? <b>{header}</b> : header}
+                </button>
+            </div>}
+            <div className='msp-control-offset'>
+                {(!header || this.state.isExpanded) && items.map((x, i) => {
+                    if (typeof x === 'string') return null;
+                    if (isItem(x)) return <Action key={i} item={x} onSelect={onSelect} current={current} />
+                    return <Section key={i} header={typeof x[0] === 'string' ? x[0] : void 0} items={x} onSelect={onSelect} current={current} />
+                })}
+            </div>
+        </div>;
+    }
+}
+
+const Action: React.FC<{ item: ActionMenu.Item, onSelect: ActionMenu.OnSelect, current: ActionMenu.Item | undefined }> = ({ item, onSelect, current }) => {
+    const isCurrent = current === item;
+    return <div className='msp-control-row'>
+        <button onClick={() => onSelect(item)}>
+            {item.icon && <Icon name={item.icon} />}
+            {isCurrent ? <b>{item.label}</b> : item.label}
+        </button>
+    </div>;
+}
+
+function isItem(x: any): x is ActionMenu.Item {
+    const v = x as ActionMenu.Item;
+    return v && !!v.label && typeof v.value !== 'undefined';
+}

+ 25 - 13
src/mol-plugin-ui/controls/common.tsx

@@ -308,16 +308,28 @@ export function SectionHeader(props: { icon?: string, title: string | JSX.Elemen
     </div>
 }
 
-// export const ToggleButton = (props: {
-//     onChange: (v: boolean) => void,
-//     value: boolean,
-//     label: string,
-//     title?: string
-// }) => <div className='lm-control-row lm-toggle-button' title={props.title}>
-//         <span>{props.label}</span>
-//         <div>
-//             <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}>
-//                     <span className={ `lm-icon lm-icon-${props.value ? 'ok' : 'off'}` }></span> {props.value ? 'On' : 'Off'}
-//             </button>
-//         </div>
-//     </div>
+export type ToggleButtonProps = {
+    style?: React.CSSProperties,
+    className?: string,
+    disabled?: boolean,
+    label: string | JSX.Element,
+    title?: string,
+    isSelected?: boolean,
+    toggle: () => void
+}
+
+export class ToggleButton extends React.PureComponent<ToggleButtonProps> {
+    onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
+        e.currentTarget.blur();
+        this.props.toggle();
+    }
+
+    render() {
+        const props = this.props;
+        const label = props.label;
+        return <button onClick={this.onClick} title={this.props.title}
+            disabled={props.disabled} style={props.style} className={props.className}>
+            {this.props.isSelected ? <b>{label}</b> : label}
+        </button>;
+    }
+}

+ 191 - 63
src/mol-plugin-ui/controls/parameters.tsx

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -8,27 +8,33 @@
 import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { Color } from '../../mol-util/color';
 import { ColorListName, getColorListFromName } from '../../mol-util/color/lists';
-import { memoize1 } from '../../mol-util/memoize';
+import { memoize1, memoizeLatest } from '../../mol-util/memoize';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { camelCaseToWords } from '../../mol-util/string';
 import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
-import { NumericInput, IconButton, ControlGroup } from './common';
-import { _Props, _State } from '../base';
+import { NumericInput, IconButton, ControlGroup, ToggleButton } from './common';
+import { _Props, _State, PluginUIComponent } from '../base';
 import { legendFor } from './legend';
 import { Legend as LegendData } from '../../mol-util/legend';
 import { CombinedColorControl, ColorValueOption, ColorOptions } from './color';
+import { getPrecision } from '../../mol-util/number';
+import { ParamMapping } from '../../mol-util/param-mapping';
+import { PluginContext } from '../../mol-plugin/context';
+import { ActionMenu } from './action-menu';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
-    onChange: ParamOnChange,
+    onChange: ParamsOnChange<PD.Values<P>>,
     isDisabled?: boolean,
     onEnter?: () => void
 }
 
 export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
+    onChange: ParamOnChange = (params) => this.props.onChange(params, this.props.values);
+
     render() {
         const params = this.props.params;
         const values = this.props.values;
@@ -40,12 +46,31 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
                 if (param.isHidden) return null;
                 const Control = controlFor(param);
                 if (!Control) return null;
-                return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
+                return <Control param={param} key={key} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
             })}
         </>;
     }
 }
 
+export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping: ParamMapping<S, T, PluginContext> }> {
+    setSettings = (p: { param: PD.Base<any>, name: string, value: any }, old: any) => {
+        const values = { ...old, [p.name]: p.value };
+        const t = this.props.mapping.update(values, this.plugin);
+        this.props.mapping.apply(t, this.plugin);
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+    }
+
+    render() {
+        const t = this.props.mapping.getTarget(this.plugin);
+        const values = this.props.mapping.getValues(t, this.plugin);
+        const params = this.props.mapping.params(this.plugin) as any as PD.Params;
+        return <ParameterControls params={params} values={values} onChange={this.setSettings} />
+    }
+}
+
 function controlFor(param: PD.Any): ParamControl | undefined {
     switch (param.type) {
         case 'value': return void 0;
@@ -90,6 +115,7 @@ export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legen
     }
 }
 
+export type ParamsOnChange<P> = (params: { param: PD.Base<any>, name: string, value: any }, values: Readonly<P>) => void
 export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
 export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
     name: string,
@@ -101,51 +127,63 @@ export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
 }
 export type ParamControl = React.ComponentClass<ParamProps<any>>
 
-export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { isExpanded: boolean }> {
-    state = { isExpanded: false };
+function renderSimple(options: { props: ParamProps<any>, state: { showHelp: boolean }, control: JSX.Element, addOn: JSX.Element | null, toggleHelp: () => void }) {
+    const { props, state, control, toggleHelp, addOn } = options;
+
+    const _className = ['msp-control-row'];
+    if (props.param.shortLabel) _className.push('msp-control-label-short')
+    if (props.param.twoColumns) _className.push('msp-control-col-2')
+    const className = _className.join(' ');
+
+    const label = props.param.label || camelCaseToWords(props.name);
+    const help = props.param.help
+        ? props.param.help(props.value)
+        : { description: props.param.description, legend: props.param.legend }
+    const desc = props.param.description;
+    const hasHelp = help.description || help.legend
+    return <>
+        <div className={className}>
+            <span title={desc}>
+                {label}
+                {hasHelp &&
+                    <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={toggleHelp}
+                        title={desc || `${state.showHelp ? 'Hide' : 'Show'} help`}
+                        style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
+                        <span className={`msp-icon msp-icon-help-circle-${state.showHelp ? 'collapse' : 'expand'}`} />
+                    </button>
+                }
+            </span>
+            <div>
+                {control}
+            </div>
+        </div>
+        {hasHelp && state.showHelp && <div className='msp-control-offset'>
+            <ParamHelp legend={help.legend} description={help.description} />
+        </div>}
+        {addOn}
+    </>;
+}
+
+export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { showHelp: boolean }> {
+    state = { showHelp: false };
 
     protected update(value: P['defaultValue']) {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
     }
 
     abstract renderControl(): JSX.Element;
+    renderAddOn(): JSX.Element | null { return null; }
 
-    private get className() {
-        const className = ['msp-control-row'];
-        if (this.props.param.shortLabel) className.push('msp-control-label-short')
-        if (this.props.param.twoColumns) className.push('msp-control-col-2')
-        return className.join(' ')
-    }
-
-    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+    toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
 
     render() {
-        const label = this.props.param.label || camelCaseToWords(this.props.name);
-        const help = this.props.param.help
-            ? this.props.param.help(this.props.value)
-            : { description: this.props.param.description, legend: this.props.param.legend }
-        const desc = this.props.param.description;
-        const hasHelp = help.description || help.legend
-        return <>
-            <div className={this.className}>
-                <span title={desc}>
-                    {label}
-                    {hasHelp &&
-                        <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={this.toggleExpanded}
-                            title={desc || `${this.state.isExpanded ? 'Hide' : 'Show'} help`}
-                            style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
-                            <span className={`msp-icon msp-icon-help-circle-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
-                        </button>
-                    }
-                </span>
-                <div>
-                    {this.renderControl()}
-                </div>
-            </div>
-            {hasHelp && this.state.isExpanded && <div className='msp-control-offset'>
-                <ParamHelp legend={help.legend} description={help.description} />
-            </div>}
-        </>;
+        return renderSimple({
+            props: this.props,
+            state: this.state,
+            control: this.renderControl(),
+            toggleHelp: this.toggleHelp,
+            addOn: this.renderAddOn()
+        });
     }
 }
 
@@ -214,17 +252,20 @@ export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeri
     state = { value: '0' };
 
     update = (value: number) => {
+        const p = getPrecision(this.props.param.step || 0.01)
+        value = parseFloat(value.toFixed(p))
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
     }
 
     render() {
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
         const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const p = getPrecision(this.props.param.step || 0.01)
         return <div className='msp-control-row'>
             <span title={this.props.param.description}>{label}</span>
             <div>
                 <NumericInput
-                    value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder}
+                    value={parseFloat(this.props.value.toFixed(p))} onEnter={this.props.onEnter} placeholder={placeholder}
                     isDisabled={this.props.isDisabled} onChange={this.update} />
             </div>
         </div>;
@@ -289,27 +330,97 @@ export class PureSelectControl extends  React.PureComponent<ParamProps<PD.Select
     }
 }
 
-export class SelectControl extends SimpleParam<PD.Select<string | number>> {
-    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-        if (typeof this.props.param.defaultValue === 'number') {
-            this.update(parseInt(e.target.value, 10));
+export class SelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>>, { showHelp: boolean, showOptions: boolean }> {
+    state = { showHelp: false, showOptions: false };
+
+    onSelect: ActionMenu.OnSelect = item => {
+        if (!item || item.value === this.props.value) {
+            this.setState({ showOptions: false });
         } else {
-            this.update(e.target.value);
+            this.setState({ showOptions: false }, () => {
+                this.props.onChange({ param: this.props.param, name: this.props.name, value: item.value });
+            });
         }
     }
+
+    toggle = () => this.setState({ showOptions: !this.state.showOptions });
+
+    items = memoizeLatest((param: PD.Select<any>) => ActionMenu.createItemsFromSelectParam(param));
+
     renderControl() {
-        const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
-        return <select value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
-            {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
-            {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
-        </select>;
+        const items = this.items(this.props.param);
+        const current = this.props.value !== undefined ? ActionMenu.findItem(items, this.props.value) : void 0;
+        const label = current
+            ? current.label
+            : typeof this.props.value === 'undefined'
+            ? `${ActionMenu.getFirstItem(items)?.label || ''} [Default]`
+            : `[Invalid] ${this.props.value}`;
+        
+        return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }}
+            label={label} title={label as string} toggle={this.toggle} isSelected={this.state.showOptions} />;
+    }
+
+    renderAddOn() {
+        if (!this.state.showOptions) return null;
+
+        const items = this.items(this.props.param);
+        const current = ActionMenu.findItem(items, this.props.value);
+
+        return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
+    }
+
+    toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
+
+    render() {
+        return renderSimple({
+            props: this.props,
+            state: this.state,
+            control: this.renderControl(),
+            toggleHelp: this.toggleHelp,
+            addOn: this.renderAddOn()
+        });
     }
 }
 
-export class IntervalControl extends SimpleParam<PD.Interval> {
-    onChange = (v: [number, number]) => { this.update(v); }
-    renderControl() {
-        return <span>interval TODO</span>;
+export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> {
+    state = { isExpanded: false }
+
+    components = {
+        0: PD.Numeric(0, { step: this.props.param.step }, { label: 'Min' }),
+        1: PD.Numeric(0, { step: this.props.param.step }, { label: 'Max' })
+    }
+
+    change(value: PD.MultiSelect<any>['defaultValue']) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    componentChange: ParamOnChange = ({ name, value }) => {
+        const v = [...this.props.value];
+        v[+name] = value;
+        this.change(v);
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    render() {
+        const v = this.props.value;
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const p = getPrecision(this.props.param.step || 0.01)
+        const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}]`;
+        return <>
+            <div className='msp-control-row'>
+                <span>{label}</span>
+                <div>
+                    <button onClick={this.toggleExpanded}>{value}</button>
+                </div>
+            </div>
+            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
+            </div>
+        </>;
     }
 }
 
@@ -399,9 +510,9 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
     state = { isExpanded: false }
 
     components = {
-        0: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
-        1: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
-        2: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
+        0: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
+        1: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
+        2: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
     }
 
     change(value: PD.MultiSelect<any>['defaultValue']) {
@@ -422,7 +533,8 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
     render() {
         const v = this.props.value;
         const label = this.props.param.label || camelCaseToWords(this.props.name);
-        const value = `[${v[0].toFixed(2)}, ${v[1].toFixed(2)}, ${v[2].toFixed(2)}]`;
+        const p = getPrecision(this.props.param.step || 0.01)
+        const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}, ${v[2].toFixed(p)}]`;
         return <>
             <div className='msp-control-row'>
                 <span>{label}</span>
@@ -529,7 +641,7 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
     }
 }
 
-export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> {
+export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>> & { inMapped?: boolean }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.param.isExpanded }
 
     change(value: any) {
@@ -552,6 +664,10 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
 
         const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
 
+        if (this.props.inMapped) {
+            return <div className='msp-control-offset'>{controls}</div>;
+        }
+
         if (this.props.param.isFlat) {
             return controls;
         }
@@ -570,7 +686,9 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
     }
 }
 
-export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
+export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>, { isExpanded: boolean }> {
+    state = { isExpanded: false }
+
     private valuesCache: { [name: string]: PD.Values<any> } = {}
     private setValues(name: string, values: PD.Values<any>) {
         this.valuesCache[name] = values
@@ -596,6 +714,8 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
         this.change({ name: this.props.value.name, params: e.value });
     }
 
+    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+
     render() {
         const value: PD.Mapped<any>['defaultValue'] = this.props.value;
         const param = this.props.param.map(value.name);
@@ -618,6 +738,14 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
             return Select;
         }
 
+        if (param.type === 'group' && !param.isFlat && Object.keys(param.params).length > 0) {
+            return <div className='msp-mapped-parameter-group'>
+                {Select}
+                <IconButton icon='log' onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} />
+                {this.state.isExpanded && <GroupControl inMapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />}
+            </div>
+        }
+
         return <>
             {Select}
             <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />

+ 1 - 0
src/mol-plugin-ui/skin/base/components/controls.scss

@@ -31,6 +31,7 @@
         overflow: hidden;
         text-overflow: ellipsis;
         white-space: nowrap;
+        position: relative;
 
         @include non-selectable;
     }

+ 17 - 0
src/mol-plugin-ui/skin/base/components/misc.scss

@@ -166,4 +166,21 @@
     .msp-scrollable-container {
         left: $row-height + 1px;
     }
+}
+
+.msp-mapped-parameter-group {
+    position: relative;
+
+    > .msp-control-row:first-child {
+        > div:nth-child(2) {
+            right: 33px;
+        }
+    }
+
+    > .msp-btn-icon {
+        position: absolute;
+        right: 0;
+        width: 32px;
+        top: 0;
+    }
 }

+ 31 - 0
src/mol-plugin-ui/skin/base/components/temp.scss

@@ -66,6 +66,7 @@
         text-align-last: center;
         background: none;
         padding: 0 $control-spacing;
+        overflow: hidden;
 
         > option[value = _] {
             display: none;
@@ -274,4 +275,34 @@
     .msp-transform-wrapper:last-child {
         margin-bottom: 10px;
     }
+}
+
+.msp-button-row {
+    display:flex;
+    flex-direction:row;
+    height: $row-height;
+    width: inherit;
+
+    > button {
+        margin: 0;
+        flex: 1 1 auto;
+        margin-right: 1px;
+        height: $row-height;
+
+        text-align-last: center;
+        background: none;
+        padding: 0 $control-spacing;
+        overflow: hidden;
+    }
+}
+
+.msp-action-menu-options {
+    .msp-control-row, button, .msp-icon {
+        height: 24px;
+        line-height: 24px;
+    }
+
+    button {
+        text-align: left;
+    }
 }

+ 1 - 18
src/mol-plugin-ui/structure/representation.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>
  */
@@ -13,8 +13,6 @@ import { Color } from '../../mol-util/color';
 import { ButtonSelect, Options } from '../controls/common'
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { VisualQuality, VisualQualityOptions } from '../../mol-geo/geometry/base';
-import { StructureRepresentationPresets as P } from '../../mol-plugin/util/structure-representation-helper';
-import { camelCaseToWords } from '../../mol-util/string';
 import { CollapsableControls } from '../base';
 import { StateSelection, StateObject } from '../../mol-state';
 import { PluginStateObject } from '../../mol-plugin/state/objects';
@@ -133,13 +131,6 @@ export class StructureRepresentationControls extends CollapsableControls<Collaps
         this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v }))
     }
 
-    preset = async (value: string) => {
-        const presetFn = P[value as keyof typeof P]
-        if (presetFn) {
-            await presetFn(this.plugin.helpers.structureRepresentation)
-        }
-    }
-
     onChange = async (p: { param: PD.Base<any>, name: string, value: any }) => {
         if (p.name === 'options') {
             await this.plugin.helpers.structureRepresentation.setIgnoreHydrogens(!p.value.showHydrogens)
@@ -178,15 +169,7 @@ export class StructureRepresentationControls extends CollapsableControls<Collaps
     }
 
     renderControls() {
-        const presets = PD.objectToOptions(P, camelCaseToWords);
         return <div>
-            <div className='msp-control-row'>
-                <div className='msp-select-row'>
-                    <ButtonSelect label='Preset' onChange={this.preset}>
-                        <optgroup label='Preset'>{Options(presets)}</optgroup>
-                    </ButtonSelect>
-                </div>
-            </div>
             <EverythingStructureRepresentationControls />
             <SelectionStructureRepresentationControls />
 

+ 51 - 44
src/mol-plugin-ui/structure/selection.tsx

@@ -1,28 +1,26 @@
 /**
- * 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>
+ * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import * as React from 'react';
 import { CollapsableControls, CollapsableState } from '../base';
-import { StructureSelectionQueries, SelectionModifier } from '../../mol-plugin/util/structure-selection-helper';
-import { ButtonSelect, Options } from '../controls/common';
+import { StructureSelectionQuery, SelectionModifier, StructureSelectionQueryList } from '../../mol-plugin/util/structure-selection-helper';
 import { PluginCommands } from '../../mol-plugin/command';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Interactivity } from '../../mol-plugin/util/interactivity';
 import { ParameterControls } from '../controls/parameters';
 import { stripTags } from '../../mol-util/string';
 import { StructureElement } from '../../mol-model/structure';
+import { ActionMenu } from '../controls/action-menu';
+import { ToggleButton } from '../controls/common';
 
-const SSQ = StructureSelectionQueries
-const DefaultQueries: (keyof typeof SSQ)[] = [
-    'all', 'polymer', 'trace', 'backbone', 'protein', 'nucleic',
-    'helix', 'beta',
-    'water', 'branched', 'ligand', 'nonStandardPolymer',
-    'ring', 'aromaticRing',
-    'surroundings', 'complement', 'bonded'
-]
+export const DefaultQueries = ActionMenu.createItems(StructureSelectionQueryList, {
+    label: q => q.label,
+    category: q => q.category
+});
 
 const StructureSelectionParams = {
     granularity: Interactivity.Params.granularity,
@@ -33,7 +31,9 @@ interface StructureSelectionControlsState extends CollapsableState {
     extraRadius: number,
     durationMs: number,
 
-    isDisabled: boolean
+    isDisabled: boolean,
+
+    queryAction?: SelectionModifier
 }
 
 export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
@@ -46,7 +46,9 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             this.forceUpdate()
         });
 
-        this.subscribe(this.plugin.state.dataState.events.isUpdating, v => this.setState({ isDisabled: v }))
+        this.subscribe(this.plugin.state.dataState.events.isUpdating, v => {
+            this.setState({ isDisabled: v, queryAction: void 0 })
+        })
     }
 
     get stats() {
@@ -115,38 +117,41 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         }
     }
 
-    set = (modifier: SelectionModifier, value: string) => {
-        const query = SSQ[value as keyof typeof SSQ]
-        this.plugin.helpers.structureSelection.set(modifier, query.query, false)
-    }
-
-    add = (value: string) => this.set('add', value)
-    remove = (value: string) => this.set('remove', value)
-    only = (value: string) => this.set('only', value)
-
-    queries = Options(Object.keys(StructureSelectionQueries)
-            .map(name => [name, SSQ[name as keyof typeof SSQ].label] as [string, string])
-            .filter(pair => DefaultQueries.includes(pair[0] as keyof typeof SSQ)));
-
-    controls = <div className='msp-control-row'>
-        <div className='msp-select-row'>
-            <ButtonSelect label='Select' onChange={this.add} disabled={this.state.isDisabled}>
-                <optgroup label='Select'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
-            <ButtonSelect label='Deselect' onChange={this.remove} disabled={this.state.isDisabled}>
-                <optgroup label='Deselect'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
-            <ButtonSelect label='Only' onChange={this.only} disabled={this.state.isDisabled}>
-                <optgroup label='Only'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
+    set = (modifier: SelectionModifier, selectionQuery: StructureSelectionQuery) => {
+        this.plugin.helpers.structureSelection.set(modifier, selectionQuery, false)
+    }
+
+    selectQuery: ActionMenu.OnSelect = item => {
+        if (!item || !this.state.queryAction) {
+            this.setState({ queryAction: void 0 });
+            return;
+        }
+        const q = this.state.queryAction!;
+        this.setState({ queryAction: void 0 }, () => {
+            this.set(q, item.value as StructureSelectionQuery);
+        })
+    }
+
+    queries = DefaultQueries
+
+    private showQueries(q: SelectionModifier) {
+        return () => this.setState({ queryAction: this.state.queryAction === q ? void 0 : q });
+    }
+
+    toggleAdd = this.showQueries('add')
+    toggleRemove = this.showQueries('remove')
+    toggleOnly = this.showQueries('only')
+
+    get controls() {
+        return <div>
+            <div className='msp-control-row msp-button-row'>
+                <ToggleButton label='Select' toggle={this.toggleAdd} isSelected={this.state.queryAction === 'add'} disabled={this.state.isDisabled} />
+                <ToggleButton label='Deselect' toggle={this.toggleRemove} isSelected={this.state.queryAction === 'remove'} disabled={this.state.isDisabled} />
+                <ToggleButton label='Only' toggle={this.toggleOnly} isSelected={this.state.queryAction === 'only'} disabled={this.state.isDisabled} />
+            </div>
+            {this.state.queryAction && <ActionMenu items={this.queries} onSelect={this.selectQuery} />}
         </div>
-    </div>
+    }
 
     defaultState() {
         return {
@@ -157,6 +162,8 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             extraRadius: 4,
             durationMs: 250,
 
+            queryAction: void 0,
+
             isDisabled: false
         } as S
     }

+ 55 - 90
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -9,10 +9,23 @@ import * as React from 'react';
 import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
 import { PluginCommands } from '../../mol-plugin/command';
 import { ColorNames } from '../../mol-util/color/names';
-import { ParameterControls } from '../controls/parameters';
+import { ParameterMappingControl } from '../controls/parameters';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { PluginUIComponent } from '../base';
 import { Color } from '../../mol-util/color';
+import { ParamMapping } from '../../mol-util/param-mapping';
+import { PluginContext } from '../../mol-plugin/context';
+
+export class SimpleSettingsControl extends PluginUIComponent {
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+    }
+
+    render() {
+        if (!this.plugin.canvas3d) return null;
+        return <ParameterMappingControl mapping={SimpleSettingsMapping} />
+    }
+}
 
 const SimpleSettingsParams = {
     spin: Canvas3DParams.trackball.params.spin,
@@ -21,80 +34,21 @@ const SimpleSettingsParams = {
         'transparent': PD.EmptyGroup(),
         'opaque': PD.Group({ color: PD.Color(Color(0xFCFBF9), { description: 'Custom background color' }) }, { isFlat: true })
     }, { description: 'Background of the 3D canvas' }),
-    renderStyle: PD.Select('glossy', [['flat', 'Flat'], ['matte', 'Matte'], ['glossy', 'Glossy'], ['metallic', 'Metallic']], { description: 'Style in which the 3D scene is rendered' }),
+    renderStyle: PD.Select('glossy', PD.arrayToOptions(['flat', 'matte', 'glossy', 'metallic']), { description: 'Style in which the 3D scene is rendered' }),
     occlusion: PD.Boolean(false, { description: 'Darken occluded crevices with the ambient occlusion effect' }),
     outline: PD.Boolean(false, { description: 'Draw outline around 3D objects' }),
     fog: PD.Boolean(false, { description: 'Show fog in the distance' }),
     clipFar: PD.Boolean(true, { description: 'Clip scene in the distance' }),
 };
 
-export class SimpleSettingsControl extends PluginUIComponent {
-    setSettings = (p: { param: PD.Base<any>, name: keyof typeof SimpleSettingsParams | string, value: any }) => {
-        if (p.name === 'spin') {
-            if (!this.plugin.canvas3d) return;
-            const trackball = this.plugin.canvas3d.props.trackball;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: p.value } } });
-        } else if (p.name === 'camera') {
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { cameraMode: p.value }});
-        } else if (p.name === 'background') {
-            if (!this.plugin.canvas3d) return;
-            const renderer = this.plugin.canvas3d.props.renderer;
-            const color: typeof SimpleSettingsParams['background']['defaultValue'] = p.value;
-            if (color.name === 'transparent') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: ColorNames.white }, transparentBackground: true } });
-            } else {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: color.params.color }, transparentBackground: false } });
-            }
-        } else if (p.name === 'renderStyle') {
-            if (!this.plugin.canvas3d) return;
-
-            const renderer = this.plugin.canvas3d.props.renderer;
-            if (p.value === 'flat') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }
-                } });
-            } else if (p.value === 'matte') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }
-                } });
-            } else if (p.value === 'glossy') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 }
-                } });
-            } else if (p.value === 'metallic') {
-                PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                    renderer: { ...renderer, lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }
-                } });
-            }
-        } else if (p.name === 'occlusion') {
-            if (!this.plugin.canvas3d) return;
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                postprocessing: { ...postprocessing, occlusionEnable: p.value, occlusionBias: 0.5, occlusionRadius: 64 },
-            } });
-        } else if (p.name === 'outline') {
-            if (!this.plugin.canvas3d) return;
-            const postprocessing = this.plugin.canvas3d.props.postprocessing;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                postprocessing: { ...postprocessing, outlineEnable: p.value },
-            } });
-        } else if (p.name === 'fog') {;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                cameraFog: p.value ? 50 : 0,
-            } });
-        } else if (p.name === 'clipFar') {;
-            PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: {
-                cameraClipFar: p.value,
-            } });
-        }
-    }
-
-    get values () {
-        const renderer = this.plugin.canvas3d?.props.renderer;
-
-        let renderStyle = 'custom'
-        let background: typeof SimpleSettingsParams['background']['defaultValue'] = { name: 'transparent', params: { } }
+type SimpleSettingsParams = typeof SimpleSettingsParams
+const SimpleSettingsMapping = ParamMapping({
+    params: SimpleSettingsParams,
+    target(ctx: PluginContext) { return ctx.canvas3d?.props!; } })({
+    values(t, ctx) {
+        const renderer = t.renderer;
 
+        let renderStyle: SimpleSettingsParams['renderStyle']['defaultValue'] = 'custom' as any;
         if (renderer) {
             if (renderer.lightIntensity === 0 && renderer.ambientIntensity === 1 && renderer.roughness === 0.4 && renderer.metalness === 0) {
                 renderStyle = 'flat'
@@ -107,31 +61,42 @@ export class SimpleSettingsControl extends PluginUIComponent {
                     renderStyle = 'metallic'
                 }
             }
-
-            if (renderer.backgroundColor === ColorNames.white && this.plugin.canvas3d?.props.transparentBackground) {
-                background = { name: 'transparent', params: { } }
-            } else {
-                background = { name: 'opaque', params: { color: renderer.backgroundColor } }
-            }
         }
 
         return {
-            spin: !!this.plugin.canvas3d?.props.trackball.spin,
-            camera: this.plugin.canvas3d?.props.cameraMode,
-            background,
+            spin: !!t.trackball.spin,
+            camera: t.cameraMode,
+            background:  (renderer.backgroundColor === ColorNames.white && t.transparentBackground) 
+                ? { name: 'transparent', params: { } }
+                : { name: 'opaque', params: { color: renderer.backgroundColor } },
             renderStyle,
-            occlusion: this.plugin.canvas3d?.props.postprocessing.occlusionEnable,
-            outline: this.plugin.canvas3d?.props.postprocessing.outlineEnable,
-            fog: this.plugin.canvas3d ? this.plugin.canvas3d.props.cameraFog > 1 : false,
-            clipFar: this.plugin.canvas3d?.props.cameraClipFar
+            occlusion: t.postprocessing.occlusionEnable,
+            outline: t.postprocessing.outlineEnable,
+            fog: ctx.canvas3d ? t.cameraFog > 1 : false,
+            clipFar: t.cameraClipFar
+        };
+    },
+    update(s, t) {
+        t.trackball.spin = s.spin;
+        t.cameraMode = s.camera;
+        t.transparentBackground = s.background.name === 'transparent';
+        t.renderer.backgroundColor = s.background.name === 'transparent' ? ColorNames.white : s.background.params.color;
+        switch (s.renderStyle) {
+            case 'flat': Object.assign(t.renderer, { lightIntensity: 0, ambientIntensity: 1, roughness: 0.4, metalness: 0 }); break;
+            case 'matte':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 1, metalness: 0 }); break;
+            case 'glossy':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.4, metalness: 0 }); break;
+            case 'metallic':  Object.assign(t.renderer, { lightIntensity: 0.6, ambientIntensity: 0.4, roughness: 0.6, metalness: 0.4 }); break;
         }
+        t.postprocessing.occlusionEnable = s.occlusion;
+        if (s.occlusion) { 
+            t.postprocessing.occlusionBias = 0.5;
+            t.postprocessing.occlusionRadius = 64;
+        }
+        t.postprocessing.outlineEnable = s.outline;
+        t.cameraFog = s.fog ? 50 : 0;
+        t.cameraClipFar = s.clipFar;
+    },
+    apply(settings, ctx) {
+        return PluginCommands.Canvas3D.SetSettings.dispatch(ctx, { settings });
     }
-
-    componentDidMount() {
-        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
-    }
-
-    render() {
-        return <ParameterControls params={SimpleSettingsParams} values={this.values} onChange={this.setSettings} />
-    }
-}
+})

+ 2 - 0
src/mol-plugin/behavior/dynamic/custom-props.ts

@@ -10,6 +10,8 @@ export { Interactions } from './custom-props/computed/interactions'
 export { SecondaryStructure } from './custom-props/computed/secondary-structure'
 export { ValenceModel } from './custom-props/computed/valence-model'
 
+export { CrossLinkRestraint } from './custom-props/integrative/cross-link-restraint'
+
 export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report'
 export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'
 export { RCSBValidationReport } from './custom-props/rcsb/validation-report'

+ 6 - 0
src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts

@@ -11,6 +11,7 @@ import { Loci } from '../../../../../mol-model/loci';
 import { AccessibleSurfaceAreaColorThemeProvider } from '../../../../../mol-model-props/computed/themes/accessible-surface-area';
 import { OrderedSet } from '../../../../../mol-data/int';
 import { arraySum } from '../../../../../mol-util/array';
+import { DefaultQueryRuntimeTable } from '../../../../../mol-script/runtime/query/compiler';
 
 export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
     name: 'computed-accessible-surface-area-prop',
@@ -36,12 +37,17 @@ export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean
         }
 
         register(): void {
+            DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
+
             this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('accessible-surface-area', AccessibleSurfaceAreaColorThemeProvider)
             this.ctx.lociLabels.addProvider(this.label);
         }
 
         unregister() {
+            // TODO
+            // DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
+
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('accessible-surface-area')
             this.ctx.lociLabels.removeProvider(this.label);

+ 45 - 0
src/mol-plugin/behavior/dynamic/custom-props/integrative/cross-link-restraint.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from '../../../behavior';
+import { ModelCrossLinkRestraint } from '../../../../../mol-model-props/integrative/cross-link-restraint/format';
+import { Model } from '../../../../../mol-model/structure';
+import { MmcifFormat } from '../../../../../mol-model-formats/structure/mmcif';
+import { CrossLinkRestraintRepresentationProvider } from '../../../../../mol-model-props/integrative/cross-link-restraint/representation';
+import { CrossLinkColorThemeProvider } from '../../../../../mol-model-props/integrative/cross-link-restraint/color';
+import { CrossLinkRestraint as _CrossLinkRestraint } from '../../../../../mol-model-props/integrative/cross-link-restraint/property';
+
+const Tag = _CrossLinkRestraint.Tag
+
+export const CrossLinkRestraint = PluginBehavior.create<{ }>({
+    name: 'integrative-cross-link-restraint',
+    category: 'custom-props',
+    display: { name: 'Cross Link Restraint' },
+    ctor: class extends PluginBehavior.Handler<{ }> {
+        private provider = ModelCrossLinkRestraint.Provider
+
+        register(): void {
+            this.provider.formatRegistry.add('mmCIF', crossLinkRestraintFromMmcif)
+
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.CrossLinkRestraint, CrossLinkColorThemeProvider)
+            this.ctx.structureRepresentation.registry.add(Tag.CrossLinkRestraint, CrossLinkRestraintRepresentationProvider)
+        }
+
+        unregister() {
+            this.provider.formatRegistry.remove('mmCIF')
+
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.CrossLinkRestraint)
+            this.ctx.structureRepresentation.registry.remove(Tag.CrossLinkRestraint)
+        }
+    }
+});
+
+function crossLinkRestraintFromMmcif(model: Model) {
+    if (!MmcifFormat.is(model.sourceData)) return;
+    const { ihm_cross_link_restraint } = model.sourceData.data.db;
+    if (ihm_cross_link_restraint._rowCount === 0) return;
+    return ModelCrossLinkRestraint.fromTable(ihm_cross_link_restraint, model)
+}

+ 6 - 3
src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts

@@ -15,7 +15,10 @@ import { PluginBehavior } from '../../../behavior';
 export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
     name: 'pdbe-structure-quality-report-prop',
     category: 'custom-props',
-    display: { name: 'PDBe Structure Quality Report' },
+    display: {
+        name: 'Structure Quality Report',
+        description: 'Data from wwPDB Validation Report, obtained via PDBe.'
+    },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
 
         private provider = StructureQualityReportProvider
@@ -32,8 +35,8 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
 
                     const se = StructureElement.Location.create(loci.structure, u, u.elements[OrderedSet.getAt(e.indices, 0)]);
                     const issues = StructureQualityReport.getIssues(se);
-                    if (issues.length === 0) return 'PDBe Validation: No Issues';
-                    return `PDBe Validation: ${issues.join(', ')}`;
+                    if (issues.length === 0) return 'Validation: No Issues';
+                    return `Validation: ${issues.join(', ')}`;
 
                 default: return void 0;
             }

+ 23 - 10
src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts

@@ -14,17 +14,22 @@ import { Task } from '../../../../../mol-task';
 import { PluginContext } from '../../../../context';
 import { StateTransformer, StateAction, StateObject } from '../../../../../mol-state';
 
+const Tag = AssemblySymmetry.Tag
+
 export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'rcsb-assembly-symmetry-prop',
     category: 'custom-props',
-    display: { name: 'RCSB Assembly Symmetry' },
+    display: {
+        name: 'Assembly Symmetry',
+        description: 'Assembly Symmetry data calculated with BioJava, obtained via RCSB PDB.'
+    },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private provider = AssemblySymmetryProvider
 
         register(): void {
             this.ctx.state.dataState.actions.add(InitAssemblySymmetry3D)
             this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
-            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('rcsb-assembly-symmetry-cluster', AssemblySymmetryClusterColorThemeProvider)
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.Cluster, AssemblySymmetryClusterColorThemeProvider)
         }
 
         update(p: { autoAttach: boolean }) {
@@ -37,7 +42,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
         unregister() {
             this.ctx.state.dataState.actions.remove(InitAssemblySymmetry3D)
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
-            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('rcsb-assembly-symmetry-cluster')
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.Cluster)
         }
     },
     params: () => ({
@@ -47,24 +52,32 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
 });
 
 const InitAssemblySymmetry3D = StateAction.build({
-    display: { name: 'RCSB Assembly Symmetry' },
+    display: {
+        name: 'Assembly Symmetry',
+        description: 'Initialize Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
+    },
     from: PluginStateObject.Molecule.Structure,
     isApplicable: (a) => AssemblySymmetry.isApplicable(a.data)
-})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init RCSB Assembly Symmetry', async ctx => {
+})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
     try {
         await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
     } catch(e) {
-        plugin.log.error(`RCSB Assembly Symmetry: ${e}`)
+        plugin.log.error(`Assembly Symmetry: ${e}`)
         return
     }
     const tree = state.build().to(ref).apply(AssemblySymmetry3D);
     await state.updateTree(tree).runInContext(ctx);
 }));
 
+export { AssemblySymmetry3D }
+
 type AssemblySymmetry3D = typeof AssemblySymmetry3D
 const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
-    name: 'rcsb-assembly-symmetry-3d',
-    display: 'RCSB Assembly Symmetry',
+    name: Tag.Representation,
+    display: {
+        name: 'Assembly Symmetry',
+        description: 'Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
+    },
     from: PluginStateObject.Molecule.Structure,
     to: PluginStateObject.Shape.Representation3D,
     params: (a) => {
@@ -78,7 +91,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
         return true;
     },
     apply({ a, params }, plugin: PluginContext) {
-        return Task.create('RCSB Assembly Symmetry', async ctx => {
+        return Task.create('Assembly Symmetry', async ctx => {
             await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value
             if (!assemblySymmetry || assemblySymmetry.length === 0) {
@@ -91,7 +104,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
         });
     },
     update({ a, b, newParams }, plugin: PluginContext) {
-        return Task.create('RCSB Assembly Symmetry', async ctx => {
+        return Task.create('Assembly Symmetry', async ctx => {
             await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value
             if (!assemblySymmetry || assemblySymmetry.length === 0) {

+ 11 - 2
src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts

@@ -14,13 +14,17 @@ import { OrderedSet } from '../../../../../mol-data/int';
 import { ClashesRepresentationProvider } from '../../../../../mol-model-props/rcsb/representations/validation-report-clashes';
 import { DensityFitColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/density-fit';
 import { cantorPairing } from '../../../../../mol-data/util';
+import { DefaultQueryRuntimeTable } from '../../../../../mol-script/runtime/query/compiler';
 
 const Tag = ValidationReport.Tag
 
 export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
     name: 'rcsb-validation-report-prop',
     category: 'custom-props',
-    display: { name: 'RCSB Validation Report' },
+    display: {
+        name: 'Validation Report',
+        description: 'Data from wwPDB Validation Report, obtained via RCSB PDB.'
+    },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
         private provider = ValidationReportProvider
 
@@ -34,6 +38,8 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
         }
 
         register(): void {
+            DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
+
             this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
 
             this.ctx.lociLabels.addProvider(this.label);
@@ -54,6 +60,9 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
         }
 
         unregister() {
+            // TODO
+            // DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
+
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
 
             this.ctx.lociLabels.removeProvider(this.label);
@@ -93,7 +102,7 @@ function geometryQualityLabel(loci: Loci): string | undefined {
             if (angles) angles.forEach(a => issues.add(angleOutliers.data[a].tag))
 
             if (issues.size === 0) {
-                return `RCSB Geometry Quality <small>(1 Atom)</small>: no issues`;
+                return `Geometry Quality <small>(1 Atom)</small>: no issues`;
             }
 
             const summary: string[] = []

+ 2 - 2
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -93,8 +93,8 @@ export namespace VolumeStreaming {
                 }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
                 'selection-box': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
-                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
-                    topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
+                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
+                    topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                 }, { description: 'Box around last-interacted element.', isFlat: true }),
                 'cell': PD.Group({}),
                 // 'auto': PD.Group({  }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.

+ 2 - 2
src/mol-plugin/behavior/static/camera.ts

@@ -15,8 +15,8 @@ export function registerDefault(ctx: PluginContext) {
 }
 
 export function Reset(ctx: PluginContext) {
-    PluginCommands.Camera.Reset.subscribe(ctx, () => {
-        ctx.canvas3d?.requestCameraReset();
+    PluginCommands.Camera.Reset.subscribe(ctx, options => {
+        ctx.canvas3d?.requestCameraReset(options);
     })
 }
 

+ 1 - 1
src/mol-plugin/command.ts

@@ -59,7 +59,7 @@ export const PluginCommands = {
         Hide: PluginCommand<{ key: string }>()
     },
     Camera: {
-        Reset: PluginCommand<{}>(),
+        Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(),
         SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(),
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string }>(),

+ 4 - 1
src/mol-plugin/index.ts

@@ -70,14 +70,17 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
+        PluginSpec.Behavior(StructureRepresentationInteraction),
+
         PluginSpec.Behavior(PluginBehaviors.CustomProps.AccessibleSurfaceArea),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.SecondaryStructure),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.ValenceModel),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.CrossLinkRestraint),
+
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true, showTooltip: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBValidationReport),
-        PluginSpec.Behavior(StructureRepresentationInteraction)
     ],
     customParamEditors: [
         [CreateVolumeStreamingBehavior, VolumeStreamingCustomControls]

+ 48 - 6
src/mol-plugin/state/representation/model.ts

@@ -1,12 +1,13 @@
 /**
- * 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 David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Model, Structure, StructureSymmetry } from '../../../mol-model/structure';
 import { stringToWords } from '../../../mol-util/string';
-import { SpacegroupCell } from '../../../mol-math/geometry';
+import { SpacegroupCell, Spacegroup } from '../../../mol-math/geometry';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { RuntimeContext } from '../../../mol-task';
@@ -16,12 +17,28 @@ import { PluginStateObject as SO } from '../objects';
 import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
 
 export namespace ModelStructureRepresentation {
-    export function getParams(model?: Model, defaultValue?: 'deposited' | 'assembly' | 'symmetry' | 'symmetry-mates') {
+    export function getParams(model?: Model, defaultValue?: 'deposited' | 'assembly' | 'symmetry' | 'symmetry-mates' | 'symmetry-assembly') {
         const symmetry = model && ModelSymmetry.Provider.get(model)
 
         const assemblyIds = symmetry ? symmetry.assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]) : [];
         const showSymm = !symmetry ? true : !SpacegroupCell.isZero(symmetry.spacegroup.cell);
 
+        const operatorOptions: [number, string][] = []
+        if (symmetry) {
+            const { operators } = symmetry.spacegroup
+            for (let i = 0, il = operators.length; i < il; i++) {
+                operatorOptions.push([i, `${i + 1}: ${Spacegroup.getOperatorXyz(operators[i])}`])
+            }
+        }
+
+        const asymIdsOptions: [string, string][] = []
+        if (model) {
+            model.properties.structAsymMap.forEach(v => {
+                const label = v.id === v.auth_id ? v.id : `${v.id} [auth ${v.auth_id}]`
+                asymIdsOptions.push([v.id, label])
+            })
+        }
+
         const modes = {
             deposited: PD.EmptyGroup(),
             assembly: PD.Group({
@@ -33,8 +50,21 @@ export namespace ModelStructureRepresentation {
                 radius: PD.Numeric(5)
             }, { isFlat: true }),
             'symmetry': PD.Group({
-                ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
-                ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+                ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { step: 1 }, { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
+                ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { step: 1 }, { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+            }, { isFlat: true }),
+            'symmetry-assembly': PD.Group({
+                generators: PD.ObjectList({
+                    operators: PD.ObjectList({
+                        index: PD.Select(0, operatorOptions),
+                        shift: PD.Vec3(Vec3(), { step: 1 }, { label: 'IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+                    }, e => `${e.index + 1}_${e.shift.map(a => a + 5).join('')}`, {
+                        defaultValue: [] as { index: number, shift: Vec3 }[]
+                    }),
+                    asymIds: PD.MultiSelect([] as string[], asymIdsOptions)
+                }, e => `${e.asymIds.length} asym ids, ${e.operators.length} operators`, {
+                    defaultValue: [] as { operators: { index: number, shift: Vec3 }[], asymIds: string[] }[]
+                })
             }, { isFlat: true })
         };
 
@@ -49,6 +79,7 @@ export namespace ModelStructureRepresentation {
         if (showSymm) {
             options.push(['symmetry-mates', 'Symmetry Mates']);
             options.push(['symmetry', 'Symmetry (indices)']);
+            options.push(['symmetry-assembly', 'Symmetry (assembly)']);
         }
 
         return {
@@ -105,8 +136,16 @@ export namespace ModelStructureRepresentation {
         return new SO.Molecule.Structure(s, props);
     }
 
+    async function buildSymmetryAssembly(ctx: RuntimeContext, model: Model, generators: StructureSymmetry.Generators, symmetry: Symmetry) {
+        const base = Structure.ofModel(model);
+        const s = await StructureSymmetry.buildSymmetryAssembly(base, generators, symmetry).runInContext(ctx);
+        const props = { label: `Symmetry Assembly`, description: Structure.elementDescription(s) };
+        return new SO.Molecule.Structure(s, props);
+    }
+
     export async function create(plugin: PluginContext, ctx: RuntimeContext, model: Model, params?: Params): Promise<SO.Molecule.Structure> {
-        if (!params || params.name === 'deposited') {
+        const symmetry = ModelSymmetry.Provider.get(model)
+        if (!symmetry || !params || params.name === 'deposited') {
             const s = Structure.ofModel(model);
             return new SO.Molecule.Structure(s, { label: 'Deposited', description: Structure.elementDescription(s) });
         }
@@ -119,6 +158,9 @@ export namespace ModelStructureRepresentation {
         if (params.name === 'symmetry-mates') {
             return buildSymmetryMates(ctx, model, params.params.radius)
         }
+        if (params.name === 'symmetry-assembly') {
+            return buildSymmetryAssembly(ctx, model, params.params.generators, symmetry)
+        }
 
         throw new Error(`Unknown represetation type: ${(params as any).name}`);
     }

+ 1 - 1
src/mol-plugin/state/representation/structure/preset.ts

@@ -52,7 +52,7 @@ const defaultPreset = StructureRepresentationProvider({
         const ligandRepr = ligand.applyOrUpdateTagged(reprTags, StateTransforms.Representation.StructureRepresentation3D,
             StructureRepresentation3DHelpers.getDefaultParams(plugin, 'ball-and-stick', structure));
 
-        applyComplex(root, 'modified')
+        applyComplex(root, 'non-standard')
             .applyOrUpdateTagged(reprTags, StateTransforms.Representation.StructureRepresentation3D,
                 StructureRepresentation3DHelpers.getDefaultParamsWithTheme(plugin, 'ball-and-stick', 'polymer-id', structure, void 0));
 

+ 4 - 33
src/mol-plugin/state/transforms/model.ts

@@ -16,7 +16,6 @@ import Expression from '../../../mol-script/language/expression';
 import { StateObject, StateTransformer } from '../../../mol-state';
 import { RuntimeContext, Task } from '../../../mol-task';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { stringToWords } from '../../../mol-util/string';
 import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 import { trajectoryFromGRO } from '../../../mol-model-formats/structure/gro';
 import { parseGRO } from '../../../mol-io/reader/gro/parser';
@@ -32,7 +31,6 @@ import { parseDcd } from '../../../mol-io/reader/dcd/parser';
 import { coordinatesFromDcd } from '../../../mol-model-formats/structure/dcd';
 import { topologyFromPsf } from '../../../mol-model-formats/structure/psf';
 import { deepEqual } from '../../../mol-util';
-import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
 
 export { CoordinatesFromDcd };
 export { TopologyFromPsf };
@@ -45,7 +43,6 @@ export { TrajectoryFrom3DG };
 export { ModelFromTrajectory };
 export { StructureFromTrajectory };
 export { StructureFromModel };
-export { StructureAssemblyFromModel };
 export { TransformStructureConformation };
 export { TransformStructureConformationByMatrix };
 export { StructureSelectionFromExpression };
@@ -290,32 +287,6 @@ const StructureFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
-// TODO: deprecate this in favor of StructureFromModel
-type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
-const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
-    name: 'structure-assembly-from-model',
-    display: { name: 'Structure Assembly', description: 'Create a molecular structure assembly.' },
-    from: SO.Molecule.Model,
-    to: SO.Molecule.Structure,
-    params(a) {
-        if (!a) {
-            return { id: PD.Optional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. Value \'deposited\' can be used to specify deposited asymmetric unit.' })) };
-        }
-        const assemblies = ModelSymmetry.Provider.get(a.data)?.assemblies || []
-        const ids = assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]);
-        ids.push(['deposited', 'Deposited']);
-        return {
-            id: PD.Optional(PD.Select(ids[0][0], ids, { label: 'Asm Id', description: 'Assembly Id' }))
-        };
-    }
-})({
-    apply({ a, params }, plugin: PluginContext) {
-        return Task.create('Build Assembly', async ctx => {
-            return ModelStructureRepresentation.create(plugin, ctx, a.data, { name: 'assembly', params });
-        })
-    }
-});
-
 const _translation = Vec3(), _m = Mat4(), _n = Mat4();
 type TransformStructureConformation = typeof TransformStructureConformation
 const TransformStructureConformation = PluginStateTransform.BuiltIn({
@@ -643,7 +614,7 @@ export const StructureComplexElementTypes = {
 
     'branched': 'branched', // = carbs
     'ligand': 'ligand',
-    'modified': 'modified',
+    'non-standard': 'non-standard',
 
     'coarse': 'coarse',
 
@@ -678,7 +649,7 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
             case 'branched': query = StructureSelectionQueries.branchedPlusConnected.query; label = 'Branched'; break;
             case 'ligand': query = StructureSelectionQueries.ligandPlusConnected.query; label = 'Ligand'; break;
 
-            case 'modified': query = StructureSelectionQueries.modified.query; label = 'Modified'; break;
+            case 'non-standard': query = StructureSelectionQueries.nonStandardPolymer.query; label = 'Non-standard'; break;
 
             case 'coarse': query = StructureSelectionQueries.coarse.query; label = 'Coarse'; break;
 
@@ -719,7 +690,7 @@ async function attachModelProps(model: Model, ctx: PluginContext, taskCtx: Runti
     const { autoAttach, properties } = params
     for (const name of Object.keys(properties)) {
         const property = ctx.customModelProperties.get(name)
-        const props = params[name as keyof typeof params]
+        const props = properties[name]
         if (autoAttach.includes(name)) {
             try {
                 await property.attach(propertyCtx, model, props)
@@ -754,7 +725,7 @@ async function attachStructureProps(structure: Structure, ctx: PluginContext, ta
     const { autoAttach, properties } = params
     for (const name of Object.keys(properties)) {
         const property = ctx.customStructureProperties.get(name)
-        const props = params[name as keyof typeof params]
+        const props = properties[name]
         if (autoAttach.includes(name)) {
             try {
                 await property.attach(propertyCtx, structure, props)

+ 23 - 20
src/mol-plugin/state/transforms/representation.ts

@@ -63,37 +63,40 @@ namespace StructureRepresentation3DHelpers {
         })
     }
 
+    export type Props<R extends RepresentationProvider<Structure, any, any> = any, C extends ColorTheme.Provider<any> = any, S extends SizeTheme.Provider<any> = any> = {
+        repr?: R | [R, (r: R, ctx: ThemeRegistryContext, s: Structure) => Partial<RepresentationProvider.ParamValues<R>>],
+        color?: C | [C, (c: C, ctx: ThemeRegistryContext) => Partial<ColorTheme.ParamValues<C>>],
+        size?: S | [S, (c: S, ctx: ThemeRegistryContext) => Partial<SizeTheme.ParamValues<S>>]
+    }
+
     export function createParams<R extends RepresentationProvider<Structure, any, any>, C extends ColorTheme.Provider<any>, S extends SizeTheme.Provider<any>>(
-        ctx: PluginContext, structure: Structure, params: {
-            repr?: R | [R, (r: R, ctx: ThemeRegistryContext, s: Structure) => Partial<RepresentationProvider.ParamValues<R>>],
-            color?: C | [C, (c: C, ctx: ThemeRegistryContext) => Partial<ColorTheme.ParamValues<C>>],
-            size?: S | [S, (c: S, ctx: ThemeRegistryContext) => Partial<SizeTheme.ParamValues<S>>]
-        }): StateTransformer.Params<StructureRepresentation3D> {
+        ctx: PluginContext, structure: Structure, props: Props<R, C, S> = {}): StateTransformer.Params<StructureRepresentation3D> {
 
-        const themeCtx = ctx.structureRepresentation.themeCtx
+        const { themeCtx } = ctx.structureRepresentation
+        const themeDataCtx = { structure }
 
-        const repr = params.repr
-            ? params.repr instanceof Array ? params.repr[0] : params.repr
+        const repr = props.repr
+            ? props.repr instanceof Array ? props.repr[0] : props.repr
             : ctx.structureRepresentation.registry.default.provider;
         const reprDefaultParams = PD.getDefaultValues(repr.getParams(themeCtx, structure));
-        const reprParams = params.repr instanceof Array
-            ? { ...reprDefaultParams, ...params.repr[1](repr as R, themeCtx, structure) }
+        const reprParams = props.repr instanceof Array
+            ? { ...reprDefaultParams, ...props.repr[1](repr as R, themeCtx, structure) }
             : reprDefaultParams;
 
-        const color = params.color
-            ? params.color instanceof Array ? params.color[0] : params.color
+        const color = props.color
+            ? props.color instanceof Array ? props.color[0] : props.color
             : themeCtx.colorThemeRegistry.get(repr.defaultColorTheme.name);
-        const colorDefaultParams = { ...PD.getDefaultValues(color.getParams(themeCtx)), ...repr.defaultColorTheme.props }
-        const colorParams = params.color instanceof Array
-            ? { ...colorDefaultParams, ...params.color[1](color as C, themeCtx) }
+        const colorDefaultParams = { ...PD.getDefaultValues(color.getParams(themeDataCtx)), ...repr.defaultColorTheme.props }
+        const colorParams = props.color instanceof Array
+            ? { ...colorDefaultParams, ...props.color[1](color as C, themeCtx) }
             : colorDefaultParams;
 
-        const size = params.size
-            ? params.size instanceof Array ? params.size[0] : params.size
+        const size = props.size
+            ? props.size instanceof Array ? props.size[0] : props.size
             : themeCtx.sizeThemeRegistry.get(repr.defaultSizeTheme.name);
-        const sizeDefaultParams = { ...PD.getDefaultValues(size.getParams(themeCtx)), ...repr.defaultSizeTheme.props }
-        const sizeParams = params.size instanceof Array
-            ? { ...sizeDefaultParams, ...params.size[1](size as S, themeCtx) }
+        const sizeDefaultParams = { ...PD.getDefaultValues(size.getParams(themeDataCtx)), ...repr.defaultSizeTheme.props }
+        const sizeParams = props.size instanceof Array
+            ? { ...sizeDefaultParams, ...props.size[1](size as S, themeCtx) }
             : sizeDefaultParams;
 
         return ({

+ 1 - 1
src/mol-plugin/util/structure-complex-helper.ts

@@ -22,7 +22,7 @@ export function createDefaultStructureComplex(
         .apply(StateTransforms.Representation.StructureRepresentation3D,
             StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
 
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'modified' }, { tags: StructureComplexElementTypes.modified })
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'non-standard' }, { tags: StructureComplexElementTypes['non-standard'] })
         .apply(StateTransforms.Representation.StructureRepresentation3D,
             StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', void 0, 'polymer-id'));
 

+ 15 - 5
src/mol-plugin/util/structure-overpaint-helper.ts

@@ -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>
  */
@@ -7,10 +7,12 @@
 import { PluginStateObject } from '../../mol-plugin/state/objects';
 import { StateTransforms } from '../../mol-plugin/state/transforms';
 import { StateSelection, StateObjectCell, StateTransform, StateBuilder } from '../../mol-state';
-import { Structure, StructureElement } from '../../mol-model/structure';
+import { Structure, StructureElement, StructureSelection, QueryContext } from '../../mol-model/structure';
 import { PluginContext } from '../context';
 import { Color } from '../../mol-util/color';
 import { Overpaint } from '../../mol-theme/overpaint';
+import Expression from '../../mol-script/language/expression';
+import { compile } from '../../mol-script/runtime/query/compiler';
 
 type OverpaintEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, overpaint?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle>>) => void
 const OverpaintManagerTag = 'overpaint-controls'
@@ -29,7 +31,7 @@ export class StructureOverpaintHelper {
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }));
     }
 
-    async set(color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci, types?: string[]) {
+    async set(color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci, types?: string[], alpha = 1) {
         await this.eachRepr((update, repr, overpaintCell) => {
             if (types && !types.includes(repr.params!.values.type.name)) return
 
@@ -48,15 +50,23 @@ export class StructureOverpaintHelper {
             if (overpaintCell) {
                 const bundleLayers = [ ...overpaintCell.params!.values.layers, layer ]
                 const filtered = getFilteredBundle(bundleLayers, structure)
-                update.to(overpaintCell).update(Overpaint.toBundle(filtered, 1))
+                update.to(overpaintCell).update(Overpaint.toBundle(filtered, alpha))
             } else {
                 const filtered = getFilteredBundle([ layer ], structure)
                 update.to(repr.transform.ref)
-                    .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered, 1), { tags: OverpaintManagerTag });
+                    .apply(StateTransforms.Representation.OverpaintStructureRepresentation3DFromBundle, Overpaint.toBundle(filtered, alpha), { tags: OverpaintManagerTag });
             }
         })
     }
 
+    async setFromExpression(color: Color | -1, expression: Expression, types?: string[], alpha = 1) {
+        return this.set(color, (structure) => {
+            const compiled = compile<StructureSelection>(expression)
+            const result = compiled(new QueryContext(structure))
+            return StructureSelection.toLociWithSourceUnits(result)
+        }, types, alpha)
+    }
+
     constructor(private plugin: PluginContext) {
 
     }

+ 94 - 90
src/mol-plugin/util/structure-representation-helper.ts

@@ -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>
  */
@@ -12,8 +12,6 @@ import { PluginContext } from '../context';
 import { StructureRepresentation3DHelpers } from '../state/transforms/representation';
 import Expression from '../../mol-script/language/expression';
 import { compile } from '../../mol-script/runtime/query/compiler';
-import { StructureSelectionQueries as Q } from '../util/structure-selection-helper';
-import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { VisualQuality } from '../../mol-geo/geometry/base';
 
 type StructureTransform = StateObjectCell<PSO.Molecule.Structure, StateTransform<StateTransformer<any, PSO.Molecule.Structure, any>>>
@@ -34,6 +32,12 @@ function getCombinedLoci(mode: SelectionModifier, loci: StructureElement.Loci, c
 
 type SelectionModifier = 'add' | 'remove' | 'only'
 
+type ReprProps = {
+    repr?: {},
+    color?: string | [string, {}],
+    size?: string | [string, {}],
+}
+
 export class StructureRepresentationHelper {
     getRepresentationStructure(rootRef: string, type: string) {
         const state = this.plugin.state.dataState
@@ -49,12 +53,52 @@ export class StructureRepresentationHelper {
         return selections.length > 0 ? selections[0] : undefined
     }
 
-    private async _set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform, props = {}) {
+    private getRepresentationParams(structure: Structure, type: string, repr: RepresentationTransform | undefined, props: ReprProps = {}) {
+        const reprProps = {
+            ...(repr?.params && repr.params.values.type.params),
+            ignoreHydrogens: this._ignoreHydrogens,
+            quality: this._quality,
+            ...props.repr
+        }
+        const { themeCtx } =  this.plugin.structureRepresentation
+
+        const p: StructureRepresentation3DHelpers.Props = {
+            repr: [
+                this.plugin.structureRepresentation.registry.get(type),
+                () => reprProps
+            ]
+        }
+        if (props.color) {
+            const colorType = props.color instanceof Array ? props.color[0] : props.color
+            const colorTheme = themeCtx.colorThemeRegistry.get(colorType)
+            const colorProps = {
+                ...(repr?.params && repr.params.values.colorTheme.name === colorType && repr.params.values.colorTheme.params),
+                ...(props.color instanceof Array ? props.color[1] : {})
+            }
+            p.color = [colorTheme, () => colorProps]
+        }
+        if (props.size) {
+            const sizeType = props.size instanceof Array ? props.size[0] : props.size
+            const sizeTheme = themeCtx.sizeThemeRegistry.get(sizeType)
+            const sizeProps = {
+                ...(repr?.params && repr.params.values.sizeTheme.name === sizeType && repr.params.values.sizeTheme.params),
+                ...(props.size instanceof Array ? props.size[1] : {})
+            }
+            p.size = [sizeTheme, () => sizeProps]
+        }
+        if (props.size) p.size = props.size
+
+        return StructureRepresentation3DHelpers.createParams(this.plugin, structure, p)
+    }
+
+    private async _set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform, props: ReprProps = {}) {
         const state = this.plugin.state.dataState
         const update = state.build()
         const s = structure.obj!.data
 
+        const repr = this.getRepresentation(structure.transform.ref, type)
         const reprStructure = this.getRepresentationStructure(structure.transform.ref, type)
+        const reprParams = this.getRepresentationParams(s.root, type, repr, props)
 
         if (reprStructure) {
             const currentLoci = StructureElement.Bundle.toLoci(reprStructure.params!.values.bundle, s)
@@ -64,14 +108,9 @@ export class StructureRepresentationHelper {
                 ...reprStructure.params!.values,
                 bundle: StructureElement.Bundle.fromLoci(combinedLoci)
             })
+            if (repr) update.to(repr).update(reprParams)
         } else {
             const combinedLoci = getCombinedLoci(modifier, loci, StructureElement.Loci.none(s))
-            const params = StructureRepresentation3DHelpers.getDefaultParams(this.plugin, type as any, s)
-
-            const p = params.type.params
-            Object.assign(p, props)
-            if (p.ignoreHydrogens !== undefined) p.ignoreHydrogens = this._ignoreHydrogens
-            if (p.quality !== undefined) p.quality = this._quality
 
             update.to(structure.transform.ref)
                 .apply(
@@ -79,13 +118,13 @@ export class StructureRepresentationHelper {
                     { bundle: StructureElement.Bundle.fromLoci(combinedLoci), label: type },
                     { tags: [ RepresentationManagerTag, getRepresentationManagerTag(type) ] }
                 )
-                .apply( StateTransforms.Representation.StructureRepresentation3D, params)
+                .apply(StateTransforms.Representation.StructureRepresentation3D, reprParams)
         }
 
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
     }
 
-    async set(modifier: SelectionModifier, type: string, lociGetter: (structure: Structure) => StructureElement.Loci, props = {}) {
+    async set(modifier: SelectionModifier, type: string, lociGetter: (structure: Structure) => StructureElement.Loci, props: ReprProps = {}) {
         const state = this.plugin.state.dataState;
         const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
 
@@ -96,7 +135,7 @@ export class StructureRepresentationHelper {
         }
     }
 
-    async setFromExpression(modifier: SelectionModifier, type: string, expression: Expression, props = {}) {
+    async setFromExpression(modifier: SelectionModifier, type: string, expression: Expression, props: ReprProps = {}) {
         return this.set(modifier, type, (structure) => {
             const compiled = compile<StructureSelection>(expression)
             const result = compiled(new QueryContext(structure))
@@ -104,44 +143,76 @@ export class StructureRepresentationHelper {
         }, props)
     }
 
-    async clear() {
+    async eachStructure(callback: (structure: StructureTransform, type: string, update: StateBuilder.Root) => void) {
         const { registry } = this.plugin.structureRepresentation
         const state = this.plugin.state.dataState;
         const update = state.build()
         const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
-        const bundle = StructureElement.Bundle.Empty
 
         for (const structure of structures) {
             for (let i = 0, il = registry.types.length; i < il; ++i) {
                 const type = registry.types[i][0]
                 const reprStructure = this.getRepresentationStructure(structure.transform.ref, type)
-                if (reprStructure) {
-                    update.to(reprStructure).update({ ...reprStructure.params!.values, bundle })
-                }
+                if (reprStructure) callback(reprStructure, type, update)
             }
         }
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
     }
 
-    async eachRepresentation(callback: (repr: RepresentationTransform, update: StateBuilder.Root) => void) {
+    async clear() {
+        const bundle = StructureElement.Bundle.Empty
+        await this.eachStructure((structure, type, update) => {
+            update.to(structure).update({ ...structure.params!.values, bundle })
+        })
+    }
+
+    async clearExcept(exceptTypes: string[]) {
+        const bundle = StructureElement.Bundle.Empty
+        await this.eachStructure((structure, type, update) => {
+            if (!exceptTypes.includes(type)) {
+                update.to(structure).update({ ...structure.params!.values, bundle })
+            }
+        })
+    }
+
+    async eachRepresentation(callback: (repr: RepresentationTransform, type: string, update: StateBuilder.Root) => void) {
         const { registry } = this.plugin.structureRepresentation
         const state = this.plugin.state.dataState;
         const update = state.build()
         const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
         for (const structure of structures) {
             for (let i = 0, il = registry.types.length; i < il; ++i) {
-                const repr = this.getRepresentation(structure.transform.ref, registry.types[i][0])
-                if (repr) callback(repr, update)
+                const type = registry.types[i][0]
+                const repr = this.getRepresentation(structure.transform.ref, type)
+                if (repr) callback(repr, type, update)
             }
         }
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
     }
 
+    setRepresentationParams(repr: RepresentationTransform, type: string, update: StateBuilder.Root, props: ReprProps) {
+        const state = this.plugin.state.dataState;
+        const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
+
+        for (const structure of structures) {
+            const s = structure.obj!.data
+            const reprParams = this.getRepresentationParams(s.root, type, repr, props)
+            update.to(repr).update(reprParams)
+        }
+    }
+
+    async updateRepresentation(repr: RepresentationTransform, type: string, props: ReprProps) {
+        const state = this.plugin.state.dataState;
+        const update = state.build()
+        this.setRepresentationParams(repr, type, update, props)
+        await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }))
+    }
+
     private _ignoreHydrogens = false
     get ignoreHydrogens () { return this._ignoreHydrogens }
     async setIgnoreHydrogens(ignoreHydrogens: boolean) {
         if (ignoreHydrogens === this._ignoreHydrogens) return
-        await this.eachRepresentation((repr, update) => {
+        await this.eachRepresentation((repr, type, update) => {
             if (repr.params && repr.params.values.type.params.ignoreHydrogens !== undefined) {
                 const { name, params } = repr.params.values.type
                 update.to(repr.transform.ref).update(
@@ -157,7 +228,7 @@ export class StructureRepresentationHelper {
     get quality () { return this._quality }
     async setQuality(quality: VisualQuality) {
         if (quality === this._quality) return
-        await this.eachRepresentation((repr, update) => {
+        await this.eachRepresentation((repr, type, update) => {
             if (repr.params && repr.params.values.type.params.quality !== undefined) {
                 const { name, params } = repr.params.values.type
                 update.to(repr.transform.ref).update(
@@ -169,74 +240,7 @@ export class StructureRepresentationHelper {
         this._quality = quality
     }
 
-    async preset() {
-        // TODO option to limit to specific structure
-        const state = this.plugin.state.dataState;
-        const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure))
-
-        if (structures.length === 0) return
-        const s = structures[0].obj!.data
-
-        if (s.elementCount < 50000) {
-            await polymerAndLigand(this)
-        } else if (s.elementCount < 200000) {
-            await proteinAndNucleic(this)
-        } else {
-            if (s.unitSymmetryGroups[0].units.length > 10) {
-                await capsid(this)
-            } else {
-                await coarseCapsid(this)
-            }
-        }
-    }
-
     constructor(private plugin: PluginContext) {
 
     }
-}
-
-//
-
-async function polymerAndLigand(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'cartoon', Q.polymer.expression)
-    await r.setFromExpression('add', 'carbohydrate', Q.branchedPlusConnected.expression)
-    await r.setFromExpression('add', 'ball-and-stick', MS.struct.modifier.union([
-        MS.struct.combinator.merge([
-            Q.ligandPlusConnected.expression,
-            Q.branchedConnectedOnly.expression,
-            Q.disulfideBridges.expression,
-            Q.nonStandardPolymer.expression,
-            Q.water.expression
-        ])
-    ]))
-}
-
-async function proteinAndNucleic(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'cartoon', Q.protein.expression)
-    await r.setFromExpression('add', 'gaussian-surface', Q.nucleic.expression)
-}
-
-async function capsid(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'gaussian-surface', Q.polymer.expression, {
-        smoothness: 0.5,
-    })
-}
-
-async function coarseCapsid(r: StructureRepresentationHelper) {
-    await r.clear()
-    await r.setFromExpression('add', 'gaussian-surface', Q.trace.expression, {
-        radiusOffset: 1,
-        smoothness: 0.5,
-        visuals: ['structure-gaussian-surface-mesh']
-    })
-}
-
-export const StructureRepresentationPresets = {
-    polymerAndLigand,
-    proteinAndNucleic,
-    capsid,
-    coarseCapsid
 }

+ 209 - 55
src/mol-plugin/util/structure-selection-helper.ts

@@ -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>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -8,32 +8,76 @@
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { StateSelection, StateBuilder } from '../../mol-state';
 import { PluginStateObject } from '../state/objects';
-import { QueryContext, StructureSelection, StructureQuery, StructureElement } from '../../mol-model/structure';
+import { QueryContext, StructureSelection, StructureQuery, StructureElement, Structure } from '../../mol-model/structure';
 import { compile } from '../../mol-script/runtime/query/compiler';
 import { Loci } from '../../mol-model/loci';
 import { PluginContext } from '../context';
 import Expression from '../../mol-script/language/expression';
 import { BondType, ProteinBackboneAtoms, NucleicBackboneAtoms, SecondaryStructureType } from '../../mol-model/structure/model/types';
 import { StateTransforms } from '../state/transforms';
+import { SetUtils } from '../../mol-util/set';
+import { ValidationReport, ValidationReportProvider } from '../../mol-model-props/rcsb/validation-report';
+import { CustomProperty } from '../../mol-model-props/common/custom-property';
+import { Task } from '../../mol-task';
+import { AccessibleSurfaceAreaSymbols, AccessibleSurfaceAreaProvider } from '../../mol-model-props/computed/accessible-surface-area';
+import { stringToWords } from '../../mol-util/string';
+
+export enum StructureSelectionCategory {
+    Type = 'Type',
+    Structure = 'Structure Property',
+    Atom = 'Atom Property',
+    Bond = 'Bond Property',
+    Residue = 'Residue Property',
+    AminoAcid = 'Amino Acid',
+    NucleicBase = 'Nucleic Base',
+    Manipulate = 'Manipulate Selection',
+    Validation = 'Validation',
+    Misc = 'Miscellaneous',
+    Internal = 'Internal',
+}
+
+export { StructureSelectionQuery }
+
+interface StructureSelectionQuery {
+    readonly label: string
+    readonly expression: Expression
+    readonly description: string
+    readonly category: string
+    readonly isHidden: boolean
+    readonly query: StructureQuery
+    readonly ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void>
+}
 
-export interface StructureSelectionQuery {
-    label: string
-    query: StructureQuery
-    expression: Expression
-    description: string
+interface StructureSelectionQueryProps {
+    description?: string,
+    category?: string
+    isHidden?: boolean
+    ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void>
 }
 
-export function StructureSelectionQuery(label: string, expression: Expression, description = ''): StructureSelectionQuery {
-    return { label, expression, query: compile<StructureSelection>(expression), description }
+function StructureSelectionQuery(label: string, expression: Expression, props: StructureSelectionQueryProps = {}): StructureSelectionQuery {
+    let _query: StructureQuery
+    return {
+        label,
+        expression,
+        description: props.description || '',
+        category: props.category ?? StructureSelectionCategory.Misc,
+        isHidden: !!props.isHidden,
+        get query() {
+            if (!_query) _query = compile<StructureSelection>(expression)
+            return _query
+        },
+        ensureCustomProperties: props.ensureCustomProperties
+    }
 }
 
-const all = StructureSelectionQuery('All', MS.struct.generator.all())
+const all = StructureSelectionQuery('All', MS.struct.generator.all(), { category: '' })
 
 const polymer = StructureSelectionQuery('Polymer', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
         'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer'])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([
     MS.struct.combinator.merge([
@@ -53,7 +97,7 @@ const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([
             })
         ])
     ])
-]))
+]), { category: StructureSelectionCategory.Structure })
 
 // TODO maybe pre-calculate atom properties like backbone/sidechain
 const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
@@ -68,7 +112,7 @@ const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
                     ])
                 ]),
                 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-                'atom-test': MS.core.set.has([MS.set(...Array.from(ProteinBackboneAtoms.values())), MS.ammp('label_atom_id')])
+                'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(ProteinBackboneAtoms)), MS.ammp('label_atom_id')])
             })
         ]),
         MS.struct.modifier.union([
@@ -81,11 +125,11 @@ const backbone = StructureSelectionQuery('Backbone', MS.struct.modifier.union([
                     ])
                 ]),
                 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-                'atom-test': MS.core.set.has([MS.set(...Array.from(NucleicBackboneAtoms.values())), MS.ammp('label_atom_id')])
+                'atom-test': MS.core.set.has([MS.set(...SetUtils.toArray(NucleicBackboneAtoms)), MS.ammp('label_atom_id')])
             })
         ])
     ])
-]))
+]), { category: StructureSelectionCategory.Structure })
 
 const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -97,7 +141,7 @@ const protein = StructureSelectionQuery('Protein', MS.struct.modifier.union([
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const nucleic = StructureSelectionQuery('Nucleic', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -109,7 +153,7 @@ const nucleic = StructureSelectionQuery('Nucleic', MS.struct.modifier.union([
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const proteinOrNucleic = StructureSelectionQuery('Protein or Nucleic', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -121,7 +165,7 @@ const proteinOrNucleic = StructureSelectionQuery('Protein or Nucleic', MS.struct
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -137,7 +181,7 @@ const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([
             MS.core.type.bitflags([SecondaryStructureType.Flag.Helix])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -153,13 +197,13 @@ const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.uni
             MS.core.type.bitflags([SecondaryStructureType.Flag.Beta])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const water = StructureSelectionQuery('Water', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
         'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'water'])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const branched = StructureSelectionQuery('Carbohydrate', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -174,20 +218,20 @@ const branched = StructureSelectionQuery('Carbohydrate', MS.struct.modifier.unio
             ])
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Type })
 
 const branchedPlusConnected = StructureSelectionQuery('Carbohydrate with Connected', MS.struct.modifier.union([
     MS.struct.modifier.includeConnected({
         0: branched.expression, 'layer-count': 1, 'as-whole-residues': true
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const branchedConnectedOnly = StructureSelectionQuery('Connected to Carbohydrate', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
         0: branchedPlusConnected.expression,
         by: branched.expression
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([
     MS.struct.combinator.merge([
@@ -221,7 +265,7 @@ const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([
             })
         ])
     ]),
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 // don't include branched entities as they have their own link representation
 const ligandPlusConnected = StructureSelectionQuery('Ligand with Connected', MS.struct.modifier.union([
@@ -241,14 +285,14 @@ const ligandPlusConnected = StructureSelectionQuery('Ligand with Connected', MS.
         ]),
         by: branched.expression
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const ligandConnectedOnly = StructureSelectionQuery('Connected to Ligand', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
         0: ligandPlusConnected.expression,
         by: ligand.expression
     })
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 // residues connected to ligands or branched entities
 const connectedOnly = StructureSelectionQuery('Connected to Ligand or Carbohydrate', MS.struct.modifier.union([
@@ -256,7 +300,7 @@ const connectedOnly = StructureSelectionQuery('Connected to Ligand or Carbohydra
         branchedConnectedOnly.expression,
         ligandConnectedOnly.expression
     ]),
-]))
+]), { category: StructureSelectionCategory.Internal })
 
 const disulfideBridges = StructureSelectionQuery('Disulfide Bridges', MS.struct.modifier.union([
     MS.struct.modifier.wholeResidues([
@@ -269,14 +313,7 @@ const disulfideBridges = StructureSelectionQuery('Disulfide Bridges', MS.struct.
             })
         ])
     ])
-]))
-
-const modified = StructureSelectionQuery('Modified Residues', MS.struct.modifier.union([
-    MS.struct.generator.atomGroups({
-        'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-        'residue-test': MS.ammp('isModified')
-    })
-]))
+]), { category: StructureSelectionCategory.Bond })
 
 const nonStandardPolymer = StructureSelectionQuery('Non-standard Residues in Polymers', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -284,7 +321,7 @@ const nonStandardPolymer = StructureSelectionQuery('Non-standard Residues in Pol
         'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
         'residue-test': MS.ammp('isNonStandard')
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const coarse = StructureSelectionQuery('Coarse Elements', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -292,15 +329,15 @@ const coarse = StructureSelectionQuery('Coarse Elements', MS.struct.modifier.uni
             MS.set('sphere', 'gaussian'), MS.ammp('objectPrimitive')
         ])
     })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const ring = StructureSelectionQuery('Rings in Residues', MS.struct.modifier.union([
     MS.struct.generator.rings()
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const aromaticRing = StructureSelectionQuery('Aromatic Rings in Residues', MS.struct.modifier.union([
     MS.struct.generator.rings({ 'only-aromatic': true })
-]))
+]), { category: StructureSelectionCategory.Residue })
 
 const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of Selection', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
@@ -311,20 +348,121 @@ const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of
         }),
         by: MS.internal.generator.current()
     })
-]), 'Select residues within 5 \u212B of the current selection.')
+]), {
+    description: 'Select residues within 5 \u212B of the current selection.',
+    category: StructureSelectionCategory.Manipulate
+})
 
 const complement = StructureSelectionQuery('Inverse / Complement of Selection', MS.struct.modifier.union([
     MS.struct.modifier.exceptBy({
         0: MS.struct.generator.all(),
         by: MS.internal.generator.current()
     })
-]), 'Select everything not in the current selection.')
+]), {
+    description: 'Select everything not in the current selection.',
+    category: StructureSelectionCategory.Manipulate
+})
 
 const bonded = StructureSelectionQuery('Residues Bonded to Selection', MS.struct.modifier.union([
     MS.struct.modifier.includeConnected({
         0: MS.internal.generator.current(), 'layer-count': 1, 'as-whole-residues': true
     })
-]), 'Select residues covalently bonded to current selection.')
+]), {
+    description: 'Select residues covalently bonded to current selection.',
+    category: StructureSelectionCategory.Manipulate
+})
+
+const hasClash = StructureSelectionQuery('Residues with Clashes', MS.struct.modifier.union([
+    MS.struct.modifier.wholeResidues([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'atom-test': ValidationReport.symbols.hasClash.symbol(),
+            })
+        ])
+    ])
+]), {
+    description: 'Select residues with clashes in the wwPDB validation report.',
+    category: StructureSelectionCategory.Residue,
+    ensureCustomProperties: (ctx, structure) => {
+        return ValidationReportProvider.attach(ctx, structure.models[0])
+    }
+})
+
+const isBuried = StructureSelectionQuery('Buried Protein Residues', MS.struct.modifier.union([
+    MS.struct.modifier.wholeResidues([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'residue-test': AccessibleSurfaceAreaSymbols.isBuried.symbol(),
+            })
+        ])
+    ])
+]), {
+    description: 'Select buried protein residues.',
+    category: StructureSelectionCategory.Residue,
+    ensureCustomProperties: (ctx, structure) => {
+        return AccessibleSurfaceAreaProvider.attach(ctx, structure)
+    }
+})
+
+const isAccessible = StructureSelectionQuery('Accessible Protein Residues', MS.struct.modifier.union([
+    MS.struct.modifier.wholeResidues([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                'residue-test': AccessibleSurfaceAreaSymbols.isAccessible.symbol(),
+            })
+        ])
+    ])
+]), {
+    description: 'Select accessible protein residues.',
+    category: StructureSelectionCategory.Residue,
+    ensureCustomProperties: (ctx, structure) => {
+        return AccessibleSurfaceAreaProvider.attach(ctx, structure)
+    }
+})
+
+const StandardAminoAcids = [
+    [['HIS'], 'HISTIDINE'],
+    [['ARG'], 'ARGININE'],
+    [['LYS'], 'LYSINE'],
+    [['ILE'], 'ISOLEUCINE'],
+    [['PHE'], 'PHENYLALANINE'],
+    [['LEU'], 'LEUCINE'],
+    [['TRP'], 'TRYPTOPHAN'],
+    [['ALA'], 'ALANINE'],
+    [['MET'], 'METHIONINE'],
+    [['CYS'], 'CYSTEINE'],
+    [['ASN'], 'ASPARAGINE'],
+    [['VAL'], 'VALINE'],
+    [['GLY'], 'GLYCINE'],
+    [['SER'], 'SERINE'],
+    [['GLN'], 'GLUTAMINE'],
+    [['TYR'], 'TYROSINE'],
+    [['ASP'], 'ASPARTIC ACID'],
+    [['GLU'], 'GLUTAMIC ACID'],
+    [['THR'], 'THREONINE'],
+    [['SEC'], 'SELENOCYSTEINE'],
+    [['PYL'], 'PYRROLYSINE'],
+].sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) as [string[], string][]
+
+const StandardNucleicBases = [
+    [['A', 'DA'], 'ADENOSINE'],
+    [['C', 'DC'], 'CYTIDINE'],
+    [['T', 'DT'], 'THYMIDINE'],
+    [['G', 'DG'], 'GUANOSINE'],
+    [['I', 'DI'], 'INOSINE'],
+    [['U', 'DU'], 'URIDINE'],
+].sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) as [string[], string][]
+
+function ResidueQuery([names, label]: [string[], string], category: string) {
+    return StructureSelectionQuery(`${stringToWords(label)} (${names.join(', ')})`, MS.struct.modifier.union([
+        MS.struct.generator.atomGroups({
+            'residue-test': MS.core.set.has([MS.set(...names), MS.ammp('auth_comp_id')])
+        })
+    ]), { category })
+}
 
 export const StructureSelectionQueries = {
     all,
@@ -345,7 +483,6 @@ export const StructureSelectionQueries = {
     ligandConnectedOnly,
     connectedOnly,
     disulfideBridges,
-    modified,
     nonStandardPolymer,
     coarse,
     ring,
@@ -353,8 +490,18 @@ export const StructureSelectionQueries = {
     surroundings,
     complement,
     bonded,
+
+    hasClash,
+    isBuried,
+    isAccessible
 }
 
+export const StructureSelectionQueryList = [
+    ...Object.values(StructureSelectionQueries),
+    ...StandardAminoAcids.map(v => ResidueQuery(v, StructureSelectionCategory.AminoAcid)),
+    ...StandardNucleicBases.map(v => ResidueQuery(v, StructureSelectionCategory.NucleicBase)),
+]
+
 export function applyBuiltInSelection(to: StateBuilder.To<PluginStateObject.Molecule.Structure>, query: keyof typeof StructureSelectionQueries, customTag?: string) {
     return to.apply(StateTransforms.Model.StructureSelectionFromExpression,
         { expression: StructureSelectionQueries[query].expression, label: StructureSelectionQueries[query].label },
@@ -384,17 +531,24 @@ export class StructureSelectionHelper {
         }
     }
 
-    set(modifier: SelectionModifier, query: StructureQuery, applyGranularity = true) {
-        for (const s of this.structures) {
-            const current = this.plugin.helpers.structureSelectionManager.get(s)
-            const currentSelection = Loci.isEmpty(current)
-                ? StructureSelection.Empty(s)
-                : StructureSelection.Singletons(s, StructureElement.Loci.toStructure(current))
-
-            const result = query(new QueryContext(s, { currentSelection }))
-            const loci = StructureSelection.toLociWithSourceUnits(result)
-            this._set(modifier, loci, applyGranularity)
-        }
+    async set(modifier: SelectionModifier, selectionQuery: StructureSelectionQuery, applyGranularity = true) {
+        this.plugin.runTask(Task.create('Structure Selection', async runtime => {
+            const ctx = { fetch: this.plugin.fetch, runtime }
+            for (const s of this.structures) {
+                const current = this.plugin.helpers.structureSelectionManager.get(s)
+                const currentSelection = Loci.isEmpty(current)
+                    ? StructureSelection.Empty(s)
+                    : StructureSelection.Singletons(s, StructureElement.Loci.toStructure(current))
+
+                if (selectionQuery.ensureCustomProperties) {
+                    await selectionQuery.ensureCustomProperties(ctx, s)
+                }
+
+                const result = selectionQuery.query(new QueryContext(s, { currentSelection }))
+                const loci = StructureSelection.toLociWithSourceUnits(result)
+                this._set(modifier, loci, applyGranularity)
+            }
+        }))
     }
 
     constructor(private plugin: PluginContext) {

+ 0 - 2
src/mol-repr/structure/registry.ts

@@ -11,7 +11,6 @@ import { BallAndStickRepresentationProvider } from './representation/ball-and-st
 import { GaussianSurfaceRepresentationProvider } from './representation/gaussian-surface';
 import { CarbohydrateRepresentationProvider } from './representation/carbohydrate';
 import { SpacefillRepresentationProvider } from './representation/spacefill';
-import { DistanceRestraintRepresentationProvider } from './representation/distance-restraint';
 import { PointRepresentationProvider } from './representation/point';
 import { StructureRepresentationState } from './representation';
 import { PuttyRepresentationProvider } from './representation/putty';
@@ -34,7 +33,6 @@ export const BuiltInStructureRepresentations = {
     'cartoon': CartoonRepresentationProvider,
     'ball-and-stick': BallAndStickRepresentationProvider,
     'carbohydrate': CarbohydrateRepresentationProvider,
-    'distance-restraint': DistanceRestraintRepresentationProvider,
     'ellipsoid': EllipsoidRepresentationProvider,
     'gaussian-surface': GaussianSurfaceRepresentationProvider,
     // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work

+ 0 - 43
src/mol-repr/structure/representation/distance-restraint.ts

@@ -1,43 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { CrossLinkRestraintVisual, CrossLinkRestraintParams } from '../visual/cross-link-restraint-cylinder';
-import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { ComplexRepresentation } from '../complex-representation';
-import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
-import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
-import { ThemeRegistryContext } from '../../../mol-theme/theme';
-import { Structure } from '../../../mol-model/structure';
-import { UnitKind, UnitKindOptions } from '../visual/util/common';
-
-const DistanceRestraintVisuals = {
-    'distance-restraint': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CrossLinkRestraintParams>) => ComplexRepresentation('Cross-link restraint', ctx, getParams, CrossLinkRestraintVisual),
-}
-
-export const DistanceRestraintParams = {
-    ...CrossLinkRestraintParams,
-    unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions),
-}
-export type DistanceRestraintParams = typeof DistanceRestraintParams
-export function getDistanceRestraintParams(ctx: ThemeRegistryContext, structure: Structure) {
-    return PD.clone(DistanceRestraintParams)
-}
-
-export type DistanceRestraintRepresentation = StructureRepresentation<DistanceRestraintParams>
-export function DistanceRestraintRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, DistanceRestraintParams>): DistanceRestraintRepresentation {
-    return Representation.createMulti('DistanceRestraint', ctx, getParams, StructureRepresentationStateBuilder, DistanceRestraintVisuals as unknown as Representation.Def<Structure, DistanceRestraintParams>)
-}
-
-export const DistanceRestraintRepresentationProvider: StructureRepresentationProvider<DistanceRestraintParams> = {
-    label: 'Distance Restraint',
-    description: 'Displays cross-link distance restraints.',
-    factory: DistanceRestraintRepresentation,
-    getParams: getDistanceRestraintParams,
-    defaultValues: PD.getDefaultValues(DistanceRestraintParams),
-    defaultColorTheme: { name: 'cross-link' },
-    defaultSizeTheme: { name: 'uniform' },
-    isApplicable: (structure: Structure) => structure.crossLinkRestraints.count > 0
-}

+ 0 - 119
src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts

@@ -1,119 +0,0 @@
-/**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { VisualContext } from '../../visual';
-import { Structure, StructureElement, Bond } from '../../../mol-model/structure';
-import { Theme } from '../../../mol-theme/theme';
-import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
-import { Vec3 } from '../../../mol-math/linear-algebra';
-import { createLinkCylinderMesh, LinkCylinderParams } from './util/link';
-import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../complex-visual';
-import { VisualUpdateState } from '../../util';
-import { LocationIterator } from '../../../mol-geo/util/location-iterator';
-import { PickingId } from '../../../mol-geo/geometry/picking';
-import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Interval } from '../../../mol-data/int';
-
-function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintParams>, mesh?: Mesh) {
-
-    const crossLinks = structure.crossLinkRestraints
-    if (!crossLinks.count) return Mesh.createEmpty(mesh)
-    const { sizeFactor } = props
-
-    const location = StructureElement.Location.create(structure)
-
-    const builderProps = {
-        linkCount: crossLinks.count,
-        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
-            const b = crossLinks.pairs[edgeIndex]
-            const uA = b.unitA, uB = b.unitB
-            uA.conformation.position(uA.elements[b.indexA], posA)
-            uB.conformation.position(uB.elements[b.indexB], posB)
-        },
-        radius: (edgeIndex: number) => {
-            const b = crossLinks.pairs[edgeIndex]
-            location.unit = b.unitA
-            location.element = b.unitA.elements[b.indexA]
-            return theme.size.size(location) * sizeFactor
-        },
-    }
-
-    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
-}
-
-export const CrossLinkRestraintParams = {
-    ...ComplexMeshParams,
-    ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
-}
-export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams
-
-export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintParams> {
-    return ComplexMeshVisual<CrossLinkRestraintParams>({
-        defaultProps: PD.getDefaultValues(CrossLinkRestraintParams),
-        createGeometry: createCrossLinkRestraintCylinderMesh,
-        createLocationIterator: CrossLinkRestraintIterator,
-        getLoci: getLinkLoci,
-        eachLocation: eachCrossLink,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintParams>, currentProps: PD.Values<CrossLinkRestraintParams>) => {
-            state.createGeometry = (
-                newProps.sizeFactor !== currentProps.sizeFactor ||
-                newProps.radialSegments !== currentProps.radialSegments ||
-                newProps.linkCap !== currentProps.linkCap
-            )
-        }
-    }, materialId)
-}
-
-function CrossLinkRestraintIterator(structure: Structure): LocationIterator {
-    const { pairs } = structure.crossLinkRestraints
-    const groupCount = pairs.length
-    const instanceCount = 1
-    const location = Bond.Location()
-    const getLocation = (groupIndex: number) => {
-        const pair = pairs[groupIndex]
-        location.aStructure = structure
-        location.aUnit = pair.unitA
-        location.aIndex = pair.indexA
-        location.bStructure = structure
-        location.bUnit = pair.unitB
-        location.bIndex = pair.indexB
-        return location
-    }
-    return LocationIterator(groupCount, instanceCount, getLocation, true)
-}
-
-function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
-    const { objectId, groupId } = pickingId
-    if (id === objectId) {
-        const pair = structure.crossLinkRestraints.pairs[groupId]
-        if (pair) {
-            return Bond.Loci(structure, [
-                Bond.Location(structure, pair.unitA, pair.indexA, structure, pair.unitB, pair.indexB),
-                Bond.Location(structure, pair.unitB, pair.indexB, structure, pair.unitA, pair.indexA)
-            ])
-        }
-    }
-    return EmptyLoci
-}
-
-function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
-    const crossLinks = structure.crossLinkRestraints
-    let changed = false
-    if (Bond.isLoci(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false
-        for (const b of loci.bonds) {
-            const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
-            if (indices) {
-                for (let i = 0, il = indices.length; i < il; ++i) {
-                    if (apply(Interval.ofSingleton(indices[i]))) changed = true
-                }
-            }
-        }
-    }
-    return changed
-}

+ 1 - 4
src/mol-repr/structure/visual/nucleotide-block-mesh.ts

@@ -53,7 +53,6 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh)
 
     const { elements, model } = unit
-    const { modifiedResidues } = model.properties
     const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy
     const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue
     const { label_comp_id } = residues
@@ -72,9 +71,7 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
             const { index: residueIndex } = residueIt.move();
 
             if (isNucleic(moleculeType[residueIndex])) {
-                let compId = label_comp_id.value(residueIndex)
-                const parentId = modifiedResidues.parentId.get(compId)
-                if (parentId !== undefined) compId = parentId
+                const compId = label_comp_id.value(residueIndex)
                 let idx1: ElementIndex | -1 = -1, idx2: ElementIndex | -1 = -1, idx3: ElementIndex | -1 = -1, idx4: ElementIndex | -1 = -1, idx5: ElementIndex | -1 = -1, idx6: ElementIndex | -1 = -1
                 let width = 4.5, height = 4.5, depth = 2.5 * sizeFactor
 

+ 1 - 4
src/mol-repr/structure/visual/nucleotide-ring-mesh.ts

@@ -72,7 +72,6 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh)
 
     const { elements, model } = unit
-    const { modifiedResidues } = model.properties
     const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy
     const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue
     const { label_comp_id } = residues
@@ -93,9 +92,7 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str
             const { index: residueIndex } = residueIt.move();
 
             if (isNucleic(moleculeType[residueIndex])) {
-                let compId = label_comp_id.value(residueIndex)
-                const parentId = modifiedResidues.parentId.get(compId)
-                if (parentId !== undefined) compId = parentId
+                const compId = label_comp_id.value(residueIndex)
 
                 let idxTrace: ElementIndex | -1 = -1, idxN1: ElementIndex | -1 = -1, idxC2: ElementIndex | -1 = -1, idxN3: ElementIndex | -1 = -1, idxC4: ElementIndex | -1 = -1, idxC5: ElementIndex | -1 = -1, idxC6: ElementIndex | -1 = -1, idxN7: ElementIndex | -1 = -1, idxC8: ElementIndex | -1 = -1, idxN9: ElementIndex | -1 = -1
 

+ 0 - 2
src/mol-script/runtime/query/table.ts

@@ -325,8 +325,6 @@ const symbols = [
     D(MolScript.structureQuery.atomProperty.macromolecular.entitySubtype, atomProp(StructureProperties.entity.subtype)),
     D(MolScript.structureQuery.atomProperty.macromolecular.objectPrimitive, atomProp(StructureProperties.unit.object_primitive)),
 
-    D(MolScript.structureQuery.atomProperty.macromolecular.isModified, atomProp(StructureProperties.residue.isModified)),
-    D(MolScript.structureQuery.atomProperty.macromolecular.modifiedParentName, atomProp(StructureProperties.residue.modifiedParentName)),
     D(MolScript.structureQuery.atomProperty.macromolecular.isNonStandard, atomProp(StructureProperties.residue.isNonStandard)),
     D(MolScript.structureQuery.atomProperty.macromolecular.secondaryStructureKey, atomProp(StructureProperties.residue.secondary_structure_key)),
     D(MolScript.structureQuery.atomProperty.macromolecular.secondaryStructureFlags, atomProp(StructureProperties.residue.secondary_structure_type)),

+ 10 - 3
src/mol-theme/color.ts

@@ -13,7 +13,6 @@ import { deepEqual } from '../mol-util';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { ThemeDataContext, ThemeRegistry, ThemeProvider } from './theme';
 import { ChainIdColorThemeProvider } from './color/chain-id';
-import { CrossLinkColorThemeProvider } from './color/cross-link';
 import { ElementIndexColorThemeProvider } from './color/element-index';
 import { ElementSymbolColorThemeProvider } from './color/element-symbol';
 import { MoleculeTypeColorThemeProvider } from './color/molecule-type';
@@ -47,6 +46,15 @@ interface ColorTheme<P extends PD.Params> {
     readonly legend?: Readonly<ScaleLegend | TableLegend>
 }
 namespace ColorTheme {
+    export const enum Category {
+        Atom = 'Atom Property',
+        Chain = 'Chain Property',
+        Residue = 'Residue Property',
+        Symmetry = 'Symmetry',
+        Validation = 'Validation',
+        Misc = 'Miscellaneous',
+    }
+
     export type Props = { [k: string]: any }
     export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P>
     export const EmptyFactory = () => Empty
@@ -63,7 +71,7 @@ namespace ColorTheme {
     }
 
     export interface Provider<P extends PD.Params> extends ThemeProvider<ColorTheme<P>, P> { }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
+    export const EmptyProvider: Provider<{}> = { label: '', category: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {}, isApplicable: () => true }
 
     export type Registry = ThemeRegistry<ColorTheme<any>>
     export function createRegistry() {
@@ -76,7 +84,6 @@ namespace ColorTheme {
 export const BuiltInColorThemes = {
     'carbohydrate-symbol': CarbohydrateSymbolColorThemeProvider,
     'chain-id': ChainIdColorThemeProvider,
-    'cross-link': CrossLinkColorThemeProvider,
     'element-index': ElementIndexColorThemeProvider,
     'element-symbol': ElementSymbolColorThemeProvider,
     'entity-source': EntitySourceColorThemeProvider,

+ 1 - 0
src/mol-theme/color/carbohydrate-symbol.ts

@@ -62,6 +62,7 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
 
 export const CarbohydrateSymbolColorThemeProvider: ColorTheme.Provider<CarbohydrateSymbolColorThemeParams> = {
     label: 'Carbohydrate Symbol',
+    category: ColorTheme.Category.Residue,
     factory: CarbohydrateSymbolColorTheme,
     getParams: getCarbohydrateSymbolColorThemeParams,
     defaultValues: PD.getDefaultValues(CarbohydrateSymbolColorThemeParams),

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません