Source: Slot.js

//////////////////////////////////////////////////////////////////////////////
// Slot
//////////////////////////////////////////////////////////////////////////////

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

/**
 * A Slot is a single ring on the Map.
 *
 * @extends CGObject
 */
class Slot extends CGObject {

  /**
   * Slot
   */
  constructor(track, data = {}, meta = {}) {
    super(track.viewer, data, meta);
    this.track = track;
    this._strand = utils.defaultFor(data.strand, 'direct');
    this._features = new CGArray();
    this._plot;
    this.refresh();
  }

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

  /** * @member {Track} - Get the *Track*
   */
  get track() {
    return this._track;
  }

  set track(track) {
    if (this.track) {
      // TODO: Remove if already attached to Track
    }
    this._track = track;
    track._slots.push(this);
  }

  /** * @member {String} - Get the Track Type
   */
  get type() {
    return this.track.type;
  }

  /** * @member {Layout} - Get the *Layout*
   */
  get layout() {
    return this.track.layout;
  }

  /**
   * @member {String} - Get the position of the slot in relation to the backbone
   */
  get position() {
    if (this.track.position === 'both') {
      return (this.isDirect() ? 'outside' : 'inside');
    } else {
      return this.track.position;
    }
  }

  /** * @member {String} - Get the *Track* drawOrder
   */
  get drawOrder() {
    return this._track.drawOrder;
  }

  /** * @member {Boolean} - Return true if drawing by score
   */
  get drawByScore() {
    return this.drawOrder === 'score';
  }

  /**
   * @member {Boolean} - Is the slot position inside the backbone
   */
  get inside() {
    return this.position === 'inside';
  }

  /**
   * @member {Boolean} - Is the slot position outside the backbone
   */
  get outside() {
    return this.position === 'outside';
  }

  /**
   * @member {Viewer} - Get or set the track size as a proportion of the map thickness 
   * @private
   */
  get proportionOfMap() {
    return this._proportionOfMap;
  }

  set proportionOfMap(value) {
    this._proportionOfMap = value;
  }

  /**
   * @member {Viewer} - Get the track size as a ratio to all other tracks
   * @private
   */
  get thicknessRatio() {
    return this.track.thicknessRatio;
  }

  /**
   * @member {Number} - Get the current offset of the center of the slot from the backbone.
   */
  get bbOffset() {
    return this._bbOffset;
  }

  /**
   * @member {Number} - Get the current center offset of the center of the slot.
   */
  get centerOffset() {
    return this.bbOffset + this.viewer.backbone.adjustedCenterOffset;
  }

  /**
   * @member {Number} - Get the current thickness of the slot.
   */
  get thickness() {
    return this._thickness;
  }


  get strand() {
    return this._strand;
  }

  isDirect() {
    return this.strand === 'direct';
  }

  isReverse() {
    return this.strand === 'reverse';
  }

  get hasFeatures() {
    return this._features.length > 0;
  }

  get hasPlot() {
    return this._plot;
  }

  features(term) {
    return this._features.get(term);
  }

  replaceFeatures(features) {
    this._features = features;
    this.refresh();
  }

  /**
   * The number of pixels per basepair along the feature track circumference.
   * @return {Number}
   * @private
   */
  pixelsPerBp() {
    return this.layout.pixelsPerBp(this.centerOffset);
  }

  // Refresh needs to be called when new features are added, etc
  refresh() {
    this._featureNCList = new NCList(this._features, {circularLength: this.sequence.length, startProperty: 'mapStart', stopProperty: 'mapStop'});
  }

  /**
   * Get the visible range
   * @member {Range}
   */
  get visibleRange() {
    return this._visibleRange;
  }

  /**
   * Does the slot contain the given *centerOffset*.
   * @param {Number} offset - The centerOffset.
   * @return {Boolean}
   */
  containsCenterOffset(offset) {
    const halfthickness = this.thickness / 2;
    return (offset >= (this.centerOffset - halfthickness)) && (offset <= (this.centerOffset + halfthickness));
  }

  /**
   * Return the first feature in this slot that contains the given bp.
   * @param {Number} bp - the position in bp to search for.
   * @return {Feature}
   */
  findFeaturesForBp(bp) {
    return this._featureNCList.find(bp);
  }

