feat(plugin): add plugin-chart-cartodiagram (#25869)

Co-authored-by: Jakob Miksch <jakob@meggsimum.de>
This commit is contained in:
Jan Suleiman
2025-01-06 17:58:03 +01:00
committed by GitHub
parent 5484db34f9
commit a986a61b5f
72 changed files with 8434 additions and 193 deletions

View File

@@ -0,0 +1,146 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PlusOutlined } from '@ant-design/icons';
import { css, styled, t } from '@superset-ui/core';
import { Button, Tree } from 'antd';
import { TreeProps } from 'antd/lib/tree';
import { forwardRef } from 'react';
import { FlatLayerDataNode, FlatLayerTreeProps, LayerConf } from './types';
import { handleDrop } from './dragDropUtil';
import LayerTreeItem from './LayerTreeItem';
export const StyledLayerTreeItem = styled(LayerTreeItem)`
${({ theme }) => css`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: unset;
border: none;
border-radius: ${theme.borderRadius}px;
background-color: ${theme.colors.grayscale.light3};
font-size: ${theme.typography.sizes.s}px;
font-weight: ${theme.typography.weights.normal};
&:hover {
background-color: ${theme.colors.grayscale.light3};
}
& .layer-tree-item-close {
border-right: solid;
border-right-width: 1px;
border-right-color: ${theme.colors.grayscale.light2};
}
& .layer-tree-item-edit {
border-left: solid;
border-left-width: 1px;
border-left-color: ${theme.colors.grayscale.light2};
}
& .layer-tree-item-title {
flex: 1;
padding-left: 4px;
}
& .layer-tree-item-type {
padding-left: 4px;
font-size: ${theme.typography.sizes.xs}px;
font-family: ${theme.typography.families.monospace};
}
& > button {
border: none;
background-color: unset;
color: ${theme.colors.grayscale.light1};
}
& > button:hover {
background-color: unset;
color: ${theme.colors.grayscale.light1};
}
`}
`;
// forwardRef is needed here in order for emotion and antd tree to work properly
export const FlatLayerTree = forwardRef<HTMLDivElement, FlatLayerTreeProps>(
(
{
layerConfigs,
onAddLayer = () => {},
onRemoveLayer = () => {},
onEditLayer = () => {},
onMoveLayer = () => {},
draggable,
className,
},
ref,
) => {
const layerConfigsToTreeData = (
configs: LayerConf[],
): FlatLayerDataNode[] =>
configs.map((config, idx) => ({
layerConf: config,
key: idx,
title: (
<StyledLayerTreeItem
layerConf={config}
onEditClick={() => onEditLayer(config, idx)}
onRemoveClick={() => onRemoveLayer(idx)}
/>
),
selectable: false,
isLeaf: true,
checkable: false,
}));
const treeDataToLayerConfigs = (
treeData: FlatLayerDataNode[],
): LayerConf[] => treeData.map(data => data.layerConf);
const treeData = layerConfigsToTreeData(layerConfigs);
const onDrop: TreeProps['onDrop'] = info => {
const data = handleDrop(info, treeData);
const movedLayerConfigs = treeDataToLayerConfigs(data);
onMoveLayer(movedLayerConfigs);
};
const addLayerLabel = t('Click to add new layer');
return (
<div className={className} ref={ref}>
<Button
className="add-layer-btn"
onClick={onAddLayer}
size="small"
type="dashed"
icon={<PlusOutlined />}
>
{addLayerLabel}
</Button>
<Tree treeData={treeData} draggable={draggable} onDrop={onDrop} />
</div>
);
},
);
export default FlatLayerTree;

View File

@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* This component is needed to be able to style GeoStyler
* via emotion. Emotion can only be used on a component that
* accepts a className property.
*/
import CardStyle from 'geostyler/dist/Component/CardStyle/CardStyle';
import { FC } from 'react';
import { GeoStylerWrapperProps } from './types';
export const GeoStylerWrapper: FC<GeoStylerWrapperProps> = ({
className,
...passThroughProps
}) => (
<div className={className}>
<CardStyle {...passThroughProps} />
</div>
);
export default GeoStylerWrapper;

