Source

components/MoorhenContainer.tsx

import { useEffect, useCallback, useRef, useMemo } from 'react';
import { Container, Col, Row, Spinner } from 'react-bootstrap';
import { MoorhenWebMG } from './webMG/MoorhenWebMG';
import { createLocalStorageInstance, parseAtomInfoLabel } from '../utils/utils';
import { MoorhenCommandCentre } from "../utils/MoorhenCommandCentre";
import { MoorhenTimeCapsule } from '../utils/MoorhenTimeCapsule';
import { Backdrop } from "@mui/material";
import { isDarkBackground } from '../WebGLgComponents/mgWebGL'
import { MoorhenNavBar } from "./navbar-menus/MoorhenNavBar"
import { MoorhenModalsContainer } from './misc/MoorhenModalsContainer';
import { moorhen } from '../types/moorhen';
import { webGL } from '../types/mgWebGL';
import { MoorhenPreferencesContainer } from './misc/MoorhenPreferencesContainer';
import { useSelector, useDispatch } from 'react-redux';
import { setDefaultBackgroundColor, setBackgroundColor, setHeight, setIsDark, setWidth } from '../store/sceneSettingsSlice';
import { setCootInitialized, setTheme, toggleCootCommandExit, toggleCootCommandStart } from '../store/generalStatesSlice';
import { setEnableAtomHovering, setHoveredAtom } from '../store/hoveringStatesSlice';
import { setRefinementSelection } from '../store/refinementSettingsSlice';
import { MoorhenSnackBarManager } from '../components/snack-bar/MoorhenSnackBarManager';
import MoorhenReduxStore from '../store/MoorhenReduxStore';
import { SnackbarProvider } from 'notistack';
import { MoorhenGoToResidueSnackbar } from './snack-bar/MoorhenGoToResidueSnackbar';
import { MoorhenRecordingSnackBar } from './snack-bar/MoorhenRecordingSnackBar'
import { MoorhenResidueSelectionSnackBar } from './snack-bar/MoorhenResidueSelectionSnackBar';
import { MoorhenAcceptRejectDragAtomsSnackBar } from './snack-bar/MoorhenAcceptRejectDragAtomsSnackBar';
import { MoorhenAcceptRejectRotateTranslateSnackBar } from './snack-bar/MoorhenAcceptRejectRotateTranslateSnackBar';
import { MoorhenAcceptRejectMatchingLigandSnackBar } from './snack-bar/MoorhenAcceptRejectMatchingLigandSnackBar';
import { MoorhenLongJobSnackBar } from './snack-bar/MoorhenLongJobSnackBar';
import { MoorhenResidueStepsSnackBar } from './snack-bar/MoorhenResidueStepsSnackBar';
import { MoorhenUpdatingMapsManager, MoorhenUpdatingMapsSnackBar } from './snack-bar/MoorhenUpdatingMapsSnackBar';
import { MoorhenModelTrajectorySnackBar } from './snack-bar/MoorhenModelTrajectorySnackBar';
import { MoorhenTomogramSnackBar } from './snack-bar/MoorhenTomogramSnackBar';
import { MoorhenMapContourLevelSnackBar } from './snack-bar/MoorhenMapContourLevelSnackBar';
import { MoorhenRotamerChangeSnackBar } from './snack-bar/MoorhenRotamerChangeSnackbar';
import { MoorhenScreenshotSnackBar } from './snack-bar/MoorhenScreenshotSnackBar';
import { MoorhenSideBar } from './snack-bar/MoorhenSideBar';

