Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #1919. Implemented WFS download plugin #1942

Merged
merged 2 commits into from
Jun 21, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docma-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
"web/client/plugins/Search.jsx",
"web/client/plugins/SearchServicesConfig.jsx",
"web/client/plugins/TOC.jsx",
"web/client/plugins/WFSDownload.jsx",
"web/client/plugins/ZoomIn.jsx",
"web/client/plugins/ZoomOut.jsx"
]
Expand Down
39 changes: 39 additions & 0 deletions web/client/actions/__tests__/wfsdownload-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

var expect = require('expect');
var {
DOWNLOAD_FEATURES,
DOWNLOAD_OPTIONS_CHANGE,
DOWNLOAD_FINISHED,
downloadFeatures,
onDownloadOptionChange,
onDownloadFinished
} = require('../wfsdownload');

describe('Test correctness of the wfsdownload actions', () => {
it('test downloadFeatures action', () => {
let {type, url, filterObj, downloadOptions} = downloadFeatures("url", "filterObj", "downloadOptions");
expect(type).toBe(DOWNLOAD_FEATURES);
expect(url).toBe("url");
expect(filterObj).toBe("filterObj");
expect(downloadOptions).toBe("downloadOptions");
});
it('test onDownloadOptionChange action', () => {
let {type, key, value} = onDownloadOptionChange("key", "value");
expect(type).toBe(DOWNLOAD_OPTIONS_CHANGE);
expect(key).toBe("key");
expect(value).toBe("value");
});
it('test onDownloadFinished action', () => {
let {type} = onDownloadFinished();
expect(type).toBe(DOWNLOAD_FINISHED);
});


});
57 changes: 57 additions & 0 deletions web/client/actions/wfsdownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

const DOWNLOAD_FEATURES = "WFSDOWNLOAD::DOWNLOAD_FEATURES";
const DOWNLOAD_FINISHED = "WFSDOWNLOAD::DOWNLOAD_FINISHED";
const DOWNLOAD_OPTIONS_CHANGE = "WFSDOWNLOAD::FORMAT_SELECTED";

