Source: LegendItem.js

//////////////////////////////////////////////////////////////////////////////
// LegendItem
//////////////////////////////////////////////////////////////////////////////

import CGObject from './CGObject';
import Color from './Color';
import Font from './Font';
import utils from './Utils';

/**
 * A legendItem is used to add text to a map legend. Individual
 * Features and Plots can be linked to a legendItem, so that the feature
 * or plot color will use the swatchColor of legendItem.
 *
 * ### Action and Events
 *
 * Action                                     | Legend Method                            | LegendItem Method   | Event
 * -------------------------------------------|------------------------------------------|---------------------|-----
 * [Add](../docs.html#adding-records)         | [addItems()](Legend.html#addItems)       | -                   | legendItems-add
 * [Update](../docs.html#updating-records)    | [updateItems()](Legend.html#updateItems) | [update()](#update) | legendItems-update
 * [Remove](../docs.html#removing-records)    | [removeItems()](Legend.html#removeItems) | [remove()](#remove) | legendItems-remove
 * [Reorder](../docs.html#reordering-records) | [moveItem()](Legend.html#moveItem)       | [move()](#move)     | legendItems-reorder
 * [Read](../docs.html#reading-records)       | [items()](Legend.html#items)             | -                   | -
 *
 * <a name="attributes"></a>
 * ### Attributes
 *
 * Attribute                        | Type      | Description
 * ---------------------------------|-----------|------------
 * [name](#name)                    | String    | Name to diplay for legendItem 
 * [font](#font)                    | String    | A string describing the font [Default: 'SansSerif, plain, 8']. See {@link Font} for details.
 * [fontColor](#fontColor)          | String    | A string describing the font color [Default: 'black']. See {@link Color} for details.
 * [decoration](#decoration)        | String    | How the features should be drawn. Choices: 'arc' [Default], 'arrow', 'score', 'none' [Default: 'arc']
 * [swatchColor](#swatchColor)      | String    | A string describing the legendItem display color [Default: 'black']. See {@link Color} for details.
 * [minArcLength](#minArcLength)    | Number    | Minimum length in pixels to use when drawing arcs. From 0 to 2 pixels [Default: 1]
 * [drawSwatch](#drawSwatch)        | Boolean   | Draw the swatch beside the legendItem name [Default: true]
 * [favorite](#favorite)            | Boolean   | LegendItem is a favorite [Default: false]
 * [visible](CGObject.html#visible) | Boolean   | LegendItem is visible [Default: true]
 * [meta](CGObject.html#meta)       | Object    | [Meta data](../tutorials/details-meta-data.html)
 *
 * ### Examples
 *
 * @extends CGObject
 */
class LegendItem extends CGObject {

  /**
   * Create a new legendItem. By default a legendItem will use its parent legend defaultFont, and defaultFontColor.
   * @param {Viewer} viewer - The viewer
   * @param {Object} options - [Attributes](#attributes) used to create the legendItem
   * @param {Object} [meta] - User-defined [Meta data](../tutorials/details-meta-data.html) to add to the legendItem.
   */
  constructor(legend, options = {}, meta = {}) {
    super(legend.viewer, options, meta);
    this.legend = legend;

    this.name = utils.defaultFor(options.name, '');
    this.font = options.font;
    this.fontColor = options.fontColor;
    this.minArcLength = options.minArcLength;
    this._drawSwatch = utils.defaultFor(options.drawSwatch, true);
    this._swatchColor = new Color( utils.defaultFor(options.swatchColor, 'black') );
    this._decoration = utils.defaultFor(options.decoration, 'arc');
    this._initializationComplete = true;
    this.refresh();
  }

  /**
   * Return the class name as a string.
   * @return {String} - 'LegendItem'
   */
  toString() {
    return 'LegendItem';
  }

  /**
   * @member {Legend} - Get the *Legend*
   */
  get legend() {
    return this._legend;
  }

  set legend(legend) {
    legend._items.push(this);
    this._legend = legend;
  }

  get visible() {
    return this._visible;
  }

  set visible(value) {
    // super.visible = value;
    this._visible = value;
    this.refresh();
  }

