Browse Source

draft for membrane orientation UI

JonStargaryen 4 years ago
parent
commit
c700edfbdd
5 changed files with 203 additions and 1 deletions
  1. 4 0
      src/viewer/index.html
  2. 6 1
      src/viewer/index.ts
  3. 2 0
      src/viewer/types.ts
  4. 2 0
      src/viewer/ui/controls.tsx
  5. 189 0
      src/viewer/ui/membrane.tsx

+ 4 - 0
src/viewer/index.html

@@ -285,6 +285,10 @@
                             label_asym_id: 'E'
                         }
                     }
+                },
+                {
+                    id: '3SN6',
+                    info: 'membrane: Crystal structure of the beta2 adrenergic receptor-Gs protein complex'
                 }
             ];
 

+ 6 - 1
src/viewer/index.ts

@@ -40,6 +40,7 @@ import { StructureRepresentationRegistry } from 'molstar/lib/mol-repr/structure/
 import { Mp4Export } from 'molstar/lib/extensions/mp4-export';
 import { DefaultPluginUISpec, PluginUISpec } from 'molstar/lib/mol-plugin-ui/spec';
 import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
+import { ANVILMembraneOrientation } from 'molstar/lib/extensions/anvil/behavior';
 
 /** package version, filled in at bundle build time */
 declare const __RCSB_MOLSTAR_VERSION__: string;
