Browse Source

basic G3D support

David Sehnal 4 years ago
parent
commit
6b1edd9d10

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

@@ -26,22 +26,30 @@ import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
 import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
+import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
+import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
 
 require('mol-plugin-ui/skin/light.scss');
 
 export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
 export { setProductionMode, setDebugMode } from '../../mol-util/debug';
 
+const CustomFormats = [
+    ['g3d', G3dProvider] as const
+];
+
 const Extensions = {
     'cellpack': PluginSpec.Behavior(CellPack),
     'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
     'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
     'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
     'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
-    'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation)
+    'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
+    'g3d': PluginSpec.Behavior(G3DFormat)
 };
 
 const DefaultViewerOptions = {
+    customFormats: CustomFormats as [string, DataFormatProvider][],
     extensions: ObjectKeys(Extensions),
     layoutIsExpanded: true,
     layoutShowControls: true,
@@ -77,6 +85,7 @@ export class Viewer {
             ],
             animations: [...DefaultPluginSpec.animations || []],
             customParamEditors: DefaultPluginSpec.customParamEditors,
+            customFormats: o?.customFormats,
             layout: {
                 initial: {
                     isExpanded: o.layoutIsExpanded,

+ 66 - 0
src/extensions/g3d/data.ts

@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import msgpackDecode from '../../mol-io/common/msgpack/decode';
+import { PluginContext } from '../../mol-plugin/context';
+import { Task } from '../../mol-task';
+import { inflate } from '../../mol-util/zip/zip';
+
+export interface G3dHeader {
+    magic: 'G3D',
+    version: number,
+    genome: string,
+    name: string,
+    offsets: { [resolution: string]: { offset: number, size: number } },
+    resolutions: number[]
+}
+
+export type G3dDataBlock = {
+    header: G3dHeader,
+    resolution: number,
+    data: {
+        [HTMLBRElement in 'paternal' | 'maternal']: {
+            [ch: string]: {
+                start: number[]
+                x: number[],
+                y: number[],
+                z: number[],
+            }
+        }
+    }
+}
+
+const HEADER_SIZE = 64000;
+
+export async function getG3dHeader(ctx: PluginContext, urlOrData: string | Uint8Array): Promise<G3dHeader> {
+    const data: Uint8Array = await getRawData(ctx, urlOrData, { offset: 0, size: HEADER_SIZE });
+    let last = data.length - 1;
+    for (; last >= 0; last--) {
+        if (data[last] !== 0) break;
+    }
+    const header = msgpackDecode(data.slice(0, last + 1));
+    return header;
+}
+
+export async function getG3dDataBlock(ctx: PluginContext, header: G3dHeader, urlOrData: string | Uint8Array, resolution: number): Promise<G3dDataBlock> {
+    if (!header.offsets[resolution]) throw new Error(`Resolution ${resolution} not available.`);
+    const data = await getRawData(ctx, urlOrData, header.offsets[resolution]);
+    const unzipped = await ctx.runTask(Task.create('Unzip', ctx => inflate(ctx, data)));
+
+    return {
+        header,
+        resolution,
+        data: msgpackDecode(unzipped)
+    };
+}
+
+async function getRawData(ctx: PluginContext, urlOrData: string | Uint8Array, range: { offset: number, size: number }) {
+    if (typeof urlOrData === 'string') {
+        return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: [['Range', `bytes=${range.offset}-${range.offset + range.size - 1}`]], type: 'binary' }));
+    } else {
+        return urlOrData.slice(range.offset, range.offset + range.size);
+    }
+}

+ 147 - 0
src/extensions/g3d/format.ts

