├── .gitattributes ├── .npmignore ├── favicon.ico ├── src ├── app │ ├── styles │ │ ├── pdbe-molstar-dark.scss │ │ ├── pdbe-molstar-light.scss │ │ └── pdbe-molstar │ │ │ └── _index.scss │ ├── index.ts │ ├── ui │ │ ├── left-panel │ │ │ ├── pdbe-left-panel.ts │ │ │ ├── tabs.tsx │ │ │ └── core.tsx │ │ ├── custom-controls.tsx │ │ ├── layout-no-controls-unless-expanded.tsx │ │ ├── split-ui │ │ │ ├── components.ts │ │ │ └── split-ui.tsx │ │ ├── pdbe-viewport-controls.tsx │ │ ├── overlay.tsx │ │ ├── alphafold-tranparency.tsx │ │ ├── export-superposition.tsx │ │ ├── icons.tsx │ │ ├── annotation-row-controls.tsx │ │ ├── pdbe-viewport.tsx │ │ ├── pdbe-structure-controls.tsx │ │ └── pdbe-screenshot-controls.tsx │ ├── labels.ts │ ├── domain-annotations │ │ ├── behavior.ts │ │ ├── color.ts │ │ └── prop.ts │ ├── extensions │ │ ├── state-gallery │ │ │ ├── config.ts │ │ │ ├── titles.ts │ │ │ └── behavior.ts │ │ ├── interactions │ │ │ └── index.ts │ │ └── complexes │ │ │ ├── superpose-by-biggest-chain.ts │ │ │ ├── coloring.ts │ │ │ └── superpose-by-sequence-alignment.ts │ ├── custom-events.ts │ ├── sequence-color │ │ ├── sequence-color-annotations-prop.ts │ │ ├── behavior.ts │ │ ├── sequence-color-theme-prop.ts │ │ └── color.ts │ ├── sifts-mappings-behaviour.ts │ ├── sifts-mapping.ts │ ├── alphafold-transparency.ts │ ├── superposition-export.ts │ ├── plugin-custom-state.ts │ ├── spec-from-html.ts │ ├── loci-details.ts │ └── superposition-focus-representation.ts └── web-component │ └── index.js ├── .gitignore ├── webpack.config.development.js ├── tsconfig.json ├── .gitlab-ci.yml ├── .github └── workflows │ └── node.yml ├── README.md ├── package.json ├── scripts.js ├── webpack.config.production.js ├── CHANGELOG.md ├── eslint.config.mjs └── static-demo.html /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !build/ 2 | src/ 3 | lib/ -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molstar/pdbe-molstar/HEAD/favicon.ico -------------------------------------------------------------------------------- /src/app/styles/pdbe-molstar-dark.scss: -------------------------------------------------------------------------------- 1 | @use 'molstar/lib/mol-plugin-ui/skin/dark'; 2 | @use './pdbe-molstar/_index'; 3 | -------------------------------------------------------------------------------- /src/app/styles/pdbe-molstar-light.scss: -------------------------------------------------------------------------------- 1 | @use 'molstar/lib/mol-plugin-ui/skin/light'; 2 | @use './pdbe-molstar/_index'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | lib/ 3 | tmp/ 4 | 5 | node_modules/ 6 | debug.log 7 | npm-debug.log 8 | tsconfig.tsbuildinfo 9 | 10 | *.sublime-workspace 11 | .idea 12 | .DS_Store 13 | 14 | # NPM built package 15 | /package/ 16 | /pdbe-molstar-*.tgz 17 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | const productionConfig = require('./webpack.config.production.js'); 2 | 3 | const developmentConfig = productionConfig.map(conf => ({ 4 | ...conf, 5 | devtool: 'eval-source-map', 6 | })); 7 | 8 | module.exports = developmentConfig; 9 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | /** Entrypoint for the bundler. If you want to import PDBeMolstarPlugin without CSS, import from ./viewer instead. */ 2 | 3 | export * from './viewer'; 4 | import { PDBeMolstarPlugin } from './viewer'; 5 | 6 | import './styles/pdbe-molstar-dark.scss'; 7 | 8 | (window as any).PDBeMolstarPlugin = PDBeMolstarPlugin; 9 | -------------------------------------------------------------------------------- /src/web-component/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, no-restricted-syntax */ 2 | 3 | import { LitElement } from 'lit-element'; 4 | 5 | class PdbeMolstar extends LitElement { 6 | connectedCallback() { 7 | super.connectedCallback(); 8 | this.viewerInstance = new PDBeMolstarPlugin(); 9 | this.initParams = PDBeMolstarPlugin.initParamsFromHtmlAttributes(this); 10 | console.log('PdbeMolstar initParams:', this.initParams); 11 | this.viewerInstance.render(this, this.initParams); 12 | } 13 | 14 | createRenderRoot() { 15 | return this; 16 | } 17 | } 18 | 19 | export default PdbeMolstar; 20 | 21 | customElements.define('pdbe-molstar', PdbeMolstar); 22 | -------------------------------------------------------------------------------- /src/app/ui/left-panel/pdbe-left-panel.ts: -------------------------------------------------------------------------------- 1 | import { LeftPanel } from './core'; 2 | import { CoreLeftPanelTabs, PDBeLeftPanelTabs } from './tabs'; 3 | 4 | 5 | /** Left panel in core Molstar */ 6 | export const DefaultLeftPanelControls = LeftPanel({ 7 | tabsTop: [ 8 | CoreLeftPanelTabs.root, 9 | CoreLeftPanelTabs.data, 10 | CoreLeftPanelTabs.states, 11 | CoreLeftPanelTabs.help, 12 | PDBeLeftPanelTabs.segments, 13 | ], 14 | tabsBottom: [ 15 | CoreLeftPanelTabs.settings, 16 | ], 17 | defaultTab: 'none', 18 | }); 19 | 20 | 21 | /** Left panel in PDBe Molstar */ 22 | export const PDBeLeftPanelControls = LeftPanel({ 23 | tabsTop: [ 24 | CoreLeftPanelTabs.help, 25 | PDBeLeftPanelTabs.segments, 26 | ], 27 | tabsBottom: [ 28 | CoreLeftPanelTabs.settings, 29 | ], 30 | defaultTab: 'none', 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/ui/custom-controls.tsx: -------------------------------------------------------------------------------- 1 | import { PluginUIComponent } from 'molstar/lib/mol-plugin-ui/base'; 2 | import { PluginCustomControlRegion, PluginCustomControls } from '../plugin-custom-state'; 3 | 4 | 5 | /** Render all registered custom UI controls for the specified region */ 6 | export class CustomControls extends PluginUIComponent<{ region: PluginCustomControlRegion }> { 7 | componentDidMount() { 8 | this.subscribe(this.plugin.state.behaviors.events.changed, () => this.forceUpdate()); 9 | } 10 | render() { 11 | const customControls = Array.from(PluginCustomControls.get(this.plugin, this.props.region).entries()); 12 | return <> 13 | {customControls.map(([name, Control]) => 14 |
15 | 16 |
17 | )} 18 | ; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es2015", 5 | "alwaysStrict": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "sourceMap": false, 9 | "noUnusedLocals": true, 10 | "strictNullChecks": true, 11 | "strictFunctionTypes": true, 12 | "module": "CommonJS", 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "importHelpers": true, 16 | "noEmitHelpers": true, 17 | "allowSyntheticDefaultImports": true, 18 | "jsx": "react-jsx", 19 | "lib": ["es2018", "dom"], 20 | "rootDir": "src/app", 21 | "outDir": "lib", 22 | "composite": true, // required if we want to set "tsBuildInfoFile" 23 | "tsBuildInfoFile": "tsconfig.tsbuildinfo", // default setting would place this file out of the repo directory 24 | "baseUrl": "./", 25 | }, 26 | "include": ["src/app"] 27 | } -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - publish 3 | 4 | npm_publish: 5 | stage: publish 6 | only: 7 | - tags 8 | image: node:20 9 | script: 10 | # Ensure the git tag matches the npm package version (e.g. tag v1.0.0 for version 1.0.0) 11 | - PKG_NAME=$(node -e 'console.log(JSON.parse(fs.readFileSync("package.json")).name)') 12 | - PKG_VERSION=$(node -e 'console.log(JSON.parse(fs.readFileSync("package.json")).version)') 13 | - echo 'Package name:' $PKG_NAME 14 | - echo 'Package version:' $PKG_VERSION 15 | - echo 'Tag:' $CI_COMMIT_TAG 16 | - test "$CI_COMMIT_TAG" = "v$PKG_VERSION" 17 | 18 | # Determine release status ('latest' for proper releases, 'dev' for beta versions) 19 | - STATUS=$([[ "$PKG_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo latest || echo dev) 20 | - echo 'Release status (latest|dev):' $STATUS 21 | 22 | # Build 23 | - npm ci --omit optional 24 | - npm run lint 25 | - npm run rebuild 26 | 27 | # Publish to npm registry (with 'latest' and 'dev' tag for proper releases, with only 'dev' tag for beta versions) 28 | - test -n "${NPM_AUTH_TOKEN}" && echo 'NPM_AUTH_TOKEN is available' || echo 'NPM_AUTH_TOKEN is not available (not set or this branch/tag has insufficient permissions)' 29 | - npm config set -- '//registry.npmjs.org/:_authToken'="${NPM_AUTH_TOKEN}" 30 | - npm publish --verbose --tag "$STATUS" 31 | - npm dist-tags add "$PKG_NAME@$PKG_VERSION" dev 32 | - npm view "$PKG_NAME" 33 | -------------------------------------------------------------------------------- /src/app/ui/layout-no-controls-unless-expanded.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'molstar/lib/mol-plugin-ui/plugin'; 2 | import { PluginConfig } from 'molstar/lib/mol-plugin/config'; 3 | import { PluginCustomState } from '../plugin-custom-state'; 4 | 5 | 6 | export class FullLayoutNoControlsUnlessExpanded extends Layout { 7 | override componentDidMount(): void { 8 | super.componentDidMount(); 9 | // Hide Toggle Controls button unless expanded: 10 | this.subscribe(this.plugin.layout.events.updated, () => { 11 | const isExpanded = this.plugin.layout.state.isExpanded; 12 | const hideControlToggle = PluginCustomState(this.plugin).initParams?.hideCanvasControls.includes('controlToggle') ?? false; 13 | this.plugin.config.set(PluginConfig.Viewport.ShowControls, isExpanded && !hideControlToggle); 14 | }); 15 | } 16 | 17 | override get layoutVisibilityClassName(): string { 18 | const classes = super.layoutVisibilityClassName.split(' '); 19 | const state = this.plugin.layout.state; 20 | if (!state.isExpanded) { 21 | if (!classes.includes('msp-layout-hide-top')) classes.push('msp-layout-hide-top'); 22 | if (!classes.includes('msp-layout-hide-bottom')) classes.push('msp-layout-hide-bottom'); 23 | if (!classes.includes('msp-layout-hide-left')) classes.push('msp-layout-hide-left'); 24 | if (!classes.includes('msp-layout-hide-right')) classes.push('msp-layout-hide-right'); 25 | } 26 | return classes.join(' '); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/ui/split-ui/components.ts: -------------------------------------------------------------------------------- 1 | import { ControlsWrapper, Log } from 'molstar/lib/mol-plugin-ui/plugin'; 2 | import { SequenceView } from 'molstar/lib/mol-plugin-ui/sequence'; 3 | import { JSXElementConstructor } from 'react'; 4 | import { FullLayoutNoControlsUnlessExpanded } from '../layout-no-controls-unless-expanded'; 5 | import { DefaultLeftPanelControls, PDBeLeftPanelControls } from '../left-panel/pdbe-left-panel'; 6 | import { PDBeViewport } from '../pdbe-viewport'; 7 | 8 | 9 | export const UIComponents = { 10 | /** 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. */ 11 | PDBeViewport, 12 | 13 | /** Component containing 1D view of the sequences (top panel in default layout) */ 14 | SequenceView, 15 | 16 | /** Component containing log messages (bottom panel in default layout) */ 17 | Log, 18 | 19 | /** Component containing left panel controls (contents depend on PDBeMolstar init params (superposition/ligand/default view)) */ 20 | PDBeLeftPanelControls, 21 | 22 | /** Component containing left panel controls as in core Molstar, plus PDBeMolstar-specific tabs */ 23 | DefaultLeftPanelControls, 24 | 25 | /** Component containing right panel controls (contents depend on PDBeMolstar init params (superposition/ligand/default view)) */ 26 | DefaultRightPanelControls: ControlsWrapper, 27 | 28 | /** Component containing only `PDBeViewport` when not in fullscreen view, but shows full UI layout when in fullscreen view. */ 29 | FullLayoutNoControlsUnlessExpanded, 30 | 31 | // TODO add all meaningful components, 32 | } as const satisfies Record>; 33 | -------------------------------------------------------------------------------- /src/app/labels.ts: -------------------------------------------------------------------------------- 1 | import { Loci } from 'molstar/lib/mol-model/loci'; 2 | import { StructureElement, StructureProperties } from 'molstar/lib/mol-model/structure'; 3 | import { LociLabel } from 'molstar/lib/mol-plugin-state/manager/loci-label'; 4 | import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior'; 5 | import { PluginContext } from 'molstar/lib/mol-plugin/context'; 6 | import { lociLabel } from 'molstar/lib/mol-theme/label'; 7 | import { PluginCustomState } from './plugin-custom-state'; 8 | 9 | 10 | export const PDBeLociLabelProvider = PluginBehavior.create({ 11 | name: 'pdbe-loci-label-provider', 12 | category: 'interaction', 13 | ctor: class implements PluginBehavior { 14 | private f = { 15 | label: (loci: Loci) => { 16 | const superpositionView = PluginCustomState(this.ctx).initParams!.superposition; 17 | 18 | const label: string[] = []; 19 | if (!superpositionView && StructureElement.Loci.is(loci) && loci.elements.length === 1) { 20 | const entityNames = new Set(); 21 | for (const { unit: u } of loci.elements) { 22 | const l = StructureElement.Location.create(loci.structure, u, u.elements[0]); 23 | const name = StructureProperties.entity.pdbx_description(l).join(', '); 24 | entityNames.add(name); 25 | } 26 | if (entityNames.size === 1) entityNames.forEach(name => label.push(name)); 27 | } 28 | label.push(lociLabel(loci)); 29 | return label.filter(l => !!l).join('
'); 30 | }, 31 | group: (label: LociLabel) => label.toString().replace(/Model [0-9]+/g, 'Models'), 32 | priority: 100, 33 | }; 34 | register() { this.ctx.managers.lociLabels.addProvider(this.f); } 35 | unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); } 36 | constructor(protected ctx: PluginContext) { } 37 | }, 38 | display: { name: 'Provide PDBe Loci Label' }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/app/domain-annotations/behavior.ts: -------------------------------------------------------------------------------- 1 | import { Loci } from 'molstar/lib/mol-model/loci'; 2 | import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior'; 3 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition'; 4 | import { DomainAnnotationsColorThemeProvider } from './color'; 5 | import { DomainAnnotationsProvider } from './prop'; 6 | 7 | 8 | export const PDBeDomainAnnotations = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({ 9 | name: 'pdbe-domain-annotations-prop', 10 | category: 'custom-props', 11 | display: { 12 | name: 'Domain annotations', 13 | description: 'Data for domain annotations, obtained via PDBe.', 14 | }, 15 | ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> { 16 | 17 | private provider = DomainAnnotationsProvider; 18 | 19 | private labelDomainAnnotations = { 20 | label: (loci: Loci): string | undefined => undefined, 21 | }; 22 | 23 | register(): void { 24 | this.ctx.customModelProperties.register(this.provider, this.params.autoAttach); 25 | this.ctx.managers.lociLabels.addProvider(this.labelDomainAnnotations); 26 | this.ctx.representation.structure.themes.colorThemeRegistry.add(DomainAnnotationsColorThemeProvider); 27 | } 28 | 29 | update(p: { autoAttach: boolean, showTooltip: boolean }) { 30 | const updated = this.params.autoAttach !== p.autoAttach; 31 | this.params.autoAttach = p.autoAttach; 32 | this.params.showTooltip = p.showTooltip; 33 | this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach); 34 | return updated; 35 | } 36 | 37 | unregister() { 38 | this.ctx.customModelProperties.unregister(DomainAnnotationsProvider.descriptor.name); 39 | this.ctx.managers.lociLabels.removeProvider(this.labelDomainAnnotations); 40 | this.ctx.representation.structure.themes.colorThemeRegistry.remove(DomainAnnotationsColorThemeProvider); 41 | } 42 | }, 43 | params: () => ({ 44 | autoAttach: PD.Boolean(false), 45 | showTooltip: PD.Boolean(true), 46 | }), 47 | }); 48 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 14 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | # Build 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | - run: npm ci 29 | - name: Lint 30 | run: npm run lint 31 | - name: Build 32 | run: npm run build 33 | - name: Upload build artifact 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: build 37 | path: build 38 | 39 | # Check if this is 'master' branch 40 | master-branch: 41 | needs: [build] 42 | if: github.ref == 'refs/heads/master' # Only run on master branch 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Echo OK 46 | run: echo OK 47 | 48 | # Build pages 49 | build-pages: 50 | needs: [master-branch] 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout 54 | uses: actions/checkout@v4 55 | - name: Download build artifact 56 | uses: actions/download-artifact@v4 57 | with: 58 | name: build 59 | path: build 60 | - name: Setup Pages 61 | uses: actions/configure-pages@v5 62 | - name: Build with Jekyll 63 | uses: actions/jekyll-build-pages@v1 64 | with: 65 | source: ./ 66 | destination: ./_site 67 | - name: Upload pages artifact 68 | uses: actions/upload-pages-artifact@v3 69 | 70 | # Deploy pages 71 | deploy-pages: 72 | needs: [build-pages] 73 | runs-on: ubuntu-latest 74 | environment: 75 | name: github-pages 76 | url: ${{ steps.deployment.outputs.page_url }} 77 | steps: 78 | - name: Deploy to GitHub Pages 79 | id: deployment 80 | uses: actions/deploy-pages@v4 81 | -------------------------------------------------------------------------------- /src/app/ui/pdbe-viewport-controls.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'molstar/lib/mol-plugin-ui/controls/common'; 2 | import { ViewportControls } from 'molstar/lib/mol-plugin-ui/viewport'; 3 | import { PluginCustomState } from '../plugin-custom-state'; 4 | 5 | 6 | export class PDBeViewportControls extends ViewportControls { 7 | render() { 8 | const initParams = PluginCustomState(this.plugin).initParams; 9 | const showPDBeLink = initParams?.moleculeId && initParams?.pdbeLink && !initParams?.superposition; 10 | 11 | return <> 12 | {showPDBeLink && 13 |
14 |
15 | 25 |
26 | } 27 |
28 | {super.render()} 29 |
30 | ; 31 | } 32 | } 33 | 34 | function PDBeLogo({ className }: { className?: string }) { 35 | return 36 | 37 | 38 | ; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/extensions/state-gallery/config.ts: -------------------------------------------------------------------------------- 1 | import { PluginConfigItem } from 'molstar/lib/mol-plugin/config'; 2 | import { PluginContext } from 'molstar/lib/mol-plugin/context'; 3 | import { PluginConfigUtils } from '../../helpers'; 4 | 5 | 6 | /** Default values of plugin config items for the `StateGallery` extension */ 7 | export const StateGalleryConfigDefaults = { 8 | /** Base URL of the state API (list of states will be downloaded from `{ServerUrl}/{entryId}.json`, states from `{ServerUrl}/{stateName}.molj`) */ 9 | ServerUrl: 'https://www.ebi.ac.uk/pdbe/static/entry', 10 | /** Load canvas properties, such as background, axes indicator, fog, outline (if false, keep current canvas properties) */ 11 | LoadCanvasProps: false, 12 | /** Load camera orientation when loading state (if false, keep current orientation) */ 13 | LoadCameraOrientation: true, 14 | /** Time in miliseconds between loading state and starting camera transition */ 15 | CameraPreTransitionMs: 100, 16 | /** Duration of the camera transition in miliseconds */ 17 | CameraTransitionMs: 400, 18 | }; 19 | /** Values of plugin config items for the `StateGallery` extension */ 20 | export type StateGalleryConfigValues = typeof StateGalleryConfigDefaults; 21 | 22 | /** Definition of plugin config items for the `StateGallery` extension */ 23 | export const StateGalleryConfig: PluginConfigUtils.ConfigFor = { 24 | ServerUrl: new PluginConfigItem('pdbe-state-gallery.server-url', StateGalleryConfigDefaults.ServerUrl), 25 | LoadCanvasProps: new PluginConfigItem('pdbe-state-gallery.load-canvas-props', StateGalleryConfigDefaults.LoadCanvasProps), 26 | LoadCameraOrientation: new PluginConfigItem('pdbe-state-gallery.load-camera-orientation', StateGalleryConfigDefaults.LoadCameraOrientation), 27 | CameraPreTransitionMs: new PluginConfigItem('pdbe-state-gallery.camera-pre-transition-ms', StateGalleryConfigDefaults.CameraPreTransitionMs), 28 | CameraTransitionMs: new PluginConfigItem('pdbe-state-gallery.camera-transition-ms', StateGalleryConfigDefaults.CameraTransitionMs), 29 | }; 30 | 31 | /** Retrieve config values the `StateGallery` extension from the current plugin config */ 32 | export function getStateGalleryConfig(plugin: PluginContext): StateGalleryConfigValues { 33 | return PluginConfigUtils.getConfigValues(plugin, StateGalleryConfig, StateGalleryConfigDefaults); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/ui/overlay.tsx: -------------------------------------------------------------------------------- 1 | import { PurePluginUIComponent } from 'molstar/lib/mol-plugin-ui/base'; 2 | import { ComponentClass, JSXElementConstructor } from 'react'; 3 | import { PluginCustomState } from '../plugin-custom-state'; 4 | 5 | 6 | /** Return a React component with MainContent, overlayed by OverlayContent when `PluginCustomState(this.plugin).events?.isBusy` last emitted value is true. */ 7 | export function WithLoadingOverlay(MainContent: JSXElementConstructor<{}>, OverlayContent: JSXElementConstructor<{}> = PDBeLoadingOverlayBox): ComponentClass<{}> { 8 | return class _WithLoadingOverlay extends PurePluginUIComponent<{}, { showOverlay: boolean }> { 9 | state: Readonly<{ showOverlay: boolean }> = { showOverlay: false }; 10 | componentDidMount(): void { 11 | super.componentDidMount?.(); 12 | const busyEvent = PluginCustomState(this.plugin).events?.isBusy; 13 | if (busyEvent) { 14 | this.subscribe(busyEvent, busy => { 15 | this.setState({ showOverlay: busy && !!PluginCustomState(this.plugin).initParams?.loadingOverlay }); 16 | }); 17 | } 18 | } 19 | render() { 20 | return <> 21 | 22 | {this.state.showOverlay &&
23 | 24 |
} 25 | ; 26 | } 27 | }; 28 | } 29 | 30 | /** Overlay component with animated PDBe logo */ 31 | function PDBeLoadingOverlayBox() { 32 | return
33 | 34 | 35 | 36 | 37 | ; 38 |
; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/ui/alphafold-tranparency.tsx: -------------------------------------------------------------------------------- 1 | import { CollapsableControls } from 'molstar/lib/mol-plugin-ui/base'; 2 | import { SuperpositionSvg } from 'molstar/lib/mol-plugin-ui/controls/icons'; 3 | import { ParameterControls } from 'molstar/lib/mol-plugin-ui/controls/parameters'; 4 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition'; 5 | import { applyAFTransparency, clearStructureTransparency } from '../alphafold-transparency'; 6 | import { PluginCustomState } from '../plugin-custom-state'; 7 | 8 | 9 | const TransparencyParams = { 10 | score: PD.Numeric(70, { min: 0, max: 100, step: 1 }, { label: 'pLDDT less than', description: 'pLDDT score value in the range of 0 to 100' }), 11 | opacity: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }, { description: 'Opacity value in the range 0 to 1' }), 12 | }; 13 | type TransparencyParams = PD.Values; 14 | 15 | export class AlphafoldTransparencyControls extends CollapsableControls<{}, { transpareny: any }> { 16 | defaultState() { 17 | return { 18 | isCollapsed: false, 19 | header: 'AlphaFold Structure Opacity', 20 | brand: { accent: 'gray' as const, svg: SuperpositionSvg }, 21 | isHidden: true, 22 | transpareny: { 23 | score: 70, 24 | opacity: 0.2, 25 | }, 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, sel => { 31 | const superpositionState = PluginCustomState(this.plugin).superpositionState; 32 | if (superpositionState && superpositionState.alphafold.ref && superpositionState.alphafold.ref !== '') { 33 | this.setState({ isHidden: false }); 34 | } 35 | }); 36 | } 37 | 38 | updateTransparency = async (val: any) => { 39 | this.setState({ transpareny: val }); 40 | const superpositionState = PluginCustomState(this.plugin).superpositionState; 41 | if (!superpositionState) throw new Error('customState.superpositionState has not been initialized'); 42 | const afStr: any = this.plugin.managers.structure.hierarchy.current.refs.get(superpositionState.alphafold.ref!); 43 | await clearStructureTransparency(this.plugin, afStr.components); 44 | await applyAFTransparency(this.plugin, afStr, 1 - val.opacity, val.score); 45 | }; 46 | 47 | renderControls() { 48 | return <> 49 | 50 | ; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PDBe Molstar 2 | 3 | [![Published on NPM](https://img.shields.io/npm/v/pdbe-molstar.svg)](https://www.npmjs.com/package/pdbe-molstar) 4 | 5 | PDBe implementation of [Mol\* (/'mol-star/)](https://github.com/molstar/molstar) 6 | 7 | **Refer [PDBe Molstar Wiki](https://github.com/PDBeurope/pdbe-molstar/wiki) for detailed documentation and examples** 8 | 9 | ## Building & Running locally 10 | 11 | ```sh 12 | npm install 13 | npm run build 14 | # npm run rebuild # for a clean build 15 | npm run serve 16 | ``` 17 | 18 | ## Build automatically on file save: 19 | 20 | ```sh 21 | npm run watch 22 | ``` 23 | 24 | ## Manual testing 25 | 26 | - Run locally by `npm run serve` 27 | - Go to and check the viewer with various different setting (some of these reflect the actual setting on PDBe pages) 28 | - If you want to tweak the options, go to "Frame URL" and change the options in the URL 29 | 30 | ## Deployment 31 | 32 | - Bump version in `package.json` using semantic versioning 33 | - Use a version number like "1.2.3-beta.1" for development versions (to be used in development environment wwwdev.ebi.ac.uk) 34 | - Use a version number like "1.2.3" for proper releases 35 | - Ensure `npm install && npm run lint && npm run rebuild` works locally 36 | - Update `CHANGELOG.md` 37 | - Git commit and push (commit message e.g. "Version 1.2.3") 38 | - Create a git tag matching the version with prepended "v" (e.g. "v1.2.3") 39 | - The GitHub repo will automatically be mirrored to EBI GitLab (might take up to 1 hour) 40 | - CICD pipeline in EBI GitLab will automatically publish the package to npm (https://www.npmjs.com/package/pdbe-molstar) 41 | - The files will become available via JSDeliver 42 | - Latest version including development versions: 43 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@dev/build/pdbe-molstar-plugin.js 44 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@dev/build/pdbe-molstar-component.js 45 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@dev/build/pdbe-molstar.css 46 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@dev/build/pdbe-molstar-light.css 47 | - Latest proper release: 48 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@latest/build/pdbe-molstar-plugin.js 49 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@latest/build/pdbe-molstar-component.js 50 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@latest/build/pdbe-molstar.css 51 | - https://cdn.jsdelivr.net/npm/pdbe-molstar@latest/build/pdbe-molstar-light.css 52 | - Go to https://www.jsdelivr.com/tools/purge and purge the cache for abovementioned URLs (otherwise it might take up to 7 days to before `@latest` starts pointing to the new version) 53 | -------------------------------------------------------------------------------- /src/app/custom-events.ts: -------------------------------------------------------------------------------- 1 | import { InteractivityManager } from 'molstar/lib/mol-plugin-state/manager/interactivity'; 2 | import { PluginContext } from 'molstar/lib/mol-plugin/context'; 3 | import { debounceTime } from 'rxjs/operators'; 4 | import { EventDetail, lociDetails } from './loci-details'; 5 | 6 | 7 | export namespace CustomEvents { 8 | function createEvent(eventType: string): MouseEvent { 9 | if (typeof MouseEvent === 'function') { 10 | // current standard 11 | return new MouseEvent(eventType, { 'view': window, 'bubbles': true, 'cancelable': true }); 12 | } else if (typeof document.createEvent === 'function') { 13 | // older standard 14 | const event = document.createEvent('MouseEvents'); 15 | event.initEvent(eventType, true /* bubbles */, true /* cancelable */); 16 | return event; 17 | } else { 18 | throw new Error('Cannot create event'); 19 | } 20 | } 21 | 22 | function dispatchCustomEvent(event: UIEvent, eventData: EventDetail, targetElement: HTMLElement) { 23 | if (eventData !== undefined) { 24 | if (eventData.seq_id !== undefined) { 25 | (eventData as any).residueNumber = eventData.seq_id; 26 | } 27 | (event as any).eventData = eventData; 28 | targetElement.dispatchEvent(event); 29 | } 30 | } 31 | 32 | export function add(plugin: PluginContext, targetElement: HTMLElement) { 33 | const PDB_molstar_click = createEvent('PDB.molstar.click'); 34 | const PDB_molstar_mouseover = createEvent('PDB.molstar.mouseover'); 35 | const PDB_molstar_mouseout = createEvent('PDB.molstar.mouseout'); 36 | 37 | plugin.behaviors.interaction.click.subscribe((e: InteractivityManager.ClickEvent) => { 38 | if (e.button === 1 && e.current && e.current.loci.kind !== 'empty-loci') { 39 | const evData = lociDetails(e.current.loci); 40 | if (evData) dispatchCustomEvent(PDB_molstar_click, evData, targetElement); 41 | } 42 | }); 43 | plugin.behaviors.interaction.hover.pipe(debounceTime(100)).subscribe((e: InteractivityManager.HoverEvent) => { 44 | if (e.current && e.current.loci && e.current.loci.kind !== 'empty-loci') { 45 | const evData = lociDetails(e.current.loci); 46 | if (evData) dispatchCustomEvent(PDB_molstar_mouseover, evData, targetElement); 47 | } 48 | 49 | if (e.current && e.current.loci && e.current.loci.kind === 'empty-loci') { 50 | dispatchCustomEvent(PDB_molstar_mouseout, {}, targetElement); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/ui/export-superposition.tsx: -------------------------------------------------------------------------------- 1 | import { CollapsableControls, CollapsableState } from 'molstar/lib/mol-plugin-ui/base'; 2 | import { Button } from 'molstar/lib/mol-plugin-ui/controls/common'; 3 | import { GetAppSvg } from 'molstar/lib/mol-plugin-ui/controls/icons'; 4 | import { ParameterControls } from 'molstar/lib/mol-plugin-ui/controls/parameters'; 5 | import { useBehavior } from 'molstar/lib/mol-plugin-ui/hooks/use-behavior'; 6 | import { PluginContext } from 'molstar/lib/mol-plugin/context'; 7 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition'; 8 | import React from 'react'; 9 | import { superpositionExportHierarchy } from '../superposition-export'; 10 | 11 | 12 | export class SuperpositionModelExportUI extends CollapsableControls<{}, {}> { 13 | protected defaultState(): CollapsableState { 14 | return { 15 | header: 'Export Models', 16 | isCollapsed: true, 17 | brand: { accent: 'cyan', svg: GetAppSvg }, 18 | }; 19 | } 20 | protected renderControls(): JSX.Element | null { 21 | return ; 22 | } 23 | } 24 | 25 | const Params = { 26 | format: PD.Select<'cif' | 'bcif'>('cif', [['cif', 'mmCIF'], ['bcif', 'Binary mmCIF']]), 27 | }; 28 | const DefaultParams = PD.getDefaultValues(Params); 29 | 30 | function SuperpositionExportControls({ plugin }: { plugin: PluginContext }) { 31 | const [params, setParams] = React.useState(DefaultParams); 32 | const [exporting, setExporting] = React.useState(false); 33 | useBehavior(plugin.managers.structure.hierarchy.behaviors.selection); // triggers UI update 34 | const isBusy = useBehavior(plugin.behaviors.state.isBusy); 35 | const hierarchy = plugin.managers.structure.hierarchy.current; 36 | 37 | let label: string = 'Nothing to Export'; 38 | if (hierarchy.structures.length === 1) { 39 | label = 'Export'; 40 | } if (hierarchy.structures.length > 1) { 41 | label = 'Export (as ZIP)'; 42 | } 43 | 44 | const onExport = async () => { 45 | setExporting(true); 46 | try { 47 | await superpositionExportHierarchy(plugin, { format: params.format }); 48 | } finally { 49 | setExporting(false); 50 | } 51 | }; 52 | 53 | return <> 54 | 55 | 63 | ; 64 | } 65 | -------------------------------------------------------------------------------- /src/app/extensions/state-gallery/titles.ts: -------------------------------------------------------------------------------- 1 | import { Image } from './manager'; 2 | 3 | 4 | type Titles = Pick; 5 | 6 | 7 | /** Functions for creating informative image (3D state) titles for display in UI */ 8 | export const ImageTitles = { 9 | entry(img: Image): Titles { 10 | if (img.filename.includes('_chemically_distinct_molecules')) { 11 | return { title: 'Deposited model (color by entity)' }; 12 | } 13 | if (img.filename.includes('_chain')) { 14 | return { title: 'Deposited model (color by chain)' }; 15 | } 16 | return {}; 17 | }, 18 | validation(img: Image): Titles { 19 | return { title: 'Geometry validation' }; 20 | }, 21 | bfactor(img: Image): Titles { 22 | return { title: 'B-factor' }; 23 | }, 24 | assembly(img: Image, info: { assemblyId: string }): Titles { 25 | if (img.filename.includes('_chemically_distinct_molecules')) { 26 | return { title: `Assembly ${info.assemblyId} (color by entity)` }; 27 | } 28 | if (img.filename.includes('_chain')) { 29 | return { title: `Assembly ${info.assemblyId} (color by chain)` }; 30 | } 31 | return {}; 32 | }, 33 | entity(img: Image, info: { entityId: string }): Titles { 34 | const entityName = getSpans(img.description)[1]; 35 | return { 36 | title: `Entity ${info.entityId}`, 37 | subtitle: entityName, 38 | }; 39 | }, 40 | ligand(img: Image, info: { compId: string }): Titles { 41 | const ligandName = getParenthesis(getSpans(img.description)[0]); 42 | return { 43 | title: `Ligand environment for ${info.compId}`, 44 | subtitle: ligandName, 45 | }; 46 | }, 47 | modres(img: Image, info: { compId: string }): Titles { 48 | const modresName = getParenthesis(getSpans(img.description)[1]); 49 | return { 50 | title: `Modified residue ${info.compId}`, 51 | subtitle: modresName, 52 | }; 53 | }, 54 | domain(img: Image, info: { db: string, familyId: string, entityId: string }): Titles { 55 | const familyName = getParenthesis(getSpans(img.description)[1]); 56 | return { 57 | title: `${info.db} ${info.familyId} (entity ${info.entityId})`, 58 | subtitle: familyName, 59 | }; 60 | }, 61 | }; 62 | 63 | 64 | /** Get contents of `...` tags from an HTML string */ 65 | function getSpans(text: string | undefined): string[] { 66 | const matches = (text ?? '').matchAll(/]*>([^<]*)<\/span>/g); 67 | return Array.from(matches).map(match => match[1]); 68 | } 69 | 70 | /** Get content of parenthesis (`(...)`) from a string */ 71 | function getParenthesis(text: string | undefined): string | undefined { 72 | return text?.match(/\((.*)\)/)?.[1]; 73 | } 74 | -------------------------------------------------------------------------------- /src/app/sequence-color/sequence-color-annotations-prop.ts: -------------------------------------------------------------------------------- 1 | import { ElementSet, Selector, SelectorParams } from 'molstar/lib/extensions/mvs/components/selector'; 2 | import { CustomProperty } from 'molstar/lib/mol-model-props/common/custom-property'; 3 | import { CustomStructureProperty } from 'molstar/lib/mol-model-props/common/custom-structure-property'; 4 | import { CustomPropertyDescriptor } from 'molstar/lib/mol-model/custom-property'; 5 | import { ElementIndex, Structure } from 'molstar/lib/mol-model/structure'; 6 | import { Color } from 'molstar/lib/mol-util/color'; 7 | import { ColorNames } from 'molstar/lib/mol-util/color/names'; 8 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition'; 9 | 10 | 11 | export namespace SequenceColorAnnotationsProperty { 12 | /** Provider name (key) for this custom property */ 13 | export const Name = 'sequence-color-annotations'; 14 | 15 | /** Parameter definition for this custom property */ 16 | export type Params = typeof Params; 17 | export const Params = { 18 | colors: PD.ObjectList( 19 | { 20 | color: PD.Color(ColorNames.grey, { description: 'Color to apply to a substructure' }), 21 | selector: SelectorParams, 22 | }, 23 | obj => Color.toHexStyle(obj.color), 24 | { description: 'List of substructure-color assignments' } 25 | ), 26 | }; 27 | 28 | /** Type of parameter values for this custom property */ 29 | export type Props = PD.Values; 30 | 31 | /** Type of values of this custom property */ 32 | export interface Data { 33 | items: { 34 | selector: Selector, 35 | color: Color, 36 | elementSet?: ElementSet, 37 | }[], 38 | colorCache: { 39 | [unitId: number]: { 40 | [elemIdx: ElementIndex]: Color, 41 | }, 42 | }, 43 | } 44 | 45 | /** Provider for this custom property */ 46 | export const Provider: CustomStructureProperty.Provider = CustomStructureProperty.createProvider({ 47 | label: 'Sequence Color Annotations', 48 | descriptor: CustomPropertyDescriptor({ 49 | name: Name, 50 | }), 51 | type: 'root', 52 | defaultParams: Params, 53 | getParams: (data: Structure) => Params, 54 | isApplicable: (data: Structure) => data.root === data, 55 | obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial) => { 56 | const fullProps = { ...PD.getDefaultValues(Params), ...props }; 57 | const items = fullProps.colors.map(t => ({ 58 | // creating a copy, so we don't polute props later 59 | selector: t.selector, 60 | color: t.color, 61 | } satisfies Data['items'][number])); 62 | return { value: { items, colorCache: {} } } satisfies CustomProperty.Data; 63 | }, 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/app/sifts-mappings-behaviour.ts: -------------------------------------------------------------------------------- 1 | import { OrderedSet } from 'molstar/lib/mol-data/int'; 2 | import { SIFTSMappingColorThemeProvider } from 'molstar/lib/mol-model-props/sequence/themes/sifts-mapping'; 3 | import { Loci } from 'molstar/lib/mol-model/loci'; 4 | import { StructureElement } from 'molstar/lib/mol-model/structure'; 5 | import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior'; 6 | import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition'; 7 | import { SIFTSMapping as BestDatabaseSequenceMappingProp } from './sifts-mapping'; 8 | 9 | 10 | export const PDBeSIFTSMapping = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({ 11 | name: 'pdbe-sifts-mapping-prop', 12 | category: 'custom-props', 13 | display: { name: 'PDBe SIFTS Mapping' }, 14 | ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> { 15 | private provider = BestDatabaseSequenceMappingProp.Provider; 16 | 17 | private labelProvider = { 18 | label: (loci: Loci): string | undefined => { 19 | if (!this.params.showTooltip) return; 20 | return PDBeBestDatabaseSequenceMappingLabel(loci); 21 | }, 22 | }; 23 | 24 | update(p: { autoAttach: boolean, showTooltip: boolean }) { 25 | const updated = ( 26 | this.params.autoAttach !== p.autoAttach || 27 | this.params.showTooltip !== p.showTooltip 28 | ); 29 | this.params.autoAttach = p.autoAttach; 30 | this.params.showTooltip = p.showTooltip; 31 | this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach); 32 | return updated; 33 | } 34 | 35 | register(): void { 36 | this.ctx.customModelProperties.register(this.provider, this.params.autoAttach); 37 | this.ctx.representation.structure.themes.colorThemeRegistry.add(SIFTSMappingColorThemeProvider); 38 | this.ctx.managers.lociLabels.addProvider(this.labelProvider); 39 | } 40 | 41 | unregister() { 42 | this.ctx.customModelProperties.unregister(this.provider.descriptor.name); 43 | this.ctx.representation.structure.themes.colorThemeRegistry.remove(SIFTSMappingColorThemeProvider); 44 | this.ctx.managers.lociLabels.removeProvider(this.labelProvider); 45 | } 46 | }, 47 | params: () => ({ 48 | autoAttach: PD.Boolean(true), 49 | showTooltip: PD.Boolean(true), 50 | }), 51 | }); 52 | 53 | 54 | function PDBeBestDatabaseSequenceMappingLabel(loci: Loci): string | undefined { 55 | if (loci.kind === 'element-loci') { 56 | if (loci.elements.length === 0) return; 57 | 58 | const e = loci.elements[0]; 59 | const u = e.unit; 60 | const se = StructureElement.Location.create(loci.structure, u, u.elements[OrderedSet.getAt(e.indices, 0)]); 61 | return BestDatabaseSequenceMappingProp.getLabel(se); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdbe-molstar", 3 | "version": "3.9.0", 4 | "description": "Molstar implementation for PDBe", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\"", 8 | "lint": "eslint src", 9 | "build": "npm run build-tsc && npm run build-extra && npm run build-webpack && node scripts.js clean-rubbish && npm run bundle-webcomponent && node scripts.js add-banners", 10 | "rebuild": "npm run clean && npm run build", 11 | "clean": "node scripts.js clean-all", 12 | "build-tsc": "tsc --incremental", 13 | "build-extra": "cpx 'src/app/**/*.{scss,html,ico}' lib/", 14 | "build-webpack": "webpack --mode production --config ./webpack.config.production.js", 15 | "watch": "concurrently -c 'green,gray,blue' --names 'tsc,ext,wpc' --kill-others 'npm:watch-tsc' 'npm:watch-extra' 'npm:watch-webpack'", 16 | "watch-tsc": "tsc --watch --incremental", 17 | "watch-extra": "cpx 'src/app/**/*.{scss,html,ico}' lib/ --watch", 18 | "watch-webpack": "webpack -w --mode development --stats minimal --config ./webpack.config.development.js", 19 | "serve": "http-server -p 1339 -g -c-1", 20 | "bundle-webcomponent": "node scripts.js bundle-webcomponent" 21 | }, 22 | "files": [ 23 | "lib/", 24 | "build/" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/PDBeurope/pdbe-molstar.git" 29 | }, 30 | "keywords": [ 31 | "Molstar", 32 | "3D viewer", 33 | "PDBe", 34 | "biojs" 35 | ], 36 | "author": "Protein Data Bank in Europe (PDBe), European Bioinformatics Institute (EMBL-EBI)", 37 | "contributors": [ 38 | "Mandar Deshpande ", 39 | "Adam Midlik " 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

83 |
{message}
84 |
; 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 |
163 |

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