@@ -53,7 +54,8 @@ export const BUILD_DATE = new Date(BUILD_TIMESTAMP);
 const Extensions = {
     'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
     'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
-    'mp4-export': PluginSpec.Behavior(Mp4Export)
+    'mp4-export': PluginSpec.Behavior(Mp4Export),
+    'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation)
 };
 
 const DefaultViewerProps = {
@@ -62,6 +64,7 @@ const DefaultViewerProps = {
     showSessionControls: false,
     showStructureSourceControls: true,
     showSuperpositionControls: true,
+    showMembraneOrientationControls: true,
     modelUrlProviders: [
         (pdbId: string) => ({
             url: `//models.rcsb.org/${pdbId.toLowerCase()}.bcif`,
@@ -152,6 +155,7 @@ export class Viewer {
             showSessionControls: o.showSessionControls,
             showStructureSourceControls: o.showStructureSourceControls,
             showSuperpositionControls: o.showSuperpositionControls,
+            showMembraneOrientationControls: o.showMembraneOrientationControls,
             modelLoader: new ModelLoader(this.plugin),
             collapsed: new BehaviorSubject<CollapsedState>({
                 selection: true,
@@ -161,6 +165,7 @@ export class Viewer {
                 component: false,
                 volume: true,
                 custom: true,
+                membrane: true
             })
         };
 

+ 2 - 0
src/viewer/types.ts

@@ -40,6 +40,7 @@ export type CollapsedState = {
     component: boolean
     volume: boolean
     custom: boolean
+    membrane: boolean
 }
 
 export interface ViewerState {
@@ -48,6 +49,7 @@ export interface ViewerState {
     showSessionControls: boolean
     showStructureSourceControls: boolean
     showSuperpositionControls: boolean
+    showMembraneOrientationControls: boolean
     modelLoader: ModelLoader
     collapsed: BehaviorSubject<CollapsedState>
 }

+ 2 - 0
src/viewer/ui/controls.tsx

@@ -16,6 +16,7 @@ import { StructureComponentControls } from 'molstar/lib/mol-plugin-ui/structure/
 import { VolumeStreamingControls } from 'molstar/lib/mol-plugin-ui/structure/volume';
 import { SessionControls } from './session';
 import { StrucmotifSubmitControls } from './strucmotif';
+import { MembraneOrientationControls } from './membrane';
 
 export class StructureTools extends PluginUIComponent {
     get customState() {
@@ -35,6 +36,7 @@ export class StructureTools extends PluginUIComponent {
             {this.customState.showSuperpositionControls && <StructureSuperpositionControls initiallyCollapsed={collapsed.superposition} />}
             <StructureComponentControls initiallyCollapsed={collapsed.component} />
             <VolumeStreamingControls header='Density' initiallyCollapsed={collapsed.volume} />
+            <MembraneOrientationControls initiallyCollapsed={collapsed.membrane} />
             <CustomStructureControls initiallyCollapsed={collapsed.custom} />
         </>;
     }

+ 189 - 0
src/viewer/ui/membrane.tsx

@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
+ */
+
+import { CollapsableControls, CollapsableState } from 'molstar/lib/mol-plugin-ui/base';
+import { StateTransform } from 'molstar/lib/mol-state/transform';
+import { StructureHierarchyManager } from 'molstar/lib/mol-plugin-state/manager/structure/hierarchy';
+import { ApplyActionControl } from 'molstar/lib/mol-plugin-ui/state/apply-action';
+import { CheckSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
+import { StateAction, StateSelection } from 'molstar/lib/mol-state';
+import { PluginStateObject } from 'molstar/lib/mol-plugin-state/objects';
+import { PluginContext } from 'molstar/lib/mol-plugin/context';
+import { Task } from 'molstar/lib/mol-task';
+import { MembraneOrientationPreset, tryCreateMembraneOrientation } from 'molstar/lib/extensions/anvil/behavior';
+import { ParameterControls } from 'molstar/lib/mol-plugin-ui/controls/parameters';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import {
+    MembraneOrientation,
+    MembraneOrientationProps,
+    MembraneOrientationProvider
+} from 'molstar/lib/extensions/anvil/prop';
+
+interface MembraneOrientationControlState extends CollapsableState {
+    isBusy: boolean
+    error: boolean
+}
+
+/**
+ * The component that exposes the ANVIL functionality.
+ */
+export class MembraneOrientationControls extends CollapsableControls<{}, MembraneOrientationControlState> {
+    protected defaultState() {
+        return {
+            header: 'Membrane Layer',
+            isCollapsed: false,
+            isBusy: false,
+            isHidden: true,
+            brand: { accent:  'gray' as const, svg: MembraneIconSvg },
+            error: false
+        };
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => {
+            this.setState({
+                isHidden: !this.canEnable(),
+                description: StructureHierarchyManager.getSelectedStructuresDescription(this.plugin)
+            });
+        });
+        this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
+            if (StateTransform.hasTag(e.cell.transform, MembraneOrientation.Tag.Representation)) this.forceUpdate();
+        });
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => {
+            this.setState({ isBusy: v });
+        });
+    }
+
+    get pivot() {
+        return this.plugin.managers.structure.hierarchy.selection.structures[0];
+    }
+
+    canEnable() {
+        const { selection } = this.plugin.managers.structure.hierarchy;
+        if (selection.structures.length !== 1) return false;
+        const pivot = this.pivot.cell;
+        if (!pivot.obj) return false;
+        // TODO check if delegate makes sense
+        return true;
+    }
+
+    renderEnable() {
+        const pivot = this.pivot;
+        if (!pivot.cell.parent) return null;
+        return <ApplyActionControl state={pivot.cell.parent} action={EnableMembraneOrientation} initiallyCollapsed={true} nodeRef={pivot.cell.transform.ref} simpleApply={{ header: 'Enable', icon: CheckSvg }} />;
+    }
+
+    renderNoMembraneProtein() {
+        // TODO allow to 'force' calculation?
+        return <div className='msp-row-text'>
+            <div>Not registered as Membrane Protein</div>
+        </div>;
+    }
+
+    renderError() {
+        return <div className='msp-row-text'>
+
+        </div>;
+    }
+
+    get params() {
+        const structure = this.pivot.cell.obj?.data;
+        const params = PD.clone(structure ? MembraneOrientationProvider.getParams(structure) : MembraneOrientationProvider.defaultParams);
+        // TODO more?
+        return params;
+    }
+
+    get values() {
+        const structure = this.pivot.cell.obj?.data;
+        if (structure) {
+            return MembraneOrientationProvider.props(structure);
+        } else {
+            return PD.getDefaultValues(MembraneOrientationProvider.defaultParams);
+        }
+    }
+
+    async updateMembraneOrientation(values: MembraneOrientationProps) {
+        const s = this.pivot;
+        const currValues = MembraneOrientationProvider.props(s.cell.obj!.data);
+        if (PD.areEqual(MembraneOrientationProvider.defaultParams, currValues, values)) return;
+
+        if (s.properties) {
+            const b = this.plugin.state.data.build();
+            b.to(s.properties.cell).update(old => {
+                old.properties[MembraneOrientationProvider.descriptor.name] = values;
+            });
+            await b.commit();
+        } else {
+            const pd = this.plugin.customStructureProperties.getParams(s.cell.obj?.data);
+            const params = PD.getDefaultValues(pd);
+            params.properties[MembraneOrientationProvider.descriptor.name] = values;
+            await this.plugin.builders.structure.insertStructureProperties(s.cell, params);
+        }
+
+        for (const components of this.plugin.managers.structure.hierarchy.currentComponentGroups) {
+            tryCreateMembraneOrientation(this.plugin, s.cell);
+            await this.plugin.managers.structure.component.updateRepresentationsTheme(components, { color: 'default' });
+        }
+    }
+
+    paramsOnChange = (options: MembraneOrientationProps) => {
+        this.updateMembraneOrientation(options);
+    }
+
+    get hasMembraneOrientation() {
+        return !this.pivot.cell.parent || !!StateSelection.findTagInSubtree(this.pivot.cell.parent.tree, this.pivot.cell.transform.ref, MembraneOrientation.Tag.Representation);
+    }
+
+    get enable() {
+        return !this.hasMembraneOrientation;
+    }
+
+    get noMembraneProtein() {
+        const structure = this.pivot.cell.obj?.data;
+        const data = structure && MembraneOrientationProvider.get(structure).value;
+        return !!data;
+    }
+
+    renderParams() {
+        return <>
+            <ParameterControls params={this.params} values={this.values} onChangeValues={this.paramsOnChange} />
+        </>;
+    }
+
+    renderControls() {
+        if (!this.pivot) return null;
+        if (this.noMembraneProtein) return this.renderNoMembraneProtein();
+        if (!this.noMembraneProtein && this.state.error) return this.renderError();
+        if (this.enable) return this.renderEnable();
+        return this.renderParams();
+    }
+}
+
+const EnableMembraneOrientation = StateAction.build({
+    from: PluginStateObject.Molecule.Structure,
+})(({ a, ref, state }, plugin: PluginContext) => Task.create('Enable Membrane Orientation', async ctx => {
+    await MembraneOrientationPreset.apply(ref, Object.create(null), plugin);
+}));
+
+const _MembraneIcon = <svg width='24px' height='24px' viewBox='0 0 12 12'>
+    <rect x="6.71" y="2.45" width="1.96" height="7.1" ry=".979" fill="none" strokeLinejoin="round"/>
+    <rect x="3.33" y="2.45" width="1.96" height="7.1" ry=".979" fill="none" strokeLinejoin="round"/>
+    <g>
+        <ellipse cx="1.77" cy="4.64" rx=".433" ry=".456"/>
+        <ellipse cx=".685" cy="4.64" rx=".433" ry=".456"/>
+        <ellipse cx=".685" cy="7.36" rx=".433" ry=".456"/>
+        <ellipse cx="1.77" cy="7.36" rx=".433" ry=".456"/>
+        <ellipse cx="9.19" cy="7.36" rx=".433" ry=".456"/>
+        <ellipse cx="11.3" cy="7.36" rx=".433" ry=".456"/>
+        <ellipse cx="10.3" cy="4.64" rx=".433" ry=".456"/>
+        <ellipse cx="9.19" cy="4.64" rx=".433" ry=".456"/>
+        <ellipse cx="2.86" cy="4.64" rx=".433" ry=".456"/>
+        <ellipse cx="2.86" cy="7.36" rx=".433" ry=".456"/>
+        <ellipse cx="10.3" cy="7.36" rx=".433" ry=".456"/>
+        <ellipse cx="11.3" cy="4.64" rx=".433" ry=".456"/>
+    </g>
+</svg>;
+export function MembraneIconSvg() { return _MembraneIcon; }