Browse Source

Merge branch 'master-source'

* master-source: (21 commits)
  tweaked polymer trace for coarse units
  provide some feedback for users in canvas app
  wip, carbohydrates and branched entity
  support cif files with only safeFrames
  wip, carbohydrate related schema additions
  tweaks to carbohydrate handling for remediated structures (and fixes)
  added alt_id to element label
  package update
  optimized idField creation when creating gaussian density field
  wip, gaussian surface picking / coloring
  fixed normal dir in normal calculation
  added molecule-type color scheme
  ModelServer: custom property providers defined by config file
  ModelServer: support BinaryCIF "encoding providers" from source file
  cleanup and package updates
  added sequence-id and secondary-structure color schemes
  added matplotlib derived and other color scales
  Don't track JetBrains files (IntelliJ IDE)
  Docker'ized the canvas example
  tweaks for coarse/backbone only models
  ...
Cole Christie 6 years ago
parent
commit
743279007d
54 changed files with 1302 additions and 300 deletions
  1. 4 0
      .dockerignore
  2. 1 2
      .gitignore
  3. 37 0
      Dockerfile
  4. 13 0
      README.md
  5. 49 2
      data/mmcif-field-names.csv
  6. 108 118
      package-lock.json
  7. 9 9
      package.json
  8. 27 4
      src/apps/canvas/app.ts
  9. 7 5
      src/apps/canvas/component/structure-representation.tsx
  10. 1 0
      src/apps/canvas/component/structure-view.tsx
  11. 26 6
      src/apps/canvas/component/viewport.tsx
  12. 10 7
      src/apps/canvas/structure-view.ts
  13. 21 6
      src/apps/schema-generator/schema-from-cif-dic.ts
  14. 67 1
      src/mol-geo/geometry/mesh/mesh.ts
  15. 3 3
      src/mol-geo/representation/structure/visual/gaussian-surface-mesh.ts
  16. 2 0
      src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
  17. 20 11
      src/mol-geo/representation/structure/visual/util/gaussian.ts
  18. 2 2
      src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts
  19. 2 1
      src/mol-geo/representation/util.ts
  20. 2 2
      src/mol-gl/shader/point.frag
  21. 5 5
      src/mol-io/common/binary-cif/array-encoder.ts
  22. 6 2
      src/mol-io/common/binary-cif/encoding.ts
  23. 6 3
      src/mol-io/reader/cif/data-model.ts
  24. 1 1
      src/mol-io/reader/cif/schema/bird.ts
  25. 1 1
      src/mol-io/reader/cif/schema/ccd.ts
  26. 227 1
      src/mol-io/reader/cif/schema/mmcif.ts
  27. 1 1
      src/mol-io/reader/cif/text/parser.ts
  28. 4 4
      src/mol-math/geometry/lookup3d/grid.ts
  29. 6 0
      src/mol-model/structure/export/mmcif.ts
  30. 11 4
      src/mol-model/structure/model/properties/utils/atomic-ranges.ts
  31. 48 20
      src/mol-model/structure/structure/carbohydrates/compute.ts
  32. 1 0
      src/mol-model/structure/structure/carbohydrates/data.ts
  33. 17 1
      src/mol-model/structure/structure/structure.ts
  34. 1 1
      src/mol-model/structure/structure/util/polymer.ts
  35. 25 2
      src/mol-model/structure/util.ts
  36. 8 0
      src/mol-util/color/color.ts
  37. 19 4
      src/mol-util/color/scale.ts
  38. 38 0
      src/mol-util/color/tables.ts
  39. 17 16
      src/mol-view/label.ts
  40. 21 12
      src/mol-view/theme/color.ts
  41. 59 0
      src/mol-view/theme/color/molecule-type.ts
  42. 82 0
      src/mol-view/theme/color/secondary-structure.ts
  43. 92 0
      src/mol-view/theme/color/sequence-id.ts
  44. 10 1
      src/servers/model/config.ts
  45. 2 1
      src/servers/model/preprocess/preprocess.ts
  46. 0 19
      src/servers/model/properties.ts
  47. 9 10
      src/servers/model/properties/pdbe.ts
  48. 18 0
      src/servers/model/properties/providers/pdbe.ts
  49. 12 0
      src/servers/model/properties/providers/rcsb.ts
  50. 8 3
      src/servers/model/properties/rcsb.ts
  51. 28 0
      src/servers/model/provider.ts
  52. 16 2
      src/servers/model/server/query.ts
  53. 11 7
      src/servers/model/server/structure-wrapper.ts
  54. 81 0
      web/render-test/index.js

+ 4 - 0
.dockerignore

@@ -0,0 +1,4 @@
+.*
+node_modules
+npm-debug.log
+*.sublime-project

+ 1 - 2
.gitignore

@@ -5,5 +5,4 @@ debug.log
 npm-debug.log
 
 *.sublime-workspace
-
-web/render-test/index.js
+.idea

+ 37 - 0
Dockerfile

@@ -0,0 +1,37 @@
+# This is to build a container that demos the Mol* canvas app
+# Source material: https://nodejs.org/en/docs/guides/nodejs-docker-webapp/
+# Source material: https://derickbailey.com/2017/05/31/how-a-650mb-node-js-image-for-docker-uses-less-space-than-a-50mb-image/
+# Source material: https://hub.docker.com/_/node/
+
+# Use the slimed NodeJS source, yielding a space savings of 600MB (~66% of total)
+FROM node:alpine
+
+# Create app directory
+WORKDIR /usr/src/app
+
+# Install app dependencies
+# A wildcard is used to ensure both package.json AND package-lock.json AND tslint.json AND tsconfig.json are copied
+# where available (npm@5+)
+COPY *.json ./
+
+# Install all dependencies and copy results
+RUN npm install
+COPY . .
+
+# Build library and canvas application then copy results
+RUN npm run build
+RUN npm run build-canvas
+COPY build/canvas/ build/canvas/
+
+# Open ports for HTTP
+EXPOSE 8080/tcp
+
+# Setup standalone simple webserver to run the demo
+RUN npm install http-server -g
+
+# Start NodeJS at container stand up
+CMD [ "http-server", "build/canvas/", "-p", "8080" ]
+
+# Developer helpers (what is inside this container?)
+RUN node -v
+RUN ls -alh

+ 13 - 0
README.md

@@ -78,6 +78,19 @@ From the root of the project:
 
 and navigate to `build/viewer`
 
+
+**Run via Docker**
+
+Build the docker image
+
+    docker build -t molstar-proto .
+
+Run the image
+
+    docker run -p 8080:8080 molstar-proto
+
+
+
 ### Code generation
 **CIF schemas**
 

+ 49 - 2
data/mmcif-field-names.csv

@@ -41,6 +41,12 @@ chem_comp_bond.atom_id_1
 chem_comp_bond.atom_id_2
 chem_comp_bond.value_order
 
+pdbx_chem_comp_identifier.comp_id
+pdbx_chem_comp_identifier.type
+pdbx_chem_comp_identifier.program
+pdbx_chem_comp_identifier.program_version
+pdbx_chem_comp_identifier.identifier
+
 cell.entry_id
 cell.length_a
 cell.length_b
@@ -66,8 +72,8 @@ entity_poly.entity_id
 entity_poly.type
 entity_poly.nstd_linkage
 entity_poly.nstd_monomer
-entity_poly.pdbx_seq_one_letter_code  
-entity_poly.pdbx_seq_one_letter_code_can 
+entity_poly.pdbx_seq_one_letter_code
+entity_poly.pdbx_seq_one_letter_code_can
 entity_poly.pdbx_strand_id
 entity_poly.pdbx_target_identifier
 
@@ -76,6 +82,47 @@ entity_poly_seq.num
 entity_poly_seq.mon_id
 entity_poly_seq.hetero
 
+pdbx_entity_branch.entity_id
+pdbx_entity_branch.type
+
+pdbx_entity_branch_list.entity_id
+pdbx_entity_branch_list.comp_id
+pdbx_entity_branch_list.num
+pdbx_entity_branch_list.component_comp_id
+pdbx_entity_branch_list.hetero
+
+pdbx_entity_branch_link.link_id
+pdbx_entity_branch_link.entity_id
+pdbx_entity_branch_link.entity_branch_list_num_1
+pdbx_entity_branch_link.comp_id_1
+pdbx_entity_branch_link.atom_id_1
+pdbx_entity_branch_link.leaving_atom_id_1
+pdbx_entity_branch_link.atom_stereo_config_1
+pdbx_entity_branch_link.entity_branch_list_num_2
+pdbx_entity_branch_link.comp_id_2
+pdbx_entity_branch_link.atom_id_2
+pdbx_entity_branch_link.leaving_atom_id_2
+pdbx_entity_branch_link.atom_stereo_config_2
+pdbx_entity_branch_link.value_order
+pdbx_entity_branch_link.details
+
+pdbx_branch_scheme.asym_id
+pdbx_branch_scheme.entity_id
+pdbx_branch_scheme.mon_id
+pdbx_branch_scheme.num
+pdbx_branch_scheme.auth_seq_num
+pdbx_branch_scheme.auth_mon_id
+pdbx_branch_scheme.auth_strand_id
+pdbx_branch_scheme.auth_ins_code
+pdbx_branch_scheme.hetero
+
+pdbx_entity_descriptor.ordinal
+pdbx_entity_descriptor.entity_id
+pdbx_entity_descriptor.descriptor
+pdbx_entity_descriptor.type
+pdbx_entity_descriptor.program
+pdbx_entity_descriptor.program_version
+
 entry.id
 
 exptl.entry_id

+ 108 - 118
package-lock.json

@@ -143,12 +143,6 @@
         "@types/range-parser": "*"
       }
     },
-    "@types/graphql": {
-      "version": "0.12.6",
-      "resolved": "http://registry.npmjs.org/@types/graphql/-/graphql-0.12.6.tgz",
-      "integrity": "sha512-wXAVyLfkG1UMkKOdMijVWFky39+OD/41KftzqfX1Oejd0Gm6dOIKjCihSVECg6X7PHjftxXmfOKA/d1H79ZfvQ==",
-      "dev": true
-    },
     "@types/handlebars": {
       "version": "4.0.39",
       "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.39.tgz",
@@ -174,9 +168,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "10.9.4",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz",
-      "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==",
+      "version": "10.10.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.10.1.tgz",
+      "integrity": "sha512-nzsx28VwfaIykfzMAG9TB3jxF5Nn+1/WMKnmVZc8TsB+LMIVvwUscVn7PAq+LFaY5ng5u4jp5mRROSswo76PPA==",
       "dev": true
     },
     "@types/node-fetch": {
@@ -587,14 +581,13 @@
       }
     },
     "apollo-link": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.2.tgz",
-      "integrity": "sha512-Uk/BC09dm61DZRDSu52nGq0nFhq7mcBPTjy5EEH1eunJndtCaNXQhQz/BjkI2NdrfGI+B+i5he6YSoRBhYizdw==",
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.3.tgz",
+      "integrity": "sha512-iL9yS2OfxYhigme5bpTbmRyC+Htt6tyo2fRMHT3K1XRL/C5IQDDz37OjpPy4ndx7WInSvfSZaaOTKFja9VWqSw==",
       "dev": true,
       "requires": {
-        "@types/graphql": "0.12.6",
         "apollo-utilities": "^1.0.0",
-        "zen-observable-ts": "^0.8.9"
+        "zen-observable-ts": "^0.8.10"
       }
     },
     "apollo-utilities": {
@@ -1437,6 +1430,15 @@
         "pako": "~1.0.5"
       }
     },
+    "bs-logger": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.5.tgz",
+      "integrity": "sha512-uFLE0LFMxrH8Z5Hd9QgivvRbrl/NFkOTHzGhlqQxsnmx5JBLrp4bc249afLL+GccyY/8hkcGi2LpVaOzaEY0nQ==",
+      "dev": true,
+      "requires": {
+        "fast-json-stable-stringify": "^2.0.0"
+      }
+    },
     "bser": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz",
@@ -1673,9 +1675,9 @@
       }
     },
     "chownr": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
-      "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
+      "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
       "dev": true
     },
     "chrome-trace-event": {
@@ -1787,12 +1789,6 @@
         }
       }
     },
-    "closest-file-data": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmjs.org/closest-file-data/-/closest-file-data-0.1.4.tgz",
-      "integrity": "sha1-l1+HwTLymdJKA3W59jyj+4j3Kzo=",
-      "dev": true
-    },
     "co": {
       "version": "4.6.0",
       "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3321,17 +3317,6 @@
         "readable-stream": "^2.0.0"
       }
     },
-    "fs-extra": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
-      "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.1.2",
-        "jsonfile": "^4.0.0",
-        "universalify": "^0.1.0"
-      }
-    },
     "fs-write-stream-atomic": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
