Преглед на файлове

Merge pull request #334 from molstar/export-extension

Model export extension & related improvements
David Sehnal преди 3 години
родител
ревизия
5e25716c98

+ 9 - 1
CHANGELOG.md

@@ -6,6 +6,14 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Assembly handling tweaks:
+    - Do not include suffix for "identity assembly operators"
+    - Do not include assembly-related categories to export if the structure was composed from an assembly
+    - Special case for ``structAsymMap`` if Mol* asym id operator mapping is present
+- Support for opening ZIP files with multiple entries
+- Add Model Export extension
+- Bugfix: Automatically treat empty string as "non-present" value in BinaryCIF writer.
+
 ## [v3.0.0-dev.10] - 2022-01-17
 
 - Fix ``getOperatorsForIndex``
@@ -19,7 +27,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add "camera rock" state animation
 - Add support for custom colors to "molecule-type" theme
 - [Breaking] Add style parameter to "illustrative" color theme
-    - Defaults to "entity-id" style instad of "chain-id"
+    - Defaults to "entity-id" style instead of "chain-id"
 - Add "illustrative" representation preset
 
 ## [v3.0.0-dev.9] - 2022-01-09

+ 2 - 0
src/apps/viewer/app.ts

@@ -13,6 +13,7 @@ import { GeometryExport } from '../../extensions/geo-export';
 import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior';
 import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
 import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
+import { ModelExport } from '../../extensions/model-export';
 import { Mp4Export } from '../../extensions/mp4-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
@@ -57,6 +58,7 @@ const Extensions = {
     'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
     'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
     'g3d': PluginSpec.Behavior(G3DFormat),
+    'model-export': PluginSpec.Behavior(ModelExport),
     'mp4-export': PluginSpec.Behavior(Mp4Export),
     'geo-export': PluginSpec.Behavior(GeometryExport),
     'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),

+ 1 - 1
src/apps/viewer/index.ts

@@ -9,4 +9,4 @@ import './embedded.html';
 import './favicon.ico';
 import './index.html';
 require('mol-plugin-ui/skin/light.scss');
-export * from './app';
+export * from './app';

+ 83 - 0
src/extensions/model-export/export.ts

@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
+import { to_mmCIF } from '../../mol-model/structure';
+import { PluginContext } from '../../mol-plugin/context';
+import { Task } from '../../mol-task';
+import { getFormattedTime } from '../../mol-util/date';
+import { download } from '../../mol-util/download';
+import { zip } from '../../mol-util/zip/zip';
+
+export async function exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
+    try {
+        await plugin.runTask(_exportHierarchy(plugin, options), { useOverlay: true });
+    } catch (e) {
+        console.error(e);
+        plugin.log.error(`Model export failed. See console for details.`);
+    }
+}
+
+function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
+    return Task.create('Export', async ctx => {
+        await ctx.update({ message: 'Exporting...', isIndeterminate: true, canAbort: false });
+
+        const format = options?.format ?? 'cif';
+        const { structures } = plugin.managers.structure.hierarchy.current;
+
+        const files: [name: string, data: string | Uint8Array][] = [];
+        const entryMap = new Map<string, number>();
+
+        for (const _s of structures) {
+            const s = _s.transform?.cell.obj?.data ?? _s.cell.obj?.data;
+            if (!s) continue;
+            if (s.models.length > 1) {
+                plugin.log.warn(`[Export] Skipping ${_s.cell.obj?.label}: Multimodel exports not supported.`);
+                continue;
+            }
+
+            const name = entryMap.has(s.model.entryId)
+                ? `${s.model.entryId}_${entryMap.get(s.model.entryId)! + 1}.${format}`
+                : `${s.model.entryId}.${format}`;
+            entryMap.set(s.model.entryId, (entryMap.get(s.model.entryId) ?? 0) + 1);
+
+            await ctx.update({ message: `Exporting ${s.model.entryId}...`, isIndeterminate: true, canAbort: false });
+            if (s.elementCount > 100000) {
+                // Give UI chance to update, only needed for larger structures.
+                await new Promise(res => setTimeout(res, 50));
+            }
+
+            try {
+                files.push([name, to_mmCIF(s.model.entryId, s, format === 'bcif', { copyAllCategories: true })]);
+            } catch (e) {
+                if (format === 'cif' && s.elementCount > 2000000) {
+                    plugin.log.warn(`[Export] The structure might be too big to be exported as Text CIF, consider using the BinaryCIF format instead.`);
+                }
+                throw e;
+            }
+        }
+
+        if (files.length === 1) {
+            download(new Blob([files[0][1]]), files[0][0]);
+        } else if (files.length > 1) {
+            const zipData: Record<string, Uint8Array> = {};
+            for (const [fn, data] of files) {
+                if (data instanceof Uint8Array) {
+                    zipData[fn] = data;
+                } else {
+                    const bytes = new Uint8Array(utf8ByteCount(data));
+                    utf8Write(bytes, 0, data);
+                    zipData[fn] = bytes;
+                }
+            }
+            await ctx.update({ message: `Compressing Data...`, isIndeterminate: true, canAbort: false });
+            const buffer = await zip(ctx, zipData);
+            download(new Blob([new Uint8Array(buffer, 0, buffer.byteLength)]), `structures_${getFormattedTime()}.zip`);
+        }
+
+        plugin.log.info(`[Export] Done.`);
+    });
+}

