// 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 */