UNPKG

molstar

Version:

A comprehensive macromolecular library.

171 lines (170 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VolsegUI = void 0; const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); /** * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik <midlik@gmail.com> */ const react_1 = require("react"); const base_1 = require("../../mol-plugin-ui/base"); const common_1 = require("../../mol-plugin-ui/controls/common"); const Icons = tslib_1.__importStar(require("../../mol-plugin-ui/controls/icons")); const parameters_1 = require("../../mol-plugin-ui/controls/parameters"); const slider_1 = require("../../mol-plugin-ui/controls/slider"); const use_behavior_1 = require("../../mol-plugin-ui/hooks/use-behavior"); const update_transform_1 = require("../../mol-plugin-ui/state/update-transform"); const mol_util_1 = require("../../mol-util"); const param_definition_1 = require("../../mol-util/param-definition"); const sleep_1 = require("../../mol-util/sleep"); const entry_root_1 = require("./entry-root"); const entry_volume_1 = require("./entry-volume"); const global_state_1 = require("./global-state"); const helpers_1 = require("./helpers"); var VolsegUIData; (function (VolsegUIData) { function changeAvailableNodes(data, newNodes) { var _a; const newActiveNode = newNodes.length > data.availableNodes.length ? newNodes[newNodes.length - 1] : (_a = newNodes.find(node => { var _a; return node.data.ref === ((_a = data.activeNode) === null || _a === void 0 ? void 0 : _a.data.ref); })) !== null && _a !== void 0 ? _a : newNodes[0]; return { ...data, availableNodes: newNodes, activeNode: newActiveNode }; } VolsegUIData.changeAvailableNodes = changeAvailableNodes; function changeActiveNode(data, newActiveRef) { var _a; const newActiveNode = (_a = data.availableNodes.find(node => node.data.ref === newActiveRef)) !== null && _a !== void 0 ? _a : data.availableNodes[0]; return { ...data, availableNodes: data.availableNodes, activeNode: newActiveNode }; } VolsegUIData.changeActiveNode = changeActiveNode; function equals(data1, data2) { return (0, mol_util_1.shallowEqualArrays)(data1.availableNodes, data2.availableNodes) && data1.activeNode === data2.activeNode && data1.globalState === data2.globalState; } VolsegUIData.equals = equals; })(VolsegUIData || (VolsegUIData = {})); class VolsegUI extends base_1.CollapsableControls { defaultState() { return { header: 'Volume & Segmentation', isCollapsed: true, brand: { accent: 'orange', svg: Icons.ExtensionSvg }, data: { globalState: undefined, availableNodes: [], activeNode: undefined, } }; } renderControls() { return (0, jsx_runtime_1.jsx)(VolsegControls, { plugin: this.plugin, data: this.state.data, setData: d => this.setState({ data: d }) }); } componentDidMount() { this.setState({ isHidden: true, isCollapsed: false }); this.subscribe(this.plugin.state.data.events.changed, e => { var _a, _b, _c; const nodes = e.state.selectQ(q => q.ofType(entry_root_1.VolsegEntry)).map(cell => cell === null || cell === void 0 ? void 0 : cell.obj).filter(helpers_1.isDefined); const isHidden = nodes.length === 0; const newData = VolsegUIData.changeAvailableNodes(this.state.data, nodes); if (!((_a = this.state.data.globalState) === null || _a === void 0 ? void 0 : _a.isRegistered())) { const globalState = (_c = (_b = e.state.selectQ(q => q.ofType(global_state_1.VolsegGlobalState))[0]) === null || _b === void 0 ? void 0 : _b.obj) === null || _c === void 0 ? void 0 : _c.data; if (globalState) newData.globalState = globalState; } if (!VolsegUIData.equals(this.state.data, newData) || this.state.isHidden !== isHidden) { this.setState({ data: newData, isHidden: isHidden }); } }); } } exports.VolsegUI = VolsegUI; function VolsegControls({ plugin, data, setData }) { var _a; const entryData = (_a = data.activeNode) === null || _a === void 0 ? void 0 : _a.data; if (!entryData) { return (0, jsx_runtime_1.jsx)("p", { children: "No data!" }); } if (!data.globalState) { return (0, jsx_runtime_1.jsx)("p", { children: "No global state!" }); } const params = { /** Reference to the active VolsegEntry node */ entry: param_definition_1.ParamDefinition.Select(data.activeNode.data.ref, data.availableNodes.map(entry => [entry.data.ref, entry.data.entryId])) }; const values = { entry: data.activeNode.data.ref, }; const globalState = (0, use_behavior_1.useBehavior)(data.globalState.currentState); return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(parameters_1.ParameterControls, { params: params, values: values, onChangeValues: next => setData(VolsegUIData.changeActiveNode(data, next.entry)) }), (0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: 'Global options', children: (0, jsx_runtime_1.jsx)(WaitingParameterControls, { params: global_state_1.VolsegGlobalStateParams, values: globalState, onChangeValues: async (next) => { var _a; return await ((_a = data.globalState) === null || _a === void 0 ? void 0 : _a.updateState(plugin, next)); } }) }), (0, jsx_runtime_1.jsx)(VolsegEntryControls, { entryData: entryData }, entryData.ref)] }); } function VolsegEntryControls({ entryData }) { var _a, _b, _c; const state = (0, use_behavior_1.useBehavior)(entryData.currentState); const allSegments = entryData.metadata.allSegments; const selectedSegment = entryData.metadata.getSegment(state.selectedSegment); const visibleSegments = state.visibleSegments.map(seg => seg.segmentId); const visibleModels = state.visibleModels.map(model => model.pdbId); const allPdbs = entryData.pdbs; return (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { style: { fontWeight: 'bold', padding: 8, paddingTop: 6, paddingBottom: 4, overflow: 'hidden' }, children: (_b = (_a = entryData.metadata.raw.annotation) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'Unnamed Annotation' }), allPdbs.length > 0 && (0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: 'Fitted models in PDB', initiallyExpanded: true, children: allPdbs.map(pdb => (0, jsx_runtime_1.jsx)(WaitingButton, { onClick: () => entryData.actionShowFittedModel(visibleModels.includes(pdb) ? [] : [pdb]), style: { fontWeight: visibleModels.includes(pdb) ? 'bold' : undefined, textAlign: 'left', marginTop: 1 }, children: pdb }, pdb)) }), (0, jsx_runtime_1.jsx)(VolumeControls, { entryData: entryData }), (0, jsx_runtime_1.jsxs)(common_1.ExpandGroup, { header: 'Segmentation data', initiallyExpanded: true, children: [(0, jsx_runtime_1.jsx)(common_1.ControlRow, { label: 'Opacity', control: (0, jsx_runtime_1.jsx)(WaitingSlider, { min: 0, max: 1, value: state.segmentOpacity, step: 0.05, onChange: async (v) => await entryData.actionSetOpacity(v) }) }), allSegments.length > 0 && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(WaitingButton, { onClick: async () => { await (0, sleep_1.sleep)(20); await entryData.actionToggleAllSegments(); }, style: { marginTop: 1 }, children: "Toggle All segments" }), (0, jsx_runtime_1.jsx)("div", { style: { maxHeight: 200, overflow: 'hidden', overflowY: 'auto', marginBlock: 1 }, children: allSegments.map(segment => { var _a, _b; return (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', marginBottom: 1 }, onMouseEnter: () => entryData.actionHighlightSegment(segment), onMouseLeave: () => entryData.actionHighlightSegment(), children: [(0, jsx_runtime_1.jsx)(common_1.Button, { onClick: () => entryData.actionSelectSegment(segment !== selectedSegment ? segment.id : undefined), style: { fontWeight: segment.id === (selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.id) ? 'bold' : undefined, marginRight: 1, flexGrow: 1, textAlign: 'left' }, children: (0, jsx_runtime_1.jsxs)("div", { title: (_a = segment.biological_annotation.name) !== null && _a !== void 0 ? _a : 'Unnamed segment', style: { maxWidth: 240, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: [(_b = segment.biological_annotation.name) !== null && _b !== void 0 ? _b : 'Unnamed segment', " (", segment.id, ")"] }) }), (0, jsx_runtime_1.jsx)(common_1.IconButton, { svg: visibleSegments.includes(segment.id) ? Icons.VisibilityOutlinedSvg : Icons.VisibilityOffOutlinedSvg, onClick: () => entryData.actionToggleSegment(segment.id) })] }, segment.id); }) })] })] }), (0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: 'Selected segment annotation', initiallyExpanded: true, children: (0, jsx_runtime_1.jsxs)("div", { style: { paddingTop: 4, paddingRight: 8, maxHeight: 300, overflow: 'hidden', overflowY: 'auto' }, children: [!selectedSegment && 'No segment selected', selectedSegment && (0, jsx_runtime_1.jsxs)("b", { children: ["Segment ", selectedSegment.id, ":", (0, jsx_runtime_1.jsx)("br", {}), (_c = selectedSegment.biological_annotation.name) !== null && _c !== void 0 ? _c : 'Unnamed segment'] }), selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.biological_annotation.external_references.map(ref => (0, jsx_runtime_1.jsxs)("p", { style: { marginTop: 4 }, children: [(0, jsx_runtime_1.jsxs)("small", { children: [ref.resource, ":", ref.accession] }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("b", { children: capitalize(ref.label) }), (0, jsx_runtime_1.jsx)("br", {}), ref.description] }, ref.id))] }) })] }); } function VolumeControls({ entryData }) { var _a, _b; const vol = (0, use_behavior_1.useBehavior)(entryData.currentVolume); if (!vol) return null; const volumeValues = { volumeType: vol.state.isHidden ? 'off' : (_a = vol.params) === null || _a === void 0 ? void 0 : _a.type.name, opacity: (_b = vol.params) === null || _b === void 0 ? void 0 : _b.type.params.alpha, }; return (0, jsx_runtime_1.jsxs)(common_1.ExpandGroup, { header: 'Volume data', initiallyExpanded: true, children: [(0, jsx_runtime_1.jsx)(WaitingParameterControls, { params: entry_volume_1.SimpleVolumeParams, values: volumeValues, onChangeValues: async (next) => { await (0, sleep_1.sleep)(20); await entryData.actionUpdateVolumeVisual(next); } }), (0, jsx_runtime_1.jsx)(common_1.ExpandGroup, { header: 'Detailed Volume Params', headerStyle: { marginTop: 1 }, children: (0, jsx_runtime_1.jsx)(update_transform_1.UpdateTransformControl, { state: entryData.plugin.state.data, transform: vol, customHeader: 'none' }) })] }); } function WaitingSlider({ value, onChange, ...etc }) { const [changing, sliderValue, execute] = useAsyncChange(value); return (0, jsx_runtime_1.jsx)(slider_1.Slider, { value: sliderValue, disabled: changing, onChange: newValue => execute(onChange, newValue), ...etc }); } function WaitingButton({ onClick, ...etc }) { const [changing, _, execute] = useAsyncChange(undefined); return (0, jsx_runtime_1.jsx)(common_1.Button, { disabled: changing, onClick: () => execute(onClick, undefined), ...etc, children: etc.children }); } function WaitingParameterControls({ values, onChangeValues, ...etc }) { const [changing, currentValues, execute] = useAsyncChange(values); return (0, jsx_runtime_1.jsx)(parameters_1.ParameterControls, { isDisabled: changing, values: currentValues, onChangeValues: newValue => execute(onChangeValues, newValue), ...etc }); } function capitalize(text) { const first = text.charAt(0); const rest = text.slice(1); return first.toUpperCase() + rest; } function useAsyncChange(initialValue) { const [isExecuting, setIsExecuting] = (0, react_1.useState)(false); const [value, setValue] = (0, react_1.useState)(initialValue); const isMounted = (0, react_1.useRef)(false); (0, react_1.useEffect)(() => setValue(initialValue), [initialValue]); (0, react_1.useEffect)(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); const execute = (0, react_1.useCallback)(async (func, val) => { setIsExecuting(true); setValue(val); try { await func(val); } catch (err) { if (isMounted.current) { setValue(initialValue); } throw err; } finally { if (isMounted.current) { setIsExecuting(false); } } }, []); return [isExecuting, value, execute]; }