Vizzuality/gfw-climate

View on GitHub
app/assets/javascripts/abstract/layer/CanvasJSONLayerClass.js

Summary

Maintainability
B
5 hrs
Test Coverage
/**
 * The HTML5 Canvas map layer module.
 *
 * @return CanvasLayer class (extends Class).
 */
define(['underscore', '_string', 'abstract/layer/OverlayLayerClass'], function(
  _,
  _string,
  OverlayLayerClass
) {
  'use strict';

  var MAX_MONTHS = 200;
  var BASE_MONTH = 72;

  var CanvasJSONLayerClass = OverlayLayerClass.extend({
    defaults: {
      cartodbUserName: 'wri-01',
      dataMaxZoom: 17
    },

    init: function(layer, options, map) {
      this.tiles = {};
      this.layer = layer;
      this.top_date =
        (moment(this.layer.maxdate).year() -
          moment(this.layer.mindate).year()) *
          12 +
        moment(this.layer.maxdate).month() -
        2;
      this._super(layer, options, map);
      this.getDates();
      this.cartoSQL = new cartodb.SQL({
        user: this.options.cartodbUserName
      });
    },

    _getLayer: function() {
      var deferred = new $.Deferred();
      deferred.resolve(this);
      return deferred.promise();
    },

    _getCanvas: function(coord, zoom, ownerDocument) {
      // create canvas and reset style
      var canvas = ownerDocument.createElement('canvas');
      canvas.className = this.layer.slug;
      canvas.style.border = 'none';
      canvas.style.margin = '0';
      canvas.style.padding = '0';

      // prepare canvas and context sizes
      var ctx = canvas.getContext('2d');
      ctx.width = canvas.width = this.tileSize.width;
      ctx.height = canvas.height = this.tileSize.height;

      canvas.ctx = ctx;
      return canvas;
    },

    /**
     * Called whenever the Google Maps API determines that the map needs to
     * display new tiles for the given viewport.
     *
     * @param  {obj}     coord         coordenades {x ,y}
     * @param  {integer} zoom          current map zoom
     * @param  {object}  ownerDocument
     *
     * @return {canvas}  canvas        tile canvas
     */
    getTile: function(coord, zoom, ownerDocument) {
      var canvas = this._getCanvas(coord, zoom, ownerDocument);
      var sql = this._getSQL(coord.x, coord.y, zoom);
      var zoomDiff = zoom + 8 - Math.min(zoom + 8, 16);

      this.cartoSQL.execute(
        sql,
        _.bind(function(data) {
          if (!data) {
            return;
          }

          var tile = {
            canvas: canvas,
            ctx: canvas.ctx,
            width: this.tileSize.width,
            coord: coord,
            zoom: zoom,
            height: this.tileSize.height,
            cells: this.preCacheMonths(data.rows, coord, zoom, zoomDiff),
            top_date: data.top_date
          };

          //set unique id
          var tileId = '{0}_{1}_{2}'.format(coord.x, coord.y, zoom);
          canvas.setAttribute('id', tileId);

          if (tileId in this.tiles) {
            delete this.tiles[tileId];
          }

          this.tiles[tileId] = tile;

          this._render(tile);
        }, this)
      );

      return canvas;
    },

    _getSQL: function(x, y, z) {
      // zoom + 8 is get because a tile in 'zoom' zoom level is a pixel in 'zoom + 8'
      // level. Remember, it is a quadtree, 1^8 = 256 and tile size is 256px
      var pixel_zoom = Math.min(z + 8, 16);
      var zoom_diff = z + 8 - pixel_zoom;

      // get x, y for cells and sd, se for deforestation changes
      // sd contains the months
      // se contains the deforestation for each entry in sd
      // take se and sd as a matrix [se|sd]
      var sql = _.str.sprintf(
        'SELECT cartodb_id, x, y, sd, se FROM %(tableName)s WHERE z = %(z)s  AND x >= %(cx)s AND x < %(cx1)s AND y >= %(cy)s AND y < %(cy1)s',
        {
          tableName: this.layer.table_name,
          cx: (x * 256) >> zoom_diff,
          cx1: ((x + 1) * 256) >> zoom_diff,
          cy: (y * 256) >> zoom_diff,
          cy1: ((y + 1) * 256) >> zoom_diff,
          z: pixel_zoom
        }
      );
      return sql;
    },

    _render: function(tile) {
      var month = this.endMonth || -BASE_MONTH + MAX_MONTHS;
      var month_start = this.startMonth || 0;
      var month_change = this.top_date;
      var w = tile.canvas.width;
      var ctx = tile.ctx;
      var cells = tile.cells;
      if (!cells || cells.length === 0) {
        return;
      }

      // clear canvas
      tile.canvas.width = w;
      var xc = cells.xcoords;
      var yc = cells.ycoords;

      // render cells
      var len = cells.length;
      var pixel_size = cells.size;
      var index, index0, index1, mul;
      var previous = [];
      for (var i = 0; i < len; ++i) {
        mul = MAX_MONTHS * i;
        index = mul + month;
        index0 = mul + month_start;
        index1 = mul + month_change;
        // set pixel by hand faster than doing fill rect (below)
        if (cells.deforestation[index] - cells.deforestation[index0] > 0) {
          ctx.fillStyle = '#F69';
          ctx.fillRect(xc[i], yc[i], pixel_size, pixel_size);
        }
        if (
          month >= month_change &&
          cells.deforestation[index] - cells.deforestation[index1] > 0
        ) {
          ctx.fillStyle = 'rgb(233, 189, 21)';
          ctx.fillRect(xc[i], yc[i], pixel_size, pixel_size);
        }
      }
    },

    preCacheMonths: function(rows, coord, zoom, zoom_diff) {
      var row, xcoords, ycoords, deforestation;

      if (typeof ArrayBuffer !== 'undefined') {
        xcoords = new Uint8Array(new ArrayBuffer(rows.length));
        ycoords = new Uint8Array(new ArrayBuffer(rows.length));
        deforestation = new Uint8Array(
          new ArrayBuffer(rows.length * MAX_MONTHS)
        ); // 256 months
      } else {
        // fallback
        xcoords = [];
        ycoords = [];
        deforestation = [];

        // array buffer set by default to 0
        // fucking javascript arrays are not
        for (var r = 0; r < rows.length * MAX_MONTHS; ++r) {
          deforestation[r] = 0;
        }
      }

      // base tile x, y
      var tile_base_x = coord.x * 256;
      var tile_base_y = coord.y * 256;
      for (var i in rows) {
        row = rows[i];
        xcoords[i] = (row.x - tile_base_x) << zoom_diff;
        ycoords[i] = (row.y - tile_base_y) << zoom_diff;
        var base_idx = i * MAX_MONTHS;

        if (row.sd !== null) {
          for (var j = 0; j < row.sd.length; ++j) {
            var base = base_idx + row.sd[j] - BASE_MONTH;
            deforestation[base] = row.se[j];
          }
        }

        // add to the deforestation array each previous value, thus we have the whole picture at the end
        for (var j = 1; j < MAX_MONTHS; ++j) {
          deforestation[base_idx + j] += deforestation[base_idx + j - 1];
        }
      }

      return {
        length: rows.length,
        xcoords: xcoords,
        ycoords: ycoords,
        deforestation: deforestation,
        size: 1 << zoom_diff
      };
    },

    updateTiles: function() {
      this.getDates();
      _.each(
        this.tiles,
        _.bind(function(tile) {
          this._render(tile);
        }, this)
      );
    },

    getDates: function() {
      this.startMonth = Math.abs(
        moment(this.layer.mindate).diff(this.currentDate[0], 'months')
      );
      this.endMonth = Math.abs(
        moment(this.layer.mindate).diff(this.currentDate[1], 'months')
      );
    }
  });

  return CanvasJSONLayerClass;
});