diff --git a/karma.conf.continuous-test.js b/karma.conf.continuous-test.js index 8241d5cce3..ba83232e92 100644 --- a/karma.conf.continuous-test.js +++ b/karma.conf.continuous-test.js @@ -8,7 +8,8 @@ module.exports = function karmaConfig(config) { frameworks: [ 'mocha' ], files: [ - 'tests.webpack.js' + 'tests.webpack.js', + { pattern: './web/client/test-resources/**/*', included: false } ], preprocessors: { diff --git a/karma.conf.single-run.js b/karma.conf.single-run.js index af53be0119..fc3697ff91 100644 --- a/karma.conf.single-run.js +++ b/karma.conf.single-run.js @@ -8,7 +8,8 @@ module.exports = function karmaConfig(config) { frameworks: [ 'mocha' ], files: [ - 'tests.webpack.js' + 'tests.webpack.js', + { pattern: './web/client/test-resources/**/*', included: false } ], preprocessors: { diff --git a/package.json b/package.json index 2e6819175e..113045fb19 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,13 @@ "react": "^0.13.3", "react-bootstrap": "^0.24.3", "react-intl": "^1.2.0", + "react-redux": "^1.0.0", + "redux": "^1.0.0", + "redux-thunk": "^0.1.0", "url": "~0.10.3" }, "scripts": { - "clean": "rm -Rf ./web/dist", + "clean": "rm -Rf ./web/client/dist", "compile": "npm run clean && mkdirp ./web/client/dist && webpack", "start": "webpack-dev-server --progress --colors --port 8081 --content-base web/client", "test": "karma start ./karma.conf.single-run.js", diff --git a/web/client/actions/I18NActions.jsx b/web/client/actions/I18NActions.jsx deleted file mode 100644 index 0ba834d59a..0000000000 --- a/web/client/actions/I18NActions.jsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright 2015, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -var assign = require('object-assign'); -var keymirror = require('keymirror'); - -var Dispatcher = require('../dispatchers/Dispatcher'); - -var I18NActions = { - launch(actionType, args) { - Dispatcher.dispatch(assign({type: actionType}, args)); - } -}; - -assign(I18NActions, keymirror({ - CHANGE_LANG: null -})); - -module.exports = I18NActions; diff --git a/web/client/actions/__tests__/I18NActions-test.jsx b/web/client/actions/__tests__/I18NActions-test.jsx deleted file mode 100644 index a957adaa7d..0000000000 --- a/web/client/actions/__tests__/I18NActions-test.jsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright 2015, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -var expect = require('expect'); -var assign = require('object-assign'); -var Dispatcher = require('../../dispatchers/Dispatcher'); - -describe('This test for I18NActions', () => { - var I18NActions; - - beforeEach((done) => { - I18NActions = require('../I18NActions'); - setTimeout(done); - }); - - afterEach((done) => { - var name = require.resolve('../I18NActions'); - delete require.cache[name]; - setTimeout(done); - }); - - it('checks launch(actionType, args)', () => { - const aType = "test"; - const aArgs = { - k0: "v0", - k1: "v1" - }; - const testArgs = assign({type: aType}, aArgs); - const spy = expect.spyOn(Dispatcher, 'dispatch'); - I18NActions.launch(aType, aArgs); - - expect(spy.calls.length).toBe(1); - expect(spy.calls[0].context).toBe(Dispatcher); - expect(spy.calls[0].arguments).toEqual([ testArgs ]); - }); -}); diff --git a/web/client/actions/__tests__/config-test.js b/web/client/actions/__tests__/config-test.js new file mode 100644 index 0000000000..0ce9ef7a39 --- /dev/null +++ b/web/client/actions/__tests__/config-test.js @@ -0,0 +1,36 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var expect = require('expect'); +var loadMapConfig = require('../config').loadMapConfig; + +describe('Test configuration related actions', () => { + it('does not load a missing configuration file', (done) => { + loadMapConfig('missingConfig.json')((e) => { + try { + expect(e).toExist(); + expect(e.type).toBe('MAP_CONFIG_LOAD_ERROR'); + done(); + } catch(ex) { + done(ex); + } + }); + }); + + it('loads an existing configuration file', (done) => { + loadMapConfig('base/web/client/test-resources/testConfig.json')((e) => { + try { + expect(e).toExist(); + expect(e.type).toBe('MAP_CONFIG_LOADED'); + done(); + } catch(ex) { + done(ex); + } + }); + }); +}); diff --git a/web/client/actions/__tests__/locale-test.js b/web/client/actions/__tests__/locale-test.js new file mode 100644 index 0000000000..3c50a7ab3e --- /dev/null +++ b/web/client/actions/__tests__/locale-test.js @@ -0,0 +1,36 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var expect = require('expect'); +var loadLocale = require('../locale').loadLocale; + +describe('Test locale related actions', () => { + it('does not load a missing translation file', (done) => { + loadLocale('', 'unknown')((e) => { + try { + expect(e).toExist(); + expect(e.type).toBe('LOCALE_LOAD_ERROR'); + done(); + } catch(ex) { + done(ex); + } + }); + }); + + it('loads an existing translation file', (done) => { + loadLocale('base/web/client/test-resources', 'it-IT')((e) => { + try { + expect(e).toExist(); + expect(e.type).toBe('CHANGE_LOCALE'); + done(); + } catch(ex) { + done(ex); + } + }); + }); +}); diff --git a/web/client/actions/config.js b/web/client/actions/config.js new file mode 100644 index 0000000000..ca460363fe --- /dev/null +++ b/web/client/actions/config.js @@ -0,0 +1,39 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var axios = require('axios'); + +const MAP_CONFIG_LOADED = 'MAP_CONFIG_LOADED'; +const MAP_CONFIG_LOAD_ERROR = 'MAP_CONFIG_LOAD_ERROR'; + +function configureMap(conf, legacy) { + return { + type: MAP_CONFIG_LOADED, + config: conf, + legacy: legacy || false + }; +} + +function configureError(e) { + return { + type: MAP_CONFIG_LOAD_ERROR, + error: e + }; +} + +function loadMapConfig(configName, legacy) { + return (dispatch) => { + return axios.get(configName).then((response) => { + dispatch(configureMap(response.data, legacy)); + }).catch((e) => { + dispatch(configureError(e)); + }); + }; +} + +module.exports = {MAP_CONFIG_LOADED, MAP_CONFIG_LOAD_ERROR, loadMapConfig}; diff --git a/web/client/actions/locale.js b/web/client/actions/locale.js new file mode 100644 index 0000000000..5a45f79128 --- /dev/null +++ b/web/client/actions/locale.js @@ -0,0 +1,39 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var axios = require('axios'); + +const CHANGE_LOCALE = 'CHANGE_LOCALE'; +const LOCALE_LOAD_ERROR = 'LOCALE_LOAD_ERROR'; + +function changeLocale(data) { + return { + type: CHANGE_LOCALE, + messages: data.messages, + locale: data.locale + }; +} + +function localeError(e) { + return { + type: LOCALE_LOAD_ERROR, + error: e + }; +} + +function loadLocale(translationFolder, language) { + return (dispatch) => { + return axios.get(translationFolder + '/data.' + language).then((response) => { + dispatch(changeLocale(response.data)); + }).catch((e) => { + dispatch(localeError(e)); + }); + }; +} + +module.exports = {CHANGE_LOCALE, LOCALE_LOAD_ERROR, loadLocale}; diff --git a/web/client/components/I18N/Localized.jsx b/web/client/components/I18N/Localized.jsx new file mode 100644 index 0000000000..87e533e49e --- /dev/null +++ b/web/client/components/I18N/Localized.jsx @@ -0,0 +1,36 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var React = require('react'); + +var Localized = React.createClass({ + propTypes: { + locale: React.PropTypes.string, + messages: React.PropTypes.object + }, + childContextTypes: { + locale: React.PropTypes.string, + messages: React.PropTypes.object + }, + getChildContext() { + return { + locale: this.props.locale, + messages: this.props.messages + }; + }, + render() { + let { children } = this.props; + + if (typeof children === 'function') { + children = children(); + } + + return React.Children.only(children); + } + }); + +module.exports = Localized; diff --git a/web/client/components/I18N/Message.jsx b/web/client/components/I18N/Message.jsx index 8420148a54..bcd5984192 100644 --- a/web/client/components/I18N/Message.jsx +++ b/web/client/components/I18N/Message.jsx @@ -9,40 +9,22 @@ var React = require('react'); var ReactIntl = require('react-intl'); var FormattedMessage = ReactIntl.FormattedMessage; -var I18NStore = require('../../stores/I18NStore'); - var Message = React.createClass({ propTypes: { + locale: React.PropTypes.string, + messages: React.PropTypes.object, msgId: React.PropTypes.string.isRequired, msgParams: React.PropTypes.object }, - getInitialState() { - const currentLocale = I18NStore.getCurrentLocale(); - const msg = I18NStore.getMsgById(this.props.msgId); - return { - locales: currentLocale, - msg: msg - }; - }, - // it makes this component reactive when a new language is loaded in - // language store. - componentDidMount() { - I18NStore.register(I18NStore.Event.LANG_CHANGED, this.onLangChanged); - }, - componentWillUnmount() { - I18NStore.unregister(I18NStore.Event.LANG_CHANGED, this.onLangChanged); - }, - // it updates the state of this component when the language store loads a new one. - onLangChanged() { - const currentLocale = I18NStore.getCurrentLocale(); - const msg = I18NStore.getMsgById(this.props.msgId); - this.setState({ - locales: currentLocale, - msg: msg - }); + contextTypes: { + locale: React.PropTypes.string.isRequired, + messages: React.PropTypes.object }, render() { - return ; + var locale = this.props.locale || this.context.locale; + var messages = this.props.messages || this.context.messages; + + return ; } }); diff --git a/web/client/components/I18N/__tests__/I18N.Message-test.jsx b/web/client/components/I18N/__tests__/I18N.Message-test.jsx index 3bc3cf61f5..958e1684d0 100644 --- a/web/client/components/I18N/__tests__/I18N.Message-test.jsx +++ b/web/client/components/I18N/__tests__/I18N.Message-test.jsx @@ -8,46 +8,55 @@ var expect = require('expect'); var React = require('react/addons'); -var I18NStore = require('../../../stores/I18NStore'); var I18N = require('../I18N'); -var I18NActions = require('../../../actions/I18NActions'); -var ita = require('../../../stores/data.it-IT'); -var eng = require('../../../stores/data.en-US'); +var ita = { + "locale": "it-IT", + "messages": { + "aboutLbl": "Info" + } +}; +var eng = { + "locale": "en-US", + "messages": { + "aboutLbl": "About" + } +}; describe('This test for I18N.Message', () => { + const msgId = "aboutLbl"; + const data = { + "en-US": eng, + "it-IT": ita + }; + afterEach((done) => { React.unmountComponentAtNode(document.body); document.body.innerHTML = ''; setTimeout(done); }); - it('checks if the component reacts at I18NStore edits', () => { - var testMsg; - var currentData; - const msgId = "aboutLbl"; - const data = { - "en-US": eng, - "it-IT": ita - }; - currentData = data[I18NStore.getCurrentLocale()]; - testMsg = currentData.messages[msgId]; - - const cmp = React.render(, document.body); + it('checks if the component renders english messages', () => { + var currentData = data["en-US"]; + var testMsg = currentData.messages[msgId]; + + const cmp = React.render(, document.body); expect(cmp).toExist(); const cmpDom = React.findDOMNode(cmp); expect(cmpDom).toExist(); expect(cmpDom.innerHTML).toBe(testMsg); + }); - const nextLocale = I18NStore.getCurrentLocale() === "it-It" ? "en-US" : "it-IT"; - I18NStore._emulate_dispatcher({ - locale: nextLocale, - type: I18NActions.CHANGE_LANG - }); + it('checks if the component renders italian messages', () => { + var currentData = data["it-IT"]; + var testMsg = currentData.messages[msgId]; - currentData = data[I18NStore.getCurrentLocale()]; - testMsg = currentData.messages[msgId]; + const cmp = React.render(, document.body); + expect(cmp).toExist(); + + const cmpDom = React.findDOMNode(cmp); + expect(cmpDom).toExist(); expect(cmpDom.innerHTML).toBe(testMsg); }); }); diff --git a/web/client/components/I18N/__tests__/Localized-test.jsx b/web/client/components/I18N/__tests__/Localized-test.jsx new file mode 100644 index 0000000000..6b0cbfd3fc --- /dev/null +++ b/web/client/components/I18N/__tests__/Localized-test.jsx @@ -0,0 +1,35 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var expect = require('expect'); + +var React = require('react/addons'); +var Localized = require('../Localized'); +var Message = require('../Message'); + +const messages = { + "testMsg": "my message" +}; + +describe('Test the localization support HOC', () => { + afterEach((done) => { + React.unmountComponentAtNode(document.body); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('localizes wrapped component', () => { + var localized = React.render( + + {() => } + + , document.body); + var dom = React.findDOMNode(localized); + expect(dom).toExist(); + expect(dom.innerHTML).toBe("my message"); + }); +}); diff --git a/web/client/components/LangSelector/LangSelector.jsx b/web/client/components/LangSelector/LangSelector.jsx index 9ef8e962e2..07bbec24ef 100644 --- a/web/client/components/LangSelector/LangSelector.jsx +++ b/web/client/components/LangSelector/LangSelector.jsx @@ -9,17 +9,18 @@ var React = require('react'); var BootstrapReact = require('react-bootstrap'); var Input = BootstrapReact.Input; - -var I18NStore = require('../../stores/I18NStore'); -var I18NActions = require('../../actions/I18NActions'); +var LocaleUtils = require('../../utils/LocaleUtils'); var LangSelector = React.createClass({ propTypes: { - locales: React.PropTypes.object + locales: React.PropTypes.object, + currentLocale: React.PropTypes.string, + onLanguageChange: React.PropTypes.func }, getDefaultProps() { return { - locales: I18NStore.getSupportedLocales() + locales: LocaleUtils.getSupportedLocales(), + currentLocale: 'en-US' }; }, render() { @@ -28,13 +29,13 @@ var LangSelector = React.createClass({ var list = []; for (let lang in this.props.locales) { if (this.props.locales.hasOwnProperty(lang)) { - val = this.props.locales[lang]; - label = lang; + val = this.props.locales[lang].code; + label = this.props.locales[lang].description; list.push(); } } return ( - + {list} ); @@ -42,7 +43,7 @@ var LangSelector = React.createClass({ launchNewLangAction() { var element = React.findDOMNode(this); var selectNode = element.getElementsByTagName('select').item(0); - I18NActions.launch(I18NActions.CHANGE_LANG, {locale: selectNode.value}); + this.props.onLanguageChange(selectNode.value); } }); diff --git a/web/client/components/LangSelector/__tests__/LangSelector-test.jsx b/web/client/components/LangSelector/__tests__/LangSelector-test.jsx index 1867a5787b..5772b1d72d 100644 --- a/web/client/components/LangSelector/__tests__/LangSelector-test.jsx +++ b/web/client/components/LangSelector/__tests__/LangSelector-test.jsx @@ -8,8 +8,6 @@ var expect = require('expect'); var React = require('react/addons'); -var I18NStore = require('../../../stores/I18NStore'); -var I18NActions = require('../../../actions/I18NActions'); var LangSelector = require('../LangSelector'); describe('LangSelector', () => { @@ -32,7 +30,7 @@ describe('LangSelector', () => { const select = cmpDom.getElementsByTagName("select").item(0); const opts = select.childNodes; - const langs = I18NStore.getSupportedLocales(); + const langs = {'Italiano': 'it-IT', 'English': 'en-US'}; for (let i = 0; i < opts.length; i++) { lbl = opts[i].innerHTML; @@ -42,10 +40,9 @@ describe('LangSelector', () => { } }); - it('checks if a change of the compo fires the proper action', () => { - const spy = expect.spyOn(I18NActions, 'launch'); - - const cmp = React.render(, document.body); + it('checks if a change of the combo fires the proper action', () => { + let newLang; + const cmp = React.render( {newLang = lang; }}/>, document.body); const cmpDom = React.findDOMNode(cmp); const select = cmpDom.getElementsByTagName("select").item(0); @@ -53,7 +50,6 @@ describe('LangSelector', () => { React.addons.TestUtils.Simulate.change(select, {target: {value: 'it-IT'}}); // select.children[1].click(); - expect(spy.calls.length).toBe(1); - expect(spy.calls[0].arguments).toEqual([I18NActions.CHANGE_LANG, {locale: "it-IT"}]); + expect(newLang).toBe('it-IT'); }); }); diff --git a/web/client/components/leaflet/Layer.jsx b/web/client/components/leaflet/Layer.jsx index 8c28473d08..f3544e462c 100644 --- a/web/client/components/leaflet/Layer.jsx +++ b/web/client/components/leaflet/Layer.jsx @@ -41,8 +41,8 @@ var LeafletLayer = React.createClass({ break; case "wms": this.layer = L.tileLayer.wms( - WMSUtils.getWMSURL(options.sourceOptions.url), - WMSUtils.wmsToLeafletOptions(options.sourceOptions, options) + WMSUtils.getWMSURL(options.url), + WMSUtils.wmsToLeafletOptions(options) ); break; default: diff --git a/web/client/components/leaflet/__tests__/Layer-test.jsx b/web/client/components/leaflet/__tests__/Layer-test.jsx index 85d59c4c1c..1b5b480cf6 100644 --- a/web/client/components/leaflet/__tests__/Layer-test.jsx +++ b/web/client/components/leaflet/__tests__/Layer-test.jsx @@ -52,14 +52,12 @@ describe('Leaflet layer', () => { }); it('creates a wms layer for leaflet map', () => { var options = { - "source": "demo", + "type": "wms", "visibility": true, "name": "nurc:Arc_Sample", "group": "Meteo", "format": "image/png", - "sourceOptions": { - "url": "http://demo.geo-solutions.it/geoserver/wms" - } + "url": "http://demo.geo-solutions.it/geoserver/wms" }; // create layers var layer = React.render( diff --git a/web/client/dispatchers/Dispatcher.jsx b/web/client/dispatchers/Dispatcher.jsx deleted file mode 100644 index ccdf52787c..0000000000 --- a/web/client/dispatchers/Dispatcher.jsx +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright 2015, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -var Dispatcher = require('flux').Dispatcher; - -module.exports = new Dispatcher(); diff --git a/web/client/examples/viewer/app.jsx b/web/client/examples/viewer/app.jsx index 28dd61f1bc..a5d3067368 100644 --- a/web/client/examples/viewer/app.jsx +++ b/web/client/examples/viewer/app.jsx @@ -6,47 +6,30 @@ * LICENSE file in the root directory of this source tree. */ var React = require('react'); -var MapViewController = require('../../components/Map/MapViewController'); -var InfoButton = require('../../components/InfoButton/InfoButton'); -var I18N = require('../../components/I18N/I18N'); -var LangSelector = require('../../components/LangSelector/LangSelector'); -var ConfigUtils = require('../../utils/ConfigUtils'); + +var Provider = require('react-redux').Provider; +var Viewer = require('./containers/Viewer'); var url = require('url'); -var Api = require('../../api/MapConfigDAO'); - -// window.geoStoreBase = "http://mapstore.geo-solutions.it/geostore/rest/" -Api.getMergedConfig("../data/mapStoreConfig.json", url.parse(window.location.href, true).query.mapId, window.geoStoreBase).then( function(legacyConfig) { - const mapId = "map"; - // convert from legacy - const conf = ConfigUtils.convertFromLegacy(legacyConfig); - React.render(, document.getElementById("mapContainer")); -}); - -React.render(} - title={} - glyphicon="info-sign" - body={ -
-

MapStore 2

-

- OpenLayers 3 Leaflet. -

-

- -

-

- -

-

-
- }/>, document.getElementById("aboutContainer")); -React.render(, document.getElementById("langSelContainer")); +var loadMapConfig = require('../../actions/config').loadMapConfig; +var loadLocale = require('../../actions/locale').loadLocale; +var ConfigUtils = require('../../utils/ConfigUtils'); +var LocaleUtils = require('../../utils/LocaleUtils'); + +var store = require('./stores/viewerstore'); + +const urlQuery = url.parse(window.location.href, true).query; + +const { configUrl, legacy } = ConfigUtils.getConfigurationOptions(urlQuery, 'config', 'json'); + +store.dispatch(loadMapConfig(configUrl, legacy)); + +let locale = LocaleUtils.getLocale(urlQuery); +store.dispatch(loadLocale('../../translations', locale)); + +React.render( + + {() => } + , + document.getElementById('container') +); diff --git a/web/client/examples/viewer/config.json b/web/client/examples/viewer/config.json new file mode 100644 index 0000000000..5c5514f956 --- /dev/null +++ b/web/client/examples/viewer/config.json @@ -0,0 +1,39 @@ +{ + "map": { + "projection": "EPSG:900913", + "units": "m", + "center": [1250000.000000, 5370000.000000], + "zoom":5, + "maxExtent": [ + -20037508.34, -20037508.34, + 20037508.34, 20037508.34 + ], + "layers": [ + { + "type": "osm", + "title": "Open Street Map", + "name": "mapnik", + "group": "background", + "visibility": true + },{ + "type": "wms", + "url":"http://213.215.135.196/reflector/open/service", + "visibility": false, + "title": "e-Geos Ortofoto RealVista 1.0", + "name": "rv1", + "group": "background", + "format": "image/png" + }, + { + "type": "wms", + "url":"http://demo.geo-solutions.it/geoserver/wms", + "visibility": true, + "opacity": 0.5, + "title": "Weather data", + "name": "nurc:Arc_Sample", + "group": "Meteo", + "format": "image/png" + } + ] + } +} diff --git a/web/client/examples/viewer/containers/Viewer.jsx b/web/client/examples/viewer/containers/Viewer.jsx new file mode 100644 index 0000000000..9575be008e --- /dev/null +++ b/web/client/examples/viewer/containers/Viewer.jsx @@ -0,0 +1,103 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var React = require('react'); +var connect = require('react-redux').connect; +var LMap = require('../../../components/leaflet/Map'); +var LLayer = require('../../../components/leaflet/Layer'); +var ConfigUtils = require('../../../utils/ConfigUtils'); +var LangSelector = require('../../../components/LangSelector/LangSelector'); +var InfoButton = require('../../../components/InfoButton/InfoButton'); +var I18N = require('../../../components/I18N/I18N'); +var Localized = require('../../../components/I18N/Localized'); +var loadLocale = require('../../../actions/locale').loadLocale; + +var Viewer = React.createClass({ + propTypes: { + mapConfig: React.PropTypes.object, + messages: React.PropTypes.object, + locale: React.PropTypes.string, + dispatch: React.PropTypes.func + }, + renderLayers(layers) { + if (layers) { + return layers.map(function(layer) { + return ; + }); + } + return null; + }, + renderPlugins(locale) { + return [ +
+ +
, +
+ } + title={} + glyphicon="info-sign" + body={ +
+

MapStore 2

+

+ OpenLayers 3 Leaflet. +

+

+ +

+

+ +

+

+
+ }/> +
+ ]; + }, + render() { + if (this.props.mapConfig && this.props.messages) { + let config = this.props.mapConfig; + if (config.loadingError) { + return
{config.loadingError}
; + } + let center = ConfigUtils.getCenter(config.center, config.projection); + return ( + + {() => +
+ + {this.renderLayers(config.layers)} + + {this.renderPlugins(this.props.locale)} +
+ } + +
+ ); + } + return null; + }, + switchLanguage(lang) { + this.props.dispatch(loadLocale('../../translations', lang)); + } +}); + +module.exports = connect((state) => { + return { + mapConfig: state.mapConfig, + messages: state.locale ? state.locale.messages : null, + locale: state.locale ? state.locale.current : null + }; +})(Viewer); diff --git a/web/client/examples/viewer/index.html b/web/client/examples/viewer/index.html index 9d86b29d59..1378eb6322 100644 --- a/web/client/examples/viewer/index.html +++ b/web/client/examples/viewer/index.html @@ -13,7 +13,7 @@ diff --git a/web/client/examples/viewer/stores/viewerstore.js b/web/client/examples/viewer/stores/viewerstore.js new file mode 100644 index 0000000000..c0c211849f --- /dev/null +++ b/web/client/examples/viewer/stores/viewerstore.js @@ -0,0 +1,26 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var createStore = require('redux').createStore; +var combineReducers = require('redux').combineReducers; +var applyMiddleware = require('redux').applyMiddleware; +var thunkMiddleware = require('redux-thunk'); +var mapConfig = require('../../../reducers/config'); +var locale = require('../../../reducers/locale'); + + // reducers +const reducers = combineReducers({ + mapConfig, + locale +}); + + // store +const createStoreWithMiddleware = applyMiddleware( + thunkMiddleware +)(createStore); + +module.exports = createStoreWithMiddleware(reducers, {}); diff --git a/web/client/reducers/__tests__/config-test.js b/web/client/reducers/__tests__/config-test.js new file mode 100644 index 0000000000..94397878c8 --- /dev/null +++ b/web/client/reducers/__tests__/config-test.js @@ -0,0 +1,33 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var expect = require('expect'); + +var mapConfig = require('../config'); + + +describe('Test the mapConfig reducer', () => { + it('creates a configuration object from loaded config', () => { + var state = mapConfig({}, {type: 'MAP_CONFIG_LOADED', config: { map: { zoom: 11 }}}); + expect(state.zoom).toExist(); + }); + + it('creates a configuration object from legacy config', () => { + var state = mapConfig({}, {type: 'MAP_CONFIG_LOADED', config: { map: { zoom: 11 }}}, true); + expect(state.zoom).toExist(); + }); + + it('creates an error on wrongly loaded config', () => { + var state = mapConfig({}, {type: 'MAP_CONFIG_LOAD_ERROR', error: 'error'}); + expect(state.loadingError).toExist(); + }); + + it('returns original state on unrecognized action', () => { + var state = mapConfig(1, {type: 'UNKNOWN'}); + expect(state).toBe(1); + }); +}); diff --git a/web/client/reducers/__tests__/locale-test.js b/web/client/reducers/__tests__/locale-test.js new file mode 100644 index 0000000000..def3f465a6 --- /dev/null +++ b/web/client/reducers/__tests__/locale-test.js @@ -0,0 +1,30 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +var expect = require('expect'); + +var locale = require('../locale'); + + +describe('Test the locale reducer', () => { + it('fills localization data from loaded translation file', () => { + var state = locale({}, {type: 'CHANGE_LOCALE', messages: {}, locale: 'it-IT'}); + expect(state.current).toExist(); + expect(state.current).toBe('it-IT'); + expect(state.messages).toExist(); + }); + + it('creates an error on wrongly translation file', () => { + var state = locale({}, {type: 'LOCALE_LOAD_ERROR', error: 'error'}); + expect(state.loadingError).toExist(); + }); + + it('returns original state on unrecognized action', () => { + var state = locale(1, {type: 'UNKNOWN'}); + expect(state).toBe(1); + }); +}); diff --git a/web/client/reducers/config.js b/web/client/reducers/config.js new file mode 100644 index 0000000000..a3620d57c7 --- /dev/null +++ b/web/client/reducers/config.js @@ -0,0 +1,25 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var {MAP_CONFIG_LOADED, MAP_CONFIG_LOAD_ERROR} = require('../actions/config'); +var ConfigUtils = require('../utils/ConfigUtils'); + +function mapConfig(state = null, action) { + switch (action.type) { + case MAP_CONFIG_LOADED: + return action.legacy ? ConfigUtils.convertFromLegacy(action.config) : action.config.map; + case MAP_CONFIG_LOAD_ERROR: + return { + loadingError: action.error + }; + default: + return state; + } +} + +module.exports = mapConfig; diff --git a/web/client/reducers/locale.js b/web/client/reducers/locale.js new file mode 100644 index 0000000000..292bcf007a --- /dev/null +++ b/web/client/reducers/locale.js @@ -0,0 +1,27 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +var {CHANGE_LOCALE, LOCALE_LOAD_ERROR} = require('../actions/locale'); + +function locale(state = null, action) { + switch (action.type) { + case CHANGE_LOCALE: + return { + messages: action.messages, + current: action.locale + }; + case LOCALE_LOAD_ERROR: + return { + loadingError: action.error + }; + default: + return state; + } +} + +module.exports = locale; diff --git a/web/client/stores/I18NStore.jsx b/web/client/stores/I18NStore.jsx deleted file mode 100644 index 0c0b364923..0000000000 --- a/web/client/stores/I18NStore.jsx +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright 2015, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -var EventEmitter = require('events').EventEmitter; -var assign = require('object-assign'); -var keymirror = require('keymirror'); - -var Dispatcher = require('../dispatchers/Dispatcher'); -var I18NActions = require('../actions/I18NActions'); - -var it = require('./data.it-IT'); -var us = require('./data.en-US'); - -const StaticLangStore = { - "it-IT": it, - "en-US": us -}; -const AvailableLang = { - "English": "en-US", - "Italiano": "it-IT" -}; - -const _getDefaultLang = locale => { - if (locale) { - for (let l in AvailableLang) { - if (AvailableLang.hasOwnProperty(l)) { - if (locale === AvailableLang[l]) { - return locale; - } - } - } - } - return "en-US"; -}; - -var _i18nStore; - -const _initStore = () => { - var l = navigator ? navigator.language || navigator.browserLanguage : "en-US"; - - _i18nStore = { - locales: AvailableLang, - data: StaticLangStore[_getDefaultLang(l)] - }; -}; - -var I18NStore = assign({}, EventEmitter.prototype, { - Event: keymirror({ - LANG_CHANGED: null - }), - register(event, handler) { - this.on(event, handler); - }, - unregister(event, handler) { - this.removeListener(event, handler); - }, - getCurrentLocale() { - return _i18nStore.data.locale; - }, - getSupportedLocales() { - return _i18nStore.locales; - }, - trigger(event) { - this.emit(event); - }, - getMsgById(id) { - return _i18nStore.data.messages[id]; - } -}); - -// this private function does: -// - gets internationalization data given a locale value -// - changes the internal state of this store -// - moves on calling the given callback function -const _fetchIntlData = (locale, callback) => { - _i18nStore.data = StaticLangStore[locale]; - callback(); -}; - -// this function manages actions which come from the dispatcher -const _actionsManager = action => { - switch (action.type) { - case I18NActions.CHANGE_LANG: { - _fetchIntlData(action.locale, () => { - I18NStore.trigger(I18NStore.Event.LANG_CHANGED); - }); - } break; - default: // ignore other kind of actions. - } -}; - -_initStore(); - -Dispatcher.register(_actionsManager); - -module.exports = I18NStore; - -// these stuff was exported to easly perform unit tests. -const _emulateDispatcher = action => { - _actionsManager(action); -}; -module.exports._emulate_dispatcher = _emulateDispatcher; - -const _setMockedData = obj => { - _i18nStore = obj; -}; -module.exports._set_mocked_data = _setMockedData; -module.exports._get_default_language = _getDefaultLang; diff --git a/web/client/stores/__tests__/I18NStore-test.jsx b/web/client/stores/__tests__/I18NStore-test.jsx deleted file mode 100644 index 2479dcd2e5..0000000000 --- a/web/client/stores/__tests__/I18NStore-test.jsx +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Copyright 2015, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -var expect = require('expect'); -var I18NActions = require('../../actions/I18NActions'); - -describe('This test for I18NStore', () => { - var I18NStore; - beforeEach((done) => { - I18NStore = require('../I18NStore'); - setTimeout(done); - }); - - afterEach((done) => { - var name = require.resolve('../I18NStore'); - delete require.cache[name]; - setTimeout(done); - }); - - it('checks _getDefaultLang()', () => { - expect(I18NStore._get_default_language()).toEqual("en-US"); - expect(I18NStore._get_default_language("it-IT")).toEqual("it-IT"); - expect(I18NStore._get_default_language("something")).toEqual("en-US"); - }); - - it('checks getCurrentLocale()', () => { - const mockStoreData = { - data: { - locale: "foo" - } - }; - I18NStore._set_mocked_data(mockStoreData); - - expect(I18NStore.getCurrentLocale()).toBe(mockStoreData.data.locale); - }); - - it('checks default locale', () => { - var locale; - if (navigator) { - locale = navigator.language || navigator.browserLanguage; - if (locale && (locale === "it-IT" || locale === "en-US")) { - expect(I18NStore.getCurrentLocale()).toBe(locale); - } - } - expect(I18NStore.getCurrentLocale()).toBe("en-US"); - }); - - it('checks getSupportedLocales()', () => { - const mockStoreData = { - locales: { - "l0": "00", - "l1": "01" - } - }; - I18NStore._set_mocked_data(mockStoreData); - - const output = I18NStore.getSupportedLocales(); - for (let p in mockStoreData.locales) { - if (mockStoreData.locales.hasOwnProperty(p)) { - expect(output[p]).toBe(mockStoreData.locales[p]); - } - } - for (let p in output) { - if (output.hasOwnProperty(p)) { - expect(mockStoreData.locales[p]).toBe(output[p]); - } - } - }); - - it('checks getMsgById(id)', () => { - const mockStoreData = { - data: { - messages: { - "id0": "id0", - "id1": "id1", - "id2": "id2" - } - } - }; - I18NStore._set_mocked_data(mockStoreData); - - for (let id in mockStoreData.data.messages) { - if (mockStoreData.data.messages.hasOwnProperty(id)) { - expect(I18NStore.getMsgById(id)).toBe(id); - } - } - }); - - it('checks register(event, handler)', () => { - const mockEvent = "anEvent"; - const testHandlers = { h() {} }; - const spy = expect.spyOn(testHandlers, "h"); - - I18NStore.register(mockEvent, testHandlers.h); - I18NStore.emit(mockEvent); - - expect(spy.calls.length).toBe(1); - }); - - it('checks trigger(event)', () => { - const mockEvent = "anEvent"; - const testHandlers = { h() {} }; - const spy = expect.spyOn(testHandlers, "h"); - - I18NStore.register(mockEvent, testHandlers.h); - I18NStore.trigger(mockEvent); - - expect(spy.calls.length).toBe(1); - }); - - it('unregister(event, handler)', () => { - const mockEvent = "anEvent"; - const testHandlers = { h() {} }; - const spy = expect.spyOn(testHandlers, "h"); - - I18NStore.register(mockEvent, testHandlers.h); - I18NStore.trigger(mockEvent); - - expect(spy.calls.length).toBe(1); - - I18NStore.unregister(mockEvent, testHandlers.h); - I18NStore.trigger(mockEvent); - - expect(spy.calls.length).toBe(1); - }); - - it('checks the correctness of I18NActions.CHANGE_LANG menagement', () => { - const currentLocale = I18NStore.getCurrentLocale(); - const locales = I18NStore.getSupportedLocales(); - var newLocale; - - for (let l in locales) { - if (locales.hasOwnProperty(l)) { - if (locales[l] !== currentLocale) { - newLocale = locales[l]; - } - } - } - I18NStore._emulate_dispatcher({ - locale: newLocale, - type: I18NActions.CHANGE_LANG - }); - - expect(I18NStore.getCurrentLocale()).toBe(newLocale); - }); -}); diff --git a/web/client/stores/data.it-IT b/web/client/test-resources/data.it-IT similarity index 72% rename from web/client/stores/data.it-IT rename to web/client/test-resources/data.it-IT index 8c33f06a25..9a2b00af8d 100644 --- a/web/client/stores/data.it-IT +++ b/web/client/test-resources/data.it-IT @@ -1,15 +1,8 @@ -/** - * Copyright 2015, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -module.exports = { - locale: "it-IT", - messages: { - msgId0: "{name} ha scattato {numPhotos, plural, =0 {nessuna foto} other {# foto}} il {takenDate, date, long}.", - htmlTest: "{name} {surname}", +{ + "locale": "it-IT", + "messages": { + "msgId0": "{name} ha scattato {numPhotos, plural, =0 {nessuna foto} other {# foto}} il {takenDate, date, long}.", + "htmlTest": "{name} {surname}", "about_title": "Riguardo a questa app...", "aboutLbl": "Info", "about_p0-0": "MapStore 2 un framework per realizzare applicazioni di web mapping usando librerie di mapping standard, come", @@ -23,4 +16,4 @@ module.exports = { "about_a0": "questa", "about_p5-1": "pagina." } -}; +} diff --git a/web/client/test-resources/testConfig.json b/web/client/test-resources/testConfig.json new file mode 100644 index 0000000000..5c5514f956 --- /dev/null +++ b/web/client/test-resources/testConfig.json @@ -0,0 +1,39 @@ +{ + "map": { + "projection": "EPSG:900913", + "units": "m", + "center": [1250000.000000, 5370000.000000], + "zoom":5, + "maxExtent": [ + -20037508.34, -20037508.34, + 20037508.34, 20037508.34 + ], + "layers": [ + { + "type": "osm", + "title": "Open Street Map", + "name": "mapnik", + "group": "background", + "visibility": true + },{ + "type": "wms", + "url":"http://213.215.135.196/reflector/open/service", + "visibility": false, + "title": "e-Geos Ortofoto RealVista 1.0", + "name": "rv1", + "group": "background", + "format": "image/png" + }, + { + "type": "wms", + "url":"http://demo.geo-solutions.it/geoserver/wms", + "visibility": true, + "opacity": 0.5, + "title": "Weather data", + "name": "nurc:Arc_Sample", + "group": "Meteo", + "format": "image/png" + } + ] + } +} diff --git a/web/client/stores/data.en-US b/web/client/translations/data.en-US similarity index 82% rename from web/client/stores/data.en-US rename to web/client/translations/data.en-US index 1352dee0ef..5586309d59 100644 --- a/web/client/stores/data.en-US +++ b/web/client/translations/data.en-US @@ -1,13 +1,6 @@ -/** - * Copyright 2015, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -module.exports = { - locale: "en-US", - messages: { +{ + "locale": "en-US", + "messages": { "msgId0": "{name} took {numPhotos, plural, =0 {no photos} =1 {one photo} other {# photos}} on {takenDate, date, long}.", "htmlTest": "{name} {surname}", "about_title": "About this app...", @@ -23,4 +16,4 @@ module.exports = { "about_a0": "this", "about_p5-1": "page." } -}; +} diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT new file mode 100644 index 0000000000..9a2b00af8d --- /dev/null +++ b/web/client/translations/data.it-IT @@ -0,0 +1,19 @@ +{ + "locale": "it-IT", + "messages": { + "msgId0": "{name} ha scattato {numPhotos, plural, =0 {nessuna foto} other {# foto}} il {takenDate, date, long}.", + "htmlTest": "{name} {surname}", + "about_title": "Riguardo a questa app...", + "aboutLbl": "Info", + "about_p0-0": "MapStore 2 un framework per realizzare applicazioni di web mapping usando librerie di mapping standard, come", + "about_p0-1": "e", + "about_p1": "MapStore 2 contiene alcune applicazioni di esempio:", + "about_ul0_li0": "MapViewer è un semplice visualizzatore di mappe preconfigurate (opzionalmente memorizzare in un database usando GeoStore)", + "about_ul0_li1": "MapPublisher è stato sviluppato per creare, salvare a condividere in modo semplice ed intuitivo mappe e mashups creati selezionando contenuti da server come OpenStreetMap, Google Maps, MapQuest o da specifici servizi forniti dalla tua organizzazione o da terze parti. Per maggiori informazioni controlla la", + "about_h20": "Licenza", + "about_p3": "MapStore 2 è un software Free ed Open Source, basato su OpenLayers 3, Leaflet e ReactJS, ed è rilasciato sotto licenza Simplified BSD License.", + "about_p5-0": "Per informazioni controlla", + "about_a0": "questa", + "about_p5-1": "pagina." + } +} diff --git a/web/client/utils/ConfigUtils.js b/web/client/utils/ConfigUtils.js index 76f97b9f8f..374576dece 100644 --- a/web/client/utils/ConfigUtils.js +++ b/web/client/utils/ConfigUtils.js @@ -7,24 +7,45 @@ */ var Proj4js = require('proj4'); +const epsg4326 = new Proj4js.Proj('EPSG:4326'); + var ConfigUtils = { defaultSourceType: "gxp_wmssource", backgroundGroup: "background", + + getCenter: function(center, projection) { + if (center.lat && center.lng) { + return center; + } + let xy = Proj4js.toPoint(center); + if (projection) { + const epsgMap = new Proj4js.Proj(projection); + Proj4js.transform(epsgMap, epsg4326, xy); + } + return {lat: xy.y, lng: xy.x}; + }, + + getConfigurationOptions: function(query, defaultName, extension, geoStoreBase) { + const mapId = query.mapId; + let url; + if (mapId) { + url = ( geoStoreBase || "/mapstore/rest/geostore/" ) + "data/" + mapId; + } else { + url = (query.config || defaultName || 'config') + '.' + (extension || 'json'); + } + return { + configUrl: url, + legacy: !!mapId + }; + }, + convertFromLegacy: function(config) { var mapConfig = config.map; - var sources = config.gsSources; + var sources = config.gsSources || config.sources; var layers = mapConfig.layers; - var center = mapConfig.center; + var latLng = ConfigUtils.getCenter(mapConfig.center, mapConfig.projection); var zoom = mapConfig.zoom; var maxExtent = mapConfig.maxExtent; - var projection = mapConfig.projection; - - // manage projection conversions - var epsgMap = new Proj4js.Proj(projection); - var epsg4326 = new Proj4js.Proj('EPSG:4326'); - var xy = Proj4js.toPoint(center); - Proj4js.transform(epsgMap, epsg4326, xy); - const latLng = {lat: xy.y, lng: xy.x}; // setup layers and sources with defaults this.setupSources(sources, config.defaultSourceType); @@ -54,6 +75,16 @@ var ConfigUtils = { } } }, + /** + * Copy important source options to layer options. + */ + copySourceOptions: function(layer, source) { + Object.keys(source).forEach((option) => { + if (['url', 'baseParams'].indexOf(option) !== -1) { + layer[option] = source[option]; + } + }); + }, /** * Setup the layer visibility for the background group. @@ -66,8 +97,8 @@ var ConfigUtils = { for (i = 0; i < layers.length; i++) { layer = layers[i]; source = sources[layer.source]; - layer.sourceOptions = source; - let type = layer.sourceOptions.ptype; + ConfigUtils.copySourceOptions(layer, source); + let type = source.ptype; if (type) { layer.type = type.replace(/^gxp_(.*)source$/i, "$1"); } else { diff --git a/web/client/utils/LocaleUtils.js b/web/client/utils/LocaleUtils.js new file mode 100644 index 0000000000..af69809900 --- /dev/null +++ b/web/client/utils/LocaleUtils.js @@ -0,0 +1,29 @@ +/** + * Copyright 2015, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +const supportedLocales = { + "it": { + code: "it-IT", + description: "Italiano" + }, + "en": { + code: "en-US", + description: "English" + } +}; + +var LocaleUtils = { + getLocale: function(query) { + let locale = supportedLocales[query.locale || (navigator ? navigator.language || navigator.browserLanguage : "en")]; + return locale ? locale.code : "en-US"; + }, + getSupportedLocales: function() { + return supportedLocales; + } +}; + +module.exports = LocaleUtils; diff --git a/web/client/utils/leaflet/WMSUtils.js b/web/client/utils/leaflet/WMSUtils.js index f174e51b5a..1b6c765cdb 100644 --- a/web/client/utils/leaflet/WMSUtils.js +++ b/web/client/utils/leaflet/WMSUtils.js @@ -1,7 +1,7 @@ var objectAssign = require('object-assign'); var WMSUtils = { - wmsToLeafletOptions: function(source, options) { + wmsToLeafletOptions: function(options) { var opacity = options.opacity !== undefined ? options.opacity : 1; // NOTE: can we use opacity to manage visibility? return objectAssign({ @@ -10,7 +10,7 @@ var WMSUtils = { format: options.format || 'image/png', transparent: options.transparent !== undefined ? options.transparent : true, opacity: opacity - }, source.baseParams || {}, options.params || {}); + }, options.params || {}); }, getWMSURL: function( url ) { return url.split("\?")[0]; diff --git a/webpack.config.js b/webpack.config.js index 1d2af7c933..e3a9fc4375 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,6 +8,9 @@ var rewriteUrl = function(replacePath) { req.url = req.path.replace(opt.path, replacePath) + query; }; }; + +var PROD = JSON.parse(process.env.PROD_DEV || "0"); + module.exports = { entry: { viewer: path.join(__dirname, "web", "client", "examples", "viewer", "app"), @@ -18,12 +21,14 @@ module.exports = { publicPath: "/dist/", filename: "[name].js" }, - plugins: [ + plugins: PROD ? [ new CommonsChunkPlugin("commons", "mapstore-commons.js"), new UglifyJsPlugin({ compress: {warnings: false}, mangle: true }) + ] : [ + new CommonsChunkPlugin("commons", "mapstore-commons.js") ], resolve: { extensions: ["", ".js", ".jsx"]