Source: src/data/store/WfsFeatures.js

/* 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);
  },
});