  /**
   * @member {String} - Get or set the name. The name is the text shown for the legendItem.
   * When setting a name, if it's not unique it will be appended with a number.
   * For example, if 'my_name' already exists, it will be changed to 'my_name-2'.
   */
  get name() {
    return this._name;
  }

  set name(value) {
    const valueString = `${value}`;
    const allNames = this.legend._items.map( i => i.name);
    this._name = utils.uniqueName(valueString, allNames)
    if (this._name !== valueString) {
      console.log(`LegendItem with name '${valueString}' already exists, using name '${this._name}' instead.`)
    }
    this.refresh();
  }

  /**
   * @member {String} - Get the text alignment of the parent *Legend* text alignment. Possible values are *left*, *center*, or *right*.
   * @private
   */
  get textAlignment() {
    return this.legend.textAlignment;
  }

  /**
   * @member {Number} - Get the width in pixels.
   */
  get width() {
    return this._width;
  }

  /**
   * @member {Number} - Get the height in pixels. This will be the same as the font size.
   */
  get height() {
    return this.font.height;
  }

  /**
   * @member {Font} - Get or set the font. When setting the font, a string representing the font or a {@link Font} object can be used. For details see {@link Font}.
   */
  // get font() {
  //   return this._font;
  // }
  //
  // set font(value) {
  //   if (value === undefined) {
  //     this._font = this.legend.defaultFont;
  //   } else if (value.toString() === 'Font') {
  //     this._font = value;
  //   } else {
  //     this._font = new Font(value);
  //   }
  //   this.refresh();
  // }
  get font() {
    return this._font || this.legend.defaultFont;
  }

  set font(value) {
    if (value === undefined) {
      this._font = undefined;
    } else if (value.toString() === 'Font') {
      this._font = value;
    } else {
      this._font = new Font(value);
    }
    this.refresh();
  }

  /**
   * @member {Boolean} - Returns true if using the default legend font
   */
  get usingDefaultFont() {
    return this.font === this.legend.defaultFont;
  }

  /**
   * @member {Color} - Get or set the fontColor. When setting the color, a string representing the color or a {@link Color} object can be used. For details see {@link Color}.
   */
  // get fontColor() {
  //   return this._fontColor;
  // }
  //
  // set fontColor(color) {
  //   if (color === undefined) {
  //     this._fontColor = this.legend.defaultFontColor;
  //   } else if (color.toString() === 'Color') {
  //     this._fontColor = color;
  //   } else {
  //     this._fontColor = new Color(color);
  //   }
  //   this.refresh();
  // }
  get fontColor() {
    return this._fontColor || this.legend.defaultFontColor;
  }

  set fontColor(color) {
    if (color === undefined) {
      // this._fontColor = this.legend.defaultFontColor;
      this._fontColor = undefined;
    } else if (color.toString() === 'Color') {
      this._fontColor = color;
    } else {
      this._fontColor = new Color(color);
    }
    this.refresh();
  }

  get usingDefaultFontColor() {
    return this.fontColor === this.legend.defaultFontColor;
  }

  /**
   * @member {Number} - Get or set the minArcLength for legend items. The value must be between 0 to 2 pixels [Default: 1].
   *   Minimum arc length refers to the minimum size (in pixels) an arc will be drawn.
   *   At some scales, small features will have an arc length of a fraction
   *   of a pixel. In these cases, the arcs are hard to see.
   *   A minArcLength of 0 means no adjustments will be made.
   */
  get minArcLength() {
    return (this._minArcLength === undefined) ? this.legend.defaultMinArcLength : this._minArcLength;
  }

  set minArcLength(value) {
    if (value === undefined) {
      this._minArcLength = undefined;
    } else {
      this._minArcLength = utils.constrain(Number(value), 0, 2);
    }
  }

  /**
   * @member {Boolean} - Returns true if using the default min arc length
   */
  get usingDefaultMinArcLength() {
    // return this.minArcLength === this.legend.defaultMinArcLength;
    return this._minArcLength === undefined;
  }

  /**
   * @member {Boolean} - Get or set the drawSwatch property. If true a swatch will be
   * drawn beside the legendItem text.
   */
  get drawSwatch() {
    return this._drawSwatch;
  }

