Bladeren bron

Merge branch 'master' into volume

# Conflicts:
#	src/apps/structure-info/volume.ts
#	src/mol-plugin/state/actions/basic.ts
#	src/mol-plugin/state/transforms/model.ts
Alexander Rose 6 jaren geleden
bovenliggende
commit
a2ba753acf
93 gewijzigde bestanden met toevoegingen van 1784 en 581 verwijderingen
  1. 1 0
      README.md
  2. 1 0
      package.json
  3. 8 4
      src/apps/basic-wrapper/index.html
  4. 41 15
      src/apps/basic-wrapper/index.ts
  5. 4 3
      src/apps/structure-info/model.ts
  6. 2 1
      src/apps/structure-info/volume.ts
  7. 4 5
      src/mol-io/reader/_spec/cif.spec.ts
  8. 1 1
      src/mol-io/reader/_spec/csv.spec.ts
  9. 1 1
      src/mol-io/reader/ccp4/parser.ts
  10. 1 1
      src/mol-io/reader/cif/binary/parser.ts
  11. 119 2
      src/mol-io/reader/cif/data-model.ts
  12. 0 54
      src/mol-io/reader/cif/text/field.ts
  13. 4 5
      src/mol-io/reader/cif/text/parser.ts
  14. 55 15
      src/mol-io/reader/common/text/tokenizer.ts
  15. 2 2
      src/mol-io/reader/csv/field.ts
  16. 3 3
      src/mol-io/reader/csv/parser.ts
  17. 1 1
      src/mol-io/reader/dsn6/parser.ts
  18. 2 2
      src/mol-io/reader/gro/parser.ts
  19. 16 16
      src/mol-io/reader/mol2/parser.ts
  20. 1 1
      src/mol-io/reader/obj/parser.ts
  21. 14 0
      src/mol-io/reader/pdb/parser.ts
  22. 12 0
      src/mol-io/reader/pdb/schema.ts
  23. 17 17
      src/mol-io/reader/result.ts
  24. 4 0
      src/mol-math/linear-algebra/3d/mat4.ts
  25. 18 0
      src/mol-model-formats/structure/format.ts
  26. 0 0
      src/mol-model-formats/structure/gro.ts
  27. 16 0
      src/mol-model-formats/structure/mmcif.ts
  28. 5 6
      src/mol-model-formats/structure/mmcif/assembly.ts
  29. 13 13
      src/mol-model-formats/structure/mmcif/atomic.ts
  30. 0 0
      src/mol-model-formats/structure/mmcif/bonds.ts
  31. 4 4
      src/mol-model-formats/structure/mmcif/bonds/comp.ts
  32. 5 5
      src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
  33. 6 6
      src/mol-model-formats/structure/mmcif/ihm.ts
  34. 0 0
      src/mol-model-formats/structure/mmcif/pair-restraint.ts
  35. 3 3
      src/mol-model-formats/structure/mmcif/pair-restraints/cross-links.ts
  36. 0 0
      src/mol-model-formats/structure/mmcif/pair-restraints/predicted-contacts.ts
  37. 46 45
      src/mol-model-formats/structure/mmcif/parser.ts
  38. 4 4
      src/mol-model-formats/structure/mmcif/secondary-structure.ts
  39. 4 4
      src/mol-model-formats/structure/mmcif/sequence.ts
  40. 0 0
      src/mol-model-formats/structure/mmcif/sort.ts
  41. 2 2
      src/mol-model-formats/structure/mmcif/util.ts
  42. 21 0
      src/mol-model-formats/structure/pdb.ts
  43. 234 0
      src/mol-model-formats/structure/pdb/assembly.ts
  44. 262 0
      src/mol-model-formats/structure/pdb/secondary-structure.ts
  45. 296 0
      src/mol-model-formats/structure/pdb/to-cif.ts
  46. 1 1
      src/mol-model-formats/volume/ccp4.ts
  47. 1 1
      src/mol-model-formats/volume/density-server.ts
  48. 1 1
      src/mol-model-formats/volume/dsn6.ts
  49. 1 1
      src/mol-model-props/pdbe/themes/structure-quality-report.ts
  50. 2 2
      src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
  51. 1 1
      src/mol-model/structure/export/categories/utils.ts
  52. 1 2
      src/mol-model/structure/model.ts
  53. 0 24
      src/mol-model/structure/model/format.ts
  54. 10 16
      src/mol-model/structure/model/model.ts
  55. 5 11
      src/mol-model/structure/model/properties/chemical-component.ts
  56. 15 6
      src/mol-model/structure/model/properties/utils/atomic-derived.ts
  57. 8 16
      src/mol-model/structure/model/properties/utils/atomic-ranges.ts
  58. 81 61
      src/mol-model/structure/model/types.ts
  59. 4 0
      src/mol-model/structure/structure/structure.ts
  60. 27 19
      src/mol-model/structure/structure/symmetry.ts
  61. 2 2
      src/mol-model/structure/structure/unit/links/data.ts
  62. 1 1
      src/mol-model/structure/structure/unit/links/inter-compute.ts
  63. 1 1
      src/mol-model/structure/structure/unit/links/intra-compute.ts
  64. 1 1
      src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
  65. 11 16
      src/mol-model/structure/util.ts
  66. 1 2
      src/mol-model/volume.ts
  67. 3 3
      src/mol-plugin/behavior/dynamic/animation.ts
  68. 1 1
      src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
  69. 3 4
      src/mol-plugin/behavior/dynamic/labels.ts
  70. 3 3
      src/mol-plugin/behavior/static/state.ts
  71. 1 1
      src/mol-plugin/command/base.ts
  72. 22 14
      src/mol-plugin/context.ts
  73. 1 0
      src/mol-plugin/index.ts
  74. 1 0
      src/mol-plugin/spec.ts
  75. 44 26
      src/mol-plugin/state/actions/basic.ts
  76. 63 11
      src/mol-plugin/state/transforms/model.ts
  77. 2 0
      src/mol-plugin/state/transforms/representation.ts
  78. 106 29
      src/mol-plugin/ui/controls/parameters.tsx
  79. 1 0
      src/mol-plugin/ui/state/apply-action.tsx
  80. 7 1
      src/mol-plugin/ui/state/common.tsx
  81. 1 0
      src/mol-plugin/ui/state/update-transform.tsx
  82. 1 1
      src/mol-state/action.ts
  83. 1 1
      src/mol-state/object.ts
  84. 65 44
      src/mol-state/state.ts
  85. 4 0
      src/mol-state/tree/builder.ts
  86. 3 1
      src/mol-util/param-definition.ts
  87. 1 1
      src/mol-util/set.ts
  88. 9 0
      src/mol-util/string.ts
  89. 4 4
      src/perf-tests/lookup3d.ts
  90. 4 7
      src/perf-tests/structure.ts
  91. 3 2
      src/servers/model/server/structure-wrapper.ts
  92. 1 1
      src/servers/volume/pack.ts
  93. 1 0
      tsconfig.json

+ 1 - 0
README.md

@@ -17,6 +17,7 @@ The core of Mol* currently consists of these modules:
 - `mol-math` Math related (loosely) algorithms and data structures.
 - `mol-io` Parsing library. Each format is parsed into an interface that corresponds to the data stored by it. Support for common coordinate, experimental/map, and annotation data formats.
 - `mol-model` Data structures and algorithms (such as querying) for representing molecular data (including coordinate, experimental/map, and annotation data).
+- `mol-model-formats` Data format parsers for `mol-model`.
 - `mol-model-props` Common "custom properties".
 - `mol-script` A scriting language for creating representations/scenes and querying (includes the [MolQL query language](https://molql.github.io)).
 - `mol-geo` Creating (molecular) geometries.

+ 1 - 0
package.json

@@ -54,6 +54,7 @@
       "mol-math($|/.*)": "<rootDir>/src/mol-math$1",
       "mol-model($|/.*)": "<rootDir>/src/mol-model$1",
       "mol-model-props($|/.*)": "<rootDir>/src/mol-model-props$1",
+      "mol-model-formats($|/.*)": "<rootDir>/src/mol-model-formats$1",
       "mol-plugin($|/.*)": "<rootDir>/src/mol-plugin$1",
       "mol-ql($|/.*)": "<rootDir>/src/mol-ql$1",
       "mol-repr($|/.*)": "<rootDir>/src/mol-repr$1",

+ 8 - 4
src/apps/basic-wrapper/index.html

@@ -3,7 +3,7 @@
     <head>
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
-        <title>Mol* Viewer</title>
+        <title>Mol* Plugin Wrapper</title>
         <style>
             * {
                 margin: 0;
@@ -30,15 +30,19 @@
         <script>            
             var pdbId = '5ire', assemblyId= '1';
             var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif';
+            var format = 'cif';
+
+            // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent';
+            // var format = 'pdb';
 
             BasicMolStarWrapper.init('app' /** or document.getElementById('app') */);
             BasicMolStarWrapper.setBackground(0xffffff);
-            BasicMolStarWrapper.loadCif(url, assemblyId);
+            BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
             BasicMolStarWrapper.toggleSpin();
 
             document.getElementById('spin').onclick = () => BasicMolStarWrapper.toggleSpin();
-            document.getElementById('asym').onclick = () => BasicMolStarWrapper.loadCif(url);
-            document.getElementById('asm').onclick = () => BasicMolStarWrapper.loadCif(url, assemblyId);
+            document.getElementById('asym').onclick = () => BasicMolStarWrapper.load({ url: url, format: format });
+            document.getElementById('asm').onclick = () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
         </script>
     </body>
 </html>

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

@@ -10,13 +10,16 @@ import { PluginContext } from 'mol-plugin/context';
 import { PluginCommands } from 'mol-plugin/command';
 import { StateTransforms } from 'mol-plugin/state/transforms';
 import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
-import { StateTree } from 'mol-state';
 import { Color } from 'mol-util/color';
+import { StateTreeBuilder } from 'mol-state/tree/builder';
+import { PluginStateObject as PSO } from 'mol-plugin/state/objects';
 require('mol-plugin/skin/light.scss')
 
+type SupportedFormats = 'cif' | 'pdb'
+type LoadParams = { url: string, format?: SupportedFormats, assemblyId?: string }
+
 class BasicWrapper {
     plugin: PluginContext;
-    stateTemplate: StateTree;
 
     init(target: string | HTMLElement) {
         this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
@@ -26,15 +29,23 @@ class BasicWrapper {
                 showControls: false
             }
         });
+    }
+
+    private download(b: StateTreeBuilder.To<PSO.Root>, url: string) {
+        return b.apply(StateTransforms.Data.Download, { url, isBinary: false })
+    }
 
-        const state = this.plugin.state.dataState.build();
-        const visualRoot = state.toRoot()
-            .apply(StateTransforms.Data.Download, { url: '', isBinary: false }, { ref: 'url' })
-            .apply(StateTransforms.Data.ParseCif)
-            .apply(StateTransforms.Model.TrajectoryFromMmCif)
+    private parse(b: StateTreeBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) {
+        const parsed = format === 'cif'
+            ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
+            : b.apply(StateTransforms.Model.TrajectoryFromPDB);
+
+        return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: '' }, { ref: 'asm' })
+            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+    }
 
+    private visual(visualRoot: StateTreeBuilder.To<PSO.Molecule.Structure>) {
         visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
             .apply(StateTransforms.Representation.StructureRepresentation3D,
                 StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'cartoon'));
@@ -47,18 +58,33 @@ class BasicWrapper {
         visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
             .apply(StateTransforms.Representation.StructureRepresentation3D,
                 StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'spacefill'));
-
-        this.stateTemplate = state.getTree();
+        return visualRoot;
     }
 
-    async loadCif(url: string, assemblyId?: string) {
-        const state = this.stateTemplate.build();
+    private loadedParams: LoadParams = { url: '', format: 'cif', assemblyId: '' };
+    async load({ url, format = 'cif', assemblyId = '' }: LoadParams) {
+        let loadType: 'full' | 'update' = 'full';
+
+        const state = this.plugin.state.dataState;
 
-        state.to('url').update(StateTransforms.Data.Download, p => ({ ...p, url }));
-        state.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId }));
+        if (this.loadedParams.url !== url || this.loadedParams.format !== format) {
+            loadType = 'full';
+        } else if (this.loadedParams.url === url) {
+            if (state.select('asm').length > 0) loadType = 'update';
+        }
 
-        await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree: state });
+        let tree: StateTreeBuilder.Root;
+        if (loadType === 'full') {
+            await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref });
+            tree = state.build();
+            this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId));
+        } else {
+            tree = state.build();
+            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+        }
 
+        await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
+        this.loadedParams = { url, format, assemblyId };
         PluginCommands.Camera.Reset.dispatch(this.plugin, { });
     }
 

+ 4 - 3
src/apps/structure-info/model.ts

@@ -9,11 +9,12 @@ import * as argparse from 'argparse'
 require('util.promisify').shim();
 
 import { CifFrame } from 'mol-io/reader/cif'
