Browse Source

Merge branch 'master' into dev-sb-strucmotifwizard

# Conflicts:
#	CHANGELOG.md
JonStargaryen 4 years ago
parent
commit
2093bc6935

+ 4 - 0
CHANGELOG.md

@@ -6,6 +6,10 @@
 ### Bug fixes
 - limit number of exchanges per position
 
+## [1.2.0] - 2021-02-16
+### General
+-  structure alignment data visualization
+
 ## [1.1.0] - 2021-02-08
 ### General
 - structural motif search wizard

File diff suppressed because it is too large
+ 118 - 118
package-lock.json


+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
     "name": "@rcsb/rcsb-molstar",
-    "version": "1.1.0",
+    "version": "1.2.0",
     "description": "RCSB PDB apps and props based on Mol*.",
     "homepage": "https://github.com/rcsb/rcsb-molstar#readme",
     "repository": {
@@ -48,7 +48,7 @@
         "extra-watch-webpack-plugin": "^1.0.3",
         "file-loader": "^6.2.0",
         "mini-css-extract-plugin": "^1.3.2",
-        "molstar": "^1.3.0",
+        "molstar": "^1.3.1",
         "node-sass": "^5.0.0",
         "raw-loader": "^4.0.2",
         "react": "^17.0.1",

+ 92 - 0
src/viewer/helpers/export.ts

@@ -0,0 +1,92 @@
+import { PluginContext } from 'molstar/lib/mol-plugin/context';
+import { StateSelection } from 'molstar/lib/mol-state';
+import { PluginStateObject } from 'molstar/lib/mol-plugin-state/objects';
+import { StructureSelection, Structure } from 'molstar/lib/mol-model/structure';
+import { CifExportContext, encode_mmCIF_categories } from 'molstar/lib/mol-model/structure/export/mmcif';
+import { utf8ByteCount, utf8Write } from 'molstar/lib/mol-io/common/utf8';
+import { zip } from 'molstar/lib/mol-util/zip/zip';
+import { getFormattedTime } from 'molstar/lib/mol-util/date';
+import { download } from 'molstar/lib/mol-util/download';
+import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property';
+import { CifWriter } from 'molstar/lib/mol-io/writer/cif';
+
+type encode_mmCIF_categories_Params = {
+    skipCategoryNames?: Set<string>,
+    exportCtx?: CifExportContext,
+    copyAllCategories?: boolean,
+    customProperties?: CustomPropertyDescriptor[]
+}
+
+function exportParams(): encode_mmCIF_categories_Params {
+    const skipCategories: Set<string> = new Set();
+    skipCategories
+        // Basics
+        .add('entry')
+        // Symmetry
+        .add('cell')
+        .add('symmetry')
+        // Secondary structure
+        .add('struct_conf')
+        .add('struct_sheet_range')
+        // Assemblies
+        .add('pdbx_struct_assembly')
+        .add('pdbx_struct_assembly_gen')
+        .add('pdbx_struct_oper_list');
+    const params: encode_mmCIF_categories_Params = {
+        skipCategoryNames: skipCategories
+    };
+    return params;
+}
+
+function to_mmCIF(name: string, structure: Structure, asBinary = false) {
+    const enc = CifWriter.createEncoder({ binary: asBinary });
+    enc.startDataBlock(name);
+    encode_mmCIF_categories(enc, structure, exportParams());
+    return enc.getData();
+}
+
+function getDecorator(plugin: PluginContext, root: string): string {
+    const tree = plugin.state.data.tree;
+    const children = tree.children.get(root);
+    if (children.size !== 1) return root;
+    const child = children.first();
+    if (tree.transforms.get(child).transformer.definition.isDecorator) {
+        return getDecorator(plugin, child);
+    }
+    return root;
+}
+
+function extractStructureDataFromState(plugin: PluginContext): { [k: string]: Structure } {
+    const content: { [k: string]: Structure } = Object.create(null);
+    const cells = plugin.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure));
+    for (let i = 0; i < cells.length; i++) {
+        const c = cells[i];
+        const nodeRef = getDecorator(plugin, c.transform.ref);
+        const children = plugin.state.data.select(StateSelection.Generators.byRef(nodeRef))
+            .map(child => child.obj!.data);
+        const sele = StructureSelection.Sequence(c.obj!.data, children);
+        const structure = StructureSelection.unionStructure(sele);
+        const name = `${i + 1}-${structure.model.entryId}`;
+        content[name] = structure;
+    }
+    return content;
+}
+
+export function encodeStructureData(plugin: PluginContext): { [k: string]: Uint8Array } {
+    const content: { [k: string]: Uint8Array } = Object.create(null);
+    const structures = extractStructureDataFromState(plugin);
+    for (const [key, structure] of Object.entries(structures)) {
+        const filename = `${key}.cif`;
+        const str = to_mmCIF(filename, structure, false) as string;
+        const data = new Uint8Array(utf8ByteCount(str));
+        utf8Write(data, 0, str);
+        content[filename] = data;
+    }
+    return content;
+}
+
+export function downloadAsZipFile(content: { [k: string]: Uint8Array }) {
+    const filename = `mol-star_download_${getFormattedTime()}.zip`;
+    const buf = zip(content);
+    download(new Blob([buf]), filename);
+}

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

