UNPKG

kepler.gl

Version:

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

508 lines (462 loc) 15.3 kB
// Copyright (c) 2020 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, {Component, useCallback} from 'react'; import PropTypes from 'prop-types'; import {createSelector} from 'reselect'; import styled from 'styled-components'; import {FormattedMessage} from 'react-intl'; import classnames from 'classnames'; import {IconRoundSmall, MapControlButton, Tooltip} from 'components/common/styled-components'; import MapLayerSelector from 'components/common/map-layer-selector'; import KeplerGlLogo from 'components/common/logo'; import MapLegend from './map-legend'; import { Close, Cube3d, CursorClick, Delete, DrawPolygon, EyeSeen, EyeUnseen, Layers, Legend, Polygon, Rectangle, Split } from 'components/common/icons'; import VerticalToolbar from 'components/common/vertical-toolbar'; import ToolbarItem from 'components/common/toolbar-item'; import {EDITOR_MODES, LOCALES} from 'constants/default-settings'; const StyledMapControl = styled.div` right: 0; width: ${props => props.theme.mapControl.width}px; padding: ${props => props.theme.mapControl.padding}px; z-index: 10; top: ${props => props.top}px; position: absolute; `; const StyledMapControlAction = styled.div` padding: 4px 0; display: flex; justify-content: flex-end; `; const StyledMapControlPanel = styled.div` background-color: ${props => props.theme.mapPanelBackgroundColor}; flex-grow: 1; z-index: 1; p { margin-bottom: 0; } `; const StyledMapControlPanelContent = styled.div` ${props => props.theme.dropdownScrollBar}; max-height: 500px; min-height: 100px; overflow: auto; `; const StyledMapControlPanelHeader = styled.div` display: flex; justify-content: space-between; background-color: ${props => props.theme.mapPanelHeaderBackgroundColor}; height: 32px; padding: 6px 12px; font-size: 11px; color: ${props => props.theme.titleTextColor}; position: relative; button { width: 18px; height: 18px; } `; const ActionPanel = ({className, children}) => ( <StyledMapControlAction className={className}>{children}</StyledMapControlAction> ); ActionPanel.displayName = 'ActionPanel'; const MapControlTooltip = React.memo(({id, message}) => ( <Tooltip id={id} place="left" effect="solid"> <span> <FormattedMessage id={message} /> </span> </Tooltip> )); MapControlTooltip.displayName = 'MapControlTooltip'; const MapLegendTooltip = ({id, message}) => ( <Tooltip id={id} place="left" effect="solid"> <span> <FormattedMessage id={message} /> </span> </Tooltip> ); const LayerSelectorPanel = React.memo(({items, onMapToggleLayer, isActive, toggleMenuPanel}) => !isActive ? ( <MapControlButton key={1} onClick={e => { e.preventDefault(); toggleMenuPanel(); }} className="map-control-button toggle-layer" data-tip data-for="toggle-layer" > <Layers height="22px" /> <MapControlTooltip id="toggle-layer" message={isActive ? 'tooltip.hideLayerPanel' : 'tooltip.showLayerPanel'} /> </MapControlButton> ) : ( <MapControlPanel header="header.visibleLayers" onClick={toggleMenuPanel}> <MapLayerSelector layers={items} onMapToggleLayer={onMapToggleLayer} /> </MapControlPanel> ) ); LayerSelectorPanel.displayName = 'LayerSelectorPanel'; const MapControlPanel = React.memo(({children, header, onClick, scale = 1, isExport}) => ( <StyledMapControlPanel style={{ transform: `scale(${scale}) translate(calc(-${25 * (scale - 1)}% - ${10 * scale}px), calc(${25 * (scale - 1)}% + ${10 * scale}px))`, marginBottom: '8px' }} > <StyledMapControlPanelHeader> {isExport ? ( <KeplerGlLogo version={false} appName="kepler.gl" /> ) : ( <span style={{verticalAlign: 'middle'}}> <FormattedMessage id={header} /> </span> )} {isExport ? null : ( <IconRoundSmall className="close-map-control-item" onClick={onClick}> <Close height="16px" /> </IconRoundSmall> )} </StyledMapControlPanelHeader> <StyledMapControlPanelContent>{children}</StyledMapControlPanelContent> </StyledMapControlPanel> )); MapControlPanel.displayName = 'MapControlPanel'; const MapLegendPanel = ({layers, isActive, scale, onToggleMenuPanel, isExport}) => !isActive ? ( <MapControlButton key={2} data-tip data-for="show-legend" className="map-control-button show-legend" onClick={e => { e.preventDefault(); onToggleMenuPanel(); }} > <Legend height="22px" /> <MapLegendTooltip id="show-legend" message={'tooltip.showLegend'} /> </MapControlButton> ) : ( <MapControlPanel scale={scale} header={'header.layerLegend'} onClick={onToggleMenuPanel} isExport={isExport} > <MapLegend layers={layers} /> </MapControlPanel> ); MapLegendPanel.displayName = 'MapControlPanel'; const SplitMapButton = React.memo(({isSplit, mapIndex, onToggleSplitMap}) => ( <MapControlButton active={isSplit} onClick={e => { e.preventDefault(); onToggleSplitMap(isSplit ? mapIndex : undefined); }} key={`split-${isSplit}`} className={classnames('map-control-button', 'split-map', {'close-map': isSplit})} data-tip data-for="action-toggle" > {isSplit ? <Delete height="18px" /> : <Split height="18px" />} <MapControlTooltip id="action-toggle" message={isSplit ? 'tooltip.closePanel' : 'tooltip.switchToDualView'} /> </MapControlButton> )); SplitMapButton.displayName = 'SplitMapButton'; const Toggle3dButton = React.memo(({dragRotate, onTogglePerspective}) => ( <MapControlButton onClick={e => { e.preventDefault(); onTogglePerspective(); }} active={dragRotate} data-tip data-for="action-3d" > <Cube3d height="22px" /> <MapControlTooltip id="action-3d" message={dragRotate ? 'tooltip.disable3DMap' : 'tooltip.3DMap'} /> </MapControlButton> )); Toggle3dButton.displayName = 'Toggle3dButton'; const StyledToolbar = styled(VerticalToolbar)` position: absolute; right: 32px; `; const MapDrawPanel = React.memo( ({editor, isActive, onToggleMenuPanel, onSetEditorMode, onToggleEditorVisibility}) => { return ( <div className="map-draw-controls" style={{position: 'relative'}}> {isActive ? ( <StyledToolbar show={isActive}> <ToolbarItem className="edit-feature" onClick={() => onSetEditorMode(EDITOR_MODES.EDIT)} label="toolbar.select" iconHeight="22px" icon={CursorClick} active={editor.mode === EDITOR_MODES.EDIT} /> <ToolbarItem className="draw-feature" onClick={() => onSetEditorMode(EDITOR_MODES.DRAW_POLYGON)} label="toolbar.polygon" iconHeight="22px" icon={Polygon} active={editor.mode === EDITOR_MODES.DRAW_POLYGON} /> <ToolbarItem className="draw-rectangle" onClick={() => onSetEditorMode(EDITOR_MODES.DRAW_RECTANGLE)} label="toolbar.rectangle" iconHeight="22px" icon={Rectangle} active={editor.mode === EDITOR_MODES.DRAW_RECTANGLE} /> <ToolbarItem className="toggle-features" onClick={onToggleEditorVisibility} label={editor.visible ? 'toolbar.hide' : 'toolbar.show'} iconHeight="22px" icon={editor.visible ? EyeSeen : EyeUnseen} /> </StyledToolbar> ) : null} <MapControlButton onClick={e => { e.preventDefault(); onToggleMenuPanel(); }} active={isActive} data-tip data-for="map-draw" > <DrawPolygon height="22px" /> <MapControlTooltip id="map-draw" message="tooltip.DrawOnMap" /> </MapControlButton> </div> ); } ); MapDrawPanel.displayName = 'MapDrawPanel'; const LocalePanel = React.memo( ({availableLocales, isActive, onToggleMenuPanel, onSetLocale, activeLocale}) => { const onClickItem = useCallback( locale => { onSetLocale(locale); }, [onSetLocale] ); const onClickButton = useCallback( e => { e.preventDefault(); onToggleMenuPanel(); }, [onToggleMenuPanel] ); const getLabel = useCallback(locale => `toolbar.${locale}`, []); return ( <div style={{position: 'relative'}}> {isActive ? ( <StyledToolbar show={isActive}> {availableLocales.map(locale => ( <ToolbarItem key={locale} onClick={() => onClickItem(locale)} label={getLabel(locale)} active={activeLocale === locale} /> ))} </StyledToolbar> ) : null} <MapControlButton onClick={onClickButton} active={isActive} data-tip data-for="locale"> {activeLocale.toUpperCase()} <MapControlTooltip id="locale" message="tooltip.selectLocale" /> </MapControlButton> </div> ); } ); LocalePanel.displayName = 'LocalePanel'; const MapControlFactory = () => { class MapControl extends Component { static propTypes = { datasets: PropTypes.object.isRequired, dragRotate: PropTypes.bool.isRequired, isSplit: PropTypes.bool.isRequired, layers: PropTypes.arrayOf(PropTypes.object), layersToRender: PropTypes.object.isRequired, mapIndex: PropTypes.number.isRequired, mapControls: PropTypes.object.isRequired, onTogglePerspective: PropTypes.func.isRequired, onToggleSplitMap: PropTypes.func.isRequired, onToggleMapControl: PropTypes.func.isRequired, onSetEditorMode: PropTypes.func.isRequired, onToggleEditorVisibility: PropTypes.func.isRequired, top: PropTypes.number.isRequired, onSetLocale: PropTypes.func.isRequired, locale: PropTypes.string.isRequired, // optional readOnly: PropTypes.bool, scale: PropTypes.number, mapLayers: PropTypes.object, editor: PropTypes.object }; static defaultProps = { isSplit: false, top: 0, mapIndex: 0 }; layerSelector = props => props.layers; layersToRenderSelector = props => props.layersToRender; layerPanelItemsSelector = createSelector( this.layerSelector, this.layersToRenderSelector, (layers, layersToRender) => layers .filter(l => l.config.isVisible) .map(layer => ({ id: layer.id, name: layer.config.label, // layer isVisible: layersToRender[layer.id] })) ); render() { const { dragRotate, layers, layersToRender, isSplit, isExport, mapIndex, mapControls, onTogglePerspective, onToggleSplitMap, onMapToggleLayer, onToggleMapControl, editor, scale, readOnly, locale } = this.props; const { visibleLayers = {}, mapLegend = {}, toggle3d = {}, splitMap = {}, mapDraw = {}, mapLocale = {} } = mapControls; return ( <StyledMapControl className="map-control"> {/* Split Map */} {splitMap.show && readOnly !== true ? ( <ActionPanel className="split-map" key={0}> <SplitMapButton isSplit={isSplit} mapIndex={mapIndex} onToggleSplitMap={onToggleSplitMap} /> </ActionPanel> ) : null} {/* Map Layers */} {isSplit && visibleLayers.show && readOnly !== true ? ( <ActionPanel className="map-layers" key={1}> <LayerSelectorPanel items={this.layerPanelItemsSelector(this.props)} onMapToggleLayer={onMapToggleLayer} isActive={visibleLayers.active} toggleMenuPanel={() => onToggleMapControl('visibleLayers')} /> </ActionPanel> ) : null} {/* 3D Map */} {toggle3d.show ? ( <ActionPanel className="toggle-3d" key={2}> <Toggle3dButton dragRotate={dragRotate} onTogglePerspective={onTogglePerspective} /> </ActionPanel> ) : null} {/* Map Legend */} {mapLegend.show ? ( <ActionPanel className="show-legend" key={3}> <MapLegendPanel layers={layers.filter(l => layersToRender[l.id])} scale={scale} isExport={isExport} onMapToggleLayer={onMapToggleLayer} isActive={mapLegend.active} onToggleMenuPanel={() => onToggleMapControl('mapLegend')} /> </ActionPanel> ) : null} {mapDraw.show ? ( <ActionPanel key={4}> <MapDrawPanel isActive={mapDraw.active && mapDraw.activeMapIndex === mapIndex} editor={editor} onToggleMenuPanel={() => onToggleMapControl('mapDraw')} onSetEditorMode={this.props.onSetEditorMode} onToggleEditorVisibility={this.props.onToggleEditorVisibility} /> </ActionPanel> ) : null} {mapLocale.show ? ( <ActionPanel key={5}> <LocalePanel isActive={mapLocale.active} activeLocale={locale} availableLocales={Object.keys(LOCALES)} onSetLocale={this.props.onSetLocale} onToggleMenuPanel={() => onToggleMapControl('mapLocale')} /> </ActionPanel> ) : null} </StyledMapControl> ); } } MapControl.displayName = 'MapControl'; return MapControl; }; export default MapControlFactory;