From 40c21786f9c04cfa55c4ea5db52aea83d1204d1e Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Tue, 20 May 2025 20:23:24 -0600 Subject: [PATCH 1/6] Implement new "Canonical" render type for vector layers This render type uses a hex code in a vector attribute to define each feature's color. --- .../vector_layer/VectorRendering.tsx | 14 ++- .../vector_layer/types/Canonical.tsx | 99 +++++++++++++++++++ packages/base/src/tools.ts | 36 +++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx diff --git a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx index 3fa94d712..55a5aa352 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { ISymbologyDialogProps } from '../symbologyDialog'; +import Canonical from './types/Canonical'; import Categorized from './types/Categorized'; import Graduated from './types/Graduated'; import Heatmap from './types/Heatmap'; @@ -36,7 +37,7 @@ const VectorRendering = ({ setSelectedRenderType(renderType); const options: Record = { - VectorLayer: ['Single Symbol', 'Graduated', 'Categorized', 'Heatmap'], + VectorLayer: ['Single Symbol', 'Graduated', 'Categorized', 'Canonical', 'Heatmap'], VectorTileLayer: ['Single Symbol'], HeatmapLayer: ['Single Symbol', 'Graduated', 'Categorized', 'Heatmap'] }; @@ -78,6 +79,17 @@ const VectorRendering = ({ /> ); break; + case 'Canonical': + RenderComponent = ( + + ); + break; case 'Heatmap': RenderComponent = ( { + const selectedValueRef = useRef(); + + const [selectedValue, setSelectedValue] = useState(''); + const [attributes, setAttributes] = useState>>({}); + + if (!layerId) { + return; + } + const layer = model.getLayer(layerId); + if (!layer?.parameters) { + return; + } + const { featureProperties } = useGetProperties({ + layerId, + model: model + }); + + useEffect(() => { + okSignalPromise.promise.then(okSignal => { + okSignal.connect(handleOk, this); + }); + + return () => { + okSignalPromise.promise.then(okSignal => { + okSignal.disconnect(handleOk, this); + }); + }; + }, [selectedValue]); + + useEffect(() => { + // Filter for hex color code attributes + const hexAttributes = getColorCodeFeatureAttributes(featureProperties); + + setAttributes(hexAttributes); + + const layerParams = layer.parameters as IVectorLayer; + const value = + layerParams.symbologyState?.value ?? Object.keys(hexAttributes)[0]; + + setSelectedValue(value); + }, [featureProperties]); + + useEffect(() => { + selectedValueRef.current = selectedValue; + }, [selectedValue]); + + const handleOk = () => { + if (!layer.parameters) { + return; + } + + const colorExpr: ExpressionValue[] = ['get', selectedValue]; + const newStyle = {...layer.parameters.color}; + newStyle['fill-color'] = colorExpr; + newStyle['stroke-color'] = colorExpr; + newStyle['circle-fill-color'] = colorExpr; + + const symbologyState = { + renderType: 'Canonical', + value: selectedValueRef.current, + }; + + layer.parameters.symbologyState = symbologyState; + layer.parameters.color = newStyle; + if (layer.type === 'HeatmapLayer') { + layer.type = 'VectorLayer'; + } + + model.sharedModel.updateLayer(layerId, layer); + cancel(); + }; + + return ( +
+ +
+ ); +}; + +export default Canonical; diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 59aa534cd..8473531b6 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -855,6 +855,12 @@ export const stringToArrayBuffer = async ( return await base64Response.arrayBuffer(); }; +/** + * Get attributes of the feature which are numeric. + * + * @param featureProperties - Attributes of a feature. + * @returns - Attributes which are numeric. + */ export const getNumericFeatureAttributes = ( featureProperties: Record> ) => { @@ -876,6 +882,36 @@ export const getNumericFeatureAttributes = ( return filteredRecord; }; +// TODO: Extract the outer logic of the getSomthingFeatureAttributes functions +// to a generic function which accepts a tester function as an arg. + +/** + * Get attributes of the feature which look like hex color codes. + * + * @param featureProperties - Attributes of a feature. + * @returns - Attributes which look like hex color codes. + */ +export const getColorCodeFeatureAttributes = ( + featureProperties: Record> +): Record> => { + const filteredRecord: Record> = {}; + + for (const [key, set] of Object.entries(featureProperties)) { + const firstValue = set.values().next().value; + + // Check if the first value is a string that can be parsed as a hex color code + const regex = new RegExp('^#[0-9a-f]{6}$') + const isHexCode = + typeof firstValue === 'string' && regex.test(firstValue) + + if (isHexCode) { + filteredRecord[key] = set; + } + } + + return filteredRecord; +}; + export function downloadFile( content: BlobPart, fileName: string, From f3674b39e9a0978254165eceb981506fcc81b0a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 02:29:30 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../base/src/dialogs/symbology/vector_layer/types/Canonical.tsx | 2 +- packages/base/src/tools.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx index 1f7ec0d3d..654c97dab 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx @@ -43,7 +43,7 @@ const Canonical = ({ }, [selectedValue]); useEffect(() => { - // Filter for hex color code attributes + // Filter for hex color code attributes const hexAttributes = getColorCodeFeatureAttributes(featureProperties); setAttributes(hexAttributes); diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 8473531b6..3147b7db6 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -908,7 +908,7 @@ export const getColorCodeFeatureAttributes = ( filteredRecord[key] = set; } } - + return filteredRecord; }; From 22c4517bc6f0a5a87afc23e146ed55ca68c47803 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Tue, 20 May 2025 21:01:21 -0600 Subject: [PATCH 3/6] Extract generic function to dedup two very similar functions --- packages/base/src/tools.ts | 64 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 3147b7db6..9b371f994 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -855,35 +855,40 @@ export const stringToArrayBuffer = async ( return await base64Response.arrayBuffer(); }; -/** - * Get attributes of the feature which are numeric. - * - * @param featureProperties - Attributes of a feature. - * @returns - Attributes which are numeric. - */ -export const getNumericFeatureAttributes = ( - featureProperties: Record> -) => { - // We only want number values here - const filteredRecord: Record> = {}; +const getFeatureAttributes = ( + featureProperties: Record>, + predicate: (key: string, value: any) => boolean = (key: string, value) => true, +): Record> => { + const filteredRecord: Record> = {}; for (const [key, set] of Object.entries(featureProperties)) { const firstValue = set.values().next().value; + const isValid = predicate(key, firstValue); - // Check if the first value is a string that cannot be parsed as a number - const isInvalidString = - typeof firstValue === 'string' && isNaN(Number(firstValue)); - - if (!isInvalidString) { + if (isValid) { filteredRecord[key] = set; } } return filteredRecord; -}; +} -// TODO: Extract the outer logic of the getSomthingFeatureAttributes functions -// to a generic function which accepts a tester function as an arg. +/** + * Get attributes of the feature which are numeric. + * + * @param featureProperties - Attributes of a feature. + * @returns - Attributes which are numeric. + */ +export const getNumericFeatureAttributes = ( + featureProperties: Record> +): Record> => { + return getFeatureAttributes( + featureProperties, + (_: string, value) => { + return !(typeof value === 'string' && isNaN(Number(value))); + } + ); +}; /** * Get attributes of the feature which look like hex color codes. @@ -894,22 +899,13 @@ export const getNumericFeatureAttributes = ( export const getColorCodeFeatureAttributes = ( featureProperties: Record> ): Record> => { - const filteredRecord: Record> = {}; - - for (const [key, set] of Object.entries(featureProperties)) { - const firstValue = set.values().next().value; - - // Check if the first value is a string that can be parsed as a hex color code - const regex = new RegExp('^#[0-9a-f]{6}$') - const isHexCode = - typeof firstValue === 'string' && regex.test(firstValue) - - if (isHexCode) { - filteredRecord[key] = set; + return getFeatureAttributes( + featureProperties, + (_, value) => { + const regex = new RegExp('^#[0-9a-f]{6}$'); + return (typeof value === 'string' && regex.test(value)); } - } - - return filteredRecord; + ); }; export function downloadFile( From 647bd871645e0fddd8632fe31eb0e6009049a759 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Tue, 20 May 2025 21:02:46 -0600 Subject: [PATCH 4/6] Lint! :bell: --- .../vector_layer/VectorRendering.tsx | 8 ++++++- .../vector_layer/types/Canonical.tsx | 4 ++-- packages/base/src/tools.ts | 24 +++++++------------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx index 55a5aa352..93334d5c7 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx @@ -37,7 +37,13 @@ const VectorRendering = ({ setSelectedRenderType(renderType); const options: Record = { - VectorLayer: ['Single Symbol', 'Graduated', 'Categorized', 'Canonical', 'Heatmap'], + VectorLayer: [ + 'Single Symbol', + 'Graduated', + 'Categorized', + 'Canonical', + 'Heatmap' + ], VectorTileLayer: ['Single Symbol'], HeatmapLayer: ['Single Symbol', 'Graduated', 'Categorized', 'Heatmap'] }; diff --git a/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx b/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx index 654c97dab..3b5ef999a 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/types/Canonical.tsx @@ -65,14 +65,14 @@ const Canonical = ({ } const colorExpr: ExpressionValue[] = ['get', selectedValue]; - const newStyle = {...layer.parameters.color}; + const newStyle = { ...layer.parameters.color }; newStyle['fill-color'] = colorExpr; newStyle['stroke-color'] = colorExpr; newStyle['circle-fill-color'] = colorExpr; const symbologyState = { renderType: 'Canonical', - value: selectedValueRef.current, + value: selectedValueRef.current }; layer.parameters.symbologyState = symbologyState; diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 9b371f994..c505e8fe2 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -857,7 +857,7 @@ export const stringToArrayBuffer = async ( const getFeatureAttributes = ( featureProperties: Record>, - predicate: (key: string, value: any) => boolean = (key: string, value) => true, + predicate: (key: string, value: any) => boolean = (key: string, value) => true ): Record> => { const filteredRecord: Record> = {}; @@ -871,7 +871,7 @@ const getFeatureAttributes = ( } return filteredRecord; -} +}; /** * Get attributes of the feature which are numeric. @@ -882,12 +882,9 @@ const getFeatureAttributes = ( export const getNumericFeatureAttributes = ( featureProperties: Record> ): Record> => { - return getFeatureAttributes( - featureProperties, - (_: string, value) => { - return !(typeof value === 'string' && isNaN(Number(value))); - } - ); + return getFeatureAttributes(featureProperties, (_: string, value) => { + return !(typeof value === 'string' && isNaN(Number(value))); + }); }; /** @@ -899,13 +896,10 @@ export const getNumericFeatureAttributes = ( export const getColorCodeFeatureAttributes = ( featureProperties: Record> ): Record> => { - return getFeatureAttributes( - featureProperties, - (_, value) => { - const regex = new RegExp('^#[0-9a-f]{6}$'); - return (typeof value === 'string' && regex.test(value)); - } - ); + return getFeatureAttributes(featureProperties, (_, value) => { + const regex = new RegExp('^#[0-9a-f]{6}$'); + return typeof value === 'string' && regex.test(value); + }); }; export function downloadFile( From caf90c2b415ac2d56ba1c1447b32202ef8468c22 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Wed, 21 May 2025 10:27:23 -0600 Subject: [PATCH 5/6] Check feature properties before offering render type options --- .../vector_layer/VectorRendering.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx index 93334d5c7..5fb6189e8 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx @@ -5,6 +5,8 @@ import Categorized from './types/Categorized'; import Graduated from './types/Graduated'; import Heatmap from './types/Heatmap'; import SimpleSymbol from './types/SimpleSymbol'; +import { useGetProperties } from '../hooks/useGetProperties'; +import { getColorCodeFeatureAttributes, getNumericFeatureAttributes } from '../../../tools'; const VectorRendering = ({ model, @@ -29,6 +31,11 @@ const VectorRendering = ({ return; } + const { featureProperties } = useGetProperties({ + layerId, + model: model + }); + useEffect(() => { let renderType = layer.parameters?.symbologyState?.renderType; if (!renderType) { @@ -36,14 +43,19 @@ const VectorRendering = ({ } setSelectedRenderType(renderType); + let vectorLayerOptions = [ + 'Single Symbol', + 'Heatmap' + ] + if (getColorCodeFeatureAttributes(featureProperties)) { + vectorLayerOptions.push('Canonical') + } + if (getNumericFeatureAttributes(featureProperties)) { + vectorLayerOptions.push('Graduated', 'Categorized') + } + const options: Record = { - VectorLayer: [ - 'Single Symbol', - 'Graduated', - 'Categorized', - 'Canonical', - 'Heatmap' - ], + VectorLayer: vectorLayerOptions, VectorTileLayer: ['Single Symbol'], HeatmapLayer: ['Single Symbol', 'Graduated', 'Categorized', 'Heatmap'] }; From f4186d2f444227bb7b718e939b1178f6a03b6161 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Wed, 21 May 2025 14:07:52 -0600 Subject: [PATCH 6/6] Check render types are supported before displaying them --- .../vector_layer/VectorRendering.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx index 5fb6189e8..73686f0fd 100644 --- a/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx +++ b/packages/base/src/dialogs/symbology/vector_layer/VectorRendering.tsx @@ -6,7 +6,10 @@ import Graduated from './types/Graduated'; import Heatmap from './types/Heatmap'; import SimpleSymbol from './types/SimpleSymbol'; import { useGetProperties } from '../hooks/useGetProperties'; -import { getColorCodeFeatureAttributes, getNumericFeatureAttributes } from '../../../tools'; +import { + getColorCodeFeatureAttributes, + getNumericFeatureAttributes +} from '../../../tools'; const VectorRendering = ({ model, @@ -43,15 +46,17 @@ const VectorRendering = ({ } setSelectedRenderType(renderType); - let vectorLayerOptions = [ - 'Single Symbol', - 'Heatmap' - ] - if (getColorCodeFeatureAttributes(featureProperties)) { - vectorLayerOptions.push('Canonical') + const vectorLayerOptions = ['Single Symbol', 'Heatmap']; + + if ( + Object.keys(getColorCodeFeatureAttributes(featureProperties)).length > 0 + ) { + vectorLayerOptions.push('Canonical'); } - if (getNumericFeatureAttributes(featureProperties)) { - vectorLayerOptions.push('Graduated', 'Categorized') + if ( + Object.keys(getNumericFeatureAttributes(featureProperties)).length > 0 + ) { + vectorLayerOptions.push('Graduated', 'Categorized'); } const options: Record = { @@ -60,7 +65,7 @@ const VectorRendering = ({ HeatmapLayer: ['Single Symbol', 'Graduated', 'Categorized', 'Heatmap'] }; setRenderTypeOptions(options[layer.type]); - }, []); + }, [featureProperties]); useEffect(() => { switch (selectedRenderType) {