Browse Source

Merge branch 'master' into gl-lines

Alexander Rose 6 years ago
parent
commit
56a56a4edb

+ 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>

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

@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureQualityReport } 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';
+import { ThemeDataContext } from 'mol-theme/theme';
+import { Color } from 'mol-util/color';
+import { TableLegend } from 'mol-util/color/tables';
+
+const ValidationColors = [
+    Color.fromRgb(170, 170, 170), // not applicable
+    Color.fromRgb(0, 255, 0), // 0 issues
+    Color.fromRgb(255, 255, 0), // 1
+    Color.fromRgb(255, 128, 0), // 2
+    Color.fromRgb(255, 0, 0), // 3 or more
+]
+
+const ValidationColorTable: [string, Color][] = [
+    ['No Issues', ValidationColors[1]],
+    ['One Issue', ValidationColors[2]],
+    ['Two Issues', ValidationColors[3]],
+    ['Three Or More Issues', ValidationColors[4]],
+    ['Not Applicable', ValidationColors[9]]
+]
+
+export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
+    let color: LocationColor
+
+    if (ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
+        const getIssues = StructureQualityReport.getIssues;
+        color = (location: Location) => {
+            if (StructureElement.isLocation(location)) {
+                return ValidationColors[Math.min(3, getIssues(location).length) + 1];
+            }
+            return ValidationColors[0];
+        }
+    } else {
+        color = () => ValidationColors[0];
+    }
+
+    return {
+        factory: StructureQualityReportColorTheme,
+        granularity: 'group',
+        color: color,
+        props: props,
+        description: 'Assigns residue colors according to the number of issues in the PDBe Validation Report.',
+        legend: TableLegend(ValidationColorTable)
+    }
+}

+ 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
 }

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

@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2018 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 { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report';
+import { Loci } from 'mol-model/loci';
+import { StructureElement } from 'mol-model/structure';
+import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginBehavior } from '../behavior';
+
+export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'pdbe-structure-quality-report-prop',
+    display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
+    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);
+
+            // TODO: support filtering of themes based on the input structure
+            // in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
+            // TODO: add remove functionality
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', {
+                label: 'PDBe Structure Quality Report',
+                factory: StructureQualityReportColorTheme,
+                getParams: () => ({})
+            })
+        }
+
+        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);
+
+            // TODO: add remove functionality to registry
+            // this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report')
+        }
+    },
+    params: () => ({
+        autoAttach: PD.Boolean(false)
+    })
+});
+
+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 })
     ]
 }
 

+ 31 - 13
src/mol-plugin/state/actions/basic.ts