@@ -4313,9 +4298,9 @@
       }
     },
     "graphql-code-generator": {
-      "version": "0.12.2",
-      "resolved": "https://registry.npmjs.org/graphql-code-generator/-/graphql-code-generator-0.12.2.tgz",
-      "integrity": "sha512-y8fEQzYe+Nxvc4R4etQvX4MPFmlU+VguiIiOqtJGcT4vuTRa2O6LK8QKC37NikbISC2/Iy334443Ug1AD08hSQ==",
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/graphql-code-generator/-/graphql-code-generator-0.12.5.tgz",
+      "integrity": "sha512-TZyNKCQ1uiO5Drwvozeic21K2EbEHdfNXGZ4Z2mL5LDh0/fu+hVL8Mww/I8/rE8HD1FGzjuaOtrU94NwZoK/kA==",
       "dev": true,
       "requires": {
         "@graphql-modules/epoxy": "0.1.6",
@@ -4328,8 +4313,8 @@
         "commander": "2.18.0",
         "fb-watchman": "2.0.0",
         "glob": "7.1.3",
-        "graphql-codegen-compiler": "0.12.2",
-        "graphql-codegen-core": "0.12.2",
+        "graphql-codegen-compiler": "0.12.5",
+        "graphql-codegen-core": "0.12.5",
         "graphql-import": "0.7.1",
         "is-glob": "4.0.0",
         "is-valid-path": "0.1.1",
@@ -4489,23 +4474,23 @@
       }
     },
     "graphql-codegen-compiler": {
-      "version": "0.12.2",
-      "resolved": "https://registry.npmjs.org/graphql-codegen-compiler/-/graphql-codegen-compiler-0.12.2.tgz",
-      "integrity": "sha512-0S9liwQw9Yr1FmmViHukd3H0y9TyVI0SJ0Po2q+FDhaybuD8NBISfI/2BGok341/CQrIQezKvdPyd1A3XVCKmA==",
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/graphql-codegen-compiler/-/graphql-codegen-compiler-0.12.5.tgz",
+      "integrity": "sha512-oP5FCKcvuFIbilpfQsBOi7a1Sb7YaPQ5gy1eJ8pqhtsdhfZuLOniNmZuMt4FoYaBBizHx+Gcs+TljqL3kxt+lA==",
       "dev": true,
       "requires": {
         "@types/handlebars": "4.0.39",
         "change-case": "3.0.2",
         "common-tags": "1.8.0",
-        "graphql-codegen-core": "0.12.2",
+        "graphql-codegen-core": "0.12.5",
         "handlebars": "4.0.12",
         "moment": "2.22.2"
       }
     },
     "graphql-codegen-core": {
-      "version": "0.12.2",
-      "resolved": "https://registry.npmjs.org/graphql-codegen-core/-/graphql-codegen-core-0.12.2.tgz",
-      "integrity": "sha512-fZcsacdD9tBkxstXd6dXygWBYNgXRwBTpEfSaSpCIl9ZJsBqAOx9R86bgHbMoRbaFb7rjlPxMdfnFKuTA6MMTw==",
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/graphql-codegen-core/-/graphql-codegen-core-0.12.5.tgz",
+      "integrity": "sha512-KKTDTClEQTdMrRMkkMhbVB0z/3u6N43Ey5ORdYYAgpCO6WD5fPkJfnldsJjE5Ys83nGJZm00UtERId9AUTv+HQ==",
       "dev": true,
       "requires": {
         "graphql-tag": "2.9.2",
@@ -4564,9 +4549,9 @@
       }
     },
     "graphql-codegen-typescript-template": {
-      "version": "0.12.2",
-      "resolved": "https://registry.npmjs.org/graphql-codegen-typescript-template/-/graphql-codegen-typescript-template-0.12.2.tgz",
-      "integrity": "sha512-zsz820TLmIQZPLrh79R/NIr7AQbsIGw18eZL6wInaOdK+JCS+bqLYOudrewYkeUwZdRlNTk5CEPjHxANA5gKKw==",
+      "version": "0.12.5",
+      "resolved": "https://registry.npmjs.org/graphql-codegen-typescript-template/-/graphql-codegen-typescript-template-0.12.5.tgz",
+      "integrity": "sha512-2FXJIE1Fcjh0St49H6GoKddjzkxWhdzVxVoVEYX2d+mZr59b6KTtRYPxcSlztjNwvX6PSbQsZoZcJmU6zertDA==",
       "dev": true
     },
     "graphql-import": {
@@ -4873,9 +4858,9 @@
       "dev": true
     },
     "immer": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/immer/-/immer-1.6.0.tgz",
-      "integrity": "sha512-DxsP1sFSI85TQpNwBAH4m485FFPPtLc0rJyrWuLxSk97W6/BSnvZ0mksaw6p35KwhNNKXZ7WT2gU6I6FiWG+Eg=="
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-1.7.1.tgz",
+      "integrity": "sha512-EhaCOnzhNazyzfWVKVUIQwcNwKMK+bpyBRsFajul8eivaloB/JtCjSoZwUR9Ml/+ZhuPXufnp7QYvvSJ2ye/Eg=="
     },
     "import-local": {
       "version": "1.0.0",
@@ -5956,15 +5941,6 @@
       "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
       "dev": true
     },
-    "jsonfile": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
-      "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.1.6"
-      }
-    },
     "jsonify": {
       "version": "0.0.0",
       "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
@@ -6224,9 +6200,9 @@
       "dev": true
     },
     "logform": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/logform/-/logform-1.9.1.tgz",
-      "integrity": "sha512-ZHrZE8VSf7K3xKxJiQ1aoTBp2yK+cEbFcgarsjzI3nt3nE/3O0heNSppoOQMUJVMZo/xiVwCxiXIabaZApsKNQ==",
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz",
+      "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==",
       "dev": true,
       "requires": {
         "colors": "^1.2.1",
@@ -6304,6 +6280,12 @@
         }
       }
     },
+    "make-error": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
+      "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
+      "dev": true
+    },
     "makeerror": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
@@ -6507,9 +6489,9 @@
       "dev": true
     },
     "mini-css-extract-plugin": {
-      "version": "0.4.2",
-      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.2.tgz",
-      "integrity": "sha512-ots7URQH4wccfJq9Ssrzu2+qupbncAce4TmTzunI9CIwlQMp2XI+WNUw6xWF6MMAGAm1cbUVINrSjATaVMyKXg==",
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.3.tgz",
+      "integrity": "sha512-Mxs0nxzF1kxPv4TRi2NimewgXlJqh0rGE30vviCU2WHrpbta6wklnUV9dr9FUtoAHmB3p3LeXEC+ZjgHvB0Dzg==",
       "dev": true,
       "requires": {
         "loader-utils": "^1.1.0",
@@ -7852,25 +7834,25 @@
       "dev": true
     },
     "react": {
-      "version": "16.5.1",
-      "resolved": "https://registry.npmjs.org/react/-/react-16.5.1.tgz",
-      "integrity": "sha512-E+23+rbpPsJgSX812LQkwupUCFnbVE84+L8uxlkqN5MU0DcraWMlVf9cRvKCKtGu0XvScyRnW7Z+9d7ymkjy3A==",
+      "version": "16.5.2",
+      "resolved": "https://registry.npmjs.org/react/-/react-16.5.2.tgz",
+      "integrity": "sha512-FDCSVd3DjVTmbEAjUNX6FgfAmQ+ypJfHUsqUJOYNCBUp1h8lqmtC+0mXJ+JjsWx4KAVTkk1vKd1hLQPvEviSuw==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "schedule": "^0.4.0"
+        "schedule": "^0.5.0"
       }
     },
     "react-dom": {
-      "version": "16.5.1",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.5.1.tgz",
-      "integrity": "sha512-l4L9GtX7ezgnDIIr6AaNvGBM4BiK0fSs4/V8bdsu9X6xqrtHr+jp6auT0hbHpN7bH9WRvDBZceWQ9WJ3lGCIvQ==",
+      "version": "16.5.2",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.5.2.tgz",
+      "integrity": "sha512-RC8LDw8feuZOHVgzEf7f+cxBr/DnKdqp56VU0lAs1f4UfKc4cU8wU4fTq/mgnvynLQo8OtlPC19NUFh/zjZPuA==",
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
         "prop-types": "^15.6.2",
-        "schedule": "^0.4.0"
+        "schedule": "^0.5.0"
       }
     },
     "read-pkg": {
@@ -8766,9 +8748,9 @@
       "dev": true
     },
     "schedule": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/schedule/-/schedule-0.4.0.tgz",
-      "integrity": "sha512-hYjmoaEMojiMkWCxKr6ue+LYcZ29u29+AamWYmzwT2VOO9ws5UJp/wNhsVUPiUeNh+EdRfZm7nDeB40ffTfMhA==",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz",
+      "integrity": "sha512-HUcJicG5Ou8xfR//c2rPT0lPIRR09vVvN81T9fqfVgBmhERUbDEQoYKjpBxbueJnCPpSu2ujXzOnRQt6x9o/jw==",
       "requires": {
         "object-assign": "^4.1.1"
       }
@@ -9853,15 +9835,45 @@
       }
     },
     "ts-jest": {
-      "version": "23.1.4",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.1.4.tgz",
-      "integrity": "sha512-9rCSxbWfoZxxeXnSoEIzRNr9hDIQ8iEJAWmSRsWhDHDT8OeuGfURhJQUE8jtJlkyEygs6rngH8RYtHz9cfjmEA==",
-      "dev": true,
-      "requires": {
-        "closest-file-data": "^0.1.4",
-        "fs-extra": "6.0.1",
-        "json5": "^0.5.0",
-        "lodash": "^4.17.10"
+      "version": "23.10.0",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-23.10.0.tgz",
+      "integrity": "sha512-SbqUbCRjlPKQjm9kANW3FebLx4iLxJG/HlK+Ds3nuVlr5Z3kX7YSES/OuIPwX/mPUds4MlA5W+/C4H/njztqtw==",
+      "dev": true,
+      "requires": {
+        "bs-logger": "0.x",
+        "buffer-from": "1.x",
+        "fast-json-stable-stringify": "2.x",
+        "json5": "2.x",
+        "make-error": "1.x",
+        "mkdirp": "0.x",
+        "semver": "5.x",
+        "yargs-parser": "10.x"
+      },
+      "dependencies": {
+        "json5": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-2.0.1.tgz",
+          "integrity": "sha512-t6N/86QDIRYvOL259jR5c5TbtMnekl2Ib314mGeMh37zAwjgbWHieqijPH7pWaogmJq1F2I4Sphg19U1s+ZnXQ==",
+          "dev": true,
+          "requires": {
+            "minimist": "^1.2.0"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "yargs-parser": {
+          "version": "10.1.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz",
+          "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==",
+          "dev": true,
+          "requires": {
+            "camelcase": "^4.1.0"
+          }
+        }
       }
     },
     "ts-log": {
@@ -10084,12 +10096,6 @@
         "imurmurhash": "^0.1.4"
       }
     },
-    "universalify": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
-      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
-      "dev": true
-    },
     "unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -10697,9 +10703,9 @@
       "dev": true
     },
     "webpack": {
-      "version": "4.19.0",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.19.0.tgz",
-      "integrity": "sha512-Ak3mMGtA8F1ar4ZP6VCLiICNIPoillROGYstnEd+LzI5Tkvz0qTITeTMcAFjxyYsaxu98F97yrCWdcxRUMPAYw==",
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.19.1.tgz",
+      "integrity": "sha512-j7Q/5QqZRqIFXJvC0E59ipLV5Hf6lAnS3ezC3I4HMUybwEDikQBVad5d+IpPtmaQPQArvgUZLXIN6lWijHBn4g==",
       "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.7.6",
@@ -11032,12 +11038,6 @@
             "to-regex": "^3.0.2"
           }
         },
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
-        },
         "tapable": {
           "version": "1.1.0",
           "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz",
@@ -11052,16 +11052,6 @@
           "requires": {
             "punycode": "^2.1.0"
           }
-        },
-        "webpack-sources": {
-          "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.2.0.tgz",
-          "integrity": "sha512-9BZwxR85dNsjWz3blyxdOhTgtnQvv3OEs5xofI0wPYTwu5kaWxS08UuD1oI7WLBLpRO+ylf0ofnXLXWmGb2WMw==",
-          "dev": true,
-          "requires": {
-            "source-list-map": "^2.0.0",
-            "source-map": "~0.6.1"
-          }
         }
       }
     },
@@ -11224,9 +11214,9 @@
       }
     },
     "webpack-sources": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz",
-      "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.2.0.tgz",
+      "integrity": "sha512-9BZwxR85dNsjWz3blyxdOhTgtnQvv3OEs5xofI0wPYTwu5kaWxS08UuD1oI7WLBLpRO+ylf0ofnXLXWmGb2WMw==",
       "dev": true,
       "requires": {
         "source-list-map": "^2.0.0",
@@ -11546,9 +11536,9 @@
       "dev": true
     },
     "zen-observable-ts": {
-      "version": "0.8.9",
-      "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.9.tgz",
-      "integrity": "sha512-KJz2O8FxbAdAU5CSc8qZ1K2WYEJb1HxS6XDRF+hOJ1rOYcg6eTMmS9xYHCXzqZZzKw6BbXWyF4UpwSsBQnHJeA==",
+      "version": "0.8.10",
+      "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.10.tgz",
+      "integrity": "sha512-5vqMtRggU/2GhePC9OU4sYEWOdvmayp2k3gjPf4F0mXwB3CSbbNznfDUvDJx9O2ZTa1EIXdJhPchQveFKwNXPQ==",
       "dev": true,
       "requires": {
         "zen-observable": "^0.8.0"

+ 9 - 9
package.json

@@ -76,7 +76,7 @@
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.0",
     "@types/jest": "^23.3.2",
-    "@types/node": "^10.9.4",
+    "@types/node": "^10.10.1",
     "@types/node-fetch": "^2.1.2",
     "@types/react": "^16.4.14",
     "@types/react-dom": "^16.0.7",
@@ -87,22 +87,22 @@
     "file-loader": "^2.0.0",
     "glslify-import": "^3.1.0",
     "glslify-loader": "^1.0.2",
-    "graphql-code-generator": "^0.12.2",
-    "graphql-codegen-typescript-template": "^0.12.2",
+    "graphql-code-generator": "^0.12.5",
+    "graphql-codegen-typescript-template": "^0.12.5",
     "jest": "^23.6.0",
     "jest-raw-loader": "^1.0.1",
-    "mini-css-extract-plugin": "^0.4.2",
+    "mini-css-extract-plugin": "^0.4.3",
     "node-sass": "^4.9.3",
     "raw-loader": "^0.5.1",
     "resolve-url-loader": "^3.0.0",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.0",
-    "ts-jest": "^23.1.4",
+    "ts-jest": "^23.10.0",
     "tslint": "^5.11.0",
     "typescript": "^3.0.3",
     "uglify-js": "^3.4.9",
     "util.promisify": "^1.0.0",
-    "webpack": "^4.19.0",
+    "webpack": "^4.19.1",
     "webpack-cli": "^3.1.0"
   },
   "dependencies": {
@@ -111,10 +111,10 @@
     "express": "^4.16.3",
     "graphql": "^14.0.2",
     "graphql-request": "^1.8.2",
-    "immer": "^1.6.0",
+    "immer": "^1.7.1",
     "node-fetch": "^2.2.0",
-    "react": "^16.5.1",
-    "react-dom": "^16.5.1",
+    "react": "^16.5.2",
+    "react-dom": "^16.5.2",
     "rxjs": "^6.3.2"
   }
 }

+ 27 - 4
src/apps/canvas/app.ts

@@ -32,23 +32,46 @@ export class App {
         }
     }
 