@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { TrajectoryFormatProvider, TrajectoryFormatCategory } from '../../mol-plugin-state/formats/trajectory';
+import { PluginStateTransform, PluginStateObject as SO } from '../../mol-plugin-state/objects';
+import { G3dHeader, getG3dHeader, getG3dDataBlock } from './data';
+import { Task } from '../../mol-task';
+import { PluginContext } from '../../mol-plugin/context';
+import { ParamDefinition } from '../../mol-util/param-definition';
+import { trajectoryFromG3D } from './model';
+import { StateObjectRef, StateAction } from '../../mol-state';
+import { PluginBehavior } from '../../mol-plugin/behavior';
+
+export const G3dProvider: TrajectoryFormatProvider = {
+    label: 'G3D',
+    description: 'G3D',
+    category: TrajectoryFormatCategory,
+    binaryExtensions: ['g3d'],
+    parse: async (plugin, data) => {
+        const trajectory = await plugin.state.data.build()
+            .to(data)
+            .apply(G3DHeaderFromFile, {}, { state: { isGhost: true } })
+            .apply(G3DTrajectory)
+            .commit();
+
+        return { trajectory };
+    },
+    visuals: defaultVisuals
+};
+
+async function defaultVisuals(plugin: PluginContext, data: { trajectory: StateObjectRef<SO.Molecule.Trajectory> }) {
+    const builder = plugin.builders.structure;
+    const model = await builder.createModel(data.trajectory);
+    const modelProperties = await builder.insertModelProperties(model);
+    const structure = await builder.createStructure(modelProperties);
+    const all = await builder.tryCreateComponentStatic(structure, 'all');
+
+    if (all) {
+        await builder.representation.addRepresentation(all, {
+            type: 'cartoon',
+            color: 'polymer-index',
+            size: 'uniform',
+            sizeParams: { value: 2 }
+        });
+    }
+}
+
+export class G3dHeaderObject extends SO.Create<{
+    header: G3dHeader,
+    urlOrData: Uint8Array | string
+}>({ name: 'G3D Header', typeClass: 'Data' }) { }
+
+export type G3DHeaderFromFile = typeof G3DHeaderFromFile
+export const G3DHeaderFromFile = PluginStateTransform.BuiltIn({
+    name: 'g3d-header-from-file',
+    display: { name: 'G3D Header', description: 'Parse G3D Header' },
+    from: SO.Data.Binary,
+    to: G3dHeaderObject
+})({
+    apply({ a }, plugin: PluginContext) {
+        return Task.create('Parse G3D', async () => {
+            const header = await getG3dHeader(plugin, a.data);
+            return new G3dHeaderObject({ header, urlOrData: a.data }, { label: header.name, description: header.genome });
+        });
+    }
+});
+
+export type G3DHeaderFromUrl = typeof G3DHeaderFromUrl
+export const G3DHeaderFromUrl = PluginStateTransform.BuiltIn({
+    name: 'g3d-header-from-url',
+    display: { name: 'G3D Header', description: 'Parse G3D Header' },
+    params: { url: ParamDefinition.Text('') },
+    from: SO.Root,
+    to: G3dHeaderObject
+})({
+    apply({ params }, plugin: PluginContext) {
+        return Task.create('Parse G3D', async () => {
+            const header = await getG3dHeader(plugin, params.url);
+            return new G3dHeaderObject({ header, urlOrData: params.url }, { label: header.name, description: header.genome });
+        });
+    }
+});
+
+export type G3DTrajectory = typeof G3DHeaderFromUrl
+export const G3DTrajectory = PluginStateTransform.BuiltIn({
+    name: 'g3d-trajecotry',
+    display: { name: 'G3D Trajectory', description: 'Create G3D Trajectory' },
+    params: a => {
+        if (!a) return { resolution: ParamDefinition.Numeric(200000) };
+        const rs = a.data.header.resolutions;
+        return {
+            resolution: ParamDefinition.Select(rs[rs.length - 1], rs.map(r => [r, '' + r] as const))
+        };
+    },
+    from: G3dHeaderObject,
+    to: SO.Molecule.Trajectory
+})({
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('G3D Trajectory', async ctx => {
+            const data = await getG3dDataBlock(plugin, a.data.header, a.data.urlOrData, params.resolution);
+            const traj = await trajectoryFromG3D(data).runInContext(ctx);
+            return new SO.Molecule.Trajectory(traj, { label: a.label, description: a.description });
+        });
+    }
+});
+
+export const LoadG3D = StateAction.build({
+    display: { name: 'Load Genome 3D (G3D)', description: 'Load G3D file from the specified URL.' },
+    from: SO.Root,
+    params: { url: ParamDefinition.Text('') }
+})(({ params, state }, ctx: PluginContext) => Task.create('Genome3D', taskCtx => {
+    return state.transaction(async () => {
+        if (params.url.trim().length === 0) {
+            throw new Error('Specify URL');
+        }
+
+        ctx.behaviors.layout.leftPanelTabName.next('data');
+
+        const trajectory = await state.build().toRoot()
+            .apply(G3DHeaderFromUrl, { url: params.url })
+            .apply(G3DTrajectory)
+            .commit();
+
+        await defaultVisuals(ctx, { trajectory });
+    }).runInContext(taskCtx);
+}));
+
+export const G3DFormat = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
+    name: 'g3d',
+    category: 'misc',
+    display: {
+        name: 'G3D',
+        description: 'G3D Format Support'
+    },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
+        register() {
+            this.ctx.state.data.actions.add(LoadG3D);
+        }
+        unregister() {
+            this.ctx.state.data.actions.remove(LoadG3D);
+        }
+    }
+});