+ 30 - 0
src/extensions/model-export/index.ts

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
+import { ModelExportUI } from './ui';
+
+export const ModelExport = PluginBehavior.create<{}>({
+    name: 'extension-model-export',
+    category: 'misc',
+    display: {
+        name: 'Model Export'
+    },
+    ctor: class extends PluginBehavior.Handler<{}> {
+        register(): void {
+            this.ctx.customStructureControls.set('model-export', ModelExportUI as any);
+        }
+
+        update() {
+            return false;
+        }
+
+        unregister() {
+            this.ctx.customStructureControls.delete('model-export');
+        }
+    },
+    params: () => ({})
+});

+ 69 - 0
src/extensions/model-export/ui.tsx

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { useState } from 'react';
+import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
+import { Button } from '../../mol-plugin-ui/controls/common';
+import { GetAppSvg } from '../../mol-plugin-ui/controls/icons';
+import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
+import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
+import { PluginContext } from '../../mol-plugin/context';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { exportHierarchy } from './export';
+
+export class ModelExportUI extends CollapsableControls<{}, {}> {
+    protected defaultState(): CollapsableState {
+        return {
+            header: 'Export Models',
+            isCollapsed: true,
+            brand: { accent: 'cyan', svg: GetAppSvg }
+        };
+    }
+    protected renderControls(): JSX.Element | null {
+        return <ExportControls plugin={this.plugin} />;
+    }
+}
+
+const Params = {
+    format: PD.Select<'cif' | 'bcif'>('cif', [['cif', 'mmCIF'], ['bcif', 'Binary mmCIF']])
+};
+const DefaultParams = PD.getDefaultValues(Params);
+
+function ExportControls({ plugin }: { plugin: PluginContext }) {
+    const [params, setParams] = useState(DefaultParams);
+    const [exporting, setExporting] = useState(false);
+    useBehavior(plugin.managers.structure.hierarchy.behaviors.selection); // triggers UI update
+    const isBusy = useBehavior(plugin.behaviors.state.isBusy);
+    const hierarchy = plugin.managers.structure.hierarchy.current;
+
+    let label: string = 'Nothing to Export';
+    if (hierarchy.structures.length === 1) {
+        label = 'Export';
+    } if (hierarchy.structures.length > 1) {
+        label = 'Export (as ZIP)';
+    }
+
+    const onExport = async () => {
+        setExporting(true);
+        try {
+            await exportHierarchy(plugin, { format: params.format });
+        } finally {
+            setExporting(false);
+        }
+    };
+
+    return <>
+        <ParameterControls params={Params} values={params} onChangeValues={setParams} isDisabled={isBusy || exporting} />
+        <Button
+            onClick={onExport}
+            style={{ marginTop: 1 }}
+            disabled={isBusy || hierarchy.structures.length === 0 || exporting}
+            commit={hierarchy.structures.length ? 'on' : 'off'}
+        >
+            {label}
+        </Button>
+    </>;
+}

+ 8 - 2
src/mol-io/writer/cif/encoder/binary.ts

