graphics3d/mesh/genomeScene.js

/*
 * @Description:
 * @Author: Hongpeng Ma
 * @Github: gitlab.com/hongpengm
 * @Date: 2019-03-27 22:18:23
 * @LastEditTime: 2019-04-14 01:33:27
 */
'use strict'
import { Color, Object3D, Vector3 } from 'three'
import { LineScene } from './lineScene'
import { ExtrudeScene } from './extrudeScene'
import { GeneticElement } from './geneticElement'
import { argsParser } from '../../utils/args'
import { hColor } from '../../utils/color'

const EventEmitter = require('events').EventEmitter
const uidv4 = require('uuid/v4')
const GENOME_SCENE_SKELETON_TYPE = 'line' // ['line', 'tube']

/**
 *
 *
 * @class GenomeScene
 * @extends {EventEmitter}
 */
class GenomeScene extends EventEmitter {
  /**
   *Creates an instance of GenomeScene.
   * @constructor
   * @param {Graphics3DApplication} app - parent application
   * @param {chrom3DModel} [chrom3DModel=undefined] - chrome data
   * @memberof GenomeScene
   * @property {Object} chroms - {chromkey:{mesh, color, highlight,},...}
   * @property {chrom3DModel} data - data of genome scene
   * @property {THREE.Object3D} baseObject - base object, all chromosome objects are appended to this object
   * @property {Array} _updateFunctions - [function] collection of update functions in mesh.
   */
  constructor (app, chrom3DModel, args) {
    super()
    let optionsDefault = {
      skeletonType: GENOME_SCENE_SKELETON_TYPE
    }
    const parsedArgs = argsParser(args, {
      options: optionsDefault
    })
    let { options } = parsedArgs
    this.id = uidv4()
    this.options = options
    this.subscribe = []
    this.subscribers = []
    this._updateFunctions = []

    let highlightColor = new Color()
    let chroms = {}

    // baseObject.add(chroms['1'].getLine());

    this.baseObject = new Object3D()
    this.chroms = chroms
    this.allChromsVisible = true
    this.app = app
    this.data = chrom3DModel
  }

  /**
   * Set data
   * @param {chrom3DModel} x - new data that will replace current one
   * @memberof GenomeScene
   */
  set chromData (x) {
    this.data = x
  }

  /**
   *
   * @return {chrom3DModel}
   * @readonly
   * @memberof GenomeScene
   */
  get chromData () {
    return this.data
  }

  /**
   * Set resolution params, may update line materials
   *
   * @param {number} width - viewport width
   * @param {number} height - viewport height
   * @memberof GenomeScene
   */
  setResolution (width, height) {
    this.resWidth = width
    this.resHeight = height
  }

  /**
   * return an object that can be rendered in THREE.scene
   *
   * @return {THREE.Object3D}
   * @memberof GenomeScene
   */
  sceneObject () {
    return this.baseObject
  }

  /**
   * Generate genome mesh, making an offset and add the mesh to base object.
   *
   * @memberof GenomeScene
   */
  loadGenomeMesh () {
    let color = new Color()
    // i := chromeKey.index
    for (let i = 0, l = this.data.getChromKeys().length; i < l; i++) {
      let _chromKey = this.data.getChromKeys()[i]
      color.setHSL(i / l, 0.8, 0.7)
      let chr_
      switch (this.options.skeletonType) {
        case 'line':
          chr_ = new LineScene(this.data.getChromPositions(_chromKey), color)
          chr_.setResolution(this.resWidth, this.resHeight)
          this._updateFunctions.push(chr_.updateFunctions())
          break
        case 'tube':
          chr_ = new ExtrudeScene(this.data.getChromPositions(_chromKey), color, {
	  options: {
	    shape: 'circle',
	    radius: 1,
	    shapeDivisions: 4
	  }
          })
          break
        default:
      }

      this.chroms[_chromKey] = {
        line: chr_,
        color: color.clone(),
        color255: new hColor(color).rgb255,
        visible: true,
        highlight: undefined
      }
      this.baseObject.add(chr_.mesh)
    }
    this.moveToCenter()
    this.updateAppGUI()
  }

  /**
   * add genome scene to a renderable scene. If not designated, will add to current parent application's scene.
   *
   * @param {THREE.scene} [scene=undefined] - THREE.scene to add
   * @memberof GenomeScene
   * @return {GenomeScene} return current object
   */
  addToScene (scene = undefined) {
    // if scene exist, add this base obj to designated scene
    if (scene !== undefined) {
      scene.add(this.baseObject)
    } else { // else add to this's parent app's scene
      this.app.scene.add(this.baseObject)
    }
    return this
  }

