map/wfs-layer.js

import 'whatwg-fetch';
import 'url-search-params-polyfill';

import GeoJSON from 'ol/format/geojson';
import Vector from 'ol/layer/vector';
import Heatmap from 'ol/layer/heatmap';
import VectorSource from 'ol/source/vector';
import loadingstrategy from 'ol/loadingstrategy';
import tilegrid from 'ol/tilegrid';
import Attribution from 'ol/attribution';

import VectorLayer from './vector-layer';

/**
 * Web Feature Service Layer
 * @extends VectorLayer
 */
class WFSLayer extends VectorLayer {
  /**
   * @param {Object} config - Configuration object
   * @param {String} [config.title='OverlayLayer'] - Layer title
   * @param {Boolean} [config.visible=false] - Layer initial status
   * @param {String} config.server - URL of map server
   * @param {String} config.layerName - Name of layer to display
   * @param {String} [config.attribution=''] - Layer data attribution
   * @param {Boolean} [config.exclusive=false] - If true, when the layer is shown, all other overlay layers are hidden
   * @param {Object} config.style - Style configuration
   * @param {String} config.style.property - Property that defines the style to use
   * @param {Object} config.style.values - Object with possible values and their corresponding style
   * @param {Object} [config.style.minRadius=3] - Mininum radius to use for feature style
   * @param {Object} [config.style.maxRadius=10] - Maximum radius to use for feature style
   * @param {Object[]} [config.popup] - Data to show when user clicks on a feature in the map
   * @param {String|String[]} [config.popup[].property] - Name of field or array of fields names to show
   * @param {String} [config.popup[].title] - Text to show as title
   * @param {Function} [config.popup[].format] - Function to process field or fields value
   * @param {Float} [config.opacity=1] - Layer opacity
   * @param {Object} [config.heatmap] - Show layer as heatmap
   * @param {Integer} [config.heatmap.blur=15] - Blur size in pixels
   * @param {Integer} [config.heatmap.radius=8] - Radius size in pixels
   * @param {String[]} [config.heatmap.gradient=['#00f', '#0ff', '#0f0', '#ff0', '#fa0', '#f00']] - Gradient to use
   * @param {String|Array} [config.properties] - Define feature properties to retrive from server. If not defined,
   *    it will retrieve only properties defined in popup.
   * @param {Filter[]} [config.filters] - Set of filters to apply to the layer. Overrides global dashboard filters.
   * @param {Object} [config.layerParams] - Extra params for OpenLayers Layer constructor
   * @param {Object} [config.sourceParams] - Extra params for OpenLayers Source constructor
   */
  constructor(config = {}) {
    super(Vector, config);

    this.server = `${config.server}/wfs/`;
    this.format = new GeoJSON();

    if (config.heatmap) {
      config.blur = config.blur || 15;
      config.radius = config.radius || 8;
      config.gradient = ['#00f', '#0ff', '#0f0', '#ff0', '#fa0', '#f00'];

      this.layer = new Heatmap(Object.assign({
        title: this.title,
        visible: this.visible,
        exclusive: this.exclusive,
        blur: config.blur,
        radius: config.radius,
        gradient: config.gradient,
        opacity: config.opacity,
      }, this.layerParams));
    }

    this.source = new VectorSource(Object.assign({
      loader: this.loadFeatures.bind(this),
      strategy: loadingstrategy.tile(tilegrid.createXYZ({
        maxZoom: 19,
      })),
      attributions: [new Attribution({
        html: this.attribution,
      })],
    }, this.sourceParams));

    this.loading = 0;

    this.properties = config.properties || this.popup.map(item =>
      (Array.isArray(item.property) ? item.property.join(',') : item.property));
    this.properties.push(this.geometryName);
    if (config.style.property) this.properties.push(config.style.property);
  }

  /**
   * Loads features from server via WFS service
   * @param {Number[]} extent - Array of numbers representing an extent: [minx, miny, maxx, maxy]
   * @private
   */
  loadFeatures(extent) {
    this.loading += 1;
    const params = new URLSearchParams();
    params.append('service', 'WFS');
    params.append('version', '1.0.0');
    params.append('request', 'GetFeature');
    params.append('outputFormat', 'application/json');
    params.append('format_options', 'CHARSET:UTF-8');
    params.append('typename', this.layerName);
    params.append('srsname', this.manager.viewProjection.getCode());
    params.append('cql_filter', this.buildCQLFilter(extent));
    params.append('propertyName', this.properties.join(','));
    fetch(`${this.server}?${params.toString()}`, {
      mode: 'cors',
    }).then(response => response.json())
      .catch(() => null)
      .then((data) => {
        if (data) {
          this.source.addFeatures(this.format.readFeatures(data));
        }
        this.loading -= 1;
        if (this.loading === 0) {
          this.emit('loaded');
        }
      });
  }

  /**
   * Builds CQLFilter string based on current extent and dashboard filters
   * @param {Number[]} extent - Array of numbers representing an extent: [minx, miny, maxx, maxy]
   * @returns {String}
   * @private
   */
  buildCQLFilter(extent) {
    let cqlFilter = `bbox(${this.geometryName}, ${extent.join(',')}, '${this.manager.viewProjection.getCode()}')`; // eslint-disable-line max-len
    if (this.filterString) {
      cqlFilter = `${cqlFilter} AND ${this.filterString}`;
    } else if (this.manager.filterString) {
      cqlFilter = `${cqlFilter} AND ${this.manager.filterString}`;
    }
    return cqlFilter;
  }
}

export default WFSLayer;