Skip to content

Commit

Permalink
geosolutions-it#10026: Interactive legend for TOC layers
Browse files Browse the repository at this point in the history
Description:
- Adding interactive legend for wfs and vector layers
- Add checkbox for enable the interactive legend for wfs and vector into layer settings and for the catalog as well [just for wfs]
- write unit tests based on code changes
  • Loading branch information
mahmoudadel54 committed Apr 8, 2024
1 parent c291f32 commit 85617f2
Show file tree
Hide file tree
Showing 33 changed files with 1,038 additions and 109 deletions.
18 changes: 17 additions & 1 deletion web/client/actions/__tests__/layerFilter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
DISCARD_CURRENT_FILTER,
discardCurrentFilter,
applyFilter,
APPLY_FILTER
APPLY_FILTER,
LAYER_FILTER_BY_LEGEND,
layerFilterByLegend,
RESET_LAYER_FILTER_BY_LEGEND,
resetLegendFilter
} from '../layerFilter';


Expand Down Expand Up @@ -64,4 +68,16 @@ describe('Test correctness of the layerFilter actions', () => {
expect(retval.type).toBe(APPLY_FILTER);
});

it('layerFilterByLegend', () => {
var retval = layerFilterByLegend();
expect(retval).toExist();
expect(retval.type).toBe(LAYER_FILTER_BY_LEGEND);
});

it('resetLegendFilter', () => {
var retval = resetLegendFilter();
expect(retval).toExist();
expect(retval.type).toBe(RESET_LAYER_FILTER_BY_LEGEND);
});

});
5 changes: 3 additions & 2 deletions web/client/actions/layerFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ export function layerFilterByLegend(layerId, nodeType, legendCQLFilter) {
};
}

export function resetLegendFilter(style) {
export function resetLegendFilter(reason, value) {
return {
type: RESET_LAYER_FILTER_BY_LEGEND,
newSelectedStyle: style
reason, // here the reason for reset is change 'style' or change the enable/disable interactive legend config 'disableEnableInteractiveLegend'
value
};
}
1 change: 1 addition & 0 deletions web/client/api/WMS.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export const getJsonWMSLegend = (url) => {
const request = layerLegendJsonData[url]
? () => Promise.resolve(layerLegendJsonData[url])
: () => axios.get(url).then(({ data }) => {
layerLegendJsonData[url] = data?.Legend;
return data?.Legend || [];
});
return request().then((data) => data);
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]).toExist();
expect(result[0].layerName).toExist();
expect(result[0].layerName).toEqual('layer');
expect(result[0].rules).toExist();
expect(result[0].rules.length).toEqual(2);
done();
} catch (ex) {
done(ex);
}
});
});
});
11 changes: 9 additions & 2 deletions web/client/api/catalog/WFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ const searchAndPaginate = (json = {}, startPosition, maxRecords, text) => {
};
};