+    setStatus(msg: string) {
+
+    }
+
+    private taskCount = 0
+    taskCountChanged = new BehaviorSubject({ count: 0, info: '' })
+
+    private changeTaskCount(delta: number, info = '') {
+        this.taskCount += delta
+        this.taskCountChanged.next({ count: this.taskCount, info })
+    }
+
+    async runTask<T>(promise: Promise<T>, info: string) {
+        this.changeTaskCount(1, info)
+        let result: T
+        try {
+            result = await promise
+        } finally {
+            this.changeTaskCount(-1)
+        }
+        return result
+    }
+
     async loadCif(cif: CifBlock, assemblyId?: string) {
-        const models = await getModelsFromMmcif(cif)
-        this.structureView = await StructureView(this.viewer, models, { assemblyId })
+        const models = await this.runTask(getModelsFromMmcif(cif), 'Build models')
+        this.structureView = await this.runTask(StructureView(this, this.viewer, models, { assemblyId }), 'Init structure view')
         this.pdbIdLoaded.next(this.structureView)
     }
 
     async loadPdbIdOrUrl(idOrUrl: string, options?: { assemblyId?: string, binary?: boolean }) {
         if (this.structureView) this.structureView.destroy();
         const url = idOrUrl.length <= 4 ? `https://files.rcsb.org/download/${idOrUrl}.cif` : idOrUrl;
-        const cif = await getCifFromUrl(url, options ? !!options.binary : false)
+        const cif = await this.runTask(getCifFromUrl(url, options ? !!options.binary : false), 'Load mmCIF from URL')
         this.loadCif(cif, options ? options.assemblyId : void 0)
     }
 
     async loadCifFile(file: File) {
         if (this.structureView) this.structureView.destroy();
         const binary = /\.bcif$/.test(file.name);
-        const cif = await getCifFromFile(file, binary)
+        const cif = await this.runTask(getCifFromFile(file, binary), 'Load mmCIF from file')
         this.loadCif(cif)
     }
 }

+ 7 - 5
src/apps/canvas/component/structure-representation.tsx

@@ -12,8 +12,10 @@ import { Color } from 'mol-util/color';
 import { Progress } from 'mol-task';
 import { VisualQuality, VisualQualityNames } from 'mol-geo/geometry/geometry';
 import { SizeThemeProps } from 'mol-view/theme/size';
+import { App } from '../app';
 
 export interface StructureRepresentationComponentProps {
+    app: App
     viewer: Viewer
     representation: StructureRepresentation<StructureProps>
 }
@@ -82,9 +84,9 @@ export class StructureRepresentationComponent extends React.Component<StructureR
         if (state.pointFilledCircle !== undefined) (props as any).pointFilledCircle = state.pointFilledCircle
         if (state.pointEdgeBleach !== undefined) (props as any).pointEdgeBleach = state.pointEdgeBleach
 
-        await repr.createOrUpdate(props).run(
+        await this.props.app.runTask(repr.createOrUpdate(props).run(
             progress => console.log(Progress.format(progress))
-        )
+        ), 'Create/update representation')
         this.props.viewer.add(repr)
         this.props.viewer.draw(true)
         console.log(this.props.viewer.stats)
@@ -163,7 +165,7 @@ export class StructureRepresentationComponent extends React.Component<StructureR
                     <input type='range'
                         defaultValue={this.state.radiusOffset.toString()}
                         min='0'
-                        max='10'
+                        max='4'
                         step='0.1'
                         onInput={(e) => this.update({ radiusOffset: parseFloat(e.currentTarget.value) })}
                     >
@@ -220,8 +222,8 @@ export class StructureRepresentationComponent extends React.Component<StructureR
                                     background: `linear-gradient(to right, ${ct.legend.colors.map(c => Color.toStyle(c)).join(', ')})`
                                 }}
                             >
