import React, { FunctionComponent, useRef, useEffect, useState } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { connect } from 'react-redux'; import { replace, Replace } from 'connected-react-router'; import { useSpring, animated } from 'react-spring'; import { Link } from 'react-router-dom'; import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; import upperFirst from 'lodash/upperFirst'; import BrowserBar from './browser-bar/BrowserBar'; import BrowserImage from './browser-image/BrowserImage'; import BrowserNavBar from './browser-nav/BrowserNavBar'; import TrackPanel from './track-panel/TrackPanel'; import AppBar from 'src/shared/app-bar/AppBar'; import { RootState } from 'src/store'; import { ChrLocation } from './browserState'; import { changeBrowserLocation, setDataFromUrlAndSave, ParsedUrlPayload } from './browserActions'; import { getBrowserNavOpened, getChrLocation, getGenomeSelectorActive, getBrowserActivated, getBrowserActiveGenomeId, getBrowserQueryParams, getBrowserActiveEnsObjectId, getBrowserActiveEnsObjectIds, getAllChrLocations } from './browserSelectors'; import { getLaunchbarExpanded } from 'src/header/headerSelectors'; import { getTrackPanelOpened } from './track-panel/trackPanelSelectors'; import { getChrLocationFromStr, getChrLocationStr } from './browserHelper'; import { getDrawerOpened } from './drawer/drawerSelectors'; import { getEnabledCommittedSpecies } from 'src/content/app/species-selector/state/speciesSelectorSelectors'; import { CommittedItem } from 'src/content/app/species-selector/types/species-search'; import { getExampleEnsObjects } from 'src/ens-object/ensObjectSelectors'; import { EnsObject } from 'src/ens-object/ensObjectTypes'; import { fetchGenomeData } from 'src/genome/genomeActions'; import { toggleDrawer } from './drawer/drawerActions'; import browserStorageService from './browser-storage-service'; import { TrackStates } from './track-panel/trackPanelConfig'; import { AppName } from 'src/global/globalConfig'; import * as urlFor from 'src/shared/helpers/urlHelper'; import styles from './Browser.scss'; import 'static/browser/browser.js'; type StateProps = { activeGenomeId: string | null; activeEnsObjectId: string | null; allActiveEnsObjectIds: { [genomeId: string]: string }; browserActivated: boolean; browserNavOpened: boolean; browserQueryParams: { [key: string]: string }; chrLocation: ChrLocation | null; allChrLocations: { [genomeId: string]: ChrLocation }; drawerOpened: boolean; genomeSelectorActive: boolean; trackPanelOpened: boolean; launchbarExpanded: boolean; exampleEnsObjects: EnsObject[]; committedSpecies: CommittedItem[]; }; type DispatchProps = { changeBrowserLocation: (genomeId: string, chrLocation: ChrLocation) => void; fetchGenomeData: (genomeId: string) => void; replace: Replace; toggleDrawer: (drawerOpened: boolean) => void; setDataFromUrlAndSave: (payload: ParsedUrlPayload) => void; }; type OwnProps = {}; type MatchParams = { genomeId: string; }; type BrowserProps = RouteComponentProps & StateProps & DispatchProps & OwnProps; export const Browser: FunctionComponent = ( props: BrowserProps ) => { const browserRef: React.RefObject = useRef(null); const [trackStatesFromStorage, setTrackStatesFromStorage] = useState< TrackStates >({}); const lastGenomeIdRef = useRef(props.activeGenomeId); const setDataFromUrl = () => { const { genomeId = null } = props.match.params; const { focus = null, location = null } = props.browserQueryParams; const chrLocation = location ? getChrLocationFromStr(location) : null; const lastGenomeId = lastGenomeIdRef.current; /* before committing url changes to redux: - make sure we don't already have these same values in redux store; - if we already have these values, it's possible that this is because the user is switching back to a previously viewed species; so check whether the genome id has changed from the previous render (that's the reason for lastGenomeIdRef here) TODO: after both genome browser and browser chrome are updated so that we do not update url location while moving or zooming the image; we can remove the genomeId === lastGenomeId check in the if-statement below and move dispatchBrowserLocation(genomeId, chrLocation) above the if-statement */ if ( !genomeId || (genomeId === lastGenomeId && genomeId === props.activeGenomeId && focus === props.activeEnsObjectId && isEqual(chrLocation, props.chrLocation)) ) { return; } const payload = { activeGenomeId: genomeId, activeEnsObjectId: focus || null, chrLocation }; props.setDataFromUrlAndSave(payload); chrLocation && dispatchBrowserLocation(genomeId, chrLocation); lastGenomeIdRef.current = genomeId; }; const dispatchBrowserLocation = ( genomeId: string, chrLocation: ChrLocation ) => { props.changeBrowserLocation(genomeId, chrLocation); }; const changeSelectedSpecies = (genomeId: string) => { const { allChrLocations, allActiveEnsObjectIds } = props; const chrLocation = allChrLocations[genomeId]; const activeEnsObjectId = allActiveEnsObjectIds[genomeId]; const params = { genomeId, focus: activeEnsObjectId, location: chrLocation ? getChrLocationStr(chrLocation) : null }; props.replace(urlFor.browser(params)); }; // handle url changes useEffect(() => { // handle navigation to /app/browser if (!props.match.params.genomeId) { // select either the species that the user viewed during the previous visit, // of the first selected species const { activeGenomeId, committedSpecies } = props; if ( activeGenomeId && find( committedSpecies, ({ genome_id }: CommittedItem) => genome_id === activeGenomeId ) ) { changeSelectedSpecies(activeGenomeId); } else { if (committedSpecies[0]) { changeSelectedSpecies(committedSpecies[0].genome_id); } } } else { // handle navigation to /app/browser/:genomeId?focus=:focus&location=:location setDataFromUrl(); } }, [props.match.params.genomeId, props.location.search]); useEffect(() => { const { activeGenomeId, fetchGenomeData } = props; activeGenomeId && fetchGenomeData(activeGenomeId); }, [props.activeGenomeId]); useEffect(() => { setTrackStatesFromStorage(browserStorageService.getTrackStates()); }, [props.activeGenomeId, props.activeEnsObjectId]); useEffect(() => { const { match: { params: { genomeId } }, browserQueryParams: { location } } = props; const chrLocation = location ? getChrLocationFromStr(location) : null; if (props.browserActivated && genomeId && chrLocation) { dispatchBrowserLocation(genomeId, chrLocation); } }, [props.browserActivated]); const closeTrack = () => { if (props.drawerOpened === false) { return; } props.toggleDrawer(false); }; const [trackAnimation, setTrackAnimation] = useSpring(() => ({ config: { tension: 280, friction: 45 }, height: '100%', width: 'calc(-36px + 100vw )' })); const getBrowserWidth = (): string => { if (props.drawerOpened) { return 'calc(41px + 0vw)'; } return props.trackPanelOpened ? 'calc(-356px + 100vw)' : 'calc(-36px + 100vw)'; }; useEffect(() => { setTrackAnimation({ width: getBrowserWidth() }); }, [props.drawerOpened, props.trackPanelOpened]); const getHeightClass = (launchbarExpanded: boolean): string => { return launchbarExpanded ? styles.shorter : styles.taller; }; return props.activeGenomeId ? ( <> {!props.browserQueryParams.focus && (
)} {props.browserQueryParams.focus && (
{props.genomeSelectorActive && (
)}
{props.browserNavOpened && !props.drawerOpened && browserRef.current ? ( ) : null}
)} ) : null; }; const ExampleObjectLinks = (props: BrowserProps) => { const { activeGenomeId } = props; if (!activeGenomeId) { return null; } const links = props.exampleEnsObjects.map((exampleObject: EnsObject) => { const location = `${exampleObject.location.chromosome}:${exampleObject.location.start}-${exampleObject.location.end}`; const path = urlFor.browser({ genomeId: activeGenomeId, focus: exampleObject.ensembl_object_id, location }); return (
{upperFirst(exampleObject.object_type)} {exampleObject.label}
); }); return
{links}
; }; const mapStateToProps = (state: RootState): StateProps => ({ activeGenomeId: getBrowserActiveGenomeId(state), activeEnsObjectId: getBrowserActiveEnsObjectId(state), allActiveEnsObjectIds: getBrowserActiveEnsObjectIds(state), browserActivated: getBrowserActivated(state), browserNavOpened: getBrowserNavOpened(state), browserQueryParams: getBrowserQueryParams(state), chrLocation: getChrLocation(state), allChrLocations: getAllChrLocations(state), drawerOpened: getDrawerOpened(state), genomeSelectorActive: getGenomeSelectorActive(state), trackPanelOpened: getTrackPanelOpened(state), launchbarExpanded: getLaunchbarExpanded(state), exampleEnsObjects: getExampleEnsObjects(state), committedSpecies: getEnabledCommittedSpecies(state) }); const mapDispatchToProps: DispatchProps = { changeBrowserLocation, fetchGenomeData, replace, toggleDrawer, setDataFromUrlAndSave }; export default withRouter( connect( mapStateToProps, mapDispatchToProps )(Browser) );