Procházet zdrojové kódy

Merge branch 'master' into dev-sb-v2

# Conflicts:
#	CHANGELOG.md
#	package-lock.json
#	package.json
#	src/viewer/index.ts
Sebastian Bittrich před 3 roky
rodič
revize
c2e989df8a

+ 4 - 0
CHANGELOG.md

@@ -18,6 +18,10 @@
   - The loading configuration includes an optional trajectory preset provider `TrajectoryHierarchyPresetProvider`
 - Remove `alignMotif` methods (& pecos-integration) as the strucmotif service now reports RMSD and transformations for all hits
 
+## [1.10.0] - 2021-11-22
+### Added
+- Dedicated UI for quality assessment & validation reports
+
 ## [1.9.6] - 2021-11-16
 ### General
 - Try to fix packing issues

+ 6 - 6
package.json

@@ -52,19 +52,19 @@
         "file-loader": "^6.2.0",
         "fs-extra": "^10.0.0",
         "mini-css-extract-plugin": "^2.3.0",
-        "molstar": "^2.3.7",
-        "node-sass": "^6.0.1",
+        "molstar": "^2.3.9",
         "path-browserify": "^1.0.1",
         "raw-loader": "^4.0.2",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
         "rxjs": "^7.3.1",
-        "sass-loader": "^12.1.0",
+        "sass": "^1.43.4",
+        "sass-loader": "^12.3.0",
         "stream-browserify": "^3.0.0",
         "style-loader": "^3.3.0",
         "tslib": "^2.3.1",
-        "typescript": "^4.4.3",
-        "webpack": "^5.56.0",
-        "webpack-cli": "^4.8.0"
+        "typescript": "^4.5.2",
+        "webpack": "^5.64.1",
+        "webpack-cli": "^4.9.1"
     }
 }

+ 4 - 1
src/viewer/index.ts

