// Copyright (c) 2022 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. /* eslint-disable complexity */ import React, {Component, Fragment} from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import {FormattedMessage} from 'localization'; import {Button, Input, PanelLabel, SidePanelSection} from 'components/common/styled-components'; import ItemSelector from 'components/common/item-selector/item-selector'; import VisConfigByFieldSelectorFactory from './vis-config-by-field-selector'; import LayerColumnConfigFactory from './layer-column-config'; import LayerTypeSelectorFactory from './layer-type-selector'; import LayerVisibilityByZoomFactory from './layer-visibility-by-zoom'; import DimensionScaleSelector from './dimension-scale-selector'; import SourceDataSelectorFactory from 'components/side-panel/common/source-data-selector'; import VisConfigSwitchFactory from './vis-config-switch'; import VisConfigSliderFactory from './vis-config-slider'; import LayerConfigGroupFactory, {ConfigGroupCollapsibleContent} from './layer-config-group'; import CustomMarkersConfigGroupFactory from './custom-markers-config-group'; import TextLabelPanelFactory from './text-label-panel'; import {capitalizeFirstLetter} from 'utils/utils'; import { CHANNEL_SCALES, CHANNEL_SCALE_SUPPORTED_FIELDS, SCALE_TYPES } from 'constants/default-settings'; import {LAYER_TYPES} from 'layers/types'; import {injectIntl} from 'react-intl'; import ColorDimensionScaleSelectorFactory from './color-dimension-scale-selector'; import ColorSelectorFactory from './color-selector'; import LayerAggregationResolutionFactory from './layer-aggregation-resolution'; import {DEFAULT_COLOR_RANGE, hexColorCustomPalette} from 'constants/color-ranges'; const StyledLayerConfigurator = styled.div.attrs({ className: 'layer-panel__config' })` position: relative; margin-top: ${props => props.theme.layerConfiguratorMargin}; padding: ${props => props.theme.layerConfiguratorPadding}; border-left: ${props => props.theme.layerConfiguratorBorder} dashed ${props => props.theme.layerConfiguratorBorderColor}; `; const StyledLayerVisualConfigurator = styled.div.attrs({ className: 'layer-panel__config__visualC-config' })` margin-top: 12px; `; export const getLayerFields = (datasets, layer) => layer.config && datasets[layer.config.dataId] ? datasets[layer.config.dataId].fields : []; export const getLayerDataset = (datasets, layer) => layer.config && datasets[layer.config.dataId] ? datasets[layer.config.dataId] : null; export const getLayerConfiguratorProps = props => ({ layer: props.layer, disabled: Boolean(props.disabled), fields: getLayerFields(props.datasets, props.layer), onChange: props.updateLayerConfig, setColorUI: props.updateLayerColorUI }); export const getVisConfiguratorProps = props => ({ layer: props.layer, fields: getLayerFields(props.datasets, props.layer), onChange: props.updateLayerVisConfig, setColorUI: props.updateLayerColorUI }); export const getLayerChannelConfigProps = props => ({ layer: props.layer, fields: getLayerFields(props.datasets, props.layer), onChange: props.updateLayerVisualChannelConfig, onChangeVisConfig: props.updateLayerVisConfig }); LayerConfiguratorFactory.deps = [ SourceDataSelectorFactory, VisConfigSliderFactory, TextLabelPanelFactory, LayerConfigGroupFactory, ChannelByValueSelectorFactory, LayerColumnConfigFactory, LayerTypeSelectorFactory, VisConfigSwitchFactory, AggrScaleSelectorFactory, LayerVisibilityByZoomFactory, LayerAggregationResolutionFactory, LayerColorSelectorFactory, LayerColorRangeSelectorFactory, ArcLayerColorSelectorFactory, CustomMarkersConfigGroupFactory ]; export default function LayerConfiguratorFactory( SourceDataSelector, VisConfigSlider, TextLabelPanel, LayerConfigGroup, ChannelByValueSelector, LayerColumnConfig, LayerTypeSelector, VisConfigSwitch, AggrScaleSelector, LayerVisibilityByZoom, LayerAggregationResolution, LayerColorSelector, LayerColorRangeSelector, ArcLayerColorSelector, CustomMarkersConfigGroup ) { class LayerConfigurator extends Component { static propTypes = { disabled: PropTypes.bool, layer: PropTypes.object.isRequired, datasets: PropTypes.object.isRequired, layerTypeOptions: PropTypes.arrayOf(PropTypes.any).isRequired, openModal: PropTypes.func.isRequired, updateLayerConfig: PropTypes.func.isRequired, updateLayerType: PropTypes.func.isRequired, updateLayerTextLabel: PropTypes.func.isRequired, updateLayerVisConfig: PropTypes.func.isRequired, updateLayerVisualChannelConfig: PropTypes.func.isRequired, updateLayerColorUI: PropTypes.func.isRequired }; _renderPointLayerConfig(props) { return this._renderScatterplotLayerConfig(props); } _renderIconLayerConfig(props) { return this._renderScatterplotLayerConfig(props); } _renderScatterplotLayerConfig({ layer, visConfiguratorProps, layerChannelConfigProps, layerConfiguratorProps }) { return ( {/* Fill Color */} {layer.config.colorField ? ( ) : ( )} {/* outline color */} {layer.type === LAYER_TYPES.point ? ( {layer.config.strokeColorField ? ( ) : ( )} ) : null} {layer.type === LAYER_TYPES.point ? ( ) : null} {/* Radius */} {!layer.config.sizeField ? ( ) : ( )} {/* text label */} ); } _renderClusterLayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { return ( {/* Color */} {layer.visConfigSettings.colorAggregation.condition(layer.config) ? ( ) : null} {/* Cluster Radius */} ); } _renderHeatmapLayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { return ( {/* Color */} {/* Radius */} {/* Weight */} ); } _renderGridLayerConfig(props) { return this._renderAggregationLayerConfig(props); } _renderHexagonLayerConfig(props) { return this._renderAggregationLayerConfig(props); } _renderAggregationLayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { const {config} = layer; const { visConfig: {enable3d} } = config; const elevationByDescription = 'layer.elevationByDescription'; const colorByDescription = 'layer.colorByDescription'; return ( {/* Color */} {layer.visConfigSettings.colorAggregation.condition(layer.config) ? ( ) : null} {layer.visConfigSettings.percentile && layer.visConfigSettings.percentile.condition(layer.config) ? ( ) : null} {/* Cell size */} {/* Elevation */} {layer.visConfigSettings.enable3d ? ( {layer.visConfigSettings.sizeAggregation.condition(layer.config) ? ( ) : null} {layer.visConfigSettings.elevationPercentile.condition(layer.config) ? ( ) : null} ) : null} ); } // TODO: Shan move these into layer class _renderHexagonIdLayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { return ( {/* Color */} {layer.config.colorField ? ( ) : ( )} {/* Coverage */} {!layer.config.coverageField ? ( ) : ( )} {/* height */} ); } _renderArcLayerConfig(args) { return this._renderLineLayerConfig(args); } _renderLineLayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { return ( {/* Color */} {layer.config.colorField ? ( ) : ( )} {/* thickness */} {layer.config.sizeField ? ( ) : ( )} {/* elevation scale */} {layer.visConfigSettings.elevationScale ? ( ) : null} ); } _renderTripLayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { const { meta: {featureTypes = {}} } = layer; return ( {/* Color */} {layer.config.colorField ? ( ) : ( )} {/* Stroke Width */} {layer.config.sizeField ? ( ) : ( )} {/* Trail Length*/} ); } _renderGeojsonLayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { const { meta: {featureTypes = {}}, config: {visConfig} } = layer; return ( {/* Fill Color */} {featureTypes.polygon || featureTypes.point ? ( {layer.config.colorField ? ( ) : ( )} ) : null} {/* stroke color */} {layer.config.strokeColorField ? ( ) : ( )} {/* Stroke Width */} {layer.config.sizeField ? ( ) : ( )} {/* Elevation */} {featureTypes.polygon ? ( ) : null} {/* Radius */} {featureTypes.point ? ( {!layer.config.radiusField ? ( ) : ( )} ) : null} ); } _render3DLayerConfig({layer, visConfiguratorProps}) { return ( { if (e.target.files && e.target.files[0]) { const url = URL.createObjectURL(e.target.files[0]); visConfiguratorProps.onChange({scenegraph: url}); } }} /> ); } _renderS2LayerConfig({ layer, visConfiguratorProps, layerConfiguratorProps, layerChannelConfigProps }) { const { config: {visConfig} } = layer; return ( {/* Color */} {layer.config.colorField ? ( ) : ( )} {/* Stroke */} {layer.config.strokeColorField ? ( ) : ( )} {/* Stroke Width */} {layer.config.sizeField ? ( ) : ( )} {/* Elevation */} ); } render() { // @ts-expect-error const {layer, datasets, updateLayerConfig, layerTypeOptions, updateLayerType} = this.props; const {fields = [], fieldPairs = undefined} = layer.config.dataId ? datasets[layer.config.dataId] : {}; const {config} = layer; const visConfiguratorProps = getVisConfiguratorProps(this.props); const layerConfiguratorProps = getLayerConfiguratorProps(this.props); const layerChannelConfigProps = getLayerChannelConfigProps(this.props); const dataset = getLayerDataset(datasets, layer); const renderTemplate = layer.type && `_render${capitalizeFirstLetter(layer.type)}LayerConfig`; return ( {layer.layerInfoModal ? ( // @ts-expect-error this.props.openModal(layer.layerInfoModal)} /> ) : null} 0} expanded={!layer.hasAllColumns()} > {Object.keys(datasets).length > 1 && ( updateLayerConfig({dataId: value})} /> )} {Object.keys(layer.config.columns).length > 0 && ( )} {this[renderTemplate] && this[renderTemplate]({ layer, dataset, visConfiguratorProps, layerChannelConfigProps, layerConfiguratorProps })} ); } } return LayerConfigurator; } /* * Componentize config component into pure functional components */ const StyledHowToButton = styled.div` position: absolute; right: 12px; top: -4px; `; export const HowToButton = ({onClick}) => ( ); LayerColorSelectorFactory.deps = [ColorSelectorFactory]; export function LayerColorSelectorFactory(ColorSelector) { class LayerColorSelector extends Component { render() { // @ts-expect-error const {layer, onChange, selectedColor, property = 'color', setColorUI, fields} = this.props; return ( onChange({[property]: rgbValue}) } ]} colorUI={layer.config.colorUI[property]} setColorUI={newConfig => setColorUI(property, newConfig)} /> ); } } return LayerColorSelector; } ArcLayerColorSelectorFactory.deps = [ColorSelectorFactory]; export function ArcLayerColorSelectorFactory(ColorSelector) { class ArcLayerColorSelector extends Component { render() { const { layer, onChangeConfig, onChangeVisConfig, property = 'color', setColorUI, fields } = this.props as any; return ( onChangeConfig({color: rgbValue}), label: 'Source' }, { selectedColor: layer.config.visConfig.targetColor || layer.config.color, setColor: rgbValue => onChangeVisConfig({targetColor: rgbValue}), label: 'Target' } ]} colorUI={layer.config.colorUI[property]} setColorUI={newConfig => setColorUI(property, newConfig)} /> ); } } return ArcLayerColorSelector; } LayerColorRangeSelectorFactory.deps = [ColorSelectorFactory]; export function LayerColorRangeSelectorFactory(ColorSelector) { class LayerColorRangeSelector extends Component { __renderHeatmapColorSelectorComponent() { const property = 'colorRange'; const {layer, onChange, setColorUI} = this.props as any; return ( { const {colorMap} = layer.config.visConfig[property]; const colorRangeUpdated = colorMap ? layer.updateColorMap(property, colorMap, colorRange) : colorRange; onChange({[property]: colorRangeUpdated}); } } ]} colorUI={layer.config.colorUI[property]} setColorUI={newConfig => setColorUI(property, newConfig)} /> ); } __renderDefaultColorSelectorComponent() { const { layer, onChange, channel = 'color', setColorUI, fields, handleCustomOnSelectHexColumn = () => null } = this.props as any; const {range: property, field, scale} = layer.visualChannels[channel]; return ( { const {colorMap} = layer.config.visConfig[property]; const colorRangeUpdated = colorMap ? layer.updateColorMap(property, colorMap, colorRange) : colorRange; // Ensure that scale changes from identity to ordinal // Validate scale to ensure that it changes from `identity` to the correct one. if (layer.isColoredByColorColumn(property)) { layer.updateLayerConfig({ [field]: { ...layer.config[field], colorColumn: null, colorColumnIdx: null }, [scale]: SCALE_TYPES.ordinal }); layer.validateScale(channel); } // --------------------------------------- onChange({[property]: colorRangeUpdated}); } } ]} colorUI={layer.config.colorUI[property]} setColorUI={newConfig => setColorUI(property, newConfig)} onSelectHexColorColumn={colorColumn => { // It's defined in CN: see workspace-www/src/features/builder/ui/KeplerGl/SidePanel/LayerPanel/LayerColorRangeSelector.tsx handleCustomOnSelectHexColumn(layer, colorColumn); layer.updateLayerConfig({ [field]: { ...layer.config[field], colorColumn: colorColumn?.name, colorColumnIdx: colorColumn?.fieldIdx }, [scale]: SCALE_TYPES.identity }); onChange({[property]: hexColorCustomPalette}); }} /> ); } render() { const {layer} = this.props as any; let renderColorSelectorComponent = this.__renderDefaultColorSelectorComponent.bind(this); if (layer.type === LAYER_TYPES.heatmap) { renderColorSelectorComponent = this.__renderHeatmapColorSelectorComponent.bind(this); } return ( {renderColorSelectorComponent()} ); } } return LayerColorRangeSelector; } ChannelByValueSelectorFactory.deps = [VisConfigByFieldSelectorFactory]; export function ChannelByValueSelectorFactory(VisConfigByFieldSelector) { const ChannelByValueSelector = ({ layer, channel, onChange, onChangeVisConfig, fields, fieldsAggregated, description, isLoading }) => { const { channelScaleType, domain, field, aggregation, key, property, range, scale, defaultMeasure, supportedFieldTypes } = channel; const channelSupportedFieldTypes = supportedFieldTypes || CHANNEL_SCALE_SUPPORTED_FIELDS[channelScaleType]; const supportedFields = fields.filter(({type}) => channelSupportedFieldTypes.includes(type)); const scaleOptions = layer.getScaleOptions(channel.key); const showScaleCustomizable = channelScaleType === CHANNEL_SCALES.color && scale && layer.config[scale] && Boolean(layer.config[field]) && scaleOptions?.length > 0; const showScale = Boolean( !showScaleCustomizable && scale && !layer.isAggregated && layer.config[scale] && scaleOptions?.length > 1 ); const defaultDescription = 'layerConfiguration.defaultDescription'; const visRange = layer.config.visConfig[range]; const aggregator = layer.config.visConfig[aggregation]; return ( { if (visRange?.colorMap) { onChangeVisConfig({[range]: {...visRange, colorMap: undefined}}); } if (!val) { // Clean everything and comeback to default behaviour onChange({[field]: null}, key); onChangeVisConfig({[range]: DEFAULT_COLOR_RANGE}); } onChange( { [field]: val, ...(fieldsAggregated && { visConfig: { ...layer.config.visConfig, [aggregation]: val?.selectedAggregator } }) }, key ); layer.validateScale(key); }} updateScale={val => onChange({[scale]: val}, key)} updateCustomPalette={(colorMap, uiCustomScaleType) => { onChangeVisConfig({[range]: {...visRange, colorMap, uiCustomScaleType}}); }} isLoading={isLoading} /> ); }; return ChannelByValueSelector; } AggrScaleSelectorFactory.deps = [ColorDimensionScaleSelectorFactory]; function AggrScaleSelectorFactory(ColorDimensionScaleSelector) { class AggrScaleSelector extends Component { render() { const {channel, layer, onChange, onChangeVisConfig} = this.props as any; const {scale, key, channelScaleType, domain, range} = channel; const scaleOptions = layer.getScaleOptions(key); const visRange = layer.config.visConfig[range]; const showScaleCustomizable = channelScaleType === CHANNEL_SCALES.colorAggr && Array.isArray(scaleOptions) && scaleOptions.length > 0; const showScale = Array.isArray(scaleOptions) && scaleOptions.length > 1; if (showScaleCustomizable) { return ( onChange({[scale]: val}, key)} onSetCustomPalette={(colorMap, uiCustomScaleType) => { onChangeVisConfig({[range]: {...visRange, colorMap, uiCustomScaleType}}); }} /> ); } if (showScale) { return ( onChange({[scale]: val}, key)} /> ); } return null; } } return injectIntl(AggrScaleSelector); } export const AggregationTypeSelector = ({layer, channel, onChange}) => { const {field, aggregation, key} = channel; const selectedField = layer.config[field]; const {visConfig} = layer.config; // aggregation should only be selectable when field is selected const aggregationOptions = layer.getAggregationOptions(key); return ( onChange( { visConfig: { ...layer.config.visConfig, [aggregation]: value } }, channel.key ) } /> ); }; /* eslint-enable max-params */