Source: Highlighter.js

//////////////////////////////////////////////////////////////////////////////
// Highlighter
//////////////////////////////////////////////////////////////////////////////

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

// FIXME: There are 2 clasess here

/**
 * The Highlighter object controls highlighting and popovers of features,
 * plots and other elements on the Viewer when the mouse hovers over them.
 *
 * <a name="attributes"></a>
 * ### Attributes
 *
 *  Option                        | Default                    | Description
 *  ------------------------------|----------------------------|--------------------------
 *  [feature](#feature)           | {@link HighlighterElement} | Describes the highlightling options for features
 *  [plot](#plot)                 | {@link HighlighterElement} | Describes the highlightling options for plots
 *  [contig](#plot)               | {@link HighlighterElement} | Describes the highlightling options for contigs
 *  [backbone](#plot)             | {@link HighlighterElement} | Describes the highlightling options for the backbone
 *  [showMetaData](#showMetaData) | true                       | Should meta data be shown in popovers
 *
 * @extends CGObject
 */
class Highlighter extends CGObject {

  /**
   * Create a Highlighter
   * @param {Viewer} viewer - The viewer
   * @param {Object} options - [Attributes](#attributes) used to create the highlighter.
   */
  constructor(viewer, options = {}, meta = {}) {
    super(viewer, options, meta);
    this._viewer = viewer;
    this.showMetaData = utils.defaultFor(options.showMetaData, true);
    // this.popoverBox = viewer._container.append('div').attr('class', 'cgv-highlighter-popover-box').style('visibility', 'hidden');
    this.popoverBox = viewer._wrapper.append('div').attr('class', 'cgv-highlighter-popover-box').style('visibility', 'hidden');
    this._feature = new HighlighterElement('feature', options.feature);
    this._plot = new HighlighterElement('plot', options.plot);
    this._contig = new HighlighterElement('contig', options.contig);
    this._backbone = new HighlighterElement('backbone', options.contig);
    this.initializeEvents();

    // Set up position constants (Distance from mouse pointer to top-left of popup)
    this._offsetLeft = 8;
    this._offsetTop = -18;
  }

  /**
   * @member {Viewer} - Get the viewer.
   */
  get viewer() {
    return this._viewer;
  }

  /**
   * @member {HighlighterElement} - Get the feature HighlighterElement
   */
  get feature() {
    return this._feature;
  }

  /**
   * @member {HighlighterElement} - Get the plot HighlighterElement
   */
  get plot() {
    return this._plot;
  }

  /**
   * @member {HighlighterElement} - Get the contig HighlighterElement
   */
  get contig() {
    return this._contig;
  }

  /**
   * @member {HighlighterElement} - Get the backbone HighlighterElement
   */
  get backbone() {
    return this._backbone;
  }

  position(e) {
    const originX = e.canvasX + this._offsetLeft;
    const originY = e.canvasY + this._offsetTop;
    return { x: originX,  y: originY};
  }

  initializeEvents() {
    this.viewer.off('.cgv-highlighter');
    this.viewer.on('mousemove.cgv-highlighter', (e) => {
      this.mouseOver(e);
      // if (e.feature) {
      //   this.mouseOver('feature', e);
      // } else if (e.plot) {
      //   this.mouseOver('plot', e);
      // } else {
      //   this.hidePopoverBox();
      // }
    });
  }

  // mouseOver(type, e) {
  mouseOver(e) {
    const type = e.elementType;
    if (!type || !this[type]) {
      this.hidePopoverBox();
      return;
    }
    if (this[type].highlighting) {
      this[`highlight${utils.capitalize(type)}`](e);
    }
    if (this[type].popovers && this.visible) {
      const position = this.position(e);
      const html = (this[type].popoverContents && this[type].popoverContents(e)) || this[`${type}PopoverContentsDefault`](e);
      this.showPopoverBox({position: position, html: html});
    } else {
      this.hidePopoverBox();
    }
  }

  getTrackDiv(e) {
    let trackDiv = '';
    if (e.slot) {
      const track = e.slot.track;
      let direction = '';
      if (track.type === 'feature' && track.separateFeaturesBy !== 'none') {
        direction = e.slot.isDirect() ? '(+)' : '(-)';
      }
      trackDiv = `<div class='track-data'>Track: ${track.name} ${direction}</div>`;
    }
    return trackDiv;
  }

  getPositionDiv(e) {
    const bp = utils.commaNumber(e.bp);
    let div = `<div class='track-data'>Map: ${bp} bp</div>`;
    if (e.elementType === 'contig') {
      const contig = e.element;
      const contigBp = utils.commaNumber(e.bp - contig.lengthOffset);
      div = `<div class='track-data'>Contig: ${contigBp} bp</div>` + div;
    }
    return div;
  }

  featurePopoverContentsDefault(e) {
    const feature = e.element;
    // return `<div style='margin: 0 5px; font-size: 14px'>${feature.type}: ${feature.name}</div>`;
    const keys = Object.keys(feature.meta);
    let metaDivs = '';
    if (this.showMetaData && keys.length > 0) {
      metaDivs = keys.map( k => `<div class='meta-data'><span class='meta-data-key'>${k}</span>: <span class='meta-data-value'>${feature.meta[k]}</span></div>`).join('');
      metaDivs = `<div class='meta-data-container'>${metaDivs}</div>`;
    }
    return (`
      <div style='margin: 0 5px; font-size: 14px'>
        <div>${feature.type}: ${feature.name}<div>
        <div class='track-data'>Length: ${utils.commaNumber(feature.length)} bp</div>
        ${metaDivs}
        ${this.getTrackDiv(e)}
      </div>
    `);
  }