View File

@@ -0,0 +1,193 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ControlHeader } from '@superset-ui/chart-controls';
import { css, styled, t } from '@superset-ui/core';
import { Popover } from 'antd';
import { FC, useState } from 'react';
import { EditItem, LayerConf, LayerConfigsControlProps } from './types';
import LayerConfigsPopoverContent from './LayerConfigsPopoverContent';
import FlatLayerTree from './FlatLayerTree';
export const StyledFlatLayerTree = styled(FlatLayerTree)`
${({ theme }) => css`
display: flex;
flex-direction: column;
border: solid;
border-width: 1px;
border-radius: ${theme.borderRadius}px;
border-color: ${theme.colors.grayscale.light2};
& .add-layer-btn {
display: flex;
align-items: center;
margin: 4px;
color: ${theme.colors.grayscale.light1};
font-size: ${theme.typography.sizes.s}px;
font-weight: ${theme.typography.weights.normal};
&:hover {
background-color: ${theme.colors.grayscale.light4};
border-color: ${theme.colors.grayscale.light2};
}
}
& .ant-tree .ant-tree-treenode {
display: block;
}
& .ant-tree-list-holder-inner {
display: block !important;
}
& .ant-tree-node-content-wrapper {
display: block;
}
& .ant-tree-node-content-wrapper:hover {
background-color: unset;
}
`}
`;
const getEmptyEditItem = (): EditItem => ({
idx: NaN,
layerConf: {
type: 'WMS',
version: '1.3.0',
title: '',
url: '',
layersParam: '',
},
});
export const LayerConfigsControl: FC<LayerConfigsControlProps> = ({
value,
onChange = () => {},
name,
label,
description,
renderTrigger,
hovered,
validationErrors,
}) => {
const [popoverVisible, setPopoverVisible] = useState<boolean>(false);
const [editItem, setEditItem] = useState<EditItem>(getEmptyEditItem());
const onAddClick = () => {
setEditItem(getEmptyEditItem());
setPopoverVisible(true);
};
const onEditClick = (layerConf: LayerConf, idx: number) => {
if (popoverVisible) {
return;
}
setEditItem({
idx,
layerConf: { ...layerConf },
});
setPopoverVisible(true);
};
const onRemoveClick = (idx: number) => {
const newValue = value ? [...value] : [];
newValue.splice(idx, 1);
onChange(newValue);
};
const onPopoverClose = () => {
setPopoverVisible(false);
};
const computeNewValue = (layerConf: LayerConf) => {
const newValue = value ? [...value] : [];
if (!editItem) {
return undefined;
}
if (Number.isNaN(editItem.idx)) {
newValue.unshift(layerConf);
} else if (editItem) {
newValue[editItem.idx] = layerConf;
}
return newValue;
};
const onPopoverSave = (layerConf: LayerConf) => {
const newValue = computeNewValue(layerConf);
setPopoverVisible(false);
if (!newValue) {
return;
}
onChange(newValue);
};
const onMoveLayer = (newConfigs: LayerConf[]) => {
onChange(newConfigs);
};
const popoverTitle = editItem.layerConf.title
? editItem.layerConf.title
: t('Add Layer');
const controlHeaderProps = {
name,
label,
description,
renderTrigger,
hovered,
validationErrors,
};
return (
<div>
<ControlHeader {...controlHeaderProps} />
<Popover
visible={popoverVisible}
trigger="click"
title={popoverTitle}
placement="right"
overlayStyle={{
maxWidth: '400px',
maxHeight: '700px',
overflowY: 'auto',
}}
content={
<LayerConfigsPopoverContent
layerConf={editItem.layerConf}
onClose={onPopoverClose}
onSave={onPopoverSave}
/>
}
>
<StyledFlatLayerTree
layerConfigs={value ?? []}
onMoveLayer={onMoveLayer}
onEditLayer={onEditClick}
onRemoveLayer={onRemoveClick}
onAddLayer={onAddClick}
draggable={!popoverVisible}
/>
</Popover>
</div>
);
};
export default LayerConfigsControl;

