Source: src/data/store/WfsFeatures.js

  1. /* Copyright (c) 2015-present The Open Source Geospatial Foundation
  2. *
  3. * This program is free software: you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation, either version 3 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License
  14. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. /**
  17. * A data store loading features from an OGC WFS.
  18. *
  19. * @class GeoExt.data.store.WfsFeatures
  20. */
  21. Ext.define('GeoExt.data.store.WfsFeatures', {
  22. extend: 'GeoExt.data.store.Features',
  23. mixins: ['GeoExt.mixin.SymbolCheck', 'GeoExt.util.OGCFilter'],
  24. /**
  25. * If autoLoad is true, this store's loadWfs method is automatically called
  26. * after creation.
  27. * @cfg {boolean}
  28. */
  29. autoLoad: true,
  30. /**
  31. * Default to using server side sorting
  32. * @cfg {boolean}
  33. */
  34. remoteSort: true,
  35. /**
  36. * Default to using server side filtering
  37. * @cfg {boolean}
  38. */
  39. remoteFilter: true,
  40. /**
  41. * Default logical comperator to combine filters sent to WFS
  42. * @cfg {string}
  43. */
  44. logicalFilterCombinator: 'And',
  45. /**
  46. * Default request method to use in AJAX requests
  47. * @cfg {string}
  48. */
  49. requestMethod: 'GET',
  50. /**
  51. * The 'service' param value used in the WFS request.
  52. * @cfg {string}
  53. */
  54. service: 'WFS',
  55. /**
  56. * The 'version' param value used in the WFS request.
  57. * This should be '2.0.0' or higher at least if the paging mechanism
  58. * should be used.
  59. * @cfg {string}
  60. */
  61. version: '2.0.0',
  62. /**
  63. * The 'request' param value used in the WFS request.
  64. * @cfg {string}
  65. */
  66. request: 'GetFeature',
  67. /**
  68. * The 'typeName' param value used in the WFS request.
  69. * @cfg {string}
  70. */
  71. typeName: null,
  72. /**
  73. * The 'srsName' param value used in the WFS request. If not set
  74. * it is automatically set to the map projection when available.
  75. * @cfg {string}
  76. */
  77. srsName: null,
  78. /**
  79. * The 'outputFormat' param value used in the WFS request.
  80. * @cfg {string}
  81. */
  82. outputFormat: 'application/json',
  83. /**
  84. * The 'startIndex' param value used in the WFS request.
  85. * @cfg {string}
  86. */
  87. startIndex: 0,
  88. /**
  89. * The 'count' param value used in the WFS request.
  90. * @cfg {string}
  91. */
  92. count: null,
  93. /**
  94. * A comma-separated list of property names to retrieve
  95. * from the server. If left as null all properties are returned.
  96. * @cfg {string}
  97. */
  98. propertyName: null,
  99. /**
  100. * Offset to add to the #startIndex in the WFS request.
  101. * @cfg {number}
  102. */
  103. startIndexOffset: 0,
  104. /**
  105. * The OL format used to parse the WFS GetFeature response.
  106. * @cfg {ol.format.Feature}
  107. */
  108. format: null,
  109. /**
  110. * The attribution added to the created vector layer source. Only has an
  111. * effect if #createLayer is set to `true`
  112. * @cfg {string}
  113. */
  114. layerAttribution: null,
  115. /**
  116. * Additional OpenLayers properties to apply to the created vector layer.
  117. * Only has an effect if #createLayer is set to `true`
  118. * @cfg {string}
  119. */
  120. layerOptions: null,
  121. /**
  122. * Cache the total number of features be queried from when the store is
  123. * first loaded to use for the remaining life of the store.
  124. * This uses resultType=hits to get the number of features and can improve
  125. * performance rather than calculating on each request. It should be used
  126. * for read-only layers, or when the server does not return the
  127. * feature count on each request.
  128. * @cfg {boolean}
  129. */
  130. cacheFeatureCount: false,
  131. /**
  132. * The outputFormat sent with the resultType=hits request.
  133. * Defaults to GML3 as some WFS servers do not support this
  134. * request type when using application/json.
  135. * Only has an effect if #cacheFeatureCount is set to `true`
  136. * @cfg {boolean}
  137. */
  138. featureCountOutputFormat: 'gml3',
  139. /**
  140. * Time any request will be debounced. This will prevent too
  141. * many successive request.
  142. * @cfg {number}
  143. */
  144. debounce: 300,
  145. /**
  146. * Constructs the WFS feature store.
  147. *
  148. * @param {Object} config The configuration object.
  149. * @private
  150. */
  151. constructor: function (config) {
  152. const me = this;
  153. config = config || {};
  154. // apply count as store's pageSize
  155. config.pageSize = config.count || me.count;
  156. if (config.pageSize > 0) {
  157. // calculate initial page
  158. const startIndex = config.startIndex || me.startIndex;
  159. config.currentPage = Math.floor(startIndex / config.pageSize) + 1;
  160. }
  161. // avoid creation of vector layer by parent class (raises error when
  162. // applying WFS data) so we can create the WFS vector layer on our own
  163. // (if needed)
  164. const createLayer = config.createLayer;
  165. config.createLayer = false;
  166. config.passThroughFilter = false; // only has effect for layers
  167. me.callParent([config]);
  168. me.loadWfsTask_ = new Ext.util.DelayedTask();
  169. if (!me.url) {
  170. Ext.raise('No URL given to WfsFeaturesStore');
  171. }
  172. if (createLayer) {
  173. // the WFS vector layer showing the WFS features on the map
  174. me.source = new ol.source.Vector({
  175. features: new ol.Collection(),
  176. attributions: me.layerAttribution,
  177. });
  178. const layerOptions = {
  179. source: me.source,
  180. style: me.style,
  181. };
  182. if (me.layerOptions) {
  183. Ext.applyIf(layerOptions, me.layerOptions);
  184. }
  185. me.layer = new ol.layer.Vector(layerOptions);
  186. me.layerCreated = true;
  187. }
  188. if (me.cacheFeatureCount === true) {
  189. me.cacheTotalFeatureCount(!me.autoLoad);
  190. } else {
  191. if (me.autoLoad) {
  192. // initial load of the WFS data
  193. me.loadWfs();
  194. }
  195. }
  196. // before the store gets re-loaded (e.g. by a paging toolbar) we trigger
  197. // the re-loading of the WFS, so the data keeps in sync
  198. me.on('beforeload', me.loadWfs, me);
  199. // add layer to connected map, if available
  200. if (me.map && me.layer) {
  201. me.map.addLayer(me.layer);
  202. }
  203. },
  204. /**
  205. * Detects the total amount of features (without paging) of the given
  206. * WFS response. The detection is based on the response format (currently
  207. * GeoJSON and GML >=v3 are supported).
  208. *
  209. * @private
  210. * @param {Object} wfsResponse The XMLHttpRequest object
  211. * @return {number} Total amount of features
  212. */
  213. getTotalFeatureCount: function (wfsResponse) {
  214. let totalCount = -1;
  215. // get the response type from the header
  216. const contentType = wfsResponse.getResponseHeader('Content-Type');
  217. try {
  218. if (contentType.indexOf('application/json') !== -1) {
  219. const respJson = Ext.decode(wfsResponse.responseText);
  220. totalCount = respJson.numberMatched;
  221. } else {
  222. // assume GML
  223. const xml = wfsResponse.responseXML;
  224. if (xml && xml.firstChild) {
  225. const total = xml.firstChild.getAttribute('numberMatched');
  226. totalCount = parseInt(total, 10);
  227. }
  228. }
  229. } catch (e) {
  230. Ext.Logger.warn(
  231. 'Error while detecting total feature count from ' + 'WFS response',
  232. );
  233. }
  234. return totalCount;
  235. },
  236. /**
  237. * Sends the sortBy parameter to the WFS Server
  238. * If multiple sorters are specified then multiple fields are
  239. * sent to the server.
  240. * Ascending sorts will append ASC and descending sorts DESC
  241. * E.g. sortBy=attribute1 DESC,attribute2 ASC
  242. * @private
  243. * @return {string} The sortBy string
  244. */
  245. createSortByParameter: function () {
  246. const me = this;
  247. const sortStrings = [];
  248. let direction;
  249. let property;
  250. me.getSorters().each(function (sorter) {
  251. // direction will be ASC or DESC
  252. direction = sorter.getDirection();
  253. property = sorter.getProperty();
  254. sortStrings.push(Ext.String.format('{0} {1}', property, direction));
  255. });
  256. return sortStrings.join(',');
  257. },
  258. /**
  259. * Create filter parameter string (according to Filter Encoding standard)
  260. * based on the given instances in filters ({Ext.util.FilterCollection}) of
  261. * the store.
  262. *
  263. * @private
  264. * @return {string} The filter XML encoded as string
  265. */
  266. createOgcFilter: function () {
  267. const me = this;
  268. const filters = [];
  269. me.getFilters().each(function (item) {
  270. filters.push(item);
  271. });
  272. if (filters.length === 0) {
  273. return null;
  274. }
  275. return GeoExt.util.OGCFilter.getOgcWfsFilterFromExtJsFilter(
  276. filters,
  277. me.logicalFilterCombinator,
  278. me.version,
  279. );
  280. },
  281. /**
  282. * Gets the number of features for the WFS typeName
  283. * using resultType=hits and caches it so it only needs to be calculated
  284. * the first time the store is used.
  285. *
  286. * @param {boolean} skipLoad Avoids loading the store if set to `true`
  287. * @private
  288. */
  289. cacheTotalFeatureCount: function (skipLoad) {
  290. const me = this;
  291. const url = me.url;
  292. me.cachedTotalCount = 0;
  293. const params = {
  294. service: me.service,
  295. version: me.version,
  296. request: me.request,
  297. typeName: me.typeName,
  298. outputFormat: me.featureCountOutputFormat,
  299. resultType: 'hits',
  300. };
  301. Ext.Ajax.request({
  302. url: url,
  303. method: me.requestMethod,
  304. params: params,
  305. success: function (resp) {
  306. // set number of total features (needed for paging)
  307. me.cachedTotalCount = me.getTotalFeatureCount(resp);
  308. if (!skipLoad) {
  309. me.loadWfs();
  310. }
  311. },
  312. failure: function (resp) {
  313. Ext.Logger.warn(
  314. 'Error while requesting features from WFS: ' +
  315. resp.responseText +
  316. ' Status: ' +
  317. resp.status,
  318. );
  319. },
  320. });
  321. },
  322. /**
  323. * Handles the 'filterchange'-event.
  324. * Reload data using updated filter config.
  325. * @private
  326. */
  327. onFilterChange: function () {
  328. const me = this;
  329. if (me.getFilters() && me.getFilters().length > 0) {
  330. me.loadWfs();
  331. }
  332. },
  333. /**
  334. * Create a parameters object used to make a WFS feature request
  335. * @return {Object} A object of WFS parameter keys and values
  336. */
  337. createParameters: function () {
  338. const me = this;
  339. const params = {
  340. service: me.service,
  341. version: me.version,
  342. request: me.request,
  343. typeName: me.typeName,
  344. outputFormat: me.outputFormat,
  345. };
  346. // add a propertyName parameter if set
  347. if (me.propertyName !== null) {
  348. params.propertyName = me.propertyName;
  349. }
  350. // add a srsName parameter
  351. if (me.srsName) {
  352. params.srsName = me.srsName;
  353. } else {
  354. // if it has not been set manually retrieve from the map
  355. if (me.map) {
  356. params.srsName = me.map.getView().getProjection().getCode();
  357. }
  358. }
  359. // send the sortBy parameter only when remoteSort is true
  360. // as it is not supported by all WFS servers
  361. if (me.remoteSort === true) {
  362. const sortBy = me.createSortByParameter();
  363. if (sortBy) {
  364. params.sortBy = sortBy;
  365. }
  366. }
  367. // create filter string if remoteFilter is activated
  368. if (me.remoteFilter === true) {
  369. const filter = me.createOgcFilter();
  370. if (filter) {
  371. params.filter = filter;
  372. }
  373. }
  374. // apply paging parameters if necessary
  375. if (me.pageSize) {
  376. me.startIndex = (me.currentPage - 1) * me.pageSize + me.startIndexOffset;
  377. params.startIndex = me.startIndex;
  378. params.count = me.pageSize;
  379. }
  380. return params;
  381. },
  382. /**
  383. * Loads the data from the connected WFS.
  384. * @private
  385. */
  386. loadWfs: function () {
  387. const me = this;
  388. if (me.loadWfsTask_.id === null) {
  389. me.loadWfsTask_.delay(me.debounce, function () {});
  390. me.loadWfsInternal();
  391. } else {
  392. me.loadWfsTask_.delay(me.debounce, function () {
  393. me.loadWfsInternal();
  394. });
  395. }
  396. },
  397. loadWfsInternal: function () {
  398. const me = this;
  399. const url = me.url;
  400. const params = me.createParameters();
  401. // fire event 'gx-wfsstoreload-beforeload' and skip loading if listener
  402. // function returns false
  403. if (me.fireEvent('gx-wfsstoreload-beforeload', me, params) === false) {
  404. return;
  405. }
  406. // request features from WFS
  407. Ext.Ajax.request({
  408. url: url,
  409. method: me.requestMethod,
  410. params: params,
  411. success: function (resp) {
  412. if (!me.format) {
  413. Ext.Logger.warn(
  414. 'No format given for WfsFeatureStore. ' +
  415. 'Skip parsing feature data.',
  416. );
  417. return;
  418. }
  419. if (me.cacheFeatureCount === true) {
  420. // me.totalCount is reset to 0 on each load so reset it here
  421. me.totalCount = me.cachedTotalCount;
  422. } else {
  423. // set number of total features (needed for paging)
  424. me.totalCount = me.getTotalFeatureCount(resp);
  425. }
  426. // parse WFS response to OL features
  427. let wfsFeats = [];
  428. try {
  429. wfsFeats = me.format.readFeatures(resp.responseText);
  430. } catch (error) {
  431. Ext.Logger.warn(
  432. 'Error parsing features into the ' +
  433. 'OpenLayers format. Check the server response.',
  434. );
  435. }
  436. // set data for store
  437. me.setData(wfsFeats);
  438. if (me.layer) {
  439. // add features to WFS layer
  440. me.source.clear();
  441. me.source.addFeatures(wfsFeats);
  442. }
  443. me.fireEvent('gx-wfsstoreload', me, wfsFeats, true);
  444. },
  445. failure: function (resp) {
  446. if (resp.aborted !== true) {
  447. Ext.Logger.warn(
  448. 'Error while requesting features from WFS: ' +
  449. resp.responseText +
  450. ' Status: ' +
  451. resp.status,
  452. );
  453. }
  454. me.fireEvent('gx-wfsstoreload', me, null, false);
  455. },
  456. });
  457. },
  458. doDestroy: function () {
  459. const me = this;
  460. if (me.loadWfsTask_.id !== null) {
  461. me.loadWfsTask_.cancel();
  462. }
  463. me.callParent(arguments);
  464. },
  465. });