-import { Model, Structure, StructureElement, Unit, Format, StructureProperties, UnitRing } from 'mol-model/structure'
+import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing } from 'mol-model/structure'
 // import { Run, Progress } from 'mol-task'
 import { OrderedSet } from 'mol-data/int';
 import { openCif, downloadCif } from './helpers';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 
 
 async function downloadFromPdb(pdb: string) {
@@ -198,7 +199,7 @@ export function printModelStats(models: ReadonlyArray<Model>) {
 }
 
 export async function getModelsAndStructure(frame: CifFrame) {
-    const models = await Model.create(Format.mmCIF(frame)).run();
+    const models = await trajectoryFromMmCIF(frame).run();
     const structure = Structure.ofModel(models[0]);
     return { models, structure };
 }
@@ -247,7 +248,7 @@ interface Args {
     download?: string,
     file?: string,
 
-    models?:boolean,
+    models?: boolean,
     seq?: boolean,
     ihm?: boolean,
     units?: boolean,

+ 2 - 1
src/apps/structure-info/volume.ts

@@ -8,7 +8,7 @@ import * as fs from 'fs'
 import * as argparse from 'argparse'
 import * as util from 'util'
 
-import { VolumeData, volumeFromDensityServerData, VolumeIsoValue } from 'mol-model/volume'
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume'
 import { downloadCif } from './helpers'
 import CIF from 'mol-io/reader/cif'
 import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server';
@@ -17,6 +17,7 @@ import { StringBuilder } from 'mol-util';
 import { Task } from 'mol-task';
 import { createVolumeIsosurfaceMesh } from 'mol-repr/volume/isosurface';
 import { createEmptyTheme } from 'mol-theme/theme';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
 
 require('util.promisify').shim();
 const writeFileAsync = util.promisify(fs.writeFile);

+ 4 - 5
src/mol-io/reader/_spec/cif.spec.ts

@@ -6,17 +6,16 @@
  */
 
 import * as Data from '../cif/data-model'
-import TextField from '../cif/text/field'
 import * as Schema from '../cif/schema'
 import { Column } from 'mol-data/db'
 
 const columnData = `123abc d,e,f '4 5 6'`;
 // 123abc d,e,f '4 5 6'
 
-const intField = TextField({ data: columnData, indices: [0, 1, 1, 2, 2, 3], count: 3 }, 3);
-const strField = TextField({ data: columnData, indices: [3, 4, 4, 5, 5, 6], count: 3 }, 3);
-const strListField = TextField({ data: columnData, indices: [7, 12], count: 1 }, 1);
-const intListField = TextField({ data: columnData, indices: [14, 19], count: 1 }, 1);
+const intField = Data.CifField.ofTokens({ data: columnData, indices: [0, 1, 1, 2, 2, 3], count: 3 });
+const strField = Data.CifField.ofTokens({ data: columnData, indices: [3, 4, 4, 5, 5, 6], count: 3 });
+const strListField = Data.CifField.ofTokens({ data: columnData, indices: [7, 12], count: 1 });
+const intListField = Data.CifField.ofTokens({ data: columnData, indices: [14, 19], count: 1 });
 
 const testBlock = Data.CifBlock(['test'], {
     test: Data.CifCategory('test', 3, ['int', 'str', 'strList', 'intList'], {

+ 1 - 1
src/mol-io/reader/_spec/csv.spec.ts

@@ -62,7 +62,7 @@ describe('csv reader', () => {
     });
 
     it('tabs', async () => {
-        const parsed = await Csv(tabString, { delimiter: '\t' }).run();;
+        const parsed = await Csv(tabString, { delimiter: '\t' }).run();
         if (parsed.isError) return;
         const csvFile = parsed.result;
 

+ 1 - 1
src/mol-io/reader/ccp4/parser.ts

@@ -6,7 +6,7 @@
 
 import { Task, RuntimeContext } from 'mol-task';
 import { Ccp4File, Ccp4Header } from './schema'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { FileHandle } from '../../common/file-handle';
 
 async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Result<Ccp4File>> {

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

@@ -7,7 +7,7 @@
 import * as Data from '../data-model'
 import { EncodedCategory, EncodedFile } from '../../../common/binary-cif'
 import Field from './field'
-import Result from '../../result'
+import { ReaderResult as Result } from '../../result'
 import decodeMsgPack from '../../../common/msgpack/decode'
 import { Task } from 'mol-task'
 

+ 119 - 2
src/mol-io/reader/cif/data-model.ts

@@ -5,10 +5,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Column } from 'mol-data/db'
+import { Column, ColumnHelpers } from 'mol-data/db'
 import { Tensor } from 'mol-math/linear-algebra'
-import { getNumberType, NumberType } from '../common/text/number-parser';
+import { getNumberType, NumberType, parseInt as fastParseInt, parseFloat as fastParseFloat } from '../common/text/number-parser';
 import { Encoding } from '../../common/binary-cif';
+import { Tokens } from '../common/text/tokenizer';
+import { areValuesEqualProvider } from '../common/text/column/token';
 
 export interface CifFile {
     readonly name?: string,
@@ -55,6 +57,19 @@ export namespace CifCategory {
     export function empty(name: string): CifCategory {
         return { rowCount: 0, name, fieldNames: [], getField(name: string) { return void 0; } };
     };
+
+    export type SomeFields<S> = { [P in keyof S]?: CifField }
+    export type Fields<S> = { [P in keyof S]: CifField }
+
+    export function ofFields(name: string, fields: { [name: string]: CifField | undefined }): CifCategory {
+        const fieldNames = Object.keys(fields);
+        return {
+            rowCount: fieldNames.length > 0 ? fields[fieldNames[0]]!.rowCount : 0,
+            name,
+            fieldNames,
+            getField(name) { return fields[name]; }
+        };
+    }
 }
 
 /**
@@ -81,6 +96,108 @@ export interface CifField {
     toFloatArray(params?: Column.ToArrayParams<number>): ReadonlyArray<number>
 }
 
+export namespace CifField {
+    export function ofString(value: string) {
+        return ofStrings([value]);
+    }
+
+    export function ofStrings(values: string[]): CifField {
+        const rowCount = values.length;
+        const str: CifField['str'] = row => { const ret = values[row]; if (!ret || ret === '.' || ret === '?') return ''; return ret; };
+        const int: CifField['int'] = row => { const v = values[row]; return fastParseInt(v, 0, v.length) || 0; };
+        const float: CifField['float'] = row => { const v = values[row]; return fastParseFloat(v, 0, v.length) || 0; };
+        const valueKind: CifField['valueKind'] = row => {
+            const v = values[row], l = v.length;
+            if (l > 1) return Column.ValueKind.Present;
+            if (l === 0) return Column.ValueKind.NotPresent;
+            const c = v.charCodeAt(0);
+            if (c === 46 /* . */) return Column.ValueKind.NotPresent;
+            if (c === 63 /* ? */) return Column.ValueKind.Unknown;
+            return Column.ValueKind.Present;
+        };
+
+        return {
+            __array: void 0,
+            binaryEncoding: void 0,
+            isDefined: true,
+            rowCount,
+            str,
+            int,
+            float,
+            valueKind,
+            areValuesEqual: (rowA, rowB) => values[rowA] === values[rowB],
+            toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
+            toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, int, params),
+            toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
+        }
+    }
+
+    export function ofNumbers(values: number[]): CifField {
+        const rowCount = values.length;
+        const str: CifField['str'] = row => { return '' + values[row]; };
+        const float: CifField['float'] = row => values[row];
+        const valueKind: CifField['valueKind'] = row => Column.ValueKind.Present;
+
+        return {
+            __array: void 0,
+            binaryEncoding: void 0,
+            isDefined: true,
+            rowCount,
+            str,
+            int: float,
+            float,
+            valueKind,
+            areValuesEqual: (rowA, rowB) => values[rowA] === values[rowB],
+            toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
+            toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params),
+            toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
+        }
+    }
+
+    export function ofTokens(tokens: Tokens): CifField {
+        const { data, indices, count: rowCount } = tokens;
+
+        const str: CifField['str'] = row => {
+            const ret = data.substring(indices[2 * row], indices[2 * row + 1]);
+            if (ret === '.' || ret === '?') return '';
+            return ret;
+        };
+
+        const int: CifField['int'] = row => {
+            return fastParseInt(data, indices[2 * row], indices[2 * row + 1]) || 0;
+        };
+
+        const float: CifField['float'] = row => {
+            return fastParseFloat(data, indices[2 * row], indices[2 * row + 1]) || 0;
+        };
+
+        const valueKind: CifField['valueKind'] = row => {
+            const s = indices[2 * row], l = indices[2 * row + 1] - s;
+            if (l > 1) return Column.ValueKind.Present;
+            if (l === 0) return Column.ValueKind.NotPresent;
+            const v = data.charCodeAt(s);
+            if (v === 46 /* . */) return Column.ValueKind.NotPresent;
+            if (v === 63 /* ? */) return Column.ValueKind.Unknown;
+            return Column.ValueKind.Present;
+        };
+
+        return {
+            __array: void 0,
+            binaryEncoding: void 0,
+            isDefined: true,
+            rowCount,
+            str,
+            int,
+            float,
+            valueKind,
+            areValuesEqual: areValuesEqualProvider(tokens),
+            toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
+            toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, int, params),
+            toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
+        }
+    }
+}
+
 export function getTensor(category: CifCategory, field: string, space: Tensor.Space, row: number, zeroIndexed: boolean): Tensor.Data {
     const ret = space.create();
     const offset = zeroIndexed ? 0 : 1;

+ 0 - 54
src/mol-io/reader/cif/text/field.ts

@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Column, ColumnHelpers } from 'mol-data/db'
-import * as TokenColumn from '../../common/text/column/token'
-import { Tokens } from '../../common/text/tokenizer'
-import * as Data from '../data-model'
-import { parseInt as fastParseInt, parseFloat as fastParseFloat } from '../../common/text/number-parser'
-
-export default function CifTextField(tokens: Tokens, rowCount: number): Data.CifField {
-    const { data, indices } = tokens;
-
-    const str: Data.CifField['str'] = row => {
-        const ret = data.substring(indices[2 * row], indices[2 * row + 1]);
-        if (ret === '.' || ret === '?') return '';
-        return ret;
-    };
-
-    const int: Data.CifField['int'] = row => {
-        return fastParseInt(data, indices[2 * row], indices[2 * row + 1]) || 0;
-    };
-
-    const float: Data.CifField['float'] = row => {
-        return fastParseFloat(data, indices[2 * row], indices[2 * row + 1]) || 0;
-    };
-
-    const valueKind: Data.CifField['valueKind'] = row => {
-        const s = indices[2 * row];
-        if (indices[2 * row + 1] - s !== 1) return Column.ValueKind.Present;
-        const v = data.charCodeAt(s);
-        if (v === 46 /* . */) return Column.ValueKind.NotPresent;
-        if (v === 63 /* ? */) return Column.ValueKind.Unknown;
-        return Column.ValueKind.Present;
-    };
-
-    return {
-        __array: void 0,
-        binaryEncoding: void 0,
-        isDefined: true,
-        rowCount,
-        str,
-        int,
-        float,
-        valueKind,
-        areValuesEqual: TokenColumn.areValuesEqualProvider(tokens),
-        toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
-        toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, int, params),
-        toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
-    }
-}

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

@@ -23,9 +23,8 @@
  */
 
 import * as Data from '../data-model'
-import Field from './field'
 import { Tokens, TokenBuilder } from '../../common/text/tokenizer'
-import Result from '../../result'
+import { ReaderResult as Result } from '../../result'
 import { Task, RuntimeContext, chunkedSubtask } from 'mol-task'
 
 /**
@@ -445,7 +444,7 @@ function handleSingle(tokenizer: TokenizerState, ctx: FrameContext): CifCategory
                 errorMessage: 'Expected value.'
             }
         }
-        fields[fieldName] = Field({ data: tokenizer.data, indices: [tokenizer.tokenStart, tokenizer.tokenEnd], count: 1 }, 1);
+        fields[fieldName] = Data.CifField.ofTokens({ data: tokenizer.data, indices: [tokenizer.tokenStart, tokenizer.tokenEnd], count: 1 });
         fieldNames[fieldNames.length] = fieldName;
         moveNext(tokenizer);
     }
@@ -507,7 +506,7 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise
     const rowCountEstimate = name === '_atom_site' ? (tokenizer.data.length / 100) | 0 : 32;
     const tokens: Tokens[] = [];
     const fieldCount = fieldNames.length;
-    for (let i = 0; i < fieldCount; i++) tokens[i] = TokenBuilder.create(tokenizer, rowCountEstimate);
+    for (let i = 0; i < fieldCount; i++) tokens[i] = TokenBuilder.create(tokenizer.data, rowCountEstimate);
 
     const state: LoopReadState = {
         fieldCount,
@@ -529,7 +528,7 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise
     const rowCount = (state.tokenCount / fieldCount) | 0;
     const fields = Object.create(null);
     for (let i = 0; i < fieldCount; i++) {
-        fields[fieldNames[i]] = Field(tokens[i], rowCount);
+        fields[fieldNames[i]] = Data.CifField.ofTokens(tokens[i]);
     }
 
     const catName = name.substr(1);

+ 55 - 15
src/mol-io/reader/common/text/tokenizer.ts

@@ -8,7 +8,9 @@
 
 import { chunkedSubtask, RuntimeContext } from 'mol-task'
 
-export interface Tokenizer {
+export { Tokenizer }
+
+interface Tokenizer {
     data: string,
 
     position: number,
@@ -25,7 +27,7 @@ export interface Tokens {
     indices: ArrayLike<number>
 }
 
-export function Tokenizer(data: string): Tokenizer {
+function Tokenizer(data: string): Tokenizer {
     return {
         data,
         position: 0,
@@ -36,7 +38,7 @@ export function Tokenizer(data: string): Tokenizer {
     };
 }
 
-export namespace Tokenizer {
+namespace Tokenizer {
     export function getTokenString(state: Tokenizer) {
         return state.data.substring(state.tokenStart, state.tokenEnd);
     }
@@ -52,7 +54,7 @@ export namespace Tokenizer {
     /**
      * Eat everything until a newline occurs.
      */
-    export function eatLine(state: Tokenizer) {
+    export function eatLine(state: Tokenizer): boolean {
         const { data } = state;
         while (state.position < state.length) {
             switch (data.charCodeAt(state.position)) {
@@ -60,7 +62,7 @@ export namespace Tokenizer {
                     state.tokenEnd = state.position;
                     ++state.position;
                     ++state.lineNumber;
-                    return;
+                    return true;
                 case 13: // \r
                     state.tokenEnd = state.position;
                     ++state.position;
@@ -68,13 +70,14 @@ export namespace Tokenizer {
                     if (data.charCodeAt(state.position) === 10) {
                         ++state.position;
                     }
-                    return;
+                    return true;
                 default:
                     ++state.position;
                     break;
             }
         }
         state.tokenEnd = state.position;
+        return state.tokenStart !== state.tokenEnd;
     }
 
     /** Sets the current token start to the current position */
@@ -85,7 +88,7 @@ export namespace Tokenizer {
     /** Sets the current token start to current position and moves to the next line. */
     export function markLine(state: Tokenizer) {
         state.tokenStart = state.position;
-        eatLine(state);
+        return eatLine(state);
     }
 
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
@@ -95,15 +98,18 @@ export namespace Tokenizer {
     }
 
     function readLinesChunk(state: Tokenizer, count: number, tokens: Tokens) {
+        let read = 0;
         for (let i = 0; i < count; i++) {
-            markLine(state);
+            if (!markLine(state)) return read;
             TokenBuilder.addUnchecked(tokens, state.tokenStart, state.tokenEnd);
+            read++;
         }
+        return read;
     }
 
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
     export function readLines(state: Tokenizer, count: number): Tokens {
-        const lineTokens = TokenBuilder.create(state, count * 2);
+        const lineTokens = TokenBuilder.create(state.data, count * 2);
         readLinesChunk(state, count, lineTokens);
         return lineTokens;
     }
@@ -111,7 +117,7 @@ export namespace Tokenizer {
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
     export async function readLinesAsync(state: Tokenizer, count: number, ctx: RuntimeContext, initialLineCount = 100000): Promise<Tokens> {
         const { length } = state;
-        const lineTokens = TokenBuilder.create(state, count * 2);
+        const lineTokens = TokenBuilder.create(state.data, count * 2);
 
         let linesAlreadyRead = 0;
         await chunkedSubtask(ctx, initialLineCount, state, (chunkSize, state) => {
@@ -124,6 +130,37 @@ export namespace Tokenizer {
         return lineTokens;
     }
 
+    export function readAllLines(data: string) {
+        const state = Tokenizer(data);
+        const tokens = TokenBuilder.create(state.data, Math.max(data.length / 80, 2))
+        while (markLine(state)) {
+            TokenBuilder.add(tokens, state.tokenStart, state.tokenEnd);
+        }
+        return tokens;
+    }
+
+    function readLinesChunkChecked(state: Tokenizer, count: number, tokens: Tokens) {
+        let read = 0;
+        for (let i = 0; i < count; i++) {
+            if (!markLine(state)) return read;
+            TokenBuilder.add(tokens, state.tokenStart, state.tokenEnd);
+            read++;
+        }
+        return read;
+    }
+
+    export async function readAllLinesAsync(data: string, ctx: RuntimeContext, chunkSize = 100000) {
+        const state = Tokenizer(data);
+        const tokens = TokenBuilder.create(state.data, Math.max(data.length / 80, 2));
+
+        await chunkedSubtask(ctx, chunkSize, state, (chunkSize, state) => {
+            readLinesChunkChecked(state, chunkSize, tokens);
+            return state.position < state.length ? chunkSize : 0;
+        }, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: length }));
+
+        return tokens;
+    }
+
     /**
      * Eat everything until a whitespace/newline occurs.
      */
@@ -191,6 +228,7 @@ export namespace Tokenizer {
         state.tokenStart = s;
         state.tokenEnd = e + 1;
         state.position = end;
+        return state;
     }
 }
 
@@ -228,22 +266,24 @@ export namespace TokenBuilder {
         tokens.count++;
     }
 
+    export function addToken(tokens: Tokens, tokenizer: Tokenizer) {
+        add(tokens, tokenizer.tokenStart, tokenizer.tokenEnd);
+    }
+
     export function addUnchecked(tokens: Tokens, start: number, end: number) {
         (tokens as Builder).indices[(tokens as Builder).offset++] = start;
         (tokens as Builder).indices[(tokens as Builder).offset++] = end;
         tokens.count++;
     }
 
-    export function create(tokenizer: Tokenizer, size: number): Tokens {
+    export function create(data: string, size: number): Tokens {
         size = Math.max(10, size)
         return <Builder>{
-            data: tokenizer.data,
+            data,
             indicesLenMinus2: (size - 2) | 0,
             count: 0,
             offset: 0,
             indices: new Uint32Array(size)
         }
     }
-}
-
-export default Tokenizer
+}

+ 2 - 2
src/mol-io/reader/csv/field.ts

@@ -4,6 +4,6 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Field from '../cif/text/field'
+import { CifField } from '../cif/data-model';
 
-export default Field
+export default CifField.ofTokens

+ 3 - 3
src/mol-io/reader/csv/parser.ts

@@ -8,7 +8,7 @@
 import { Tokens, TokenBuilder, Tokenizer } from '../common/text/tokenizer'
 import * as Data from './data-model'
 import Field from './field'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext, chunkedSubtask, } from 'mol-task'
 
 const enum CsvTokenType {
@@ -231,7 +231,7 @@ function readRecordsChunks(state: State) {
 
 function addColumn (state: State) {
     state.columnNames.push(Tokenizer.getTokenString(state.tokenizer))
-    state.tokens.push(TokenBuilder.create(state.tokenizer, state.data.length / 80))
+    state.tokens.push(TokenBuilder.create(state.tokenizer.data, state.data.length / 80))
 }
 
 function init(state: State) {
@@ -254,7 +254,7 @@ async function handleRecords(state: State): Promise<Data.CsvTable> {
 
     const columns: Data.CsvColumns = Object.create(null);
     for (let i = 0; i < state.columnCount; ++i) {
-        columns[state.columnNames[i]] = Field(state.tokens[i], state.recordCount);
+        columns[state.columnNames[i]] = Field(state.tokens[i]);
     }
 
     return Data.CsvTable(state.recordCount, state.columnNames, columns)

+ 1 - 1
src/mol-io/reader/dsn6/parser.ts

@@ -6,7 +6,7 @@
 
 import { Task, RuntimeContext } from 'mol-task';
 import { Dsn6File, Dsn6Header } from './schema'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { FileHandle } from '../../common/file-handle';
 
 function parseBrixHeader(str: string): Dsn6Header {

+ 2 - 2
src/mol-io/reader/gro/parser.ts

@@ -6,10 +6,10 @@
  */
 
 import { Column } from 'mol-data/db'
-import Tokenizer from '../common/text/tokenizer'
+import { Tokenizer } from '../common/text/tokenizer'
 import FixedColumn from '../common/text/column/fixed'
 import * as Schema from './schema'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext } from 'mol-task'
 
 interface State {

+ 16 - 16
src/mol-io/reader/mol2/parser.ts

@@ -15,7 +15,7 @@ import { Column } from 'mol-data/db'
 import { TokenBuilder, Tokenizer } from '../common/text/tokenizer'
 import TokenColumn from '../common/text/column/token'
 import * as Schema from './schema'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext, chunkedSubtask } from 'mol-task'
 
 const { skipWhitespace, eatValue, markLine, getTokenString, readLine } = Tokenizer;
@@ -130,12 +130,12 @@ async function handleAtoms(state: State): Promise<Schema.Mol2Atoms> {
     }
 
     // required columns
-    const atom_idTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const atom_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const xTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const yTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const zTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const atom_typeTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
+    const atom_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const atom_nameTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const xTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const yTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const zTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const atom_typeTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
 
     const atom_idTokenColumn = TokenColumn(atom_idTokens);
     const atom_nameTokenColumn = TokenColumn(atom_nameTokens);
@@ -145,10 +145,10 @@ async function handleAtoms(state: State): Promise<Schema.Mol2Atoms> {
     const atom_typeColumn = TokenColumn(atom_typeTokens);
 
     // optional columns
-    const subst_idTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const subst_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const chargeTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const status_bitTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
+    const subst_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const subst_nameTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const chargeTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const status_bitTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
 
     const subst_idTokenColumn = TokenColumn(subst_idTokens);
     const subst_nameTokenColumn = TokenColumn(subst_nameTokens);
@@ -257,10 +257,10 @@ async function handleBonds(state: State): Promise<Schema.Mol2Bonds> {
     }
 
     // required columns
-    const bond_idTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
-    const origin_bond_idTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
-    const target_bond_idTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
-    const bondTypeTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
+    const bond_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
+    const origin_bond_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
+    const target_bond_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
+    const bondTypeTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
 
     const bond_idTokenColumn = TokenColumn(bond_idTokens);
     const origin_bond_idTokenColumn = TokenColumn(origin_bond_idTokens);
@@ -268,7 +268,7 @@ async function handleBonds(state: State): Promise<Schema.Mol2Bonds> {
     const bondTypeTokenColumn = TokenColumn(bondTypeTokens);
 
     // optional columns
-    const status_bitTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
+    const status_bitTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
     const status_bitTokenColumn = TokenColumn(status_bitTokens);
     const undefStr = Column.Undefined(molecule.num_bonds, Column.Schema.str);
 

+ 1 - 1
src/mol-io/reader/obj/parser.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext } from 'mol-task'
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 

+ 14 - 0
src/mol-io/reader/pdb/parser.ts

@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PdbFile } from './schema';
+import { Task } from 'mol-task';
+import { ReaderResult } from '../result';
+import { Tokenizer } from '../common/text/tokenizer';
+
+export function parsePDB(data: string, id?: string): Task<ReaderResult<PdbFile>> {
+    return Task.create('Parse PDB', async ctx => ReaderResult.success({ id, lines: await Tokenizer.readAllLinesAsync(data, ctx) }));
+}

+ 12 - 0
src/mol-io/reader/pdb/schema.ts

@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Tokens } from '../common/text/tokenizer';
+
+export interface PdbFile {
+    id?: string,
+    lines: Tokens
+}

+ 17 - 17
src/mol-io/reader/result.ts

@@ -5,7 +5,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-type ReaderResult<T> = Success<T> | Error
+type ReaderResult<T> = ReaderResult.Success<T> | ReaderResult.Error
 
 namespace ReaderResult {
     export function error<T>(message: string, line = -1): ReaderResult<T> {
@@ -15,28 +15,28 @@ namespace ReaderResult {
     export function success<T>(result: T, warnings: string[] = []): ReaderResult<T> {
         return new Success<T>(result, warnings);
     }
-}
 
-export class Error {
-    isError: true = true;
+    export class Error {
+        isError: true = true;
 
-    toString() {
-        if (this.line >= 0) {
-            return `[Line ${this.line}] ${this.message}`;
+        toString() {
+            if (this.line >= 0) {
+                return `[Line ${this.line}] ${this.message}`;
+            }
+            return this.message;
         }
-        return this.message;
-    }
 
-    constructor(
-        public message: string,
-        public line: number) {
+        constructor(
+            public message: string,
+            public line: number) {
+        }
     }
-}
 
-export class Success<T> {
-    isError: false = false;
+    export class Success<T> {
+        isError: false = false;
 
-    constructor(public result: T, public warnings: string[]) { }
+        constructor(public result: T, public warnings: string[]) { }
+    }
 }
 
-export default ReaderResult
+export { ReaderResult }

+ 4 - 0
src/mol-math/linear-algebra/3d/mat4.ts

@@ -119,6 +119,10 @@ namespace Mat4 {
         a[4 * j + i] = value;
     }
 
+    export function getValue(a: Mat4, i: number, j: number) {
+        return a[4 * j + i];
+    }
+
     export function toArray(a: Mat4, out: NumberArray, offset: number) {
         out[offset + 0] = a[0];
         out[offset + 1] = a[1];

+ 18 - 0
src/mol-model-formats/structure/format.ts

@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
+import CIF, { CifFrame } from 'mol-io/reader/cif';
+
+type ModelFormat =
+    | ModelFormat.mmCIF
+
+namespace ModelFormat {
+    export interface mmCIF { kind: 'mmCIF', data: mmCIF_Database, frame: CifFrame }
+    export function mmCIF(frame: CifFrame, data?: mmCIF_Database): mmCIF { return { kind: 'mmCIF', data: data || CIF.schema.mmCIF(frame), frame }; }
+}
+
+export { ModelFormat }

+ 0 - 0
src/mol-model/structure/model/formats/gro.ts → src/mol-model-formats/structure/gro.ts


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

@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Model } from 'mol-model/structure/model/model';
+import { Task } from 'mol-task';
+import { ModelFormat } from './format';
+import { _parse_mmCif } from './mmcif/parser';
+import { CifFrame } from 'mol-io/reader/cif';
+
+export function trajectoryFromMmCIF(frame: CifFrame): Task<Model.Trajectory> {
+    return Task.create('Create mmCIF Model', ctx => _parse_mmCif(ModelFormat.mmCIF(frame), ctx));
+}

+ 5 - 6
src/mol-model/structure/model/formats/mmcif/assembly.ts → src/mol-model-formats/structure/mmcif/assembly.ts

@@ -6,12 +6,11 @@
 
 import { Mat4, Tensor } from 'mol-math/linear-algebra'
 import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
-import Format from '../../format'
-import { Assembly, OperatorGroup, OperatorGroups } from '../../properties/symmetry'
-import { Queries as Q } from '../../../query'
-
-import mmCIF_Format = Format.mmCIF
-import { StructureProperties } from '../../../structure';
+import { Assembly, OperatorGroup, OperatorGroups } from 'mol-model/structure/model/properties/symmetry'
+import { Queries as Q } from 'mol-model/structure'
+import { StructureProperties } from 'mol-model/structure';
+import { ModelFormat } from '../format';
+import mmCIF_Format = ModelFormat.mmCIF
 
 export function createAssemblies(format: mmCIF_Format): ReadonlyArray<Assembly> {
     const { pdbx_struct_assembly } = format.data;

+ 13 - 13
src/mol-model/structure/model/formats/mmcif/atomic.ts → src/mol-model-formats/structure/mmcif/atomic.ts

@@ -8,18 +8,18 @@ import { Column, Table } from 'mol-data/db';
 import { Interval, Segmentation } from 'mol-data/int';
 import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
 import UUID from 'mol-util/uuid';
-import { ElementIndex } from '../../../../structure';
-import Format from '../../format';
-import { Model } from '../../model';
-import { AtomicConformation, AtomicData, AtomicHierarchy, AtomicSegments, AtomsSchema, ChainsSchema, ResiduesSchema } from '../../properties/atomic';
-import { getAtomicIndex } from '../../properties/utils/atomic-index';
-import { ElementSymbol } from '../../types';
-import { Entities } from '../../properties/common';
-
-import mmCIF_Format = Format.mmCIF
-import { getAtomicRanges } from '../../properties/utils/atomic-ranges';
-import { FormatData } from '../mmcif';
-import { getAtomicDerivedData } from '../../properties/utils/atomic-derived';
+import { ElementIndex } from 'mol-model/structure';
+import { Model } from 'mol-model/structure/model/model';
+import { AtomicConformation, AtomicData, AtomicHierarchy, AtomicSegments, AtomsSchema, ChainsSchema, ResiduesSchema } from 'mol-model/structure/model/properties/atomic';
+import { getAtomicIndex } from 'mol-model/structure/model/properties/utils/atomic-index';
+import { ElementSymbol } from 'mol-model/structure/model/types';
+import { Entities } from 'mol-model/structure/model/properties/common';
+import { getAtomicRanges } from 'mol-model/structure/model/properties/utils/atomic-ranges';
+import { getAtomicDerivedData } from 'mol-model/structure/model/properties/utils/atomic-derived';
+import { ModelFormat } from '../format';
+import mmCIF_Format = ModelFormat.mmCIF
+import { FormatData } from './parser';
+
 
 type AtomSite = mmCIF_Database['atom_site']
 
@@ -101,7 +101,7 @@ export function getAtomicHierarchyAndConformation(format: mmCIF_Format, atom_sit
 
     const index = getAtomicIndex(hierarchyData, entities, hierarchySegments);
     const derived = getAtomicDerivedData(hierarchyData, index, formatData.chemicalComponentMap);
-    const hierarchyRanges = getAtomicRanges(hierarchyData, hierarchySegments, conformation, formatData.chemicalComponentMap);
+    const hierarchyRanges = getAtomicRanges(hierarchyData, hierarchySegments, conformation, derived.residue.moleculeType);
     const hierarchy: AtomicHierarchy = { ...hierarchyData, ...hierarchySegments, ...hierarchyRanges, index, derived };
     return { sameAsPrevious: false, hierarchy, conformation };
 }

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


+ 4 - 4
src/mol-model/structure/model/formats/mmcif/bonds/comp.ts → src/mol-model-formats/structure/mmcif/bonds/comp.ts

@@ -5,11 +5,11 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../model'
-import { LinkType } from '../../../types'
-import { ModelPropertyDescriptor } from '../../../properties/custom';
+import { Model } from 'mol-model/structure/model/model'
+import { LinkType } from 'mol-model/structure/model/types'
+import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
 import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
-import { Structure, Unit, StructureProperties, StructureElement } from '../../../../structure';
+import { Structure, Unit, StructureProperties, StructureElement } from 'mol-model/structure';
 import { Segmentation } from 'mol-data/int';
 import { CifWriter } from 'mol-io/writer/cif'
 

+ 5 - 5
src/mol-model/structure/model/formats/mmcif/bonds/struct_conn.ts → src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts

@@ -5,16 +5,16 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../model'
-import { Structure } from '../../../../structure'
-import { LinkType } from '../../../types'
+import { Model } from 'mol-model/structure/model/model'
+import { Structure } from 'mol-model/structure'
+import { LinkType } from 'mol-model/structure/model/types'
 import { findEntityIdByAsymId, findAtomIndexByLabelName } from '../util'
 import { Column } from 'mol-data/db'
-import { ModelPropertyDescriptor } from '../../../properties/custom';
+import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { SortedArray } from 'mol-data/int';
 import { CifWriter } from 'mol-io/writer/cif'
-import { ElementIndex, ResidueIndex } from '../../../indexing';
+import { ElementIndex, ResidueIndex } from 'mol-model/structure/model/indexing';
 
 export interface StructConn {
     getResidueEntries(residueAIndex: ResidueIndex, residueBIndex: ResidueIndex): ReadonlyArray<StructConn.Entry>,

+ 6 - 6
src/mol-model/structure/model/formats/mmcif/ihm.ts → src/mol-model-formats/structure/mmcif/ihm.ts

@@ -5,16 +5,16 @@
  */
 
 import { mmCIF_Database as mmCIF, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'
-import { CoarseHierarchy, CoarseConformation, CoarseElementData, CoarseSphereConformation, CoarseGaussianConformation } from '../../properties/coarse'
-import { Entities } from '../../properties/common';
+import { CoarseHierarchy, CoarseConformation, CoarseElementData, CoarseSphereConformation, CoarseGaussianConformation } from 'mol-model/structure/model/properties/coarse'
+import { Entities } from 'mol-model/structure/model/properties/common';
 import { Column } from 'mol-data/db';
-import { getCoarseKeys } from '../../properties/utils/coarse-keys';
+import { getCoarseKeys } from 'mol-model/structure/model/properties/utils/coarse-keys';
 import { UUID } from 'mol-util';
 import { Segmentation, Interval } from 'mol-data/int';
 import { Mat3, Tensor } from 'mol-math/linear-algebra';
-import { ElementIndex, ChainIndex } from '../../indexing';
-import { getCoarseRanges } from '../../properties/utils/coarse-ranges';
-import { FormatData } from '../mmcif';
+import { ElementIndex, ChainIndex } from 'mol-model/structure/model/indexing';
+import { getCoarseRanges } from 'mol-model/structure/model/properties/utils/coarse-ranges';
+import { FormatData } from './parser';
 
 export interface IHMData {
     model_id: number,

+ 0 - 0
src/mol-model/structure/model/formats/mmcif/pair-restraint.ts → src/mol-model-formats/structure/mmcif/pair-restraint.ts


+ 3 - 3
src/mol-model/structure/model/formats/mmcif/pair-restraints/cross-links.ts → src/mol-model-formats/structure/mmcif/pair-restraints/cross-links.ts

@@ -4,12 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../model'
+import { Model } from 'mol-model/structure/model/model'
 import { Table } from 'mol-data/db'
 import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { findAtomIndexByLabelName } from '../util';
-import { Unit } from '../../../../structure';
-import { ElementIndex } from '../../../indexing';
+import { Unit } from 'mol-model/structure';
+import { ElementIndex } from 'mol-model/structure/model/indexing';
 
 function findAtomIndex(model: Model, entityId: string, asymId: string, seqId: number, atomId: string) {
     if (!model.atomicHierarchy.atoms.auth_atom_id.isDefined) return -1

+ 0 - 0
src/mol-model/structure/model/formats/mmcif/pair-restraints/predicted-contacts.ts → src/mol-model-formats/structure/mmcif/pair-restraints/predicted-contacts.ts


+ 46 - 45
src/mol-model/structure/model/formats/mmcif.ts → src/mol-model-formats/structure/mmcif/parser.ts

@@ -9,26 +9,32 @@ import { Column, Table } from 'mol-data/db';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { Spacegroup, SpacegroupCell, SymmetryOperator } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';
-import { Task, RuntimeContext } from 'mol-task';
+import { RuntimeContext } from 'mol-task';
 import UUID from 'mol-util/uuid';
-import Format from '../format';
-import { Model } from '../model';
-import { Entities } from '../properties/common';
-import { CustomProperties } from '../properties/custom';
-import { ModelSymmetry } from '../properties/symmetry';
-import { createAssemblies } from './mmcif/assembly';
-import { getAtomicHierarchyAndConformation } from './mmcif/atomic';
-import { ComponentBond } from './mmcif/bonds';
-import { getIHMCoarse, EmptyIHMCoarse, IHMData } from './mmcif/ihm';
-import { getSecondaryStructureMmCif } from './mmcif/secondary-structure';
-import { getSequence } from './mmcif/sequence';
-import { sortAtomSite } from './mmcif/sort';
-import { StructConn } from './mmcif/bonds/struct_conn';
-import { ChemicalComponent, ChemicalComponentMap } from '../properties/chemical-component';
-import { ComponentType, getMoleculeType, MoleculeType } from '../types';
-
-import mmCIF_Format = Format.mmCIF
+import { Model } from 'mol-model/structure/model/model';
+import { Entities } from 'mol-model/structure/model/properties/common';
+import { CustomProperties } from 'mol-model/structure/model/properties/custom';
+import { ModelSymmetry } from 'mol-model/structure/model/properties/symmetry';
+import { createAssemblies } from './assembly';
+import { getAtomicHierarchyAndConformation } from './atomic';
+import { ComponentBond } from './bonds';
+import { getIHMCoarse, EmptyIHMCoarse, IHMData } from './ihm';
+import { getSecondaryStructureMmCif } from './secondary-structure';
+import { getSequence } from './sequence';
+import { sortAtomSite } from './sort';
+import { StructConn } from './bonds/struct_conn';
+import { ChemicalComponent } from 'mol-model/structure/model/properties/chemical-component';
+import { getMoleculeType, MoleculeType } from 'mol-model/structure/model/types';
+import { ModelFormat } from '../format';
 import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from 'mol-model/structure/structure/carbohydrates/constants';
+import mmCIF_Format = ModelFormat.mmCIF
+import { memoize1 } from 'mol-util/memoize';
+
+export async function _parse_mmCif(format: mmCIF_Format, ctx: RuntimeContext) {
+    const formatData = getFormatData(format)
+    const isIHM = format.data.ihm_model_list._rowCount > 0;
+    return isIHM ? await readIHM(ctx, format, formatData) : await readStandard(ctx, format, formatData);
+}
 
 type AtomSite = mmCIF_Database['atom_site']
 
@@ -73,6 +79,7 @@ function getNcsOperators(format: mmCIF_Format) {
     }
     return opers;
 }
+
 function getModifiedResidueNameMap(format: mmCIF_Format): Model['properties']['modifiedResidues'] {
     const data = format.data.pdbx_struct_mod_residue;
     const parentId = new Map<string, string>();
@@ -89,22 +96,14 @@ function getModifiedResidueNameMap(format: mmCIF_Format): Model['properties']['m
     return { parentId, details };
 }
 
-function getChemicalComponentMap(format: mmCIF_Format): ChemicalComponentMap {
+function getChemicalComponentMap(format: mmCIF_Format): Model['properties']['chemicalComponentMap'] {
     const map = new Map<string, ChemicalComponent>();
-    const { id, type, name, pdbx_synonyms, formula, formula_weight } = format.data.chem_comp
-    for (let i = 0, il = id.rowCount; i < il; ++i) {
-        const _id = id.value(i)
-        const _type = type.value(i)
-        const cc: ChemicalComponent = {
-            id: _id,
-            type: ComponentType[_type],
-            moleculeType: getMoleculeType(_type, _id),
-            name: name.value(i),
-            synonyms: pdbx_synonyms.value(i),
-            formula: formula.value(i),
-            formulaWeight: formula_weight.value(i),
+    const { chem_comp } = format.data
+    if (chem_comp._rowCount > 0) {
+        const { id } = format.data.chem_comp
+        for (let i = 0, il = id.rowCount; i < il; ++i) {
+            map.set(id.value(i), Table.getRow(format.data.chem_comp, i))
         }
-        map.set(_id, cc)
     }
     return map
 }
@@ -137,12 +136,24 @@ function getSaccharideComponentMap(format: mmCIF_Format): SaccharideComponentMap
             }
         }
     } else {
-        // TODO check if present in format.data.atom_site.label_comp_id
-        SaccharideCompIdMap.forEach((v, k) => map.set(k, v))
+        const uniqueNames = getUniqueComponentNames(format)
+        SaccharideCompIdMap.forEach((v, k) => {
+            if (uniqueNames.has(k)) map.set(k, v)
+        })
     }
     return map
 }
 
+const getUniqueComponentNames = memoize1((format: mmCIF_Format) => {
+    const uniqueNames = new Set<string>()
+    const data = format.data.atom_site
+    const comp_id = data.label_comp_id.isDefined ? data.label_comp_id : data.auth_comp_id;
+    for (let i = 0, il = comp_id.rowCount; i < il; ++i) {
+        uniqueNames.add(comp_id.value(i))
+    }
+    return uniqueNames
+})
+
 export interface FormatData {
     modifiedResidues: Model['properties']['modifiedResidues']
     chemicalComponentMap: Model['properties']['chemicalComponentMap']
@@ -298,14 +309,4 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo
     }
 
     return models;
-}
-
-function buildModels(format: mmCIF_Format): Task<ReadonlyArray<Model>> {
-    const formatData = getFormatData(format)
-    return Task.create('Create mmCIF Model', async ctx => {
-        const isIHM = format.data.ihm_model_list._rowCount > 0;
-        return isIHM ? await readIHM(ctx, format, formatData) : await readStandard(ctx, format, formatData);
-    });
-}
-
-export default buildModels;
+}

+ 4 - 4
src/mol-model/structure/model/formats/mmcif/secondary-structure.ts → src/mol-model-formats/structure/mmcif/secondary-structure.ts

@@ -6,11 +6,11 @@
  */
 
 import { mmCIF_Database as mmCIF, mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'
-import { SecondaryStructureType } from '../../types';
-import { AtomicHierarchy } from '../../properties/atomic';
-import { SecondaryStructure } from '../../properties/seconday-structure';
+import { SecondaryStructureType } from 'mol-model/structure/model/types';
+import { AtomicHierarchy } from 'mol-model/structure/model/properties/atomic';
+import { SecondaryStructure } from 'mol-model/structure/model/properties/seconday-structure';
 import { Column } from 'mol-data/db';
-import { ChainIndex, ResidueIndex } from '../../indexing';
+import { ChainIndex, ResidueIndex } from 'mol-model/structure/model/indexing';
 
 export function getSecondaryStructureMmCif(data: mmCIF_Database, hierarchy: AtomicHierarchy): SecondaryStructure {
     const map: SecondaryStructureMap = new Map();

+ 4 - 4
src/mol-model/structure/model/formats/mmcif/sequence.ts → src/mol-model-formats/structure/mmcif/sequence.ts

@@ -5,11 +5,11 @@
  */
 
 import { mmCIF_Database as mmCIF } from 'mol-io/reader/cif/schema/mmcif'
-import StructureSequence from '../../properties/sequence'
+import StructureSequence from 'mol-model/structure/model/properties/sequence'
 import { Column } from 'mol-data/db';
-import { AtomicHierarchy } from '../../properties/atomic';
-import { Entities } from '../../properties/common';
-import { Sequence } from '../../../../sequence';
+import { AtomicHierarchy } from 'mol-model/structure/model/properties/atomic';
+import { Entities } from 'mol-model/structure/model/properties/common';
+import { Sequence } from 'mol-model/sequence';
 
 // TODO how to handle microheterogeneity
 //    see http://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Categories/entity_poly_seq.html

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


+ 2 - 2
src/mol-model/structure/model/formats/mmcif/util.ts → src/mol-model-formats/structure/mmcif/util.ts

@@ -4,8 +4,8 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../model'
-import { ElementIndex } from '../../indexing';
+import { Model } from 'mol-model/structure/model'
+import { ElementIndex } from 'mol-model/structure/model/indexing';
 
 export function findEntityIdByAsymId(model: Model, asymId: string) {
     if (model.sourceData.kind !== 'mmCIF') return ''

+ 21 - 0
src/mol-model-formats/structure/pdb.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PdbFile } from 'mol-io/reader/pdb/schema';
+import { pdbToMmCif } from './pdb/to-cif';
+import { Model } from 'mol-model/structure/model';
+import { Task } from 'mol-task';
+import { ModelFormat } from './format';
+import { _parse_mmCif } from './mmcif/parser';
+
+export function trajectoryFromPDB(pdb: PdbFile): Task<Model.Trajectory> {
+    return Task.create('Parse PDB', async ctx => {
+        await ctx.update('Converting to mmCIF');
+        const cif = await pdbToMmCif(pdb);
+        const format = ModelFormat.mmCIF(cif);
+        return _parse_mmCif(format, ctx);
+    })
+}

+ 234 - 0
src/mol-model-formats/structure/pdb/assembly.ts

@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CifCategory, CifField } from 'mol-io/reader/cif';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { Mat4 } from 'mol-math/linear-algebra';
+import { Tokens } from 'mol-io/reader/common/text/tokenizer';
+
+export function parseCryst1(id: string, record: string): CifCategory[] {
+    // COLUMNS       DATA TYPE      CONTENTS
+    // --------------------------------------------------------------------------------
+    //  1 -  6       Record name    "CRYST1"
+    //  7 - 15       Real(9.3)      a (Angstroms)
+    // 16 - 24       Real(9.3)      b (Angstroms)
+    // 25 - 33       Real(9.3)      c (Angstroms)
+    // 34 - 40       Real(7.2)      alpha (degrees)
+    // 41 - 47       Real(7.2)      beta (degrees)
+    // 48 - 54       Real(7.2)      gamma (degrees)
+    // 56 - 66       LString        Space group
+    // 67 - 70       Integer        Z value
+
+    const get = (s: number, l: number) => (record.substr(s, l) || '').trim()
+
+    const cell: CifCategory.Fields<mmCIF_Schema['cell']> = {
+        entry_id: CifField.ofString(id),
+        length_a: CifField.ofString(get(6, 9)),
+        length_b: CifField.ofString(get(15, 9)),
+        length_c: CifField.ofString(get(24, 9)),
+        angle_alpha: CifField.ofString(get(33, 7)),
+        angle_beta: CifField.ofString(get(40, 7)),
+        angle_gamma: CifField.ofString(get(47, 7)),
+        Z_PDB: CifField.ofString(get(66, 4)),
+        pdbx_unique_axis: CifField.ofString('?')
+    };
+    const symmetry: CifCategory.Fields<mmCIF_Schema['symmetry']> = {
+        entry_id: CifField.ofString(id),
+        'space_group_name_H-M': CifField.ofString(get(55, 11)),
+        Int_Tables_number: CifField.ofString('?'),
+        cell_setting: CifField.ofString('?'),
+        space_group_name_Hall: CifField.ofString('?')
+    }
+    return [CifCategory.ofFields('cell', cell), CifCategory.ofFields('symmetry', symmetry)];
+}
+
+interface PdbAssembly {
+    id: string,
+    details: string,
+    groups: { chains: string[], operators: { id: number, matrix: Mat4 }[] }[]
+}
+
+function PdbAssembly(id: string, details: string): PdbAssembly {
+    return { id, details, groups: [] };
+}
+
+export function parseRemark350(lines: Tokens, lineStart: number, lineEnd: number): CifCategory[] {
+    const assemblies: PdbAssembly[] = [];
+
+    // Read the assemblies
+    let current: PdbAssembly, group: PdbAssembly['groups'][0], matrix: Mat4, operId = 1;
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+        if (line.substr(11, 12) === 'BIOMOLECULE:') {
+            const id = line.substr(23).trim();
+            let details: string = `Biomolecule ` + id;
+            line = getLine(i + 1);
+            if (line.substr(11, 30) !== 'APPLY THE FOLLOWING TO CHAINS:') {
+                i++;
+                details = line.substr(11).trim();
+            }
+            current = PdbAssembly(id, details);
+            assemblies.push(current);
+        } else if (line.substr(13, 5) === 'BIOMT') {
+            const biomt = line.split(/\s+/)
+            const row = parseInt(line[18]) - 1
+
+            if (row === 0) {
+                matrix = Mat4.identity();
+                group!.operators.push({ id: operId++, matrix });
+            }
+
+            Mat4.setValue(matrix!, row, 0, parseFloat(biomt[4]));
+            Mat4.setValue(matrix!, row, 1, parseFloat(biomt[5]));
+            Mat4.setValue(matrix!, row, 2, parseFloat(biomt[6]));
+            Mat4.setValue(matrix!, row, 3, parseFloat(biomt[7]));
+        } else if (
+            line.substr(11, 30) === 'APPLY THE FOLLOWING TO CHAINS:' ||
+            line.substr(11, 30) === '                   AND CHAINS:') {
+
+            if (line.substr(11, 5) === 'APPLY') {
+                group = { chains: [], operators: [] };
+                current!.groups.push(group);
+            }
+
+            const chainList = line.substr(41, 30).split(',');
+            for (let j = 0, jl = chainList.length; j < jl; ++j) {
+                const c = chainList[j].trim();
+                if (c) group!.chains.push(c);
+            }
+        }
+    }
+
+    if (assemblies.length === 0) return [];
+
+    // Generate CIF
+
+    // pdbx_struct_assembly
+    const pdbx_struct_assembly: CifCategory.SomeFields<mmCIF_Schema['pdbx_struct_assembly']> = {
+        id: CifField.ofStrings(assemblies.map(a => a.id)),
+        details: CifField.ofStrings(assemblies.map(a => a.details))
+    };
+
+
+    // pdbx_struct_assembly_gen
+    const pdbx_struct_assembly_gen_rows: { [P in keyof CifCategory.Fields<mmCIF_Schema['pdbx_struct_assembly_gen']>]: string }[] = [];
+    for (const asm of assemblies) {
+        for (const group of asm.groups) {
+            pdbx_struct_assembly_gen_rows.push({
+                assembly_id: asm.id,
+                oper_expression: group.operators.map(o => o.id).join(','),
+                asym_id_list: group.chains.join(',')
+            });
+        }
+    }
+    const pdbx_struct_assembly_gen: CifCategory.Fields<mmCIF_Schema['pdbx_struct_assembly_gen']> = {
+        assembly_id: CifField.ofStrings(pdbx_struct_assembly_gen_rows.map(r => r.assembly_id)),
+        oper_expression: CifField.ofStrings(pdbx_struct_assembly_gen_rows.map(r => r.oper_expression)),
+        asym_id_list: CifField.ofStrings(pdbx_struct_assembly_gen_rows.map(r => r.asym_id_list))
+    };
+
+    // pdbx_struct_oper_list
+    const pdbx_struct_oper_list_rows: { [P in keyof CifCategory.Fields<mmCIF_Schema['pdbx_struct_oper_list']>]?: string }[] = [];
+    for (const asm of assemblies) {
+        for (const group of asm.groups) {
+            for (const oper of group.operators) {
+                const row = {
+                    id: '' + oper.id,
+                    type: '?',
+                    name: '?',
+                    symmetry_operation: '?'
+                } as (typeof pdbx_struct_oper_list_rows)[0] as any;
+                for (let i = 0; i < 3; i++) {
+                    for (let j = 0; j < 3; j++) {
+                        row[`matrix[${i + 1}][${j + 1}]`] = '' + Mat4.getValue(oper.matrix, i, j);
+                    }
+                    row[`vector[${i + 1}]`] = '' + Mat4.getValue(oper.matrix, i, 3);
+                }
+                pdbx_struct_oper_list_rows.push(row);
+            }
+        }
+    }
+
+    const pdbx_struct_oper_list: CifCategory.SomeFields<mmCIF_Schema['pdbx_struct_oper_list']> = {
+        id: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.id!)),
+        type: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.type!)),
+        name: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.name!)),
+        symmetry_operation: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.symmetry_operation!))
+    };
+    for (let i = 0; i < 3; i++) {
+        for (let j = 0; j < 3; j++) {
+            const k = `matrix[${i + 1}][${j + 1}]`;
+            (pdbx_struct_oper_list as any)[k] = CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => (r as any)[k]!));
+        }
+        const k = `vector[${i + 1}]`;
+        (pdbx_struct_oper_list as any)[k] = CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => (r as any)[k]!));
+    }
+
+    return [
+        CifCategory.ofFields('pdbx_struct_assembly', pdbx_struct_assembly),
+        CifCategory.ofFields('pdbx_struct_assembly_gen', pdbx_struct_assembly_gen),
+        CifCategory.ofFields('pdbx_struct_oper_list', pdbx_struct_oper_list)
+    ];
+}
+
+export function parseMtrix(lines: Tokens, lineStart: number, lineEnd: number): CifCategory[] {
+    const matrices: Mat4[] = [];
+    let matrix: Mat4;
+
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+
+        const ncs = line.split(/\s+/);
+        const row = parseInt(line[5]) - 1;
+
+        if (row === 0) {
+            matrix = Mat4.identity();
+            matrices.push(matrix);
+        }
+
+        Mat4.setValue(matrix!, row, 0, parseFloat(ncs[2]));
+        Mat4.setValue(matrix!, row, 1, parseFloat(ncs[3]));
+        Mat4.setValue(matrix!, row, 2, parseFloat(ncs[4]));
+        Mat4.setValue(matrix!, row, 3, parseFloat(ncs[5]));
+    }
+
+    if (matrices.length === 0) return [];
+
+    const struct_ncs_oper_rows: { [P in keyof CifCategory.Fields<mmCIF_Schema['struct_ncs_oper']>]?: string }[] = [];
+    let id = 1;
+    for (const oper of matrices) {
+            const row = {
+                id: 'ncsop' + (id++),
+                code: '.',
+                details: '.'
+            } as (typeof struct_ncs_oper_rows)[0] as any;
+            for (let i = 0; i < 3; i++) {
+                for (let j = 0; j < 3; j++) {
+                    row[`matrix[${i + 1}][${j + 1}]`] = '' + Mat4.getValue(oper, i, j);
+                }
+                row[`vector[${i + 1}]`] = '' + Mat4.getValue(oper, i, 3);
+            }
+            struct_ncs_oper_rows.push(row);
+    }
+
+    const struct_ncs_oper: CifCategory.SomeFields<mmCIF_Schema['struct_ncs_oper']> = {
+        id: CifField.ofStrings(struct_ncs_oper_rows.map(r => r.id!)),
+        code: CifField.ofStrings(struct_ncs_oper_rows.map(r => r.code!)),
+        details: CifField.ofStrings(struct_ncs_oper_rows.map(r => r.details!)),
+    };
+    for (let i = 0; i < 3; i++) {
+        for (let j = 0; j < 3; j++) {
+            const k = `matrix[${i + 1}][${j + 1}]`;
+            (struct_ncs_oper as any)[k] = CifField.ofStrings(struct_ncs_oper_rows.map(r => (r as any)[k]!));
+        }
+        const k = `vector[${i + 1}]`;
+        (struct_ncs_oper as any)[k] = CifField.ofStrings(struct_ncs_oper_rows.map(r => (r as any)[k]!));
+    }
+
+    return [CifCategory.ofFields('struct_ncs_oper', struct_ncs_oper)];
+}

