Source: Contig.js

//////////////////////////////////////////////////////////////////////////////
// Contig
//////////////////////////////////////////////////////////////////////////////

import CGObject from './CGObject';
import CGArray from './CGArray';
import CGRange from './CGRange';
import Sequence from './Sequence';
import Color from './Color';
import utils from './Utils';

/**
 * The Contig class contains details for a single contig.
 *
 * ### Action and Events
 *
 * Action                                     | Sequence Method                                | Contig Method       | Event
 * -------------------------------------------|------------------------------------------------|---------------------|-----
 * [Add](../docs.html#adding-records)         | [addContigs()](Sequence.html#addContigs)       | -                   | contigs-add
 * [Update](../docs.html#updating-records)    | [updateContigs()](Sequence.html#updateContigs) | [update()](#update) | contigs-update
 * [Remove](../docs.html#removing-records)    | [removeContigs()](Sequence.html#removeContigs) | [remove()](#remove) | contigs-remove
 * [Reorder](../docs.html#reordering-records) | [moveContig()](Sequence.html#moveContig)       | [move()](#move)     | contigs-reorder
 * [Read](../docs.html#reading-records)       | [contigs()](Sequence.html#contigs)             | -                   | -
 *
 * <a name="attributes"></a>
 * ### Attributes
 *
 * Attribute                        | Type      | Description
 * ---------------------------------|-----------|------------
 * [name](#name)                    | String    | Contig name.
 * [seq](#seq)<sup>iu</sup>         | String    | The contig sequence.
 * [length](#length)<sup>iu</sup>   | Number    | The length of the sequence. This is ignored if a seq is provided.
 * [orientation](#orientation)      | String    | '+' for forward orientation and '-' for the reverse.
 * [color](#color)                  | Color     | A string describing the color [Default: 'black']. See {@link Color} for details.
 * [visible](CGObject.html#visible) | Boolean   | Contig is visible [Default: true].
 * [meta](CGObject.html#meta)       | Object    | [Meta data](../tutorials/details-meta-data.html)
 * 
 * <sup>iu</sup> Ignored on Contig update
 *
 * @extends CGObject
 */
class Contig extends CGObject {

  /**
   * Create a Contig
   * @param {Viewer} viewer - The viewer
   * @param {Object} options - [Attributes](#attributes) used to create the contig
   * @param {Object} [meta] - User-defined [Meta data](../tutorials/details-meta-data.html) to add to the contig
   */
  constructor(sequence, options = {}, meta = {}) {
    super(sequence.viewer, options, meta);
    this._sequence = sequence;
    this._viewer = sequence.viewer;

    // this.id = utils.defaultFor(options.id, this.cgvID);
    // this.name = utils.defaultFor(options.name, this.id);
    this.name = utils.defaultFor(options.name, 'Unknown');
    this.orientation = utils.defaultFor(options.orientation, '+');
    this.seq = options.seq;
    this.color = options.color;
    this._features = new CGArray();
    this._updateLengthOffset(0);

    if (!this.seq) {
      this.length = options.length;
    }
    if (!this.length) {
      console.error(`Contig '${this.name}'  has no sequence or length set!`)
    }

  }

  //////////////////////////////////////////////////////////////////////////
  // STATIC
  //////////////////////////////////////////////////////////////////////////

  /**
   * Removes supplied features from their contigs
   * @private
   */
  static removeFeatures(features) {
    features = CGArray.arrayerize(features);
    if (features.length === 0) { return }
    const viewer = features[0].viewer;
    const contigMap = {};
    for (const feature of features) {
      const cgvID = feature.contig && feature.contig.cgvID;
      if (cgvID) {
        contigMap[cgvID] ? contigMap[cgvID].push(feature) : contigMap[cgvID] = [feature];
      }
    }
    const cgvIDs = Object.keys(contigMap);
    for (const cgvID of cgvIDs) {
      const contig = viewer.objects(cgvID);
      contig._features = contig._features.filter ( f => !contigMap[cgvID].includes(f) );
    }
  }

  //////////////////////////////////////////////////////////////////////////
  // MEMBERS
  //////////////////////////////////////////////////////////////////////////

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

