polygonFeature.js

var inherit = require('./inherit');
var feature = require('./feature');
var transform = require('./transform');

/**
 * Polygon feature specification.
 *
 * @typedef {geo.feature.spec} geo.polygonFeature.spec
 * @extends geo.feature.spec
 * @property {geo.geoPosition|function} [position] Position of the data.
 *   Default is (data).
 * @property {geo.polygon|function} [polygon] Polygons from the data.  Default
 *   (data).
 * @property {geo.polygonFeature.styleSpec} [style] Style object with default
 *   style options.
 */

/**
 * Style specification for a polygon feature.
 *
 * @typedef {geo.lineFeature.styleSpec} geo.polygonFeature.styleSpec
 * @extends geo.lineFeature.styleSpec
 * @property {boolean|function} [fill=true] True to fill polygon.
 * @property {geo.geoColor|function} [fillColor] Color to fill each polygon.
 *   The color can vary by vertex.
 * @property {number|function} [fillOpacity] Opacity for each polygon.  The
 *   opacity can vary by vertex.  Opacity is on a [0-1] scale.
 * @property {boolean|function} [stroke=false] True to stroke polygon.
 * @property {boolean|function} [uniformPolygon=false] Boolean indicating if
 *   each polygon has a uniform style (uniform fill color, fill opacity, stroke
 *   color, and stroke opacity).  Can vary by polygon.
 * @property {boolean|function} [closed=true] Ignored.  Always `true`.
 * @property {number[]|function} [origin] Origin in map gcs coordinates used
 *   for to ensure high precision drawing in this location.  When called as a
 *   function, this is passed an array of items, each of which has a vertices
 *   property that is a single continuous array in map gcs coordinates.  It
 *   defaults to the first polygon's first vertex's position.
 */

/**
 * Create a new instance of class polygonFeature.
 *
 * @class
 * @alias geo.polygonFeature
 * @extends geo.feature
 * @param {geo.polygonFeature.spec} arg
 * @returns {geo.polygonFeature}
 */
