Source: src/data/store/Features.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 holding OpenLayers feature objects (`ol.Feature`).
 *
 * @class GeoExt.data.store.Features
 */
Ext.define('GeoExt.data.store.Features', {
  extend: 'GeoExt.data.store.OlObjects',
  mixins: ['GeoExt.mixin.SymbolCheck'],

  // <debug>
  symbols: [
    'ol.Collection',
    'ol.layer.Vector',
    'ol.Map',
    'ol.Map#addLayer',
    'ol.Map#removeLayer',
    'ol.source.Vector',
    'ol.source.Vector#getFeatures',
    'ol.source.Vector#on',
    'ol.source.Vector#un',
    'ol.style.Circle',
    'ol.style.Fill',
    'ol.style.Stroke',
    'ol.style.Style',
  ],
  // </debug>

  model: 'GeoExt.data.model.Feature',

  config: {
    /**
     * Initial layer holding features which will be added to the store.
     *
     * The layer object which is in sync with this store.
     *
     * The layer needs to be constructed with an ol.source.Vector that
     * has an ol.Collection (constructor option `features` was set to
     * an ol.Collection).
     *
     * @property {ol.layer.Vector} layer Layer to be in sync with store
     * @readonly
     */
    layer: null,
  },

  /**
   * A map object to which a possible #layer will be added.
   *
   * @cfg {ol.Map}
   */
  map: null,

  /**
   * Setting this flag to `true` will create a vector #layer with the
   * given #features and adds it to the given #map (if available).
   *
   * @cfg {boolean}
   */
  createLayer: false,

  /**
   * Shows if the #layer has been created by constructor.
   *
   * @private
   * @property {boolean} layerCreated Layer created flag
   */
  layerCreated: false,

  /**
   * An OpenLayers 3 style object to style the vector #layer representing
   * the features of this store.
   *
   * @cfg {ol.Style}
   */
  style: null,

  /**
   * Initial set of features. Has to be an `ol.Collection` object with
   * `ol.Feature` objects in it.
   *
   * @cfg {ol.Collection}
   */
  features: null,

  /**
   * Setting this flag to true the filter of the store will be
   * applied to the underlying vector #layer.
   * This will only have an effect if the source of the #layer is NOT
   * configured with an 'url' parameter.
   *
   * @cfg {boolean}
   */
  passThroughFilter: false,

  /**
   * Constructs the feature store.
   *
   * @param {Object} config The configuration object.
   */
  constructor: function (config) {
    const me = this;

    me.onOlCollectionAdd = me.onOlCollectionAdd.bind(me);
    me.onOlCollectionRemove = me.onOlCollectionRemove.bind(me);

    const cfg = config || {};

    if (me.style === null) {
      me.style = new ol.style.Style({
        image: new ol.style.Circle({
          radius: 6,
          fill: new ol.style.Fill({
            color: '#3399CC',
          }),
          stroke: new ol.style.Stroke({
            color: '#fff',
            width: 2,
          }),
        }),
      });
    }

    if (cfg.features !== undefined && cfg.layer !== undefined) {
      throw new Error(
        'GeoExt.data.store.Features should only be' +
          ' configured with one or less of `features` and `layer`.',
      );
    }

    const configErrorMessage =
      'GeoExt.data.store.Features needs to be' +
      ' configured with a feature collection or with a layer with a' +
      ' source with a feature collection.';

    if (cfg.features === undefined && cfg.layer === undefined) {
      cfg.data = new ol.Collection();
    } else if (cfg.features !== undefined) {
      if (!(cfg.features instanceof ol.Collection)) {
        throw new Error('Features are not a collection. ' + configErrorMessage);
      }

      cfg.data = cfg.features;
    } else {
      if (!(cfg.layer instanceof ol.layer.BaseVector)) {
        throw new Error('Layer is no vector layer. ' + configErrorMessage);
      }
      if (!cfg.layer.getSource()) {
        throw new Error('Layer has no source. ' + configErrorMessage);
      }

      const features = cfg.layer.getSource().getFeaturesCollection();
      if (!features) {
        throw new Error('Source has no collection. ' + configErrorMessage);
      }

      cfg.data = features;
    }

    me.callParent([cfg]);

    // create a vector layer and add to map if configured accordingly
    if (me.createLayer === true && !me.layer) {
      me.drawFeaturesOnMap();
    }

    this.olCollection.on('add', this.onOlCollectionAdd);
    this.olCollection.on('remove', this.onOlCollectionRemove);

    if (me.passThroughFilter === true) {
      me.on('filterchange', me.onFilterChange);
    }
  },

  /**
   * Forwards changes to the `ol.Collection` to the Ext.data.Store.
   *
   * @param {ol.CollectionEvent} evt The event emitted by the `ol.Collection`.
   * @private
   */
  onOlCollectionAdd: function (evt) {
    const target = evt.target;
    const element = evt.element;
    const idx = Ext.Array.indexOf(target.getArray(), element);

    if (!this.__updating) {
      this.insert(idx, element);
    }
  },

  /**
   * Forwards changes to the `ol.Collection` to the Ext.data.Store.
   *
   * @param {ol.CollectionEvent} evt The event emitted by the `ol.Collection`.
   * @private
   */
  onOlCollectionRemove: function (evt) {
    const element = evt.element;
    const idx = this.findBy(function (rec) {
      return rec.olObject === element;
    });

    if (idx !== -1) {
      if (!this.__updating) {
        this.removeAt(idx);
      }
    }
  },

  applyFields: function (fields) {
    const me = this;
    if (fields) {
      this.setModel(Ext.data.schema.Schema.lookupEntity(me.config.model));
    }
  },

  /**
   * Returns the FeatureCollection which is in sync with this store.
   *
   * @return {ol.Collection} The underlying OpenLayers `ol.Collection` of
   *     `ol.Feature`.
   */
  getFeatures: function () {
    return this.olCollection;
  },

  /**
   * Returns the record corresponding to a feature.
   *
   * @param {ol.Feature} feature An ol.Feature object to get the record for
   * @return {Ext.data.Model} The model instance corresponding to the feature
   */
  getByFeature: function (feature) {
    return this.getAt(
      this.findBy(function (record) {
        return record.getFeature() === feature;
      }),
    );
  },

  /**
   * Overwrites the destroy function to ensure the #layer is removed from
   * the #map when it has been created automatically while construction in
   * case of destruction of this store.
   *
   * @protected
   */
  destroy: function () {
    this.olCollection.un('add', this.onCollectionAdd);
    this.olCollection.un('remove', this.onCollectionRemove);

    const me = this;

    if (me.map && me.layerCreated === true) {
      me.map.removeLayer(me.layer);
    }

    me.callParent(arguments);
  },

  /**
   * Draws the given #features on the #map.
   *
   * @private
   */
  drawFeaturesOnMap: function () {
    const me = this;

    // create a layer representation of our features
    me.source = new ol.source.Vector({
      features: me.getFeatures(),
    });
    me.layer = new ol.layer.Vector({
      source: me.source,
      style: me.style,
    });
    // add layer to connected map, if available
    if (me.map) {
      me.map.addLayer(me.layer);
    }

    me.layerCreated = true;
  },

  /**
   * Handles the 'filterchange'-event.
   * Applies the filter of this store to the underlying layer.
   * @private
   */
  onFilterChange: function () {
    const me = this;
    if (me.layer && me.layer.getSource() instanceof ol.source.Vector) {
      if (!me.__updating) {
        me.__updating = true;

        me.olCollection.clear();

        // add the filtered features to the collection
        me.each(function (rec) {
          me.olCollection.push(rec.getFeature());
        });

        delete me.__updating;
      }
    }
  },
});