From 90dba3a5ebc1acf2c627e6baa63a7337cf7b14eb Mon Sep 17 00:00:00 2001 From: "Gustavo A. Salazar" Date: Mon, 8 Feb 2021 12:09:30 +0000 Subject: [PATCH 01/15] structural model subpage- displaying contacts --- package-lock.json | 50 +++++++++++++++ package.json | 5 +- src/higherOrder/loadData/defaults/index.js | 1 + src/menuConfig.js | 20 ++++++ src/subPages/EntryAlignments/Viewer/index.js | 31 ++++++++++ src/subPages/StructuralModel/index.js | 64 ++++++++++++++++++++ src/subPages/index.js | 5 ++ tsconfig.json | 13 ++++ webpack.config.js | 13 +++- 9 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/subPages/StructuralModel/index.js create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index 9e5f99fa8..32aa30aa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23052,6 +23052,26 @@ "protvista-track": "^3.2.0" } }, + "protvista-links": { + "version": "3.3.0-alpha.1", + "resolved": "https://registry.npmjs.org/protvista-links/-/protvista-links-3.3.0-alpha.1.tgz", + "integrity": "sha512-JeAEVZy0+5VVAt2nY9u/apQj5lNlRv25esSA+WQmh0BMYI5SxiGME3I5YeeiK2cz0uZZOTXB7UmZ0W+bU6XjNw==", + "requires": { + "lodash-es": "^4.17.11", + "protvista-track": "^3.2.1" + }, + "dependencies": { + "protvista-track": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/protvista-track/-/protvista-track-3.2.1.tgz", + "integrity": "sha512-jlaZ3X3dqAn+WqDn3hWIhQYihQGhFEDpYMlyjnyeb2K9k7hUhtXmhDJzexEtOVyJkEEQyrMfTbpJuTkcg7BImA==", + "requires": { + "lodash-es": "^4.17.11", + "protvista-zoomable": "^3.0.0" + } + } + } + }, "protvista-manager": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/protvista-manager/-/protvista-manager-3.0.0.tgz", @@ -27514,6 +27534,30 @@ "integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==", "dev": true }, + "ts-loader": { + "version": "8.0.15", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.15.tgz", + "integrity": "sha512-WYXfCEglgUPU6adGcx6I9DsMwSxYFU99rzteIEoZKDQn4IMbe4KpO934zRkwSOFcwEzh+gx/RaH8hhgoCAfF9w==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "ts-pnp": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", @@ -27597,6 +27641,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "dev": true + }, "ua-parser-js": { "version": "0.7.22", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.22.tgz", diff --git a/package.json b/package.json index 79bc7af82..f9922a95d 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "prop-types": "15.7.2", "protvista-coloured-sequence": "3.0.0", "protvista-interpro-track": "3.2.0", + "protvista-links": "^3.3.0-alpha.1", "protvista-manager": "3.0.0", "protvista-msa": "3.2.0", "protvista-navigation": "3.0.0", @@ -209,6 +210,8 @@ "source-map": "0.7.3", "style-loader": "2.0.0", "to-string-loader": "1.1.6", + "ts-loader": "^8.0.15", + "typescript": "^4.1.3", "url-loader": "4.1.1", "webapp-webpack-plugin": "2.7.1", "webpack": "4.44.2", @@ -217,4 +220,4 @@ "worker-loader": "3.0.7", "yaml-loader": "0.6.0" } -} \ No newline at end of file +} diff --git a/src/higherOrder/loadData/defaults/index.js b/src/higherOrder/loadData/defaults/index.js index af010fd58..5fc3e9be7 100644 --- a/src/higherOrder/loadData/defaults/index.js +++ b/src/higherOrder/loadData/defaults/index.js @@ -197,6 +197,7 @@ export const getUrlForApi = (...parameters) => .replace('/alignments', '/') .replace('/entry_alignments', '/') .replace('/logo', '/') + .replace('/model', '/') .replace('/domain_architecture', '/') .replace('/interactions', '/') .replace('/pathways', '/') diff --git a/src/menuConfig.js b/src/menuConfig.js index 4f04688b3..95a70b8ef 100644 --- a/src/menuConfig.js +++ b/src/menuConfig.js @@ -410,6 +410,26 @@ export const singleEntity /*: Map */ = new Map([ name: 'Signature', }, ], + [ + 'model', + { + to(customLocation) { + const { key } = customLocation.description.main; + return { + description: { + ...getEmptyDescription(), + main: { key }, + [key]: { + ...customLocation.description[key], + detail: 'model', + }, + }, + }; + }, + name: 'Structural Model', + counter: 'structural_models', + }, + ], [ 'entry_alignments', { diff --git a/src/subPages/EntryAlignments/Viewer/index.js b/src/subPages/EntryAlignments/Viewer/index.js index e127cefb9..067ff272d 100644 --- a/src/subPages/EntryAlignments/Viewer/index.js +++ b/src/subPages/EntryAlignments/Viewer/index.js @@ -14,6 +14,7 @@ import ProtVistaMSA from 'protvista-msa'; import ProtVistaManager from 'protvista-manager'; import ProtVistaNavigation from 'protvista-navigation'; import ProtvistaZoomTool from 'protvista-zoom-tool'; +import ProtvistaLinks from 'protvista-links'; import { foundationPartial } from 'styles/foundation'; @@ -37,6 +38,9 @@ const loadProtVistaWebComponents = () => { webComponents.push( loadWebComponent(() => ProtvistaZoomTool).as('protvista-zoom-tool'), ); + webComponents.push( + loadWebComponent(() => ProtvistaLinks).as('protvista-links'), + ); } return Promise.all(webComponents); }; @@ -49,8 +53,11 @@ const AlignmentViewer = ({ onConservationProgress, setColorMap, overlayConservation, + contacts = null, + contactThreshold = 0.9, }) => { const msaTrack = useRef(null); + const linksTrack = useRef(null); const [align, setAlign] = useState(null); useEffect(() => { (async () => await loadProtVistaWebComponents())(); @@ -73,6 +80,9 @@ const AlignmentViewer = ({ const { map } = msaTrack.current.getColorMap(); setColorMap(map || {}); }); + if (contacts && linksTrack.current) { + linksTrack.current.data = contacts; + } } }, [align]); @@ -128,6 +138,25 @@ const AlignmentViewer = ({ + {contacts && ( +
+
+ Contacts +
+ +
+ )} { + if (!data || data.loading || !data.payload) return null; + return ( +
+

Predicted Model

+ +

SEED alignment

+

+ The model above was predicted by estimating the likelyhood of contacts + between the residues in the alignment of the Pfam entry, The + visualization below shows the contacts with higher probability. +

+ null} + onConservationProgress={() => null} + type="alignment:seed" + colorscheme="clustal2" + contacts={data.payload} + contactThreshold={0.99} + /> +
+ ); +}; + +const mapStateToPropsForContacts = createSelector( + (state) => state.settings.api, + (state) => state.customLocation.description, + ({ protocol, hostname, port, root }, description) => { + const newDescription = { + main: { key: 'entry' }, + entry: { + db: description.entry.db || 'pfam', + accession: description.entry.accession, + }, + }; + return format({ + protocol, + hostname, + port, + pathname: root + descriptionToPath(newDescription), + query: { 'model:contacts': null }, + }); + }, +); + +export default loadData({ + getUrl: mapStateToPropsForContacts, +})(StructuralModel); diff --git a/src/subPages/index.js b/src/subPages/index.js index bd5c77168..ba081f28a 100644 --- a/src/subPages/index.js +++ b/src/subPages/index.js @@ -62,6 +62,10 @@ const Genome3d = loadable({ const Curation = loadable({ loader: () => import(/* webpackChunkName: "curation-subpage" */ './Curation'), }); +const StructuralModel = loadable({ + loader: () => + import(/* webpackChunkName: "model-subpage" */ './StructuralModel'), +}); const defaultMapStateToProps = createSelector( (state) => state.settings.api, (state) => state.settings.navigation.pageSize, @@ -208,6 +212,7 @@ const subPages = new Map([ loadData(getInterProModifierURL('interactions'))(InteractionsSubPage), ], ['pathways', loadData(getInterProModifierURL('pathways'))(PathwaysSubPage)], + ['model', StructuralModel], ['alignments', SetAlignments], ['entry_alignments', EntryAlignments], ['logo', loadData(mapStateToPropsForHMMModel)(HMMModel)], diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..d08ffdbaa --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "sourceMap": true, + "moduleResolution": "Node", + "module": "CommonJS", + "target": "ES6", + "declaration": false + }, + "include": [ + "src/**/*", + "node_modules/protvista-links/src/*" + ], +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 3d9b80922..113a2bdf3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -87,7 +87,7 @@ const getConfigFor = (env, mode, module = false) => { // RESOLVE resolve: { modules: [path.resolve('.', 'src'), 'node_modules'], - extensions: ['.js', '.json', '.worker.js'], + extensions: ['.js', '.ts', '.json', '.worker.js'], alias: { '../libraries': 'ebi-framework/libraries', 'EBI-Common': 'EBI-Icon-fonts/EBI-Common', @@ -181,6 +181,17 @@ const getConfigFor = (env, mode, module = false) => { }, ], }, + { + test: /\.ts$/, + use: [ + { + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + }, + ], + }, { test: /\.(txt|fast[aq])/i, use: [ From 8a4f6d55ff6063ee2d8f4705032cbdf9d594247c Mon Sep 17 00:00:00 2001 From: "Gustavo A. Salazar" Date: Tue, 9 Feb 2021 17:29:13 +0000 Subject: [PATCH 02/15] extracting the PIP component out of the structure viewer --- .../PictureInPicturePanel/index.js | 101 +++++++++ .../PictureInPicturePanel/style.css | 65 ++++++ src/components/Structure/Viewer/index.js | 209 ++++++------------ src/components/Structure/Viewer/style.css | 96 ++------ src/subPages/EntryAlignments/Viewer/index.js | 2 +- 5 files changed, 259 insertions(+), 214 deletions(-) create mode 100644 src/components/SimpleCommonComponents/PictureInPicturePanel/index.js create mode 100644 src/components/SimpleCommonComponents/PictureInPicturePanel/style.css diff --git a/src/components/SimpleCommonComponents/PictureInPicturePanel/index.js b/src/components/SimpleCommonComponents/PictureInPicturePanel/index.js new file mode 100644 index 000000000..ec68e0aca --- /dev/null +++ b/src/components/SimpleCommonComponents/PictureInPicturePanel/index.js @@ -0,0 +1,101 @@ +// @flow +import React, { useState, useEffect, useRef } from 'react'; +import T from 'prop-types'; + +import { intersectionObserver as intersectionObserverPolyfill } from 'utils/polyfills'; + +import { foundationPartial } from 'styles/foundation'; +import style from './style.css'; +import fonts from 'EBI-Icon-fonts/fonts.css'; + +const f = foundationPartial(style, fonts); + +const NUMBER_OF_CHECKS = 10; +const optionsForObserver = { + root: null, + rootMargin: '0px', + /* eslint-disable-next-line prefer-spread */ + threshold: Array.apply(null, { length: NUMBER_OF_CHECKS }).map( + // $FlowFixMe + Number.call, + (n) => (n + 1) / NUMBER_OF_CHECKS, + ), +}; + +const PictureInPicturePanel = ({ + className, + testId, + hideBar = false, + OtherControls = null, + OtherButtons = null, + onChangingMode = () => null, + children, +}) => { + const [isStuck, setStuck] = useState(false); + const [isMinimized, setMinimized] = useState(false); + const wrapperRef /*: { current: null | HTMLElement } */ = useRef(null); + let observer = null; + const threshold = 0.4; + useEffect(() => { + const asynLoadPolyfill = async () => await intersectionObserverPolyfill(); + asynLoadPolyfill(); + }, []); + useEffect(() => { + if (wrapperRef?.current) { + observer = new IntersectionObserver((entries) => { + setStuck( + ((wrapperRef?.current?.getBoundingClientRect() /*: any */)?.y || 0) < + 0 && entries[0].intersectionRatio < threshold, + ); + onChangingMode(); + }, optionsForObserver); + observer.observe(wrapperRef.current); + } + return () => { + if (observer) observer.disconnect(); + }; + }, [wrapperRef]); + return ( +
+
+ {children} +
+ {OtherControls} +
+ {OtherButtons} + {isStuck && ( +
+
+
+
+ ); +}; + +PictureInPicturePanel.propTypes = { + className: T.string, + testId: T.string, + hideBar: T.bool, + OtherControls: T.any, + OtherButtons: T.any, + onChangingMode: T.function, + children: T.any, +}; + +export default PictureInPicturePanel; diff --git a/src/components/SimpleCommonComponents/PictureInPicturePanel/style.css b/src/components/SimpleCommonComponents/PictureInPicturePanel/style.css new file mode 100644 index 000000000..dd225143a --- /dev/null +++ b/src/components/SimpleCommonComponents/PictureInPicturePanel/style.css @@ -0,0 +1,65 @@ +@import '../../../styles/timing.css'; +@import '../../../styles/colors.css'; +@import '../../../styles/z-index.css'; + +:root { + --height-viewer: 50vh; + --height-bar: 2.3em; +} + +.wrapper { + height: var(--height-viewer); + width: 100%; + + & .control-bar { + width: auto; + height: var(--height-bar); + margin-top: calc(0 - var(--height-bar)); + display: flex; + justify-content: space-between; + background: white; + + & .controls { + white-space: nowrap; + overflow: visible; + flex-grow: 1; + display: flex; + justify-content: flex-end; + & button { + font-size: 1.5em; + margin: 4px; + } + } + } + & .is-stuck { + position: fixed; + z-index: var(--z-index-over-main); + right: 10px; + top: auto; + bottom: 10px; + -webkit-transform: none; + -ms-transform: none; + transform: none; + border-radius: 0; + display: block; + -webkit-box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.3); + box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.3); + width: 40%; + height: calc(var(--height-viewer) - var(--height-bar)); + + & .structure-viewer-select { + left: 1rem; + } + + &.structure-icon { + font-size: 1em; + } + + &.is-minimized { + height: var(--height-bar); + & .structure-viewer-ref { + display: none; + } + } + } +} diff --git a/src/components/Structure/Viewer/index.js b/src/components/Structure/Viewer/index.js index 4e57d326f..f654f45ea 100644 --- a/src/components/Structure/Viewer/index.js +++ b/src/components/Structure/Viewer/index.js @@ -11,10 +11,9 @@ import EntrySelection from './EntrySelection'; import { NO_SELECTION } from './EntrySelection'; import { EntryColorMode, getTrackColor } from 'utils/entry-color'; -import { intersectionObserver as intersectionObserverPolyfill } from 'utils/polyfills'; - import ProtVistaForStructure from './ProtVistaForStructures'; import FullScreenButton from 'components/SimpleCommonComponents/FullScreenButton'; +import PictureInPicturePanel from 'components/SimpleCommonComponents/PictureInPicturePanel'; import getMapper from './proteinToStructureMapper'; @@ -39,36 +38,19 @@ const f = foundationPartial(style, fonts); entryMap: Object, selectedEntry: string, selectedEntryToKeep: ?Object, - isStuck: boolean, isSpinning: boolean, - isStructureFullScreen: boolean, isSplitScreen: boolean, - isMinimized: boolean, }; */ -const NUMBER_OF_CHECKS = 10; -const optionsForObserver = { - root: null, - rootMargin: '0px', - /* eslint-disable-next-line prefer-spread */ - threshold: Array.apply(null, { length: NUMBER_OF_CHECKS }).map( - // $FlowFixMe - Number.call, - (n) => (n + 1) / NUMBER_OF_CHECKS, - ), -}; - class StructureView extends PureComponent /*:: */ { /*:: _structureViewer: { current: ?HTMLElement }; */ /*:: stage: Object; */ /*:: _protein2structureMappers: Object; */ /*:: name: Object; */ /*:: _structurevViewer: Object; */ - /*:: _structureSection: Object; */ /*:: _protvista: Object; */ /*:: _splitView: Object; */ /*:: splitViewStyle: Object; */ - /*:: observer: IntersectionObserver; */ /*:: handlingSequenceHighlight: bool; */ static propTypes = { @@ -86,11 +68,8 @@ class StructureView extends PureComponent /*:: */ { entryMap: {}, selectedEntry: '', selectedEntryToKeep: null, - isStuck: false, isSpinning: false, - isStructureFullScreen: false, isSplitScreen: false, - isMinimized: false, }; this.stage = null; @@ -99,14 +78,11 @@ class StructureView extends PureComponent /*:: */ { this.name = `${this.props.id}`; this._structurevViewer = React.createRef(); - this._structureSection = React.createRef(); this._protvista = React.createRef(); this._splitView = React.createRef(); this.splitViewStyle = {}; } async componentDidMount() { - await intersectionObserverPolyfill(); - const pdbid = this.props.id; this.stage = new Stage(this._structurevViewer.current); this.stage.setParameters({ backgroundColor: 0xfcfcfc }); @@ -124,18 +100,6 @@ class StructureView extends PureComponent /*:: */ { } }); - const threshold = 0.4; - this.observer = new IntersectionObserver((entries) => { - this.setState({ - isStuck: - this._structureSection.current.getBoundingClientRect().y < 0 && - entries[0].intersectionRatio < threshold, - }); - if (this.stage && this.state.isStuck) { - this.stage.handleResize(); - } - }, optionsForObserver); - this.observer.observe(this._structureSection.current); this._protvista.current.addEventListener( 'change', ({ detail: { eventtype, highlight, feature, chain, protein } }) => { @@ -229,10 +193,6 @@ class StructureView extends PureComponent /*:: */ { } } - componentWillUnmount() { - this.observer.disconnect(); - } - _toggleStructureSpin = () => { if (this.stage) { const isSpinning = !this.state.isSpinning; @@ -468,9 +428,6 @@ class StructureView extends PureComponent /*:: */ { } }; - _toggleMinimize = () => - this.setState({ isMinimized: !this.state.isMinimized }); - showRegionInStructure(chain, start, stop) { const components = this.stage.getComponentsByName(this.name); if (components) { @@ -491,110 +448,82 @@ class StructureView extends PureComponent /*:: */ { } } render() { - const { - isStuck, - entryMap, - selectedEntry, - isSpinning, - isSplitScreen, - isMinimized, - isStructureFullScreen, - } = this.state; + const { entryMap, selectedEntry, isSpinning, isSplitScreen } = this.state; return ( <>
-
-
{ + if (this.stage) this.stage.handleResize(); + }} + OtherControls={ + this.props.matches ? ( + + ) : null + } + OtherButtons={ + <> +
-
-
- + ); + }} + +
div { + width: 50vw; + } & .protvista-container { overflow: scroll; - width: 50vw; } - & .structure-wrapper { - width: 50vw; + & .structure-viewer { height: initial; & .structure-viewer-ref { height: 95vh; diff --git a/src/subPages/EntryAlignments/Viewer/index.js b/src/subPages/EntryAlignments/Viewer/index.js index 067ff272d..fd2ba6a52 100644 --- a/src/subPages/EntryAlignments/Viewer/index.js +++ b/src/subPages/EntryAlignments/Viewer/index.js @@ -154,7 +154,7 @@ const AlignmentViewer = ({ length={length} ref={linksTrack} threshold={contactThreshold} - > + />
)} Date: Wed, 10 Feb 2021 16:52:01 +0000 Subject: [PATCH 03/15] separating the structure viewer --- .../FullScreenButton/index.js | 4 +- .../PictureInPicturePanel/index.js | 2 +- .../Summary/__snapshots__/test.js.snap | 2 +- src/components/Structure/Summary/index.js | 2 +- src/components/Structure/Viewer/index.js | 560 +++--------------- src/components/Structure/Viewer/style.css | 56 +- .../EntrySelection.js | 2 +- .../ProtVistaForStructures.js | 0 .../Structure/ViewerAndEntries/index.js | 456 ++++++++++++++ .../proteinToStructureMapper.js | 2 +- .../Structure/ViewerAndEntries/style.css | 44 ++ .../{Viewer => ViewerAndEntries}/test.js | 0 .../Structure/ViewerOnDemand/index.js | 16 +- 13 files changed, 593 insertions(+), 553 deletions(-) rename src/components/Structure/{Viewer => ViewerAndEntries}/EntrySelection.js (98%) rename src/components/Structure/{Viewer => ViewerAndEntries}/ProtVistaForStructures.js (100%) create mode 100644 src/components/Structure/ViewerAndEntries/index.js rename src/components/Structure/{Viewer => ViewerAndEntries}/proteinToStructureMapper.js (94%) create mode 100644 src/components/Structure/ViewerAndEntries/style.css rename src/components/Structure/{Viewer => ViewerAndEntries}/test.js (100%) diff --git a/src/components/SimpleCommonComponents/FullScreenButton/index.js b/src/components/SimpleCommonComponents/FullScreenButton/index.js index 3209a58f4..e8d0121d6 100644 --- a/src/components/SimpleCommonComponents/FullScreenButton/index.js +++ b/src/components/SimpleCommonComponents/FullScreenButton/index.js @@ -36,12 +36,14 @@ const FullScreenButton = ( return () => document.removeEventListener('fullscreenchange', onFullscreen); }, []); if (!element) return null; + const elementInDOM = + typeof element === 'string' ? document.getElementById(element) : element; const _handleFullScreen = () => { if (isFull) { exitFullScreen(); onExitFullScreenHook(); } else { - requestFullScreen(element); + requestFullScreen(elementInDOM); onFullScreenHook(); } setFull(!isFull); diff --git a/src/components/SimpleCommonComponents/PictureInPicturePanel/index.js b/src/components/SimpleCommonComponents/PictureInPicturePanel/index.js index ec68e0aca..a5da5f6a2 100644 --- a/src/components/SimpleCommonComponents/PictureInPicturePanel/index.js +++ b/src/components/SimpleCommonComponents/PictureInPicturePanel/index.js @@ -94,7 +94,7 @@ PictureInPicturePanel.propTypes = { hideBar: T.bool, OtherControls: T.any, OtherButtons: T.any, - onChangingMode: T.function, + onChangingMode: T.func, children: T.any, }; diff --git a/src/components/Structure/Summary/__snapshots__/test.js.snap b/src/components/Structure/Summary/__snapshots__/test.js.snap index abb0988dd..e805b2cae 100644 --- a/src/components/Structure/Summary/__snapshots__/test.js.snap +++ b/src/components/Structure/Summary/__snapshots__/test.js.snap @@ -132,7 +132,7 @@ exports[` should render 1`] = `
- , - highlight?: string, - colorDomainsBy: ColorMode -}; */ - -/*:: type State = { - plugin: ?Object, - entryMap: Object, - selectedEntry: string, - selectedEntryToKeep: ?Object, - isSpinning: boolean, - isSplitScreen: boolean, + elementId: string, + onStructureLoaded?: function, + isSpinning?: boolean, + shouldResetViewer?: boolean, + selections: Array>, }; */ -class StructureView extends PureComponent /*:: */ { +class StructureView extends PureComponent /*:: */ { /*:: _structureViewer: { current: ?HTMLElement }; */ /*:: stage: Object; */ - /*:: _protein2structureMappers: Object; */ /*:: name: Object; */ - /*:: _structurevViewer: Object; */ - /*:: _protvista: Object; */ - /*:: _splitView: Object; */ - /*:: splitViewStyle: Object; */ - /*:: handlingSequenceHighlight: bool; */ static propTypes = { id: T.oneOfType([T.string, T.number]).isRequired, - matches: T.array, - highlight: T.string, - colorDomainsBy: T.string, + elementId: T.string, + onStructureLoaded: T.func, + isSpinning: T.bool, + shouldResetViewer: T.bool, + selections: T.array, }; constructor(props /*: Props */) { super(props); - this.state = { - plugin: null, - entryMap: {}, - selectedEntry: '', - selectedEntryToKeep: null, - isSpinning: false, - isSplitScreen: false, - }; - this.stage = null; - - this._protein2structureMappers = {}; this.name = `${this.props.id}`; - - this._structurevViewer = React.createRef(); - this._protvista = React.createRef(); - this._splitView = React.createRef(); - this.splitViewStyle = {}; + this._structureViewer = React.createRef(); } + async componentDidMount() { - const pdbid = this.props.id; - this.stage = new Stage(this._structurevViewer.current); + this.stage = new Stage(this._structureViewer.current); this.stage.setParameters({ backgroundColor: 0xfcfcfc }); - this.stage - .loadFile(`rcsb://${this.name}.mmtf`, { defaultRepresentation: false }) - .then((component) => { - component.addRepresentation('cartoon', { colorScheme: 'chainname' }); - component.autoView(); - }) - .then(() => { - this.stage.handleResize(); - if (this.props.matches) { - const entryMap = this.createEntryMap(); - this.setState({ entryMap }); - } - }); - - this._protvista.current.addEventListener( - 'change', - ({ detail: { eventtype, highlight, feature, chain, protein } }) => { - const { - accession, - source_database: sourceDB, - type, - chain: chainF, - protein: proteinF, - parent, - } = feature || {}; - let proteinD = proteinF; - - switch (eventtype) { - case 'sequence-chain': - if (highlight) { - const [start, stop] = highlight.split(':'); - const p2s = this._protein2structureMappers[ - `${protein}->${chain}`.toUpperCase() - ]; - this.showRegionInStructure( - chain, - Math.round(p2s(start)), - Math.round(p2s(stop)), - ); - this.handlingSequenceHighlight = true; - } else this.showRegionInStructure(); - break; - case 'click': - // bit of a hack to handle missing data in some entries - if (!proteinD && parent) { - proteinD = parent.protein; - } - this.setState({ - selectedEntryToKeep: - type === 'chain' - ? { - accession: pdbid, - db: 'pdb', - chain: accession, - protein: proteinD, - } - : { - accession: accession, - db: sourceDB, - chain: chainF, - protein: proteinD, - }, - }); - break; - case 'mouseover': - if (this.handlingSequenceHighlight) { - this.handlingSequenceHighlight = false; - return; - } - if (type === 'chain') - this.showEntryInStructure('pdb', pdbid, accession, protein); - else if (type === 'secondary_structure') - this.showSecondaryStructureEntries(feature); - else if (!accession.startsWith('G3D:')) - // TODO: Needs refactoring - this.showEntryInStructure(sourceDB, accession, chainF, proteinF); - break; - case 'mouseout': - if (type !== 'secondary_structure') this.showEntryInStructure(); - break; - default: - break; - } - }, - ); + this.loadURLInStage(`rcsb://${this.name}.mmtf`); } componentDidUpdate() { if (this.name !== `${this.props.id}`) { this.name = `${this.props.id}`; this.stage.removeAllComponents(); - - this.stage - .loadFile(`rcsb://${this.name}.mmtf`, { defaultRepresentation: false }) - .then((component) => { - component.addRepresentation('cartoon', { colorScheme: 'chainname' }); - component.autoView(); - }) - .then(() => { - this.stage.handleResize(); - if (this.props.matches) { - const entryMap = this.createEntryMap(); - this.setState({ entryMap }); - } - }); - } - } - - _toggleStructureSpin = () => { - if (this.stage) { - const isSpinning = !this.state.isSpinning; - this.stage.setSpin(isSpinning); - this.setState({ isSpinning }); + this.loadURLInStage(`rcsb://${this.name}.mmtf`); } - }; - - _resetStructureView = () => { if (this.stage) { - this.stage.autoView(); - } - }; - - _getChainMap(chain, locations, p2s) { - const chainMap = []; - for (const location of locations) { - for (const { start, end } of location.fragments) { - chainMap.push({ - struct_asym_id: chain, - start_residue_number: p2s(start), - end_residue_number: p2s(end), - accession: chain, - source_database: 'pdb', - }); + this.stage.setSpin(this.props.isSpinning); + if (this.props.shouldResetViewer) { + this.stage.autoView(); } - } - return chainMap; - } - - _mapLocations(map, { chain, protein, locations, entry, db, match }, p2s) { - for (const location of locations) { - for (const fragment of location.fragments) { - map[chain][protein].push({ - struct_asym_id: chain, - start_residue_number: Math.round(p2s(fragment.start)), - end_residue_number: Math.round(p2s(fragment.end)), - accession: entry, - source_database: db, - parent: match.metadata.integrated - ? { accession: match.metadata.integrated } - : null, - }); - } - } - } - - _collateHits(database, accession, chain, protein) { - let hits = []; - if (database && accession) { - if (chain && protein) { - hits = hits.concat( - this.state.entryMap[database][accession][chain][protein], - ); - } else if (chain) { - Object.keys(this.state.entryMap[database][accession][chain]).forEach( - (p) => { - hits = hits.concat( - this.state.entryMap[database][accession][chain][p], - ); - }, - ); + if (this.props.selections?.length) { + this.highlightSelections(this.props.selections); } else { - Object.keys(this.state.entryMap[database][accession]).forEach((c) => { - Object.keys(this.state.entryMap[database][accession][c]).forEach( - (p) => { - hits = hits.concat( - this.state.entryMap[database][accession][c][p], - ); - }, - ); - }); + this.clearSelections(); } } - - hits.forEach( - (hit) => (hit.color = getTrackColor(hit, this.props.colorDomainsBy)), - ); - return hits; } - - createEntryMap() { - const memberDBMap = { pdb: {} }; - - if (this.props.matches) { - // create matches in structure hierarchy - for (const match of this.props.matches) { - const entry = match.metadata.accession; - const db = match.metadata.source_database; - if (!memberDBMap[db]) memberDBMap[db] = {}; - if (!memberDBMap[db][entry]) memberDBMap[db][entry] = {}; - - for (const structure of match.structures) { - const chain = structure.chain; - const protein = structure.protein; - const p2s = getMapper(structure.protein_structure_mapping[chain]); - this._protein2structureMappers[ - `${protein}->${chain}`.toUpperCase() - ] = p2s; - if (!memberDBMap[db][entry][chain]) - memberDBMap[db][entry][chain] = {}; - if (!memberDBMap[db][entry][chain][protein]) - memberDBMap[db][entry][chain][protein] = []; - this._mapLocations( - memberDBMap[db][entry], - { - chain, - protein, - locations: structure.entry_protein_locations, - entry, - db, - match, - }, - p2s, - ); - // create PDB chain mapping - if (!memberDBMap.pdb[structure.accession]) - memberDBMap.pdb[structure.accession] = {}; - if (!memberDBMap.pdb[structure.accession][chain]) { - memberDBMap.pdb[structure.accession][chain] = {}; - } - if (!memberDBMap.pdb[structure.accession][chain][structure.protein]) { - memberDBMap.pdb[structure.accession][chain][ - structure.protein - ] = this._getChainMap( - chain, - structure.structure_protein_locations, - p2s, - ); - } - } - } - } - return memberDBMap; + loadURLInStage(url) { + this.stage + .loadFile(url, { defaultRepresentation: false }) + .then((component) => { + component.addRepresentation('cartoon', { colorScheme: 'chainname' }); + component.autoView(); + }) + .then(() => { + this.stage.handleResize(); + if (this.props?.onStructureLoaded) this.props?.onStructureLoaded(); + }); } - // eslint-disable-next-line complexity - showEntryInStructure = (memberDB, entry, chain, protein) => { - const keep = this.state.selectedEntryToKeep; - let db; - let acc; - let ch; - let prot; - - // reset keep when 'no entry' is selected via selection input - if (entry === NO_SELECTION && keep) { - keep.db = null; - keep.accession = null; - keep.chain = null; - keep.protein = null; - } else if (memberDB !== undefined && entry !== undefined) { - db = memberDB; - acc = entry; - ch = chain; - prot = protein; - } else if ( - keep && - keep.db !== null && - keep.accession !== null && - keep.chain !== null && - keep.protein !== null - ) { - db = keep.db; - acc = keep.accession; - ch = keep.chain; - prot = keep.protein; - } - - if (acc && acc.startsWith('Chain')) return; // Skip the keep procedure for secondary structure - const hits = this._collateHits(db, acc, ch, prot); - if (hits.length > 0) { - if (this.stage) { - const components = this.stage.getComponentsByName(this.name); - if (components) { - components.forEach((component) => { - const selections = []; - hits.forEach((hit) => { - selections.push([ - hit.color, - `${hit.start_residue_number}-${hit.end_residue_number}:${hit.struct_asym_id}`, - ]); - }); - const theme = ColormakerRegistry.addSelectionScheme( - selections, - acc, - ); - component.addRepresentation('cartoon', { color: theme }); - }); - } - } - } else { - // default view when no entry selected - const components = this.stage.getComponentsByName(this.name); - if (components) { - components.forEach((component) => { - component.addRepresentation('cartoon', { colorScheme: 'chainname' }); - }); - } - } - this.setState({ selectedEntry: acc || '' }); - }; - - showSecondaryStructureEntries = (feature) => { - const hits = []; - if (feature.locations) { - for (const loc of feature.locations) { - for (const frag of loc.fragments) { - hits.push({ color: feature.color, start: frag.start, end: frag.end }); - } - } - } - - if (hits.length > 0) { - if (this.stage) { - const components = this.stage.getComponentsByName(this.name); - if (components) { - components.forEach((component) => { - const selections = []; - hits.forEach((hit) => { - selections.push([ - hit.color, - `${hit.start}-${hit.end}:${feature.chain}`, - ]); - }); - const theme = ColormakerRegistry.addSelectionScheme( - selections, - feature.accession, - ); - component.addRepresentation('cartoon', { color: theme }); - }); - } - } + highlightSelections(selections) { + if (!this.stage) return; + const components = this.stage.getComponentsByName(this.name); + if (components) { + components.forEach((component) => { + const theme = ColormakerRegistry.addSelectionScheme( + selections, + 'highlight', + ); + component.addRepresentation('cartoon', { color: theme }); + }); } - }; - - showRegionInStructure(chain, start, stop) { + } + clearSelections() { + if (!this.stage) return; const components = this.stage.getComponentsByName(this.name); if (components) { components.forEach((component) => { - if (chain && start && stop) { - const selection = `${start}-${stop}:${chain}`; - const theme = ColormakerRegistry.addSelectionScheme( - [['red', selection]], - selection, - ); - component.addRepresentation('cartoon', { color: theme }); - } else { - component.addRepresentation('cartoon', { - colorScheme: 'chainname', - }); - } + component.addRepresentation('cartoon', { + colorScheme: 'chainname', + }); }); } } + render() { - const { entryMap, selectedEntry, isSpinning, isSplitScreen } = this.state; return ( - <> -
- { - if (this.stage) this.stage.handleResize(); - }} - OtherControls={ - this.props.matches ? ( - - ) : null - } - OtherButtons={ - <> -