Skip to content

Commit

Permalink
Fix geosolutions-it#2842. Implemented legend widget
Browse files Browse the repository at this point in the history
  • Loading branch information
offtherailz committed May 10, 2018
1 parent 1e856ff commit 9e98c4e
Show file tree
Hide file tree
Showing 20 changed files with 430 additions and 103 deletions.
2 changes: 1 addition & 1 deletion web/client/actions/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const loadDependencies = (dependencies) => ({
* Action triggered to start the connection flow. Typically starts the connection flow
* @param {array} availableDependencies Array of available dependency keys
* @param {object} the map of available dependencies where to choose.
* @param {object} options a map of connections to apply when the dependencies has been resolved
* @param {object} options a map of connections to apply when the dependencies has been resolved. E.g. mappings for dependenciesMap
* @param {string} target target of the connection. If not present we assume is the current editing widget (not yet supported)
*/
const toggleConnection = (active, availableDependencies, options, target) => ({
Expand Down
4 changes: 2 additions & 2 deletions web/client/components/TOC/FloatingLegend.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ const WMSLegend = require('./fragments/WMSLegend');
* @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 {function} onExpand return current expanded state
* @prop {node} toggleButton component added on legend header, left side
* @prop {number} minHeight minimun height of legend
* @prop {number} minHeight minimum 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
Expand Down
6 changes: 4 additions & 2 deletions web/client/components/misc/cardgrids/SideGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,22 @@ class SideGrid extends React.Component {
onItemClick: PropTypes.func,
colProps: PropTypes.object,
items: PropTypes.array,
cardComponent: PropTypes.element
cardComponent: PropTypes.element,
className: PropTypes.string
};

static defaultProps = {
size: '',
onItemClick: () => {},
colProps: {xs: 12},
className: "",
items: []
};

render() {
const {cardComponent, items, colProps, onItemClick, size} = this.props;
const Card = cardComponent || SideCard;
return (<div className="msSideGrid">
return (<div className={"msSideGrid" + (this.props.className ? " " + this.props.className : "")}>
<Row className="items-list">
{items.map((item, i) =>
(<Col key={item.id || i} {...colProps}>
Expand Down
66 changes: 66 additions & 0 deletions web/client/components/widgets/builder/wizard/LegendWizard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 {compose} = require('recompose');
const emptyState = require('../../../misc/enhancers/emptyState');


const {wizardHandlers} = require('../../../misc/wizard/enhancers');
const {Row, Col} = require('react-bootstrap');
const legendWidget = require('../../enhancers/legendWidget');

const WidgetOptions = require('./common/WidgetOptions');
const Wizard = wizardHandlers(require('../../../misc/wizard/WizardContainer'));
const StepHeader = require('../../../misc/wizard/StepHeader');
const Message = require('../../../I18N/Message');
const emptyLegendState = require('../../enhancers/emptyLegendState');

const enhancePreview = compose(
legendWidget,
emptyState(
({valid}) => !valid,
{
title: <Message msgId="widgets.builder.errors.noMapAvailableForLegend" />,
description: <Message msgId="widgets.builder.errors.noMapAvailableForLegendDescription" />
}
),
emptyLegendState(false)
);
const LegendPreview = enhancePreview(require('../../widget/LegendView'));
module.exports = ({
onChange = () => {}, onFinish = () => {}, setPage= () => {},
step=0,
dependencies,
valid,
data = {}
} = {}) => (
<Wizard
step={step}
setPage={setPage}
onFinish={onFinish}
hideButtons>
<Row>
<StepHeader title={<Message msgId={`widgets.builder.wizard.preview`} />} />
<Col xs={12}>
<div style={{ marginBottom: "30px" }}>
<LegendPreview
valid={valid}
dependencies={dependencies}
dependenciesMap={data.dependenciesMap}
key="widget-options"
onChange={onChange}
/>
</div>
</Col>
</Row>
<WidgetOptions
key="widget-options"
onChange={onChange}
/>
</Wizard>
);
55 changes: 55 additions & 0 deletions web/client/components/widgets/builder/wizard/legend/Toolbar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 Toolbar = require('../../../../misc/toolbar/Toolbar');
const getBackTooltipId = step => {
switch (step) {
case 1:
return "widgets.builder.wizard.backToPreview";
case 2:
return "widgets.builder.wizard.backToChartOptions";
default:
return "back";

}
};
const getNextTooltipId = (step, valid) => !valid
? undefined
: "widgets.builder.wizard.configureWidgetOptions";

const isValidStep1 = ({mapSync}) => mapSync;
const getSaveTooltipId = (step, {id} = {}) => {
if (id) {
return "widgets.builder.wizard.updateWidget";
}
return "widgets.builder.wizard.addToTheMap";
};

module.exports = ({ step = 0, editorData = {}, valid, stepButtons = [], onFinish = () => { }, setPage = () => { }} = {}) => (<Toolbar btnDefaultProps={{
bsStyle: "primary",
bsSize: "sm"
}}
buttons={[{
onClick: () => setPage(Math.max(0, step - 1)),
visible: step > 0,
glyph: "arrow-left",
tooltipId: getBackTooltipId(step)
}, ...stepButtons, {
onClick: () => setPage(Math.min(step + 1, 1)),
visible: step === 0,
disabled: step === 0 && !isValidStep1(editorData) || !valid,
glyph: "arrow-right",
tooltipId: getNextTooltipId(step, valid)
}, {
onClick: () => onFinish(Math.min(step + 1, 1)),
visible: step === 1,
glyph: "floppy-disk",
tooltipId: getSaveTooltipId(step, editorData)
}]} />);
18 changes: 18 additions & 0 deletions web/client/components/widgets/enhancers/emptyLegendState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 emptyState = require('../../misc/enhancers/emptyState');
const Message = require('../../I18N/Message');

module.exports = (asTooltip = true) => emptyState(
({ layers = [] }) => layers.length === 0,
{
[asTooltip ? "tooltip" : "title"]: <Message msgId="widgets.errors.noLegend" />,
description: !asTooltip && <Message msgId="widgets.errors.noLegendDescription" />
}
);
28 changes: 28 additions & 0 deletions web/client/components/widgets/enhancers/legendWidget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 {compose, withProps} = require('recompose');
const dependenciesToWidget = require('./dependenciesToWidget');
const {get} = require('lodash');
const {getScales} = require('../../../utils/MapUtils');

module.exports = compose(
dependenciesToWidget,
withProps(({ dependencies = {} }) => ({
layers: dependencies.layers || [],
scales: getScales(
// TODO: this is a fallback that checks the viewport if projection is not defined. We should use only projection
dependencies.projection || dependencies.viewport && dependencies.viewport.crs || 'EPSG:3857',
get( dependencies, "mapOptions.view.DPI")
),
currentZoomLvl: dependencies.zoom
})),
// filter backgrounds
withProps(
({ layers = [] }) => ({ layers: layers.filter((l = {}) => l.group !== "background" && l.visibility !== false && l.type !== "vector")})
),
);
12 changes: 11 additions & 1 deletion web/client/components/widgets/widget/DefaultWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const React = require('react');
const enhanceChartWidget = require('../enhancers/chartWidget');
const deleteWidget = require('../enhancers/deleteWidget');
const enhanceTableWidget = require('../enhancers/tableWidget');
const legendWidget = require('../enhancers/legendWidget');
const wpsChart = require('../enhancers/wpsChart');
const {compose} = require('recompose');
const dependenciesToFilter = require('../enhancers/dependenciesToFilter');
Expand Down Expand Up @@ -38,6 +39,11 @@ const CounterWidget = compose(
dependenciesToFilter,
enhanceCounter
)(require("./CounterWidget"));

const LegendWidget = compose(
legendWidget,
deleteWidget
)(require("./LegendWidget"));
module.exports = ({
dependencies,
exportCSV = () => {},
Expand Down Expand Up @@ -66,7 +72,11 @@ module.exports = ({
dependencies={dependencies}
onDelete={onDelete}
onEdit={onEdit} />

: w.widgetType === "legend"
? <LegendWidget {...w}
dependencies={dependencies}
onDelete={onDelete}
onEdit={onEdit} />
: (<ChartWidget {...w}
exportCSV={exportCSV}
dependencies={dependencies}
Expand Down
60 changes: 60 additions & 0 deletions web/client/components/widgets/widget/LegendView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 { isNil } = require('lodash');
const SideGrid = require('../../misc/cardgrids/SideGrid');
const { Glyphicon, Grid, Row, Col} = require('react-bootstrap');
const Slider = require('../../misc/Slider');
const WMSLegend = require('../../TOC/fragments/WMSLegend');

module.exports = ({
layers = [],
onChange = () => {},
legendProps = {},
currentZoomLvl,
disableOpacitySlider = true,
disableVisibility = true,
scales
}) => <SideGrid
className="compact-legend-grid"
size="sm"
items={layers.map(layer => ({
title: layer.title,
preview: disableVisibility
? null
: <Glyphicon className="text-primary"
glyph={layer.visibility ? 'eye-open' : 'eye-close'}
/>, // TODO: manage onClick
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={currentZoomLvl}
scales={scales}
{...legendProps} />
</Col>
</Row>
</Grid>
{!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) => onChange(layer.id, 'layers', { opacity: parseFloat((value[0] / 100).toFixed(2)) })} />
</div>}
</div>
)
})
)} />;
49 changes: 49 additions & 0 deletions web/client/components/widgets/widget/LegendWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 WidgetContainer = require('./WidgetContainer');
const Message = require('../../I18N/Message');
const emptyLegendState = require('../enhancers/emptyLegendState');
const InfoPopover = require('./InfoPopover');

const LegendView = emptyLegendState()(require('./LegendView'));
const renderHeaderLeftTopItem = ({ title, description } = {}) => {
return description ? <InfoPopover placement="top" title={title} text={description} /> : null;
};
const {
Glyphicon,
ButtonToolbar,
DropdownButton,
MenuItem
} = require('react-bootstrap');

module.exports = ({
onEdit = () => {},
toggleDeleteConfirm = () => {},
id, title,
headerStyle,
confirmDelete= false,
onDelete=() => {},
loading,
description,
...props
} = {}) =>
(<WidgetContainer id={`widget-text-${id}`} title={title} confirmDelete={confirmDelete} onDelete={onDelete} toggleDeleteConfirm={toggleDeleteConfirm} headerStyle={headerStyle}
topLeftItems={renderHeaderLeftTopItem({ loading, title, description })}

topRightItems={<ButtonToolbar>
<DropdownButton pullRight bsStyle="default" className="widget-menu" title={<Glyphicon glyph="option-vertical" />} noCaret id="dropdown-no-caret">
<MenuItem onClick={() => onEdit()} eventKey="3"><Glyphicon glyph="pencil"/>&nbsp;<Message msgId="widgets.widget.menu.edit" /></MenuItem>
<MenuItem onClick={() => toggleDeleteConfirm(true)} eventKey="2"><Glyphicon glyph="trash"/>&nbsp;<Message msgId="widgets.widget.menu.delete" /></MenuItem>
</DropdownButton>
</ButtonToolbar>}
>
<LegendView {...props} />
</WidgetContainer>

);
5 changes: 3 additions & 2 deletions web/client/epics/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ module.exports = {
...deps,
[m === "map" ? "viewport" : `${m}.viewport`]: `${m}.bbox`, // {viewport: "map.bbox"} or {"widgets[ID_W].viewport": "widgets[ID_W].bbox"}
[m === "map" ? "center" : `${m}.center`]: `${m}.center`, // {center: "map.center"} or {"widgets[ID_W].center": "widgets[ID_W].center"}
[m === "map" ? "zoom" : `${m}.zoom`]: `${m}.zoom`
[m === "map" ? "zoom" : `${m}.zoom`]: `${m}.zoom`,
[m === "map" ? "layers" : `${m}.layers`]: `${m}.layers`
}), {}))
),
/**
Expand All @@ -91,7 +92,7 @@ module.exports = {
*/
toggleWidgetConnectFlow: (action$, {getState = () => {}} = {}) =>
action$.ofType(TOGGLE_CONNECTION).switchMap(({ active, availableDependencies = [], options}) =>
active
(active && availableDependencies.length > 0)
// activate flow
? availableDependencies.length === 1
// case singleMap
Expand Down
2 changes: 1 addition & 1 deletion web/client/plugins/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const WidgetsView = compose(
}
})),
withHandlers({
// TODO: maybe using availableDependencies here will be better when different widget's type dependencies are supported
// TODO: maybe using availableDependencies here will be better when different widgets type dependencies are supported
isWidgetSelectable: ({ editingWidgetId }) => ({ widgetType, id }) => widgetType === "map" && id !== editingWidgetId
})
)(require('../components/dashboard/Dashboard'));
Expand Down
2 changes: 1 addition & 1 deletion web/client/plugins/WidgetsBuilder.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class SideBarComponent extends React.Component {
fluid={this.props.fluid}
dockStyle={{...this.props.layout, background: "white" /* TODO set it to undefined when you can inject a class inside Dock, to use theme */}}
>
<Builder enabled={this.props.enabled} onClose={this.props.onClose} typeFilter={({ type } = {}) => type !== 'map'}/>
<Builder enabled={this.props.enabled} onClose={this.props.onClose} typeFilter={({ type } = {}) => type !== 'map' && type !== 'legend' }/>
</Dock>);

}
Expand Down
Loading

0 comments on commit 9e98c4e

Please sign in to comment.