+ 262 - 0
src/mol-model-formats/structure/pdb/secondary-structure.ts

@@ -0,0 +1,262 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { CifCategory, CifField } from 'mol-io/reader/cif';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { Tokens } from 'mol-io/reader/common/text/tokenizer';
+
+const HelixTypes: {[k: string]: mmCIF_Schema['struct_conf']['conf_type_id']['T']} = {
+    // CLASS NUMBER
+    // TYPE OF  HELIX                     (COLUMNS 39 - 40)
+    // --------------------------------------------------------------
+    // Right-handed alpha (default)                1
+    // Right-handed omega                          2
+    // Right-handed pi                             3
+    // Right-handed gamma                          4
+    // Right-handed 3 - 10                         5
+    // Left-handed alpha                           6
+    // Left-handed omega                           7
+    // Left-handed gamma                           8
+    // 2 - 7 ribbon/helix                          9
+    // Polyproline                                10
+    1: 'HELX_RH_AL_P',
+    2: 'HELX_RH_OM_P',
+    3: 'HELX_RH_PI_P',
+    4: 'HELX_RH_GA_P',
+    5: 'HELX_RH_3T_P',
+    6: 'HELX_LH_AL_P',
+    7: 'HELX_LH_OM_P',
+    8: 'HELX_LH_GA_P',
+    9: 'HELX_RH_27_P', // TODO or left-handed???
+    10: 'HELX_RH_PP_P', // TODO or left-handed???
+}
+function getStructConfTypeId(type: string): mmCIF_Schema['struct_conf']['conf_type_id']['T'] {
+    return HelixTypes[type] || 'HELX_P'
+}
+
+interface PdbHelix {
+    serNum: string,
+    helixID: string,
+    initResName: string,
+    initChainID: string,
+    initSeqNum: string,
+    initICode: string,
+    endResName: string,
+    endChainID: string,
+    endSeqNum: string,
+    endICode: string,
+    helixClass: string,
+    comment: string,
+    length: string
+}
+
+export function parseHelix(lines: Tokens, lineStart: number, lineEnd: number): CifCategory {
+    const helices: PdbHelix[] = []
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+        // COLUMNS        DATA  TYPE     FIELD         DEFINITION
+        // -----------------------------------------------------------------------------------
+        // 1 -  6        Record name    "HELIX "
+        // 8 - 10        Integer        serNum        Serial number of the helix. This starts
+        //                                             at 1  and increases incrementally.
+        // 12 - 14        LString(3)     helixID       Helix  identifier. In addition to a serial
+        //                                             number, each helix is given an
+        //                                             alphanumeric character helix identifier.
+        // 16 - 18        Residue name   initResName   Name of the initial residue.
+        // 20             Character      initChainID   Chain identifier for the chain containing
+        //                                             this  helix.
+        // 22 - 25        Integer        initSeqNum    Sequence number of the initial residue.
+        // 26             AChar          initICode     Insertion code of the initial residue.
+        // 28 - 30        Residue  name  endResName    Name of the terminal residue of the helix.
+        // 32             Character      endChainID    Chain identifier for the chain containing
+        //                                             this  helix.
+        // 34 - 37        Integer        endSeqNum     Sequence number of the terminal residue.
+        // 38             AChar          endICode      Insertion code of the terminal residue.
+        // 39 - 40        Integer        helixClass    Helix class (see below).
+        // 41 - 70        String         comment       Comment about this helix.
+        // 72 - 76        Integer        length        Length of this helix.
+        helices.push({
+            serNum: line.substr(7, 3).trim(),
+            helixID: line.substr(11, 3).trim(),
+            initResName: line.substr(15, 3).trim(),
+            initChainID: line.substr(19, 1).trim(),
+            initSeqNum: line.substr(21, 4).trim(),
+            initICode: line.substr(25, 1).trim(),
+            endResName: line.substr(27, 3).trim(),
+            endChainID: line.substr(31, 3).trim(),
+            endSeqNum: line.substr(33, 4).trim(),
+            endICode: line.substr(37, 1).trim(),
+            helixClass: line.substr(38, 2).trim(),
+            comment: line.substr(40, 30).trim(),
+            length: line.substr(71, 5).trim()
+        })
+    }
+
+    const beg_auth_asym_id = CifField.ofStrings(helices.map(h => h.initChainID))
+    const beg_auth_comp_id = CifField.ofStrings(helices.map(h => h.initResName))
+    const beg_auth_seq_id = CifField.ofStrings(helices.map(h => h.initSeqNum))
+
+    const end_auth_asym_id = CifField.ofStrings(helices.map(h => h.endChainID))
+    const end_auth_comp_id = CifField.ofStrings(helices.map(h => h.endResName))
+    const end_auth_seq_id = CifField.ofStrings(helices.map(h => h.endSeqNum))
+
+    const struct_conf: CifCategory.Fields<mmCIF_Schema['struct_conf']> = {
+        beg_label_asym_id: beg_auth_asym_id,
+        beg_label_comp_id: beg_auth_comp_id,
+        beg_label_seq_id: beg_auth_seq_id,
+        beg_auth_asym_id,
+        beg_auth_comp_id,
+        beg_auth_seq_id,
+
+        conf_type_id: CifField.ofStrings(helices.map(h => getStructConfTypeId(h.helixClass))),
+        details: CifField.ofStrings(helices.map(h => h.comment)),
+
+        end_label_asym_id: end_auth_asym_id,
+        end_label_comp_id: end_auth_asym_id,
+        end_label_seq_id: end_auth_seq_id,
+        end_auth_asym_id,
+        end_auth_comp_id,
+        end_auth_seq_id,
+
+        id: CifField.ofStrings(helices.map(h => h.serNum)),
+        pdbx_beg_PDB_ins_code: CifField.ofStrings(helices.map(h => h.initICode)),
+        pdbx_end_PDB_ins_code: CifField.ofStrings(helices.map(h => h.endICode)),
+        pdbx_PDB_helix_class: CifField.ofStrings(helices.map(h => h.helixClass)),
+        pdbx_PDB_helix_length: CifField.ofStrings(helices.map(h => h.length)),
+        pdbx_PDB_helix_id: CifField.ofStrings(helices.map(h => h.helixID)),
+    };
+    return CifCategory.ofFields('struct_conf', struct_conf);
+}
+
+//
+
+interface PdbSheet {
+    strand: string,
+    sheetID: string,
+    numStrands: string,
+    initResName: string,
+    initChainID: string,
+    initSeqNum: string,
+    initICode: string,
+    endResName: string,
+    endChainID: string,
+    endSeqNum: string,
+    endICode: string,
+    sense: string,
+    curAtom: string,
+    curResName: string,
+    curChainId: string,
+    curResSeq: string,
+    curICode: string,
+    prevAtom: string,
+    prevResName: string,
+    prevChainId: string,
+    prevResSeq: string,
+    prevICode: string,
+}
+
+export function parseSheet(lines: Tokens, lineStart: number, lineEnd: number): CifCategory {
+    const sheets: PdbSheet[] = []
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+        // COLUMNS       DATA  TYPE     FIELD          DEFINITION
+        // -------------------------------------------------------------------------------------
+        // 1 -  6        Record name   "SHEET "
+        // 8 - 10        Integer       strand         Strand  number which starts at 1 for each
+        //                                             strand within a sheet and increases by one.
+        // 12 - 14        LString(3)    sheetID        Sheet  identifier.
+        // 15 - 16        Integer       numStrands     Number  of strands in sheet.
+        // 18 - 20        Residue name  initResName    Residue  name of initial residue.
+        // 22             Character     initChainID    Chain identifier of initial residue
+        //                                             in strand.
+        // 23 - 26        Integer       initSeqNum     Sequence number of initial residue
+        //                                             in strand.
+        // 27             AChar         initICode      Insertion code of initial residue
+        //                                             in  strand.
+        // 29 - 31        Residue name  endResName     Residue name of terminal residue.
+        // 33             Character     endChainID     Chain identifier of terminal residue.
+        // 34 - 37        Integer       endSeqNum      Sequence number of terminal residue.
+        // 38             AChar         endICode       Insertion code of terminal residue.
+        // 39 - 40        Integer       sense          Sense of strand with respect to previous
+        //                                             strand in the sheet. 0 if first strand,
+        //                                             1 if  parallel,and -1 if anti-parallel.
+        // 42 - 45        Atom          curAtom        Registration.  Atom name in current strand.
+        // 46 - 48        Residue name  curResName     Registration.  Residue name in current strand
+        // 50             Character     curChainId     Registration. Chain identifier in
+        //                                             current strand.
+        // 51 - 54        Integer       curResSeq      Registration.  Residue sequence number
+        //                                             in current strand.
+        // 55             AChar         curICode       Registration. Insertion code in
+        //                                             current strand.
+        // 57 - 60        Atom          prevAtom       Registration.  Atom name in previous strand.
+        // 61 - 63        Residue name  prevResName    Registration.  Residue name in
+        //                                             previous strand.
+        // 65             Character     prevChainId    Registration.  Chain identifier in
+        //                                             previous  strand.
+        // 66 - 69        Integer       prevResSeq     Registration. Residue sequence number
+        //                                             in previous strand.
+        // 70             AChar         prevICode      Registration.  Insertion code in
+        //                                             previous strand.
+        sheets.push({
+            strand: line.substr(7, 3).trim(),
+            sheetID: line.substr(11, 3).trim(),
+            numStrands: line.substr(14, 2).trim(),
+            initResName: line.substr(17, 3).trim(),
+            initChainID: line.substr(21, 1).trim(),
+            initSeqNum: line.substr(22, 4).trim(),
+            initICode: line.substr(26, 1).trim(),
+            endResName: line.substr(28, 3).trim(),
+            endChainID: line.substr(32, 1).trim(),
+            endSeqNum: line.substr(33, 4).trim(),
+            endICode: line.substr(37, 1).trim(),
+            sense: line.substr(38, 2).trim(),
+            curAtom: line.substr(41, 4).trim(),
+            curResName: line.substr(45, 3).trim(),
+            curChainId: line.substr(49, 1).trim(),
+            curResSeq: line.substr(50, 4).trim(),
+            curICode: line.substr(54, 1).trim(),
+            prevAtom: line.substr(56, 4).trim(),
+            prevResName: line.substr(60, 3).trim(),
+            prevChainId: line.substr(64, 1).trim(),
+            prevResSeq: line.substr(65, 4).trim(),
+            prevICode: line.substr(69, 1).trim(),
+        })
+    }
+
+    const beg_auth_asym_id = CifField.ofStrings(sheets.map(s => s.initChainID))
+    const beg_auth_comp_id = CifField.ofStrings(sheets.map(s => s.initResName))
+    const beg_auth_seq_id = CifField.ofStrings(sheets.map(s => s.initSeqNum))
+
+    const end_auth_asym_id = CifField.ofStrings(sheets.map(s => s.endChainID))
+    const end_auth_comp_id = CifField.ofStrings(sheets.map(s => s.endResName))
+    const end_auth_seq_id = CifField.ofStrings(sheets.map(s => s.endSeqNum))
+
+    const struct_sheet_range: CifCategory.Fields<mmCIF_Schema['struct_sheet_range']> = {
+        beg_label_asym_id: beg_auth_asym_id,
+        beg_label_comp_id: beg_auth_comp_id,
+        beg_label_seq_id: beg_auth_seq_id,
+        beg_auth_asym_id,
+        beg_auth_comp_id,
+        beg_auth_seq_id,
+
+        end_label_asym_id: end_auth_asym_id,
+        end_label_comp_id: end_auth_asym_id,
+        end_label_seq_id: end_auth_seq_id,
+        end_auth_asym_id,
+        end_auth_comp_id,
+        end_auth_seq_id,
+
+        id: CifField.ofStrings(sheets.map(s => s.strand)),
+        sheet_id: CifField.ofStrings(sheets.map(s => s.sheetID)), // TODO wrong, needs to point to _struct_sheet.id
+        pdbx_beg_PDB_ins_code: CifField.ofStrings(sheets.map(s => s.initICode)),
+        pdbx_end_PDB_ins_code: CifField.ofStrings(sheets.map(s => s.endICode)),
+    };
+    return CifCategory.ofFields('struct_sheet_range', struct_sheet_range);
+}

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

