-
Notifications
You must be signed in to change notification settings - Fork 445
Floating Dynamic Legend Plugin for ArcGIS & WMS layers #11118
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
Changes from 10 commits
f662957
c2cd086
4d299a2
c563d76
ad6aca6
60f145b
c4b1197
7ded0c4
2a40ff6
5d60a51
a11b673
6880a60
f56a754
5d26cde
9892f8c
95268df
c8272e8
887c50a
53211d8
a20f887
17d2612
0ade2b1
dbd9faf
25bb1a0
1fd74e7
41c74aa
ee68b3b
86425e7
d696ed6
dcb0e0b
0a5a87a
b9be5c1
d30f233
d09437b
3db940a
d894351
e806336
4905f46
da60c28
a80efc4
351ed73
86d31dc
ff65658
74a90d4
6ee200e
6725264
a52f299
eabc567
5a9f94b
e3dabff
6079e21
77a7228
415053c
db3ab7b
d127f27
32deb5d
b76ccfc
05f4202
b384da1
e7c2a16
939a76f
359f508
b59850a
65d5f5e
a4ec629
ab98067
61b3699
6827b21
affec99
8d8d61c
76c37ea
d21ee32
17d00dc
15a1775
458ff5d
33b7fcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -412,7 +412,7 @@ | |
| { "name": "WidgetsTray" } | ||
| ], | ||
| "desktop": [ | ||
| "Details", | ||
| "Details","DynamicLegend", | ||
|
||
| { | ||
| "name": "BrandNavbar", | ||
| "cfg": { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,6 +76,9 @@ | |
| } | ||
| ], | ||
| "desktop": [ | ||
| { | ||
| "name": "DynamicLegend" | ||
| }, | ||
|
||
| { | ||
| "name": "Map", | ||
| "cfg": { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /* eslint-disable react/jsx-boolean-value */ | ||
| import React from 'react'; | ||
| import { connect } from 'react-redux'; | ||
| import { createSelector } from 'reselect'; | ||
| import { get } from 'lodash'; | ||
| import { Glyphicon } from 'react-bootstrap'; | ||
|
|
||
| import { createPlugin } from '../utils/PluginsUtils'; | ||
| import { groupsSelector, layersSelector } from '../selectors/layers'; | ||
| import { keepLayer } from '../selectors/dynamiclegend'; | ||
| import { mapSelector } from '../selectors/map'; | ||
| import { updateNode } from '../actions/layers'; | ||
| import controls from '../reducers/controls'; | ||
| import { toggleControl } from '../actions/controls'; | ||
| import Message from '../components/I18N/Message'; | ||
|
|
||
| import DynamicLegend from './dynamicLegend/components/DynamicLegend'; | ||
|
|
||
| export default createPlugin('DynamicLegend', { | ||
| component: connect( | ||
| createSelector([ | ||
| (state) => get(state, 'controls.dynamic-legend.enabled'), | ||
| groupsSelector, | ||
| layersSelector, | ||
| mapSelector | ||
| ], (isVisible, groups, layers, map) => ({ | ||
| isVisible, | ||
| groups, | ||
| layers: layers.filter(keepLayer), | ||
| currentZoomLvl: map?.zoom, | ||
| mapBbox: map?.bbox | ||
| })), | ||
| { | ||
| onClose: toggleControl.bind(null, 'dynamic-legend', null), | ||
| onUpdateNode: updateNode | ||
| } | ||
| )(DynamicLegend), | ||
| options: { | ||
| disablePluginIf: "{state('router') && (state('router').endsWith('new') || state('router').includes('newgeostory') || state('router').endsWith('dashboard'))}" | ||
| }, | ||
|
||
| reducers: { controls }, | ||
| epics: {}, | ||
| containers: { | ||
| BurgerMenu: { | ||
| name: 'dynamic-legend', | ||
| position: 1000, | ||
| priority: 2, | ||
| doNotHide: true, | ||
| text: <Message msgId="dynamiclegend.title" />, | ||
| tooltip: <Message msgId="dynamiclegend.tooltip" />, | ||
| icon: <Glyphicon glyph="align-left" />, | ||
| action: toggleControl.bind(null, 'dynamic-legend', null), | ||
| toggle: true | ||
| }, | ||
| SidebarMenu: { | ||
| name: 'dynamic-legend', | ||
| position: 1000, | ||
| priority: 1, | ||
| doNotHide: true, | ||
| text: <Message msgId="dynamiclegend.title" />, | ||
| tooltip: <Message msgId="dynamiclegend.tooltip" />, | ||
| icon: <Glyphicon glyph="align-left" />, | ||
| action: toggleControl.bind(null, 'dynamic-legend', null), | ||
| toggle: true | ||
| }, | ||
| Toolbar: { | ||
| name: 'dynamic-legend', | ||
| alwaysVisible: true, | ||
| position: 2, | ||
| priority: 0, | ||
| doNotHide: true, | ||
| tooltip: <Message msgId="dynamiclegend.title" />, | ||
| icon: <Glyphicon glyph="align-left" />, | ||
| action: toggleControl.bind(null, 'dynamic-legend', null), | ||
| toggle: true | ||
| } | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,9 +17,11 @@ import { getLayerIds } from '../../../utils/ArcGISUtils'; | |
| /** | ||
| * ArcGISLegend renders legend from a MapServer or ImageServer service | ||
| * @prop {object} node layer node options | ||
| * @prop {function} onUpdateNode return the changes of a specific node | ||
| */ | ||
| function ArcGISLegend({ | ||
| node = {} | ||
| node = {}, | ||
| onUpdateNode = () => {} | ||
| }) { | ||
| const [legendData, setLegendData] = useState(null); | ||
| const [error, setError] = useState(false); | ||
|
|
@@ -31,7 +33,13 @@ function ArcGISLegend({ | |
| f: 'json' | ||
| } | ||
| }) | ||
| .then(({ data }) => setLegendData(data)) | ||
| .then(({ data }) => { | ||
| const dynamicLegendIsEmpty = data.layers.every(layer => layer.legend.length === 0); | ||
| if ((node.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { | ||
| onUpdateNode({ dynamicLegendIsEmpty }); | ||
| } | ||
|
||
| setLegendData(data); | ||
| }) | ||
| .catch(() => setError(true)); | ||
| } | ||
| }, [legendUrl]); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ import { getWMSLegendConfig, LEGEND_FORMAT } from '../../../utils/LegendUtils'; | |
| * @prop {string} language current language code | ||
| * @prop {number} legendWidth width of the legend symbols | ||
| * @prop {number} legendHeight height of the legend symbols | ||
| * @prop {function} onUpdateNode return the changes of a specific node | ||
| */ | ||
| class Legend extends React.Component { | ||
| static propTypes = { | ||
|
|
@@ -49,15 +50,17 @@ class Legend extends React.Component { | |
| language: PropTypes.string, | ||
| projection: PropTypes.string, | ||
| mapSize: PropTypes.object, | ||
| bbox: PropTypes.object | ||
| bbox: PropTypes.object, | ||
| onUpdateNode: PropTypes.func | ||
| }; | ||
|
|
||
| static defaultProps = { | ||
| legendHeight: 12, | ||
| legendWidth: 12, | ||
| legendOptions: "forceLabels:on", | ||
| style: {maxWidth: "100%"}, | ||
| scaleDependent: true | ||
| scaleDependent: true, | ||
| onUpdateNode: () => {} | ||
| }; | ||
| state = { | ||
| error: false | ||
|
|
@@ -132,9 +135,13 @@ class Legend extends React.Component { | |
| validateImg = (img) => { | ||
| // GeoServer response is a 1x2 px size when legend is not available. | ||
| // In this case we need to show the "Legend Not available" message | ||
| if (img.height <= 1 && img.width <= 2) { | ||
| const imgError = img.height <= 1 && img.width <= 2; | ||
| if (imgError) { | ||
| this.onImgError(); | ||
| } | ||
| if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== imgError) { | ||
| this.props.onUpdateNode({ dynamicLegendIsEmpty: imgError }); | ||
| } | ||
|
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,7 +46,8 @@ class StyleBasedWMSJsonLegend extends React.Component { | |
| interactive: PropTypes.bool, // the indicator flag that refers if this legend is interactive or not | ||
| projection: PropTypes.string, | ||
| mapSize: PropTypes.object, | ||
| mapBbox: PropTypes.object | ||
| mapBbox: PropTypes.object, | ||
| onUpdateNode: PropTypes.func | ||
| }; | ||
|
|
||
| static defaultProps = { | ||
|
|
@@ -56,7 +57,8 @@ class StyleBasedWMSJsonLegend extends React.Component { | |
| style: {maxWidth: "100%"}, | ||
| scaleDependent: true, | ||
| onChange: () => {}, | ||
| interactive: false | ||
| interactive: false, | ||
| onUpdateNode: () => {} | ||
| }; | ||
| state = { | ||
| error: false, | ||
|
|
@@ -104,6 +106,10 @@ class StyleBasedWMSJsonLegend extends React.Component { | |
| } | ||
| this.setState({ loading: true }); | ||
| getJsonWMSLegend(jsonLegendUrl).then(data => { | ||
| const dynamicLegendIsEmpty = data.length === 0 || data[0].rules.length === 0; | ||
| if ((this.props.layer.dynamicLegendIsEmpty ?? null) !== dynamicLegendIsEmpty) { | ||
| this.props.onUpdateNode({ dynamicLegendIsEmpty }); | ||
| } | ||
|
||
| this.setState({ jsonLegend: data[0], loading: false }); | ||
| }).catch(() => { | ||
| this.setState({ error: true, loading: false }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| .ms-resizable-modal > .modal-content.legend-dialog { | ||
| top: 0vh; | ||
| right: -100vw; | ||
| } | ||
|
|
||
| .legend-content * .ms-node-title { | ||
| font-weight: bold !important | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import React from 'react'; | ||
| import { keepNode, isLayerVisible } from '../../../selectors/dynamiclegend'; | ||
| import { getResolutions } from '../../../utils/MapUtils'; | ||
| import Message from '../../../components/I18N/Message'; | ||
| import ResizableModal from '../../../components/misc/ResizableModal'; | ||
| import { ControlledTOC } from '../../TOC/components/TOC'; | ||
| import DefaultGroup from '../../TOC/components/DefaultGroup'; | ||
| import DefaultLayer from '../../TOC/components/DefaultLayer'; | ||
|
|
||
| import '../assets/dynamicLegend.css'; | ||
|
|
||
| function applyVersionParamToLegend(layer) { | ||
| // we need to pass a parameter that invalidate the cache for GetLegendGraphic | ||
| // all layer inside the dataset viewer apply a new _v_ param each time we switch page | ||
| return { ...layer, legendParams: { ...layer?.legendParams, _v_: layer?._v_ } }; | ||
| } | ||
|
|
||
| function filterNode(node, currentResolution) { | ||
| const nodes = Array.isArray(node.nodes) ? node.nodes.filter(keepNode).map(applyVersionParamToLegend).map(n => filterNode(n, currentResolution)) : undefined; | ||
|
|
||
| return { | ||
| ...node, | ||
| isVisible: (node.visibility ?? true) && (nodes ? nodes.length > 0 && nodes.some(n => n.isVisible) : isLayerVisible(node, currentResolution)), | ||
| ...(nodes && { nodes }) | ||
| }; | ||
| } | ||
|
|
||
| export default ({ | ||
| layers, | ||
| onUpdateNode, | ||
| currentZoomLvl, | ||
| onClose, | ||
| isVisible, | ||
| groups, | ||
| mapBbox | ||
| }) => { | ||
| const layerDict = layers.reduce((acc, layer) => ({ | ||
| ...acc, | ||
| ...{ | ||
| [layer.id]: { | ||
| ...layer, | ||
| ...{enableDynamicLegend: true, enableInteractiveLegend: false} | ||
| } | ||
| } | ||
| }), {}); | ||
| const getVisibilityStyle = nodeVisibility => ({ | ||
| opacity: nodeVisibility ? 1 : 0, | ||
| height: nodeVisibility ? "auto" : "0" | ||
| }); | ||
|
|
||
| const customGroupNodeComponent = props => ( | ||
| <div style={getVisibilityStyle(props.node?.isVisible ?? true)}> | ||
| <DefaultGroup {...props} /> | ||
| </div> | ||
| ); | ||
| const customLayerNodeComponent = props => { | ||
| const layer = layerDict[props.node.id]; | ||
| if (!layer) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div style={getVisibilityStyle(props.node?.isVisible ?? true)}> | ||
| <DefaultLayer {...props} node={layer} /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
||
|
|
||
| return ( | ||
| <ResizableModal | ||
| onClose = {onClose} | ||
| enableFooter={false} | ||
| title={<Message msgId="dynamiclegend.title" />} | ||
| dialogClassName=" legend-dialog" | ||
| show={isVisible} | ||
| draggable | ||
| style={{zIndex: 1993}}> | ||
| <ControlledTOC | ||
| tree={[filterNode(groups[0] ?? {}, getResolutions()[Math.round(currentZoomLvl)])]} | ||
| className="legend-content" | ||
| theme="legend" | ||
| onChange={onUpdateNode} | ||
| groupNodeComponent={customGroupNodeComponent} | ||
| layerNodeComponent={customLayerNodeComponent} | ||
| config={{ | ||
| sortable: false, | ||
| showFullTitle: true, | ||
| hideOpacitySlider: true, | ||
| hideVisibilityButton: true, | ||
| expanded: true, | ||
| zoom: currentZoomLvl, | ||
| layerOptions: { | ||
| enableDynamicLegend: true, | ||
| legendOptions: { | ||
| legendWidth: 12, | ||
| legendHeight: 12, | ||
| mapBbox | ||
| } | ||
| } | ||
| }} | ||
| /> | ||
| </ResizableModal> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In allyoucanmap@a11b673 only the SidebarMenu is included as dependency because is the current preferred container for all side plugins