diff --git a/package.json b/package.json index 224ddd0f79..84b44f4b9d 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "react-motion": "0.4.4", "react-router": "2.4.0", "react-router-redux": "2.1.0", - "react-select": "1.0.0-beta14", + "react-select": "1.0.0-rc.1", "react-transform-catch-errors": "1.0.2", "redbox-react": "1.2.4", "redux-devtools": "3.1.1", diff --git a/web/client/actions/__tests__/layers-test.js b/web/client/actions/__tests__/layers-test.js index 937d5f4d8f..3fc4c94c24 100644 --- a/web/client/actions/__tests__/layers-test.js +++ b/web/client/actions/__tests__/layers-test.js @@ -33,7 +33,8 @@ var { removeLayer, showSettings, hideSettings, - updateSettings + updateSettings, + getLayerCapabilities } = require('../layers'); describe('Test correctness of the layers actions', () => { @@ -167,4 +168,25 @@ describe('Test correctness of the layers actions', () => { expect(action.type).toBe(UPDATE_SETTINGS); expect(action.options).toEqual({opacity: 0.5, size: 500}); }); + it('get layer capabilities', (done) => { + const layer = { + id: "TEST_ID", + name: 'testworkspace:testlayer', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'shapefile', + url: 'base/web/client/test-resources/geoserver/wms' + }; + const actionCall = getLayerCapabilities(layer); + expect(actionCall).toExist(); + actionCall((action)=> { + expect(action).toExist(); + expect(action.options).toExist(); + expect(action.type === UPDATE_NODE); + if (action.options.capabilities) { + done(); + } + }); + }); }); diff --git a/web/client/actions/catalog.js b/web/client/actions/catalog.js index 0f9c96f602..ce7a49045d 100644 --- a/web/client/actions/catalog.js +++ b/web/client/actions/catalog.js @@ -16,7 +16,6 @@ const RECORD_LIST_LOAD_ERROR = 'RECORD_LIST_LOAD_ERROR'; const CHANGE_CATALOG_FORMAT = 'CHANGE_CATALOG_FORMAT'; const ADD_LAYER_ERROR = 'ADD_LAYER_ERROR'; const CATALOG_RESET = 'CATALOG_RESET'; - function recordsLoaded(options, result) { return { type: RECORD_LIST_LOADED, diff --git a/web/client/actions/layers.js b/web/client/actions/layers.js index 4e1dedeea1..7f6358e381 100644 --- a/web/client/actions/layers.js +++ b/web/client/actions/layers.js @@ -185,7 +185,7 @@ function getDescribeLayer(url, layer, options) { } function getLayerCapabilities(layer, options) { - // geoserver's specific. + // geoserver's specific. TODO parse layer.capabilitiesURL. let reqUrl = layer.url; let urlParts = reqUrl.split("/geoserver/"); if (urlParts.length === 2) { @@ -193,18 +193,20 @@ function getLayerCapabilities(layer, options) { if (layerParts.length === 2) { reqUrl = urlParts[0] + "/geoserver/" + layerParts [0] + "/" + layerParts[1] + "/" + urlParts[1]; } - } return (dispatch) => { // TODO, look ad current cached capabilities; + dispatch(updateNode(layer.id, "id", { + capabilitiesLoading: true + })); return WMS.getCapabilities(reqUrl, options).then((capabilities) => { let layers = _.get(capabilities, "capability.layer.layer"); let layerCapability; layerCapability = _.head(layers.filter( ( capability ) => { - if (layer.name.split(":").length === 2 && capability.name.split(":").length === 2 ) { + if (layer.name.split(":").length === 2 && capability.name && capability.name.split(":").length === 2 ) { return layer.name === capability.name; - } else if (capability.name.split(":").length === 2) { + } else if (capability.name && capability.name.split(":").length === 2) { return (layer.name === capability.name.split(":")[1]); } else if (layer.name.split(":").length === 2) { return layer.name.split(":")[1] === capability.name; @@ -212,12 +214,17 @@ function getLayerCapabilities(layer, options) { return layer.name === capability.name; })); if (layerCapability) { - dispatch(updateNode(layer.id, "id", {capabilities: layerCapability, boundingBox: layerCapability.latLonBoundingBox})); + dispatch(updateNode(layer.id, "id", { + capabilities: layerCapability, + capabilitiesLoading: null, + boundingBox: layerCapability.latLonBoundingBox, + availableStyles: layerCapability.style && (Array.isArray(layerCapability.style) ? layerCapability.style : [layerCapability.style]) + })); } // return dispatch(updateNode(layer.id, "id", {capabilities: capabilities || {"error": "no describe Layer found"}})); }).catch((error) => { - dispatch(updateNode(layer.id, "id", {capabilities: {error: "error getting capabilities", details: error}} )); + dispatch(updateNode(layer.id, "id", {capabilitiesLoading: null, capabilities: {error: "error getting capabilities", details: error}} )); // return dispatch(updateNode(layer.id, "id", {capabilities: capabilities || {"error": "no describe Layer found"}})); diff --git a/web/client/api/WMS.js b/web/client/api/WMS.js index 49cf4b032f..095fb91c93 100644 --- a/web/client/api/WMS.js +++ b/web/client/api/WMS.js @@ -67,7 +67,6 @@ const Api = { resolve(axios.get(parseUrl(getCapabilitiesUrl)).then((response) => { let json = unmarshaller.unmarshalString(response.data); return json && json.value; - })); }); }); diff --git a/web/client/components/TOC/DefaultLayer.jsx b/web/client/components/TOC/DefaultLayer.jsx index c9b3d8559e..25767f74af 100644 --- a/web/client/components/TOC/DefaultLayer.jsx +++ b/web/client/components/TOC/DefaultLayer.jsx @@ -23,6 +23,7 @@ var DefaultLayer = React.createClass({ node: React.PropTypes.object, settings: React.PropTypes.object, propertiesChangeHandler: React.PropTypes.func, + retrieveLayerData: React.PropTypes.func, onToggle: React.PropTypes.func, onZoom: React.PropTypes.func, onSettings: React.PropTypes.func, @@ -56,6 +57,7 @@ var DefaultLayer = React.createClass({ onToggle: () => {}, onZoom: () => {}, onSettings: () => {}, + retrieveLayerData: () => {}, activateRemoveLayer: false, activateLegendTool: false, activateSettingsTool: false, @@ -106,6 +108,7 @@ var DefaultLayer = React.createClass({ tools.push( {}, updateNode: () => {}, removeNode: () => {}, + retrieveLayerData: () => {}, asModal: true, buttonSize: "large", closeGlyph: "", panelStyle: { minWidth: "300px", - zIndex: 100, + zIndex: 2000, position: "absolute", // overflow: "auto", top: "100px", @@ -98,22 +101,39 @@ const SettingsModal = React.createClass({ ); this.props.hideSettings(); }, - render() { - const general = (); - const display = ( this.updateParams({[key]: value}, this.props.realtimeUpdate)} />); + }, + renderDisplay() { + return ( this.updateParams({[key]: value}, this.props.realtimeUpdate)} />); + }, + renderStyleTab() { + if (this.props.element.type === "wms") { + return (); + } + }, + render() { + const general = this.renderGeneral(); + const display = this.renderDisplay(); + const style = this.renderStyleTab(); const tabs = ( }>{general} }>{display} - } disabled>Tab 3 content + } disabled={!style} >{style} ); const footer = ( {this.props.includeCloseButton ? : } diff --git a/web/client/components/TOC/fragments/settings/WMSStyle.jsx b/web/client/components/TOC/fragments/settings/WMSStyle.jsx new file mode 100644 index 0000000000..37ead5a8ce --- /dev/null +++ b/web/client/components/TOC/fragments/settings/WMSStyle.jsx @@ -0,0 +1,82 @@ +/** + * 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 React = require('react'); +const Message = require('../../../I18N/Message'); +const Select = require('react-select'); +const {Button, Glyphicon, Alert} = require('react-bootstrap'); + +/** + * General Settings form for layer + */ +const WMSStyle = React.createClass({ + propTypes: { + retrieveLayerData: React.PropTypes.func, + updateSettings: React.PropTypes.func, + element: React.PropTypes.object, + groups: React.PropTypes.array + }, + getDefaultProps() { + return { + element: {}, + retrieveLayerData: () => {}, + updateSettings: () => {} + }; + }, + renderLegend() { + // legend can not added because of this issue + // https://github.com/highsource/ogc-schemas/issues/183 + return null; + }, + renderError() { + if (this.props.element && this.props.element.capabilities && this.props.element && this.props.element.capabilities.error) { + return ; + } + }, + render() { + let options = [{label: "Default Style", value: ""}].concat((this.props.element.availableStyles || []).map((item) => { + return {label: item.title || item.name, value: item.name}; + })); + let currentStyleIndex = this.props.element.style && this.props.element.availableStyles && this.props.element.availableStyles.findIndex( el => el.name === this.props.element.style); + if (!(currentStyleIndex >= 0) && this.props.element.style) { + options.push({label: this.props.element.style, value: this.props.element.style }); + } + return (
+ { + // automatic retrieve if availableStyles are not available or capabilities is not present + // that means you don't have a list and you didn't try to load it. + if (this.props.element && !(this.props.element.capabilities && this.props.element.availableStyles)) { + this.props.retrieveLayerData(this.props.element); + } + }} + promptTextCreator={(value) => { + return ; + }} + onChange={(selected) => { + this.updateEntry("style", {target: {value: (selected && selected.value) || ""}}); + }} + /> +
+ {this.renderLegend()} + {this.renderError()} + +
+ ); + }, + updateEntry(key, event) { + let value = event.target.value; + this.props.updateSettings({[key]: value}); + } +}); + +module.exports = WMSStyle; diff --git a/web/client/components/TOC/fragments/settings/__tests__/WMSStyle-test.jsx b/web/client/components/TOC/fragments/settings/__tests__/WMSStyle-test.jsx new file mode 100644 index 0000000000..907e066abd --- /dev/null +++ b/web/client/components/TOC/fragments/settings/__tests__/WMSStyle-test.jsx @@ -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. + */ + +var React = require('react/addons'); +var ReactDOM = require('react-dom'); +var ReactTestUtils = require('react-addons-test-utils'); +var WMSStyle = require('../WMSStyle'); + +var expect = require('expect'); + +describe('test Layer Properties General module component', () => { + beforeEach((done) => { + document.body.innerHTML = '
'; + setTimeout(done); + }); + + afterEach((done) => { + ReactDOM.unmountComponentAtNode(document.getElementById("container")); + document.body.innerHTML = ''; + setTimeout(done); + }); + + it('tests component rendering', () => { + const l = { + name: 'testworkspace:testlayer', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'shapefile', + url: 'base/web/client/test-resources/geoserver/wms' + }; + const settings = { + options: {opacity: 1} + }; + + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + const form = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "form" ); + expect(form).toExist(); + + }); + it('tests component events', () => { + const l = { + name: 'testworkspace:testlayer', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'base/web/client/test-resources/geoserver/wms', + availableStyles: [{name: 'style1'}] + + }; + const settings = { + options: {opacity: 1} + }; + const handlers = { + retrieveLayerData: () => {}, + updateSettings: () => {} + }; + let spyRetrive = expect.spyOn(handlers, "retrieveLayerData"); + let spyUpdate = expect.spyOn(handlers, "updateSettings"); + + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + // refresh layers list button click + const buttons = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "button" ); + expect(buttons).toExist(); + expect(buttons.length).toBe(1); + ReactTestUtils.Simulate.click(buttons[0]); + expect(spyRetrive.calls.length).toBe(1); + + // Simpulate selection + const selectArrow = ReactDOM.findDOMNode(comp).querySelector('.Select-arrow'); + const selectControl = ReactDOM.findDOMNode(comp).querySelector('.Select-control'); + const inputs = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "input" ); + ReactTestUtils.Simulate.mouseDown(selectArrow, { button: 0 }); + ReactTestUtils.Simulate.keyDown(selectControl, { keyCode: 40, key: 'ArrowDown' }); + ReactTestUtils.Simulate.keyDown(inputs[0], { keyCode: 13, key: 'Enter' }); + expect(spyUpdate.calls.length).toBe(1); + + // click on arrow of the select auto try to retrieve data if not present + const l2 = { + name: 'testworkspace:testlayer', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'wms', + url: 'base/web/client/test-resources/geoserver/wms' + }; + const comp1 = ReactDOM.render(, document.getElementById("container")); + const selectArrow1 = ReactDOM.findDOMNode(comp1).querySelector('.Select-arrow'); + ReactTestUtils.Simulate.click(selectArrow1); + expect(spyRetrive.calls.length).toBe(2); + }); + it('tests rendering error', () => { + const l = { + name: 'testworkspace:testlayer', + title: 'Layer', + visibility: true, + storeIndex: 9, + type: 'shapefile', + url: 'base/web/client/test-resources/geoserver/wms', + capabilities: { + error: "unable to retrieve capabiltiies" + } + }; + const settings = { + options: {opacity: 1} + }; + const comp = ReactDOM.render(, document.getElementById("container")); + expect(comp).toExist(); + const form = ReactTestUtils.scryRenderedDOMComponentsWithTag( comp, "form" ); + expect(form).toExist(); + + }); + +}); diff --git a/web/client/components/catalog/RecordItem.jsx b/web/client/components/catalog/RecordItem.jsx index e4b2e1f31d..0431f6c149 100644 --- a/web/client/components/catalog/RecordItem.jsx +++ b/web/client/components/catalog/RecordItem.jsx @@ -83,12 +83,14 @@ const RecordItem = React.createClass({ let links = []; if (wmsGetCap) { links.push({ + type: "WMS_GET_CAPABILITIES", url: wmsGetCap.url, labelId: 'catalog.wmsGetCapLink' }); } if (wfsGetCap) { links.push({ + type: "WFS_GET_CAPABILITIES", url: wfsGetCap.url, labelId: 'catalog.wfsGetCapLink' }); diff --git a/web/client/plugins/Save.jsx b/web/client/plugins/Save.jsx index 26cd5b3145..809db226b8 100644 --- a/web/client/plugins/Save.jsx +++ b/web/client/plugins/Save.jsx @@ -92,6 +92,9 @@ const Save = React.createClass({ opacity: layer.opacity, provider: layer.provider, styles: layer.styles, + style: layer.style, + availableStyles: layer.availableStyles, + capabilitiesURL: layer.capabilitiesURL, title: layer.title, transparent: layer.transparent, type: layer.type, diff --git a/web/client/plugins/SaveAs.jsx b/web/client/plugins/SaveAs.jsx index 56873414ee..d908c4c4c6 100644 --- a/web/client/plugins/SaveAs.jsx +++ b/web/client/plugins/SaveAs.jsx @@ -124,6 +124,9 @@ const SaveAs = React.createClass({ opacity: layer.opacity, provider: layer.provider, styles: layer.styles, + style: layer.style, + availableStyles: layer.availableStyles, + capabilitiesURL: layer.capabilitiesURL, title: layer.title, transparent: layer.transparent, type: layer.type, diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index 69f376dfa8..d6314b6dae 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -9,7 +9,7 @@ const React = require('react'); const {connect} = require('react-redux'); const {createSelector} = require('reselect'); const {changeLayerProperties, changeGroupProperties, toggleNode, - sortNode, showSettings, hideSettings, updateSettings, updateNode, removeNode} = require('../actions/layers'); + sortNode, showSettings, hideSettings, updateSettings, updateNode, removeNode, getLayerCapabilities} = require('../actions/layers'); const {zoomToExtent} = require('../actions/map'); const {groupsSelector} = require('../selectors/layers'); @@ -49,6 +49,7 @@ const LayerTree = React.createClass({ onToggleGroup: React.PropTypes.func, onToggleLayer: React.PropTypes.func, onZoomToExtent: React.PropTypes.func, + retrieveLayerData: React.PropTypes.func, onSort: React.PropTypes.func, onSettings: React.PropTypes.func, hideSettings: React.PropTypes.func, @@ -66,6 +67,7 @@ const LayerTree = React.createClass({ return { groupPropertiesChangeHandler: () => {}, layerPropertiesChangeHandler: () => {}, + retrieveLayerData: () => {}, onToggleGroup: () => {}, onToggleLayer: () => {}, onZoomToExtent: () => {}, @@ -115,6 +117,7 @@ const LayerTree = React.createClass({ activateLegendTool={this.props.activateLegendTool} activateZoomTool={this.props.activateZoomTool} activateSettingsTool={this.props.activateSettingsTool} + retrieveLayerData={this.props.retrieveLayerData} settingsText={} opacityText={} saveText={} @@ -130,6 +133,7 @@ const LayerTree = React.createClass({ const TOCPlugin = connect(tocSelector, { groupPropertiesChangeHandler: changeGroupProperties, layerPropertiesChangeHandler: changeLayerProperties, + retrieveLayerData: getLayerCapabilities, onToggleGroup: LayersUtils.toggleByType('groups', toggleNode), onToggleLayer: LayersUtils.toggleByType('layers', toggleNode), onSort: LayersUtils.sortUsing(LayersUtils.sortLayers, sortNode), diff --git a/web/client/test-resources/geoserver/testworkspace/testlayer/wms b/web/client/test-resources/geoserver/testworkspace/testlayer/wms new file mode 100644 index 0000000000..8dd5389112 --- /dev/null +++ b/web/client/test-resources/geoserver/testworkspace/testlayer/wms @@ -0,0 +1,165 @@ + + + + + OGC:WMS + GeoServer Web Map Service + A compliant implementation of WMS plus most of the SLD extension (dynamic styling). Can also generate PDF, SVG, KML, GeoRSS + + WFS + WMS + GEOSERVER + + + + + Claudius Ptolomaeus + The ancient geographes INC + + Chief geographer + + Work +
+ Alexandria + + + Egypt + + + + claudius.ptolomaeus@gmail.com + + NONE + NONE + + + + + application/vnd.ogc.wms_xml + + + + + + + + + + + + + image/png + application/openlayers + image/jpeg + image/png8 + image/png; mode=8bit + openlayers + text/html; subtype=openlayers + + + + + + + + + + text/plain + application/vnd.ogc.gml + text/xml + application/vnd.ogc.gml/3.1.1 + text/xml; subtype=gml/3.1.1 + text/html + application/json + + + + + + + + + + + + + application/vnd.ogc.wms_xml + + + + + + + + + + image/png + image/jpeg + image/gif + + + + + + + + + + application/vnd.ogc.sld+xml + + + + + + + + + + + application/vnd.ogc.se_xml + application/vnd.ogc.se_inimage + application/vnd.ogc.se_blank + application/json + + + + GeoServer Web Map Service + A compliant implementation of WMS plus most of the SLD extension (dynamic styling). Can also generate PDF, SVG, KML, GeoRSS + + EPSG:3857 + EPSG:4326 + EPSG:900913 + + + testlayer + Test Layer + + + testlayer + features + + EPSG:4326 + + + + + + + + diff --git a/web/client/translations/data.de-DE b/web/client/translations/data.de-DE index c9250b0ca5..49986958a3 100644 --- a/web/client/translations/data.de-DE +++ b/web/client/translations/data.de-DE @@ -28,6 +28,9 @@ "general": "General", "display": "Display", "style": "Style", + "styleCustom": "Use style named \"{value}\"", + "styleListLoadError": "There was an error loading the styles list", + "stylesRefreshList": "Refresh Styles List", "format": "Format", "delete": "Delete", "deleteLayer":"Delete Layer", diff --git a/web/client/translations/data.en-US b/web/client/translations/data.en-US index c9250b0ca5..49986958a3 100644 --- a/web/client/translations/data.en-US +++ b/web/client/translations/data.en-US @@ -28,6 +28,9 @@ "general": "General", "display": "Display", "style": "Style", + "styleCustom": "Use style named \"{value}\"", + "styleListLoadError": "There was an error loading the styles list", + "stylesRefreshList": "Refresh Styles List", "format": "Format", "delete": "Delete", "deleteLayer":"Delete Layer", diff --git a/web/client/translations/data.fr-FR b/web/client/translations/data.fr-FR index adb777a5d3..9e38ebcae5 100644 --- a/web/client/translations/data.fr-FR +++ b/web/client/translations/data.fr-FR @@ -28,6 +28,9 @@ "general": "Général", "display": "Affichage", "style": "Style", + "styleCustom": "Utilisez un style nommé \"{value} \"", + "styleListLoadError": "Une erreur s'est produite lors du chargement de la liste de styles", + "stylesRefreshList": "Actualiser la liste des styles", "format": "Format", "delete": "Effacer", "deleteLayer":"Supprimer le calque", diff --git a/web/client/translations/data.it-IT b/web/client/translations/data.it-IT index cbe5a38d08..fc2eb5d67c 100644 --- a/web/client/translations/data.it-IT +++ b/web/client/translations/data.it-IT @@ -28,6 +28,9 @@ "general": "Generale", "display": "Visualizzazione", "style": "Stile", + "styleCustom": "Usa stile con nome \"{value}\"", + "styleListLoadError": "Si è verificato un errore durante il caricamento della lista degli stili", + "stylesRefreshList": "Aggiorna lista degli stili", "format": "Formato", "delete": "Elimina", "deleteLayer":"Elimina Livello",