View File

@@ -0,0 +1,506 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { css, JsonValue, styled, t } from '@superset-ui/core';
import { Button, Form, Tabs } from 'antd';
import { mix } from 'polished';
import { Data as GsData } from 'geostyler-data';
import { Style as GsStyle } from 'geostyler-style';
import WfsDataParser, {
RequestParams1_1_0,
RequestParams2_0_0,
} from 'geostyler-wfs-parser';
import { FC, useEffect, useState } from 'react';
import { isWfsLayerConf, isWmsLayerConf, isXyzLayerConf } from './typeguards';
import {
BaseLayerConf,
LayerConf,
LayerConfigsPopoverContentProps,
WfsLayerConf,
WmsLayerConf,
XyzLayerConf,
} from './types';
import { getServiceVersions, hasAllRequiredWfsParams } from './serviceUtil';
import { ControlFormItem } from '../ColumnConfigControl/ControlForm';
import GeoStylerWrapper from './GeoStylerWrapper';
// Enum for the different tabs
const LAYER_CONFIG_TABS = {
LAYER: '1',
GEOSTYLER: '2',
};
export const StyledButtonContainer = styled.div`
display: flex;
margin: 8px;
`;
export const StyledCloseButton = styled(Button)`
${({ theme }) => css`
flex: 1;
margin-right: 4px;
line-height: 1.5715;
border-radius: ${theme.borderRadius}px;
background-color: ${theme.colors.primary.light4};
color: ${theme.colors.primary.dark1};
font-size: ${theme.typography.sizes.s}px;
font-weight: ${theme.typography.weights.bold};
text-transform: uppercase;
min-width: ${theme.gridUnit * 36};
min-height: ${theme.gridUnit * 8};
box-shadow: none;
border-width: 0px;
border-style: none;
border-color: transparent;
&:hover {
background-color: ${mix(
0.1,
theme.colors.primary.base,
theme.colors.primary.light4,
)};
color: ${theme.colors.primary.dark1};
}
`}
`;
export const StyledControlFormItem = styled(ControlFormItem)`
${({ theme }) => css`
border-radius: ${theme.borderRadius}px;
`}
`;
export const StyledControlNumberFormItem = styled(ControlFormItem)`
${({ theme }) => css`
border-radius: ${theme.borderRadius}px;
width: 100%;
`}
`;
export const StyledGeoStyler = styled(GeoStylerWrapper)`
${({ theme }) => css`
h2 {
font-weight: ${theme.typography.weights.normal};
font-size: ${theme.typography.sizes.xl}px;
}
.ant-form-item-control {
flex: unset;
}
`}
`;
export const StyledSaveButton = styled(Button)`
${({ theme }) => css`
flex: 1;
margin-left: 4px;
line-height: 1.5715;
border-radius: ${theme.borderRadius}px;
background-color: ${theme.colors.primary.base};
color: ${theme.colors.grayscale.light5};
font-size: ${theme.typography.sizes.s}px;
font-weight: ${theme.typography.weights.bold};
text-transform: uppercase;
min-width: ${theme.gridUnit * 36};
min-height: ${theme.gridUnit * 8};
box-shadow: none;
border-width: 0px;
border-style: none;
border-color: transparent;
&:hover {
background-color: ${theme.colors.primary.dark1};
}
`}
`;
export const LayerConfigsPopoverContent: FC<
LayerConfigsPopoverContentProps
> = ({ onClose = () => {}, onSave = () => {}, layerConf }) => {
const [currentLayerConf, setCurrentLayerConf] =
useState<LayerConf>(layerConf);
const initialWmsVersion =
layerConf.type === 'WMS' ? layerConf.version : undefined;
const [wmsVersion, setWmsVersion] = useState<string | undefined>(
initialWmsVersion,
);
const initialWfsVersion =
layerConf.type === 'WFS' ? layerConf.version : undefined;
const [wfsVersion, setWfsVersion] = useState<string | undefined>(
initialWfsVersion,
);
const [geostylerData, setGeoStylerData] = useState<GsData | undefined>(
undefined,
);
const serviceVersions = getServiceVersions();
// This is needed to force mounting the form every time
// we get a new layerConf prop. Otherwise the input fields
// will not be updated properly, since ControlFormItem only
// recognises the `value` property once and then handles the
// values in its on state. Remounting creates a new component
// and thereby starts with a fresh state.
const [formKey, setFormKey] = useState<number>(0);
useEffect(() => {
setCurrentLayerConf({ ...layerConf });
setFormKey(oldFormKey => oldFormKey + 1);
}, [layerConf]);
const onFieldValueChange = (value: JsonValue, key: string) => {
setCurrentLayerConf({
...currentLayerConf,
[key]: value,
});
};
const onLayerTypeChange = (value: LayerConf['type']) => {
if (value === 'WFS') {
setCurrentLayerConf({
...currentLayerConf,
type: value,
version: serviceVersions[value][0],
style: {
name: 'Default Style',
rules: [
{
name: 'Default Rule',
symbolizers: [
{
kind: 'Line',
// eslint-disable-next-line theme-colors/no-literal-colors
color: '#000000',
width: 2,
},
{
kind: 'Mark',
wellKnownName: 'circle',
// eslint-disable-next-line theme-colors/no-literal-colors
color: '#000000',
},
{
kind: 'Fill',
// eslint-disable-next-line theme-colors/no-literal-colors
color: '#000000',
},
],
},
],
},
} as WfsLayerConf);
} else if (value === 'XYZ') {
setCurrentLayerConf({
...currentLayerConf,
type: value,
} as XyzLayerConf);
} else {
setCurrentLayerConf({
...currentLayerConf,
type: value,
version: serviceVersions[value][0],
} as WmsLayerConf);
}
};
const onLayerTitleChange = (fieldValue: string) => {
onFieldValueChange(fieldValue, 'title');
};
const onLayerUrlChange = (fieldValue: string) => {
onFieldValueChange(fieldValue, 'url');
};
const onLayersParamChange = (fieldValue: string) => {
onFieldValueChange(fieldValue, 'layersParam');
};
const onTypeNameChange = (fieldValue: string) => {
onFieldValueChange(fieldValue, 'typeName');
};
const onWmsVersionChange = (fieldValue: string) => {
onFieldValueChange(fieldValue, 'version');
setWmsVersion(fieldValue);
};
const onWfsVersionChange = (fieldValue: string) => {
onFieldValueChange(fieldValue, 'version');
setWfsVersion(fieldValue);
};
const onMaxFeaturesChange = (fieldValue: number) => {
onFieldValueChange(fieldValue, 'maxFeatures');
};
const onStyleChange = (fieldValue: GsStyle) => {
onFieldValueChange(fieldValue, 'style');
};
const onAttributionChange = (fieldValue: string) => {
onFieldValueChange(fieldValue, 'attribution');
};
const onCloseClick = () => {
onClose();
};
const onSaveClick = () => {
const baseConfs: BaseLayerConf = {
title: currentLayerConf.title,
url: currentLayerConf.url,
type: currentLayerConf.type,
attribution: currentLayerConf.attribution,
};
let conf: LayerConf;
if (isWmsLayerConf(currentLayerConf)) {
conf = {
...baseConfs,
version: currentLayerConf.version,
type: currentLayerConf.type,
layersParam: currentLayerConf.layersParam,
};
} else if (isXyzLayerConf(currentLayerConf)) {
conf = {
...baseConfs,
type: currentLayerConf.type,
};
} else {
conf = {
...baseConfs,
type: currentLayerConf.type,
version: currentLayerConf.version,
typeName: currentLayerConf.typeName,
maxFeatures: currentLayerConf.maxFeatures,
style: currentLayerConf.style,
};
}
onSave(conf);
};
useEffect(() => {
if (
!isWfsLayerConf(currentLayerConf) ||
!hasAllRequiredWfsParams(currentLayerConf)
) {
setGeoStylerData(undefined);
return undefined;
}
const readWfsData = async (conf: WfsLayerConf) => {
const wfsParser = new WfsDataParser();
try {
let requestParams: RequestParams1_1_0 | RequestParams2_0_0 = {} as
| RequestParams1_1_0
| RequestParams2_0_0;
if (conf.version.startsWith('1.')) {
requestParams = {
version: conf.version as RequestParams1_1_0['version'],
maxFeatures: conf.maxFeatures,
typeName: conf.typeName,
};
}
if (conf.version.startsWith('2.')) {
requestParams = {
version: conf.version as RequestParams2_0_0['version'],
count: conf.maxFeatures,
typeNames: conf.typeName,
};
}
const gsData = await wfsParser.readData({
url: conf.url,
requestParams,
});
setGeoStylerData(gsData);
} catch {
console.warn('Could not read geostyler data');
setGeoStylerData(undefined);
}
};
// debounce function
const timer = setTimeout(() => readWfsData(currentLayerConf), 500);
return () => {
clearTimeout(timer);
};
}, [currentLayerConf]);
const layerTabLabel = t('Layer');
const styleTabLabel = t('Style');
const layerTypeLabel = t('Layer type');
const layerTypeDescription = t('The type of the layer');
const serviceVersionLabel = t('Service version');
const serviceVersionDescription = t('The version of the service');
const layersParamLabel = t('Layer Name');
const layersParamDescription = t(
'The name of the layer as described in GetCapabilities',
);
const layersParamPlaceholder = t('Layer Name');
const layerTitleLabel = t('Layer title');
const layerTitleDescription = t('The visible title of the layer');
const layerTitlePlaceholder = t('Insert Layer title');
const layerUrlLabel = t('Layer URL');
const layerUrlDescription = t('The service url of the layer');
const layerUrlPlaceholder = t('Insert Layer URL');
const maxFeaturesLabel = t('Max. features');
const maxFeaturesDescription = t(
'Maximum number of features to fetch from service',
);
const maxFeaturesPlaceholder = t('10000');
const attributionLabel = t('Attribution');
const attributionDescription = t('The layer attribution');
const attributionPlaceholder = t('© Layer attribution');
const wmsVersionOptions: { value: any; label: string }[] =
serviceVersions.WMS.map(version => ({ value: version, label: version }));
const wfsVersionOptions: { value: any; label: string }[] =
serviceVersions.WFS.map(version => ({ value: version, label: version }));
return (
<div>
<Form key={JSON.stringify(formKey)}>
<Tabs defaultActiveKey={LAYER_CONFIG_TABS.LAYER}>
<Tabs.TabPane tab={layerTabLabel} key={LAYER_CONFIG_TABS.LAYER}>
<StyledControlFormItem
controlType="Input"
label={layerUrlLabel}
description={layerUrlDescription}
placeholder={layerUrlPlaceholder}
value={currentLayerConf.url}
name="url"
onChange={onLayerUrlChange}
/>
<StyledControlFormItem
controlType="Select"
label={layerTypeLabel}
description={layerTypeDescription}
options={[
{ value: 'WMS', label: t('WMS') },
{ value: 'WFS', label: t('WFS') },
{ value: 'XYZ', label: t('XYZ') },
]}
value={currentLayerConf.type}
defaultValue={currentLayerConf.type}
name="type"
onChange={onLayerTypeChange}
/>
{isWmsLayerConf(currentLayerConf) && (
<StyledControlFormItem
controlType="Select"
label={serviceVersionLabel}
description={serviceVersionDescription}
options={wmsVersionOptions}
value={wmsVersion}
defaultValue={wmsVersionOptions[0].value as string}
name="wmsVersion"
onChange={onWmsVersionChange}
/>
)}
{isWfsLayerConf(currentLayerConf) && (
<StyledControlFormItem
controlType="Select"
label={serviceVersionLabel}
description={serviceVersionDescription}
options={wfsVersionOptions}
value={wfsVersion}
defaultValue={wfsVersionOptions[0].value as string}
name="wfsVersion"
onChange={onWfsVersionChange}
/>
)}
{isWmsLayerConf(currentLayerConf) && (
<StyledControlFormItem
controlType="Input"
label={layersParamLabel}
description={layersParamDescription}
placeholder={layersParamPlaceholder}
value={currentLayerConf.layersParam}
name="layersParam"
onChange={onLayersParamChange}
/>
)}
{isWfsLayerConf(currentLayerConf) && (
<StyledControlFormItem
controlType="Input"
label={layersParamLabel}
description={layersParamDescription}
placeholder={layersParamPlaceholder}
value={currentLayerConf.typeName}
name="typeName"
onChange={onTypeNameChange}
/>
)}
<StyledControlFormItem
controlType="Input"
label={layerTitleLabel}
description={layerTitleDescription}
placeholder={layerTitlePlaceholder}
value={currentLayerConf.title}
name="title"
onChange={onLayerTitleChange}
/>
{isWfsLayerConf(currentLayerConf) && (
<StyledControlNumberFormItem
controlType="InputNumber"
label={maxFeaturesLabel}
description={maxFeaturesDescription}
placeholder={maxFeaturesPlaceholder}
value={currentLayerConf.maxFeatures}
name="maxFeatures"
onChange={onMaxFeaturesChange}
/>
)}
<StyledControlFormItem
controlType="Input"
label={attributionLabel}
description={attributionDescription}
placeholder={attributionPlaceholder}
value={currentLayerConf.attribution}
name="attribution"
onChange={onAttributionChange}
/>
</Tabs.TabPane>
<Tabs.TabPane
tab={styleTabLabel}
key={LAYER_CONFIG_TABS.GEOSTYLER}
disabled={!isWfsLayerConf(currentLayerConf)}
>
{isWfsLayerConf(currentLayerConf) && (
<StyledGeoStyler
style={currentLayerConf.style}
onStyleChange={onStyleChange}
data={geostylerData}
/>
)}
</Tabs.TabPane>
</Tabs>
<StyledButtonContainer>
<StyledCloseButton type="default" onClick={onCloseClick}>
{t('Close')}
</StyledCloseButton>
<StyledSaveButton type="primary" onClick={onSaveClick}>
{t('Save')}
</StyledSaveButton>
</StyledButtonContainer>
</Form>
</div>
);
};
export default LayerConfigsPopoverContent;