@@ -0,0 +1,296 @@
+/**
+ * Copyright (c) 2019 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 { substringStartsWith } from 'mol-util/string';
+import { CifField, CifCategory, CifFrame } from 'mol-io/reader/cif';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { TokenBuilder, Tokenizer } from 'mol-io/reader/common/text/tokenizer';
+import { PdbFile } from 'mol-io/reader/pdb/schema';
+import { parseCryst1, parseRemark350, parseMtrix } from './assembly';
+import { WaterNames } from 'mol-model/structure/model/types';
+import { parseHelix, parseSheet } from './secondary-structure';
+
+function _entity(): { [K in keyof mmCIF_Schema['entity']]?: CifField } {
+    return {
+        id: CifField.ofStrings(['1', '2', '3']),
+        type: CifField.ofStrings(['polymer', 'non-polymer', 'water'])
+    }
+}
+
+type AtomSiteTemplate = typeof atom_site_template extends (...args: any) => infer T ? T : never
+function atom_site_template(data: string, count: number) {
+    const str = () => [] as string[];
+    const ts = () => TokenBuilder.create(data, 2 * count);
+    return {
+        index: 0,
+        count,
+        group_PDB: ts(),
+        id: str(),
+        auth_atom_id: ts(),
+        label_alt_id: ts(),
+        auth_comp_id: ts(),
+        auth_asym_id: ts(),
+        auth_seq_id: ts(),
+        pdbx_PDB_ins_code: ts(),
+        Cartn_x: ts(),
+        Cartn_y: ts(),
+        Cartn_z: ts(),
+        occupancy: ts(),
+        B_iso_or_equiv: ts(),
+        type_symbol: ts(),
+        pdbx_PDB_model_num: str(),
+        label_entity_id: str()
+    };
+}
+
+function _atom_site(sites: AtomSiteTemplate): { [K in keyof mmCIF_Schema['atom_site']]?: CifField } {
+    const auth_asym_id = CifField.ofTokens(sites.auth_asym_id);
+    const auth_atom_id = CifField.ofTokens(sites.auth_atom_id);
+    const auth_comp_id = CifField.ofTokens(sites.auth_comp_id);
+    const auth_seq_id = CifField.ofTokens(sites.auth_seq_id);
+
+    return {
+        auth_asym_id,
+        auth_atom_id,
+        auth_comp_id,
+        auth_seq_id,
+        B_iso_or_equiv: CifField.ofTokens(sites.B_iso_or_equiv),
+        Cartn_x: CifField.ofTokens(sites.Cartn_x),
+        Cartn_y: CifField.ofTokens(sites.Cartn_y),
+        Cartn_z: CifField.ofTokens(sites.Cartn_z),
+        group_PDB: CifField.ofTokens(sites.group_PDB),
+        id: CifField.ofStrings(sites.id),
+
+        label_alt_id: CifField.ofTokens(sites.label_alt_id),
+
+        label_asym_id: auth_asym_id,
+        label_atom_id: auth_atom_id,
+        label_comp_id: auth_comp_id,
+        label_seq_id: auth_seq_id,
+        label_entity_id: CifField.ofStrings(sites.label_entity_id),
+
+        occupancy: CifField.ofTokens(sites.occupancy),
+        type_symbol: CifField.ofTokens(sites.type_symbol),
+
+        pdbx_PDB_ins_code: CifField.ofTokens(sites.pdbx_PDB_ins_code),
+        pdbx_PDB_model_num: CifField.ofStrings(sites.pdbx_PDB_model_num)
+    };
+}
+
+function getEntityId(residueName: string, isHet: boolean) {
+    if (isHet) {
+        if (WaterNames.has(residueName)) return '3';
+        return '2';
+    }
+    return '1';
+}
+
+function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: number, e: number, isHet: boolean) {
+    const { data: str } = data;
+    const length = e - s;
+
+    // TODO: filter invalid atoms
+
+    // COLUMNS        DATA TYPE       CONTENTS
+    // --------------------------------------------------------------------------------
+    // 1 -  6        Record name     "ATOM  "
+    TokenBuilder.addToken(sites.group_PDB, Tokenizer.trim(data, s, s + 6));
+
+    // 7 - 11        Integer         Atom serial number.
+    // TODO: support HEX
+    Tokenizer.trim(data, s + 6, s + 11);
+    sites.id[sites.index] = data.data.substring(data.tokenStart, data.tokenEnd);
+
+    // 13 - 16        Atom            Atom name.
+    TokenBuilder.addToken(sites.auth_atom_id, Tokenizer.trim(data, s + 12, s + 16));
+
+    // 17             Character       Alternate location indicator.
+    if (str.charCodeAt(s + 16) === 32) { // ' '
+        TokenBuilder.add(sites.label_alt_id, 0, 0);
+    } else {
+        TokenBuilder.add(sites.label_alt_id, s + 16, s + 17);
+    }
+
+    // 18 - 20        Residue name    Residue name.
+    TokenBuilder.addToken(sites.auth_comp_id, Tokenizer.trim(data, s + 17, s + 20));
+    const residueName = str.substring(data.tokenStart, data.tokenEnd);
+
+    // 22             Character       Chain identifier.
+    TokenBuilder.add(sites.auth_asym_id, s + 21, s + 22);
+
+    // 23 - 26        Integer         Residue sequence number.
+    // TODO: support HEX
+    TokenBuilder.addToken(sites.auth_seq_id, Tokenizer.trim(data, s + 22, s + 26));
+
+    // 27             AChar           Code for insertion of residues.
+    if (str.charCodeAt(s + 26) === 32) { // ' '
+        TokenBuilder.add(sites.label_alt_id, 0, 0);
+    } else {
+        TokenBuilder.add(sites.label_alt_id, s + 26, s + 27);
+    }
+
+    // 31 - 38        Real(8.3)       Orthogonal coordinates for X in Angstroms.
+    TokenBuilder.addToken(sites.Cartn_x, Tokenizer.trim(data, s + 30, s + 38));
+
+    // 39 - 46        Real(8.3)       Orthogonal coordinates for Y in Angstroms.
+    TokenBuilder.addToken(sites.Cartn_y, Tokenizer.trim(data, s + 38, s + 46));
+
+    // 47 - 54        Real(8.3)       Orthogonal coordinates for Z in Angstroms.
+    TokenBuilder.addToken(sites.Cartn_z, Tokenizer.trim(data, s + 46, s + 54));
+
+    // 55 - 60        Real(6.2)       Occupancy.
+    TokenBuilder.addToken(sites.occupancy, Tokenizer.trim(data, s + 54, s + 60));
+
+    // 61 - 66        Real(6.2)       Temperature factor (Default = 0.0).
+    if (length >= 66) {
+        TokenBuilder.addToken(sites.B_iso_or_equiv, Tokenizer.trim(data, s + 60, s + 66));
+    } else {
+        TokenBuilder.add(sites.label_alt_id, 0, 0);
+    }
+
+    // 73 - 76        LString(4)      Segment identifier, left-justified.
+    // ignored
+
+    // 77 - 78        LString(2)      Element symbol, right-justified.
+    if (length >= 78) {
+        Tokenizer.trim(data, s + 76, s + 78);
+
+        if (data.tokenStart < data.tokenEnd) {
+            TokenBuilder.addToken(sites.type_symbol, data);
+        } else {
+            // "guess" the symbol
+            TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+        }
+    } else {
+        TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+    }
+
+    sites.label_entity_id[sites.index] = getEntityId(residueName, isHet);
+    sites.pdbx_PDB_model_num[sites.index] = model;
+
+    sites.index++;
+}
+
+export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
+    const { lines } = pdb;
+    const { data, indices } = lines;
+    const tokenizer = Tokenizer(data);
+
+    // Count the atoms
+    let atomCount = 0;
+    for (let i = 0, _i = lines.count; i < _i; i++) {
+        const s = indices[2 * i], e = indices[2 * i + 1];
+        switch (data[s]) {
+            case 'A':
+                if (substringStartsWith(data, s, e, 'ATOM  ')) atomCount++;
+                break;
+            case 'H':
+                if (substringStartsWith(data, s, e, 'HETATM')) atomCount++;
+                break;
+        }
+    }
+
+    const atom_site = atom_site_template(data, atomCount);
+
+    const helperCategories: CifCategory[] = [];
+
+    let modelNum = 0, modelStr = '';
+
+    for (let i = 0, _i = lines.count; i < _i; i++) {
+        let s = indices[2 * i], e = indices[2 * i + 1];
+        switch (data[s]) {
+            case 'A':
+                if (!substringStartsWith(data, s, e, 'ATOM  ')) continue;
+                if (!modelNum) { modelNum++; modelStr = '' + modelNum; }
+                addAtom(atom_site, modelStr, tokenizer, s, e, false);
+                break;
+            case 'C':
+                if (substringStartsWith(data, s, e, 'CRYST1')) {
+                    helperCategories.push(...parseCryst1(pdb.id || '?', data.substring(s, e)));
+                }
+                // TODO CONNECT records => struct_conn
+                // TODO COMPND records => entity
+                break;
+            case 'H':
+                if (substringStartsWith(data, s, e, 'HETATM')) {
+                    if (!modelNum) { modelNum++; modelStr = '' + modelNum; }
+                    addAtom(atom_site, modelStr, tokenizer, s, e, true);
+                } else if (substringStartsWith(data, s, e, 'HELIX')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'HELIX')) break;
+                        j++;
+                    }
+                    helperCategories.push(parseHelix(lines, i, j));
+                    i = j - 1;
+                }
+                // TODO HETNAM records => chem_comp (at least partially, needs to be completed with common bases and amino acids)
+                break;
+            case 'M':
+                if (substringStartsWith(data, s, e, 'MODEL ')) {
+                    modelNum++;
+                    modelStr = '' + modelNum;
+                }
+                if (substringStartsWith(data, s, e, 'MTRIX')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'MTRIX')) break;
+                        j++;
+                    }
+                    helperCategories.push(...parseMtrix(lines, i, j));
+                    i = j - 1;
+                }
+                // TODO MODRES records => pdbx_struct_mod_residue
+                break;
+            case 'O':
+                // TODO ORIGX record => cif.database_PDB_matrix.origx, cif.database_PDB_matrix.origx_vector
+                break;
+            case 'R':
+                if (substringStartsWith(data, s, e, 'REMARK 350')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'REMARK 350')) break;
+                        j++;
+                    }
+                    helperCategories.push(...parseRemark350(lines, i, j));
+                    i = j - 1;
+                }
+                break;
+            case 'S':
+                if (substringStartsWith(data, s, e, 'SHEET')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'SHEET')) break;
+                        j++;
+                    }
+                    helperCategories.push(parseSheet(lines, i, j));
+                    i = j - 1;
+                }
+                // TODO SCALE record => cif.atom_sites.fract_transf_matrix, cif.atom_sites.fract_transf_vector
+                break;
+        }
+    }
+
+    const categories = {
+        entity: CifCategory.ofFields('entity', _entity()),
+        atom_site: CifCategory.ofFields('atom_site', _atom_site(atom_site))
+    } as any;
+
+    for (const c of helperCategories) {
+        categories[c.name] = c;
+    }
+
+    return {
+        header: pdb.id || 'PDB',
+        categoryNames: Object.keys(categories),
+        categories
+    };
+}

+ 1 - 1
src/mol-model/volume/formats/ccp4.ts → src/mol-model-formats/volume/ccp4.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { VolumeData } from '../data'
+import { VolumeData } from 'mol-model/volume/data'
 import { Task } from 'mol-task';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';

+ 1 - 1
src/mol-model/volume/formats/density-server.ts → src/mol-model-formats/volume/density-server.ts

@@ -5,7 +5,7 @@
  */
 
 import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server'