@@ -64,6 +64,7 @@ const DefaultViewerProps = {
     showStructureSourceControls: true,
     showSuperpositionControls: true,
     showMembraneOrientationPreset: false,
+    showValidationReportControls: true,
     /**
      * Needed when running outside of sierra. If set to true, the strucmotif UI will use an absolute URL to sierra-prod.
      * Otherwise, the link will be relative on the current host.
@@ -156,6 +157,7 @@ export class Viewer {
             showSessionControls: o.showSessionControls,
             showStructureSourceControls: o.showStructureSourceControls,
             showSuperpositionControls: o.showSuperpositionControls,
+            showValidationReportControls: o.showValidationReportControls,
             modelLoader: new ModelLoader(this._plugin),
             collapsed: new BehaviorSubject<CollapsedState>({
                 selection: true,
@@ -166,7 +168,8 @@ export class Viewer {
                 volume: true,
                 custom: true,
                 // this must be set to true as the Mp4Controls depends on the canvas which will be undefined at init() time
-                mp4export: true
+                mp4export: true,
+                validationReport: true
             }),
             detachedFromSierra: o.detachedFromSierra
         };

+ 3 - 0
src/viewer/types.ts

@@ -41,6 +41,7 @@ export type CollapsedState = {
     volume: boolean
     custom: boolean
     mp4export: boolean
+    validationReport: boolean
 }
 
 export interface ViewerState {
@@ -49,10 +50,12 @@ export interface ViewerState {
     showSessionControls: boolean
     showStructureSourceControls: boolean
     showSuperpositionControls: boolean
+    showValidationReportControls: boolean
     modelLoader: ModelLoader
     collapsed: BehaviorSubject<CollapsedState>
     detachedFromSierra: boolean
 }
+
 export function ViewerState(plugin: PluginContext) {
     return plugin.customState as ViewerState;
 }

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

@@ -17,6 +17,7 @@ import { VolumeStreamingControls } from 'molstar/lib/mol-plugin-ui/structure/vol
 import { SessionControls } from './session';
 import { StrucmotifSubmitControls } from './strucmotif';
 import { Mp4EncoderUI } from 'molstar/lib/extensions/mp4-export/ui';
+import { ValidationReportControls } from './validation';
 
 export class StructureTools extends PluginUIComponent {
     get customState() {
@@ -36,6 +37,7 @@ export class StructureTools extends PluginUIComponent {
             {this.customState.showSuperpositionControls && <StructureSuperpositionControls initiallyCollapsed={collapsed.superposition} />}
             <StructureComponentControls initiallyCollapsed={collapsed.component} />
             <VolumeStreamingControls header='Density' initiallyCollapsed={collapsed.volume} />
+            {this.customState.showValidationReportControls && <ValidationReportControls initiallyCollapsed={collapsed.validationReport} />}
             <CustomStructureControls initiallyCollapsed={collapsed.custom} />
             <Mp4EncoderUI initiallyCollapsed={collapsed.mp4export}/>
         </>;

+ 116 - 0
src/viewer/ui/validation.tsx

@@ -0,0 +1,116 @@
+/**
+ * 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 { StructureHierarchyManager } from 'molstar/lib/mol-plugin-state/manager/structure/hierarchy';
+import { ValidationReport } from 'molstar/lib/extensions/rcsb/validation-report/prop';
+import { ValidationReportGeometryQualityPreset } from 'molstar/lib/extensions/rcsb/validation-report/behavior';
+import { ActionMenu } from 'molstar/lib/mol-plugin-ui/controls/action-menu';
+import { Model } from 'molstar/lib/mol-model/structure/model';
+import { MmcifFormat } from 'molstar/lib/mol-model-formats/structure/mmcif';
+
+interface ValidationReportState extends CollapsableState {
+    errorStates: Set<string>
+}
+
+const ValidationReportTag = 'validation-report';
+
+const _QualityIcon = <svg width='50px' height='50px' viewBox='0 0 38 47'>
+    <g strokeWidth='4' fill='none'>
+        <path d='m19 4.8c-3.7 3.6-9 5.8-15 5.8v4.3c0 25 14 29 14 29s16-4.5 16-29v-4.3c-6 0-11-2.3-15-5.8z' stroke='#000' strokeLinecap='square' strokeMiterlimit='10'/>
+        <path d='m13 23 3.5 3.5 9.4-9.4' stroke='#000'/>
+    </g>
+</svg>;
+export function QualityIconSvg() { return _QualityIcon; }
+
+/**
+ * A high-level component that gives access to the validation report preset.
+ */
+export class ValidationReportControls extends CollapsableControls<{}, ValidationReportState> {
+    protected defaultState() {
+        return {
+            header: 'Quality Assessment',
+            isCollapsed: false,
+            isHidden: true,
+            errorStates: new Set<string>(),
+            brand: { accent: 'cyan' as const, svg: QualityIconSvg }
+        };
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => {
+            const { errorStates, description } = this.state;
+            const nextDescription = StructureHierarchyManager.getSelectedStructuresDescription(this.plugin);
+            this.setState({
+                isHidden: !this.canEnable(),
+                // if structure is unchanged then keep old error states
+                errorStates: nextDescription === description ? errorStates : new Set<string>(),
+                description: nextDescription
+            });
+        });
+    }
+
+    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;
+        return pivot.obj.data.models.length === 1 && ValidationReport.isApplicable(pivot.obj.data.models[0]);
+    }
+
+    get noValidationReport() {
+        const structure = this.pivot.cell.obj?.data;
+        if (!structure || structure.models.length !== 1) return true;
+        const model = structure.models[0];
+        return !model || !this.isFromPdbArchive(model);
+    }
+
+    isFromPdbArchive(model: Model) {
+        if (!MmcifFormat.is(model.sourceData)) return false;
+        return model.entryId.match(/^[1-9][a-z0-9]{3}$/i) !== null ||
+            model.entryId.match(/^pdb_[0-9]{4}[1-9][a-z0-9]{3}$/i) !== null;
+    }
+
+    requestValidationReportPreset = async () => {
+        try {
+            await ValidationReportGeometryQualityPreset.apply(this.pivot.cell, Object.create(null), this.plugin);
+        } catch (err) {
+            // happens e.g. for 2W4S
+            this.setState(({ errorStates }) => {
+                const errors = new Set(errorStates);
+                errors.add(ValidationReportTag);
+                return { errorStates: errors };
+            });
+        }
+    }
+
+    get actions(): ActionMenu.Items {
+        // TODO this could support other kinds of reports/validation like the AlphaFold confidence scores
+        const noValidationReport = this.noValidationReport;
+        const validationReportError = this.state.errorStates.has(ValidationReportTag);
+        return [
+            {
+                kind: 'item',
+                label: validationReportError ? 'Failed to Obtain Validation Report' : (noValidationReport ? 'No Validation Report Available' : 'RCSB PDB Validation Report'),
+                value: this.requestValidationReportPreset,
+                disabled: noValidationReport || validationReportError
+            },
+        ];
+    }
+
+    selectAction: ActionMenu.OnSelect = item => {
+        if (!item) return;
+        (item?.value as any)();
+    }
+
+    renderControls() {
+        return <ActionMenu items={this.actions} onSelect={this.selectAction} />;
+    }
+}