Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#10026: Interactive legend for TOC layers [WMS] #10180

Merged
merged 18 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
124e01b
#10026: Interactive legend for TOC layers
mahmoudadel54 Mar 24, 2024
c291f32
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 2, 2024
85617f2
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 8, 2024
5277175
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 9, 2024
a29974f
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 9, 2024
161178a
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 10, 2024
b0a2693
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 15, 2024
3af0429
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 15, 2024
c76bd16
#10026: Interactive legend for TOC layers
mahmoudadel54 Apr 15, 2024
d177ccf
#10026: Interactive legend for TOC layers [Resolve review comments]
mahmoudadel54 Apr 18, 2024
6b7e7e3
#10026: Interactive legend for TOC layers [Resolve review comments]
mahmoudadel54 Apr 19, 2024
ff42bc6
#10026: Interactive legend for TOC layers [Resolve review comments]
mahmoudadel54 Apr 19, 2024
7b9bc99
#10026: Interactive legend for TOC layers [Resolve review comments]
mahmoudadel54 Apr 19, 2024
bab1218
Update layerFilter.js
mahmoudadel54 Apr 19, 2024
2c0eaa0
#10026: Interactive legend for TOC layers [WMS]
mahmoudadel54 Apr 19, 2024
65e5deb
#10026: Interactive legend for TOC layers [WMS]
mahmoudadel54 Apr 19, 2024
4aa80c9
#10026: Interactive legend for TOC layers [Resolve review comments]
mahmoudadel54 Apr 22, 2024
d39a0d0
Merge branch 'feature_10026' of https://github.com/mahmoudadel54/MapS…
mahmoudadel54 Apr 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion web/client/actions/layerFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const APPLY_FILTER = 'LAYER_FILTER:APPLY_FILTER';
*/
export const OPEN_QUERY_BUILDER = 'LAYER_FILTER:OPEN_QUERY_BUILDER';

export const LAYER_FILTER_BY_LEGEND = 'LAYER_FILTER:LAYER_FILTER_BY_LEGEND';

export const RESET_LAYER_FILTER_BY_LEGEND = 'LAYER_FILTER:RESET_LAYER_FILTER_BY_LEGEND';
mahmoudadel54 marked this conversation as resolved.
Show resolved Hide resolved

export function storeCurrentFilter() {
return {
Expand Down Expand Up @@ -61,4 +64,3 @@ export function initLayerFilter(filter) {
filter
};
}

mahmoudadel54 marked this conversation as resolved.
Show resolved Hide resolved
16 changes: 16 additions & 0 deletions web/client/api/WMS.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,22 @@ export const getSupportedFormat = (url, includeGFIFormats = false) => {
.catch(() => includeGFIFormats ? { imageFormats: [], infoFormats: [] } : []);
};

let layerLegendJsonData = {};
export const getJsonWMSLegend = (url) => {
const request = layerLegendJsonData[url]
? () => Promise.resolve(layerLegendJsonData[url])
: () => axios.get(url).then((response) => {
if (typeof response?.data === 'string' && response.data.includes("Exception")) {
throw new Error("Faild to get json legend");
}
layerLegendJsonData[url] = response?.data?.Legend;
return response?.data?.Legend || [];
});
return request().then((data) => data).catch(err => {
throw err;
});
};

const Api = {
flatLayers,
parseUrl,
Expand Down
67 changes: 67 additions & 0 deletions web/client/api/__tests__/WMS-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,70 @@ describe('Test correctness of the WMS APIs (mock axios)', () => {
API.describeLayer(url, layers, { query });
});
});