-import { VolumeData } from '../data'
+import { VolumeData } from 'mol-model/volume/data'
 import { Task } from 'mol-task';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';

+ 1 - 1
src/mol-model/volume/formats/dsn6.ts → src/mol-model-formats/volume/dsn6.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { VolumeData } from '../data'
+import { VolumeData } from 'mol-model/volume/data'
 import { Task } from 'mol-task';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';

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

@@ -31,7 +31,7 @@ const ValidationColorTable: [string, Color][] = [
 export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
     let color: LocationColor
 
-    if (ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
+    if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
         const getIssues = StructureQualityReport.getIssues;
         color = (location: Location) => {
             if (StructureElement.isLocation(location)) {

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

@@ -47,7 +47,7 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
 
     const { symmetryId } = props
 
-    if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
+    if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
         const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])!
 
         const s = assemblySymmetry.db.rcsb_assembly_symmetry
@@ -99,5 +99,5 @@ export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<Asse
     factory: AssemblySymmetryClusterColorTheme,
     getParams: getAssemblySymmetryClusterColorThemeParams,
     defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),
-    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)
 }

+ 1 - 1
src/mol-model/structure/export/categories/utils.ts

@@ -20,7 +20,7 @@ export function getModelMmCifCategory<K extends keyof mmCIF_Schema>(model: Model
 }
 
 export function getUniqueResidueNamesFromStructures(structures: Structure[]) {
-    return SetUtils.unionMany(structures.map(s => s.uniqueResidueNames));
+    return SetUtils.unionMany(...structures.map(s => s.uniqueResidueNames));
 }
 
 export function getUniqueEntityIdsFromStructures(structures: Structure[]): Set<string> {

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

@@ -6,10 +6,9 @@
 
 import { Model } from './model/model'
 import * as Types from './model/types'
-import Format from './model/format'
 import { ModelSymmetry } from './model/properties/symmetry'
 import StructureSequence from './model/properties/sequence'
 
 export * from './model/properties/custom'
 export * from './model/indexing'
-export { Model, Types, Format, ModelSymmetry, StructureSequence }
+export { Model, Types, ModelSymmetry, StructureSequence }

+ 0 - 24
src/mol-model/structure/model/format.ts

@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-// import { File as GroFile } from 'mol-io/reader/gro/schema'
-import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'
-import CIF, { CifFrame } from 'mol-io/reader/cif';
-
-type Format =
-    // | Format.gro
-    | Format.mmCIF
-
-namespace Format {
-    // export interface gro { kind: 'gro', data: GroFile }
-    export interface mmCIF { kind: 'mmCIF', data: mmCIF_Database, frame: CifFrame }
-
-    export function mmCIF(frame: CifFrame, data?: mmCIF_Database): mmCIF {
-        return { kind: 'mmCIF', data: data || CIF.schema.mmCIF(frame), frame };
-    }
-}
-
-export default Format

+ 10 - 16
src/mol-model/structure/model/model.ts

@@ -4,19 +4,17 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import UUID from 'mol-util/uuid'
-import Format from './format'
-import StructureSequence from './properties/sequence'
-import { AtomicHierarchy, AtomicConformation } from './properties/atomic'
-import { ModelSymmetry } from './properties/symmetry'
-import { CoarseHierarchy, CoarseConformation } from './properties/coarse'
+import UUID from 'mol-util/uuid';
+import StructureSequence from './properties/sequence';
+import { AtomicHierarchy, AtomicConformation } from './properties/atomic';
+import { ModelSymmetry } from './properties/symmetry';
+import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
 import { Entities } from './properties/common';
 import { CustomProperties } from './properties/custom';
 import { SecondaryStructure } from './properties/seconday-structure';
-
-import from_mmCIF from './formats/mmcif'
-import { ChemicalComponentMap } from './properties/chemical-component';
 import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
+import { ModelFormat } from 'mol-model-formats/structure/format';
+import { ChemicalComponentMap } from './properties/chemical-component';
 
 /**
  * Interface to the "source data" of the molecule.
@@ -30,7 +28,7 @@ export interface Model extends Readonly<{
     // for IHM, corresponds to ihm_model_list.model_id
     modelNum: number,
 
-    sourceData: Format,
+    sourceData: ModelFormat,
 
     symmetry: ModelSymmetry,
     entities: Entities,
@@ -69,10 +67,6 @@ export interface Model extends Readonly<{
 } { }
 
 export namespace Model {
-    export function create(format: Format) {
-        switch (format.kind) {
-            // case 'gro': return from_gro(format);
-            case 'mmCIF': return from_mmCIF(format);
-        }
-    }
+    // TODO: is this enough?
+    export type Trajectory = ReadonlyArray<Model>
 }

+ 5 - 11
src/mol-model/structure/model/properties/chemical-component.ts

@@ -4,16 +4,10 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { MoleculeType, ComponentType } from '../types'
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { Table } from 'mol-data/db';
 
-export interface ChemicalComponent {
-    id: string
-    type: ComponentType
-    moleculeType: MoleculeType
-    name: string
-    synonyms: string[]
-    formula: string
-    formulaWeight: number
-}
+export type ChemicalComponent = Table.Row<mmCIF_Schema['chem_comp']>
+export type ChemicalComponentMap = ReadonlyMap<string, ChemicalComponent>
 
-export type ChemicalComponentMap = ReadonlyMap<string, ChemicalComponent>
+// TODO add data for common chemical components

+ 15 - 6
src/mol-model/structure/model/properties/utils/atomic-derived.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -8,22 +8,32 @@ import { AtomicData } from '../atomic';
 import { ChemicalComponentMap } from '../chemical-component';
 import { AtomicIndex, AtomicDerivedData } from '../atomic/hierarchy';
 import { ElementIndex, ResidueIndex } from '../../indexing';
-import { MoleculeType } from '../../types';
+import { MoleculeType, getMoleculeType, getComponentType } from '../../types';
 import { getAtomIdForAtomRole } from 'mol-model/structure/util';
 
 export function getAtomicDerivedData(data: AtomicData, index: AtomicIndex, chemicalComponentMap: ChemicalComponentMap): AtomicDerivedData {
-    
     const { label_comp_id, _rowCount: n } = data.residues
 
     const traceElementIndex = new Uint32Array(n)
     const directionElementIndex = new Uint32Array(n)
     const moleculeType = new Uint8Array(n)
 
+    const moleculeTypeMap = new Map<string, MoleculeType>()
+
     for (let i = 0; i < n; ++i) {
         const compId = label_comp_id.value(i)
         const chemCompMap = chemicalComponentMap
-        const cc = chemCompMap.get(compId)
-        const molType = cc ? cc.moleculeType : MoleculeType.unknown
+        let molType: MoleculeType
+        if (moleculeTypeMap.has(compId)){
+            molType = moleculeTypeMap.get(compId)!
+        } else if (chemCompMap.has(compId)) {
+            molType = getMoleculeType(chemCompMap.get(compId)!.type, compId)
+            moleculeTypeMap.set(compId, molType)
+        } else {
+            molType = getMoleculeType(getComponentType(compId), compId)
+            // TODO if unknown molecule type, use atom names to guess molecule type
+            moleculeTypeMap.set(compId, molType)
+        }
         moleculeType[i] = molType
 
         const traceAtomId = getAtomIdForAtomRole(molType, 'trace')
@@ -33,7 +43,6 @@ export function getAtomicDerivedData(data: AtomicData, index: AtomicIndex, chemi
         directionElementIndex[i] = index.findAtomOnResidue(i as ResidueIndex, directionAtomId)
     }
 
-
     return {
         residue: {
             traceElementIndex: traceElementIndex as unknown as ArrayLike<ElementIndex>,

+ 8 - 16
src/mol-model/structure/model/properties/utils/atomic-ranges.ts

@@ -8,7 +8,6 @@ import { AtomicSegments } from '../atomic';
 import { AtomicData, AtomicRanges } from '../atomic/hierarchy';
 import { Segmentation, Interval } from 'mol-data/int';
 import SortedRanges from 'mol-data/int/sorted-ranges';
-import { ChemicalComponentMap } from '../chemical-component';
 import { MoleculeType, isPolymer } from '../../types';
 import { ElementIndex, ResidueIndex } from '../../indexing';
 import { getAtomIdForAtomRole } from '../../../util';
@@ -17,11 +16,6 @@ import { Vec3 } from 'mol-math/linear-algebra';
 
 // TODO add gaps at the ends of the chains by comparing to the polymer sequence data
 
-function getMoleculeType(compId: string, chemicalComponentMap: ChemicalComponentMap) {
-    const cc = chemicalComponentMap.get(compId)
-    return cc ? cc.moleculeType : MoleculeType.unknown
-}
-
 function getElementIndexForAtomId(rI: ResidueIndex, atomId: string, data: AtomicData, segments: AtomicSegments): ElementIndex {
     const { offsets } = segments.residueAtomSegments
     const { label_atom_id } = data.atoms
@@ -31,10 +25,9 @@ function getElementIndexForAtomId(rI: ResidueIndex, atomId: string, data: Atomic
     return offsets[rI]
 }
 
-function areBackboneConnected(riStart: ResidueIndex, riEnd: ResidueIndex, data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, chemicalComponentMap: ChemicalComponentMap) {
-    const { label_comp_id } = data.residues
-    const mtStart = getMoleculeType(label_comp_id.value(riStart), chemicalComponentMap)
-    const mtEnd = getMoleculeType(label_comp_id.value(riEnd), chemicalComponentMap)
+function areBackboneConnected(riStart: ResidueIndex, riEnd: ResidueIndex, data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, moleculeType: ArrayLike<MoleculeType>) {
+    const mtStart = moleculeType[riStart]
+    const mtEnd = moleculeType[riEnd]
     if (!isPolymer(mtStart) || !isPolymer(mtEnd)) return false
 
     const startId = getAtomIdForAtomRole(mtStart, 'backboneStart')
@@ -49,13 +42,13 @@ function areBackboneConnected(riStart: ResidueIndex, riEnd: ResidueIndex, data:
     return Vec3.distance(pStart, pEnd) < 10
 }
 
-export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, chemicalComponentMap: ChemicalComponentMap): AtomicRanges {
+export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, moleculeType: ArrayLike<MoleculeType>): AtomicRanges {
     const polymerRanges: number[] = []
     const gapRanges: number[] = []
     const cyclicPolymerMap = new Map<ResidueIndex, ResidueIndex>()
     const chainIt = Segmentation.transientSegments(segments.chainAtomSegments, Interval.ofBounds(0, data.atoms._rowCount))
     const residueIt = Segmentation.transientSegments(segments.residueAtomSegments, Interval.ofBounds(0, data.atoms._rowCount))
-    const { label_seq_id, label_comp_id } = data.residues
+    const { label_seq_id } = data.residues
 
     let prevSeqId: number
     let prevStart: number
@@ -72,7 +65,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conf
 
         const riStart = segments.residueAtomSegments.index[chainSegment.start]
         const riEnd = segments.residueAtomSegments.index[chainSegment.end - 1]
-        if (areBackboneConnected(riStart, riEnd, data, segments, conformation, chemicalComponentMap)) {
+        if (areBackboneConnected(riStart, riEnd, data, segments, conformation, moleculeType)) {
             cyclicPolymerMap.set(riStart, riEnd)
             cyclicPolymerMap.set(riEnd, riStart)
         }
@@ -80,9 +73,8 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conf
         while (residueIt.hasNext) {
             const residueSegment = residueIt.move();
             const residueIndex = residueSegment.index
-            const moleculeType = getMoleculeType(label_comp_id.value(residueIndex), chemicalComponentMap)
             const seqId = label_seq_id.value(residueIndex)
-            if (isPolymer(moleculeType)) {
+            if (isPolymer(moleculeType[residueIndex])) {
                 if (startIndex !== -1) {
                     if (seqId !== prevSeqId + 1) {
                         polymerRanges.push(startIndex, prevEnd - 1)
@@ -93,7 +85,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conf
                     } else {
                         const riStart = segments.residueAtomSegments.index[residueSegment.start]
                         const riEnd = segments.residueAtomSegments.index[prevEnd - 1]
-                        if (!areBackboneConnected(riStart, riEnd, data, segments, conformation, chemicalComponentMap)) {
+                        if (!areBackboneConnected(riStart, riEnd, data, segments, conformation, moleculeType)) {
                             polymerRanges.push(startIndex, prevEnd - 1)
                             startIndex = residueSegment.start
                         }

+ 81 - 61
src/mol-model/structure/model/types.ts

@@ -1,11 +1,14 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import BitFlags from 'mol-util/bit-flags'
+import { SaccharideCompIdMap } from '../structure/carbohydrates/constants';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { SetUtils } from 'mol-util/set';
 
 const _esCache = (function () {
     const cache = Object.create(null);
@@ -85,51 +88,28 @@ export const MoleculeTypeAtomRoleId: { [k: number]: { [k in AtomRole]: string }
     }
 }
 
-export const ProteinBackboneAtoms = [
+export const ProteinBackboneAtoms = new Set([
     'CA', 'C', 'N', 'O',
     'O1', 'O2', 'OC1', 'OC2', 'OX1', 'OXT',
     'H', 'H1', 'H2', 'H3', 'HA', 'HN',
     'BB'
-]
+])
 
-export const NucleicBackboneAtoms = [
+export const NucleicBackboneAtoms = new Set([
     'P', 'OP1', 'OP2', 'HOP2', 'HOP3',
     'O2\'', 'O3\'', 'O4\'', 'O5\'', 'C1\'', 'C2\'', 'C3\'', 'C4\'', 'C5\'',
     'H1\'', 'H2\'', 'H2\'\'', 'HO2\'', 'H3\'', 'H4\'', 'H5\'', 'H5\'\'', 'HO3\'', 'HO5\'',
     'O2*', 'O3*', 'O4*', 'O5*', 'C1*', 'C2*', 'C3*', 'C4*', 'C5*'
-]
-
-/** Chemical component types as defined in the mmCIF CCD */
-export enum ComponentType {
-    // protein
-    'D-peptide linking', 'L-peptide linking', 'D-peptide NH3 amino terminus',
-    'L-peptide NH3 amino terminus', 'D-peptide COOH carboxy terminus',
-    'L-peptide COOH carboxy terminus', 'peptide linking', 'peptide-like',
-    'L-gamma-peptide, C-delta linking', 'D-gamma-peptide, C-delta linking',
-    'L-beta-peptide, C-gamma linking', 'D-beta-peptide, C-gamma linking',
-
-    // DNA
-    'DNA linking', 'L-DNA linking', 'DNA OH 5 prime terminus', 'DNA OH 3 prime terminus',
-
-    // RNA
-    'RNA linking', 'L-RNA linking', 'RNA OH 5 prime terminus', 'RNA OH 3 prime terminus',
-
-    // sacharide
-    'D-saccharide 1,4 and 1,4 linking', 'L-saccharide 1,4 and 1,4 linking',
-    'D-saccharide 1,4 and 1,6 linking', 'L-saccharide 1,4 and 1,6 linking', 'L-saccharide',
-    'D-saccharide', 'saccharide',
-
-    'non-polymer', 'other'
-}
+])
 
 /** Chemical component type names for protein */