@@ -27,10 +27,23 @@ const DownloadStructure = StateAction.build({
     display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
     params: {
         source: PD.MappedStatic('bcif-static', {
-            'pdbe-updated': PD.Text('1cbs', { label: 'Id' }),
-            'rcsb': PD.Text('1tqn', { label: 'Id' }),
-            'bcif-static': PD.Text('1tqn', { label: 'Id' }),
-            'url': PD.Group({ url: PD.Text(''), isBinary: PD.Boolean(false) }, { isExpanded: true })
+            'pdbe-updated': PD.Group({
+                id: PD.Text('1cbs', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'rcsb': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'bcif-static': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'url': PD.Group({
+                url: PD.Text(''),
+                isBinary: PD.Boolean(false),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true })
         }, {
             options: [
                 ['pdbe-updated', 'PDBe Updated'],
@@ -50,19 +63,19 @@ const DownloadStructure = StateAction.build({
             url = src.params;
             break;
         case 'pdbe-updated':
-            url = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params}` };
+            url = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
             break;
         case 'rcsb':
-            url = { url: `https://files.rcsb.org/download/${src.params.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params}` };
+            url = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
             break;
         case 'bcif-static':
-            url = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params}` };
+            url = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
             break;
         default: throw new Error(`${(src as any).name} not supported.`);
     }
 
     const data = b.toRoot().apply(StateTransforms.Data.Download, url);
-    return state.update(createStructureTree(data));
+    return state.update(createStructureTree(data, params.source.params.supportProps));
 });
 
 export const OpenStructure = StateAction.build({
@@ -72,15 +85,20 @@ export const OpenStructure = StateAction.build({
 })(({ params, state }) => {
     const b = state.build();
     const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
-    return state.update(createStructureTree(data));
+    return state.update(createStructureTree(data, false));
 });
 
-function createStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>): StateTree {
-    const root = b
+function createStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, supportProps: boolean): StateTree {
+    let root = b
         .apply(StateTransforms.Data.ParseCif)
         .apply(StateTransforms.Model.TrajectoryFromMmCif, {})
-        .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-        .apply(StateTransforms.Model.StructureAssemblyFromModel);
+        .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+
+    if (supportProps) {
+        // TODO: implement automatic default property assigment in State.update
+        root = root.apply(StateTransforms.Model.CustomModelProperties, { properties: [] });
+    }
+    root = root.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: 'Props', description: `${params.properties.length} Selected` });
+        });
+    }
+});
+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);
+    }
+}

+ 1 - 1
src/mol-plugin/ui/controls.tsx

@@ -45,7 +45,7 @@ export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArr
     }
 
     render() {
-        return <div>
+        return <div style={{ textAlign: 'right' }}>
             {this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)}
         </div>
     }

+ 7 - 1
src/mol-plugin/ui/controls/parameters.tsx

@@ -284,6 +284,12 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
         const params = this.props.param.params;
         const label = this.props.param.label || camelCaseToWords(this.props.name);
 
+        const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
+
+        if (this.props.param.isFlat) {
+            return controls;
+        }
+
         return <div className='msp-control-group-wrapper'>
             <div className='msp-control-group-header'>
                 <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
@@ -292,7 +298,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
                 </button>
             </div>
             {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
-                <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
+                {controls}
             </div>
             }
         </div>

+ 6 - 3
src/mol-plugin/ui/plugin.tsx

@@ -96,6 +96,7 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
     private wrapper = React.createRef<HTMLDivElement>();
 
     componentDidMount() {
+        // TODO: only show last 100 entries.
         this.subscribe(this.plugin.events.log, e => this.setState({ entries: this.state.entries.push(e) }));
     }
 
@@ -111,10 +112,12 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
     }
 
     render() {
-        return <div ref={this.wrapper} style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}>
-            <ul style={{ listStyle: 'none' }} className='msp-log-list'>
+        return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}>
+            <ul className='msp-list-unstyled'>
                 {this.state.entries.map((e, i) => <li key={i}>
-                    <b>[{formatTime(e!.timestamp)} | {e!.type}]</b> {e!.message}
+                    <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} />
+                    <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div>
+                    <div className='msp-log-entry'>{e!.message}</div>
                 </li>)}
             </ul>
         </div>;

+ 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>
+    }
+}

+ 4 - 2
src/mol-util/param-definition.ts

@@ -141,11 +141,13 @@ export namespace ParamDefinition {
     export interface Group<T> extends Base<T> {
         type: 'group',
         params: Params,
-        isExpanded?: boolean
+        isExpanded?: boolean,
+        isFlat?: boolean
     }
-    export function Group<P extends Params>(params: P, info?: Info & { isExpanded?: boolean }): Group<Values<P>> {
+    export function Group<P extends Params>(params: P, info?: Info & { isExpanded?: boolean, isFlat?: boolean }): Group<Values<P>> {
         const ret = setInfo<Group<Values<P>>>({ type: 'group', defaultValue: getDefaultValues(params) as any, params }, info);
         if (info && info.isExpanded) ret.isExpanded = info.isExpanded;
+        if (info && info.isFlat) ret.isFlat = info.isFlat;
         return ret;
     }