ソースを参照

Merge branch 'dev-sb-v2' of github.com:rcsb/rcsb-molstar into dev-sb-v2

# Conflicts:
#	src/viewer/helpers/viewer.ts
bioinsilico 3 年 前
コミット
e07513ac09

+ 34 - 3
.eslintrc.json

@@ -31,14 +31,38 @@
         "no-unsafe-finally": "warn",
         "no-var": "error",
         "spaced-comment": "error",
-        "semi": "warn"
+        "semi": "warn",
+        "no-restricted-syntax": [
+            "error",
+            {
+                "selector": "ExportDefaultDeclaration",
+                "message": "Default exports are not allowed"
+            }
+        ],
+        "no-throw-literal": "error",
+        "key-spacing": "error",
+        "object-curly-spacing": ["error", "always"],
+        "array-bracket-spacing": "error",
+        "space-in-parens": "error",
+        "computed-property-spacing": "error",
+        "prefer-const": ["error", {
+            "destructuring": "all",
+            "ignoreReadBeforeAssign": false
+        }],
+        "space-before-function-paren": "off",
+        "func-call-spacing": "off",
+        "no-multi-spaces": "error",
+        "block-spacing": "error",
+        "keyword-spacing": "off",
+        "space-before-blocks": "error",
+        "semi-spacing": "error"
     },
     "overrides": [
         {
             "files": ["**/*.ts", "**/*.tsx"],
             "parser": "@typescript-eslint/parser",
             "parserOptions": {
-                "project": ["tsconfig.json"],
+                "project": ["tsconfig.json", "tsconfig.commonjs.json"],
                 "sourceType": "module"
             },
             "plugins": [
@@ -82,7 +106,14 @@
                     "error",
                     "1tbs", { "allowSingleLine": true }
                 ],
-                "@typescript-eslint/comma-spacing": "error"
+                "@typescript-eslint/comma-spacing": "error",
+                "@typescript-eslint/space-before-function-paren": ["error", {
+                    "anonymous": "always",
+                    "named": "never",
+                    "asyncArrow": "always"
+                }],
+                "@typescript-eslint/func-call-spacing": ["error"],
+                "@typescript-eslint/keyword-spacing": ["error"]
             }
         }
     ]

+ 57 - 1
CHANGELOG.md

@@ -15,7 +15,63 @@
   - `positions` and `selection` props renamed to `targets` of type `Target[]`
 - Changed loading methods signature 
   - Added optional configuration parameter `config?: {props?: PresetProps; matrix?: Mat4; reprProvider?: TrajectoryHierarchyPresetProvider, params?: P}`
-  - The loading configuration includes an optional trajectory preset provider `TrajectoryHierarchyPresetProvider` 
+  - 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.9.1] - 2021-10-14
+### Bug fixes
+- More robust 'chain-mode' check in feature preset
+- Don't expand MP4 export controls as part of the symmetry preset
+
+## [1.9.0] - 2021-09-29
+### Added
+- Integrate PDBe/AlphaFold confidence coloring theme
+
+## [1.8.8] - 2021-09-28
+### General
+- Mol* 2.3.1
+
+## [1.8.7] - 2021-08-25
+### General
+- Mol* 2.2.3
+
+## [1.8.6] - 2021-08-23
+### General
+- Display warning if membrane preset calculation fails
+- Add fallback if membrane preset calculation fails
+
+## [1.8.5] - 2021-08-11
+### General
+- Update strucmotif integration with rcsb.org
+
+## [1.8.4] - 2021-08-03
+### General
+- Mol* 2.2.1
+- Reset camera for membrane preset
+
+## [1.8.3] - 2021-07-23
+### General
+- Rename 'Structural Motif Search' to 'Structure Motif Search'
+
+## [1.8.2] - 2021-07-20
+### Bug fixes
+- Post-pare for pecos API changes
+
+## [1.8.1] - 2021-07-16
+### Added
+- Prepare for pecos API changes
+
+## [1.8.0] - 2021-07-13
+### Added
+- Moved code for motif alignment (i.e., talking to pecos) here
+
+## [1.7.4] - 2021-07-12
+### Bug fixes
+- structure selection: handle selection of full chains
+
+## [1.7.3] - 2021-07-08
+### Bug fixes
+- Strucmotif: relative URLs when running inside of sierra
 
 ## [1.7.2] - 2021-07-05
 ### Bug fixes

+ 9 - 0
README.md

