Source: draw.js

/** @module draw */
define(['underscore', 'three', 'jquery'], function(_, THREE, $) {

  var NUM_TUBE_SEGMENTS = 3;
  var NUM_TUBE_CROSS_SECTION_POINTS = 10;

  // useful for some calculations
  var ZERO = new THREE.Vector3();

  /**
   *
   * @class EmperorTrajectory
   *
   * This class represents the internal logic for a linearly interpolated
   * tube/trajectory in THREE.js
   *
   * [This answer]{@link http://stackoverflow.com/a/18580832/379593} on
   * StackOverflow helped a lot.
   * @return {EmperorTrajectory}
   * @extends THREE.Curve
   */
  THREE.EmperorTrajectory = THREE.Curve.create(
      function(points) {
        this.points = (points === undefined) ? [] : points;
      },

      function(t) {
        var points = this.points;
        var index = (points.length - 1) * t;
        var floorIndex = Math.floor(index);

        if (floorIndex == points.length - 1) {
          return points[floorIndex];
        }

        var floorPoint = points[floorIndex];
        var ceilPoint = points[floorIndex + 1];

        return floorPoint.clone().lerp(ceilPoint, index - floorIndex);
      }
      );

  /** @private */
  THREE.EmperorTrajectory.prototype.getUtoTmapping = function(u) {
    return u;
  };

  /**
   *
   * @class EmperorArrowHelper
   *
   * Subclass of THREE.ArrowHelper to make raycasting work on the line and cone
   * children.
   *
   * For more information about the arguments, see the [online documentation]
   * {@link https://threejs.org/docs/#api/helpers/ArrowHelper}.
   * @return {EmperorArrowHelper}
   * @extends THREE.ArrowHelper
   *
   */
  function EmperorArrowHelper(dir, origin, length, color, headLength,
                              headWidth, name) {
    THREE.ArrowHelper.call(this, dir, origin, length, color, headLength,
                           headWidth);

    this.name = name;
    this.line.name = this.name;
    this.cone.name = this.name;

    this.label = makeLabel(this.cone.position.toArray(), this.name, color);
    this.add(this.label);

    return this;
  }
  EmperorArrowHelper.prototype = Object.create(THREE.ArrowHelper.prototype);
  EmperorArrowHelper.prototype.constructor = THREE.ArrowHelper;

  /**
   *
   * Check for ray casting with arrow's cone.
   *
   * This class may need to disappear if THREE.ArrowHelper implements the
   * raycast method, for more information see the [online documentation]
   * {@link https://threejs.org/docs/#api/helpers/ArrowHelper}.
   *
   */
  EmperorArrowHelper.prototype.raycast = function(raycaster, intersects) {
    // Two considerations:
    // * Don't raycast the label since that one is self-explanatory
    // * Don't raycast to the line as it adds a lot of noise to the raycaster.
    //   If raycasting is enabled for lines, this will result in incorrect
    //   intersects showing as the closest to the ray i.e. wrong labels.
    this.cone.raycast(raycaster, intersects);
  };

  /**
   *
   * Set the arrow's color
   *
   * @param {THREE.Color} color The color to set for the line, cone and label.
   *
   */
  EmperorArrowHelper.prototype.setColor = function(color) {
    THREE.ArrowHelper.prototype.setColor.call(this, color);
    this.label.material.color.set(color);
  };

  /**
   *
   * Change the vector where the arrow points to
   *
   * @param {THREE.Vector3} target The vector where the arrow will point to.
   * Note, the label will also change position.
   *
   */
  EmperorArrowHelper.prototype.setPointsTo = function(target) {
    var length;

    // calculate the length before normalizing to a unit vector
    target = target.sub(ZERO);
    length = ZERO.distanceTo(target);
    target.normalize();

    this.setDirection(target.sub(ZERO));
    this.setLength(length);

    this.label.position.copy(this.cone.position);
  };

  /**
   * Dispose of underlying objects
   */
  EmperorArrowHelper.prototype.dispose = function() {
    // dispose each object according to THREE's guide
    this.label.material.map.dispose();
    this.label.material.dispose();
    this.label.geometry.dispose();

    this.cone.material.dispose();
    this.cone.geometry.dispose();

    this.line.material.dispose();
    this.line.geometry.dispose();

    this.remove(this.label);
    this.remove(this.cone);
    this.remove(this.line);

    this.label = null;
    this.cone = null;
    this.line = null;
  };

  /**
   *
   * Create a generic THREE.Line object
   *
   * @param {float[]} start The x, y and z coordinates of one of the ends
   * of the line.
   * @param {float[]} end The x, y and z coordinates of one of the ends
   * of the line.
   * @param {integer} color Hexadecimal base that specifies the color of the
   * line.
   * @param {float} width The width of the line being drawn.
   * @param {boolean} transparent Whether the line will be transparent or not.
   *
   * @return {THREE.Line}
   * @function makeLine
   */
  function makeLine(start, end, color, width, transparent) {
    // based on the example described in:
    // https://github.com/mrdoob/three.js/wiki/Drawing-lines
    var material, geometry, line;

    // make the material transparent and with full opacity
    material = new THREE.LineBasicMaterial({color: color, linewidth: width});
    material.matrixAutoUpdate = true;
    material.transparent = transparent;
    material.opacity = 1.0;

    // add the two vertices to the geometry
    geometry = new THREE.Geometry();
    geometry.vertices.push(new THREE.Vector3(start[0], start[1], start[2]));
    geometry.vertices.push(new THREE.Vector3(end[0], end[1], end[2]));

    // the line will contain the two vertices and the described material
    line = new THREE.Line(geometry, material);

    return line;
  }

  /**
   *
   * @class EmperorLineSegments
   *
   * Subclass of THREE.LineSegments to make vertex modifications easier.
   *
   * @return {EmperorLineSegments}
   * @extends THREE.LineSegments
   */
  function EmperorLineSegments(geometry, material) {
    THREE.LineSegments.call(this, geometry, material);

    return this;
  }
  EmperorLineSegments.prototype = Object.create(THREE.LineSegments.prototype);
  EmperorLineSegments.prototype.constructor = THREE.LineSegments;

  /**
   *
   * Set the start and end points for a line in the collection.
   *
   * @param {Integer} i The index of the line;
   * @param {Float[]} start An array of the starting point of the line ([x, y,
   * z]).
   * @param {Float[]} start An array of the ending point of the line ([x, y,
   * z]).
   */
  EmperorLineSegments.prototype.setLineAtIndex = function(i, start, end) {
    var vertices = this.geometry.attributes.position.array;

    vertices[(i * 6)] = start[0];
    vertices[(i * 6) + 1] = start[1];
    vertices[(i * 6) + 2] = start[2];
    vertices[(i * 6) + 3] = end[0];
    vertices[(i * 6) + 4] = end[1];
    vertices[(i * 6) + 5] = end[2];
  };

  /**
   *
   * Create a collection of disconnected lines.
   *
   * This function is specially useful when creating a lot of lines as it uses
   * a BufferGeometry for improved performance.
   *
   * @param {Array[]} vertices List of vertices used to create the lines. Each
   * line is connected on as (vertices[i], vertices[i+1),
   * (vertices[i+2], vertices[i+3]), etc.
   * @param {integer} color Hexadecimal base that specifies the color of the
   * line.
   *
   * @return {EmperorLineSegments}
   * @function makeLineCollection
   *
   */
  function makeLineCollection(vertices, color) {
    // based on https://jsfiddle.net/wilt/bd8trrLx/
    var material = new THREE.LineBasicMaterial({
      color: color || 0xff0000
    });

    var positions = new Float32Array(vertices.length * 3);

    for (var i = 0; i < vertices.length; i++) {

      positions[i * 3] = vertices[i][0];
      positions[i * 3 + 1] = vertices[i][1];
      positions[i * 3 + 2] = vertices[i][2];

    }

    var indices = _.range(vertices.length);
    var geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));

    return new EmperorLineSegments(geometry, material);
  }

  /**
   *
   * Create a generic Arrow object (composite of a cone and line)
   *
   * @param {float[]} from The x, y and z coordinates where the arrow
   * originates from.
   * @param {float[]} to The x, y and z coordinates where the arrow points to.
   * @param {integer} color Hexadecimal base that specifies the color of the
   * line.
   * @param {String} name The text to be used in the label, and the name of
   * the line and cone (used for raycasting).
   *
   * @return {THREE.Object3D}
   * @function makeArrow
   */
  function makeArrow(from, to, color, name) {
    var target, origin, direction, length, arrow;

    target = new THREE.Vector3(to[0], to[1], to[2]);
    origin = new THREE.Vector3(from[0], from[1], from[2]);

    length = origin.distanceTo(target);

    // https://stackoverflow.com/a/20558498/379593
    direction = target.sub(origin);

    direction.normalize();

    // don't set the head size or width, defaults are good enough
    arrow = new EmperorArrowHelper(direction, origin, length, color,
                                   undefined, undefined, name);

    return arrow;
  }

  /**
   * Returns a new trajectory line dynamic mesh
   */
  function drawTrajectoryLineDynamic(trajectory, currentFrame, color, radius) {
    // based on the example described in:
    // https://github.com/mrdoob/three.js/wiki/Drawing-lines
    var material, points = [], lineGeometry, limit = 0, path;

    _trajectory = trajectory.representativeInterpolatedCoordinatesAtIndex(
                                                                  currentFrame);
    if (_trajectory === null || _trajectory.length == 0)
      return null;

    material = new THREE.MeshPhongMaterial({
        color: color,
        transparent: false});

    for (var index = 0; index < _trajectory.length; index++) {
      points.push(new THREE.Vector3(_trajectory[index].x,
                  _trajectory[index].y, _trajectory[index].z));
    }

    path = new THREE.EmperorTrajectory(points);

    // the line will contain the two vertices and the described material
    // we increase the number of points to have a smoother transition on
    // edges i. e. where the trajectory changes the direction it is going
    lineGeometry = new THREE.TubeGeometry(path,
                                    (points.length - 1) * NUM_TUBE_SEGMENTS,
                                    radius,
                                    NUM_TUBE_CROSS_SECTION_POINTS,
                                    false);

    return new THREE.Mesh(lineGeometry, material);
  }

  /**
   * Disposes a trajectory line dynamic mesh
   */
  function disposeTrajectoryLineDynamic(mesh) {
    mesh.geometry.dispose();
    mesh.material.dispose();
  }

  /**
   * Returns a new trajectory line static mesh
   */
  function drawTrajectoryLineStatic(trajectory, color, radius) {
    var _trajectory = trajectory.coordinates;

    var material = new THREE.MeshPhongMaterial({
      color: color,
      transparent: false}
    );

    var allPoints = [];
    for (var index = 0; index < _trajectory.length; index++) {
      allPoints.push(new THREE.Vector3(_trajectory[index].x,
                  _trajectory[index].y, _trajectory[index].z));
    }

    var path = new THREE.EmperorTrajectory(allPoints);

    //Tubes are straight segments, but adding vertices along them might change
    //lighting effects under certain models and lighting conditions.
    var tubeBufferGeom = new THREE.TubeBufferGeometry(
                                path,
                                (allPoints.length - 1) * NUM_TUBE_SEGMENTS,
                                radius,
                                NUM_TUBE_CROSS_SECTION_POINTS,
                                false);

    return new THREE.Mesh(tubeBufferGeom, material);
  }

  /**
   * Disposes a trajectory line static mesh
   */
  function disposeTrajectoryLineStatic(mesh) {
    mesh.geometry.dispose();
    mesh.material.dispose();
  }

  function updateStaticTrajectoryDrawRange(trajectory, currentFrame, threeMesh)
  {
    //Reverse engineering the number of points in a THREE tube is not fun, and
    //may be implementation/version dependent.
    //Number of points drawn per tube segment =
    //  2 (triangles) * 3 (points per triangle) * NUM_TUBE_CROSS_SECTION_POINTS
    //Number of tube segments per pair of consecutive points =
    //  NUM_TUBE_SEGMENTS

    var multiplier = 2 * 3 * NUM_TUBE_CROSS_SECTION_POINTS * NUM_TUBE_SEGMENTS;
    if (currentFrame < trajectory._intervalValues.length)
    {
      var intervalValue = trajectory._intervalValues[currentFrame];
      threeMesh.geometry.setDrawRange(0, intervalValue * multiplier);
    }
    else
    {
      threeMesh.geometry.setDrawRange(0,
                            (trajectory.coordinates.length - 1) * multiplier);
    }
  }


  /**
   *
   * Create a THREE object that displays 2D text, this implementation is based
   * on the answer found
   * [here]{@link http://stackoverflow.com/a/14106703/379593}
   *
   * The text is returned scaled to its size in pixels, hence you'll need to
   * scale it down depending on the scene's dimensions.
   *
   * Warning: The text sizes vary slightly depending on the browser and OS you
   * use. This is specially important for testing.
   *
   * @param {float[]} position The x, y, and z location of the label.
   * @param {string} text The text to be shown on screen.
   * @param {integer|string} Color Hexadecimal base that represents the color
   * of the text.
   *
   * @return {THREE.Sprite} Object with the text displaying in it.
   * @function makeLabel
   **/
  function makeLabel(position, text, color) {
    // the font size determines the resolution relative to the sprite object
    var fontSize = 32, canvas, context, measure;

    canvas = document.createElement('canvas');
    context = canvas.getContext('2d');

    // set the font size so we can measure the width
    context.font = fontSize + 'px Arial';
    measure = context.measureText(text);

    // make the dimensions a power of 2 (for use in THREE.js)
    canvas.width = THREE.Math.ceilPowerOfTwo(measure.width);
    canvas.height = THREE.Math.ceilPowerOfTwo(fontSize);

    // after changing the canvas' size we need to reset the font attributes
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.font = fontSize + 'px Arial';
    if (_.isNumber(color)) {
      context.fillStyle = '#' + color.toString(16);
    }
    else {
      context.fillStyle = color;
    }
    context.fillText(text, canvas.width / 2, canvas.height / 2);

    var amap = new THREE.Texture(canvas);
    amap.needsUpdate = true;

    var mat = new THREE.SpriteMaterial({
        map: amap,
        transparent: true,
        color: color
    });

    var sp = new THREE.Sprite(mat);
    sp.position.set(position[0], position[1], position[2]);
    sp.scale.set(canvas.width, canvas.height, 1);

    // add an extra attribute so we can render this properly when we use
    // SVGRenderer
    sp.text = text;

    return sp;
  }

  /**
   *
   * Format an SVG string with labels and colors.
   *
   * @param {string[]} labels The names for the label.
   * @param {integer[]} colors The colors for each label.
   *
   * @return {string} SVG string with the labels and colors values formated as
   * a legend.
   * @function formatSVGLegend
   */
  function formatSVGLegend(labels, colors) {
    var labels_svg = '', pos_y = 1, increment = 40, max_len = 0, rect_width,
    font_size = 12;

    for (var i = 0; i < labels.length; i++) {
      // add the rectangle with the corresponding color
      labels_svg += '<rect height="27" width="27" y="' + pos_y +
        '" x="5" style="stroke-width:1;stroke:rgb(0,0,0)" fill="' +
        colors[i] + '"/>';

      // add the name of the category
      labels_svg += '<text xml:space="preserve" y="' + (pos_y + 20) +
        '" x="40" font-size="' + font_size +
        '" stroke-width="0" stroke="#000000" fill="#000000">' + labels[i] +
        '</text>';

      pos_y += increment;
    }

    // get the name with the maximum number of characters and get the length
    max_len = _.max(labels, function(a) {return a.length}).length;

      // duplicate the size of the rectangle to make sure it fits the labels
      rect_width = font_size * max_len * 2;

    labels_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' +
      rect_width + '" height="' + (pos_y - 10) + '"><g>' + labels_svg +
      '</g></svg>';

    return labels_svg;
  }

  return {'formatSVGLegend': formatSVGLegend, 'makeLine': makeLine,
          'makeLabel': makeLabel, 'makeArrow': makeArrow,
          'drawTrajectoryLineStatic': drawTrajectoryLineStatic,
          'disposeTrajectoryLineStatic': disposeTrajectoryLineStatic,
          'drawTrajectoryLineDynamic': drawTrajectoryLineDynamic,
          'disposeTrajectoryLineDynamic': disposeTrajectoryLineDynamic,
          'updateStaticTrajectoryDrawRange': updateStaticTrajectoryDrawRange,
          'makeLineCollection': makeLineCollection};
});