@@ -191,8 +191,14 @@ function getFieldData(field: Field<any, any>, arrayCtor: ArrayCtor<string | numb
                     array[offset] = '';
                 allPresent = false;
             } else {
-                mask[offset] = Column.ValueKind.Present;
-                array[offset] = getter(key, d, offset);
+                const value = getter(key, d, offset);
+                if (typeof value === 'string' && !value) {
+                    mask[offset] = Column.ValueKind.NotPresent;
+                    allPresent = false;
+                } else {
+                    mask[offset] = Column.ValueKind.Present;
+                }
+                array[offset] = value;
             }
             offset++;
         }

+ 11 - 5
src/mol-math/geometry/symmetry-operator.ts

@@ -52,24 +52,30 @@ namespace SymmetryOperator {
     export const RotationTranslationEpsilon = 0.005;
 
     export type CreateInfo = { assembly?: SymmetryOperator['assembly'], ncsId?: number, hkl?: Vec3, spgrOp?: number }
-    export function create(name: string, matrix: Mat4, info?: CreateInfo): SymmetryOperator {
+    export function create(name: string, matrix: Mat4, info?: CreateInfo | SymmetryOperator): SymmetryOperator {
         let { assembly, ncsId, hkl, spgrOp } = info || { };
         const _hkl = hkl ? Vec3.clone(hkl) : Vec3();
         spgrOp = defaults(spgrOp, -1);
         ncsId = ncsId || -1;
-        const suffix = getSuffix(info);
-        if (Mat4.isIdentity(matrix)) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl, spgrOp, ncsId, suffix };
+        const isIdentity = Mat4.isIdentity(matrix);
+        const suffix = getSuffix(info, isIdentity);
+        if (isIdentity) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl, spgrOp, ncsId, suffix };
         if (!Mat4.isRotationAndTranslation(matrix, RotationTranslationEpsilon)) {
             console.warn(`Symmetry operator (${name}) should be a composition of rotation and translation.`);
         }
         return { name, assembly, matrix, inverse: Mat4.invert(Mat4(), matrix), isIdentity: false, hkl: _hkl, spgrOp, ncsId, suffix };
     }
 
