Bladeren bron

mol-plugin: initial custom property support

David Sehnal 6 jaren geleden
bovenliggende
commit
64276543c3

+ 28 - 0
src/mol-model-props/pdbe/structure-quality-report.ts

@@ -15,6 +15,7 @@ import { CustomPropSymbol } from 'mol-script/language/symbol';
 import Type from 'mol-script/language/type';
 import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
 import { PropertyWrapper } from '../common/wrapper';
+import { Task } from 'mol-task';
 
 export namespace StructureQualityReport {
     export type IssueMap = IndexedCustomProperty.Residue<string[]>
@@ -82,6 +83,33 @@ export namespace StructureQualityReport {
         }
     }
 
+    export function createAttachTask(mapUrl: (model: Model) => string, fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) {
+        return (model: Model) => Task.create('PDBe Structure Quality Report', async ctx => {
+            if (get(model)) return true;
+
+            let issueMap: IssueMap | undefined;
+            let info;
+            // TODO: return from CIF support once the data is recomputed
+            //  = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
+            // if (info) {
+            //     const data = getCifData(model);
+            //     issueMap = createIssueMapFromCif(model, data.residues, data.groups);
+            // } else
+            {
+                const url = mapUrl(model);
+                const dataStr = await fetch(url, 'string').runInContext(ctx) as string;
+                const data = JSON.parse(dataStr)[model.label.toLowerCase()];
+                if (!data) return false;
+                info = PropertyWrapper.createInfo();
+                issueMap = createIssueMapFromJson(model, data);
+            }
+
+            model.customProperties.add(Descriptor);
+            set(model, { info, data: issueMap });
+            return false;
+        });
+    }
+
     export async function attachFromCifOrApi(model: Model, params: {
         // optional JSON source
         PDBe_apiSourceJson?: (model: Model) => Promise<any>

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

@@ -13,6 +13,7 @@ import * as StaticMisc from './behavior/static/misc'
 
 import * as DynamicRepresentation from './behavior/dynamic/representation'
 import * as DynamicCamera from './behavior/dynamic/camera'
+import * as DynamicCustomProps from './behavior/dynamic/custom-props'
 
 export const BuiltInPluginBehaviors = {
     State: StaticState,
@@ -24,4 +25,5 @@ export const BuiltInPluginBehaviors = {
 export const PluginBehaviors = {
     Representation: DynamicRepresentation,
     Camera: DynamicCamera,
+    CustomProps: DynamicCustomProps
 }

+ 70 - 0
src/mol-plugin/behavior/dynamic/custom-props.ts

@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ParamDefinition } from 'mol-util/param-definition';
+import { PluginBehavior } from '../behavior';
+import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
+import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
+import { Loci } from 'mol-model/loci';
+import { StructureElement } from 'mol-model/structure';
+import { OrderedSet } from 'mol-data/int';
+
+// TODO: make auto attach working better for "initial state" by supporting default props in state updates
+
+export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'pdbe-structure-quality-report-prop',
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
+        private attach = StructureQualityReport.createAttachTask(
+            m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`,
+            this.ctx.fetch
+        );
+
+        private provider: CustomPropertyRegistry.Provider = {
+            option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'],
+            descriptor: StructureQualityReport.Descriptor,
+            defaultSelected: false,
+            attachableTo: () => true,
+            attach: this.attach
+        }
+
+        register(): void {
+            this.ctx.customModelProperties.register(this.provider);
+            this.ctx.lociLabels.addProvider(labelPDBeValidation);
+        }
+
+        update(p: { autoAttach: boolean }) {
+            let updated = this.params.autoAttach !== p.autoAttach
+            this.params.autoAttach = p.autoAttach;
+            this.provider.defaultSelected = p.autoAttach;
+            return updated;
+        }
+
+        unregister() {
+            this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name);
+            this.ctx.lociLabels.removeProvider(labelPDBeValidation);
+        }
+    },
+    params: () => ({
+        autoAttach: ParamDefinition.Boolean(false)
+    }),
+    display: { name: 'Focus Loci on Select', group: 'Camera' }
+});
+
+function labelPDBeValidation(loci: Loci): string | undefined {
+    switch (loci.kind) {
+        case 'element-loci':
+            const e = loci.elements[0];
+            const u = e.unit;
+            if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0;
+
+            const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]);
+            const issues = StructureQualityReport.getIssues(se);
+            if (issues.length === 0) return 'PDBe Validation: No Issues';
+            return `PDBe Validation: ${issues.join(', ')}`;
+
+        default: return void 0;
+    }
+}

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

@@ -24,6 +24,7 @@ import { TaskManager } from './util/task-manager';
 import { Color } from 'mol-util/color';
 import { LociLabelEntry, LociLabelManager } from './util/loci-label-manager';
 import { ajaxGet } from 'mol-util/data-source';
+import { CustomPropertyRegistry } from './util/custom-prop-registry';
 
 export class PluginContext {
     private disposed = false;
@@ -58,13 +59,6 @@ export class PluginContext {
         }
     };
 
-    readonly lociLabels: LociLabelManager;
-
-    readonly structureRepresentation = {
-        registry: new StructureRepresentationRegistry(),
-        themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext
-    }
-
     readonly behaviors = {
         canvas: {
             highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
@@ -75,6 +69,14 @@ export class PluginContext {
 
     readonly canvas3d: Canvas3D;
 
+    readonly lociLabels: LociLabelManager;
+
+    readonly structureRepresentation = {
+        registry: new StructureRepresentationRegistry(),
+        themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext
+    }
+
+    readonly customModelProperties = new CustomPropertyRegistry();
 
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {

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

@@ -35,7 +35,8 @@ const DefaultSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
-        PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 })
+        PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: false })
     ]
 }
 

+ 1 - 0
src/mol-plugin/state/actions/basic.ts

@@ -80,6 +80,7 @@ function createStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binar
         .apply(StateTransforms.Data.ParseCif)
         .apply(StateTransforms.Model.TrajectoryFromMmCif, {})
         .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
+        .apply(StateTransforms.Model.CustomModelProperties, { properties: [] })
         .apply(StateTransforms.Model.StructureAssemblyFromModel);
 
     complexRepresentation(root);

+ 23 - 1
src/mol-plugin/state/transforms/model.ts

@@ -6,7 +6,7 @@
 
 import { PluginStateTransform } from '../objects';
 import { PluginStateObject as SO } from '../objects';
-import { Task } from 'mol-task';
+import { Task, RuntimeContext } from 'mol-task';
 import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import Expression from 'mol-script/language/expression';
@@ -168,3 +168,25 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
     }
 });
 
+export { CustomModelProperties }
+type CustomModelProperties = typeof CustomModelProperties
+const CustomModelProperties = PluginStateTransform.BuiltIn({
+    name: 'custom-model-properties',
+    display: { name: 'Custom Model Properties' },
+    from: SO.Molecule.Model,
+    to: SO.Molecule.Model,
+    params: (a, ctx: PluginContext) => ({ properties: ctx.customModelProperties.getSelect(a.data) })
+})({
+    apply({ a, params }, ctx: PluginContext) {
+        return Task.create('Custom Props', async taskCtx => {
+            await attachProps(a.data, ctx, taskCtx, params.properties);
+            return new SO.Molecule.Model(a.data, { label: a.label, description: 'Custom Props' });
+        });
+    }
+});
+async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, names: string[]) {
+    for (const name of names) {
+        const p = ctx.customModelProperties.get(name);
+        await p.attach(model).runInContext(taskCtx);
+    }
+}

+ 65 - 0
src/mol-plugin/util/custom-prop-registry.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ModelPropertyDescriptor, Model } from 'mol-model/structure';
+import { OrderedMap } from 'immutable';
+import { ParamDefinition } from 'mol-util/param-definition';
+import { Task } from 'mol-task';
+
+export { CustomPropertyRegistry }
+
+class CustomPropertyRegistry {
+    private providers = OrderedMap<string, CustomPropertyRegistry.Provider>().asMutable();
+
+    getSelect(model: Model) {
+        const values = this.providers.values();
+        const options: [string, string][] = [], selected: string[] = [];
+        while (true) {
+            const v = values.next();
+            if (v.done) break;
+            if (!v.value.attachableTo(model)) continue;
+            options.push(v.value.option);
+            if (v.value.defaultSelected) selected.push(v.value.option[0]);
+        }
+        return ParamDefinition.MultiSelect(selected, options);
+    }
+
+    getDefault(model: Model) {
+        const values = this.providers.values();
+        const selected: string[] = [];
+        while (true) {
+            const v = values.next();
+            if (v.done) break;
+            if (!v.value.attachableTo(model)) continue;
+            if (v.value.defaultSelected) selected.push(v.value.option[0]);
+        }
+        return selected;
+    }
+
+    get(name: string) {
+        const prop = this.providers.get(name);
+        if (!prop) throw new Error(`Custom prop '${name}' is not registered.`);
+        return this.providers.get(name);
+    }
+
+    register(provider: CustomPropertyRegistry.Provider) {
+        this.providers.set(provider.descriptor.name, provider);
+    }
+
+    unregister(name: string) {
+        this.providers.delete(name);
+    }
+}
+
+namespace CustomPropertyRegistry {
+    export interface Provider {
+        option: [string, string],
+        defaultSelected: boolean,
+        descriptor: ModelPropertyDescriptor<any, any>,
+        attachableTo: (model: Model) => boolean,
+        attach: (model: Model) => Task<boolean>
+    }
+}