From 2f9cdbe3ec61f6ac05f84a64e76d2dc68f2e5ff8 Mon Sep 17 00:00:00 2001 From: Mirco Bertelli Date: Tue, 15 Sep 2015 16:41:59 +0200 Subject: [PATCH] Fixes #146, GetFeatureInfo improvements - returnded feature info are grouped by layer in separated tabs - mouse pointer has been changed when component become active/deactive - react-bootstrap has been updated to v0.25.2 to have and components. Also: - added a webpack proxy configuration to use a local instance of http-proxy - code refactoring --- package.json | 2 +- web/client/actions/__tests__/map-test.js | 78 ++----- web/client/actions/__tests__/mapInfo-test.js | 96 +++++++++ web/client/actions/map.js | 109 ++-------- web/client/actions/mapInfo.js | 122 +++++++++++ web/client/components/map/leaflet/Map.jsx | 11 +- .../map/leaflet/__tests__/Map-test.jsx | 27 +++ web/client/components/map/openlayers/Map.jsx | 11 +- .../map/openlayers/__tests__/Map-test.jsx | 27 +++ web/client/components/misc/HtmlRenderer.jsx | 2 +- .../viewer/components/GetFeatureInfo.jsx | 194 ++++++++++++++++-- web/client/examples/viewer/components/Map.jsx | 3 +- .../examples/viewer/containers/Viewer.jsx | 51 ++--- web/client/examples/viewer/index.html | 1 + web/client/reducers/__tests__/config-test.js | 14 ++ web/client/reducers/__tests__/mapInfo-test.js | 87 ++++++-- web/client/reducers/config.js | 6 +- web/client/reducers/mapInfo.js | 42 +++- web/client/translations/data.en-US | 3 +- web/client/translations/data.it-IT | 3 +- web/client/utils/CoordinatesUtils.js | 24 +++ .../utils/__tests__/CoordinatesUtils-test.js | 10 + webpack.config.js | 1 + 23 files changed, 675 insertions(+), 249 deletions(-) create mode 100644 web/client/actions/__tests__/mapInfo-test.js create mode 100644 web/client/actions/mapInfo.js diff --git a/package.json b/package.json index 8bcfd71a0f9..62106945dda 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "openlayers": "^3.8.2", "proj4": "~2.3.6", "react": "^0.13.3", - "react-bootstrap": "^0.24.3", + "react-bootstrap": "^0.25.2", "react-intl": "^1.2.0", "react-redux": "^2.0.0", "redux": "^2.0.0", diff --git a/web/client/actions/__tests__/map-test.js b/web/client/actions/__tests__/map-test.js index 655c6e8019f..ab7354a7445 100644 --- a/web/client/actions/__tests__/map-test.js +++ b/web/client/actions/__tests__/map-test.js @@ -9,17 +9,11 @@ var expect = require('expect'); var { CHANGE_MAP_VIEW, - ERROR_FEATURE_INFO, - EXCEPTIONS_FEATURE_INFO, - LOAD_FEATURE_INFO, - CHANGE_MAPINFO_STATE, - NEW_MAPINFO_REQUEST, - PURGE_MAPINFO_RESULTS, - getFeatureInfo, + CLICK_ON_MAP, + CHANGE_MOUSE_POINTER, changeMapView, - changeMapInfoState, - newMapInfoRequest, - purgeMapInfoResults + clickOnMap, + changeMousePointer } = require('../map'); describe('Test correctness of the map actions', () => { @@ -39,63 +33,21 @@ describe('Test correctness of the map actions', () => { expect(retval.size).toBe(testSize); }); - it('get feature info data', (done) => { - getFeatureInfo('base/web/client/test-resources/featureInfo-response.json', {})((e) => { - try { - expect(e).toExist(); - expect(e.type).toBe(LOAD_FEATURE_INFO); - done(); - } catch(ex) { - done(ex); - } - }); - }); - - it('get feature info exception', (done) => { - getFeatureInfo('base/web/client/test-resources/featureInfo-exception.json', {})((e) => { - try { - expect(e).toExist(); - expect(e.type).toBe(EXCEPTIONS_FEATURE_INFO); - done(); - } catch(ex) { - done(ex); - } - }); - }); - - it('get feature info error', (done) => { - getFeatureInfo('requestError.json', {})((e) => { - try { - expect(e).toExist(); - expect(e.type).toBe(ERROR_FEATURE_INFO); - done(); - } catch(ex) { - done(ex); - } - }); - }); - - it('change map info state', () => { + it('set a new clicked point', () => { const testVal = "val"; - const retval = changeMapInfoState(testVal); + const retval = clickOnMap(testVal); - expect(retval.type).toBe(CHANGE_MAPINFO_STATE); - expect(retval.enabled).toExist(); - expect(retval.enabled).toBe(testVal); + expect(retval.type).toBe(CLICK_ON_MAP); + expect(retval.point).toExist(); + expect(retval.point).toBe(testVal); }); - it('add new info request', () => { - const testVal = "val"; - const retval = newMapInfoRequest(testVal); - - expect(retval.type).toBe(NEW_MAPINFO_REQUEST); - expect(retval.request).toExist(); - expect(retval.request).toBe(testVal); - }); + it('set a new mouse pointer', () => { + const testVal = 'pointer'; + const retval = changeMousePointer(testVal); - it('delete all results', () => { - const retval = purgeMapInfoResults(); - - expect(retval.type).toBe(PURGE_MAPINFO_RESULTS); + expect(retval).toExist(); + expect(retval.type).toBe(CHANGE_MOUSE_POINTER); + expect(retval.pointer).toBe(testVal); }); }); diff --git a/web/client/actions/__tests__/mapInfo-test.js b/web/client/actions/__tests__/mapInfo-test.js new file mode 100644 index 00000000000..7f7b46e769b --- /dev/null +++ b/web/client/actions/__tests__/mapInfo-test.js @@ -0,0 +1,96 @@ +/** + * 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 { + ERROR_FEATURE_INFO, + EXCEPTIONS_FEATURE_INFO, + LOAD_FEATURE_INFO, + CHANGE_MAPINFO_STATE, + NEW_MAPINFO_REQUEST, + PURGE_MAPINFO_RESULTS, + getFeatureInfo, + changeMapInfoState, + newMapInfoRequest, + purgeMapInfoResults +} = require('../mapInfo'); + +describe('Test correctness of the map actions', () => { + + it('get feature info data', (done) => { + getFeatureInfo('base/web/client/test-resources/featureInfo-response.json', {p: "p"}, "meta")((e) => { + try { + expect(e).toExist(); + expect(e.type).toBe(LOAD_FEATURE_INFO); + expect(e.data).toExist(); + expect(e.requestParams).toExist(); + expect(e.requestParams.p).toBe("p"); + expect(e.layerMetadata).toBe("meta"); + done(); + } catch(ex) { + done(ex); + } + }); + }); + + it('get feature info exception', (done) => { + getFeatureInfo('base/web/client/test-resources/featureInfo-exception.json', {p: "p"}, "meta")((e) => { + try { + expect(e).toExist(); + expect(e.type).toBe(EXCEPTIONS_FEATURE_INFO); + expect(e.exceptions).toExist(); + expect(e.requestParams).toExist(); + expect(e.requestParams.p).toBe("p"); + expect(e.layerMetadata).toBe("meta"); + done(); + } catch(ex) { + done(ex); + } + }); + }); + + it('get feature info error', (done) => { + getFeatureInfo('requestError.json', {p: "p"}, "meta")((e) => { + try { + expect(e).toExist(); + expect(e.type).toBe(ERROR_FEATURE_INFO); + expect(e.error).toExist(); + expect(e.requestParams).toExist(); + expect(e.requestParams.p).toBe("p"); + expect(e.layerMetadata).toBe("meta"); + done(); + } catch(ex) { + done(ex); + } + }); + }); + + it('change map info state', () => { + const testVal = "val"; + const retval = changeMapInfoState(testVal); + + expect(retval.type).toBe(CHANGE_MAPINFO_STATE); + expect(retval.enabled).toExist(); + expect(retval.enabled).toBe(testVal); + }); + + it('add new info request', () => { + const testVal = "val"; + const retval = newMapInfoRequest(testVal); + + expect(retval.type).toBe(NEW_MAPINFO_REQUEST); + expect(retval.request).toExist(); + expect(retval.request).toBe(testVal); + }); + + it('delete all results', () => { + const retval = purgeMapInfoResults(); + + expect(retval.type).toBe(PURGE_MAPINFO_RESULTS); + }); +}); diff --git a/web/client/actions/map.js b/web/client/actions/map.js index c3f462ff652..b84d27cfb13 100644 --- a/web/client/actions/map.js +++ b/web/client/actions/map.js @@ -5,16 +5,10 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -const assign = require('object-assign'); -const axios = require('axios'); const CHANGE_MAP_VIEW = 'CHANGE_MAP_VIEW'; -const LOAD_FEATURE_INFO = 'LOAD_FEATURE_INFO'; -const ERROR_FEATURE_INFO = 'ERROR_FEATURE_INFO'; -const EXCEPTIONS_FEATURE_INFO = 'EXCEPTIONS_FEATURE_INFO'; -const CHANGE_MAPINFO_STATE = 'CHANGE_MAPINFO_STATE'; -const NEW_MAPINFO_REQUEST = 'NEW_MAPINFO_REQUEST'; -const PURGE_MAPINFO_RESULTS = 'PURGE_MAPINFO_RESULTS'; +const CLICK_ON_MAP = 'CLICK_ON_MAP'; +const CHANGE_MOUSE_POINTER = 'CHANGE_MOUSE_POINTER'; function changeMapView(center, zoom, bbox, size) { return { @@ -26,104 +20,25 @@ function changeMapView(center, zoom, bbox, size) { }; } -/** - * Private - * @return a LOAD_FEATURE_INFO action with the response data to a wms GetFeatureInfo - */ -function loadFeatureInfo(data) { - return { - type: LOAD_FEATURE_INFO, - data: data - }; -} - -/** - * Private - * @return a ERROR_FEATURE_INFO action with the error occured - */ -function errorFeatureInfo(e) { - return { - type: ERROR_FEATURE_INFO, - error: e - }; -} - -/** - * Private - * @return a EXCEPTIONS_FEATURE_INFO action with the wms exception occured - * during a GetFeatureInfo request. - */ -function exceptionsFeatureInfo(exceptions) { - return { - type: EXCEPTIONS_FEATURE_INFO, - exceptions: exceptions - }; -} - -/** - * Sends a wms GetFeatureInfo request and dispatches the right action - * in case of success, error or exceptions. - * - * @param wmsBasePath {string} base path to the wms service - * @param requestParams {object} map of params for a getfeatureinfo request. - */ -function getFeatureInfo(wmsBasePath, requestParams) { - const defaultParams = { - service: 'WMS', - version: '1.0.0', - request: 'GetFeatureInfo', - crs: 'EPSG:4326', - info_format: 'application/json', - feature_count: 50, - x: 0, - y: 0, - exceptions: 'application/json' - }; - const param = assign({}, defaultParams, requestParams); - return (dispatch) => { - return axios.get(wmsBasePath, {params: param}).then((response) => { - if (response.data.exceptions) { - dispatch(exceptionsFeatureInfo(response.data.exceptions)); - } else { - dispatch(loadFeatureInfo(response.data)); - } - }).catch((e) => { - dispatch(errorFeatureInfo(e)); - }); - }; -} - -function changeMapInfoState(enabled) { - return { - type: CHANGE_MAPINFO_STATE, - enabled: enabled - }; -} - -function newMapInfoRequest(reqConfig) { +function clickOnMap(point) { return { - type: NEW_MAPINFO_REQUEST, - request: reqConfig + type: CLICK_ON_MAP, + point: point }; } -function purgeMapInfoResults() { +function changeMousePointer(pointerType) { return { - type: PURGE_MAPINFO_RESULTS + type: CHANGE_MOUSE_POINTER, + pointer: pointerType }; } module.exports = { CHANGE_MAP_VIEW, - ERROR_FEATURE_INFO, - EXCEPTIONS_FEATURE_INFO, - LOAD_FEATURE_INFO, - CHANGE_MAPINFO_STATE, - NEW_MAPINFO_REQUEST, - PURGE_MAPINFO_RESULTS, - getFeatureInfo, + CLICK_ON_MAP, + CHANGE_MOUSE_POINTER, changeMapView, - changeMapInfoState, - newMapInfoRequest, - purgeMapInfoResults + clickOnMap, + changeMousePointer }; diff --git a/web/client/actions/mapInfo.js b/web/client/actions/mapInfo.js new file mode 100644 index 00000000000..286f0c6767e --- /dev/null +++ b/web/client/actions/mapInfo.js @@ -0,0 +1,122 @@ +/** + * 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 assign = require('object-assign'); +const axios = require('axios'); + +const LOAD_FEATURE_INFO = 'LOAD_FEATURE_INFO'; +const ERROR_FEATURE_INFO = 'ERROR_FEATURE_INFO'; +const EXCEPTIONS_FEATURE_INFO = 'EXCEPTIONS_FEATURE_INFO'; +const CHANGE_MAPINFO_STATE = 'CHANGE_MAPINFO_STATE'; +const NEW_MAPINFO_REQUEST = 'NEW_MAPINFO_REQUEST'; +const PURGE_MAPINFO_RESULTS = 'PURGE_MAPINFO_RESULTS'; + +/** + * Private + * @return a LOAD_FEATURE_INFO action with the response data to a wms GetFeatureInfo + */ +function loadFeatureInfo(data, rParams, lMetaData) { + return { + type: LOAD_FEATURE_INFO, + data: data, + requestParams: rParams, + layerMetadata: lMetaData + }; +} + +/** + * Private + * @return a ERROR_FEATURE_INFO action with the error occured + */ +function errorFeatureInfo(e, rParams, lMetaData) { + return { + type: ERROR_FEATURE_INFO, + error: e, + requestParams: rParams, + layerMetadata: lMetaData + }; +} + +/** + * Private + * @return a EXCEPTIONS_FEATURE_INFO action with the wms exception occured + * during a GetFeatureInfo request. + */ +function exceptionsFeatureInfo(exceptions, rParams, lMetaData) { + return { + type: EXCEPTIONS_FEATURE_INFO, + exceptions: exceptions, + requestParams: rParams, + layerMetadata: lMetaData + }; +} + +/** + * Sends a wms GetFeatureInfo request and dispatches the right action + * in case of success, error or exceptions. + * + * @param wmsBasePath {string} base path to the wms service + * @param requestParams {object} map of params for a getfeatureinfo request. + */ +function getFeatureInfo(wmsBasePath, requestParams, lMetaData) { + const defaultParams = { + service: 'WMS', + version: '1.1.1', + request: 'GetFeatureInfo', + srs: 'EPSG:4326', + info_format: 'application/json', + x: 0, + y: 0, + exceptions: 'application/json' + }; + const param = assign({}, defaultParams, requestParams); + return (dispatch) => { + return axios.get(wmsBasePath, {params: param}).then((response) => { + if (response.data.exceptions) { + dispatch(exceptionsFeatureInfo(response.data.exceptions, requestParams, lMetaData)); + } else { + dispatch(loadFeatureInfo(response.data, requestParams, lMetaData)); + } + }).catch((e) => { + dispatch(errorFeatureInfo(e, requestParams, lMetaData)); + }); + }; +} + +function changeMapInfoState(enabled) { + return { + type: CHANGE_MAPINFO_STATE, + enabled: enabled + }; +} + +function newMapInfoRequest(reqConfig) { + return { + type: NEW_MAPINFO_REQUEST, + request: reqConfig + }; +} + +function purgeMapInfoResults() { + return { + type: PURGE_MAPINFO_RESULTS + }; +} + +module.exports = { + ERROR_FEATURE_INFO, + EXCEPTIONS_FEATURE_INFO, + LOAD_FEATURE_INFO, + CHANGE_MAPINFO_STATE, + NEW_MAPINFO_REQUEST, + PURGE_MAPINFO_RESULTS, + getFeatureInfo, + changeMapInfoState, + newMapInfoRequest, + purgeMapInfoResults +}; diff --git a/web/client/components/map/leaflet/Map.jsx b/web/client/components/map/leaflet/Map.jsx index b1e1788ce13..1be5ebee8bc 100644 --- a/web/client/components/map/leaflet/Map.jsx +++ b/web/client/components/map/leaflet/Map.jsx @@ -17,7 +17,8 @@ var LeafletMap = React.createClass({ projection: React.PropTypes.string, onMapViewChanges: React.PropTypes.func, onClick: React.PropTypes.func, - mapOptions: React.PropTypes.object + mapOptions: React.PropTypes.object, + mousePointer: React.PropTypes.string }, getDefaultProps() { return { @@ -43,10 +44,12 @@ var LeafletMap = React.createClass({ this.map.on('click', (event) => { this.props.onClick(event.containerPoint); }); this.updateMapInfoState(); + this.setMousePointer(this.props.mousePointer); // NOTE: this re-call render function after div creation to have the map initialized. this.forceUpdate(); }, componentWillReceiveProps(newProps) { + this.setMousePointer(newProps.mousePointer); const currentCenter = this.map.getCenter(); const centerIsUpdate = newProps.center.y === currentCenter.lat && newProps.center.x === currentCenter.lng; @@ -89,6 +92,12 @@ var LeafletMap = React.createClass({ crs: 'EPSG:4326', rotation: 0 }, size); + }, + setMousePointer(pointer) { + if (this.map) { + const mapDiv = this.map.getContainer(); + mapDiv.style.cursor = pointer || 'auto'; + } } }); diff --git a/web/client/components/map/leaflet/__tests__/Map-test.jsx b/web/client/components/map/leaflet/__tests__/Map-test.jsx index 063badd77a7..b242ffe832d 100644 --- a/web/client/components/map/leaflet/__tests__/Map-test.jsx +++ b/web/client/components/map/leaflet/__tests__/Map-test.jsx @@ -163,4 +163,31 @@ describe('LeafletMap', () => { expect(leafletMap.getCenter().lat).toBe(44); expect(leafletMap.getCenter().lng).toBe(10); }); + + it('check if the map has "auto" cursor as default', () => { + const map = React.render( + + , document.body); + + const leafletMap = map.map; + const mapDiv = leafletMap.getContainer(); + expect(mapDiv.style.cursor).toBe("auto"); + }); + + it('check if the map can be created with a custom cursor', () => { + const map = React.render( + + , document.body); + + const leafletMap = map.map; + const mapDiv = leafletMap.getContainer(); + expect(mapDiv.style.cursor).toBe("pointer"); + }); }); diff --git a/web/client/components/map/openlayers/Map.jsx b/web/client/components/map/openlayers/Map.jsx index e781f263eac..dfbff48b600 100644 --- a/web/client/components/map/openlayers/Map.jsx +++ b/web/client/components/map/openlayers/Map.jsx @@ -18,7 +18,8 @@ var OpenlayersMap = React.createClass({ projection: React.PropTypes.string, onMapViewChanges: React.PropTypes.func, onClick: React.PropTypes.func, - mapOptions: React.PropTypes.object + mapOptions: React.PropTypes.object, + mousePointer: React.PropTypes.string }, getDefaultProps() { return { @@ -84,11 +85,13 @@ var OpenlayersMap = React.createClass({ }); this.map = map; + this.setMousePointer(this.props.mousePointer); // NOTE: this re-call render function after div creation to have the map initialized. this.forceUpdate(); }, componentWillReceiveProps(newProps) { var view = this.map.getView(); + this.setMousePointer(newProps.mousePointer); const currentCenter = this.normalizeCenter(view.getCenter()); const centerIsUpdated = newProps.center.y === currentCenter[1] && newProps.center.x === currentCenter[0]; @@ -117,6 +120,12 @@ var OpenlayersMap = React.createClass({ }, normalizeCenter: function(center) { return ol.proj.transform(center, this.props.projection, 'EPSG:4326'); + }, + setMousePointer(pointer) { + if (this.map) { + const mapDiv = this.map.getViewport(); + mapDiv.style.cursor = pointer || 'auto'; + } } }); diff --git a/web/client/components/map/openlayers/__tests__/Map-test.jsx b/web/client/components/map/openlayers/__tests__/Map-test.jsx index 086fec45886..abe6a322291 100644 --- a/web/client/components/map/openlayers/__tests__/Map-test.jsx +++ b/web/client/components/map/openlayers/__tests__/Map-test.jsx @@ -165,4 +165,31 @@ describe('OpenlayersMap', () => { expect(olMap.getView().getCenter()[1]).toBe(44); expect(olMap.getView().getCenter()[0]).toBe(10); }); + + it('check if the map has "auto" cursor as default', () => { + const map = React.render( + + , document.body); + + const olMap = map.map; + const mapDiv = olMap.getViewport(); + expect(mapDiv.style.cursor).toBe("auto"); + }); + + it('check if the map can be created with a custom cursor', () => { + const map = React.render( + + , document.body); + + const olMap = map.map; + const mapDiv = olMap.getViewport(); + expect(mapDiv.style.cursor).toBe("pointer"); + }); }); diff --git a/web/client/components/misc/HtmlRenderer.jsx b/web/client/components/misc/HtmlRenderer.jsx index c2295586d44..cfd8061dae1 100644 --- a/web/client/components/misc/HtmlRenderer.jsx +++ b/web/client/components/misc/HtmlRenderer.jsx @@ -25,7 +25,7 @@ var HtmlRenderer = React.createClass({ }; }, render() { - return
; + return
; } }); diff --git a/web/client/examples/viewer/components/GetFeatureInfo.jsx b/web/client/examples/viewer/components/GetFeatureInfo.jsx index f0861b3eb55..409e3a46721 100644 --- a/web/client/examples/viewer/components/GetFeatureInfo.jsx +++ b/web/client/examples/viewer/components/GetFeatureInfo.jsx @@ -9,11 +9,16 @@ var React = require('react'); var BootstrapReact = require('react-bootstrap'); var Modal = BootstrapReact.Modal; +var Tabs = BootstrapReact.Tabs; +var Tab = BootstrapReact.Tab; var I18N = require('../../../components/I18N/I18N'); var ToggleButton = require('../../../components/buttons/ToggleButton'); var HtmlRenderer = require('../../../components/misc/HtmlRenderer'); +var CoordinatesUtils = require('../../../utils/CoordinatesUtils'); +var assign = require('object-assign'); + var GetFeatureInfo = React.createClass({ propTypes: { htmlResponses: React.PropTypes.array, @@ -21,37 +26,170 @@ var GetFeatureInfo = React.createClass({ btnText: React.PropTypes.string, btnIcon: React.PropTypes.string, enabled: React.PropTypes.bool, - btnClick: React.PropTypes.func, - onCloseResult: React.PropTypes.func + mapConfig: React.PropTypes.object, + layerFilter: React.PropTypes.func, + actions: React.PropTypes.shape({ + getFeatureInfo: React.PropTypes.func, + changeMapInfoState: React.PropTypes.func, + purgeMapInfoResults: React.PropTypes.func, + changeMousePointer: React.PropTypes.func + }), + clickedMapPoint: React.PropTypes.shape({ + x: React.PropTypes.number, + y: React.PropTypes.number + }) }, getDefaultProps() { return { enabled: false, htmlResponses: [], - btnClick() {} + mapConfig: {layers: []}, + layerFilter(l) { + return l.visibility && + l.type === 'wms' && + (l.queryable === undefined || l.queryable) && + l.group !== "background" + ; + }, + actions: { + getFeatureInfo() {}, + changeMapInfoState() {}, + purgeMapInfoResults() {}, + changeMousePointer() {} + } }; }, getInitialState() { return { showModal: true }; }, + componentWillReceiveProps(newProps) { + // if there's a new clicked point on map and GetFeatureInfo is active + // it composes and sends a getFeatureInfo action. + if ( + this.props.clickedMapPoint === undefined && newProps.clickedMapPoint !== undefined || + this.props.clickedMapPoint !== undefined && newProps.clickedMapPoint !== undefined && ( + this.props.clickedMapPoint.x !== newProps.clickedMapPoint.x || + this.props.clickedMapPoint.y !== newProps.clickedMapPoint.y + ) + ) { + if (newProps.enabled) { + const wmsVisibleLayers = newProps.mapConfig.layers.filter(newProps.layerFilter); + const {bounds, crs} = this.reprojectBbox(newProps.mapConfig.bbox, newProps.mapConfig.projection); + for (let l = 0; l < wmsVisibleLayers.length; l++) { + const layer = wmsVisibleLayers[l]; + const requestConf = { + layers: layer.name, + query_layers: layer.name, + x: newProps.clickedMapPoint.x, + y: newProps.clickedMapPoint.y, + height: newProps.mapConfig.size.height, + width: newProps.mapConfig.size.width, + srs: crs, + bbox: bounds.minx + "," + + bounds.miny + "," + + bounds.maxx + "," + + bounds.maxy, + info_format: "text/html" + }; + const layerMetadata = { + title: layer.title + }; + const url = layer.url.replace(/[?].*$/g, ''); + newProps.actions.getFeatureInfo(url, requestConf, layerMetadata); + } + } + } + + if (newProps.enabled && !this.props.enabled) { + this.props.actions.changeMousePointer('pointer'); + } else if (!newProps.enabled && this.props.enabled) { + this.props.actions.changeMousePointer('auto'); + } + }, + onToggleButtonClick(btnEnabled) { + this.props.actions.changeMapInfoState(!btnEnabled); + }, + onModalHiding() { + this.props.actions.purgeMapInfoResults(); + }, + // returns a array of tabs where each one contains feature info for + // a specific layer. getModalContent(responses) { var output = []; var content = ""; - const regexp = /^.*(.*)<\/body>.*$/g; + var title = ""; + var style = ""; + const regexpBody = /^[\s\S]*([\s\S]*)<\/body>[\s\S]*$/i; + const regexpStyle = /([^<]*<\/style>)/i; + const regexpException = /([^<]*)<\/ServiceException>/i; + for (let i = 0; i < responses.length; i++) { - if (typeof responses[i] === "string") { - content = responses[i].replace(regexp, '$1'); - output.push(); - } else if (responses[i].length !== undefined) { - const exArray = responses[i]; + const {response, layerMetadata} = responses[i]; + + title = ( +
+ {layerMetadata.title} +
+ ); + + if (typeof response === "string") { + // response can be a HTML feature info or XML exception + if (response.indexOf('

Exception

{exceptionMsg}

); + } else { // HTML feature info + // gets css rules from the response and removes which are related to body tag. + let styleMatch = regexpStyle.exec(response); + style = styleMatch && styleMatch.length === 2 ? regexpStyle.exec(response)[1] : ""; + style = style.replace(/body[,]+/g, ''); + // gets feature info managing an eventually empty response + content = response.replace(regexpBody, '$1').trim(); + content = content.length === 0 ? + (

) : + ; + } + output.push( + +
+ {content} +
+
+ ); + } else if (response.length !== undefined) { + // response is an array of exceptions + const exArray = response; for (let j = 0; j < exArray.length; j++) { - output.push(Exception: ' + j + '' + - '

' + exArray[j].text + '

' - }/>); + output.push( + +
+ Exception: ' + j + '' + + '

' + exArray[j].text + '

' + }/> +
+
+ ); } } else { - output.push(' + responses[i].data + '

'}/>); + let match = regexpBody.exec(response.data); + if (match && match.length === 2) { + content = match[1]; + } else { + content = '

' + response.data + '

'; + } + output.push( + +
+ +
+
+ ); } } return output; @@ -64,24 +202,42 @@ var GetFeatureInfo = React.createClass({ btnConfig={this.props.btnConfig} text={this.props.btnText} glyphicon={this.props.btnIcon} - onClick={this.props.btnClick} + onClick={this.onToggleButtonClick} /> + onHide={this.onModalHiding} + bsStyle="info" + dialogClassName="getFeatureInfo"> -
+ {this.getModalContent(this.props.htmlResponses)} -
+
); + }, + reprojectBbox(bbox, destSRS) { + let newBbox = CoordinatesUtils.reprojectBbox([ + bbox.bounds.minx, + bbox.bounds.miny, + bbox.bounds.maxx, + bbox.bounds.maxy + ], bbox.crs, destSRS); + return assign({}, { + crs: destSRS, + bounds: { + minx: newBbox[0], + miny: newBbox[1], + maxx: newBbox[2], + maxy: newBbox[3] + } + }); } }); diff --git a/web/client/examples/viewer/components/Map.jsx b/web/client/examples/viewer/components/Map.jsx index c2ed8e650f4..5ae4ca4171e 100644 --- a/web/client/examples/viewer/components/Map.jsx +++ b/web/client/examples/viewer/components/Map.jsx @@ -40,7 +40,8 @@ var VMap = React.createClass({ zoom={this.props.config.zoom} projection={this.props.config.projection || 'EPSG:3857'} onMapViewChanges={this.props.onMapViewChanges} - onClick={this.props.onClick}> + onClick={this.props.onClick} + mousePointer={this.props.config.mousePointer}> {this.renderLayers(this.props.config.layers)} ); diff --git a/web/client/examples/viewer/containers/Viewer.jsx b/web/client/examples/viewer/containers/Viewer.jsx index 42c8c054419..7f07221c3b3 100644 --- a/web/client/examples/viewer/containers/Viewer.jsx +++ b/web/client/examples/viewer/containers/Viewer.jsx @@ -14,7 +14,8 @@ var {bindActionCreators} = require('redux'); var ConfigUtils = require('../../../utils/ConfigUtils'); var {loadLocale} = require('../../../actions/locale'); -var {changeMapView, getFeatureInfo, changeMapInfoState, purgeMapInfoResults} = require('../../../actions/map'); +var {changeMapView, clickOnMap, changeMousePointer} = require('../../../actions/map'); +var {getFeatureInfo, changeMapInfoState, purgeMapInfoResults} = require('../../../actions/mapInfo'); var VMap = require('../components/Map'); var LangSelector = require('../../../components/I18N/LangSelector'); @@ -33,7 +34,9 @@ var Viewer = React.createClass({ changeMapView: React.PropTypes.func, getFeatureInfo: React.PropTypes.func, changeMapInfoState: React.PropTypes.func, - purgeMapInfoResults: React.PropTypes.func + purgeMapInfoResults: React.PropTypes.func, + clickOnMap: React.PropTypes.func, + changeMousePointer: React.PropTypes.func }, getFirstWmsVisibleLayer() { for (let i = 0; i < this.props.mapConfig.layers.length; i++) { @@ -52,8 +55,14 @@ var Viewer = React.createClass({ enabled={this.props.mapInfo.enabled} htmlResponses={this.props.mapInfo.responses} btnIcon="info-sign" - btnClick={this.manageGetFeatureInfoClick} - onCloseResult={this.manageCloseResults} + mapConfig={this.props.mapConfig} + actions={{ + getFeatureInfo: this.props.getFeatureInfo, + changeMapInfoState: this.props.changeMapInfoState, + purgeMapInfoResults: this.props.purgeMapInfoResults, + changeMousePointer: this.props.changeMousePointer + }} + clickedMapPoint={this.props.mapInfo.clickPoint} /> ]; }, @@ -68,7 +77,7 @@ var Viewer = React.createClass({ {() =>
- + {this.renderPlugins(this.props.locale)}
} @@ -79,34 +88,6 @@ var Viewer = React.createClass({ }, manageNewMapView(center, zoom, bbox, size) { this.props.changeMapView(center, zoom, bbox, size); - }, - manageClickOnMap(clickPoint) { - const bboxBounds = this.props.mapConfig.bbox.bounds; - const layer = this.getFirstWmsVisibleLayer(); - if (this.props.mapInfo && this.props.mapInfo.enabled) { - const requestConf = { - layers: layer.name, - query_layers: layer.name, - x: clickPoint.x, - y: clickPoint.y, - height: this.props.mapConfig.size.height, - width: this.props.mapConfig.size.width, - crs: this.props.mapConfig.bbox.crs, - bbox: bboxBounds.minx + "," + - bboxBounds.miny + "," + - bboxBounds.maxx + "," + - bboxBounds.maxy, - info_format: "text/html" - }; - const url = layer.url.replace(/[?].*$/g, ''); - this.props.getFeatureInfo(url, requestConf); - } - }, - manageGetFeatureInfoClick(btnEnabled) { - this.props.changeMapInfoState(!btnEnabled); - }, - manageCloseResults() { - this.props.purgeMapInfoResults(); } }); @@ -124,6 +105,8 @@ module.exports = connect((state) => { changeMapView, getFeatureInfo, changeMapInfoState, - purgeMapInfoResults + purgeMapInfoResults, + clickOnMap, + changeMousePointer }), dispatch); })(Viewer); diff --git a/web/client/examples/viewer/index.html b/web/client/examples/viewer/index.html index 798f92448d1..5ff7d7975be 100644 --- a/web/client/examples/viewer/index.html +++ b/web/client/examples/viewer/index.html @@ -58,6 +58,7 @@ position: absolute; margin: 8px; } + diff --git a/web/client/reducers/__tests__/config-test.js b/web/client/reducers/__tests__/config-test.js index 3811555dfbf..804cc1f87eb 100644 --- a/web/client/reducers/__tests__/config-test.js +++ b/web/client/reducers/__tests__/config-test.js @@ -54,4 +54,18 @@ describe('Test the mapConfig reducer', () => { expect(state.bbox).toBe(0); expect(state.size).toBe(0); }); + + it('sets a new mouse pointer used over the map', () => { + const action = { + type: 'CHANGE_MOUSE_POINTER', + pointer: "testPointer" + }; + + var state = mapConfig({}, action); + expect(state.mousePointer).toBe(action.pointer); + + state = mapConfig({prop: 'prop'}, action); + expect(state.prop).toBe('prop'); + expect(state.mousePointer).toBe(action.pointer); + }); }); diff --git a/web/client/reducers/__tests__/mapInfo-test.js b/web/client/reducers/__tests__/mapInfo-test.js index a04eafbf0e1..3e460428a7d 100644 --- a/web/client/reducers/__tests__/mapInfo-test.js +++ b/web/client/reducers/__tests__/mapInfo-test.js @@ -15,57 +15,98 @@ describe('Test the mapInfo reducer', () => { }); it('creates a general error ', () => { - let state = mapInfo({}, {type: 'ERROR_FEATURE_INFO', error: "error"}); + let testAction = { + type: 'ERROR_FEATURE_INFO', + error: "error", + requestParams: "params", + layerMetadata: "meta" + }; + + let state = mapInfo({}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(1); - expect(state.responses[0]).toBe("error"); + expect(state.responses[0].response).toBe("error"); + expect(state.responses[0].queryParams).toBe("params"); + expect(state.responses[0].layerMetadata).toBe("meta"); - state = mapInfo({responses: []}, {type: 'ERROR_FEATURE_INFO', error: "error"}); + state = mapInfo({responses: []}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(1); - expect(state.responses[0]).toBe("error"); + expect(state.responses[0].response).toBe("error"); + expect(state.responses[0].queryParams).toBe("params"); + expect(state.responses[0].layerMetadata).toBe("meta"); - state = mapInfo({responses: ["test"]}, {type: 'ERROR_FEATURE_INFO', error: "error"}); + state = mapInfo({responses: ["test"]}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(2); expect(state.responses[0]).toBe("test"); - expect(state.responses[1]).toBe("error"); + expect(state.responses[1].response).toBe("error"); + expect(state.responses[1].queryParams).toBe("params"); + expect(state.responses[1].layerMetadata).toBe("meta"); }); it('creates an wms feature info exception', () => { - let state = mapInfo({}, {type: 'EXCEPTIONS_FEATURE_INFO', exceptions: "exception"}); + let testAction = { + type: 'EXCEPTIONS_FEATURE_INFO', + exceptions: "exception", + requestParams: "params", + layerMetadata: "meta" + }; + + let state = mapInfo({}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(1); - expect(state.responses[0]).toBe("exception"); + expect(state.responses[0].response).toBe("exception"); + expect(state.responses[0].queryParams).toBe("params"); + expect(state.responses[0].layerMetadata).toBe("meta"); - state = mapInfo({responses: []}, {type: 'EXCEPTIONS_FEATURE_INFO', exceptions: "exception"}); + state = mapInfo({responses: []}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(1); - expect(state.responses[0]).toBe("exception"); + expect(state.responses[0].response).toBe("exception"); + expect(state.responses[0].queryParams).toBe("params"); + expect(state.responses[0].layerMetadata).toBe("meta"); - state = mapInfo({responses: ["test"]}, {type: 'EXCEPTIONS_FEATURE_INFO', exceptions: "exception"}); + + state = mapInfo({responses: ["test"]}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(2); expect(state.responses[0]).toBe("test"); - expect(state.responses[1]).toBe("exception"); + expect(state.responses[1].response).toBe("exception"); + expect(state.responses[1].queryParams).toBe("params"); + expect(state.responses[1].layerMetadata).toBe("meta"); + }); it('creates a feature info data from succesfull request', () => { - let state = mapInfo({}, {type: 'LOAD_FEATURE_INFO', data: "data"}); + let testAction = { + type: 'LOAD_FEATURE_INFO', + data: "data", + requestParams: "params", + layerMetadata: "meta" + }; + + let state = mapInfo({}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(1); - expect(state.responses[0]).toBe("data"); + expect(state.responses[0].response).toBe("data"); + expect(state.responses[0].queryParams).toBe("params"); + expect(state.responses[0].layerMetadata).toBe("meta"); - state = mapInfo({responses: []}, {type: 'LOAD_FEATURE_INFO', data: "data"}); + state = mapInfo({responses: []}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(1); - expect(state.responses[0]).toBe("data"); + expect(state.responses[0].response).toBe("data"); + expect(state.responses[0].queryParams).toBe("params"); + expect(state.responses[0].layerMetadata).toBe("meta"); - state = mapInfo({responses: ["test"]}, {type: 'LOAD_FEATURE_INFO', data: "data"}); + state = mapInfo({responses: ["test"]}, testAction); expect(state.responses).toExist(); expect(state.responses.length).toBe(2); expect(state.responses[0]).toBe("test"); - expect(state.responses[1]).toBe("data"); + expect(state.responses[1].response).toBe("data"); + expect(state.responses[1].queryParams).toBe("params"); + expect(state.responses[1].layerMetadata).toBe("meta"); }); it('creates a new mapinfo request', () => { @@ -100,6 +141,16 @@ describe('Test the mapInfo reducer', () => { expect(state.responses.length).toBe(0); }); + it('set a new point on map which has been clicked', () => { + let state = mapInfo({}, {type: 'CLICK_ON_MAP', point: "p"}); + expect(state.clickPoint).toExist(); + expect(state.clickPoint).toBe('p'); + + state = mapInfo({clickPoint: 'oldP'}, {type: 'CLICK_ON_MAP', point: "p"}); + expect(state.clickPoint).toExist(); + expect(state.clickPoint).toBe('p'); + }); + it('enables map info', () => { let state = mapInfo({}, {type: 'CHANGE_MAPINFO_STATE', enabled: true}); expect(state).toExist(); diff --git a/web/client/reducers/config.js b/web/client/reducers/config.js index 6ddf46c1901..ad646101c8b 100644 --- a/web/client/reducers/config.js +++ b/web/client/reducers/config.js @@ -7,7 +7,7 @@ */ var {MAP_CONFIG_LOADED, MAP_CONFIG_LOAD_ERROR} = require('../actions/config'); -var {CHANGE_MAP_VIEW} = require('../actions/map'); +var {CHANGE_MAP_VIEW, CHANGE_MOUSE_POINTER} = require('../actions/map'); var ConfigUtils = require('../utils/ConfigUtils'); var assign = require('object-assign'); @@ -27,6 +27,10 @@ function mapConfig(state = null, action) { bbox: action.bbox, size: action.size }); + case CHANGE_MOUSE_POINTER: + return assign({}, state, { + mousePointer: action.pointer + }); default: return state; } diff --git a/web/client/reducers/mapInfo.js b/web/client/reducers/mapInfo.js index 2118db2d151..49ffda815ba 100644 --- a/web/client/reducers/mapInfo.js +++ b/web/client/reducers/mapInfo.js @@ -6,14 +6,16 @@ * LICENSE file in the root directory of this source tree. */ +var {CLICK_ON_MAP} = require('../actions/map'); + var { + ERROR_FEATURE_INFO, + EXCEPTIONS_FEATURE_INFO, + LOAD_FEATURE_INFO, CHANGE_MAPINFO_STATE, NEW_MAPINFO_REQUEST, - LOAD_FEATURE_INFO, - EXCEPTIONS_FEATURE_INFO, - ERROR_FEATURE_INFO, PURGE_MAPINFO_RESULTS -} = require('../actions/map'); +} = require('../actions/mapInfo'); const assign = require('object-assign'); @@ -49,11 +51,16 @@ function mapInfo(state = null, action) { * else is a [string] (for eg. if HTML data has been requested) */ let newResponses; + let obj = { + response: action.data, + queryParams: action.requestParams, + layerMetadata: action.layerMetadata + }; if (state.responses) { newResponses = state.responses.slice(); - newResponses.push(action.data); + newResponses.push(obj); } else { - newResponses = [action.data]; + newResponses = [obj]; } return assign({}, state, { responses: newResponses @@ -68,11 +75,16 @@ function mapInfo(state = null, action) { * }, ...] */ let newResponses; + let obj = { + response: action.exceptions, + queryParams: action.requestParams, + layerMetadata: action.layerMetadata + }; if (state.responses) { newResponses = state.responses.slice(); - newResponses.push(action.exceptions); + newResponses.push(obj); } else { - newResponses = [action.exceptions]; + newResponses = [obj]; } return assign({}, state, { responses: newResponses @@ -89,16 +101,26 @@ function mapInfo(state = null, action) { * } */ let newResponses; + let obj = { + response: action.error, + queryParams: action.requestParams, + layerMetadata: action.layerMetadata + }; if (state.responses) { newResponses = state.responses.slice(); - newResponses.push(action.error); + newResponses.push(obj); } else { - newResponses = [action.error]; + newResponses = [obj]; } return assign({}, state, { responses: newResponses }); } + case CLICK_ON_MAP: { + return assign({}, state, { + clickPoint: action.point + }); + } default: return state; } diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index 764a14ee5fe..7b919f9820d 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -46,6 +46,7 @@ "maps_title": "Maps", "locales_combo": "Language:" }, - "getFeatureInfoTitle": "Feature Info" + "getFeatureInfoTitle": "Feature Info", + "noFeatureInfo": "There is no information available for the feature you clicked" } } diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index dbb7d323f09..ca25687c6ee 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -46,6 +46,7 @@ "maps_title": "Mappe", "locales_combo": "Lingua:" }, - "getFeatureInfoTitle": "Informazioni Sulle Feature" + "getFeatureInfoTitle": "Informazioni Sulle Feature", + "noFeatureInfo": "Non ci sono informazioni disponibili per la feature selezionata" } } diff --git a/web/client/utils/CoordinatesUtils.js b/web/client/utils/CoordinatesUtils.js index 844bb882592..271af2e1487 100644 --- a/web/client/utils/CoordinatesUtils.js +++ b/web/client/utils/CoordinatesUtils.js @@ -15,6 +15,30 @@ var CoordinatesUtils = { return assign({}, Proj4js.transform(sourceProj, destProj, Proj4js.toPoint(point)), {srs: dest}); }, + /** + * Reprojects a bounding box. + * + * @param bbox {array} [minx, miny, maxx, maxy] + * @param source {string} SRS of the given bbox + * @param dest {string} SRS of the returned bbox + * + * @return {array} [minx, miny, maxx, maxy] + */ + reprojectBbox: function(bbox, source, dest) { + let points = { + sw: [bbox[0], bbox[1]], + ne: [bbox[2], bbox[3]] + }; + let projPoints = []; + for (let p in points) { + if (points.hasOwnProperty(p)) { + let {x, y} = CoordinatesUtils.reproject(points[p], source, dest); + projPoints.push(x); + projPoints.push(y); + } + } + return projPoints; + }, normalizeSRS: function(srs) { return srs === 'EPSG:900913' ? 'EPSG:3857' : srs; } diff --git a/web/client/utils/__tests__/CoordinatesUtils-test.js b/web/client/utils/__tests__/CoordinatesUtils-test.js index 3e43dc394e6..53e87ad3cb1 100644 --- a/web/client/utils/__tests__/CoordinatesUtils-test.js +++ b/web/client/utils/__tests__/CoordinatesUtils-test.js @@ -28,4 +28,14 @@ describe('CoordinatesUtils', () => { expect(transformed.y).toNotBe(13); expect(transformed.srs).toBe('EPSG:900913'); }); + it('convert lat lon bbox to marcator bbox', () => { + var bbox = [44, 12, 45, 13]; + var projbbox = CoordinatesUtils.reprojectBbox(bbox, 'EPSG:4326', 'EPSG:900913'); + + expect(projbbox).toExist(); + expect(projbbox.length).toBe(4); + for (let i = 0; i < 4; i++) { + expect(projbbox[i]).toNotBe(bbox[i]); + } + }); }); diff --git a/webpack.config.js b/webpack.config.js index db9e3c12266..6091594062d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -48,6 +48,7 @@ module.exports = { target: "http://mapstore.geo-solutions.it" }, { path: new RegExp("/mapstore/proxy(.*)"), + rewrite: rewriteUrl("/http_proxy/proxy$1"), target: "http://localhost:8083" }] },