vtkjs/vtkjsRenderer.js

var inherit = require('../inherit');
var registerRenderer = require('../registry').registerRenderer;
var renderer = require('../renderer');

/**
 * Create a new instance of class vtkjsRenderer.
 *
 * @class
 * @alias geo.vtkjs.vtkjsRenderer
 * @extends geo.renderer
 * @param {object} arg Options for the renderer.
 * @param {geo.layer} [arg.layer] Layer associated with the renderer.
 * @param {HTMLElement} [arg.canvas] Canvas element associated with the
 *   renderer.
 * @returns {geo.vtkjs.vtkjsRenderer}
 */
var vtkjsRenderer = function (arg) {
  'use strict';

  if (!(this instanceof vtkjsRenderer)) {
    return new vtkjsRenderer(arg);
  }
  arg = arg || {};
  renderer.call(this, arg);

  var mat4 = require('gl-mat4');
  var geo_event = require('../event');
  var vtkjs = vtkjsRenderer.vtkjs;
  var vtkGenericRenderWindow = vtkjs.Rendering.Misc.vtkGenericRenderWindow;

  var m_this = this,
      s_init = this._init;

  var vtkRenderer = vtkGenericRenderWindow.newInstance({
    background: [0, 0, 0, 0]});
  vtkRenderer.setContainer(m_this.layer().node().get(0));
  // TODO: Is there a way to start with no interactor rather than unbinding it?
  vtkRenderer.getInteractor().unbindEvents();
  vtkRenderer.resize();
  var vtkjsren = vtkRenderer.getRenderer();
  var renderWindow = vtkRenderer.getRenderWindow();

  /**
   * Get context specific renderer.
   *
   * @returns {object} The vtkjs context renderer.
   */
  this.contextRenderer = function () {
    return vtkjsren;
  };

  /**
   * Get API used by the renderer.
   *
   * @returns {string} `vtkjs`.
   */
  this.api = function () {
    return vtkjsRenderer.apiname;
  };

  /**
   * Initialize.
   *
   * @returns {this}
   */
  this._init = function () {
    if (m_this.initialized()) {
      return m_this;
    }

    s_init.call(m_this);

    /* Initialize the size of the renderer */
    var map = m_this.layer().map(),
        mapSize = map.size();
    m_this._resize(0, 0, mapSize.width, mapSize.height);
    // TODO: figure out what the clipbounds actually should be and handle
    // perspective modes properly.
    map.camera().clipbounds = {near: -map.unitsPerPixel(), far: map.unitsPerPixel()};
    return m_this;
  };

  /**
   * Handle resize event.
   *
   * @param {number} x The left coordinate.
   * @param {number} y The top coordinate.
   * @param {number} w The width in pixels.
   * @param {number} h The height in pixels.
   * @returns {this}
   */
  this._resize = function (x, y, w, h) {
    m_this._setWidthHeight(w, h);
    vtkRenderer.resize();
    m_this._render();
    return m_this;
  };

  /**
   * Render.  This actually schedules rendering for the next animation frame.
   *
   * @returns {this}
   */
  this._render = function () {
    /* If we are already scheduled to render, don't schedule again.  Rather,
     * mark that we should render after other animation frame requests occur.
     * It would be nice if we could just reschedule the call by removing and
     * readding the animation frame request, but this doesn't work for if the
     * reschedule occurs during another animation frame callback (it then waits
     * until a subsequent frame). */
    m_this.layer().map().scheduleAnimationFrame(m_this._renderFrame, true);
    return m_this;
  };

  /**
   * This actually renders.
   */
  this._renderFrame = function () {
    var layer = m_this.layer(),
        features = layer.features(),
        i;
    // TODO: draw something else should trigger feature update
    for (i = 0; i < features.length; i += 1) {
      if (features[i].visible()) {
        features[i]._update();
      }
    }
    m_this._updateRendererCamera();
    renderWindow.render();
  };

  /**
   * Exit.
   */
  this._exit = function () {
    // DO NOTHING
  };

  this._updateRendererCamera = function () {
    var map = m_this.layer().map(),
        camera = map.camera(),
        view = camera.view,
        proj = camera.projectionMatrix;
    var viewmat = mat4.create();
    mat4.copy(viewmat, view);
    var projmat = mat4.create();
    mat4.copy(projmat, proj);
    m_this.contextRenderer().getActiveCamera().setViewMatrix(viewmat);
    m_this.contextRenderer().getActiveCamera().setProjectionMatrix(projmat);
  };

  /* Connect to pan event.  This is sufficient, as all zooms and rotations also
   * produce a pan */
  m_this.layer().geoOn(geo_event.pan, function (evt) {
    // TODO: We may only need to do this if the zoom level has changed.
    m_this._render();
  });

  /* Connect to parallelprojection event. */
  m_this.layer().geoOn(geo_event.parallelprojection, function (evt) {
    // DO NOTHING
  });

  return this;
};
vtkjsRenderer.apiname = 'vtkjs';

inherit(vtkjsRenderer, renderer);

registerRenderer('vtkjs', vtkjsRenderer);

/**
 * Report if the vtkjs renderer is supported.  This is just a check if vtkjs is
 * available.
 *
 * @returns {boolean} true if available.
 */
vtkjsRenderer.supported = function () {
  delete vtkjsRenderer.vtkjs;
  // webpack expects optional dependencies to be wrapped in a try-catch
  try {
    vtkjsRenderer.vtkjs = require('vtk.js');
  } catch (_error) {}
  if ((!vtkjsRenderer.vtkjs || !vtkjsRenderer.vtkjs.Rendering) && window.vtk && window.vtk.Rendering) {
    vtkjsRenderer.vtkjs = window.vtk;
  }
  if (!vtkjsRenderer.vtkjs || !vtkjsRenderer.vtkjs.Rendering) {
    vtkjsRenderer.vtkjs = undefined;
  }
  return vtkjsRenderer.vtkjs !== undefined;
};

/**
 * If the vtkjs renderer is not supported, supply the name of a renderer that
 * should be used instead.  This asks for the null renderer.
 *
 * @returns {null} `null` for the null renderer.
 */
vtkjsRenderer.fallback = function () {
  return null;
};

vtkjsRenderer.supported();  // cache reference to vtkjs if it is available

module.exports = vtkjsRenderer;