  plotPopoverContentsDefault(e) {
    const plot = e.element;
    const score = plot.scoreForPosition(e.bp);
    return (`
      <div style='margin: 0 5px; font-size: 14px'>
        <div>Score: ${score.toFixed(2)}</div>
        ${this.getTrackDiv(e)}
      </div>
    `);
  }

  backbonePopoverContentsDefault(e) {
    const length = utils.commaNumber(this.sequence.length);
    // return `<div style='margin: 0 5px; font-size: 14px'>Backbone: ${length} bp</div>`;
    return (`
      <div style='margin: 0 5px; font-size: 14px'>
        <div>Backbone: ${length} bp</div>
        ${this.getPositionDiv(e)}
      </div>
    `);
  }

  contigPopoverContentsDefault(e) {
    const contig = e.element;
    const length = utils.commaNumber(contig.length);
    // return `<div style='margin: 0 5px; font-size: 14px'>Contig ${contig.index}/${this.sequence.contigs().length} [${length} bp]: ${contig.name}</div>`;
    return (`
      <div style='margin: 0 5px; font-size: 14px'>
        <div>Contig ${contig.index}/${this.sequence.contigs().length} [${length} bp]: ${contig.name}</div>
        ${this.getPositionDiv(e)}
      </div>
    `);
  }

  highlightFeature(e) {
    e.element.highlight(e.slot);
  }

  highlightPlot(e) {
    const viewer = this.viewer;
    const plot = e.element;
    const score = plot.scoreForPosition(e.bp);
    if (score) {
      const startIndex = utils.indexOfValue(plot.positions, e.bp, false);
      const start = plot.positions[startIndex];
      const stop = plot.positions[startIndex + 1] || viewer.sequence.length;
      const baselineCenterOffset = e.slot.centerOffset - (e.slot.thickness / 2) + (e.slot.thickness * plot.baseline);
      const scoredCenterOffset = baselineCenterOffset + ((score - plot.baseline) * e.slot.thickness);
      const thickness = Math.abs(baselineCenterOffset - scoredCenterOffset);
      const centerOffset = Math.min(baselineCenterOffset, scoredCenterOffset) + (thickness / 2);
      const color = (score >= plot.baseline) ? plot.colorPositive.copy() : plot.colorNegative.copy();
      color.highlight();

      viewer.canvas.drawElement('ui', start, stop, centerOffset, color.rgbaString, thickness);
    }
  }

  highlightBackbone(e) {
    // e.element.highlight(e.slot);
  }

  highlightContig(e) {
    // e.element.highlight(e.slot);
  }

  hidePopoverBox() {
    this.popoverBox.style('visibility', 'hidden');
  }

  showPopoverBox(options = {}) {
    if (options.html) {
      this.popoverBox.html(options.html);
    }
    if (options.position) {
      this.popoverBox
        .style('left', `${options.position.x}px`)
        .style('top', `${options.position.y}px`);
    }
    this.popoverBox.style('visibility', 'visible');
  }

  toJSON() {
    return {
      visible: this.visible
    };
  }

}


//////////////////////////////////////////////////////////////////////////////
// Highlighter Element
//////////////////////////////////////////////////////////////////////////////
/**
 * A HighlighterElement indicates whether highlighting and popovers should appear.
 *
 * <a name="attributes"></a>
 * ### Attributes
 *
 *  Option                              | Default     | Description
 *  ------------------------------------|-------------|-----------------------------------
 *  [highlighting](#highlighting)       | true        | Highlight a element when the mouse is over it
 *  [popovers](#popovers)               | true        | Show a popover for the element when the mouse is over it
 *  [popoverContents](#popoverContents) | undefined   | Function to create html for the popover
 *
 */
class HighlighterElement {

  /**
   * Create a HighlighterElement
   * @param {String} type - The element type: 'feature', 'plot', 'contig', 'backbone'.
   * @param {Object} options - [Attributes](#attributes) used to create the highlighter element.
   */
  constructor(type, options = {}) {
    this.type = type;
    this.highlighting = utils.defaultFor(options.highlighting, true);
    this.popovers = utils.defaultFor(options.popovers, true);
    this.popoverContents = options.popoverContents;
  }

  /**
   * @member {String} - Get or set the type (e.g. 'feature', 'plot', 'contig', 'backbone')
   */
  get type() {
    return this._type;
  }

  set type(value) {
    this._type = value;
  }

  /**
   * @member {Boolean} - Get or set whether highlighting should occur
   */
  get highlighting() {
    return this._highlighting;
  }

  set highlighting(value) {
    this._highlighting = value;
  }

  /**
   * @member {Boolean} - Get or set whether popovers should occur
   */
  get popover() {
    return this._popover;
  }

  set popover(value) {
    this._popover = value;
  }

  /**
   * @member {Function} - Get or set the function to call to produce HTML for the popover.
   * The provided function will be called with one argument: an [event-like object](EventMonitor.html).
   */
  get popoverContents() {
    return this._popoverContents;
  }

  set popoverContents(value) {
    this._popoverContents = value;
  }

}

export { Highlighter, HighlighterElement };