From af77da7532a87e02145c17035f717aa73f26ba6b Mon Sep 17 00:00:00 2001 From: stefano bovio Date: Mon, 7 May 2018 18:07:27 +0200 Subject: [PATCH] Fix #2856 Legend action plugin (#2857) --- docma-config.json | 6 + .../actions/__tests__/floatinglegend-test.js | 32 +++ web/client/actions/floatinglegend.js | 44 ++++ web/client/components/TOC/DefaultLayer.jsx | 41 ++-- web/client/components/TOC/FloatingLegend.jsx | 229 ++++++++++++++++++ .../TOC/__tests__/DefaultLayer-test.jsx | 35 ++- .../TOC/__tests__/FloatingLegend-test.jsx | 226 +++++++++++++++++ web/client/components/misc/Slider.jsx | 24 ++ .../components/misc/cardgrids/SideGrid.jsx | 54 +++-- web/client/epics/identify.js | 12 +- web/client/localConfig.json | 3 +- web/client/plugins/DrawerMenu.jsx | 61 ++++- web/client/plugins/FloatingLegend.jsx | 119 +++++++++ web/client/product/plugins.js | 3 +- .../reducers/__tests__/floatinglegend.js | 33 +++ web/client/reducers/floatinglegend.js | 34 +++ .../__tests__/floatinglegend-test.js | 38 +++ web/client/selectors/floatinglegend.js | 33 +++ web/client/selectors/map.js | 2 +- web/client/themes/default/less/toc.less | 197 ++++++++++++++- web/client/translations/data.de-DE | 5 + web/client/translations/data.en-US | 5 + web/client/translations/data.es-ES | 5 + web/client/translations/data.fr-FR | 5 + web/client/translations/data.hr-HR | 5 + web/client/translations/data.it-IT | 5 + web/client/translations/data.nl-NL | 5 + web/client/translations/data.zh-ZH | 5 + web/client/utils/CoordinatesUtils.js | 33 +-- web/client/utils/MapUtils.js | 22 +- web/client/utils/__tests__/MapUtils-test.js | 14 +- 31 files changed, 1238 insertions(+), 97 deletions(-) create mode 100644 web/client/actions/__tests__/floatinglegend-test.js create mode 100644 web/client/actions/floatinglegend.js create mode 100644 web/client/components/TOC/FloatingLegend.jsx create mode 100644 web/client/components/TOC/__tests__/FloatingLegend-test.jsx create mode 100644 web/client/components/misc/Slider.jsx create mode 100644 web/client/plugins/FloatingLegend.jsx create mode 100644 web/client/reducers/__tests__/floatinglegend.js create mode 100644 web/client/reducers/floatinglegend.js create mode 100644 web/client/selectors/__tests__/floatinglegend-test.js create mode 100644 web/client/selectors/floatinglegend.js diff --git a/docma-config.json b/docma-config.json index 8b000814ba..0d280d9d54 100644 --- a/docma-config.json +++ b/docma-config.json @@ -132,6 +132,8 @@ "web/client/components/misc/toolbar/Toolbar.jsx", "web/client/components/misc/EmptyView.jsx", "web/client/components/misc/ResizableModal.jsx", + "web/client/components/misc/Slider.jsx", + "web/client/components/TOC/FloatingLegend.jsx", "web/client/components/TOC/TOCItemsSettings.jsx", "web/client/components/TOC/fragments/settings/FeatureInfo.jsx", "web/client/components/TOC/fragments/settings/FeatureInfoEditor.jsx", @@ -140,6 +142,7 @@ "web/client/actions/controls.js", "web/client/actions/fullscreen.js", "web/client/actions/globeswitcher.js", + "web/client/actions/floatinglegend.js", "web/client/actions/maplayout.js", "web/client/actions/maps.js", "web/client/actions/maptype.js", @@ -148,6 +151,7 @@ "web/client/selectors/index.jsdoc", "web/client/selectors/featuregrid.js", + "web/client/selectors/floatinglegend.js", "web/client/selectors/map.js", "web/client/selectors/mapinfo.js", "web/client/selectors/maplayout.js", @@ -158,6 +162,7 @@ "web/client/reducers/controls.js", "web/client/reducers/featuregrid.js", "web/client/reducers/globeswitcher.js", + "web/client/reducers/floatinglegend.js", "web/client/reducers/maps.js", "web/client/reducers/maptype.js", "web/client/reducers/notifications.js", @@ -205,6 +210,7 @@ "web/client/plugins/MeasureResults.jsx", "web/client/plugins/FullScreen.jsx", "web/client/plugins/Identify.jsx", + "web/client/plugins/FloatingLegend.jsx", "web/client/plugins/Locate.jsx", "web/client/plugins/Login.jsx", "web/client/plugins/MousePosition.jsx", diff --git a/web/client/actions/__tests__/floatinglegend-test.js b/web/client/actions/__tests__/floatinglegend-test.js new file mode 100644 index 0000000000..a4de85b2c4 --- /dev/null +++ b/web/client/actions/__tests__/floatinglegend-test.js @@ -0,0 +1,32 @@ +/* + * Copyright 2018, 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. + */ + +const expect = require('expect'); +const { + RESIZE_LEGEND, + EXPAND_LEGEND, + resizeLegend, + expandLegend +} = require('../floatinglegend'); + +describe('Test floatinglegend actions', () => { + it('resizeLegend', () => { + const size = {height: 100}; + const retval = resizeLegend(size); + expect(retval).toExist(); + expect(retval.type).toBe(RESIZE_LEGEND); + expect(retval.size).toBe(size); + }); + it('expandLegend', () => { + const expanded = true; + const retval = expandLegend(expanded); + expect(retval).toExist(); + expect(retval.type).toEqual(EXPAND_LEGEND); + expect(retval.expanded).toEqual(expanded); + }); +}); diff --git a/web/client/actions/floatinglegend.js b/web/client/actions/floatinglegend.js new file mode 100644 index 0000000000..61bff25549 --- /dev/null +++ b/web/client/actions/floatinglegend.js @@ -0,0 +1,44 @@ +/* + * Copyright 2018, 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. + */ + +const RESIZE_LEGEND = 'FLOATINGLEGEND:RESIZE_LEGEND'; +const EXPAND_LEGEND = 'FLOATINGLEGEND:EXPAND_LEGEND'; +/** + * resizeLegend action, type `RESIZE_LEGEND` + * @memberof actions.floatinglegend + * @param {object} size size of legend {width: 0, height: 0} + * @return {action} type `RESIZE_LEGEND` with size + */ +function resizeLegend(size) { + return { + type: RESIZE_LEGEND, + size + }; +} +/** + * expandLegend action, type `EXPAND_LEGEND` + * @memberof actions.floatinglegend + * @param {boolean} expanded expanded state of legend action + * @return {action} type `EXPAND_LEGEND` with expanded + */ +function expandLegend(expanded) { + return { + type: EXPAND_LEGEND, + expanded + }; +} +/** + * Actions for floatinglegend. + * @name actions.floatinglegend + */ +module.exports = { + RESIZE_LEGEND, + EXPAND_LEGEND, + resizeLegend, + expandLegend +}; diff --git a/web/client/components/TOC/DefaultLayer.jsx b/web/client/components/TOC/DefaultLayer.jsx index 4780ee9e12..6c9038eac6 100644 --- a/web/client/components/TOC/DefaultLayer.jsx +++ b/web/client/components/TOC/DefaultLayer.jsx @@ -15,7 +15,7 @@ const VisibilityCheck = require('./fragments/VisibilityCheck'); const Title = require('./fragments/Title'); const WMSLegend = require('./fragments/WMSLegend'); const LayersTool = require('./fragments/LayersTool'); -const Slider = require('react-nouislider'); +const Slider = require('../misc/Slider'); class DefaultLayer extends React.Component { static propTypes = { @@ -67,31 +67,26 @@ class DefaultLayer extends React.Component { return translation || layer.name; }; - renderCollapsible = () => { + renderOpacitySlider = () => { const layerOpacity = this.props.node.opacity !== undefined ? Math.round(this.props.node.opacity * 100) : 100; + return this.props.activateOpacityTool ? + { + if (isArray(opacity) && opacity[0]) { + this.props.onUpdateNode(this.props.node.id, 'layers', { opacity: parseFloat(opacity[0].replace(' %', '')) / 100 }); + } + }}/> + : null; + } + + renderCollapsible = () => { return (
{this.props.showFullTitleOnExpand ? {this.getTitle(this.props.node)} : null} - {this.props.activateOpacityTool ? - - - - Math.round(value), - to: value => Math.round(value) + ' %' - }} - onChange={(opacity) => { - if (isArray(opacity) && opacity[0]) { - this.props.onUpdateNode(this.props.node.id, 'layers', { opacity: parseFloat(opacity[0].replace(' %', '')) / 100 }); - } - }} /> - - : null} {this.props.activateLegendTool ? @@ -99,6 +94,7 @@ class DefaultLayer extends React.Component { : null} + {this.renderOpacitySlider()}
); }; @@ -131,7 +127,7 @@ class DefaultLayer extends React.Component { } renderNode = (grab, hide, selected, error, warning, other) => { - const isEmpty = !this.props.activateLegendTool && !this.props.activateOpacityTool; + const isEmpty = !this.props.activateLegendTool && !this.props.showFullTitleOnExpand; return (
@@ -140,6 +136,7 @@ class DefaultLayer extends React.Component { {this.props.node.loading ? <div className="toc-inline-loader"></div> : this.renderToolsLegend(isEmpty)} </div> + {!this.props.activateOpacityTool || this.props.node.expanded || !this.props.node.visibility || this.props.node.loadingError === 'Error' ? null : this.renderOpacitySlider()} {isEmpty ? null : this.renderCollapsible()} </Node> ); diff --git a/web/client/components/TOC/FloatingLegend.jsx b/web/client/components/TOC/FloatingLegend.jsx new file mode 100644 index 0000000000..1f2f936d7e --- /dev/null +++ b/web/client/components/TOC/FloatingLegend.jsx @@ -0,0 +1,229 @@ +/* + * Copyright 2018, 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. + */ + +const React = require('react'); +const ReactDOM = require('react-dom'); +const PropTypes = require('prop-types'); +const {isNil, isEqual, delay} = require('lodash'); + +const {Glyphicon, Panel, Grid, Row, Col, Button: ButtonB} = require('react-bootstrap'); +const {Resizable} = require('react-resizable'); +const ContainerDimensions = require('react-container-dimensions').default; +const tooltip = require('../misc/enhancers/tooltip'); +const Button = tooltip(ButtonB); +const SideGrid = require('../misc/cardgrids/SideGrid'); +const Slider = require('../misc/Slider'); +const WMSLegend = require('./fragments/WMSLegend'); + +/** + * Component for rendering a legend component. + * This component shows a list of layers with visibility and opacity controls + * @memberof components.TOC + * @name FloatingLegend + * @class + * @prop {array} layers array of layer objects + * @prop {bool} expanded expanded state + * @prop {number} width width of legend + * @prop {number} height height of legend + * @prop {title} title title of map + * @prop {object} legendProps props for WMSLegend + * @prop {function} onChange return three arguments, layerId, node type and object with changed value of layer + * @prop {function} onResize return changed size of legend, e.g {height: 700} + * @prop {function} onExpand return current expaneded state + * @prop {node} toggleButton component added on legend header, left side + * @prop {number} minHeight minimun height of legend + * @prop {number} maxHeight maximum height of legend + * @prop {number} deltaHeight additional height to increase the evaluated list height + * @prop {bool} disabled disable and hide the component + * @prop {number} currentZoomLvl current zoom level of map + * @prop {array} scales array of supported scales + * @prop {bool} disableOpacitySlider disable and hide opacity slider + * @prop {bool} expandedOnMount show expanded legend when component did mount + */ + +class FloatingLegend extends React.Component { + + static propTypes = { + layers: PropTypes.array, + expanded: PropTypes.bool, + width: PropTypes.number, + height: PropTypes.number, + title: PropTypes.string, + legendProps: PropTypes.object, + onChange: PropTypes.func, + onResize: PropTypes.func, + onExpand: PropTypes.func, + toggleButton: PropTypes.node, + minHeight: PropTypes.number, + maxHeight: PropTypes.number, + disabled: PropTypes.bool, + currentZoomLvl: PropTypes.number, + scales: PropTypes.array, + disableOpacitySlider: PropTypes.bool, + deltaHeight: PropTypes.number, + expandedOnMount: PropTypes.bool + }; + + static defaultProps = { + layers: [], + expanded: true, + width: 300, + height: 900, + title: '', + legendProps: {}, + onChange: () => {}, + onResize: () => {}, + onExpand: () => {}, + toggleButton: <Glyphicon glyph="1-map" style={{margin: '0 8px'}}/>, + minHeight: 150, + maxHeight: 9999, + deltaHeight: 110 + }; + + state = {}; + + componentDidMount() { + this.props.onExpand(this.props.expandedOnMount); + } + + componentDidUpdate(prevProps) { + delay(() => { + if (!isEqual(this.props.layers, prevProps.layers) + || this.props.maxHeight !== prevProps.maxHeight + || this.props.disabled !== prevProps.disabled) { + const listHeight = this.findListHeight(); + if (this.props.maxHeight < this.props.height) { + this.props.onResize({ + height: this.props.maxHeight + }); + } else if (listHeight && listHeight < this.props.height) { + this.props.onResize({ + height: listHeight + }); + } + } + if (this.props.layers.length !== prevProps.layers.length + || this.props.expanded && !prevProps.expanded) { + const listHeight = this.findListHeight(); + this.props.onResize({ + height: listHeight < this.props.maxHeight ? listHeight : this.props.maxHeight + }); + } + }, 100); + } + + render() { + const expanded = this.props.expanded && this.props.layers && this.props.layers.length > 0; + return this.props.layers && this.props.layers.length === 0 && !this.props.title && + <div + id="ms-legend-action" + className="ms-legend-action"> + {this.props.toggleButton} + </div> + || !this.props.disabled && ( + <ContainerDimensions> + {({height: containerHeight}) => + <Resizable + height={this.props.height} + axis="y" + minConstraints={[ + this.props.width, + this.props.minHeight + ]} + maxConstraints={[ + this.props.width, + this.state.listHeight && this.state.listHeight < this.props.maxHeight && this.state.listHeight + || containerHeight && containerHeight < this.props.maxHeight && containerHeight + || this.props.maxHeight + ]} + onResize={(e, data) => + this.props.onResize({ + height: data.size && data.size.height + }) + }> + <Panel + id="ms-legend-action" + className="ms-legend-action" + collapsible + header={ + <Grid fluid> + <Row> + <Col xs={12} className="ms-legend-header"> + {this.props.toggleButton} + <div> + <h5>{this.props.title}</h5> + </div> + {this.props.layers && this.props.layers.length > 0 && <Button + tooltipId={this.props.expanded ? 'floatinglegend.hideLegend' : 'floatinglegend.showLegend'} + tooltipPosition="bottom" + className="no-border square-button-md" + onClick={() => this.props.onExpand(!this.props.expanded)}> + <Glyphicon glyph={this.props.expanded ? "chevron-down" : "chevron-left"} /> + </Button>} + </Col> + </Row> + </Grid> + } + expanded={expanded} + footer={expanded && <Grid fluid/>} + style={{ + width: this.props.width, + ...(this.props.height && expanded ? {height: this.props.minHeight > this.props.height ? this.props.minHeight : this.props.height} : {}) + }}> + <SideGrid + ref={list => { this.list = list; }} + size="sm" + items={this.props.layers.map(layer => ({ + title: layer.title, + preview: <Glyphicon className="text-primary" + glyph={layer.visibility ? 'eye-open' : 'eye-close'} + onClick={() => this.props.onChange(layer.id, 'layers', {visibility: !layer.visibility})}/>, + style: { + opacity: layer.visibility ? 1 : 0.4 + }, + body: !layer.visibility ? null + : ( + <div> + <Grid fluid> + <Row> + <Col xs={12} className="ms-legend-container"> + <WMSLegend + node={{...layer}} + currentZoomLvl={this.props.currentZoomLvl} + scales={this.props.scales} + {...this.props.legendProps}/> + </Col> + </Row> + </Grid> + {!this.props.disableOpacitySlider && <div className="mapstore-slider" onClick={(e) => { e.stopPropagation(); }}> + <Slider + disabled={!layer.visibility} + start={[isNil(layer.opacity) ? 100 : Math.round(layer.opacity * 100) ]} + range={{min: 0, max: 100}} + onChange={(value) => this.props.onChange(layer.id, 'layers', {opacity: parseFloat((value[0] / 100).toFixed(2))}) }/> + </div>} + </div> + ) + }) + )}/> + </Panel> + </Resizable> + } + </ContainerDimensions> + ); + } + + findListHeight = () => { + const list = this.list && ReactDOM.findDOMNode(this.list); + const listHeight = list && list.clientHeight && list.clientHeight + this.props.deltaHeight || 9999; + this.setState({ listHeight }); + return listHeight; + }; +} + +module.exports = FloatingLegend; diff --git a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx index 2ffc1e417d..03025049c6 100644 --- a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx +++ b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx @@ -149,8 +149,6 @@ describe('test DefaultLayer module component', () => { expect(comp).toExist(); const domNode = ReactDOM.findDOMNode(comp); expect(domNode).toExist(); - const opacity = domNode.getElementsByClassName("noUi-tooltip")[0]; - expect(opacity.innerHTML).toBe('100 %'); const tool = domNode.getElementsByClassName("noUi-target")[0]; expect(tool).toExist(); expect(tool.getAttribute('disabled')).toBe(null); @@ -171,8 +169,6 @@ describe('test DefaultLayer module component', () => { expect(comp).toExist(); const domNode = ReactDOM.findDOMNode(comp); expect(domNode).toExist(); - const opacity = domNode.getElementsByClassName("noUi-tooltip")[0]; - expect(opacity.innerHTML).toBe('50 %'); const tool = domNode.getElementsByClassName("noUi-target")[0]; expect(tool).toExist(); expect(tool.getAttribute('disabled')).toBe('true'); @@ -221,28 +217,41 @@ describe('test DefaultLayer module component', () => { const slider = domNode.getElementsByClassName("mapstore-slider"); expect(slider.length).toBe(0); }); - it('show full title', () => { + it('show full title enabled', () => { const l = { name: 'layer00', title: 'Layer', visibility: false, storeIndex: 9, type: 'wms', - opacity: 0.5 + opacity: 0.5, + expanded: true }; - let comp = ReactDOM.render(<Layer showFullTitleOnExpand visibilityCheckType="checkbox" node={l} />, + const comp = ReactDOM.render(<Layer showFullTitleOnExpand node={l} />, document.getElementById("container")); expect(comp).toExist(); - let domNode = ReactDOM.findDOMNode(comp); + const domNode = ReactDOM.findDOMNode(comp); expect(domNode).toExist(); - let title = domNode.getElementsByClassName("toc-full-title"); + const title = domNode.getElementsByClassName("toc-full-title"); expect(title.length).toBe(1); - comp = ReactDOM.render(<Layer visibilityCheckType="checkbox" node={l} />, - document.getElementById("container")); - domNode = ReactDOM.findDOMNode(comp); + }); + it('show full title disabled', () => { + const l = { + name: 'layer00', + title: 'Layer', + visibility: false, + storeIndex: 9, + type: 'wms', + opacity: 0.5, + expanded: true + }; + + const comp = ReactDOM.render(<Layer showFullTitleOnExpand={false} node={l} />, + document.getElementById("container")); + const domNode = ReactDOM.findDOMNode(comp); expect(domNode).toExist(); - title = domNode.getElementsByClassName("toc-full-title"); + const title = domNode.getElementsByClassName("toc-full-title"); expect(title.length).toBe(0); }); diff --git a/web/client/components/TOC/__tests__/FloatingLegend-test.jsx b/web/client/components/TOC/__tests__/FloatingLegend-test.jsx new file mode 100644 index 0000000000..ed89a6bec5 --- /dev/null +++ b/web/client/components/TOC/__tests__/FloatingLegend-test.jsx @@ -0,0 +1,226 @@ +/* + * Copyright 2017, 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. + */ +const React = require('react'); +const ReactDOM = require('react-dom'); + +const FloatingLegend = require('../FloatingLegend'); +const expect = require('expect'); +const TestUtils = require('react-dom/test-utils'); + +describe('tests FloatingLegend component', () => { + beforeEach((done) => { + document.body.innerHTML = '<div id="container"></div>'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('render component', () => { + const cmp = ReactDOM.render(<FloatingLegend />, document.getElementById("container")); + expect(cmp).toExist(); + + const toggleButtonContainer = document.getElementById('ms-legend-action'); + expect(toggleButtonContainer).toExist(); + expect(toggleButtonContainer.children.length).toBe(1); + + const toggleButtonPlaceholder = toggleButtonContainer.children[0]; + expect(toggleButtonPlaceholder.getAttribute('class')).toBe('glyphicon glyphicon-1-map'); + }); + + it('render width layers', () => { + const cmp = ReactDOM.render( + <FloatingLegend + expanded + layers={[ + { + name: 'layer:00', + title: 'Layer', + visibility: true, + type: 'wms' + }, + { + name: 'layer:01', + title: 'Layer:01', + visibility: false, + type: 'wms', + opacity: 0.5 + } + ]}/>, document.getElementById("container")); + + expect(cmp).toExist(); + + const layers = document.getElementsByClassName('mapstore-side-card'); + expect(layers.length).toBe(2); + + const sliders = document.getElementsByClassName('mapstore-slider'); + expect(sliders.length).toBe(1); + + const visibleLayer = document.getElementsByClassName('glyphicon-eye-open'); + expect(visibleLayer.length).toBe(1); + }); + + it('render width layers and disabled opacity slider', () => { + const cmp = ReactDOM.render( + <FloatingLegend + expanded + disableOpacitySlider + layers={[ + { + name: 'layer:00', + title: 'Layer', + visibility: true, + type: 'wms' + }, + { + name: 'layer:01', + title: 'Layer:01', + visibility: false, + type: 'wms', + opacity: 0.5 + } + ]}/>, document.getElementById("container")); + + expect(cmp).toExist(); + + const layers = document.getElementsByClassName('mapstore-side-card'); + expect(layers.length).toBe(2); + + const sliders = document.getElementsByClassName('mapstore-slider'); + expect(sliders.length).toBe(0); + + const visibleLayer = document.getElementsByClassName('glyphicon-eye-open'); + expect(visibleLayer.length).toBe(1); + }); + + it('on expand', () => { + + const actions = { + onExpand: () => {} + }; + + const spyExpand = expect.spyOn(actions, 'onExpand'); + + ReactDOM.render( + <FloatingLegend + onExpand={actions.onExpand} + layers={[ + { + name: 'layer:00', + title: 'Layer', + visibility: true, + type: 'wms' + } + ]}/>, document.getElementById("container")); + + let expandButton = document.getElementsByClassName('square-button-md')[0]; + expect(expandButton.children[0].getAttribute('class')).toBe('glyphicon glyphicon-chevron-down'); + + TestUtils.Simulate.click(expandButton); + expect(spyExpand).toHaveBeenCalledWith(false); + + ReactDOM.render( + <FloatingLegend + expanded={false} + layers={[ + { + name: 'layer:00', + title: 'Layer', + visibility: true, + type: 'wms' + } + ]}/>, document.getElementById("container")); + + expandButton = document.getElementsByClassName('square-button-md')[0]; + expect(expandButton.children[0].getAttribute('class')).toBe('glyphicon glyphicon-chevron-left'); + + }); + + it('on change', () => { + + const actions = { + onChange: () => {} + }; + + const spyChange = expect.spyOn(actions, 'onChange'); + + ReactDOM.render( + <FloatingLegend + onChange={actions.onChange} + layers={[ + { + name: 'layer:00', + title: 'Layer', + visibility: true, + type: 'wms' + } + ]}/>, document.getElementById("container")); + + const visibilityButton = document.getElementsByClassName('glyphicon-eye-open')[0]; + TestUtils.Simulate.click(visibilityButton); + expect(spyChange).toHaveBeenCalled(); + }); + + it('update height', (done) => { + const deltaHeight = 110; + + let cmp = ReactDOM.render( + <FloatingLegend + expanded + deltaHeight={deltaHeight} + layers={[ + { + name: 'layer:00', + title: 'Layer', + visibility: true, + type: 'wms' + }, + { + name: 'layer:01', + title: 'Layer:01', + visibility: false, + type: 'wms', + opacity: 0.5 + } + ]}/>, document.getElementById("container")); + + expect(cmp).toExist(); + + expect(cmp.list).toExist(); + const list = ReactDOM.findDOMNode(cmp.list); + const prevListSize = list.clientHeight; + + const onResize = ({height}) => { + try { + const listSize = list.clientHeight; + expect(prevListSize > listSize).toBe(true); + expect(listSize === height - deltaHeight).toBe(true); + done(); + } catch(e) { + done(e); + } + }; + + cmp = ReactDOM.render( + <FloatingLegend + expanded + deltaHeight={deltaHeight} + layers={[ + { + name: 'layer:00', + title: 'Layer', + visibility: true, + type: 'wms' + } + ]} + onResize={onResize}/>, document.getElementById("container")); + }); +}); diff --git a/web/client/components/misc/Slider.jsx b/web/client/components/misc/Slider.jsx new file mode 100644 index 0000000000..b0682f61f1 --- /dev/null +++ b/web/client/components/misc/Slider.jsx @@ -0,0 +1,24 @@ +/* + * Copyright 2018, 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. + */ + +const {isEqual} = require('lodash'); +const {shouldUpdate} = require('recompose'); +/** + * Component for rendering react-nouislider. + * It will update the component only when start and disabled props change, + * this improvement removes glitches due to excessive updates. (eg. filckering of TOC slider while zooming/panning on map). + * It uses props of react-nouislider. + * @memberof components.misc + * @name Slider + * @class + */ +module.exports = shouldUpdate( + (props, nexProps) => + !isEqual(props.start, nexProps.start) + || props.disabled !== nexProps.disabled +)(require('react-nouislider')); diff --git a/web/client/components/misc/cardgrids/SideGrid.jsx b/web/client/components/misc/cardgrids/SideGrid.jsx index 1861c9751a..6991b7e195 100644 --- a/web/client/components/misc/cardgrids/SideGrid.jsx +++ b/web/client/components/misc/cardgrids/SideGrid.jsx @@ -8,6 +8,7 @@ const React = require('react'); const SideCard = require('./SideCard'); +const PropTypes = require('prop-types'); const {Row, Col} = require('react-bootstrap'); /** * Component for rendering a list of SideCard. @@ -20,19 +21,40 @@ const {Row, Col} = require('react-bootstrap'); * @prop {element} cardComponent custom component for card in list * @prop {object} colProps props for react-bootstrap col component */ -module.exports = ({cardComponent, items = [], colProps = {xs: 12}, onItemClick = () => {}, size = ''} = {}) => { - const Card = cardComponent || SideCard; - return (<div className="msSideGrid"> - <Row className="items-list"> - {items.map((item, i) => - (<Col key={item.id || i} {...colProps}> - <Card - onClick={() => onItemClick(item)} - size={size} - {...item} - /> - </Col>) - )} - </Row> - </div>); -}; +class SideGrid extends React.Component { + /* React class needed to retrive ref of current component */ + static propTypes = { + size: PropTypes.string, + onItemClick: PropTypes.func, + colProps: PropTypes.object, + items: PropTypes.array, + cardComponent: PropTypes.element + }; + + static defaultProps = { + size: '', + onItemClick: () => {}, + colProps: {xs: 12}, + items: [] + }; + + render() { + const {cardComponent, items, colProps, onItemClick, size} = this.props; + const Card = cardComponent || SideCard; + return (<div className="msSideGrid"> + <Row className="items-list"> + {items.map((item, i) => + (<Col key={item.id || i} {...colProps}> + <Card + onClick={() => onItemClick(item)} + size={size} + {...item} + /> + </Col>) + )} + </Row> + </div>); + } +} + +module.exports = SideGrid; diff --git a/web/client/epics/identify.js b/web/client/epics/identify.js index e8390c81eb..6aca270c26 100644 --- a/web/client/epics/identify.js +++ b/web/client/epics/identify.js @@ -16,7 +16,7 @@ const {centerToMarkerSelector} = require('../selectors/layers'); const {mapSelector} = require('../selectors/map'); const {boundingMapRectSelector} = require('../selectors/maplayout'); const {centerToVisibleArea, isInsideVisibleArea} = require('../utils/CoordinatesUtils'); -const {getCurrentResolution} = require('../utils/MapUtils'); +const {getCurrentResolution, parseLayoutValue} = require('../utils/MapUtils'); /** * Epics for Identify and map info @@ -57,11 +57,17 @@ module.exports = { const boundingMapRect = boundingMapRectSelector(state); const coords = action.point && action.point && action.point.latlng; const resolution = getCurrentResolution(Math.round(map.zoom), 0, 21, 96); + const layoutBounds = boundingMapRect && map && map.size && { + left: parseLayoutValue(boundingMapRect.left, map.size.width), + bottom: parseLayoutValue(boundingMapRect.bottom, map.size.height), + right: parseLayoutValue(boundingMapRect.right, map.size.width), + top: parseLayoutValue(boundingMapRect.top, map.size.height) + }; // exclude cesium with cartographic options - if (!map || !boundingMapRect || !coords || action.point.cartographic || isInsideVisibleArea(coords, map, boundingMapRect, resolution)) { + if (!map || !layoutBounds || !coords || action.point.cartographic || isInsideVisibleArea(coords, map, layoutBounds, resolution)) { return Rx.Observable.of(updateCenterToMarker('disabled')); } - const center = centerToVisibleArea(coords, map, boundingMapRect, resolution); + const center = centerToVisibleArea(coords, map, layoutBounds, resolution); return Rx.Observable.of(updateCenterToMarker('enabled'), zoomToPoint(center.pos, center.zoom, center.crs)); }) ) diff --git a/web/client/localConfig.json b/web/client/localConfig.json index aed2549af9..111df42691 100644 --- a/web/client/localConfig.json +++ b/web/client/localConfig.json @@ -354,7 +354,8 @@ "declineUrl" : "http://www.google.com" } }, - "OmniBar", "Login", "Save", "SaveAs", "BurgerMenu", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher", "SearchServicesConfig", "WidgetsBuilder", "Widgets" + "OmniBar", "Login", "Save", "SaveAs", "BurgerMenu", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher", "SearchServicesConfig", "WidgetsBuilder", "Widgets", + "FloatingLegend" ], "embedded": [{ "name": "Map", diff --git a/web/client/plugins/DrawerMenu.jsx b/web/client/plugins/DrawerMenu.jsx index 7ac2083b6a..80f040607e 100644 --- a/web/client/plugins/DrawerMenu.jsx +++ b/web/client/plugins/DrawerMenu.jsx @@ -10,7 +10,6 @@ const React = require('react'); const PropTypes = require('prop-types'); const {connect} = require('react-redux'); const {createSelector} = require('reselect'); -const OverlayTrigger = require('../components/misc/OverlayTrigger'); const Message = require('./locale/Message'); @@ -18,7 +17,7 @@ const {toggleControl, setControlProperty} = require('../actions/controls'); const {changeMapStyle} = require('../actions/map'); -const {Button, Glyphicon, Panel, Tooltip} = require('react-bootstrap'); +const {Button: ButtonB, Glyphicon, Panel} = require('react-bootstrap'); const Section = require('./drawer/Section'); @@ -27,6 +26,8 @@ const {partialRight} = require('lodash'); const assign = require('object-assign'); const {mapLayoutValuesSelector} = require('../selectors/maplayout'); +const tooltip = require('../components/misc/enhancers/tooltip'); +const Button = tooltip(ButtonB); const menuSelector = createSelector([ state => state.controls.drawer && state.controls.drawer.enabled, @@ -48,6 +49,35 @@ const Menu = connect(menuSelector, { require('./drawer/drawer.css'); +const DrawerButton = connect(state => ({ + disabled: state.controls && state.controls.drawer && state.controls.drawer.disabled +}), { + toggleMenu: toggleControl.bind(null, 'drawer', null) +})(({ + id = '', + menuButtonStyle = {}, + buttonStyle = 'primary', + buttonClassName = 'square-button', + toggleMenu = () => {}, + disabled = false, + glyph = '1-layer', + tooltipId = 'toc.drawerButton', + tooltipPosition = 'bottom' +}) => + <Button + id={id} + style={menuButtonStyle} + bsStyle={buttonStyle} + key="menu-button" + className={buttonClassName} + onClick={toggleMenu} + disabled={disabled} + tooltipId={tooltipId} + tooltipPosition={tooltipPosition}> + <Glyphicon glyph={glyph}/> + </Button> +); + /** * DrawerMenu plugin. Shows a left menu with some pluins rendered inside it (typically the TOC). * @prop {string} cfg.glyph glyph icon to use for the button @@ -127,13 +157,9 @@ class DrawerMenu extends React.Component { }; render() { - let tooltip = <Tooltip key="drawerButtonTooltip" id="drawerButtonTooltip"><Message msgId={"toc.drawerButton"}/></Tooltip>; return ( <div id={this.props.id}> - <OverlayTrigger placement="bottom" key="drawerButtonTooltip" - overlay={tooltip}> - <Button id="drawer-menu-button" style={this.props.menuButtonStyle} bsStyle={this.props.buttonStyle} key="menu-button" className={this.props.buttonClassName} onClick={this.props.toggleMenu} disabled={this.props.disabled}><Glyphicon glyph={this.props.glyph}/></Button> - </OverlayTrigger> + <DrawerButton {...this.props} id="drawer-menu-button"/> <Menu single={this.props.singleSection} {...this.props.menuOptions} title={<Message msgId="menu" />} alignment="left"> {this.renderItems()} </Menu> @@ -142,12 +168,21 @@ class DrawerMenu extends React.Component { } } +const DrawerMenuPlugin = connect((state) => ({ + active: state.controls && state.controls.drawer && state.controls.drawer.active, + disabled: state.controls && state.controls.drawer && state.controls.drawer.disabled +}), { + toggleMenu: toggleControl.bind(null, 'drawer', null) +})(DrawerMenu); + module.exports = { - DrawerMenuPlugin: connect((state) => ({ - active: state.controls && state.controls.drawer && state.controls.drawer.active, - disabled: state.controls && state.controls.drawer && state.controls.drawer.disabled - }), { - toggleMenu: toggleControl.bind(null, 'drawer', null) - })(assign(DrawerMenu, {disablePluginIf: "{state('featuregridmode') === 'EDIT'}"})), + DrawerMenuPlugin: assign(DrawerMenuPlugin, { + disablePluginIf: "{state('featuregridmode') === 'EDIT'}", + FloatingLegend: { + priority: 1, + name: 'drawer-menu', + button: DrawerButton + } + }), reducers: {} }; diff --git a/web/client/plugins/FloatingLegend.jsx b/web/client/plugins/FloatingLegend.jsx new file mode 100644 index 0000000000..c090d8afd9 --- /dev/null +++ b/web/client/plugins/FloatingLegend.jsx @@ -0,0 +1,119 @@ +/* + * Copyright 2018, 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. + */ + +const React = require('react'); +const PropTypes = require('prop-types'); +const assign = require('object-assign'); +const {connect} = require('react-redux'); +const {createSelector} = require('reselect'); +const {reverse, head, get, isObject} = require('lodash'); +const {updateNode} = require('../actions/layers'); +const {resizeLegend, expandLegend} = require('../actions/floatinglegend'); +const {layersSelector} = require('../selectors/layers'); +const {currentLocaleSelector} = require('../selectors/locale'); +const {mapSelector} = require('../selectors/map'); +const {boundingMapRectSelector} = require('../selectors/maplayout'); +const {isFeatureGridOpen} = require('../selectors/featuregrid'); +const {legendSizeSelector, legendExpandedSelector} = require('../selectors/floatinglegend'); +const FloatingLegend = require('../components/TOC/FloatingLegend'); +const {parseLayoutValue, getScales} = require('../utils/MapUtils'); + +/** + * FloatingLegend plugin. + * This plugin shows a list of layers with visibility and opacity controls. + * If DrawerMenu is in localConfig it will be integrated with current plugin + * @memberof plugins + * @name FloatingLegend + * @class + * @prop {bool} cfg.disableOpacitySlider disable and hide opacity slider + * @prop {bool} expandedOnMount show expanded legend when component did mount + * @prop {number} width width dimension of legend + */ + +class FloatingLegendComponent extends React.Component { + static propTypes = { + items: PropTypes.array, + pluginName: PropTypes.string, + tooltipId: PropTypes.string + }; + + static defaultProps = { + items: [], + pluginName: 'drawer-menu', + tooltipId: 'floatinglegend.showTOC' + }; + + renderPanel() { + const Plugin = this.props.items && head(this.props.items.filter(item => item && item.name === this.props.pluginName)); + return Plugin && Plugin.plugin && <Plugin.plugin {...(Plugin.cgf || {})} items={Plugin.items || []} menuButtonStyle={{display: 'none'}}/>; + } + + renderToggleButton() { + const Plugin = this.props.items && head(this.props.items.filter(item => item && item.name === this.props.pluginName)); + return Plugin && Plugin.button && <Plugin.button {...(Plugin.cgf || {})} tooltipId={this.props.tooltipId}/>; + } + + render() { + return ( + <div style={{position: 'absolute', height: '100%'}}> + <FloatingLegend + {...this.props} + toggleButton={this.renderToggleButton()}/> + {this.renderPanel()} + </div> + ); + } +} + +const parseTitleObject = (title, currentLocale) => title && isObject(title) && (title[currentLocale] || title.default) || title || ''; + +const floatingLegendSelector = createSelector( + [ + layersSelector, + currentLocaleSelector, + legendSizeSelector, + legendExpandedSelector, + state => get(state, 'controls.drawer.enabled'), + mapSelector, + boundingMapRectSelector, + isFeatureGridOpen + ], + (layers, currentLocale, size, expanded, drawerEnabled, map, boundingMapRect, featuredGridOpen) => ({ + layers: featuredGridOpen && [] || layers && reverse([ + ...layers + .filter(layer => layer && layer.group !== 'background' && !layer.loadingError) + .map(({title, ...layer}) => ({...layer, title: parseTitleObject(title, currentLocale)})) + ]) || [], + title: map && map.info && map.info.name || '', + height: size.height || 300, + expanded, + disabled: drawerEnabled ? true : false, + maxHeight: map && map.size && map.size.height - 134 - (boundingMapRect && boundingMapRect.bottom && parseLayoutValue(boundingMapRect.bottom, map && map.size && map.size.height) || 0) + || 9999, + currentZoomLvl: map && map.zoom, + scales: getScales( + map && map.projection || 'EPSG:3857', + map && map.mapOptions && map.mapOptions.view && map.mapOptions.view.DPI || null + ) + }) +); + +const FloatingLegendPlugin = connect(floatingLegendSelector, { + onChange: updateNode, + onResize: resizeLegend, + onExpand: expandLegend +})(FloatingLegendComponent); + +module.exports = { + FloatingLegendPlugin: assign(FloatingLegendPlugin, { + disablePluginIf: "{state('featuregridmode') === 'EDIT'}" + }), + reducers: { + floatinglegend: require('../reducers/floatinglegend') + } +}; diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index fbe33e6d22..dd169044b9 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -90,7 +90,8 @@ module.exports = { RulesManagerFooter: require('../plugins/RulesManagerFooter'), FeaturedMaps: require('../plugins/FeaturedMaps'), NavMenu: require('./plugins/NavMenu'), - RulesEditorPlugin: require('../plugins/RulesEditor') + RulesEditorPlugin: require('../plugins/RulesEditor'), + FloatingLegendPlugin: require('../plugins/FloatingLegend') }, requires: { ReactSwipe: require('react-swipeable-views').default, diff --git a/web/client/reducers/__tests__/floatinglegend.js b/web/client/reducers/__tests__/floatinglegend.js new file mode 100644 index 0000000000..03aac4ca72 --- /dev/null +++ b/web/client/reducers/__tests__/floatinglegend.js @@ -0,0 +1,33 @@ +/* + * Copyright 2018, 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. + */ + +const expect = require('expect'); + +const {RESIZE_LEGEND, EXPAND_LEGEND} = require('../../actions/floatinglegend'); +const floatinglegend = require('../floatinglegend'); + +describe('Test the floatinglegend reducer', () => { + it('change legend size height and/or width', () => { + const size = {height: 300, width: 300}; + const action = { + type: RESIZE_LEGEND, + size + }; + const state = floatinglegend({}, action); + expect(state.size).toEqual(size); + }); + it('change legend expanded state', () => { + const expanded = true; + const action = { + type: EXPAND_LEGEND, + expanded + }; + const state = floatinglegend({}, action); + expect(state.expanded).toEqual(expanded); + }); +}); diff --git a/web/client/reducers/floatinglegend.js b/web/client/reducers/floatinglegend.js new file mode 100644 index 0000000000..6007bcbae6 --- /dev/null +++ b/web/client/reducers/floatinglegend.js @@ -0,0 +1,34 @@ +/* + * Copyright 2018, 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. + */ + +const { RESIZE_LEGEND, EXPAND_LEGEND } = require('../actions/floatinglegend'); + +const {set} = require('../utils/ImmutableUtils'); +/** + * Manages the state of the floatinglegend + * The properties represent the shape of the state + * @prop {object} size size of legend {width: 0, height: 0} + * @prop {boolan} expanded expanded state + * @memberof reducers + */ +function floatinglegend(state = {}, action) { + switch (action.type) { + case RESIZE_LEGEND: { + return set('size', { + width: action.size && action.size.width, + height: action.size && action.size.height + }, state); + } + case EXPAND_LEGEND: { + return {...state, expanded: action.expanded}; + } + default: + return state; + } +} +module.exports = floatinglegend; diff --git a/web/client/selectors/__tests__/floatinglegend-test.js b/web/client/selectors/__tests__/floatinglegend-test.js new file mode 100644 index 0000000000..8ae3af63cb --- /dev/null +++ b/web/client/selectors/__tests__/floatinglegend-test.js @@ -0,0 +1,38 @@ +/** +* Copyright 2017, 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. +*/ + +const expect = require('expect'); +const {legendSizeSelector, legendExpandedSelector} = require('../floatinglegend'); + +describe('Test floatinglegend selectors', () => { + it('test legendSizeSelector', () => { + let props = legendSizeSelector({}); + expect(props).toEqual({width: 0, height: 0}); + + const floatinglegend = { + size: { + width: 300, + height: 7 + } + }; + props = legendSizeSelector({floatinglegend}); + expect(props).toBe(floatinglegend.size); + }); + + it('test legendExpandedSelector', () => { + let props = legendExpandedSelector({}); + expect(props).toBe(false); + + const floatinglegend = { + expanded: true + }; + props = legendExpandedSelector({floatinglegend}); + expect(props).toBe(floatinglegend.expanded); + }); + +}); diff --git a/web/client/selectors/floatinglegend.js b/web/client/selectors/floatinglegend.js new file mode 100644 index 0000000000..0e951a99ce --- /dev/null +++ b/web/client/selectors/floatinglegend.js @@ -0,0 +1,33 @@ +/* +* Copyright 2018, 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. +*/ + +/** + * selects floatinglegend state + * @name floatinglegend + * @memberof selectors + * @static + */ + +module.exports = { + /** + * Get size of floatinglegend + * @function + * @memberof selectors.floatinglegend + * @param {object} state the state + * @return {object} size {width: 0, height: 0} + */ + legendSizeSelector: state => state.floatinglegend && state.floatinglegend.size || {width: 0, height: 0}, + /** + * Get expanded state of floatinglegend + * @function + * @memberof selectors.floatinglegend + * @param {object} state the state + * @return {boolean} + */ + legendExpandedSelector: state => state.floatinglegend && state.floatinglegend.expanded ? true : false +}; diff --git a/web/client/selectors/map.js b/web/client/selectors/map.js index 35ce985ca4..60351e2145 100644 --- a/web/client/selectors/map.js +++ b/web/client/selectors/map.js @@ -60,7 +60,7 @@ const scalesSelector = createSelector( */ const mapVersionSelector = (state) => state.map && state.map.present && state.map.present.version || 1; /** - * Get name/titlet of the map + * Get name/title of the map * @function * @memberof selectors.map * @param {object} state the state diff --git a/web/client/themes/default/less/toc.less b/web/client/themes/default/less/toc.less index 02a0bcf9d4..e71eb3958d 100644 --- a/web/client/themes/default/less/toc.less +++ b/web/client/themes/default/less/toc.less @@ -181,6 +181,11 @@ } +#mapstore-layers > .Sortable > .toc-default-group > span > .toc-group-children { + margin-left: 8px; + margin-right: 8px; +} + .toc-default-group { background-color: @ms2-color-background; @@ -321,14 +326,32 @@ -webkit-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.12), 2px -2px 6px rgba(0, 0, 0, 0.06); -moz-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.12), 2px -2px 6px rgba(0, 0, 0, 0.06); - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.12), 2px -2px 6px rgba(0, 0, 0, 0.06); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.12), 2px -2px 6px rgba(0, 0, 0, 0.06); + + .noUi-target { + background-color: @ms2-color-primary; + &.noUi-horizontal { + margin: 0; + .noUi-base { + .noUi-origin { + .noUi-handle { + border-top: 1px solid @ms2-color-background; + width: 10px; + height: 15px; + left: -5px; + top: -8px; + } + } + } + } + } .toc-default-layer-head { height: @square-btn-size; width: 100%; color: @ms2-color-text; background-color: @ms2-color-background; - margin-bottom: 10px; + margin-bottom: 0; padding: floor((@square-btn-size - @icon-size-md) / 2) 0; .toc-title { @@ -363,7 +386,7 @@ } .collapsible-toc { - padding: 10px; + padding: 10px 0 0 0; .row { margin-bottom: 10px; @@ -481,6 +504,10 @@ color: @ms2-color-text; } } + + .noUi-target { + display: none; + } } .SortableItem.is-placeholder.toc-default-layer, @@ -497,6 +524,9 @@ .collapsible-toc { display: none; } + .noUi-target { + display: none; + } } .SortableItem.is-dragging.toc-default-group, @@ -519,6 +549,10 @@ color: @ms2-color-text; } } + + .noUi-target { + display: none; + } } .SortableItem.is-placeholder.toc-default-group, @@ -541,4 +575,161 @@ .toc-group-children { display: none; } + + .noUi-target { + display: none; + } +} + + +.ms-legend-action { + .shadow-far; + position: absolute; + background-color: @ms2-color-background; + overflow: hidden; + display: flex; + left: 4px; + top: 4px; + flex-direction: column; + margin: 0; + border: none; + .panel-heading { + a { + &:hover { + text-decoration: none; + } + } + border: none; + padding: 0; + .panel-title { + a { + text-decoration: none; + cursor: default; + &:focus { + text-decoration: none; + } + } + .row { + display: flex; + align-items: center; + .ms-legend-header { + padding: 0; + display: flex; + align-items: center; + > .glyphicon { + color: @ms2-color-primary; + } + > div { + flex: 1; + padding: 0 8px; + overflow: hidden; + h5 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + > button { + &:last-child { + margin: 4px; + } + } + } + } + } + } + .panel-collapse { + flex: 1; + transition: unset; + overflow: auto; + border-top: 1px solid @ms2-color-shade-lighter; + .panel-body { + padding: 0; + .msSideGrid { + margin: 15px; + background-color: @ms2-color-background; + width: ~"calc(100% - 30px)"; + position: relative; + .mapstore-side-card { + &:hover { + transform: unset; + cursor: default; + .shadow-soft; + } + .ms-head { + display: flex; + .mapstore-side-preview { + width: auto; + padding: 8px; + overflow: visible; + > span { + font-size: @icon-size-md; + cursor: pointer; + } + } + .mapstore-side-card-info { + flex: 1; + display: flex; + flex-direction: column; + padding: 8px 16px 8px 8px; + .mapstore-side-card-title { + flex: 1; + font-size: @font-size-small; + } + } + } + + .ms-legend-container { + > div { + padding: 8px 0 8px 24px; + span { + font-size: @font-size-small * 0.85; + font-style: italic; + } + } + } + + } + } + + .noUi-target { + background-color: @ms2-color-primary; + &.noUi-horizontal { + margin: 0; + .noUi-base { + .noUi-origin { + .noUi-handle { + border-top: 1px solid @ms2-color-background; + width: 10px; + height: 15px; + left: -5px; + top: -8px; + } + } + } + } + } + } + } + + .react-resizable { + position: relative; + } + + .react-resizable-handle { + position: absolute; + width: 20px; + height: 20px; + bottom: 0; + right: 0; + background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+'); + background-position: bottom right; + padding: 0 3px 3px 0; + background-repeat: no-repeat; + background-origin: content-box; + box-sizing: border-box; + cursor: se-resize; + } } + diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index ef9206ff02..85674b6eca 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -342,6 +342,11 @@ "fieldsChanged": "Einige Felder wurden geändert" } }, + "floatinglegend": { + "showTOC": "Ebenen anzeigen", + "showLegend": "Legende anzeigen", + "hideLegend": "Legende ausblenden" + }, "toc": { "toggleLayerVisibility": "Ändere die Ebenen Sichtbarkeit", "displayLegendAndTools": "Legende und Werkzeuge zeigen", diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index 233644ccbd..fbbb8d9701 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -343,6 +343,11 @@ "fieldsChanged": "Some fields has been changed" } }, + "floatinglegend": { + "showTOC": "Show layers", + "showLegend": "Show legend", + "hideLegend": "Hide legend" + }, "toc": { "toggleLayerVisibility": "Toggle layer visibility", "displayLegendAndTools": "Display legend and tools", diff --git a/web/client/translations/data.es-ES b/web/client/translations/data.es-ES index 7244e25aa9..329c1d42e0 100644 --- a/web/client/translations/data.es-ES +++ b/web/client/translations/data.es-ES @@ -342,6 +342,11 @@ "fieldsChanged": "Algunos campos han sido cambiados" } }, + "floatinglegend": { + "showTOC": "Mostrar capas", + "showLegend": "Mostrar leyenda", + "hideLegend": "Ocultar leyenda" + }, "toc": { "toggleLayerVisibility": "Activar / desactivar la visibilidad del mapa", "displayLegendAndTools": "Mostrar la leyenda y las herramientas", diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index 8ecc53c95a..ae915afad3 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -343,6 +343,11 @@ "fieldsChanged": "Certains champs ont été modifiés" } }, + "floatinglegend": { + "showTOC": "Afficher les calques", + "showLegend": "Afficher la légende", + "hideLegend": "Cacher la légende" + }, "toc": { "toggleLayerVisibility": "Activer / désactiver la visibilité du calque", "displayLegendAndTools": "Afficher la légende et les outils", diff --git a/web/client/translations/data.hr-HR b/web/client/translations/data.hr-HR index 3f36e58fb5..facd4447c6 100644 --- a/web/client/translations/data.hr-HR +++ b/web/client/translations/data.hr-HR @@ -314,6 +314,11 @@ "fieldsChanged": "Neka polja su izmijenjena" } }, + "floatinglegend": { + "showTOC": "Show layers", + "showLegend": "Show legend", + "hideLegend": "Hide legend" + }, "toc": { "toggleLayerVisibility": "Izmijeni vidljivost sloja", "displayLegendAndTools": "Prikaži kazalo i alate", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index f4ecc895c5..78aafe91ca 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -342,6 +342,11 @@ "fieldsChanged": "Alcuni dati sono cambiati" } }, + "floatinglegend": { + "showTOC": "Mostra layers", + "showLegend": "Mostra legenda", + "hideLegend": "Nascondi legenda" + }, "toc": { "toggleLayerVisibility": "Attiva o disattiva la visibilità del livello", "displayLegendAndTools": "Visualizza legenda e strumenti aggiuntivi", diff --git a/web/client/translations/data.nl-NL b/web/client/translations/data.nl-NL index e2a1f31fd0..01c48aa138 100644 --- a/web/client/translations/data.nl-NL +++ b/web/client/translations/data.nl-NL @@ -244,6 +244,11 @@ "title": "geautoriseerde groepen" } }, + "floatinglegend": { + "showTOC": "Show layers", + "showLegend": "Show legend", + "hideLegend": "Hide legend" + }, "toc": { "toggleLayerVisibility": "Zet de zichtbaarheid van de laag aan / uit", "displayLegendAndTools": "Toon de legende en de tools", diff --git a/web/client/translations/data.zh-ZH b/web/client/translations/data.zh-ZH index 62b6134f62..221a804805 100644 --- a/web/client/translations/data.zh-ZH +++ b/web/client/translations/data.zh-ZH @@ -243,6 +243,11 @@ "title": "权限组" } }, + "floatinglegend": { + "showTOC": "Show layers", + "showLegend": "Show legend", + "hideLegend": "Hide legend" + }, "toc": { "toggleLayerVisibility": "切换图层可见性", "displayLegendAndTools": "显示图例和工具", diff --git a/web/client/utils/CoordinatesUtils.js b/web/client/utils/CoordinatesUtils.js index d51911e4ae..1a351aaed1 100644 --- a/web/client/utils/CoordinatesUtils.js +++ b/web/client/utils/CoordinatesUtils.js @@ -14,7 +14,7 @@ const assign = require('object-assign'); const {isArray, flattenDeep, chunk, cloneDeep} = require('lodash'); const lineIntersect = require('@turf/line-intersect'); const polygonToLinestring = require('@turf/polygon-to-linestring'); -const {head, isString, trim, isNumber} = require('lodash'); +const {head} = require('lodash'); const greatCircle = require('@turf/great-circle').default; const toPoint = require('turf-point'); @@ -210,19 +210,6 @@ const getExtentFromNormalized = (bounds, projection) => { isIDL}; }; -/** - * Return parsed number from layout value - * @param value {number|string} number or percentage value string - * @param size {number} only in case of percentage - * @return {number} - */ -const parseLayoutValue = (value, size = 0) => { - if (isString(value) && value.indexOf('%') !== -1) { - return parseFloat(trim(value)) * size / 100; - } - return isNumber(value) ? value : 0; -}; - /** * Utilities for Coordinates conversion. * @memberof utils @@ -722,10 +709,11 @@ const CoordinatesUtils = { const bbox = CoordinatesUtils.reprojectBbox(map.bbox.bounds, map.bbox.crs, map.projection); const layoutBounds = { - left: parseLayoutValue(layout.left, map.size.width), - bottom: parseLayoutValue(layout.bottom, map.size.height), - right: parseLayoutValue(layout.right, map.size.width), - top: parseLayoutValue(layout.top, map.size.height) + left: 0, + right: 0, + top: 0, + bottom: 0, + ...layout }; const visibleExtent = { @@ -759,10 +747,11 @@ const CoordinatesUtils = { const reprojectedCoords = reproject([normalizedCoords.lng, normalizedCoords.lat], 'EPSG:4326', map.projection); const layoutBounds = { - left: parseLayoutValue(layout.left, map.size.width), - bottom: parseLayoutValue(layout.bottom, map.size.height), - right: parseLayoutValue(layout.right, map.size.width), - top: parseLayoutValue(layout.top, map.size.height) + left: 0, + right: 0, + top: 0, + bottom: 0, + ...layout }; const visibleSize = { diff --git a/web/client/utils/MapUtils.js b/web/client/utils/MapUtils.js index 0f8711242f..3f9cd54f48 100644 --- a/web/client/utils/MapUtils.js +++ b/web/client/utils/MapUtils.js @@ -6,6 +6,8 @@ * LICENSE file in the root directory of this source tree. */ +const {isString, trim, isNumber} = require('lodash'); + const DEFAULT_SCREEN_DPI = 96; const METERS_PER_UNIT = { @@ -395,6 +397,23 @@ const getIdFromUri = (uri, regex = /data\/(\d+)/) => { return findDataDigit && findDataDigit.length && findDataDigit.length > 1 ? findDataDigit[1] : null; }; +/** + * Return parsed number from layout value + * if percentage returns percentage of second argument that should be a number + * eg. 20% of map height parseLayoutValue(20%, map.size.height) + * but if value is stored as number it will return the number + * eg. parseLayoutValue(50, map.size.height) returns 50 + * @param value {number|string} number or percentage value string + * @param size {number} only in case of percentage + * @return {number} + */ +const parseLayoutValue = (value, size = 0) => { + if (isString(value) && value.indexOf('%') !== -1) { + return parseFloat(trim(value)) * size / 100; + } + return isNumber(value) ? value : 0; +}; + module.exports = { EXTENT_TO_ZOOM_HOOK, RESOLUTIONS_HOOK, @@ -425,5 +444,6 @@ module.exports = { isSimpleGeomType, getSimpleGeomType, extractTileMatrixSetFromLayers, - getIdFromUri + getIdFromUri, + parseLayoutValue }; diff --git a/web/client/utils/__tests__/MapUtils-test.js b/web/client/utils/__tests__/MapUtils-test.js index 5c8744417f..968bc8d175 100644 --- a/web/client/utils/__tests__/MapUtils-test.js +++ b/web/client/utils/__tests__/MapUtils-test.js @@ -27,7 +27,8 @@ var { getCurrentResolution, saveMapConfiguration, extractTileMatrixSetFromLayers, - getIdFromUri + getIdFromUri, + parseLayoutValue } = require('../MapUtils'); describe('Test the MapUtils', () => { @@ -1178,4 +1179,15 @@ describe('Test the MapUtils', () => { expect(getIdFromUri('rest%2Fgeostore%2Fdata%2F')).toBe(null); }); + it('test parseLayoutValue', () => { + const percentageValue = parseLayoutValue('20%', 500); + expect(percentageValue).toBe(100); + + const numberValue = parseLayoutValue(20); + expect(numberValue).toBe(20); + + const noNumberValue = parseLayoutValue('value'); + expect(noNumberValue).toBe(0); + }); + });