@@ -35,7 +35,7 @@ export class ModelLoader {
             preset: props || { kind: 'standard', assemblyId: '' }
         });
 
-        if (matrix && selector) {
+        if (matrix && selector?.structureProperties) {
             const params = {
                 transform: {
                     name: 'matrix' as const,

+ 233 - 11
src/viewer/helpers/preset.ts

@@ -18,8 +18,13 @@ import { Structure, StructureSelection, QueryContext, StructureElement } from 'm
 import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
 import { InitVolumeStreaming } from 'molstar/lib/mol-plugin/behavior/dynamic/volume-streaming/transformers';
 import { ViewerState } from '../types';
-import { StateSelection } from 'molstar/lib/mol-state';
+import { StateSelection, StateObjectSelector, StateObject, StateTransformer, StateObjectRef } from 'molstar/lib/mol-state';
 import { VolumeStreaming } from 'molstar/lib/mol-plugin/behavior/dynamic/volume-streaming/behavior';
+import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
+import { CustomStructureProperties } from 'molstar/lib/mol-plugin-state/transforms/model';
+import { FlexibleStructureFromModel as FlexibleStructureFromModel } from './superpose/flexible-structure';
+import { StructureRepresentationRegistry } from 'molstar/lib/mol-repr/structure/registry';
+import { StructureSelectionQueries as Q } from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query';
 import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
 import { InteractivityManager } from 'molstar/lib/mol-plugin-state/manager/interactivity';
 
@@ -68,11 +73,30 @@ function targetToLoci(target: Target, structure: Structure): StructureElement.Lo
     return StructureSelection.toLociWithSourceUnits(selection);
 }
 
+type Range = {
+    label_asym_id: string
+    label_seq_id?: { beg: number, end?: number }
+}
+
 type BaseProps = {
     assemblyId?: string
     modelIndex?: number
 }
 
+type ColorProp = {
+    name: 'color',
+    value: number,
+    positions: Range[]
+};
+
+export type PropsetProps = {
+    kind: 'prop-set',
+    selection?: (Range & {
+        matrix?: Mat4
+    })[],
+    representation: ColorProp[]
+} & BaseProps
+
 type ValidationProps = {
     kind: 'validation'
     colorTheme?: string
@@ -97,12 +121,74 @@ type DensityProps = {
     kind: 'density'
 } & BaseProps
 
-export type PresetProps = ValidationProps | StandardProps | SymmetryProps | FeatureProps | DensityProps
+export type PresetProps = ValidationProps | StandardProps | SymmetryProps | FeatureProps | DensityProps | PropsetProps
 
 const RcsbParams = (a: PluginStateObject.Molecule.Trajectory | undefined, plugin: PluginContext) => ({
     preset: PD.Value<PresetProps>({ kind: 'standard', assemblyId: '' }, { isHidden: true })
 });
 
+type StructureObject = StateObjectSelector<PluginStateObject.Molecule.Structure, StateTransformer<StateObject<any, StateObject.Type<any>>, StateObject<any, StateObject.Type<any>>, any>>
+
+const CommonParams = StructureRepresentationPresetProvider.CommonParams;
+
+const reprBuilder = StructureRepresentationPresetProvider.reprBuilder;
+const updateFocusRepr = StructureRepresentationPresetProvider.updateFocusRepr;
+
+type SelectionExpression = {
+    tag: string
+    type: StructureRepresentationRegistry.BuiltIn
+    label: string
+    expression: Expression
+};
+
+export const RcsbSuperpositionRepresentationPreset = StructureRepresentationPresetProvider({
+    id: 'preset-superposition-representation-rcsb',
+    display: {
+        group: 'Superposition',
+        name: 'Alignment',
+        description: 'Show representations based on the structural alignment data.'
+    },
+    params: () => ({
+        ...CommonParams,
+        selectionExpressions: PD.Value<SelectionExpression[]>([])
+    }),
+    async apply(ref, params, plugin) {
+
+        const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
+        if (!structureCell) return Object.create(null);
+
+        const structure = structureCell.obj!.data;
+        const cartoonProps = {sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2};
+
+        let components = Object.create(null);
+        let representations = Object.create(null);
+        for (const expr of params.selectionExpressions) {
+
+            const comp = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, expr.expression, expr.label, { label: expr.label });
+            Object.assign(components, {[expr.label]: comp});
+
+            const { update, builder, typeParams, color } = reprBuilder(plugin, params);
+
+            let typeProps = {...typeParams};
+            if (expr.type === 'cartoon') {
+                Object.assign(typeProps, {...cartoonProps});
+            }
+
+            Object.assign(representations, {
+                [expr.label]: builder.buildRepresentation(update, comp, {type: expr.type,
+                    typeParams: typeProps, color: color as any}, { tag: expr.tag }),
+            });
+
+            await update.commit({ revertOnError: false });
+
+        }
+        // needed to apply same coloring scheme to focus representation
+        await updateFocusRepr(plugin, structure, params.theme?.focus?.name, params.theme?.focus?.params);
+
+        return representations;
+    }
+});
+
 export const RcsbPreset = TrajectoryHierarchyPresetProvider({
     id: 'preset-trajectory-rcsb',
     display: { name: 'RCSB' },
@@ -127,27 +213,84 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
         const model = await builder.createModel(trajectory, modelParams);
         const modelProperties = await builder.insertModelProperties(model);
 
-        const structure = await builder.createStructure(modelProperties || model, structureParams);
-        const structureProperties = await builder.insertStructureProperties(structure);
+        let structure: StructureObject | undefined = undefined;
+        let structureProperties: StructureObject | undefined = undefined;
+
+        // If flexible transformation is allowed, we may need to create a single structure component
+        // from transformed substructures
+        const allowsFlexTransform = p.kind === 'prop-set';
+        if (!allowsFlexTransform) {
+            structure = await builder.createStructure(modelProperties || model, structureParams);
+            structureProperties = await builder.insertStructureProperties(structure);
+        }
 
         const unitcell = await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: true });
 
         let representation: StructureRepresentationPresetProvider.Result | undefined = undefined;
 
-        if (p.kind === 'validation') {
-            representation = await plugin.builders.structure.representation.applyPreset(structureProperties, ValidationReportGeometryQualityPreset);
+        if (p.kind === 'prop-set') {
+
+            // This creates a single structure from selections/transformations as specified
+            const _structure = plugin.state.data.build().to(modelProperties)
+                .apply(FlexibleStructureFromModel, { selection: p.selection });
+            structure = await _structure.commit();
+
+            const _structureProperties = plugin.state.data.build().to(structure)
+                .apply(CustomStructureProperties);
+            structureProperties = await _structureProperties.commit();
+
+            // adding coloring lookup scheme
+            structure.data!.inheritedPropertyData.colors = Object.create(null);
+            for (const repr of p.representation) {
+                if (repr.name === 'color') {
+                    const colorValue = repr.value;
+                    const positions = repr.positions;
+                    for (const range of positions) {
+                        if (!structure.data!.inheritedPropertyData.colors[range.label_asym_id])
+                            structure.data!.inheritedPropertyData.colors[range.label_asym_id] = new Map();
+                        const residues: number[] = (range.label_seq_id) ? toRange(range.label_seq_id.beg, range.label_seq_id.end) : [];
+                        for (const num of residues) {
+                            structure.data!.inheritedPropertyData.colors[range.label_asym_id].set(num, colorValue);
+                        }
+                    }
+                }
+            }
+
+            // At this we have a structure that contains only the transformed substructres,
+            // creating structure selections to have multiple components per each flexible part
+            const entryId = model.data!.entryId;
+            let selectionExpressions: SelectionExpression[] = [];
+            if (p.selection) {
+                for (const range of p.selection) {
+                    selectionExpressions = selectionExpressions.concat(createSelectionExpression(entryId, range));
+                }
+            } else {
+                selectionExpressions = selectionExpressions.concat(createSelectionExpression(entryId));
+            }
+
+            const params = {
+                ignoreHydrogens: CommonParams.ignoreHydrogens.defaultValue,
+                quality: CommonParams.quality.defaultValue,
+                theme: { globalName: 'superpose' as any, focus: { name: 'superpose' } },
+                selectionExpressions: selectionExpressions
+            };
+            representation = await RcsbSuperpositionRepresentationPreset.apply(structure, params, plugin);
+
+        } else if (p.kind === 'validation') {
+            representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, ValidationReportGeometryQualityPreset);
+
         } else if (p.kind === 'symmetry') {
-            representation = await plugin.builders.structure.representation.applyPreset<any>(structureProperties, AssemblySymmetryPreset, { symmetryIndex: p.symmetryIndex });
+            representation = await plugin.builders.structure.representation.applyPreset<any>(structureProperties!, AssemblySymmetryPreset, { symmetryIndex: p.symmetryIndex });
 
             ViewerState(plugin).collapsed.next({
                 ...ViewerState(plugin).collapsed.value,
                 custom: false
             });
         } else {
-            representation = await plugin.builders.structure.representation.applyPreset(structureProperties, 'auto');
+            representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, 'auto');
         }
 
-        if (p.kind === 'feature' && structure.obj) {
+        if (p.kind === 'feature' && structure?.obj) {
             const loci = targetToLoci(p.target, structure.obj.data);
             // if target is only defined by chain: then don't force first residue
             const chainMode = p.target.label_asym_id && !p.target.auth_seq_id && !p.target.label_seq_id && !p.target.label_comp_id;
@@ -156,7 +299,7 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
             plugin.managers.camera.focusLoci(target);
         }
 
-        if (p.kind === 'density' && structure.cell?.parent) {
+        if (p.kind === 'density' && structure?.cell?.parent) {
             const volumeRoot = StateSelection.findTagInSubtree(structure.cell.parent.tree, structure.cell.transform.ref, VolumeStreaming.RootTag);
             if (!volumeRoot) {
                 const params = PD.getDefaultValues(InitVolumeStreaming.definition.params!(structure.obj!, plugin));
@@ -191,4 +334,83 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
             representation
         };
     }
-});
+});
+
+export function createSelectionExpression(entryId: string, range?: Range): SelectionExpression[] {
+    if (range) {
+        const residues: number[] = (range.label_seq_id) ? toRange(range.label_seq_id.beg, range.label_seq_id.end) : [];
+        const test = selectionTest(range.label_asym_id, residues);
+        const label = labelFromProps(entryId, range);
+        return [{
+            expression: MS.struct.generator.atomGroups(test),
+            label: `${label}`,
+            type: 'cartoon',
+            tag: 'polymer'
+        }];
+    } else {
+        return [
+            {
+                expression: Q.polymer.expression,
+                label: `${entryId} - Polymers`,
+                type: 'cartoon',
+                tag: 'polymer'
+            },
+            {
+                expression: Q.ligand.expression,
+                label: `${entryId} - Ligands`,
+                type: 'ball-and-stick',
+                tag: 'ligand'
+            },
+            {
+                expression: Q.ion.expression,
+                label: `${entryId} - Ions`,
+                type: 'ball-and-stick',
+                tag: 'ion'
+            },
+            {
+                expression: Q.branched.expression,
+                label: `${entryId} - Carbohydrates`,
+                type: 'carbohydrate',
+                tag: 'branched-snfg-3d'
+            },
+            {
+                expression: Q.lipid.expression,
+                label: `${entryId} - Lipids`,
+                type: 'ball-and-stick',
+                tag: 'lipid'
+            },
+            {
+                expression: Q.water.expression,
+                label: `${entryId} - Waters`,
+                type: 'ball-and-stick',
+                tag: 'water'
+            }
+        ];
+    }
+}
+
+export const selectionTest = (asymId: string, residues: number[]) => {
+    if (residues.length > 0) {
+        return {
+            'chain-test': MS.core.rel.eq([MS.ammp('label_asym_id'), asymId]),
+            'residue-test': MS.core.set.has([MS.set(...residues), MS.ammp('label_seq_id')])
+        };
+    } else {
+        return { 'chain-test': MS.core.rel.eq([MS.ammp('label_asym_id'), asymId]) };
+    }
+};
+
+export const toRange = (start: number, end?: number) => {
+    if (!end) return [start];
+    const b = start < end ? start : end;
+    const e = start < end ? end : start;
+    return [...Array(e - b + 1)].map((_, i) => b + i);
+};
+
+const labelFromProps = (entryId: string, range: Range) => {
+
+    const residues: number[] = (range.label_seq_id) ? toRange(range.label_seq_id.beg, range.label_seq_id.end) : [];
+    return entryId + (range.label_asym_id ? `.${range.label_asym_id}` : '') +
+        (residues ? `:${residues[0].toString()}` : '') +
+        (residues && residues.length > 1 ? `-${residues[residues.length - 1].toString()}` : '');
+};