  /**
   * @member {String} - Get the sequence.
   */
  get sequence() {
    return this._sequence;
  }

  // /**
  //  * @member {String} - Get or set the contig ID. Must be unique for all contigs
  //  */
  // get id() {
  //   return this._id;
  // }
  //
  // set id(value) {
  //   // TODO: Check if id is unique
  //   this._id = value;
  // }

  /**
   * @member {String} - Get or set the contig name.
   * 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'.
   * Empty names will be changed to 'Unknown'.
   */
  get name() {
    return this._name;
  }

  set name(value) {
    // this._name = value;
    let valueString = `${value}`;
    if (valueString == "") {
      valueString = 'Unknown';
    }
    const allNames = this.sequence._contigs.map( i => i.name);
    this._name = utils.uniqueName(valueString, allNames)
    if (this._name !== valueString) {
      console.log(`Contig with name '${valueString}' already exists, using name '${this._name}' instead.`)
    }
  }

  /**
   * @member {Number} - Returns true if this contig is the mapContig
   */
  get isMapContig() {
    return (this.sequence.mapContig === this);
  }

  /**
   * @member {Number} - Get the contig index (base-1) in relation to all the other contigs.
   */
  get index() {
    return this._index;
  }

  /**
   * @member {String} - Get or set the contig orientation. Value must be '+' or '-'.
   *   Flipping the orienation will reverse complement the contig sequence and
   *   adjust all the features on this contig.
   */
  get orientation() {
    return this._orientation;
  }

  set orientation(value) {
    const validKeys = ['-', '+'];
    if (!utils.validate(value, validKeys)) { return; }
    if (this._orientation && (value !== this._orientation)) {
      this.reverseFeatureOrientations();
    }
    if (this.seq) {
      this.seq = this.reverseComplement();
    }
    this._orientation = value;
    // FIXME: reverse complement the sequence
  }

  /**
   * @member {String} - Get or set the seqeunce.
   */
  get seq() {
    return this._seq;
  }

  set seq(value) {
    this._seq = value;
    if (this._seq) {
      this._seq = this._seq.toUpperCase();
      this._length = value.length;
      // TODO: check if features still fit, if the length is reduced
    }
  }

  /**
   * @member {Number} - Get or set the sequence length. If the *seq* property is set, the length can not be adjusted.
   */
  get length() {
    return this._length;
  }

  set length(value) {
    if (value) {
      if (!this.seq) {
        this._length = Number(value);
        this.sequence._updateScale();
        // TODO: check if features still fit, if the length is reduced
      } else {
        console.error('Can not change the sequence length if *seq* is set.');
      }
    }
  }

  /**
   * @member {Number} - Get the length of all the contigs before this one.
   */
  get lengthOffset() {
    return this._lengthOffset;
  }

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

  set color(color) {
    if (color === undefined) {
      this._color = undefined;
    } else if (color.toString() === 'Color') {
      this._color = color;
    } else {
      this._color = new Color(color);
    }
  }

  /**
   * @member {CGRange} - Get the range of the contig in relation to the entire map.
   *   The range start is the total length of the contigs before this one plus 1.
   *   The range stop is the total length of the contigs before this one plus this contigs length.
   */
  get mapRange() {
    // FIXME: this need to be stored better
    // return this._mapRange;
    return new CGRange(this.sequence.mapContig, this.lengthOffset + 1, this.lengthOffset + this.length);
  }

  /**
   * @member {Number} - Get the start position (bp) of the contig in relation to the entire map.
   *   The start is the total length of the contigs before this one plus 1.
   */
  get mapStart() {
    return this.mapRange.start;
  }

  /**
   * @member {Number} - Get the stop position (bp) of the contig in relation to the entire map.
   *   The stop is the total length of the contigs before this one plus this contigs length.
   */
  get mapStop() {
    return this.mapRange.stop;
  }

  /**
   * Updates the lengthOffset for this contig and also update the mapRange.
   * @param {length} - Total length of all the contigs before this one.
   * @private
   */
  _updateLengthOffset(length) {
    this._lengthOffset = length;
    // this._mapRange = new CGV.CGRange(this.sequence.mapContig, length + 1, length + this.length);
  }

