webgl/polygonFeature.js

var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var polygonFeature = require('../polygonFeature');

/**
 * Create a new instance of webgl.polygonFeature.
 *
 * @class
 * @alias geo.webgl.polygonFeature
 * @extends geo.polygonFeature
 * @param {geo.polygonFeature.spec} arg
 * @returns {geo.webgl.polygonFeature}
 */
var webgl_polygonFeature = function (arg) {
  'use strict';
  if (!(this instanceof webgl_polygonFeature)) {
    return new webgl_polygonFeature(arg);
  }
  arg = arg || {};
  polygonFeature.call(this, arg);

  var vgl = require('../vgl');
  var earcut = require('earcut');
  var transform = require('../transform');
  var util = require('../util');
  var object = require('./object');
  var fragmentShader = require('./polygonFeature.frag');
  var vertexShader = require('./polygonFeature.vert');

  object.call(this);

  /**
   * @private
   */
  var m_this = this,
      s_exit = this._exit,
      m_actor = vgl.actor(),
      m_mapper = vgl.mapper(),
      m_material = vgl.material(),
      m_geometry,
      m_origin,
      m_modelViewUniform,
      s_init = this._init,
      s_update = this._update,
      m_builtOnce,
      m_updateAnimFrameRef;

  function createVertexShader() {
    var shader = new vgl.shader(vgl.GL.VERTEX_SHADER);
    shader.setShaderSource(vertexShader);
    return shader;
  }

  function createFragmentShader() {
    var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER);
    shader.setShaderSource(fragmentShader);
    return shader;
  }

  /**
   * Create and style the triangles needed to render the polygons.
   *
   * There are several optimizations to do less work when possible.  If only
   * styles have changed, the triangulation is not recomputed, nor is the
   * geometry re-transformed.  If styles use static values (rather than
   * functions), they are only calculated once.  If a polygon reports that it
   * has a uniform style, then styles are only calculated once for that polygon
   * (the uniform property may be different per polygon or per update).
   * Array.map is slower in Chrome that using a loop, so loops are used in
   * places that would be conceptually served by maps.
   *
   * @param {boolean} onlyStyle if true, use the existing geometry and just
   *    recalculate the style.
   */
  function createGLPolygons(onlyStyle) {
    var posBuf, posFunc, polyFunc,
        fillColor, fillColorFunc, fillColorVal,
        fillOpacity, fillOpacityFunc, fillOpacityVal,
        fillFunc, fillVal,
        uniformFunc, uniformVal, uniform,
        indices,
        items = [],
        target_gcs = m_this.gcs(),
        map_gcs = m_this.layer().map().gcs(),
        numPts = 0,
        geom = m_mapper.geometryData(),
        color, opacity, fill, d, d3, vertices, i, j, k, n,
        record, item, itemIndex, original;

    fillColorFunc = m_this.style.get('fillColor');
    fillColorVal = util.isFunction(m_this.style('fillColor')) ? undefined : fillColorFunc();
    fillOpacityFunc = m_this.style.get('fillOpacity');
    fillOpacityVal = util.isFunction(m_this.style('fillOpacity')) ? undefined : fillOpacityFunc();
    fillFunc = m_this.style.get('fill');
    fillVal = util.isFunction(m_this.style('fill')) ? undefined : fillFunc();
    uniformFunc = m_this.style.get('uniformPolygon');
    uniformVal = util.isFunction(m_this.style('uniformPolygon')) ? undefined : uniformFunc();

    if (!onlyStyle) {
      posFunc = m_this.style.get('position');
      posFunc = posFunc === util.identityFunction ? null : posFunc;
      polyFunc = m_this.style.get('polygon');
      polyFunc = polyFunc === util.identityFunction ? null : polyFunc;
      m_this.data().forEach(function (item, itemIndex) {
        var polygon, outer, geometry, c;

        polygon = polyFunc ? polyFunc(item, itemIndex) : item;
        if (!polygon) {
          return;
        }
        outer = polygon.outer || (Array.isArray(polygon) ? polygon : []);
        if (outer.length < 3) {
          return;
        }

        /* expand to an earcut polygon geometry.  We had been using a map call,
         * but using loops is much faster in Chrome (4 versus 33 ms for one
         * test). */
        geometry = new Array(outer.length * 3);
        for (i = d3 = 0; i < outer.length; i += 1, d3 += 3) {
          c = posFunc ? posFunc(outer[i], i, item, itemIndex) : outer[i];
          geometry[d3] = c.x;
          geometry[d3 + 1] = c.y;
          // ignore the z values until we support them
          geometry[d3 + 2] = 0;  // c.z || 0;
        }
        geometry = {vertices: geometry, dimensions: 3, holes: []};
        original = outer;

        if (polygon.inner) {
          polygon.inner.forEach(function (hole) {
            if (hole.length < 3) {
              return;
            }
            original = original.concat(hole);
            geometry.holes.push(d3 / 3);
            for (i = 0; i < hole.length; i += 1, d3 += 3) {
              c = posFunc ? posFunc(hole[i], i, item, itemIndex) : hole[i];
              geometry.vertices[d3] = c.x;
              geometry.vertices[d3 + 1] = c.y;
              // ignore the z values until we support them
              geometry.vertices[d3 + 2] = 0;  // c.z || 0;
            }
          });
        }

        // transform to map gcs
        geometry.vertices = transform.transformCoordinates(
          target_gcs,
          map_gcs,
          geometry.vertices,
          geometry.dimensions
        );

        record = {
          // triangulate
          triangles: earcut(geometry.vertices, geometry.holes, geometry.dimensions),
          vertices: geometry.vertices,
          original: original,
          item: item,
          itemIndex: itemIndex
        };
        if (record.triangles.length) {
          items.push(record);
          numPts += record.triangles.length;
        }
      });
      posBuf = util.getGeomBuffer(geom, 'pos', numPts * 3);
      indices = geom.primitive(0).indices();
      if (!(indices instanceof Uint16Array) || indices.length !== numPts) {
        indices = new Uint16Array(numPts);
        geom.primitive(0).setIndices(indices);
      }
      m_geometry = {items: items, numPts: numPts};
      m_origin = new Float32Array(m_this.style.get('origin')(items));
      m_modelViewUniform.setOrigin(m_origin);
    } else {
      items = m_geometry.items;
      numPts = m_geometry.numPts;
    }
    fillColor = util.getGeomBuffer(geom, 'fillColor', numPts * 3);
    fillOpacity = util.getGeomBuffer(geom, 'fillOpacity', numPts);
    d = d3 = 0;
    color = fillColorVal;
    fill = fillVal;
    for (k = 0; k < items.length; k += 1) {
      n = items[k].triangles.length;
      vertices = items[k].vertices;
      item = items[k].item;
      itemIndex = items[k].itemIndex;
      original = items[k].original;
      uniform = uniformVal === undefined ? uniformFunc(item, itemIndex) : uniformVal;
      opacity = fillOpacityVal;
      if (uniform) {
        if (fillColorVal === undefined) {
          color = fillColorFunc(vertices[0], 0, item, itemIndex);
        }
        if (fillOpacityVal === undefined) {
          opacity = fillOpacityFunc(vertices[0], 0, item, itemIndex);
        }
      }
      if (fillVal === undefined) {
        fill = fillFunc(item, itemIndex);
      }
      if (!fill) {
        opacity = 0;
      }
      if (uniform && onlyStyle && items[k].uniform && items[k].color &&
          color.r === items[k].color.r && color.g === items[k].color.g &&
          color.b === items[k].color.b && opacity === items[k].opacity) {
        d += n;
        d3 += n * 3;
        continue;
      }
      for (i = 0; i < n; i += 1, d += 1, d3 += 3) {
        if (onlyStyle && uniform) {
          fillColor[d3] = color.r;
          fillColor[d3 + 1] = color.g;
          fillColor[d3 + 2] = color.b;
          fillOpacity[d] = opacity;
        } else {
          j = items[k].triangles[i] * 3;
          if (!onlyStyle) {
            posBuf[d3] = vertices[j] - m_origin[0];
            posBuf[d3 + 1] = vertices[j + 1] - m_origin[1];
            posBuf[d3 + 2] = vertices[j + 2] - m_origin[2];
            indices[d] = i;
          }
          if (!uniform && fillColorVal === undefined) {
            color = fillColorFunc(original[j], j, item, itemIndex);
          }
          fillColor[d3] = color.r;
          fillColor[d3 + 1] = color.g;
          fillColor[d3 + 2] = color.b;
          if (!uniform && fill && fillOpacityVal === undefined) {
            opacity = fillOpacityFunc(original[j], j, item, itemIndex);
          }
          fillOpacity[d] = opacity;
        }
      }
      if (uniform || items[k].uniform) {
        items[k].uniform = uniform;
        items[k].color = color;
        items[k].opacity = opacity;
      }
    }
    if (!onlyStyle) {
      m_mapper.modified();
      geom.boundsDirty(true);
      m_mapper.boundsDirtyTimestamp().modified();
    } else {
      m_mapper.updateSourceBuffer('fillOpacity');
      m_mapper.updateSourceBuffer('fillColor');
    }
  }

  /**
   * Initialize.
   *
   * @param {geo.polygonFeature.spec} arg An object with options for the
   *    feature.
   */
  this._init = function (arg) {
    var prog = vgl.shaderProgram(),
        posAttr = vgl.vertexAttribute('pos'),
        fillColorAttr = vgl.vertexAttribute('fillColor'),
        fillOpacityAttr = vgl.vertexAttribute('fillOpacity'),
        projectionUniform = new vgl.projectionUniform('projectionMatrix'),
        vertexShader = createVertexShader(),
        fragmentShader = createFragmentShader(),
        blend = vgl.blend(),
        geom = vgl.geometryData(),
        sourcePositions = vgl.sourceDataP3fv({name: 'pos'}),
        sourceFillColor = vgl.sourceDataAnyfv(
          3, vgl.vertexAttributeKeysIndexed.Two, {name: 'fillColor'}),
        sourceFillOpacity = vgl.sourceDataAnyfv(
          1, vgl.vertexAttributeKeysIndexed.Three, {name: 'fillOpacity'}),
        trianglePrimitive = vgl.triangles();
    m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix');

    prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position);
    prog.addVertexAttribute(fillColorAttr, vgl.vertexAttributeKeysIndexed.Two);
    prog.addVertexAttribute(fillOpacityAttr, vgl.vertexAttributeKeysIndexed.Three);

    prog.addUniform(m_modelViewUniform);
    prog.addUniform(projectionUniform);

    prog.addShader(fragmentShader);
    prog.addShader(vertexShader);

    m_material.addAttribute(prog);
    m_material.addAttribute(blend);

    m_actor.setMaterial(m_material);
    m_actor.setMapper(m_mapper);

    geom.addSource(sourcePositions);
    geom.addSource(sourceFillColor);
    geom.addSource(sourceFillOpacity);
    geom.addPrimitive(trianglePrimitive);
    /* We don't need vgl to compute bounds, so make the geo.computeBounds just
     * set them to 0. */
    geom.computeBounds = function () {
      geom.setBounds(0, 0, 0, 0, 0, 0);
    };
    m_mapper.setGeometryData(geom);

    s_init.call(m_this, arg);
  };

  /**
   * List vgl actors.
   *
   * @returns {vgl.actor[]} The list of actors.
   */
  this.actors = function () {
    return [m_actor];
  };

  /**
   * Build.
   */
  this._build = function () {
    createGLPolygons(!!(m_this.dataTime().timestamp() < m_this.buildTime().timestamp() && m_geometry));

    if (!m_this.renderer().contextRenderer().hasActor(m_actor)) {
      m_this.renderer().contextRenderer().addActor(m_actor);
      m_builtOnce = true;
    }
    m_this.buildTime().modified();
  };

  /**
   * Update.
   *
   * @param {object} [opts] Update options.
   * @param {boolean} [opts.mayDelay] If truthy, wait until the next animation
   *    frame for the update.
   */
  this._update = function (opts) {
    if (!m_this.ready) {
      return;
    }
    if (opts && opts.mayDelay && m_builtOnce) {
      m_updateAnimFrameRef = m_this.layer().map().scheduleAnimationFrame(m_this._update);
      return;
    }
    if (m_updateAnimFrameRef) {
      m_this.layer().map().scheduleAnimationFrame(m_this._update, 'remove');
      m_updateAnimFrameRef = null;
    }
    s_update.call(m_this);

    if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp() ||
        m_this.updateTime().timestamp() <= m_this.timestamp()) {
      m_this._build();
    }

    m_actor.setVisible(m_this.visible());
    m_actor.material().setBinNumber(m_this.bin());
    m_this.updateTime().modified();
  };

  /**
   * Destroy.
   */
  this._exit = function () {
    if (m_updateAnimFrameRef && m_this.layer()) {
      m_this.layer().map().scheduleAnimationFrame(m_this._update, 'remove');
      m_updateAnimFrameRef = null;
    }
    m_this.renderer().contextRenderer().removeActor(m_actor);
    s_exit();
  };

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

inherit(webgl_polygonFeature, polygonFeature);

// Now register it
registerFeature('webgl', 'polygon', webgl_polygonFeature);
module.exports = webgl_polygonFeature;