-export const ProteinComponentTypeNames = [
+export const ProteinComponentTypeNames = new Set([
     'D-PEPTIDE LINKING', 'L-PEPTIDE LINKING', 'D-PEPTIDE NH3 AMINO TERMINUS',
     'L-PEPTIDE NH3 AMINO TERMINUS', 'D-PEPTIDE COOH CARBOXY TERMINUS',
     'L-PEPTIDE COOH CARBOXY TERMINUS', 'PEPTIDE LINKING', 'PEPTIDE-LIKE',
     'L-GAMMA-PEPTIDE, C-DELTA LINKING', 'D-GAMMA-PEPTIDE, C-DELTA LINKING',
     'L-BETA-PEPTIDE, C-GAMMA LINKING', 'D-BETA-PEPTIDE, C-GAMMA LINKING',
-]
+])
 
 /** Chemical component type names for DNA */
 export const DNAComponentTypeNames = [
@@ -137,66 +117,104 @@ export const DNAComponentTypeNames = [
 ]
 
 /** Chemical component type names for RNA */
-export const RNAComponentTypeNames = [
+export const RNAComponentTypeNames = new Set([
     'RNA LINKING', 'L-RNA LINKING', 'RNA OH 5 PRIME TERMINUS', 'RNA OH 3 PRIME TERMINUS',
-]
+])
 
 /** Chemical component type names for saccharide */
-export const SaccharideComponentTypeNames = [
+export const SaccharideComponentTypeNames = new Set([
     'D-SACCHARIDE 1,4 AND 1,4 LINKING', 'L-SACCHARIDE 1,4 AND 1,4 LINKING',
     'D-SACCHARIDE 1,4 AND 1,6 LINKING', 'L-SACCHARIDE 1,4 AND 1,6 LINKING', 'L-SACCHARIDE',
     'D-SACCHARIDE', 'SACCHARIDE',
-]
+])
 
 /** Chemical component type names for other */
-export const OtherComponentTypeNames = [
+export const OtherComponentTypeNames = new Set([
     'NON-POLYMER', 'OTHER'
-]
+])
 
 /** Common names for water molecules */
-export const WaterNames = [
+export const WaterNames = new Set([
     'SOL', 'WAT', 'HOH', 'H2O', 'W', 'DOD', 'D3O', 'TIP3', 'TIP4', 'SPC'
-]
-
-export const ExtraSaccharideNames = [
-    'MLR'
-]
-
-export const RnaBaseNames = [ 'A', 'C', 'T', 'G', 'I', 'U' ]
-export const DnaBaseNames = [ 'DA', 'DC', 'DT', 'DG', 'DI', 'DU' ]
-export const PeptideBaseNames = [ 'APN', 'CPN', 'TPN', 'GPN' ]
-export const PurinBaseNames = [ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ]
-export const PyrimidineBaseNames = [ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ]
-export const BaseNames = RnaBaseNames.concat(DnaBaseNames, PeptideBaseNames)
-
-export const isPurinBase = (compId: string) => PurinBaseNames.includes(compId.toUpperCase())
-export const isPyrimidineBase = (compId: string) => PyrimidineBaseNames.includes(compId.toUpperCase())
+])
+
+export const AminoAcidNames = new Set([
+    'HIS', 'ARG', 'LYS', 'ILE', 'PHE', 'LEU', 'TRP', 'ALA', 'MET', 'PRO', 'CYS',
+    'ASN', 'VAL', 'GLY', 'SER', 'GLN', 'TYR', 'ASP', 'GLU', 'THR', 'SEC', 'PYL',
+
+    'DAL', // D-ALANINE
+    'DAR', // D-ARGININE
+    'DSG', // D-ASPARAGINE
+    'DAS', // D-ASPARTIC ACID
+    'DCY', // D-CYSTEINE
+    'DGL', // D-GLUTAMIC ACID
+    'DGN', // D-GLUTAMINE
+    'DHI', // D-HISTIDINE
+    'DIL', // D-ISOLEUCINE
+    'DLE', // D-LEUCINE
+    'DLY', // D-LYSINE
+    'MED', // D-METHIONINE
+    'DPN', // D-PHENYLALANINE
+    'DPR', // D-PROLINE
+    'DSN', // D-SERINE
+    'DTH', // D-THREONINE
+    'DTR', // D-TRYPTOPHAN
+    'DTY', // D-TYROSINE
+    'DVA', // D-VALINE
+    'DNE' // D-NORLEUCINE
+    // ???  // D-SELENOCYSTEINE
+])
+
+export const RnaBaseNames = new Set([ 'A', 'C', 'T', 'G', 'I', 'U' ])
+export const DnaBaseNames = new Set([ 'DA', 'DC', 'DT', 'DG', 'DI', 'DU' ])
+export const PeptideBaseNames = new Set([ 'APN', 'CPN', 'TPN', 'GPN' ])
+export const PurinBaseNames = new Set([ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ])
+export const PyrimidineBaseNames = new Set([ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ])
+export const BaseNames = SetUtils.unionMany(RnaBaseNames, DnaBaseNames, PeptideBaseNames)
+
+export const isPurinBase = (compId: string) => PurinBaseNames.has(compId.toUpperCase())
+export const isPyrimidineBase = (compId: string) => PyrimidineBaseNames.has(compId.toUpperCase())
 
 /** get the molecule type from component type and id */
 export function getMoleculeType(compType: string, compId: string) {
     compType = compType.toUpperCase()
     compId = compId.toUpperCase()
-    if (PeptideBaseNames.includes(compId)) {
+    if (PeptideBaseNames.has(compId)) {
         return MoleculeType.PNA
-    } else if (ProteinComponentTypeNames.includes(compType)) {
+    } else if (ProteinComponentTypeNames.has(compType)) {
         return MoleculeType.protein
-    } else if (RNAComponentTypeNames.includes(compType)) {
+    } else if (RNAComponentTypeNames.has(compType)) {
         return MoleculeType.RNA
     } else if (DNAComponentTypeNames.includes(compType)) {
         return MoleculeType.DNA
-    } else if (SaccharideComponentTypeNames.includes(compType) || ExtraSaccharideNames.includes(compId)) {
+    } else if (SaccharideComponentTypeNames.has(compType)) {
         return MoleculeType.saccharide
-    } else if (WaterNames.includes(compId)) {
+    } else if (WaterNames.has(compId)) {
         return MoleculeType.water
-    } else if (IonNames.includes(compId)) {
+    } else if (IonNames.has(compId)) {
         return MoleculeType.ion
-    } else if (OtherComponentTypeNames.includes(compType)) {
+    } else if (OtherComponentTypeNames.has(compType)) {
         return MoleculeType.other
     } else {
         return MoleculeType.unknown
     }
 }
 
+export function getComponentType(compId: string): mmCIF_Schema['chem_comp']['type']['T'] {
+    compId = compId.toUpperCase()
+    if (AminoAcidNames.has(compId)) {
+        return 'peptide linking'
+    } else if (RnaBaseNames.has(compId)) {
+        return 'RNA linking'
+    } else if (DnaBaseNames.has(compId)) {
+        return 'DNA linking'
+    } else if (SaccharideCompIdMap.has(compId)) {
+        return 'saccharide'
+    } else {
+        return 'other'
+    }
+}
+
 export function isPolymer(moleculeType: MoleculeType) {
     return moleculeType === MoleculeType.protein || moleculeType === MoleculeType.DNA || moleculeType === MoleculeType.RNA || moleculeType === MoleculeType.PNA
 }
@@ -206,6 +224,8 @@ export function isNucleic(moleculeType: MoleculeType) {
 }
 
 /**
+ * TODO write script that read CCD and outputs list of ion names
+ *
  * all chemical components with the word "ion" in their name, Sep 2016
  *
  * SET SESSION group_concat_max_len = 1000000;
@@ -216,7 +236,7 @@ export function isNucleic(moleculeType: MoleculeType) {
  *     GROUP BY id_
  * ) AS t1;
  */
-export const IonNames = [
+export const IonNames = new Set([
   '118', '119', '1AL', '1CU', '2FK', '2HP', '2OF', '3CO',
   '3MT', '3NI', '3OF', '3P8', '4MO', '4PU', '543', '6MO', 'ACT', 'AG', 'AL',
   'ALF', 'AM', 'ATH', 'AU', 'AU3', 'AUC', 'AZI', 'BA', 'BCT', 'BEF', 'BF4', 'BO4',
@@ -237,7 +257,7 @@ export const IonNames = [
   'YB2', 'YH', 'YT3', 'ZCM', 'ZN', 'ZN2', 'ZN3', 'ZNO', 'ZO3',
     // additional ion names
   'OHX'
-]
+])
 
 export interface SecondaryStructureType extends BitFlags<SecondaryStructureType.Flag> { }
 export namespace SecondaryStructureType {

+ 4 - 0
src/mol-model/structure/structure/structure.ts

@@ -72,6 +72,10 @@ class Structure {
         return prc && ec ? ec / prc < 2 : false
     }
 
+    get isEmpty() {
+        return this.units.length === 0;
+    }
+
     get hashCode() {
         if (this._props.hashCode !== -1) return this._props.hashCode;
         return this.computeHash();

+ 27 - 19
src/mol-model/structure/structure/symmetry.ts

@@ -1,19 +1,19 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 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 Structure from './structure'
-import { StructureSelection, QueryContext } from '../query'
-import { ModelSymmetry } from '../model'
-import { Task, RuntimeContext } from 'mol-task';
 import { SortedArray } from 'mol-data/int';
-import Unit from './unit';
 import { EquivalenceClasses } from 'mol-data/util';
+import { Spacegroup, SpacegroupCell, SymmetryOperator } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
-import { SymmetryOperator, Spacegroup, SpacegroupCell } from 'mol-math/geometry';
+import { RuntimeContext, Task } from 'mol-task';
+import { ModelSymmetry } from '../model';
+import { QueryContext, StructureSelection } from '../query';
+import Structure from './structure';
+import Unit from './unit';
 
 namespace StructureSymmetry {
     export function buildAssembly(structure: Structure, asmName: string) {
@@ -90,26 +90,34 @@ namespace StructureSymmetry {
 }
 
 function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {
-    const operators: SymmetryOperator[] = symmetry._operators_333 || [];
+    const operators: SymmetryOperator[] = [];
     const { spacegroup } = symmetry;
-    if (operators.length === 0) {
+    if (ijkMin[0] <= 0 && ijkMax[0] >= 0 &&
+        ijkMin[1] <= 0 && ijkMax[1] >= 0 &&
+        ijkMin[2] <= 0 && ijkMax[2] >= 0) {
         operators[0] = Spacegroup.getSymmetryOperator(spacegroup, 0, 0, 0, 0)
-        for (let op = 0; op < spacegroup.operators.length; op++) {
-            for (let i = ijkMin[0]; i < ijkMax[0]; i++) {
-                for (let j = ijkMin[1]; j < ijkMax[1]; j++) {
-                    for (let k = ijkMin[2]; k < ijkMax[2]; k++) {
-                        // we have added identity as the 1st operator.
-                        if (op === 0 && i === 0 && j === 0 && k === 0) continue;
-                        operators[operators.length] = Spacegroup.getSymmetryOperator(spacegroup, op, i, j, k);
-                    }
+    }
+
+    for (let op = 0; op < spacegroup.operators.length; op++) {
+        for (let i = ijkMin[0]; i <= ijkMax[0]; i++) {
+            for (let j = ijkMin[1]; j <= ijkMax[1]; j++) {
+                for (let k = ijkMin[2]; k <= ijkMax[2]; k++) {
+                    // we have added identity as the 1st operator.
+                    if (op === 0 && i === 0 && j === 0 && k === 0) continue;
+                    operators[operators.length] = Spacegroup.getSymmetryOperator(spacegroup, op, i, j, k);
                 }
             }
         }
-        symmetry._operators_333 = operators;
     }
     return operators;
 }
 
+function getOperatorsCached333(symmetry: ModelSymmetry) {
+    if (typeof symmetry._operators_333 !== 'undefined') return symmetry._operators_333;
+    symmetry._operators_333 = getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3));
+    return symmetry._operators_333;
+}
+
 function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) {
     const assembler = Structure.Builder();
     const { units } = structure;
@@ -150,7 +158,7 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
     if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
 
     if (ctx.shouldUpdate) await ctx.update('Initialing...');
-    const operators = getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3));
+    const operators = getOperatorsCached333(symmetry);
     const lookup = structure.lookup3d;
 
     const assembler = Structure.Builder();

+ 2 - 2
src/mol-model/structure/structure/unit/links/data.ts

@@ -66,10 +66,10 @@ class InterUnitBonds {
                 pairBonds.linkedElementIndices.forEach(indexA => {
                     pairBonds.getBonds(indexA).forEach(bondInfo => {
                         const { unitA, unitB } = pairBonds
-                        
+
                         const bondKey = InterUnitBonds.getBondKey(indexA, unitA, bondInfo.indexB, unitB)
                         bondKeyIndex.set(bondKey, bonds.length)
-                        
+
                         const elementKey = InterUnitBonds.getElementKey(indexA, unitA)
                         const e = elementKeyIndex.get(elementKey)
                         if (e === undefined) elementKeyIndex.set(elementKey, [bonds.length])

+ 1 - 1
src/mol-model/structure/structure/unit/links/inter-compute.ts

@@ -4,7 +4,6 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StructConn } from '../../../model/formats/mmcif/bonds';
 import { LinkType } from '../../../model/types';
 import Structure from '../../structure';
 import Unit from '../../unit';
@@ -14,6 +13,7 @@ import { UniqueArray } from 'mol-data/generic';
 import { SortedArray } from 'mol-data/int';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import StructureElement from '../../element';
+import { StructConn } from 'mol-model-formats/structure/mmcif/bonds';
 
 const MAX_RADIUS = 4;
 

+ 1 - 1
src/mol-model/structure/structure/unit/links/intra-compute.ts

@@ -6,11 +6,11 @@
 
 import { LinkType } from '../../../model/types'
 import { IntraUnitLinks } from './data'
-import { StructConn, ComponentBond } from '../../../model/formats/mmcif/bonds'
 import Unit from '../../unit'
 import { IntAdjacencyGraph } from 'mol-math/graph';
 import { LinkComputationParameters, getElementIdx, MetalsSet, getElementThreshold, isHydrogen, getElementPairThreshold } from './common';
 import { SortedArray } from 'mol-data/int';
+import { StructConn, ComponentBond } from 'mol-model-formats/structure/mmcif/bonds';
 
 function getGraph(atomA: number[], atomB: number[], _order: number[], _flags: number[], atomCount: number): IntraUnitLinks {
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);

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

@@ -6,9 +6,9 @@
 
 import Unit from '../../unit';
 import Structure from '../../structure';
-import { IHMCrossLinkRestraint } from '../../../model/formats/mmcif/pair-restraint';
 import { PairRestraints, CrossLinkRestraint } from './data';
 import { StructureElement } from '../../../structure';
+import { IHMCrossLinkRestraint } from 'mol-model-formats/structure/mmcif/pair-restraint';
 
 function _addRestraints(map: Map<number, number>, unit: Unit, restraints: IHMCrossLinkRestraint) {
     const { elements } = unit;

+ 11 - 16
src/mol-model/structure/util.ts

@@ -1,11 +1,11 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Model, ResidueIndex, ElementIndex } from './model';
-import { MoleculeType, AtomRole, MoleculeTypeAtomRoleId } from './model/types';
+import { MoleculeType, AtomRole, MoleculeTypeAtomRoleId, getMoleculeType } from './model/types';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { Unit } from './structure';
 import Matrix from 'mol-math/linear-algebra/matrix/matrix';
@@ -17,27 +17,22 @@ export function getCoarseBegCompId(unit: Unit.Spheres | Unit.Gaussians, element:
     return seq.compId.value(seq_id_begin - 1) // 1-indexed
 }
 
-export function getElementMoleculeType(unit: Unit, element: ElementIndex) {
-    let compId = ''
+export function getElementMoleculeType(unit: Unit, element: ElementIndex): MoleculeType {
     switch (unit.kind) {
         case Unit.Kind.Atomic:
-            compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
-            break
+            return unit.model.atomicHierarchy.derived.residue.moleculeType[unit.residueIndex[element]]
         case Unit.Kind.Spheres:
         case Unit.Kind.Gaussians:
-            compId = getCoarseBegCompId(unit, element)
-            break
+            // TODO add unit.model.coarseHierarchy.derived.residue.moleculeType
+            const compId = getCoarseBegCompId(unit, element)
+            const cc = unit.model.properties.chemicalComponentMap.get(compId)
+            if (cc) return getMoleculeType(cc.type, compId)
     }
-    const chemCompMap = unit.model.properties.chemicalComponentMap
-    const cc = chemCompMap.get(compId)
-    return cc ? cc.moleculeType : MoleculeType.unknown
+    return 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)
-    return cc ? cc.moleculeType : MoleculeType.unknown
+export function getAtomicMoleculeType(model: Model, rI: ResidueIndex): MoleculeType {
+    return model.atomicHierarchy.derived.residue.moleculeType[rI]
 }
 
 export function getAtomIdForAtomRole(moleculeType: MoleculeType, atomRole: AtomRole) {

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

@@ -4,5 +4,4 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-export * from './volume/data'
-export * from './volume/formats/density-server'
+export * from './volume/data'

+ 3 - 3
src/mol-plugin/behavior/dynamic/animation.ts

@@ -64,7 +64,7 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
         rotate(rad: number) {
             this.updatedUnitTransforms.clear()
             const state = this.ctx.state.dataState
-            const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D))
+            const reprs = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D))
             Mat4.rotate(this.rotMat, this.tmpMat, rad, this.rotVec)
             for (const r of reprs) {
                 if (!SO.isRepresentation3D(r.obj)) return
@@ -112,7 +112,7 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
         explode(p: number) {
             this.updatedUnitTransforms.clear()
             const state = this.ctx.state.dataState
-            const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
+            const reprs = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
             for (const r of reprs) {
                 if (!SO.isRepresentation3D(r.obj)) return
                 const structure = getRootStructure(r, state)
@@ -187,5 +187,5 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
 //
 
 function getRootStructure(root: StateObjectCell, state: State) {
-    return state.query(StateSelection.Generators.byValue(root).rootOfType([PluginStateObject.Molecule.Structure]))[0];
+    return state.select(StateSelection.Generators.byValue(root).rootOfType([PluginStateObject.Molecule.Structure]))[0];
 }

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

@@ -40,7 +40,7 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
                 factory: StructureQualityReportColorTheme,
                 getParams: () => ({}),
                 defaultValues: {},
-                isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
+                isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
             })
         }
 

+ 3 - 4
src/mol-plugin/behavior/dynamic/labels.ts

@@ -21,6 +21,7 @@ import { Unit, StructureElement, StructureProperties } from 'mol-model/structure
 import { SetUtils } from 'mol-util/set';
 import { arrayEqual } from 'mol-util';
 import { MoleculeType } from 'mol-model/structure/model/types';
+import { getElementMoleculeType } from 'mol-model/structure/util';
 
 // TODO
 // - support more object types than structures
@@ -112,7 +113,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
         /** Update structures to be labeled, returns true if changed */
         private updateStructures(p: SceneLabelsProps) {
             const state = this.ctx.state.dataState
-            const structures = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
+            const structures = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
             const rootStructures = new Set<SO.Molecule.Structure>()
             for (const s of structures) {
                 const rootStructure = getRootStructure(s, state)
@@ -157,9 +158,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
                     }
 
                     if (p.levels.includes('ligand') && !u.polymerElements.length) {
-                        const compId = StructureProperties.residue.label_comp_id(l)
-                        const chemComp = u.model.properties.chemicalComponentMap.get(compId)
-                        const moleculeType = chemComp ? chemComp.moleculeType : MoleculeType.unknown
+                        const moleculeType = getElementMoleculeType(u, u.elements[0])
                         if (moleculeType === MoleculeType.other || moleculeType === MoleculeType.saccharide) {
                             label = `${StructureProperties.entity.pdbx_description(l).join(', ')} (${getAsymId(u)(l)})`
                         }

+ 3 - 3
src/mol-plugin/behavior/static/state.ts

@@ -51,17 +51,17 @@ export function SetCurrentObject(ctx: PluginContext) {
 }
 
 export function Update(ctx: PluginContext) {
-    PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.update(tree)));
+    PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.updateTree(tree)));
 }
 
 export function ApplyAction(ctx: PluginContext) {
-    PluginCommands.State.ApplyAction.subscribe(ctx, ({ state, action, ref }) => ctx.runTask(state.apply(action.action, action.params, ref)));
+    PluginCommands.State.ApplyAction.subscribe(ctx, ({ state, action, ref }) => ctx.runTask(state.applyAction(action.action, action.params, ref)));
 }
 
 export function RemoveObject(ctx: PluginContext) {
     PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref }) => {
         const tree = state.tree.build().delete(ref).getTree();
-        return ctx.runTask(state.update(tree));
+        return ctx.runTask(state.updateTree(tree));
     });
 }
 

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

@@ -42,7 +42,7 @@ namespace PluginCommand {
         unsubscribe(): void
     }
 
-    export type Action<T> = (params: T) => void | Promise<void>
+    export type Action<T> = (params: T) => unknown | Promise<unknown>
     type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void }
 
     export class Manager {

+ 22 - 14
src/mol-plugin/context.ts

@@ -29,6 +29,7 @@ import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry';
 import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
 import { PluginLayout } from './layout';
 import { List } from 'immutable';
+import { StateTransformParameters } from './ui/state/common';
 
 export class PluginContext {
     private disposed = false;
@@ -87,6 +88,7 @@ export class PluginContext {
     }
 
     readonly customModelProperties = new CustomPropertyRegistry();
+    readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
 
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
@@ -136,6 +138,16 @@ export class PluginContext {
         this.disposed = true;
     }
 
+    applyTransform(state: State, a: Transform.Ref, transformer: Transformer, params: any) {
+        const tree = state.tree.build().to(a).apply(transformer, params);
+        return PluginCommands.State.Update.dispatch(this, { state, tree });
+    }
+
+    updateTransform(state: State, a: Transform.Ref, params: any) {
+        const tree = state.build().to(a).update(params);
+        return PluginCommands.State.Update.dispatch(this, { state, tree });
+    }
+
     private initBuiltInBehavior() {
         BuiltInPluginBehaviors.State.registerDefault(this);
         BuiltInPluginBehaviors.Representation.registerDefault(this);
@@ -145,30 +157,28 @@ export class PluginContext {
         merge(this.state.dataState.events.log, this.state.behaviorState.events.log).subscribe(e => this.events.log.next(e));
     }
 
-    async initBehaviors() {
+    private async initBehaviors() {
         const tree = this.state.behaviorState.tree.build();
 
         for (const b of this.spec.behaviors) {
             tree.toRoot().apply(b.transformer, b.defaultParams, { ref: b.transformer.id });
         }
 
-        await this.runTask(this.state.behaviorState.update(tree, true));
+        await this.runTask(this.state.behaviorState.updateTree(tree, true));
     }
 
-    initDataActions() {
+    private initDataActions() {
         for (const a of this.spec.actions) {
             this.state.dataState.actions.add(a.action);
         }
     }
 
-    applyTransform(state: State, a: Transform.Ref, transformer: Transformer, params: any) {
-        const tree = state.tree.build().to(a).apply(transformer, params);
-        return PluginCommands.State.Update.dispatch(this, { state, tree });
-    }
+    private initCustomParamEditors() {
+        if (!this.spec.customParamEditors) return;
 
-    updateTransform(state: State, a: Transform.Ref, params: any) {
-        const tree = state.build().to(a).update(params);
-        return PluginCommands.State.Update.dispatch(this, { state, tree });
+        for (const [t, e] of this.spec.customParamEditors) {
+            this.customParamEditors.set(t.id, e);
+        }
     }
 
     constructor(public spec: PluginSpec) {
@@ -178,12 +188,10 @@ export class PluginContext {
 
         this.initBehaviors();
         this.initDataActions();
+        this.initCustomParamEditors();
 
         this.lociLabels = new LociLabelManager(this);
 
-        // TODO: find a better solution for this.
-        setTimeout(() => this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`), 500);
+        this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`);
     }
-
-    // settings = ;
 }

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

@@ -31,6 +31,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Data.ParseCif),
         PluginSpec.Action(StateTransforms.Data.ParseCcp4),
         PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel),
+        PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureFromModel),
         PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory),
         PluginSpec.Action(StateTransforms.Model.VolumeFromCcp4),

+ 1 - 0
src/mol-plugin/spec.ts

@@ -14,6 +14,7 @@ export { PluginSpec }
 interface PluginSpec {
     actions: PluginSpec.Action[],
     behaviors: PluginSpec.Behavior[],
+    customParamEditors?: [StateAction | Transformer, StateTransformParameters.Class][]
     initialLayout?: PluginLayoutStateProps
 }
 

+ 44 - 26
src/mol-plugin/state/actions/basic.ts

@@ -14,8 +14,9 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PluginStateObject } from '../objects';
 import { StateTransforms } from '../transforms';
 import { Download } from '../transforms/data';
-import { StructureRepresentation3DHelpers, VolumeRepresentation3DHelpers } from '../transforms/representation';
+import { StructureRepresentation3DHelpers } from '../transforms/representation';
 import { getFileInfo, FileInput } from 'mol-util/file-info';
+import { Task } from 'mol-task';
 
 // TODO: "structure/volume parser provider"
 
@@ -40,6 +41,7 @@ const DownloadStructure = StateAction.build({
             }, { isFlat: true }),
             'url': PD.Group({
                 url: PD.Text(''),
+                format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]),
                 isBinary: PD.Boolean(false),
                 supportProps: PD.Boolean(false)
             }, { isFlat: true })
@@ -59,7 +61,7 @@ const DownloadStructure = StateAction.build({
 
     switch (src.name) {
         case 'url':
-            downloadParams = src.params;
+            downloadParams = { url: src.params.url, isBinary: src.params.isBinary };
             break;
         case 'pdbe-updated':
             downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
@@ -74,7 +76,8 @@ const DownloadStructure = StateAction.build({
     }
 
     const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams);
-    return state.update(createStructureTree(ctx, data, params.source.params.supportProps));
+    const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
+    return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps));
 });
 
 export const OpenStructure = StateAction.build({
@@ -84,15 +87,20 @@ export const OpenStructure = StateAction.build({
 })(({ params, state }, ctx: PluginContext) => {
     const b = state.build();
     const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
-    return state.update(createStructureTree(ctx, data, false));
+    const traj = createModelTree(data, 'cif');
+    return state.updateTree(createStructureTree(ctx, traj, false));
 });
 
-function createStructureTree(ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, supportProps: boolean): StateTree {
-    let root = b
-        .apply(StateTransforms.Data.ParseCif)
-        .apply(StateTransforms.Model.TrajectoryFromMmCif)
-        .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+function createModelTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') {
+    const parsed = format === 'cif'
+        ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
+        : b.apply(StateTransforms.Model.TrajectoryFromPDB);
+
+    return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+}
 
+function createStructureTree(ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean): StateTree {
+    let root = b;
     if (supportProps) {
         root = root.apply(StateTransforms.Model.CustomModelProperties);
     }
@@ -123,7 +131,7 @@ export const CreateComplexRepresentation = StateAction.build({
 })(({ ref, state }, ctx: PluginContext) => {
     const root = state.build().to(ref);
     complexRepresentation(ctx, root);
-    return state.update(root.getTree());
+    return state.updateTree(root.getTree());
 });
 
 export const UpdateTrajectory = StateAction.build({
@@ -133,7 +141,7 @@ export const UpdateTrajectory = StateAction.build({
         by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
     }
 })(({ params, state }) => {
-    const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model)
+    const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
         .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
 
     const update = state.build();
@@ -157,7 +165,7 @@ export const UpdateTrajectory = StateAction.build({
         }
     }
 
-    return state.update(update);
+    return state.updateTree(update);
 });
 
 //
@@ -177,15 +185,15 @@ function getVolumeData(format: VolumeFormat, b: StateTreeBuilder.To<PluginStateO
 }
 
 function createVolumeTree(format: VolumeFormat, ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>): StateTree {
-
-    const root = getVolumeData(format, b)
-        .apply(StateTransforms.Representation.VolumeRepresentation3D,
-            VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface'));
-
-    return root.getTree();
+    return getVolumeData(format, b)
+        .apply(StateTransforms.Representation.VolumeRepresentation3D)
+            // the parameters will be used automatically by the reconciler and the IsoValue object
+            // will get the correct Stats object instead of the empty one
+            // VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface'))
+        .getTree();
 }
 
-function getFileFormat(format: VolumeFormat | 'auto', file: FileInput): VolumeFormat {
+function getFileFormat(format: VolumeFormat | 'auto', file: FileInput, data?: Uint8Array): VolumeFormat {
     if (format === 'auto') {
         const fileFormat = getFileInfo(file).ext
         if (fileFormat in VolumeFormats) {
@@ -208,12 +216,22 @@ export const OpenVolume = StateAction.build({
             ['auto', 'Automatic'], ['ccp4', 'CCP4'], ['mrc', 'MRC'], ['map', 'MAP'], ['dsn6', 'DSN6'], ['brix', 'BRIX'], ['dscif', 'densityServerCIF']
         ]),
     }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: params.isBinary });
-    const format = getFileFormat(params.format, params.file)
-    return state.update(createVolumeTree(format, ctx, data));
-});
+})(({ params, state }, ctx: PluginContext) => Task.create('Open Volume', async taskCtx => {
+    const dataTree = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: true });
+    const volumeData = await state.updateTree(dataTree).runInContext(taskCtx);
+
+    // Alternative for more complex states where the builder is not a simple StateTreeBuilder.To<>:
+    /*
+    const dataRef = dataTree.ref;
+    await state.updateTree(dataTree).runInContext(taskCtx);
+    const dataCell = state.select(dataRef)[0];
+    */
+
+    const format = getFileFormat(params.format, params.file, volumeData.data as Uint8Array);
+    const volumeTree = state.build().to(dataTree.ref);
+    // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
+    await state.updateTree(createVolumeTree(format, ctx, volumeTree)).runInContext(taskCtx);
+}));
 
 export { DownloadDensity };
 type DownloadDensity = typeof DownloadDensity
@@ -280,5 +298,5 @@ const DownloadDensity = StateAction.build({
     }
 
     const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams);
-    return state.update(createVolumeTree(format, ctx, data));
+    return state.updateTree(createVolumeTree(format, ctx, data));
 });

