graphics2d/heatmap2DTrack.js

/*
 * @Description:
 * @Author: Hongpeng Ma
 * @Github: gitlab.com/hongpengm
 * @Date: 2019-03-29 02:24:30
 * @LastEditTime: 2019-04-14 02:08:56
 */
import * as PIXI from 'pixi.js'
import { HiCEvent } from '../events/events'
const d3 = require('d3')
const uidv4 = require('uuid/v4')
const EventEmitter = require('events').EventEmitter
const HEATMAP2D_CANVAS_CLASS = 'heatmap_2d_track_canvas'
const HEATMAP2D_SVG_CLASS = 'heatmap_2d_track_svg'
const HEATMAP2D_BRUSH_CLASS = 'heatmap_2d_track_brush'
const HEATMAP2D_ZOOM_EVENT = 'g2dzoom'

/**
 *
 *
 * @class Heatmap2DTrack
 * @extends {EventEmitter}
 */
class Heatmap2DTrack extends EventEmitter {
  
  /**
   *Creates an instance of Heatmap2DTrack.
   * @param {*} parentDOM
   * @memberof Heatmap2DTrack
   */
  constructor (parentDOM) {
    super()
    this.initBBox(parentDOM)
    this.app = new PIXI.Application(this.width, this.height,
      {
        backgroundColor: 0xffffff
      })

    this.responsiveSVG = d3.select(this.baseDOM).append('svg')
      .style('z-index', 2)
      .attr('width', this.width)
      .attr('height', this.height)
      .attr('class', HEATMAP2D_SVG_CLASS)
      .attr('id', uidv4())

    this.app.view.className = HEATMAP2D_CANVAS_CLASS
    this.app.view.id = uidv4()
    this.canvas = this.app.view

    // Be sure the PIXI is rendered below responsive svg
    this.baseDOM.appendChild(this.app.view)

    this.sprite = undefined

    this.initBehaviourHandler()
    this.external_behaviour_function = []
    this.subscribe = []
    this.subscribers = []
    this.emitEventType = undefined
    if (Heatmap2DTrack.instances === undefined) {
      Heatmap2DTrack.instances = []
    }
    Heatmap2DTrack.instances.push(this)
  }

  /// //////////////////////////////////////////////////////////////////////////
  //                                   Init                                  //
  /// //////////////////////////////////////////////////////////////////////////
  /**
   * Init 2D Track bounding box
   *
   * @param {*} parentDOM
   * @memberof Heatmap2DTrack
   */
  initBBox (parentDOM) {
    let parentBBox = parentDOM.getBoundingClientRect()
    let parentHeight = parentBBox.height
    let parentWidth = parentBBox.width
    this.width = parentWidth
    this.height = parentHeight
    let base = document.createElement('div')
    d3.select(base)
      .attr('id', uidv4())
      .attr('width', this.width + 'px')
      .attr('height', this.height + 'px')
      .style('position', 'absolute')
    this.baseDOM = base
    parentDOM.appendChild(this.baseDOM)
  }

  /// //////////////////////////////////////////////////////////////////////////
  //                               Manipulate Sprite                           //
  /// //////////////////////////////////////////////////////////////////////////

  /**
   * Get Sprite
   *
   * @return {PIXI.Sprite}
   * @memberof Heatmap2DTrack
   */
  getSprite () {
    return this.sprite
  }

  
  // Set heatmap's image's transform
  /**
   * Transform the Sprite
   *
   * @param {Object} transform
   * @memberof Heatmap2DTrack
   */
  setSpriteTransform (transform) {
    if (this.sprite !== undefined) {
      // this.sprite.scale.set(this.sprite.load_scale.x * transform.k, this.sprite.load_scale.y * transform.k)
      // this.sprite.position.set(this.sprite.load_scale.x * transform.x, this.sprite.load_scale.y * transform.y)
      this.sprite.scale.set(transform.k)
      this.sprite.position.set(transform.x, transform.y)
    }
  }

  /**
   * Load image
   *
   * @param {String} url
   * @memberof Heatmap2DTrack
   */
  loadImgURL (url) {
    this.texture = PIXI.Texture.from(url)
    this.texture.trim = new PIXI.Rectangle(0, 0, this.width, this.height)
    this.texture.updateUvs()
    // console.log(this.texture.trim)
    this.sprite = new PIXI.Sprite(this.texture)
    this.sprite.x = 0
    this.sprite.y = 0
    this.sprite.interactive = true
    this.sprite.buttonMode = true
    // this.sprite.load_scale = this.sprite.scale.clone()
    this.app.stage.addChild(this.sprite)
    // console.log(this.sprite)
  }