+ 73 - 0
src/viewer/helpers/superpose/color.ts

@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Yana Rose
+ */
+
+import { ThemeDataContext } from 'molstar/lib/mol-theme/theme';
+import { ColorTheme } from 'molstar/lib/mol-theme/color';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { Color } from 'molstar/lib/mol-util/color';
+import { StructureElement, StructureProperties, Bond } from 'molstar/lib/mol-model/structure';
+import { Location } from 'molstar/lib/mol-model/location';
+
+export function SuperposeColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
+    const colorLookup = ctx.structure?.inheritedPropertyData.colors;
+    const defaultColorLookup: Map<string, Color> = new Map();
+    for (const [asymId, seqIds] of Object.entries(colorLookup)) {
+        const colorValue = (seqIds as Map<number, Color>).values().next().value;
+        const defaultColor = Color.desaturate(Color.lighten(colorValue, 1.7), 1.2);
+        defaultColorLookup.set(asymId, defaultColor);
+    }
+
+    let DefaultColor = Color(0xCCCCCC);
+    const colorValues: Color[] = Array.from(defaultColorLookup.values());
+    if (colorValues.every( (val, i, arr) => val === arr[0] )) {
+        DefaultColor = colorValues[0];
+    }
+
+    const l = StructureElement.Location.create();
+
+    const _color = (location: StructureElement.Location) => {
+        const asymId = StructureProperties.chain.label_asym_id(location);
+        const seqId = StructureProperties.residue.label_seq_id(location);
+        if (colorLookup?.[asymId]?.has(seqId)) {
+            if (colorLookup[asymId]?.get(seqId) !== undefined) {
+                return colorLookup[asymId]?.get(seqId);
+            }
+        } else if (colorLookup?.[asymId]) {
+            return defaultColorLookup.get(asymId)!;
+        }
+        return DefaultColor;
+    };
+
+    const color = (location: Location): Color => {
+        if (StructureElement.Location.is(location)) {
+            return _color(location);
+        } else if (Bond.isLocation(location)) {
+            l.structure = location.aStructure;
+            l.unit = location.aUnit;
+            l.element = location.aUnit.elements[location.aIndex];
+            return _color(l);
+        }
+        return DefaultColor;
+    };
+
+    return {
+        factory: SuperposeColorTheme,
+        granularity: 'group',
+        color,
+        props,
+        description: 'Superpose coloring',
+    };
+}
+
+export const SuperposeColorThemeProvider: ColorTheme.Provider<{}, 'superpose'> = {
+    name: 'superpose',
+    label: 'Superpose',
+    category: ColorTheme.Category.Misc,
+    factory: SuperposeColorTheme,
+    getParams: () => ({}),
+    defaultValues: PD.getDefaultValues({}),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !!ctx.structure.inheritedPropertyData.colors,
+};