+ 114 - 0
src/extensions/g3d/model.ts

@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Model } from '../../mol-model/structure/model';
+import { Task } from '../../mol-task';
+import { Column, Table } from '../../mol-data/db';
+import { MoleculeType } from '../../mol-model/structure/model/types';
+import { EntityBuilder } from '../../mol-model-formats/structure/common/entity';
+import { BasicSchema, createBasic } from '../../mol-model-formats/structure/basic/schema';
+import { createModels } from '../../mol-model-formats/structure/basic/parser';
+import { G3dDataBlock } from './data';
+import { objectForEach } from '../../mol-util/object';
+
+interface Columns {
+    entity_id: string[],
+    chromosome: string[],
+    start: Int32Array,
+    end: Int32Array,
+    x: Float32Array
+    y: Float32Array
+    z: Float32Array
+    haplotype: string[]
+}
+
+function getColumns(block: G3dDataBlock) {
+    const { data } = block;
+    let size = 0;
+
+    objectForEach(data, h => objectForEach(h, g => size += g.start.length));
+
+    const columns: Columns = {
+        entity_id: new Array(size),
+        chromosome: new Array(size),
+        start: new Int32Array(size),
+        end: new Int32Array(size),
+        x: new Float32Array(size),
+        y: new Float32Array(size),
+        z: new Float32Array(size),
+        haplotype: new Array(size)
+    };
+
+    let o = 0;
+    objectForEach(data, (hs, h) => {
+        objectForEach(hs, (chs, ch) => {
+            const entity_id = `${ch}-${h}`;
+            for (let i = 0, _i = chs.start.length; i < _i; i++) {
+                columns.entity_id[o] = entity_id;
+                columns.chromosome[o] = ch;
+                columns.start[o] = o + 1;
+                columns.end[o] = o + 2;
+                columns.x[o] = 10 * chs.x[i];
+                columns.y[o] = 10 * chs.y[i];
+                columns.z[o] = 10 * chs.z[i];
+                columns.haplotype[o] = h;
+                o++;
+            }
+        });
+    });
+
+    return columns;
+}
+
+function getBasic(data: G3dDataBlock) {
+    const columns = getColumns(data);
+
+    const rowCount = columns.start.length;
+    const entityIds = new Array<string>(rowCount);
+    const entityBuilder = new EntityBuilder();
+
+    const stride = columns.start[1] - columns.start[0];
+
+    const objectRadius = stride / 3500;
+
+    for (let i = 0; i < rowCount; ++i) {
+        const e = columns.entity_id[i];
+        const entityId = entityBuilder.getEntityId(e, MoleculeType.DNA, e);
+        entityIds[i] = entityId;
+    }
+
+    const ihm_sphere_obj_site = Table.ofPartialColumns(BasicSchema.ihm_sphere_obj_site, {
+        id: Column.range(0, rowCount),
+        entity_id: Column.ofStringArray(entityIds),
+        seq_id_begin: Column.ofIntArray(columns.start),
+        seq_id_end: Column.ofIntArray(columns.end),
+        asym_id: Column.ofStringArray(columns.chromosome),
+
+        Cartn_x: Column.ofFloatArray(columns.x),
+        Cartn_y: Column.ofFloatArray(columns.y),
+        Cartn_z: Column.ofFloatArray(columns.z),
+
+        object_radius: Column.ofConst(objectRadius, rowCount, Column.Schema.float),
+        rmsf: Column.ofConst(0, rowCount, Column.Schema.float),
+        model_id: Column.ofConst(1, rowCount, Column.Schema.int),
+    }, rowCount);
+
+    return createBasic({
+        entity: entityBuilder.getEntityTable(),
+        ihm_model_list: Table.ofPartialColumns(BasicSchema.ihm_model_list, {
+            model_id: Column.ofIntArray([1]),
+            model_name: Column.ofStringArray(['3DG Model']),
+        }, 1),
+        ihm_sphere_obj_site
+    });
+}
+
+export function trajectoryFromG3D(data: G3dDataBlock): Task<Model.Trajectory> {
+    return Task.create('Parse G3D', async ctx => {
+        const basic = getBasic(data);
+        return createModels(basic, { kind: 'g3d', name: 'G3D', data }, ctx);
+    });
+}