const recordToLayer = (record) => {
const recordToLayer = (record, {
service
}) => {
// this is for getting the configration of interactive legend
const {
layerOptions
} = service || {};
return {
type: record.type || "wfs",
search: {
Expand All @@ -90,7 +96,8 @@ const recordToLayer = (record) => {
description: record.description || "",
bbox: record.boundingBox,
links: getRecordLinks(record),
...record.layerOptions
...record.layerOptions,
...layerOptions
};
};

Expand Down
5 changes: 1 addition & 4 deletions web/client/components/TOC/DefaultLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ class DefaultLayer extends React.Component {
sortableStyle: PropTypes.object,
activateLegendTool: PropTypes.bool,
activateOpacityTool: PropTypes.bool,
legendType: PropTypes.string,
indicators: PropTypes.array,
visibilityCheckType: PropTypes.string,
currentZoomLvl: PropTypes.number,
Expand Down Expand Up @@ -77,7 +76,6 @@ class DefaultLayer extends React.Component {
onSelect: () => {},
activateLegendTool: false,
activateOpacityTool: true,
legendType: '',
indicators: [],
visibilityCheckType: "glyph",
additionalTools: [],
Expand Down Expand Up @@ -137,15 +135,14 @@ class DefaultLayer extends React.Component {
<div key="legend" position="collapsible" className="collapsible-toc">
<Grid fluid>
{this.props.showFullTitleOnExpand ? <Row><Col xs={12} className="toc-full-title">{this.getTitle(this.props.node)}</Col></Row> : null}
{/** todo: add wmsJsonLegend here */}
{this.props.activateLegendTool && this.props.node.type === 'wms' &&
<Row>
<Col xs={12}>
<WMSLegend onLayerFilterByLegend={this.props.onLayerFilterByLegend} node={this.props.node} currentZoomLvl={this.props.currentZoomLvl} scales={this.props.scales} language={this.props.language} {...this.props.legendOptions} />
</Col>
</Row>}
{this.props.activateLegendTool && ['wfs', 'vector'].includes(this.props.node.type) &&
<StyleBasedLegend style={this.props.node.style}/>
<StyleBasedLegend style={this.props.node.style} onLayerFilterByLegend={this.props.onLayerFilterByLegend} layer={this.props.node} />
}
</Grid>
{this.renderOpacitySlider(this.props.hideOpacityTooltip)}
Expand Down
22 changes: 18 additions & 4 deletions web/client/components/TOC/fragments/StyleBasedLegend.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import RuleLegendIcon from '../../styleeditor/RuleLegendIcon';
import {parseGeoStylerFilterToCql} from '../../../utils/StyleEditorUtils';
function StyleBasedLegend({ style, layer, onLayerFilterByLegend }) {

function StyleBasedLegend({ style }) {

const handleLegendFilter = (filter) => {
if (!layer?.enableInteractiveLegend || !layer?.visibility) return;
const cql = filter ? parseGeoStylerFilterToCql(filter) : filter;
const isLegendFilterIncluded = layer?.layerFilter?.filters?.find(f=>f.id === 'interactiveLegend');
const prevFilter = isLegendFilterIncluded ? isLegendFilterIncluded?.filters?.[0]?.body : '';
onLayerFilterByLegend(layer.id, 'layers', cql === prevFilter ? '' : cql);
};
const renderRules = (rules) => {
return (rules || []).map((rule) => {
return (<div className="wfs-legend-rule" key={rule.ruleId}>
const isLegendFilterIncluded = layer?.layerFilter?.filters?.find(f=>f.id === 'interactiveLegend');
const prevFilter = isLegendFilterIncluded ? isLegendFilterIncluded?.filters?.[0]?.body : '';
// if isLegendFilterIncluded && rule.filter ---> get cql to compare current with prev filter
const ruleFilter = rule.filter && isLegendFilterIncluded ? parseGeoStylerFilterToCql(rule.filter) : '';

return (<div className={`wfs-legend-rule ${layer?.enableInteractiveLegend && layer?.visibility ? 'json-legend-rule' : ''} ${ruleFilter && prevFilter === ruleFilter ? 'active' : ''}`} key={rule.ruleId || rule.name} onClick={()=>handleLegendFilter(rule?.filter)}>
<RuleLegendIcon rule={rule} />
<span>{rule.name || ''}</span>
</div>);
Expand All @@ -30,7 +42,9 @@ function StyleBasedLegend({ style }) {
}

StyleBasedLegend.propTypes = {
style: PropTypes.object
style: PropTypes.object,
layer: PropTypes.object,
onLayerFilterByLegend: PropTypes.func
};

export default StyleBasedLegend;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2015, GeoSolutions Sas.
* Copyright 2024, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
Expand Down Expand Up @@ -56,13 +56,19 @@ class StyleBasedWMSJsonLegend extends React.Component {
componentDidUpdate(prevProps) {
const prevLayerStyle = prevProps?.layer?.style;
const currentLayerStyle = this.props?.layer?.style;
// get the new json legend and rerender it in case change style
if (currentLayerStyle !== prevLayerStyle) {
this.getLegendData();
}
}

getLegendData() {
let jsonLegendUrl = this.getUrl(this.props);
if (!jsonLegendUrl) {
this.setState({ error: true });
return;
}
this.setState({ loading: true });
getJsonWMSLegend(jsonLegendUrl).then(data => {
this.setState({ jsonLegend: data[0], loading: false });
}).catch(() => {
Expand Down
Loading

0 comments on commit 85617f2

Please sign in to comment.