describe('Test get json wms graphic legend (mock axios)', () => {
beforeEach(done => {
mockAxios = new MockAdapter(axios);
setTimeout(done);
});

afterEach(done => {
mockAxios.restore();
setTimeout(done);
});

it('get json wms graphic legend', (done) => {
let url = "http://localhost:8080/geoserver/wms?service=WMS&request=GetLegendGraphic&format=application/json&layers=workspace:layer&style=pophade&version=1.3.0&SLD_VERSION=1.1.0";
mockAxios.onGet().reply(() => {
return [ 200, {
"Legend": [{
"layerName": "layer",
"title": "Layer",
"rules": [
{
"name": ">= 159.05 and < 5062.5",
"filter": "[field >= '159.05' AND field < '5062.5']",
"symbolizers": [{"Polygon": {
"uom": "in/72",
"stroke": "#ffffff",
"stroke-width": "1.0",
"stroke-opacity": "0.35",
"stroke-linecap": "butt",
"stroke-linejoin": "miter",
"fill": "#8DD3C7",
"fill-opacity": "0.75"
}}]
},
{
"name": ">= 5062.5 and < 20300.35",
"filter": "[field >= '5062.5' AND field < '20300.35']",
"symbolizers": [{"Polygon": {
"uom": "in/72",
"stroke": "#ffffff",
"stroke-width": "1.0",
"stroke-opacity": "0.35",
"stroke-linecap": "butt",
"stroke-linejoin": "miter",
"fill": "#ABD9C5",
"fill-opacity": "0.75"
}}]
}]
}]
}];
});

API.getJsonWMSLegend(url).then(result => {
try {
expect(result.length).toEqual(1);
expect(result[0]).toBeTruthy();
expect(result[0].layerName).toBeTruthy();
expect(result[0].layerName).toEqual('layer');
expect(result[0].rules).toBeTruthy();
expect(result[0].rules.length).toEqual(2);
done();
} catch (ex) {
done(ex);
}
});
});
});
67 changes: 53 additions & 14 deletions web/client/components/TOC/fragments/settings/Display.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { clamp, isNil, isNumber } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import {Checkbox, Col, ControlLabel, FormGroup, Glyphicon, Grid, Row, Button as ButtonRB } from 'react-bootstrap';
import {Checkbox, Col, ControlLabel, FormGroup, Glyphicon, Grid, Row, Button as ButtonRB, Tooltip } from 'react-bootstrap';
import tooltip from '../../../misc/enhancers/buttonTooltip';
const Button = tooltip(ButtonRB);
import IntlNumberFormControl from '../../../I18N/IntlNumberFormControl';
Expand All @@ -18,11 +18,14 @@ import InfoPopover from '../../../widgets/widget/InfoPopover';
import Legend from '../../../../plugins/TOC/components/Legend';
import VisibilityLimitsForm from './VisibilityLimitsForm';
import { ServerTypes } from '../../../../utils/LayersUtils';
import {updateLayerLegendFilter} from '../../../../utils/FilterUtils';
import Select from 'react-select';
import { getSupportedFormat } from '../../../../api/WMS';
import WMSCacheOptions from './WMSCacheOptions';
import ThreeDTilesSettings from './ThreeDTilesSettings';
import ModelTransformation from './ModelTransformation';
import StyleBasedWMSJsonLegend from '../../../../plugins/TOC/components/StyleBasedWMSJsonLegend';
import OverlayTrigger from '../../../misc/OverlayTrigger';
export default class extends React.Component {
static propTypes = {
opacityText: PropTypes.node,
Expand Down Expand Up @@ -216,7 +219,6 @@ export default class extends React.Component {
layer={this.props.element}
onChange={this.props.onChange}
/>

{this.props.element.type === "wms" &&
<Row>
<Col xs={12}>
Expand Down Expand Up @@ -261,7 +263,32 @@ export default class extends React.Component {
<Col xs={12} className={"legend-label"}>
<label key="legend-options-title" className="control-label"><Message msgId="layerProperties.legendOptions.title" /></label>
</Col>
<Col xs={12} sm={6} className="first-selectize">
{ this.props.element?.serverType !== ServerTypes.NO_VENDOR &&
<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={this.props.element.enableInteractiveLegend} >
<Message msgId="layerProperties.enableInteractiveLegendInfo.label"/>
&nbsp;<InfoPopover text={<Message msgId="layerProperties.enableInteractiveLegendInfo.tooltip" />} /> &nbsp;
<OverlayTrigger placement={"bottom"} overlay={<Tooltip id={"interactiveLegendInfo"}>
<Message msgId={"layerProperties.enableInteractiveLegendInfo.info"} />
</Tooltip>}>
<Glyphicon
style={{ marginLeft: 4 }}
glyph={"info-sign"}
/>
</OverlayTrigger>
</Checkbox>
}
{!this.props.element?.enableInteractiveLegend && <><Col xs={12} sm={6} className="first-selectize">
<FormGroup validationState={this.getValidationState("legendWidth")}>
<ControlLabel><Message msgId="layerProperties.legendOptions.legendWidth" /></ControlLabel>
<IntlNumberFormControl
Expand Down Expand Up @@ -290,20 +317,32 @@ export default class extends React.Component {
onBlur={this.onBlur}
/>
</FormGroup>
</Col>
</Col></>}
<Col xs={12} className="legend-preview">
<ControlLabel><Message msgId="layerProperties.legendOptions.legendPreview" /></ControlLabel>
<div style={this.setOverFlow() && this.state.containerStyle || {}} ref={this.containerRef} >
<Legend
style={this.setOverFlow() && {} || undefined}
layer={this.props.element}
legendHeight={
this.useLegendOptions() && this.state.legendOptions.legendHeight || undefined}
legendWidth={
this.useLegendOptions() && this.state.legendOptions.legendWidth || undefined}
language={
this.props.isLocalizedLayerStylesEnabled ? this.props.currentLocaleLanguage : undefined}
/>
{ this.props.element?.enableInteractiveLegend ?
<StyleBasedWMSJsonLegend
owner="legendPreview"
style={this.setOverFlow() && {} || undefined}
layer={this.props.element}
legendHeight={
this.useLegendOptions() && this.state.legendOptions.legendHeight || undefined}
legendWidth={
this.useLegendOptions() && this.state.legendOptions.legendWidth || undefined}
language={
this.props.isLocalizedLayerStylesEnabled ? this.props.currentLocaleLanguage : undefined}
/> :
<Legend
style={this.setOverFlow() && {} || undefined}
layer={this.props.element}
legendHeight={
this.useLegendOptions() && this.state.legendOptions.legendHeight || undefined}
legendWidth={
this.useLegendOptions() && this.state.legendOptions.legendWidth || undefined}
language={
this.props.isLocalizedLayerStylesEnabled ? this.props.currentLocaleLanguage : undefined}
/>}
</div>
</Col>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ describe('test Layer Properties Display module component', () => {
// wrap in a stateful component, stateless components render return null
// see: https://facebook.github.io/react/docs/top-level-api.html#reactdom.render
const comp = ReactDOM.render(<Display element={l} settings={settings} />, document.getElementById("container"));
expect(comp).toExist();
expect(comp).toBeTruthy();
const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" );
expect(inputs).toExist();
expect(inputs).toBeTruthy();
expect(inputs.length).toBe(5);
ReactTestUtils.Simulate.focus(inputs[0]);
expect(inputs[0].value).toBe('100');
Expand All @@ -71,10 +71,10 @@ describe('test Layer Properties Display module component', () => {
// wrap in a stateful component, stateless components render return null
// see: https://facebook.github.io/react/docs/top-level-api.html#reactdom.render
const comp = ReactDOM.render(<Display element={l} settings={settings} onChange={handlers.onChange}/>, document.getElementById("container"));
expect(comp).toExist();
expect(comp).toBeTruthy();
const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" );
expect(inputs).toExist();
expect(inputs.length).toBe(13);
expect(inputs).toBeTruthy();
expect(inputs.length).toBe(14);
ReactTestUtils.Simulate.focus(inputs[2]);
expect(inputs[2].value).toBe('70');
inputs[8].click();
Expand Down Expand Up @@ -109,7 +109,7 @@ describe('test Layer Properties Display module component', () => {
};

const comp = ReactDOM.render(<Display element={l} settings={settings} onChange={handlers.onChange}/>, document.getElementById("container"));
expect(comp).toExist();
expect(comp).toBeTruthy();
const formatRefresh = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "format-refresh" );
ReactTestUtils.Simulate.click(formatRefresh[0]);
});
Expand Down Expand Up @@ -165,7 +165,7 @@ describe('test Layer Properties Display module component', () => {
};
ReactDOM.render(<Display isLocalizedLayerStylesEnabled element={l} settings={settings}/>, document.getElementById("container"));
const isLocalizedLayerStylesOption = document.querySelector('[data-qa="display-lacalized-layer-styles-option"]');
expect(isLocalizedLayerStylesOption).toExist();
expect(isLocalizedLayerStylesOption).toBeTruthy();
});

it('tests Display component for wms with force proxy option displayed', () => {
Expand Down Expand Up @@ -243,11 +243,11 @@ describe('test Layer Properties Display module component', () => {
onChange() {}
};
const comp = ReactDOM.render(<Display element={l} settings={settings} onChange={handlers.onChange}/>, document.getElementById("container"));
expect(comp).toExist();
expect(comp).toBeTruthy();
const labels = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "control-label" );
const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" );
const legendWidth = inputs[11];
const legendHeight = inputs[12];
const legendWidth = inputs[12];
const legendHeight = inputs[13];
// Default legend values
expect(legendWidth.value).toBe('12');
expect(legendHeight.value).toBe('12');
Expand All @@ -269,7 +269,8 @@ describe('test Layer Properties Display module component', () => {
legendOptions: {
legendWidth: 15,
legendHeight: 15
}
},
enableInteractiveLegend: false
};
const settings = {
options: {
Expand All @@ -281,14 +282,15 @@ describe('test Layer Properties Display module component', () => {
};
let spy = expect.spyOn(handlers, "onChange");
const comp = ReactDOM.render(<Display element={l} settings={settings} onChange={handlers.onChange}/>, document.getElementById("container"));
expect(comp).toExist();
expect(comp).toBeTruthy();
const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" );
const legendPreview = ReactTestUtils.scryRenderedDOMComponentsWithClass( comp, "legend-preview" );
expect(legendPreview).toExist();
expect(inputs).toExist();
expect(inputs.length).toBe(13);
let legendWidth = inputs[11];
let legendHeight = inputs[12];
expect(legendPreview).toBeTruthy();
expect(inputs).toBeTruthy();
expect(inputs.length).toBe(14);
let interactiveLegendConfig = inputs[11];
let legendWidth = inputs[12];
let legendHeight = inputs[13];
const img = ReactTestUtils.scryRenderedDOMComponentsWithTag(comp, 'img');

// Check value in img src
Expand Down Expand Up @@ -329,6 +331,14 @@ describe('test Layer Properties Display module component', () => {
expect(params.get("width")).toBe('12');
expect(params.get("height")).toBe('12');

// change enableInteractiveLegend to enable interactive legend
interactiveLegendConfig.checked = true;
ReactTestUtils.Simulate.change(interactiveLegendConfig);
expect(spy).toHaveBeenCalled();
expect(spy.calls[4].arguments[0]).toEqual("enableInteractiveLegend");
expect(spy.calls[4].arguments[1]).toEqual(true);


});

it("tests Layer Properties Legend component with values from layers", () => {
Expand All @@ -350,11 +360,11 @@ describe('test Layer Properties Display module component', () => {
}
};
const comp = ReactDOM.render(<Display element={l} settings={settings}/>, document.getElementById("container"));
expect(comp).toExist();
expect(comp).toBeTruthy();
const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" );
expect(inputs).toExist();
expect(inputs.length).toBe(13);
expect(inputs[11].value).toBe("20");
expect(inputs[12].value).toBe("40");
expect(inputs).toBeTruthy();
expect(inputs.length).toBe(14);
expect(inputs[12].value).toBe("20");
expect(inputs[13].value).toBe("40");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,28 @@ export default ({
<Select
value={service.layerOptions?.serverType}
options={serverTypeOptions}
onChange={event => onChangeServiceProperty("layerOptions", { ...service.layerOptions, serverType: event?.value })} />
onChange={event => {
if (event?.value === ServerTypes.NO_VENDOR) onChangeServiceProperty("layerOptions", { ...service.layerOptions, enableInteractiveLegend: undefined});
onChangeServiceProperty("layerOptions", { ...service.layerOptions, serverType: event?.value });
}} />
</InputGroup>
</FormGroup>
{![ServerTypes.NO_VENDOR].includes(service.layerOptions?.serverType) && ['wms', 'csw'].includes(service.type) && <FormGroup 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.tooltip" />} /> &nbsp;
<OverlayTrigger placement={"bottom"} overlay={<Tooltip id={"interactiveLegendInfo"}>
<Message msgId={"layerProperties.enableInteractiveLegendInfo.info"} />
</Tooltip>}>
<Glyphicon
style={{ marginLeft: 4 }}
glyph={"info-sign"}
/>
</OverlayTrigger>
</Checkbox>
</FormGroup>}
<hr style={{margin: "8px 0"}}/>
<FormGroup style={advancedRasterSettingsStyles} className="form-group-flex">
<ControlLabel className="strong"><Message msgId="layerProperties.format.title" /></ControlLabel>
Expand Down
Loading
Loading