From 618c611e0a47eb1fe42e4925b88c59bb62f543a9 Mon Sep 17 00:00:00 2001 From: stefano bovio Date: Wed, 3 May 2017 10:18:10 +0200 Subject: [PATCH 1/6] Added support tosnapshot in IE/EDGE (Leaflet) (#1769) --- .../map/leaflet/snapshot/GrabMap.jsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/web/client/components/map/leaflet/snapshot/GrabMap.jsx b/web/client/components/map/leaflet/snapshot/GrabMap.jsx index 10ce043a1e..4ba39290ad 100644 --- a/web/client/components/map/leaflet/snapshot/GrabMap.jsx +++ b/web/client/components/map/leaflet/snapshot/GrabMap.jsx @@ -136,6 +136,7 @@ let GrabLMap = React.createClass({ doSnapshot(props) { // get map style shifted var leftString = window.getComputedStyle(this.mapDiv).getPropertyValue("left"); + leftString = leftString === 'auto' ? 0 : leftString; // get all the informations needed to snap svg before let mapPanes = this.mapDiv.getElementsByClassName("leaflet-map-pane"); @@ -149,10 +150,15 @@ let GrabLMap = React.createClass({ let svgH; let svgW; let svgString; - if (svg && svg.outerHTML) { - svgOffsetX = svgs[0] ? svgs[0].getBoundingClientRect().left : 0; - svgOffsetY = svgs[0] ? svgs[0].getBoundingClientRect().top : 0; - svgString = svgs[0].outerHTML; + if (svg) { + // get svg translate position to avoid overlay issue on IE + let svgTranslate = svg.style && svg.style.transform ? svg.style.transform.replace(/\(|\)|translate3d|translate/g, '').split('px, ') : ['0', '0']; + svgTranslate = [Number.parseFloat(svgTranslate[0].replace('px', '')), Number.parseFloat(svgTranslate[1].replace('px', ''))]; + svgTranslate = svg.style.transform && svg.style.transform.substr(0, 11) === 'translate3d' ? [0, 0] : svgTranslate; + + svgOffsetX = svgs[0] ? svgs[0].getBoundingClientRect().left - svgTranslate[0] : 0; + svgOffsetY = svgs[0] ? svgs[0].getBoundingClientRect().top - svgTranslate[1] : 0; + svgString = svgs[0].outerHTML ? svgs[0].outerHTML : this.outerHTML(svgs[0]); svgW = svg.getAttribute("width"); svgH = svg.getAttribute("height"); svg.setAttribute("style", ""); @@ -162,10 +168,10 @@ let GrabLMap = React.createClass({ left = parseInt( leftString.replace('px', ''), 10); } - // get pan position from translate 3d + // get pan position from translate 3d or translate (IE) let leafletPane = this.mapDiv.getElementsByClassName("leaflet-map-pane"); - let panPosition = leafletPane && leafletPane[0] && leafletPane[0].style && leafletPane[0].style.transform ? leafletPane[0].style.transform.replace(/\(|\)|translate3d/g, '').split('px, ') : ['0', '0']; - panPosition = [Number.parseFloat(panPosition[0]), Number.parseFloat(panPosition[1])]; + let panPosition = leafletPane && leafletPane[0] && leafletPane[0].style && leafletPane[0].style.transform ? leafletPane[0].style.transform.replace(/\(|\)|translate3d|translate/g, '').split('px, ') : ['0', '0']; + panPosition = [Number.parseFloat(panPosition[0].replace('px', '')), Number.parseFloat(panPosition[1].replace('px', ''))]; let tilePane = this.mapDiv.getElementsByClassName("leaflet-tile-pane"); // clone to change style attributes let tilePaneClone = tilePane && tilePane[0].cloneNode(true); @@ -306,6 +312,11 @@ let GrabLMap = React.createClass({ }, exportImage() { return this.refs.canvas.toDataURL(); + }, + outerHTML(node) { + const parent = document.createElement('div'); + parent.appendChild(node.cloneNode(true)); + return parent.innerHTML; } }); From 3212993f5e86748dacb5d035832d4633494b4768 Mon Sep 17 00:00:00 2001 From: stefano bovio Date: Wed, 3 May 2017 10:21:42 +0200 Subject: [PATCH 2/6] Fix #1692 WFS spatial query works only if geometry field name is the_geom (#1773) * Added geometry attribute on feautere type selected * Removed comment * Modified wfsquery epic structure --- web/client/actions/catalog.js | 2 +- web/client/actions/queryform.js | 11 ++- web/client/actions/wfsquery.js | 23 +----- .../components/data/query/QueryBuilder.jsx | 16 +--- .../query/__tests__/QueryBuilder-test.jsx | 7 -- web/client/epics/wfsquery.js | 77 +++++++++++++++++++ web/client/plugins/QueryPanel.jsx | 5 +- web/client/plugins/TOC.jsx | 5 +- web/client/reducers/query.js | 28 +------ web/client/reducers/queryform.js | 4 + 10 files changed, 101 insertions(+), 77 deletions(-) create mode 100644 web/client/epics/wfsquery.js diff --git a/web/client/actions/catalog.js b/web/client/actions/catalog.js index 392afa79a4..9b9c58d5ea 100644 --- a/web/client/actions/catalog.js +++ b/web/client/actions/catalog.js @@ -90,7 +90,7 @@ function textSearch(format, url, startPosition, maxRecords, text, options) { function addLayerAndDescribe(layer) { return (dispatch, getState) => { const state = getState(); - const layers = state && state.layers; + const layers = state && state.layers && state.layers.flat; const id = LayersUtils.getLayerId(layer, layers || []); dispatch(addLayer({...layer, id})); if (layer.type === 'wms') { diff --git a/web/client/actions/queryform.js b/web/client/actions/queryform.js index d2219f193e..74541a2992 100644 --- a/web/client/actions/queryform.js +++ b/web/client/actions/queryform.js @@ -17,6 +17,7 @@ const EXPAND_ATTRIBUTE_PANEL = 'EXPAND_ATTRIBUTE_PANEL'; const EXPAND_SPATIAL_PANEL = 'EXPAND_SPATIAL_PANEL'; const SELECT_SPATIAL_METHOD = 'SELECT_SPATIAL_METHOD'; const SELECT_SPATIAL_OPERATION = 'SELECT_SPATIAL_OPERATION'; +const CHANGE_SPATIAL_ATTRIBUTE = 'CHANGE_SPATIAL_ATTRIBUTE'; const REMOVE_SPATIAL_SELECT = 'REMOVE_SPATIAL_SELECT'; const SHOW_SPATIAL_DETAILS = 'SHOW_SPATIAL_DETAILS'; // const QUERY_FORM_SEARCH = 'QUERY_FORM_SEARCH'; @@ -133,6 +134,13 @@ function selectSpatialOperation(operation, fieldName) { }; } +function changeSpatialAttribute(attribute) { + return { + type: CHANGE_SPATIAL_ATTRIBUTE, + attribute + }; +} + function removeSpatialSelection() { return { type: REMOVE_SPATIAL_SELECT @@ -159,7 +167,6 @@ function changeDwithinValue(distance) { response: response }; } - function wfsLoadError(e) { return { type: WFS_LOAD_ERROR, @@ -297,6 +304,7 @@ module.exports = { EXPAND_SPATIAL_PANEL, SELECT_SPATIAL_METHOD, SELECT_SPATIAL_OPERATION, + CHANGE_SPATIAL_ATTRIBUTE, REMOVE_SPATIAL_SELECT, SHOW_SPATIAL_DETAILS, // QUERY_FORM_SEARCH, @@ -333,6 +341,7 @@ module.exports = { expandSpatialFilterPanel, selectSpatialMethod, selectSpatialOperation, + changeSpatialAttribute, removeSpatialSelection, showSpatialSelectionDetails, query, diff --git a/web/client/actions/wfsquery.js b/web/client/actions/wfsquery.js index c27a8feef0..ac601462d5 100644 --- a/web/client/actions/wfsquery.js +++ b/web/client/actions/wfsquery.js @@ -76,26 +76,6 @@ function queryError(error) { }; } -function describeFeatureType(baseUrl, typeName) { - return (dispatch) => { - return axios.get(baseUrl + '?service=WFS&version=1.1.0&request=DescribeFeatureType&typeName=' + typeName + '&outputFormat=application/json').then((response) => { - if (typeof response.data === 'object') { - dispatch(featureTypeLoaded(typeName, response.data)); - } else { - try { - JSON.parse(response.data); - } catch(e) { - dispatch(featureTypeError(typeName, 'Error from WFS: ' + e.message)); - } - - } - - }).catch((e) => { - dispatch(featureTypeError(typeName, e)); - }); - }; -} - function loadFeature(baseUrl, typeName) { return (dispatch) => { return axios.get(baseUrl + '?service=WFS&version=1.1.0&request=GetFeature&typeName=' + typeName + '&outputFormat=application/json').then((response) => { @@ -196,7 +176,8 @@ module.exports = { QUERY_ERROR, RESET_QUERY, featureTypeSelected, - describeFeatureType, + featureTypeLoaded, + featureTypeError, loadFeature, createQuery, query, diff --git a/web/client/components/data/query/QueryBuilder.jsx b/web/client/components/data/query/QueryBuilder.jsx index aa5a723d10..33786527ee 100644 --- a/web/client/components/data/query/QueryBuilder.jsx +++ b/web/client/components/data/query/QueryBuilder.jsx @@ -87,8 +87,7 @@ const QueryBuilder = React.createClass({ onUpdateLogicCombo: () => {}, onRemoveGroupField: () => {}, onChangeCascadingValue: () => {}, - onExpandAttributeFilterPanel: () => {}, - onLoadFeatureTypeConfig: () => {} + onExpandAttributeFilterPanel: () => {} }, spatialFilterActions: { onExpandSpatialFilterPanel: () => {}, @@ -107,19 +106,6 @@ const QueryBuilder = React.createClass({ } }; }, - componentDidMount() { - if (this.props.featureTypeConfigUrl && this.props.attributes.length < 1) { - this.props.attributeFilterActions.onLoadFeatureTypeConfig( - this.props.featureTypeConfigUrl, this.props.params); - } - }, - componentWillReceiveProps(props) { - let url = props.featureTypeConfigUrl; - let params = props.params !== this.props.params ? props.params : this.props.params; - if (url !== this.props.featureTypeConfigUrl) { - this.props.attributeFilterActions.onLoadFeatureTypeConfig(url, params); - } - }, render() { if (this.props.featureTypeError !== "") { return (
{this.props.featureTypeErrorText}
); diff --git a/web/client/components/data/query/__tests__/QueryBuilder-test.jsx b/web/client/components/data/query/__tests__/QueryBuilder-test.jsx index 1ad5fdaffe..734add2b4b 100644 --- a/web/client/components/data/query/__tests__/QueryBuilder-test.jsx +++ b/web/client/components/data/query/__tests__/QueryBuilder-test.jsx @@ -145,20 +145,13 @@ describe('QueryBuilder', () => { it('creates the QueryBuilder component in error state', () => { - let attributeFilterActions = { - onLoadFeatureTypeConfig: () => {} - }; - let spy = expect.spyOn(attributeFilterActions, 'onLoadFeatureTypeConfig'); - const querybuilder = ReactDOM.render(, document.getElementById("container")); expect(querybuilder).toExist(); - expect(spy.calls.length).toEqual(1); }); it('creates the QueryBuilder component with empty filter support', () => { diff --git a/web/client/epics/wfsquery.js b/web/client/epics/wfsquery.js new file mode 100644 index 0000000000..fe37e2b15b --- /dev/null +++ b/web/client/epics/wfsquery.js @@ -0,0 +1,77 @@ +/* + * 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 Rx = require('rxjs'); +const axios = require('../libs/ajax'); +const {changeSpatialAttribute} = require('../actions/queryform'); +const {FEATURE_TYPE_SELECTED, featureTypeLoaded, featureTypeError} = require('../actions/wfsquery'); + +const types = { + 'xsd:string': 'string', + 'xsd:dateTime': 'date', + 'xsd:number': 'number', + 'xsd:int': 'number' +}; +const fieldConfig = {}; +const extractInfo = (data) => { + return { + geometry: data.featureTypes[0].properties + .filter((attribute) => attribute.type.indexOf('gml:') === 0) + .map((attribute) => { + let conf = { + label: attribute.name, + attribute: attribute.name, + type: 'geometry', + valueId: "id", + valueLabel: "name", + values: [] + }; + conf = fieldConfig[attribute.name] ? {...conf, ...fieldConfig[attribute.name]} : conf; + return conf; + }), + attributes: data.featureTypes[0].properties + .filter((attribute) => attribute.type.indexOf('gml:') !== 0) + .map((attribute) => { + let conf = { + label: attribute.name, + attribute: attribute.name, + type: types[attribute.type], + valueId: "id", + valueLabel: "name", + values: [] + }; + conf = fieldConfig[attribute.name] ? {...conf, ...fieldConfig[attribute.name]} : conf; + return conf; + }) + }; +}; + +const featureTypeSelectedEpic = action$ => + action$.ofType(FEATURE_TYPE_SELECTED).switchMap(action => { + return Rx.Observable.defer( () => + axios.get(action.url + '?service=WFS&version=1.1.0&request=DescribeFeatureType&typeName=' + action.typeName + '&outputFormat=application/json')) + .map((response) => { + if (typeof response.data === 'object' && response.data.featureTypes && response.data.featureTypes[0]) { + const info = extractInfo(response.data); + const geometry = info.geometry[0] && info.geometry[0].attribute ? info.geometry[0].attribute : 'the_geom'; + return Rx.Observable.from([featureTypeLoaded(action.typeName, info), changeSpatialAttribute(geometry)]); + } + try { + JSON.parse(response.data); + } catch(e) { + return Rx.Observable.from([featureTypeError(action.typeName, 'Error from WFS: ' + e.message)]); + } + return Rx.Observable.from([featureTypeError(action.typeName, 'Error: feature types are empty')]); + }) + .mergeAll() + .catch(e => Rx.Observable.of(featureTypeError(action.typeName, e.message))); + }); + +module.exports = { + featureTypeSelectedEpic +}; diff --git a/web/client/plugins/QueryPanel.jsx b/web/client/plugins/QueryPanel.jsx index 492665f7b6..b506e16cef 100644 --- a/web/client/plugins/QueryPanel.jsx +++ b/web/client/plugins/QueryPanel.jsx @@ -24,6 +24,8 @@ const LayersUtils = require('../utils/LayersUtils'); // include application component const QueryBuilder = require('../components/data/query/QueryBuilder'); +const {featureTypeSelectedEpic} = require('../epics/wfsquery'); + const {bindActionCreators} = require('redux'); const { // QueryBuilder action functions @@ -234,5 +236,6 @@ module.exports = { reducers: { queryform: require('../reducers/queryform'), query: require('../reducers/query') - } + }, + epics: {featureTypeSelectedEpic} }; diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index 6bc20641fa..f61d991c5f 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -52,7 +52,7 @@ const { zoneChange } = require('../actions/queryform'); -const {createQuery, toggleQueryPanel, describeFeatureType} = require('../actions/wfsquery'); +const {createQuery, toggleQueryPanel} = require('../actions/wfsquery'); const { changeDrawingStatus, @@ -92,9 +92,6 @@ const SmartQueryForm = connect((state) => { return { attributeFilterActions: bindActionCreators({ - onLoadFeatureTypeConfig: (url, params) => { - return describeFeatureType(url, params.typeName); - }, onAddGroupField: addGroupField, onAddFilterField: addFilterField, onRemoveFilterField: removeFilterField, diff --git a/web/client/reducers/query.js b/web/client/reducers/query.js index 1f47ef5be0..a77565356c 100644 --- a/web/client/reducers/query.js +++ b/web/client/reducers/query.js @@ -24,32 +24,6 @@ const {RESET_CONTROLS} = require('../actions/controls'); const assign = require('object-assign'); -const types = { - 'xsd:string': 'string', - 'xsd:dateTime': 'date', - 'xsd:number': 'number', - 'xsd:int': 'number' -}; -const fieldConfig = {}; -const extractInfo = (featureType) => { - return { - attributes: featureType.featureTypes[0].properties - .filter((attribute) => attribute.type.indexOf('gml:') !== 0) - .map((attribute) => { - let conf = { - label: attribute.name, - attribute: attribute.name, - type: types[attribute.type], - valueId: "id", - valueLabel: "name", - values: [] - }; - conf = fieldConfig[attribute.name] ? {...conf, ...fieldConfig[attribute.name]} : conf; - return conf; - }) - }; -}; - const extractData = (feature) => { return ['STATE_NAME', 'STATE_ABBR', 'SUB_REGION', 'STATE_FIPS' ].map((attribute) => ({ attribute, @@ -86,7 +60,7 @@ function query(state = initialState, action) { } case FEATURE_TYPE_LOADED: { return assign({}, state, { - featureTypes: assign({}, state.featureTypes, {[action.typeName]: extractInfo(action.featureType)}) + featureTypes: assign({}, state.featureTypes, {[action.typeName]: action.featureType}) }); } case FEATURE_TYPE_ERROR: { diff --git a/web/client/reducers/queryform.js b/web/client/reducers/queryform.js index fe658a75f2..0ed268517c 100644 --- a/web/client/reducers/queryform.js +++ b/web/client/reducers/queryform.js @@ -19,6 +19,7 @@ const { EXPAND_SPATIAL_PANEL, SELECT_SPATIAL_METHOD, SELECT_SPATIAL_OPERATION, + CHANGE_SPATIAL_ATTRIBUTE, REMOVE_SPATIAL_SELECT, SHOW_SPATIAL_DETAILS, QUERY_FORM_RESET, @@ -167,6 +168,9 @@ function queryform(state = initialState, action) { case SELECT_SPATIAL_OPERATION: { return assign({}, state, {spatialField: assign({}, state.spatialField, {[action.fieldName]: action.operation})}); } + case CHANGE_SPATIAL_ATTRIBUTE: { + return assign({}, state, { spatialField: assign({}, state.spatialField, {attribute: action.attribute}) }); + } case CHANGE_DRAWING_STATUS: { if (action.owner === "queryform" && action.status === "start") { return assign({}, state, {toolbarEnabled: false}); From 58292f534b4b6a22344bb673f1e18e04ce34f51e Mon Sep 17 00:00:00 2001 From: stefano bovio Date: Wed, 3 May 2017 10:22:28 +0200 Subject: [PATCH 3/6] Added print zoom scale to action (#1770) --- web/client/components/mapcontrols/scale/ScaleBox.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/components/mapcontrols/scale/ScaleBox.jsx b/web/client/components/mapcontrols/scale/ScaleBox.jsx index d2c52610af..af0f2152bb 100644 --- a/web/client/components/mapcontrols/scale/ScaleBox.jsx +++ b/web/client/components/mapcontrols/scale/ScaleBox.jsx @@ -39,7 +39,7 @@ var ScaleBox = React.createClass({ }, onComboChange(event) { var selectedZoomLvl = parseInt(event.nativeEvent.target.value, 10); - this.props.onChange(selectedZoomLvl); + this.props.onChange(selectedZoomLvl, this.props.scales[selectedZoomLvl]); }, getOptions() { return this.props.scales.map((item, index) => { From 6a3ea6162c0f61638766152eae8e8471030b899f Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 3 May 2017 11:44:52 +0200 Subject: [PATCH 4/6] Fix #1777. Forced version of react-input-autosize (#1778) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3e8bba105e..d1fd3528b9 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "react-dom": "15.4.2", "react-draggable": "2.2.3", "react-dropzone": "3.4.0", + "react-input-autosize": "1.1.0", "react-intl": "2.2.3", "react-joyride": "1.10.1", "react-nouislider": "1.11.0", From 049f697eaadf44dc2ed9d5a8becdce38e61fb3f3 Mon Sep 17 00:00:00 2001 From: Matteo V Date: Wed, 3 May 2017 12:17:24 +0200 Subject: [PATCH 5/6] Fixed draw support and epic general error handling (#1779) * added drawing of passed geometry when draw interasction starts * Added a wrapper to manage un-handled epic error --- .../components/map/openlayers/DrawSupport.jsx | 5 ++- web/client/utils/PluginsUtils.js | 18 ++++++-- .../utils/__tests__/PluginUtils-test.js | 44 ++++++++++++++++++- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/web/client/components/map/openlayers/DrawSupport.jsx b/web/client/components/map/openlayers/DrawSupport.jsx index 7fec566132..ce0bbd93c7 100644 --- a/web/client/components/map/openlayers/DrawSupport.jsx +++ b/web/client/components/map/openlayers/DrawSupport.jsx @@ -169,7 +169,6 @@ const DrawSupport = React.createClass({ features: features, condition: ol.events.condition.always }; - // Prepare the properties for the BBOX drawing let roiProps = {}; switch (geometryType) { @@ -241,7 +240,6 @@ const DrawSupport = React.createClass({ } default : return {}; } - let drawProps = assign({}, drawBaseProps, roiProps); // create an interaction to draw with @@ -293,6 +291,9 @@ const DrawSupport = React.createClass({ this.props.map.addInteraction(draw); this.drawInteraction = draw; this.drawSource.clear(); + if (newProps.features.length > 0 ) { + this.addFeatures(newProps.features || []); + } }, removeDrawInteraction: function() { if (this.drawInteraction !== null) { diff --git a/web/client/utils/PluginsUtils.js b/web/client/utils/PluginsUtils.js index 0e6ce5eb0e..f6180194f3 100644 --- a/web/client/utils/PluginsUtils.js +++ b/web/client/utils/PluginsUtils.js @@ -142,13 +142,24 @@ const pluginsMergeProps = (stateProps, dispatchProps, ownProps) => { const {pluginCfg, ...otherProps} = ownProps; return assign({}, otherProps, stateProps, dispatchProps, pluginCfg || {}); }; - +/** + * default wrapper for the epics. + * @param epic the epic to wrap + * @return the epic wrapped with error catch and re-subscribe functionalities.S + * @memberof utils.PluginsUtils + */ +const defaultEpicWrapper = epic => (...args) => + epic(...args).catch((error, source) => { + setTimeout(() => { throw error; }, 0); + return source; + }); /** * Utilities to manage plugins * @class * @memberof utils */ const PluginsUtils = { + defaultEpicWrapper, /** * Produces the reducers from the plugins, combined with other plugins * @param {array} plugins the plugins @@ -163,11 +174,12 @@ const PluginsUtils = { * Produces the rootEpic for the plugins, combined with other epics passed as 2nd argument * @param {array} plugins the plugins * @param {function[]} [epics] the epics to add to the plugins' ones + * @param {function} [epicWrapper] returns a function that wraps the epic * @return {function} the rootEpic, obtained combining plugins' epics and the other epics passed as argument. */ - combineEpics: (plugins, epics = {}) => { + combineEpics: (plugins, epics = {}, epicWrapper = defaultEpicWrapper) => { const pluginEpics = assign({}, getEpics(plugins), epics); - return combineEpics( ...Object.keys(pluginEpics).map(k => pluginEpics[k])); + return combineEpics( ...Object.keys(pluginEpics).map(k => pluginEpics[k]).map(epicWrapper)); }, getReducers, filterState, diff --git a/web/client/utils/__tests__/PluginUtils-test.js b/web/client/utils/__tests__/PluginUtils-test.js index 4d428a0716..78a9908996 100644 --- a/web/client/utils/__tests__/PluginUtils-test.js +++ b/web/client/utils/__tests__/PluginUtils-test.js @@ -12,7 +12,23 @@ const expect = require('expect'); const PluginsUtils = require('../PluginsUtils'); const assign = require('object-assign'); const MapSearchPlugin = require('../../plugins/MapSearch'); +const Rx = require('rxjs'); +const { ActionsObservable } = require('redux-observable'); +const epicTest = (epic, count, action, callback, state = {}) => { + const actions = new Rx.Subject(); + const actions$ = new ActionsObservable(actions); + const store = { getState: () => state }; + epic(actions$, store) + .take(count) + .toArray() + .subscribe(callback); + if (action.length) { + action.map(act => actions.next(act)); + } else { + actions.next(action); + } +}; describe('PluginsUtils', () => { beforeEach((done) => { document.body.innerHTML = '
'; @@ -80,10 +96,36 @@ describe('PluginsUtils', () => { }); it('combineEpics', () => { const plugins = {MapSearchPlugin: MapSearchPlugin}; - const appEpics = {appEpics: (actions$) => actions$.ofType('TEST_ACTION').map({type: "NEW_ACTION_TEST"})}; + const appEpics = {appEpics: (actions$) => actions$.ofType('TEST_ACTION').map(() => ({type: "NEW_ACTION_TEST"}))}; const epics = PluginsUtils.combineEpics(plugins, appEpics); expect(typeof epics ).toEqual('function'); }); + it('combineEpics with defaultEpicWrapper', (done) => { + const plugins = {MapSearchPlugin: MapSearchPlugin}; + const appEpics = { + appEpics: (actions$) => actions$.filter( a => a.type === 'TEST_ACTION').map(() => ({type: "RESPONSE"})), + appEpics2: (actions$) => actions$.filter( a => a.type === 'TEST_ACTION1').map(() => {throw new Error(); })}; + const epics = PluginsUtils.combineEpics(plugins, appEpics); + expect(typeof epics ).toEqual('function'); + epicTest(epics, 1, [{type: 'TEST_ACTION1'}, {type: 'TEST_ACTION'}], actions => { + expect(actions.length).toBe(1); + expect(actions[0].type).toBe("RESPONSE"); + done(); + }); + }); + + it('combineEpics with custom wrapper', (done) => { + const plugins = {MapSearchPlugin: MapSearchPlugin}; + let counter = 0; + const appEpics = { + appEpics: (actions$) => actions$.filter( a => a.type === 'TEST_ACTION').map(() => ({type: "RESPONSE"}))}; + const epics = PluginsUtils.combineEpics(plugins, appEpics, epic => (...args) => {counter++; return epic(...args); }); + epicTest( epics, 1, [{type: 'TEST_ACTION1'}, {type: 'TEST_ACTION'}], () => { + expect(counter).toBe(1); + done(); + }); + }); + it('connect', () => { const MyComponent = (props) =>
{props.test}
; const Connected = PluginsUtils.connect((state) => ({test: state.test}), {})(MyComponent); From 5359643a1b047f2e98acdf9863a90fc0d127d7fb Mon Sep 17 00:00:00 2001 From: Lorenzo Natali Date: Wed, 3 May 2017 12:25:32 +0200 Subject: [PATCH 6/6] First Implementation of the 3d switcher. (#1754) * First Implementation of the 3d switcher. The GlobeSwitcher plugin allow to switch to Globe view. In this pull request I also syncronize the maptype with it's reducer to keep in sync the maptype forever in the state. This is a first step to manage maptype's dependent tools (e.g. navigation tools) using react-redux system. * Fixed duplicated export and docs --- docma-config.json | 8 ++ .../actions/__tests__/globeswitcher-test.js | 31 +++++++ web/client/actions/__tests__/maptype-test.jsx | 23 +++++ web/client/actions/globeswitcher.js | 58 ++++++++++++ .../actions/home.js => actions/maptype.js} | 15 +++- .../buttons/GlobeViewSwitcherButton.jsx | 89 +++++++++++++++++++ .../GlobeViewSwitcherButton-test.jsx | 36 ++++++++ .../epics/__tests__/globeswitcher-test.js | 52 +++++++++++ web/client/epics/globeswitcher.js | 43 +++++++++ web/client/epics/maptype.js | 39 ++++++++ web/client/localConfig.json | 4 +- web/client/plugins/GlobeViewSwitcher.jsx | 49 ++++++++++ web/client/plugins/Maps.jsx | 4 +- web/client/plugins/searchbar/ToggleButton.jsx | 2 +- web/client/product/app.jsx | 2 +- web/client/product/plugins.js | 3 +- web/client/product/plugins/MapType.jsx | 19 ++-- web/client/product/reducers/home.js | 20 ----- .../reducers/__tests__/globeswitcher-test.js | 24 +++++ web/client/reducers/__tests__/maps-test.jsx | 6 +- web/client/reducers/__tests__/maptype-test.js | 17 ++++ web/client/reducers/globeswitcher.js | 35 ++++++++ web/client/reducers/maps.js | 7 -- web/client/reducers/maptype.js | 30 +++++++ 24 files changed, 568 insertions(+), 48 deletions(-) create mode 100644 web/client/actions/__tests__/globeswitcher-test.js create mode 100644 web/client/actions/__tests__/maptype-test.jsx create mode 100644 web/client/actions/globeswitcher.js rename web/client/{product/actions/home.js => actions/maptype.js} (53%) create mode 100644 web/client/components/buttons/GlobeViewSwitcherButton.jsx create mode 100644 web/client/components/buttons/__tests__/GlobeViewSwitcherButton-test.jsx create mode 100644 web/client/epics/__tests__/globeswitcher-test.js create mode 100644 web/client/epics/globeswitcher.js create mode 100644 web/client/epics/maptype.js create mode 100644 web/client/plugins/GlobeViewSwitcher.jsx delete mode 100644 web/client/product/reducers/home.js create mode 100644 web/client/reducers/__tests__/globeswitcher-test.js create mode 100644 web/client/reducers/__tests__/maptype-test.js create mode 100644 web/client/reducers/globeswitcher.js create mode 100644 web/client/reducers/maptype.js diff --git a/docma-config.json b/docma-config.json index 520037d904..551cfb114d 100644 --- a/docma-config.json +++ b/docma-config.json @@ -108,6 +108,7 @@ "framework" : [ "web/client/components/index.jsdoc", "web/client/components/buttons/FullScreenButton.jsx", + "web/client/components/buttons/GlobeViewSwitcherButton.jsx", "web/client/components/buttons/GoFullButton.jsx", "web/client/components/mapcontrols/search/SearchBar.jsx", "web/client/components/buttons/ToggleButton.jsx", @@ -116,16 +117,22 @@ "web/client/actions/index.jsdoc", "web/client/actions/controls.js", "web/client/actions/fullscreen.js", + "web/client/actions/globeswitcher.js", "web/client/actions/maps.js", + "web/client/actions/maptype.js", "web/client/actions/search.js", "web/client/reducers/index.jsdoc", "web/client/reducers/controls.js", + "web/client/reducers/globeswitcher.js", "web/client/reducers/maps.js", + "web/client/reducers/maptype.js", "web/client/reducers/search.js", "web/client/epics/index.jsdoc", "web/client/epics/fullscreen.js", + "web/client/epics/globeswitcher.js", + "web/client/epics/maptype.js", "web/client/epics/search.js", "web/client/utils/index.jsdoc", @@ -137,6 +144,7 @@ "web/client/plugins/index.jsdoc", "web/client/plugins/BackgroundSwitcher.jsx", "web/client/plugins/DrawerMenu.jsx", + "web/client/plugins/GlobeViewSwitcher.jsx", "web/client/plugins/GoFull.jsx", "web/client/plugins/Map.jsx", "web/client/plugins/FullScreen.jsx", diff --git a/web/client/actions/__tests__/globeswitcher-test.js b/web/client/actions/__tests__/globeswitcher-test.js new file mode 100644 index 0000000000..c3eaa937c8 --- /dev/null +++ b/web/client/actions/__tests__/globeswitcher-test.js @@ -0,0 +1,31 @@ +/* + * 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 { + toggle3d, + updateLast2dMapType, + TOGGLE_3D, + UPDATE_LAST_2D_MAPTYPE +} = require('../globeswitcher'); + +describe('Test correctness of the maptype actions', () => { + + it('toggle3d', () => { + const retVal = toggle3d(true); + expect(retVal).toExist(); + expect(retVal.type).toBe(TOGGLE_3D); + expect(retVal.enable).toBe(true); + }); + it('updateLast2dMapType', () => { + const retVal = updateLast2dMapType("leaflet"); + expect(retVal).toExist(); + expect(retVal.type).toBe(UPDATE_LAST_2D_MAPTYPE); + expect(retVal.mapType).toBe('leaflet'); + }); +}); diff --git a/web/client/actions/__tests__/maptype-test.jsx b/web/client/actions/__tests__/maptype-test.jsx new file mode 100644 index 0000000000..053f44486f --- /dev/null +++ b/web/client/actions/__tests__/maptype-test.jsx @@ -0,0 +1,23 @@ +/* + * 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 { + MAP_TYPE_CHANGED, + changeMapType +} = require('../maptype'); + +describe('Test correctness of the maptype actions', () => { + + it('changeMapType', () => { + const retVal = changeMapType('maptype'); + expect(retVal).toExist(); + expect(retVal.type).toBe(MAP_TYPE_CHANGED); + expect(retVal.mapType).toBe('maptype'); + }); +}); diff --git a/web/client/actions/globeswitcher.js b/web/client/actions/globeswitcher.js new file mode 100644 index 0000000000..4713314d8e --- /dev/null +++ b/web/client/actions/globeswitcher.js @@ -0,0 +1,58 @@ +/* + * 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 TOGGLE_3D = "TOGGLE_3D"; +const UPDATE_LAST_2D_MAPTYPE = "UPDATE_LAST_2D_MAPTYPE"; +/** + * Emitted when 3d map have to be toggled + * @memberof actions.globeswitcher + * @param {boolean} enable true for enable, false for disable + * @return {action} the action of type `TOGGLE_FULLSCREEN` with enable flag and element selector. + * ``` + * { + * type: TOGGLE_3D, + * enable + * } + * ``` + */ +function toggle3d(enable, originalMapType) { + return { + type: TOGGLE_3D, + enable, + originalMapType + }; +} +/** + * Saves the last 2d map + * @memberof actions.globeswitcher + * @param {string} mapType last maptype + * @return {object} action + * ``` + * { + * type: MAPTYPE_2D_SELECTED, + * mapType + * } + * ``` + */ +function updateLast2dMapType(mapType) { + return { + type: UPDATE_LAST_2D_MAPTYPE, + mapType + }; +} +/** + * Actions for Globe Switcher Plugin. + * @name actions.globeswitcher + */ +module.exports = { + toggle3d, + updateLast2dMapType, + UPDATE_LAST_2D_MAPTYPE, + TOGGLE_3D +}; diff --git a/web/client/product/actions/home.js b/web/client/actions/maptype.js similarity index 53% rename from web/client/product/actions/home.js rename to web/client/actions/maptype.js index be79586c48..aec102bbc5 100644 --- a/web/client/product/actions/home.js +++ b/web/client/actions/maptype.js @@ -1,5 +1,5 @@ -/** - * Copyright 2016, GeoSolutions Sas. +/* + * Copyright 2017, GeoSolutions Sas. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -8,11 +8,20 @@ const MAP_TYPE_CHANGED = 'MAP_TYPE_CHANGED'; +/** + * changes the map type + * @memberof actions.maptype + * @param {string} mapType the mapType. + * @return {action} the action of type `MAP_TYPE_CHANGED` with mapType + */ function changeMapType(mapType) { return { type: MAP_TYPE_CHANGED, mapType }; } - +/** + * Actions for map type management.Allow to manage the default map type. + * @name actions.maptype + */ module.exports = {MAP_TYPE_CHANGED, changeMapType}; diff --git a/web/client/components/buttons/GlobeViewSwitcherButton.jsx b/web/client/components/buttons/GlobeViewSwitcherButton.jsx new file mode 100644 index 0000000000..4b0c6f5a4e --- /dev/null +++ b/web/client/components/buttons/GlobeViewSwitcherButton.jsx @@ -0,0 +1,89 @@ +/* + * 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 ToggleButton = require('./ToggleButton'); +const {Tooltip} = require('react-bootstrap'); +const Message = require('../I18N/Message'); +/** + * Toggle button for 3d. Wraps {@link #components.buttons.ToggleButton} with some defaults + * @memberof components.buttons + * @class + * @prop {string} [id] an id for the html component + * @prop {object} [btnConfig] the configuration to pass to the bootstrap button + * @prop {object} [options] the options to send when toggle is clicked + * @prop {string|element} [text] the text to disaplay + * @prop {string|element} [help] the help text + * @prop {string} glyphicon the icon name + * @prop {bool} active the status of the button + * @prop {function} onClick. The method to call when clicked. the method will return as parameter the toggled `pressed` prop and the `options` object + * @prop {node} [activeTooltip] the tooltip to use on mouse hover + * @prop {node} [notActiveTooltip] the tooltip to use on mouse hover when the button is active + * @prop {string} [tooltipPlace] positon of the tooltip, one of: 'top', 'right', 'bottom', 'left' + * @prop {object} css style object for the component + * @prop {btnType} [btnType] one of 'normal', 'image' + * @prop {string} image if type is 'image', the src of the image + * @prop {string} pressedStyle the bootstrap style for pressedStyle + * @prop {string} defaultStyle the bootstrap style when not pressed + * + */ +const GlobeViewSwitcherButton = React.createClass({ + propTypes: { + id: React.PropTypes.string, + btnConfig: React.PropTypes.object, + options: React.PropTypes.object, + text: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]), + help: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]), + glyphicon: React.PropTypes.string, + active: React.PropTypes.bool, + onClick: React.PropTypes.func, + activeTooltip: React.PropTypes.string, + notActiveTooltip: React.PropTypes.string, + tooltipPlace: React.PropTypes.string, + style: React.PropTypes.object, + btnType: React.PropTypes.oneOf(['normal', 'image']), + image: React.PropTypes.string, + pressedStyle: React.PropTypes.string, + defaultStyle: React.PropTypes.string + }, + getDefaultProps() { + return { + id: 'globeviewswitcher-btn', + activeTooltip: 'globeswitcher.tooltipDeactivate', + notActiveTooltip: 'globeswitcher.tooltipActivate', + tooltipPlace: 'left', + defaultStyle: 'primary', + pressedStyle: 'success', + glyphicon: 'globe', + btnConfig: { + className: "square-button" + } + }; + }, + getButtonProperties() { + return ['id', + 'btnConfig', + 'options', + 'text', + 'glyphicon', + 'onClick', + 'tooltipPlace', + 'style', + 'btnType', + 'image', + 'pressedStyle', + 'defaultStyle' + ].reduce((result, key) => { result[key] = this.props[key]; return result; }, {}); + }, + render() { + return } />; + } +}); + +module.exports = GlobeViewSwitcherButton; diff --git a/web/client/components/buttons/__tests__/GlobeViewSwitcherButton-test.jsx b/web/client/components/buttons/__tests__/GlobeViewSwitcherButton-test.jsx new file mode 100644 index 0000000000..dc5294c1c2 --- /dev/null +++ b/web/client/components/buttons/__tests__/GlobeViewSwitcherButton-test.jsx @@ -0,0 +1,36 @@ +/* + * 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. + */ +var expect = require('expect'); + +var React = require('react'); +var ReactDOM = require('react-dom'); +var GlobeViewSwitcherButton = require('../GlobeViewSwitcherButton'); + +describe("test the GlobeViewSwitcherButton", () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('test default properties', () => { + const tb = ReactDOM.render(, document.getElementById("container")); + expect(tb).toExist(); + + const tbNode = ReactDOM.findDOMNode(tb); + expect(tbNode).toExist(); + expect(tbNode.id).toBe('globeviewswitcher-btn'); + expect(tbNode).toExist(); + expect(tbNode.className.indexOf('primary') >= 0).toBe(true); + }); +}); diff --git a/web/client/epics/__tests__/globeswitcher-test.js b/web/client/epics/__tests__/globeswitcher-test.js new file mode 100644 index 0000000000..a3e441634a --- /dev/null +++ b/web/client/epics/__tests__/globeswitcher-test.js @@ -0,0 +1,52 @@ +/* + * 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. + */ + +var expect = require('expect'); + +const {toggle3d, UPDATE_LAST_2D_MAPTYPE} = require('../../actions/globeswitcher'); +const assign = require('object-assign'); +const Rx = require('rxjs'); +const { ActionsObservable } = require('redux-observable'); +const {updateRouteOn3dSwitch} = require('../globeswitcher'); +const epicTest = (epic, count, action, callback, state = {}) => { + const actions = new Rx.Subject(); + const actions$ = new ActionsObservable(actions); + const store = { getState: () => state }; + epic(actions$, store) + .take(count) + .toArray() + .subscribe(callback); + if (action.length) { + action.map(act => actions.next(act)); + } else { + actions.next(action); + } +}; +describe('globeswitcher Epics', () => { + it('produces the search epic', (done) => { + epicTest(updateRouteOn3dSwitch, 2, assign({hash: "/viewer/leaflet/2"}, toggle3d(true, "leaflet")), actions => { + expect(actions.length).toBe(2); + actions.map((action) => { + switch (action.type) { + case "@@router/TRANSITION": + expect(action.payload.method).toBe('push'); + expect(action.payload.args.length).toBe(1); + break; + case UPDATE_LAST_2D_MAPTYPE: + expect(action.mapType).toBe("leaflet"); + break; + default: + expect(true).toBe(false); + + } + }); + done(); + }); + + }); +}); diff --git a/web/client/epics/globeswitcher.js b/web/client/epics/globeswitcher.js new file mode 100644 index 0000000000..94a373503e --- /dev/null +++ b/web/client/epics/globeswitcher.js @@ -0,0 +1,43 @@ +/* + * 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 {TOGGLE_3D, updateLast2dMapType} = require('../actions/globeswitcher'); + +const Rx = require('rxjs'); +const {get} = require('lodash'); +const defaultRegex = /\/(viewer)\/(\w+)\/(\d+)/; +import { push } from 'react-router-redux'; + +const replaceMapType = (path, newMapType) => { + let match = path.match(defaultRegex); + if (match) { + return `/viewer/${newMapType}/${match[3]}`; + } +}; +/** + * Gets every `TOGGLE_3D` event. + * @memberof epics.globeswitcher + * @param {external:Observable} action$ manages `TOGGLE_3D`. + * @return {external:Observable} emitting react-router-redux push action and {@link #actions.globeswitcher.updateLast2dMapType} actions + */ +const updateRouteOn3dSwitch = (action$, store) => + action$.ofType(TOGGLE_3D) + .switchMap( action => { + const newPath = replaceMapType(action.hash || location.hash, action.enable ? "cesium" : get(store.getState(), "globeswitcher.last2dMapType") || "leaflet"); + if (newPath) { + return Rx.Observable.from([push(newPath), updateLast2dMapType(action.originalMapType)]); + } + Rx.Observable.of(updateLast2dMapType(action.mapType)); + }); +/** + * Epics for 3d switcher functionality + * @name epics.globeswitcher + * @type {Object} + */ +module.exports = { + updateRouteOn3dSwitch +}; diff --git a/web/client/epics/maptype.js b/web/client/epics/maptype.js new file mode 100644 index 0000000000..5289491a4c --- /dev/null +++ b/web/client/epics/maptype.js @@ -0,0 +1,39 @@ +/* + * 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 {changeMapType} = require('../actions/maptype'); +const Rx = require('rxjs'); +const {get} = require('lodash'); +const defaultRegex = /\/(viewer)\/(\w+)\/(\d+)/; +const findMapType = path => path.match(defaultRegex) && path.replace(defaultRegex, "$2"); +import { UPDATE_LOCATION } from 'react-router-redux'; + +/** + * keep the default mapType in sync when change the URL of the map for viewer + * @memberof epics.maptype + * @param {external:Observable} action$ the stream of actions, acts on `UPDATE_LOCATION` + * @param {object} store the store middleware API from redux `createMiddleware` + * @return {external:Observable} the stream of the actions to emit. (`changeMapType`) + */ +const syncMapType = (action$, store) => + action$.ofType(UPDATE_LOCATION) + .filter(action => + action.payload + && action.payload.pathname + && action.payload.pathname.match(defaultRegex) + && findMapType(action.payload.pathname) !== get(store.getState(), "maptype.mapType")) + .switchMap((action) => + Rx.Observable.of(changeMapType(findMapType(action.payload.pathname))) + ); +/** + * Epics for maptype switch functionalities + * @name epics.maptype + * @type {Object} + */ +module.exports = { + syncMapType +}; diff --git a/web/client/localConfig.json b/web/client/localConfig.json index 768d01cc33..ad40e9e39c 100644 --- a/web/client/localConfig.json +++ b/web/client/localConfig.json @@ -109,7 +109,7 @@ } } }, "Login", - "OmniBar", "BurgerMenu", "Expander" + "OmniBar", "BurgerMenu", "Expander", "GlobeViewSwitcher" ], "desktop": ["Map", "HelpLink", "Share", "DrawerMenu", { "name": "Identify", @@ -198,7 +198,7 @@ } } }, - "OmniBar", "Login", "Save", "SaveAs", "BurgerMenu", "Expander", "Undo", "Redo", "FullScreen" + "OmniBar", "Login", "Save", "SaveAs", "BurgerMenu", "Expander", "Undo", "Redo", "FullScreen", "GlobeViewSwitcher" ], "embedded": [{ "name": "Map", diff --git a/web/client/plugins/GlobeViewSwitcher.jsx b/web/client/plugins/GlobeViewSwitcher.jsx new file mode 100644 index 0000000000..64af87dd42 --- /dev/null +++ b/web/client/plugins/GlobeViewSwitcher.jsx @@ -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 {connect} = require('react-redux'); + + +const assign = require('object-assign'); +const globeswitcher = require('../reducers/globeswitcher'); +const epics = require('../epics/globeswitcher'); +const {toggle3d} = require('../actions/globeswitcher'); +const GlobeViewSwitcherButton = require('../components/buttons/GlobeViewSwitcherButton'); + +/** + * GlobeViewSwitcher Plugin. A button that toggles to 3d mode + * @class GlobeViewSwitcher + * @memberof plugins + * @static + * + * @prop {string} cfg.id identifier of the Plugin + * + */ +const GlobeViewSwitcher = connect( ({maptype = {}} = {}) => ({ + active: maptype && maptype.mapType === "cesium", + options: { + originalMapType: maptype && maptype.mapType || "leaflet" + } +}), { + onClick: (pressed, options) => toggle3d(pressed, options.originalMapType) +})(GlobeViewSwitcherButton); + +module.exports = { + GlobeViewSwitcherPlugin: assign(GlobeViewSwitcher, { + Toolbar: { + name: '3d', + position: 5, + alwaysVisible: true, + tool: true, + priority: 1 + } + }), + reducers: { + globeswitcher + }, + epics +}; diff --git a/web/client/plugins/Maps.jsx b/web/client/plugins/Maps.jsx index bca9a3d254..0d537a67d4 100644 --- a/web/client/plugins/Maps.jsx +++ b/web/client/plugins/Maps.jsx @@ -127,12 +127,14 @@ const Maps = React.createClass({ module.exports = { MapsPlugin: connect((state) => ({ - mapType: state.home && state.home.mapType || (state.maps && state.maps.mapType) || 'leaflet' + mapType: (state.maptype && state.maptype.mapType) || 'leaflet' }), { loadMaps })(Maps), + epics: require('../epics/maptype'), reducers: { maps: require('../reducers/maps'), + maptype: require('../reducers/maptype'), currentMap: require('../reducers/currentMap') } }; diff --git a/web/client/plugins/searchbar/ToggleButton.jsx b/web/client/plugins/searchbar/ToggleButton.jsx index cc64317117..942f7976f2 100644 --- a/web/client/plugins/searchbar/ToggleButton.jsx +++ b/web/client/plugins/searchbar/ToggleButton.jsx @@ -1,4 +1,4 @@ -/** +/* * Copyright 2016, GeoSolutions Sas. * All rights reserved. * diff --git a/web/client/product/app.jsx b/web/client/product/app.jsx index 25b4ca690e..40d761ba14 100644 --- a/web/client/product/app.jsx +++ b/web/client/product/app.jsx @@ -25,7 +25,7 @@ const startApp = () => { }))(require('../components/app/StandardRouter')); const appStore = require('../stores/StandardStore').bind(null, initialState, { - home: require('./reducers/home'), + maptype: require('../reducers/maptype'), maps: require('../reducers/maps') }, {}); diff --git a/web/client/product/plugins.js b/web/client/product/plugins.js index 0ad15ac250..b7e1dd1372 100644 --- a/web/client/product/plugins.js +++ b/web/client/product/plugins.js @@ -67,7 +67,8 @@ module.exports = { TutorialPlugin: require('../plugins/Tutorial'), ThemeSwitcherPlugin: require('../plugins/ThemeSwitcher'), ScrollTopPlugin: require('../plugins/ScrollTop'), - GoFull: require('../plugins/GoFull') + GoFull: require('../plugins/GoFull'), + GlobeViewSwitcherPlugin: require('../plugins/GlobeViewSwitcher') }, requires: { ReactSwipe: require('react-swipeable-views').default, diff --git a/web/client/product/plugins/MapType.jsx b/web/client/product/plugins/MapType.jsx index 5b5daa0d6c..c36dcc17ce 100644 --- a/web/client/product/plugins/MapType.jsx +++ b/web/client/product/plugins/MapType.jsx @@ -1,4 +1,4 @@ -/** +/* * Copyright 2016, GeoSolutions Sas. * All rights reserved. * @@ -9,7 +9,7 @@ const React = require('react'); const {Label, FormControl, FormGroup} = require('react-bootstrap'); const Message = require('../../components/I18N/Message'); const {compose} = require('redux'); -const {changeMapType} = require('../actions/home'); +const {changeMapType} = require('../../actions/maptype'); const {connect} = require('react-redux'); const assign = require('object-assign'); @@ -18,12 +18,18 @@ const MapType = React.createClass({ style: React.PropTypes.object, className: React.PropTypes.object, mapType: React.PropTypes.string, + mapTypes: React.PropTypes.array, onChangeMapType: React.PropTypes.func }, getDefaultProps() { return { mapType: 'leaflet', - onChangeMapType: () => {} + onChangeMapType: () => {}, + mapTypes: [ + { key: "leaflet", label: "Leaflet"}, + { key: "openlayers", label: "OpenLayers"}, + { key: "cesium", label: "Cesium"} + ] }; }, render() { @@ -32,8 +38,7 @@ const MapType = React.createClass({ - - + {this.props.mapTypes.map(type => )} @@ -42,7 +47,7 @@ const MapType = React.createClass({ }); const MapTypePlugin = connect((state) => ({ - mapType: state.home && state.home.mapType || 'leaflet' + mapType: state.maptype && state.maptype.mapType || 'leaflet' }), { onChangeMapType: compose(changeMapType, (event) => event.target.value) })(MapType); @@ -56,5 +61,5 @@ module.exports = { priority: 1 } }), - reducers: {home: require('../reducers/home')} + reducers: {maptype: require('../../reducers/maptype')} }; diff --git a/web/client/product/reducers/home.js b/web/client/product/reducers/home.js deleted file mode 100644 index ab1c443784..0000000000 --- a/web/client/product/reducers/home.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2016, 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. - */ - -var {MAP_TYPE_CHANGED} = require('../actions/home'); - -function home(state = {mapType: "leaflet"}, action) { - switch (action.type) { - case MAP_TYPE_CHANGED: - return {mapType: action.mapType}; - default: - return state; - } -} - -module.exports = home; diff --git a/web/client/reducers/__tests__/globeswitcher-test.js b/web/client/reducers/__tests__/globeswitcher-test.js new file mode 100644 index 0000000000..d7d5c9f4cd --- /dev/null +++ b/web/client/reducers/__tests__/globeswitcher-test.js @@ -0,0 +1,24 @@ +/* + * 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 globeswitcher = require('../globeswitcher'); +const {changeMapType} = require('../../actions/maptype'); +const {updateLast2dMapType} = require('../../actions/globeswitcher'); + +describe('Test the globeswitcher reducer', () => { + it('check to store last 2d map type', () => { + const state = globeswitcher(undefined, changeMapType("leaflet")); + expect(state.last2dMapType).toBe('leaflet'); + const state2 = globeswitcher(state, changeMapType("cesium")); + expect(state2.last2dMapType).toBe('leaflet'); + const state3 = globeswitcher(state2, updateLast2dMapType("openlayers")); + expect(state3.last2dMapType).toBe('openlayers'); + const state4 = globeswitcher(state3, {type: "UNKNOWN"}); + expect(state4.last2dMapType).toBe('openlayers'); + }); +}); diff --git a/web/client/reducers/__tests__/maps-test.jsx b/web/client/reducers/__tests__/maps-test.jsx index 9e8b6b8fb7..77a7ba8d30 100644 --- a/web/client/reducers/__tests__/maps-test.jsx +++ b/web/client/reducers/__tests__/maps-test.jsx @@ -13,7 +13,6 @@ const { mapMetadataUpdated, mapDeleting, mapDeleted, attributeUpdated, thumbnailError, permissionsLoading, permissionsLoaded, saveMap, permissionsUpdated, resetUpdating, mapsSearchTextChanged} = require('../../actions/maps'); -const MAP_TYPE_CHANGED = "MAP_TYPE_CHANGED"; // NOTE: this is from home action in product. move to maps actions when finished; const sampleMap = { canDelete: false, @@ -46,10 +45,7 @@ describe('Test the maps reducer', () => { expect(state.enabled).toBe(false); expect(state.searchText).toBe(""); }); - it('on MAP_TYPE_CHANGED action', () => { - let state = maps(null, {type: MAP_TYPE_CHANGED, mapType: "cesium"}); - expect(state.mapType).toBe("cesium"); - }); + it('on mapsSearchTextChanged action', () => { let state = maps(null, mapsSearchTextChanged("TEST")); expect(state.searchText).toBe("TEST"); diff --git a/web/client/reducers/__tests__/maptype-test.js b/web/client/reducers/__tests__/maptype-test.js new file mode 100644 index 0000000000..7928e051bf --- /dev/null +++ b/web/client/reducers/__tests__/maptype-test.js @@ -0,0 +1,17 @@ +/* + * 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 maptype = require('../maptype'); +const {changeMapType} = require('../../actions/maptype'); + +describe('Test the maptype reducer', () => { + it('set a maptype', () => { + const state = maptype(undefined, changeMapType("leaflet")); + expect(state.mapType).toBe('leaflet'); + }); +}); diff --git a/web/client/reducers/globeswitcher.js b/web/client/reducers/globeswitcher.js new file mode 100644 index 0000000000..adf398fe66 --- /dev/null +++ b/web/client/reducers/globeswitcher.js @@ -0,0 +1,35 @@ +/* + * 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 {MAP_TYPE_CHANGED} = require('../actions/maptype'); +const {UPDATE_LAST_2D_MAPTYPE} = require('../actions/globeswitcher'); +/** + * state for globeswitcher tooltip. holds the last 2d mapType. + * @memberof reducers + * @param {Object} [state={last2dMapType: "leaflet"}] the state + * @param {action} action the actions (receives MAP_TYPE_CHANGED and UPDATE_LAST_2D_MAPTYPE) + * @return {Object} the new state + * @example + * { + * last2dMapType: "leaflet" + * } + */ +function globeswitcher(state = {last2dMapType: "leaflet"}, action) { + switch (action.type) { + case MAP_TYPE_CHANGED: + case UPDATE_LAST_2D_MAPTYPE: + if (action.mapType && action.mapType !== "cesium" && action.mapType !== state.last2dMapType) { + return {last2dMapType: action.mapType}; + } + return state; + default: + return state; + } +} + +module.exports = globeswitcher; diff --git a/web/client/reducers/maps.js b/web/client/reducers/maps.js index 725ba5b069..9b0205fce8 100644 --- a/web/client/reducers/maps.js +++ b/web/client/reducers/maps.js @@ -11,7 +11,6 @@ const { MAP_METADATA_UPDATED, MAP_DELETING, MAP_DELETED, ATTRIBUTE_UPDATED, PERMISSIONS_LIST_LOADING, PERMISSIONS_LIST_LOADED, SAVE_MAP, PERMISSIONS_UPDATED, THUMBNAIL_ERROR, RESET_UPDATING, MAPS_SEARCH_TEXT_CHANGED} = require('../actions/maps'); -const MAP_TYPE_CHANGED = "MAP_TYPE_CHANGED"; // NOTE: this is from home action in product. move to maps actions when finished; const assign = require('object-assign'); const _ = require('lodash'); /** @@ -60,16 +59,10 @@ const _ = require('lodash'); * @memberof reducers */ function maps(state = { - mapType: "leaflet", enabled: false, errors: [], searchText: ""}, action) { switch (action.type) { - case MAP_TYPE_CHANGED: { - return assign({}, state, { - mapType: action.mapType - }); - } case MAPS_SEARCH_TEXT_CHANGED: { return assign({}, state, { searchText: action.text diff --git a/web/client/reducers/maptype.js b/web/client/reducers/maptype.js new file mode 100644 index 0000000000..4719133a8c --- /dev/null +++ b/web/client/reducers/maptype.js @@ -0,0 +1,30 @@ +/* + * Copyright 2016, 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. + */ + +var {MAP_TYPE_CHANGED} = require('../actions/maptype'); +/** + * stores state for the mapType to use (typically one of leaflet, openlayers, cesium... ) + * @memberof reducers + * @param {Object} [state={mapType: "leaflet"}] the initial state + * @param {} action the action gets `MAP_TYPE_CHANGED` + * @return {Object} the new state + * @example + * { + * mapType: "leaflet" + * } + */ +function maptype(state = {mapType: "leaflet"}, action) { + switch (action.type) { + case MAP_TYPE_CHANGED: + return {mapType: action.mapType}; + default: + return state; + } +} + +module.exports = maptype;