-    function getSuffix(info?: CreateInfo) {
+    function isSymmetryOperator(x: any): x is SymmetryOperator {
+        return !!x && !!x.matrix && !!x.inverse && typeof x.name === 'string';
+    }
+
+    function getSuffix(info?: CreateInfo | SymmetryOperator, isIdentity?: boolean) {
         if (!info) return '';
 
         if (info.assembly) {
-            return `_${info.assembly.operId}`;
+            if (isSymmetryOperator(info)) return info.suffix;
+            return isIdentity ? '' : `_${info.assembly.operId}`;
         }
 
         if (typeof info.spgrOp !== 'undefined' && typeof info.hkl !== 'undefined' && info.spgrOp !== -1) {

+ 9 - 9
src/mol-model-formats/structure/basic/atomic.ts

@@ -7,21 +7,21 @@
 
 import { Column, Table } from '../../../mol-data/db';
 import { Interval, Segmentation } from '../../../mol-data/int';
-import { UUID } from '../../../mol-util/uuid';
-import { ElementIndex, ChainIndex } from '../../../mol-model/structure';
+import { toDatabase } from '../../../mol-io/reader/cif/schema';
+import { SymmetryOperator } from '../../../mol-math/geometry';
+import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
+import { ChainIndex, ElementIndex } from '../../../mol-model/structure';
+import { AtomSiteOperatorMappingSchema } from '../../../mol-model/structure/export/categories/atom_site_operator_mapping';
 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 { getAtomicDerivedData } from '../../../mol-model/structure/model/properties/utils/atomic-derived';
-import { AtomSite } from './schema';
+import { getAtomicIndex } from '../../../mol-model/structure/model/properties/utils/atomic-index';
+import { ElementSymbol } from '../../../mol-model/structure/model/types';
+import { UUID } from '../../../mol-util/uuid';
 import { ModelFormat } from '../../format';
-import { SymmetryOperator } from '../../../mol-math/geometry';
 import { MmcifFormat } from '../mmcif';
-import { AtomSiteOperatorMappingSchema } from '../../../mol-model/structure/export/categories/atom_site_operator_mapping';
-import { toDatabase } from '../../../mol-io/reader/cif/schema';
-import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
+import { AtomSite } from './schema';
 
 function findHierarchyOffsets(atom_site: AtomSite) {
     if (atom_site._rowCount === 0) return { residues: [], chains: [] };

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

@@ -30,7 +30,7 @@ export interface CoarseData {
 
 export const EmptyCoarse = { hierarchy: CoarseHierarchy.Empty, conformation: void 0 as any };
 
-export function getCoarse(data: CoarseData, properties: Model['properties']): { hierarchy: CoarseHierarchy, conformation: CoarseConformation } {
+export function getCoarse(data: CoarseData, chemicalComponentMap: Model['properties']['chemicalComponentMap']): { hierarchy: CoarseHierarchy, conformation: CoarseConformation } {
     const { ihm_sphere_obj_site, ihm_gaussian_obj_site } = data;
 
     if (ihm_sphere_obj_site._rowCount === 0 && ihm_gaussian_obj_site._rowCount === 0) return EmptyCoarse;
@@ -38,12 +38,12 @@ export function getCoarse(data: CoarseData, properties: Model['properties']): {
     const sphereData = getData(ihm_sphere_obj_site);
     const sphereConformation = getSphereConformation(ihm_sphere_obj_site);
     const sphereKeys = getCoarseKeys(sphereData, data.entities);
-    const sphereRanges = getCoarseRanges(sphereData, properties.chemicalComponentMap);
+    const sphereRanges = getCoarseRanges(sphereData, chemicalComponentMap);
 
     const gaussianData = getData(ihm_gaussian_obj_site);
     const gaussianConformation = getGaussianConformation(ihm_gaussian_obj_site);
     const gaussianKeys = getCoarseKeys(gaussianData, data.entities);
-    const gaussianRanges = getCoarseRanges(gaussianData, properties.chemicalComponentMap);
+    const gaussianRanges = getCoarseRanges(gaussianData, chemicalComponentMap);
 
     return {
         hierarchy: {

+ 21 - 17
src/mol-model-formats/structure/basic/entities.ts

@@ -11,7 +11,7 @@ import { getEntityType, getEntitySubtype } from '../../../mol-model/structure/mo
 import { ElementIndex, EntityIndex, Model } from '../../../mol-model/structure/model';
 import { BasicData, BasicSchema, Entity } from './schema';
 
-export function getEntities(data: BasicData, properties: Model['properties']): Entities {
+export function getEntityData(data: BasicData): Entities {
     let entityData: Entity;
 
     if (!data.entity.id.isDefined) {
@@ -121,28 +121,32 @@ export function getEntities(data: BasicData, properties: Model['properties']): E
 
     const subtypeColumn = Column.ofArray({ array: subtypes, schema: EntitySubtype });
 
-    //
+    return {
+        data: entityData,
+        subtype: subtypeColumn,
+        getEntityIndex
+    };
+}
 
-    const prdIds: string[] = new Array(entityData._rowCount);
-    prdIds.fill('');
+export function getEntitiesWithPRD(data: BasicData, entities: Entities, structAsymMap: Model['properties']['structAsymMap']): Entities {
+    if (!data.pdbx_molecule || !data.pdbx_molecule.prd_id.isDefined) {
+        return entities;
+    }
 
-    if (data.pdbx_molecule && data.pdbx_molecule.prd_id.isDefined) {
-        const { asym_id, prd_id, _rowCount } = data.pdbx_molecule;
-        for (let i = 0; i < _rowCount; ++i) {
-            const asymId = asym_id.value(i);
-            const entityId = properties.structAsymMap.get(asymId)?.entity_id;
-            if (entityId !== undefined) {
-                prdIds[getEntityIndex(entityId)] = prd_id.value(i);
-            }
+    const prdIds: string[] = new Array(entities.data._rowCount);
+    prdIds.fill('');
+    const { asym_id, prd_id, _rowCount } = data.pdbx_molecule;
+    for (let i = 0; i < _rowCount; ++i) {
+        const asymId = asym_id.value(i);
+        const entityId = structAsymMap.get(asymId)?.entity_id;
+        if (entityId !== undefined) {
+            prdIds[entities.getEntityIndex(entityId)] = prd_id.value(i);
         }
     }
-
     const prdIdColumn = Column.ofArray({ array: prdIds, schema: Column.Schema.str });
 
     return {
-        data: entityData,
-        subtype: subtypeColumn,
-        prd_id: prdIdColumn,
-        getEntityIndex
+        ...entities,
+        prd_id: prdIdColumn
     };
 }

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

@@ -18,13 +18,13 @@ import { sortAtomSite } from './sort';
 import { ModelFormat } from '../../format';
 import { getAtomicRanges } from '../../../mol-model/structure/model/properties/utils/atomic-ranges';
 import { AtomSite, BasicData } from './schema';
-import { getProperties } from './properties';
-import { getEntities } from './entities';
+import { getChemicalComponentMap, getMissingResidues, getSaccharideComponentMap, getStructAsymMap } from './properties';
+import { getEntitiesWithPRD, getEntityData } from './entities';
 import { getModelGroupName } from './util';
 import { ArrayTrajectory } from '../../../mol-model/structure/trajectory';
 
 export async function createModels(data: BasicData, format: ModelFormat, ctx: RuntimeContext) {
-    const properties = getProperties(data);
+    const properties = getCommonProperties(data, format);
     const models = data.ihm_model_list._rowCount > 0
         ? await readIntegrative(ctx, data, properties, format)
         : await readStandard(ctx, data, properties, format);
@@ -36,9 +36,18 @@ export async function createModels(data: BasicData, format: ModelFormat, ctx: Ru
     return new ArrayTrajectory(models);
 }
 
-/** Standard atomic model */
-function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, properties: Model['properties'], format: ModelFormat, previous?: Model): Model {
+type CommonProperties = Omit<Model['properties'], 'structAsymMap'>
 
+function getCommonProperties(data: BasicData, format: ModelFormat): CommonProperties {
+    return {
+        missingResidues: getMissingResidues(data),
+        chemicalComponentMap: getChemicalComponentMap(data),
+        saccharideComponentMap: getSaccharideComponentMap(data)
+    };
+}
+
+/** Standard atomic model */
+function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, properties: CommonProperties, format: ModelFormat, previous?: Model): Model {
     const atomic = getAtomicHierarchyAndConformation(atom_site, sourceIndex, entities, properties.chemicalComponentMap, format, previous);
     const modelNum = atom_site.pdbx_PDB_model_num.value(0);
     if (previous && atomic.sameAsPrevious) {
@@ -54,6 +63,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
     const coarse = EmptyCoarse;
     const sequence = getSequence(data, entities, atomic.hierarchy, coarse.hierarchy);
     const atomicRanges = getAtomicRanges(atomic.hierarchy, entities, atomic.conformation, sequence);
+    const structAsymMap = getStructAsymMap(atomic.hierarchy);
 
     const entry = data.entry.id.valueKind(0) === Column.ValueKind.Present
         ? data.entry.id.value(0) : format.name;
@@ -70,7 +80,7 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
         sourceData: format,
         modelNum,
         parent: undefined,
-        entities,
+        entities: getEntitiesWithPRD(data, entities, structAsymMap),
         sequence,
         atomicHierarchy: atomic.hierarchy,
         atomicConformation: atomic.conformation,
@@ -78,7 +88,10 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
         atomicChainOperatorMappinng: atomic.chainOperatorMapping,
         coarseHierarchy: coarse.hierarchy,
         coarseConformation: coarse.conformation,
-        properties,
+        properties: {
+            ...properties,
+            structAsymMap
+        },
         customProperties: new CustomProperties(),
         _staticPropertyData: Object.create(null),
         _dynamicPropertyData: Object.create(null)
@@ -86,9 +99,9 @@ function createStandardModel(data: BasicData, atom_site: AtomSite, sourceIndex:
 }
 
 /** Integrative model with atomic/coarse parts */
-function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: Model['properties'], format: ModelFormat): Model {
+function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: CommonProperties, format: ModelFormat): Model {
     const atomic = getAtomicHierarchyAndConformation(ihm.atom_site, ihm.atom_site_sourceIndex, ihm.entities, properties.chemicalComponentMap, format);
-    const coarse = getCoarse(ihm, properties);
+    const coarse = getCoarse(ihm, properties.chemicalComponentMap);
     const sequence = getSequence(data, ihm.entities, atomic.hierarchy, coarse.hierarchy);
     const atomicRanges = getAtomicRanges(atomic.hierarchy, ihm.entities, atomic.conformation, sequence);
 
@@ -101,6 +114,9 @@ function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: Mo
     if (ihm.model_name) label.push(ihm.model_name);
     if (ihm.model_group_name) label.push(ihm.model_group_name);
 
+    // TODO: should this contain anything from coarse hierarchy?
+    const structAsymMap = getStructAsymMap(atomic.hierarchy);
+
     return {
         id: UUID.create22(),
         entryId: entry,
@@ -109,7 +125,7 @@ function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: Mo
         sourceData: format,
         modelNum: ihm.model_id,
         parent: undefined,
-        entities: ihm.entities,
+        entities: getEntitiesWithPRD(data, ihm.entities, structAsymMap),
         sequence,
         atomicHierarchy: atomic.hierarchy,
         atomicConformation: atomic.conformation,
@@ -117,7 +133,10 @@ function createIntegrativeModel(data: BasicData, ihm: CoarseData, properties: Mo
         atomicChainOperatorMappinng: atomic.chainOperatorMapping,
         coarseHierarchy: coarse.hierarchy,
         coarseConformation: coarse.conformation,
-        properties,
+        properties: {
+            ...properties,
+            structAsymMap
+        },
         customProperties: new CustomProperties(),
         _staticPropertyData: Object.create(null),
         _dynamicPropertyData: Object.create(null)
@@ -132,12 +151,12 @@ function findModelEnd(num: Column<number>, startIndex: number) {
     return endIndex;
 }
 
-async function readStandard(ctx: RuntimeContext, data: BasicData, properties: Model['properties'], format: ModelFormat) {
+async function readStandard(ctx: RuntimeContext, data: BasicData, properties: CommonProperties, format: ModelFormat) {
     const models: Model[] = [];
 
     if (data.atom_site) {
         const atomCount = data.atom_site.id.rowCount;
-        const entities = getEntities(data, properties);
+        const entities = getEntityData(data);
 
         let modelStart = 0;
         while (modelStart < atomCount) {
@@ -170,8 +189,8 @@ function splitTable<T extends Table<any>>(table: T, col: Column<number>) {
 
 
 
-async function readIntegrative(ctx: RuntimeContext, data: BasicData, properties: Model['properties'], format: ModelFormat) {
-    const entities = getEntities(data, properties);
+async function readIntegrative(ctx: RuntimeContext, data: BasicData, properties: CommonProperties, format: ModelFormat) {
+    const entities = getEntityData(data);
     // when `atom_site.ihm_model_id` is undefined fall back to `atom_site.pdbx_PDB_model_num`
     const atom_sites_modelColumn = data.atom_site.ihm_model_id.isDefined
         ? data.atom_site.ihm_model_id : data.atom_site.pdbx_PDB_model_num;
@@ -206,7 +225,7 @@ async function readIntegrative(ctx: RuntimeContext, data: BasicData, properties:
                 model_id: id,
                 model_name: model_name.value(i),
                 model_group_name: getModelGroupName(id, data),
-                entities: entities,
+                entities,
                 atom_site,
                 atom_site_sourceIndex,
                 ihm_sphere_obj_site: sphere_sites.has(id) ? sphere_sites.get(id)!.table : Table.window(data.ihm_sphere_obj_site, data.ihm_sphere_obj_site._schema, 0, 0),

+ 12 - 38
src/mol-model-formats/structure/basic/properties.ts

@@ -5,15 +5,16 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import { Table } from '../../../mol-data/db';
 import { Model } from '../../../mol-model/structure/model/model';
+import { AtomicHierarchy } from '../../../mol-model/structure/model/properties/atomic';
 import { ChemicalComponent, MissingResidue, StructAsym } from '../../../mol-model/structure/model/properties/common';
-import { getMoleculeType, MoleculeType, getDefaultChemicalComponent } from '../../../mol-model/structure/model/types';
-import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from '../../../mol-model/structure/structure/carbohydrates/constants';
+import { getDefaultChemicalComponent, getMoleculeType, MoleculeType } from '../../../mol-model/structure/model/types';
+import { SaccharideCompIdMap, SaccharideComponent, SaccharideComponentMap, SaccharidesSnfgMap, UnknownSaccharideComponent } from '../../../mol-model/structure/structure/carbohydrates/constants';
 import { memoize1 } from '../../../mol-util/memoize';
 import { BasicData } from './schema';
-import { Table } from '../../../mol-data/db';
 
-function getMissingResidues(data: BasicData): Model['properties']['missingResidues'] {
+export function getMissingResidues(data: BasicData): Model['properties']['missingResidues'] {
     const map = new Map<string, MissingResidue>();
     const getKey = (model_num: number, asym_id: string, seq_id: number) => {
         return `${model_num}|${asym_id}|${seq_id}`;
@@ -36,7 +37,7 @@ function getMissingResidues(data: BasicData): Model['properties']['missingResidu
     };
 }
 
-function getChemicalComponentMap(data: BasicData): Model['properties']['chemicalComponentMap'] {
+export function getChemicalComponentMap(data: BasicData): Model['properties']['chemicalComponentMap'] {
     const map = new Map<string, ChemicalComponent>();
 
     if (data.chem_comp._rowCount > 0) {
@@ -53,7 +54,7 @@ function getChemicalComponentMap(data: BasicData): Model['properties']['chemical
     return map;
 }
 
-function getSaccharideComponentMap(data: BasicData): SaccharideComponentMap {
+export function getSaccharideComponentMap(data: BasicData): SaccharideComponentMap {
     const map = new Map<string, SaccharideComponent>();
 
     if (data.pdbx_chem_comp_identifier._rowCount > 0) {
@@ -108,42 +109,15 @@ const getUniqueComponentNames = memoize1((data: BasicData) => {
 });
 
 
-function getStructAsymMap(data: BasicData): Model['properties']['structAsymMap'] {
+export function getStructAsymMap(atomic: AtomicHierarchy): Model['properties']['structAsymMap'] {
     const map = new Map<string, StructAsym>();
 
-    const { label_asym_id, auth_asym_id, label_entity_id } = data.atom_site;
-    for (let i = 0, il = label_asym_id.rowCount; i < il; ++i) {
+    const { auth_asym_id, label_asym_id, label_entity_id } = atomic.chains;
+
+    for (let i = 0, _i = atomic.chains._rowCount; i < _i; i ++) {
         const id = label_asym_id.value(i);
-        if (!map.has(id)) {
-            map.set(id, {
-                id,
-                auth_id: auth_asym_id.value(i),
-                entity_id: label_entity_id.value(i)
-            });
-        }
+        map.set(id, { id, auth_id: auth_asym_id.value(i), entity_id: label_entity_id.value(i) });
     }
 
-    if (data.struct_asym._rowCount > 0) {
-        const { id, entity_id } = data.struct_asym;
-        for (let i = 0, il = id.rowCount; i < il; ++i) {
-            const _id = id.value(i);
-            if (!map.has(_id)) {
-                map.set(_id, {
-                    id: _id,
-                    auth_id: '',
-                    entity_id: entity_id.value(i)
-                });
-            }
-        }
-    }
     return map;
-}
-
-export function getProperties(data: BasicData): Model['properties'] {
-    return {
-        missingResidues: getMissingResidues(data),
-        chemicalComponentMap: getChemicalComponentMap(data),
-        saccharideComponentMap: getSaccharideComponentMap(data),
-        structAsymMap: getStructAsymMap(data)
-    };
 }

+ 9 - 5
src/mol-model/structure/export/mmcif.ts

@@ -52,6 +52,10 @@ function isWithoutSymmetry(structure: Structure) {
     return structure.units.every(u => u.conformation.operator.isIdentity);
 }
 
+function isWithoutOperator(structure: Structure) {
+    return isWithoutSymmetry(structure) && structure.units.every(u => !u.conformation.operator.assembly && !u.conformation.operator.suffix);
+}
+
 const Categories = [
     // Basics
     copy_mmCif_category('entry'),
@@ -63,9 +67,9 @@ const Categories = [
     copy_mmCif_category('symmetry', isWithoutSymmetry),
 
     // Assemblies
-    copy_mmCif_category('pdbx_struct_assembly', isWithoutSymmetry),
-    copy_mmCif_category('pdbx_struct_assembly_gen', isWithoutSymmetry),
-    copy_mmCif_category('pdbx_struct_oper_list', isWithoutSymmetry),
+    copy_mmCif_category('pdbx_struct_assembly', isWithoutOperator),
+    copy_mmCif_category('pdbx_struct_assembly_gen', isWithoutOperator),
+    copy_mmCif_category('pdbx_struct_oper_list', isWithoutOperator),
 
     // Secondary structure
     _struct_conf,
@@ -250,10 +254,10 @@ function encode_mmCIF_categories_copyAll(encoder: CifWriter.Encoder, ctx: CifExp
 }
 
 
-function to_mmCIF(name: string, structure: Structure, asBinary = false) {
+function to_mmCIF(name: string, structure: Structure, asBinary = false, params?: encode_mmCIF_categories_Params) {
     const enc = CifWriter.createEncoder({ binary: asBinary });
     enc.startDataBlock(name);
-    encode_mmCIF_categories(enc, structure);
+    encode_mmCIF_categories(enc, structure, params);
     return enc.getData();
 }
 

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

@@ -22,7 +22,7 @@ export const EntitySubtype = Column.Schema.Aliased<EntitySubtype>(Column.Schema.
 export interface Entities {
     data: mmCIF_Database['entity'],
     subtype: Column<EntitySubtype>,
-    prd_id: Column<string>,
+    prd_id?: Column<string>,
     getEntityIndex(id: string): EntityIndex
 }
 

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

@@ -162,7 +162,7 @@ const entity = {
     pdbx_ec: p(l => l.unit.model.entities.data.pdbx_ec.value(eK(l))),
 
     subtype: p(l => l.unit.model.entities.subtype.value(eK(l))),
-    prd_id: p(l => l.unit.model.entities.prd_id.value(eK(l))),
+    prd_id: p(l => l.unit.model.entities.prd_id?.value(eK(l)) ?? ''),
 };
 
 const _emptyList: any[] = [];

+ 31 - 16
src/mol-plugin-state/actions/file.ts

@@ -7,8 +7,10 @@
 import { PluginContext } from '../../mol-plugin/context';
 import { StateAction } from '../../mol-state';
 import { Task } from '../../mol-task';
+import { Asset } from '../../mol-util/assets';
 import { getFileInfo } from '../../mol-util/file-info';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { unzip } from '../../mol-util/zip/zip';
 import { PluginStateObject } from '../objects';
 
 export const OpenFiles = StateAction.build({
@@ -33,24 +35,37 @@ export const OpenFiles = StateAction.build({
             plugin.log.error('No file(s) selected');
             return;
         }
-        for (const file of params.files) {
-            try {
-                const info = getFileInfo(file.file!);
-                const isBinary = plugin.dataFormats.binaryExtensions.has(info.ext);
-                const { data } = await plugin.builders.data.readFile({ file, isBinary });
-                const provider = params.format.name === 'auto'
-                    ? plugin.dataFormats.auto(info, data.cell?.obj!)
-                    : plugin.dataFormats.get(params.format.params);
 
-                if (!provider) {
-                    plugin.log.warn(`OpenFiles: could not find data provider for '${info.name}.${info.ext}'`);
-                    continue;
-                }
+        const processFile = async (file: Asset.File) => {
+            const info = getFileInfo(file.file!);
+            const isBinary = plugin.dataFormats.binaryExtensions.has(info.ext);
+            const { data } = await plugin.builders.data.readFile({ file, isBinary });
+            const provider = params.format.name === 'auto'
+                ? plugin.dataFormats.auto(info, data.cell?.obj!)
+                : plugin.dataFormats.get(params.format.params);
+
+            if (!provider) {
+                plugin.log.warn(`OpenFiles: could not find data provider for '${info.name}.${info.ext}'`);
+                return;
+            }
 
-                // need to await so that the enclosing Task finishes after the update is done.
-                const parsed = await provider.parse(plugin, data);
-                if (params.visuals) {
-                    await provider.visuals?.(plugin, parsed);
+            // need to await so that the enclosing Task finishes after the update is done.
+            const parsed = await provider.parse(plugin, data);
+            if (params.visuals) {
+                await provider.visuals?.(plugin, parsed);
+            }
+        };
+
+        for (const file of params.files) {
+            try {
+                if (file.file && file.name.toLowerCase().endsWith('.zip')) {
+                    const zippedFiles = await unzip(taskCtx, await file.file.arrayBuffer());
+                    for (const [fn, filedata] of Object.entries(zippedFiles)) {
+                        const asset = Asset.File(new File([filedata as Uint8Array], fn));
+                        await processFile(asset);
+                    }
+                } else {
+                    await processFile(file);
                 }
             } catch (e) {
                 console.error(e);