+ 63 - 11
src/mol-plugin/state/transforms/model.ts

@@ -8,7 +8,7 @@
 import { PluginStateTransform } from '../objects';
 import { PluginStateObject as SO } from '../objects';
 import { Task, RuntimeContext } from 'mol-task';
-import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure';
+import { Model, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import Expression from 'mol-script/language/expression';
 import { compile } from 'mol-script/runtime/query/compiler';
@@ -16,11 +16,14 @@ import { MolScriptBuilder } from 'mol-script/language/builder';
 import { StateObject } from 'mol-state';
 import { PluginContext } from 'mol-plugin/context';
 import { stringToWords } from 'mol-util/string';
-import { volumeFromCcp4 } from 'mol-model/volume/formats/ccp4';
+import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4';
 import { Vec3 } from 'mol-math/linear-algebra';
-import { volumeFromDsn6 } from 'mol-model/volume/formats/dsn6';
-import { volumeFromDensityServerData } from 'mol-model/volume';
 import CIF from 'mol-io/reader/cif';
+import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
+import { parsePDB } from 'mol-io/reader/pdb/parser';
+import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
 
 export { TrajectoryFromMmCif }
 type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
@@ -47,7 +50,7 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
             const header = params.blockHeader || a.data.blocks[0].header;
             const block = a.data.blocks.find(b => b.header === header);
             if (!block) throw new Error(`Data block '${[header]}' not found.`);
-            const models = await Model.create(Format.mmCIF(block)).runInContext(ctx);
+            const models = await trajectoryFromMmCIF(block).runInContext(ctx);
             if (models.length === 0) throw new Error('No models found.');
             const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
             return new SO.Molecule.Trajectory(models, props);
@@ -55,6 +58,27 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     }
 });
 
+
+export { TrajectoryFromPDB }
+type TrajectoryFromPDB = typeof TrajectoryFromPDB
+const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
+    name: 'trajectory-from-pdb',
+    display: { name: 'Parse PDB string and create trajectory' },
+    from: [SO.Data.String],
+    to: SO.Molecule.Trajectory
+})({
+    apply({ a }) {
+        return Task.create('Parse PDB', async ctx => {
+            const parsed = await parsePDB(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            const models = await trajectoryFromPDB(parsed.result).runInContext(ctx);
+            const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            return new SO.Molecule.Trajectory(models, props);
+        });
+    }
+});
+
+
 export { ModelFromTrajectory }
 const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
 type ModelFromTrajectory = typeof ModelFromTrajectory
@@ -107,28 +131,30 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     to: SO.Molecule.Structure,
     params(a) {
         if (!a) {
-            return { id: PD.makeOptional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. If none specified (undefined or empty string), the asymmetric unit is used.' })) };
+            return { id: PD.makeOptional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. Value \'deposited\' can be used to specify deposited asymmetric unit.' })) };
         }
         const model = a.data;
         const ids = model.symmetry.assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]);
-        if (!ids.length) ids.push(['deposited', 'Deposited'])
+        ids.push(['deposited', 'Deposited']);
         return { id: PD.makeOptional(PD.Select(ids[0][0], ids, { label: 'Asm Id', description: 'Assembly Id' })) };
     }
 })({
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Build Assembly', async ctx => {
             const model = a.data;
-            const id = params.id;
-            const asm = ModelSymmetry.findAssembly(model, id || '');
+            let id = params.id;
+            let asm = ModelSymmetry.findAssembly(model, id || '');
             if (!!id && id !== 'deposited' && !asm) throw new Error(`Assembly '${id}' not found`);
 
             const base = Structure.ofModel(model);
-            if (!asm) {
-                plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`);
+            if ((id && !asm) || model.symmetry.assemblies.length === 0) {
+                if (!!id && id !== 'deposited') plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`);
                 const label = { label: a.data.label, description: structureDesc(base) };
                 return new SO.Molecule.Structure(base, label);
             }
 
+            asm = model.symmetry.assemblies[0];
+            id = asm.id;
             const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
             const props = { label: `Assembly ${id}`, description: structureDesc(s) };
             return new SO.Molecule.Structure(s, props);
@@ -136,6 +162,32 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
+export { StructureSymmetryFromModel }
+type StructureSymmetryFromModel = typeof StructureSymmetryFromModel
+const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
+    name: 'structure-symmetry-from-model',
+    display: { name: 'Structure Symmetry', description: 'Create a molecular structure symmetry.' },
+    from: SO.Molecule.Model,
+    to: SO.Molecule.Structure,
+    params(a) {
+        return {
+            ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
+            ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+        }
+    }
+})({
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Build Symmetry', async ctx => {
+            const { ijkMin, ijkMax } = params
+            const model = a.data;
+            const base = Structure.ofModel(model);
+            const s = await StructureSymmetry.buildSymmetryRange(base, ijkMin, ijkMax).runInContext(ctx);
+            const props = { label: `Symmetry [${ijkMin}] to [${ijkMax}]`, description: structureDesc(s) };
+            return new SO.Molecule.Structure(s, props);
+        })
+    }
+});
+
 export { StructureSelection }
 type StructureSelection = typeof StructureSelection
 const StructureSelection = PluginStateTransform.BuiltIn({

+ 2 - 0
src/mol-plugin/state/transforms/representation.ts

@@ -233,6 +233,8 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
             const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
             repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params))
             // TODO set initial state, repr.setState({})
+            // TODO include isoValue in the label where available
+            console.log(params.type.params);
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
             return new SO.Volume.Representation3D(repr, { label: provider.label });
         });

+ 106 - 29
src/mol-plugin/ui/controls/parameters.tsx

@@ -5,7 +5,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Vec2 } from 'mol-math/linear-algebra';
+import { Vec2, Vec3 } from 'mol-math/linear-algebra';
 import { Color } from 'mol-util/color';
 import { ColorListName, getColorListFromName } from 'mol-util/color/scale';
 import { ColorNames, ColorNamesValueMap } from 'mol-util/color/tables';
@@ -109,20 +109,20 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
     }
 
     onHover = (point?: Vec2) => {
-        this.setState({isOverPoint: !this.state.isOverPoint});
+        this.setState({ isOverPoint: !this.state.isOverPoint });
         if (point) {
-            this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+            this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
             return;
         }
-        this.setState({message: `${this.props.value.length} points`});
+        this.setState({ message: `${this.props.value.length} points` });
     }
 
     onDrag = (point: Vec2) => {
-        this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+        this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
     }
 
