UNPKG

@geoext/geoext

Version:

GeoExt is Open Source and enables building desktop-like GIS applications through the web. It is a JavaScript framework that combines the GIS functionality of OpenLayers with the user interface savvy of the ExtJS library provided by Sencha.

537 lines (465 loc) 14.1 kB
/* Copyright (c) 2015-present The Open Source Geospatial Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * A data store loading features from an OGC WFS. * * @class GeoExt.data.store.WfsFeatures */ Ext.define('GeoExt.data.store.WfsFeatures', { extend: 'GeoExt.data.store.Features', mixins: ['GeoExt.mixin.SymbolCheck', 'GeoExt.util.OGCFilter'], /** * If autoLoad is true, this store's loadWfs method is automatically called * after creation. * @cfg {boolean} */ autoLoad: true, /** * Default to using server side sorting * @cfg {boolean} */ remoteSort: true, /** * Default to using server side filtering * @cfg {boolean} */ remoteFilter: true, /** * Default logical comperator to combine filters sent to WFS * @cfg {string} */ logicalFilterCombinator: 'And', /** * Default request method to use in AJAX requests * @cfg {string} */ requestMethod: 'GET', /** * The 'service' param value used in the WFS request. * @cfg {string} */ service: 'WFS', /** * The 'version' param value used in the WFS request. * This should be '2.0.0' or higher at least if the paging mechanism * should be used. * @cfg {string} */ version: '2.0.0', /** * The 'request' param value used in the WFS request. * @cfg {string} */ request: 'GetFeature', /** * The 'typeName' param value used in the WFS request. * @cfg {string} */ typeName: null, /** * The 'srsName' param value used in the WFS request. If not set * it is automatically set to the map projection when available. * @cfg {string} */ srsName: null, /** * The 'outputFormat' param value used in the WFS request. * @cfg {string} */ outputFormat: 'application/json', /** * The 'startIndex' param value used in the WFS request. * @cfg {string} */ startIndex: 0, /** * The 'count' param value used in the WFS request. * @cfg {string} */ count: null, /** * A comma-separated list of property names to retrieve * from the server. If left as null all properties are returned. * @cfg {string} */ propertyName: null, /** * Offset to add to the #startIndex in the WFS request. * @cfg {number} */ startIndexOffset: 0, /** * The OL format used to parse the WFS GetFeature response. * @cfg {ol.format.Feature} */ format: null, /** * The attribution added to the created vector layer source. Only has an * effect if #createLayer is set to `true` * @cfg {string} */ layerAttribution: null, /** * Additional OpenLayers properties to apply to the created vector layer. * Only has an effect if #createLayer is set to `true` * @cfg {string} */ layerOptions: null, /** * Cache the total number of features be queried from when the store is * first loaded to use for the remaining life of the store. * This uses resultType=hits to get the number of features and can improve * performance rather than calculating on each request. It should be used * for read-only layers, or when the server does not return the * feature count on each request. * @cfg {boolean} */ cacheFeatureCount: false, /** * The outputFormat sent with the resultType=hits request. * Defaults to GML3 as some WFS servers do not support this * request type when using application/json. * Only has an effect if #cacheFeatureCount is set to `true` * @cfg {boolean} */ featureCountOutputFormat: 'gml3', /** * Time any request will be debounced. This will prevent too * many successive request. * @cfg {number} */ debounce: 300, /** * Constructs the WFS feature store. * * @param {Object} config The configuration object. * @private */ constructor: function (config) { const me = this; config = config || {}; // apply count as store's pageSize config.pageSize = config.count || me.count; if (config.pageSize > 0) { // calculate initial page const startIndex = config.startIndex || me.startIndex; config.currentPage = Math.floor(startIndex / config.pageSize) + 1; } // avoid creation of vector layer by parent class (raises error when // applying WFS data) so we can create the WFS vector layer on our own // (if needed) const createLayer = config.createLayer; config.createLayer = false; config.passThroughFilter = false; // only has effect for layers me.callParent([config]); me.loadWfsTask_ = new Ext.util.DelayedTask(); if (!me.url) { Ext.raise('No URL given to WfsFeaturesStore'); } if (createLayer) { // the WFS vector layer showing the WFS features on the map me.source = new ol.source.Vector({ features: new ol.Collection(), attributions: me.layerAttribution, }); const layerOptions = { source: me.source, style: me.style, }; if (me.layerOptions) { Ext.applyIf(layerOptions, me.layerOptions); } me.layer = new ol.layer.Vector(layerOptions); me.layerCreated = true; } if (me.cacheFeatureCount === true) { me.cacheTotalFeatureCount(!me.autoLoad); } else { if (me.autoLoad) { // initial load of the WFS data me.loadWfs(); } } // before the store gets re-loaded (e.g. by a paging toolbar) we trigger // the re-loading of the WFS, so the data keeps in sync me.on('beforeload', me.loadWfs, me); // add layer to connected map, if available if (me.map && me.layer) { me.map.addLayer(me.layer); } }, /** * Detects the total amount of features (without paging) of the given * WFS response. The detection is based on the response format (currently * GeoJSON and GML >=v3 are supported). * * @private * @param {Object} wfsResponse The XMLHttpRequest object * @return {number} Total amount of features */ getTotalFeatureCount: function (wfsResponse) { let totalCount = -1; // get the response type from the header const contentType = wfsResponse.getResponseHeader('Content-Type'); try { if (contentType.indexOf('application/json') !== -1) { const respJson = Ext.decode(wfsResponse.responseText); totalCount = respJson.numberMatched; } else { // assume GML const xml = wfsResponse.responseXML; if (xml && xml.firstChild) { const total = xml.firstChild.getAttribute('numberMatched'); totalCount = parseInt(total, 10); } } } catch (e) { Ext.Logger.warn( 'Error while detecting total feature count from ' + 'WFS response', ); } return totalCount; }, /** * Sends the sortBy parameter to the WFS Server * If multiple sorters are specified then multiple fields are * sent to the server. * Ascending sorts will append ASC and descending sorts DESC * E.g. sortBy=attribute1 DESC,attribute2 ASC * @private * @return {string} The sortBy string */ createSortByParameter: function () { const me = this; const sortStrings = []; let direction; let property; me.getSorters().each(function (sorter) { // direction will be ASC or DESC direction = sorter.getDirection(); property = sorter.getProperty(); sortStrings.push(Ext.String.format('{0} {1}', property, direction)); }); return sortStrings.join(','); }, /** * Create filter parameter string (according to Filter Encoding standard) * based on the given instances in filters ({Ext.util.FilterCollection}) of * the store. * * @private * @return {string} The filter XML encoded as string */ createOgcFilter: function () { const me = this; const filters = []; me.getFilters().each(function (item) { filters.push(item); }); if (filters.length === 0) { return null; } return GeoExt.util.OGCFilter.getOgcWfsFilterFromExtJsFilter( filters, me.logicalFilterCombinator, me.version, ); }, /** * Gets the number of features for the WFS typeName * using resultType=hits and caches it so it only needs to be calculated * the first time the store is used. * * @param {boolean} skipLoad Avoids loading the store if set to `true` * @private */ cacheTotalFeatureCount: function (skipLoad) { const me = this; const url = me.url; me.cachedTotalCount = 0; const params = { service: me.service, version: me.version, request: me.request, typeName: me.typeName, outputFormat: me.featureCountOutputFormat, resultType: 'hits', }; Ext.Ajax.request({ url: url, method: me.requestMethod, params: params, success: function (resp) { // set number of total features (needed for paging) me.cachedTotalCount = me.getTotalFeatureCount(resp); if (!skipLoad) { me.loadWfs(); } }, failure: function (resp) { Ext.Logger.warn( 'Error while requesting features from WFS: ' + resp.responseText + ' Status: ' + resp.status, ); }, }); }, /** * Handles the 'filterchange'-event. * Reload data using updated filter config. * @private */ onFilterChange: function () { const me = this; if (me.getFilters() && me.getFilters().length > 0) { me.loadWfs(); } }, /** * Create a parameters object used to make a WFS feature request * @return {Object} A object of WFS parameter keys and values */ createParameters: function () { const me = this; const params = { service: me.service, version: me.version, request: me.request, typeName: me.typeName, outputFormat: me.outputFormat, }; // add a propertyName parameter if set if (me.propertyName !== null) { params.propertyName = me.propertyName; } // add a srsName parameter if (me.srsName) { params.srsName = me.srsName; } else { // if it has not been set manually retrieve from the map if (me.map) { params.srsName = me.map.getView().getProjection().getCode(); } } // send the sortBy parameter only when remoteSort is true // as it is not supported by all WFS servers if (me.remoteSort === true) { const sortBy = me.createSortByParameter(); if (sortBy) { params.sortBy = sortBy; } } // create filter string if remoteFilter is activated if (me.remoteFilter === true) { const filter = me.createOgcFilter(); if (filter) { params.filter = filter; } } // apply paging parameters if necessary if (me.pageSize) { me.startIndex = (me.currentPage - 1) * me.pageSize + me.startIndexOffset; params.startIndex = me.startIndex; params.count = me.pageSize; } return params; }, /** * Loads the data from the connected WFS. * @private */ loadWfs: function () { const me = this; if (me.loadWfsTask_.id === null) { me.loadWfsTask_.delay(me.debounce, function () {}); me.loadWfsInternal(); } else { me.loadWfsTask_.delay(me.debounce, function () { me.loadWfsInternal(); }); } }, loadWfsInternal: function () { const me = this; const url = me.url; const params = me.createParameters(); // fire event 'gx-wfsstoreload-beforeload' and skip loading if listener // function returns false if (me.fireEvent('gx-wfsstoreload-beforeload', me, params) === false) { return; } // request features from WFS Ext.Ajax.request({ url: url, method: me.requestMethod, params: params, success: function (resp) { if (!me.format) { Ext.Logger.warn( 'No format given for WfsFeatureStore. ' + 'Skip parsing feature data.', ); return; } if (me.cacheFeatureCount === true) { // me.totalCount is reset to 0 on each load so reset it here me.totalCount = me.cachedTotalCount; } else { // set number of total features (needed for paging) me.totalCount = me.getTotalFeatureCount(resp); } // parse WFS response to OL features let wfsFeats = []; try { wfsFeats = me.format.readFeatures(resp.responseText); } catch (error) { Ext.Logger.warn( 'Error parsing features into the ' + 'OpenLayers format. Check the server response.', ); } // set data for store me.setData(wfsFeats); if (me.layer) { // add features to WFS layer me.source.clear(); me.source.addFeatures(wfsFeats); } me.fireEvent('gx-wfsstoreload', me, wfsFeats, true); }, failure: function (resp) { if (resp.aborted !== true) { Ext.Logger.warn( 'Error while requesting features from WFS: ' + resp.responseText + ' Status: ' + resp.status, ); } me.fireEvent('gx-wfsstoreload', me, null, false); }, }); }, doDestroy: function () { const me = this; if (me.loadWfsTask_.id !== null) { me.loadWfsTask_.cancel(); } me.callParent(arguments); }, });