ソースを参照

refactored custom-model-property

- analogous to custom-structure-property
Alexander Rose 5 年 前
コミット
c09991573c
31 ファイル変更328 行追加281 行削除
  1. 4 4
      src/apps/basic-wrapper/coloring.ts
  2. 5 5
      src/apps/basic-wrapper/index.ts
  3. 9 7
      src/examples/proteopedia-wrapper/annotation.ts
  4. 5 5
      src/examples/proteopedia-wrapper/index.ts
  5. 0 1
      src/mol-model-formats/structure/mmcif/anisotropic.ts
  6. 0 1
      src/mol-model-formats/structure/mmcif/bonds/comp.ts
  7. 0 1
      src/mol-model-formats/structure/mmcif/bonds/index-pair.ts
  8. 0 1
      src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
  9. 66 78
      src/mol-model-props/common/custom-element-property.ts
  10. 72 0
      src/mol-model-props/common/custom-model-property.ts
  11. 14 12
      src/mol-model-props/common/custom-property.ts
  12. 4 4
      src/mol-model-props/common/custom-structure-property.ts
  13. 0 1
      src/mol-model-props/computed/accessible-surface-area.ts
  14. 0 1
      src/mol-model-props/computed/interactions.ts
  15. 0 1
      src/mol-model-props/computed/secondary-structure.ts
  16. 0 1
      src/mol-model-props/computed/valence-model.ts
  17. 0 1
      src/mol-model-props/pdbe/preferred-assembly.ts
  18. 0 1
      src/mol-model-props/pdbe/struct-ref-domain.ts
  19. 87 93
      src/mol-model-props/pdbe/structure-quality-report.ts
  20. 2 2
      src/mol-model-props/pdbe/themes/structure-quality-report.ts
  21. 0 1
      src/mol-model-props/rcsb/assembly-symmetry.ts
  22. 0 1
      src/mol-model/structure/common/custom-property.ts
  23. 3 2
      src/mol-plugin/behavior/dynamic/custom-props.ts
  24. 12 19
      src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
  25. 2 1
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
  26. 1 2
      src/mol-plugin/context.ts
  27. 2 1
      src/mol-plugin/index.ts
  28. 5 6
      src/mol-plugin/state/actions/structure.ts
  29. 23 14
      src/mol-plugin/state/transforms/model.ts
  30. 7 9
      src/perf-tests/mol-script.ts
  31. 5 5
      src/servers/model/properties/providers/pdbe.ts

+ 4 - 4
src/apps/basic-wrapper/coloring.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-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 { CustomElementProperty } from '../../mol-model-props/common/custom-element-property';
@@ -9,9 +10,8 @@ import { Model, ElementIndex } from '../../mol-model/structure';
 import { Color } from '../../mol-util/color';
 
 export const StripedResidues = CustomElementProperty.create<number>({
-    isStatic: true,
+    label: 'Residue Stripes',
     name: 'basic-wrapper-residue-striping',
-    display: 'Residue Stripes',
     getData(model: Model) {
         const map = new Map<ElementIndex, number>();
         const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
@@ -24,7 +24,7 @@ export const StripedResidues = CustomElementProperty.create<number>({
         getColor(e) { return e === 0 ? Color(0xff0000) : Color(0x0000ff) },
         defaultColor: Color(0x777777)
     },
-    format(e) {
+    getLabel(e) {
         return e === 0 ? 'Odd stripe' : 'Even stripe'
     }
 })

+ 5 - 5
src/apps/basic-wrapper/index.ts

@@ -47,9 +47,9 @@ class BasicWrapper {
             }
         });
 
-        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(StripedResidues.Descriptor.name, StripedResidues.colorTheme!);
-        this.plugin.lociLabels.addProvider(StripedResidues.labelProvider);
-        this.plugin.customModelProperties.register(StripedResidues.propertyProvider);
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(StripedResidues.propertyProvider.descriptor.name, StripedResidues.colorThemeProvider!);
+        this.plugin.lociLabels.addProvider(StripedResidues.labelProvider!);
+        this.plugin.customModelProperties.register(StripedResidues.propertyProvider, true);
     }
 
     private download(b: StateBuilder.To<PSO.Root>, url: string) {
@@ -63,7 +63,7 @@ class BasicWrapper {
 
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', state: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.propertyProvider.descriptor.name] }, { ref: 'props', state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
     }
 
@@ -141,7 +141,7 @@ class BasicWrapper {
 
             const visuals = state.selectQ(q => q.ofTransformer(StateTransforms.Representation.StructureRepresentation3D));
             const tree = state.build();
-            const colorTheme = { name: StripedResidues.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.Descriptor.name).defaultValues };
+            const colorTheme = { name: StripedResidues.propertyProvider.descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.propertyProvider.descriptor.name).defaultValues };
 
             for (const v of visuals) {
                 tree.to(v).update(old => ({ ...old, colorTheme }));

+ 9 - 7
src/examples/proteopedia-wrapper/annotation.ts

@@ -1,12 +1,14 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-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 { CustomElementProperty } from '../../mol-model-props/common/custom-element-property';
 import { Model, ElementIndex, ResidueIndex } from '../../mol-model/structure';
 import { Color } from '../../mol-util/color';
+import { CustomProperty } from '../../mol-model-props/common/custom-property';
 
 const EvolutionaryConservationPalette: Color[] = [
     [255, 255, 129], // insufficient
@@ -23,13 +25,13 @@ const EvolutionaryConservationPalette: Color[] = [
 const EvolutionaryConservationDefaultColor = Color(0x999999);
 
 export const EvolutionaryConservation = CustomElementProperty.create<number>({
-    isStatic: true,
     name: 'proteopedia-wrapper-evolutionary-conservation',
-    display: 'Evolutionary Conservation',
-    async getData(model: Model) {
+    label: 'Evolutionary Conservation',
+    type: 'static',
+    async getData(model: Model, ctx: CustomProperty.Context) {
         const id = model.entryId.toLowerCase();
-        const req = await fetch(`https://proteopedia.org/cgi-bin/cnsrf?${id}`);
-        const json = await req.json();
+        const url = `https://proteopedia.org/cgi-bin/cnsrf?${id}`
+        const json = await ctx.fetch({ url, type: 'json' }).runInContext(ctx.runtime)
         const annotations = (json && json.residueAnnotations) || [];
 
         const conservationMap = new Map<string, number>();
@@ -65,7 +67,7 @@ export const EvolutionaryConservation = CustomElementProperty.create<number>({
         },
         defaultColor: EvolutionaryConservationDefaultColor
     },
-    format(e) {
+    getLabel(e) {
         if (e === 10) return `Evolutionary Conservation: InsufficientData`;
         return e ? `Evolutionary Conservation: ${e}` : void 0;
     }

+ 5 - 5
src/examples/proteopedia-wrapper/index.ts

@@ -68,9 +68,9 @@ class MolStarProteopediaWrapper {
         const customColoring = createProteopediaCustomTheme((options && options.customColorList) || []);
 
         this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add('proteopedia-custom', customColoring);
-        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.Descriptor.name, EvolutionaryConservation.colorTheme!);
-        this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider);
-        this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider);
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.propertyProvider.descriptor.name, EvolutionaryConservation.colorThemeProvider!);
+        this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider!);
+        this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider, true);
     }
 
     get state() {
@@ -94,7 +94,7 @@ class MolStarProteopediaWrapper {
         const model = this.state.build().to(StateElements.Model);
 
         const s = model
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: StateElements.ModelProps, state: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.propertyProvider.descriptor.name] }, { ref: StateElements.ModelProps, state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly });
 
         s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence });
@@ -299,7 +299,7 @@ class MolStarProteopediaWrapper {
             // }
 
             const tree = state.build();
-            const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues };
+            const colorTheme = { name: EvolutionaryConservation.propertyProvider.descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.propertyProvider.descriptor.name).defaultValues };
 
             if (!params || !!params.sequence) {
                 tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));

+ 0 - 1
src/mol-model-formats/structure/mmcif/anisotropic.ts

@@ -43,7 +43,6 @@ export namespace AtomSiteAnisotrop {
     export type Schema = typeof Schema
 
     export const Descriptor: CustomPropertyDescriptor = {
-        isStatic: true,
         name: 'atom_site_anisotrop',
         cifExport: {
             prefix: '',

+ 0 - 1
src/mol-model-formats/structure/mmcif/bonds/comp.ts

@@ -18,7 +18,6 @@ export interface ComponentBond {
 
 export namespace ComponentBond {
     export const Descriptor: CustomPropertyDescriptor = {
-        isStatic: true,
         name: 'chem_comp_bond',
         cifExport: {
             prefix: '',

+ 0 - 1
src/mol-model-formats/structure/mmcif/bonds/index-pair.ts

@@ -24,7 +24,6 @@ function getGraph(indexA: ArrayLike<number>, indexB: ArrayLike<number>, _order:
 
 export namespace IndexPairBonds {
     export const Descriptor: CustomPropertyDescriptor = {
-        isStatic: true,
         name: 'index_pair_bonds',
     }
 

+ 0 - 1
src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts

@@ -25,7 +25,6 @@ export interface StructConn {
 
 export namespace StructConn {
     export const Descriptor: CustomPropertyDescriptor = {
-        isStatic: true,
         name: 'struct_conn',
         cifExport: {
             prefix: '',

+ 66 - 78
src/mol-model-props/common/custom-element-property.ts

@@ -1,93 +1,84 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-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 { ElementIndex, Model, CustomPropertyDescriptor } from '../../mol-model/structure';
 import { StructureElement } from '../../mol-model/structure/structure';
 import { Location } from '../../mol-model/location';
-import { CustomPropertyRegistry } from './custom-property-registry';
-import { Task } from '../../mol-task';
-import { ThemeDataContext, ThemeProvider } from '../../mol-theme/theme';
+import { ThemeDataContext } from '../../mol-theme/theme';
 import { ColorTheme, LocationColor } from '../../mol-theme/color';
 import { Color } from '../../mol-util/color';
-import { TableLegend } from '../../mol-util/legend';
 import { Loci } from '../../mol-model/loci';
 import { OrderedSet } from '../../mol-data/int';
+import { CustomModelProperty } from './custom-model-property';
+import { CustomProperty } from './custom-property';
+import { LociLabelProvider } from '../../mol-plugin/util/loci-label-manager';
 
 export { CustomElementProperty };
 
+interface CustomElementProperty<T> {
+    propertyProvider: CustomModelProperty.Provider<{}, CustomElementProperty.Value<T>>
+    colorThemeProvider?: ColorTheme.Provider<{}>
+    labelProvider?: LociLabelProvider
+}
+
 namespace CustomElementProperty {
-    export interface CreateParams<T> {
-        isStatic: boolean,
-        name: string,
-        autoAttach?: boolean,
-        display: string,
-        attachableTo?: (model: Model) => boolean,
-        getData(model: Model): Map<ElementIndex, T> | Promise<Map<ElementIndex, T>>,
-        format?(e: T): string | undefined,
+    export type Value<T> = Map<ElementIndex, T>
+
+    export interface Builder<T> {
+        label: string
+        name: string
+        getData(model: Model, ctx?: CustomProperty.Context): Value<T> | Promise<Value<T>>
         coloring?: {
-            getColor: (e: T) => Color,
+            getColor: (p: T) => Color
             defaultColor: Color
         }
+        getLabel?: (p: T) => string | undefined
+        isApplicable?: (data: Model) => boolean,
+        type?: 'dynamic' | 'static',
     }
 
-    export function create<T>(params: CreateParams<T>) {
-        const name = params.name;
-
-        const Descriptor = CustomPropertyDescriptor({
-            isStatic: params.isStatic,
-            name: params.name,
-        });
-
-        function attach(model: Model) {
-            return Task.create(`Attach ${params.display}`, async () => {
-                try {
-                    if (model.customProperties.has(Descriptor)) return true;
-
-                    const data = await params.getData(model);
-
-                    if (params.isStatic) {
-                        model._staticPropertyData[name] = data;
-                    } else {
-                        model._dynamicPropertyData[name] = data;
-                    }
-
-                    model.customProperties.add(Descriptor);
-
-                    return true;
-                } catch (e) {
-                    console.warn('Attach Property', e);
-                    return false;
-                }
-            })
+    export function create<T>(builder: Builder<T>): CustomElementProperty<T> {
+        const modelProperty = createModelProperty(builder)
+        return {
+            propertyProvider: modelProperty,
+            colorThemeProvider: builder.coloring?.getColor && createColorThemeProvider(modelProperty, builder.coloring.getColor, builder.coloring.defaultColor),
+            labelProvider: builder.getLabel && createLabelProvider(modelProperty, builder.getLabel)
         }
+    }
 
-        function getStatic(e: StructureElement.Location) { return e.unit.model._staticPropertyData[name].get(e.element); }
-        function getDynamic(e: StructureElement.Location) { return e.unit.model._staticPropertyData[name].get(e.element); }
-
-        const propertyProvider: CustomPropertyRegistry.ModelProvider = {
-            option: [name, params.display],
-            descriptor: Descriptor,
-            defaultSelected: !!params.autoAttach,
-            attachableTo: params.attachableTo || (() => true),
-            attach
-        };
-
-        const get = params.isStatic ? getStatic : getDynamic;
+    function createModelProperty<T>(builder: Builder<T>) {
+        return CustomModelProperty.createProvider({
+            label: builder.label,
+            descriptor: CustomPropertyDescriptor({
+                name: builder.name,
+            }),
+            type: builder.type || 'dynamic',
+            defaultParams: {},
+            getParams: (data: Model) => ({}),
+            isApplicable: (data: Model) => !!builder.isApplicable?.(data),
+            obtain: async (ctx: CustomProperty.Context, data: Model) => {
+                return await builder.getData(data, ctx)
+            }
+        })
+    }
 
-        function has(model: Model) { return model.customProperties.has(Descriptor); }
+    function createColorThemeProvider<T>(modelProperty: CustomModelProperty.Provider<{}, Value<T>>, getColor: (p: T) => Color, defaultColor: Color) {
 
         function Coloring(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
             let color: LocationColor;
-            const getColor = params.coloring!.getColor;
-            const defaultColor = params.coloring!.defaultColor;
 
-            if (ctx.structure && !ctx.structure.isEmpty && has(ctx.structure.models[0])) {
+            const property = ctx.structure && modelProperty.get(ctx.structure.models[0])
+            const contextHash = property?.version
+
+            if (property?.value && ctx.structure) {
+                const data = property.value
                 color = (location: Location) => {
                     if (StructureElement.Location.is(location)) {
-                        const e = get(location);
+                        const e = data.get(location.element);
                         if (typeof e !== 'undefined') return getColor(e);
                     }
                     return defaultColor;
@@ -101,35 +92,32 @@ namespace CustomElementProperty {
                 granularity: 'group',
                 color: color,
                 props: props,
-                description: 'Assign element colors based on the provided data.',
-                legend: TableLegend([])
+                contextHash,
+                description: `Assign element colors based on '${modelProperty.label}' data.`
             };
         }
 
-        const colorTheme: ThemeProvider<ColorTheme<{}>, {}> = {
-            label: params.display,
+        return {
+            label: modelProperty.label,
             factory: Coloring,
             getParams: () => ({}),
             defaultValues: {},
-            isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !ctx.structure.isEmpty && has(ctx.structure.models[0])
+            isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !!modelProperty.get(ctx.structure.models[0]).value
         }
+    }
 
-        function LabelProvider(loci: Loci): string | undefined {
+    function createLabelProvider<T>(modelProperty: CustomModelProperty.Provider<{}, Value<T>>, getLabel: (p: T) => string | undefined) {
+        return function(loci: Loci): string | undefined {
             if (loci.kind === 'element-loci') {
                 const e = loci.elements[0];
-                if (!e || !has(e.unit.model)) return void 0;
-                return params.format!(get(StructureElement.Location.create(e.unit, e.unit.elements[OrderedSet.getAt(e.indices, 0)])));
+                if (!e) return
+                const data = modelProperty.get(e.unit.model).value
+                const element = e.unit.elements[OrderedSet.start(e.indices)]
+                const value = data?.get(element)
+                if (value === undefined) return
+                return getLabel(value);
             }
-            return void 0;
+            return
         }
-
-        return {
-            Descriptor,
-            attach,
-            get,
-            propertyProvider,
-            colorTheme: params.coloring ? colorTheme : void 0,
-            labelProvider: params.format ? LabelProvider : ((loci: Loci) => void 0)
-        };
     }
 }

+ 72 - 0
src/mol-model-props/common/custom-model-property.ts

@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { CustomPropertyDescriptor, Model } from '../../mol-model/structure';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ValueBox } from '../../mol-util';
+import { CustomProperty } from './custom-property';
+
+export { CustomModelProperty }
+
+namespace CustomModelProperty {
+    export interface Provider<Params extends PD.Params, Value> extends CustomProperty.Provider<Model, Params, Value> { }
+
+    export interface ProviderBuilder<Params extends PD.Params, Value> {
+        readonly label: string
+        readonly descriptor: CustomPropertyDescriptor
+        readonly defaultParams: Params
+        readonly getParams: (data: Model) => Params
+        readonly isApplicable: (data: Model) => boolean
+        readonly obtain: (ctx: CustomProperty.Context, data: Model, props: PD.Values<Params>) => Promise<Value>
+        readonly type: 'static' | 'dynamic'
+    }
+
+    export function createProvider<Params extends PD.Params, Value>(builder: ProviderBuilder<Params, Value>): CustomProperty.Provider<Model, Params, Value> {
+        const descriptorName = builder.descriptor.name
+        const propertyDataName = builder.type === 'static' ? '_staticPropertyData' : '_dynamicPropertyData'
+
+        const get = (data: Model) => {
+            if (!(descriptorName in data[propertyDataName])) {
+                (data[propertyDataName][descriptorName] as CustomProperty.Container<PD.Values<Params>, Value>) = {
+                    props: { ...PD.getDefaultValues(builder.getParams(data)) },
+                    data: ValueBox.create(undefined)
+                }
+            }
+            return data[propertyDataName][descriptorName] as CustomProperty.Container<PD.Values<Params>, Value>;
+        }
+        const set = (data: Model, props: PD.Values<Params>, value: Value | undefined) => {
+            const property = get(data);
+            (data[propertyDataName][descriptorName] as CustomProperty.Container<PD.Values<Params>, Value>) = {
+                props,
+                data: ValueBox.withValue(property.data, value)
+            };
+        }
+
+        return {
+            label: builder.label,
+            descriptor: builder.descriptor,
+            getParams: builder.getParams,
+            isApplicable: builder.isApplicable,
+            attach: async (ctx: CustomProperty.Context, data: Model, props: Partial<PD.Values<Params>> = {}) => {
+                const property = get(data)
+                const p = { ...property.props, ...props }
+                if (property.data.value && PD.areEqual(builder.defaultParams, property.props, p)) return
+                const value = await builder.obtain(ctx, data, p)
+                data.customProperties.add(builder.descriptor);
+                set(data, p, value);
+            },
+            get: (data: Model) => get(data)?.data,
+            set: (data: Model, props: Partial<PD.Values<Params>> = {}) => {
+                const property = get(data)
+                const p = { ...property.props, ...props }
+                if (!PD.areEqual(builder.defaultParams, property.props, p)) {
+                    // this invalidates property.value
+                    set(data, p, undefined)
+                }
+            }
+        }
+    }
+}

+ 14 - 12
src/mol-model-props/common/custom-property.ts

@@ -32,7 +32,7 @@ namespace CustomProperty {
         readonly isApplicable: (data: Data) => boolean
         readonly attach: (ctx: Context, data: Data, props?: Partial<PD.Values<Params>>) => Promise<void>
         readonly get: (data: Data) => ValueBox<Value | undefined>
-        readonly setProps: (data: Data, props: PD.Values<Params>) => void
+        readonly set: (data: Data, props: PD.Values<Params>, value?: Value) => void
     }
 
     export class Registry<Data> {
@@ -40,18 +40,20 @@ namespace CustomProperty {
         private defaultAutoAttachValues = new Map<string, boolean>()
 
         /** Get params for all applicable property providers */
-        getParams(data: Data) {
-            const values = this.providers.values();
+        getParams(data?: Data) {
             const params: PD.Params = {}
-            while (true) {
-                const v = values.next()
-                if (v.done) break
-                const provider = v.value
-                if (!provider.isApplicable(data)) continue
-                params[provider.descriptor.name] = PD.Group({
-                    autoAttach: PD.Boolean(this.defaultAutoAttachValues.get(provider.descriptor.name)!),
-                    ...provider.getParams(data),
-                }, { label: v.value.label })
+            if (data) {
+            const values = this.providers.values();
+                while (true) {
+                    const v = values.next()
+                    if (v.done) break
+                    const provider = v.value
+                    if (!provider.isApplicable(data)) continue
+                    params[provider.descriptor.name] = PD.Group({
+                        autoAttach: PD.Boolean(this.defaultAutoAttachValues.get(provider.descriptor.name)!),
+                        ...provider.getParams(data),
+                    }, { label: v.value.label })
+                }
             }
             return params
         }

+ 4 - 4
src/mol-model-props/common/custom-structure-property.ts

@@ -5,7 +5,6 @@
  */
 
 import { CustomPropertyDescriptor, Structure } from '../../mol-model/structure';
-
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ValueBox } from '../../mol-util';
 import { CustomProperty } from './custom-property';
@@ -60,13 +59,14 @@ namespace CustomStructureProperty {
                 data.customPropertyDescriptors.add(builder.descriptor);
                 set(data, p, value);
             },
-            get: (data: Structure) => get(data)?.data,
-            setProps: (data: Structure, props: Partial<PD.Values<Params>> = {}) => {
+            get: (data: Structure) => get(data).data,
+            set: (data: Structure, props: Partial<PD.Values<Params>> = {}, value?: Value) => {
+                if (builder.type === 'root') data = data.root
                 const property = get(data)
                 const p = { ...property.props, ...props }
                 if (!PD.areEqual(builder.defaultParams, property.props, p)) {
                     // this invalidates property.value
-                    set(data, p, undefined)
+                    set(data, p, value)
                 }
             }
         }

+ 0 - 1
src/mol-model-props/computed/accessible-surface-area.ts

@@ -22,7 +22,6 @@ export type AccessibleSurfaceAreaValue = AccessibleSurfaceArea
 export const AccessibleSurfaceAreaProvider: CustomStructureProperty.Provider<AccessibleSurfaceAreaParams, AccessibleSurfaceAreaValue> = CustomStructureProperty.createProvider({
     label: 'Accessible Surface Area',
     descriptor: CustomPropertyDescriptor({
-        isStatic: true,
         name: 'molstar_accessible_surface_area',
         // TODO `cifExport` and `symbol`
     }),

+ 0 - 1
src/mol-model-props/computed/interactions.ts

@@ -21,7 +21,6 @@ export type InteractionsValue = Interactions
 export const InteractionsProvider: CustomStructureProperty.Provider<InteractionsParams, InteractionsValue> = CustomStructureProperty.createProvider({
     label: 'Interactions',
     descriptor: CustomPropertyDescriptor({
-        isStatic: true,
         name: 'molstar_computed_interactions',
         // TODO `cifExport` and `symbol`
     }),

+ 0 - 1
src/mol-model-props/computed/secondary-structure.ts

@@ -48,7 +48,6 @@ export type SecondaryStructureValue = Map<number, SecondaryStructure>
 export const SecondaryStructureProvider: CustomStructureProperty.Provider<SecondaryStructureParams, SecondaryStructureValue> = CustomStructureProperty.createProvider({
     label: 'Secondary Structure',
     descriptor: CustomPropertyDescriptor({
-        isStatic: true,
         name: 'molstar_computed_secondary_structure',
         // TODO `cifExport` and `symbol`
     }),

+ 0 - 1
src/mol-model-props/computed/valence-model.ts

@@ -21,7 +21,6 @@ export type ValenceModelValue = Map<number, ValenceModel>
 export const ValenceModelProvider: CustomStructureProperty.Provider<ValenceModelParams, ValenceModelValue> = CustomStructureProperty.createProvider({
     label: 'Valence Model',
     descriptor: CustomPropertyDescriptor({
-        isStatic: true,
         name: 'molstar_computed_valence_model',
         // TODO `cifExport` and `symbol`
     }),

+ 0 - 1
src/mol-model-props/pdbe/preferred-assembly.ts

@@ -32,7 +32,6 @@ export namespace PDBePreferredAssembly {
     export type Schema = typeof Schema
 
     export const Descriptor = CustomPropertyDescriptor({
-        isStatic: true,
         name: 'pdbe_preferred_assembly',
         cifExport: {
             prefix: 'pdbe',

+ 0 - 1
src/mol-model-props/pdbe/struct-ref-domain.ts

@@ -40,7 +40,6 @@ export namespace PDBeStructRefDomain {
     export type Schema = typeof Schema
 
     export const Descriptor = CustomPropertyDescriptor({
-        isStatic: true,
         name: 'pdbe_struct_ref_domain',
         cifExport: {
             prefix: 'pdbe',

+ 87 - 93
src/mol-model-props/pdbe/structure-quality-report.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-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 { Column, Table } from '../../mol-data/db';
@@ -15,15 +16,27 @@ 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';
+import { CustomModelProperty } from '../common/custom-model-property';
+import { ParamDefinition as PD } from '../../mol-util/param-definition'
+import { CustomProperty } from '../common/custom-property';
 
-export namespace StructureQualityReport {
-    export type IssueMap = IndexedCustomProperty.Residue<string[]>
-    export type Property = PropertyWrapper<IssueMap | undefined>
+export { StructureQualityReport }
 
-    export function get(model: Model): Property | undefined {
-        // must be defined before the descriptor so it's not undefined.
-        return model._dynamicPropertyData.__PDBeStructureQualityReport__;
+type StructureQualityReport = PropertyWrapper<IndexedCustomProperty.Residue<string[]> | undefined>
+
+namespace StructureQualityReport {
+    export const DefaultServerUrl = 'https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/'
+    export function getEntryUrl(pdbId: string, serverUrl: string) {
+        return `${serverUrl}/${pdbId.toLowerCase()}`
+    }
+
+    export function isApplicable(model?: Model): boolean {
+        return (
+            !!model &&
+            model.sourceData.kind === 'mmCIF' &&
+            (model.sourceData.data.database_2.database_id.isDefined ||
+                model.entryId.length === 4)
+        )
     }
 
     export const Schema = {
@@ -43,12 +56,63 @@ export namespace StructureQualityReport {
     };
     export type Schema = typeof Schema
 
-    export const Descriptor = CustomPropertyDescriptor({
-        isStatic: false,
+    export function fromJson(model: Model, data: any) {
+        const info = PropertyWrapper.createInfo();
+        const issueMap = createIssueMapFromJson(model, data);
+        return { info, data: issueMap }
+    }
+
+    export async function fromServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<StructureQualityReport> {
+        const url = getEntryUrl(model.entryId, props.serverUrl)
+        const json = await ctx.fetch({ url, type: 'json' }).runInContext(ctx.runtime)
+        const data = json[model.entryId.toLowerCase()];
+        if (!data) throw new Error('missing data');
+        return fromJson(model, data)
+    }
+
+    export function fromCif(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): StructureQualityReport | undefined {
+        let info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
+        if (!info) return
+        const data = getCifData(model);
+        const issueMap = createIssueMapFromCif(model, data.residues, data.groups);
+        return { info, data: issueMap }
+    }
+
+    export async function fromCifOrServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<StructureQualityReport> {
+        return fromCif(ctx, model, props) || fromServer(ctx, model, props)
+    }
+
+    const _emptyArray: string[] = [];
+    export function getIssues(e: StructureElement.Location) {
+        if (!Unit.isAtomic(e.unit)) return _emptyArray;
+        const prop = StructureQualityReportProvider.get(e.unit.model).value;
+        if (!prop || !prop.data) return _emptyArray;
+        const rI = e.unit.residueIndex[e.element];
+        return prop.data.has(rI) ? prop.data.get(rI)! : _emptyArray;
+    }
+
+    function getCifData(model: Model) {
+        if (model.sourceData.kind !== 'mmCIF') throw new Error('Data format must be mmCIF.');
+        return {
+            residues: toTable(Schema.pdbe_structure_quality_report_issues, model.sourceData.frame.categories.pdbe_structure_quality_report_issues),
+            groups: toTable(Schema.pdbe_structure_quality_report_issue_types, model.sourceData.frame.categories.pdbe_structure_quality_report_issue_types),
+        }
+    }
+}
+
+export const StructureQualityReportParams = {
+    serverUrl: PD.Text(StructureQualityReport.DefaultServerUrl, { description: 'JSON API Server URL' })
+}
+export type StructureQualityReportParams = typeof StructureQualityReportParams
+export type StructureQualityReportProps = PD.Values<StructureQualityReportParams>
+
+export const StructureQualityReportProvider: CustomModelProperty.Provider<StructureQualityReportParams, StructureQualityReport> = CustomModelProperty.createProvider({
+    label: 'PDBe Structure Quality Report',
+    descriptor: CustomPropertyDescriptor<ReportExportContext, any>({
         name: 'pdbe_structure_quality_report',
         cifExport: {
             prefix: 'pdbe',
-            context(ctx) {
+            context(ctx: CifExportContext) {
                 return createExportContext(ctx);
             },
             categories: [
@@ -73,86 +137,16 @@ export namespace StructureQualityReport {
                 ctx => StructureQualityReport.getIssues(ctx.element).length),
             // TODO: add (hasIssue :: IssueType(extends string) -> boolean) symbol
         }
-    });
-
-    function getCifData(model: Model) {
-        if (model.sourceData.kind !== 'mmCIF') throw new Error('Data format must be mmCIF.');
-        return {
-            residues: toTable(Schema.pdbe_structure_quality_report_issues, model.sourceData.frame.categories.pdbe_structure_quality_report_issues),
-            groups: toTable(Schema.pdbe_structure_quality_report_issue_types, model.sourceData.frame.categories.pdbe_structure_quality_report_issue_types),
-        }
-    }
-
-    export function createAttachTask(mapUrl: (model: Model) => string, fetch: import('../../mol-util/data-source').AjaxTask) {
-        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 }).runInContext(ctx) as string;
-                const data = JSON.parse(dataStr)[model.entryId.toLowerCase()];
-                if (!data) return false;
-                info = PropertyWrapper.createInfo();
-                issueMap = createIssueMapFromJson(model, data);
-            }
-
-            model.customProperties.add(Descriptor);
-            set(model, { info, data: issueMap });
-            return false;
-        });
+    }),
+    type: 'static',
+    defaultParams: StructureQualityReportParams,
+    getParams: (data: Model) => StructureQualityReportParams,
+    isApplicable: (data: Model) => StructureQualityReport.isApplicable(data),
+    obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<StructureQualityReportProps>) => {
+        const p = { ...PD.getDefaultValues(StructureQualityReportParams), ...props }
+        return await StructureQualityReport.fromCifOrServer(ctx, data, p)
     }
-
-    export async function attachFromCifOrApi(model: Model, params: {
-        // optional JSON source
-        PDBe_apiSourceJson?: (model: Model) => Promise<any>
-    }) {
-        if (get(model)) return true;
-
-        let issueMap: IssueMap | undefined;
-        let info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
-        if (info) {
-            const data = getCifData(model);
-            issueMap = createIssueMapFromCif(model, data.residues, data.groups);
-        } else if (params.PDBe_apiSourceJson) {
-            const data = await params.PDBe_apiSourceJson(model);
-            if (!data) return false;
-            info = PropertyWrapper.createInfo();
-            issueMap = createIssueMapFromJson(model, data);
-        } else {
-            return false;
-        }
-
-        model.customProperties.add(Descriptor);
-        set(model, { info, data: issueMap });
-        return true;
-    }
-
-    function set(model: Model, prop: Property) {
-        (model._dynamicPropertyData.__PDBeStructureQualityReport__ as Property) = prop;
-    }
-
-    export function getIssueMap(model: Model): IssueMap | undefined {
-        const prop = get(model);
-        return prop && prop.data;
-    }
-
-    const _emptyArray: string[] = [];
-    export function getIssues(e: StructureElement.Location) {
-        if (!Unit.isAtomic(e.unit)) return _emptyArray;
-        const prop = StructureQualityReport.get(e.unit.model);
-        if (!prop || !prop.data) return _emptyArray;
-        const rI = e.unit.residueIndex[e.element];
-        return prop.data.has(rI) ? prop.data.get(rI)! : _emptyArray;
-    }
-}
+})
 
 const _structure_quality_report_issues_fields = CifWriter.fields<number, ReportExportContext['models'][0]>()
     .index('id')
@@ -176,7 +170,7 @@ function createExportContext(ctx: CifExportContext): ReportExportContext {
     let info: PropertyWrapper.Info = PropertyWrapper.createInfo();
 
     for (const s of ctx.structures) {
-        const prop = StructureQualityReport.get(s.model);
+        const prop = StructureQualityReportProvider.get(s.model).value;
         if (prop) info = prop.info;
         if (!prop || !prop.data) continue;
 
@@ -207,7 +201,7 @@ function createExportContext(ctx: CifExportContext): ReportExportContext {
     }
 }
 
-function createIssueMapFromJson(modelData: Model, data: any): StructureQualityReport.IssueMap | undefined {
+function createIssueMapFromJson(modelData: Model, data: any): StructureQualityReport['data'] | undefined {
     const ret = new Map<ResidueIndex, string[]>();
     if (!data.molecules) return;
 
@@ -233,7 +227,7 @@ function createIssueMapFromJson(modelData: Model, data: any): StructureQualityRe
 
 function createIssueMapFromCif(modelData: Model,
     residueData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issues>,
-    groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): StructureQualityReport.IssueMap | undefined {
+    groupData: Table<typeof StructureQualityReport.Schema.pdbe_structure_quality_report_issue_types>): StructureQualityReport['data'] | undefined {
 
     const ret = new Map<ResidueIndex, string[]>();
     const { label_entity_id, label_asym_id, auth_seq_id, pdbx_PDB_ins_code, issue_type_group_id, pdbx_PDB_model_num, _rowCount } = residueData;

+ 2 - 2
src/mol-model-props/pdbe/themes/structure-quality-report.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StructureQualityReport } from '../../../mol-model-props/pdbe/structure-quality-report';
+import { StructureQualityReport, StructureQualityReportProvider } from '../../../mol-model-props/pdbe/structure-quality-report';
 import { Location } from '../../../mol-model/location';
 import { StructureElement } from '../../../mol-model/structure';
 import { ColorTheme, LocationColor } from '../../../mol-theme/color';
@@ -31,7 +31,7 @@ const ValidationColorTable: [string, Color][] = [
 export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
     let color: LocationColor
 
-    if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
+    if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReportProvider.descriptor)) {
         const getIssues = StructureQualityReport.getIssues;
         color = (location: Location) => {
             if (StructureElement.Location.is(location)) {

+ 0 - 1
src/mol-model-props/rcsb/assembly-symmetry.ts

@@ -73,7 +73,6 @@ export type AssemblySymmetryValue = NonNullableArray<NonNullable<NonNullable<Ass
 export const AssemblySymmetryProvider: CustomStructureProperty.Provider<AssemblySymmetryParams, AssemblySymmetryValue> = CustomStructureProperty.createProvider({
     label: 'Assembly Symmetry',
     descriptor: CustomPropertyDescriptor({
-        isStatic: true,
         name: 'rcsb_struct_symmetry',
         // TODO `cifExport` and `symbol`
     }),

+ 0 - 1
src/mol-model/structure/common/custom-property.ts

@@ -12,7 +12,6 @@ import { UUID } from '../../../mol-util';
 export { CustomPropertyDescriptor, CustomProperties }
 
 interface CustomPropertyDescriptor<ExportCtx = CifExportContext, Symbols extends { [name: string]: QuerySymbolRuntime } = { }> {
-    readonly isStatic: boolean,
     readonly name: string,
 
     cifExport?: {

+ 3 - 2
src/mol-plugin/behavior/dynamic/custom-props.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-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>
@@ -11,4 +11,5 @@ export { SecondaryStructure } from './custom-props/computed/secondary-structure'
 export { ValenceModel } from './custom-props/computed/valence-model'
 
 export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report'
-export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'
+export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'
+export { RCSBValidationReport } from './custom-props/rcsb/validation-report'

+ 12 - 19
src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts

@@ -1,36 +1,26 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import { OrderedSet } from '../../../../../mol-data/int';
-import { StructureQualityReport } from '../../../../../mol-model-props/pdbe/structure-quality-report';
+import { StructureQualityReport, StructureQualityReportProvider } from '../../../../../mol-model-props/pdbe/structure-quality-report';
 import { StructureQualityReportColorTheme } from '../../../../../mol-model-props/pdbe/themes/structure-quality-report';
 import { Loci } from '../../../../../mol-model/loci';
 import { StructureElement } from '../../../../../mol-model/structure';
 import { ParamDefinition as PD } from '../../../../../mol-util/param-definition';
 import { PluginBehavior } from '../../../behavior';
 import { ThemeDataContext } from '../../../../../mol-theme/theme';
-import { CustomPropertyRegistry } from '../../../../../mol-model-props/common/custom-property-registry';
+import { CustomProperty } from '../../../../../mol-model-props/common/custom-property';
 
 export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
     name: 'pdbe-structure-quality-report-prop',
     category: 'custom-props',
     display: { name: 'PDBe Structure Quality Report' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
-        private attach = StructureQualityReport.createAttachTask(
-            m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.entryId.toLowerCase()}`,
-            this.ctx.fetch
-        );
 
-        private provider: CustomPropertyRegistry.ModelProvider = {
-            option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'],
-            descriptor: StructureQualityReport.Descriptor,
-            defaultSelected: this.params.autoAttach,
-            attachableTo: () => true,
-            attach: this.attach
-        }
+        private provider = StructureQualityReportProvider
 
         private labelPDBeValidation = (loci: Loci): string | undefined => {
             if (!this.params.showTooltip) return void 0;
@@ -40,7 +30,7 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
                     if (loci.elements.length === 0) return void 0;
                     const e = loci.elements[0];
                     const u = e.unit;
-                    if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0;
+                    if (!u.model.customProperties.has(StructureQualityReportProvider.descriptor)) return void 0;
 
                     const se = StructureElement.Location.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]);
                     const issues = StructureQualityReport.getIssues(se);
@@ -52,7 +42,7 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
         }
 
         register(): void {
-            this.ctx.customModelProperties.register(this.provider);
+            this.ctx.customModelProperties.register(this.provider, false);
             this.ctx.lociLabels.addProvider(this.labelPDBeValidation);
 
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', {
@@ -60,7 +50,10 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
                 factory: StructureQualityReportColorTheme,
                 getParams: () => ({}),
                 defaultValues: {},
-                isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
+                isApplicable: (ctx: ThemeDataContext) => StructureQualityReport.isApplicable(ctx.structure?.models[0]),
+                ensureCustomProperties: (ctx: CustomProperty.Context, data: ThemeDataContext) => {
+                    return data.structure ? StructureQualityReportProvider.attach(ctx, data.structure.models[0]) : Promise.resolve()
+                }
             })
         }
 
@@ -68,12 +61,12 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
             let updated = this.params.autoAttach !== p.autoAttach
             this.params.autoAttach = p.autoAttach;
             this.params.showTooltip = p.showTooltip;
-            this.provider.defaultSelected = p.autoAttach;
+            this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             return updated;
         }
 
         unregister() {
-            this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name);
+            this.ctx.customModelProperties.unregister(StructureQualityReportProvider.descriptor.name);
             this.ctx.lociLabels.removeProvider(this.labelPDBeValidation);
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report')
         }

+ 2 - 1
src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts

@@ -71,8 +71,9 @@ const AssemblySymmetryAxes3D = PluginStateTransform.BuiltIn({
             return new PluginStateObject.Shape.Representation3D({ repr, source: a }, { label: `Axes`, description: `${symbol} ${kind}` });
         });
     },
-    update({ a, b, newParams }) {
+    update({ a, b, newParams }, plugin: PluginContext) {
         return Task.create('RCSB Assembly Symmetry Axes', async ctx => {
+            await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data)
             await getAssemblySymmetryAxesRepresentation(ctx, a.data, newParams, b.data.repr);
             const { symbol, kind } = AssemblySymmetryProvider.get(a.data).value![newParams.symmetryIndex]
             b.description = `${symbol} ${kind}`

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

@@ -7,7 +7,6 @@
 
 import { List } from 'immutable';
 import { Canvas3D } from '../mol-canvas3d/canvas3d';
-import { CustomPropertyRegistry } from '../mol-model-props/common/custom-property-registry';
 import { StructureRepresentationRegistry } from '../mol-repr/structure/registry';
 import { VolumeRepresentationRegistry } from '../mol-repr/volume/registry';
 import { State, StateTransform, StateTransformer } from '../mol-state';
@@ -129,7 +128,7 @@ export class PluginContext {
         registry: new DataFormatRegistry()
     } as const
 
-    readonly customModelProperties = new CustomPropertyRegistry<Model>();
+    readonly customModelProperties = new CustomProperty.Registry<Model>();
     readonly customStructureProperties = new CustomProperty.Registry<Structure>();
     readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
 

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-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>
@@ -75,6 +75,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.CustomProps.ValenceModel),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true, showTooltip: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBValidationReport),
         PluginSpec.Behavior(StructureRepresentationInteraction)
     ],
     customParamEditors: [

+ 5 - 6
src/mol-plugin/state/actions/structure.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-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>
@@ -15,7 +15,7 @@ import { CustomModelProperties, StructureSelectionFromExpression, CustomStructur
 import { DataFormatProvider, guessCifVariant, DataFormatBuilderOptions } from './data-format';
 import { FileInfo } from '../../../mol-util/file-info';
 import { Task } from '../../../mol-task';
-import { StructureElement, Structure } from '../../../mol-model/structure';
+import { StructureElement } from '../../../mol-model/structure';
 import { createDefaultStructureComplex } from '../../util/structure-complex-helper';
 import { ModelStructureRepresentation } from '../representation/model';
 
@@ -372,11 +372,10 @@ export const UpdateTrajectory = StateAction.build({
 });
 
 export const EnableModelCustomProps = StateAction.build({
-    display: { name: 'Custom Model Properties', description: 'Enable the addition of custom properties to the model.' },
+    display: { name: 'Custom Model Properties', description: 'Enable parameters for custom properties of the model.' },
     from: PluginStateObject.Molecule.Model,
     params(a, ctx: PluginContext) {
-        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of model property descriptor ids.' }) };
-        return { properties: ctx.customModelProperties.getSelect(a.data) };
+        return ctx.customModelProperties.getParams(a?.data)
     },
     isApplicable(a, t, ctx: PluginContext) {
         return t.transformer !== CustomModelProperties;
@@ -390,7 +389,7 @@ export const EnableStructureCustomProps = StateAction.build({
     display: { name: 'Custom Structure Properties', description: 'Enable parameters for custom properties of the structure.' },
     from: PluginStateObject.Molecule.Structure,
     params(a, ctx: PluginContext) {
-        return ctx.customStructureProperties.getParams(a?.data || Structure.Empty)
+        return ctx.customStructureProperties.getParams(a?.data)
     },
     isApplicable(a, t, ctx: PluginContext) {
         return t.transformer !== CustomStructureProperties;

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

@@ -703,24 +703,29 @@ const CustomModelProperties = PluginStateTransform.BuiltIn({
     from: SO.Molecule.Model,
     to: SO.Molecule.Model,
     params: (a, ctx: PluginContext) => {
-        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) };
-        return { properties: ctx.customModelProperties.getSelect(a.data) };
+        return ctx.customModelProperties.getParams(a?.data)
     }
 })({
     apply({ a, params }, ctx: PluginContext) {
         return Task.create('Custom Props', async taskCtx => {
-            await attachModelProps(a.data, ctx, taskCtx, params.properties);
-            return new SO.Molecule.Model(a.data, { label: 'Model Props', description: `${params.properties.length} Selected` });
+            await attachModelProps(a.data, ctx, taskCtx, params);
+            return new SO.Molecule.Model(a.data, { label: 'Model Props' });
         });
     }
 });
-async function attachModelProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, names: string[]) {
-    for (const name of names) {
-        try {
-            const p = ctx.customModelProperties.get(name);
-            await p.attach(model).runInContext(taskCtx);
-        } catch (e) {
-            ctx.log.warn(`Error attaching model prop '${name}': ${e}`);
+async function attachModelProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, params: PD.Values<PD.Params>) {
+    const propertyCtx = { runtime: taskCtx, fetch: ctx.fetch }
+    for (const name of Object.keys(params)) {
+        const property = ctx.customModelProperties.get(name)
+        const props = params[name as keyof typeof params]
+        if (props.autoAttach) {
+            try {
+                await property.attach(propertyCtx, model, props)
+            } catch (e) {
+                ctx.log.warn(`Error attaching model prop '${name}': ${e}`);
+            }
+        } else {
+            property.set(model, props)
         }
     }
 }
@@ -732,7 +737,7 @@ const CustomStructureProperties = PluginStateTransform.BuiltIn({
     from: SO.Molecule.Structure,
     to: SO.Molecule.Structure,
     params: (a, ctx: PluginContext) => {
-        return ctx.customStructureProperties.getParams(a?.data || Structure.Empty)
+        return ctx.customStructureProperties.getParams(a?.data)
     }
 })({
     apply({ a, params }, ctx: PluginContext) {
@@ -748,9 +753,13 @@ async function attachStructureProps(structure: Structure, ctx: PluginContext, ta
         const property = ctx.customStructureProperties.get(name)
         const props = params[name as keyof typeof params]
         if (props.autoAttach) {
-            await property.attach(propertyCtx, structure, props)
+            try {
+                await property.attach(propertyCtx, structure, props)
+            } catch (e) {
+                ctx.log.warn(`Error attaching structure prop '${name}': ${e}`);
+            }
         } else {
-            property.setProps(structure, props)
+            property.set(structure, props)
         }
     }
 }

+ 7 - 9
src/perf-tests/mol-script.ts

@@ -8,7 +8,7 @@ import { parseMolScript } from '../mol-script/language/parser';
 import * as util from 'util'
 import { transpileMolScript } from '../mol-script/script/mol-script/symbols';
 import { formatMolScript } from '../mol-script/language/expression-formatter';
-import { StructureQualityReport } from '../mol-model-props/pdbe/structure-quality-report';
+import { StructureQualityReport, StructureQualityReportProvider } from '../mol-model-props/pdbe/structure-quality-report';
 import fetch from 'node-fetch';
 
 // import Examples from 'mol-script/script/mol-script/examples'
@@ -48,7 +48,6 @@ console.log(result);
 
 const CustomProp = CustomPropertyDescriptor({
     name: 'test_prop',
-    isStatic: true,
     cifExport: { prefix: '', categories: [ ]},
     symbols: {
         residueIndex: QuerySymbolRuntime.Dynamic(CustomPropSymbol('custom.test-prop', 'residue-index', Type.Num), ctx => {
@@ -61,18 +60,17 @@ const CustomProp = CustomPropertyDescriptor({
 
 DefaultQueryRuntimeTable.addCustomProp(CustomProp);
 
-DefaultQueryRuntimeTable.addCustomProp(StructureQualityReport.Descriptor);
+DefaultQueryRuntimeTable.addCustomProp(StructureQualityReportProvider.descriptor);
 
 export async function testQ() {
     const frame = await readCifFile('e:/test/quick/1cbs_updated.cif');
     const { structure } = await getModelsAndStructure(frame);
+    const model = structure.models[0]
 
-    await StructureQualityReport.attachFromCifOrApi(structure.models[0], {
-        PDBe_apiSourceJson: async model => {
-            const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.entryId.toLowerCase()}`, { timeout: 1500 });
-            return await rawData.json();
-        }
-    })
+    const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.entryId.toLowerCase()}`, { timeout: 1500 });
+    const data = StructureQualityReport.fromJson(model, await rawData.json());
+
+    StructureQualityReportProvider.set(model, { serverUrl: '' }, data)
 
     let expr = MolScriptBuilder.struct.generator.atomGroups({
         'atom-test': MolScriptBuilder.core.rel.eq([

+ 5 - 5
src/servers/model/properties/providers/pdbe.ts

@@ -7,7 +7,7 @@
 import * as fs from 'fs'
 import * as path from 'path'
 import { Model } from '../../../../mol-model/structure';
-import { StructureQualityReport } from '../../../../mol-model-props/pdbe/structure-quality-report';
+import { StructureQualityReportProvider, StructureQualityReport } from '../../../../mol-model-props/pdbe/structure-quality-report';
 import { fetchRetry } from '../../utils/fetch-retry';
 import { UUID } from '../../../../mol-util';
 import { PDBePreferredAssembly } from '../../../../mol-model-props/pdbe/preferred-assembly';
@@ -16,12 +16,12 @@ import { AttachModelProperty } from '../../property-provider';
 import { ConsoleLogger } from '../../../../mol-util/console-logger';
 import { getParam } from '../../../common/util';
 
-export const PDBe_structureQualityReport: AttachModelProperty = ({ model, params, cache }) => {
+export const PDBe_structureQualityReport: AttachModelProperty = async ({ model, params, cache }) => {
     const PDBe_apiSourceJson = useFileSource(params)
         ? residuewise_outlier_summary.getDataFromAggregateFile(getFilePrefix(params, 'residuewise_outlier_summary'))
-        : apiQueryProvider(getApiUrl(params, 'residuewise_outlier_summary', 'https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry'), cache);
-
-    return StructureQualityReport.attachFromCifOrApi(model, { PDBe_apiSourceJson });
+        : apiQueryProvider(getApiUrl(params, 'residuewise_outlier_summary', StructureQualityReport.DefaultServerUrl), cache);
+    const data = StructureQualityReport.fromJson(model, await PDBe_apiSourceJson(model))
+    return StructureQualityReportProvider.set(model, { serverUrl: StructureQualityReport.DefaultServerUrl }, data)
 }
 
 export const PDBe_preferredAssembly: AttachModelProperty = ({ model, params, cache }) => {