UNPKG

kepler.gl

Version:

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

397 lines (360 loc) 11.2 kB
// Copyright (c) 2018 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} from 'react'; import PropTypes from 'prop-types'; import {createSelector} from 'reselect'; import styled from 'styled-components'; import {Tooltip, IconRoundSmall} 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, Split, Legend, Cube3d, Delete, Layers } from 'components/common/icons'; const StyledMapControl = styled.div` right: 0; width: ${props => props.theme.mapControl.width}px; padding: ${props => props.theme.mapControl.padding}px; z-index: 1; top: ${props => props.top}px; position: absolute; `; const StyledMapControlAction = styled.div` padding: 4px 0; display: flex; justify-content: flex-end; `; const StyledMapControlButton = styled.div` align-items: center; background-color: ${props => props.active ? props.theme.secondaryBtnActBgd : props.theme.secondaryBtnBgd}; border-radius: 18px; border: 0; box-shadow: 0 6px 12px 0 rgba(0, 0, 0, 0.16); color: ${props => props.active ? props.theme.secondaryBtnActColor : props.theme.secondaryBtnColor}; cursor: pointer; display: flex; height: 36px; justify-content: center; margin: 0; outline: none; padding: 0; transition: ${props => props.theme.transition}; width: 36px; :focus { outline: none; } :hover { cursor: pointer; background-color: ${props => props.theme.secondaryBtnActBgd}; color: ${props => props.theme.secondaryBtnActColor}; } `; 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.secondaryBtnColor}; button { width: 18px; height: 18px; } `; /** * Generates all layers available for the current map * TODO: this may be moved into map-container or map-control or even at the reducer level * @param layers * @param mapLayers * @returns {[id, label, isVisible]} */ const layerSelector = (layers, mapLayers) => { const availableItems = Object.keys(layers).reduce( (availableLayers, currentLayerId) => { // is available ? if yes add to available list const currentLayer = layers[currentLayerId]; // if maplayers exists we need to make sure currentlayer // is contained in mapLayers in order to add onto availableLayers // otherwise we add all layers const layerConfig = mapLayers ? mapLayers[currentLayer.id] : currentLayer.config; const mustBeAdded = mapLayers && mapLayers[currentLayer.id] ? mapLayers[currentLayer.id].isAvailable : layerConfig.isVisible; return mustBeAdded ? [ ...availableLayers, { id: currentLayer.id, name: currentLayer.config.label, isVisible: mapLayers && mapLayers[currentLayer.id] ? mapLayers[currentLayer.id].isVisible : layerConfig.isVisible, layer: currentLayer } ] : availableLayers; }, [] ); return availableItems; }; export class MapControl extends Component { static propTypes = { datasets: PropTypes.object.isRequired, dragRotate: PropTypes.bool.isRequired, isSplit: PropTypes.bool.isRequired, layers: PropTypes.arrayOf(PropTypes.object), mapIndex: PropTypes.number.isRequired, mapControls: PropTypes.object.isRequired, onToggleFullScreen: PropTypes.func.isRequired, onTogglePerspective: PropTypes.func.isRequired, onToggleSplitMap: PropTypes.func.isRequired, onToggleMapControl: PropTypes.func.isRequired, onMapToggleLayer: PropTypes.func.isRequired, top: PropTypes.number.isRequired, // optional scale: PropTypes.number, mapLayers: PropTypes.object }; static defaultProps = { isSplit: false, top: 0 }; layerSelector = state => state.layers; mapLayersSelector = state => state.mapLayers; initialDataSelector = createSelector( this.layerSelector, this.mapLayersSelector, layerSelector ); render() { const items = this.initialDataSelector(this.props); if (!items) { return null; } const { dragRotate, isSplit, isExport, mapIndex, mapControls, onTogglePerspective, onToggleSplitMap, onMapToggleLayer, onToggleMapControl, scale } = this.props; const { visibleLayers = {}, mapLegend = {}, toggle3d = {}, splitMap = {} } = mapControls; return ( <StyledMapControl className="map-control"> {/* Split Map */} {splitMap.show ? ( <ActionPanel key={0}> <StyledMapControlButton active={isSplit} onClick={e => { e.preventDefault(); onToggleSplitMap(isSplit ? mapIndex : undefined); }} key={`split-${isSplit}`} className="map-control-button split-map" data-tip data-for="action-toggle" > {isSplit ? <Delete height="18px" /> : <Split height="18px" />} <MapLegendTooltip id="action-toggle" message={ isSplit ? 'Close current panel' : 'Switch to dual map view' } /> </StyledMapControlButton> </ActionPanel> ) : null} {/* Map Layers */} {isSplit && visibleLayers.show ? ( <ActionPanel key={1}> <LayerSelectorPanel items={items} onMapToggleLayer={onMapToggleLayer} isActive={visibleLayers.active} toggleMenuPanel={() => onToggleMapControl('visibleLayers')} /> </ActionPanel> ) : null} {/* 3D Map */} {toggle3d.show ? ( <ActionPanel key={2}> <StyledMapControlButton onClick={e => { e.preventDefault(); onTogglePerspective(); }} active={dragRotate} data-tip data-for="action-3d" > <Cube3d height="22px" /> {/* No icon since we are injecting through css .threeD-map class*/} <MapLegendTooltip id="action-3d" message={dragRotate ? 'Disable 3D Map' : '3D Map'} /> </StyledMapControlButton> </ActionPanel> ) : null} {/* Map Legend */} {mapLegend.show ? ( <ActionPanel key={3}> <MapLegendPanel items={items} scale={scale} isExport={isExport} onMapToggleLayer={onMapToggleLayer} isActive={mapLegend.active} toggleMenuPanel={() => onToggleMapControl('mapLegend')} /> </ActionPanel> ) : null} </StyledMapControl> ); } } const MapControlPanel = ({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))` }} > <StyledMapControlPanelHeader style={{position: 'relative'}}> {isExport ? ( <KeplerGlLogo version={false} appName="kepler.gl"/> ) : ( <span style={{verticalAlign: 'middle'}}>{header}</span> )} {isExport ? null : ( <IconRoundSmall> <Close height="16px" onClick={onClick} /> </IconRoundSmall> )} </StyledMapControlPanelHeader> <StyledMapControlPanelContent>{children}</StyledMapControlPanelContent> </StyledMapControlPanel> ); const MapLegendPanel = ({items, isActive, scale, toggleMenuPanel, isExport}) => !isActive ? ( <StyledMapControlButton key={2} data-tip data-for="show-legend" className="map-control-button show-legend" onClick={e => { e.preventDefault(); toggleMenuPanel(); }} > <Legend height="22px" /> <MapLegendTooltip id="show-legend" message={'show legend'} /> </StyledMapControlButton> ) : ( <MapControlPanel scale={scale} header={'Layer Legend'} onClick={toggleMenuPanel} isExport={isExport} > <MapLegend layers={items.filter(item => item.isVisible).map(item => item.layer)} /> </MapControlPanel> ); const LayerSelectorPanel = ({ items, onMapToggleLayer, isActive, toggleMenuPanel }) => !isActive ? ( <StyledMapControlButton key={1} onClick={e => { e.preventDefault(); toggleMenuPanel(); }} className="map-control-button toggle-layer" data-tip data-for="toggle-layer" > <Layers height="22px" /> <MapLegendTooltip id="toggle-layer" message={isActive ? 'Hide layer panel' : 'Show layer panel'} /> </StyledMapControlButton> ) : ( <MapControlPanel header="Visible layers" onClick={toggleMenuPanel}> <MapLayerSelector layers={items} onMapToggleLayer={onMapToggleLayer} /> </MapControlPanel> ); const ActionPanel = ({children}) => ( <StyledMapControlAction>{children}</StyledMapControlAction> ); const MapLegendTooltip = ({id, message}) => ( <Tooltip id={id} place="left" effect="solid"> <span>{message}</span> </Tooltip> ); const MapControlFactory = () => MapControl; export default MapControlFactory;