mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
feat(chart): support icons and text in the deck.gl Geojson visualization (#36201)
Co-authored-by: Joshua Daniel <jdaniel@gflenv.com>
This commit is contained in:
committed by
Beto Dealmeida
parent
f342ddcd98
commit
38fe9bcf42
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* 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 { SqlaFormData } from '@superset-ui/core';
|
||||||
|
import {
|
||||||
|
computeGeoJsonTextOptionsFromJsOutput,
|
||||||
|
computeGeoJsonTextOptionsFromFormData,
|
||||||
|
computeGeoJsonIconOptionsFromJsOutput,
|
||||||
|
computeGeoJsonIconOptionsFromFormData,
|
||||||
|
} from './Geojson';
|
||||||
|
|
||||||
|
jest.mock('@deck.gl/react', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||||
|
expect(computeGeoJsonTextOptionsFromJsOutput(null)).toEqual({});
|
||||||
|
expect(computeGeoJsonTextOptionsFromJsOutput(42)).toEqual({});
|
||||||
|
expect(computeGeoJsonTextOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||||
|
expect(computeGeoJsonTextOptionsFromJsOutput('string')).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeGeoJsonTextOptionsFromJsOutput extracts valid text options from the input object', () => {
|
||||||
|
const input = {
|
||||||
|
getText: 'name',
|
||||||
|
getTextColor: [1, 2, 3, 255],
|
||||||
|
invalidOption: true,
|
||||||
|
};
|
||||||
|
const expectedOutput = {
|
||||||
|
getText: 'name',
|
||||||
|
getTextColor: [1, 2, 3, 255],
|
||||||
|
};
|
||||||
|
expect(computeGeoJsonTextOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeGeoJsonTextOptionsFromFormData computes text options based on form data', () => {
|
||||||
|
const formData: SqlaFormData = {
|
||||||
|
label_property_name: 'name',
|
||||||
|
label_color: { r: 1, g: 2, b: 3, a: 1 },
|
||||||
|
label_size: 123,
|
||||||
|
label_size_unit: 'pixels',
|
||||||
|
datasource: 'test_datasource',
|
||||||
|
viz_type: 'deck_geojson',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedOutput = {
|
||||||
|
getText: expect.any(Function),
|
||||||
|
getTextColor: [1, 2, 3, 255],
|
||||||
|
getTextSize: 123,
|
||||||
|
textSizeUnits: 'pixels',
|
||||||
|
};
|
||||||
|
|
||||||
|
const actualOutput = computeGeoJsonTextOptionsFromFormData(formData);
|
||||||
|
expect(actualOutput).toEqual(expectedOutput);
|
||||||
|
|
||||||
|
const sampleFeature = { properties: { name: 'Test' } };
|
||||||
|
expect(actualOutput.getText(sampleFeature)).toBe('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeGeoJsonIconOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||||
|
expect(computeGeoJsonIconOptionsFromJsOutput(null)).toEqual({});
|
||||||
|
expect(computeGeoJsonIconOptionsFromJsOutput(42)).toEqual({});
|
||||||
|
expect(computeGeoJsonIconOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||||
|
expect(computeGeoJsonIconOptionsFromJsOutput('string')).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeGeoJsonIconOptionsFromJsOutput extracts valid icon options from the input object', () => {
|
||||||
|
const input = {
|
||||||
|
getIcon: 'icon_name',
|
||||||
|
getIconColor: [1, 2, 3, 255],
|
||||||
|
invalidOption: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedOutput = {
|
||||||
|
getIcon: 'icon_name',
|
||||||
|
getIconColor: [1, 2, 3, 255],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(computeGeoJsonIconOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeGeoJsonIconOptionsFromFormData computes icon options based on form data', () => {
|
||||||
|
const formData: SqlaFormData = {
|
||||||
|
icon_url: 'https://example.com/icon.png',
|
||||||
|
icon_size: 123,
|
||||||
|
icon_size_unit: 'pixels',
|
||||||
|
datasource: 'test_datasource',
|
||||||
|
viz_type: 'deck_geojson',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedOutput = {
|
||||||
|
getIcon: expect.any(Function),
|
||||||
|
getIconSize: 123,
|
||||||
|
iconSizeUnits: 'pixels',
|
||||||
|
};
|
||||||
|
|
||||||
|
const actualOutput = computeGeoJsonIconOptionsFromFormData(formData);
|
||||||
|
expect(actualOutput).toEqual(expectedOutput);
|
||||||
|
|
||||||
|
expect(actualOutput.getIcon()).toEqual({
|
||||||
|
url: 'https://example.com/icon.png',
|
||||||
|
height: 128,
|
||||||
|
width: 128,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { GeoJsonLayer } from '@deck.gl/layers';
|
import { GeoJsonLayer, GeoJsonLayerProps } from '@deck.gl/layers';
|
||||||
// ignoring the eslint error below since typescript prefers 'geojson' to '@types/geojson'
|
// ignoring the eslint error below since typescript prefers 'geojson' to '@types/geojson'
|
||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { Feature, Geometry, GeoJsonProperties } from 'geojson';
|
import { Feature, Geometry, GeoJsonProperties } from 'geojson';
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
JsonValue,
|
JsonValue,
|
||||||
QueryFormData,
|
QueryFormData,
|
||||||
SetDataMaskHook,
|
SetDataMaskHook,
|
||||||
|
SqlaFormData,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +45,7 @@ import { TooltipProps } from '../../components/Tooltip';
|
|||||||
import { Point } from '../../types';
|
import { Point } from '../../types';
|
||||||
import { GetLayerType } from '../../factory';
|
import { GetLayerType } from '../../factory';
|
||||||
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
|
||||||
|
import { BLACK_COLOR, PRIMARY_COLOR } from '../../utilities/controls';
|
||||||
|
|
||||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||||
properties: JsonObject;
|
properties: JsonObject;
|
||||||
@@ -137,6 +139,114 @@ const getFillColor = (feature: JsonObject, filterStateValue: unknown[]) => {
|
|||||||
};
|
};
|
||||||
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
||||||
|
|
||||||
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
|
||||||
|
export const computeGeoJsonTextOptionsFromJsOutput = (
|
||||||
|
output: unknown,
|
||||||
|
): Partial<GeoJsonLayerProps> => {
|
||||||
|
if (!isObject(output)) return {};
|
||||||
|
|
||||||
|
// Properties sourced from:
|
||||||
|
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-2
|
||||||
|
const options: (keyof GeoJsonLayerProps)[] = [
|
||||||
|
'getText',
|
||||||
|
'getTextColor',
|
||||||
|
'getTextAngle',
|
||||||
|
'getTextSize',
|
||||||
|
'getTextAnchor',
|
||||||
|
'getTextAlignmentBaseline',
|
||||||
|
'getTextPixelOffset',
|
||||||
|
'getTextBackgroundColor',
|
||||||
|
'getTextBorderColor',
|
||||||
|
'getTextBorderWidth',
|
||||||
|
'textSizeUnits',
|
||||||
|
'textSizeScale',
|
||||||
|
'textSizeMinPixels',
|
||||||
|
'textSizeMaxPixels',
|
||||||
|
'textCharacterSet',
|
||||||
|
'textFontFamily',
|
||||||
|
'textFontWeight',
|
||||||
|
'textLineHeight',
|
||||||
|
'textMaxWidth',
|
||||||
|
'textWordBreak',
|
||||||
|
'textBackground',
|
||||||
|
'textBackgroundPadding',
|
||||||
|
'textOutlineColor',
|
||||||
|
'textOutlineWidth',
|
||||||
|
'textBillboard',
|
||||||
|
'textFontSettings',
|
||||||
|
];
|
||||||
|
|
||||||
|
const allEntries = Object.entries(output);
|
||||||
|
const validEntries = allEntries.filter(([k]) =>
|
||||||
|
options.includes(k as keyof GeoJsonLayerProps),
|
||||||
|
);
|
||||||
|
return Object.fromEntries(validEntries);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeGeoJsonTextOptionsFromFormData = (
|
||||||
|
fd: SqlaFormData,
|
||||||
|
): Partial<GeoJsonLayerProps> => {
|
||||||
|
const lc = fd.label_color ?? BLACK_COLOR;
|
||||||
|
|
||||||
|
return {
|
||||||
|
getText: (f: JsonObject) => f?.properties?.[fd.label_property_name],
|
||||||
|
getTextColor: [lc.r, lc.g, lc.b, 255 * lc.a],
|
||||||
|
getTextSize: parseInt(fd.label_size, 10),
|
||||||
|
textSizeUnits: fd.label_size_unit,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeGeoJsonIconOptionsFromJsOutput = (
|
||||||
|
output: unknown,
|
||||||
|
): Partial<GeoJsonLayerProps> => {
|
||||||
|
if (!isObject(output)) return {};
|
||||||
|
|
||||||
|
// Properties sourced from:
|
||||||
|
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-1
|
||||||
|
const options: (keyof GeoJsonLayerProps)[] = [
|
||||||
|
'getIcon',
|
||||||
|
'getIconSize',
|
||||||
|
'getIconColor',
|
||||||
|
'getIconAngle',
|
||||||
|
'getIconPixelOffset',
|
||||||
|
'iconSizeUnits',
|
||||||
|
'iconSizeScale',
|
||||||
|
'iconSizeMinPixels',
|
||||||
|
'iconSizeMaxPixels',
|
||||||
|
'iconAtlas',
|
||||||
|
'iconMapping',
|
||||||
|
'iconBillboard',
|
||||||
|
'iconAlphaCutoff',
|
||||||
|
];
|
||||||
|
|
||||||
|
const allEntries = Object.entries(output);
|
||||||
|
const validEntries = allEntries.filter(([k]) =>
|
||||||
|
options.includes(k as keyof GeoJsonLayerProps),
|
||||||
|
);
|
||||||
|
return Object.fromEntries(validEntries);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeGeoJsonIconOptionsFromFormData = (
|
||||||
|
fd: SqlaFormData,
|
||||||
|
): Partial<GeoJsonLayerProps> => ({
|
||||||
|
getIcon: fd.icon_url
|
||||||
|
? () => ({
|
||||||
|
url: fd.icon_url,
|
||||||
|
// This is the size deck.gl resizes the icon internally while preserving
|
||||||
|
// its aspect ratio. This is not the actual size the icon is rendered at,
|
||||||
|
// which is instead controlled by getIconSize below. These are set because
|
||||||
|
// deck.gl requires it, and 128x128 is a reasonable default. Read more at:
|
||||||
|
// https://deck.gl/docs/api-reference/layers/icon-layer#geticon
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
getIconSize: parseInt(fd.icon_size, 10),
|
||||||
|
iconSizeUnits: fd.icon_size_unit,
|
||||||
|
});
|
||||||
|
|
||||||
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||||
formData,
|
formData,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
@@ -147,8 +257,8 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
|||||||
emitCrossFilters,
|
emitCrossFilters,
|
||||||
}) {
|
}) {
|
||||||
const fd = formData;
|
const fd = formData;
|
||||||
const fc = fd.fill_color_picker;
|
const fc = fd.fill_color_picker ?? PRIMARY_COLOR;
|
||||||
const sc = fd.stroke_color_picker;
|
const sc = fd.stroke_color_picker ?? PRIMARY_COLOR;
|
||||||
const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a];
|
const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a];
|
||||||
const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a];
|
const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a];
|
||||||
const propOverrides: JsonObject = {};
|
const propOverrides: JsonObject = {};
|
||||||
@@ -169,6 +279,38 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
|||||||
processedFeatures = jsFnMutator(features) as ProcessedFeature[];
|
processedFeatures = jsFnMutator(features) as ProcessedFeature[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pointType = 'circle';
|
||||||
|
if (fd.enable_labels) {
|
||||||
|
pointType = `${pointType}+text`;
|
||||||
|
}
|
||||||
|
if (fd.enable_icons) {
|
||||||
|
pointType = `${pointType}+icon`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelOpts: Partial<GeoJsonLayerProps> = {};
|
||||||
|
if (fd.enable_labels) {
|
||||||
|
if (fd.enable_label_javascript_mode) {
|
||||||
|
const generator = sandboxedEval(fd.label_javascript_config_generator);
|
||||||
|
if (typeof generator === 'function') {
|
||||||
|
labelOpts = computeGeoJsonTextOptionsFromJsOutput(generator());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
labelOpts = computeGeoJsonTextOptionsFromFormData(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let iconOpts: Partial<GeoJsonLayerProps> = {};
|
||||||
|
if (fd.enable_icons) {
|
||||||
|
if (fd.enable_icon_javascript_mode) {
|
||||||
|
const generator = sandboxedEval(fd.icon_javascript_config_generator);
|
||||||
|
if (typeof generator === 'function') {
|
||||||
|
iconOpts = computeGeoJsonIconOptionsFromJsOutput(generator());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iconOpts = computeGeoJsonIconOptionsFromFormData(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new GeoJsonLayer({
|
return new GeoJsonLayer({
|
||||||
id: `geojson-layer-${fd.slice_id}` as const,
|
id: `geojson-layer-${fd.slice_id}` as const,
|
||||||
data: processedFeatures,
|
data: processedFeatures,
|
||||||
@@ -181,6 +323,9 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
|||||||
getLineWidth: fd.line_width || 1,
|
getLineWidth: fd.line_width || 1,
|
||||||
pointRadiusScale: fd.point_radius_scale,
|
pointRadiusScale: fd.point_radius_scale,
|
||||||
lineWidthUnits: fd.line_width_unit,
|
lineWidthUnits: fd.line_width_unit,
|
||||||
|
pointType,
|
||||||
|
...labelOpts,
|
||||||
|
...iconOpts,
|
||||||
...commonLayerProps({
|
...commonLayerProps({
|
||||||
formData: fd,
|
formData: fd,
|
||||||
setTooltip,
|
setTooltip,
|
||||||
|
|||||||
@@ -17,7 +17,12 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||||
import { t, legacyValidateInteger } from '@superset-ui/core';
|
import {
|
||||||
|
t,
|
||||||
|
legacyValidateInteger,
|
||||||
|
isFeatureEnabled,
|
||||||
|
FeatureFlag,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import { formatSelectOptions } from '../../utilities/utils';
|
import { formatSelectOptions } from '../../utilities/utils';
|
||||||
import {
|
import {
|
||||||
filterNulls,
|
filterNulls,
|
||||||
@@ -36,8 +41,27 @@ import {
|
|||||||
lineWidth,
|
lineWidth,
|
||||||
tooltipContents,
|
tooltipContents,
|
||||||
tooltipTemplate,
|
tooltipTemplate,
|
||||||
|
jsFunctionControl,
|
||||||
} from '../../utilities/Shared_DeckGL';
|
} from '../../utilities/Shared_DeckGL';
|
||||||
import { dndGeojsonColumn } from '../../utilities/sharedDndControls';
|
import { dndGeojsonColumn } from '../../utilities/sharedDndControls';
|
||||||
|
import { BLACK_COLOR } from '../../utilities/controls';
|
||||||
|
|
||||||
|
const defaultLabelConfigGenerator = `() => ({
|
||||||
|
// Check the documentation at:
|
||||||
|
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-2
|
||||||
|
getText: f => f.properties.name,
|
||||||
|
getTextColor: [0, 0, 0, 255],
|
||||||
|
getTextSize: 24,
|
||||||
|
textSizeUnits: 'pixels',
|
||||||
|
})`;
|
||||||
|
|
||||||
|
const defaultIconConfigGenerator = `() => ({
|
||||||
|
// Check the documentation at:
|
||||||
|
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-1
|
||||||
|
getIcon: () => ({ url: '', height: 128, width: 128 }),
|
||||||
|
getIconSize: 32,
|
||||||
|
iconSizeUnits: 'pixels',
|
||||||
|
})`;
|
||||||
|
|
||||||
const config: ControlPanelConfig = {
|
const config: ControlPanelConfig = {
|
||||||
controlPanelSections: [
|
controlPanelSections: [
|
||||||
@@ -63,6 +87,245 @@ const config: ControlPanelConfig = {
|
|||||||
[fillColorPicker, strokeColorPicker],
|
[fillColorPicker, strokeColorPicker],
|
||||||
[filled, stroked],
|
[filled, stroked],
|
||||||
[extruded],
|
[extruded],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'enable_labels',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Enable labels'),
|
||||||
|
description: t('Enables rendering of labels for GeoJSON points'),
|
||||||
|
default: false,
|
||||||
|
renderTrigger: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'enable_label_javascript_mode',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Enable label JavaScript mode'),
|
||||||
|
description: t(
|
||||||
|
'Enables custom label configuration via JavaScript',
|
||||||
|
),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_labels &&
|
||||||
|
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||||
|
default: false,
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'label_property_name',
|
||||||
|
config: {
|
||||||
|
type: 'TextControl',
|
||||||
|
label: t('Label property name'),
|
||||||
|
description: t('The feature property to use for point labels'),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_labels &&
|
||||||
|
(!form_data.enable_label_javascript_mode ||
|
||||||
|
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||||
|
default: 'name',
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'label_color',
|
||||||
|
config: {
|
||||||
|
type: 'ColorPickerControl',
|
||||||
|
label: t('Label color'),
|
||||||
|
description: t('The color of the point labels'),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_labels &&
|
||||||
|
(!form_data.enable_label_javascript_mode ||
|
||||||
|
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||||
|
default: BLACK_COLOR,
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'label_size',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
freeForm: true,
|
||||||
|
label: t('Label size'),
|
||||||
|
description: t('The font size of the point labels'),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_labels &&
|
||||||
|
(!form_data.enable_label_javascript_mode ||
|
||||||
|
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||||
|
validators: [legacyValidateInteger],
|
||||||
|
choices: formatSelectOptions([8, 16, 24, 32, 64, 128]),
|
||||||
|
default: 24,
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'label_size_unit',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
label: t('Label size unit'),
|
||||||
|
description: t('The unit for label size'),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_labels &&
|
||||||
|
(!form_data.enable_label_javascript_mode ||
|
||||||
|
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||||
|
choices: [
|
||||||
|
['meters', t('Meters')],
|
||||||
|
['pixels', t('Pixels')],
|
||||||
|
],
|
||||||
|
default: 'pixels',
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'label_javascript_config_generator',
|
||||||
|
config: {
|
||||||
|
...jsFunctionControl(
|
||||||
|
t('Label JavaScript config generator'),
|
||||||
|
t(
|
||||||
|
'A JavaScript function that generates a label configuration object',
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
defaultLabelConfigGenerator,
|
||||||
|
),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_labels &&
|
||||||
|
!!form_data.enable_label_javascript_mode &&
|
||||||
|
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'enable_icons',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Enable icons'),
|
||||||
|
description: t('Enables rendering of icons for GeoJSON points'),
|
||||||
|
default: false,
|
||||||
|
renderTrigger: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'enable_icon_javascript_mode',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Enable icon JavaScript mode'),
|
||||||
|
description: t(
|
||||||
|
'Enables custom icon configuration via JavaScript',
|
||||||
|
),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_icons &&
|
||||||
|
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||||
|
default: false,
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'icon_url',
|
||||||
|
config: {
|
||||||
|
type: 'TextControl',
|
||||||
|
label: t('Icon URL'),
|
||||||
|
description: t(
|
||||||
|
'The image URL of the icon to display for GeoJSON points. ' +
|
||||||
|
'Note that the image URL must conform to the content ' +
|
||||||
|
'security policy (CSP) in order to load correctly.',
|
||||||
|
),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_icons &&
|
||||||
|
(!form_data.enable_icon_javascript_mode ||
|
||||||
|
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||||
|
default: '',
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'icon_size',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
freeForm: true,
|
||||||
|
label: t('Icon size'),
|
||||||
|
description: t('The size of the point icons'),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_icons &&
|
||||||
|
(!form_data.enable_icon_javascript_mode ||
|
||||||
|
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||||
|
validators: [legacyValidateInteger],
|
||||||
|
choices: formatSelectOptions([16, 24, 32, 64, 128]),
|
||||||
|
default: 32,
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'icon_size_unit',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
label: t('Icon size unit'),
|
||||||
|
description: t('The unit for icon size'),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_icons &&
|
||||||
|
(!form_data.enable_icon_javascript_mode ||
|
||||||
|
!isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
|
||||||
|
choices: [
|
||||||
|
['meters', t('Meters')],
|
||||||
|
['pixels', t('Pixels')],
|
||||||
|
],
|
||||||
|
default: 'pixels',
|
||||||
|
renderTrigger: true,
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'icon_javascript_config_generator',
|
||||||
|
config: {
|
||||||
|
...jsFunctionControl(
|
||||||
|
t('Icon JavaScript config generator'),
|
||||||
|
t(
|
||||||
|
'A JavaScript function that generates an icon configuration object',
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
defaultIconConfigGenerator,
|
||||||
|
),
|
||||||
|
visibility: ({ form_data }) =>
|
||||||
|
!!form_data.enable_icons &&
|
||||||
|
!!form_data.enable_icon_javascript_mode &&
|
||||||
|
isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
|
||||||
|
resetOnHide: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
[lineWidth],
|
[lineWidth],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const jsFunctionInfo = (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
function jsFunctionControl(
|
export function jsFunctionControl(
|
||||||
label: string,
|
label: string,
|
||||||
description: string,
|
description: string,
|
||||||
extraDescr = null,
|
extraDescr = null,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function columnChoices(datasource: Dataset | QueryResponse | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 };
|
export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 };
|
||||||
|
export const BLACK_COLOR = { r: 0, g: 0, b: 0, a: 1 };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
default: null,
|
default: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user