View File

@@ -0,0 +1,72 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CloseOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Tag } from 'antd';
import { FC } from 'react';
import { LayerTreeItemProps } from './types';
export const LayerTreeItem: FC<LayerTreeItemProps> = ({
layerConf,
onEditClick = () => {},
onRemoveClick = () => {},
className,
}) => {
const onCloseTag = () => {
onRemoveClick();
};
const onEditTag = () => {
onEditClick();
};
return (
<Tag className={className}>
<Button
className="layer-tree-item-close"
icon={<CloseOutlined />}
onClick={onCloseTag}
size="small"
/>
<span
className="layer-tree-item-type"
onClick={onEditTag}
role="button"
tabIndex={0}
>
{layerConf.type}
</span>
<span
className="layer-tree-item-title"
onClick={onEditTag}
role="button"
tabIndex={0}
>
{layerConf.title}
</span>
<Button
className="layer-tree-item-edit"
icon={<RightOutlined />}
onClick={onEditTag}
size="small"
/>
</Tag>
);
};
export default LayerTreeItem;

View File

@@ -0,0 +1,64 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { TreeProps } from 'antd/lib/tree';
import { DropInfoType, FlatLayerDataNode } from './types';
/**
* Util for drag and drop related operations.
*/
/**
* Handle drop of flat antd tree.
*
* Functionality is roughly based on antd tree examples:
* https://ant.design/components/tree/
*
* @param info The argument of the onDrop callback.
* @param treeData The list of DataNodes on which the drop event occurred.
* @returns A copy of the list with the new sorting.
*/
export const handleDrop = (
info: DropInfoType<TreeProps['onDrop']>,
treeData: FlatLayerDataNode[],
) => {
if (info === undefined) {
return [...treeData];
}
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const data = [...treeData];
const dragObjIndex = data.findIndex(d => d.key === dragKey);
const dragObj = data[dragObjIndex];
data.splice(dragObjIndex, 1);
const dropObjIndex = data.findIndex(d => d.key === dropKey);
if (dropPosition === -1) {
data.splice(dropObjIndex, 0, dragObj!);
} else {
data.splice(dropObjIndex + 1, 0, dragObj!);
}
return data;
};

