UNPKG

kepler.gl.geoiq

Version:

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

286 lines (261 loc) 9.19 kB
// Copyright (c) 2023 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 classnames from 'classnames'; import PropTypes from 'prop-types'; import {sortableContainer, sortableElement} from 'react-sortable-hoc'; import styled from 'styled-components'; import {createSelector} from 'reselect'; import {arrayMove} from 'utils/data-utils'; import LayerPanelFactory from './layer-panel/layer-panel'; import SourceDataCatalogFactory from './common/source-data-catalog'; import {Add} from 'components/common/icons'; import ItemSelector from 'components/common/item-selector/item-selector'; import { PanelLabel, SidePanelDivider, SidePanelSection, Button } from 'components/common/styled-components'; import {LAYER_BLENDINGS} from 'constants/default-settings'; // make sure the element is always visible while is being dragged // item being dragged is appended in body, here to reset its global style const SortableStyledItem = styled.div` z-index: ${props => props.theme.dropdownWrapperZ + 1}; &.sorting { pointer-events: none; } &.sorting-layers .layer-panel__header { background-color: ${props => props.theme.panelBackgroundHover}; font-family: ${props => props.theme.fontFamily}; font-weight: ${props => props.theme.fontWeight}; font-size: ${props => props.theme.fontSize}; line-height: ${props => props.theme.lineHeight}; width: 100%; *, *:before, *:after { box-sizing: border-box; } .layer__drag-handle { opacity: 1; color: ${props => props.theme.textColorHl}; } } `; export function AddDataButtonFactory() { const AddDataButton = ({onClick, isInactive}) => ( <Button onClick={onClick} isInactive={!isInactive} width="105px" secondary> <Add height="12px" /> Add Data </Button> ); return AddDataButton; } export function AddExternalDataButtonFactory() { const AddExternalDataButton = ({onClick, isInactive}) => ( <Button onClick={onClick} isInactive={!isInactive} width="135" margin={'0px 0px 0px 16px'} secondary > <Add height="12px" /> Add Ext. Data </Button> ); return AddExternalDataButton; } LayerManagerFactory.deps = [ AddDataButtonFactory, AddExternalDataButtonFactory, LayerPanelFactory, SourceDataCatalogFactory ]; function LayerManagerFactory( AddDataButton, AddExternalDataButton, LayerPanel, SourceDataCatalog ) { // By wrapping layer panel using a sortable element we don't have to implement the drag and drop logic into the panel itself; // Developers can provide any layer panel implementation and it will still be sortable const SortableItem = sortableElement(({children, isSorting}) => { return ( <SortableStyledItem className={classnames('sortable-layer-items', {sorting: isSorting})} > {children} </SortableStyledItem> ); }); const SortableContainer = sortableContainer(({children}) => { return <div>{children}</div>; }); return class LayerManager extends Component { static propTypes = { datasets: PropTypes.object.isRequired, layerBlending: PropTypes.string.isRequired, layerClasses: PropTypes.object.isRequired, layers: PropTypes.arrayOf(PropTypes.any).isRequired, // functions addLayer: PropTypes.func.isRequired, layerColorUIChange: PropTypes.func.isRequired, layerConfigChange: PropTypes.func.isRequired, layerTextLabelChange: PropTypes.func.isRequired, layerVisualChannelConfigChange: PropTypes.func.isRequired, layerTypeChange: PropTypes.func.isRequired, layerVisConfigChange: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired, removeLayer: PropTypes.func.isRequired, removeDataset: PropTypes.func.isRequired, showDatasetTable: PropTypes.func.isRequired, updateLayerBlending: PropTypes.func.isRequired, updateLayerOrder: PropTypes.func.isRequired, updateLayerData: PropTypes.func.isRequired, axiosAPICAll: PropTypes.func.isRequired }; state = { isSorting: false }; layerClassSelector = props => props.layerClasses; layerTypeOptionsSelector = createSelector( this.layerClassSelector, layerClasses => Object.keys(layerClasses).map(key => { const layer = new layerClasses[key](); return { id: key, label: layer.name, icon: layer.layerIcon }; }) ); _addEmptyNewLayer = () => { this.props.addLayer(); }; _handleSort = ({oldIndex, newIndex}) => { this.props.updateLayerOrder( arrayMove(this.props.layerOrder, oldIndex, newIndex) ); this.setState({isSorting: false}); }; _onSortStart = () => { this.setState({isSorting: true}); }; _updateBeforeSortStart = ({index}) => { // if layer config is active, close it const {layerOrder, layers, layerConfigChange} = this.props; const layerIdx = layerOrder[index]; if (layers[layerIdx].config.isConfigActive) { layerConfigChange(layers[layerIdx], {isConfigActive: false}); } }; render() { const { layers, datasets, layerOrder, openModal, mapState, filters, isPublic } = this.props; const defaultDataset = Object.keys(datasets)[0]; const layerTypeOptions = this.layerTypeOptionsSelector(this.props); const layerActions = { layerColorUIChange: this.props.layerColorUIChange, layerConfigChange: this.props.layerConfigChange, layerVisualChannelConfigChange: this.props .layerVisualChannelConfigChange, layerTypeChange: this.props.layerTypeChange, layerVisConfigChange: this.props.layerVisConfigChange, layerTextLabelChange: this.props.layerTextLabelChange, removeLayer: this.props.removeLayer, updateLayerData: this.props.updateLayerData }; const panelProps = { datasets, openModal, layerTypeOptions, mapState, filters }; return ( <div className="layer-manager"> <SourceDataCatalog datasets={datasets} showDatasetTable={isPublic ? this.props.showDatasetTable : false} removeDataset={this.props.removeDataset} showDeleteDataset /> {isPublic && ( <AddDataButton onClick={this.props.showAddDataModal} isInactive={!defaultDataset} /> )} {/* <AddExternalDataButton onClick={this.props.showAddExternalDataModal} isInactive={!defaultDataset} /> */} <SidePanelDivider /> <SidePanelSection> <SortableContainer onSortEnd={this._handleSort} onSortStart={this._onSortStart} updateBeforeSortStart={this._updateBeforeSortStart} lockAxis="y" helperClass="sorting-layers" useDragHandle > {layerOrder.map((layerIdx, index) => ( <SortableItem key={`layer-${layerIdx}`} index={index} isSorting={this.state.isSorting} > <LayerPanel {...panelProps} {...layerActions} sortData={layerIdx} key={layers[layerIdx].id} idx={layerIdx} layer={layers[layerIdx]} /> </SortableItem> ))} </SortableContainer> </SidePanelSection> <SidePanelSection> {defaultDataset ? ( <Button onClick={this._addEmptyNewLayer} width="105px"> <Add height="12px" /> Add Layer </Button> ) : null} </SidePanelSection> </div> ); } }; } export default LayerManagerFactory;