canvas/pixelmapFeature.js

var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var pixelmapFeature = require('../pixelmapFeature');
var geo_event = require('../event');
var util = require('../util');

/**
 * Pixelmap feature information record.
 *
 * @typedef {object} geo.pixelmapFeature.info
 * @property {number} width The width of the source image.
 * @property {number} height The width of the source image.
 * @property {CanvasRenderingContext2D} context The HTMLCanvasElement context
 *    used for handling the pixelmap.
 * @property {ImageData} imageData The context's image data.
 * @property {number[]} indices An array, one per pixel, of the index value in
 *    the image.  This decodes the pixel value to the corresponding integer.
 * @property number} area The number of pixels in the image.  This is
 *    `width * height`.
 * @property {object[]} mappedColors This has one entry for each distinct index
 *    value.  Each entry has `first` and `last` with the first and last pixel
 *    locations where that index occurs.  Note that last is the inclusive value
 *    of the location (so its maximum possible value is `size - 1`).
 */

/**
 * Create a new instance of class pixelmapFeature.
 *
 * @class
 * @alias geo.canvas.pixelmapFeature
 * @extends geo.pixelmapFeature
 * @param {geo.pixelmapFeature.spec} arg
 * @returns {geo.canvas.pixelmapFeature}
 */
var canvas_pixelmapFeature = function (arg) {
  'use strict';

  if (!(this instanceof canvas_pixelmapFeature)) {
    return new canvas_pixelmapFeature(arg);
  }
  pixelmapFeature.call(this, arg);

  var object = require('./object');
  object.call(this);

  var m_quadFeature,
      s_exit = this._exit,
      m_this = this;

  /**
   * If the specified coordinates are in the rendered quad, use the basis
   * information from the quad to determine the pixelmap index value so that it
   * can be included in the `found` results.
   *
   * @param {geo.geoPosition} geo Coordinate.
   * @param {string|geo.transform|null} [gcs] Input gcs.  `undefined` to use
   *    the interface gcs, `null` to use the map gcs, or any other transform.
   * @returns {geo.feature.searchResult} An object with a list of features and
   *    feature indices that are located at the specified point.
   */
  this.pointSearch = function (geo, gcs) {
    if (m_quadFeature && m_this.m_info) {
      var result = m_quadFeature.pointSearch(geo, gcs);
      if (result.index.length === 1 && result.extra && result.extra[result.index[0]].basis) {
        var basis = result.extra[result.index[0]].basis, x, y, idx;
        x = Math.floor(basis.x * m_this.m_info.width);
        y = Math.floor(basis.y * m_this.m_info.height);
        if (x >= 0 && x < m_this.m_info.width &&
            y >= 0 && y < m_this.m_info.height) {
          idx = m_this.m_info.indices[y * m_this.m_info.width + x];
          result = {
            index: [idx],
            found: [m_this.data()[idx]]
          };
          return result;
        }
      }
    }
    return {index: [], found: []};
  };

  /**
   * Compute information for this pixelmap image.  It is wasteful to call this
   * if the pixelmap has already been prepared (it is invalidated by a change
   * in the image).
   *
   * @returns {geo.pixelmapFeature.info}
   */
  this._preparePixelmap = function () {
    var i, idx, pixelData;

    if (!util.isReadyImage(m_this.m_srcImage)) {
      return;
    }
    m_this.m_info = {
      width: m_this.m_srcImage.naturalWidth,
      height: m_this.m_srcImage.naturalHeight,
      canvas: document.createElement('canvas')
    };

    m_this.m_info.canvas.width = m_this.m_info.width;
    m_this.m_info.canvas.height = m_this.m_info.height;
    m_this.m_info.context = m_this.m_info.canvas.getContext('2d');

    m_this.m_info.context.drawImage(m_this.m_srcImage, 0, 0);
    m_this.m_info.imageData = m_this.m_info.context.getImageData(
      0, 0, m_this.m_info.canvas.width, m_this.m_info.canvas.height);
    pixelData = m_this.m_info.imageData.data;
    m_this.m_info.indices = new Array(pixelData.length / 4);
    m_this.m_info.area = pixelData.length / 4;

    m_this.m_info.mappedColors = {};
    for (i = 0; i < pixelData.length; i += 4) {
      idx = pixelData[i] + (pixelData[i + 1] << 8) + (pixelData[i + 2] << 16);
      m_this.m_info.indices[i / 4] = idx;
      if (!m_this.m_info.mappedColors[idx]) {
        m_this.m_info.mappedColors[idx] = {first: i / 4};
      }
      m_this.m_info.mappedColors[idx].last = i / 4;
    }
    return m_this.m_info;
  };

  /**
   * Given the loaded pixelmap image, create a canvas the size of the image.
   * Compute a color for each distinct index and recolor the canvas based on
   * these colors, then draw the resultant image as a quad.
   *
   * @fires geo.event.pixelmap.prepared
   */
  this._computePixelmap = function () {
    var data = m_this.data() || [],
        colorFunc = m_this.style.get('color'),
        i, idx, lastidx, color, pixelData, indices, mappedColors,
        updateFirst, updateLast = -1, update, prepared;

    if (!m_this.m_info) {
      m_this.indexModified(undefined, 'clear');
      if (!m_this._preparePixelmap()) {
        return;
      }
      prepared = true;
    }
    m_this.indexModified(undefined, 'clear');
    mappedColors = m_this.m_info.mappedColors;
    updateFirst = m_this.m_info.area;
    for (idx in mappedColors) {
      if (mappedColors.hasOwnProperty(idx)) {
        color = colorFunc(data[idx], +idx) || {};
        color = [
          (color.r || 0) * 255,
          (color.g || 0) * 255,
          (color.b || 0) * 255,
          color.a === undefined ? 255 : (color.a * 255)
        ];
        mappedColors[idx].update = (
          !mappedColors[idx].color ||
          mappedColors[idx].color[0] !== color[0] ||
          mappedColors[idx].color[1] !== color[1] ||
          mappedColors[idx].color[2] !== color[2] ||
          mappedColors[idx].color[3] !== color[3]);
        if (mappedColors[idx].update) {
          mappedColors[idx].color = color;
          updateFirst = Math.min(mappedColors[idx].first, updateFirst);
          updateLast = Math.max(mappedColors[idx].last, updateLast);
        }
      }
    }
    /* If nothing was updated, we are done */
    if (updateFirst >= updateLast) {
      return;
    }
    /* Update only the extent that has changed */
    pixelData = m_this.m_info.imageData.data;
    indices = m_this.m_info.indices;
    for (i = updateFirst; i <= updateLast; i += 1) {
      idx = indices[i];
      if (idx !== lastidx) {
        lastidx = idx;
        color = mappedColors[idx].color;
        update = mappedColors[idx].update;
      }
      if (update) {
        pixelData[i * 4] = color[0];
        pixelData[i * 4 + 1] = color[1];
        pixelData[i * 4 + 2] = color[2];
        pixelData[i * 4 + 3] = color[3];
      }
    }
    /* Place the updated area into the canvas */
    m_this.m_info.context.putImageData(
      m_this.m_info.imageData, 0, 0, 0, Math.floor(updateFirst / m_this.m_info.width),
      m_this.m_info.width, Math.ceil((updateLast + 1) / m_this.m_info.width));

    /* If we haven't made a quad feature, make one now.  The quad feature needs
     * to have the canvas capability. */
    if (!m_quadFeature) {
      m_quadFeature = m_this.layer().createFeature('quad', {
        selectionAPI: false,
        gcs: m_this.gcs(),
        visible: m_this.visible(undefined, true)
      });
      m_this.dependentFeatures([m_quadFeature]);
      m_quadFeature.style({
        image: m_this.m_info.canvas,
        position: m_this.style.get('position')})
      .data([{}])
      .draw();
    }
    /* If we prepared the pixelmap and rendered it, send a prepared event */
    if (prepared) {
      m_this.geoTrigger(geo_event.pixelmap.prepared, {
        pixelmap: m_this
      });
    }
  };

  /**
   * Destroy.  Deletes the associated quadFeature.
   *
   * @returns {this}
   */
  this._exit = function () {
    if (m_quadFeature && m_this.layer()) {
      m_this.layer().deleteFeature(m_quadFeature);
      m_quadFeature = null;
      m_this.dependentFeatures([]);
    }
    s_exit();
    return m_this;
  };

  this._init(arg);
  return this;
};

inherit(canvas_pixelmapFeature, pixelmapFeature);

// Now register it
registerFeature('canvas', 'pixelmap', canvas_pixelmapFeature);
module.exports = canvas_pixelmapFeature;