  findLargestFeatureLength() {
    let length = 0;
    let nextLength;
    for (let i = 0, len = this._features.length; i < len; i++) {
      nextLength = this._features[i].length;
      if (nextLength > length) {
        length = nextLength;
      }
    }
    return length;
  }

  clear() {
    const range = this._visibleRange;
    if (range) {
      const centerOffset = this.centerOffset;
      const slotThickness = this.thickness;
      const ctx = this.canvas.context('map');
      ctx.globalCompositeOperation = 'destination-out'; // The existing content is kept where it doesn't overlap the new shape.
      this.canvas.drawElement('map', range.start, range.stop, centerOffset, 'white', slotThickness);
      ctx.globalCompositeOperation = 'source-over'; // Default
    }
  }

  highlight(color = '#FFB') {
    const range = this._visibleRange;
    if (range && this.visible) {
      const centerOffset = this.centerOffset;
      const slotThickness = this.thickness;
      this.canvas.drawElement('background', range.start, range.stop, centerOffset, color, slotThickness);
    }
  }

  draw(canvas, fast) {
    const slotCenterOffset = this.centerOffset;
    const slotThickness = this.thickness;
    const range = canvas.visibleRangeForCenterOffset(slotCenterOffset, slotThickness);
    this._visibleRange = range;
    if (range) {
      const start = range.start;
      const stop = range.stop;
      if (this.hasFeatures) {
        let featureCount = this._features.length;
        if (!range.isMapLength()) {
          featureCount = this._featureNCList.count(start, stop);
        }
        let step = 1;
        // Change step if drawing fast and there are too many features
        if (fast && featureCount > this.layout.fastFeaturesPerSlot) {
          // Use a step that is rounded up to the nearest power of 2
          // This combined with eachFromRange altering the start index based on the step
          // means that as we zoom, the visible features remain consistent.
          // e.g. When zooming all the features visible at a step of 16
          // will be visible when the step is 8 and so on.
          const initialStep = Math.ceil(featureCount / this.layout.fastFeaturesPerSlot);
          step = utils.base2(initialStep);
        }
        const showShading = fast ? false : undefined;
        // When drawing shadows, draw in reverse order to make them look better
        if (this.viewer.settings.showShading && this.isDirect()) { step *= -1; }

        // Draw Features
        if (this.drawByScore) {
          // Special case where we draw with features sorted by score (draw highest score last)
          const sortedFeatures = this._featureNCList.find(start, stop, step).sort((a, b) => (a.score - b.score) || (a.length - b.length) );
          for (const feature of sortedFeatures) {
            feature.draw('map', slotCenterOffset, slotThickness, range, {showShading: showShading});
          }
        } else {
          // Draw by position (more efficient)
          this._featureNCList.run(start, stop, step, (feature) => {
            feature.draw('map', slotCenterOffset, slotThickness, range, {showShading: showShading});
          });
        }

        // Debug
        if (this.viewer.debug && this.viewer.debug.data.n) {
          const index = this.viewer.slots().indexOf(this);
          this.viewer.debug.data.n[`slot_${index}`] = featureCount;
        }
      } else if (this.hasPlot) {
        this._plot.draw(canvas, slotCenterOffset, slotThickness, fast, range);
      }
    }
  }

  drawProgress(progress) {
    const canvas = this.canvas;
    const centerOffset = this.centerOffset;
    const slotThickness = this.thickness;
    const range = this._visibleRange;
    // Draw progress like thickening circle
    if (progress > 0 && progress < 100 && range) {
      const thickness = slotThickness * progress / 100;
      canvas.drawElement('background', range.start, range.stop, centerOffset, '#EAEAEE', thickness, 'arc', false);
    }
  }

  /**
   * Remove a feature or array of features from the slot.
   * @param {Feature|Array} features - The Feature(s) to remove.
   * @private
   */
  removeFeatures(features) {
    features = (features.toString() === 'CGArray') ? features : new CGArray(features);
    this._features = this._features.filter( f => !features.includes(f) );
    this.refresh();
  }

  /**
   * Remove the plot from the slot.
   * @private
   */
  removePlot() {
    this._plot = undefined;
    this.refresh();
  }


}

export default Slot;