  getGenomeOffset () {
    // Calculate the avg center of genome and move the genome object to center
    const data = this.data
    let len = 0
    let x = new Vector3()
    Object.values(data.data).forEach((e) => {
      len += e.length
      e.forEach((e_) => {
        let point_ = e_.slice(1)
        x.add(new Vector3(point_[0],
			  point_[1],
			  point_[2]))
      })
    })
    // this genome's average offset
    x.divideScalar(len)
    this.offset = x
  }
  /**
   * Move the genome object to (0, 0, 0)
   *
   * @memberof GenomeScene
   */
  moveToCenter () {
    this.getGenomeOffset()
    // Compensate the offset
    this.baseObject.position.sub(this.offset)
  }
  loadTestGeneticElements () {
    /*
      Data format
      --------------------
      Chrom start end color
      1     0.5   0.7 0x445500
    */
    let testData = [{ chrom: '1', start: 0.51, end: 0.512, color: 0x445500 },
      { chrom: '1', start: 0.54, end: 0.545, color: 0x445500 }]
    let updatedData = []
    let testObject = new Object3D()
    testData.forEach(d => {
      let start = this.chroms[d.chrom].line.getPoint(d.start)
      let end = this.chroms[d.chrom].line.getPoint(d.end)
      updatedData.push({
        chrom: d.chrom,
        local: this.chroms[d.chrom].line.getPoint((d.start + d.end) / 2),
        lookAt: (new Vector3((end.x - start.x),
			     (end.y - start.y),
			     (end.z - start.z))).normalize(),
        color: new Color(d.color)
      })
    })
    console.log(updatedData)
    let geMesh = new GeneticElement(updatedData)
    console.log(this.offset)
//    geMesh.mesh.position.add(this.offset)
    this.baseObject.add(geMesh.mesh)
    // console.log(this.getChromPositions('1'))
  }
  /**
   * Highlight part of a chromosome, actually this function adds a extrude line.
   *
   * @param {*} chromKey
   * @param {number} start - [0-1]
   * @param {number} end - [0-1]
   * @param {THREE.Color} [color=undefined] color of the new extrude mesh.
   * @memberof GenomeScene
   */
  setChromHighlight (chromKey, start, end, color = undefined) {
    let chromPositions = this.getChromPositions(chromKey, start, end)
    if (color === undefined) {
      color = this.chroms[chromKey].color
    }
    let highlightLine = new ExtrudeScene(chromPositions, color, {
      options: {
        shape: 'circle',
        radius: 1.5,
        shapeDivisions: 20
      }
    })
    this.chroms[chromKey].highlight = highlightLine
    if (this.chroms[chromKey].visible === true) {
      this.baseObject.add(highlightLine.mesh)
    }
  }
  highlightChroms (highlightOptions) {
    Object.keys(highlightOptions).forEach(chr => {
      const options = highlightOptions[chr]
      const start = options.start
      const end = options.end
      const color = options.color
      this.setChromHighlight(chr, start, end, color)
    })
  }
  removeAllHighlightChroms () {
    const chroms = this.chroms
    Object.keys(chroms).forEach(chr => {
      const sceneObject = chroms[chr].highlight
      if (sceneObject !== undefined) {
        this.baseObject.remove(sceneObject.mesh)
        sceneObject.mesh.geometry.dispose()
        sceneObject.mesh.material.dispose()
      }
    })
  }
  /**
   * get chromosome's positions points array.
   *
   * @param {*} chromKey
   * @param {number} [start=0]
   * @param {number} [end=1]
   * @return {Array} [THREE.Vector3] path points array.
   * @memberof GenomeScene
   */
  getChromPositions (chromKey, start = 0, end = 1) {
    let chrom = this.chroms[chromKey]
    let len = chrom.line.length
    return chrom.line.points.slice(Math.round(start * len),
				   Math.round(end * len))
  }
  updateAppGUI () {
    const { app } = this
    const gui = app.gui
    console.log(this.chroms)
    // Add Chromosome Specific Visible Checkbox
    Object.keys(this.chroms).forEach(k => {
      gui.__folders['Chromosomes']
        .add(this.chroms[k], 'visible')
        .name('Chrom ' + k)
        .onChange(() => {
          this.updateVisibility()
        })
    })
    console.log(gui)
    // Add Toggle All Chromosomes
    gui.__folders['Chromosomes']
      .add(this, 'allChromsVisible')
      .name('Toggle All')
      .onChange(() => {
        this.chromsForEach((d) => {
	  d.visible = !d.visible
        })
        this.updateVisibility()
        Object.keys(gui.__folders).forEach(folder => {
	  gui.__folders[folder].__controllers.forEach(c => {
            {
	    c.updateDisplay()
	  }
          })
        })
      })
    // Add Chromosome Specific Color Checkbox
    Object.keys(this.chroms).forEach(k => {
      gui.__folders['Chrom Colors']
        .addColor(this.chroms[k], 'color255')
        .name(k + 'Color')
        .onChange(() => {
	  this.updateColor255()
        })
    })
  }
  updateVisibility () {
    Object.keys(this.chroms).forEach(k => {
      const d = this.chroms[k]
      d.line.mesh.visible = d.visible
      if (d.highlight) {
        d.highlight.mesh.visible = d.visible
      }
    })
  }
  updateColor255 () {
    Object.keys(this.chroms).forEach(k => {
      const d = this.chroms[k]
      // Compare color and color 255
      console.log(d)
      let colorConvert = new hColor(d.color).rgb255
      if (d.color255.r !== colorConvert.r ||
	  d.color255.r !== colorConvert.g ||
	  d.color255.r !== colorConvert.b
	 ) {
        // If not equal, set color to color 255
        d.color.r = d.color255.r / 255
        d.color.g = d.color255.g / 255
        d.color.b = d.color255.b / 255
        // update material
        d.line.mesh.material.color.set(d.color)
      }
    })
  }
  updateFunctions () {
    return this._updateFunctions
  }

