"
40 | ],
41 | "license": "Apache-2.0",
42 | "bugs": {
43 | "url": "https://github.com/PDBeurope/pdbe-molstar/issues"
44 | },
45 | "homepage": "https://github.com/PDBeurope/pdbe-molstar#readme",
46 | "devDependencies": {
47 | "@babel/core": "^7.17.10",
48 | "@babel/plugin-transform-runtime": "^7.17.10",
49 | "@babel/preset-env": "^7.17.10",
50 | "@babel/runtime": "^7.17.9",
51 | "@stylistic/eslint-plugin": "^2.6.4",
52 | "@types/d3": "^7.4.0",
53 | "@types/react": "^18.0.17",
54 | "@types/react-dom": "^18.0.6",
55 | "@types/webxr": "^0.5.23",
56 | "babel-loader": "^8.2.5",
57 | "concurrently": "^7.3.0",
58 | "cpx2": "^7.0.1",
59 | "css-loader": "^7.1.2",
60 | "eslint": "^9.9.0",
61 | "file-loader": "^6.2.0",
62 | "http-server": "^14.1.0",
63 | "mini-css-extract-plugin": "^2.6.1",
64 | "react": "^18.2.0",
65 | "react-dom": "^18.2.0",
66 | "sass": "^1.80.4",
67 | "sass-loader": "^16.0.2",
68 | "style-loader": "^4.0.0",
69 | "typescript": "^5.7.3",
70 | "typescript-eslint": "^8.2.0",
71 | "webpack": "^5.95.0",
72 | "webpack-cli": "^5.1.4"
73 | },
74 | "dependencies": {
75 | "buffer": "^6.0.3",
76 | "crypto-browserify": "^3.12.0",
77 | "d3-axis": "^3.0.0",
78 | "d3-brush": "^3.0.0",
79 | "d3-scale": "^4.0.2",
80 | "d3-selection": "^3.0.0",
81 | "lit": "^3.1.1",
82 | "molstar": "5.4.2",
83 | "path-browserify": "^1.0.1",
84 | "stream-browserify": "^3.0.0",
85 | "vm-browserify": "^1.1.2"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/app/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | /** SVG icons for UI */
2 |
3 | import React from 'react';
4 |
5 |
6 | function svgIcon(path: string): React.FunctionComponent<{}> {
7 | const svg = ;
8 | return function SvgIcon() { return svg; };
9 | }
10 |
11 | // Source: https://mui.com/material-ui/material-icons/
12 |
13 | /** Empty icon */
14 | export const EmptyIconSvg = svgIcon('');
15 |
16 | /** Image gallery icon */
17 | export const CollectionsOutlinedSvg = svgIcon('M20 4v12H8V4zm0-2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2m-8.5 9.67 1.69 2.26 2.48-3.1L19 15H9zM2 6v14c0 1.1.9 2 2 2h14v-2H4V6z');
18 |
19 | // /** Image gallery icon, dark-background image */
20 | // export const CollectionsSvg = svgIcon('M22 16V4c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2m-11-4 2.03 2.71L16 11l4 5H8zM2 6v14c0 1.1.9 2 2 2h14v-2H4V6z');
21 |
22 | // /** View carousel icon */
23 | // export const ViewCarouselOutlinedSvg = svgIcon('M2 7h4v10H2zm5 12h10V5H7zM9 7h6v10H9zm9 0h4v10h-4z');
24 |
25 | /** Alignment icon */
26 | export const WavesIconSvg = svgIcon('M17 16.99c-1.35 0-2.2.42-2.95.8-.65.33-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.38-1.57-.8-2.95-.8s-2.2.42-2.95.8c-.65.33-1.17.6-2.05.6v1.95c1.35 0 2.2-.42 2.95-.8.65-.33 1.17-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.42 2.95-.8c.65-.33 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8v-1.95c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8zm0-4.45c-1.35 0-2.2.43-2.95.8-.65.32-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.38-1.57-.8-2.95-.8s-2.2.43-2.95.8c-.65.32-1.17.6-2.05.6v1.95c1.35 0 2.2-.43 2.95-.8.65-.35 1.15-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.35 1.15-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.58.8 2.95.8v-1.95c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8zm2.95-8.08c-.75-.38-1.58-.8-2.95-.8s-2.2.42-2.95.8c-.65.32-1.18.6-2.05.6-.9 0-1.4-.25-2.05-.6-.75-.37-1.57-.8-2.95-.8s-2.2.42-2.95.8c-.65.33-1.17.6-2.05.6v1.93c1.35 0 2.2-.43 2.95-.8.65-.33 1.17-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.32 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8V5.04c-.9 0-1.4-.25-2.05-.58zM17 8.09c-1.35 0-2.2.43-2.95.8-.65.35-1.15.6-2.05.6s-1.4-.25-2.05-.6c-.75-.38-1.57-.8-2.95-.8s-2.2.43-2.95.8c-.65.35-1.15.6-2.05.6v1.95c1.35 0 2.2-.43 2.95-.8.65-.32 1.18-.6 2.05-.6s1.4.25 2.05.6c.75.38 1.57.8 2.95.8s2.2-.43 2.95-.8c.65-.32 1.18-.6 2.05-.6.9 0 1.4.25 2.05.6.75.38 1.58.8 2.95.8V9.49c-.9 0-1.4-.25-2.05-.6-.75-.38-1.6-.8-2.95-.8z');
27 |
28 | /** Info icon */
29 | export const InfoIconSvg = svgIcon('M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z');
30 |
31 | /** Annotation icon */
32 | export const TextsmsOutlinedSvg = svgIcon('M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2m0 14H6l-2 2V4h16zM7 9h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z');
33 |
34 | /** "Previous" icon */
35 | export const ChevronLeftSvg = svgIcon('M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z');
36 |
37 | /** "Next" icon */
38 | export const ChevronRightSvg = svgIcon('M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z');
39 |
40 | /** "Waiting" icon */
41 | export const HourglassBottomSvg = svgIcon('m18 22-.01-6L14 12l3.99-4.01L18 2H6v6l4 4-4 3.99V22zM8 7.5V4h8v3.5l-4 4z');
42 |
--------------------------------------------------------------------------------
/src/app/ui/annotation-row-controls.tsx:
--------------------------------------------------------------------------------
1 | import { Button, IconButton } from 'molstar/lib/mol-plugin-ui/controls/common';
2 | import { MoreHorizSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
3 | import { ParameterControls, ParameterControlsProps } from 'molstar/lib/mol-plugin-ui/controls/parameters';
4 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
5 | import React from 'react';
6 |
7 |
8 | interface AnnotationRowControlsProps extends ParameterControlsProps
{
9 | shortTitle?: string,
10 | title: string,
11 | applied?: boolean,
12 | onChangeApplied?: (applied: boolean) => void,
13 | errorMessage?: string,
14 | }
15 |
16 | interface AnnotationRowControlsState {
17 | applied: boolean,
18 | optionsExpanded: boolean,
19 | }
20 |
21 |
22 | /** UI controls for a single annotation source (row) in Annotations section */
23 | export class AnnotationRowControls
extends React.PureComponent, AnnotationRowControlsState> {
24 | state = { applied: false, optionsExpanded: false };
25 |
26 | isApplied() {
27 | return this.props.applied ?? this.state.applied;
28 | }
29 |
30 | toggleApplied(applied?: boolean) {
31 | const newState = applied ?? !this.isApplied();
32 | if (this.props.applied === undefined) {
33 | this.setState({ applied: newState });
34 | }
35 | this.props.onChangeApplied?.(newState);
36 | }
37 |
38 | toggleOptions() {
39 | this.setState(s => ({ optionsExpanded: !s.optionsExpanded }));
40 | }
41 |
42 | render() {
43 | if (!this.props.params) return null;
44 | return <>
45 |
46 |
49 | this.toggleApplied()} toggleState={false}
50 | svg={!this.isApplied() ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg}
51 | title={`Click to ${this.isApplied() ? 'hide' : 'show'} ${this.props.title}`} small className='msp-form-control' flex />
52 | this.toggleOptions()} svg={MoreHorizSvg} title='Options' toggleState={this.state.optionsExpanded} className='msp-form-control' flex />
53 |
54 | {this.state.optionsExpanded &&
55 |
56 |
57 |
58 | {this.renderOptions()}
59 |
60 |
61 |
62 | }
63 | >;
64 | }
65 |
66 | renderOptions() {
67 | if (this.props.errorMessage) {
68 | return {this.props.errorMessage}
;
69 | }
70 | return ;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/scripts.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const argparse = require('argparse');
4 |
5 | const PACKAGE_ROOT_PATH = process.cwd();
6 | const PACKAGE = require(path.join(PACKAGE_ROOT_PATH, 'package.json'));
7 |
8 | const banner = [
9 | '/**',
10 | ` * ${PACKAGE.name}`,
11 | ` * @version ${PACKAGE.version}`,
12 | ' * @link https://github.com/PDBeurope/pdbe-molstar',
13 | ' * @license Apache 2.0',
14 | ' */',
15 | ].join('\n');
16 |
17 | const license = [
18 | '/**',
19 | ' * Copyright 2019-2023 Mandar Deshpande , Adam Midlik ',
20 | ' * European Bioinformatics Institute (EBI, http://www.ebi.ac.uk/)',
21 | ' * European Molecular Biology Laboratory (EMBL, http://www.embl.de/)',
22 | ' * Licensed under the Apache License, Version 2.0 (the "License");',
23 | ' * you may not use this file except in compliance with the License.',
24 | ' * You may obtain a copy of the License at ',
25 | ' * http://www.apache.org/licenses/LICENSE-2.0',
26 | ' * ',
27 | ' * Unless required by applicable law or agreed to in writing, software',
28 | ' * distributed under the License is distributed on an "AS IS" BASIS, ',
29 | ' * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.',
30 | ' * See the License for the specific language governing permissions and ',
31 | ' * limitations under the License.',
32 | ' */',
33 | ].join('\n');
34 |
35 | function removeFiles(...paths) {
36 | for (const path of paths) {
37 | fs.rmSync(path, { recursive: true, force: true });
38 | }
39 | }
40 |
41 | function addBanner(file) {
42 | if (!fs.existsSync(file)) return;
43 | const contents = [
44 | banner,
45 | fs.readFileSync(file, { encoding: 'utf8' }),
46 | ];
47 | fs.writeFileSync(file, contents.join('\n\n'), { encoding: 'utf8' });
48 | }
49 |
50 | const scripts = {
51 | /** Remove any files produced by the build process */
52 | 'clean-all': () => {
53 | removeFiles('lib', 'build', 'tsconfig.tsbuildinfo');
54 | },
55 |
56 | /** Remove unnecessary files produced by the build process */
57 | 'clean-rubbish': () => {
58 | removeFiles(`build/${PACKAGE.name}-light-plugin.js`);
59 | },
60 |
61 | /** Build web component */
62 | 'bundle-webcomponent': () => {
63 | const outputFile = `build/${PACKAGE.name}-component.js`;
64 | removeFiles(outputFile);
65 | const contents = [
66 | license,
67 | fs.readFileSync(`build/${PACKAGE.name}-plugin.js`, { encoding: 'utf8' }),
68 | fs.readFileSync(`lib/${PACKAGE.name}-component-build.js`, { encoding: 'utf8' }),
69 | ];
70 | fs.writeFileSync(outputFile, contents.join('\n\n'), { encoding: 'utf8' });
71 | },
72 |
73 | /** Add a banner with version info to the built files */
74 | 'add-banners': () => {
75 | addBanner(`build/${PACKAGE.name}-plugin.js`);
76 | addBanner(`build/${PACKAGE.name}-plugin.js.LICENSE.txt`);
77 | addBanner(`build/${PACKAGE.name}-component.js`);
78 | addBanner(`build/${PACKAGE.name}.css`);
79 | addBanner(`build/${PACKAGE.name}-light.css`);
80 | },
81 | };
82 |
83 |
84 | const parser = new argparse.ArgumentParser({ description: '' });
85 | parser.add_argument('script_name', { choices: Object.keys(scripts) });
86 | const args = parser.parse_args();
87 |
88 | console.log('Running script', args.script_name);
89 |
90 | scripts[args.script_name]();
91 |
--------------------------------------------------------------------------------
/src/app/ui/pdbe-viewport.tsx:
--------------------------------------------------------------------------------
1 | import { PurePluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
2 | import { AnimationViewportControls, LociLabels, SelectionViewportControls, StateSnapshotViewportControls, TrajectoryViewportControls, ViewportSnapshotDescription } from 'molstar/lib/mol-plugin-ui/controls';
3 | import { DefaultViewport } from 'molstar/lib/mol-plugin-ui/plugin';
4 | import { BackgroundTaskProgress } from 'molstar/lib/mol-plugin-ui/task';
5 | import { Toasts } from 'molstar/lib/mol-plugin-ui/toast';
6 | import { Viewport, ViewportControls } from 'molstar/lib/mol-plugin-ui/viewport';
7 | import { ComponentClass, JSXElementConstructor } from 'react';
8 | import { CustomControls } from './custom-controls';
9 | import { WithLoadingOverlay } from './overlay';
10 |
11 |
12 | /** A modified copy of DefaultViewport */
13 | export class CustomizableDefaultViewport extends DefaultViewport {
14 | render() {
15 | const VPControls = this.plugin.spec.components?.viewport?.controls || ViewportControls;
16 | const SnapshotDescription = this.plugin.spec.components?.viewport?.snapshotDescription || ViewportSnapshotDescription;
17 |
18 | return <>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | >;
42 | }
43 | }
44 |
45 | /** Return a React component with MainContent, expanded to whole browser window whenever `this.plugin.layout.state.isExpanded === true`. */
46 | function Fullscreenable(MainContent: JSXElementConstructor<{}> | React.FC): ComponentClass<{}> {
47 | return class _Fullscreenable extends PurePluginUIComponent<{}, { fullscreen: boolean }> {
48 | state = { fullscreen: this.plugin.layout.state.isExpanded };
49 |
50 | componentDidMount(): void {
51 | this.subscribe(this.plugin.layout.events.updated, () => {
52 | this.setState({ fullscreen: this.plugin.layout.state.isExpanded });
53 | });
54 | }
55 |
56 | render() {
57 | return
58 | ;
59 |
;
60 | }
61 | };
62 | }
63 |
64 |
65 | /** Version of `PDBeViewport` to use as part of other components. Does not expand to fullscreen individually. */
66 | export const PDBeViewport_NoFullscreen = WithLoadingOverlay(CustomizableDefaultViewport);
67 |
68 | /** Component containing 3D canvas, button in top left and top right corners, and tooltip box (center panel in default layout). Changes to fullscreen view by "Toggle Expanded Viewport" button, or "expanded" option. */
69 | export const PDBeViewport = Fullscreenable(PDBeViewport_NoFullscreen);
70 |
--------------------------------------------------------------------------------
/src/app/ui/pdbe-structure-controls.tsx:
--------------------------------------------------------------------------------
1 | import { PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
2 | import { StructureComponentControls } from 'molstar/lib/mol-plugin-ui/structure/components';
3 | import { StructureMeasurementsControls } from 'molstar/lib/mol-plugin-ui/structure/measurements';
4 | import { StructureSourceControls } from 'molstar/lib/mol-plugin-ui/structure/source';
5 | import { VolumeStreamingControls, VolumeSourceControls } from 'molstar/lib/mol-plugin-ui/structure/volume';
6 | import { AnnotationsComponentControls } from './annotation-controls';
7 | import { Icon, BuildSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
8 | import { SuperpositionComponentControls } from './superposition-components';
9 | import { StructureQuickStylesControls } from 'molstar/lib/mol-plugin-ui/structure/quick-styles';
10 | import { AlphafoldPaeControls, AlphafoldSuperpositionControls } from './alphafold-superposition';
11 | import { SuperpositionModelExportUI } from './export-superposition';
12 | import { AlphafoldTransparencyControls } from './alphafold-tranparency';
13 | import { AssemblySymmetryData } from 'molstar/lib/extensions/assembly-symmetry/prop';
14 |
15 |
16 | export class PDBeStructureTools extends PluginUIComponent {
17 | render() {
18 | const AssemblySymmetryKey = AssemblySymmetryData.Tag.Representation;
19 | return <>
20 | Structure Tools
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | >;
31 | }
32 | }
33 |
34 | export class CustomStructureControls extends PluginUIComponent<{ initiallyCollapsed?: boolean, takeKeys?: string[], skipKeys?: string[] }> {
35 | componentDidMount() {
36 | this.subscribe(this.plugin.state.behaviors.events.changed, () => this.forceUpdate());
37 | }
38 |
39 | render() {
40 | const takeKeys = this.props.takeKeys ?? Array.from(this.plugin.customStructureControls.keys());
41 | const result: JSX.Element[] = [];
42 | for (const key of takeKeys) {
43 | if (this.props.skipKeys?.includes(key)) continue;
44 | const Controls = this.plugin.customStructureControls.get(key);
45 | if (!Controls) continue;
46 | result.push();
47 | }
48 | return result.length > 0 ? <>{result}> : null;
49 | }
50 | }
51 |
52 | export class PDBeLigandViewStructureTools extends PluginUIComponent {
53 | render() {
54 | return <>
55 | Structure Tools
56 |
57 |
58 |
59 |
60 | >;
61 | }
62 | }
63 |
64 | export class PDBeSuperpositionStructureTools extends PluginUIComponent {
65 | render() {
66 | return <>
67 | Structure Tools
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | >;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/sequence-color/behavior.ts:
--------------------------------------------------------------------------------
1 | import { ColorTypeLocation } from 'molstar/lib/mol-geo/geometry/color-data';
2 | import { CustomStructureProperties } from 'molstar/lib/mol-plugin-state/transforms/model';
3 | import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
4 | import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior';
5 | import { ColorTheme } from 'molstar/lib/mol-theme/color';
6 | import { ThemeDataContext } from 'molstar/lib/mol-theme/theme';
7 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
8 | import { BehaviorSubject, Unsubscribable } from 'rxjs';
9 | import { CustomSequenceColorTheme } from './color';
10 | import { SequenceColorAnnotationsProperty } from './sequence-color-annotations-prop';
11 | import { SequenceColorThemeProperty } from './sequence-color-theme-prop';
12 |
13 |
14 | interface ColorThemeSpec {
15 | provider: ColorTheme.Provider
,
16 | getProps?: (ctx: ThemeDataContext) => PD.Values
,
17 | }
18 |
19 | /** Allows coloring residues in sequence panel */
20 | export const SequenceColor = PluginBehavior.create<{ autoAttach: boolean }>({
21 | name: 'sequence-color',
22 | category: 'misc',
23 | display: {
24 | name: 'Sequence Color',
25 | description: 'Sequence Color extension, allows assigning custom residue colors to be shown in the sequence panel, based on a custom structure property',
26 | },
27 | ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
28 | sub?: Unsubscribable;
29 |
30 | register(): void {
31 | const colorThemeRegistry = this.ctx.representation.structure.themes.colorThemeRegistry;
32 | this.ctx.customStructureProperties.register(SequenceColorThemeProperty.makeProvider(colorThemeRegistry), this.params.autoAttach);
33 | this.ctx.customStructureProperties.register(SequenceColorAnnotationsProperty.Provider, this.params.autoAttach);
34 | const customColorThemeProvider = CustomSequenceColorTheme.makeProvider(colorThemeRegistry);
35 | if (this.ctx instanceof PluginUIContext) {
36 | const theme: BehaviorSubject = this.ctx.customUIState.experimentalSequenceColorTheme ??= new BehaviorSubject(undefined);
37 | this.sub = this.ctx.state.events.cell.stateUpdated.subscribe(s => {
38 | if (s.cell.transform.transformer === CustomStructureProperties) {
39 | theme.next({ provider: customColorThemeProvider });
40 | }
41 | });
42 | }
43 | }
44 | update(p: { autoAttach: boolean }) {
45 | const updated = this.params.autoAttach !== p.autoAttach;
46 | this.params.autoAttach = p.autoAttach;
47 | this.ctx.customStructureProperties.setDefaultAutoAttach(SequenceColorThemeProperty.Name, this.params.autoAttach);
48 | this.ctx.customStructureProperties.setDefaultAutoAttach(SequenceColorAnnotationsProperty.Name, this.params.autoAttach);
49 | return updated;
50 | }
51 | unregister() {
52 | this.ctx.customStructureProperties.unregister(SequenceColorThemeProperty.Name);
53 | this.ctx.customStructureProperties.unregister(SequenceColorAnnotationsProperty.Name);
54 | this.sub?.unsubscribe();
55 | this.sub = undefined;
56 | if (this.ctx instanceof PluginUIContext) {
57 | const theme: BehaviorSubject | undefined = this.ctx.customUIState.experimentalSequenceColorTheme;
58 | theme?.next(undefined);
59 | }
60 | }
61 | },
62 | params: () => ({
63 | autoAttach: PD.Boolean(true),
64 | }),
65 | });
66 |
--------------------------------------------------------------------------------
/src/app/extensions/state-gallery/behavior.ts:
--------------------------------------------------------------------------------
1 | import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior';
2 | import { shallowEqual } from 'molstar/lib/mol-util';
3 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
4 | import { BehaviorSubject } from 'rxjs';
5 | import { ExtensionCustomState, PluginCustomControls } from '../../plugin-custom-state';
6 | import { Image, LoadingStatus, StateGalleryManager } from './manager';
7 | import { StateGalleryControls, StateGalleryTitleBox } from './ui';
8 |
9 |
10 | /** Name used when registering extension, custom controls, etc. */
11 | export const StateGalleryExtensionName = 'pdbe-state-gallery';
12 |
13 | /** Plugin-bound state for StateGallery extension */
14 | export interface StateGalleryCustomState {
15 | requestedImage: BehaviorSubject,
16 | manager: BehaviorSubject,
17 | status: BehaviorSubject,
18 | }
19 | export const StateGalleryCustomState = ExtensionCustomState.getter(StateGalleryExtensionName);
20 |
21 | /** Parameters for StateGallery extension */
22 | export interface StateGalleryParams {
23 | /** Show "3D State Gallery" section in Structure Tools controls */
24 | showControls: boolean,
25 | /** Show a box in viewport with state title and arrows to move between states */
26 | showTitleBox: boolean,
27 | }
28 |
29 |
30 | /** `StateGallery` extension allows browsing pre-computed 3D states for a PDB entry */
31 | export const StateGallery = PluginBehavior.create({
32 | name: StateGalleryExtensionName,
33 | category: 'misc',
34 | display: {
35 | name: '3D State Gallery',
36 | description: 'Browse pre-computed 3D states for a PDB entry',
37 | },
38 | ctor: class extends PluginBehavior.Handler {
39 | register(): void {
40 | StateGalleryCustomState(this.ctx).requestedImage = new BehaviorSubject(undefined);
41 | StateGalleryCustomState(this.ctx).manager = new BehaviorSubject(undefined);
42 | StateGalleryCustomState(this.ctx).status = new BehaviorSubject('ready');
43 | this.toggleStructureControls(this.params.showControls);
44 | this.toggleTitleBox(this.params.showTitleBox);
45 | }
46 |
47 | update(p: StateGalleryParams): boolean {
48 | if (shallowEqual(p, this.params)) return false;
49 | this.toggleStructureControls(p.showControls);
50 | this.toggleTitleBox(p.showTitleBox);
51 | this.params = p;
52 | return true;
53 | }
54 |
55 | unregister() {
56 | this.toggleStructureControls(false);
57 | this.toggleTitleBox(false);
58 | ExtensionCustomState.clear(this.ctx, StateGalleryExtensionName);
59 | }
60 |
61 | /** Register/unregister custom structure controls */
62 | private toggleStructureControls(show: boolean) {
63 | PluginCustomControls.toggle(this.ctx, 'structure-tools', StateGalleryExtensionName, StateGalleryControls, show);
64 | }
65 | /** Register/unregister title box */
66 | private toggleTitleBox(show: boolean) {
67 | PluginCustomControls.toggle(this.ctx, 'viewport-top-center', StateGalleryExtensionName, StateGalleryTitleBox, show);
68 | }
69 | },
70 | params: () => ({
71 | showControls: PD.Boolean(true, { description: 'Show "3D State Gallery" section in Structure Tools controls' }),
72 | showTitleBox: PD.Boolean(true, { description: 'Show a box in viewport with state title and arrows to move between states' }),
73 | }),
74 | });
75 |
76 |
77 | /** Public functions provided by the `StateGallery` extension */
78 | export const StateGalleryExtensionFunctions = {
79 | StateGalleryManager,
80 | StateGalleryCustomState,
81 | UI: {
82 | StateGalleryControls,
83 | },
84 | };
85 |
--------------------------------------------------------------------------------
/src/app/sifts-mapping.ts:
--------------------------------------------------------------------------------
1 | import { Column } from 'molstar/lib/mol-data/db';
2 | import { MmcifFormat } from 'molstar/lib/mol-model-formats/structure/mmcif';
3 | import { CustomModelProperty } from 'molstar/lib/mol-model-props/common/custom-model-property';
4 | import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property';
5 | import { Model } from 'molstar/lib/mol-model/structure';
6 | import { StructureElement } from 'molstar/lib/mol-model/structure/structure';
7 |
8 |
9 | export { SIFTSMapping as SIFTSMapping };
10 |
11 | export interface SIFTSMappingMapping {
12 | readonly dbName: string[],
13 | readonly accession: string[],
14 | readonly num: string[],
15 | readonly residue: string[],
16 | }
17 |
18 | namespace SIFTSMapping {
19 | export const Provider: CustomModelProperty.Provider<{}, SIFTSMappingMapping> = CustomModelProperty.createProvider({
20 | label: 'SIFTS Mapping',
21 | descriptor: CustomPropertyDescriptor({
22 | name: 'sifts_sequence_mapping',
23 | }),
24 | type: 'static',
25 | defaultParams: {},
26 | getParams: () => ({}),
27 | isApplicable: (data: Model) => isAvailable(data),
28 | obtain: async (ctx, data) => {
29 | return { value: fromCif(data) };
30 | },
31 | });
32 |
33 | export function isAvailable(model: Model) {
34 | if (!MmcifFormat.is(model.sourceData)) return false;
35 |
36 | const {
37 | pdbx_sifts_xref_db_name: db_name,
38 | pdbx_sifts_xref_db_acc: db_acc,
39 | pdbx_sifts_xref_db_num: db_num,
40 | pdbx_sifts_xref_db_res: db_res,
41 | } = model.sourceData.data.db.atom_site;
42 |
43 | return db_name.isDefined && db_acc.isDefined && db_num.isDefined && db_res.isDefined;
44 | }
45 |
46 | export function getKey(loc: StructureElement.Location) {
47 | const model = loc.unit.model;
48 | const data = Provider.get(model).value;
49 | if (!data) return '';
50 | const rI = model.atomicHierarchy.residueAtomSegments.index[loc.element];
51 | return data.accession[rI];
52 | }
53 |
54 | export function getLabel(loc: StructureElement.Location) {
55 | const model = loc.unit.model;
56 | const data = Provider.get(model).value;
57 | if (!data) return;
58 | const rI = model.atomicHierarchy.residueAtomSegments.index[loc.element];
59 | const dbName = data.dbName[rI];
60 | if (!dbName) return;
61 | return `${dbName} ${data.accession[rI]} ${data.num[rI]} ${data.residue[rI]}`;
62 | }
63 |
64 | function fromCif(model: Model): SIFTSMappingMapping | undefined {
65 | if (!MmcifFormat.is(model.sourceData)) return;
66 |
67 | const {
68 | pdbx_sifts_xref_db_name: db_name,
69 | pdbx_sifts_xref_db_acc: db_acc,
70 | pdbx_sifts_xref_db_num: db_num,
71 | pdbx_sifts_xref_db_res: db_res,
72 | } = model.sourceData.data.db.atom_site;
73 |
74 | if (!db_name.isDefined || !db_acc.isDefined || !db_num.isDefined || !db_res.isDefined) return;
75 |
76 | const { atomSourceIndex } = model.atomicHierarchy;
77 | const { count, offsets: residueOffsets } = model.atomicHierarchy.residueAtomSegments;
78 | const dbName = new Array(count);
79 | const accession = new Array(count);
80 | const num = new Array(count);
81 | const residue = new Array(count);
82 |
83 | for (let i = 0; i < count; i++) {
84 | const row = atomSourceIndex.value(residueOffsets[i]);
85 |
86 | if (db_name.valueKind(row) !== Column.ValueKind.Present) {
87 | dbName[i] = '';
88 | accession[i] = '';
89 | num[i] = '';
90 | residue[i] = '';
91 | continue;
92 | }
93 |
94 | dbName[i] = db_name.value(row);
95 | accession[i] = db_acc.value(row);
96 | num[i] = db_num.value(row);
97 | residue[i] = db_res.value(row);
98 | }
99 |
100 | return { dbName, accession, num, residue };
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/app/sequence-color/sequence-color-theme-prop.ts:
--------------------------------------------------------------------------------
1 | import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property';
2 | import { CustomStructureProperty } from 'molstar/lib/mol-model-props/common/custom-structure-property';
3 | import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property';
4 | import { Structure } from 'molstar/lib/mol-model/structure';
5 | import { ColorTheme } from 'molstar/lib/mol-theme/color';
6 | import { ColorNames } from 'molstar/lib/mol-util/color/names';
7 | import { deepClone } from 'molstar/lib/mol-util/object';
8 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
9 |
10 |
11 | const AnyParams = PD.Value({}, { description: 'Parameter description not available' });
12 |
13 | export namespace SequenceColorThemeProperty {
14 | /** Provider name (key) for this custom property */
15 | export const Name = 'sequence-color-theme';
16 |
17 | /** Parameter definition for this custom property */
18 | export type Params = ReturnType;
19 | /** Create parameter definition for this custom property, using information from a color theme registry.
20 | * If `colorThemeRegistry` is undefined, the `theme` parameter will be typed as `any` (i.e. no UI support). */
21 | export function makeParams(colorThemeRegistry: ColorTheme.Registry | undefined) {
22 | return {
23 | useTheme: PD.Boolean(false, { description: 'Turn on/off background sequence color theme' }),
24 | theme: colorThemeRegistry ?
25 | PD.Mapped(
26 | 'uniform',
27 | colorThemeRegistry.types,
28 | name => {
29 | try {
30 | return PD.Group(colorThemeRegistry.get(name).getParams({ structure: Structure.Empty }));
31 | } catch (err) {
32 | console.warn(`Failed to obtain parameter definition for theme "${name}"`);
33 | return AnyParams;
34 | }
35 | },
36 | { hideIf: p => !p.useTheme })
37 | : PD.Group({ name: PD.Text(), params: AnyParams }, { hideIf: p => !p.useTheme }),
38 | themeStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { hideIf: p => !p.useTheme, description: 'Allows to "dilute" color coming from background sequence color theme' }),
39 | dilutionColor: PD.Color(ColorNames.white, { hideIf: p => !p.useTheme || p.themeStrength === 1, description: 'Color used for "diluting" background sequence color theme' }),
40 | };
41 | }
42 |
43 | /** Type of parameter values for this custom property */
44 | export type Props = PD.Values;
45 |
46 | /** Type of values of this custom property */
47 | export type Data = Props;
48 |
49 | /** Create a provider for this custom property, using information from a color theme registry.
50 | * If `colorThemeRegistry` is undefined, the provider will work, but parameter definitions will not be inferred (i.e. limited UI support). */
51 | export function makeProvider(colorThemeRegistry: ColorTheme.Registry | undefined): CustomStructureProperty.Provider {
52 | const params = makeParams(colorThemeRegistry);
53 | return CustomStructureProperty.createProvider({
54 | label: 'Sequence Color Theme',
55 | descriptor: CustomPropertyDescriptor({
56 | name: Name,
57 | }),
58 | type: 'root',
59 | defaultParams: params,
60 | getParams: (data: Structure) => params,
61 | isApplicable: (data: Structure) => data.root === data,
62 | obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial) => {
63 | const fullProps = { ...PD.getDefaultValues(params), ...props };
64 | return { value: deepClone(fullProps) } satisfies CustomProperty.Data;
65 | },
66 | });
67 | }
68 |
69 | /** Default provider for this custom property, without type information from a color theme registry (i.e. limited UI support).
70 | * Use `makeProvider` to get a provider with full UI support. */
71 | export const Provider = makeProvider(undefined);
72 | }
73 |
--------------------------------------------------------------------------------
/src/app/ui/split-ui/split-ui.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { PluginReactContext } from 'molstar/lib/mol-plugin-ui/base';
3 | import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
4 | import { renderReact18 } from 'molstar/lib/mol-plugin-ui/react18';
5 | import { PluginUISpec } from 'molstar/lib/mol-plugin-ui/spec';
6 | import { ComponentProps, JSXElementConstructor, createElement, useEffect, useState } from 'react';
7 | import { DefaultPluginUISpec } from '../../spec';
8 |
9 |
10 | export interface LayoutSpecComponent> {
11 | target: string | HTMLElement,
12 | component: T,
13 | props?: ComponentProps,
14 | }
15 | export function LayoutSpecComponent>(target: string | HTMLElement, component: T, props?: ComponentProps): LayoutSpecComponent {
16 | return { target, component, props };
17 | }
18 |
19 | export type LayoutSpec = LayoutSpecComponent[];
20 |
21 |
22 | export async function createPluginSplitUI(options: {
23 | layout: LayoutSpec,
24 | spec?: PluginUISpec,
25 | render?: (component: any, container: Element) => any,
26 | onBeforeUIRender?: (ctx: PluginUIContext) => (Promise | void),
27 | }) {
28 | const { spec, layout, onBeforeUIRender } = options;
29 | const render = options.render ?? renderReact18;
30 | const ctx = new PluginUIContext(spec || DefaultPluginUISpec());
31 | await ctx.init();
32 | if (onBeforeUIRender) {
33 | await onBeforeUIRender(ctx);
34 | }
35 | for (const { target, component, props } of layout) {
36 | let targetElement: HTMLElement | undefined = undefined;
37 | try {
38 | targetElement = resolveHTMLElement(target);
39 | } catch (err) {
40 | console.warn('Skipping rendering a UI component because its target HTML element was not found.', err);
41 | }
42 | if (targetElement) {
43 | render(, targetElement);
44 | }
45 | // TODO in future: consider adding a listener that re-renders the React component when the div is removed and re-added to DOM
46 | }
47 | try {
48 | await ctx.canvas3dInitialized;
49 | } catch {
50 | // Error reported in UI/console elsewhere.
51 | }
52 | return ctx;
53 | }
54 |
55 | export function resolveHTMLElement(element: HTMLElement | string): HTMLElement {
56 | if (typeof element === 'string') {
57 | const result = document.getElementById(element);
58 | if (!result) throw new Error(`Element #${element} not found in DOM`);
59 | return result;
60 | } else {
61 | return element;
62 | }
63 | }
64 |
65 | type LoadState = { kind: 'initialized' } | { kind: 'pending' } | { kind: 'error', message: string };
66 |
67 | function PluginPanelWrapper({ plugin, component, props }: { plugin: PluginUIContext, component: JSXElementConstructor
, props: P }) {
68 | const [state, setState] = useState({ kind: 'pending' });
69 | useEffect(() => {
70 | setState(plugin.isInitialized ? { kind: 'initialized' } : { kind: 'pending' });
71 | let mounted = true;
72 | plugin.initialized.then(() => {
73 | if (mounted) setState({ kind: 'initialized' });
74 | }).catch(err => {
75 | if (mounted) setState({ kind: 'error', message: `${err}` });
76 | });
77 | return () => { mounted = false; };
78 | }, [plugin]);
79 |
80 | if (state.kind !== 'initialized') {
81 | const message = state.kind === 'error' ? `Initialization error: ${state.message}` : 'Waiting for plugin initialization';
82 | return ;
85 | }
86 |
87 | return
88 |
89 |
90 | {createElement(component, props)}
91 |
92 |
93 | ;
94 | }
95 |
--------------------------------------------------------------------------------
/src/app/domain-annotations/color.ts:
--------------------------------------------------------------------------------
1 | import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property';
2 | import { Location } from 'molstar/lib/mol-model/location';
3 | import { StructureElement } from 'molstar/lib/mol-model/structure';
4 | import { ColorTheme, LocationColor } from 'molstar/lib/mol-theme/color';
5 | import { ThemeDataContext } from 'molstar/lib/mol-theme/theme';
6 | import { Color } from 'molstar/lib/mol-util/color';
7 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
8 | import { DomainAnnotations, DomainAnnotationsProvider } from './prop';
9 |
10 |
11 | const DomainColors = {
12 | /** Applied to a part of structure which is not included in the domain */
13 | outside: Color.fromRgb(170, 170, 170),
14 | /** Applied to a part of structure which is included in the domain */
15 | inside: Color.fromRgb(255, 112, 3),
16 | };
17 |
18 | function makeDomainAnnotationsColorThemeParams(domainSources: string[], domainNames: string[][]) {
19 | const map = {} as Record>;
20 | let defaultSource: string | undefined = undefined; // will be assigned to the first source with non-empty list of domains
21 | const nSources = domainSources.length;
22 | for (let i = 0; i < nSources; i++) {
23 | const source = domainSources[i];
24 | const domains = domainNames[i];
25 | if (domains.length > 0) {
26 | defaultSource ??= source;
27 | map[source] = PD.Group({
28 | domain: PD.Select(domains[0], domains.map(dom => [dom, dom])),
29 | }, { isFlat: true });
30 | }
31 | }
32 | map['None'] = PD.Group({
33 | domain: PD.Select('None', [['None', 'None']], { isHidden: true }), // this is to keep the same shape of props but `domain` param will not be displayed in UI
34 | }, { isFlat: true });
35 | return {
36 | source: PD.MappedStatic(defaultSource ?? 'None', map, { options: Object.keys(map).map(source => [source, source]) }), // `options` is to keep case-sensitive database names in UI
37 | };
38 | }
39 | /** DomainAnnotationsColorThemeParams for when the data are not available (yet or at all) */
40 | const DummyDomainAnnotationsColorThemeParams = makeDomainAnnotationsColorThemeParams([], []);
41 |
42 | export type DomainAnnotationsColorThemeParams = typeof DummyDomainAnnotationsColorThemeParams;
43 | export type DomainAnnotationsColorThemeProps = PD.Values;
44 |
45 | export function DomainAnnotationsColorTheme(ctx: ThemeDataContext, props: DomainAnnotationsColorThemeProps): ColorTheme {
46 | let color: LocationColor;
47 |
48 | const domainSource = props.source.name;
49 | const domainName = props.source.params.domain;
50 |
51 | if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(DomainAnnotationsProvider.descriptor) && domainSource !== 'None') {
52 | color = (location: Location) => {
53 | if (StructureElement.Location.is(location) && DomainAnnotations.isInDomain(location, domainSource, domainName)) {
54 | return DomainColors.inside;
55 | }
56 | return DomainColors.outside;
57 | };
58 | } else {
59 | color = () => DomainColors.outside;
60 | }
61 |
62 | return {
63 | factory: DomainAnnotationsColorTheme,
64 | granularity: 'group',
65 | color: color,
66 | props: props,
67 | description: 'Highlights Sequence and Structure Domain Annotations. Data obtained via PDBe.',
68 | };
69 | }
70 |
71 | export const DomainAnnotationsColorThemeProvider: ColorTheme.Provider = {
72 | name: 'pdbe-domain-annotations',
73 | label: 'Domain annotations',
74 | category: ColorTheme.Category.Misc,
75 | factory: DomainAnnotationsColorTheme,
76 | getParams: ctx => {
77 | const domainTypes = DomainAnnotations.getDomainTypes(ctx.structure);
78 | const domainNames = DomainAnnotations.getDomainNames(ctx.structure);
79 | return makeDomainAnnotationsColorThemeParams(domainTypes, domainNames);
80 | },
81 | defaultValues: PD.getDefaultValues(DummyDomainAnnotationsColorThemeParams),
82 | isApplicable: (ctx: ThemeDataContext) => DomainAnnotations.isApplicable(ctx.structure?.models[0]),
83 | ensureCustomProperties: {
84 | attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => {
85 | return data.structure ? DomainAnnotationsProvider.attach(ctx, data.structure.models[0], undefined, true) : Promise.resolve();
86 | },
87 | detach: (data) => data.structure && DomainAnnotationsProvider.ref(data.structure.models[0], false),
88 | },
89 | };
90 |
--------------------------------------------------------------------------------
/webpack.config.production.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 |
5 | const PACKAGE_ROOT_PATH = process.cwd();
6 | const PACKAGE = require(path.join(PACKAGE_ROOT_PATH, 'package.json'));
7 |
8 | /** Webpack configuration for building the plugin bundle (pdbe-molstar-plugin.js, pdbe-molstar.css).
9 | * Also builds the light-skin version (pdbe-molstar-light-plugin.js (empty), pdbe-molstar-light.css). */
10 | const molstarConfig = {
11 | entry: {
12 | [PACKAGE.name]: path.resolve(__dirname, 'lib/index.js'),
13 | [PACKAGE.name + '-light']: path.resolve(__dirname, 'lib/styles/pdbe-molstar-light.scss'),
14 | },
15 | output: {
16 | filename: `[name]-plugin.js`,
17 | path: path.resolve(__dirname, 'build/'),
18 | },
19 | target: 'web',
20 | module: {
21 | rules: [
22 | {
23 | test: /\.(html|ico)$/,
24 | use: [
25 | {
26 | loader: 'file-loader',
27 | options: { name: '[name].[ext]' },
28 | },
29 | ],
30 | },
31 | {
32 | test: /\.(css|scss)$/,
33 | use: [
34 | MiniCssExtractPlugin.loader,
35 | { loader: 'css-loader', options: { sourceMap: false } },
36 | { loader: 'sass-loader', options: { sourceMap: false } },
37 | ],
38 | },
39 | ],
40 | },
41 | plugins: [
42 | new webpack.DefinePlugin({
43 | 'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
44 | __MOLSTAR_DEBUG_TIMESTAMP__: webpack.DefinePlugin.runtimeValue(() => `${new Date().valueOf()}`, true),
45 | }),
46 | new MiniCssExtractPlugin({
47 | filename: `[name].css`,
48 | }),
49 | ],
50 | resolve: {
51 | modules: ['node_modules', path.resolve(__dirname, 'lib/')],
52 | fallback: {
53 | fs: false,
54 | crypto: require.resolve('crypto-browserify'),
55 | path: require.resolve('path-browserify'),
56 | stream: require.resolve('stream-browserify'),
57 | vm: require.resolve('vm-browserify'),
58 | },
59 | },
60 | watchOptions: {
61 | aggregateTimeout: 750,
62 | },
63 | };
64 |
65 | /** Webpack configuration for building a part of the web-component bundle,
66 | * which will be concatenated with the plugin bundle to build the full
67 | * web-component bundle (pdbe-molstar-component.js) */
68 | const componentConfig = {
69 | entry: path.resolve(__dirname, `src/web-component/index.js`),
70 | output: {
71 | filename: `${PACKAGE.name}-component-build.js`,
72 | path: path.resolve(__dirname, 'lib/'),
73 | },
74 | target: 'web',
75 | resolve: {
76 | extensions: ['.js'],
77 | },
78 | externals: {
79 | PDBeMolstarPlugin: 'PDBeMolstarPlugin',
80 | },
81 | module: {
82 | rules: [
83 | {
84 | test: /\.css$/,
85 | use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }],
86 | },
87 | {
88 | test: /.(jpg|jpeg|png|svg)$/,
89 | use: ['url-loader'],
90 | },
91 | {
92 | test: /\.(js)$/,
93 | exclude: function excludeCondition(path) {
94 | const nonEs5SyntaxPackages = ['lit-element', 'lit-html'];
95 |
96 | // DO transpile these packages
97 | if (nonEs5SyntaxPackages.some(pkg => path.match(pkg))) {
98 | return false;
99 | }
100 |
101 | // Ignore all other modules that are in node_modules
102 | if (path.match(/node_modules\\/)) {
103 | return true;
104 | } else return false;
105 | },
106 | use: {
107 | loader: 'babel-loader',
108 | options: {
109 | babelrc: false,
110 | plugins: [
111 | [
112 | '@babel/plugin-transform-runtime',
113 | {
114 | regenerator: true,
115 | },
116 | ],
117 | ],
118 | },
119 | },
120 | },
121 | ],
122 | },
123 | };
124 |
125 | module.exports = [molstarConfig, componentConfig];
126 |
--------------------------------------------------------------------------------
/src/app/sequence-color/color.ts:
--------------------------------------------------------------------------------
1 | import { ElementSet } from 'molstar/lib/extensions/mvs/components/selector';
2 | import { StructureElement } from 'molstar/lib/mol-model/structure';
3 | import { ColorTheme, LocationColor } from 'molstar/lib/mol-theme/color';
4 | import { ThemeDataContext } from 'molstar/lib/mol-theme/theme';
5 | import { Color } from 'molstar/lib/mol-util/color';
6 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
7 | import { SequenceColorAnnotationsProperty } from './sequence-color-annotations-prop';
8 | import { SequenceColorThemeProperty } from './sequence-color-theme-prop';
9 |
10 |
11 | /** Special color value meaning "no color assigned" */
12 | const NoColor = Color(-1);
13 |
14 |
15 | export namespace CustomSequenceColorTheme {
16 | /** Provider name (key) for this color theme */
17 | export const Name = 'custom-sequence-color';
18 |
19 | /** Parameter definition for this color theme */
20 | export const Params = {};
21 | export type Params = typeof Params;
22 |
23 | /** Type of parameter values for this color theme */
24 | export type Props = PD.Values;
25 |
26 | export function Theme(ctx: ThemeDataContext, props: Props, colorThemeRegistry: ColorTheme.Registry | undefined): ColorTheme {
27 | return {
28 | factory: (ctx_, props_) => Theme(ctx_, props_, colorThemeRegistry),
29 | granularity: 'groupInstance',
30 | color: colorFn(ctx, colorThemeRegistry),
31 | props: props,
32 | description: 'Assigns colors based on custom structure property `SequenceColorProperty` and `SequenceColorBackgroundProperty`.',
33 | };
34 | }
35 |
36 | /** Create a provider for this color theme */
37 | export function makeProvider(colorThemeRegistry: ColorTheme.Registry | undefined): ColorTheme.Provider {
38 | return {
39 | name: Name,
40 | label: 'Custom Sequence Color',
41 | category: 'Miscellaneous',
42 | factory: (ctx, props) => Theme(ctx, props, colorThemeRegistry),
43 | getParams: ctx => Params,
44 | defaultValues: PD.getDefaultValues(Params),
45 | isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
46 | };
47 | }
48 | }
49 |
50 |
51 | /** Create color function based on `SequenceColorAnnotationsProperty` and `SequenceColorThemeProperty` */
52 | function colorFn(ctx: ThemeDataContext, colorThemeRegistry: ColorTheme.Registry | undefined): LocationColor {
53 | const background = themeColorFn(ctx, colorThemeRegistry);
54 | const foreground = annotColorFn(ctx);
55 | if (foreground && background) {
56 | return (location, isSecondary) => {
57 | const fgColor = foreground(location, isSecondary);
58 | if (fgColor >= 0) return fgColor;
59 | else return background(location, isSecondary);
60 | };
61 | }
62 | if (foreground) return foreground;
63 | if (background) return background;
64 | return () => NoColor;
65 | }
66 | /** Create color function based on `SequenceColorThemeProperty` */
67 | function themeColorFn(ctx: ThemeDataContext, colorThemeRegistry: ColorTheme.Registry | undefined): LocationColor | undefined {
68 | if (!colorThemeRegistry) return undefined;
69 | if (!ctx.structure || ctx.structure.isEmpty) return undefined;
70 | const data = SequenceColorThemeProperty.Provider.get(ctx.structure).value;
71 | if (!data?.useTheme) return undefined;
72 | const theme = colorThemeRegistry.get(data.theme.name)?.factory(ctx, data.theme.params);
73 | if (!theme || !('color' in theme)) return undefined;
74 | if (data.themeStrength === 1) return theme.color;
75 | if (data.themeStrength === 0) return () => data.dilutionColor;
76 | return (location, isSecondary) => Color.interpolate(data.dilutionColor, theme.color(location, isSecondary), data.themeStrength);
77 | }
78 | /** Create color function based on `SequenceColorAnnotationsProperty` */
79 | function annotColorFn(ctx: ThemeDataContext): LocationColor | undefined {
80 | if (!ctx.structure || ctx.structure.isEmpty) return undefined;
81 | const colorData = SequenceColorAnnotationsProperty.Provider.get(ctx.structure).value;
82 | if (!colorData || colorData.items.length === 0) return undefined;
83 | return location => StructureElement.Location.is(location) ? sequenceColorForLocation(colorData, location) : NoColor;
84 | }
85 |
86 | function sequenceColorForLocation(colorData: SequenceColorAnnotationsProperty.Data, location: StructureElement.Location): Color {
87 | const unitCache = colorData.colorCache[location.unit.id] ??= {};
88 | return unitCache[location.element] ??= findSequenceColorForLocation(colorData, location);
89 | }
90 |
91 | function findSequenceColorForLocation(colorData: SequenceColorAnnotationsProperty.Data, location: StructureElement.Location): Color {
92 | for (let i = colorData.items.length - 1; i >= 0; i--) { // last color matters
93 | const item = colorData.items[i];
94 | const elements = item.elementSet ??= ElementSet.fromSelector(location.structure, item.selector);
95 | if (ElementSet.has(elements, location)) {
96 | return item.color;
97 | }
98 | }
99 | return NoColor;
100 | }
101 |
--------------------------------------------------------------------------------
/src/app/alphafold-transparency.ts:
--------------------------------------------------------------------------------
1 | import { QualityAssessment } from 'molstar/lib/extensions/model-archive/quality-assessment/prop';
2 | import { Loci, isEmptyLoci } from 'molstar/lib/mol-model/loci';
3 | import { QueryContext, Structure, StructureElement, StructureSelection } from 'molstar/lib/mol-model/structure';
4 | import { StructureComponentRef, StructureRef } from 'molstar/lib/mol-plugin-state/manager/structure/hierarchy-state';
5 | import { PluginStateObject } from 'molstar/lib/mol-plugin-state/objects';
6 | import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
7 | import { PluginContext } from 'molstar/lib/mol-plugin/context';
8 | import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
9 | import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
10 | import { StateBuilder, StateObjectCell, StateSelection, StateTransform } from 'molstar/lib/mol-state';
11 | import { Transparency } from 'molstar/lib/mol-theme/transparency';
12 |
13 |
14 | type TransparencyEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell>, transparency?: StateObjectCell>) => Promise;
15 | const TransparencyManagerTag = 'transparency-controls';
16 |
17 | /** Select part of the structure with pLDDT <= score. */
18 | function getLociBelowPLDDT(score: number, contextData: Structure) {
19 | const queryExp = MS.struct.modifier.union([
20 | MS.struct.modifier.wholeResidues([
21 | MS.struct.modifier.union([
22 | MS.struct.generator.atomGroups({
23 | 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
24 | 'residue-test': MS.core.rel.lte([QualityAssessment.symbols.pLDDT.symbol(), score]),
25 | }),
26 | ]),
27 | ]),
28 | ]);
29 |
30 | const query = compile(queryExp);
31 | const sel = query(new QueryContext(contextData));
32 | return StructureSelection.toLociWithSourceUnits(sel);
33 |
34 | }
35 |
36 | export function applyAFTransparency(plugin: PluginContext, structure: Readonly, transparency: number, pLDDT = 70) {
37 | return plugin.dataTransaction(async ctx => {
38 | const loci = getLociBelowPLDDT(pLDDT, structure.cell.obj!.data);
39 | await setStructureTransparency(plugin, structure.components, transparency, loci);
40 | }, { canUndo: 'Apply Transparency' });
41 | }
42 |
43 | export async function setStructureTransparency(plugin: PluginContext, components: StructureComponentRef[], value: number, loci: StructureElement.Loci, types?: string[]) {
44 | await eachRepr(plugin, components, async (update, repr, transparencyCell) => {
45 | if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
46 |
47 | const structure = repr.obj!.data.sourceData;
48 | if (Loci.isEmpty(loci) || isEmptyLoci(loci)) return;
49 |
50 | const layer = {
51 | bundle: StructureElement.Bundle.fromLoci(loci),
52 | value,
53 | };
54 |
55 | if (transparencyCell) {
56 | const bundleLayers = [...transparencyCell.params!.values.layers, layer];
57 | const filtered = getFilteredBundle(bundleLayers, structure);
58 | update.to(transparencyCell).update(Transparency.toBundle(filtered));
59 | } else {
60 | const filtered = getFilteredBundle([layer], structure);
61 | update.to(repr.transform.ref)
62 | .apply(StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle, Transparency.toBundle(filtered), { tags: TransparencyManagerTag });
63 | }
64 | });
65 | }
66 |
67 | export async function clearStructureTransparency(plugin: PluginContext, components: StructureComponentRef[], types?: string[]) {
68 | await eachRepr(plugin, components, async (update, repr, transparencyCell) => {
69 | if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
70 | if (transparencyCell) {
71 | update.delete(transparencyCell.transform.ref);
72 | }
73 | });
74 | }
75 |
76 | async function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: TransparencyEachReprCallback) {
77 | const state = plugin.state.data;
78 | const update = state.build();
79 | for (const c of components) {
80 | for (const r of c.representations) {
81 | const transparency = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle, r.cell.transform.ref).withTag(TransparencyManagerTag));
82 | await callback(update, r.cell, transparency[0]);
83 | }
84 | }
85 |
86 | return update.commit({ doNotUpdateCurrent: true });
87 | }
88 |
89 | /** filter transparency layers for given structure */
90 | function getFilteredBundle(layers: Transparency.BundleLayer[], structure: Structure) {
91 | const transparency = Transparency.ofBundle(layers, structure.root);
92 | const merged = Transparency.merge(transparency);
93 | return Transparency.filter(merged, structure) as Transparency;
94 | }
95 |
--------------------------------------------------------------------------------
/src/app/ui/pdbe-screenshot-controls.tsx:
--------------------------------------------------------------------------------
1 | import { PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
2 | import { Button, ToggleButton } from 'molstar/lib/mol-plugin-ui/controls/common';
3 | import { CopySvg, CropFreeSvg, CropOrginalSvg, CropSvg, GetAppSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
4 | import { ParameterControls } from 'molstar/lib/mol-plugin-ui/controls/parameters';
5 | import { ScreenshotPreview } from 'molstar/lib/mol-plugin-ui/controls/screenshot';
6 | import { useBehavior } from 'molstar/lib/mol-plugin-ui/hooks/use-behavior';
7 | import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
8 | import { PluginContext } from 'molstar/lib/mol-plugin/context';
9 | import React from 'react';
10 |
11 |
12 | interface ImageControlsState {
13 | showPreview: boolean,
14 | isDisabled: boolean,
15 | imageData?: string,
16 | }
17 |
18 | export class DownloadScreenshotControls extends PluginUIComponent<{ close: () => void }, ImageControlsState> {
19 | state: ImageControlsState = {
20 | showPreview: true,
21 | isDisabled: false,
22 | } as ImageControlsState;
23 |
24 | private download = () => {
25 | this.plugin.helpers.viewportScreenshot?.download();
26 | this.props.close();
27 | };
28 |
29 | private copy = async () => {
30 | try {
31 | await this.plugin.helpers.viewportScreenshot?.copyToClipboard();
32 | PluginCommands.Toast.Show(this.plugin, {
33 | message: 'Copied to clipboard.',
34 | title: 'Screenshot',
35 | timeoutMs: 1500,
36 | });
37 | } catch {
38 | return this.copyImg();
39 | }
40 | };
41 |
42 | private copyImg = async () => {
43 | const src = await this.plugin.helpers.viewportScreenshot?.getImageDataUri();
44 | this.setState({ imageData: src });
45 | };
46 |
47 | componentDidMount() {
48 | this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
49 | this.setState({ isDisabled: v });
50 | });
51 | }
52 |
53 | componentWillUnmount() {
54 | this.setState({ imageData: undefined });
55 | }
56 |
57 | open = (e: React.ChangeEvent) => {
58 | if (!e.target.files || !e.target.files![0]) return;
59 | PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: e.target.files![0] });
60 | };
61 |
62 | render() {
63 | const hasClipboardApi = !!(navigator.clipboard as any)?.write;
64 |
65 | return
66 | {this.state.showPreview &&
67 |
68 |
69 |
}
70 |
71 | {!this.state.imageData && }
72 | {this.state.imageData && }
73 |
74 |
75 | {this.state.imageData &&
76 |
Right click below + Copy Image
77 |

78 |
}
79 |
80 |
;
81 | }
82 | }
83 |
84 | function ScreenshotParams({ plugin, isDisabled }: { plugin: PluginContext, isDisabled: boolean }) {
85 | const helper = plugin.helpers.viewportScreenshot!;
86 | const values = useBehavior(helper.behaviors.values);
87 |
88 | return helper.behaviors.values.next(v)} isDisabled={isDisabled} />;
89 | }
90 |
91 | function CropControls({ plugin }: { plugin: PluginContext }) {
92 | const helper = plugin.helpers.viewportScreenshot;
93 | const cropParams = useBehavior(helper?.behaviors.cropParams);
94 | useBehavior(helper?.behaviors.relativeCrop);
95 |
96 | if (!helper) return null;
97 |
98 | return
99 | helper.toggleAutocrop()} label={'Auto-crop ' + (cropParams?.auto ? 'On' : 'Off')} />
102 |
103 | {!cropParams?.auto &&
;
110 | }
111 |
--------------------------------------------------------------------------------
/src/app/superposition-export.ts:
--------------------------------------------------------------------------------
1 | import { utf8ByteCount, utf8Write } from 'molstar/lib/mol-io/common/utf8';
2 | import { Unit, to_mmCIF } from 'molstar/lib/mol-model/structure';
3 | import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
4 | import { PluginContext } from 'molstar/lib/mol-plugin/context';
5 | import { Task } from 'molstar/lib/mol-task';
6 | import { getFormattedTime } from 'molstar/lib/mol-util/date';
7 | import { download } from 'molstar/lib/mol-util/download';
8 | import { zip } from 'molstar/lib/mol-util/zip/zip';
9 | import { PluginCustomState } from './plugin-custom-state';
10 |
11 |
12 | export async function superpositionExportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
13 | try {
14 | await plugin.runTask(_superpositionExportHierarchy(plugin, options), { useOverlay: true });
15 | } catch (e) {
16 | console.error(e);
17 | plugin.log.error(`Model export failed. See console for details.`);
18 | }
19 | }
20 |
21 | function _superpositionExportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
22 | return Task.create('Export', async ctx => {
23 | await ctx.update({ message: 'Exporting...', isIndeterminate: true, canAbort: false });
24 |
25 | const format = options?.format ?? 'cif';
26 | // const { structures } = plugin.managers.structure.hierarchy.current;
27 | const customState = PluginCustomState(plugin);
28 | if (!customState.initParams) throw new Error('customState.initParams has not been initialized');
29 | if (!customState.superpositionState) throw new Error('customState.superpositionState has not been initialized');
30 | const superpositionState = customState.superpositionState;
31 |
32 | const segmentIndex = superpositionState.activeSegment - 1;
33 | const files: [name: string, data: string | Uint8Array][] = [];
34 | const entryMap = new Map();
35 | const structures = superpositionState.loadedStructs[segmentIndex].slice();
36 | if (!customState.initParams.moleculeId) throw new Error('initParams.moleculeId is not defined');
37 | if (superpositionState.alphafold.ref) structures.push(`AF-${customState.initParams.moleculeId}`);
38 | for (const molId of structures) {
39 | const modelRef = superpositionState.models[molId];
40 | if (!modelRef) continue;
41 | let isStrHidden = false;
42 | const _s: any = plugin.managers.structure.hierarchy.current.refs.get(modelRef!);
43 | if (_s.cell.state.isHidden) isStrHidden = true;
44 | for (const strComp of _s.components) {
45 | if (strComp.cell.state.isHidden) isStrHidden = true;
46 | }
47 | if (isStrHidden) continue;
48 |
49 | const s = _s.transform?.cell.obj?.data ?? _s.cell.obj?.data;
50 | if (!s) continue;
51 | if (s.models.length > 1) {
52 | plugin.log.warn(`[Export] Skipping ${_s.cell.obj?.label}: Multimodel exports not supported.`);
53 | continue;
54 | }
55 | if (s.units.some((u: any) => !Unit.isAtomic(u))) {
56 | plugin.log.warn(`[Export] Skipping ${_s.cell.obj?.label}: Non-atomic model exports not supported.`);
57 | continue;
58 | }
59 |
60 | const name = entryMap.has(s.model.entryId)
61 | ? `${s.model.entryId}_${entryMap.get(s.model.entryId)! + 1}.${format}`
62 | : `${s.model.entryId}.${format}`;
63 | entryMap.set(s.model.entryId, (entryMap.get(s.model.entryId) ?? 0) + 1);
64 |
65 | await ctx.update({ message: `Exporting ${s.model.entryId}...`, isIndeterminate: true, canAbort: false });
66 | if (s.elementCount > 100000) {
67 | // Give UI chance to update, only needed for larger structures.
68 | await new Promise(res => setTimeout(res, 50));
69 | }
70 |
71 | try {
72 | files.push([name, to_mmCIF(s.model.entryId, s, format === 'bcif', { copyAllCategories: true })]);
73 | } catch (e) {
74 | if (format === 'cif' && s.elementCount > 2000000) {
75 | plugin.log.warn(`[Export] The structure might be too big to be exported as Text CIF, consider using the BinaryCIF format instead.`);
76 | }
77 | throw e;
78 | }
79 | }
80 |
81 | if (files.length === 0) {
82 | PluginCommands.Toast.Show(plugin, {
83 | title: 'Export Models',
84 | message: 'No visible structure in the 3D view to export!',
85 | key: 'superposition-toast-1',
86 | timeoutMs: 7000,
87 | });
88 | return;
89 | }
90 |
91 | if (files.length === 1) {
92 | download(new Blob([files[0][1]]), files[0][0]);
93 | } else if (files.length > 1) {
94 | const zipData: Record = {};
95 | for (const [fn, data] of files) {
96 | if (data instanceof Uint8Array) {
97 | zipData[fn] = data;
98 | } else {
99 | const bytes = new Uint8Array(utf8ByteCount(data));
100 | utf8Write(bytes, 0, data);
101 | zipData[fn] = bytes;
102 | }
103 | }
104 | await ctx.update({ message: `Compressing Data...`, isIndeterminate: true, canAbort: false });
105 | const buffer = await zip(ctx, zipData);
106 | download(new Blob([new Uint8Array(buffer, 0, buffer.byteLength)]), `structures_${getFormattedTime()}.zip`);
107 | }
108 |
109 | plugin.log.info(`[Export] Done.`);
110 | });
111 | }
112 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file, following the suggestions of [Keep a CHANGELOG](http://keepachangelog.com/). This project adheres to [Semantic Versioning](http://semver.org/) for its most widely used - and defacto - public interfaces.
3 |
4 | ## [Unreleased]
5 |
6 | ## [v3.9.0] - 2025-12-18
7 | - Mol* core dependency updated to 5.4.2
8 | - Option `hideCanvasControls` accepts "reset" and "all"
9 | - Update options `highlightColor`, `selectColor`, `granularity` on update
10 | - Added option `modelId` (select model from trajectory)
11 | - Fix tooltip box flickering
12 |
13 | ## [v3.8.0] - 2025-10-03
14 | - Mol* core dependency updated to 5.0.0
15 | - Selections support `instance_id`
16 | - Added `.visual.sequenceColor`, `.visual.clearSequenceColor`
17 |
18 | ## [v3.7.2] - 2025-09-22
19 | - Fix missing PDBe logo
20 |
21 | ## [v3.7.1] - 2025-09-15
22 | - Modular layout: added `FullLayoutNoControlsUnlessExpanded`
23 |
24 | ## [v3.7.0] - 2025-09-02
25 | - Selections support `type_symbol`
26 | - Complex superposition extension allows RNA superposition
27 | - Option `mapSettings.defaultView` to set Volume Streaming view type
28 |
29 | ## [v3.6.0] - 2025-08-12
30 | - Option `hideCanvasControls` accepts "screenshot" value
31 | - Method `.visual.update` reflects changes in layout parameters (`expanded`, `hideCanvasControls`...)
32 | - Added `.visual.setViewDirection`
33 | - Mol* core dependency updated to 4.18.0
34 | - CSS building uses @use instead of @import
35 | - Complex superposition extension (PDBeMolstarPlugin.extensions.Complexes)
36 | - Selections with QueryParams implement AND-logic properly (all selector always taken into account)
37 |
38 | ## [v3.5.0] - 2025-05-28
39 | - Mol* core dependency updated to 4.17.0
40 | - Custom interactions extension (PDBeMolstarPlugin.extensions.Interactions)
41 | - MolViewSpec extension (PDBeMolstarPlugin.extensions.MVS)
42 |
43 | ## [v3.4.0] - 2025-03-31
44 | - Added `.visual.interactivityFocus` method
45 |
46 | ## [v3.3.2] - 2024-11-25
47 | - Fixed Domain Annotations crashing when no domains available
48 |
49 | ## [v3.3.1] - 2024-11-13
50 | - Mol* core dependency updated to 4.8.0
51 | - Fixes iOS volume rendering bug
52 |
53 | ## [v3.3.0] - 2024-08-22
54 | - Mol* core dependency updated to 4.5.0
55 | - Solves the bug with Export Models
56 | - Use BCIF files for AlphaFold models (unless `encoding: 'cif'`)
57 | - Added options `leftPanel`, `rightPanel`, `logPanel`, `tabs`
58 | - Option `hideCanvasControls` accepts "trajectory" value (for multi-model structures)
59 | - All color options accept color names and hexcodes
60 | - `visualStyle` option allows per-component specification
61 | - Modular UI rendering (5 top-level components renderable separately)
62 | - Foldseek extension (PDBeMolstarPlugin.extensions.Foldseek)
63 | - StateGallery extension
64 |
65 | ## [v3.2.0] - 2024-04-24
66 | - Mol* core dependency updated to 3.45.0
67 | - Removed Assembly Symmetry hack (now will hide assembly symmetry section for non-biological assemblies)
68 | - Manual testing via `portfolio.html`
69 | - Fixed `hideStructure.nonStandard` option
70 | - `hideStructure.het` option also hides ions
71 | - Removed `loadCartoonsOnly` option
72 | - Setting highlight and selection color (by `.visual.setColor()`) includes the outline color
73 | - Web-component attributes renamed to `ligand-auth-asym-id` and `ligand-struct-asym-id` (lowercase i)
74 | - `.visual.select()` function:
75 | - Improved performance
76 | - Allows `color: null` (do not apply color)
77 | - `keepColors` and `keepRepresentations` parameters to avoid clearing previous selections
78 | - Added `.visual.tooltips` and `.visual.clearTooltips` for setting custom tooltips
79 | - Built files don't contain package version in the filename
80 |
81 | ## [v3.1.3] - 2023-12-06
82 | - Added ``Assembly Symmetry`` to structure controls, requires setting ``symmetryAnnotation`` in initialization parameters
83 | - Keep sequence panel in settings even when initially hidden
84 | - Changed `tsconfig.json` to place `tsconfig.tsbuildinfo` correctly (for incremental build)
85 | - Fixed coloring after annotation is switched off (revert to `chain-id`, not `polymer-id`)
86 | - Loading overlay with animated PDBe logo (requires initParam `loadingOverlay`)
87 | - Use linting to keep code nice
88 | - Correctly handle numeric value 0 in selections
89 | - Fetch structures from static files when possible, instead of using ModelServer
90 |
91 | ## [v3.1.2] - 2023-08-01
92 | - Added PDBe Sifts Mappings module to solve UniPort mappings issue
93 | - Split Webpack config file into separate files for Production and Development
94 |
95 | ## [v3.1.1] - 2023-05-18
96 | - Controls menu visible for AlphaFold view
97 | - ``Reactive`` parameter addition for better responsive layout support
98 |
99 | ## [v3.1.0] - 2022-10-24
100 | - Mol* core dependency updated to V3.15.0
101 | - Superposition view - added option to superpose AlphaFold model
102 | - UniPort residue numbering param addition to higlight and selection helper methods
103 | - New param to display Sequence panel [62](https://github.com/molstar/pdbe-molstar/issues/62)
104 |
105 | ## [v1.2.1] - 2021-12-03
106 | - Selection helper method ``representationColor`` param issue fix
107 |
108 | ## [v1.2.0] - 2021-09-20
109 | - Add parameters to support AlphaFold Protein Structure DB view
110 | - Add parameters to customize mouse events, issues [#8](https://github.com/PDBeurope/pdbe-molstar/issues/8)
111 | - Add parameter to customize ``lighting`` setting
112 | - Extend ``Selection / highlight`` helper methods to support ``label_atom_id`` list, issues [#32](https://github.com/PDBeurope/pdbe-molstar/issues/32)
113 | - Extend helper methods support for multiple structures
114 | - Extend ``hideCanvasControls``
115 | - Fix Frame rate drop issue while rotating the canvas
116 | - Fix RGB color issue for params starting with r:0
--------------------------------------------------------------------------------
/src/app/plugin-custom-state.ts:
--------------------------------------------------------------------------------
1 | import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
2 | import { PluginContext } from 'molstar/lib/mol-plugin/context';
3 | import { StateSelection, StateTransform } from 'molstar/lib/mol-state';
4 | import { JSXElementConstructor } from 'react';
5 | import { Subject } from 'rxjs';
6 | import { InitParams } from './spec';
7 |
8 |
9 | export interface PluginCustomState {
10 | initParams?: InitParams,
11 | events?: {
12 | segmentUpdate: Subject,
13 | superpositionInit: Subject,
14 | isBusy: Subject,
15 | },
16 | superpositionState?: {
17 | models: { [molId: string]: string },
18 | entries: { [pdbId: string]: StateSelection.Selector },
19 | refMaps: { [ref: string]: string },
20 | segmentData: Segment[] | undefined,
21 | matrixData: { [key: string]: { matrix: number[][] } },
22 | activeSegment: number,
23 | loadedStructs: string[][],
24 | visibleRefs: StateTransform.Ref[][],
25 | invalidStruct: string[],
26 | noMatrixStruct: string[],
27 | hets: { [key: string]: unknown[] },
28 | /** Counts how many colors have been assigned, per segment */
29 | colorCounters: number[],
30 | alphafold: {
31 | apiData: {
32 | /** URL of BCIF file */
33 | bcif: string,
34 | /** URL of CIF file */
35 | cif: string,
36 | /** URL of PAE image */
37 | pae: string,
38 | /** Length of UniProt sequence */
39 | length: number,
40 | },
41 | length: number,
42 | ref: string,
43 | traceOnly: boolean,
44 | visibility: boolean[],
45 | transforms: Mat4[],
46 | rmsds: string[][],
47 | },
48 | },
49 | superpositionError?: string,
50 | /** Space for extensions to save their plugin-bound custom state. Only access via `ExtensionCustomState`! */
51 | extensions?: {
52 | [extensionId: string]: {} | undefined,
53 | },
54 | /** Registry for custom UI components. Only access via `PluginCustomControls`! */
55 | customControls?: { [region in PluginCustomControlRegion]?: PluginCustomControlRegistry },
56 | }
57 |
58 | export interface ClusterMember { pdb_id: string, auth_asym_id: string, struct_asym_id: string, entity_id: number, is_representative: boolean };
59 | export interface Segment { segment_start: number, segment_end: number, clusters: ClusterMember[][], isHetView?: boolean, isBinary?: boolean };
60 |
61 |
62 | /** Access `plugin.customState` only through this function to get proper typing.
63 | * Supports getting and setting properties. */
64 | export function PluginCustomState(plugin: PluginContext): PluginCustomState {
65 | return (plugin.customState as any) ??= {};
66 | }
67 |
68 |
69 | /** Functions for accessing plugin-bound custom state for extensions. */
70 | export const ExtensionCustomState = {
71 | /** Get plugin-bound custom state for a specific extension. If not present, initialize with empty object. */
72 | get(plugin: PluginContext, extensionId: string): Partial {
73 | const extensionStates = PluginCustomState(plugin).extensions ??= {};
74 | const extensionState: Partial = extensionStates[extensionId] ??= {};
75 | return extensionState;
76 | },
77 | /** Remove plugin-bound custom state for a specific extension (if present). */
78 | clear(plugin: PluginContext, extensionId: string): void {
79 | const extensionStates = PluginCustomState(plugin).extensions ??= {};
80 | delete extensionStates[extensionId];
81 | },
82 | /** Return function which gets plugin-bound custom state for a specific extension. */
83 | getter(extensionId: string) {
84 | return (plugin: PluginContext) => this.get(plugin, extensionId);
85 | },
86 | };
87 |
88 |
89 | /** UI region where custom controls can be registered */
90 | export type PluginCustomControlRegion = 'structure-tools' | 'viewport-top-center' | 'viewport-top-left';
91 |
92 | /** Collection of registered custom controls in a UI region */
93 | export type PluginCustomControlRegistry = Map>;
94 |
95 | /** Functions for registering/unregistering custom UI controls */
96 | export const PluginCustomControls = {
97 | /** Get custom controls in the specified UI `region`. */
98 | get(plugin: PluginContext, region: PluginCustomControlRegion): PluginCustomControlRegistry {
99 | const customControls = PluginCustomState(plugin).customControls ??= {};
100 | return customControls[region] ??= initialPluginCustomControls(plugin, region);
101 | },
102 | /** Register a custom control in the specified UI `region`. */
103 | add(plugin: PluginContext, region: PluginCustomControlRegion, name: string, control: JSXElementConstructor<{}>) {
104 | const registry = PluginCustomControls.get(plugin, region);
105 | if (!registry.has(name)) {
106 | registry.set(name, control);
107 | }
108 | return {
109 | delete: () => PluginCustomControls.delete(plugin, region, name),
110 | };
111 | },
112 | /** Unregister a custom control in the specified UI `region`. */
113 | delete(plugin: PluginContext, region: PluginCustomControlRegion, name: string) {
114 | const registry = PluginCustomControls.get(plugin, region);
115 | registry.delete(name);
116 | },
117 | /** Register/unregister a custom control in the specified UI `region`. */
118 | toggle(plugin: PluginContext, region: PluginCustomControlRegion, name: string, control: JSXElementConstructor<{}>, show: boolean) {
119 | if (show) {
120 | PluginCustomControls.add(plugin, region, name, control);
121 | } else {
122 | PluginCustomControls.delete(plugin, region, name);
123 | }
124 | },
125 | };
126 |
127 | function initialPluginCustomControls(plugin: PluginContext, region: PluginCustomControlRegion): PluginCustomControlRegistry {
128 | if (region === 'structure-tools') return plugin.customStructureControls;
129 | return new Map>();
130 | }
131 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import eslint from '@eslint/js';
2 | import tsEslint from 'typescript-eslint';
3 | import stylisticEslint from '@stylistic/eslint-plugin';
4 |
5 |
6 | export default tsEslint.config(
7 | eslint.configs.recommended,
8 | ...tsEslint.configs.recommended,
9 | ...tsEslint.configs.stylistic,
10 | {
11 | plugins: {
12 | '@stylistic': stylisticEslint,
13 | },
14 | rules: {
15 | // RELAX RECOMMENDED RULES
16 | 'prefer-const': [
17 | 'error',
18 | {
19 | destructuring: 'all', // Allow `let` when at least one destructured element will be changed
20 | },
21 | ],
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | '@typescript-eslint/no-inferrable-types': 'off',
24 | '@typescript-eslint/consistent-indexed-object-style': 'off',
25 | '@typescript-eslint/no-namespace': 'off',
26 | '@typescript-eslint/no-unused-vars': 'off',
27 | '@typescript-eslint/no-empty-object-type': 'off',
28 |
29 | // ADDITIONAL RULES - GENERAL
30 | 'eqeqeq': 'error', // Forbid using `==`, enforce `===`
31 | 'no-eval': 'error', // Forbid using `eval`
32 | 'no-new-wrappers': 'error',
33 | 'no-restricted-syntax': [
34 | 'error',
35 | {
36 | selector: 'ExportDefaultDeclaration',
37 | message: 'Default exports are not allowed',
38 | },
39 | ],
40 | 'no-var': 'error', // Forbid using `var`
41 | 'no-void': 'error', // Forbid using `void 0`, use `undefined` instead
42 | 'no-throw-literal': 'error', // Forbid throwing anything that's not Error, e.g. `throw 'Blabla'`
43 | '@typescript-eslint/prefer-namespace-keyword': 'error', // Forbid `module` keyword
44 |
45 | // ADDITIONAL RULES - @stylistic
46 | '@stylistic/indent': ['error', 4],
47 | '@stylistic/semi': 'error', // Enforce trailing semicolons, including after type definitions
48 | '@stylistic/comma-dangle': ['error', { // Enforce comma after last listed item when closing ] or } is on the next line
49 | arrays: 'always-multiline',
50 | objects: 'always-multiline',
51 | imports: 'always-multiline',
52 | exports: 'always-multiline',
53 | enums: 'always-multiline',
54 | tuples: 'always-multiline',
55 | functions: 'only-multiline', // This would look ugly after JSX syntax in `map`
56 | }],
57 | '@stylistic/eol-last': 'error', // Enforce newline at the end of file
58 | '@stylistic/quotes': [
59 | 'error',
60 | 'single',
61 | {
62 | // Enforce single quotes: 'hello'
63 | avoidEscape: true,
64 | allowTemplateLiterals: true,
65 | },
66 | ],
67 | '@stylistic/brace-style': [
68 | 'error',
69 | '1tbs',
70 | {
71 | // Enforce line break after { and before }
72 | allowSingleLine: true,
73 | },
74 | ],
75 | '@stylistic/member-delimiter-style': [
76 | 'error',
77 | {
78 | // Enforce commas in interfaces and types
79 | multiline: {
80 | delimiter: 'comma',
81 | requireLast: true,
82 | },
83 | singleline: {
84 | delimiter: 'comma',
85 | requireLast: false,
86 | },
87 | multilineDetection: 'brackets',
88 | },
89 | ],
90 |
91 | // ADDITIONAL RULES - @stylistic - SPACING
92 | '@stylistic/array-bracket-spacing': 'error', // Forbid spaces in array: [_1, 2_]
93 | '@stylistic/arrow-spacing': 'error', // Enforce space in lambda function: x_=>_x**2
94 | '@stylistic/block-spacing': 'error', // Enforce space in one-line block: () => {_return true;_}
95 | '@stylistic/comma-spacing': 'error', // Enforce space after comma: [1,_2,_3]
96 | '@stylistic/computed-property-spacing': 'error', // Forbid spaces in indexing: a[_0_]
97 | '@stylistic/function-call-spacing': 'error', // Forbid space when calling function: f_(0)
98 | '@stylistic/key-spacing': 'error', // Enforce space after colon in object: { a:_1, b:_2 }
99 | '@stylistic/keyword-spacing': 'error', // Enforce space after `if`, `try`, etc.: if_(true)
100 | '@stylistic/no-multi-spaces': 'error', // Forbid more than one space: x =__5
101 | '@stylistic/no-trailing-spaces': 'error', // No spaces at the end of line: foo()_
102 | '@stylistic/object-curly-spacing': ['error', 'always'], // Enforce spaces in object: {_a: 1, b: 2_}
103 | '@stylistic/semi-spacing': 'error', // Enforce space after semicolon: for (let i = 0;_i < n;_i++)
104 | '@stylistic/space-before-blocks': 'error', // Enforce space before block: if (true)_{}
105 | '@stylistic/space-before-function-paren': [
106 | 'error',
107 | {
108 | anonymous: 'always', // function_() {}
109 | named: 'never', // function foo_() {}
110 | asyncArrow: 'always', // async_() {}
111 | },
112 | ],
113 | '@stylistic/space-in-parens': 'error', // Forbid spaces in parentheses: (_1, 2_)
114 | '@stylistic/space-infix-ops': 'error', // Enforce space around infix operators: 1_+_2
115 | '@stylistic/spaced-comment': [
116 | 'error',
117 | 'always',
118 | {
119 | // Enforce space in comment: /**_Comment_*/ //_comment
120 | block: {
121 | balanced: true,
122 | },
123 | },
124 | ],
125 | '@stylistic/type-annotation-spacing': 'error', // Enforce space after type annotation colon: let x:_string;
126 | },
127 | },
128 | );
129 |
--------------------------------------------------------------------------------
/src/app/spec-from-html.ts:
--------------------------------------------------------------------------------
1 | import { ColorParams, Encoding, InitParams, Lighting, Preset, VisualStyle, validateInitParams } from './spec';
2 |
3 |
4 | /** Extract InitParams from attributes of an HTML element */
5 | export function initParamsFromHtmlAttributes(element: HTMLElement): Partial {
6 | const params = loadHtmlAttributes(element, InitParamsLoadingActions, {});
7 | const validationIssues = validateInitParams(params);
8 | if (validationIssues) console.error('Invalid PDBeMolstarPlugin options:', params);
9 | return params;
10 | }
11 |
12 |
13 | /** Actions for loading individual HTML attributes into InitParams object */
14 | const InitParamsLoadingActions: AttributeLoadingActions> = {
15 | // DATA
16 | 'molecule-id': setString('moleculeId'),
17 | 'custom-data-url': (value, params) => { (params.customData ??= defaultCustomData()).url = value; },
18 | 'custom-data-format': (value, params) => { (params.customData ??= defaultCustomData()).format = value; },
19 | 'custom-data-binary': (value, params) => { (params.customData ??= defaultCustomData()).binary = parseBool(value); },
20 | 'assembly-id': setString('assemblyId'),
21 | 'default-preset': setLiteral(Preset, 'defaultPreset'),
22 | 'ligand-label-comp-id': (value, params) => { (params.ligandView ??= {}).label_comp_id = value; },
23 | 'ligand-auth-asym-id': (value, params) => { (params.ligandView ??= {}).auth_asym_id = value; },
24 | 'ligand-struct-asym-id': (value, params) => { (params.ligandView ??= {}).struct_asym_id = value; },
25 | 'ligand-auth-seq-id': (value, params) => { (params.ligandView ??= {}).auth_seq_id = Number(value); },
26 | 'ligand-show-all': (value, params) => { (params.ligandView ??= {}).show_all = parseBool(value); },
27 | 'alphafold-view': setBool('alphafoldView'),
28 |
29 | // APPEARANCE
30 | 'visual-style': setLiteral(VisualStyle, 'visualStyle'),
31 | 'hide-polymer': pushItem('hideStructure', 'polymer'),
32 | 'hide-water': pushItem('hideStructure', 'water'),
33 | 'hide-het': pushItem('hideStructure', 'het'),
34 | 'hide-carbs': pushItem('hideStructure', 'carbs'),
35 | 'hide-non-standard': pushItem('hideStructure', 'nonStandard'),
36 | 'hide-coarse': pushItem('hideStructure', 'coarse'),
37 | 'load-maps': setBool('loadMaps'),
38 | 'bg-color-r': setColorComponent('bgColor', 'r'),
39 | 'bg-color-g': setColorComponent('bgColor', 'g'),
40 | 'bg-color-b': setColorComponent('bgColor', 'b'),
41 | 'highlight-color-r': setColorComponent('highlightColor', 'r'),
42 | 'highlight-color-g': setColorComponent('highlightColor', 'g'),
43 | 'highlight-color-b': setColorComponent('highlightColor', 'b'),
44 | 'select-color-r': setColorComponent('selectColor', 'r'),
45 | 'select-color-g': setColorComponent('selectColor', 'g'),
46 | 'select-color-b': setColorComponent('selectColor', 'b'),
47 | 'lighting': setLiteral(Lighting, 'lighting'),
48 |
49 | // BEHAVIOR
50 | 'validation-annotation': setBool('validationAnnotation'),
51 | 'domain-annotation': setBool('domainAnnotation'),
52 | 'symmetry-annotation': setBool('symmetryAnnotation'),
53 | 'pdbe-url': setString('pdbeUrl'),
54 | 'encoding': setLiteral(Encoding, 'encoding'),
55 | 'low-precision': setBool('lowPrecisionCoords'),
56 | 'select-interaction': setBool('selectInteraction'),
57 | 'subscribe-events': setBool('subscribeEvents'),
58 |
59 | // INTERFACE
60 | 'hide-controls': setBool('hideControls'),
61 | 'hide-expand-icon': pushItem('hideCanvasControls', 'expand'),
62 | 'hide-selection-icon': pushItem('hideCanvasControls', 'selection'),
63 | 'hide-animation-icon': pushItem('hideCanvasControls', 'animation'),
64 | 'hide-control-toggle-icon': pushItem('hideCanvasControls', 'controlToggle'),
65 | 'hide-control-info-icon': pushItem('hideCanvasControls', 'controlInfo'),
66 | 'sequence-panel': setBool('sequencePanel'),
67 | 'pdbe-link': setBool('pdbeLink'),
68 | 'loading-overlay': setBool('loadingOverlay'),
69 | 'expanded': setBool('expanded'),
70 | 'landscape': setBool('landscape'),
71 | 'reactive': setBool('reactive'),
72 | };
73 |
74 |
75 | /** Actions for loading individual HTML attributes into a context */
76 | interface AttributeLoadingActions {
77 | [attribute: string]: (value: string, context: TContext) => any,
78 | };
79 |
80 | /** Load attributes of an HTML element into a context */
81 | function loadHtmlAttributes(element: HTMLElement, actions: AttributeLoadingActions, context: TContext): TContext {
82 | for (const attribute in actions) {
83 | const value = element.getAttribute(attribute);
84 | if (typeof value === 'string') {
85 | actions[attribute](value, context);
86 | }
87 | }
88 | return context;
89 | }
90 |
91 |
92 | /** Select keys of an object type `T` which except type `V` as value */
93 | type KeyWith = Exclude<{ [key in keyof T]: V extends T[key] ? key : never }[keyof T], undefined>;
94 |
95 | function setString(key: KeyWith) {
96 | return (value: string, obj: T) => { obj[key] = value as any; };
97 | }
98 | function setLiteral(allowedValues: readonly E[], key: KeyWith) {
99 | return (value: string, ctx: T) => {
100 | if (!allowedValues.includes(value as E)) console.error(`Value "${value}" is not valid for type ${allowedValues.map(s => `"${s}"`).join(' | ')}`);
101 | ctx[key] = value as any;
102 | };
103 | }
104 | function setBool(key: KeyWith) {
105 | return (value: string, obj: T) => { obj[key] = parseBool(value) as any; };
106 | }
107 | function setColorComponent(key: KeyWith, component: 'r' | 'g' | 'b') {
108 | return (value: string, obj: T) => {
109 | const color = obj[key] ??= { r: 0, g: 0, b: 0 } as any;
110 | color[component] = Number(value);
111 | };
112 | }
113 | function pushItem(key: KeyWith, item: any) {
114 | return (value: string, obj: T) => {
115 | if (parseBool(value)) {
116 | const array = obj[key] ??= [] as any;
117 | array.push(item);
118 | }
119 | };
120 | }
121 |
122 | /** Parse a string into a boolean.
123 | * Consider strings like 'false', 'OFF', '0' as false; others as true.
124 | * Empty string is parsed as true, because HTML attribute without value must be treated as truthy. */
125 | function parseBool(value: string): boolean {
126 | const FalseyStrings = ['false', 'off', '0'];
127 | return !FalseyStrings.includes(value.toLowerCase());
128 | }
129 | function defaultCustomData(): Exclude {
130 | return { url: '', format: '', binary: false };
131 | }
132 |
--------------------------------------------------------------------------------
/src/app/extensions/interactions/index.ts:
--------------------------------------------------------------------------------
1 | /** Helper functions to allow showing custom atom interactions */
2 |
3 | import { MVSBuildPrimitiveShape, MVSInlinePrimitiveData } from 'molstar/lib/extensions/mvs/components/primitives';
4 | import { MVSData } from 'molstar/lib/extensions/mvs/mvs-data';
5 | import { MolstarSubtree } from 'molstar/lib/extensions/mvs/tree/molstar/molstar-tree';
6 | import { ColorT } from 'molstar/lib/extensions/mvs/tree/mvs/param-types';
7 | import { ShapeRepresentation3D } from 'molstar/lib/mol-plugin-state/transforms/representation';
8 | import { setSubtreeVisibility } from 'molstar/lib/mol-plugin/behavior/static/state';
9 | import { PDBeMolstarPlugin } from '../..';
10 | import { QueryParam, queryParamsToMvsComponentExpressions } from '../../helpers';
11 | import { ExtensionCustomState } from '../../plugin-custom-state';
12 |
13 |
14 | /** Name used when registering extension, custom state, etc. */
15 | const InteractionsExtensionName = 'pdbe-custom-interactions';
16 | const getExtensionCustomState = ExtensionCustomState.getter<{ visuals: StateObjectHandle[] }>(InteractionsExtensionName);
17 |
18 |
19 | export interface Interaction {
20 | start: QueryParam,
21 | end: QueryParam,
22 | color?: string,
23 | radius?: number,
24 | dash_length?: number,
25 | tooltip?: string,
26 | }
27 |
28 | const DEFAULT_COLOR = 'white';
29 | const DEFAULT_RADIUS = 0.075;
30 | const DEFAULT_DASH_LENGTH = 0.1;
31 | const DEFAULT_OPACITY = 1;
32 |
33 | export interface StateObjectHandle {
34 | /** State transform reference */
35 | ref: string,
36 | /** Set state object visibility on/off */
37 | setVisibility(visible: boolean): void,
38 | /** Remove state object from state hierarchy */
39 | delete: () => Promise,
40 | }
41 |
42 | export function loadInteractions_example(viewer: PDBeMolstarPlugin, params?: { opacity?: number, color?: string, radius?: number, dash_length?: number }): Promise {
43 | return loadInteractions(viewer, { ...params, interactions: exampleData });
44 | }
45 |
46 | /** Show custom atom interactions */
47 | export async function loadInteractions(viewer: PDBeMolstarPlugin, params: { interactions: Interaction[], structureId?: string, opacity?: number, color?: string, radius?: number, dash_length?: number }): Promise {
48 | const structureId = params.structureId ?? PDBeMolstarPlugin.MAIN_STRUCTURE_ID;
49 | const struct = viewer.getStructure(structureId);
50 | if (!struct) throw new Error(`Did not find structure with ID "${structureId}"`);
51 |
52 | const primitivesMvsNode = interactionsToMvsPrimitiveData(params);
53 |
54 | const update = viewer.plugin.build();
55 | const data = update.to(struct.cell).apply(MVSInlinePrimitiveData, { node: primitivesMvsNode as any }, { tags: ['custom-interactions-data'] });
56 | data.apply(MVSBuildPrimitiveShape, { kind: 'mesh' }).apply(ShapeRepresentation3D, {}, { tags: ['custom-interactions-mesh'] });
57 | data.apply(MVSBuildPrimitiveShape, { kind: 'lines' }).apply(ShapeRepresentation3D, {}, { tags: ['custom-interactions-lines'] });
58 | data.apply(MVSBuildPrimitiveShape, { kind: 'labels' }).apply(ShapeRepresentation3D, {}, { tags: ['custom-interactions-labels'] });
59 | await update.commit();
60 |
61 | const visual: StateObjectHandle = {
62 | ref: data.ref,
63 | setVisibility: (visible: boolean) => setSubtreeVisibility(viewer.plugin.state.data, data.ref, !visible /* true means hidden */),
64 | delete: () => viewer.plugin.build().delete(data.ref).commit(),
65 | };
66 | const visualsList = getExtensionCustomState(viewer.plugin).visuals ??= [];
67 | visualsList.push(visual);
68 | return visual;
69 | }
70 |
71 | /** Remove any previously added interactions */
72 | export async function clearInteractions(viewer: PDBeMolstarPlugin): Promise {
73 | const visuals = getExtensionCustomState(viewer.plugin).visuals;
74 | if (!visuals) return;
75 | for (const visual of visuals) {
76 | await visual.delete();
77 | }
78 | visuals.length = 0;
79 | }
80 |
81 | function interactionsToMvsPrimitiveData(params: { interactions: Interaction[], opacity?: number, color?: string, radius?: number, dash_length?: number }): MolstarSubtree<'primitives'> {
82 | const builder = MVSData.createBuilder();
83 | const primitives = builder.primitives({
84 | opacity: params.opacity ?? DEFAULT_OPACITY,
85 | color: params.color as ColorT ?? DEFAULT_COLOR,
86 | });
87 |
88 | for (const interaction of params.interactions) {
89 | primitives.tube({
90 | start: { expressions: queryParamsToMvsComponentExpressions([interaction.start]) },
91 | end: { expressions: queryParamsToMvsComponentExpressions([interaction.end]) },
92 | radius: interaction.radius ?? params.radius ?? DEFAULT_RADIUS,
93 | dash_length: interaction.dash_length ?? params.dash_length ?? DEFAULT_DASH_LENGTH,
94 | color: interaction.color as ColorT | undefined,
95 | tooltip: interaction.tooltip,
96 | });
97 | }
98 | const state = builder.getState();
99 | const primitivesNode = state.root.children?.find(child => child.kind === 'primitives') as MolstarSubtree<'primitives'> | undefined;
100 | if (!primitivesNode) throw new Error('AssertionError: Failed to create MVS "primitives" subtree.');
101 | return primitivesNode;
102 | }
103 |
104 | /** Selected interactions from https://www.ebi.ac.uk/pdbe/graph-api/pdb/bound_ligand_interactions/1hda/C/143 */
105 | const exampleData = [
106 | {
107 | 'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['CBC'] },
108 | 'end': { 'auth_asym_id': 'C', 'auth_seq_id': 32, 'atoms': ['CE'] },
109 | 'color': 'yellow',
110 | 'tooltip': 'Hydrophobic interaction
HEM 143 | CBC — MET 32 | CE',
111 | },
112 | {
113 | 'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['CBC'] },
114 | 'end': { 'auth_asym_id': 'C', 'auth_seq_id': 32, 'atoms': ['SD'] },
115 | 'color': 'yellow',
116 | 'tooltip': 'Hydrophobic interaction
HEM 143 | CBC — MET 32 | SD',
117 | },
118 | {
119 | 'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['CMD'] },
120 | 'end': { 'auth_asym_id': 'C', 'auth_seq_id': 42, 'atoms': ['O'] },
121 | 'color': 'gray',
122 | 'tooltip': 'Mixed interaction
Vdw, Weak polar
HEM 143 | CMD — TYR 42 | O',
123 | },
124 | {
125 | 'start': { 'auth_asym_id': 'C', 'auth_seq_id': 143, 'atoms': ['C1B', 'C2B', 'C3B', 'C4B', 'NB'] },
126 | 'end': { 'auth_asym_id': 'C', 'auth_seq_id': 136, 'atoms': ['CD1'] },
127 | 'color': 'magenta',
128 | 'tooltip': 'CARBONPI interaction
HEM 143 | C1B, C2B, C3B, C4B, NB — LEU 136 | CD1',
129 | },
130 | ];
131 |
--------------------------------------------------------------------------------
/static-demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | PDBe Molstar
8 |
9 |
10 |
11 |
12 |
13 |
135 |
136 |
137 |
138 | PDBe Molstar
139 |
140 |
141 |
142 |
Static (image-like) plugin demo
143 |
144 |
145 | Open 3D
146 |
147 |
148 |
149 |
150 |
X
151 |
152 | View:
153 | Front
154 | Right
155 | Top
156 |
157 |
158 |
159 |
160 |
161 |
Options
162 |
165 |
166 |
167 |
168 |
169 |
170 |
213 |
214 |
215 |
--------------------------------------------------------------------------------
/src/app/styles/pdbe-molstar/_index.scss:
--------------------------------------------------------------------------------
1 | /** Styles for PDBe Molstar (in addition to styles imported from core Molstar) */
2 |
3 | // Color constants must be imported before importing this file
4 | // (from molstar/lib/mol-plugin-ui/skin/*.scss or molstar/lib/mol-plugin-ui/skin/colors/*.scss)
5 |
6 | @use 'molstar/lib/mol-plugin-ui/skin/base/_vars.scss' as vars;
7 |
8 | $viewport-button-spacing: 4px;
9 |
10 | .msp-plugin {
11 | ::-webkit-scrollbar-thumb {
12 | background-color: #80808080;
13 | border-radius: 10px;
14 | border: solid 1px transparent;
15 | background-clip: content-box;
16 | }
17 |
18 | .msp-plugin-init-error {
19 | color: #808080;
20 | }
21 |
22 | .msp-layout-static {
23 | container-type: size; // define container to reference in .msp-viewport-controls-panel-controls
24 | }
25 |
26 | .msp-viewport-controls-panel .msp-viewport-controls-panel-controls {
27 | max-height: calc(100cqh - 46px - 24px - 10px); // 100cqh = viewport container, 46px = control buttons offset, 24px = header, 10px = space below
28 | }
29 |
30 | // Avoid wraping headers in left panel help
31 | .msp-simple-help-section {
32 | overflow: hidden;
33 | text-wrap: nowrap;
34 | text-overflow: ellipsis;
35 | }
36 |
37 | .msp-control-group-header button {
38 | overflow: hidden;
39 | text-wrap: nowrap;
40 | text-overflow: ellipsis;
41 | }
42 |
43 | // Avoid tooltip box flickering when hovering something under it
44 | .msp-highlight-toast-wrapper {
45 | pointer-events: none;
46 |
47 | .msp-toast-container {
48 | pointer-events: initial;
49 | }
50 | }
51 |
52 |
53 | .pdbemolstar-custom-control-viewport-top-left {
54 | float: left;
55 |
56 | &:not(:empty) {
57 | margin-right: vars.$control-spacing;
58 | }
59 | }
60 |
61 | .pdbemolstar-viewport-top-center-controls {
62 | width: 100%;
63 | display: flex;
64 | flex-direction: column;
65 | align-items: center;
66 | padding-inline: calc(vars.$control-spacing + 68px + $viewport-button-spacing); // 68px = roughly max width of PDBe logo box
67 | pointer-events: none;
68 |
69 | >* {
70 | margin-top: vars.$control-spacing;
71 | pointer-events: auto;
72 | max-width: 100%;
73 | }
74 | }
75 |
76 |
77 | .pdbemolstar-overlay {
78 | z-index: 1000;
79 | position: absolute;
80 | inset: 0px;
81 | display: flex;
82 | justify-content: center;
83 | align-items: center;
84 | pointer-events: none;
85 |
86 | .pdbemolstar-overlay-box {
87 | width: 25%;
88 | height: 25%;
89 | }
90 |
91 | svg.pdbe-animated-logo {
92 | background-color: transparent;
93 | width: 100%;
94 | height: 100%;
95 | opacity: 80%;
96 |
97 | .path-fg {
98 | stroke-dasharray: 1812 250;
99 | stroke-dashoffset: -250;
100 | animation: dash linear normal infinite;
101 | animation-duration: 5s;
102 | animation-delay: 1s;
103 | }
104 |
105 | @keyframes dash {
106 | 0% {
107 | stroke-dashoffset: 1812;
108 | }
109 |
110 | 80% {
111 | stroke-dashoffset: -250;
112 | }
113 |
114 | 100% {
115 | stroke-dashoffset: -250;
116 | }
117 | }
118 | }
119 | }
120 |
121 |
122 | .pdbemolstar-state-gallery-controls {
123 | overflow-x: hidden;
124 | word-wrap: break-word; // necessary for wrapping oligonucleotide sequences
125 |
126 | &:focus-visible {
127 | outline: none;
128 | }
129 |
130 | .pdbemolstar-state-gallery-state-button {
131 | height: 24px;
132 | line-height: 24px;
133 | padding-inline: 5px;
134 | text-align: left;
135 | overflow: hidden;
136 | text-overflow: ellipsis;
137 |
138 | .msp-icon {
139 | margin-right: 3px;
140 | }
141 | }
142 |
143 | .pdbemolstar-state-gallery-legend {
144 | margin-block: 8px;
145 |
146 | .image_legend_li {
147 | margin-left: 16px;
148 | }
149 |
150 | .highlight {
151 | font-weight: bold;
152 | }
153 | }
154 | }
155 |
156 | .pdbemolstar-state-gallery-title-box {
157 | width: 500px;
158 | max-width: 100%;
159 | background-color: vars.$msp-form-control-background;
160 | display: flex;
161 | flex-direction: row;
162 | justify-content: space-between;
163 | align-items: stretch;
164 |
165 | .msp-btn-icon {
166 | background-color: transparent;
167 | height: 100%;
168 | }
169 |
170 | .pdbemolstar-state-gallery-title {
171 | margin: 5px;
172 | min-height: 2.9em; // Enough for one normal line + one line
173 | display: flex;
174 | flex-direction: row;
175 | align-items: center;
176 | text-align: center;
177 | font-weight: bold;
178 |
179 | .pdbemolstar-state-gallery-title-icon {
180 | width: 1.2em; // width of msp-material-icon
181 | }
182 |
183 | .pdbemolstar-state-gallery-title-text {
184 | margin-right: 1.2em; // width of msp-material-icon
185 | padding-inline: 4px;
186 | }
187 | }
188 | }
189 |
190 | // PDBe link button (using nested selector to increase specificity and beat core Molstar styles)
191 | .msp-viewport-controls-buttons {
192 | .pdbemolstar-pdbe-link-box {
193 | position: absolute;
194 | right: 0px;
195 | width: fit-content;
196 | }
197 |
198 | .pdbemolstar-pdbe-link {
199 | width: fit-content;
200 | min-width: 32px;
201 | padding-inline: 6px;
202 | background: transparent;
203 | display: flex;
204 | flex-direction: row;
205 | align-items: center;
206 | }
207 |
208 | .pdbemolstar-pdbe-logo {
209 | height: 16px;
210 | width: 16px;
211 | position: relative;
212 |
213 | svg {
214 | position: absolute;
215 | inset: 0;
216 | }
217 | }
218 | }
219 |
220 | .pdbemolstar-viewport-controls-normal {
221 | position: absolute;
222 | right: 0px;
223 | top: 0px;
224 | }
225 |
226 | .pdbemolstar-viewport-controls-shifted {
227 | position: absolute;
228 | right: 0px;
229 | top: vars.$row-height + $viewport-button-spacing;
230 | }
231 | }
--------------------------------------------------------------------------------
/src/app/loci-details.ts:
--------------------------------------------------------------------------------
1 | import { OrderedSet } from 'molstar/lib/mol-data/int';
2 | import { SIFTSMapping as BestDatabaseSequenceMappingProp } from 'molstar/lib/mol-model-props/sequence/sifts-mapping';
3 | import { Loci } from 'molstar/lib/mol-model/loci';
4 | import { Bond, StructureProperties as Props, StructureElement, Unit } from 'molstar/lib/mol-model/structure';
5 |
6 |
7 | export interface EventDetail {
8 | models?: string[],
9 | entity_id?: string,
10 | label_asym_id?: string,
11 | asym_id?: string,
12 | auth_asym_id?: string,
13 | unp_accession?: string,
14 | unp_seq_id?: number,
15 | seq_id?: number,
16 | auth_seq_id?: number,
17 | ins_code?: string,
18 | comp_id?: string,
19 | atom_id?: string[],
20 | alt_id?: string,
21 | micro_het_comp_ids?: string[],
22 | seq_id_begin?: number,
23 | seq_id_end?: number,
24 | button?: number,
25 | modifiers?: any,
26 | }
27 |
28 | type LabelGranularity = 'element' | 'conformation' | 'residue' | 'chain' | 'structure';
29 |
30 | export function lociDetails(loci: Loci): EventDetail | undefined {
31 | switch (loci.kind) {
32 | case 'structure-loci':
33 | return { models: loci.structure.models.map(m => m.entry).filter(l => !!l) };
34 | case 'element-loci':
35 | return structureElementStatsDetail(StructureElement.Stats.ofLoci(loci));
36 | case 'bond-loci': {
37 | const bond = loci.bonds[0];
38 | return bond ? bondLabel(bond, 'element') : {};
39 | }
40 | default:
41 | return undefined;
42 | }
43 | }
44 |
45 | function structureElementStatsDetail(stats: StructureElement.Stats): EventDetail | undefined {
46 | const { chainCount, residueCount, elementCount } = stats;
47 |
48 | if (elementCount === 1 && residueCount === 0 && chainCount === 0) {
49 | return getElementDetails(stats.firstElementLoc, 'element');
50 | } else if (elementCount === 0 && residueCount === 1 && chainCount === 0) {
51 | return getElementDetails(stats.firstResidueLoc, 'residue');
52 | } else {
53 | return undefined;
54 | }
55 | }
56 |
57 | function getElementDetails(location: StructureElement.Location, granularity: LabelGranularity = 'element'): EventDetail {
58 | const basicDetails: any = {};
59 |
60 | let entry = location.unit.model.entry;
61 | if (entry.length > 30) entry = entry.substr(0, 27) + '\u2026'; // ellipsis
62 | basicDetails['entry_id'] = entry; // entry
63 | if (granularity !== 'structure') {
64 | basicDetails['model'] = location.unit.model.modelNum; // model
65 | basicDetails['instance'] = location.unit.conformation.operator.name; // instance
66 | }
67 |
68 | let elementDetails: any;
69 | if (Unit.isAtomic(location.unit)) {
70 | elementDetails = atomicElementDetails(location as StructureElement.Location, granularity);
71 | } else if (Unit.isCoarse(location.unit)) {
72 | elementDetails = coarseElementDetails(location as StructureElement.Location, granularity);
73 | }
74 |
75 | return { ...basicDetails, ...elementDetails };
76 | }
77 |
78 | function atomicElementDetails(location: StructureElement.Location, granularity: LabelGranularity): EventDetail {
79 | const elementDetails: EventDetail = {
80 | entity_id: Props.chain.label_entity_id(location),
81 | label_asym_id: Props.chain.label_asym_id(location),
82 | auth_asym_id: Props.chain.auth_asym_id(location),
83 | unp_accession: undefined,
84 | unp_seq_id: undefined,
85 | seq_id: Props.residue.label_seq_id(location),
86 | auth_seq_id: Props.residue.auth_seq_id(location),
87 | ins_code: Props.residue.pdbx_PDB_ins_code(location),
88 | comp_id: Props.atom.label_comp_id(location),
89 | atom_id: [Props.atom.label_atom_id(location)],
90 | alt_id: Props.atom.label_alt_id(location),
91 | };
92 |
93 | const unpLabel = BestDatabaseSequenceMappingProp.getLabel(location);
94 |
95 | if (unpLabel) {
96 | const unpLabelDetails = unpLabel.split(' ');
97 | if (unpLabelDetails[0] === 'UNP') {
98 | elementDetails.unp_accession = unpLabelDetails[1];
99 | elementDetails.unp_seq_id = +unpLabelDetails[2];
100 | }
101 | }
102 |
103 | const microHetCompIds = Props.residue.microheterogeneityCompIds(location);
104 | elementDetails['micro_het_comp_ids'] = granularity === 'residue' && microHetCompIds.length > 1 ?
105 | microHetCompIds : [elementDetails['comp_id']] as any;
106 |
107 | return elementDetails;
108 | }
109 |
110 | function coarseElementDetails(location: StructureElement.Location, granularity: LabelGranularity): EventDetail {
111 | const elementDetails: EventDetail = {
112 | asym_id: Props.coarse.asym_id(location),
113 | seq_id_begin: Props.coarse.seq_id_begin(location),
114 | seq_id_end: Props.coarse.seq_id_end(location),
115 | };
116 |
117 | if (granularity === 'residue') {
118 | if (elementDetails.seq_id_begin === elementDetails.seq_id_end) {
119 | const entityIndex = Props.coarse.entityKey(location);
120 | const seq = location.unit.model.sequence.byEntityKey[entityIndex];
121 | elementDetails['comp_id'] = seq.sequence.compId.value(elementDetails.seq_id_begin! - 1); // 1-indexed
122 | }
123 | }
124 |
125 | return elementDetails;
126 | }
127 |
128 | export function bondLabel(bond: Bond.Location, granularity: LabelGranularity): any {
129 | return _bundleLabel({
130 | loci: [
131 | StructureElement.Loci(bond.aStructure, [{ unit: bond.aUnit, indices: OrderedSet.ofSingleton(bond.aIndex) }]),
132 | StructureElement.Loci(bond.bStructure, [{ unit: bond.bUnit, indices: OrderedSet.ofSingleton(bond.bIndex) }]),
133 | ],
134 | }, granularity);
135 | }
136 |
137 | export function _bundleLabel(bundle: Loci.Bundle, granularity: LabelGranularity) {
138 | let isSingleElements = true;
139 | for (const l of bundle.loci) {
140 | if (!StructureElement.Loci.is(l) || StructureElement.Loci.size(l) !== 1) {
141 | isSingleElements = false;
142 | break;
143 | }
144 | }
145 | if (isSingleElements) {
146 | const locations = (bundle.loci as StructureElement.Loci[]).map(l => {
147 | const { unit, indices } = l.elements[0];
148 | return StructureElement.Location.create(l.structure, unit, unit.elements[OrderedSet.start(indices)]);
149 | });
150 | const elementDetailsArr: EventDetail[] = locations.map(l => getElementDetails(l, granularity));
151 | const atomIds: any = [elementDetailsArr[0].atom_id![0], elementDetailsArr[1].atom_id![0]];
152 | const elementDetails: EventDetail = elementDetailsArr[0];
153 | elementDetails['atom_id'] = atomIds;
154 | return elementDetails;
155 | } else {
156 | const elementDetails = bundle.loci.map(l => lociDetails(l));
157 | return elementDetails;
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/app/superposition-focus-representation.ts:
--------------------------------------------------------------------------------
1 | import { StructureElement } from 'molstar/lib/mol-model/structure';
2 | import { createStructureRepresentationParams } from 'molstar/lib/mol-plugin-state/helpers/structure-representation-params';
3 | import { PluginStateObject } from 'molstar/lib/mol-plugin-state/objects';
4 | import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
5 | import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior';
6 | import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
7 | import { PluginContext } from 'molstar/lib/mol-plugin/context';
8 | import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
9 | import { StateObjectCell, StateSelection, StateTransform } from 'molstar/lib/mol-state';
10 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
11 | import { lociDetails } from './loci-details';
12 |
13 |
14 | const SuperpositionFocusRepresentationParams = (plugin: PluginContext) => {
15 | const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(undefined, plugin) as PD.Params;
16 | return {
17 | expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }),
18 | surroundingsParams: PD.Group(reprParams, {
19 | label: 'Surroundings',
20 | customDefault: createStructureRepresentationParams(plugin, undefined, { type: 'ball-and-stick', size: 'physical', typeParams: { sizeFactor: 0.16 }, sizeParams: { scale: 0.3 } }),
21 | }),
22 | };
23 | };
24 |
25 | type SuperpositionFocusRepresentationProps = PD.ValuesFor>;
26 |
27 | export enum SuperpositionFocusRepresentationTags {
28 | SurrSel = 'superposition-focus-surr-sel',
29 | SurrRepr = 'superposition-focus-surr-repr',
30 | }
31 |
32 | const TagSet = new Set([SuperpositionFocusRepresentationTags.SurrSel, SuperpositionFocusRepresentationTags.SurrRepr]);
33 |
34 | class SuperpositionFocusRepresentationBehavior extends PluginBehavior.WithSubscribers {
35 | private get surrLabel() { return `[Focus] Surroundings (${this.params.expandRadius} Å)`; }
36 |
37 | private ensureShape(cell: StateObjectCell) {
38 | const state = this.plugin.state.data, tree = state.tree;
39 | const builder = state.build();
40 | const refs = StateSelection.findUniqueTagsInSubtree(tree, cell.transform.ref, TagSet);
41 |
42 | // Selections
43 | if (!refs[SuperpositionFocusRepresentationTags.SurrSel]) {
44 | refs[SuperpositionFocusRepresentationTags.SurrSel] = builder
45 | .to(cell)
46 | .apply(StateTransforms.Model.StructureSelectionFromExpression,
47 | { expression: MS.struct.generator.empty(), label: this.surrLabel }, { tags: SuperpositionFocusRepresentationTags.SurrSel }).ref;
48 | }
49 |
50 | // Representations
51 | if (!refs[SuperpositionFocusRepresentationTags.SurrRepr]) {
52 | refs[SuperpositionFocusRepresentationTags.SurrRepr] = builder
53 | .to(refs[SuperpositionFocusRepresentationTags.SurrSel]!)
54 | .apply(StateTransforms.Representation.StructureRepresentation3D, this.params.surroundingsParams, { tags: SuperpositionFocusRepresentationTags.SurrRepr }).ref;
55 | }
56 |
57 | return { state, builder, refs };
58 | }
59 |
60 | private clear(root: StateTransform.Ref) {
61 | const state = this.plugin.state.data;
62 |
63 | const surrs = state.select(StateSelection.Generators.byRef(root).subtree().withTag(SuperpositionFocusRepresentationTags.SurrSel));
64 | if (surrs.length === 0) return;
65 |
66 | const update = state.build();
67 | const expression = MS.struct.generator.empty();
68 | for (const s of surrs) {
69 | update.to(s).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression }));
70 | }
71 |
72 | return PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
73 | }
74 |
75 | private async focus(sourceLoci: StructureElement.Loci) {
76 | const parent = this.plugin.helpers.substructureParent.get(sourceLoci.structure);
77 | if (!parent || !parent.obj) return;
78 |
79 | const loci = StructureElement.Loci.remap(sourceLoci, parent.obj!.data);
80 |
81 | const residueLoci = StructureElement.Loci.extendToWholeResidues(loci);
82 | const residueBundle = StructureElement.Bundle.fromLoci(residueLoci);
83 | const target = StructureElement.Bundle.toExpression(residueBundle);
84 |
85 | let surroundings = MS.struct.modifier.includeSurroundings({
86 | 0: target,
87 | radius: this.params.expandRadius,
88 | 'as-whole-residues': true,
89 | });
90 |
91 | const lociDeatils = lociDetails(sourceLoci);
92 | if (!lociDeatils) {
93 | surroundings = MS.struct.modifier.exceptBy({
94 | 0: surroundings,
95 | by: target,
96 | });
97 | }
98 |
99 | const { state, builder, refs } = this.ensureShape(parent);
100 |
101 | builder.to(refs[SuperpositionFocusRepresentationTags.SurrSel]!).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression: surroundings, label: this.surrLabel }));
102 |
103 | await PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
104 | }
105 |
106 | register(ref: string): void {
107 | this.subscribeObservable(this.plugin.managers.structure.focus.behaviors.current, (entry) => {
108 | // if (entry) this.focus(entry.loci);
109 | // else this.clear(StateTransform.RootRef);
110 | this.clear(StateTransform.RootRef);
111 | if (entry) this.focus(entry.loci);
112 | });
113 | }
114 |
115 | async update(params: SuperpositionFocusRepresentationProps) {
116 | const old = this.params;
117 | this.params = params;
118 |
119 | const state = this.plugin.state.data;
120 | const builder = state.build();
121 |
122 | const all = StateSelection.Generators.root.subtree();
123 |
124 | for (const repr of state.select(all.withTag(SuperpositionFocusRepresentationTags.SurrRepr))) {
125 | builder.to(repr).update(this.params.surroundingsParams);
126 | }
127 |
128 | await PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
129 |
130 | if (params.expandRadius !== old.expandRadius) await this.clear(StateTransform.RootRef);
131 |
132 | return true;
133 | }
134 | }
135 |
136 | export const SuperpositionFocusRepresentation = PluginBehavior.create({
137 | name: 'create-superposition-focus-representation',
138 | display: { name: 'Superposition Focus Representation' },
139 | category: 'interaction',
140 | ctor: SuperpositionFocusRepresentationBehavior,
141 | params: (_, plugin) => SuperpositionFocusRepresentationParams(plugin),
142 | });
143 |
--------------------------------------------------------------------------------
/src/app/extensions/complexes/superpose-by-biggest-chain.ts:
--------------------------------------------------------------------------------
1 | import { MinimizeRmsd } from 'molstar/lib/mol-math/linear-algebra/3d/minimize-rmsd';
2 | import { MmcifFormat } from 'molstar/lib/mol-model-formats/structure/mmcif';
3 | import { Structure } from 'molstar/lib/mol-model/structure';
4 | import { SuperpositionResult } from './index';
5 |
6 |
7 | /** Superpose structures based on the largest common component (measured the by number of residues) with a UniProt mapping (taken from atom_site mmCIF category).
8 | * Residue-residue correspondence is determined by UniProt residue numbers.
9 | * This differs from UniProt-based superposition in core Molstar (`alignAndSuperposeWithSIFTSMapping`) which takes all components (regardless of their spacial arrangement). */
10 | export function superposeByBiggestCommonChain(structA: Structure, structB: Structure, allowedComponentsA: string[] | undefined, allowedComponentsB: string[] | undefined): SuperpositionResult | undefined {
11 | const indexA = extractUniprotIndex(structA, allowedComponentsA);
12 | const indexB = extractUniprotIndex(structB, allowedComponentsB);
13 | const bestMatch = bestUniprotMatch(indexA, indexB);
14 | if (!bestMatch) {
15 | return undefined;
16 | }
17 | const unitA = structA.unitMap.get(Number(bestMatch.unitA));
18 | const unitB = structB.unitMap.get(Number(bestMatch.unitB));
19 | const unitIndexA = indexA[bestMatch.accession][bestMatch.unitA];
20 | const unitIndexB = indexB[bestMatch.accession][bestMatch.unitB];
21 | const positionsA = MinimizeRmsd.Positions.empty(bestMatch.nMatchedElements);
22 | const positionsB = MinimizeRmsd.Positions.empty(bestMatch.nMatchedElements);
23 |
24 | let i = 0;
25 | for (const unpNum in unitIndexA.atomMap) {
26 | const iAtomB = unitIndexB.atomMap[unpNum];
27 | if (iAtomB === undefined) continue;
28 | const iAtomA = unitIndexA.atomMap[unpNum];
29 | positionsA.x[i] = unitA.conformation.coordinates.x[iAtomA];
30 | positionsA.y[i] = unitA.conformation.coordinates.y[iAtomA];
31 | positionsA.z[i] = unitA.conformation.coordinates.z[iAtomA];
32 | positionsB.x[i] = unitB.conformation.coordinates.x[iAtomB];
33 | positionsB.y[i] = unitB.conformation.coordinates.y[iAtomB];
34 | positionsB.z[i] = unitB.conformation.coordinates.z[iAtomB];
35 | i++;
36 | }
37 | const superposition = MinimizeRmsd.compute({ a: positionsA, b: positionsB });
38 |
39 | if (!isNaN(superposition.rmsd)) {
40 | return { ...superposition, method: 'uniprot-numbering', accession: bestMatch.accession };
41 | } else {
42 | return undefined;
43 | }
44 | }
45 |
46 |
47 | /** Uniprot index for specific accession in a structure */
48 | interface SingleAccessionUniprotIndex {
49 | [unitId: string]: {
50 | label_asym_id: string,
51 | auth_asym_id: string,
52 | /** Maps Uniprot number to ElementIndex of the trace atom */
53 | atomMap: { [unpSeqId: string]: number },
54 | },
55 | }
56 |
57 | /** Index for all accessions in a structure */
58 | interface UniprotIndex {
59 | [accession: string]: SingleAccessionUniprotIndex,
60 | }
61 |
62 | function extractUniprotIndex(structure: Structure, allowedAccessions: string[] | undefined): UniprotIndex {
63 | const allowedAccessionsSet = allowedAccessions ? new Set(allowedAccessions) : undefined;
64 | const seenUnitInvariantIds = new Set();
65 | const out: UniprotIndex = {};
66 | for (const unit of structure.units) {
67 | if (seenUnitInvariantIds.has(unit.invariantId)) continue;
68 | else seenUnitInvariantIds.add(unit.invariantId);
69 |
70 | const src = structure.model.sourceData;
71 | if (!MmcifFormat.is(src)) throw new Error('Source data must be mmCIF/BCIF');
72 |
73 | const h = unit.model.atomicHierarchy;
74 | const { pdbx_sifts_xref_db_acc, pdbx_sifts_xref_db_name, pdbx_sifts_xref_db_num } = src.data.db.atom_site;
75 | const atoms = unit.polymerElements;
76 | const nAtoms = atoms.length;
77 | for (let i = 0; i < nAtoms; i++) {
78 | const iAtom = atoms[i];
79 | const srcIAtom = h.atomSourceIndex.value(iAtom);
80 | const dbName = pdbx_sifts_xref_db_name.value(srcIAtom);
81 | if (dbName !== 'UNP') continue;
82 | const dbAcc = pdbx_sifts_xref_db_acc.value(srcIAtom);
83 | if (allowedAccessionsSet && !allowedAccessionsSet.has(dbAcc)) continue;
84 | const dbNum = pdbx_sifts_xref_db_num.value(srcIAtom);
85 | const structMapping = out[dbAcc] ??= {};
86 | const chainMapping = structMapping[unit.id] ??= {
87 | label_asym_id: h.chains.label_asym_id.value(h.chainAtomSegments.index[atoms[0]]),
88 | auth_asym_id: h.chains.auth_asym_id.value(h.chainAtomSegments.index[atoms[0]]),
89 | atomMap: {},
90 | };
91 | chainMapping.atomMap[dbNum] ??= iAtom;
92 | }
93 | }
94 | return out;
95 | }
96 |
97 | export interface SortedAccessionsAndUnits {
98 | accessions: string[],
99 | units: { [accession: string]: ({ unitId: string, size: number } & T)[] },
100 | }
101 |
102 | /** Sort units for each accession by decreasing size and sort accessions by decreasing biggest unit size. */
103 | function sortAccessionsAndUnits(uniprotIndex: UniprotIndex): SortedAccessionsAndUnits {
104 | const unitsByAccession: { [accession: string]: { unitId: string, size: number }[] } = {};
105 |
106 | for (const accession in uniprotIndex) {
107 | const unitIds = uniprotIndex[accession];
108 | const units: { unitId: string, size: number }[] = [];
109 | for (const unitId in unitIds) {
110 | const size = Object.keys(unitIds[unitId].atomMap).length;
111 | units.push({ unitId, size });
112 | }
113 | units.sort((a, b) => b.size - a.size);
114 | unitsByAccession[accession] = units;
115 | }
116 |
117 | return {
118 | /** Accessions sorted by decreasing biggest unit size */
119 | accessions: Object.keys(unitsByAccession).sort((a, b) => unitsByAccession[b][0].size - unitsByAccession[a][0].size),
120 | /** Units per accession, sorted by decreasing unit size */
121 | units: unitsByAccession,
122 | };
123 | }
124 |
125 | function bestUniprotMatch(a: UniprotIndex, b: UniprotIndex) {
126 | const sortedA = sortAccessionsAndUnits(a);
127 | const sortedB = sortAccessionsAndUnits(b);
128 | let bestMatch: { accession: string, unitA: string, unitB: string, nMatchedElements: number } | undefined = undefined;
129 | let bestScore = 0;
130 | for (const accession of sortedA.accessions) {
131 | const unitsA = sortedA.units[accession]!;
132 | const unitsB = sortedB.units[accession];
133 | if (!unitsB) continue;
134 | for (const ua of unitsA) {
135 | if (ua.size <= bestScore) break;
136 | const unitA = a[accession][ua.unitId];
137 | for (const ub of unitsB) {
138 | if (ub.size <= bestScore || ua.size <= bestScore) break;
139 | const unitB = b[accession][ub.unitId];
140 | const score = objectKeyOverlap(unitA.atomMap, unitB.atomMap);
141 | if (score > bestScore) {
142 | bestScore = score;
143 | bestMatch = { accession, unitA: ua.unitId, unitB: ub.unitId, nMatchedElements: score };
144 | }
145 | }
146 | }
147 | }
148 | return bestMatch;
149 | }
150 |
151 | /** Return number of keys common to objects `a` and `b` */
152 | function objectKeyOverlap(a: object, b: object) {
153 | let overlap = 0;
154 | for (const key in a) {
155 | if (key in b) {
156 | overlap++;
157 | }
158 | }
159 | return overlap;
160 | }
161 |
--------------------------------------------------------------------------------
/src/app/domain-annotations/prop.ts:
--------------------------------------------------------------------------------
1 | import { CustomModelProperty } from 'molstar/lib/mol-model-props/common/custom-model-property';
2 | import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property';
3 | import { PropertyWrapper } from 'molstar/lib/mol-model-props/common/wrapper';
4 | import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property';
5 | import { IndexedCustomProperty, Model, ResidueIndex, Unit } from 'molstar/lib/mol-model/structure';
6 | import { ChainIndex } from 'molstar/lib/mol-model/structure/model/indexing';
7 | import { Structure, StructureElement } from 'molstar/lib/mol-model/structure/structure';
8 | import { arraySetAdd } from 'molstar/lib/mol-util/array';
9 | import { Asset } from 'molstar/lib/mol-util/assets';
10 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
11 |
12 |
13 | export { DomainAnnotations };
14 | type DomainAnnotations = PropertyWrapper<{
15 | domains: IndexedCustomProperty.Residue,
16 | domainNames: string[][],
17 | domainTypes: string[],
18 | } | undefined>;
19 |
20 | namespace DomainAnnotations {
21 |
22 | export const DefaultServerUrl = 'https://www.ebi.ac.uk/pdbe/api/mappings';
23 | export function getEntryUrl(pdbId: string, serverUrl: string) {
24 | return `${serverUrl}/${pdbId.toLowerCase()}`;
25 | }
26 |
27 | export function isApplicable(model?: Model): boolean {
28 | return !!model && Model.hasPdbId(model);
29 | }
30 |
31 | export function fromJson(model: Model, data: any): DomainAnnotations {
32 | const info = PropertyWrapper.createInfo();
33 | const domainMap = createdomainMapFromJson(model, data);
34 | return { info, data: domainMap };
35 | }
36 |
37 | export async function fromServer(ctx: CustomProperty.Context, model: Model, props: DomainAnnotationsProps): Promise> {
38 | const url = Asset.getUrlAsset(ctx.assetManager, getEntryUrl(model.entryId, props.serverUrl));
39 | const json = await ctx.assetManager.resolve(url, 'json').runInContext(ctx.runtime);
40 | const data = json.data[model.entryId.toLowerCase()];
41 | if (!data) throw new Error('missing data');
42 | return { value: fromJson(model, data), assets: [json] };
43 | }
44 |
45 | const _emptyArray: string[] = [];
46 | /** Return a list of domainSource:domainName strings for a particular location (e.g. ['CATH:Globin']) */
47 | export function getDomains(e: StructureElement.Location): string[] {
48 | if (!Unit.isAtomic(e.unit)) return _emptyArray;
49 | const prop = DomainAnnotationsProvider.get(e.unit.model).value;
50 | if (!prop || !prop.data) return _emptyArray;
51 | const rI = e.unit.residueIndex[e.element];
52 | return prop.data.domains.has(rI) ? prop.data.domains.get(rI)! : _emptyArray;
53 | }
54 | /** Decide whether a structure location belongs to a domain */
55 | export function isInDomain(e: StructureElement.Location, domainSource: string, domainName: string): boolean {
56 | return getDomains(e).includes(domainKey(domainSource, domainName));
57 | }
58 |
59 | export function getDomainTypes(structure?: Structure): string[] {
60 | const model = structure?.models[0];
61 | if (!model) return _emptyArray;
62 | const prop = DomainAnnotationsProvider.get(model).value;
63 | if (!prop || !prop.data) return _emptyArray;
64 | return prop.data.domainTypes;
65 | }
66 |
67 | export function getDomainNames(structure?: Structure): string[][] {
68 | const model = structure?.models[0];
69 | if (!model) return [];
70 | const prop = DomainAnnotationsProvider.get(model).value;
71 | if (!prop || !prop.data) return [];
72 | return prop.data.domainNames;
73 | }
74 | }
75 |
76 | export const DomainAnnotationsParams = {
77 | serverUrl: PD.Text(DomainAnnotations.DefaultServerUrl, { description: 'JSON API Server URL' }),
78 | };
79 | export type DomainAnnotationsParams = typeof DomainAnnotationsParams;
80 | export type DomainAnnotationsProps = PD.Values;
81 |
82 | export const DomainAnnotationsProvider: CustomModelProperty.Provider = CustomModelProperty.createProvider({
83 | label: 'Domain annotations',
84 | descriptor: CustomPropertyDescriptor({
85 | name: 'domain_annotations',
86 | }),
87 | type: 'static',
88 | defaultParams: DomainAnnotationsParams,
89 | getParams: (data: Model) => DomainAnnotationsParams,
90 | isApplicable: (data: Model) => DomainAnnotations.isApplicable(data),
91 | obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial) => {
92 | const p = { ...PD.getDefaultValues(DomainAnnotationsParams), ...props };
93 | try {
94 | return await DomainAnnotations.fromServer(ctx, data, p);
95 | } catch {
96 | console.error('Could not obtain domain annotations');
97 | return { value: DomainAnnotations.fromJson(data, {}) };
98 | }
99 | },
100 | });
101 |
102 | function findChainLabel(map: any, label_entity_id: string, label_asym_id: string): ChainIndex {
103 | const entityIndex = map.entities.getEntityIndex;
104 | const eI = entityIndex(label_entity_id);
105 | if (eI < 0 || !map.entity_index_label_asym_id.has(eI)) return -1 as ChainIndex;
106 | const cm = map.entity_index_label_asym_id.get(eI);
107 | if (!cm) return -1 as ChainIndex;
108 | return cm.has(label_asym_id) ? cm.get(label_asym_id)! : -1 as ChainIndex;
109 | }
110 |
111 | function findResidue(modelData: Model, map: any, label_entity_id: string, label_asym_id: string, label_seq_id: number) {
112 | const cI = findChainLabel(map, label_entity_id, label_asym_id);
113 | if (cI < 0) return -1 as ResidueIndex;
114 | const rm = map.chain_index_auth_seq_id.get(cI)!;
115 | return rm.has(label_seq_id) ? rm.get(label_seq_id)! : -1 as ResidueIndex;
116 | }
117 |
118 | function createdomainMapFromJson(modelData: Model, data: any): DomainAnnotations['data'] | undefined {
119 | const domainTypes: string[] = [];
120 | const domainNames: string[][] = [];
121 | const ret = new Map();
122 | const defaultDomains = ['Pfam', 'InterPro', 'CATH', 'SCOP'];
123 |
124 | for (const db_name in data) {
125 | if (!defaultDomains.includes(db_name)) continue;
126 | const tempDomains: string[] = [];
127 | const db = data[db_name];
128 | for (const db_code in db) {
129 | const domain = db[db_code];
130 | for (const mapping of domain.mappings) {
131 | arraySetAdd(tempDomains, domain.identifier);
132 |
133 | const indexData = modelData.atomicHierarchy.index as any;
134 | const indexMap = indexData.map;
135 | for (let seq_id = mapping.start.residue_number; seq_id <= mapping.end.residue_number; seq_id++) {
136 | const idx = findResidue(modelData, indexMap, `${mapping.entity_id}`, mapping.chain_id, seq_id);
137 | const key = domainKey(db_name, domain.identifier);
138 | const prevVal = ret.get(idx);
139 | if (prevVal) {
140 | prevVal.push(key);
141 | } else {
142 | ret.set(idx, [key]);
143 | }
144 | }
145 |
146 | }
147 | }
148 | domainTypes.push(db_name);
149 | domainNames.push(tempDomains);
150 | }
151 |
152 | return {
153 | domains: IndexedCustomProperty.fromResidueMap(ret),
154 | domainNames,
155 | domainTypes,
156 | };
157 | }
158 |
159 | /** Return string used for indexing domains (e.g. `domainKey('CATH', 'Globin') -> 'CATH:Globin'`) */
160 | function domainKey(domainSource: string, domainName: string) {
161 | return `${domainSource}:${domainName}`;
162 | }
163 |
--------------------------------------------------------------------------------
/src/app/ui/left-panel/tabs.tsx:
--------------------------------------------------------------------------------
1 | import { Canvas3DParams } from 'molstar/lib/mol-canvas3d/canvas3d';
2 | import { PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
3 | import { IconButton, SectionHeader } from 'molstar/lib/mol-plugin-ui/controls/common';
4 | import { AccountTreeOutlinedSvg, DeleteOutlinedSvg, HelpOutlineSvg, HomeOutlinedSvg, SaveOutlinedSvg, TuneSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
5 | import { ParameterControls } from 'molstar/lib/mol-plugin-ui/controls/parameters';
6 | import { StateObjectActions } from 'molstar/lib/mol-plugin-ui/state/actions';
7 | import { RemoteStateSnapshots, StateSnapshots } from 'molstar/lib/mol-plugin-ui/state/snapshots';
8 | import { StateTree } from 'molstar/lib/mol-plugin-ui/state/tree';
9 | import { HelpContent, HelpGroup, HelpText } from 'molstar/lib/mol-plugin-ui/viewport/help';
10 | import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
11 | import { StateTransform } from 'molstar/lib/mol-state';
12 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
13 | import React from 'react';
14 | import { PluginCustomState } from '../../plugin-custom-state';
15 | import { WavesIconSvg } from '../icons';
16 | import { SegmentTree } from '../segment-tree';
17 | import { TabSpec } from './core';
18 |
19 |
20 | /** Body of 'root' (aka Home) tab in the left panel */
21 | class HomeTabBody extends PluginUIComponent<{}, { tab: string }> {
22 | render() {
23 | return <>
24 |
25 | {this.plugin.spec.components?.remoteState !== 'none' && }
26 | >;
27 | }
28 | }
29 |
30 | /** Header of 'data' (aka State Tree) tab in the left panel */
31 | class DataTabHeader extends PluginUIComponent<{}, { tab: string }> {
32 | render() {
33 | return State Tree>} />;
34 | }
35 | }
36 |
37 | /** Body of 'data' (aka State Tree) tab in the left panel */
38 | class DataTabBody extends PluginUIComponent<{}, { tab: string }> {
39 | render() {
40 | return ;
41 | }
42 | }
43 |
44 | /** Button for clearing state tree ("delete" icon) */
45 | class RemoveAllButton extends PluginUIComponent<{}> {
46 | componentDidMount() {
47 | this.subscribe(this.plugin.state.events.cell.created, e => {
48 | if (e.cell.transform.parent === StateTransform.RootRef) this.forceUpdate();
49 | });
50 |
51 | this.subscribe(this.plugin.state.events.cell.removed, e => {
52 | if (e.parent === StateTransform.RootRef) this.forceUpdate();
53 | });
54 | }
55 |
56 | remove = (e: React.MouseEvent) => {
57 | e.preventDefault();
58 | PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: StateTransform.RootRef });
59 | };
60 |
61 | render() {
62 | const count = this.plugin.state.data.tree.children.get(StateTransform.RootRef).size;
63 | if (count === 0) return null;
64 | return ;
65 | }
66 | }
67 |
68 | function HelpSection(props: { header: string }) { // copypaste of private HelpSection from molstar/lib/mol-plugin-ui/viewport/help.tsx
69 | return {props.header}
;
70 | }
71 |
72 | /** Help section about superposition functionality for the left panel */
73 | class SuperpositionHelpSection extends PluginUIComponent {
74 | componentDidMount() {
75 | this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
76 | }
77 | render() {
78 | return
79 |
80 |
81 |
82 | Discrete UniProt sequence range mapped to the structure
83 |
84 |
85 |
86 |
87 | Structural chains that possess significantly close superposition Q-score
88 |
89 |
90 |
91 |
92 | The best-ranked chain within a cluster chosen based on the model quality, resolution, observed residues ratio and UniProt sequence coverage
93 |
94 |
95 |
;
96 | }
97 | }
98 |
99 | /** Body of 'help' tab in the left panel (core help + PDBe-specific help) */
100 | class PDBeHelpContent extends PluginUIComponent<{}, { tab: string }> {
101 | render() {
102 | return <>
103 |
104 |
105 | >;
106 | }
107 | }
108 |
109 | /** Body of 'setting' tab in the left panel (excluding Behaviors) */
110 | class FullSettings extends PluginUIComponent { // modification of private FullSettings from molstar/lib/mol-plugin-ui/left-panel.tsx
111 | private setSettings = (p: { param: PD.Base, name: string, value: any }) => {
112 | PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
113 | };
114 |
115 | componentDidMount() {
116 | this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
117 | this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
118 |
119 | this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => {
120 | if (state.radiusMax !== undefined || state.radius !== undefined) {
121 | this.forceUpdate();
122 | }
123 | });
124 | }
125 |
126 | render() {
127 | return <>
128 | {this.plugin.canvas3d && <>
129 |
130 |
131 | >}
132 | {/*
133 | */}
134 | >;
135 | }
136 | }
137 |
138 |
139 | /** Left panel tabs in core Molstar */
140 | export const CoreLeftPanelTabs = {
141 | root: {
142 | id: 'root',
143 | title: 'Home',
144 | icon: HomeOutlinedSvg,
145 | body: HomeTabBody,
146 | },
147 | data: {
148 | id: 'data',
149 | title: 'State Tree',
150 | icon: AccountTreeOutlinedSvg,
151 | header: DataTabHeader,
152 | body: DataTabBody,
153 | dirtyOn: plugin => plugin.state.data.events.changed,
154 | },
155 | states: {
156 | id: 'states',
157 | title: 'Plugin State',
158 | icon: SaveOutlinedSvg,
159 | header: () => <>>,
160 | body: StateSnapshots,
161 | },
162 | help: {
163 | id: 'help',
164 | title: 'Help',
165 | icon: HelpOutlineSvg,
166 | body: PDBeHelpContent,
167 | },
168 | settings: {
169 | id: 'settings',
170 | title: 'Plugin Settings',
171 | icon: TuneSvg,
172 | body: FullSettings,
173 | },
174 | } as const satisfies Record;
175 |
176 |
177 | /** Additional PDBe-specific left panel tabs */
178 | export const PDBeLeftPanelTabs = {
179 | segments: {
180 | id: 'segments',
181 | title: 'Superpose Segments',
182 | icon: WavesIconSvg,
183 | header: () => <>>,
184 | body: SegmentTree,
185 | showWhen: plugin => !!PluginCustomState(plugin).initParams?.superposition,
186 | },
187 | } as const satisfies Record;
188 |
--------------------------------------------------------------------------------
/src/app/ui/left-panel/core.tsx:
--------------------------------------------------------------------------------
1 | import { PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base';
2 | import { IconButton, SectionHeader } from 'molstar/lib/mol-plugin-ui/controls/common';
3 | import { PluginCommands } from 'molstar/lib/mol-plugin/commands';
4 | import { PluginContext } from 'molstar/lib/mol-plugin/context';
5 | import { sleep } from 'molstar/lib/mol-util/sleep';
6 | import { BehaviorSubject, Subject } from 'rxjs';
7 |
8 |
9 | /** Specification of one tab in a tabbed panel */
10 | export interface TabSpec {
11 | /** Unique identifier of the tab */
12 | id: string,
13 | /** Tab title (shown in header and as tooltip on the icon) */
14 | title: string,
15 | /** Tab icon (shown in icon bar and in tab header), either a React component or a string with icon path (in 24x24 viewBox) */
16 | icon: React.JSXElementConstructor<{}> | string,
17 | /** Custom tab header (default is icon + title) */
18 | header?: React.JSXElementConstructor<{}>,
19 | /** Tab body (main content in the tab) */
20 | body: React.JSXElementConstructor<{}>,
21 | /** Limit tab visibility to when `showWhen` returns true (default: always visible) */
22 | showWhen?: (plugin: PluginContext) => boolean,
23 | /** The tab icon will show a marker whenever `dirtyOn` Subject fires a truthy value and the tab is not currently open.
24 | * The marker will disappear when the tab is opened or when `dirtyOn` fires a falsey value. */
25 | dirtyOn?: (plugin: PluginContext) => Subject | undefined,
26 | }
27 |
28 | /** Convert `Icon` to a React functional element. `Icon` can be either React element (class or functional) or a string with icon path (in 24x24 viewBox) */
29 | function resolveIcon(Icon: React.JSXElementConstructor<{}> | string): React.FC {
30 | if (typeof Icon === 'string') {
31 | return function _Icon() { return ; };
32 | } else {
33 | return function _Icon() { return ; };
34 | }
35 | }
36 |
37 | /** Special tab id value, meaning no tab is open */
38 | const NO_TAB = 'none';
39 |
40 | /** Return a React component showing a panel with tab icons on the left and tab content of the open tab on the right */
41 | export function VerticalTabbedPanel(options: { tabsTop?: TabSpec[], tabsBottom?: TabSpec[], defaultTab?: string, boundBehavior?: (plugin: PluginContext) => BehaviorSubject, onTabChange?: (plugin: PluginContext, tab: string) => any }): React.ComponentClass<{}> {
42 | const tabs = [...options.tabsTop ?? [], ...options.tabsBottom ?? []];
43 | if (tabs.some(tab => tab.id === NO_TAB)) throw new Error(`Cannot use '${NO_TAB}' as tab id because it is reserved.`);
44 |
45 | return class _GenericLeftPanelControls extends PluginUIComponent<{}, { tab: string, dirtyTabs: string[] }> {
46 | readonly boundBehavior = options.boundBehavior?.(this.plugin);
47 | readonly state = {
48 | tab: options.defaultTab ?? this.boundBehavior?.value ?? NO_TAB,
49 | dirtyTabs: [] as string[],
50 | };
51 |
52 | setTab = async (tab: string) => {
53 | if (tab === this.state.tab) return; // important to avoid infinite loop when state is bound to `boundBehavior`
54 | this.setDirtyTab(tab, false);
55 | this.setState({ tab }, () => {
56 | this.boundBehavior?.next(tab);
57 | options.onTabChange?.(this.plugin, tab);
58 | });
59 | };
60 | toggleTab = (tab: string) => {
61 | if (tab === this.state.tab) {
62 | this.setTab(NO_TAB);
63 | } else {
64 | this.setTab(tab);
65 | }
66 | };
67 | setDirtyTab = (tab: string, dirty: boolean) => {
68 | if (dirty && !this.state.dirtyTabs.includes(tab)) {
69 | this.setState({ dirtyTabs: [...this.state.dirtyTabs, tab] }); // Add to dirty tabs
70 | } else if (!dirty && this.state.dirtyTabs.includes(tab)) {
71 | this.setState({ dirtyTabs: this.state.dirtyTabs.filter(t => t !== tab) }); // Remove from dirty tabs
72 | }
73 | };
74 | componentDidMount(): void {
75 | if (this.boundBehavior) {
76 | if (this.boundBehavior.value !== this.state.tab) {
77 | this.boundBehavior.next(this.state.tab);
78 | }
79 | this.subscribe(this.boundBehavior, tab => this.setTab(tab));
80 | }
81 | for (const tab of tabs) {
82 | const dirtySubject = tab.dirtyOn?.(this.plugin);
83 | if (dirtySubject) {
84 | this.subscribe(dirtySubject, isDirty => {
85 | this.setDirtyTab(tab.id, !!isDirty && this.state.tab !== tab.id);
86 | });
87 | }
88 | }
89 | options.onTabChange?.(this.plugin, this.state.tab);
90 | }
91 |
92 | render() {
93 | const currentTabId = this.state.tab;
94 | const currentTab = currentTabId ? tabs.find(t => t.id === currentTabId) : undefined;
95 | const CurrentTabHeader = currentTab?.header;
96 | const CurrentTabBody = currentTab?.body;
97 |
98 | const iconForTab = (tab: TabSpec) => {
99 | if (tab.showWhen && !tab.showWhen(this.plugin)) return null;
100 | return this.toggleTab(tab.id)} transparent style={{ position: 'relative' }}
102 | extraContent={this.state.dirtyTabs.includes(tab.id) ? : undefined} />;
103 | };
104 |
105 | return
106 | {/* Icon bar */}
107 |
108 | {options.tabsTop?.map(iconForTab)}
109 |
110 | {options.tabsBottom?.map(iconForTab)}
111 |
112 |
113 | {/* Tab content */}
114 | {currentTab &&
115 |
116 | {CurrentTabHeader && || }
117 | {CurrentTabBody && }
118 |
119 | }
120 |
;
121 | }
122 | };
123 | }
124 |
125 | /** Set left panel state in Molstar layout to 'full' (if expanded) or 'collapsed' (if not expanded).
126 | * Do not change the state if it is 'hidden'. */
127 | async function adjustLeftPanelState(plugin: PluginContext, expanded: boolean) {
128 | await sleep(0); // this ensures PluginCommands.Layout.Update runs after componentDidMount, without this the panel will not collapse when defaultTab is none (not sure why)
129 | if (expanded && plugin.layout.state.regionState.left === 'collapsed') {
130 | await PluginCommands.Layout.Update(plugin, { state: { regionState: { ...plugin.layout.state.regionState, left: 'full' } } });
131 | }
132 | if (!expanded && plugin.layout.state.regionState.left === 'full') {
133 | await PluginCommands.Layout.Update(plugin, { state: { regionState: { ...plugin.layout.state.regionState, left: 'collapsed' } } });
134 | }
135 | }
136 |
137 | /** Like `VerticalTabbedPanel` but is bound to plugin.behaviors.layout.leftPanelTabName and plugin.layout.state.regionState (ensures left panel collapsing/expanding) */
138 | export function LeftPanel(options: { tabsTop?: TabSpec[], tabsBottom?: TabSpec[], defaultTab?: string, onTabChange?: (plugin: PluginContext, tab: string) => any }): React.ComponentClass<{}> {
139 | return VerticalTabbedPanel({
140 | ...options,
141 | boundBehavior: plugin => plugin.behaviors.layout.leftPanelTabName as BehaviorSubject,
142 | onTabChange: (plugin, tab) => {
143 | const tabPresent = !!(options.tabsTop?.some(t => t.id === tab) || options.tabsBottom?.some(t => t.id === tab));
144 | adjustLeftPanelState(plugin, tabPresent);
145 | options.onTabChange?.(plugin, tab);
146 | },
147 | });
148 | }
149 |
--------------------------------------------------------------------------------
/src/app/extensions/complexes/coloring.ts:
--------------------------------------------------------------------------------
1 | import { Color } from 'molstar/lib/mol-util/color';
2 | import { PDBeMolstarPlugin } from '../..';
3 | import { normalizeColor, QueryParam } from '../../helpers';
4 |
5 |
6 | const DEFAULT_CORE_COLOR = '#d8d8d8';
7 | const DEFAULT_UNMAPPED_COLOR = '#f000f0';
8 | const DEFAULT_COMPONENT_COLORS = [
9 | '#1b9e77', '#d95f02', '#7570b3', '#66a61e', '#e6ab02', '#a6761d', // Dark-2 without gray and magenta
10 | '#1f77b4', '#2ca02c', '#d62728', '#927ba7', '#8c564b', '#e377c2', '#bcbd22', '#17becf', // More non-conflicting colors from other palettes
11 | '#fc8d62', '#9eb9f3', '#ff9da7', '#ffff33', '#8be0a4', '#e15759', '#c69fbb', '#76b7b2', // More non-conflicting colors from other palettes
12 | ];
13 |
14 | /** How much lighter/darker colors should be for the base/other complex */
15 | const COLOR_ADJUSTMENT_STRENGTH = 0.8;
16 |
17 |
18 | interface QueryParamWithColor extends QueryParam {
19 | color: string,
20 | }
21 | interface QueryParamWithTooltip extends QueryParam {
22 | tooltip: string,
23 | }
24 |
25 | export async function colorComponents(viewer: PDBeMolstarPlugin, params: { structId: string, components: string[], mappings?: { [accession: string]: QueryParam[] }, coreColor?: string, componentColors?: string[] }) {
26 | const { coreColor = DEFAULT_CORE_COLOR, componentColors = DEFAULT_COMPONENT_COLORS, components, mappings = {} } = params;
27 |
28 | const colorData: QueryParamWithColor[] = [];
29 | const tooltipData: QueryParamWithTooltip[] = [{ tooltip: 'Base complex' }];
30 | for (let i = 0; i < components.length; i++) {
31 | const accession = components[i];
32 | const color = componentColors[i % componentColors.length];
33 | colorData.push(...selectorItems(accession, mappings[accession], { color: adjustForBase(color) }));
34 | tooltipData.push(...selectorItems(accession, mappings[accession], { tooltip: `component ${accession}` }));
35 | }
36 | await viewer.visual.select({ data: colorData, nonSelectedColor: adjustForBase(coreColor), structureId: params.structId });
37 | await viewer.visual.tooltips({ data: tooltipData, structureId: params.structId });
38 | }
39 |
40 | /**
41 | Coloring - subcomplexes:
42 | - base common -> by entity, lighter
43 | - base additional -> gray, lighter
44 | - sub common -> by entity, darker
45 | - sub additional -> gray, darker (these are all unmapped components, includes antibodies and ligands)
46 | */
47 | export async function colorSubcomplex(viewer: PDBeMolstarPlugin, params: { baseStructId?: string, otherStructId?: string, baseComponents: string[], otherComponents: string[], baseMappings?: { [accession: string]: QueryParam[] }, otherMappings?: { [accession: string]: QueryParam[] }, coreColor?: string, componentColors?: string[] }) {
48 | const { coreColor = DEFAULT_CORE_COLOR, componentColors = DEFAULT_COMPONENT_COLORS, baseComponents, baseMappings = {}, otherMappings = {} } = params;
49 | const subComponentsSet = new Set(params.otherComponents);
50 |
51 | const baseColorData: QueryParamWithColor[] = [];
52 | const baseTooltipData: QueryParamWithTooltip[] = [{ tooltip: 'Base complex' }];
53 | const subColorData: QueryParamWithColor[] = [];
54 | const subTooltipData: QueryParamWithTooltip[] = [{ tooltip: 'Subcomplex' }];
55 | for (let i = 0; i < baseComponents.length; i++) {
56 | const accession = baseComponents[i];
57 | if (subComponentsSet.has(accession)) {
58 | const color = componentColors[i % componentColors.length];
59 | baseColorData.push(...selectorItems(accession, baseMappings[accession], { color: adjustForBase(color) }));
60 | baseTooltipData.push(...selectorItems(accession, baseMappings[accession], { tooltip: `common component ${accession}` }));
61 | subColorData.push(...selectorItems(accession, otherMappings[accession], { color: adjustForOther(color) }));
62 | subTooltipData.push(...selectorItems(accession, otherMappings[accession], { tooltip: `common component ${accession}` }));
63 | } else {
64 | baseTooltipData.push(...selectorItems(accession, baseMappings[accession], { tooltip: `additional component ${accession}` }));
65 | }
66 | }
67 | if (params.baseStructId) {
68 | await viewer.visual.select({ data: baseColorData, nonSelectedColor: adjustForBase(coreColor), structureId: params.baseStructId });
69 | await viewer.visual.tooltips({ data: baseTooltipData, structureId: params.baseStructId });
70 | }
71 | if (params.otherStructId) {
72 | await viewer.visual.select({ data: subColorData, nonSelectedColor: adjustForOther(coreColor), structureId: params.otherStructId });
73 | await viewer.visual.tooltips({ data: subTooltipData, structureId: params.otherStructId });
74 | }
75 | }
76 |
77 | /**
78 | Coloring - supercomplexes:
79 | - base common -> gray, lighter
80 | - base additional -> unmapped color, lighter (these are all unmapped components, includes antibodies and ligands)
81 | - super common -> gray, darker
82 | - super additional mapped -> by entity, darker
83 | - super additional unmapped -> unmapped color, darker
84 | */
85 | export async function colorSupercomplex(viewer: PDBeMolstarPlugin, params: { baseStructId?: string, otherStructId?: string, baseComponents: string[], otherComponents: string[], baseMappings?: { [accession: string]: QueryParam[] }, otherMappings?: { [accession: string]: QueryParam[] }, coreColor?: string, unmappedColor?: string, componentColors?: string[] }) {
86 | const { coreColor = DEFAULT_CORE_COLOR, unmappedColor = DEFAULT_UNMAPPED_COLOR, componentColors = DEFAULT_COMPONENT_COLORS, baseMappings = {}, otherMappings = {} } = params;
87 | const baseComponentsSet = new Set(params.baseComponents);
88 | const superComponents = params.baseComponents.concat(params.otherComponents.filter(acc => !baseComponentsSet.has(acc))); // reorder supercomplex accessions so that colors are consistent with the base
89 |
90 | const baseColorData: QueryParamWithColor[] = [];
91 | const baseTooltipData: QueryParamWithTooltip[] = [{ tooltip: 'Base complex' }];
92 | const superColorData: QueryParamWithColor[] = [];
93 | const superTooltipData: QueryParamWithTooltip[] = [{ tooltip: 'Supercomplex' }];
94 | for (let i = 0; i < superComponents.length; i++) {
95 | const accession = superComponents[i];
96 | if (baseComponentsSet.has(accession)) {
97 | baseColorData.push(...selectorItems(accession, baseMappings[accession], { color: adjustForBase(coreColor) }));
98 | baseTooltipData.push(...selectorItems(accession, baseMappings[accession], { tooltip: `common component ${accession}` }));
99 | superColorData.push(...selectorItems(accession, otherMappings[accession], { color: adjustForOther(coreColor) }));
100 | superTooltipData.push(...selectorItems(accession, otherMappings[accession], { tooltip: `common component ${accession}` }));
101 | } else {
102 | const color = componentColors[i % componentColors.length];
103 | superColorData.push(...selectorItems(accession, otherMappings[accession], { color: adjustForOther(color) }));
104 | superTooltipData.push(...selectorItems(accession, otherMappings[accession], { tooltip: `additional component ${accession}` }));
105 | }
106 | }
107 | if (params.baseStructId) {
108 | await viewer.visual.select({ data: baseColorData, nonSelectedColor: adjustForBase(unmappedColor), structureId: params.baseStructId });
109 | await viewer.visual.tooltips({ data: baseTooltipData, structureId: params.baseStructId });
110 | }
111 | if (params.otherStructId) {
112 | await viewer.visual.select({ data: superColorData, nonSelectedColor: adjustForOther(unmappedColor), structureId: params.otherStructId });
113 | await viewer.visual.tooltips({ data: superTooltipData, structureId: params.otherStructId });
114 | }
115 | }
116 |
117 | /** Adjust color for use on the base structure (slightly lighten) */
118 | function adjustForBase(color: string) {
119 | return Color.toHexStyle(Color.lighten(normalizeColor(color), COLOR_ADJUSTMENT_STRENGTH));
120 | }
121 |
122 | /** Adjust color for use on the subcomplex/supercomplex structure (slightly darken) */
123 | function adjustForOther(color: string) {
124 | return Color.toHexStyle(Color.darken(normalizeColor(color), COLOR_ADJUSTMENT_STRENGTH));
125 | }
126 |
127 | /** Create items for `.visual.select` or `.visual.tooltip`, preferrably based on `mappings`, or based on `uniprot_accession` if `mappings` not provided. */
128 | function selectorItems(uniprot_accession: string, mappings: QueryParam[] | undefined, extras: T): (QueryParam & T)[] {
129 | if (mappings) {
130 | return mappings.map(m => ({ ...m, ...extras }));
131 | } else {
132 | return [{ uniprot_accession, ...extras }];
133 | }
134 | };
135 |
--------------------------------------------------------------------------------
/src/app/extensions/complexes/superpose-by-sequence-alignment.ts:
--------------------------------------------------------------------------------
1 | import { OrderedSet, SortedArray } from 'molstar/lib/mol-data/int';
2 | import { MinimizeRmsd } from 'molstar/lib/mol-math/linear-algebra/3d/minimize-rmsd';
3 | import { align, AlignmentOptions } from 'molstar/lib/mol-model/sequence/alignment/alignment';
4 | import { ElementIndex, Structure, StructureElement, StructureProperties } from 'molstar/lib/mol-model/structure';
5 | import { UnitIndex } from 'molstar/lib/mol-model/structure/structure/element/util';
6 | import { getPositionTable } from 'molstar/lib/mol-model/structure/structure/util/superposition';
7 | import { QueryHelper, QueryParam } from '../../helpers';
8 | import { SuperpositionResult } from './index';
9 | import { SortedAccessionsAndUnits } from './superpose-by-biggest-chain';
10 |
11 | /** Superpose structures based on the largest common component (measured the by number of residues), components being defined by `mappingsA` and `mappingsB`.
12 | * Residue-residue correspondence is determined by sequence alignment. */
13 | export function superposeBySequenceAlignment(structA: Structure, structB: Structure, mappingsA: { [accession: string]: QueryParam[] }, mappingsB: { [accession: string]: QueryParam[] }): SuperpositionResult | undefined {
14 | const sortedA = sortAccessionsAndUnits(structA, mappingsA);
15 | const sortedB = sortAccessionsAndUnits(structB, mappingsB);
16 | const bestMatch = bestMappingMatch(sortedA, sortedB);
17 | if (!bestMatch) {
18 | return undefined;
19 | }
20 | const accession = bestMatch.accession;
21 | const lociA = QueryHelper.getInteractivityLoci(mappingsA[accession], structA);
22 | const lociB = QueryHelper.getInteractivityLoci(mappingsB[accession], structB);
23 | const superposition = alignAndSuperpose(lociA, lociB);
24 | if (!isNaN(superposition.rmsd)) {
25 | return { ...superposition, method: 'sequence-alignment', accession: bestMatch.accession };
26 | } else {
27 | return undefined;
28 | }
29 | }
30 |
31 | /** Sort units for each accession by decreasing size and sort accessions by decreasing biggest unit size. */
32 | function sortAccessionsAndUnits(struct: Structure, mappings: { [accession: string]: QueryParam[] }): SortedAccessionsAndUnits<{ elements: SortedArray }> {
33 | const unitsByAccession: { [accession: string]: { unitId: string, size: number, elements: SortedArray }[] } = {};
34 |
35 | for (const accession in mappings) {
36 | const loci = QueryHelper.getInteractivityLoci(mappings[accession], struct);
37 | const units: (typeof unitsByAccession)[string] = [];
38 | for (const u of loci.elements) {
39 | const unitId = u.unit.id.toString();
40 | const elements: ElementIndex[] = [];
41 | OrderedSet.forEach(u.indices, elementUnitIndex => {
42 | const elementIndex = u.unit.elements[elementUnitIndex];
43 | if (SortedArray.has(u.unit.polymerElements, elementIndex)) elements.push(elementIndex);
44 | });
45 | units.push({ unitId, size: elements.length, elements: SortedArray.ofSortedArray(elements) });
46 | }
47 | units.sort((a, b) => b.size - a.size);
48 | unitsByAccession[accession] = units;
49 | }
50 |
51 | return {
52 | /** Accessions sorted by decreasing biggest unit size */
53 | accessions: Object.keys(unitsByAccession).sort((a, b) => unitsByAccession[b][0].size - unitsByAccession[a][0].size),
54 | /** Units per accession, sorted by decreasing unit size */
55 | units: unitsByAccession,
56 | };
57 | }
58 |
59 | function bestMappingMatch(sortedA: SortedAccessionsAndUnits<{ elements: SortedArray }>, sortedB: SortedAccessionsAndUnits<{ elements: SortedArray }>) {
60 | let bestMatch: { accession: string, unitA: string, unitB: string, elementsA: SortedArray, elementsB: SortedArray, nMatchedElements: number } | undefined = undefined;
61 | let bestScore = 0;
62 | for (const accession of sortedA.accessions) {
63 | const unitsA = sortedA.units[accession]!;
64 | const unitsB = sortedB.units[accession];
65 | if (!unitsB) continue;
66 | for (const ua of unitsA) {
67 | if (ua.size <= bestScore) break;
68 | for (const ub of unitsB) {
69 | if (ub.size <= bestScore || ua.size <= bestScore) break;
70 | const score = Math.min(ua.size, ub.size);
71 | if (score > bestScore) {
72 | bestScore = score;
73 | bestMatch = { accession, unitA: ua.unitId, unitB: ub.unitId, elementsA: ua.elements, elementsB: ub.elements, nMatchedElements: score };
74 | }
75 | }
76 | }
77 | }
78 | return bestMatch;
79 | }
80 |
81 |
82 | const reProtein = /(polypeptide|cyclic-pseudo-peptide)/i;
83 |
84 | function alignAndSuperpose(lociA: StructureElement.Loci, lociB: StructureElement.Loci): MinimizeRmsd.Result {
85 | const location = StructureElement.Loci.getFirstLocation(lociA)!;
86 | const subtype = StructureProperties.entity.subtype(location);
87 | const substMatrix = subtype.match(reProtein) ? 'blosum62' : 'default';
88 |
89 | const { matchedA, matchedB } = computeAlignment(lociA.elements[0], lociB.elements[0], { substMatrix });
90 |
91 | const n = OrderedSet.size(matchedA.indices);
92 | const coordsA = getPositionTable(StructureElement.Loci(lociA.structure, [matchedA]), n);
93 | const coordsB = getPositionTable(StructureElement.Loci(lociB.structure, [matchedB]), n);
94 | const superposition = MinimizeRmsd.compute({ a: coordsA, b: coordsB });
95 | return superposition;
96 | }
97 |
98 | /** `a` and `b` contain matching pairs, i.e. `a.indices[0]` aligns with `b.indices[0]` */
99 | interface AlignmentResult {
100 | matchedA: StructureElement.Loci.Element,
101 | matchedB: StructureElement.Loci.Element,
102 | score: number,
103 | }
104 |
105 | function computeAlignment(a: StructureElement.Loci.Element, b: StructureElement.Loci.Element, options: Partial = {}): AlignmentResult {
106 | const seqA = getSequenceFromLoci(a);
107 | const seqB = getSequenceFromLoci(b);
108 | const { aliA, aliB, score } = align(seqA.sequence.map(getOneLetterCode), seqB.sequence.map(getOneLetterCode), options);
109 |
110 | const indicesA: StructureElement.UnitIndex[] = [];
111 | const indicesB: StructureElement.UnitIndex[] = [];
112 | let seqIdxA = 0, seqIdxB = 0;
113 | for (let i = 0, n = aliA.length; i < n; ++i) {
114 | if (aliA[i] !== '-' && aliB[i] !== '-') {
115 | indicesA.push(seqA.unitElements[seqIdxA]);
116 | indicesB.push(seqB.unitElements[seqIdxB]);
117 | }
118 | if (aliA[i] !== '-') seqIdxA += 1;
119 | if (aliB[i] !== '-') seqIdxB += 1;
120 | }
121 |
122 | return {
123 | matchedA: { unit: a.unit, indices: OrderedSet.ofSortedArray(indicesA) },
124 | matchedB: { unit: b.unit, indices: OrderedSet.ofSortedArray(indicesB) },
125 | score,
126 | };
127 | }
128 |
129 | /** Extract sequence and array of corresponding trace atoms. */
130 | function getSequenceFromLoci(loci: StructureElement.Loci.Element) {
131 | const { unit, indices } = loci;
132 | const unitElements: UnitIndex[] = [];
133 | const sequence: string[] = [];
134 | OrderedSet.forEach(indices, elementUnitIndex => {
135 | const elementIndex = unit.elements[elementUnitIndex];
136 | if (OrderedSet.has(unit.polymerElements, elementIndex)) {
137 | unitElements.push(elementUnitIndex);
138 | const compId = unit.model.atomicHierarchy.atoms.label_comp_id.value(elementIndex);
139 | sequence.push(compId);
140 | }
141 | });
142 | return { sequence, unitElements };
143 | }
144 |
145 |
146 | function getOneLetterCode(compId: string) {
147 | return OneLetterCodes[compId] ?? 'X';
148 | }
149 |
150 | // Copied from Molstar
151 | const OneLetterCodes: Record = {
152 | 'HIS': 'H',
153 | 'ARG': 'R',
154 | 'LYS': 'K',
155 | 'ILE': 'I',
156 | 'PHE': 'F',
157 | 'LEU': 'L',
158 | 'TRP': 'W',
159 | 'ALA': 'A',
160 | 'MET': 'M',
161 | 'PRO': 'P',
162 | 'CYS': 'C',
163 | 'ASN': 'N',
164 | 'VAL': 'V',
165 | 'GLY': 'G',
166 | 'SER': 'S',
167 | 'GLN': 'Q',
168 | 'TYR': 'Y',
169 | 'ASP': 'D',
170 | 'GLU': 'E',
171 | 'THR': 'T',
172 |
173 | 'SEC': 'U', // as per IUPAC definition
174 | 'PYL': 'O', // as per IUPAC definition
175 |
176 | // charmm ff
177 | 'HSD': 'H', 'HSE': 'H', 'HSP': 'H',
178 | 'LSN': 'K',
179 | 'ASPP': 'D',
180 | 'GLUP': 'E',
181 |
182 | // amber ff
183 | 'HID': 'H', 'HIE': 'H', 'HIP': 'H',
184 | 'LYN': 'K',
185 | 'ASH': 'D',
186 | 'GLH': 'E',
187 |
188 | // DNA
189 | 'DA': 'A',
190 | 'DC': 'C',
191 | 'DG': 'G',
192 | 'DT': 'T',
193 | 'DU': 'U',
194 |
195 | // RNA
196 | 'A': 'A',
197 | 'C': 'C',
198 | 'G': 'G',
199 | 'T': 'T',
200 | 'U': 'U',
201 | };
202 |
--------------------------------------------------------------------------------