View File

@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { WfsLayerConf } from './types';
/**
* Get the available versions of WFS and WMS.
*
* @returns the versions
*/
export const getServiceVersions = () => ({
WMS: ['1.3.0', '1.1.1'],
WFS: ['2.0.2', '2.0.0', '1.1.0'],
});
/**
* Checks if all required WFS params are provided.
*
* @param layerConf The config to check
* @returns True, if all required params are provided. False, otherwise.
*/
export const hasAllRequiredWfsParams = (layerConf: WfsLayerConf) =>
layerConf.url && layerConf.version && layerConf.typeName;

View File

@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { LayerConf, WfsLayerConf, WmsLayerConf, XyzLayerConf } from './types';
export const isWmsLayerConf = (
layerConf: LayerConf,
): layerConf is WmsLayerConf => layerConf.type === 'WMS';
export const isWfsLayerConf = (
layerConf: LayerConf,
): layerConf is WfsLayerConf => layerConf.type === 'WFS';
export const isXyzLayerConf = (
layerConf: LayerConf,
): layerConf is XyzLayerConf => layerConf.type === 'XYZ';

View File

@@ -0,0 +1,92 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { DataNode, TreeProps } from 'antd/lib/tree';
import { ControlComponentProps } from '@superset-ui/chart-controls';
import { Style } from 'geostyler-style';
import { CardStyleProps } from 'geostyler/dist/Component/CardStyle/CardStyle';
export interface BaseLayerConf {
title: string;
url: string;
type: string;
attribution?: string;
}
export interface WfsLayerConf extends BaseLayerConf {
type: 'WFS';
typeName: string;
version: string;
maxFeatures?: number;
style?: Style;
}
export interface XyzLayerConf extends BaseLayerConf {
type: 'XYZ';
}
export interface WmsLayerConf extends BaseLayerConf {
type: 'WMS';
version: string;
layersParam: string;
}
export interface FlatLayerDataNode extends DataNode {
layerConf: LayerConf;
}
export interface FlatLayerTreeProps {
layerConfigs: LayerConf[];
onAddLayer?: () => void;
onRemoveLayer?: (idx: number) => void;
onEditLayer?: (layerConf: LayerConf, idx: number) => void;
onMoveLayer?: (layerConfigs: LayerConf[]) => void;
draggable?: boolean;
className?: string;
}
export type LayerConf = WmsLayerConf | WfsLayerConf | XyzLayerConf;
export type DropInfoType<T extends TreeProps['onDrop']> = T extends Function
? Parameters<T>[0]
: undefined;
export interface EditItem {
layerConf: LayerConf;
idx: number;
}
export type LayerConfigsControlProps = ControlComponentProps<LayerConf[]>;
export interface LayerConfigsPopoverContentProps {
onClose?: () => void;
onSave?: (layerConf: LayerConf) => void;
layerConf: LayerConf;
}
export interface GeoStylerWrapperProps extends CardStyleProps {
className?: string;
}
export interface LayerTreeItemProps {
layerConf: LayerConf;
onEditClick?: () => void;
onRemoveClick?: () => void;
className?: string;
}