-                                <span style={{float: 'left', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.min}</span>
-                                <span style={{float: 'right', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.max}</span>
+                                <span style={{float: 'left', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.minLabel}</span>
+                                <span style={{float: 'right', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.maxLabel}</span>
                             </div>
                         : ct.legend && ct.legend.kind === 'table-legend'
                             ? <div>

+ 1 - 0
src/apps/canvas/component/structure-view.tsx

@@ -185,6 +185,7 @@ export class StructureViewComponent extends React.Component<StructureViewCompone
                                 <StructureRepresentationComponent
                                     representation={structureRepresentations[k]}
                                     viewer={structureView.viewer}
+                                    app={structureView.app}
                                 />
                             </div>
                         } else {

+ 26 - 6
src/apps/canvas/component/viewport.tsx

@@ -15,8 +15,9 @@ interface ViewportProps {
 }
 
 interface ViewportState {
-    noWebGl: boolean,
-    info: string
+    noWebGl: boolean
+    pickingInfo: string
+    taskInfo: string
 }
 
 export class Viewport extends React.Component<ViewportProps, ViewportState> {
@@ -25,7 +26,8 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> {
 
     state: ViewportState = {
         noWebGl: false,
-        info: ''
+        pickingInfo: '',
+        taskInfo: ''
     };
 
     handleResize() {
@@ -55,11 +57,15 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> {
                     prevLoci = loci
 
                     const label = labelFirst(loci)
-                    const info = `${label}`
-                    this.setState({ info })
+                    const pickingInfo = `${label}`
+                    this.setState({ pickingInfo })
                 }
             }
         })
+
+        this.props.app.taskCountChanged.subscribe(({ count, info }) => {
+            this.setState({ taskInfo: count > 0 ? info : '' })
+        })
     }
 
     componentWillUnmount() {
@@ -94,8 +100,22 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> {
                     background: 'rgba(0, 0, 0, 0.2)'
                 }}
             >
-                {this.state.info}
+                {this.state.pickingInfo}
             </div>
+            { this.state.taskInfo ?
+                <div
+                    style={{
+                        position: 'absolute',
+                        top: 10,
+                        right: 10,
+                        padding: 10,
+                        color: 'lightgrey',
+                        background: 'rgba(0, 0, 0, 0.2)'
+                    }}
+                >
+                    {this.state.taskInfo}
+                </div>
+            : '' }
         </div>;
     }
 }

+ 10 - 7
src/apps/canvas/structure-view.ts

@@ -25,9 +25,11 @@ import { BehaviorSubject } from 'rxjs';
 import { SpacefillRepresentation } from 'mol-geo/representation/structure/representation/spacefill';
 import { DistanceRestraintRepresentation } from 'mol-geo/representation/structure/representation/distance-restraint';
 import { SurfaceRepresentation } from 'mol-geo/representation/structure/representation/surface';
-// import { Progress } from 'mol-task';
+import { App } from './app';
+import { Progress } from 'mol-task';
 
 export interface StructureView {
+    readonly app: App
     readonly viewer: Viewer
 
     readonly label: string
@@ -64,7 +66,7 @@ interface StructureViewProps {
 
 
 
-export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>, props: StructureViewProps = {}): Promise<StructureView> {
+export async function StructureView(app: App, viewer: Viewer, models: ReadonlyArray<Model>, props: StructureViewProps = {}): Promise<StructureView> {
     const active: { [k: string]: boolean } = {
         cartoon: true,
         point: false,
@@ -105,7 +107,7 @@ export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>
         if (!value) {
             assemblySymmetry = undefined
         } else {
-            await AssemblySymmetry.attachFromCifOrAPI(models[modelId])
+            await app.runTask(AssemblySymmetry.attachFromCifOrAPI(models[modelId]), 'Load symmetry annotation')
             assemblySymmetry = AssemblySymmetry.get(models[modelId])
         }
         active.symmetryAxes = value
@@ -195,7 +197,7 @@ export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>
     }
 
     async function getStructure() {
-        if (model) structure = await getStructureFromModel(model, assemblyId)
+        if (model) structure = await app.runTask(getStructureFromModel(model, assemblyId), 'Build structure')
         if (model && structure) {
             label = `${model.label} - Assembly ${assemblyId}`
         } else {
@@ -209,9 +211,9 @@ export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>
             console.log('createStructureRepr')
             for (const k in structureRepresentations) {
                 if (active[k]) {
-                    await structureRepresentations[k].createOrUpdate({}, structure).run(
-                        // progress => console.log(Progress.format(progress))
-                    )
+                    await app.runTask(structureRepresentations[k].createOrUpdate({}, structure).run(
+                        progress => console.log(Progress.format(progress))
+                    ), 'Create/update representation')
                     viewer.add(structureRepresentations[k])
                 } else {
                     viewer.remove(structureRepresentations[k])
@@ -288,6 +290,7 @@ export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>
     await setModel(0, props.assemblyId, props.symmetryFeatureId)
 
     return {
+        app,
         viewer,
 
         get label() { return label },

+ 21 - 6
src/apps/schema-generator/schema-from-cif-dic.ts

@@ -24,11 +24,17 @@ async function runGenerateSchema(name: string, fieldNamesPath?: string, typescri
     const ihmDic = await CIF.parseText(fs.readFileSync(IHM_DIC_PATH, 'utf8')).run();
     if (ihmDic.isError) throw ihmDic
 
+    await ensureBranchDicAvailable()
+    const branchDic = await CIF.parseText(fs.readFileSync(BRANCH_DIC_PATH, 'utf8')).run();
+    if (branchDic.isError) throw branchDic
+
     const mmcifDicVersion = CIF.schema.dic(mmcifDic.result.blocks[0]).dictionary.version.value(0)
     const ihmDicVersion = CIF.schema.dic(ihmDic.result.blocks[0]).dictionary.version.value(0)
-    const version = `Dictionary versions: mmCIF ${mmcifDicVersion}, IHM ${ihmDicVersion}.`
+    // const branchDicVersion = CIF.schema.dic(branchDic.result.blocks[0]).dictionary.version.value(0)
+    const branchDicVersion = 'draft'
+    const version = `Dictionary versions: mmCIF ${mmcifDicVersion}, IHM ${ihmDicVersion}, entity_branch ${branchDicVersion}.`
 
-    const frames: CifFrame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames]
+    const frames: CifFrame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames, ...branchDic.result.blocks[0].saveFrames]
     const schema = generateSchema(frames)
 
     const filter = fieldNamesPath ? await getFieldNamesFilter(fieldNamesPath) : undefined
@@ -70,15 +76,20 @@ async function ensureIhmDicAvailable() {
     await ensureDicAvailable(IHM_DIC_PATH, IHM_DIC_URL)
 }
 
+async function ensureBranchDicAvailable() {
+    await ensureDicAvailable(BRANCH_DIC_PATH, BRANCH_DIC_URL)
+}
+
 async function ensureDicAvailable(dicPath: string, dicUrl: string) {
     if (FORCE_DIC_DOWNLOAD || !fs.existsSync(dicPath)) {
-        console.log('downloading mmcif dic...')
+        const name = dicUrl.substr(dicUrl.lastIndexOf('/') + 1)
+        console.log(`downloading ${name}...`)
         const data = await fetch(dicUrl)
         if (!fs.existsSync(DIC_DIR)) {
             fs.mkdirSync(DIC_DIR);
         }
         fs.writeFileSync(dicPath, await data.text())
-        console.log('done downloading mmcif dic')
+        console.log(`done downloading ${name}`)
     }
 }
 
@@ -87,10 +98,12 @@ const MMCIF_DIC_PATH = `${DIC_DIR}/mmcif_pdbx_v50.dic`
 const MMCIF_DIC_URL = 'http://mmcif.wwpdb.org/dictionaries/ascii/mmcif_pdbx_v50.dic'
 const IHM_DIC_PATH = `${DIC_DIR}/ihm-extension.dic`
 const IHM_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/IHM-dictionary/master/ihm-extension.dic'
+const BRANCH_DIC_PATH = `${DIC_DIR}/entity_branch-extension.dic`
+const BRANCH_DIC_URL = 'https://raw.githubusercontent.com/wwpdb-dictionaries/mmcif_pdbx/master/extensions/entity_branch-extension.dic'
 
 const parser = new argparse.ArgumentParser({
   addHelp: true,
-  description: 'Create schema from mmcif dictionary (v50, downloaded from wwPDB)'
+  description: 'Create schema from mmcif dictionary (v50 plus IHM and entity_branch extensions, downloaded from wwPDB)'
 });
 parser.addArgument([ '--name', '-n' ], {
     defaultValue: 'mmCIF',
@@ -123,5 +136,7 @@ const args: Args = parser.parseArgs();
 const FORCE_DIC_DOWNLOAD = args.forceDicDownload
 
 if (args.name) {
-    runGenerateSchema(args.name, args.fieldNamesPath, args.typescript, args.out)
+    runGenerateSchema(args.name, args.fieldNamesPath, args.typescript, args.out).catch(e => {
+        console.error(e)
+    })
 }

+ 67 - 1
src/mol-geo/geometry/mesh/mesh.ts

@@ -15,6 +15,7 @@ import { createMarkers } from '../marker-data';
 import { TransformData } from '../transform-data';
 import { LocationIterator } from '../../util/location-iterator';
 import { createColors } from '../color-data';
+import { ChunkedArray } from 'mol-data/util';
 
 export interface Mesh {
     readonly kind: 'mesh',
@@ -80,7 +81,7 @@ export namespace Mesh {
             Vec3.fromArray(y, v, b);
             Vec3.fromArray(z, v, c);
             Vec3.sub(d1, z, y);
-            Vec3.sub(d2, y, x);
+            Vec3.sub(d2, x, y);
             Vec3.cross(n, d1, d2);
 
             normals[a] += n[0]; normals[a + 1] += n[1]; normals[a + 2] += n[2];
@@ -160,6 +161,71 @@ export namespace Mesh {
         });
     }
 
+    /**
+     * Ensure that each vertices of each triangle have the same group id.
+     * Note that normals are copied over and can't be re-created from the new mesh.
+     */
+    export function uniformTriangleGroup(mesh: Mesh) {
+        const { indexBuffer, vertexBuffer, groupBuffer, normalBuffer, triangleCount, vertexCount } = mesh
+        const ib = indexBuffer.ref.value
+        const vb = vertexBuffer.ref.value
+        const gb = groupBuffer.ref.value
+        const nb = normalBuffer.ref.value
+
+        // new
+        const index = ChunkedArray.create(Uint32Array, 3, 1024, triangleCount)
+
+        // re-use
+        const vertex = ChunkedArray.create(Float32Array, 3, 1024, vb)
+        vertex.currentIndex = vertexCount * 3
+        vertex.elementCount = vertexCount
+        const normal = ChunkedArray.create(Float32Array, 3, 1024, nb)
+        normal.currentIndex = vertexCount * 3
+        normal.elementCount = vertexCount
+        const group = ChunkedArray.create(Float32Array, 1, 1024, gb)
+        group.currentIndex = vertexCount
+        group.elementCount = vertexCount
+
+        const v = Vec3.zero()
+        const n = Vec3.zero()
+
+        function add(i: number) {
+            Vec3.fromArray(v, vb, i * 3)
+            Vec3.fromArray(n, nb, i * 3)
+            ChunkedArray.add3(vertex, v[0], v[1], v[2])
+            ChunkedArray.add3(normal, n[0], n[1], n[2])
+        }
+
+        let newVertexCount = vertexCount
+        for (let i = 0, il = triangleCount; i < il; ++i) {
+            const v0 = ib[i * 3], v1 = ib[i * 3 + 1], v2 = ib[i * 3 + 2]
+            const g0 = gb[v0], g1 = gb[v1], g2 = gb[v2]
+            if (g0 !== g1 || g0 !== g2) {
+                add(v0); add(v1); add(v2)
+                ChunkedArray.add3(index, newVertexCount, newVertexCount + 1, newVertexCount + 2)
+                const g = g1 === g2 ? g1 : g0
+                for (let j = 0; j < 3; ++j) ChunkedArray.add(group, g)
+                newVertexCount += 3
+            } else {
+                ChunkedArray.add3(index, v0, v1, v2)
+            }
+        }
+
+        const newIb = ChunkedArray.compact(index)
+        const newVb = ChunkedArray.compact(vertex)
+        const newNb = ChunkedArray.compact(normal)
+        const newGb = ChunkedArray.compact(group)
+
+        mesh.vertexCount = newVertexCount
+
+        ValueCell.update(vertexBuffer, newVb) as ValueCell<Float32Array>
+        ValueCell.update(groupBuffer, newGb) as ValueCell<Float32Array>
+        ValueCell.update(indexBuffer, newIb) as ValueCell<Uint32Array>
+        ValueCell.update(normalBuffer, newNb) as ValueCell<Float32Array>
+
+        return mesh
+    }
+
     //
 
     export const DefaultProps = {

+ 3 - 3
src/mol-geo/representation/structure/visual/gaussian-surface-mesh.ts

@@ -15,16 +15,18 @@ import { computeGaussianDensity, DefaultGaussianDensityProps } from './util/gaus
 
 async function createGaussianSurfaceMesh(ctx: RuntimeContext, unit: Unit, structure: Structure, props: GaussianSurfaceProps, mesh?: Mesh): Promise<Mesh> {
     const { smoothness } = props
-    const { transform, field } = await computeGaussianDensity(unit, structure, props).runAsChild(ctx)
+    const { transform, field, idField } = await computeGaussianDensity(unit, structure, props).runAsChild(ctx)
 
     const surface = await computeMarchingCubes({
         isoLevel: Math.exp(-smoothness),
         scalarField: field,
+        idField,
         oldSurface: mesh
     }).runAsChild(ctx)
 
     Mesh.transformImmediate(surface, transform)
     Mesh.computeNormalsImmediate(surface)
+    Mesh.uniformTriangleGroup(surface)
 
     return surface;
 }
@@ -32,8 +34,6 @@ async function createGaussianSurfaceMesh(ctx: RuntimeContext, unit: Unit, struct
 export const DefaultGaussianSurfaceProps = {
     ...DefaultUnitsMeshProps,
     ...DefaultGaussianDensityProps,
-
-    flipSided: true, // TODO should not be required
 }
 export type GaussianSurfaceProps = typeof DefaultGaussianSurfaceProps
 

+ 2 - 0
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -36,6 +36,7 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, structure
     const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2
     const builder = MeshBuilder.create(vertexCount, vertexCount / 10, mesh)
 
+    const isCoarse = Unit.isCoarse(unit)
     const state = createCurveSegmentState(linearSegments)
     const { curvePoints, normalVectors, binormalVectors } = state
 
@@ -54,6 +55,7 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, structure
         interpolateCurveSegment(state, v, tension, shift)
 
         let width = sizeTheme.size(v.center)
+        if (isCoarse) width *= aspectRatio / 2
 
         if (isSheet) {
             const height = width * aspectRatio

+ 20 - 11
src/mol-geo/representation/structure/visual/util/gaussian.ts

@@ -11,7 +11,7 @@ import { Box3D } from 'mol-math/geometry';
 import { SizeTheme } from 'mol-view/theme/size';
 
 export const DefaultGaussianDensityProps = {
-    resolutionFactor: 7,
+    resolutionFactor: 6,
     radiusOffset: 0,
     smoothness: 1.5,
 }
@@ -28,7 +28,7 @@ function getDelta(box: Box3D, resolutionFactor: number) {
     return delta
 }
 
-type Density = { transform: Mat4, field: Tensor }
+type Density = { transform: Mat4, field: Tensor, idField: Tensor }
 
 export function computeGaussianDensity(unit: Unit, structure: Structure, props: GaussianDensityProps) {
     return Task.create('Gaussian Density', async ctx => {
@@ -62,6 +62,11 @@ export async function GaussianDensity(ctx: RuntimeContext, unit: Unit, structure
     const data = space.create()
     const field = Tensor.create(space, data)
 
+    const idData = space.create()
+    const idField = Tensor.create(space, idData)
+
+    const densData = space.create()
+
     const c = Vec3.zero()
 
     const alpha = smoothness
@@ -74,6 +79,8 @@ export async function GaussianDensity(ctx: RuntimeContext, unit: Unit, structure
     const beg = Vec3.zero()
     const end = Vec3.zero()
 
+    const gridPad = 1 / Math.max(...delta)
+
     for (let i = 0; i < elementCount; i++) {
         l.element = elements[i]
         pos(elements[i], v)
@@ -84,7 +91,7 @@ export async function GaussianDensity(ctx: RuntimeContext, unit: Unit, structure
         const radius = sizeTheme.size(l) + radiusOffset
         const rSq = radius * radius
 
-        const r2 = (radiusOffset + radius * 2)
+        const r2 = radiusOffset + radius * 2 + gridPad
         const radius2 = Vec3.create(r2, r2, r2)
         Vec3.mul(radius2, radius2, delta)
         const r2sq = r2 * r2
@@ -99,7 +106,12 @@ export async function GaussianDensity(ctx: RuntimeContext, unit: Unit, structure
                     Vec3.div(p, p, delta)
                     const distSq = Vec3.squaredDistance(p, v)
                     if (distSq <= r2sq) {
-                        space.add(data, x, y, z, Math.exp(-alpha * (distSq / rSq)))
+                        const dens = Math.exp(-alpha * (distSq / rSq))
+                        space.add(data, x, y, z, dens)
+                        if (dens > space.get(densData, x, y, z)) {
+                            space.set(densData, x, y, z, dens)
+                            space.set(idData, x, y, z, i)
+                        }
                     }
                 }
             }
@@ -110,12 +122,9 @@ export async function GaussianDensity(ctx: RuntimeContext, unit: Unit, structure
         }
     }
 
-    const t = Mat4.identity()
-    Mat4.fromScaling(t, Vec3.inverse(Vec3.zero(), delta))
-    Mat4.setTranslation(t, expandedBox.min)
+    const transform = Mat4.identity()
+    Mat4.fromScaling(transform, Vec3.inverse(Vec3.zero(), delta))
+    Mat4.setTranslation(transform, expandedBox.min)
 
-    return {
-        field,
-        transform: t
-    }
+    return { field, idField, transform }
 }

+ 2 - 2
src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts

@@ -11,7 +11,7 @@ import Iterator from 'mol-data/iterator';
 import { Vec3 } from 'mol-math/linear-algebra';
 import SortedRanges from 'mol-data/int/sorted-ranges';
 import { CoarseSphereConformation, CoarseGaussianConformation } from 'mol-model/structure/model/properties/coarse';
-import { getMoleculeType, getElementIndexForAtomRole } from 'mol-model/structure/util';
+import { getAtomicMoleculeType, getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { getPolymerRanges } from '../polymer';
 
 /**
@@ -165,7 +165,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
             value.first = residueIndex === this.residueSegmentMin
             value.last = residueIndex === this.residueSegmentMax
             value.secStrucChange = this.unit.model.properties.secondaryStructure.key[residueIndex] !== this.unit.model.properties.secondaryStructure.key[residueIndex + 1]
-            value.moleculeType = getMoleculeType(this.unit.model, residueIndex)
+            value.moleculeType = getAtomicMoleculeType(this.unit.model, residueIndex)
 
             if (!residueIt.hasNext) {
                 this.state = AtomicPolymerTraceIteratorState.nextPolymer

+ 2 - 1
src/mol-geo/representation/util.ts

@@ -22,7 +22,8 @@ export function getQualityProps(props: Partial<QualityProps>, structure?: Struct
     let linearSegments = defaults(props.linearSegments, 8)
 
     if (quality === 'auto' && structure) {
-        const score = structure.elementCount
+        let score = structure.elementCount
+        if (structure.isCoarse) score *= 10
         if (score > 500_000) {
             quality = 'lowest'
         } else if (score > 100_000) {

+ 2 - 2
src/mol-gl/shader/point.frag

@@ -27,9 +27,9 @@ void main(){
 
         #ifdef dPointFilledCircle
             float dist = distance(gl_PointCoord, center);
-            float alpha = 1.0 - smoothstep(radius - uPointEdgeBleach * 2.0, radius, dist);
+            float alpha = 1.0 - smoothstep(radius - uPointEdgeBleach, radius, dist);
+            if (alpha < 0.0001) discard;
             gl_FragColor.a *= alpha;
-            if (gl_FragColor.a < 0.1) discard;
         #endif
 
         #pragma glslify: import('./chunks/apply-marker-color.glsl')

+ 5 - 5
src/mol-io/common/binary-cif/array-encoder.ts

@@ -55,8 +55,8 @@ export namespace ArrayEncoder {
     }
 
     export function fromEncoding(encoding: Encoding[]) {
-        const e = by(getProvider(encoding[0]));
-        for (let i = 1; i < encoding.length; i++) e.and(getProvider(encoding[i]));
+        let e = by(getProvider(encoding[0]));
+        for (let i = 1; i < encoding.length; i++) e = e.and(getProvider(encoding[i]));
         return e;
     }
 
@@ -358,9 +358,9 @@ export namespace ArrayEncoding {
      * Packs Int32 array. The packing level is determined automatically to either 1-, 2-, or 4-byte words.
      */
     export function integerPacking(data: Int32Array): Result {
-        if (!(data instanceof Int32Array)) {
-            throw new Error('Integer packing can only be applied to Int32 data.');
-        }
+        // if (!(data instanceof Int32Array)) {
+        //     throw new Error('Integer packing can only be applied to Int32 data.');
+        // }
 
         const packing = determinePacking(data);
 

+ 6 - 2
src/mol-io/common/binary-cif/encoding.ts

@@ -84,12 +84,16 @@ export namespace Encoding {
         else if (data instanceof Uint32Array) srcType = Encoding.IntDataType.Uint32;
         else if (data instanceof Float32Array) srcType = Encoding.FloatDataType.Float32;
         else if (data instanceof Float64Array) srcType = Encoding.FloatDataType.Float64;
-        else throw new Error('Unsupported integer data type.');
+        else srcType = Encoding.IntDataType.Int32; // throw new Error('Unsupported integer data type.');
         return srcType;
     }
 
     export function isSignedIntegerDataType(data: TypedIntArray) {
-        return data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array;
+        if (data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array) return true;
+        for (let i = 0, _i = data.length; i < _i; i++) {
+            if (i < 0) return false;
+        }
+        return true;
     }
 
     // type[] -> Uint8[]

+ 6 - 3
src/mol-io/reader/cif/data-model.ts

@@ -114,17 +114,20 @@ export function getTensor(category: CifCategory, field: string, space: Tensor.Sp
 }
 
 export function getCifFieldType(field: CifField): Column.Schema.Int | Column.Schema.Float | Column.Schema.Str {
-    let floatCount = 0, hasString = false;
+    let floatCount = 0, hasString = false, undefinedCount = 0;
     for (let i = 0, _i = field.rowCount; i < _i; i++) {
         const k = field.valueKind(i);
-        if (k !== Column.ValueKind.Present) continue;
+        if (k !== Column.ValueKind.Present) {
+            undefinedCount++;
+            continue;
+        }
         const type = getNumberType(field.str(i));
         if (type === NumberType.Int) continue;
         else if (type === NumberType.Float) floatCount++;
         else { hasString = true; break; }
     }
 
-    if (hasString) return Column.Schema.str;
+    if (hasString || undefinedCount === field.rowCount) return Column.Schema.str;
     if (floatCount > 0) return Column.Schema.float;
     return Column.Schema.int;
 }

+ 1 - 1
src/mol-io/reader/cif/schema/bird.ts

@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
- * Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.298, IHM 0.134.
+ * Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.299, IHM 0.134.
  *
  * @author mol-star package (src/apps/schema-generator/generate)
  */

+ 1 - 1
src/mol-io/reader/cif/schema/ccd.ts

@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
- * Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.298, IHM 0.134.
+ * Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.299, IHM 0.134.
  *
  * @author mol-star package (src/apps/schema-generator/generate)
  */

+ 227 - 1
src/mol-io/reader/cif/schema/mmcif.ts

@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
- * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.298, IHM 0.134.
+ * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.299, IHM 0.134, entity_branch draft.
  *
  * @author mol-star package (src/apps/schema-generator/generate)
  */
@@ -1470,6 +1470,36 @@ export const mmCIF_Schema = {
          */
         'space_group_name_H-M': str,
     },
+    /**
+     * Data items in the CHEM_COMP_IDENTIFIER category provide
+     * identifiers for chemical components.
+     */
+    pdbx_chem_comp_identifier: {
+        /**
+         * This data item is a pointer to _chem_comp.id in the CHEM_COMP
+         * category.
+         */
+        comp_id: str,
+        /**
+         * This data item contains the identifier value for this
+         * component.
+         */
+        identifier: str,
+        /**
+         * This data item contains the identifier type.
+         */
+        type: Aliased<'COMMON NAME' | 'SYSTEMATIC NAME' | 'CAS REGISTRY NUMBER' | 'PUBCHEM Identifier' | 'MDL Identifier' | 'SYNONYM'>(str),
+        /**
+         * This data item contains the name of the program
+         * or library used to compute the identifier.
+         */
+        program: str,
+        /**
+         * This data item contains the version of the program
+         * or library used to compute the identifier.
+         */
+        program_version: str,
+    },
     /**
      * Data items in the PDBX_STRUCT_MOD_RESIDUE category list the
      * modified polymer components in the entry and provide some
@@ -1886,6 +1916,40 @@ export const mmCIF_Schema = {
          */
         details: str,
     },
+    /**
+     * Data items in the PDBX_ENTITY_DESCRIPTOR category provide
+     * string descriptors of entity chemical structure.
+     */
+    pdbx_entity_descriptor: {
+        /**
+         * This data item is a pointer to _entity_poly.entity_id in the ENTITY
+         * category.
+         */
+        entity_id: str,
+        /**
+         * This data item contains the descriptor value for this
+         * entity.
+         */
+        descriptor: str,
+        /**
+         * This data item contains the descriptor type.
+         */
+        type: Aliased<'LINUCS' | 'IUPAC' | 'IUPAC Abbreviated'>(str),
+        /**
+         * This data item contains the name of the program
+         * or library used to compute the descriptor.
+         */
+        program: str,
+        /**
+         * This data item contains the version of the program
+         * or library used to compute the descriptor.
+         */
+        program_version: str,
+        /**
+         * Ordinal index for this category.
+         */
+        ordinal: int,
+    },
     /**
      * Data items in the IHM_STARTING_MODEL_DETAILS category records the
      * details about structural models used as starting inputs in
@@ -3682,6 +3746,168 @@ export const mmCIF_Schema = {
          */
         dataset_list_id: int,
     },
+    /**
+     * Data items in the PDBX_ENTITY_BRANCH_LIST category specify the list
+     * of monomers in a branched entity.  Allowance is made for the possibility
+     * of microheterogeneity in a sample by allowing a given sequence
+     * number to be correlated with more than one monomer ID. The
+     * corresponding ATOM_SITE entries should reflect this
+     * heterogeneity.
+     */
+    pdbx_entity_branch_list: {
+        /**
+         * This data item is a pointer to _entity.id in the ENTITY category.
+         */
+        entity_id: str,
+        /**
+         * A flag to indicate whether this monomer in the entity is
+         * heterogeneous in sequence.
+         */
+        hetero: Aliased<'no' | 'n' | 'yes' | 'y'>(str),
+        /**
+         * This data item is a pointer to _chem_comp.id in the CHEM_COMP
+         * category.
+         */
+        comp_id: str,
+        /**
+         * The value pair  _pdbx_entity_branch_list.num and _pdbx_entity_branch_list.comp_id
+         * must uniquely identify a record in the PDBX_ENTITY_BRANCH_LIST list.
+         */
+        num: int,
+    },
+    /**
+     * Data items in the PDBX_ENTITY_BRANCH_LINK category give details about
+     * the linkages between components within branched entities.
+     */
+    pdbx_entity_branch_link: {
+        /**
+         * The value of _pdbx_entity_branch_link.link_id uniquely identifies
+         * linkages within the branched entity.
+         */
+        link_id: int,
+        /**
+         * A description of special aspects of this linkage.
+         */
+        details: str,
+        /**
+         * The entity id for this branched entity.
+         *
+         * This data item is a pointer to _pdbx_entity_branch_list.entity_id
+         * in the PDBX_ENTITY_BRANCH_LIST category.
+         */
+        entity_id: str,
+        /**
+         * The component number for the first component making the linkage.
+         *
+         * This data item is a pointer to _pdbx_entity_branch_list.num
+         * in the PDBX_ENTITY_BRANCH_LIST category.
+         */
+        entity_branch_list_num_1: int,
+        /**
+         * The component number for the second component making the linkage.
+         *
+         * This data item is a pointer to _pdbx_entity_branch_list.num
+         * in the PDBX_ENTITY_BRANCH_LIST category.
+         */
+        entity_branch_list_num_2: int,
+        /**
+         * The component identifier for the first component making the linkage.
+         *
+         * This data item is a pointer to _pdbx_entity_branch_list.comp_id
+         * in the PDBX_ENTITY_BRANCH_LIST category.
+         */
+        comp_id_1: str,
+        /**
+         * The component identifier for the second component making the linkage.
+         *
+         * This data item is a pointer to _pdbx_entity_branch_list.comp_id
+         * in the PDBX_ENTITY_BRANCH_LIST category.
+         */
+        comp_id_2: str,
+        /**
+         * The atom identifier/name for the first atom making the linkage.
+         */
+        atom_id_1: str,
+        /**
+         * The leaving atom identifier/name bonded to the first atom making the linkage.
+         */
+        leaving_atom_id_1: str,
+        /**
+         * The chiral configuration of the first atom making the linkage.
+         */
+        atom_stereo_config_1: Aliased<'R' | 'S' | 'N'>(str),
+        /**
+         * The atom identifier/name for the second atom making the linkage.
+         */
+        atom_id_2: str,
+        /**
+         * The leaving atom identifier/name bonded to the second atom making the linkage.
+         */
+        leaving_atom_id_2: str,
+        /**
+         * The chiral configuration of the second atom making the linkage.
+         */
+        atom_stereo_config_2: Aliased<'R' | 'S' | 'N'>(str),
+        /**
+         * The bond order target for the chemical linkage.
+         */
+        value_order: Aliased<'sing' | 'doub' | 'trip' | 'quad' | 'arom' | 'poly' | 'delo' | 'pi'>(str),
+    },
+    /**
+     * Data items in the PDBX_ENTITY_BRANCH category specify the list
+     * of branched entities and the type.
+     */
+    pdbx_entity_branch: {
+        /**
+         * The entity id for this branched entity.
+         *
+         * This data item is a pointer to _entity.id
+         */
+        entity_id: str,
+        /**
+         * The type of this branched oligosaccharide.
+         */
+        type: Aliased<'oligosaccharide'>(str),
+    },
+    /**
+     * The PDBX_BRANCH_SCHEME category provides residue level nomenclature
+     * mapping for branch chain entitie.
+     */
+    pdbx_branch_scheme: {
+        /**
+         * This data item is a pointer to _entity.id in the ENTITY category.
+         */
+        entity_id: str,
+        /**
+         * A flag to indicate whether this monomer in the entity is
+         * heterogeneous in sequence.
+         */
+        hetero: Aliased<'no' | 'n' | 'yes' | 'y'>(str),
+        /**
+         * Pointer to _atom_site.label_asym_id.
+         */
+        asym_id: str,
+        /**
+         * This data item is a pointer to _atom_site.label_comp_id in the
+         * PDBX_ENTITY_BRANCH_LIST category.
+         */
+        mon_id: str,
+        /**
+         * This data item is a pointer to _pdbx_entity_branch_list.num in the
+         * PDBX_ENTITY_BRANCH_LIST category.
+         */
+        num: int,
+        /**
+         * This data item is a pointer to _atom_site.pdbx_auth_seq_id in the
+         * ATOM_SITE category.
+         */
+        auth_seq_num: str,
+        /**
+         * This data item is a pointer to _atom_site.pdbx_auth_comp_id in the
+         * ATOM_SITE category.
+         */
+        auth_mon_id: str,
+    },
 }
 
 export type mmCIF_Schema = typeof mmCIF_Schema;

+ 1 - 1
src/mol-io/reader/cif/text/parser.ts

@@ -635,7 +635,7 @@ async function parseInternal(data: string, runtimeCtx: RuntimeContext) {
         return error(tokenizer.lineNumber, `Unfinished save frame (${saveFrame.header}).`);
     }
 
-    if (blockCtx.categoryNames.length > 0) {
+    if (blockCtx.categoryNames.length > 0 || saveFrames.length > 0) {
         dataBlocks.push(Data.CifBlock(blockCtx.categoryNames, blockCtx.categories, blockHeader, saveFrames));
     }
 

+ 4 - 4
src/mol-math/geometry/lookup3d/grid.ts

@@ -212,7 +212,7 @@ function build(data: PositionData, cellSize?: Vec3) {
 }
 
 interface QueryContext {
-    structure: Grid3D,
+    grid: Grid3D,
     x: number,
     y: number,
     z: number,
@@ -221,12 +221,12 @@ interface QueryContext {
     isCheck: boolean
 }
 
-function createContext(structure: Grid3D): QueryContext {
-    return { structure, x: 0.1, y: 0.1, z: 0.1, radius: 0.1, result: Result.create(), isCheck: false }
+function createContext(grid: Grid3D): QueryContext {
+    return { grid, x: 0.1, y: 0.1, z: 0.1, radius: 0.1, result: Result.create(), isCheck: false }
 }
 
 function query(ctx: QueryContext): boolean {
-    const { min, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices, radius }, delta, maxRadius } = ctx.structure;
+    const { min, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices, radius }, delta, maxRadius } = ctx.grid;
     const { radius: inputRadius, isCheck, x, y, z, result } = ctx;
 
     const r = inputRadius + maxRadius;

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

@@ -70,9 +70,15 @@ const Categories = [
     copy_mmCif_category('entity_poly'),
     copy_mmCif_category('entity_poly_seq'),
 
+    // Branch
+    copy_mmCif_category('pdbx_entity_branch'),
+    copy_mmCif_category('pdbx_entity_branch_link'),
+    copy_mmCif_category('pdbx_branch_scheme'),
+
     // Misc
     // TODO: filter for actual present residues?
     copy_mmCif_category('chem_comp'),
+    copy_mmCif_category('pdbx_chem_comp_identifier'),
     copy_mmCif_category('atom_sites'),
 
     _pdbx_struct_mod_residue,

+ 11 - 4
src/mol-model/structure/model/properties/utils/atomic-ranges.ts

@@ -22,13 +22,13 @@ function getMoleculeType(compId: string, chemicalComponentMap: ChemicalComponent
     return cc ? cc.moleculeType : MoleculeType.unknown
 }
 
-function getElementIndexForAtomId(rI: ResidueIndex, atomId: string, data: AtomicData, segments: AtomicSegments, ): ElementIndex {
+function getElementIndexForAtomId(rI: ResidueIndex, atomId: string, data: AtomicData, segments: AtomicSegments): ElementIndex {
     const { offsets } = segments.residueAtomSegments
     const { label_atom_id } = data.atoms
     for (let j = offsets[rI], _j = offsets[rI + 1]; j < _j; j++) {
-        if (label_atom_id.value(j) === atomId) return j as ElementIndex
+        if (label_atom_id.value(j) === atomId) return j
     }
-    return offsets[rI] as ElementIndex
+    return offsets[rI]
 }
 
 function areBackboneConnected(riStart: ResidueIndex, riEnd: ResidueIndex, data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, chemicalComponentMap: ChemicalComponentMap) {
@@ -46,7 +46,7 @@ function areBackboneConnected(riStart: ResidueIndex, riEnd: ResidueIndex, data:
     const { x, y, z } = conformation
     const pStart = Vec3.create(x[eiStart], y[eiStart], z[eiStart])
     const pEnd = Vec3.create(x[eiEnd], y[eiEnd], z[eiEnd])
-    return Vec3.distance(pStart, pEnd) < 2
+    return Vec3.distance(pStart, pEnd) < 10
 }
 
 export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, chemicalComponentMap: ChemicalComponentMap): AtomicRanges {
@@ -90,6 +90,13 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conf
                         startIndex = residueSegment.start
                     } else if (!residueIt.hasNext) {
                         polymerRanges.push(startIndex, residueSegment.end - 1)
+                    } else {
+                        const riStart = segments.residueAtomSegments.index[residueSegment.start]
+                        const riEnd = segments.residueAtomSegments.index[prevEnd - 1]
+                        if (!areBackboneConnected(riStart, riEnd, data, segments, conformation, chemicalComponentMap)) {
+                            polymerRanges.push(startIndex, prevEnd - 1)
+                            startIndex = residueSegment.start
+                        }
                     }
                 } else {
                     startIndex = residueSegment.start // start polymer

+ 48 - 20
src/mol-model/structure/structure/carbohydrates/compute.ts

@@ -13,7 +13,7 @@ import PrincipalAxes from 'mol-math/linear-algebra/matrix/principal-axes';
 import { fillSerial } from 'mol-util/array';
 import { ResidueIndex } from '../../model';
 import { ElementSymbol, MoleculeType } from '../../model/types';
-import { getMoleculeType, getPositionMatrix } from '../../util';
+import { getAtomicMoleculeType, getPositionMatrix } from '../../util';
 import StructureElement from '../element';
 import Structure from '../structure';
 import Unit from '../unit';
@@ -131,13 +131,13 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
     }
 
     function fixLinkDirection(iA: number, iB: number) {
-        Vec3.sub(elements[iA].geometry!.direction, elements[iB].geometry!.center, elements[iA].geometry!.center)
-        Vec3.normalize(elements[iA].geometry!.direction, elements[iA].geometry!.direction)
+        Vec3.sub(elements[iA].geometry.direction, elements[iB].geometry.center, elements[iA].geometry.center)
+        Vec3.normalize(elements[iA].geometry.direction, elements[iA].geometry.direction)
     }
 
     const tmpV = Vec3.zero()
     function fixTerminalLinkDirection(iA: number, indexB: number, unitB: Unit.Atomic) {
-        const pos = unitB.conformation.position, geo = elements[iA].geometry!;
+        const pos = unitB.conformation.position, geo = elements[iA].geometry;
         Vec3.sub(geo.direction, pos(unitB.elements[indexB], tmpV), geo.center)
         Vec3.normalize(geo.direction, geo.direction)
     }
@@ -164,7 +164,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
 
                 const saccharideComp = SaccharideNameMap.get(label_comp_id.value(residueIndex)) || UnknownSaccharideComponent
                 if (saccharideComp === UnknownSaccharideComponent) {
-                    if (getMoleculeType(unit.model, residueIndex) !== MoleculeType.saccharide) continue
+                    if (getAtomicMoleculeType(unit.model, residueIndex) !== MoleculeType.saccharide) continue
                 }
 
                 if (!sugarResidueMap) {
@@ -191,36 +191,35 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                     const direction = getDirection(Vec3.zero(), unit, anomericCarbon, center)
                     Vec3.orthogonalize(direction, normal, direction)
 
-                    const altId = getRingAltId(unit, ringAtoms)
+                    const ringAltId = getRingAltId(unit, ringAtoms)
                     const elementIndex = elements.length
                     ringElements.push(elementIndex)
-                    elementsWithRingMap.set(ringElementKey(residueIndex, unit.id, altId), elementIndex)
+                    elementsWithRingMap.set(ringElementKey(residueIndex, unit.id, ringAltId), elementIndex)
                     elements.push({
                         geometry: { center, normal, direction },
                         component: saccharideComp,
-                        unit, residueIndex, anomericCarbon
+                        unit, residueIndex, anomericCarbon, ringAltId
                     })
                 }
 
                 // add carbohydrate links induced by intra-residue bonds
+                // (e.g. for structures from the PDB archive __before__ carbohydrate remediation)
                 const ringCombinations = combinations(fillSerial(new Array(sugarRings.length) as number[]), 2)
                 for (let j = 0, jl = ringCombinations.length; j < jl; ++j) {
                     const rc = ringCombinations[j];
                     const r0 = rings.all[sugarRings[rc[0]]], r1 = rings.all[sugarRings[rc[1]]];
                     // 1,6 glycosidic links are distance 3 and 1,4 glycosidic links are distance 2
                     if (IntAdjacencyGraph.areVertexSetsConnected(unit.links, r0, r1, 3)) {
-                        // TODO handle better, for now fix both directions as it is unclear where the C1 atom is
-                        //  would need to know the path connecting the two rings
-                        fixLinkDirection(ringElements[rc[0]], ringElements[rc[1]])
-                        fixLinkDirection(ringElements[rc[1]], ringElements[rc[0]])
-                        links.push({
-                            carbohydrateIndexA: ringElements[rc[0]],
-                            carbohydrateIndexB: ringElements[rc[1]]
-                        })
-                        links.push({
-                            carbohydrateIndexA: ringElements[rc[1]],
-                            carbohydrateIndexB: ringElements[rc[0]]
-                        })
+                        const re0 = ringElements[rc[0]]
+                        const re1 = ringElements[rc[1]]
+                        if (elements[re0].ringAltId === elements[re1].ringAltId) {
+                            // TODO handle better, for now fix both directions as it is unclear where the C1 atom is
+                            //  would need to know the path connecting the two rings
+                            fixLinkDirection(re0, re1)
+                            fixLinkDirection(re1, re0)
+                            links.push({ carbohydrateIndexA: re0, carbohydrateIndexB: re1 })
+                            links.push({ carbohydrateIndexA: re1, carbohydrateIndexB: re0 })
+                        }
                     }
                 }
             }
@@ -231,7 +230,36 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
         return elementsWithRingMap.get(ringElementKey(unit.getResidueIndex(index), unit.id, getAltId(unit, index)))
     }
 
+    // add carbohydrate links induced by intra-unit bonds
+    // (e.g. for structures from the PDB archive __after__ carbohydrate remediation)
+    for (let i = 0, il = elements.length; i < il; ++i) {
+        const carbohydrate = elements[i]
+        const { unit, residueIndex, anomericCarbon } = carbohydrate
+        const { offset, b } = unit.links
+        const ac = SortedArray.indexOf(unit.elements, anomericCarbon) as StructureElement.UnitIndex
+
+        for (let j = offset[ac], jl = offset[ac + 1]; j < jl; ++j) {
+            const bj = b[j] as StructureElement.UnitIndex
+            if (residueIndex !== unit.getResidueIndex(bj)) {
+                const ringElementIndex = getRingElementIndex(unit, bj)
+                if (ringElementIndex !== undefined && ringElementIndex !== i) {
+                    fixLinkDirection(i, ringElementIndex)
+                    links.push({
+                        carbohydrateIndexA: i,
+                        carbohydrateIndexB: ringElementIndex
+                    })
+                    links.push({
+                        carbohydrateIndexA: ringElementIndex,
+                        carbohydrateIndexB: i
+                    })
+                }
+            }
+        }
+
+    }
+
     // get carbohydrate links induced by inter-unit bonds
+    // (e.g. for structures from the PDB archive __before__ carbohydrate remediation)
     for (let i = 0, il = structure.units.length; i < il; ++i) {
         const unit = structure.units[i]
         if (!Unit.isAtomic(unit)) continue

+ 1 - 0
src/mol-model/structure/structure/carbohydrates/data.ts

@@ -28,6 +28,7 @@ export interface CarbohydrateElement {
     readonly unit: Unit.Atomic,
     readonly residueIndex: ResidueIndex,
     readonly component: SaccharideComponent,
+    readonly ringAltId: string,
 }
 
 // partial carbohydrate with no ring present

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

@@ -40,7 +40,8 @@ class Structure {
         models?: ReadonlyArray<Model>,
         hashCode: number,
         elementCount: number,
-    } = { hashCode: -1, elementCount: 0 };
+        polymerResidueCount: number,
+    } = { hashCode: -1, elementCount: 0, polymerResidueCount: 0 };
 
     subsetBuilder(isSorted: boolean) {
         return new StructureSubsetBuilder(this, isSorted);
@@ -51,6 +52,18 @@ class Structure {
         return this._props.elementCount;
     }
 
+    /** Count of all polymer residues in the structure */
+    get polymerResidueCount() {
+        return this._props.polymerResidueCount;
+    }
+
+    /** Coarse structure, defined as Containing less than twice as many elements as polymer residues */
+    get isCoarse() {
+        const ec = this.elementCount
+        const prc = this.polymerResidueCount
+        return prc && ec ? ec / prc < 2 : false
+    }
+
     get hashCode() {
         if (this._props.hashCode !== -1) return this._props.hashCode;
         return this.computeHash();
@@ -123,12 +136,14 @@ class Structure {
     constructor(units: ArrayLike<Unit>) {
         const map = IntMap.Mutable<Unit>();
         let elementCount = 0;
+        let polymerResidueCount = 0;
         let isSorted = true;
         let lastId = units.length > 0 ? units[0].id : 0;
         for (let i = 0, _i = units.length; i < _i; i++) {
             const u = units[i];
             map.set(u.id, u);
             elementCount += u.elements.length;
+            polymerResidueCount += u.polymerElements.length;
             if (u.id < lastId) isSorted = false;
             lastId = u.id;
         }
@@ -136,6 +151,7 @@ class Structure {
         this.unitMap = map;
         this.units = units as ReadonlyArray<Unit>;
         this._props.elementCount = elementCount;
+        this._props.polymerResidueCount = polymerResidueCount;
     }
 }
 

+ 1 - 1
src/mol-model/structure/structure/util/polymer.ts

@@ -21,7 +21,7 @@ export function getAtomicPolymerElements(unit: Unit.Atomic) {
         while (residueIt.hasNext) {
             const residueSegment = residueIt.move()
             const { start, end, index } = residueSegment
-            if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) {
+            if (OrderedSet.areIntersecting(Interval.ofRange(elements[start], elements[end - 1]), elements)) {
                 const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
                 indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
             }

+ 25 - 2
src/mol-model/structure/util.ts

@@ -10,7 +10,30 @@ import { Vec3 } from 'mol-math/linear-algebra';
 import { Unit } from './structure';
 import Matrix from 'mol-math/linear-algebra/matrix/matrix';
 
-export function getMoleculeType(model: Model, rI: ResidueIndex) {
+export function getCoarseBegCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIndex) {
+    const entityKey = unit.coarseElements.entityKey[element]
+    const seq = unit.model.sequence.byEntityKey[entityKey]
+    const seq_id_begin = unit.coarseElements.seq_id_begin.value(element)
+    return seq.compId.value(seq_id_begin - 1) // 1-indexed
+}
+
+export function getElementMoleculeType(unit: Unit, element: ElementIndex) {
+    let compId = ''
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
+            break
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            compId = getCoarseBegCompId(unit, element)
+            break
+    }
+    const chemCompMap = unit.model.properties.chemicalComponentMap
+    const cc = chemCompMap.get(compId)
+    return cc ? cc.moleculeType : MoleculeType.unknown
+}
+
+export function getAtomicMoleculeType(model: Model, rI: ResidueIndex) {
     const compId = model.atomicHierarchy.residues.label_comp_id.value(rI)
     const chemCompMap = model.properties.chemicalComponentMap
     const cc = chemCompMap.get(compId)
@@ -36,7 +59,7 @@ export function getElementIndexForAtomId(model: Model, rI: ResidueIndex, atomId:
 }
 
 export function getElementIndexForAtomRole(model: Model, rI: ResidueIndex, atomRole: AtomRole) {
-    const atomId = getAtomIdForAtomRole(getMoleculeType(model, rI), atomRole)
+    const atomId = getAtomIdForAtomRole(getAtomicMoleculeType(model, rI), atomRole)
     return getElementIndexForAtomId(model, rI, atomId)
 }
 

+ 8 - 0
src/mol-util/color/color.ts

@@ -14,6 +14,10 @@ export namespace Color {
         return `rgb(${hexColor >> 16 & 255}, ${hexColor >> 8 & 255}, ${hexColor & 255})`
     }
 
+    export function toHexString(hexColor: Color) {
+        return '0x' + ('000000' + hexColor.toString(16)).slice(-6)
+    }
+
     export function toRgb(hexColor: Color) {
         return [ hexColor >> 16 & 255, hexColor >> 8 & 255, hexColor & 255 ]
     }
@@ -26,6 +30,10 @@ export namespace Color {
         return ((r << 16) | (g << 8) | b) as Color
     }
 
+    export function fromNormalizedRgb(r: number, g: number, b: number ): Color {
+        return (((r * 255) << 16) | ((g * 255) << 8) | (b * 255)) as Color
+    }
+
     /** Copies hex color to rgb array */
     export function toArray(hexColor: Color, array: Helpers.NumberArray, offset: number) {
         array[ offset ] = (hexColor >> 16 & 255)

+ 19 - 4
src/mol-util/color/scale.ts

@@ -7,6 +7,7 @@
 import { Color } from './color'
 import { ColorBrewer } from './tables'
 import { ScaleLegend } from 'mol-view/theme/color';
+import { defaults } from 'mol-util';
 
 export interface ColorScale {
     /** Returns hex color for given value */
@@ -15,7 +16,9 @@ export interface ColorScale {
     colorToArray: (value: number, array: Helpers.NumberArray, offset: number) => void
     /** Copies normalized (0 to 1) hex color to rgb array */
     normalizedColorToArray: (value: number, array: Helpers.NumberArray, offset: number) => void
-    /** */
+    /**  */
+    setDomain: (min: number, max: number) => void
+    /** Legend */
     readonly legend: ScaleLegend
 }
 
@@ -23,16 +26,27 @@ export const DefaultColorScale = {
     domain: [0, 1],
     reverse: false,
     colors: ColorBrewer.RdYlBu,
+    minLabel: '' as string | undefined,
+    maxLabel: '' as string | undefined,
 }
 export type ColorScaleProps = Partial<typeof DefaultColorScale>
 
 export namespace ColorScale {
     export function create(props: ColorScaleProps): ColorScale {
         const { domain, reverse, colors: _colors } = { ...DefaultColorScale, ...props }
-        const [ min, max ] = domain
         const colors = reverse ? _colors.slice().reverse() : _colors
         const count1 = colors.length - 1
-        const diff = (max - min) || 1
+
+        let diff = 0, min = 0, max = 0
+        function setDomain(_min: number, _max: number) {
+            min = _min
+            max = _max
+            diff = (max - min) || 1
+        }
+        setDomain(domain[0], domain[1])
+
+        const minLabel = defaults(props.minLabel, min.toString())
+        const maxLabel = defaults(props.maxLabel, max.toString())
 
         function color(value: number) {
             const t = Math.min(colors.length - 1, Math.max(0, ((value - min) / diff) * count1))
@@ -49,7 +63,8 @@ export namespace ColorScale {
             normalizedColorToArray: (value: number, array: Helpers.NumberArray, offset: number) => {
                 Color.toArrayNormalized(color(value), array, offset)
             },
-            get legend() { return ScaleLegend(min, max, colors) }
+            setDomain,
+            get legend() { return ScaleLegend(minLabel, maxLabel, colors) }
         }
     }
 }

+ 38 - 0
src/mol-util/color/tables.ts

@@ -56,6 +56,44 @@ export const ColorBrewer = ColorTable({
     Pastel1: [0xfbb4ae, 0xb3cde3, 0xccebc5, 0xdecbe4, 0xfed9a6, 0xffffcc, 0xe5d8bd, 0xfddaec, 0xf2f2f2]
 })
 
+/**
+ * Matplotlib colormaps, including various perceptually uniform shades, see https://bids.github.io/colormap/
+ */
+export const ColorMatplotlib = ColorTable({
+    /** perceptually uniform shades of black-red-white */
+    magma: [
+        0x420f74, 0x4a1079, 0x52127c, 0x5a157e, 0x61187f, 0x691c80, 0x711f81, 0x792281, 0x812581, 0x892881, 0x912a80, 0x992d7f, 0xa12f7e, 0xa9327c, 0xb1357a, 0xb93778, 0xc23a75, 0xca3e72, 0xd1426e, 0xd9466a, 0xe04b66, 0xe65162, 0xec585f, 0xf0605d, 0xf4685b, 0xf7715b, 0xf97b5d, 0xfb8460, 0xfc8e63, 0xfd9768, 0xfda16e, 0xfeaa74, 0xfeb37b, 0xfebc82, 0xfec689, 0xfdcf92, 0xfdd89a, 0xfde1a3, 0xfceaac, 0xfcf3b5, 0xfbfcbf
+    ],
+    /** perceptually uniform shades of black-red-yellow */
+    inferno: [
+        0x480b6a, 0x500d6c, 0x58106d, 0x60136e, 0x68166e, 0x70196e, 0x781c6d, 0x801f6b, 0x88216a, 0x902468, 0x982765, 0xa02a62, 0xa72d5f, 0xaf315b, 0xb73456, 0xbe3852, 0xc53d4d, 0xcc4148, 0xd24742, 0xd94d3d, 0xde5337, 0xe45a31, 0xe8612b, 0xed6825, 0xf0701e, 0xf37918, 0xf68111, 0xf88a0b, 0xfa9306, 0xfb9d06, 0xfba60b, 0xfbb014, 0xfbb91e, 0xf9c32a, 0xf8cd37, 0xf5d745, 0xf3e056, 0xf1e968, 0xf1f27d, 0xf5f891, 0xfcfea4
+    ],
+    /** perceptually uniform shades of blue-red-yellow */
+    plasma: [
+        0x1b068c, 0x250591, 0x2f0495, 0x380499, 0x40039c, 0x49029f, 0x5101a2, 0x5901a4, 0x6100a6, 0x6800a7, 0x7000a8, 0x7801a8, 0x7f03a7, 0x8607a6, 0x8e0ca4, 0x9511a1, 0x9b179e, 0xa21c9a, 0xa82296, 0xae2791, 0xb42d8d, 0xb93388, 0xbe3883, 0xc33e7f, 0xc8447a, 0xcd4975, 0xd14f71, 0xd6556d, 0xda5a68, 0xde6064, 0xe26660, 0xe56c5b, 0xe97257, 0xec7853, 0xef7e4e, 0xf2854a, 0xf58b46, 0xf79241, 0xf9993d, 0xfaa039, 0xfca735, 0xfdaf31, 0xfdb62d, 0xfdbe29, 0xfdc626, 0xfcce25, 0xfad624, 0xf8df24, 0xf5e726, 0xf2f026, 0xeff821
+    ],
+    /** perceptually uniform shades of blue-green-yellow */
+    viridis: [
+        0x45085b, 0x470f62, 0x471669, 0x481d6f, 0x482374, 0x472a79, 0x46307d, 0x453681, 0x433c84, 0x414286, 0x3e4888, 0x3c4d8a, 0x3a538b, 0x37588c, 0x355d8c, 0x32628d, 0x30678d, 0x2e6c8e, 0x2c718e, 0x2a768e, 0x287a8e, 0x267f8e, 0x24848d, 0x23898d, 0x218d8c, 0x1f928c, 0x1e978a, 0x1e9b89, 0x1ea087, 0x20a585, 0x23a982, 0x28ae7f, 0x2eb27c, 0x35b778, 0x3dbb74, 0x45bf6f, 0x4fc369, 0x59c764, 0x64cb5d, 0x70ce56, 0x7cd24f, 0x88d547, 0x95d73f, 0xa2da37, 0xafdc2e, 0xbdde26, 0xcae01e, 0xd7e219, 0xe4e318, 0xf1e51c, 0xfde724
+    ],
+    /**
+     * perceptually uniform shades of blue-green-yellow,
+     * should look effectively identical to colorblind and non-colorblind users
+     */
+    cividis: [
+        0x002c67, 0x003070, 0x083370, 0x16366f, 0x1f3a6e, 0x273d6d, 0x2e416c, 0x34446c, 0x39486c, 0x3f4b6b, 0x444f6b, 0x49526b, 0x4e566c, 0x52596c, 0x575d6d, 0x5b606e, 0x60646e, 0x64676f, 0x686b71, 0x6d6e72, 0x717273, 0x757575, 0x797977, 0x7e7d78, 0x838078, 0x878478, 0x8c8878, 0x918c77, 0x968f77, 0x9b9376, 0xa09775, 0xa59b73, 0xaa9f72, 0xafa370, 0xb4a76f, 0xb9ab6d, 0xbeb06a, 0xc4b468, 0xc9b865, 0xcebc62, 0xd4c15e, 0xd9c55a, 0xdfca56, 0xe4ce51, 0xead34c, 0xefd846, 0xf5dc3f, 0xfbe136, 0xfde737
+    ],
+    /** perceptually uniform shades of white-blue-black-red-white, cyclic */
+    twilight: [
+        0xdfd9e1, 0xd8d7dd, 0xced3d8, 0xc2cdd3, 0xb4c7ce, 0xa7c0ca, 0x9ab8c7, 0x8eb0c5, 0x83a8c3, 0x7a9fc2, 0x7297c0, 0x6b8ebf, 0x6684bd, 0x637bbb, 0x6171b9, 0x5f67b6, 0x5e5cb2, 0x5e51ad, 0x5d46a7, 0x5c3c9f, 0x5b3196, 0x58278b, 0x531e7d, 0x4d176e, 0x46135f, 0x3e1151, 0x381045, 0x32113b, 0x301336, 0x361138, 0x3e113c, 0x471240, 0x521445, 0x5e1749, 0x6a1a4d, 0x761e4f, 0x812350, 0x8b2a50, 0x95324f, 0x9d3a4f, 0xa5434f, 0xac4d50, 0xb25752, 0xb86155, 0xbc6c59, 0xc0775f, 0xc48267, 0xc78d70, 0xc9987b, 0xcca389, 0xceae97, 0xd2b8a6, 0xd6c1b5, 0xdacac4, 0xddd1d1, 0xe0d6db, 0xe1d8e1
+    ]
+})
+
+export const ColorOther = ColorTable({
+    rainbow: [ 0x3361E1, 0x35A845, 0xF9FF00, 0xEC8711, 0xBF2222 ],
+    rwb: [ 0xBF2222, 0xFFFFFF, 0x3361E1 ],
+})
+
 /** X11 color names http://www.w3.org/TR/css3-color/#svg-color */
 export const ColorNames = ColorMap({
     aliceblue: 0xf0f8ff,

+ 17 - 16
src/mol-view/label.ts

@@ -9,7 +9,7 @@ import { Unit, StructureElement, StructureProperties as Props, Link } from 'mol-
 import { Loci } from 'mol-model/loci';
 import { OrderedSet } from 'mol-data/int';
 
-// for `labelFirst`, don't create right away to avaiod problems with circular dependencies/imports
+// for `labelFirst`, don't create right away to avoid problems with circular dependencies/imports
 let elementLocA: StructureElement
 let elementLocB: StructureElement
 
@@ -53,24 +53,25 @@ export function linkLabel(link: Link.Location) {
     return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}`
 }
 
-export function elementLabel(element: StructureElement) {
-    const model = element.unit.model.label
-    const instance = element.unit.conformation.operator.name
+export function elementLabel(location: StructureElement) {
+    const model = location.unit.model.label
+    const instance = location.unit.conformation.operator.name
     let label = ''
 
-    if (Unit.isAtomic(element.unit)) {
-        const asym_id = Props.chain.auth_asym_id(element)
-        const seq_id = Props.residue.auth_seq_id(element)
-        const comp_id = Props.residue.auth_comp_id(element)
-        const atom_id = Props.atom.auth_atom_id(element)
-        label = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}`
-    } else if (Unit.isCoarse(element.unit)) {
-        const asym_id = Props.coarse.asym_id(element)
-        const seq_id_begin = Props.coarse.seq_id_begin(element)
-        const seq_id_end = Props.coarse.seq_id_end(element)
+    if (Unit.isAtomic(location.unit)) {
+        const asym_id = Props.chain.auth_asym_id(location)
+        const seq_id = Props.residue.auth_seq_id(location)
+        const comp_id = Props.residue.auth_comp_id(location)
+        const atom_id = Props.atom.auth_atom_id(location)
+        const alt_id = Props.atom.label_alt_id(location)
+        label = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}${alt_id ? `%${alt_id}` : ''}`
+    } else if (Unit.isCoarse(location.unit)) {
+        const asym_id = Props.coarse.asym_id(location)
+        const seq_id_begin = Props.coarse.seq_id_begin(location)
+        const seq_id_end = Props.coarse.seq_id_end(location)
         if (seq_id_begin === seq_id_end) {
-            const entityKey = Props.coarse.entityKey(element)
-            const seq = element.unit.model.sequence.byEntityKey[entityKey]
+            const entityIndex = Props.coarse.entityKey(location)
+            const seq = location.unit.model.sequence.byEntityKey[entityIndex]
             const comp_id = seq.compId.value(seq_id_begin - 1) // 1-indexed
             label = `[${comp_id}]${seq_id_begin}:${asym_id}`
         } else {

+ 21 - 12
src/mol-view/theme/color.ts

@@ -19,17 +19,20 @@ import { CrossLinkColorTheme } from './color/cross-link';
 import { ShapeGroupColorTheme } from './color/shape-group';
 import { CustomColorTheme } from './color/custom';
 import { ResidueNameColorTheme } from './color/residue-name';
+import { SequenceIdColorTheme } from './color/sequence-id';
+import { SecondaryStructureColorTheme } from './color/secondary-structure';
+import { MoleculeTypeColorTheme } from './color/molecule-type';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
 export interface ScaleLegend {
     kind: 'scale-legend'
-    min: number,
-    max: number,
+    minLabel: string,
+    maxLabel: string,
     colors: Color[]
 }
-export function ScaleLegend(min: number, max: number, colors: Color[]): ScaleLegend {
-    return { kind: 'scale-legend', min, max, colors }
+export function ScaleLegend(minLabel: string, maxLabel: string, colors: Color[]): ScaleLegend {
+    return { kind: 'scale-legend', minLabel, maxLabel, colors }
 }
 
 export interface TableLegend {
@@ -49,16 +52,19 @@ export interface ColorTheme {
 
 export function ColorTheme(props: ColorThemeProps): ColorTheme {
     switch (props.name) {
-        case 'element-index': return ElementIndexColorTheme(props)
         case 'carbohydrate-symbol': return CarbohydrateSymbolColorTheme(props)
-        case 'cross-link': return CrossLinkColorTheme(props)
         case 'chain-id': return ChainIdColorTheme(props)
+        case 'cross-link': return CrossLinkColorTheme(props)
+        case 'custom': return CustomColorTheme(props)
+        case 'element-index': return ElementIndexColorTheme(props)
         case 'element-symbol': return ElementSymbolColorTheme(props)
+        case 'molecule-type': return MoleculeTypeColorTheme(props)
         case 'residue-name': return ResidueNameColorTheme(props)
+        case 'secondary-structure': return SecondaryStructureColorTheme(props)
+        case 'sequence-id': return SequenceIdColorTheme(props)
+        case 'shape-group': return ShapeGroupColorTheme(props)
         case 'unit-index': return UnitIndexColorTheme(props)
         case 'uniform': return UniformColorTheme(props)
-        case 'shape-group': return ShapeGroupColorTheme(props)
-        case 'custom': return CustomColorTheme(props)
     }
 }
 
@@ -74,16 +80,19 @@ export interface ColorThemeProps {
 }
 
 export const ColorThemeInfo = {
-    'element-index': {},
     'carbohydrate-symbol': {},
-    'cross-link': {},
     'chain-id': {},
+    'cross-link': {},
+    'custom': {},
+    'element-index': {},
     'element-symbol': {},
+    'molecule-type': {},
     'residue-name': {},
+    'secondary-structure': {},
+    'sequence-id': {},
+    'shape-group': {},
     'unit-index': {},
     'uniform': {},
-    'shape-group': {},
-    'custom': {}
 }
 export type ColorThemeName = keyof typeof ColorThemeInfo
 export const ColorThemeNames = Object.keys(ColorThemeInfo)

+ 59 - 0
src/mol-view/theme/color/molecule-type.ts

@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color, ColorMap } from 'mol-util/color';
+import { StructureElement, Unit, Link, ElementIndex } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { ColorThemeProps, ColorTheme, TableLegend } from '../color';
+import { MoleculeType } from 'mol-model/structure/model/types';
+import { getElementMoleculeType } from 'mol-model/structure/util';
+
+const MoleculeTypeColors = ColorMap({
+    water: 0x386cb0,
+    ion: 0xf0027f,
+    protein: 0xbeaed4,
+    RNA: 0xfdc086,
+    DNA: 0xbf5b17,
+    PNA: 0x42A49A,
+    saccharide: 0x7fc97f,
+})
+
+const DefaultMoleculeTypeColor = Color(0xffff99)
+const Description = 'Assigns a color based on the molecule type of a residue.'
+
+export function moleculeTypeColor(unit: Unit, element: ElementIndex): Color {
+    const moleculeType = getElementMoleculeType(unit, element)
+    switch (moleculeType) {
+        case MoleculeType.water: return MoleculeTypeColors.water
+        case MoleculeType.ion: return MoleculeTypeColors.ion
+        case MoleculeType.protein: return MoleculeTypeColors.protein
+        case MoleculeType.RNA: return MoleculeTypeColors.RNA
+        case MoleculeType.DNA: return MoleculeTypeColors.DNA
+        case MoleculeType.PNA: return MoleculeTypeColors.PNA
+        case MoleculeType.saccharide: return MoleculeTypeColors.saccharide
+    }
+    return DefaultMoleculeTypeColor
+}
+
+export function MoleculeTypeColorTheme(props: ColorThemeProps): ColorTheme {
+    function color(location: Location): Color {
+        if (StructureElement.isLocation(location)) {
+            return moleculeTypeColor(location.unit, location.element)
+        } else if (Link.isLocation(location)) {
+            return moleculeTypeColor(location.aUnit, location.aUnit.elements[location.aIndex])
+        }
+        return DefaultMoleculeTypeColor
+    }
+
+    return {
+        granularity: 'group',
+        color,
+        description: Description,
+        legend: TableLegend(Object.keys(MoleculeTypeColors).map(name => {
+            return [name, (MoleculeTypeColors as any)[name] as Color] as [string, Color]
+        }).concat([[ 'Other/unknown', DefaultMoleculeTypeColor ]]))
+    }
+}

+ 82 - 0
src/mol-view/theme/color/secondary-structure.ts

@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color, ColorMap } from 'mol-util/color';
+import { StructureElement, Unit, Link, ElementIndex } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { ColorThemeProps, ColorTheme, TableLegend } from '../color';
+import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
+import { getElementMoleculeType } from 'mol-model/structure/util';
+
+// from Jmol http://jmol.sourceforge.net/jscolors/ (shapely)
+const SecondaryStructureColors = ColorMap({
+    'alphaHelix': 0xFF0080,
+    'threeTenHelix': 0xA00080,
+    'piHelix': 0x600080,
+    'betaTurn': 0x6080FF,
+    'betaStrand': 0xFFC800,
+    'coil': 0xFFFFFF,
+
+    'dna': 0xAE00FE,
+    'rna': 0xFD0162,
+
+    'carbohydrate': 0xA6A6FA
+})
+
+const DefaultSecondaryStructureColor = Color(0x808080)
+const Description = 'Assigns a color based on the type of secondary structure and basic molecule type.'
+
+export function secondaryStructureColor(unit: Unit, element: ElementIndex): Color {
+    let secStrucType = SecondaryStructureType.create(SecondaryStructureType.Flag.None)
+    if (Unit.isAtomic(unit)) {
+        secStrucType = unit.model.properties.secondaryStructure.type[unit.residueIndex[element]]
+    }
+
+    if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Helix)) {
+        if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Helix3Ten)) {
+            return SecondaryStructureColors.threeTenHelix
+        } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.HelixPi)) {
+            return SecondaryStructureColors.piHelix
+        }
+        return SecondaryStructureColors.alphaHelix
+    } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Beta)) {
+        return SecondaryStructureColors.betaStrand
+    } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Turn)) {
+        return SecondaryStructureColors.coil
+    } else {
+        const moleculeType = getElementMoleculeType(unit, element)
+        if (moleculeType === MoleculeType.DNA) {
+            return SecondaryStructureColors.dna
+        } else if (moleculeType === MoleculeType.RNA) {
+            return SecondaryStructureColors.rna
+        } else if (moleculeType === MoleculeType.saccharide) {
+            return SecondaryStructureColors.carbohydrate
+        } else if (moleculeType === MoleculeType.protein) {
+            return SecondaryStructureColors.coil
+        }
+    }
+    return DefaultSecondaryStructureColor
+}
+
+export function SecondaryStructureColorTheme(props: ColorThemeProps): ColorTheme {
+    function color(location: Location): Color {
+        if (StructureElement.isLocation(location)) {
+            return secondaryStructureColor(location.unit, location.element)
+        } else if (Link.isLocation(location)) {
+            return secondaryStructureColor(location.aUnit, location.aUnit.elements[location.aIndex])
+        }
+        return DefaultSecondaryStructureColor
+    }
+
+    return {
+        granularity: 'group',
+        color,
+        description: Description,
+        legend: TableLegend(Object.keys(SecondaryStructureColors).map(name => {
+            return [name, (SecondaryStructureColors as any)[name] as Color] as [string, Color]
+        }).concat([[ 'Other', DefaultSecondaryStructureColor ]]))
+    }
+}

+ 92 - 0
src/mol-view/theme/color/sequence-id.ts

@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, StructureElement, Link, ElementIndex } from 'mol-model/structure';
+
+import { ColorScale, Color } from 'mol-util/color';
+import { Location } from 'mol-model/location';
+import { ColorThemeProps, ColorTheme } from '../color';
+import { ColorOther } from 'mol-util/color/tables';
+
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Gives every polymer residue a color based on its `seq_id` value.'
+
+function getSeqId(unit: Unit, element: ElementIndex): number {
+    const { model } = unit
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            const residueIndex = model.atomicHierarchy.residueAtomSegments.index[element]
+            return model.atomicHierarchy.residues.label_seq_id.value(residueIndex)
+        case Unit.Kind.Spheres:
+            return Math.round(
+                (model.coarseHierarchy.spheres.seq_id_begin.value(element) +
+                    model.coarseHierarchy.spheres.seq_id_end.value(element)) / 2
+            )
+        case Unit.Kind.Gaussians:
+            return Math.round(
+                (model.coarseHierarchy.gaussians.seq_id_begin.value(element) +
+                    model.coarseHierarchy.gaussians.seq_id_end.value(element)) / 2
+            )
+    }
+}
+
+function getSequenceLength(unit: Unit, element: ElementIndex) {
+    const { model } = unit
+    let entityId = ''
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            const chainIndex = model.atomicHierarchy.chainAtomSegments.index[element]
+            entityId = model.atomicHierarchy.chains.label_entity_id.value(chainIndex)
+            break
+        case Unit.Kind.Spheres:
+            entityId = model.coarseHierarchy.spheres.entity_id.value(element)
+            break
+        case Unit.Kind.Gaussians:
+            entityId = model.coarseHierarchy.gaussians.entity_id.value(element)
+            break
+    }
+    if (entityId === '') return 0
+    const entityIndex = model.entities.getEntityIndex(entityId)
+    if (entityIndex === -1) return 0
+    return model.sequence.byEntityKey[entityIndex].sequence.sequence.length
+}
+
+export function SequenceIdColorTheme(props: ColorThemeProps): ColorTheme {
+    const p = {
+        ...props,
+        colors: ColorOther.rainbow,
+        minLabel: 'Start',
+        maxLabel: 'End',
+    }
+
+    const scale = ColorScale.create(p)
+    const color = (location: Location): Color => {
+        if (StructureElement.isLocation(location)) {
+            const { unit, element } = location
+            const seq_id = getSeqId(unit, element)
+            if (seq_id > 0) {
+                scale.setDomain(0, getSequenceLength(unit, element) - 1)
+                return scale.color(seq_id)
+            }
+        } else if (Link.isLocation(location)) {
+            const { aUnit, aIndex } = location
+            const seq_id = getSeqId(aUnit, aUnit.elements[aIndex])
+            if (seq_id > 0) {
+                scale.setDomain(0, getSequenceLength(aUnit, aUnit.elements[aIndex]) - 1)
+                return scale.color(seq_id)
+            }
+        }
+        return DefaultColor
+    }
+
+    return {
+        granularity: 'group',
+        color,
+        description: Description,
+        // legend: scale ? TableLegend(table) : undefined
+        legend: scale ? scale.legend : undefined
+    }
+}

+ 10 - 1
src/servers/model/config.ts

@@ -47,6 +47,14 @@ const config = {
     /** Maximum number of requests before "server busy" */
     maxQueueLength: 30,
 
+    /**
+     * Paths (relative to the root directory of the model server) to JavaScript files that specify custom properties
+     */
+    customPropertyProviders: [
+        './properties/pdbe',
+        './properties/rcsb'
+    ],
+
     /**
      * Maps a request identifier to a filename.
      *
@@ -57,7 +65,8 @@ const config = {
      */
     mapFile(source: string, id: string) {
         switch (source.toLowerCase()) {
-            case 'pdb': return `e:/test/quick/${id}_updated.cif`;
+            // case 'pdb': return `e:/test/quick/${id}_updated.cif`;
+            case 'pdb': return `e:/test/mol-star/model/out/${id}_updated.bcif`;
             default: return void 0;
         }
     }

+ 2 - 1
src/servers/model/preprocess/preprocess.ts

@@ -23,7 +23,8 @@ export async function preprocessFile(filename: string, outputCif?: string, outpu
 
     //const started = now();
     //ConsoleLogger.log(`${linearId}`, `Reading '${filename}'...`);
-    const input = await readStructure('entry', '_local_', filename);
+    // TODO: support the custom prop provider list here.
+    const input = await readStructure('entry', '_local_', filename, void 0);
     //ConsoleLogger.log(`${linearId}`, `Classifying CIF categories...`);
     const categories = await classifyCif(input.cifFrame);
     //clearLine();

+ 0 - 19
src/servers/model/properties.ts

@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 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 { Model } from 'mol-model/structure';
-//import { PDBe_structureQualityReport } from './properties/pdbe';
-//import { RCSB_assemblySymmetry } from './properties/rcsb';
-
-export function attachModelProperties(model: Model): Promise<any>[] {
-    // return a list of promises that start attaching the props in parallel
-    // (if there are downloads etc.)
-    return [
-        //PDBe_structureQualityReport(model),
-        //RCSB_assemblySymmetry(model)
-    ];
-}

+ 9 - 10
src/servers/model/properties/pdbe.ts

@@ -2,17 +2,16 @@
  * Copyright (c) 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 { Model } from 'mol-model/structure';
-import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
-import { fetchRetry } from '../utils/fetch-retry';
+import { Model } from 'mol-model/structure';
+import { PDBe_structureQualityReport } from './providers/pdbe';
 
-export function PDBe_structureQualityReport(model: Model) {
-    return StructureQualityReport.attachFromCifOrApi(model, {
-        PDBe_apiSourceJson: async model => {
-            const rawData = await fetchRetry(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`, 1500, 5);
-            return await rawData.json();
-        }
-    });
+export function attachModelProperties(model: Model): Promise<any>[] {
+    // return a list of promises that start attaching the props in parallel
+    // (if there are downloads etc.)
+    return [
+        PDBe_structureQualityReport(model)
+    ];
 }

+ 18 - 0
src/servers/model/properties/providers/pdbe.ts

@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+ import { Model } from 'mol-model/structure';
+import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
+import { fetchRetry } from '../../utils/fetch-retry';
+
+export function PDBe_structureQualityReport(model: Model) {
+    return StructureQualityReport.attachFromCifOrApi(model, {
+        PDBe_apiSourceJson: async model => {
+            const rawData = await fetchRetry(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`, 1500, 5);
+            return await rawData.json();
+        }
+    });
+}

+ 12 - 0
src/servers/model/properties/providers/rcsb.ts

@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Model } from 'mol-model/structure';
+import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+
+export function RCSB_assemblySymmetry(model: Model) {
+    return AssemblySymmetry.attachFromCifOrAPI(model)
+}

+ 8 - 3
src/servers/model/properties/rcsb.ts

@@ -1,12 +1,17 @@
 /**
  * Copyright (c) 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 { Model } from 'mol-model/structure';
-import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { RCSB_assemblySymmetry } from './providers/rcsb';
 
-export function RCSB_assemblySymmetry(model: Model) {
-    return AssemblySymmetry.attachFromCifOrAPI(model)
+export function attachModelProperties(model: Model): Promise<any>[] {
+    // return a list of promises that start attaching the props in parallel
+    // (if there are downloads etc.)
+    return [
+        RCSB_assemblySymmetry(model)
+    ];
 }

+ 28 - 0
src/servers/model/provider.ts

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Model } from 'mol-model/structure';
+import Config from './config';
+
+export type ModelPropertiesProvider = (model: Model) => Promise<any>[]
+
+export function createModelPropertiesProviderFromConfig(): ModelPropertiesProvider {
+    if (!Config.customPropertyProviders || Config.customPropertyProviders.length === 0) return () => [];
+
+    const ps: ModelPropertiesProvider[] = [];
+    for (const p of Config.customPropertyProviders) {
+        ps.push(require(p).attachModelProperties);
+    }
+
+    return model => {
+        const ret: Promise<any>[] = [];
+        for (const p of ps) {
+            for (const e of p(model)) ret.push(e);
+        }
+        return ret;
+    }
+}
+

+ 16 - 2
src/servers/model/server/query.ts

@@ -16,6 +16,7 @@ import Version from '../version';
 import { Job } from './jobs';
 import { getStructure, StructureWrapper } from './structure-wrapper';
 import CifField = CifWriter.Field
+import { createModelPropertiesProviderFromConfig } from '../provider';
 
 export interface Stats {
     structure: StructureWrapper,
@@ -25,13 +26,14 @@ export interface Stats {
 
 const perf = new PerformanceMonitor();
 
+const propertyProvider = createModelPropertiesProviderFromConfig();
+
 export async function resolveJob(job: Job): Promise<CifWriter.Encoder<any>> {
     ConsoleLogger.logId(job.id, 'Query', 'Starting.');
 
-    const wrappedStructure = await getStructure(job);
+    const wrappedStructure = await getStructure(job, propertyProvider);
 
     try {
-        const encoder = CifWriter.createEncoder({ binary: job.responseFormat.isBinary, encoderName: `ModelServer ${Version}` });
         perf.start('query');
         const structure = job.queryDefinition.structureTransform
             ? await job.queryDefinition.structureTransform(job.normalizedParams, wrappedStructure.structure)
@@ -40,6 +42,13 @@ export async function resolveJob(job: Job): Promise<CifWriter.Encoder<any>> {
         const result = await StructureSelection.unionStructure(StructureQuery.run(query, structure, Config.maxQueryTimeInMs));
         perf.end('query');
 
+        const encoder = CifWriter.createEncoder({
+            binary: job.responseFormat.isBinary,
+            encoderName: `ModelServer ${Version}`,
+            binaryEncodingPovider: getEncodingProvider(wrappedStructure),
+            binaryAutoClassifyEncoding: true
+        });
+
         ConsoleLogger.logId(job.id, 'Query', 'Query finished.');
 
         perf.start('encode');
@@ -68,6 +77,11 @@ export async function resolveJob(job: Job): Promise<CifWriter.Encoder<any>> {
     }
 }
 
+function getEncodingProvider(structure: StructureWrapper) {
+    if (!structure.isBinary) return void 0;
+    return CifWriter.createEncodingProviderFromCifFrame(structure.cifFrame);
+}
+
 function doError(job: Job, e: any) {
     const encoder = CifWriter.createEncoder({ binary: job.responseFormat.isBinary, encoderName: `ModelServer ${Version}` });
     encoder.writeCategory(_model_server_result, [job]);

+ 11 - 7
src/servers/model/server/structure-wrapper.ts

@@ -14,7 +14,7 @@ import * as fs from 'fs'
 import * as zlib from 'zlib'
 import { Job } from './jobs';
 import { ConsoleLogger } from 'mol-util/console-logger';
-import { attachModelProperties } from '../properties';
+import { ModelPropertiesProvider } from '../provider';
 
 require('util.promisify').shim();
 
@@ -37,18 +37,19 @@ export interface StructureInfo {
 export interface StructureWrapper {
     info: StructureInfo,
 
+    isBinary: boolean,
     key: string,
     approximateSize: number,
     structure: Structure,
     cifFrame: CifFrame
 }
 
-export async function getStructure(job: Job, allowCache = true): Promise<StructureWrapper> {
+export async function getStructure(job: Job, propertyProvider: ModelPropertiesProvider | undefined, allowCache = true): Promise<StructureWrapper> {
     if (allowCache && Config.cacheParams.useCache) {
         const ret = StructureCache.get(job.key);
         if (ret) return ret;
     }
-    const ret = await readStructure(job.key, job.sourceId, job.entryId);
+    const ret = await readStructure(job.key, job.sourceId, job.entryId, propertyProvider);
     if (allowCache && Config.cacheParams.useCache) {
         StructureCache.add(ret);
     }
@@ -85,7 +86,7 @@ async function parseCif(data: string|Uint8Array) {
     return parsed.result;
 }
 
-export async function readStructure(key: string, sourceId: string | '_local_', entryId: string) {
+export async function readStructure(key: string, sourceId: string | '_local_', entryId: string, propertyProvider: ModelPropertiesProvider | undefined) {
     const filename = sourceId === '_local_' ? entryId : Config.mapFile(sourceId, entryId);
     if (!filename) throw new Error(`Cound not map '${key}' to a valid filename.`);
     if (!fs.existsSync(filename)) throw new Error(`Could not find source file for '${key}'.`);
@@ -108,9 +109,11 @@ export async function readStructure(key: string, sourceId: string | '_local_', e
     perf.end('createModel');
 
     perf.start('attachProps');
-    const modelProps = attachModelProperties(models[0]);
-    for (const p of modelProps) {
-        await tryAttach(key, p);
+    if (propertyProvider) {
+        const modelProps = propertyProvider(models[0]);
+        for (const p of modelProps) {
+            await tryAttach(key, p);
+        }
     }
     perf.end('attachProps');
 
@@ -126,6 +129,7 @@ export async function readStructure(key: string, sourceId: string | '_local_', e
             sourceId,
             entryId
         },
+        isBinary: /\.bcif/.test(filename),
         key,
         approximateSize: typeof data === 'string' ? 2 * data.length : data.length,
         structure,

File diff suppressed because it is too large
+ 81 - 0
web/render-test/index.js


Some files were not shown because too many files changed in this diff