var polygonFeature = function (arg) {
  'use strict';
  if (!(this instanceof polygonFeature)) {
    return new polygonFeature(arg);
  }
  arg = arg || {};
  feature.call(this, arg);

  var util = require('./util');

  /**
   * @private
   */
  var m_this = this,
      m_lineFeature,
      s_init = this._init,
      s_exit = this._exit,
      s_data = this.data,
      s_draw = this.draw,
      s_modified = this.modified,
      s_style = this.style,
      m_coordinates = [];

  this.featureType = 'polygon';
  this._subfeatureStyles = {
    fillColor: true,
    fillOpacity: true,
    lineCap: true,
    lineJoin: true,
    strokeColor: true,
    strokeOffset: true,
    strokeOpacity: true,
    strokeWidth: true
  };

  /**
   * Get/set data.
   *
   * @param {object} [arg] if specified, use this for the data and return the
   *    feature.  If not specified, return the current data.
   * @returns {geo.polygonFeature|object}
   */
  this.data = function (arg) {
    var ret = s_data(arg);
    if (arg !== undefined) {
      m_coordinates = getCoordinates();
      m_this._checkForStroke();
    }
    return ret;
  };

  /**
   * Get the internal coordinates whenever the data changes.  Also compute the
   * extents of the outside of each polygon for faster checking if points are
   * in the polygon.
   *
   * @private
   * @param {object[]} [data=this.data()] The data to process.
   * @param {function} [posFunc=this.style.get('position')] The function to
   *    get the position of each vertex.
   * @param {function} [polyFunc=this.style.get('polygon')] The function to
   *    get each polygon.
   * @returns {geo.polygonObject[]} An array of polygon positions.  Each has
   *    `outer` and `inner` if it has any coordinates, or is `undefined`.
   */
  function getCoordinates(data, posFunc, polyFunc) {
    const fcs = m_this.gcs(),
        mapgcs = m_this.layer().map().gcs();
    data = data || m_this.data();
    posFunc = posFunc || m_this.style.get('position');
    polyFunc = polyFunc || m_this.style.get('polygon');
    var coordinates = data.map(function (d, i) {
      var poly = polyFunc(d, i);
      if (!poly) {
        return undefined;
      }
      var outer, inner, range, coord, j, x, y, mapouter, mapinner, maprange;

      coord = poly.outer || (Array.isArray(poly) ? poly : []);
      outer = new Array(coord.length);
      for (j = 0; j < coord.length; j += 1) {
        outer[j] = posFunc.call(m_this, coord[j], j, d, i);
        x = outer[j].x;
        y = outer[j].y;
        if (!j) {
          range = {min: {x: x, y: y}, max: {x: x, y: y}};
        } else {
          if (x < range.min.x) { range.min.x = x; }
          if (y < range.min.y) { range.min.y = y; }
          if (x > range.max.x) { range.max.x = x; }
          if (y > range.max.y) { range.max.y = y; }
        }
      }
      inner = (poly.inner || []).map(function (hole) {
        coord = hole || [];
        var trans = new Array(coord.length);
        for (j = 0; j < coord.length; j += 1) {
          trans[j] = posFunc.call(m_this, coord[j], j, d, i);
        }
        return trans;
      });
      mapouter = transform.transformCoordinates(fcs, mapgcs, outer);
      mapinner = inner.map(part => transform.transformCoordinates(fcs, mapgcs, part));
      for (j = 0; j < mapouter.length; j += 1) {
        x = mapouter[j].x;
        y = mapouter[j].y;
        if (!j) {
          maprange = {min: {x: x, y: y}, max: {x: x, y: y}};
        } else {
          if (x < maprange.min.x) { maprange.min.x = x; }
          if (y < maprange.min.y) { maprange.min.y = y; }
          if (x > maprange.max.x) { maprange.max.x = x; }
          if (y > maprange.max.y) { maprange.max.y = y; }
        }
      }
      return {
        outer: outer,
        inner: inner,
        range: range,
        mapouter: mapouter,
        mapinner: mapinner,
        maprange: maprange
      };
    });
    return coordinates;
  }

  /**
   * Get the set of normalized polygon coordinates.
   *
   * @returns {geo.polygonObject[]} An array of polygon positions.  Each has
   *    `outer` and `inner` if it has any coordinates, or is `undefined`.
   */
  this.polygonCoordinates = function () {
    return m_coordinates;
  };

  /**
   * Get the style for the stroke of the polygon.  Since polygons can have
   * holes, the number of stroke lines may not be the same as the number of
   * polygons.  If the style for a stroke is a function, this calls the
   * appropriate value for the polygon.  Any style set for a stroke line should
   * be wrapped in this function.
   *
   * @param {(object|function)?} styleValue The polygon's style value used for
   *    the stroke.  This should be m_this.style(<name of style>) and not
   *    m_this.style.get(<name of style>), as the result is more efficient if
   *    the style is not a function.
   * @returns {object|function} A style that can be used for the stroke.
   * @private
   */
  function linePolyStyle(styleValue) {
    if (util.isFunction(styleValue)) {
      return function (d) {
        return styleValue(d[0], d[1], d[2], d[3]);
      };
    } else {
      return styleValue;
    }
  }

  /**
   * Get/set polygon accessor.
   *
   * @param {object|function} [val] If not specified, return the current
   *    polygon accessor.  If specified, use this for the polygon accessor and
   *    return `this`.  If a function is given, the function is passed
   *    `(dataElement, dataIndex)` and returns a {@link geo.polygon}.
   * @returns {object|function|this} The current polygon accessor or this
   *    feature.
   */
  this.polygon = function (val) {
    if (val === undefined) {
      return m_this.style('polygon');
    } else {
      m_this.style('polygon', val);
      m_this.dataTime().modified();
      m_this.modified();
      m_coordinates = getCoordinates();
    }
    return m_this;
  };

  /**
   * Get/Set position accessor.
   *
   * @param {geo.geoPosition|function} [val] If not specified, return the
   *    current position accessor.  If specified, use this for the position
   *    accessor and return `this`.  If a function is given, this is called
   *    with `(vertexElement, vertexIndex, dataElement, dataIndex)`.
   * @returns {geo.geoPosition|this} The current position or this feature.
   */
  this.position = function (val) {
    if (val === undefined) {
      return m_this.style('position');
    } else {
      m_this.style('position', val);
      m_this.dataTime().modified();
      m_this.modified();
      m_coordinates = getCoordinates();
    }
    return m_this;
  };

  /**
   * Point search method for selection api.  Returns markers containing the
   * given point.
   *
   * @param {geo.geoPosition} coordinate point to search for.
   * @param {string|geo.transform|null} [gcs] Input gcs.  `undefined` to use
   *    the interface gcs, `null` to use the map gcs, or any other transform.
   * @returns {object} An object with `index`: a list of polygon indices, and
   *    `found`: a list of polygons that contain the specified coordinate.
   */
  this.pointSearch = function (coordinate, gcs) {
    var found = [], indices = [], irecord = {}, data = m_this.data(),
        map = m_this.layer().map();
    gcs = (gcs === null ? map.gcs() : (gcs === undefined ? map.ingcs() : gcs));
    var pt = transform.transformCoordinates(gcs, map.gcs(), coordinate);
    m_coordinates.forEach(function (coord, i) {
      var inside = util.pointInPolygon(
        pt,
        coord.mapouter,
        coord.mapinner,
        coord.maprange
      );
      if (inside) {
        indices.push(i);
        irecord[i] = true;
        found.push(data[i]);
      }
    });
    if (m_lineFeature) {
      var lineFound = m_lineFeature.pointSearch(coordinate);
      lineFound.found.forEach(function (lineData) {
        if (lineData.length && lineData[0].length === 4 && !irecord[lineData[0][3]]) {
          indices.push(lineData[0][3]);
          irecord[lineData[0][3]] = true;
          found.push(data[lineData[0][3]]);
        }
      });
    }
    return {
      index: indices,
      found: found
    };
  };

  /**
   * Returns polygons that are contained in the given polygon.  This could fail
   * to return polygons that are less than their stroke width outside of the
   * specified polygon and whose vertices are not near the selected polygon.
   *
   * @param {geo.polygonObject} poly A polygon as an array of coordinates or an
   *    object with `outer` and optionally `inner` parameters.
   * @param {object} [opts] Additional search options.
   * @param {boolean} [opts.partial=false] If truthy, include polygons that are
   *    partially in the polygon, otherwise only include polygons that are fully
   *    within the region.
   * @param {string|geo.transform|null} [gcs] Input gcs.  `undefined` to use
   *    the interface gcs, `null` to use the map gcs, or any other transform.
   * @returns {object} An object with `index`: a list of polygon indices,
   *    `found`: a list of polygons within the polygon, and `extra`: an object
   *    with index keys containing an object with a `partial` key and a boolean
   *    value to indicate if the polygon is on the specified polygon's border.
   */
  this.polygonSearch = function (poly, opts, gcs) {
    var data = m_this.data(), indices = [], found = [], extra = {}, min, max,
        origPoly = poly, irecord = {},
        map = m_this.layer().map();
    gcs = (gcs === null ? map.gcs() : (gcs === undefined ? map.ingcs() : gcs));
    if (!poly.outer) {
      poly = {outer: poly, inner: []};
    }
    if (!data || !data.length || poly.outer.length < 3) {
      return {
        found: found,
        index: indices,
        extra: extra
      };
    }
    opts = opts || {};
    opts.partial = opts.partial || false;
    poly = {outer: transform.transformCoordinates(gcs, map.gcs(), poly.outer), inner: (poly.inner || []).map(inner => transform.transformCoordinates(gcs, map.gcs(), inner))};
    poly.outer.forEach(p => {
      if (!min) {
        min = {x: p.x, y: p.y};
        max = {x: p.x, y: p.y};
      }
      if (p.x < min.x) { min.x = p.x; }
      if (p.x > max.x) { max.x = p.x; }
      if (p.y < min.y) { min.y = p.y; }
      if (p.y > max.y) { max.y = p.y; }
    });
    m_coordinates.forEach(function (coord, idx) {
      if (!coord.mapouter.length || (coord.maprange &&
          (coord.maprange.max.x < min.x || coord.maprange.min.x > max.x ||
           coord.maprange.max.y < min.y || coord.maprange.min.y > max.y))) {
        return;
      }
      let inside, partial;
      // do something similar to the line's polygonSearch
      for (let r = -1; r < coord.mapinner.length && !partial; r += 1) {
        const record = r < 0 ? coord.mapouter : coord.mapinner[r];
        for (let i = 0, len = record.length, j = len - 1; i < len; j = i, i += 1) {
          const dist0 = util.distanceToPolygon2d(record[i], poly),
              dist1 = util.distanceToPolygon2d(record[j], poly);
          if (dist0 * dist1 < 0) {
            partial = true;
            break;
          }
          if (util.crossedLineSegmentPolygon2d(record[i], record[j], poly)) {
            partial = true;
            break;
          }
          if (dist0 > 0) {
            inside = true;
          }
        }
      }
      // check if the selection polygon is inside of this polygon.  Any point
      // is sufficient as otherwise the previous crossing test would have been
      // triggered.
      if (!inside && !partial && util.pointInPolygon(poly.outer[0], coord.mapouter, coord.mapinner, coord.maprange)) {
        partial = true;
      }
      if ((!opts.partial && inside && !partial) || (opts.partial && (inside || partial))) {
        indices.push(idx);
        found.push(data[idx]);
        extra[idx] = {
          partial: partial
        };
        irecord[idx] = true;
      }
    });
    if (m_lineFeature) {
      var lineFound = m_lineFeature.polygonSearch(origPoly, opts);
      lineFound.found.forEach(function (lineData, idx) {
        if (lineData.length && lineData[0].length === 4) {
          if (!irecord[lineData[0][3]]) {
            indices.push(lineData[0][3]);
            irecord[lineData[0][3]] = true;
            found.push(data[lineData[0][3]]);
            extra[lineFound.index[idx]] = {partial: false};
          }
          if (lineFound.extra[lineFound.index[idx]].partial) {
            extra[lineData[0][3]].partial = true;
          }
        }
      });
    }
    return {
      found: found,
      index: indices,
      extra: extra
    };
  };

  /**
   * Get/Set style used by the feature.  This calls the super function, then
   * checks if strokes are required.
   *
   * See the <a href="#.styleSpec">style specification
   * <code>styleSpec</code></a> for available styles.
   *
   * @param {string|object} [arg1] If `undefined`, return the current style
   *    object.  If a string and `arg2` is undefined, return the style
   *    associated with the specified key.  If a string and `arg2` is defined,
   *    set the named style to the specified value.  Otherwise, extend the
   *    current style with the values in the specified object.
   * @param {*} [arg2] If `arg1` is a string, the new value for that style.
   * @returns {object|this} Either the entire style object, the value of a
   *    specific style, or the current class instance.
   */
  this.style = function (arg1, arg2) {
    var result = s_style.apply(m_this, arguments);
    if (arg1 !== undefined && (typeof arg1 !== 'string' || arg2 !== undefined)) {
      m_this._checkForStroke();
    }
    return result;
  };

  this.style.get = s_style.get;

  /**
   * Get an outer or inner loop of a polygon and return the necessary data to
   * use it for a closed polyline.
   *
   * @param {object} item: the polygon.
   * @param {number} itemIndex: the index of the polygon
   * @param {Array} loop: the inner or outer loop.
   * @returns {Array} the loop with the data necessary to send to the position
   *    function for each vertex.
   */
  this._getLoopData = function (item, itemIndex, loop) {
    var line = [], i;

    for (i = 0; i < loop.length; i += 1) {
      line.push([loop[i], i, item, itemIndex]);
    }
    return line;
  };

  /**
   * Check if we need to add a line feature to the layer, and update it as
   * necessary.
   */
  this._checkForStroke = function () {
    if (s_style('stroke') === false) {
      if (m_lineFeature && m_this.layer()) {
        m_this.layer().deleteFeature(m_lineFeature);
        m_lineFeature = null;
        m_this.dependentFeatures([]);
      }
      return;
    }
    if (!m_this.layer()) {
      return;
    }
    if (!m_lineFeature) {
      m_lineFeature = m_this.layer().createFeature('line', {
        selectionAPI: false,
        gcs: m_this.gcs(),
        visible: m_this.visible(undefined, true)
      });
      m_this.dependentFeatures([m_lineFeature]);
    }
    var polyStyle = m_this.style(),
        strokeOpacity;
    if (util.isFunction(polyStyle.stroke) || !polyStyle.stroke) {
      var strokeFunc = m_this.style.get('stroke'),
          strokeOpacityFunc = m_this.style.get('strokeOpacity');
      strokeOpacity = function (d) {
        return strokeFunc(d[2], d[3]) ? strokeOpacityFunc(d[0], d[1], d[2], d[3]) : 0;
      };
    } else {
      strokeOpacity = linePolyStyle(polyStyle.strokeOpacity);
    }
    m_lineFeature.style({
      antialiasing: linePolyStyle(polyStyle.antialiasing),
      closed: true,
      lineCap: linePolyStyle(polyStyle.lineCap),
      lineJoin: linePolyStyle(polyStyle.lineJoin),
      miterLimit: linePolyStyle(polyStyle.miterLimit),
      strokeWidth: linePolyStyle(polyStyle.strokeWidth),
      strokeStyle: linePolyStyle(polyStyle.strokeStyle),
      strokeColor: linePolyStyle(polyStyle.strokeColor),
      strokeOffset: linePolyStyle(polyStyle.strokeOffset),
      strokeOpacity: strokeOpacity,
      uniformLine: linePolyStyle(polyStyle.uniformPolygon)
    });
    var data = m_this.data(),
        posVal = m_this.style('position');
    if (data !== m_lineFeature._lastData || posVal !== m_lineFeature._lastPosVal) {
      var lineData = [], i, polygon, loop,
          posFunc = m_this.style.get('position'),
          polyFunc = m_this.style.get('polygon');

      for (i = 0; i < data.length; i += 1) {
        polygon = polyFunc(data[i], i);
        if (!polygon) {
          continue;
        }
        loop = polygon.outer || (Array.isArray(polygon) ? polygon : []);
        if (loop.length >= 2) {
          lineData.push(m_this._getLoopData(data[i], i, loop));
          if (polygon.inner) {
            polygon.inner.forEach(function (loop) {
              if (loop.length >= 2) {
                lineData.push(m_this._getLoopData(data[i], i, loop));
              }
            });
          }
        }
      }
      m_lineFeature.position(function (d, i, item, itemIndex) {
        return posFunc(d[0], d[1], d[2], d[3]);
      });
      m_lineFeature.data(lineData);
      m_lineFeature._lastData = data;
      m_lineFeature._lastPosVal = posVal;
    }
  };

  /**
   * Redraw the object.
   *
   * @returns {object} The results of the superclass draw function.
   */
  this.draw = function () {
    var result = s_draw();
    if (m_lineFeature) {
      m_lineFeature.draw();
    }
    return result;
  };

  /**
   * Update the timestamp to the next global timestamp value.  Mark
   * sub-features as modified, too.
   *
   * @returns {object} The results of the superclass modified function.
   */
  this.modified = function () {
    var result = s_modified();
    if (m_lineFeature) {
      m_lineFeature.modified();
    }
    return result;
  };

  /**
   * Take a set of data, reduce the number of vertices per polygon using the
   * Ramer–Douglas–Peucker algorithm, and use the result as the new data.
   * This changes the instance's data, the position accessor, and the polygon
   * accessor.
   *
   * @param {array} data A new data array.
   * @param {number} [tolerance] The maximum variation allowed in map.gcs
   *    units.  A value of zero will only remove perfectly collinear points.
   *    If not specified, this is set to a half display pixel at the map's
   *    current zoom level.
   * @param {function} [posFunc=this.style.get('position')] The function to
   *    get the position of each vertex.
   * @param {function} [polyFunc=this.style.get('polygon')] The function to
   *    get each polygon.
   * @param {boolean} [returnData=false] If truthy, return the new data array
   *    rather than modifying the feature.
   * @returns {this|array}
   */
  this.rdpSimplifyData = function (data, tolerance, posFunc, polyFunc, returnData) {
    var map = m_this.layer().map(),
        mapgcs = map.gcs(),
        featuregcs = m_this.gcs(),
        coordinates = getCoordinates(data, posFunc, polyFunc);
    if (tolerance === undefined) {
      tolerance = map.unitsPerPixel(map.zoom()) * 0.5;
    }

    /* transform the coordinates to the map gcs */
    coordinates = coordinates.map(function (poly) {
      return {
        outer: transform.transformCoordinates(featuregcs, mapgcs, poly.outer),
        inner: poly.inner.map(function (hole) {
          return transform.transformCoordinates(featuregcs, mapgcs, hole);
        })
      };
    });
    data = data.map(function (d, idx) {
      var poly = coordinates[idx],
          elem = {};
      /* Copy element properties, as they might be used by styles */
      for (var key in d) {
        if (d.hasOwnProperty(key) && !(Array.isArray(d) && key >= 0 && key < d.length)) {
          elem[key] = d[key];
        }
      }
      if (poly && poly.outer.length >= 3) {
        // discard degenerate holes before anything else
        elem.inner = poly.inner.filter(function (hole) {
          return hole.length >= 3;
        });
        // simplify the outside of the polygon without letting it cross holes
        elem.outer = util.rdpLineSimplify(poly.outer, tolerance, true, elem.inner);
        if (elem.outer.length >= 3) {
          var allButSelf = elem.inner.slice();
          // simplify holes without crossing other holes or the outside
          elem.inner.map(function (hole, idx) {
            allButSelf[idx] = elem.outer;
            var result = util.rdpLineSimplify(hole, tolerance, true, allButSelf);
            allButSelf[idx] = result;
            return result;
          }).filter(function (hole) {
            return hole.length >= 3;
          });
          // transform coordinates back to the feature gcs
          elem.outer = transform.transformCoordinates(mapgcs, featuregcs, elem.outer);
          elem.inner = elem.inner.map(function (hole) {
            return transform.transformCoordinates(mapgcs, featuregcs, hole);
          });
        } else {
          elem.outer = elem.inner = [];
        }
      } else {
        elem.outer = [];
      }
      return elem;
    });
    if (returnData) {
      return data;
    }

    /* Set the reduced polygons as the data and use simple accessors. */
    m_this.style('position', util.identityFunction);
    m_this.style('polygon', util.identityFunction);
    m_this.data(data);
    return m_this;
  };

  /**
   * If the selectionAPI is on, then setting
   * `this.geoOn(geo.event.feature.mouseover_order, this.mouseOverOrderClosestBorder)`
   * will make it so that the mouseon events prefer the polygon with the
   * closet border, including hole edges.
   *
   * @param {geo.event} evt The event; this should be triggered from
   *    {@link geo.event.feature.mouseover_order}.
   */
  this.mouseOverOrderClosestBorder = function (evt) {
    var data = evt.feature.data(),
        map = evt.feature.layer().map(),
        pt = transform.transformCoordinates(map.ingcs(), evt.feature.gcs(), evt.mouse.geo),
        coor = evt.feature.polygonCoordinates(),
        dist = {};
    evt.over.index.forEach(function (di, idx) {
      var poly = coor[di], mindist;
      poly.outer.forEach(function (line1, pidx) {
        var line2 = poly.outer[(pidx + 1) % poly.outer.length];
        var dist = util.distance2dToLineSquared(pt, line1, line2);
        if (mindist === undefined || dist < mindist) {
          mindist = dist;
        }
      });
      poly.inner.forEach(function (inner) {
        inner.forEach(function (line1, pidx) {
          var line2 = inner[(pidx + 1) % inner.length];
          var dist = util.distance2dToLineSquared(pt, line1, line2);
          if (mindist === undefined || dist < mindist) {
            mindist = dist;
          }
        });
      });
      dist[di] = mindist;
    });
    evt.over.index.sort(function (i1, i2) {
      return dist[i1] - dist[i2];
    }).reverse();
    // this isn't necessary, but ensures that other event handlers have
    // consistent information
    evt.over.index.forEach(function (di, idx) {
      evt.over.found[idx] = data[di];
    });
  };

  /**
   * Return the polygons as a polygon list: an array of polygons, each of which
   * is an array of polylines, each of which is an array of points, each of
   * which is a 2-tuple of numbers.
   *
   * @param {geo.util.polyop.spec} [opts] Ignored.
   * @returns {geo.polygonList} A list of polygons.
   */
  this.toPolygonList = function (opts) {
    const polyFunc = m_this.style.get('polygon');
    const posFunc = m_this.style.get('position');
    return m_this.data().map((d, i) => {
      const polygon = polyFunc(d, i);
      const outer = polygon.outer || (Array.isArray(polygon) ? polygon : []);
      if (outer.length < 3) {
        return [];
      }
      const resp = [outer.map((p, j) => {
        const pos = posFunc(p, j, d, i);
        return [pos.x, pos.y];
      })];
      if (polygon.inner) {
        polygon.inner.forEach((h) => {
          resp.push(h.map((p, j) => {
            const pos = posFunc(p, j, d, i);
            return [pos.x, pos.y];
          }));
        });
      }
      return resp;
    });
  };

  /**
   * Set the data, position accessor, and polygon accessor to use a list of
   * polygons.
   *
   * @param {geo.polygonList} poly A list of polygons.
   * @param {geo.util.polyop.spec} [opts] Ignored.
   * @returns {this}
   */
  this.fromPolygonList = function (poly, opts) {
    m_this.style({
      position: (p) => ({x: p[0], y: p[1]}),
      polygon: (p) => ({outer: p[0], inner: p.slice(1)})
    });
    m_this.data(poly);
    return m_this;
  };

  /**
   * Destroy.
   */
  this._exit = function () {
    if (m_lineFeature && m_this.layer()) {
      m_this.layer().deleteFeature(m_lineFeature);
      m_lineFeature = null;
      m_this.dependentFeatures([]);
    }
    s_exit();
  };

  /**
   * Initialize.
   *
   * @param {geo.polygonFeature.spec} arg An object with options for the
   *    feature.
   */
  this._init = function (arg) {
    arg = arg || {};
    s_init.call(m_this, arg);

    var style = Object.assign(
      {},
      {
        // default style
        fill: true,
        fillColor: {r: 0.0, g: 0.5, b: 0.5},
        fillOpacity: 1.0,
        stroke: false,
        strokeWidth: 1.0,
        strokeStyle: 'solid',
        strokeColor: {r: 0.0, g: 1.0, b: 1.0},
        strokeOpacity: 1.0,
        polygon: util.identityFunction,
        position: (d) => Array.isArray(d) ? {x: d[0], y: d[1], z: d[2] || 0} : d,
        origin: (items) => {
          for (let i = 0; i < items.length; i += 1) {
            if (items[i].vertices.length >= 3) {
              return items[i].vertices.slice(0, 3);
            }
          }
          return [0, 0, 0];
        }
      },
      arg.style === undefined ? {} : arg.style
    );

    if (arg.polygon !== undefined) {
      style.polygon = arg.polygon;
    }
    if (arg.position !== undefined) {
      style.position = arg.position;
    }
    m_this.style(style);

    m_this._checkForStroke();
  };

  /* Don't call _init here -- let subclasses call it */
  return this;
};

/**
 * Create a polygonFeature from an object.
 *
 * @see {@link geo.feature.create}
 * @param {geo.layer} layer The layer to add the feature to
 * @param {geo.polygonFeature.spec} spec The object specification
 * @returns {geo.polygonFeature|null}
 */
polygonFeature.create = function (layer, spec) {
  'use strict';

  spec = spec || {};
  spec.type = 'polygon';
  return feature.create(layer, spec);
};

polygonFeature.capabilities = {
  /* core feature name -- support in any manner */
  feature: 'polygon'
};

inherit(polygonFeature, feature);
module.exports = polygonFeature;