  /**
   *
   *
   * @param {*} transform
   * @param {boolean} [emitEvents=true]
   * @memberof Heatmap2DTrack
   */
  applyTransform (transform, emitEvents = true) {
    this.setSpriteTransform(transform)
    if (emitEvents) {
      let emitSubEvent = new HiCEvent()
      emitSubEvent.attr('transform', transform)
      emitSubEvent.attr('sourceEvent', 'transform')
      emitSubEvent.attr('sourceObject', this)
      this.emitSubs('transform', emitSubEvent)
    }
  }

  /**
   *
   *
   * @param {HiCEvent} e
   * @memberof Heatmap2DTrack
   */
  respondEvents (e) {
    if (this !== e.sourceObject) {
      switch (e.sourceEvent) {
        case 'transform':
          this.applyTransform(e.transform)
          break
        case 'selection':
          // 2D situation
          if (Array.isArray(e.selection[0])) {

          } else { // 1D situation
            if (e.selectionType === 'x') {
              const eSelection = [e.selection[0] / e.context.width,
                e.selection[1] / e.context.width]
              let selection = [[Math.round(eSelection[0] * this.width), 0],
                [Math.round(eSelection[1] * this.width), this.height]]

              d3.brush().move(this.brush_g, selection)
            }
          }
          break
        default:
          break
      }
    }
  }
  /// //////////////////////////////////////////////////////////////////////////
  //                                Behaviour                                //
  /// //////////////////////////////////////////////////////////////////////////
  
  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  initBehaviourHandler () {
    this.zoom_handler = d3.zoom()
      .scaleExtent([1, 8])
      .on('zoom', this.zoomed)
    this.brush_handler = d3.brush()
      .extent([[0, 0], [this.width, this.height]])
      .on('brush start', this.brushed)
      .on('end', this.brushended)
    this.behaviour = undefined
  }

  /**
   *
   *
   * @param {String} behav
   * @memberof Heatmap2DTrack
   */
  addBehaviour (behav) {
    switch (behav) {
      case 'zoom':
        this.addZoomBehaviour()
        break
      case 'brush':
        this.addBrushBehaviour()
        break
      default:
        break
    }
  }

  /**
   *
   *
   * @param {*} caller
   * @param {*} handler
   * @memberof Heatmap2DTrack
   */
  callBehaviour (caller, handler) {
    caller.call(handler)
  }

  /**
   *
   *
   * @param {String} behav
   * @memberof Heatmap2DTrack
   */
  switchBehaviour (behav) {
    this.removeBehaviour()
    this.addBehaviour(behav)
  }

  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  removeBehaviour () {
    switch (this.behaviour) {
      case undefined:
        break
      case 'brush':
        this.brush_g.on('.brush', null)
        break
      case 'zoom':
        d3.select(this.canvas)
          .on('.zoom', null)
        break
      default:

        console.log('Current behaviour is unknown\nCurrent type is:', this.behaviour)
        break
    }
    this.emitEventType = undefined
  }

  /**
   *
   *
   * @param {String} behav
   * @returns {Function}
   * @memberof Heatmap2DTrack
   */
  getBehaviourCallback (behav) {
    // TODO
    switch (behav) {
      case 'zoom':
        return this.zoomedCallback
      default:
        break
    }
  }

  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  addBrushBehaviour () {
    this.removeBehaviour()
    this.behaviour = 'brush'
    this.brush_g = this.responsiveSVG.append('g')
      .attr('class', HEATMAP2D_BRUSH_CLASS)
      .attr('id', uidv4())
    this.callBehaviour(this.brush_g, this.brush_handler)
  }

  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  addZoomBehaviour () {
    this.removeBehaviour()
    // console.log(this.responsiveSVG.zIndex, this.canvas.zIndex)
    this.behaviour = 'zoom'
    this.emitEventType = HEATMAP2D_ZOOM_EVENT
    let canvas = d3.select(this.canvas)
    this.callBehaviour(canvas, this.zoom_handler)
    // canvas.call(this.zoom_handler)
  }
  // Brush Behaviour //////////////////////////////////////////////////////////