  set drawSwatch(value) {
    this._drawSwatch = value;
    this.refresh();
  }

  /**
   * @member {Number} - Get the swatch width (same as legendItem height).
   */
  get swatchWidth() {
    return this.height;
  }

  /**
   * @member {Color} - Get or set the swatchColor. When setting the color, a string representing the color or a {@link Color} object can be used. For details see {@link Color}.
   */
  get swatchColor() {
    return this._swatchColor;
  }

  set swatchColor(color) {
    if (color.toString() === 'Color') {
      this._swatchColor = color;
    } else {
      this._swatchColor.setColor(color);
    }
    this.refresh();
  }

  /**
   * @member {String} - Get or set the decoration. Choices are *arc* [Default], *arrow*, *score*, *none*.
   */
  get decoration() {
    return this._decoration || 'arc';
  }

  set decoration(value) {
    if ( utils.validate(value, ['arc', 'arrow', 'none', 'score']) ) {
      this._decoration = value;
    }
  }

  /**
   * @member {Color} - Alias for  [swatchColor](LegendItem.html#swatchColor).
   * @private
   */
  get color() {
    return this.swatchColor;
  }

  set color(color) {
    this.swatchColor = color;
  }

  /**
   * @member {Boolean} - Get or set whether this item is selected
   * @private
   */
  get swatchSelected() {
    return this.legend.selectedSwatchedItem === this;
  }

  set swatchSelected(value) {
    if (value) {
      this.legend.selectedSwatchedItem = this;
    } else {
      if (this.legend.selectedSwatchedItem === this) {
        this.legend.selectedSwatchedItem = undefined;
      }
    }
  }

  /**
   * @member {Boolean} - Get or set whether this item is highlighted
   * @private
   */
  get swatchHighlighted() {
    return this.legend.highlightedSwatchedItem === this;
  }

  set swatchHighlighted(value) {
    if (value) {
      this.legend.highlightedSwatchedItem = this;
    } else {
      if (this.legend.highlightedSwatchedItem === this) {
        this.legend.highlightedSwatchedItem = undefined;
      }
    }
  }

  /**
   * Refresh parent legend
   * @private
   */
  refresh() {
    if (this._initializationComplete) {
      this.legend.refresh();
    }
  }

  /**
   * Returns the text x position
   * @private
   */
  textX() {
    const box = this.box;
    const legend = this.legend;
    if (this.textAlignment === 'left') {
      return this.drawSwatch ? (this.swatchX() + this.swatchWidth + legend.swatchPadding) : box.leftPadded;
    // } else if (this.textAlignment === 'center') {
    //   return box.centerX;
    } else if (this.textAlignment === 'right') {
      return this.drawSwatch ? (this.swatchX() - legend.swatchPadding) : box.rightPadded;
    }
  }

  /**
   * Returns the text y position
   * @private
   */
  textY() {
    const legend = this.legend;
    // let y = legend.originY + legend.padding;
    let y = legend.box.topPadded;
    const visibleItems = this.legend.visibleItems();
    for (let i = 0, len = visibleItems.length; i < len; i++) {
      const item = visibleItems[i];
      if (item === this) { break; }
      y += (item.height * 1.5);
    }
    return y;
  }


  /**
   * Returns the swatch x position
   * @private
   */
  swatchX() {
    const box = this.legend.box;
    if (this.textAlignment === 'left') {
      return box.leftPadded;
    // } else if (this.textAlignment === 'center') {
    //   return box.leftPadded;
    } else if (this.textAlignment === 'right') {
      return box.rightPadded - this.swatchWidth;
    }
  }

  /**
   * Returns the swatch y position
   * @private
   */
  swatchY() {
    return this.textY();
  }

  /**
   * Returns true if the swatch contains the provided point
   * @private
   */
  _swatchContainsPoint(pt) {
    const x = this.swatchX();
    const y = this.swatchY();
    if (pt.x >= x && pt.x <= x + this.height && pt.y >= y && pt.y <= y + this.height) {
      return true;
    }
  }