@@ -5,6 +5,15 @@
 RCSB PDB implementation of [Mol* (/'mol-star/)](https://github.com/molstar/molstar).
 Try it [here](https://rcsb.org/3d-view/).
 
+PDBe also maintains a flavor of Mol* called [PDBe Molstar](https://github.com/PDBeurope/pdbe-molstar).
+
+## Functionality
+Provides custom features used in the Mol* viewer on [rcsb.org](https://www.rcsb.org/3d-view):
+- visualization of structure alignment
+- visualization of structure motifs & UI to launch structure motif queries
+- interactivity functionality to highlight and add representations for selections of a structure, used in the [3D Protein Feature View](https://www.rcsb.org/3d-sequence/4hhb)
+- linkable focus representation on ligands or chains
+
 ## Install
     npm install @rcsb/rcsb-molstar
 

ファイルの差分が大きいため隠しています
+ 332 - 356
package-lock.json


+ 27 - 23
package.json

@@ -1,6 +1,6 @@
 {
     "name": "@rcsb/rcsb-molstar",
-    "version": "2.0.0-dev.5",
+    "version": "2.0.0-dev.6",
     "description": "RCSB PDB apps and props based on Mol*.",
     "homepage": "https://github.com/rcsb/rcsb-molstar#readme",
     "repository": {
@@ -20,7 +20,7 @@
         "watch": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack\"",
         "watch-tsc": "tsc --watch --incremental",
         "watch-extra": "cpx \"src/**/*.{scss,html,ico}\" build/src/ --watch",
-        "watch-webpack": "webpack -w --mode development --display minimal",
+        "watch-webpack": "webpack -w --mode development --stats minimal",
         "serve": "http-server -p 1335",
         "preversion": "npm run test",
         "version": "npm run build",
@@ -37,29 +37,33 @@
     "author": "RCSB PDB and Mol* Contributors",
     "license": "MIT",
     "devDependencies": {
-        "@types/react": "^17.0.11",
-        "@types/react-dom": "^17.0.0",
-        "@typescript-eslint/eslint-plugin": "^4.9.1",
-        "@typescript-eslint/parser": "^4.9.1",
-        "concurrently": "^5.3.0",
-        "cpx2": "^3.0.0",
-        "css-loader": "^5.0.1",
-        "eslint": "^7.15.0",
+        "@types/react": "^17.0.20",
+        "@types/react-dom": "^17.0.9",
+        "@typescript-eslint/eslint-plugin": "^4.31.0",
+        "@typescript-eslint/parser": "^4.31.0",
+        "buffer": "^6.0.3",
+        "concurrently": "^6.2.1",
+        "cpx2": "^3.0.2",
+        "crypto-browserify": "^3.12.0",
+        "css-loader": "^6.2.0",
+        "eslint": "^7.32.0",
         "extra-watch-webpack-plugin": "^1.0.3",
         "file-loader": "^6.2.0",
-        "fs-extra": "^9.0.1",
-        "mini-css-extract-plugin": "^1.3.2",
-        "molstar": "^2.0.7",
-        "node-sass": "^5.0.0",
+        "fs-extra": "^10.0.0",
+        "mini-css-extract-plugin": "^2.3.0",
+        "molstar": "^2.3.1",
+        "node-sass": "^6.0.1",
+        "path-browserify": "^1.0.1",
         "raw-loader": "^4.0.2",
-        "react": "^17.0.1",
-        "react-dom": "^17.0.1",
-        "rxjs": "^6.6.6",
-        "sass-loader": "^10.1.0",
-        "style-loader": "^2.0.0",
-        "tslib": "^2.1.0",
-        "typescript": "^4.2.3",
-        "webpack": "^4.44.1",
-        "webpack-cli": "^3.3.12"
+        "react": "^17.0.2",
+        "react-dom": "^17.0.2",
+        "rxjs": "^7.3.0",
+        "sass-loader": "^12.1.0",
+        "stream-browserify": "^3.0.0",
+        "style-loader": "^3.2.1",
+        "tslib": "^2.3.1",
+        "typescript": "^4.4.3",
+        "webpack": "^5.52.1",
+        "webpack-cli": "^4.8.0"
     }
 }

+ 70 - 0
src/viewer/helpers/af-confidence/behavior.ts

@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Mandar Deshpande <mandar@ebi.ac.uk>
+ */
+
+import { OrderedSet } from 'molstar/lib/mol-data/int';
+import { AlphaFoldConfidence, AlphaFoldConfidenceProvider } from './prop';
+import { AlphaFoldConfidenceColorThemeProvider } from './color';
+import { Loci } from 'molstar/lib/mol-model/loci';
+import { StructureElement } from 'molstar/lib/mol-model/structure';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior/behavior';
+
+export const AlphaFoldConfidenceScore = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
+    name: 'af-confidence-prop',
+    category: 'custom-props',
+    display: {
+        name: 'AlphaFold Confidence Score',
+        description: 'AlphaFold Confidence Score.'
+    },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
+        private provider = AlphaFoldConfidenceProvider
+
+        private labelProvider = {
+            label: (loci: Loci): string | undefined => {
+                if (!this.params.showTooltip) return;
+
+                switch (loci.kind) {
+                    case 'element-loci':
+                        if (loci.elements.length === 0) return;
+                        const e = loci.elements[0];
+                        const u = e.unit;
+                        if (!u.model.customProperties.hasReference(AlphaFoldConfidenceProvider.descriptor)) return;
+
+                        const se = StructureElement.Location.create(loci.structure, u, u.elements[OrderedSet.getAt(e.indices, 0)]);
+                        const confidenceScore = AlphaFoldConfidence.getConfidenceScore(se);
+                        return confidenceScore ? `Confidence score: ${confidenceScore[0]} <small>( ${confidenceScore[1]} )</small>` : `No confidence score`;
+
+                    default: return;
+                }
+            }
+        }
+
+        register(): void {
+            this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
+            this.ctx.managers.lociLabels.addProvider(this.labelProvider);
+
+            this.ctx.representation.structure.themes.colorThemeRegistry.add(AlphaFoldConfidenceColorThemeProvider);
+        }
+
+        update(p: { autoAttach: boolean, showTooltip: boolean }) {
+            const updated = this.params.autoAttach !== p.autoAttach;
+            this.params.autoAttach = p.autoAttach;
+            this.params.showTooltip = p.showTooltip;
+            this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
+            return updated;
+        }
+
+        unregister() {
+            this.ctx.customModelProperties.unregister(AlphaFoldConfidenceProvider.descriptor.name);
+            this.ctx.managers.lociLabels.removeProvider(this.labelProvider);
+            this.ctx.representation.structure.themes.colorThemeRegistry.remove(AlphaFoldConfidenceColorThemeProvider);
+        }
+    },
+    params: () => ({
+        autoAttach: PD.Boolean(false),
+        showTooltip: PD.Boolean(true)
+    })
+});

+ 95 - 0
src/viewer/helpers/af-confidence/color.ts

@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Mandar Deshpande <mandar@ebi.ac.uk>
+ * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
+ */
+
+import { AlphaFoldConfidence, AlphaFoldConfidenceProvider } from './prop';
+import { Location } from 'molstar/lib/mol-model/location';
+import { StructureElement } from 'molstar/lib/mol-model/structure';
+import { ColorTheme, LocationColor } from 'molstar/lib/mol-theme/color';
+import { ThemeDataContext } from 'molstar/lib/mol-theme/theme';
+import { Color } from 'molstar/lib/mol-util/color';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property';
+import { TableLegend } from 'molstar/lib/mol-util/legend';
+
+const DefaultColor = Color(0xaaaaaa);
+const ConfidenceColors: { [k: string]: Color } = {
+    'No Score': DefaultColor,
+    'Very low': Color(0xff7d45),
+    'Low': Color(0xffdb13),
+    'Confident': Color(0x65cbf3),
+    'Very high': Color(0x0053d6)
+};
+
+const ConfidenceColorLegend = TableLegend(Object.entries(ConfidenceColors));
+
+export function getAlphaFoldConfidenceColorThemeParams(ctx: ThemeDataContext) {
+    const categories = AlphaFoldConfidence.getCategories(ctx.structure);
+    if (categories.length === 0) {
+        return {
+            type: PD.MappedStatic('score', {
+                'score': PD.Group({})
+            })
+        };
+    }
+
+    return {
+        type: PD.MappedStatic('score', {
+            'score': PD.Group({}),
+            'category': PD.Group({
+                kind: PD.Select(categories[0], PD.arrayToOptions(categories))
+            }, { isFlat: true })
+        })
+    };
+}
+export type AlphaFoldConfidenceColorThemeParams = ReturnType<typeof getAlphaFoldConfidenceColorThemeParams>
+
+export function AlphaFoldConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Values<AlphaFoldConfidenceColorThemeParams>): ColorTheme<AlphaFoldConfidenceColorThemeParams> {
+    let color: LocationColor = () => DefaultColor;
+
+    if (ctx.structure && ctx.structure.models.length > 0 && ctx.structure.models[0].customProperties.has(AlphaFoldConfidenceProvider.descriptor)) {
+        const getColor = (location: StructureElement.Location): Color => {
+            const score: string = AlphaFoldConfidence.getConfidenceScore(location)[1];
+
+            if (props.type.name !== 'score') {
+                const categoryProp = props.type.params.kind;
+                if (score === categoryProp) return ConfidenceColors[score];
+            }
+
+            return ConfidenceColors[score];
+        };
+
+        color = (location: Location) => {
+            if (StructureElement.Location.is(location)) {
+                return getColor(location);
+            }
+            return DefaultColor;
+        };
+    }
+
+    return {
+        factory: AlphaFoldConfidenceColorTheme,
+        granularity: 'group',
+        color,
+        props,
+        description: 'Assigns residue colors according to the AlphaFold Confidence score.',
+        legend: ConfidenceColorLegend
+    };
+}
+
+export const AlphaFoldConfidenceColorThemeProvider: ColorTheme.Provider<AlphaFoldConfidenceColorThemeParams, 'af-confidence'> = {
+    name: 'af-confidence',
+    label: 'AlphaFold Confidence',
+    category: ColorTheme.Category.Validation,
+    factory: AlphaFoldConfidenceColorTheme,
+    getParams: getAlphaFoldConfidenceColorThemeParams,
+    defaultValues: PD.getDefaultValues(getAlphaFoldConfidenceColorThemeParams({})),
+    isApplicable: (ctx: ThemeDataContext) => AlphaFoldConfidence.isApplicable(ctx.structure?.models[0]),
+    ensureCustomProperties: {
+        attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? AlphaFoldConfidenceProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
+        detach: (data) => data.structure && AlphaFoldConfidenceProvider.ref(data.structure.models[0], false)
+    }
+};

+ 145 - 0
src/viewer/helpers/af-confidence/prop.ts

@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Mandar Deshpande <mandar@ebi.ac.uk>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
+ */
+
+import { Column, Table } from 'molstar/lib/mol-data/db';
+import { toTable } from 'molstar/lib/mol-io/reader/cif/schema';
+import { IndexedCustomProperty, Model, ResidueIndex, Unit } from 'molstar/lib/mol-model/structure';
+import { Structure, StructureElement } from 'molstar/lib/mol-model/structure/structure';
+import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
+import { MmcifFormat } from 'molstar/lib/mol-model-formats/structure/mmcif';
+import { PropertyWrapper } from 'molstar/lib/mol-model-props/common/wrapper';
+import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property';
+import { CustomModelProperty } from 'molstar/lib/mol-model-props/common/custom-model-property';
+import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property';
+import { arraySetAdd } from 'molstar/lib/mol-util/array';
+import { dateToUtcString } from 'molstar/lib/mol-util/date';
+
+export type AlphaFoldConfidence = PropertyWrapper<{
+    score: IndexedCustomProperty.Residue<[number, string]>,
+    category: string[]
+}>
+
+export namespace AlphaFoldConfidence {
+    export function isApplicable(model?: Model): boolean {
+        if (!model || !MmcifFormat.is(model.sourceData)) return false;
+        return model.sourceData.data.frame.categoryNames.includes('ma_qa_metric_local');
+    }
+
+    export interface Info {
+        timestamp_utc: string
+    }
+
+    export const Schema = {
+        local_metric_values: {
+            label_asym_id: Column.Schema.str,
+            label_comp_id: Column.Schema.str,
+            label_seq_id: Column.Schema.int,
+            metric_id: Column.Schema.int,
+            metric_value: Column.Schema.float,
+            model_id: Column.Schema.int,
+            ordinal_id: Column.Schema.int
+        }
+    };
+    export type Schema = typeof Schema;
+
+    function tryGetInfoFromCif(categoryName: string, model: Model): undefined | Info {
+        if (!MmcifFormat.is(model.sourceData) || !model.sourceData.data.frame.categoryNames.includes(categoryName)) {
+            return;
+        }
+
+        const timestampField = model.sourceData.data.frame.categories[categoryName].getField('metric_value');
+        if (!timestampField || timestampField.rowCount === 0) return;
+
+        return { timestamp_utc: timestampField.str(0) || dateToUtcString(new Date()) };
+    }
+
+
+    export function fromCif(ctx: CustomProperty.Context, model: Model): AlphaFoldConfidence | undefined {
+        const info = tryGetInfoFromCif('ma_qa_metric_local', model);
+        if (!info) return;
+        const data = getCifData(model);
+        const metricMap = createScoreMapFromCif(model, data.residues);
+        return { info, data: metricMap };
+    }
+
+    export async function obtain(ctx: CustomProperty.Context, model: Model, _props: AlphaFoldConfidenceProps): Promise<CustomProperty.Data<any>> {
+        const cif = fromCif(ctx, model);
+        return { value: cif };
+    }
+
+    export function getConfidenceScore(e: StructureElement.Location): [number, string] {
+        if (!Unit.isAtomic(e.unit)) return [-1, 'No Score'];
+        const prop = AlphaFoldConfidenceProvider.get(e.unit.model).value;
+        if (!prop || !prop.data) return [-1, 'No Score'];
+        const rI = e.unit.residueIndex[e.element];
+        return prop.data.score.has(rI) ? prop.data.score.get(rI)! : [-1, 'No Score'];
+    }
+
+    const _emptyArray: string[] = [];
+    export function getCategories(structure?: Structure) {
+        if (!structure) return _emptyArray;
+        const prop = AlphaFoldConfidenceProvider.get(structure.models[0]).value;
+        if (!prop || !prop.data) return _emptyArray;
+        return prop.data.category;
+    }
+
+    function getCifData(model: Model) {
+        if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF.');
+        return {
+            residues: toTable(Schema.local_metric_values, model.sourceData.data.frame.categories.ma_qa_metric_local),
+        };
+    }
+}
+
+export const AlphaFoldConfidenceParams = {};
+export type AlphaFoldConfidenceParams = typeof AlphaFoldConfidenceParams
+export type AlphaFoldConfidenceProps = PD.Values<AlphaFoldConfidenceParams>
+
+export const AlphaFoldConfidenceProvider: CustomModelProperty.Provider<AlphaFoldConfidenceParams, AlphaFoldConfidence> = CustomModelProperty.createProvider({
+    label: 'AlphaFold Confidence Score',
+    descriptor: CustomPropertyDescriptor({
+        name: 'af_confidence_score'
+    }),
+    type: 'static',
+    defaultParams: AlphaFoldConfidenceParams,
+    getParams: () => AlphaFoldConfidenceParams,
+    isApplicable: (data: Model) => AlphaFoldConfidence.isApplicable(data),
+    obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<AlphaFoldConfidenceProps>) => {
+        const p = { ...PD.getDefaultValues(AlphaFoldConfidenceParams), ...props };
+        return await AlphaFoldConfidence.obtain(ctx, data, p);
+    }
+});
+
+function createScoreMapFromCif(modelData: Model, residueData: Table<typeof AlphaFoldConfidence.Schema.local_metric_values>): AlphaFoldConfidence['data'] {
+    const { label_asym_id, label_seq_id, metric_value, _rowCount } = residueData;
+
+    const ret = new Map<ResidueIndex, [number, string]>();
+    const categories: string[] = [];
+
+    const toCategory = (v: number): 'Very low' | 'Low' | 'Confident' | 'Very high' => {
+        if (v > 50 && v <= 70) return 'Low';
+        if (v > 70 && v <= 90) return 'Confident';
+        if (v > 90) return 'Very high';
+        return 'Very low';
+    };
+
+    for (let i = 0; i < _rowCount; i++) {
+        const confidenceScore = metric_value.value(i);
+        const idx = modelData.atomicHierarchy.index.findResidue('1', label_asym_id.value(i), label_seq_id.value(i));
+        const confidenceCategory = toCategory(confidenceScore);
+
+        ret.set(idx, [confidenceScore, confidenceCategory]);
+        arraySetAdd(categories, confidenceCategory);
+    }
+
+    return {
+        score: IndexedCustomProperty.fromResidueMap(ret),
+        category: categories
+    };
+}

+ 2 - 3
src/viewer/helpers/export.ts

@@ -32,10 +32,9 @@ function exportParams(): encode_mmCIF_categories_Params {
         .add('pdbx_struct_assembly')
         .add('pdbx_struct_assembly_gen')
         .add('pdbx_struct_oper_list');
-    const params: encode_mmCIF_categories_Params = {
+    return {
         skipCategoryNames: skipCategories
     };
-    return params;
 }
 
 function to_mmCIF(name: string, structure: Structure, asBinary = false) {
@@ -94,5 +93,5 @@ export function encodeStructureData(plugin: PluginContext): { [k: string]: Uint8
 export async function downloadAsZipFile(plugin: PluginContext, content: { [k: string]: Uint8Array }) {
     const filename = `mol-star_download_${getFormattedTime()}.zip`;
     const buf = await plugin.runTask(Zip(content));
-    download(new Blob([buf], { type : 'application/zip' }), filename);
+    download(new Blob([buf], { type: 'application/zip' }), filename);
 }

+ 7 - 7
src/viewer/helpers/model.ts

@@ -11,10 +11,10 @@ import { Asset } from 'molstar/lib/mol-util/assets';
 import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
 import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
 import { BuiltInTrajectoryFormat } from 'molstar/lib/mol-plugin-state/formats/trajectory';
-import {TrajectoryHierarchyPresetProvider} from 'molstar/lib/mol-plugin-state/builder/structure/hierarchy-preset';
+import { TrajectoryHierarchyPresetProvider } from 'molstar/lib/mol-plugin-state/builder/structure/hierarchy-preset';
 
 export class ModelLoader {
-    async load<P={}>(load: LoadParams, props?: PresetProps, matrix?: Mat4, reprProvider?: TrajectoryHierarchyPresetProvider<P>, params?: P) {
+    async load<P = {}>(load: LoadParams, props?: PresetProps, matrix?: Mat4, reprProvider?: TrajectoryHierarchyPresetProvider<P>, params?: P) {
         const { fileOrUrl, format, isBinary } = load;
 
         const data = fileOrUrl instanceof File
@@ -23,17 +23,17 @@ export class ModelLoader {
         await this.handleTrajectory<P>(data, format, props, matrix, reprProvider, params);
     }
 
-    async parse<P={}>(parse: ParseParams, props?: PresetProps & { dataLabel?: string }, matrix?: Mat4, reprProvider?: TrajectoryHierarchyPresetProvider<P>, params?: P) {
+    async parse<P = {}>(parse: ParseParams, props?: PresetProps & { dataLabel?: string }, matrix?: Mat4, reprProvider?: TrajectoryHierarchyPresetProvider<P>, params?: P) {
         const { data, format } = parse;
         const _data = await this.plugin.builders.data.rawData({ data, label: props?.dataLabel });
         await this.handleTrajectory(_data, format, props, matrix, reprProvider, params);
     }
 
-    private async handleTrajectory<P={}>(data: any, format: BuiltInTrajectoryFormat, props?: PresetProps, matrix?: Mat4, reprProvider?: TrajectoryHierarchyPresetProvider<P>, params?: P) {
+    private async handleTrajectory<P = {}>(data: any, format: BuiltInTrajectoryFormat, props?: PresetProps, matrix?: Mat4, reprProvider?: TrajectoryHierarchyPresetProvider<P>, params?: P) {
         const trajectory = await this.plugin.builders.structure.parseTrajectory(data, format);
-        if(reprProvider){
-            await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, reprProvider, {...params});
-        }else{
+        if (reprProvider) {
+            await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, reprProvider, { ...params });
+        } else {
             const selector = await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, RcsbPreset, {
                 preset: props || { kind: 'standard', assemblyId: '' }
             });

+ 28 - 9
src/viewer/helpers/preset.ts

@@ -238,7 +238,20 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
         } else if (p.kind === 'empty') {
             console.warn('Using empty representation');
         } else if (p.kind === 'membrane') {
-            representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, MembraneOrientationPreset);
+            try {
+                representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, MembraneOrientationPreset);
+
+                // reset the camera because the membranes render 1st and the structure might not be fully visible
+                requestAnimationFrame(() => plugin.canvas3d?.requestCameraReset());
+            } catch (error) {
+                const msg = 'Membrane calculation failed! - This can happen for tiny structures with only a dozen of residues.';
+                plugin.log.error(msg);
+                console.error(msg);
+                console.error(error);
+
+                // fall back to default representation to show something
+                representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, 'auto');
+            }
         } else {
             representation = await plugin.builders.structure.representation.applyPreset(structureProperties!, 'auto');
         }
@@ -246,7 +259,7 @@ export const RcsbPreset = TrajectoryHierarchyPresetProvider({
         if ((p.kind === 'feature' || p.kind === 'feature-density') && structure?.obj) {
             let loci = targetToLoci(p.target, structure!.obj.data);
             // if target is only defined by chain: then don't force first residue
-            const chainMode = p.target.labelAsymId && !p.target.authSeqId && !p.target.labelSeqId && !p.target.labelCompId;
+            const chainMode = Object.keys(p.target).length === 1 && !!p.target.labelAsymId;
             // HELP-16678: check for rare case where ligand is not present in requested assembly
             if (loci.elements.length === 0 && !!p.assemblyId) {
                 // switch to Model (a.k.a. show coordinates independent of assembly)
@@ -340,20 +353,26 @@ function determineAssemblyId(traj: any, p: MotifProps) {
     try {
         // find first assembly that contains all requested structOperIds - if multiple, the first will be returned
         const pdbx_struct_assembly_gen = traj.obj.data.representative.sourceData.data.frame.categories.pdbx_struct_assembly_gen;
-        const assembly_id = pdbx_struct_assembly_gen.getField('assembly_id');
-        const oper_expression = pdbx_struct_assembly_gen.getField('oper_expression');
-        const asym_id_list = pdbx_struct_assembly_gen.getField('asym_id_list');
+        if (pdbx_struct_assembly_gen) {
+            const assembly_id = pdbx_struct_assembly_gen.getField('assembly_id');
+            const oper_expression = pdbx_struct_assembly_gen.getField('oper_expression');
+            const asym_id_list = pdbx_struct_assembly_gen.getField('asym_id_list');
 
-        for (let i = 0, il = pdbx_struct_assembly_gen.rowCount; i < il; i++) {
-            if (ids.some(val => !equals(oper_expression.str(i), val[0]) || asym_id_list.str(i).indexOf(val[1]) === -1)) continue;
+            for (let i = 0, il = pdbx_struct_assembly_gen.rowCount; i < il; i++) {
+                if (ids.some(val => !equals(oper_expression.str(i), val[0]) || asym_id_list.str(i).indexOf(val[1]) === -1)) continue;
 
-            Object.assign(p, { assemblyId: assembly_id.str(i) });
-            return;
+                Object.assign(p, { assemblyId: assembly_id.str(i) });
+                return;
+            }
+        } else {
+            // this happens e.g. for AlphaFold structures
+            console.warn(`Source file is missing 'pdbx_struct_assembly_gen' category`);
         }
     } catch (error) {
         console.warn(error);
     }
     // default to '1' if error or legitimately not found
+    console.warn(`Could not auto-detect assembly-of-interest. Falling back to '1'`);
     Object.assign(p, { assemblyId: '1' });
 }
 

+ 4 - 4
src/viewer/helpers/selection.ts

@@ -62,9 +62,9 @@ export type SelectionExpression = {
  * override pre-existing 'operatorName' values.
  * @param targets collection to process
  * @param structure parent structure
- * @param operatorName optional value to which missing operators are set, will default to 'ASM_1' if not specified
+ * @param operatorName optional value to which missing operators are set
  */
-export function normalizeTargets(targets: Target[], structure: Structure, operatorName: string = 'ASM_1'): Target[] {
+export function normalizeTargets(targets: Target[], structure: Structure, operatorName = undefined): Target[] {
     return targets.map(t => {
         if (t.structOperId) {
             const { structOperId, ...others } = t;
@@ -103,7 +103,7 @@ function toOperatorName(structure: Structure, expression: string): string {
  */
 export function createSelectionExpressions(labelBase: string, selection?: Target | Target[]): SelectionExpression[] {
     if (selection) {
-        if ('labelAsymId' in selection && 'labelSeqRange' in selection) {
+        if ('labelAsymId' in selection) {
             const target = selection as Target;
             const residues: number[] = (target.labelSeqRange) ? toRange(target.labelSeqRange!.beg, target.labelSeqRange!.end) : [];
             const test = rangeToTest(target.labelAsymId!, residues);
@@ -212,7 +212,7 @@ function targetToExpression(target: Target): Expression {
         residueTests.push(MS.core.rel.eq([target.authSeqId, MS.ammp('auth_seq_id')]));
     } else if (target.labelSeqId) {
         residueTests.push(MS.core.rel.eq([target.labelSeqId, MS.ammp('label_seq_id')]));
-    }else if(target.labelSeqRange){
+    } else if (target.labelSeqRange) {
         residueTests.push(MS.core.rel.inRange([MS.ammp('label_seq_id'), target.labelSeqRange.beg, target.labelSeqRange.end ?? target.labelSeqRange.beg]));
     }
     if (target.labelCompId) {

+ 1 - 1
src/viewer/helpers/superpose/color.ts

@@ -22,7 +22,7 @@ export function SuperposeColorTheme(ctx: ThemeDataContext, props: {}): ColorThem
 
     let DefaultColor = Color(0xCCCCCC);
     const colorValues: Color[] = Array.from(defaultColorLookup.values());
-    if (colorValues.every( (val, i, arr) => val === arr[0] )) {
+    if (colorValues.every((val, i, arr) => val === arr[0])) {
         DefaultColor = colorValues[0];
     }
 

+ 2 - 2
src/viewer/helpers/superpose/preset.ts

@@ -30,8 +30,8 @@ export const RcsbSuperpositionRepresentationPreset = StructureRepresentationPres
         const structure = structureCell.obj!.data;
         const cartoonProps = { sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2 };
 
-        let components = Object.create(null);
-        let representations = Object.create(null);
+        const components = Object.create(null);
+        const representations = Object.create(null);
 
         for (const expr of params.selectionExpressions) {
             const comp = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, expr.expression, expr.label, { label: expr.label });

+ 3 - 3
src/viewer/helpers/viewer.ts

@@ -38,7 +38,7 @@ export function getStructureRefWithModelId(structures: StructureRef[], target: {
     for (const structure of structures) {
         if (!structure.cell?.obj?.data?.units) continue;
 
-        const unit =  structure.cell.obj.data.units[0];
+        const unit = structure.cell.obj.data.units[0];
         if (unit.model.id === target.modelId) return structure;
     }
 }
@@ -53,7 +53,7 @@ export function select(plugin: PluginContext, targets: SelectTarget | SelectTarg
 
         if (mode === 'hover') {
             plugin.managers.interactivity.lociHighlights.highlight({ loci });
-        }else if(mode === 'select'){
+        } else if (mode === 'select') {
             plugin.managers.structure.selection.fromLoci(n > 0 ? 'add' : modifier, loci);
         }
     });
@@ -80,7 +80,7 @@ export function clearSelection(plugin: PluginContext, mode: 'select' | 'hover',
 export async function createComponent(plugin: PluginContext, componentLabel: string, targets: SelectBase | SelectTarget | SelectTarget[], representationType: StructureRepresentationRegistry.BuiltIn) {
     for (const target of (Array.isArray(targets) ? targets : [targets])) {
         const structureRef = getStructureRefWithModelId(plugin.managers.structure.hierarchy.current.structures, target);
-        if (!structureRef) throw 'createComponent error: model not found';
+        if (!structureRef) throw Error('createComponent error: model not found');
 
         const residues = toResidues(target);
         const sel = StructureSelectionQuery('innerQuery_' + Math.random().toString(36).substr(2),

+ 115 - 61
src/viewer/index.html

@@ -40,28 +40,29 @@
         <div id="viewer"></div>
         <script>
             function getQueryParam(id) {
-                var a = new RegExp(id + '=([^&#=]*)', 'i')
-                var m = a.exec(window.location.search)
+                const a = new RegExp(id + '=([^&#=]*)', 'i')
+                const m = a.exec(window.location.search)
                 return m ? decodeURIComponent(m[1]) : undefined
             }
 
-            var isEmbedded = getQueryParam('embedded') === '1';
+            const isEmbedded = getQueryParam('embedded') === '1';
 
-            var pdbId = getQueryParam('pdbId')
-            var url = getQueryParam('url')
-            var _config = getQueryParam('config')
-            var config = _config && JSON.parse(_config)
-            var _loadPdbIds = getQueryParam('loadPdbIds')
-            var loadPdbIds = _loadPdbIds && JSON.parse(_loadPdbIds)
+            const pdbId = getQueryParam('pdbId')
+            const url = getQueryParam('url')
+            const _config = getQueryParam('config')
+            const config = _config && JSON.parse(_config)
+            const _loadPdbIds = getQueryParam('loadPdbIds')
+            const loadPdbIds = _loadPdbIds && JSON.parse(_loadPdbIds)
 
             // create an instance of the plugin
-            var viewer = new rcsbMolstar.Viewer('viewer', {
+            const viewer = new rcsbMolstar.Viewer('viewer', {
                 showImportControls: !pdbId,
                 showExportControls: true,
                 showSessionControls: !pdbId,
                 layoutShowLog: !pdbId,
                 layoutShowControls: !isEmbedded,
-                showMembraneOrientationPreset: true
+                showMembraneOrientationPreset: true,
+                detachedFromSierra: true // needed when running without sierra
             })
 
             // load pdbId or url
@@ -71,7 +72,7 @@
         </script>
         <div id="menu">
             <h2> RCSB PDB Mol* Viewer - Test Page</h2>
-            Examples
+            <label for="examples">Examples</label>
             <select id="examples" onchange="loadExample(parseInt(this.value))">
                 <option value=''></option>
             </select>
@@ -93,16 +94,21 @@
             <button style="padding: 3px;" onclick="motifs2()">Motifs 2</button>
             &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
 
+            <button style="padding: 3px;" onclick="motifsAlphaFold()">Motifs AF</button>
+            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+
             <button style="padding: 3px" onclick="propset()">Propset</button>
         </div>
         <script>
 
             function loadExample(index) {
-                var e = examples[index]
-                viewer.loadPdbId(e.id, e.config)
+                const e = examples[index];
+                // let URL, if specified, take precedence over ID
+                if (e.url) return viewer.loadStructureFromUrl(e.url, e.format ?? 'mmcif', e.isBinary ?? false, e.config)
+                if (e.id) return viewer.loadPdbId(e.id, e.config)
             }
 
-            var examples = [
+            const examples = [
                 {
                     id: '1CRN',
                     info: 'small: only polymer',
@@ -333,6 +339,17 @@
                         }
                     }
                 },
+                {
+                    id: '5XES',
+                    // this will fail membrane calculation due to tiny size but should still show default representation
+                    info: 'failing membrane: TK9 NMR structure in SDS micelle',
+                    config: {
+                        props: {
+                            kind: 'membrane',
+                            assemblyId: '1'
+                        }
+                    }
+                },
                 {
                     id: '6WJC',
                     info: 'ligand validation: Muscarinic acetylcholine receptor 1 - muscarinic toxin 7 complex: Focus + Density',
@@ -370,8 +387,8 @@
                             kind: 'motif',
                             assemblyId: '1',
                             targets: [
-                                { labelAsymId: 'A', labelSeqId: 61 },
-                                { labelAsymId: 'A', labelSeqId: 69 },
+                                { labelAsymId: 'A', labelSeqId: 61, operatorName: 'ASM_1' },
+                                { labelAsymId: 'A', labelSeqId: 69, operatorName: 'ASM_1' },
                                 { labelAsymId: 'A', labelSeqId: 87, operatorName: 'ASM_4' }
                             ],
                         }
@@ -385,8 +402,8 @@
                             kind: 'motif',
                             assemblyId: '1',
                             targets: [
-                                { labelAsymId: 'A', labelSeqId: 61 },
-                                { labelAsymId: 'A', labelSeqId: 69 },
+                                { labelAsymId: 'A', labelSeqId: 61, structOperId: '1' },
+                                { labelAsymId: 'A', labelSeqId: 69, structOperId: '1' },
                                 { labelAsymId: 'A', labelSeqId: 87, structOperId: '4' }
                             ],
                         }
@@ -420,13 +437,18 @@
                             ],
                         }
                     }
+                },
+                {
+                    id: 'AF-Q8W3K0-F1',
+                    url: 'https://alphafold.ebi.ac.uk/files/AF-Q8W3K0-F1-model_v1.cif',
+                    info: 'confidence coloring: Probable disease resistance protein At1g58602'
                 }
             ];
 
-            var examplesSelect = document.getElementById('examples');
-            for (var i = 0, il = examples.length; i < il; ++i) {
-                var e = examples[i]
-                var option = document.createElement('option')
+            const examplesSelect = document.getElementById('examples');
+            for (let i = 0, il = examples.length; i < il; ++i) {
+                const e = examples[i]
+                const option = document.createElement('option')
                 Object.assign(option, { text: '[' + e.id + '] ' + e.info, value: i })
                 examplesSelect.appendChild(option)
             }
@@ -454,46 +476,42 @@
             function motifs1() {
                 viewer.clear()
                     .then(function() {
-                        return viewer.loadPdbIds([{
-                            pdbId: '5CBG',
-                            config: {
-                                props: {
-                                    label: '5CBG',
-                                    kind: 'motif',
-                                    // assemblyId: '2', // library should be able to infer assemblyId of the query
-                                    targets: [
-                                        { labelAsymId: 'C', labelSeqId: 30, structOperId: '3' },
-                                        { labelAsymId: 'C', labelSeqId: 32, structOperId: '3' },
-                                        { labelAsymId: 'C', labelSeqId: 34, structOperId: '3' }
-                                    ],
-                                    // color: 13203230
+                        return viewer.loadPdbIds([
+                            {
+                                pdbId: '4CHA',
+                                config: {
+                                    props: {
+                                        kind: 'motif',
+                                        label: '4CHA',
+                                        targets: [
+                                            { labelAsymId: 'B', structOperId: '1', labelSeqId: 42 },
+                                            { labelAsymId: 'B', structOperId: '1', labelSeqId: 87 },
+                                            { labelAsymId: 'C', structOperId: '1', labelSeqId: 47 }
+                                        ]
+                                    }
+                                }
+                            }, {
+                                pdbId: '3RU4',
+                                config: {
+                                    props: {
+                                        kind: 'motif',
+                                        assemblyId: '1',
+                                        label: '3RU4 #2: 0.26 Å',
+                                        targets: [
+                                            { labelAsymId: 'D', structOperId: '2', labelSeqId: 42 },
+                                            { labelAsymId: 'D', structOperId: '2', labelSeqId: 87 },
+                                            { labelAsymId: 'E', structOperId: '2', labelSeqId: 46 }
+                                        ]
+                                    },
+                                    matrix: [
+                                        -0.049396686, -0.99700946, -0.059431925, 0.0,
+                                        -0.7568329, 0.076193266, -0.6491522, 0.0,
+                                        0.6517392, 0.012914069, -0.7583332, 0.0,
+                                        20.371853, 11.498471, 45.705563, 1.0
+                                    ]
                                 }
                             }
-                        }, {
-                            pdbId: '2W02',
-                            config: {
-                                props: {
-                                    label: '2W02 #1',
-                                    kind: 'motif',
-                                    assemblyId: '2',
-                                    targets: [
-                                        { labelAsymId: 'B', labelSeqId: 519 },
-                                        { labelAsymId: 'B', labelSeqId: 517 },
-                                        { labelAsymId: 'B', labelSeqId: 515 }
-                                    ],
-                                    // color: 4947916
-                                },
-                                matrix: [
-                                    0.7953831708253857, -0.6048923987758781, 0.03835097744411625, 0,
-                                    -0.23732979915044658, -0.2525976533016715, 0.9380133218572628, 0,
-                                    -0.5577097614377604, -0.7551818399893184, -0.344470913935246, 0,
-                                    154.77888998938096, 207.0215940587305, 25.17873980937774, 1
-                                ]
-                            }
-                        }]);
-                    })
-                    .then(function() {
-                        viewer.resetCamera(0)
+                        ]);
                     });
             }
 
@@ -543,6 +561,42 @@
                     });
             }
 
+            function motifsAlphaFold() {
+                viewer.clear()
+                    .then(function() {
+                        return viewer.loadPdbId('1LAP', {
+                            props: {
+                                label: '1LAP',
+                                kind: 'motif',
+                                targets: [
+                                    { labelAsymId: 'A', labelSeqId: 332, structOperId: '1' },
+                                    { labelAsymId: 'A', labelSeqId: 334, structOperId: '1' },
+                                    { labelAsymId: 'A', labelSeqId: 255, structOperId: '1' },
+                                    { labelAsymId: 'A', labelSeqId: 273, structOperId: '1' },
+                                    { labelAsymId: 'A', labelSeqId: 250, structOperId: '1' }
+                                ],
+                            }
+                        });
+                    })
+                    .then(function() {
+                        const url = 'https://alphafold.ebi.ac.uk/files/AF-F1Q6S1-F1-model_v1.cif';
+                        const label = 'AF-F1Q6S1-F1 @ 0.23 RMSD';
+                        const targets = [
+                            // AF target must be devoid of struct_oper_id
+                            { labelAsymId: 'A', labelSeqId: 260 },
+                            { labelAsymId: 'A', labelSeqId: 265 },
+                            { labelAsymId: 'A', labelSeqId: 283 },
+                            { labelAsymId: 'A', labelSeqId: 342 },
+                            { labelAsymId: 'A', labelSeqId: 344 }
+                        ];
+                        const matrix = [-0.471, 0.856, 0.215, 0, 0.405, -0.007, 0.914, 0, 0.784, 0.518, -0.343, 0, 54.981, 65.575, 12.287, 1];
+                        return viewer.loadStructureFromUrl(url, 'mmcif', false, { props: { kind: 'motif', label, targets }, matrix });
+                    })
+                    .then(function() {
+                        viewer.resetCamera(0)
+                    });
+            }
+
             function propset() {
                 viewer.clear()
                     .then(function () {

+ 25 - 14
src/viewer/index.ts

@@ -3,6 +3,8 @@
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Joan Segura <joan.segura@rcsb.org>
+ * @author Yana Rose <yana.rose@rcsb.org>
+ * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  */
 
 import { BehaviorSubject } from 'rxjs';
@@ -29,15 +31,15 @@ import { PluginLayoutControlsDisplay } from 'molstar/lib/mol-plugin/layout';
 import { SuperposeColorThemeProvider } from './helpers/superpose/color';
 import { encodeStructureData, downloadAsZipFile } from './helpers/export';
 import { setFocusFromRange, removeComponent, clearSelection, createComponent, select } from './helpers/viewer';
-import {SelectBase, SelectRange, SelectTarget, Target} from './helpers/selection';
+import { SelectBase, SelectRange, SelectTarget, Target } from './helpers/selection';
 import { StructureRepresentationRegistry } from 'molstar/lib/mol-repr/structure/registry';
-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, MembraneOrientationPreset } from 'molstar/lib/extensions/anvil/behavior';
 import { MembraneOrientationRepresentationProvider } from 'molstar/lib/extensions/anvil/representation';
-import {PluginContext} from 'molstar/lib/mol-plugin/context';
-import {TrajectoryHierarchyPresetProvider} from 'molstar/lib/mol-plugin-state/builder/structure/hierarchy-preset';
+import { AlphaFoldConfidenceScore } from './helpers/af-confidence/behavior';
+import { PluginContext } from 'molstar/lib/mol-plugin/context';
+import { TrajectoryHierarchyPresetProvider } from 'molstar/lib/mol-plugin-state/builder/structure/hierarchy-preset';
 
 /** package version, filled in at bundle build time */
 declare const __RCSB_MOLSTAR_VERSION__: string;
@@ -51,8 +53,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),
-    'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation)
+    'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
+    'af-confidence': PluginSpec.Behavior(AlphaFoldConfidenceScore)
 };
 
 const DefaultViewerProps = {
@@ -62,6 +64,11 @@ const DefaultViewerProps = {
     showStructureSourceControls: true,
     showSuperpositionControls: true,
     showMembraneOrientationPreset: false,
+    /**
+     * 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.
+     */
+    detachedFromSierra: false,
     modelUrlProviders: [
         (pdbId: string) => ({
             url: `https://models.rcsb.org/${pdbId.toLowerCase()}.bcif`,
@@ -157,13 +164,17 @@ export class Viewer {
                 superposition: true,
                 component: false,
                 volume: true,
-                custom: 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
+            }),
+            detachedFromSierra: o.detachedFromSierra
         };
 
         this._plugin.init()
             .then(async () => {
                 // hide 'Membrane Orientation' preset from UI - has to happen 'before' react render, apparently
+                // the corresponding behavior must be registered either way, because the 3d-view uses it (even without appearing in the UI)
                 if (!o.showMembraneOrientationPreset) {
                     this._plugin.builders.structure.representation.unregisterPreset(MembraneOrientationPreset);
                     this._plugin.representation.structure.registry.remove(MembraneOrientationRepresentationProvider);
@@ -193,7 +204,7 @@ export class Viewer {
         return this._plugin;
     }
 
-    pluginCall(f: (plugin: PluginContext) => void){
+    pluginCall(f: (plugin: PluginContext) => void) {
         f(this.plugin);
     }
 
@@ -207,9 +218,9 @@ export class Viewer {
         if (!expandedChanged) return;
 
         if (currExpanded && !this._plugin.layout.state.showControls) {
-            this._plugin.layout.setProps({showControls: true});
+            this._plugin.layout.setProps({ showControls: true });
         } else if (!currExpanded && this._plugin.layout.state.showControls) {
-            this._plugin.layout.setProps({showControls: false});
+            this._plugin.layout.setProps({ showControls: false });
         }
         this.prevExpanded = this._plugin.layout.state.isExpanded;
     }
@@ -223,7 +234,7 @@ export class Viewer {
         return PluginCommands.State.RemoveObject(this._plugin, { state, ref: state.tree.root.ref });
     }
 
-    async loadPdbId<P>(pdbId: string, config?: {props?: PresetProps; matrix?: Mat4; reprProvider?: TrajectoryHierarchyPresetProvider, params?: P}) {
+    async loadPdbId<P>(pdbId: string, config?: { props?: PresetProps; matrix?: Mat4; reprProvider?: TrajectoryHierarchyPresetProvider, params?: P }) {
         for (const provider of this.modelUrlProviders) {
             try {
                 const p = provider(pdbId);
@@ -255,7 +266,7 @@ export class Viewer {
     }
 
     handleResize() {
-        this._plugin.layout.events.updated.next();
+        this._plugin.layout.events.updated.next(void 0);
     }
 
     exportLoadedStructures() {
@@ -283,7 +294,7 @@ export class Viewer {
         await createComponent(this._plugin, label, targets, representationType);
     }
 
-    removeComponent(componentLabel: string): void{
+    removeComponent(componentLabel: string): void {
         removeComponent(this._plugin, componentLabel);
     }
 }

+ 2 - 0
src/viewer/types.ts

@@ -40,6 +40,7 @@ export type CollapsedState = {
     component: boolean
     volume: boolean
     custom: boolean
+    mp4export: boolean
 }
 
 export interface ViewerState {
@@ -50,6 +51,7 @@ export interface ViewerState {
     showSuperpositionControls: 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

@@ -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 { Mp4EncoderUI } from 'molstar/lib/extensions/mp4-export/ui';
 
 export class StructureTools extends PluginUIComponent {
     get customState() {
@@ -36,6 +37,7 @@ export class StructureTools extends PluginUIComponent {
             <StructureComponentControls initiallyCollapsed={collapsed.component} />
             <VolumeStreamingControls header='Density' initiallyCollapsed={collapsed.volume} />
             <CustomStructureControls initiallyCollapsed={collapsed.custom} />
+            <Mp4EncoderUI initiallyCollapsed={collapsed.mp4export}/>
         </>;
     }
 }

+ 1 - 1
src/viewer/ui/export.tsx

@@ -9,7 +9,7 @@ export class ExportControls extends CollapsableControls {
         return {
             header: 'Export',
             isCollapsed: true,
-            brand: { accent:  'gray' as const, svg: ExportOutlinedSvg }
+            brand: { accent: 'gray' as const, svg: ExportOutlinedSvg }
         };
     }
 

+ 1 - 1
src/viewer/ui/import.tsx

@@ -16,7 +16,7 @@ export class ImportControls extends CollapsableControls {
         return {
             header: 'Import',
             isCollapsed: false,
-            brand: { accent:  'gray' as const, svg: FileOutlineSvg }
+            brand: { accent: 'gray' as const, svg: FileOutlineSvg }
         };
     }
 

+ 1 - 1
src/viewer/ui/session.tsx

@@ -44,7 +44,7 @@ export class SessionControls extends CollapsableControls {
         return {
             header: 'Session',
             isCollapsed: true,
-            brand: { accent:  'gray' as const, svg: SaveOutlinedSvg }
+            brand: { accent: 'gray' as const, svg: SaveOutlinedSvg }
         };
     }
 

+ 12 - 8
src/viewer/ui/strucmotif.tsx

@@ -24,8 +24,10 @@ import { Vec3 } from 'molstar/lib/mol-math/linear-algebra/3d/vec3';
 import { Structure } from 'molstar/lib/mol-model/structure/structure/structure';
 import { Unit } from 'molstar/lib/mol-model/structure/structure/unit';
 import { UnitIndex } from 'molstar/lib/mol-model/structure/structure/element/element';
+import { ViewerState } from '../types';
 
-const ADVANCED_SEARCH_URL = 'https://rcsb.org/search?query=';
+const ABSOLUTE_ADVANCED_SEARCH_URL = 'https://rcsb.org/search?query=';
+const RELATIVE_ADVANCED_SEARCH_URL = '/search?query=';
 const RETURN_TYPE = '&return_type=assembly';
 const MIN_MOTIF_SIZE = 3;
 const MAX_MOTIF_SIZE = 10;
@@ -39,9 +41,9 @@ const MAX_MOTIF_EXTENT_SQUARED = MAX_MOTIF_EXTENT * MAX_MOTIF_EXTENT;
 export class StrucmotifSubmitControls extends CollapsableControls {
     protected defaultState() {
         return {
-            header: 'Structural Motif Search',
+            header: 'Structure Motif Search',
             isCollapsed: false,
-            brand: { accent:  'gray' as const, svg: SearchIconSvg }
+            brand: { accent: 'gray' as const, svg: SearchIconSvg }
         };
     }
 
@@ -154,7 +156,7 @@ class SubmitControls extends PurePluginUIComponent<{}, { isBusy: boolean, residu
 
             // retrieve CA/C4', used to compute residue distance
             const coords = [x(location), y(location), z(location)] as Vec3;
-            coordinates.push({coords, residueId});
+            coordinates.push({ coords, residueId });
 
             // handle potential exchanges - can be empty if deselected by users
             const residueMapEntry = this.state.residueMap.get(l)!;
@@ -207,15 +209,17 @@ class SubmitControls extends PurePluginUIComponent<{}, { isBusy: boolean, residu
             service: 'strucmotif',
             parameters: {
                 value: {
-                    data: pdbId.values().next().value as string,
+                    entry_id: pdbId.values().next().value as string,
                     residue_ids: residueIds.sort((a, b) => this.sortResidueIds(a, b))
                 },
-                score_cutoff: 0,
+                rmsd_cutoff: 2,
+                atom_pairing_scheme: 'ALL',
                 exchanges: exchanges
             }
         };
         // console.log(query);
-        const url = ADVANCED_SEARCH_URL + encodeURIComponent(JSON.stringify(query)) + RETURN_TYPE;
+        const sierraUrl = (this.plugin.customState as ViewerState).detachedFromSierra ? ABSOLUTE_ADVANCED_SEARCH_URL : RELATIVE_ADVANCED_SEARCH_URL;
+        const url = sierraUrl + encodeURIComponent(JSON.stringify(query)) + RETURN_TYPE;
         // console.log(url);
         window.open(url, '_blank');
     }
@@ -281,7 +285,7 @@ class SubmitControls extends PurePluginUIComponent<{}, { isBusy: boolean, residu
         const history = this.plugin.managers.structure.selection.additionsHistory;
         return <div key={e.entry.id}>
             <div className='msp-flex-row'>
-                <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.entry.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.entry.loci)} onMouseLeave={this.plugin.managers.interactivity.lociHighlights.clearHighlights}>
+                <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.entry.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.entry.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}>
                     {idx}. <span dangerouslySetInnerHTML={{ __html: e.entry.label }} />
                 </Button>
                 <ToggleButton icon={TuneSvg} className='msp-form-control' title='Define exchanges' toggle={() => this.toggleExchanges(idx)} isSelected={this.state.action === idx} disabled={this.state.isBusy} style={{ flex: '0 0 40px', padding: 0 }} />

+ 8 - 2
webpack.config.js

@@ -42,16 +42,22 @@ const sharedConfig = {
             'node_modules',
             path.resolve(__dirname, 'build/src/')
         ],
+        fallback: {
+            fs: false,
+            buffer: require.resolve('buffer'),
+            crypto: require.resolve('crypto-browserify'),
+            path: require.resolve('path-browserify'),
+            stream: require.resolve('stream-browserify')
+        }
     },
     watchOptions: {
         aggregateTimeout: 750
     },
-    devtool: ''
+    devtool: false
 };
 
 module.exports = [
     {
-        node: { fs: 'empty' },
         entry: path.resolve(__dirname, `build/src/viewer/index.js`),
         output: {
             library: 'rcsbMolstar',

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません