Skip to content

Commit

Permalink
Fix #9916: handle Stat Query Engine integration in Visual Style Editor (
Browse files Browse the repository at this point in the history
#9971)


---------

Co-authored-by: allyoucanmap <stefano.bovio@geosolutionsgroup.com>
  • Loading branch information
mahmoudadel54 and allyoucanmap committed Feb 29, 2024
1 parent 5261361 commit d3d5fbc
Show file tree
Hide file tree
Showing 33 changed files with 462 additions and 46 deletions.
27 changes: 22 additions & 5 deletions web/client/api/StyleEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/

import { isEqual, get, castArray } from 'lodash';
import { isEqual, get, castArray, isString } from 'lodash';

import axios from '../libs/ajax';
import StylesAPI from './geoserver/Styles';
Expand Down Expand Up @@ -223,11 +223,27 @@ const defaultClassificationRequest = ({
params,
styleService
}) => {
const paramSLDService = {

const viewparams = Object.keys(params.msViewParams || {})
.map((key) => {
const property = params.msViewParams[key];
if (property === undefined) {
return null;
}
// arrays need escaped comma
// strings need sourronding single quotes
const value = castArray(property)
.map(val => isString(val) ? `'${val}'` : val)
.join('\\,');
return `${key}:${value}`;
}).filter((value) => value).join(';');

let paramSLDService = {
intervals: params.intervals,
method: params.method,
attribute: params.attribute,
intervalsForUnique: params.intervalsForUnique
intervalsForUnique: params.intervalsForUnique,
...(viewparams && { viewparams })
};
return axios.get(SLDService.getStyleMetadataService(layer, paramSLDService, styleService));
};
Expand Down Expand Up @@ -258,7 +274,8 @@ export function classificationVector({
'reverse',
'attribute',
'ramp',
'intervalsForUnique'
'intervalsForUnique',
'msViewParams'
];
let params = { ...properties, ...values };
const { ruleId } = properties;
Expand Down Expand Up @@ -287,7 +304,7 @@ export function classificationVector({

const previousParams = paramsKeys.reduce((acc, key) => ({ ...acc, [key]: properties[key] }), {});
const currentParams = paramsKeys.reduce((acc, key) => ({ ...acc, [key]: params[key] }), {});
const validParameters = !paramsKeys.find(key => params[key] === undefined);
const validParameters = !paramsKeys.filter((key) => key !== 'msViewParams').find(key => params[key] === undefined);
const needsRequest = validParameters && !isEqual(previousParams, currentParams)
// not request if the entries are updated manually
&& values?.ramp !== 'custom'
Expand Down
3 changes: 2 additions & 1 deletion web/client/api/__tests__/StyleEditor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import CLASSIFY_VECTOR_RESPONSE from './classifyVectorResponse.json';
import CLASSIFY_RASTER_RESPONSE from './classifyRaterResponse.json';

describe('StyleEditor API', () => {
const DEFAULT_CONFIG = { intervalsForUnique: 10 };
const DEFAULT_CONFIG = { intervalsForUnique: 10, msViewParams: {} };
describe('classificationVector', () => {
beforeEach(done => {
mockAxios = new MockAdapter(axios);
Expand Down Expand Up @@ -551,6 +551,7 @@ describe('StyleEditor API', () => {
attribute: 'ATTRIBUTE',
type: 'classificationVector',
classification: [1, 2, 3, 4, 5],
...DEFAULT_CONFIG,
...param
};
const rules = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ class ThematicLayer extends React.Component {
title={this.localizedItem('toc.thematic.data_panel', '')}
buttons={[{
glyph: 'cog',
tooltip: this.localizedItem('toc.thematic.go_to_cfg', ''),
tooltip: this.localizedItem('toc.thematic.goToCfg', ''),
visible: this.props.canEditThematic,
onClick: this.toggleCfg
}]}
Expand Down
4 changes: 3 additions & 1 deletion web/client/components/misc/codeEditors/JSONEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Message from '../../I18N/Message';

import CodeMirror from '../../../libs/codemirror/react-codemirror-suspense';

export default ({onValid, onError, json = {}}) => {
export default ({onValid, onError, editorRef, json = {}, editorWillUnmount = () => {} }) => {
const [code, setCode] = useState(JSON.stringify(json, true, 2));
const [error, setError] = useState();
// parse code and set options
Expand All @@ -27,6 +27,8 @@ export default ({onValid, onError, json = {}}) => {
}, [code]);
return (<div className="code-editor">
<CodeMirror
ref={editorRef}
editorWillUnmount={() => editorWillUnmount(code)}
value={code}
onBeforeChange={(_, __, value) => {
setCode(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import expect from 'expect';
import React from 'react';
import ReactDOM from 'react-dom';

import Toolbar from '../Toolbar';

describe("Toolbar component", () => {
Expand Down
158 changes: 158 additions & 0 deletions web/client/components/styleeditor/ClassificationLayerSettings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright 2024, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React, { useState } from 'react';
import { Alert, Glyphicon } from 'react-bootstrap';
import { ControlledPopover } from './Popover';
import Toolbar from '../misc/toolbar/Toolbar';
import tooltip from '../misc/enhancers/tooltip';
import ButtonRB from '../misc/Button';
import JSONEditor from '../misc/codeEditors/JSONEditor';
import { isEqual } from 'lodash';
import Message from '../I18N/Message';
import {getMessageById} from '../../utils/LocaleUtils';

const Button = tooltip(ButtonRB);

const INITIAL_CODE_VALUE = {
"params": []
};
/**
* Component to change the layer classification options
* @memberof components.styleeditor
* @name ClassificationLayerSettings
* @prop {object} thematicCustomParams parameters to update
* @prop {function} onUpdate returns the updated value
*/
function ClassificationLayerSettings({
onUpdate,
thematicCustomParams,
editorRef
}, { messages }) {
const [open, setOpen] = useState(false);
const [isValidJson, setValid] = useState(true);
const [alert, setAlert] = useState(false);

const validateEnteredParams = (jsonParams) =>{
let valid = true;
jsonParams.params.forEach((item) => {
let isObject = typeof item === 'object';
let hasFieldProp = item?.field;
let hasValues = item?.values?.length && item?.values?.length >= 1;
let hasCorrectValues = hasValues ? item?.values.every(val => val?.value && val?.name) : false;
if (!(isObject && hasFieldProp && hasCorrectValues)) {
valid = false;
}
});
return valid;
};

// onValid handler: validate teh written json in run-time
const onValid = (config) => {
if (!config?.params?.length) return;
let isValid = validateEnteredParams(config);
if (!isValid) {
setValid(false);
throw new Error(getMessageById(messages, "styleeditor.wrongFormatMsg"));
} else {
!isValidJson && setValid(true);
}
return;
};
const onError = () => {
setValid(false);
};

// fired on editorWillUnmount
const saveChanges = (code) => {
try {
const config = JSON.parse(code);
let isValid = validateEnteredParams(config);
if (isValid && !isEqual(config, thematicCustomParams)) {
onUpdate(config);
}
} catch (e) { /**/ }
};

const onToggle = () => {
// info: showing an alert if entered json not valid
if (isValidJson) {
setOpen(prev => !prev);
} else {
setAlert(true);
}
};

return (
<ControlledPopover
open={open}
onClick={() => onToggle()}
placement={'right'}
content={
<div className="ms-classification-layer-settings">
<div className="ms-classification-layer-settings-title">
<Message msgId="styleeditor.customParams" />
<Button
className="square-button-md no-border"
onClick={() => onToggle()}
>
<Glyphicon
glyph="1-close"
/>
</Button>
</div>
<JSONEditor
json={thematicCustomParams?.params ? thematicCustomParams : INITIAL_CODE_VALUE}
editorWillUnmount={saveChanges}
onValid={onValid}
onError={onError}
editorRef={editorRef}
/>
{alert &&
<div className="ms-style-editor-alert">
<Alert bsStyle="warning">
<p><Message msgId="styleeditor.alertCustomParamsNotValid" /></p>
<p>
<Toolbar
buttons={[
{
text: <Message msgId="styleeditor.stayInTextareaEditor" />,
onClick: () => {
setAlert(false);
setOpen(true);
},
style: { marginRight: 4 }
},
{
bsStyle: 'primary',
text: <Message msgId="styleeditor.closeCustomParamsEditor" />,
onClick: () => {
setValid(true);
setAlert(false);
setOpen(false);
}
}
]}
/>
</p>
</Alert>
</div>}
</div>}
>
<Button
className="square-button-md no-border"
tooltipId="toc.thematic.goToCfg">
<Glyphicon
glyph="cog"
/>
</Button>
</ControlledPopover>
);
}

export default ClassificationLayerSettings;
11 changes: 7 additions & 4 deletions web/client/components/styleeditor/ClassificationSymbolizer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ function ClassificationSymbolizer({
supportedSymbolizerMenuOptions,
fonts,
enableFieldExpression,
thematicCustomParamsProperty, // property: [ { YEAR: 2024 }, { MONTH: 5 } ]
...props
}) {

const {
ramp,
method,
Expand All @@ -42,7 +42,8 @@ function ClassificationSymbolizer({
intervalsForUnique = config?.intervalsForUnique || 100,
reverse,
continuous,
format
format,
customParams // customParams values: e.g: { YEAR: 2024, MONTH: 5 }
} = props;

// needed for slider
Expand All @@ -58,7 +59,8 @@ function ClassificationSymbolizer({
reverse,
ramp,
continuous,
classification
classification,
customParams: customParams || {}
};

function handleColors() {
Expand Down Expand Up @@ -108,7 +110,8 @@ function ClassificationSymbolizer({
method,
methodEdit: props?.methodEdit,
fonts,
enableFieldExpression
enableFieldExpression,
thematicCustomParams: thematicCustomParamsProperty
}}
params={mergedParams}
onChange={(values) => onUpdate({
Expand Down
57 changes: 57 additions & 0 deletions web/client/components/styleeditor/Fields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,63 @@ export const fields = {
/>
</PropertyField>
);
},
customParams: (props) => {
const {
label,
value,
config: {
isValid
},
thematicCustomParams,
onChange
} = props;
// if there is no params it will be hidden
if (!(thematicCustomParams?.length)) {
return null;
}
const valid = !isValid || isValid({ value });
const handleCustomParamChange = (key, selectedVal) => {
onChange({
[key]: selectedVal
});
};

return (
<div style={{ width: '100%' }}>
<hr />
<div className="ms-symbolizer-field"><Message msgId={label} /></div>
<div>
{thematicCustomParams?.map(param=> {
const currentValue = value ? value[param?.field] : undefined;
const valueObj = param?.values?.find(val => val.value === currentValue);
return (
<PropertyField
key={param?.field}
label={param?.title || param?.field}
invalid={!valid}>
<SelectInput
{...props}
value={valueObj?.value
? { value: valueObj.value, label: valueObj.name ?? valueObj.value }
: currentValue }
onChange={(val)=> handleCustomParamChange(param.field, val)}
config={{...props.config, getOptions: () => {
return param?.values?.map(val => {
return {
value: val.value,
label: val.name
};
});
}}}
/>
</PropertyField>
);
})}
</div>
<hr />
</div>
);
}
};

Expand Down
4 changes: 3 additions & 1 deletion web/client/components/styleeditor/RulesEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ const RulesEditor = forwardRef(({
svgSymbolsPath,
lineDashOptions,
supportedSymbolizerMenuOptions,
enableFieldExpression
enableFieldExpression,
thematicCustomParams // layer.thematic.params in layers state
} = config;

// needed for slider
Expand Down Expand Up @@ -373,6 +374,7 @@ const RulesEditor = forwardRef(({
onReplace={handleReplaceRule}
format={format}
enableFieldExpression={enableFieldExpression}
thematicCustomParamsProperty={thematicCustomParams} // layer.thematic.params in layers state
/>
: symbolizers.map(({ kind = '', symbolizerId, ...properties }) => {
const { params, glyph, hideMenu } = getSymbolizerInfo(kind);
Expand Down
Loading

0 comments on commit d3d5fbc

Please sign in to comment.