textFeature.js

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

/**
 * Object specification for a text feature.
 *
 * @typedef {geo.feature.spec} geo.textFeature.spec
 * @extends geo.feature.spec
 * @property {geo.geoPosition[]|function} [position] The position of each data
 *   element.  Defaults to the `x`, `y`, and `z` properties of the data
 *   element.
 * @property {string[]|function} [text] The text of each data element.
 *   Defaults to the `text` property of the data element.
 * @property {geo.textFeature.styleSpec} [style] The style to apply to each
 *   data element.
 */

/**
 * Style specification for a text feature.
 *
 * @typedef {geo.feature.styleSpec} geo.textFeature.styleSpec
 * @extends geo.feature.styleSpec
 * @property {boolean|function} [visible=true] If falsy, don't show this data
 *    element.
 * @property {string|function} [font] A css font specification.  This is of the
 *    form `[style] [variant] [weight] [stretch] size[/line-height] family`.
 *    Individual font styles override this value if a style is specified in
 *    each.  See the individual font styles for details.
 * @property {string|function} [fontStyle='normal'] The font style.  One of
 *    `normal`, `italic`, or `oblique`.
 * @property {string|function} [fontVariant='normal'] The font variant.  This
 *    can have values such as `small-caps` or `slashed-zero`.
 * @property {string|function} [fontWeight='normal'] The font weight.  This may
 *    be a numeric value where 400 is normal and 700 is bold, or a string such
 *    as `bold` or `lighter`.
 * @property {string|function} [fontStretch='normal'] The font stretch, such as
 *    `condensed`.
 * @property {string|function} [fontSize='medium'] The font size.
 * @property {string|function} [lineHeight='normal'] The font line height.
 * @property {string|function} [fontFamily] The font family.
 * @property {string|function} [textAlign='center'] The horizontal text
 *    alignment.  One of `start`, `end`, `left`, `right`, or `center`.
 * @property {string|function} [textBaseline='middle'] The vertical text
 *    alignment.  One of `top`, `hanging`, `middle`, `alphabetic`,
 *    `ideographic`, or `bottom`.
 * @property {geo.geoColor|function} [color='black'] Text color.  May include
 *    opacity.
 * @property {number|function} [textOpacity=1] The opacity of the text.  If the
 *    color includes opacity, this is combined with that value.
 * @property {number|function} [rotation=0] Text rotation in radians.
 * @property {boolean|function} [rotateWithMap=false] If truthy, rotate the
 *    text when the map rotates.  Otherwise, the text is always in the same
 *    orientation.
 * @property {number|function} [textScaled] If defined, the text is scaled when
 *    the map zooms and this is the basis zoom for the fontSize.
 * @property {geo.screenPosition|function} [offset] Offset from the default
 *    position for the text.  This is applied before rotation.
 * @property {geo.geoColor|function} [shadowColor='black'] Text shadow color.
 *    May include opacity.
 * @property {geo.screenPosition|function} [shadowOffset] Offset for a text
 *    shadow.  This is applied before rotation.
 * @property {number|null|function} [shadowBlur] If not null, add a text shadow
 *    with this much blur.
 * @property {boolean|function} [shadowRotate=false] If truthy, rotate the
 *    shadow offset based on the text rotation (the `shadowOffset` is the
 *    offset if the text has a 0 rotation).
 * @property {geo.geoColor|function} [textStrokeColor='transparent'] Text
 *    stroke color.  May include opacity.
 * @property {geo.geoColor|function} [textStrokeWidth=0] Text stroke width in
 *    pixels.
 * @property {number|function} [renderThreshold] If this is a positive number,
 *    text elements may not be rendered if their base position (before offset
 *    and font effects are applied) is more than this distance in pixels
 *    outside of the current viewport.  If it is known that such text elements
 *    cannot affect the current viewport, setting this can speed up rendering.
 *    This is computed once for the whole feature.
 */

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

  /**
   * @private
   */
  var m_this = this,
      s_init = this._init;

  this.featureType = 'text';

  /**
   * Get/Set position.
   *
   * @param {array|function} [val] If `undefined`, return the current position
   *    setting.  Otherwise, modify the current position setting.
   * @returns {array|function|this} The current position or this feature.
   */
  this.position = function (val) {
    if (val === undefined) {
      return m_this.style('position');
    } else if (val !== m_this.style('position')) {
      m_this.style('position', val);
      m_this.dataTime().modified();
      m_this.modified();
    }
    return m_this;
  };

  /**
   * Get/Set text.
   *
   * @param {array|function} [val] If `undefined`, return the current text
   *    setting.  Otherwise, modify the current text setting.
   * @returns {array|function|this} The current text or this feature.
   */
  this.text = function (val) {
    if (val === undefined) {
      return m_this.style('text');
    } else if (val !== m_this.style('text')) {
      m_this.style('text', val);
      m_this.dataTime().modified();
      m_this.modified();
    }
    return m_this;
  };

  /**
   * Initialize.
   *
   * @param {geo.textFeature.spec} [arg] The feature specification.
   */
  this._init = function (arg) {
    arg = arg || {};
    s_init.call(m_this, arg);

    var style = Object.assign(
      {},
      {
        font: 'bold 16px sans-serif',
        textAlign: 'center',
        textBaseline: 'middle',
        color: { r: 0, g: 0, b: 0 },
        rotation: 0,  /* in radians */
        rotateWithMap: false,
        textScaled: false,
        position: (d) => Array.isArray(d) ? {x: d[0], y: d[1], z: d[2] || 0} : d,
        text: function (d) { return d.text; }
      },
      arg.style === undefined ? {} : arg.style
    );

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

    m_this.style(style);
    if (style.position) {
      m_this.position(style.position);
    }
    if (style.text) {
      m_this.text(style.text);
    }
    m_this.dataTime().modified();
  };

  this._init(arg);
  return m_this;
};

textFeature.usedStyles = [
  'visible', 'font', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch',
  'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textBaseline', 'color',
  'textOpacity', 'rotation', 'rotateWithMap', 'textScaled', 'offset',
  'shadowColor', 'shadowOffset', 'shadowBlur', 'shadowRotate',
  'textStrokeColor', 'textStrokeWidth'
];

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

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

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

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