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

#1945 Make FeatureGrid attributes visibility selector counted when user exports grid data #7845

15 changes: 11 additions & 4 deletions web/client/components/data/download/DownloadDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Message from '../../I18N/Message';
import EmptyView from '../../misc/EmptyView';
import DownloadOptions from './DownloadOptions';
import Button from '../../misc/Button';
import {getAttributesList} from "../../../utils/FeatureGridUtils";

class DownloadDialog extends React.Component {
static propTypes = {
Expand All @@ -43,7 +44,9 @@ class DownloadDialog extends React.Component {
defaultSrs: PropTypes.string,
layer: PropTypes.object,
formatsLoading: PropTypes.bool,
virtualScroll: PropTypes.bool
virtualScroll: PropTypes.bool,
customAttributeSettings: PropTypes.object,
attributes: PropTypes.array
};

static defaultProps = {
Expand Down Expand Up @@ -127,7 +130,10 @@ class DownloadDialog extends React.Component {
wpsAdvancedOptionsVisible={!this.props.layer.search?.url}
downloadFilteredVisible={!!this.props.layer.search?.url}
layer={this.props.layer}
virtualScroll={this.props.virtualScroll}/>}
virtualScroll={this.props.virtualScroll}
customAttributesSettings={this.props.customAttributeSettings}
attributes={this.props.attributes}
/>}
</div>
{!this.props.checkingWPSAvailability && <div role="footer">
<Button
Expand All @@ -141,9 +147,10 @@ class DownloadDialog extends React.Component {
</Dialog></Portal>) : null;
}
handleExport = () => {
const {url, filterObj, downloadOptions, defaultSrs, srsList, onExport, layer} = this.props;
const {url, filterObj, downloadOptions, defaultSrs, srsList, onExport, layer, attributes, customAttributeSettings} = this.props;
const selectedSrs = downloadOptions && downloadOptions.selectedSrs || defaultSrs || (srsList[0] || {}).name;
onExport(url || layer.url, filterObj, assign({}, downloadOptions, {selectedSrs}));
const propertyName = getAttributesList(attributes, customAttributeSettings);
onExport(url || layer.url, filterObj, assign({}, downloadOptions, {selectedSrs}, {propertyName}));
}
}

Expand Down
7 changes: 4 additions & 3 deletions web/client/components/data/download/FeatureEditorButton.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import Button from '../featuregrid/toolbars/TButton';
import withHint from "../featuregrid/enhancers/withHint";
import TButtonComp from "../featuregrid/toolbars/TButton";
const TButton = withHint(TButtonComp);
MV88 marked this conversation as resolved.
Show resolved Hide resolved


export default ({disabled, results = [], mode, isDownloadOpen, onClick = () => {}}) => <Button
export default ({disabled, results = [], mode, isDownloadOpen, onClick = () => {}}) => <TButton
id="download-grid"
keyProp="download-grid"
tooltipId="featuregrid.toolbar.downloadGridData"
Expand Down
28 changes: 21 additions & 7 deletions web/client/epics/layerdownload.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ import { queryPanelSelector, wfsDownloadSelector } from '../selectors/controls';
import { getSelectedLayer } from '../selectors/layers';
import { currentLocaleSelector } from '../selectors/locale';
import { mapBboxSelector } from '../selectors/map';
import { layerDescribeSelector } from "../selectors/query";
import {
isLoggedIn,
userSelector
} from '../selectors/security';

import { getLayerWFSCapabilities, getXMLFeature } from '../observables/wfs';
import { describeProcess } from '../observables/wps/describe';
import { download } from '../observables/wps/download';
import { download, downloadWithAttributesFilter } from '../observables/wps/download';
import { referenceOutputExtractor, makeOutputsExtractor, getExecutionStatus } from '../observables/wps/execute';

import { mergeFiltersToOGC } from '../utils/FilterUtils';
Expand All @@ -72,6 +73,7 @@ import { getLayerTitle } from '../utils/LayersUtils';
import { bboxToFeatureGeometry } from '../utils/CoordinatesUtils';
import { interceptOGCError } from '../utils/ObservableUtils';
import requestBuilder from '../utils/ogc/WFS/RequestBuilder';
import {extractGeometryAttributeName} from "../utils/WFSLayerUtils";

const DOWNLOAD_FORMATS_LOOKUP = {
"gml3": "GML3.1",
Expand Down Expand Up @@ -106,12 +108,12 @@ const hasOutputFormat = (data) => {
};

const getWFSFeature = ({ url, filterObj = {}, layerFilter, downloadOptions = {}, options } = {}) => {
const { sortOptions, propertyName: pn } = options;
const { sortOptions, propertyNames } = options;

const data = mergeFiltersToOGC({ ogcVersion: '1.0.0', addXmlnsToRoot: true, xmlnsToAdd: ['xmlns:ogc="http://www.opengis.net/ogc"', 'xmlns:gml="http://www.opengis.net/gml"'] }, layerFilter, filterObj);

return getXMLFeature(url, getFilterFeature(query(
filterObj.featureTypeName, [...(sortOptions ? [sortBy(sortOptions.sortBy, sortOptions.sortOrder)] : []), ...(pn ? [propertyName(pn)] : []), ...(data ? castArray(data) : [])],
filterObj.featureTypeName, [...(sortOptions ? [sortBy(sortOptions.sortBy, sortOptions.sortOrder)] : []), ...(propertyNames ? [propertyName(propertyNames)] : []), ...(data ? castArray(data) : [])],
{ srsName: downloadOptions.selectedSrs })
), options, downloadOptions.selectedFormat);

Expand All @@ -131,6 +133,7 @@ const getDefaultSortOptions = (attribute) => {
const getFirstAttribute = (state)=> {
return state.query && state.query.featureTypes && state.query.featureTypes[state.query.typeName] && state.query.featureTypes[state.query.typeName].attributes && state.query.featureTypes[state.query.typeName].attributes[0] && state.query.featureTypes[state.query.typeName].attributes[0].attribute || null;
};

const wpsExecuteErrorToMessage = e => {
switch (e.code) {
case 'ProcessFailed': {
Expand Down Expand Up @@ -237,6 +240,10 @@ export const startFeatureExportDownload = (action$, store) =>
const layer = getSelectedLayer(state);
const mapBbox = mapBboxSelector(state);
const currentLocale = currentLocaleSelector(state);
const propertyNames = action.downloadOptions.propertyName ? [
extractGeometryAttributeName(layerDescribeSelector(state, layer.name)),
...action.downloadOptions.propertyName
] : null;

const { layerFilter } = layer;

Expand All @@ -246,7 +253,8 @@ export const startFeatureExportDownload = (action$, store) =>
filterObj: action.filterObj,
layerFilter,
options: {
pagination: !virtualScroll && get(action, "downloadOptions.singlePage") ? action.filterObj && action.filterObj.pagination : null
pagination: !virtualScroll && get(action, "downloadOptions.singlePage") ? action.filterObj && action.filterObj.pagination : null,
propertyNames
}
})
.do(({ data, headers }) => {
Expand All @@ -265,7 +273,9 @@ export const startFeatureExportDownload = (action$, store) =>
layerFilter,
options: {
pagination: !virtualScroll && get(action, "downloadOptions.singlePage") ? action.filterObj && action.filterObj.pagination : null,
sortOptions: getDefaultSortOptions(getFirstAttribute(store.getState()))
sortOptions: getDefaultSortOptions(getFirstAttribute(store.getState())),
propertyNames: action.downloadOptions.propertyName ? [...action.downloadOptions.propertyName,
extractGeometryAttributeName(layerDescribeSelector(state, layer.name))] : null
}
}).do(({ data, headers }) => {
if (headers["content-type"] === "application/xml") { // TODO add expected mimetypes in the case you want application/dxf
Expand All @@ -292,6 +302,7 @@ export const startFeatureExportDownload = (action$, store) =>
);

const wpsFlow = () => {
const isVectorLayer = !!layer.search?.url;
const cropToROI = action.downloadOptions.cropDataSet && !!mapBbox && !!mapBbox.bounds;
const wpsDownloadOptions = {
layerName: layer.name,
Expand Down Expand Up @@ -327,7 +338,8 @@ export const startFeatureExportDownload = (action$, store) =>
...(action.downloadOptions.quality ? {quality: action.downloadOptions.quality} : {})
} : {})
},
notifyDownloadEstimatorSuccess: true
notifyDownloadEstimatorSuccess: true,
attribute: propertyNames
};
const newResult = {
id: uuidv1(),
Expand All @@ -339,7 +351,9 @@ export const startFeatureExportDownload = (action$, store) =>
outputsExtractor: makeOutputsExtractor(referenceOutputExtractor)
};

return download(action.url, wpsDownloadOptions, wpsExecuteOptions)
const executor = isVectorLayer && propertyNames ? downloadWithAttributesFilter : download;

return executor(action.url, wpsDownloadOptions, wpsExecuteOptions)
.takeUntil(action$.ofType(REMOVE_EXPORT_DATA_RESULT).filter(({id}) => id === newResult.id).take(1))
.flatMap((data) => {
if (data === 'DownloadEstimatorSuccess') {
Expand Down
124 changes: 111 additions & 13 deletions web/client/observables/wps/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
* LICENSE file in the root directory of this source tree.
*/

import { Observable } from 'rxjs';
import { toPairs, keys, omit } from 'lodash';
import {Observable} from 'rxjs';
import {keys, omit, toPairs} from 'lodash';

import {
processParameter,
processData,
literalData,
complexData,
cdata,
processReference,
complexData,
downloadParameter,
literalData,
processData,
processOutput,
responseForm,
responseDocument,
processParameter,
processReference,
rawDataOutput,
writingParametersData,
downloadParameter
responseDocument,
responseForm,
writingParametersData
} from './common';
import { literalDataOutputExtractor, makeOutputsExtractor, executeProcess, executeProcessXML } from './execute';
import {executeProcess, executeProcessXML, literalDataOutputExtractor, makeOutputsExtractor} from './execute';

/**
* Contains routines to work with GeoServer WPS download community module processes
Expand Down Expand Up @@ -99,6 +99,33 @@ export const downloadXML = ({layerName, dataFilter, outputFormat, targetCRS, roi
)
);

/**
* Construct gs:Query XML payload
* @memberof observables.wps.download
* @param {string} downloadOptions options object
* @param {string} downloadOptions.input WPS process reference object
* @param {string} downloadOptions.attribute comma-separated list of attributes to include in output
* @param {object} [downloadOptions.filter] object to use as filter
* @param {boolean} [downloadOptions.asynchronous] if true gs:Download will run asynchronously
* @param {boolean} [downloadOptions.outputAsReference] instructs process to return a link where output file can be downloaded instead of the file itself
* @param {string} [downloadOptions.resultOutput] MIME type of the output
*/
export const queryXML = ({input, attribute, filter, asynchronous, outputAsReference, resultOutput}) => executeProcessXML(
'vec:Query',
[
processParameter('features', processReference(input.mimeType, input.href.replace(/&/g, "&amp;"), 'GET')),
...(attribute ? attribute.map(attr => processParameter('attribute', processData(literalData(attr)))) : []),
...(filter ? [processParameter('filter', roiOrFilterToXML(filter))] : [])
],
responseForm(!asynchronous ?
rawDataOutput('result', resultOutput) :
responseDocument(true, true, outputAsReference ?
processOutput(resultOutput, true, 'result') :
rawDataOutput('result', resultOutput)
)
)
);

/**
* Execute gs:Download process, running gs:DownloadEstimator first
* @memberof observables.wps.download
Expand All @@ -122,7 +149,7 @@ export const download = (url, downloadOptions, executeOptions) => {
const resultOutput = downloadOptions.resultOutput || downloadOptions.outputFormat || 'application/zip';

const executeProcess$ = executeProcess(url, downloadXML({
...omit(downloadOptions, 'notifyDownloadEstimatorSuccess'),
...omit(downloadOptions, 'notifyDownloadEstimatorSuccess', 'attribute'),
outputAsReference: downloadOptions.asynchronous ? downloadOptions.outputAsReference : false,
resultOutput
}), executeOptions, {headers: {'Content-Type': 'application/xml', 'Accept': `application/xml, ${resultOutput}`}});
Expand All @@ -145,3 +172,74 @@ export const download = (url, downloadOptions, executeOptions) => {

return Observable.empty();
};


/**
* Execute gs:Download process and passes results to gs:Query, running gs:DownloadEstimator first
* @memberof observables.wps.download
* @param {string} url target url
* @param {object} downloadOptions options to use to construct payload xml for DownloadEstimator and Download processes(except notifyDownloadEstimatorSuccess). See {@link api/framework#observables.wps.download.exports.downloadXML|downloadXML}
* @param {boolean} downloadOptions.notifyDownloadEstimatorSuccess if true, the returned observable emits 'DownloadEstimatorSuccess' string after successful gs:DownloadEstimator run
* @param {object} executeOptions options to pass to executeProcess. See {@link api/framework#observables.wps.execute.exports.executeProcess|executeProcess}
*/
export const downloadWithAttributesFilter = (url, downloadOptions, executeOptions) => {
if (url && downloadOptions) {
const downloadEstimator$ = executeProcess(url, downloadEstimatorXML({
layerName: downloadOptions.layerName,
ROI: downloadOptions.ROI,
roiCRS: downloadOptions.roiCRS,
dataFilter: downloadOptions.dataFilter,
targetCRS: downloadOptions.targetCRS
}), {outputsExtractor: makeOutputsExtractor(literalDataOutputExtractor)});

// use the same format of outputFormat for result
// if resultOutput param is undefined
const resultOutput = downloadOptions.resultOutput || downloadOptions.outputFormat || 'application/zip';

const executeProcess$ = executeProcess(url, downloadXML({
...omit(downloadOptions, 'notifyDownloadEstimatorSuccess', 'attribute', 'asynchronous', 'outputFormat'),
asynchronous: true,
outputAsReference: true,
resultOutput: 'application/wfs-collection-1.0',
outputFormat: 'application/wfs-collection-1.0'
}), executeOptions, {headers: {'Content-Type': 'application/xml', 'Accept': `application/xml, application/wfs-collection-1.0`}});

return downloadEstimator$
.catch(() => {
throw new Error('DownloadEstimatorException');
})
.mergeMap((estimatorResult = []) => {
if (estimatorResult.length > 0 && estimatorResult[0].identifier === 'result' && estimatorResult[0].data === 'true') {
if (downloadOptions.notifyDownloadEstimatorSuccess) {
return Observable.of('DownloadEstimatorSuccess').concat(executeProcess$);
}
return executeProcess$;
}

throw new Error('DownloadEstimatorFailed');
})
.mergeMap((result) => {
if (result === 'DownloadEstimatorSuccess') {
return Observable.of('DownloadEstimatorSuccess');
}
if (result && result?.length === 1) {
return executeProcess(url, queryXML({
...omit(downloadOptions, 'notifyDownloadEstimatorSuccess'),
input: result[0],
filter: null,
outputAsReference: downloadOptions.asynchronous ? downloadOptions.outputAsReference : false,
resultOutput
}), executeOptions, {
headers: {
'Content-Type': 'application/xml',
'Accept': `application/xml, ${resultOutput}`
}
});
}

throw new Error('DownloadFailed');
});
}

return Observable.empty();
};
11 changes: 7 additions & 4 deletions web/client/plugins/LayerDownload.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,18 @@ import {
infoBubbleMessageSelector,
checkingExportDataEntriesSelector
} from '../selectors/layerdownload';
import { wfsURL } from '../selectors/query';
import {attributesSelector, wfsURL} from '../selectors/query';
import { getSelectedLayer } from '../selectors/layers';
import { currentLocaleSelector } from '../selectors/locale';
import { customAttributesSettingsSelector } from "../selectors/featuregrid";

import DownloadDialog from '../components/data/download/DownloadDialog';
import ExportDataResultsComponent from '../components/data/download/ExportDataResultsComponent';
import FeatureEditorButton from '../components/data/download/FeatureEditorButton';

import FeatureEditorButton from '../components/data/download/FeatureEditorButton';
import * as epics from '../epics/layerdownload';
import layerdownload from '../reducers/layerdownload';

import layerdownload from '../reducers/layerdownload';
import { createPlugin } from '../utils/PluginsUtils';

/**
Expand Down Expand Up @@ -110,7 +111,9 @@ const LayerDownloadPlugin = createPlugin('LayerDownload', {
layer: getSelectedLayer,
service: serviceSelector,
checkingWPSAvailability: checkingWPSAvailabilitySelector,
virtualScroll: state => state && state.featuregrid && state.featuregrid.virtualScroll
virtualScroll: state => state && state.featuregrid && state.featuregrid.virtualScroll,
customAttributeSettings: customAttributesSettingsSelector,
attributes: attributesSelector
}), {
onExport: downloadFeatures,
onDownloadOptionChange,
Expand Down
6 changes: 4 additions & 2 deletions web/client/plugins/widgetbuilder/FeatureEditorButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import React from 'react';
import {connect} from 'react-redux';
import { createChart } from '../../actions/widgets';

import Button from '../../components/data/featuregrid/toolbars/TButton';
import withHint from "../../components/data/featuregrid/enhancers/withHint";
import TButtonComp from "../../components/data/featuregrid/toolbars/TButton";
const TButton = withHint(TButtonComp);
const FeatureEditorButton = connect(
() => ({}),
{
onClick: () => createChart()
}
)(({disabled, mode, onClick = () => {}}) => {
return (<Button
return (<TButton
id="grid-map-chart"
keyProp="grid-map-chart"
tooltipId="featuregrid.toolbar.createNewChart"
Expand Down
1 change: 1 addition & 0 deletions web/client/selectors/featuregrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const getLayerById = getLayerFromId;
export const getTitle = (layer = {}) => layer.title || layer.name;
export const selectedLayerIdSelector = state => get(state, "featuregrid.selectedLayer");
export const getCustomAttributeSettings = (state, att) => get(state, `featuregrid.attributes[${att.name || att.attribute}]`);
export const customAttributesSettingsSelector = (state) => get(state, `featuregrid.attributes`);
export const selectedFeaturesSelector = state => state && state.featuregrid && state.featuregrid.select;
export const changesSelector = state => state && state.featuregrid && state.featuregrid.changes;
export const newFeaturesSelector = state => state && state.featuregrid && state.featuregrid.newFeatures;
Expand Down
Loading