+ 1 - 1
src/mol-model/structure/model/properties/utils/coarse-ranges.ts

@@ -31,7 +31,7 @@ export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: R
                 startIndex = i;
                 prevSeqEnd = seq_id_end.value(i);
             } else {
-                if (prevSeqEnd !== seq_id_begin.value(i) - 1) {
+                if (seq_id_begin.value(i) - prevSeqEnd > 1) {
                     polymerRanges.push(startIndex, i - 1);
                     gapRanges.push(i - 1, i);
                     startIndex = i;

+ 2 - 2
src/mol-plugin-state/formats/shape.ts

@@ -11,12 +11,12 @@ import { PluginContext } from '../../mol-plugin/context';
 import { StateObjectRef } from '../../mol-state';
 import { PluginStateObject } from '../objects';
 
-const Category = 'Shape';
+export const ShapeFormatCategory = 'Shape';
 
 export const PlyProvider = DataFormatProvider({
     label: 'PLY',
     description: 'PLY',
-    category: Category,
+    category: ShapeFormatCategory,
     stringExtensions: ['ply'],
     parse: async (plugin, data) => {
         const format = plugin.state.data.build()

+ 4 - 4
src/mol-plugin-state/formats/structure.ts

@@ -8,12 +8,12 @@
 import { StateTransforms } from '../transforms';
 import { DataFormatProvider } from './provider';
 
-const Category = 'Structure';
+export const StructureFormatCategory = 'Structure';
 
 export const PsfProvider = DataFormatProvider({
     label: 'PSF',
     description: 'PSF',
-    category: Category,
+    category: StructureFormatCategory,
     stringExtensions: ['psf'],
     parse: async (plugin, data) => {
         const format = plugin.state.data.build()
@@ -30,7 +30,7 @@ export const PsfProvider = DataFormatProvider({
 export const DcdProvider = DataFormatProvider({
     label: 'DCD',
     description: 'DCD',
-    category: Category,
+    category: StructureFormatCategory,
     binaryExtensions: ['dcd'],
     parse: (plugin, data) => {
         const coordinates = plugin.state.data.build()
@@ -44,7 +44,7 @@ export const DcdProvider = DataFormatProvider({
 export const XtcProvider = DataFormatProvider({
     label: 'XTC',
     description: 'XTC',
-    category: Category,
+    category: StructureFormatCategory,
     binaryExtensions: ['xtc'],
     parse: (plugin, data) => {
         const coordinates = plugin.state.data.build()

+ 9 - 9
src/mol-plugin-state/formats/trajectory.ts

@@ -15,7 +15,7 @@ export interface TrajectoryFormatProvider<P extends { trajectoryTags?: string |
     extends DataFormatProvider<P, R> {
 }
 
-const Category = 'Trajectory';
+export const TrajectoryFormatCategory = 'Trajectory';
 
 function defaultVisuals(plugin: PluginContext, data: { trajectory: StateObjectRef<PluginStateObject.Molecule.Trajectory> }) {
     return plugin.builders.structure.hierarchy.applyPreset(data.trajectory, 'default');
@@ -24,7 +24,7 @@ function defaultVisuals(plugin: PluginContext, data: { trajectory: StateObjectRe
 export const MmcifProvider: TrajectoryFormatProvider = {
     label: 'mmCIF',
     description: 'mmCIF',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['cif', 'mmcif', 'mcif'],
     binaryExtensions: ['bcif'],
     isApplicable: (info, data) => {
@@ -53,7 +53,7 @@ export const MmcifProvider: TrajectoryFormatProvider = {
 export const CifCoreProvider: TrajectoryFormatProvider = {
     label: 'cifCore',
     description: 'CIF Core',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['cif'],
     isApplicable: (info, data) => {
         if (info.ext === 'cif') return guessCifVariant(info, data) === 'coreCif';
@@ -88,7 +88,7 @@ function directTrajectory<P>(transformer: StateTransformer<PluginStateObject.Dat
 export const PdbProvider: TrajectoryFormatProvider = {
     label: 'PDB',
     description: 'PDB',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['pdb', 'ent'],
     parse: directTrajectory(StateTransforms.Model.TrajectoryFromPDB),
     visuals: defaultVisuals
@@ -97,7 +97,7 @@ export const PdbProvider: TrajectoryFormatProvider = {
 export const PdbqtProvider: TrajectoryFormatProvider = {
     label: 'PDBQT',
     description: 'PDBQT',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['pdbqt'],
     parse: directTrajectory(StateTransforms.Model.TrajectoryFromPDB, { isPdbqt: true }),
     visuals: defaultVisuals
@@ -106,7 +106,7 @@ export const PdbqtProvider: TrajectoryFormatProvider = {
 export const GroProvider: TrajectoryFormatProvider = {
     label: 'GRO',
     description: 'GRO',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['gro'],
     binaryExtensions: [],
     parse: directTrajectory(StateTransforms.Model.TrajectoryFromGRO),
@@ -116,7 +116,7 @@ export const GroProvider: TrajectoryFormatProvider = {
 export const Provider3dg: TrajectoryFormatProvider = {
     label: '3DG',
     description: '3DG',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['3dg'],
     parse: directTrajectory(StateTransforms.Model.TrajectoryFrom3DG),
     visuals: defaultVisuals
@@ -125,7 +125,7 @@ export const Provider3dg: TrajectoryFormatProvider = {
 export const MolProvider: TrajectoryFormatProvider = {
     label: 'MOL/SDF',
     description: 'MOL/SDF',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['mol', 'sdf', 'sd'],
     parse: directTrajectory(StateTransforms.Model.TrajectoryFromMOL),
     visuals: defaultVisuals
@@ -134,7 +134,7 @@ export const MolProvider: TrajectoryFormatProvider = {
 export const Mol2Provider: TrajectoryFormatProvider = {
     label: 'MOL2',
     description: 'MOL2',
-    category: Category,
+    category: TrajectoryFormatCategory,
     stringExtensions: ['mol2'],
     parse: directTrajectory(StateTransforms.Model.TrajectoryFromMOL2),
     visuals: defaultVisuals

+ 6 - 6
src/mol-plugin-state/formats/volume.ts

@@ -20,7 +20,7 @@ import { getContourLevelEmdb } from '../../mol-plugin/behavior/dynamic/volume-st
 import { Task } from '../../mol-task';
 import { DscifFormat } from '../../mol-model-formats/volume/density-server';
 
-const Category = 'Volume';
+export const VolumeFormatCategory = 'Volume';
 type Params = { entryId?: string };
 
 async function tryObtainRecommendedIsoValue(plugin: PluginContext, volume?: Volume) {
@@ -71,7 +71,7 @@ async function defaultVisuals(plugin: PluginContext, data: { volume: StateObject
 export const Ccp4Provider = DataFormatProvider({
     label: 'CCP4/MRC/MAP',
     description: 'CCP4/MRC/MAP',
-    category: Category,
+    category: VolumeFormatCategory,
     binaryExtensions: ['ccp4', 'mrc', 'map'],
     parse: async (plugin, data, params?: Params) => {
         const format = plugin.build()
@@ -91,7 +91,7 @@ export const Ccp4Provider = DataFormatProvider({
 export const Dsn6Provider = DataFormatProvider({
     label: 'DSN6/BRIX',
     description: 'DSN6/BRIX',
-    category: Category,
+    category: VolumeFormatCategory,
     binaryExtensions: ['dsn6', 'brix'],
     parse: async (plugin, data, params?: Params) => {
         const format = plugin.build()
@@ -111,7 +111,7 @@ export const Dsn6Provider = DataFormatProvider({
 export const DxProvider = DataFormatProvider({
     label: 'DX',
     description: 'DX',
-    category: Category,
+    category: VolumeFormatCategory,
     stringExtensions: ['dx'],
     binaryExtensions: ['dxbin'],
     parse: async (plugin, data, params?: Params) => {
@@ -132,7 +132,7 @@ export const DxProvider = DataFormatProvider({
 export const CubeProvider = DataFormatProvider({
     label: 'Cube',
     description: 'Cube',
-    category: Category,
+    category: VolumeFormatCategory,
     stringExtensions: ['cub', 'cube'],
     parse: async (plugin, data, params?: Params) => {
         const format = plugin.build()
@@ -194,7 +194,7 @@ export const CubeProvider = DataFormatProvider({
 export const DscifProvider = DataFormatProvider({
     label: 'DensityServer CIF',
     description: 'DensityServer CIF',
-    category: Category,
+    category: VolumeFormatCategory,
     stringExtensions: ['cif'],
     binaryExtensions: ['bcif'],
     isApplicable: (info, data) => {

+ 1 - 1
src/mol-plugin-ui/sequence/polymer.ts

@@ -27,7 +27,7 @@ export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> {
     }
 
     residueLabel(seqIdx: number) {
-        return this.sequence.label.value(seqIdx);
+        return this.sequence.label.value(seqIdx) || this.sequence.code.value(seqIdx);
     }
 
     residueColor(seqIdx: number) {

+ 9 - 0
src/mol-plugin/context.ts

@@ -324,6 +324,14 @@ export class PluginContext {
         await this.runTask(this.state.behaviors.updateTree(tree, { doNotUpdateCurrent: true, doNotLogTiming: true }));
     }
 
+    private initCustomFormats() {
+        if (!this.spec.customFormats) return;
+
+        for (const f of this.spec.customFormats) {
+            this.dataFormats.add(f[0], f[1]);
+        }
+    }
+
     private initDataActions() {
         for (const a of this.spec.actions) {
             this.state.data.actions.add(a.action);
@@ -348,6 +356,7 @@ export class PluginContext {
     async init() {
         this.events.log.subscribe(e => this.log.entries = this.log.entries.push(e));
 
+        this.initCustomFormats();
         this.initBehaviorEvents();
         this.initBuiltInBehavior();
 

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

@@ -11,6 +11,7 @@ import { PluginLayoutStateProps } from './layout';
 import { PluginStateAnimation } from '../mol-plugin-state/animation/model';
 import { PluginConfigItem } from './config';
 import { PartialCanvas3DProps } from '../mol-canvas3d/canvas3d';
+import { DataFormatProvider } from '../mol-plugin-state/formats/provider';
 
 export { PluginSpec };
 
@@ -19,6 +20,7 @@ interface PluginSpec {
     behaviors: PluginSpec.Behavior[],
     animations?: PluginStateAnimation[],
     customParamEditors?: [StateAction | StateTransformer, StateTransformParameters.Class][],
+    customFormats?: [string, DataFormatProvider][]
     layout?: {
         initial?: Partial<PluginLayoutStateProps>,
         controls?: PluginSpec.LayoutControls