Skip to content

Commit

Permalink
#10736: Interactive legend for TOC layers [WFS Layer part]
Browse files Browse the repository at this point in the history
Description:
- handle the functionality of interactive legend for WFS layers
- create geostyler converter to cql
- handle saving the legend filter of layer into layerFilter object in saving
- handle showing incompatible message in case change into filtered style
- add unit tests
- add translations
  • Loading branch information
mahmoudadel54 committed Jan 13, 2025
1 parent d1ebec1 commit d095d2f
Show file tree
Hide file tree
Showing 21 changed files with 719 additions and 38 deletions.
10 changes: 8 additions & 2 deletions web/client/api/catalog/WFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ const searchAndPaginate = (json = {}, startPosition, maxRecords, text) => {
};
};

const recordToLayer = (record) => {
const recordToLayer = (record, {
service
}) => {
const {
layerOptions
} = service || {};
return {
type: record.type || "wfs",
search: {
Expand All @@ -85,7 +90,8 @@ const recordToLayer = (record) => {
description: record.description || "",
bbox: record.boundingBox,
links: getRecordLinks(record),
...record.layerOptions
...record.layerOptions,
...layerOptions
};
};

Expand Down
37 changes: 37 additions & 0 deletions web/client/components/TOC/fragments/settings/Display.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import ThreeDTilesSettings from './ThreeDTilesSettings';
import ModelTransformation from './ModelTransformation';
import StyleBasedWMSJsonLegend from '../../../../plugins/TOC/components/StyleBasedWMSJsonLegend';
import { getMiscSetting } from '../../../../utils/ConfigUtils';
import VectorLegend from '../../../../plugins/TOC/components/VectorLegend';

export default class extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -351,6 +352,42 @@ export default class extends React.Component {
</Col>
</div>
</Row>}
{this.props.element.type === "wfs" && <Row>
<div className={"legend-options"}>
<Col xs={12} className={"legend-label"}>
<label key="legend-options-title" className="control-label"><Message msgId="layerProperties.legendOptions.title" /></label>
</Col>
{ experimentalInteractiveLegend && this.props.element?.serverType !== ServerTypes.NO_VENDOR && !this.props?.hideInteractiveLegendOption &&
<Col xs={12} className="first-selectize">
<Checkbox
data-qa="display-interactive-legend-option"
value="enableInteractiveLegend"
key="enableInteractiveLegend"
onChange={(e) => {
if (!e.target.checked) {
const newLayerFilter = updateLayerLegendFilter(this.props.element.layerFilter);
this.props.onChange("layerFilter", newLayerFilter );
}
this.props.onChange("enableInteractiveLegend", e.target.checked);
}}
checked={enableInteractiveLegend} >
<Message msgId="layerProperties.enableInteractiveLegendInfo.label"/>
&nbsp;<InfoPopover text={<Message msgId="layerProperties.enableInteractiveLegendInfo.info" />} />
</Checkbox>
</Col>
}
{enableInteractiveLegend && <Col xs={12} className="legend-preview">
<ControlLabel><Message msgId="layerProperties.legendOptions.legendPreview" /></ControlLabel>
<div style={this.setOverFlow() && this.state.containerStyle || {}} ref={this.containerRef} >
<VectorLegend
owner="legendPreview"
layer={this.props.element}
style={this.props.element.style || {}}
/>
</div>
</Col>}
</div>
</Row>}
</Grid>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('test Layer Properties Display module component', () => {
expect(labels[6].innerText).toBe("layerProperties.legendOptions.legendHeight");
expect(labels[7].innerText).toBe("layerProperties.legendOptions.legendPreview");
});
it('tests Layer Properties Legend component events', () => {
it('tests wms Layer Properties Legend component events', () => {
const l = {
name: 'layer00',
title: 'Layer',
Expand Down Expand Up @@ -369,4 +369,43 @@ describe('test Layer Properties Display module component', () => {
expect(inputs[11].value).toBe("20");
expect(inputs[12].value).toBe("40");
});
it('tests wfs Layer Properties Legend component events', () => {
const l = {
name: 'layer00',
title: 'Layer',
visibility: true,
storeIndex: 9,
type: 'wfs',
url: 'fakeurl',
legendOptions: {
legendWidth: 15,
legendHeight: 15
},
enableInteractiveLegend: false
};
const settings = {
options: {
opacity: 1
}
};
const handlers = {
onChange() {}
};
let spy = expect.spyOn(handlers, "onChange");
const comp = ReactDOM.render(<Display element={l} settings={settings} onChange={handlers.onChange}/>, document.getElementById("container"));
expect(comp).toBeTruthy();
const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" );
const legendPreview = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "legend-preview" );
expect(legendPreview).toBeTruthy();
expect(inputs).toBeTruthy();
expect(inputs.length).toBe(6);
let interactiveLegendConfig = document.querySelector(".legend-options input[data-qa='display-interactive-legend-option']");
// change enableInteractiveLegend to enable interactive legend
interactiveLegendConfig.checked = true;
ReactTestUtils.Simulate.change(interactiveLegendConfig);
expect(spy).toHaveBeenCalled();
expect(spy.calls[0].arguments[0]).toEqual("enableInteractiveLegend");
expect(spy.calls[0].arguments[1]).toEqual(true);
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FormGroup, Checkbox } from "react-bootstrap";

import Message from "../../../I18N/Message";
import InfoPopover from '../../../widgets/widget/InfoPopover';
import { getMiscSetting } from '../../../../utils/ConfigUtils';

/**
* Common Advanced settings form WMS/CSW/WMTS/WFS
Expand All @@ -24,30 +25,41 @@ export default ({
service,
onChangeServiceProperty = () => { },
onToggleThumbnail = () => { }
}) => (
<>
<FormGroup controlId="autoload" key="autoload">
{service.autoload !== undefined && <Checkbox value="autoload" onChange={(e) => onChangeServiceProperty("autoload", e.target.checked)}
checked={!isNil(service.autoload) ? service.autoload : false}>
<Message msgId="catalog.autoload" />
</Checkbox>}
</FormGroup>
<FormGroup controlId="thumbnail" key="thumbnail">
<Checkbox
onChange={() => onToggleThumbnail()}
checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}>
<Message msgId="catalog.showPreview" />
</Checkbox>
</FormGroup>
}) => {
const experimentalInteractiveLegend = getMiscSetting('experimentalInteractiveLegend', false);
return (
<>
<FormGroup controlId="autoload" key="autoload">
{service.autoload !== undefined && <Checkbox value="autoload" onChange={(e) => onChangeServiceProperty("autoload", e.target.checked)}
checked={!isNil(service.autoload) ? service.autoload : false}>
<Message msgId="catalog.autoload" />
</Checkbox>}
</FormGroup>
<FormGroup controlId="thumbnail" key="thumbnail">
<Checkbox
onChange={() => onToggleThumbnail()}
checked={!isNil(service.hideThumbnail) ? !service.hideThumbnail : true}>
<Message msgId="catalog.showPreview" />
</Checkbox>
</FormGroup>

{!isNil(service.type) && service.type === "cog" &&
{!isNil(service.type) && service.type === "cog" &&
<FormGroup controlId="fetchMetadata" key="fetchMetadata">
<Checkbox
onChange={(e) => onChangeServiceProperty("fetchMetadata", e.target.checked)}
checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : true}>
<Message msgId="catalog.fetchMetadata.label" />&nbsp;<InfoPopover text={<Message msgId="catalog.fetchMetadata.tooltip" />} />
</Checkbox>
</FormGroup>}
{children}
</>
);
{experimentalInteractiveLegend && ['wfs'].includes(service.type) && <FormGroup className="wfs-interactive-legend" controlId="enableInteractiveLegend" key="enableInteractiveLegend">
<Checkbox data-qa="display-interactive-legend-option"
onChange={(e) => onChangeServiceProperty("layerOptions", { ...service.layerOptions, enableInteractiveLegend: e.target.checked})}
checked={!isNil(service.layerOptions?.enableInteractiveLegend) ? service.layerOptions?.enableInteractiveLegend : false}>
<Message msgId="layerProperties.enableInteractiveLegendInfo.label" />
&nbsp;<InfoPopover text={<Message msgId="layerProperties.enableInteractiveLegendInfo.info" />} />
</Checkbox>
</FormGroup>}
{children}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import ReactDOM from "react-dom";
import CommonAdvancedSettings from "../CommonAdvancedSettings";
import expect from "expect";
import TestUtils from "react-dom/test-utils";
import { setConfigProp } from '../../../../../utils/ConfigUtils';

describe('Test common advanced settings', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setConfigProp('miscSettings', { experimentalInteractiveLegend: true });
setTimeout(done);
});
afterEach((done) => {
Expand Down Expand Up @@ -85,4 +87,14 @@ describe('Test common advanced settings', () => {
expect(spyOn).toHaveBeenCalled();
expect(spyOn.calls[1].arguments).toEqual([ 'fetchMetadata', false ]);
});
it('test showing/hiding interactive legend checkbox', () => {
ReactDOM.render(<CommonAdvancedSettings
service={{type: "wfs"}}
/>, document.getElementById("container"));
const interactiveLegendCheckboxInput = document.querySelector(".wfs-interactive-legend .checkbox input[data-qa='display-interactive-legend-option']");
expect(interactiveLegendCheckboxInput).toBeTruthy();
const interactiveLegendLabel = document.querySelector(".wfs-interactive-legend .checkbox span");
expect(interactiveLegendLabel).toBeTruthy();
expect(interactiveLegendLabel.innerHTML).toEqual('layerProperties.enableInteractiveLegendInfo.label');
});
});
3 changes: 3 additions & 0 deletions web/client/plugins/TOC/components/DefaultLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ const DefaultLayerNode = ({
<li>
<VectorLegend
style={node?.style}
layer={node}
owner="toc"
onChange={onChange}
/>
</li>
</>
Expand Down
69 changes: 60 additions & 9 deletions web/client/plugins/TOC/components/VectorLegend.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,68 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { Alert } from 'react-bootstrap';

import Message from '../../../components/I18N/Message';
import { ButtonWithTooltip } from '../../../components/misc/Button';
import RuleLegendIcon from '../../../components/styleeditor/RuleLegendIcon';
import { INTERACTIVE_LEGEND_ID } from '../../../utils/LegendUtils';
import { updateLayerWFSVectorLegendFilter } from '../../../utils/FilterUtils';

/**
* VectorLegend renders the legend given a valid vector style
* @prop {object} style a layer style object in geostyler format
* @prop {object} layer the vector layer object
* @prop {string} owner the owner of the compoenent
* @prop {function} onChange the onChange layer handler
*/
function VectorLegend({ style }) {

function VectorLegend({ style, layer, owner, onChange }) {
const onResetLegendFilter = () => {
const newLayerFilter = updateLayerWFSVectorLegendFilter(layer?.layerFilter);
onChange({ layerFilter: newLayerFilter });
};
const filterLayerHandler = (filter) => {
const isFilterDisabled = layer?.layerFilter?.disabled;
if (!filter || isFilterDisabled) return;
const newLayerFilter = updateLayerWFSVectorLegendFilter(layer?.layerFilter, filter);
onChange({ layerFilter: newLayerFilter });
};
const checkPreviousFiltersAreValid = (rules, prevLegendFilters) => {
const rulesFilters = rules.map(rule => rule?.filter?.toString());
return prevLegendFilters?.every(f => rulesFilters.includes(f.id));
};
const renderRules = (rules) => {
return (rules || []).map((rule) => {
return (<div className="ms-legend-rule" key={rule.ruleId}>
<RuleLegendIcon rule={rule} />
<span>{rule.name || ''}</span>
</div>);
});
const layerFilter = get(layer, 'layerFilter', {});
const interactiveLegendFilters = get(layerFilter, 'filters', []).find(f => f.id === INTERACTIVE_LEGEND_ID);
const legendFilters = get(interactiveLegendFilters, 'filters', []);
const showResetWarning = !checkPreviousFiltersAreValid(rules, legendFilters) && !layerFilter.disabled;

return (<>
{showResetWarning && owner !== 'legendPreview' ? <Alert bsStyle="warning">
<div><Message msgId={"layerProperties.interactiveLegend.incompatibleWFSFilterWarning"} /></div>
<ButtonWithTooltip
bsStyle="primary"
bsSize="xs"
style={{ marginTop: 4 }}
onClick={onResetLegendFilter}>
<Message msgId={"layerProperties.interactiveLegend.resetLegendFilter"} />
</ButtonWithTooltip>
</Alert> : null}
{isEmpty(rules)
? <Message msgId={"layerProperties.interactiveLegend.noLegendData"} />
: (rules || []).map((rule, idx) => {
const isFilterDisabled = layer?.layerFilter?.disabled;
const activeFilter = legendFilters?.some(f => f.id === rule?.filter?.toString());
return (<div key={`${rule.filter}-${idx}`}
onClick={() => filterLayerHandler(rule.filter)}
className={`ms-legend-rule ${isFilterDisabled || owner === 'legendPreview' || !rule?.filter ? "" : "filter-enabled "} ${activeFilter ? 'active' : ''}`}>
<RuleLegendIcon rule={rule} />
<span>{rule.name || ''}</span>
</div>);
})}
</>);
};

return <>
Expand All @@ -33,7 +81,10 @@ function VectorLegend({ style }) {
}

VectorLegend.propTypes = {
style: PropTypes.object
style: PropTypes.object,
layer: PropTypes.object,
owner: PropTypes.string,
onChange: PropTypes.func
};

export default VectorLegend;
Loading

0 comments on commit d095d2f

Please sign in to comment.