featureLayer.js

var inherit = require('./inherit');
var layer = require('./layer');
var geo_event = require('./event');
var registry = require('./registry');

/**
 * Layer to draw points, lines, and polygons on the map The polydata layer
 * provide mechanisms to create and draw geometrical shapes such as points,
 * lines, and polygons.
 * @class
 * @alias geo.featureLayer
 * @extends geo.layer
 * @param {geo.layer.spec} [arg] Specification for the new layer.
 * @returns {geo.featureLayer}
 */
var featureLayer = function (arg) {
  'use strict';
  if (!(this instanceof featureLayer)) {
    return new featureLayer(arg);
  }
  layer.call(this, arg);

  /**
   * private
   */
  var m_this = this,
      m_features = [],
      s_init = this._init,
      s_exit = this._exit,
      s_update = this._update,
      s_visible = this.visible,
      s_selectionAPI = this.selectionAPI,
      s_draw = this.draw;

  /**
   * Create a feature by name.
   *
   * @param {string} featureName The name of the feature to create.
   * @param {object} arg Properties for the new feature.
   * @returns {geo.feature} The created feature.
   */
  this.createFeature = function (featureName, arg) {

    var newFeature = registry.createFeature(
      featureName, m_this, m_this.renderer(), arg);
    if (newFeature) {
      m_this.addFeature(newFeature);
    } else {
      console.warn('Layer renderer (' + m_this.rendererName() + ') does not support feature type "' + featureName + '"');
    }
    return newFeature;
  };

  /**
   * Add a feature to the layer if it is not already present.
   *
   * @param {object} feature the feature to add.
   * @returns {this}
   */
  this.addFeature = function (feature) {
    /* try to remove the feature first so that we don't have two copies */
    m_this.removeFeature(feature);
    m_this.addChild(feature);
    m_features.push(feature);
    m_this.dataTime().modified();
    m_this.modified();
    return m_this;
  };

  /**
   * Remove a feature without destroying it.
   *
   * @param {geo.feature} feature The feature to remove.
   * @returns {this}
   */
  this.removeFeature = function (feature) {
    var pos;

    pos = m_features.indexOf(feature);
    if (pos >= 0) {
      m_features.splice(pos, 1);
      m_this.removeChild(feature);
      m_this.dataTime().modified();
      m_this.modified();
    }

    return m_this;
  };

  /**
   * Delete feature.
   *
   * @param {geo.feature} feature The feature to delete.
   * @returns {this}
   */
  this.deleteFeature = function (feature) {

    // call _exit first, as destroying the feature affect other features
    if (feature) {
      if (m_features.indexOf(feature) >= 0) {
        feature._exit();
      }
      m_this.removeFeature(feature);
    }

    return m_this;
  };

  /**
   * Get/Set drawables.
   *
   * @param {geo.feature[]} [val] A list of features, or unspecified to return
   *    the current feature list.  If a list is provided, features are added or
   *    removed as needed.
   * @returns {geo.feature[]|this} The current features associated with the
   *    layer or the current layer.
   */
  this.features = function (val) {
    if (val === undefined) {
      return m_features.slice();
    } else {
      // delete existing features that aren't in the new array.  Since features
      // can affect other features during their exit process, make sure each
      // feature still exists as we work through the list.
      var existing = m_features.slice();
      var i;
      for (i = 0; i < existing.length; i += 1) {
        if (val.indexOf(existing[i]) < 0 && m_features.indexOf(existing[i]) >= 0) {
          m_this.deleteFeature(existing[i]);
        }
      }
      for (i = 0; i < val.length; i += 1) {
        m_this.addFeature(val[i]);
      }
      return m_this;
    }
  };

  /**
   * Get or set the gcs for all features.  For features, the default is usually
   * the map's ingcs.
   *
   * @param {string?} [val] If `undefined`, return the current gcs.  If
   *    `null`, use the map's interface gcs.  Otherwise, set a new value for
   *    the gcs.  When getting the current gcs, if not all features have the
   *    same gcs, `undefined` will be returned.
   * @returns {string|this} A string used by {@link geo.transform}.  If the
   *    map interface gcs is in use, that value will be returned.  If the gcs
   *    is set, return the current class instance.
   */
  this.gcsFeatures = function (val) {
    if (val === undefined) {
      var gcs, mixed;
      this.features().forEach((feature) => {
        if (gcs === undefined) {
          gcs = feature.gcs();
        } else if (feature.gcs() !== gcs) {
          mixed = true;
        }
      });
      return mixed ? undefined : gcs;
    }
    m_this.features().forEach((feature) => {
      if (feature.gcs() !== val) {
        feature.gcs(val);
      }
    });
    return m_this;
  };

  /**
   * Initialize.
   *
   * @returns {this}
   */
  this._init = function () {
    if (m_this.initialized()) {
      return m_this;
    }

    // Call super class init
    s_init.call(m_this, true);

    // Bind events to handlers
    m_this.geoOn(geo_event.resize, function (event) {
      if (m_this.renderer()) {
        m_this.renderer()._resize(event.x, event.y, event.width, event.height);
        m_this._update({event: event});
        m_this.renderer()._render();
      } else {
        m_this._update({event: event});
      }
    });

    m_this.geoOn(geo_event.pan, function (event) {
      m_this._update({event: event});
      if (m_this.renderer()) {
        m_this.renderer()._render();
      }
    });

    m_this.geoOn(geo_event.rotate, function (event) {
      m_this._update({event: event});
      if (m_this.renderer()) {
        m_this.renderer()._render();
      }
    });

    m_this.geoOn(geo_event.zoom, function (event) {
      m_this._update({event: event});
      if (m_this.renderer()) {
        m_this.renderer()._render();
      }
    });

    return m_this;
  };

  /**
   * Update layer.
   *
   * @param {object} request A value to pass to the parent class.
   * @returns {this}
   */
  this._update = function (request) {
    var i;

    if (!m_features.length) {
      return m_this;
    }

    // Call base class update
    s_update.call(m_this, request);

    if (m_this.dataTime().timestamp() > m_this.updateTime().timestamp()) {
      for (i = 0; i < m_features.length; i += 1) {
        m_features[i].renderer(m_this.renderer());
      }
    }

    for (i = 0; i < m_features.length; i += 1) {
      m_features[i]._update();
    }

    m_this.updateTime().modified();

    return m_this;
  };

  /**
   * Free all resources.
   */
  this._exit = function () {
    m_this.clear();
    s_exit();
  };

  /**
   * Draw.  If the layer is visible, call the parent class's draw function and
   * the renderer's render function.
   *
   * @returns {this}
   */
  this.draw = function () {
    if (m_this.visible()) {
      // Call sceneObject.draw, which calls draw on all child objects.
      s_draw();

      // Now call render on the renderer. In certain cases it may not do
      // anything if the child objects are drawn on the screen already.
      if (m_this.renderer()) {
        m_this.renderer()._render();
      }
    }
    return m_this;
  };

  /**
   * Get/Set visibility of the layer.
   *
   * @param {boolean} [val] If specified, change the visibility, otherwise
   *    return it.
   * @returns {boolean|this} The current visibility or the layer.
   */
  this.visible = function (val) {
    if (val === undefined) {
      return s_visible();
    }
    if (m_this.visible() !== val) {
      s_visible(val);

      // take a copy of the features; changing visible could mutate them.
      var features = m_features.slice(), i;

      for (i = 0; i < features.length; i += 1) {
        features[i].visible(features[i].visible(undefined, true), true);
      }
      if (val) {
        m_this.draw();
      }
    }
    return m_this;
  };

  /**
   * Get/Set selectionAPI of the layer.
   *
   * @param {boolean} [val] If specified change the selectionAPI state of the
   *    layer, otherwise return the current state.
   * @returns {boolean|this} The selectionAPI state or the current layer.
   */
  this.selectionAPI = function (val) {
    if (val === undefined) {
      return s_selectionAPI();
    }
    if (m_this.selectionAPI() !== val) {
      s_selectionAPI(val);

      // take a copy of the features; changing selectionAPI could mutate them.
      var features = m_features.slice(), i;

      for (i = 0; i < features.length; i += 1) {
        features[i].selectionAPI(features[i].selectionAPI(undefined, true), true);
      }
    }
    return m_this;
  };

  /**
   * Clear all features in layer.
   *
   * @returns {this}
   */
  this.clear = function () {
    while (m_features.length) {
      m_this.deleteFeature(m_features[0]);
    }
    return m_this;
  };

  return m_this;
};

inherit(featureLayer, layer);
registry.registerLayer('feature', featureLayer);
module.exports = featureLayer;