  // brush actions

  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  brushed () {
    const track = Heatmap2DTrack.findInstanceByBrush(this)
    this.brushSelection = d3.event.selection
    let emitSubEvent = new HiCEvent()
    emitSubEvent
      .attr('selection', d3.event.selection)
      .attr('transform', {
        k: track.sprite.scale.x,
        x: track.sprite.position.x,
        y: track.sprite.position.y
      })
      .attr('context', {
        width: track.width,
        height: track.height })
      .attr('sourceEvent', 'selection')
    //  track.setSpriteTransform(d3.event.transform)
    track.emitSubs('selection', emitSubEvent)
  }

  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  brushended () {
    const track = Heatmap2DTrack.findInstanceByBrush(this)
    let emitSubEvent = new HiCEvent()

    emitSubEvent
      .attr('selection', this.brushSelection)
      .attr('transform', {
        k: track.sprite.scale.x,
        x: track.sprite.position.x,
        y: track.sprite.position.y
      })
      .attr('context', {
        width: track.width,
        height: track.height })
      .attr('sourceEvent', 'selectionEnds')
      //  track.setSpriteTransform(d3.event.transform)
    track.emitSubs('selectionEnds', emitSubEvent)

  }

  /**
   *
   *
   * @param {HiCEvent} hicE
   * @memberof Heatmap2DTrack
   */
  brushedCallback (hicE) {
    d3.brush().move(this.brush_g, hicE.selection)
  }

  // Zoom Behaviour ///////////////////////////////////////////////////////////

  // zoom actions

  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  zoomed () {
    // `this` in the context is the listener binded canvas
    const track = Heatmap2DTrack.findInstanceByCanvas(this)
    track.applyTransform(d3.event.transform)
    // call external behaviour
    let emitSubEvent = new HiCEvent()
    emitSubEvent
      .attr('transform', d3.event.transform)
      .attr('sourceEvent', HEATMAP2D_ZOOM_EVENT)
    track.emitSubs(HEATMAP2D_ZOOM_EVENT, emitSubEvent)
  }

  /**
   *
   *
   * @param {HiCEvent} hicE
   * @memberof Heatmap2DTrack
   */
  zoomedCallback (hicE) {
    this.applyTransform(hicE.transform)
  }

  /// //////////////////////////////////////////////////////////////////////////
  //                             Event Management                            //
  /// //////////////////////////////////////////////////////////////////////////
  
  /**
   *
   *
   * @param {*} sub
   * @param {*} type
   * @param {*} cb
   * @memberof Heatmap2DTrack
   */
  addSubs (sub, type, cb) {
    sub.on(type, cb)
    sub.subscribe.push({
      this: this,
      type: type,
      callback: cb
    })
    this.subscribers.push({
      'type': type,
      'subscriber': sub
    })
  }

  /**
   *
   *
   * @param {*} type
   * @param {*} args
   * @memberof Heatmap2DTrack
   */
  emitSubs (type, ...args) {
    this.subscribers.forEach(sub => {
      if (sub.type === type) {
        // ES6 hacky apply
        sub.subscriber.emit(...[type].concat(args))
        // Above line is equal to below ES5 one
        // sub.subscriber.emit.apply(sub.subscriber, [type].concat(args))
      }
    })
  }

  /// //////////////////////////////////////////////////////////////////////////
  //                                 Destroy                                 //
  /// //////////////////////////////////////////////////////////////////////////
  // this function is to destroy the obj to prevent memory leak
  
  /**
   *
   *
   * @memberof Heatmap2DTrack
   */
  destroy () {
    this.app.destroy()
    let i = 0
    while (Heatmap2DTrack.instances[i] !== this) { i++ }
    Heatmap2DTrack.instances.splice(i, 1)
  }
}

// Loop through heatmap2dtrack instances
Heatmap2DTrack.each = function (fn) {
  Heatmap2DTrack.instances.forEach(fn)
}

Heatmap2DTrack.findInstanceByCanvas = function (canvas) {
  let instance
  Heatmap2DTrack.each(ele => {
    if (ele.canvas === canvas) {
      instance = ele
    }
  })
  return instance
}
Heatmap2DTrack.findInstanceByBrush = function (g) {
  let instance
  Heatmap2DTrack.each(ele => {
    //    console.log(ele.brush_g.node())
    if (ele.brush_g.node() === g) {
      instance = ele
    }
  })
  return instance
}
export {
  Heatmap2DTrack
}