vtkjs/pointFeature.js

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

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

  var transform = require('../transform');
  var object = require('./object');
  var vtk = require('./vtkjsRenderer').vtkjs;
  var vtkActor = vtk.Rendering.Core.vtkActor;
  var vtkDataArray = vtk.Common.Core.vtkDataArray;
  var vtkGlyph3DMapper = vtk.Rendering.Core.vtkGlyph3DMapper;
  var vtkMapper = vtk.Rendering.Core.vtkMapper;
  var vtkPointSet = vtk.Common.DataModel.vtkPointSet;
  var vtkSphereSource = vtk.Filters.Sources.vtkSphereSource;

  object.call(this);

  var m_this = this,
      m_actor,
      m_pointSet,
      m_source,
      m_colorArray,
      m_diamArray,
      s_init = this._init,
      s_exit = this._exit,
      s_update = this._update;

  /**
   * Create pipeline.
   */
  this._createPipeline = function () {
    m_pointSet = vtkPointSet.newInstance();
    m_source = vtkSphereSource.newInstance();
    m_source.setThetaResolution(30);
    m_source.setPhiResolution(30);
    var mapper = vtkGlyph3DMapper.newInstance({
      // Orientation
      orient: false,

      // Color and Opacity
      colorByArrayName: 'color',
      scalarMode: vtkMapper.ScalarMode.USE_POINT_FIELD_DATA,
      colorMode: vtkMapper.ColorMode.DIRECT_SCALARS,

      // Scaling
      scaling: true,
      scaleArray: 'diam',
      scaleMode: vtkGlyph3DMapper.ScaleModes.SCALE_BY_MAGNITUDE
    });
    mapper.setInputData(m_pointSet, 0);
    mapper.setInputConnection(m_source.getOutputPort(), 1);
    m_actor = vtkActor.newInstance();
    m_actor.setMapper(mapper);
    m_actor.getProperty().setAmbient(1);
    m_this.renderer().contextRenderer().addActor(m_actor);
  };

  /**
   * Initialize.
   */
  this._init = function () {
    s_init.call(m_this, arg);
    m_this.renderer().contextRenderer().setLayer(0);
    m_this._createPipeline();
  };

  /**
   * Build this feature.
   */
  this._build = function () {
    var i, i3, i4, posVal, clrVal,
        nonzeroZ,
        numPts = m_this.data().length,
        position = new Array(numPts * 3),
        data = m_this.data(),
        posFunc = m_this.position(),
        radFunc = m_this.style.get('radius'),
        fillFunc = m_this.style.get('fill'),
        colorFunc = m_this.style.get('fillColor'),
        opacityFunc = m_this.style.get('fillOpacity'),
        unitsPerPixel = m_this.layer().map().unitsPerPixel(m_this.layer().map().zoom());

    if (!m_diamArray || m_diamArray.length !== numPts) {
      m_diamArray = new Float32Array(numPts);
    }
    if (!m_colorArray || m_colorArray.length !== numPts * 4) {
      m_colorArray = new Uint8Array(numPts * 4);
    }

    /* It is more efficient to do a transform on a single array rather than on
     * an array of arrays or an array of objects. */
    for (i = i3 = i4 = 0; i < numPts; i += 1, i3 += 3, i4 += 4) {
      posVal = posFunc(data[i], i);
      position[i3] = posVal.x;
      position[i3 + 1] = posVal.y;
      position[i3 + 2] = posVal.z || 0;
      nonzeroZ = nonzeroZ || position[i3 + 2];

      m_diamArray[i] = radFunc(data[i], i) * unitsPerPixel * 2;
      clrVal = colorFunc(data[i], i);
      m_colorArray[i4] = clrVal.r * 255;
      m_colorArray[i4 + 1] = clrVal.g * 255;
      m_colorArray[i4 + 2] = clrVal.b * 255;
      m_colorArray[i4 + 3] = fillFunc(data[i], i) ? opacityFunc(data[i], i) * 255 : 0;
    }
    position = transform.transformCoordinates(
      m_this.gcs(), m_this.layer().map().gcs(),
      position, 3);

    /* Some transforms modify the z-coordinate.  If we started with all zero z
     * coordinates, don't modify them.  This could be changed if the
     * z-coordinate space of the gl cube is scaled appropriately. */
    if (!nonzeroZ && m_this.gcs() !== m_this.layer().map().gcs()) {
      for (i = i3 = 0; i < numPts; i += 1, i3 += 3) {
        position[i3 + 2] = 0;
      }
    }

    m_pointSet.getPoints().setData(position, 3);

    // Attach fields
    m_pointSet.getPointData().addArray(vtkDataArray.newInstance({name: 'color', values: m_colorArray, numberOfComponents: 4}));
    m_pointSet.getPointData().addArray(vtkDataArray.newInstance({name: 'diam', values: m_diamArray}));

    m_this.buildTime().modified();
  };

  /**
   * Update.
   */
  this._update = function () {
    s_update.call(m_this);

    if (m_this.dataTime().getMTime() >= m_this.buildTime().getMTime() ||
        m_this.updateTime().getMTime() < m_this.getMTime()) {
      m_this._build();
    } else {
      var data = m_this.data(),
          radFunc = m_this.style.get('radius');

      const scalingFactor = m_this.layer().map().unitsPerPixel(m_this.layer().map().zoom());
      const dataArray = m_pointSet.getPointData().getArray('diam');
      const newScaleArray = dataArray.getData().map((v, i) => radFunc(data[i], i) * scalingFactor * 2);

      dataArray.setData(newScaleArray);
      m_pointSet.modified();
    }

    m_this.updateTime().modified();
  };

  /**
   * Destroy.
   */
  this._exit = function () {
    m_this.renderer().contextRenderer().removeActor(m_actor);
    s_exit();
  };

  m_this._init();
  return this;
};

inherit(vtkjs_pointFeature, pointFeature);

var capabilities = {};
capabilities[pointFeature.capabilities.stroke] = false;

// Now register it
registerFeature('vtkjs', 'point', vtkjs_pointFeature, capabilities);

module.exports = vtkjs_pointFeature;