wilzbach/msa

View on GitHub
src/views/canvas/CanvasSeqBlock.js

Summary

Maintainability
D
2 days
Test Coverage
const boneView = require("backbone-childs");
const mouse = require("mouse-pos");
import {throttle} from "lodash";
const jbone = require("jbone");

import CharCache from "./CanvasCharCache";
import SelectionClass from "./CanvasSelection";
import CanvasSeqDrawer from "./CanvasSeqDrawer";
import CanvasCoordsCache from "./CanvasCoordsCache";

const View = boneView.extend({

  tagName: "canvas",

  initialize: function(data) {
    this.g = data.g;

    this.listenTo(this.g.zoomer, "change:_alignmentScrollLeft change:_alignmentScrollTop", function(model,value, options) {
      if ((!(((typeof options !== "undefined" && options !== null) ? options.origin : undefined) != null)) || options.origin !== "canvasseq") {
        return this.render();
      }
    });

    this.listenTo(this.g.columns,"change:hidden", this.render);
    this.listenTo(this.g.zoomer,"change:alignmentWidth change:alignmentHeight", this.render);
    this.listenTo(this.g.colorscheme, "change", this.render);
    this.listenTo(this.g.selcol, "reset add", this.render);
    this.listenTo(this.model, "reset add", this.render);

    // el props
    this.el.style.display = "inline-block";
    this.el.style.overflowX = "hidden";
    this.el.style.overflowY = "hidden";
    this.el.className = "biojs_msa_seqblock";

    this.ctx = this.el.getContext('2d');
    this.cache = new CharCache(this.g);
    this.coordsCache = new CanvasCoordsCache(this.g, this.model);

    // clear the char cache
    this.listenTo(this.g.zoomer, "change:residueFont", function() {
      this.cache = new CharCache(this.g);
      return this.render();
    });

    // init selection
    this.sel = new SelectionClass(this.g,this.ctx);

    this._setColor();

    // throttle the expensive draw function
    this.throttleTime = 0;
    this.throttleCounts = 0;
    if ((document.documentElement.style.webkitAppearance != null)) {
      // webkit browser - no throttling needed
      this.throttledDraw = function() {
        const start = +new Date();
        this.draw();
        this.throttleTime += +new Date() - start;
        this.throttleCounts++;
        if (this.throttleCounts > 15) {
          const tTime = Math.ceil(this.throttleTime / this.throttleCounts);
          console.log("avgDrawTime/WebKit", tTime);
          // remove perf analyser
          return this.throttledDraw = this.draw;
        }
      };
    } else {
      // slow browsers like Gecko
      this.throttledDraw = throttle(this.throttledDraw, 30);
    }

    return this.manageEvents();
  },


  // measures the time of a redraw and thus set the throttle limit
  throttledDraw: function() {
    // +new is the fastest: http://jsperf.com/new-date-vs-date-now-vs-performance-now/6
    const start = +new Date();
    this.draw();
    this.throttleTime += +new Date() - start;
    this.throttleCounts++;

    // remove itself after analysis
    if (this.throttleCounts > 15) {
      let tTime = Math.ceil(this.throttleTime / this.throttleCounts);
      console.log("avgDrawTime", tTime);
      tTime *=  1.2; // add safety time
      tTime = Math.max(20, tTime); // limit for ultra fast computers
      return this.throttledDraw = _.throttle(this.draw, tTime);
    }
  },

  manageEvents: function() {
    const events = {};
    events.mousedown = "_onmousedown";
    events.touchstart = "_ontouchstart";

    if (this.g.config.get("registerMouseClicks")) {
      events.dblclick = "_onclick";
    }
    if (this.g.config.get("registerMouseHover")) {
      events.mousein = "_onmousein";
      events.mouseout = "_onmouseout";
    }

    events.mousewheel = "_onmousewheel";
    events.DOMMouseScroll = "_onmousewheel";
    this.delegateEvents(events);

    // listen for changes
    this.listenTo(this.g.config, "change:registerMouseHover", this.manageEvents);
    this.listenTo(this.g.config, "change:registerMouseClick", this.manageEvents);
    return this.dragStart = [];
  },

  _setColor: function() {
    return this.color = this.g.colorscheme.getSelectedScheme();
  },

  draw: function() {
    // fastest way to clear the canvas
    // http://jsperf.com/canvas-clear-speed/25
    this.el.width = this.el.width;

    // draw all the stuff
    if ((this.seqDrawer != null)  && this.model.length > 0) {
      // char based
      this.seqDrawer.drawLetters();
      // row based
      this.seqDrawer.drawRows(this.sel._appendSelection, this.sel);
      return this.seqDrawer.drawRows(this.drawFeatures, this);
    }
  },

  drawFeatures: function(data) {
    const rectWidth = this.g.zoomer.get("columnWidth");
    const rectHeight = this.g.zoomer.get("rowHeight");
    if (data.model.attributes.height > 1) {
      const ctx = this.ctx;
      data.model.attributes.features.each(function(feature) {
        ctx.fillStyle = feature.attributes.fillColor || "red";
        const len = feature.attributes.xEnd - feature.attributes.xStart + 1;
        const y = (feature.attributes.row + 1) * rectHeight;
        return ctx.fillRect(feature.attributes.xStart * rectWidth + data.xZero,y + data.yZero,rectWidth * len,rectHeight);
      });

      // draw text
      ctx.fillStyle = "black";
      ctx.font = this.g.zoomer.get("residueFont") + "px mono";
      ctx.textBaseline = 'middle';
      ctx.textAlign = "center";

      return data.model.attributes.features.each(function(feature) {
        const len = feature.attributes.xEnd - feature.attributes.xStart + 1;
        const y = (feature.attributes.row + 1) * rectHeight;
        return ctx.fillText( feature.attributes.text, data.xZero + feature.attributes.xStart *
        rectWidth + (len / 2) * rectWidth, data.yZero + rectHeight * 0.5 + y
        );
      });
    }
  },

  render: function() {

    this.el.setAttribute('height', this.g.zoomer.get("alignmentHeight") + "px");
    this.el.setAttribute('width', this.g.zoomer.getAlignmentWidth() + "px");

    this.g.zoomer._checkScrolling( this._checkScrolling([this.g.zoomer.get('_alignmentScrollLeft'),
    this.g.zoomer.get('_alignmentScrollTop')] ),{header: "canvasseq"});

    this._setColor();

    this.seqDrawer = new CanvasSeqDrawer( this.g,this.ctx,this.model,
      {width: this.el.width,
      height: this.el.height,
      color: this.color,
      cache: this.cache
    });

    this.throttledDraw();
    return this;
  },

  _onmousemove: function(e, reversed) {
    if (this.dragStart.length === 0) { return; }

    const dragEnd = mouse.abs(e);
    // relative to first click
    const relEnd = [dragEnd[0] - this.dragStart[0], dragEnd[1] - this.dragStart[1]];
    // relative to initial scroll status

    // scale events
    let scaleFactor = this.g.zoomer.get("canvasEventScale");
    if (reversed) {
      scaleFactor = 3;
    }
    for (let i = 0; i <= 1; i++) {
      relEnd[i] = relEnd[i] * scaleFactor;
    }

    // calculate new scrolling vals
    const relDist = [this.dragStartScroll[0] - relEnd[0], this.dragStartScroll[1] - relEnd[1]];

    // round values
    for (let i = 0; i <= 1; i++) {
      relDist[i] = Math.round(relDist[i]);
    }

    // update scrollbar
    const scrollCorrected = this._checkScrolling( relDist);
    this.g.zoomer._checkScrolling(scrollCorrected, {origin: "canvasseq"});

    // reset start if use scrolls out of bounds
    for (let i = 0; i <= 1; i++) {
      if (scrollCorrected[i] !== relDist[i]) {
        if (scrollCorrected[i] === 0) {
          // reset of left, top
          this.dragStart[i] = dragEnd[i];
          this.dragStartScroll[i] = 0;
        } else {
          // recalibrate on right, bottom
          this.dragStart[i] = dragEnd[i] - scrollCorrected[i];
        }
      }
    }

    this.throttledDraw();

    // abort selection events of the browser (mouse only)
    if ((e.preventDefault != null)) {
      e.preventDefault();
      return e.stopPropagation();
    }
  },

  // converts touches into old mouse event
  _ontouchmove: function(e) {
    this._onmousemove(e.changedTouches[0], true);
    e.preventDefault();
    return e.stopPropagation();
  },

  // start the dragging mode
  _onmousedown: function(e) {
    this.dragStart = mouse.abs(e);
    this.dragStartScroll = [this.g.zoomer.get('_alignmentScrollLeft'), this.g.zoomer.get('_alignmentScrollTop')];
    jbone(document.body).on('mousemove.overmove', (e) => this._onmousemove(e));
    jbone(document.body).on('mouseup.overup', () => this._cleanup());
    //jbone(document.body).on 'mouseout.overout', (e) => @_onmousewinout(e)
    return e.preventDefault();
  },

  // starts the touch mode
  _ontouchstart: function(e) {
    this.dragStart = mouse.abs(e.changedTouches[0]);
    this.dragStartScroll = [this.g.zoomer.get('_alignmentScrollLeft'), this.g.zoomer.get('_alignmentScrollTop')];
    jbone(document.body).on('touchmove.overtmove', (e) => this._ontouchmove(e));
      return jbone(document.body).on( 'touchend.overtend touchleave.overtleave touchcancel.overtcanel', (e) => this._touchCleanup(e)
    );
  },

  // checks whether mouse moved out of the window
  // -> terminate dragging
  _onmousewinout: function(e) {
    if (e.toElement === document.body.parentNode) {
      return this._cleanup();
    }
  },

  // terminates dragging
  _cleanup: function() {
    this.dragStart = [];
    // remove all listeners
    jbone(document.body).off('.overmove');
    jbone(document.body).off('.overup');
    return jbone(document.body).off('.overout');
  },

  // terminates touching
  _touchCleanup: function(e) {
    if (e.changedTouches.length > 0) {
      // maybe we can send a final event
      this._onmousemove(e.changedTouches[0], true);
    }

    this.dragStart = [];
    // remove all listeners
    jbone(document.body).off('.overtmove');
    jbone(document.body).off('.overtend');
    jbone(document.body).off('.overtleave');
    return jbone(document.body).off('.overtcancel');
  },

  // might be incompatible with some browsers
  _onmousewheel: function(e) {
    const delta = mouse.wheelDelta(e);
    this.g.zoomer.set('_alignmentScrollLeft', this.g.zoomer.get('_alignmentScrollLeft') + delta[0]);
    this.g.zoomer.set('_alignmentScrollTop', this.g.zoomer.get('_alignmentScrollTop') + delta[1]);
    return e.preventDefault();
  },

  _onclick: function(e) {
    const res = this._getClickPos(e);
    if ((typeof res !== "undefined" && res !== null)) {
      if ((res.feature != null)) {
        this.g.trigger("feature:click", res);
      } else {
        this.g.trigger("residue:click", res);
      }
    }
    return this.throttledDraw();
  },

  _onmousein: function(e) {
    const res = this._getClickPos(e);
    if ((typeof res !== "undefined" && res !== null)) {
      if ((res.feature != null)) {
        this.g.trigger("feature:mousein", res);
      } else {
        this.g.trigger("residue:mousein", res);
      }
    }
    return this.throttledDraw();
  },

  _onmouseout: function(e) {
    const res = this._getClickPos(e);
    if ((typeof res !== "undefined" && res !== null)) {
      if ((res.feature != null)) {
        this.g.trigger("feature:mouseout", res);
      } else {
        this.g.trigger("residue:mouseout", res);
      }
    }

    return this.throttledDraw();
  },

  _getClickPos: function(e) {
    const coords = mouse.rel(e);

    coords[0] += this.g.zoomer.get("_alignmentScrollLeft");
    let x = Math.floor(coords[0] / this.g.zoomer.get("columnWidth") );
    let [y, rowNumber] = this.seqDrawer._getSeqForYClick(coords[1]);

    // add hidden columns
    x += this.g.columns.calcHiddenColumns(x);
    // add hidden seqs
    y += this.model.calcHiddenSeqs(y);

    x = Math.max(0,x);
    y = Math.max(0,y);

    const seqId = this.model.at(y).get("id");

    if (rowNumber > 0) {
      // click on a feature
      const features = this.model.at(y).get("features").getFeatureOnRow(rowNumber - 1, x);
      if (!(features.length === 0)) {
        const feature = features[0];
        console.log(features[0].attributes);
        return {seqId:seqId, feature: feature, rowPos: x, evt:e};
      }
    } else {
      // click on a seq
      return {seqId:seqId, rowPos: x, evt:e};
    }
  },

  // checks whether the scrolling coordinates are valid
  // @returns: [xScroll,yScroll] valid coordinates
  _checkScrolling: function(scrollObj) {

    // 0: maxLeft, 1: maxTop
    const max = [this.coordsCache.maxScrollWidth, this.coordsCache.maxScrollHeight];

    for (let i = 0; i <= 1; i++) {
      if (scrollObj[i] > max[i]) {
        scrollObj[i] = max[i];
      }

      if (scrollObj[i] < 0) {
        scrollObj[i] = 0;
      }
    }

    return scrollObj;
  }
});
export default View;