declare module "notistack" {
    interface VariantOverrides {
        goToResidue: {
            glRef: React.RefObject<webGL.MGWebGL>;
            commandCentre: React.RefObject<moorhen.CommandCentre>;

        };
        screenRecorder: {
            videoRecorderRef: React.RefObject<moorhen.ScreenRecorder>;
        };
        residueSelection: true;
        acceptRejectDraggingAtoms: {
            commandCentre: React.RefObject<moorhen.CommandCentre>;
            moleculeRef: React.RefObject<moorhen.Molecule>;
            cidRef: React.RefObject<string[]>;
            glRef: React.RefObject<webGL.MGWebGL>;
            monomerLibraryPath: string;
        },
        acceptRejectRotateTranslateAtoms: {
            moleculeRef: React.RefObject<moorhen.Molecule>;
            cidRef: React.RefObject<string>;
            glRef: React.RefObject<webGL.MGWebGL>;    
        };
        acceptRejectMatchingLigand: {
            refMolNo: number;
            movingMolNo: number;
            refLigandCid: string;
            movingLigandCid: string;
            commandCentre: React.RefObject<moorhen.CommandCentre>;
        };
        longJobNotification: true;
        residueSteps: {
            timeCapsuleRef: React.RefObject<moorhen.TimeCapsule>;
            residueList: { cid: string }[];
            onStep: (stepInput: any) => Promise<void>;
            onStart?: () => Promise<void> | void;
            onStop?: () => void;
            onPause?: () => void;
            onResume?: () => void;
            onProgress?: (progress: number) => void;
            disableTimeCapsule?: boolean
            sleepTime?: number;    
        };
        updatingMaps: {
            glRef: React.RefObject<webGL.MGWebGL>;
            commandCentre: React.RefObject<moorhen.CommandCentre>;    
        };
        modelTrajectory: {
            commandCentre: React.RefObject<moorhen.CommandCentre>;
            glRef: React.RefObject<webGL.MGWebGL>;
            moleculeMolNo: number;
            representationStyle: string;
        };
        tomogram: {
            commandCentre: React.RefObject<moorhen.CommandCentre>;
            glRef: React.RefObject<webGL.MGWebGL>;
            mapMolNo: number;
        };
        mapContourLevel: {
            mapMolNo: number;
        };
        rotamerChange: {
            moleculeMolNo: number;
            chosenAtom: moorhen.ResidueSpec;
            commandCentre: React.RefObject<moorhen.CommandCentre>;
            glRef: React.RefObject<webGL.MGWebGL>;
        }
        screenshot: {
            videoRecorderRef: React.RefObject<moorhen.ScreenRecorder>;
            glRef: React.RefObject<webGL.MGWebGL>;    
        };
        sideBar: {
            children: JSX.Element;
            modalId: string;
            title: string;
        }
    }
}

/**
 * A container for the Moorhen app. Needs to be rendered within a MoorhenReduxprovider.
 * @property {React.RefObject<webGL.mgWebGL>} [glRef] - React reference holding the webGL rendering component
 * @property {React.RefObject<moorhen.TimeCapsule>} [timeCapsuleRef] - React reference holding an instance of MoorhenTimeCapsule which is in charge of backups
 * @property {React.RefObject<moorhen.commandCentre>} [commandCentre] - React reference holding an instance of MoorhenCommandCentre which is in charge of communication with libcoot instance
 * @property {React.RefObject<moorhen.Molecule[]>} [moleculesRef] - React reference holding a list of loaded MoorhenMolecule instances
 * @property {React.RefObject<moorhen.Map[]>} [mapsRef] - React reference holding a list of loaded MoorhenMap instances
 * @property {string} [urlPrefix='.'] - The root url used to load sources from public folder
 * @property {string} [monomerLibraryPath='./baby-gru/monomers'] - A string with the path to the monomer library, relative to the root of the app
 * @property {function} setMoorhenDimensions - Callback executed on window resize. Return type is an array of two numbers [width, height]
 * @property {function} onUserPreferencesChange - Callback executed whenever a user-defined preference changes (key: string, value: any) => void.
 * @property {boolean} [disableFileUploads=false] - Indicates if file uploads should b disabled
 * @property {string[]} [includeNavBarMenuNames] - An array of menu names to include in the Moorhen navbar. If empty array then all menus will be included. It can also be used to set their order.
 * @property {object[]} extraNavBarModals - A list with additional draggable modals with buttons rendered under the navigation menu
 * @property {object[]} extraNavBarMenus - A list with additional menu items rendered under the navigation menu
 * @property {JSX.Element[]} extraFileMenuItems - A list with additional menu items rendered under the "File" menu
 * @property {JSX.Element[]} extraEditMenuItems - A list with additional menu items rendered under the "Edit" menu
 * @property {JSX.Element[]} extraCalculateMenuItems - A list with additional menu items rendered under the "Calculate" menu
 * @property {JSX.Element[]} extraDraggableModals - A list with additional draggable modals to be rendered
 * @property {boolean} [viewOnly=false] - Indicates if Moorhen should work in view-only mode
 * @property {boolean} [allowScripting=true] - Indicates if the scrpting interface is enabled
 * @property {moorhen.LocalStorageInstance} backupStorageInstance - An interface used by the moorhen container to store session backups
 * @property {moorhen.AceDRGInstance} aceDRGInstance - An interface used by the moorhen container to execute aceDRG jobs
 * @example
 * import { MoorhenContainer } from "moorhen";
 *
 * const ExampleApp = () => {
 * 
 *  const doClick = (evt) => { console.log('Click!') }
 * 
 *  const exportMenuItem =  <MenuItem key={'example-key'} id='example-menu-item' onClick={doClick}>
 *                              Example extra menu
 *                          </MenuItem>
 *  
 * const setDimensions = () => {
 *   return [window.innerWidth, window.innerHeight]
 * }
 *  
 * return <MoorhenReduxProvider> 
 *              <MoorhenContainer
 *                  allowScripting={false}
 *                  setMoorhenDimensions={setDimensions}
 *                  extraFileMenuItems={[exportMenuItem]}/>
 *          </MoorhenReduxProvider>
 * 
 */