  respondEvents (e) {
    console.log('Genome Scene Received', e)
    switch (e.sourceEvent) {
      case 'selectionEnds':
        console.log(e)
        const chromKeys = this.data.getChromKeys()
        const chromSelection = [e.selection[0][0] / e.context.width * chromKeys.length,
			      e.selection[1][0] / e.context.width * chromKeys.length]
        let select = [[Math.floor(chromSelection[0]),
		     chromSelection[0] - Math.floor(chromSelection[0])],
		    [Math.floor(chromSelection[1]),
		     chromSelection[1] - Math.floor(chromSelection[1])]]
        let highlightOptions = {}
        if (select[0][0] === select[0][1]) {
          highlightOptions[chromKeys[select[0][0]]] = {
            start: select[0][1],
            end: select[1][1]
          }
        } else {
          for (let i = select[0][0]; i <= select[1][0]; i++) {
            if (i === select[0][0]) {
              highlightOptions[chromKeys[i]] = {
                start: select[0][1],
                end: 1
              }
            } else if (i === select[1][0]) {
              highlightOptions[chromKeys[i]] = {
                start: 0,
                end: select[1][1]

              }
            } else {
              highlightOptions[chromKeys[i]] = {
                start: 0,
                end: 1
              }
            }
          }
        }
        console.log(highlightOptions)
        this.removeAllHighlightChroms()
        this.highlightChroms(highlightOptions)
        break
      default:
        break
    }
  }
  /// //////////////////////////////////////////////////////////////////////////
  //                              Event Handling                             //
  /// //////////////////////////////////////////////////////////////////////////
  addSubs (sub, type, cb) {
    sub.on(type, cb)
    sub.subscribe.push({
      this: this,
      type: type,
      callback: cb
    })
    this.subscribers.push({
      'type': type,
      'subscriber': sub
    })
  }
  emitSubs (type, ...args) {
    this.subscribers.forEach(sub => {
      if (sub.type === type) {
        console.log([type].concat(args))
        console.log(sub.subscriber)
        sub.subscriber.emit(...[type].concat(args))
        // Above line is equal to below ES5 one
        // sub.subscriber.emit.apply(sub.subscriber, [type].concat(args))
      }
    })
  }
  /// //////////////////////////////////////////////////////////////////////////
  //                                  Utils                                  //
  /// //////////////////////////////////////////////////////////////////////////
  chromsForEach (f) {
    Object.keys(this.chroms).forEach((chromKey) => {
      f(this.chroms[chromKey])
    })
  }
}
export { GenomeScene }