From 64276543c38cf9ce6a0587c9d969abfd8085d495 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Mon, 26 Nov 2018 12:13:30 +0100 Subject: [PATCH] mol-plugin: initial custom property support --- .../pdbe/structure-quality-report.ts | 28 ++++++++ src/mol-plugin/behavior.ts | 2 + .../behavior/dynamic/custom-props.ts | 70 +++++++++++++++++++ src/mol-plugin/context.ts | 16 +++-- src/mol-plugin/index.ts | 3 +- src/mol-plugin/state/actions/basic.ts | 1 + src/mol-plugin/state/transforms/model.ts | 24 ++++++- src/mol-plugin/util/custom-prop-registry.ts | 65 +++++++++++++++++ 8 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 src/mol-plugin/behavior/dynamic/custom-props.ts create mode 100644 src/mol-plugin/util/custom-prop-registry.ts diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index a770aade4..12cb6d018 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/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> diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts index 98010eb2a..976e78982 100644 --- a/src/mol-plugin/behavior.ts +++ b/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 } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props.ts b/src/mol-plugin/behavior/dynamic/custom-props.ts new file mode 100644 index 000000000..2c31fddcf --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 87a1eb6cb..0d43b9b39 100644 --- a/src/mol-plugin/context.ts +++ b/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 { diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 648029b6d..2201de2ba 100644 --- a/src/mol-plugin/index.ts +++ b/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 }) ] } diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index dae5e96b5..ad60f56f2 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/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); diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 35fe8c775..d7ca14ec8 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/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); + } +} \ No newline at end of file diff --git a/src/mol-plugin/util/custom-prop-registry.ts b/src/mol-plugin/util/custom-prop-registry.ts new file mode 100644 index 000000000..b537d0df3 --- /dev/null +++ b/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> + } +} \ No newline at end of file -- GitLab