export const MoorhenContainer = (props: moorhen.ContainerProps) => {
    
    const innerGlRef = useRef<null | webGL.MGWebGL>(null)
    const innerVideoRecorderRef = useRef<null | moorhen.ScreenRecorder>(null);
    const innerTimeCapsuleRef = useRef<null | moorhen.TimeCapsule>(null);
    const innnerCommandCentre = useRef<null | moorhen.CommandCentre>(null)
    const innerMoleculesRef = useRef<null | moorhen.Molecule[]>(null)
    const innerMapsRef = useRef<null | moorhen.Map[]>(null)
    const innerActiveMapRef = useRef<null | moorhen.Map>(null)
    const innerlastHoveredAtomRef = useRef<null | moorhen.HoveredAtom>(null)
    
    const dispatch = useDispatch()
    const maps = useSelector((state: moorhen.State) => state.maps)
    const molecules = useSelector((state: moorhen.State) => state.molecules.moleculeList)
    const cursorStyle = useSelector((state: moorhen.State) => state.hoveringStates.cursorStyle)
    const hoveredAtom = useSelector((state: moorhen.State) => state.hoveringStates.hoveredAtom)
    const cootInitialized = useSelector((state: moorhen.State) => state.generalStates.cootInitialized)
    const theme = useSelector((state: moorhen.State) => state.generalStates.theme)
    const backgroundColor = useSelector((state: moorhen.State) => state.sceneSettings.backgroundColor)
    const height = useSelector((state: moorhen.State) => state.sceneSettings.height)
    const width = useSelector((state: moorhen.State) => state.sceneSettings.width)
    const isDark = useSelector((state: moorhen.State) => state.sceneSettings.isDark)
    const userPreferencesMounted = useSelector((state: moorhen.State) => state.generalStates.userPreferencesMounted)
    const drawMissingLoops = useSelector((state: moorhen.State) => state.sceneSettings.drawMissingLoops)
    const defaultMapSamplingRate = useSelector((state: moorhen.State) => state.mapContourSettings.defaultMapSamplingRate)
    const defaultBackgroundColor = useSelector((state: moorhen.State) => state.sceneSettings.defaultBackgroundColor)
    const makeBackups = useSelector((state: moorhen.State) => state.backupSettings.makeBackups)
    const maxBackupCount = useSelector((state: moorhen.State) => state.backupSettings.maxBackupCount)
    const modificationCountBackupThreshold = useSelector((state: moorhen.State) => state.backupSettings.modificationCountBackupThreshold)
    const activeMap = useSelector((state: moorhen.State) => state.generalStates.activeMap)

    const innerStatesMap: moorhen.ContainerRefs = {
        glRef: innerGlRef, timeCapsuleRef: innerTimeCapsuleRef, commandCentre: innnerCommandCentre,
        moleculesRef: innerMoleculesRef, mapsRef: innerMapsRef, activeMapRef: innerActiveMapRef,
        lastHoveredAtomRef: innerlastHoveredAtomRef, videoRecorderRef: innerVideoRecorderRef,
    }

    let refs = {} as moorhen.ContainerRefs
    Object.keys(innerStatesMap).forEach(key => {
        refs[key] = props[key] ? props[key] : innerStatesMap[key]
    })

    const { 
        glRef, timeCapsuleRef, commandCentre, moleculesRef, mapsRef, activeMapRef, videoRecorderRef, lastHoveredAtomRef
    } = refs

    activeMapRef.current = activeMap
    moleculesRef.current = molecules
    mapsRef.current = maps

    const {
        disableFileUploads, urlPrefix, extraNavBarMenus, viewOnly, extraDraggableModals, 
        monomerLibraryPath, extraFileMenuItems, allowScripting, backupStorageInstance,
        extraEditMenuItems, aceDRGInstance, extraCalculateMenuItems, setMoorhenDimensions,
        onUserPreferencesChange, extraNavBarModals, includeNavBarMenuNames, store
    } = props

    const collectedProps: moorhen.CollectedProps = {
        glRef, commandCentre, timeCapsuleRef, disableFileUploads, extraDraggableModals, aceDRGInstance, 
        urlPrefix, viewOnly, mapsRef, allowScripting, extraCalculateMenuItems, extraEditMenuItems,
        extraNavBarMenus, monomerLibraryPath, moleculesRef, extraFileMenuItems, activeMapRef,
        videoRecorderRef, lastHoveredAtomRef, onUserPreferencesChange, extraNavBarModals, store,
        includeNavBarMenuNames
    }
    
    useEffect(() => {
        let head = document.head
        let style: any = document.createElement("link")
        style.href = `${props.urlPrefix}/baby-gru/moorhen.css`
        style.rel = "stylesheet"
        style.async = true
        style.type = 'text/css'
        head.appendChild(style)
    }, [])

    const setWindowDimensions = useCallback(() => {
        let [ newWidth, newHeight ]: [number, number] = [window.innerWidth, window.innerHeight]
        if (setMoorhenDimensions) {
            [ newWidth, newHeight ] = setMoorhenDimensions()
        } 
        if (width !== newWidth) {
            dispatch(setWidth(newWidth))
        }
        if (height !== newHeight) {
            dispatch(setHeight(newHeight))
        }
    }, [width, height])

    useEffect(() => {
        window.addEventListener('resize', setWindowDimensions)
        return () => {
            window.removeEventListener('resize', setWindowDimensions)
        }
    }, [setWindowDimensions])

    useEffect(() => {
        const initTimeCapsule = async () => {
            if (userPreferencesMounted) {
                timeCapsuleRef.current = new MoorhenTimeCapsule(moleculesRef, mapsRef, activeMapRef, glRef, store)
                timeCapsuleRef.current.storageInstance = backupStorageInstance
                timeCapsuleRef.current.maxBackupCount = maxBackupCount
                timeCapsuleRef.current.modificationCountBackupThreshold = modificationCountBackupThreshold
                await timeCapsuleRef.current.init()
            }
        }
        initTimeCapsule()
    }, [userPreferencesMounted])
    
    useEffect(() => {
        const onCootInitialized = async () => {
            if (cootInitialized && userPreferencesMounted) {
                await commandCentre.current.cootCommand({
                    command: 'set_map_sampling_rate',
                    commandArgs: [defaultMapSamplingRate],
                    returnType: 'status'
                }, false)
            }
        }
        onCootInitialized()

    }, [cootInitialized, userPreferencesMounted])

    useEffect(() => {
        if (userPreferencesMounted && defaultBackgroundColor !== backgroundColor) {
            dispatch(
                setBackgroundColor(defaultBackgroundColor)
            )
        }
        
    }, [userPreferencesMounted])

    useMemo(() => {
        let head = document.head;
        let style: any = document.createElement("link");

        if (isDark) {
            style.href = `${urlPrefix}/baby-gru/darkly.css`
        } else {
            style.href = `${urlPrefix}/baby-gru/flatly.css`
        }
        
        style.rel = "stylesheet";
        style.async = true
        style.type = 'text/css'

        head.appendChild(style);
        return () => { head.removeChild(style); }
    }, [isDark])

    useEffect(() => {
        if (!userPreferencesMounted) {
            return
        }
        
        const _isDark = isDarkBackground(...backgroundColor)
        
        if (defaultBackgroundColor !== backgroundColor) {
            dispatch( setDefaultBackgroundColor(backgroundColor) )
        }
        if (isDark !== _isDark) {
            dispatch( setIsDark(_isDark) )
            dispatch( setTheme(_isDark ? "darkly" : "flatly") )
        }

    }, [backgroundColor])

    useEffect(() => {
        async function setMakeBackupsAPI() {
            await commandCentre.current.cootCommand({
                command: 'set_make_backups',
                commandArgs: [makeBackups],
                returnType: "status"
            }, false)
        }

        if (commandCentre.current && makeBackups !== null && cootInitialized) {
            setMakeBackupsAPI()
        }

    }, [makeBackups, cootInitialized])

    useEffect(() => {
        async function setDrawMissingLoopAPI() {
            await commandCentre.current.cootCommand({
                command: 'set_draw_missing_residue_loops',
                commandArgs: [drawMissingLoops],
                returnType: "status"
            }, false)
        }

        if (commandCentre.current && drawMissingLoops !== null && cootInitialized) {
            setDrawMissingLoopAPI()
        }

    }, [drawMissingLoops, cootInitialized])

    useEffect(() => {
        const initCommandCentre = async () => {
            setWindowDimensions()
            commandCentre.current = new MoorhenCommandCentre(urlPrefix, glRef, timeCapsuleRef, {
                onCootInitialized: () => {
                    dispatch( setCootInitialized(true) )
                },
                onCommandExit: (kwargs) => {
                    dispatch( toggleCootCommandExit() )
                },
                onCommandStart: (kwargs) => {
                    dispatch( toggleCootCommandStart() )
                }
            })
            await commandCentre.current.init()    
        }
        
        initCommandCentre()
        
        return () => {
            commandCentre.current.close()
        }
    }, [])

    useEffect(() => {
        const checkMoleculeSizes = async () => {
            const moleculeAtomCounts = await Promise.all(
                molecules.filter(molecule => !molecule.atomCount).map(molecule => molecule.getNumberOfAtoms())
            )
            const totalAtomCount = moleculeAtomCounts.reduce((partialAtomCount, atomCount) => partialAtomCount + atomCount, 0)
            if (totalAtomCount >= 80000) {
                dispatch( setEnableAtomHovering(false) )
            }
        }
        checkMoleculeSizes()
    }, [molecules])

    const onAtomHovered = useCallback((identifier: { buffer: { id: string; }; atom: moorhen.AtomInfo; }) => {
        if (identifier == null) {
            if (lastHoveredAtomRef.current !== null && lastHoveredAtomRef.current.molecule !== null) {
                dispatch( setHoveredAtom({ molecule: null, cid: null }) )
            }
        }
        else {
            molecules.forEach(molecule => {
                if (molecule.buffersInclude(identifier.buffer)) {
                    const newCid = parseAtomInfoLabel(identifier.atom)
                    if (molecule !== hoveredAtom.molecule || newCid !== hoveredAtom.cid) {
                        dispatch( setHoveredAtom({ molecule: molecule, cid: newCid }) )
                    }
                }
            })
        }
    }, [molecules])

    useEffect(() => {
        if (hoveredAtom && hoveredAtom.molecule && hoveredAtom.cid) {
            if (lastHoveredAtomRef.current == null ||
                hoveredAtom.molecule !== lastHoveredAtomRef.current.molecule ||
                hoveredAtom.cid !== lastHoveredAtomRef.current.cid
            ) {
                hoveredAtom.molecule.drawHover(hoveredAtom.cid)
                //if we have changed molecule, might have to clean up hover display item of previous molecule
            }
        }

        if (lastHoveredAtomRef.current !== null &&
            lastHoveredAtomRef.current.molecule !== null &&
            lastHoveredAtomRef.current.molecule !== hoveredAtom.molecule
        ) {
            lastHoveredAtomRef.current.molecule.clearBuffersOfStyle("hover")
        }

        lastHoveredAtomRef.current = hoveredAtom
    }, [hoveredAtom])

    useEffect(() => {
        glRef.current.resize(width, height)
        glRef.current.drawScene()
    }, [width, height])

    useEffect(() => {
        if (activeMap && commandCentre.current) {
            activeMap.setActive()
            if (activeMap.isEM) {
                dispatch(setRefinementSelection('SPHERE'))
            } else {
                dispatch(setRefinementSelection('TRIPLE'))
            }
        }
    }, [activeMap])

    return <SnackbarProvider 
        hideIconVariant={false}
        autoHideDuration={4000}
        maxSnack={20}
        anchorOrigin={{horizontal: 'center', vertical: 'top'}}
        transitionDuration={{ enter: 500, exit: 300 }}
        Components={{
            goToResidue: MoorhenGoToResidueSnackbar,
            screenRecorder: MoorhenRecordingSnackBar,
            residueSelection: MoorhenResidueSelectionSnackBar,
            acceptRejectDraggingAtoms: MoorhenAcceptRejectDragAtomsSnackBar,
            acceptRejectRotateTranslateAtoms: MoorhenAcceptRejectRotateTranslateSnackBar,
            acceptRejectMatchingLigand: MoorhenAcceptRejectMatchingLigandSnackBar,
            longJobNotification: MoorhenLongJobSnackBar,
            residueSteps: MoorhenResidueStepsSnackBar,
            updatingMaps: MoorhenUpdatingMapsSnackBar,
            modelTrajectory: MoorhenModelTrajectorySnackBar,
            tomogram: MoorhenTomogramSnackBar,
            mapContourLevel: MoorhenMapContourLevelSnackBar,
            rotamerChange: MoorhenRotamerChangeSnackBar,
            screenshot: MoorhenScreenshotSnackBar,
            sideBar: MoorhenSideBar
        }}
        preventDuplicate={true}>
    <div>
        <Backdrop sx={{ color: '#fff', zIndex: (_theme) => _theme.zIndex.drawer + 1 }} open={!cootInitialized}>
            <Spinner animation="border" style={{ marginRight: '0.5rem' }}/>
            <span>Starting moorhen...</span>
        </Backdrop>
        
        <MoorhenNavBar {...collectedProps}/>
        
    </div>

    <MoorhenModalsContainer {...collectedProps}/>

    <MoorhenPreferencesContainer onUserPreferencesChange={onUserPreferencesChange}/>

    <MoorhenSnackBarManager {...collectedProps}/>

    <MoorhenUpdatingMapsManager commandCentre={props.commandCentre} glRef={props.glRef}/>

    {/**
    <MoorhenSharedSessionManager
        commandCentre={props.commandCentre}
        glRef={props.glRef}
        monomerLibrary={monomerLibraryPath}
        moleculesRef={moleculesRef}
        mapsRef={mapsRef}
        activeMapRef={activeMapRef}
    />
    */}

    <Container fluid className={`baby-gru ${theme}`}>
        <Row>
            <Col style={{ paddingLeft: '0', paddingRight: '0' }}>
                <div
                    id="moorhen-canvas-background"
                    style={{
                        backgroundColor: `rgba(
                            ${255 * backgroundColor[0]},
                            ${255 * backgroundColor[1]},
                            ${255 * backgroundColor[2]}, 
                            ${backgroundColor[3]})`,
                        cursor: cursorStyle, margin: 0, padding: 0, height: Math.floor(height),
                    }}>
                    <MoorhenWebMG
                        ref={glRef}
                        monomerLibraryPath={monomerLibraryPath}
                        timeCapsuleRef={timeCapsuleRef}
                        commandCentre={commandCentre}
                        onAtomHovered={onAtomHovered}
                        urlPrefix={urlPrefix}
                        viewOnly={viewOnly}
                        videoRecorderRef={videoRecorderRef}
                    />
                </div>
            </Col>
        </Row>
    </Container>
    </SnackbarProvider>
}

MoorhenContainer.defaultProps = {
    onUserPreferencesChange: () => {},
    urlPrefix: '.',
    monomerLibraryPath: './baby-gru/monomers',
    setMoorhenDimensions: null,
    disableFileUploads: false,
    includeNavBarMenuNames: [],
    extraNavBarModals: [],
    extraNavBarMenus: [],
    extraFileMenuItems: [],
    extraEditMenuItems: [],
    extraCalculateMenuItems: [],
    extraDraggableModals: [],
    viewOnly: false,
    allowScripting: true,
    backupStorageInstance: createLocalStorageInstance('Moorhen-TimeCapsule'),
    aceDRGInstance: null,
    store: MoorhenReduxStore
}