/**
* Actions for WFS Download
* @memberof actions
* @name wfsdownload
* @type {Object}
*/
module.exports = {
DOWNLOAD_FEATURES,
DOWNLOAD_OPTIONS_CHANGE,
DOWNLOAD_FINISHED,
/**
* action to download features
* @memberof actions.wfsdownload
* @param {string} url the URL of WFSDownload
* @param {object} filterObj the object that represent the WFS filterObj
* @param {object} downloadOptions download options e.g. `{singlePage: true|false, selectedFormat: "csv"}`
* @return {action} The action of type `DOWNLOAD_FEATURES`
*/
downloadFeatures: (url, filterObj, downloadOptions) => ({
type: DOWNLOAD_FEATURES,
url,
filterObj,
downloadOptions
}),
/**
* action for change download options
* @memberof actions.wfsdownload
* @param {string} key the value key to change. e.g. selectedFormat
* @param {string|boolean} value the value of the option
* @return {action} the action of type `DOWNLOAD_OPTIONS_CHANGE`
*/
onDownloadOptionChange: (key, value) => ({
type: DOWNLOAD_OPTIONS_CHANGE,
key,
value
}),
/**
* action that notifies the end of the downloadOptions
* @memberof actions.wfsdownload
* @return {action} action of type `DOWNLOAD_FINISHED`
*/
onDownloadFinished: () => ({
type: DOWNLOAD_FINISHED
})
};
49 changes: 49 additions & 0 deletions web/client/components/data/download/DownloadOptions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const Select = require('react-select');
const {Checkbox} = require('react-bootstrap');
const {get, head} = require('lodash');
const Message = require('../../I18N/Message');
/**
* Download Options Form. Shows a selector of the options to perform a WFS download
* @memberof components.data.download
* @name DownloadOptions
* @class
* @prop {object} downloadOptions the options to set. e.g. `{singlePage: true|false, selectedFormat: "csv"}`
* @prop {array} formats the selectable format options.
* @prop {function} onChange the function to trigger when some option changes
*/
module.exports = React.createClass({
propTypes: {
downloadOptions: React.PropTypes.object,
formats: React.PropTypes.array,
onChange: React.PropTypes.func
},
getSelectedFormat() {
return get(this.props, "downloadOptions.selectedFormat") || get(head(this.props.formats), "value");
},
getDefaultProps() {
return {
downloadOptions: {}
};
},
render() {
return (<form>
<label><Message msgId="wfsdownload.format" /></label>
<Select
clearable={false}
value={this.getSelectedFormat()}
onChange={(sel) => this.props.onChange("selectedFormat", sel.value)}
options={this.props.formats.map(f => ({value: f.name, label: f.label || f.name}))} />
<Checkbox checked={this.props.downloadOptions.singlePage} onChange={() => this.props.onChange("singlePage", !this.props.downloadOptions.singlePage ) }>
<Message msgId="wfsdownload.downloadonlycurrentpage" />
</Checkbox>
</form>);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2017, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
var React = require('react');
var ReactDOM = require('react-dom');
var DownloadOptions = require('../DownloadOptions');
var expect = require('expect');
const spyOn = expect.spyOn;
const TestUtils = require('react-addons-test-utils');

describe('Test for DownloadOptions component', () => {
beforeEach((done) => {
document.body.innerHTML = '<div id="container"></div>';
setTimeout(done);
});

afterEach((done) => {
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
document.body.innerHTML = '';
setTimeout(done);
});
// test DEFAULTS
it('render with defaults', () => {
const cmp = ReactDOM.render(<DownloadOptions/>, document.getElementById("container"));
expect(cmp).toExist();
});
it('render with element selected', () => {
const cmp = ReactDOM.render(<DownloadOptions downloadOptions={{selectedFormat: "test"}} formats={[{name: "test"}]}/>, document.getElementById("container"));
expect(cmp).toExist();
expect(TestUtils.scryRenderedDOMComponentsWithClass(cmp, "Select-value-label")).toExist();
});
it('singlePage checkbox events', () => {
const events = {
onChange: () => {}
};
spyOn(events, "onChange");
ReactDOM.render(<DownloadOptions onChange={events.onChange} downloadOptions={{selectedFormat: "test"}} formats={[{name: "test"}]}/>, document.getElementById("container"));
const check = document.querySelector('input[type=checkbox]');
check.click();
expect(events.onChange).toHaveBeenCalled();

});
});
5 changes: 4 additions & 1 deletion web/client/components/data/featuregrid/DockedFeatureGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ const DockedFeatureGrid = React.createClass({
dockSize: React.PropTypes.number,
minDockSize: React.PropTypes.number,
maxDockSize: React.PropTypes.number,
setDockSize: React.PropTypes.func
setDockSize: React.PropTypes.func,
exportAction: React.PropTypes.func,
exportEnabled: React.PropTypes.bool
},
contextTypes: {
messages: React.PropTypes.object
Expand Down Expand Up @@ -251,6 +253,7 @@ const DockedFeatureGrid = React.createClass({
height: "100%"
}}>
<FeatureGrid
exportAction={this.props.exportEnabled && this.props.exportAction}
useIcons={true}
tools={[<Button onClick={this.props.onBackToSearch} ><Glyphicon glyph="arrow-left" /><I18N.Message msgId="featuregrid.backtosearch"/></Button>]}
key={"search-results-" + (this.state && this.state.searchN)}
Expand Down
Empty file.
68 changes: 68 additions & 0 deletions web/client/epics/wfsdownload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const {DOWNLOAD_FEATURES, onDownloadFinished} = require('../actions/wfsdownload');
const {error} = require('../actions/notifications');
const Rx = require('rxjs');
const {get} = require('lodash');
const {saveAs} = require('file-saver');
const axios = require('axios');
const FilterUtils = require('../utils/FilterUtils');
const {getByOutputFormat} = require('../utils/FileFormatUtils');

const getWFSFeature = ({url, filterObj = {}, downloadOptions= {}} = {}) => {
const data = FilterUtils.toOGCFilter(filterObj.featureTypeName, filterObj, filterObj.ogcVersion);
return Rx.Observable.defer( () =>
axios.post(url + `?service=WFS&outputFormat=${downloadOptions.selectedFormat}`, data, {
timeout: 60000,
responseType: 'arraybuffer',
headers: {'Content-Type': 'application/xml'}
}));
};
const getFileName = action => {
const name = get(action, "filterObj.featureTypeName");
const format = getByOutputFormat(get(action, "downloadOptions.selectedFormat"));
if (format && format.extension) {
return name + "." + format.extension;
}
return name;
};
/*
const str2bytes = (str) => {
var bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes;
};
*/
module.exports = {
startFeatureExportDownload: action$ =>
action$.ofType(DOWNLOAD_FEATURES).switchMap(action =>
getWFSFeature({
url: action.url,
downloadOptions: action.downloadOptions,
filterObj: {
...action.filterObj,
pagination: get(action, "downloadOptions.singlePage") ? action.filterObj.pagination : null
}
})
.do(({data, headers}) => {
if (headers["content-type"] === "application/xml") { // TODO add expected mimetypes in the case you want application/dxf
let xml = String.fromCharCode.apply(null, new Uint8Array(data));
if (xml.indexOf("<ows:ExceptionReport") === 0 ) {
throw xml;
}
}
saveAs(new Blob([data], {type: headers && headers["content-type"]}), getFileName(action));
})
.map( () => onDownloadFinished() )
.catch( () => Rx.Observable.of(
error({
title: "wfsdownload.error.title",
message: "wfsdownload.error.invalidOutputFormat",
autoDismiss: 5,
position: "tr"
}),
onDownloadFinished())
)

)
};
2 changes: 1 addition & 1 deletion web/client/localConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
"alwaysVisible": true
}
}
}, "Home", "FeatureGrid", {
}, "Home", "FeatureGrid", "WFSDownload", {
"name": "TOC",
"cfg": {
"activateQueryTool": true
Expand Down
3 changes: 3 additions & 0 deletions web/client/plugins/FeatureGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ const {connect} = require('react-redux');
const {selectFeatures, dockSizeFeatures} = require('../actions/featuregrid');
const {query, closeResponse} = require('../actions/wfsquery');
const {changeMapView} = require('../actions/map');
const {toggleControl} = require('../actions/controls');

module.exports = {
FeatureGridPlugin: connect((state) => ({
open: state.query && state.query.open,
exportEnabled: state && state.controls && state.controls.wfsdownload && state.controls.wfsdownload.avaliable,
features: state.query && state.query.result && state.query.result.features,
filterObj: state.query && state.query.filterObj,
searchUrl: state.query && state.query.searchUrl,
Expand All @@ -34,6 +36,7 @@ module.exports = {
}),
{
selectFeatures,
exportAction: () => toggleControl("wfsdownload"),
changeMapView,
onQuery: query,
onBackToSearch: closeResponse,
Expand Down
Loading