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 wms layers
- Add checkbox for enable the interactive legend for wms into layer settings and for the catalog as well
  • Loading branch information
mahmoudadel54 committed Mar 24, 2024
1 parent 35e1073 commit 124e01b
Show file tree
Hide file tree
Showing 23 changed files with 595 additions and 34 deletions.
9 changes: 9 additions & 0 deletions web/client/actions/layerFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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 function storeCurrentFilter() {
return {
Expand Down Expand Up @@ -62,3 +63,11 @@ export function initLayerFilter(filter) {
};
}

export function layerFilterByLegend(layerId, nodeType, legendCQLFilter) {
return {
type: LAYER_FILTER_BY_LEGEND,
legendCQLFilter,
nodeType,
layerId
};
}
10 changes: 10 additions & 0 deletions web/client/api/WMS.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,16 @@ 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(({ data }) => {
return data?.Legend || [];
});
return request().then((data) => data);
};

const Api = {
flatLayers,
parseUrl,
Expand Down
7 changes: 6 additions & 1 deletion web/client/components/TOC/DefaultLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ 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 All @@ -51,6 +52,7 @@ class DefaultLayer extends React.Component {
selectedNodes: PropTypes.array,
filterText: PropTypes.string,
onUpdateNode: PropTypes.func,
onLayerFilterByLegend: PropTypes.func,
titleTooltip: PropTypes.bool,
filter: PropTypes.func,
showFullTitleOnExpand: PropTypes.bool,
Expand All @@ -75,6 +77,7 @@ class DefaultLayer extends React.Component {
onSelect: () => {},
activateLegendTool: false,
activateOpacityTool: true,
legendType: '',
indicators: [],
visibilityCheckType: "glyph",
additionalTools: [],
Expand All @@ -83,6 +86,7 @@ class DefaultLayer extends React.Component {
selectedNodes: [],
filterText: '',
onUpdateNode: () => {},
onLayerFilterByLegend: () => {},
filter: () => true,
titleTooltip: false,
showFullTitleOnExpand: false,
Expand Down Expand Up @@ -133,10 +137,11 @@ 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 node={this.props.node} currentZoomLvl={this.props.currentZoomLvl} scales={this.props.scales} language={this.props.language} {...this.props.legendOptions} />
<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) &&
Expand Down
135 changes: 135 additions & 0 deletions web/client/components/TOC/fragments/StyleBasedWMSJsonLegend.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Copyright 2015, 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 urlUtil from 'url';

import { isArray, isNil } from 'lodash';
import assign from 'object-assign';
import PropTypes from 'prop-types';
import React from 'react';
import Loader from '../../misc/Loader';
import WMSJsonLegendIcon from '../../styleeditor/WMSJsonLegendIcon';
import {
addAuthenticationParameter,
addAuthenticationToSLD,
clearNilValuesForParams
} from '../../../utils/SecurityUtils';
import { getJsonWMSLegend } from '../../../api/WMS';
import Message from '../../I18N/Message';

class Legend extends React.Component {
static propTypes = {
layer: PropTypes.object,
legendHeight: PropTypes.number,
legendWidth: PropTypes.number,
legendOptions: PropTypes.string,
style: PropTypes.object,
currentZoomLvl: PropTypes.number,
scales: PropTypes.array,
scaleDependent: PropTypes.bool,
language: PropTypes.string,
onLayerFilterByLegend: PropTypes.func
};

static defaultProps = {
legendHeight: 12,
legendWidth: 12,
legendOptions: "forceLabels:on",
style: {maxWidth: "100%"},
scaleDependent: true,
onLayerFilterByLegend: () => {}
};
state = {
error: false,
loading: false,
jsonLegend: {}
}
componentDidMount() {
let jsonLegendUrl = this.getUrl(this.props);
getJsonWMSLegend(jsonLegendUrl).then(data => {
this.setState({ jsonLegend: data[0], loading: false });
}).catch(() => {
this.setState({ error: true, loading: false });
});
}

getScale = (props) => {
if (props.scales && props.currentZoomLvl !== undefined && props.scaleDependent) {
const zoom = Math.round(props.currentZoomLvl);
const scale = props.scales[zoom] ?? props.scales[props.scales.length - 1];
return Math.round(scale);
}
return null;
};
getUrl = (props, urlIdx) => {
if (props.layer && props.layer.type === "wms" && props.layer.url) {
const layer = props.layer;
const idx = !isNil(urlIdx) ? urlIdx : isArray(layer.url) && Math.floor(Math.random() * layer.url.length);

const url = isArray(layer.url) ?
layer.url[idx] :
layer.url.replace(/[?].*$/g, '');

let urlObj = urlUtil.parse(url);

const cleanParams = clearNilValuesForParams(layer.params);
const scale = this.getScale(props);
let query = assign({}, {
service: "WMS",
request: "GetLegendGraphic",
format: "application/json",
height: props.legendHeight,
width: props.legendWidth,
layer: layer.name,
style: layer.style || null,
version: layer.version || "1.3.0",
SLD_VERSION: "1.1.0",
LEGEND_OPTIONS: props.legendOptions
}, layer.legendParams || {},
props.language && layer.localizedLayerStyles ? {LANGUAGE: props.language} : {},
addAuthenticationToSLD(cleanParams || {}, props.layer),
cleanParams && cleanParams.SLD_BODY ? {SLD_BODY: cleanParams.SLD_BODY} : {},
scale !== null ? { SCALE: scale } : {});
addAuthenticationParameter(url, query);

return urlUtil.format({
host: urlObj.host,
protocol: urlObj.protocol,
pathname: urlObj.pathname,
query: query
});
}
return '';
}
renderRules = (rules) => {
const isLegendFilterIncluded = this.props.layer?.layerFilter?.filters?.find(f=>f.id === 'interactiveLegend');
const prevFilter = isLegendFilterIncluded ? isLegendFilterIncluded?.filters?.[0]?.id : '';
const filterWMSLayerHandler = (filter) => {
if (!filter) return;
this.props.onLayerFilterByLegend(this.props.layer.id, 'layers', filter === prevFilter ? '' : filter);
};
return (rules || []).map((rule) => {
return (<div className={`wms-json-legend-rule ${rule.filter && prevFilter === rule.filter ? 'active' : ''}`} key={rule.filter} onClick={() => filterWMSLayerHandler(rule.filter)}>
<WMSJsonLegendIcon rule={rule} />
<span>{rule.name || rule.title || ''}</span>
</div>);
});
};
render() {
if (!this.state.error && this.props.layer && this.props.layer.type === "wms" && this.props.layer.url) {
return <>
<div className="wms-legend" style={this.props.style}>
{ this.state.loading ? <Loader size={12} style={{display: 'inline-block'}} /> : this.renderRules(this.state.jsonLegend?.rules || [])}
</div>
</>;
}
return <Message msgId="layerProperties.legenderror" />;
}
}

export default Legend;
40 changes: 36 additions & 4 deletions web/client/components/TOC/fragments/WMSLegend.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, isNumber } from 'lodash';
import Legend from './legend/Legend';

import StyleBasedWMSJsonLegend from './StyleBasedWMSJsonLegend';
class WMSLegend extends React.Component {
static propTypes = {
node: PropTypes.object,
Expand All @@ -24,13 +24,15 @@ class WMSLegend extends React.Component {
scaleDependent: PropTypes.bool,
language: PropTypes.string,
legendWidth: PropTypes.number,
legendHeight: PropTypes.number
legendHeight: PropTypes.number,
onLayerFilterByLegend: PropTypes.func
};

static defaultProps = {
legendContainerStyle: {},
showOnlyIfVisible: false,
scaleDependent: true
scaleDependent: true,
onLayerFilterByLegend: () => {}
};

constructor(props) {
Expand All @@ -50,8 +52,9 @@ class WMSLegend extends React.Component {
render() {
let node = this.props.node || {};
const showLegend = this.canShow(node) && node.type === "wms" && node.group !== "background";
const isJsonLegend = this.props.node?.enableInteractiveLegend;
const useOptions = showLegend && this.useLegendOptions();
if (showLegend) {
if (showLegend && !isJsonLegend) {
return (
<div style={!this.setOverflow() ? this.props.legendContainerStyle : this.state.legendContainerStyle} ref={this.containerRef}>
<Legend
Expand Down Expand Up @@ -79,6 +82,35 @@ class WMSLegend extends React.Component {
/>
</div>
);
} else if (showLegend) {
return (
<div style={!this.setOverflow() ? this.props.legendContainerStyle : this.state.legendContainerStyle} ref={this.containerRef}>
<StyleBasedWMSJsonLegend
onLayerFilterByLegend={this.props.onLayerFilterByLegend}
style={!this.setOverflow() ? this.props.legendStyle : {}}
layer={node}
currentZoomLvl={this.props.currentZoomLvl}
scales={this.props.scales}
legendHeight={
useOptions &&
this.props.node.legendOptions &&
this.props.node.legendOptions.legendHeight ||
this.props.legendHeight ||
undefined
}
legendWidth={
useOptions &&
this.props.node.legendOptions &&
this.props.node.legendOptions.legendWidth ||
this.props.legendWidth ||
undefined
}
legendOptions={this.props.WMSLegendOptions}
scaleDependent={this.props.scaleDependent}
language={this.props.language}
/>
</div>
);
}
return null;
}
Expand Down
50 changes: 38 additions & 12 deletions web/client/components/TOC/fragments/settings/Display.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { getSupportedFormat } from '../../../../api/WMS';
import WMSCacheOptions from './WMSCacheOptions';
import ThreeDTilesSettings from './ThreeDTilesSettings';
import ModelTransformation from './ModelTransformation';
import StyleBasedWMSJsonLegend from '../StyleBasedWMSJsonLegend';

export default class extends React.Component {
static propTypes = {
opacityText: PropTypes.node,
Expand Down Expand Up @@ -261,7 +263,20 @@ 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">
{/** append the interactive legened checkbox here */}
{ this.props.element?.serverType !== ServerTypes.NO_VENDOR &&
<Col xs={12}>
<Checkbox
data-qa="display-interactive-legend-option"
value="enableInteractiveLegend"
key="enableInteractiveLegend"
onChange={(e) => 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" />} />
</Checkbox>
</Col>}
{!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 +305,31 @@ 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
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 @@ -157,9 +157,23 @@ 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>
{/**
* service.layerOptions?.serverType
*/}
{![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" />} />
</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

0 comments on commit 124e01b

Please sign in to comment.