  /**
   * Reverse complement the sequence of this contig
   */
  reverseComplement() {
    return Sequence.reverseComplement(this.seq);
  }

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

  /**
   * @member {Boolean} - Return true of this contig has a sequence
   */
  get hasSeq() {
    return typeof this.seq === 'string';
  }

  /**
   * Returns an [CGArray](CGArray.html) of Features or a single Feature from all the features on this Contig.
   * @param {Integer|String|Array} term - See [CGArray.get](CGArray.html#get) for details.
   * @return {CGArray}
   */
  features(term) {
    return this._features.get(term);
  }

  /**
   * Remove the Contig from the Sequence
   */
  remove() {
    this.sequence.removeContigs(this);
  }

  /**
   * Move this contig to a new index in the array of Sequence contigs.
   * @param {Number} newIndex - New index for this caption (0-based)
   */
  move(newIndex) {
    const currentIndex = this.sequence.contigs().indexOf(this);
    this.sequence.moveContig(currentIndex, newIndex);
  }

  /**
   * Zoom and pan map to show the contig
   * @param {Number} duration - Length of animation
   * @param {Object} ease - The d3 animation ease [Default: d3.easeCubic]
   */
  moveTo(duration, ease) {
    if (this.mapRange.isMapLength()) {
      this.viewer.reset(duration, ease);
    } else {
      const buffer = Math.ceil(this.length * 0.05);
      const start = this.sequence.subtractBp(this.mapStart, buffer);
      const stop = this.sequence.addBp(this.mapStop, buffer);
      this.viewer.moveTo(start, stop, {duration, ease});
    }
  }

  /**
   * Reverse the orientations of the features on this contig
   * @private
   */
  reverseFeatureOrientations() {
    const updates = {};
    for (let i = 0, len = this._features.length; i < len; i++) {
      const feature = this._features[i];
      updates[feature.cgvID] = {
        start: this.length - feature.stop + 1,
        stop: this.length - feature.start + 1,
        strand: -(feature.strand)
      };
    }
    this.viewer.updateFeatures(updates);
  }

  /**
   * Return the sequence for a range on this contig
   * @param {Range} range - The range of the sequence
   * @param {Boolean} revComp - If true, returns the reverse complement sequence
   * @private
   */
  forRange(range, revComp) {
    return Sequence.forRange(this.seq, range, revComp);
  }

  /**
   * Returns sequence of this contig in fasta format
   */
  asFasta() {
    return `>${this.name}\n${this.seq}`;
  }

  /**
   * Highlight the contig.
   * @param {Color} color - Color of the highlight
   * @private
   */
  highlight(color) {
    const backbone = this.viewer.backbone;
    const canvas = this.viewer.canvas;
    const visibleRange = backbone.visibleRange;
    let highlightColor;
    if (color) {
      highlightColor = new Color(color);
    } else {
      let origColor = (this.index % 2 === 0) ? backbone.color : backbone.colorAlternate;
      if (this.color) {
        origColor = this.color;
      }
      highlightColor = origColor.copy();
      highlightColor.highlight();
    }
    if (this.visible) {
      const start = this.sequence.bpForContig(this);
      const stop = this.sequence.bpForContig(this, this.length);
      this.viewer.canvas.drawElement('ui', start, stop, backbone.adjustedCenterOffset, highlightColor.rgbaString, backbone.adjustedThickness, backbone.directionalDecorationForContig(this));
    }
  }

  /**
   * Returns JSON representing the object
   */
  toJSON(options = {}) {
    const json = {
      // id: this.id,
      name: this.name,
      orientation: this.orientation,
      length: this.length,
      color: this.color && this.color.rgbaString,
      // visible: this.visible
    };
    if (this.hasSeq) {
      json.seq = this.seq;
    }
    // Optionally add default values
    // Visible is normally true
    if (!this.visible || options.includeDefaults) {
      json.visible = this.visible;
    }
    return json;
  }

}

export default Contig;