kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
219 lines (188 loc) • 7.34 kB
JavaScript
// 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, {Component} from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import {Editor as Draw} from 'react-map-gl-draw';
import window from 'global/window';
import classnames from 'classnames';
import get from 'lodash.get';
import {createSelector} from 'reselect';
import FeatureActionPanelFactory from './feature-action-panel';
import {FILTER_TYPES, EDITOR_MODES, EDITOR_AVAILABLE_LAYERS} from 'constants/default-settings';
import {DEFAULT_RADIUS, getStyle as getFeatureStyle} from './feature-styles';
import {getStyle as getEditHandleStyle, getEditHandleShape} from './handle-style';
import KeyEvent from 'constants/keyevent';
const StyledWrapper = styled.div`
cursor: ${props => (props.editor.mode === EDITOR_MODES.EDIT ? 'pointer' : 'crosshair')};
position: relative;
`;
const editorLayerFilter = layer => EDITOR_AVAILABLE_LAYERS.includes(layer.type);
EditorFactory.deps = [FeatureActionPanelFactory];
export default function EditorFactory(FeatureActionPanel) {
class Editor extends Component {
static propTypes = {
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
layers: PropTypes.arrayOf(PropTypes.object).isRequired,
datasets: PropTypes.object.isRequired,
editor: PropTypes.object.isRequired,
layersToRender: PropTypes.object.isRequired,
onSelect: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onDeleteFeature: PropTypes.func.isRequired,
onTogglePolygonFilter: PropTypes.func.isRequired,
index: PropTypes.number,
classnames: PropTypes.string,
clickRadius: PropTypes.number,
isEnabled: PropTypes.bool
};
static defaultProps = {
clickRadius: DEFAULT_RADIUS
};
state = {
showActions: false,
lastPosition: null
};
componentDidMount() {
window.addEventListener('keydown', this._onKeyPressed);
}
componentWillUnmount() {
window.removeEventListener('keydown', this._onKeyPressed);
}
layerSelector = props => props.layers;
layersToRenderSelector = props => props.layersToRender;
filterSelector = props => props.filters;
selectedFeatureIdSelector = props => get(props, ['editor', 'selectedFeature', 'id']);
editorFeatureSelector = props => get(props, ['editor', 'features']);
currentFilterSelector = createSelector(
this.filterSelector,
this.selectedFeatureIdSelector,
(filters, selectedFeatureId) => filters.find(f => f.value && f.value.id === selectedFeatureId)
);
availableLayersSeletor = createSelector(
this.layerSelector,
this.layersToRenderSelector,
(layers, layersToRender) =>
layers.filter(editorLayerFilter).filter(layer => {
return layersToRender[layer.id];
})
);
allFeaturesSelector = createSelector(
this.filterSelector,
this.editorFeatureSelector,
(filters, editorFeatures) =>
filters
.filter(f => f.type === FILTER_TYPES.polygon)
.map(f => f.value)
.concat(editorFeatures)
);
_onKeyPressed = event => {
const {isEnabled} = this.props;
if (!isEnabled) {
return;
}
switch (event.keyCode) {
case KeyEvent.DOM_VK_DELETE:
case KeyEvent.DOM_VK_BACK_SPACE:
this._onDeleteSelectedFeature();
break;
case KeyEvent.DOM_VK_ESCAPE:
this.props.onSelect(null);
break;
default:
break;
}
};
_onSelect = ({selectedFeatureId, sourceEvent}) => {
const allFeatures = this.allFeaturesSelector(this.props);
this.setState(
{
...(sourceEvent.rightButton
? {
showActions: true,
lastPosition: {
x: sourceEvent.changedPointers[0].offsetX,
y: sourceEvent.changedPointers[0].offsetY
}
}
: null)
},
() => {
this.props.onSelect(allFeatures.find(f => f.id === selectedFeatureId));
}
);
};
_onDeleteSelectedFeature = () => {
if (this.state.showActions) {
this.setState({showActions: false});
}
const {editor} = this.props;
const {selectedFeature = {}} = editor;
this.props.onDeleteFeature(selectedFeature);
};
_closeFeatureAction = () => {
this.setState({showActions: false});
};
_onToggleLayer = layer => {
const {selectedFeature} = this.props.editor;
if (!selectedFeature) {
return;
}
this.props.onTogglePolygonFilter(layer, selectedFeature);
};
render() {
const {className, clickRadius, datasets, editor, onUpdate, style} = this.props;
const {lastPosition, showActions} = this.state;
const selectedFeatureId = get(editor, ['selectedFeature', 'id']);
const currentFilter = this.currentFilterSelector(this.props);
const availableLayers = this.availableLayersSeletor(this.props);
const allFeatures = this.allFeaturesSelector(this.props);
return (
<StyledWrapper editor={editor} className={classnames('editor', className)} style={style}>
<Draw
clickRadius={clickRadius}
mode={editor.mode}
features={allFeatures}
selectedFeatureId={selectedFeatureId}
onSelect={this._onSelect}
onUpdate={onUpdate}
getEditHandleShape={getEditHandleShape}
getFeatureStyle={getFeatureStyle}
getEditHandleStyle={getEditHandleStyle}
/>
{showActions && Boolean(selectedFeatureId) ? (
<FeatureActionPanel
selectedFeature={get(editor, ['selectedFeature'])}
datasets={datasets}
layers={availableLayers}
currentFilter={currentFilter}
onClose={this._closeFeatureAction}
onDeleteFeature={this._onDeleteSelectedFeature}
onToggleLayer={this._onToggleLayer}
position={lastPosition}
/>
) : null}
</StyledWrapper>
);
}
}
Editor.displayName = 'Editor';
return Editor;
}