UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

236 lines (217 loc) 9.27 kB
// Copyright (c) 2021 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import React, {useCallback, forwardRef, useMemo} from 'react'; import styled from 'styled-components'; import TimeWidgetFactory from './filters/time-widget'; import AnimationControlFactory from './common/animation-control/animation-control'; import AnimationControllerFactory from './common/animation-control/animation-controller'; import {ANIMATION_WINDOW, FILTER_TYPES} from 'constants/default-settings'; import {getIntervalBins} from 'utils/filter-utils'; import {media} from 'styles/media-breakpoints'; const maxWidth = 1080; const BottomWidgetContainer = styled.div` display: flex; flex-direction: column; padding-top: ${props => (props.hasPadding ? props.theme.bottomWidgetPaddingTop : 0)}px; padding-right: ${props => (props.hasPadding ? props.theme.bottomWidgetPaddingRight : 0)}px; padding-bottom: ${props => (props.hasPadding ? props.theme.bottomWidgetPaddingBottom : 0)}px; padding-left: ${props => (props.hasPadding ? props.theme.bottomWidgetPaddingLeft : 0)}px; pointer-events: none !important; /* prevent padding from blocking input */ & > * { /* all children should allow input */ pointer-events: all; } width: ${props => props.width}px; z-index: 1; ${media.portable`padding: 0;`} `; FilterAnimationControllerFactory.deps = [AnimationControllerFactory]; export function FilterAnimationControllerFactory(AnimationController) { const FilterAnimationController = ({filter, filterIdx, setFilterAnimationTime, children}) => { const intervalBins = useMemo(() => getIntervalBins(filter), [filter]); const steps = useMemo(() => (intervalBins ? intervalBins.map(x => x.x0) : null), [ intervalBins ]); const updateAnimation = useCallback( value => { switch (filter.animationWindow) { case ANIMATION_WINDOW.interval: const idx = value[1]; setFilterAnimationTime(filterIdx, 'value', [ intervalBins[idx].x0, intervalBins[idx].x1 - 1 ]); break; default: setFilterAnimationTime(filterIdx, 'value', value); break; } }, [filterIdx, intervalBins, filter.animationWindow, setFilterAnimationTime] ); return ( <AnimationController key="filter-control" value={filter.value} domain={filter.domain} speed={filter.speed} isAnimating={filter.isAnimating} animationWindow={filter.animationWindow} steps={steps} updateAnimation={updateAnimation} children={children} /> ); }; return FilterAnimationController; } LayerAnimationControllerFactory.deps = [AnimationControllerFactory]; export function LayerAnimationControllerFactory(AnimationController) { const LayerAnimationController = ({animationConfig, setLayerAnimationTime, children}) => ( <AnimationController key="layer-control" value={animationConfig.currentTime} domain={animationConfig.domain} speed={animationConfig.speed} isAnimating={animationConfig.isAnimating} updateAnimation={setLayerAnimationTime} steps={animationConfig.timeSteps} animationWindow={ animationConfig.timeSteps ? ANIMATION_WINDOW.interval : ANIMATION_WINDOW.point } children={children} /> ); return LayerAnimationController; } BottomWidgetFactory.deps = [ TimeWidgetFactory, AnimationControlFactory, FilterAnimationControllerFactory, LayerAnimationControllerFactory ]; /* eslint-disable complexity */ export default function BottomWidgetFactory( TimeWidget, AnimationControl, FilterAnimationController, LayerAnimationController ) { const BottomWidget = props => { const { datasets, filters, animationConfig, visStateActions, containerW, uiState, sidePanelWidth, layers } = props; const {activeSidePanel, readOnly} = uiState; const isOpen = Boolean(activeSidePanel); const enlargedFilterIdx = useMemo( () => filters.findIndex(f => f.enlarged && f.type === FILTER_TYPES.timeRange), [filters] ); const animatedFilterIdx = useMemo(() => filters.findIndex(f => f.isAnimating), [filters]); const animatedFilter = animatedFilterIdx > -1 ? filters[animatedFilterIdx] : null; const enlargedFilterWidth = isOpen ? containerW - sidePanelWidth : containerW; // show playback control if layers contain trip layer & at least one trip layer is visible const animatableLayer = useMemo( () => layers.filter(l => l.config.animation && l.config.animation.enabled && l.config.isVisible), [layers] ); const readyToAnimation = Array.isArray(animationConfig.domain) && Number.isFinite(animationConfig.currentTime); // if animation control is showing, hide time display in time slider const showFloatingTimeDisplay = !animatableLayer.length; const showAnimationControl = animatableLayer.length && readyToAnimation && !animationConfig.hideControl; const showTimeWidget = enlargedFilterIdx > -1 && Object.keys(datasets).length > 0; // if filter is not animating, pass in enlarged filter here because // animation controller needs to call reset on it const filter = animatedFilter || filters[enlargedFilterIdx]; return ( <BottomWidgetContainer width={Math.min(maxWidth, enlargedFilterWidth)} className="bottom-widget--container" hasPadding={showAnimationControl || showTimeWidget} > <LayerAnimationController animationConfig={animationConfig} setLayerAnimationTime={visStateActions.setLayerAnimationTime} > {(isAnimating, start, pause, reset) => showAnimationControl ? ( <AnimationControl animationConfig={animationConfig} setLayerAnimationTime={visStateActions.setLayerAnimationTime} updateAnimationSpeed={visStateActions.updateLayerAnimationSpeed} toggleAnimation={visStateActions.toggleLayerAnimation} isAnimatable={!animatedFilter} isAnimating={isAnimating} resetAnimation={reset} /> ) : null } </LayerAnimationController> {filter && ( <FilterAnimationController filter={filter} filterIdx={animatedFilterIdx > -1 ? animatedFilterIdx : enlargedFilterIdx} setFilterAnimationTime={visStateActions.setFilterAnimationTime} > {(isAnimating, start, pause, resetAnimation) => showTimeWidget ? ( <TimeWidget // TimeWidget uses React.memo, here we pass width // even though it doesnt use it, to force rerender width={enlargedFilterWidth} filter={filters[enlargedFilterIdx]} index={enlargedFilterIdx} isAnyFilterAnimating={Boolean(animatedFilter)} datasets={datasets} readOnly={readOnly} showTimeDisplay={showFloatingTimeDisplay} setFilterPlot={visStateActions.setFilterPlot} setFilter={visStateActions.setFilter} setFilterAnimationTime={visStateActions.setFilterAnimationTime} setFilterAnimationWindow={visStateActions.setFilterAnimationWindow} toggleAnimation={visStateActions.toggleFilterAnimation} updateAnimationSpeed={visStateActions.updateFilterAnimationSpeed} enlargeFilter={visStateActions.enlargeFilter} resetAnimation={resetAnimation} isAnimatable={!animationConfig || !animationConfig.isAnimating} /> ) : null } </FilterAnimationController> )} </BottomWidgetContainer> ); }; /* eslint-disable react/display-name */ // @ts-ignore return forwardRef((props, ref) => <BottomWidget {...props} rootRef={ref} />); /* eslint-enable react/display-name */ } /* eslint-enable complexity */