  /**
   * Returns true if the text contains the provided point
   * @private
   */
  _textContainsPoint(pt) {
    const textX = this.textX();
    const textY = this.textY();
    if (this.textAlignment === 'right') {
      if (pt.x <= textX && pt.x >= textX - this.width && pt.y >= textY && pt.y <= textY + this.height) {
        return true;
      }
    } else {
      if (pt.x >= textX && pt.x <= textX + this.width && pt.y >= textY && pt.y <= textY + this.height) {
        return true;
      }
    }
  }

  /**
   * Highlight this legendItem
   * @param {Color} color - Color for the highlight
   */
  highlight(color = this.fontColor) {
    if (!this.visible || !this.legend.visible) { return; }
    // let ctx = this.canvas.context('background');
    // ctx.fillStyle = color;
    // ctx.fillRect(this.textX(), this.textY(), this.width, this.height);
    const ctx = this.canvas.context('ui');
    let x = this.textX();
    // if (this.textAlignment === 'center') {
    //   x -= (this.width / 2);
    if (this.textAlignment === 'right') {
      x -= this.width;
    }
    ctx.lineWidth = 1;
    ctx.strokeStyle = color.rgbaString;

    // Rectangle Outline
    // ctx.strokeRect(x, this.textY(), this.width, this.height);

    // Rounded Rectangle Outline
    const padding = 2;
    const corner = this.height / 4;
    ctx.beginPath();
    ctx.roundRect(x - padding, this.textY() - padding, this.width + (2*padding), this.height + (2*padding), [corner]);
    ctx.stroke();
  }

  /**
   * Invert the swatch color
   */
  invertColors() {
    const attributes = {
      swatchColor: this.swatchColor.invert().rgbaString
    };
    if (!this.usingDefaultFontColor) {
      attributes.fontColor = this.fontColor.invert().rgbaString;
    }
    this.update(attributes);
  }

  /**
   * Remove legendItem
   */
  remove() {
    this.legend.removeItems(this);
  }

  /**
   * Move this legendItem to a new index in the array of Legend legendItems.
   * @param {Number} newIndex - New index for this caption (0-based)
   */
  move(newIndex) {
    const currentIndex = this.legend.items().indexOf(this);
    this.legend.moveItem(currentIndex, newIndex);
  }

  /**
   * Update legendItem [attributes](#attributes).
   * See [updating records](../docs.html#s.updating-records) for details.
   * @param {Object} attributes - Object describing the properties to change
   */
  update(attributes) {
    this.legend.updateItems(this, attributes);
  }

  /**
   * Returns the features that have this legendItem
   * @param {Integer|String|Array} term - See [CGArray.get](CGArray.html#get) for details.
   * @return {Feature|CGArray}
   */
  features(term) {
    return this.viewer._features.filter( f => f.legendItem === this ).get(term);
  }

  /**
   * Returns the plots that have this legendItem
   * @param {Integer|String|Array} term - See [CGArray.get](CGArray.html#get) for details.
   * @return {Feature|CGArray}
   */
  plots(term) {
    return this.viewer._plots.filter( p => p.legendItem.includes(this) ).get(term);
  }

  /**
   * Returns JSON representing the object
   */
  toJSON(options = {}) {
    const json = {
      name: this.name,
      // font: this.font.string,
      // fontColor: this.fontColor.rgbaString,
      swatchColor: this.swatchColor.rgbaString,
      decoration: this.decoration
      // visible: this.visible
    };
    // Optionally add default values
    if (!this.visible || options.includeDefaults) {
      json.visible = this.visible;
    }
    if (!this.usingDefaultFontColor || options.includeDefaults) {
      json.fontColor = this.fontColor.rgbaString;
    }
    if (!this.usingDefaultFont || options.includeDefaults) {
      json.font = this.font.string;
    }
    if (!this.usingDefaultMinArcLength || options.includeDefaults) {
      json.minArcLength = this.minArcLength;
    }
    // Meta Data (TODO: add an option to exclude this)
    if (Object.keys(this.meta).length > 0) {
      json.meta = this.meta;
    }
    return json;
  }

}

export default LegendItem;