+ 73 - 0
src/viewer/helpers/superpose/flexible-structure.ts

@@ -0,0 +1,73 @@
+import { PluginStateTransform, PluginStateObject as SO } from 'molstar/lib/mol-plugin-state/objects';
+import { RootStructureDefinition } from 'molstar/lib/mol-plugin-state/helpers/root-structure';
+import { PluginContext } from 'molstar/lib/mol-plugin/context';
+import { Task } from 'molstar/lib/mol-task';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { PropsetProps, selectionTest, toRange } from '../preset';
+import { StructureQueryHelper } from 'molstar/lib/mol-plugin-state/helpers/structure-query';
+import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
+import { StructureSelection, Structure } from 'molstar/lib/mol-model/structure';
+
+export { FlexibleStructureFromModel as FlexibleStructureFromModel };
+type FlexibleStructureFromModel = typeof FlexibleStructureFromModel
+const FlexibleStructureFromModel = PluginStateTransform.BuiltIn({
+    name: 'flexible-structure-from-model',
+    display: { name: 'Flexible Structure', description: 'Create a molecular structure from independently transformed substructures.' },
+    from: SO.Molecule.Model,
+    to: SO.Molecule.Structure,
+    isDecorator: true,
+    params(a) {
+        return {
+            selection: PD.Value<PropsetProps['selection']>([])
+        };
+    }
+})({
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Build Flexible Structure', async ctx => {
+            const base = await RootStructureDefinition.create(plugin, ctx, a.data);
+            const { selection } = params;
+            if (!selection?.length) return base;
+
+            const selectChains: string[] = [];
+            const selectBlocks: Structure[][] = [];
+            for (const p of selection) {
+                if (!selectChains.includes(p.label_asym_id)) {
+                    selectChains.push(p.label_asym_id);
+                    selectBlocks.push([]);
+                }
+                const residues: number[] = (p.label_seq_id) ? toRange(p.label_seq_id.beg, p.label_seq_id.end) : [];
+                const test = selectionTest(p.label_asym_id, residues);
+                const expression = MS.struct.generator.atomGroups(test);
+                const { selection: sele } = StructureQueryHelper.createAndRun(base.data, expression);
+                const s = StructureSelection.unionStructure(sele);
+                if (!p.matrix) {
+                    selectBlocks[selectChains.indexOf(p.label_asym_id)].push(s);
+                } else {
+                    const ts = Structure.transform(s, p.matrix);
+                    selectBlocks[selectChains.indexOf(p.label_asym_id)].push(ts);
+                }
+            }
+
+            const builder = Structure.Builder({ label: base.data.label });
+            for (const blocks of selectBlocks) {
+                if (blocks.length === 1) {
+                    const u = blocks[0].units[0];
+                    builder.addUnit(u.kind, u.model, u.conformation.operator, u.elements, u.traits, u.invariantId);
+                } else {
+                    builder.beginChainGroup();
+                    for (const b of blocks) {
+                        const u = b.units[0];
+                        builder.addUnit(u.kind, u.model, u.conformation.operator, u.elements, u.traits, u.invariantId);
+                    }
+                    builder.endChainGroup();
+                }
+            }
+
+            const blockStructure = builder.getStructure();
+            return new SO.Molecule.Structure(blockStructure, { label: base.data.label });
+        });
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
+    }
+});