-    onChange = (value: PD.LineGraph['defaultValue'] ) => {
-        this.props.onChange({ name: this.props.name, param: this.props.param, value: value});
+    onChange = (value: PD.LineGraph['defaultValue']) => {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value: value });
     }
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -146,18 +146,61 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
                     data={this.props.param.defaultValue}
                     onChange={this.onChange}
                     onHover={this.onHover}
-                    onDrag={this.onDrag}/>
+                    onDrag={this.onDrag} />
             </div>
         </>;
     }
 }
 
-export class NumberInputControl extends SimpleParam<PD.Numeric> {
-    onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
-    renderControl() {
-        return <span>
-            number input TODO
-        </span>
+export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>, { value: string }> {
+    state = { value: '0' };
+
+    protected update(value: any) {
+        this.props.onChange({ param: this.props.param, name: this.props.name, value });
+    }
+
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        const value = +e.target.value;
+        this.setState({ value: e.target.value }, () => {
+            if (!Number.isNaN(value) && value !== this.props.value) {
+                this.update(value);
+            }
+        });
+    }
+
+    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+        if (!this.props.onEnter) return;
+        if ((e.keyCode === 13 || e.charCode === 13)) {
+            this.props.onEnter();
+        }
+    }
+
+    onBlur = () => {
+        this.setState({ value: '' + this.props.value });
+    }
+
+    static getDerivedStateFromProps(props: { value: number }, state: { value: string }) {
+        const value = +state.value;
+        if (Number.isNaN(value) || value === props.value) return null;
+        return { value: '' + props.value };
+    }
+
+    render() {
+        const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        return <div className='msp-control-row'>
+            <span title={this.props.param.description}>{label}</span>
+            <div>
+                <input type='text'
+                    onBlur={this.onBlur}
+                    value={this.state.value}
+                    placeholder={placeholder}
+                    onChange={this.onChange}
+                    onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
+                    disabled={this.props.isDisabled}
+                />
+            </div>
+        </div>;
     }
 }
 
@@ -205,8 +248,9 @@ export class SelectControl extends SimpleParam<PD.Select<string | number>> {
         }
     }
     renderControl() {
-        return <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
-            {!this.props.param.options.some(e => e[0] === this.props.value) && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
+        const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
+        return <select value={this.props.value || this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
+            {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
             {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
         </select>;
     }
@@ -231,7 +275,7 @@ let _colors: React.ReactFragment | undefined = void 0;
 function ColorOptions() {
     if (_colors) return _colors;
     _colors = <>{Object.keys(ColorNames).map(name =>
-        <option key={name} value={(ColorNames as { [k: string]: Color})[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color})[name])}` }} >
+        <option key={name} value={(ColorNames as { [k: string]: Color })[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color })[name])}` }} >
             {name}
         </option>
     )}</>;
@@ -297,17 +341,49 @@ export class ColorScaleControl extends SimpleParam<PD.ColorScale<any>> {
     }
 }
 
-export class Vec3Control extends SimpleParam<PD.Vec3> {
-    // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    //     this.setState({ value: e.target.value });
-    //     this.props.onChange(e.target.value);
-    // }
+export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isExpanded: boolean }> {
+    state = { isExpanded: false }
 
-    renderControl() {
-        return <span>vec3 TODO</span>;
+    components = {
+        0: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
+        1: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
+        2: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
+    }
+
+    change(value: PD.MultiSelect<any>['defaultValue']) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    componentChange: ParamOnChange = ({ name, value }) => {
+        const v = Vec3.copy(Vec3.zero(), this.props.value);
+        v[+name] = value;
+        this.change(v);
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    render() {
+        const v = this.props.value;
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const value = `[${v[0].toFixed(2)}, ${v[1].toFixed(2)}, ${v[2].toFixed(2)}]`;
+        return <>
+            <div className='msp-control-row'>
+                <span>{label}</span>
+                <div>
+                    <button onClick={this.toggleExpanded}>{value}</button>
+                </div>
+            </div>
+            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
+            </div>
+        </>;
     }
 }
 
+
 export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
     change(value: File) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -330,7 +406,7 @@ export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
 export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
     state = { isExpanded: false }
 
-    change(value: PD.MultiSelect<any>['defaultValue'] ) {
+    change(value: PD.MultiSelect<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
@@ -366,7 +442,8 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
                         <button onClick={this.toggle(value)} disabled={this.props.isDisabled}>
                             <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
                         </button>
-                </div> })}
+                    </div>
+                })}
             </div>
         </>;
     }
@@ -375,7 +452,7 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
 export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.param.isExpanded }
 
-    change(value: any ) {
+    change(value: any) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
@@ -423,7 +500,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
         }
     }
 
-    change(value: PD.Mapped<any>['defaultValue'] ) {
+    change(value: PD.Mapped<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
@@ -458,7 +535,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
 }
 
 export class ConditionedControl extends React.PureComponent<ParamProps<PD.Conditioned<any, any, any>>> {
-    change(value: PD.Conditioned<any, any, any>['defaultValue'] ) {
+    change(value: PD.Conditioned<any, any, any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 

+ 1 - 0
src/mol-plugin/ui/state/apply-action.tsx

@@ -41,6 +41,7 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App
         });
     }
     getInfo() { return this._getInfo(this.props.nodeRef, this.props.state.transforms.get(this.props.nodeRef).version); }
+    getTransformerId() { return this.props.state.transforms.get(this.props.nodeRef).transformer.id; }
     getHeader() { return this.props.action.definition.display; }
     canApply() { return !this.state.error && !this.state.busy; }
     canAutoApply() { return false; }

+ 7 - 1
src/mol-plugin/ui/state/common.tsx

@@ -102,6 +102,7 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
     abstract getInfo(): StateTransformParameters.Props['info'];
     abstract getHeader(): Transformer.Definition['display'];
     abstract canApply(): boolean;
+    abstract getTransformerId(): string;
     abstract canAutoApply(newParams: any): boolean;
     abstract applyText(): string;
     abstract isUpdate(): boolean;
@@ -170,13 +171,18 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
 
         const display = this.getHeader();
 
+        const tId = this.getTransformerId();
+        const ParamEditor: StateTransformParameters.Class = this.plugin.customParamEditors.has(tId)
+            ? this.plugin.customParamEditors.get(tId)!
+            : StateTransformParameters;
+
         return <div className='msp-transform-wrapper'>
             <div className='msp-transform-header'>
                 <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{display.name}</button>
                 {!this.state.isCollapsed && <button className='msp-btn msp-btn-link msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'>↻</button>}
             </div>
             {!this.state.isCollapsed && <>
-                <StateTransformParameters info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
+                <ParamEditor info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
 
                 <div className='msp-transform-apply-wrap'>
                     <button className='msp-btn msp-btn-block msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}>

+ 1 - 0
src/mol-plugin/ui/state/update-transform.tsx

@@ -28,6 +28,7 @@ namespace UpdateTransformContol {
 class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Props, UpdateTransformContol.ComponentState> {
     applyAction() { return this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params); }
     getInfo() { return this._getInfo(this.props.transform); }
+    getTransformerId() { return this.props.transform.transformer.id; }
     getHeader() { return this.props.transform.transformer.definition.display; }
     canApply() { return !this.state.error && !this.state.busy && !this.state.isInitial; }
     applyText() { return this.canApply() ? 'Update' : 'Nothing to Update'; }

+ 1 - 1
src/mol-state/action.ts

@@ -71,7 +71,7 @@ namespace StateAction {
             params: def.params as Transformer.Definition<Transformer.From<T>, any, Transformer.Params<T>>['params'],
             run({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
-                return state.update(tree);
+                return state.updateTree(tree) as Task<void>;
             }
         })
     }

+ 1 - 1
src/mol-state/object.ts

@@ -105,7 +105,7 @@ export class StateObjectTracker<T extends StateObject> {
     }
 
     update() {
-        const cell = this.state.query(this.query)[0];
+        const cell = this.state.select(this.query)[0];
         const version = cell ? cell.transform.version : void 0;
         const changed = this.cell !== cell || this.version !== version;
         this.cell = cell;

+ 65 - 44
src/mol-state/state.ts

@@ -33,7 +33,7 @@ class State {
     readonly globalContext: unknown = void 0;
     readonly events = {
         cell: {
-            stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State}>(),
+            stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             removed: this.ev<State.ObjectEvent & { parent: Transform.Ref }>(),
         },
@@ -67,7 +67,7 @@ class State {
 
     setSnapshot(snapshot: State.Snapshot) {
         const tree = StateTree.fromJSON(snapshot.tree);
-        return this.update(tree);
+        return this.updateTree(tree);
     }
 
     setCurrent(ref: Transform.Ref) {
@@ -89,26 +89,28 @@ class State {
     }
 
     /**
-     * Select Cells by ref or a query generated on the fly.
-     * @example state.select('test')
-     * @example state.select(q => q.byRef('test').subtree())
+     * Select Cells using the provided selector.
+     * @example state.query(StateSelection.Generators.byRef('test').ancestorOfType([type]))
+     * @example state.query('test')
      */
-    select(selector: Transform.Ref | ((q: typeof StateSelection.Generators) => StateSelection.Selector)) {
-        if (typeof selector === 'string') return StateSelection.select(selector, this);
-        return StateSelection.select(selector(StateSelection.Generators), this)
+    select(selector: StateSelection.Selector) {
+        return StateSelection.select(selector, this)
     }
 
     /**
-     * Select Cells using the provided selector.
-     * @example state.select('test')
+     * Select Cells by building a query generated on the fly.
      * @example state.select(q => q.byRef('test').subtree())
      */
-    query(selector: StateSelection.Selector) {
-        return StateSelection.select(selector, this)
+    selectQ(selector: (q: typeof StateSelection.Generators) => StateSelection.Selector) {
+        if (typeof selector === 'string') return StateSelection.select(selector, this);
+        return StateSelection.select(selector(StateSelection.Generators), this)
     }
 
-    /** If no ref is specified, apply to root */
-    apply<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> {
+    /**
+     * Creates a Task that applies the specified StateAction (i.e. must use run* on the result)
+     * If no ref is specified, apply to root.
+     */
+    applyAction<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> {
         return Task.create('Apply Action', ctx => {
             const cell = this.cells.get(ref);
             if (!cell) throw new Error(`'${ref}' does not exist.`);
@@ -118,41 +120,59 @@ class State {
         });
     }
 
-    update(tree: StateTree | StateTreeBuilder, silent: boolean = false): Task<void> {
-        const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient();
+    /**
+     * Reconcialites the existing state tree with the new version.
+     *
+     * If the tree is StateTreeBuilder.To<T>, the corresponding StateObject is returned by the task.
+     * @param tree Tree instance or a tree builder instance
+     * @param doNotReportTiming Indicates whether to log timing of the individual transforms
+     */
+    updateTree<T extends StateObject>(tree: StateTree | StateTreeBuilder | StateTreeBuilder.To<T>, doNotLogTiming?: boolean): Task<T>
+    updateTree(tree: StateTree | StateTreeBuilder, doNotLogTiming?: boolean): Task<void>
+    updateTree(tree: StateTree | StateTreeBuilder, doNotLogTiming: boolean = false): Task<any> {
         return Task.create('Update Tree', async taskCtx => {
             let updated = false;
             try {
-                const oldTree = this._tree;
-                this._tree = _tree;
+                const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, doNotLogTiming);
+                updated = await update(ctx);
+                if (StateTreeBuilder.isTo(tree)) {
+                    const cell = this.select(tree.ref)[0];
+                    return cell && cell.obj;
+                }
+            } finally {
+                if (updated) this.events.changed.next();
+            }
+        });
+    }
+
+    private updateTreeAndCreateCtx(tree: StateTree | StateTreeBuilder, taskCtx: RuntimeContext, doNotLogTiming: boolean) {
+        const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient();
+        const oldTree = this._tree;
+        this._tree = _tree;
 
-                const ctx: UpdateContext = {
-                    parent: this,
-                    editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0,
+        const ctx: UpdateContext = {
+            parent: this,
+            editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0,
 
-                    errorFree: this.errorFree,
-                    taskCtx,
-                    oldTree,
-                    tree: _tree,
-                    cells: this.cells as Map<Transform.Ref, StateObjectCell>,
-                    transformCache: this.transformCache,
+            errorFree: this.errorFree,
+            taskCtx,
+            oldTree,
+            tree: _tree,
+            cells: this.cells as Map<Transform.Ref, StateObjectCell>,
+            transformCache: this.transformCache,
 
-                    results: [],
+            results: [],
 
-                    silent,
+            silent: doNotLogTiming,
 
-                    changed: false,
-                    hadError: false,
-                    newCurrent: void 0
-                };
+            changed: false,
+            hadError: false,
+            newCurrent: void 0
+        };
 
-                this.errorFree = true;
-                // TODO: handle "cancelled" error? Or would this be handled automatically?
-                updated = await update(ctx);
-            } finally {
-                if (updated) this.events.changed.next();
-            }
-        });
+        this.errorFree = true;
+
+        return ctx;
     }
 
     constructor(rootObject: StateObject, params?: { globalContext?: unknown }) {
@@ -167,8 +187,8 @@ class State {
             version: root.version,
             errorText: void 0,
             params: {
-                definition: { },
-                values: { }
+                definition: {},
+                values: {}
             }
         });
 
@@ -311,7 +331,7 @@ async function update(ctx: UpdateContext) {
         const current = ctx.parent.current;
         const currentCell = ctx.cells.get(current);
         if (currentCell && (
-                currentCell.obj === StateObject.Null
+            currentCell.obj === StateObject.Null
             || (currentCell.status === 'error' && currentCell.errorText === ParentNullErrorText))) {
             newCurrent = findNewCurrent(ctx.oldTree, current, [], ctx.cells);
             ctx.parent.setCurrent(newCurrent);
@@ -509,6 +529,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
         ctx.changed = true;
         if (!ctx.hadError) ctx.newCurrent = root;
         doError(ctx, root, '' + e, false);
+        console.error(e);
         return;
     }
 
@@ -523,7 +544,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
 
 function resolveParams(ctx: UpdateContext, transform: Transform, src: StateObject) {
     const prms = transform.transformer.definition.params;
-    const definition = prms ? prms(src, ctx.parent.globalContext) : { };
+    const definition = prms ? prms(src, ctx.parent.globalContext) : {};
     const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition);
     return { definition, values };
 }

+ 4 - 0
src/mol-state/tree/builder.ts

@@ -33,6 +33,10 @@ namespace StateTreeBuilder {
         return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function';
     }
 
+    export function isTo(obj: any): obj is StateTreeBuilder.To<any> {
+        return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function' && typeof (obj as StateTreeBuilder.To<any>).ref === 'string';
+    }
+
     export class Root implements StateTreeBuilder {
         private state: State;
         get editInfo() { return this.state.editInfo; }

+ 3 - 1
src/mol-util/param-definition.ts

@@ -14,13 +14,15 @@ export namespace ParamDefinition {
     export interface Info {
         label?: string,
         description?: string,
+        fieldLabels?: { [name: string]: string },
         isHidden?: boolean,
     }
 
     function setInfo<T extends Info>(param: T, info?: Info): T {
         if (!info) return param;
-        if (info.description) param.description = info.description;
         if (info.label) param.label = info.label;
+        if (info.description) param.description = info.description;
+        if (info.fieldLabels) param.fieldLabels = info.fieldLabels;
         if (info.isHidden) param.isHidden = info.isHidden;
         return param;
     }

+ 1 - 1
src/mol-util/set.ts

@@ -22,7 +22,7 @@ export namespace SetUtils {
         return union;
     }
 
-    export function unionMany<T>(sets: Set<T>[]) {
+    export function unionMany<T>(...sets: Set<T>[]) {
         if (sets.length === 0) return new Set<T>();
         if (sets.length === 1) return sets[0];
         const union = new Set(sets[0]);

+ 9 - 0
src/mol-util/string.ts

@@ -37,4 +37,13 @@ export function snakeCaseToWords(str: string) {
 
 export function stringToWords(str: string) {
     return capitalize(splitCamelCase(splitSnakeCase(str)))
+}
+
+export function substringStartsWith(str: string, start: number, end: number, target: string) {
+    let len = target.length;
+    if (len > end - start) return false;
+    for (let i = 0; i < len; i++) {
+        if (str.charCodeAt(start + i) !== target.charCodeAt(i)) return false;
+    }
+    return true;
 }

+ 4 - 4
src/perf-tests/lookup3d.ts

@@ -2,11 +2,12 @@ import * as util from 'util'
 import * as fs from 'fs'
 import CIF from 'mol-io/reader/cif'
 
-import { Structure, Model, Format } from 'mol-model/structure'
+import { Structure } from 'mol-model/structure'
 
 import { GridLookup3D } from 'mol-math/geometry';
 // import { sortArray } from 'mol-data/util';
 import { OrderedSet } from 'mol-data/int';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 
 require('util.promisify').shim();
 const readFileAsync = util.promisify(fs.readFile);
@@ -31,11 +32,10 @@ export async function readCIF(path: string) {
         throw parsed;
     }
 
-    const mmcif = Format.mmCIF(parsed.result.blocks[0]);
-    const models = await Model.create(mmcif).run();
+    const models = await trajectoryFromMmCIF(parsed.result.blocks[0]).run();
     const structures = models.map(Structure.ofModel);
 
-    return { mmcif: mmcif.data, models, structures };
+    return { mmcif: models[0].sourceData.data, models, structures };
 }
 
 export async function test() {

+ 4 - 7
src/perf-tests/structure.ts

@@ -11,11 +11,12 @@ import * as fs from 'fs'
 import fetch from 'node-fetch'
 import CIF from 'mol-io/reader/cif'
 
-import { Structure, Model, Queries as Q, StructureElement, StructureSelection, StructureSymmetry, StructureQuery, Format, StructureProperties as SP } from 'mol-model/structure'
+import { Structure, Model, Queries as Q, StructureElement, StructureSelection, StructureSymmetry, StructureQuery, StructureProperties as SP } from 'mol-model/structure'
 // import { Segmentation, OrderedSet } from 'mol-data/int'
 
 import to_mmCIF from 'mol-model/structure/export/mmcif'
 import { Vec3 } from 'mol-math/linear-algebra';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 // import { printUnits } from 'apps/structure-info/model';
 // import { EquivalenceClasses } from 'mol-data/util';
 
@@ -69,16 +70,12 @@ export async function readCIF(path: string) {
     }
 
     const data = parsed.result.blocks[0];
-    console.time('schema')
-    const mmcif = Format.mmCIF(data);
-
-    console.timeEnd('schema')
     console.time('buildModels')
-    const models = await Model.create(mmcif).run();
+    const models = await trajectoryFromMmCIF(data).run();
     console.timeEnd('buildModels')
     const structures = models.map(Structure.ofModel);
 
-    return { mmcif, models, structures };
+    return { mmcif: models[0].sourceData.data, models, structures };
 }
 
 const DATA_DIR = './build/data';

+ 3 - 2
src/servers/model/server/structure-wrapper.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Structure, Model, Format } from 'mol-model/structure';
+import { Structure, Model } from 'mol-model/structure';
 import { PerformanceMonitor } from 'mol-util/performance-monitor';
 import { Cache } from './cache';
 import Config from '../config';
@@ -15,6 +15,7 @@ import * as zlib from 'zlib'
 import { Job } from './jobs';
 import { ConsoleLogger } from 'mol-util/console-logger';
 import { ModelPropertiesProvider } from '../property-provider';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 
 require('util.promisify').shim();
 
@@ -108,7 +109,7 @@ export async function readStructureWrapper(key: string, sourceId: string | '_loc
     const frame = (await parseCif(data)).blocks[0];
     perf.end('parse');
     perf.start('createModel');
-    const models = await Model.create(Format.mmCIF(frame)).run();
+    const models = await trajectoryFromMmCIF(frame).run();
     perf.end('createModel');
 
     const modelMap = new Map<number, Model>();

+ 1 - 1
src/servers/volume/pack.ts

@@ -32,7 +32,7 @@ function printHelp() {
         `    Optionally specify maximum block size.`,
         ``,
         `  node pack -em density.map output.mdb [-blockSize 96]`,
-        `    Pack single density into a block file.`, 
+        `    Pack single density into a block file.`,
         `    Optionally specify maximum block size.`
     ];
     console.log(help.join('\n'));

+ 1 - 0
tsconfig.json

@@ -23,6 +23,7 @@
             "mol-math": ["./mol-math"],
             "mol-model": ["./mol-model"],
             "mol-model-props": ["./mol-model-props", "./mol-model-props/index.ts"],
+            "mol-model-formats": ["./mol-model-formats"],
             "mol-ql": ["./mol-ql"],
             "mol-repr": ["./mol-repr"],
             "mol-script": ["./mol-script"],