+ 7 - 1
src/viewer/index.html

@@ -51,10 +51,13 @@
             var url = getQueryParam('url')
             var _props = getQueryParam('props')
             var props = _props && JSON.parse(_props)
-
+            var _loadPdbIds = getQueryParam('loadPdbIds')
+            var loadPdbIds = _loadPdbIds && JSON.parse(_loadPdbIds)
+            
             // create an instance of the plugin
             var viewer = new rcsbMolstar.Viewer('viewer', {
                 showImportControls: !pdbId,
+                showExportControls: true,
                 showSessionControls: !pdbId,
                 layoutShowLog: !pdbId,
                 layoutShowControls: !isEmbedded,
@@ -62,6 +65,7 @@
 
             // load pdbId or url
             if (pdbId) viewer.loadPdbId(pdbId, props)
+            else if (loadPdbIds) viewer.loadPdbIds(loadPdbIds);
             else if (url) viewer.loadUrl(url, props)
         </script>
         <div id="menu">
@@ -80,8 +84,10 @@
 
             Superposed
             <button style="padding: 3px;" onclick="superposed()">3PQR | 1U19</button>
+
         </div>
         <script>
+
             function loadExample(index) {
                 var e = examples[index]
                 viewer.loadPdbId(e.id, e.props)

+ 41 - 2
src/viewer/index.ts

@@ -27,6 +27,8 @@ import { PluginState } from 'molstar/lib/mol-plugin/state';
 import { BuiltInTrajectoryFormat } from 'molstar/lib/mol-plugin-state/formats/trajectory';
 import { ObjectKeys } from 'molstar/lib/mol-util/type-helpers';
 import { PluginLayoutControlsDisplay } from 'molstar/lib/mol-plugin/layout';
+import { SuperposeColorThemeProvider } from './helpers/superpose/color';
+import { encodeStructureData, downloadAsZipFile } from './helpers/export';
 import {Structure} from 'molstar/lib/mol-model/structure/structure';
 import {Script} from 'molstar/lib/mol-script/script';
 import {MolScriptBuilder} from 'molstar/lib/mol-script/language/builder';
@@ -53,7 +55,10 @@ const Extensions = {
 
 const DefaultViewerProps = {
     showImportControls: false,
+    showExportControls: false,
     showSessionControls: false,
+    showStructureSourceControls: true,
+    showSuperpositionControls: true,
     modelUrlProviders: [
         (pdbId: string) => ({
             url: `//models.rcsb.org/${pdbId.toLowerCase()}.bcif`,
@@ -83,6 +88,7 @@ const DefaultViewerProps = {
 };
 export type ViewerProps = typeof DefaultViewerProps
 
+
 export class Viewer {
     private readonly plugin: PluginContext;
     private readonly modelUrlProviders: ModelUrlProvider[];
@@ -114,7 +120,7 @@ export class Viewer {
                     ...DefaultPluginSpec.layout && DefaultPluginSpec.layout.controls,
                     top: o.layoutShowSequence ? undefined : 'none',
                     bottom: o.layoutShowLog ? undefined : 'none',
-                    left: 'none',
+                    // left: 'none',
                     right: ControlsWrapper,
                 }
             },
@@ -137,7 +143,10 @@ export class Viewer {
 
         (this.plugin.customState as ViewerState) = {
             showImportControls: o.showImportControls,
+            showExportControls: o.showExportControls,
             showSessionControls: o.showSessionControls,
+            showStructureSourceControls: o.showStructureSourceControls,
+            showSuperpositionControls: o.showSuperpositionControls,
             modelLoader: new ModelLoader(this.plugin),
             collapsed: new BehaviorSubject<CollapsedState>({
                 selection: true,
@@ -147,11 +156,12 @@ export class Viewer {
                 component: false,
                 volume: true,
                 custom: true,
-            }),
+            })
         };
 
         this.plugin.init();
         ReactDOM.render(React.createElement(Plugin, { plugin: this.plugin }), target);
+
         // TODO Check why this.plugin.canvas3d can be null
         // this.plugin.canvas3d can be null. The value is not assigned until React Plugin component is mounted
         // Next wait Promise guarantees that its value is defined
@@ -170,6 +180,8 @@ export class Viewer {
         wait.then(result=>{
             const renderer = this.plugin.canvas3d!.props.renderer;
             PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: o.backgroundColor } } });
+            this.plugin.representation.structure.themes.colorThemeRegistry.add(SuperposeColorThemeProvider);
+        // this.plugin.builders.structure.representation.registerPreset(RcsbSuperpositionRepresentationPreset);
         });
         if (o.showWelcomeToast) {
             PluginCommands.Toast.Show(this.plugin, {
@@ -179,6 +191,24 @@ export class Viewer {
                 timeoutMs: 5000
             });
         }
+        this.prevExpanded = this.plugin.layout.state.isExpanded;
+        this.plugin.layout.events.updated.subscribe(() => this.toggleControls());
+    }
+
+    private prevExpanded: boolean;
+
+    private toggleControls(): void {
+
+        const currExpanded = this.plugin.layout.state.isExpanded;
+        const expanedChanged = (this.prevExpanded !== currExpanded);
+        if (!expanedChanged) return;
+
+        if (currExpanded && !this.plugin.layout.state.showControls) {
+            this.plugin.layout.setProps({showControls: true});
+        } else if (!currExpanded && this.plugin.layout.state.showControls) {
+            this.plugin.layout.setProps({showControls: false});
+        }
+        this.prevExpanded = this.plugin.layout.state.isExpanded;
     }
 
     //
@@ -223,6 +253,15 @@ export class Viewer {
         return this.customState.modelLoader.parse({ data, format, isBinary }, props, matrix);
     }
 
+    handleResize() {
+        this.plugin.layout.events.updated.next();
+    }
+
+    exportLoadedStructures() {
+        const content = encodeStructureData(this.plugin);
+        downloadAsZipFile(content);
+    }
+
     pluginCall(f: (plugin: PluginContext) => void){
         f(this.plugin);
     }

+ 4 - 0
src/viewer/types.ts

@@ -41,9 +41,13 @@ export type CollapsedState = {
     volume: boolean
     custom: boolean
 }
+
 export interface ViewerState {
     showImportControls: boolean
+    showExportControls: boolean
     showSessionControls: boolean
+    showStructureSourceControls: boolean
+    showSuperpositionControls: boolean
     modelLoader: ModelLoader
     collapsed: BehaviorSubject<CollapsedState>
 }

+ 4 - 3
src/viewer/ui/controls.tsx

@@ -9,6 +9,7 @@ import { PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
 import { ViewerState } from '../types';
 import { CustomStructureControls } from 'molstar/lib/mol-plugin-ui/controls';
 import { ImportControls } from './import';
+import { ExportControls } from './export';
 import { StructureSourceControls } from 'molstar/lib/mol-plugin-ui/structure/source';
 import { StructureMeasurementsControls } from 'molstar/lib/mol-plugin-ui/structure/measurements';
 import { StructureSuperpositionControls } from 'molstar/lib/mol-plugin-ui/structure/superposition';
@@ -29,13 +30,12 @@ export class StructureTools extends PluginUIComponent {
     render() {
         const collapsed = this.customState.collapsed.value;
         return <>
-            <StructureSourceControls />
+            {this.customState.showStructureSourceControls && <StructureSourceControls />}
             <StructureMeasurementsControls initiallyCollapsed={collapsed.measurements} />
-            <StructureSuperpositionControls initiallyCollapsed={collapsed.superposition} />
             <StrucmotifSubmitControls initiallyCollapsed={collapsed.strucmotifSubmit} />
+            {this.customState.showSuperpositionControls && <StructureSuperpositionControls initiallyCollapsed={collapsed.superposition} />}
             <StructureComponentControls initiallyCollapsed={collapsed.component} />
             <VolumeStreamingControls header='Density' initiallyCollapsed={collapsed.volume} />
-
             <CustomStructureControls initiallyCollapsed={collapsed.custom} />
         </>;
     }
@@ -45,6 +45,7 @@ export class ControlsWrapper extends PluginUIComponent {
     render() {
         return <div className='msp-scrollable-container'>
             {ViewerState(this.plugin).showImportControls && <ImportControls />}
+            {ViewerState(this.plugin).showExportControls && <ExportControls />}
             {ViewerState(this.plugin).showSessionControls && <SessionControls />}
             <StructureTools />
         </div>;

+ 48 - 0
src/viewer/ui/export.tsx

@@ -0,0 +1,48 @@
+import * as React from 'react';
+import { CollapsableControls, CollapsableState, PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
+import { Button } from 'molstar/lib/mol-plugin-ui/controls/common';
+import { GetAppSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
+import { encodeStructureData, downloadAsZipFile } from '../helpers/export';
+
+export class ExportControls extends CollapsableControls {
+
+    protected defaultState(): CollapsableState {
+        return {
+            header: 'Export',
+            isCollapsed: true,
+            brand: { accent:  'gray' as const, svg: ExportOutlinedSvg }
+        };
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => {
+            this.setState({ isHidden: sel.structures.length === 0 });
+        });
+    }
+
+    protected renderControls(): JSX.Element | null {
+        return <div className={'msp-control-offset'} style={{ paddingTop: '1px' }}>
+            <CoordinatesExportControls />
+        </div>;
+    }
+}
+
+class CoordinatesExportControls extends PluginUIComponent {
+    download = () => {
+        const content = encodeStructureData(this.plugin);
+        downloadAsZipFile(content);
+    }
+
+    render() {
+        return <>
+            <div className='msp-flex-row'>
+                <Button icon={GetAppSvg} onClick={this.download} title='Save structures as mmCIF files'>
+                    Structures
+                </Button>
+            </div>
+        </>;
+    }
+}
+
+function ExportOutlinedSvg() { return _ExportOutlined; }
+const _ExportOutlined = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M19 12v7H5v-7H3v9h18v-9h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2v9.67z" /></svg>;

+ 2 - 2
webpack.config.js

@@ -47,7 +47,7 @@ const sharedConfig = {
         aggregateTimeout: 750
     },
     devtool: ''
-}
+};
 
 module.exports = [
     {
@@ -67,4 +67,4 @@ module.exports = [
         },
         ...sharedConfig
     }
-]
+];

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