1 /** @module draw */
  2 define(['underscore', 'three', 'jquery'], function(_, THREE, $) {
  3 
  4   var NUM_TUBE_SEGMENTS = 3;
  5   var NUM_TUBE_CROSS_SECTION_POINTS = 10;
  6 
  7   // useful for some calculations
  8   var ZERO = new THREE.Vector3();
  9 
 10   /**
 11    *
 12    * @class EmperorTrajectory
 13    *
 14    * This class represents the internal logic for a linearly interpolated
 15    * tube/trajectory in THREE.js
 16    *
 17    * [This answer]{@link http://stackoverflow.com/a/18580832/379593} on
 18    * StackOverflow helped a lot.
 19    * @return {EmperorTrajectory}
 20    * @extends THREE.Curve
 21    */
 22   THREE.EmperorTrajectory = THREE.Curve.create(
 23       function(points) {
 24         this.points = (points === undefined) ? [] : points;
 25       },
 26 
 27       function(t) {
 28         var points = this.points;
 29         var index = (points.length - 1) * t;
 30         var floorIndex = Math.floor(index);
 31 
 32         if (floorIndex == points.length - 1) {
 33           return points[floorIndex];
 34         }
 35 
 36         var floorPoint = points[floorIndex];
 37         var ceilPoint = points[floorIndex + 1];
 38 
 39         return floorPoint.clone().lerp(ceilPoint, index - floorIndex);
 40       }
 41       );
 42 
 43   /** @private */
 44   THREE.EmperorTrajectory.prototype.getUtoTmapping = function(u) {
 45     return u;
 46   };
 47 
 48   /**
 49    *
 50    * @class EmperorArrowHelper
 51    *
 52    * Subclass of THREE.ArrowHelper to make raycasting work on the line and cone
 53    * children.
 54    *
 55    * For more information about the arguments, see the [online documentation]
 56    * {@link https://threejs.org/docs/#api/helpers/ArrowHelper}.
 57    * @return {EmperorArrowHelper}
 58    * @extends THREE.ArrowHelper
 59    *
 60    */
 61   function EmperorArrowHelper(dir, origin, length, color, headLength,
 62                               headWidth, name) {
 63     THREE.ArrowHelper.call(this, dir, origin, length, color, headLength,
 64                            headWidth);
 65 
 66     this.name = name;
 67     this.line.name = this.name;
 68     this.cone.name = this.name;
 69 
 70     this.label = makeLabel(this.cone.position.toArray(), this.name, color);
 71     this.add(this.label);
 72 
 73     return this;
 74   }
 75   EmperorArrowHelper.prototype = Object.create(THREE.ArrowHelper.prototype);
 76   EmperorArrowHelper.prototype.constructor = THREE.ArrowHelper;
 77 
 78   /**
 79    *
 80    * Check for ray casting with arrow's cone.
 81    *
 82    * This class may need to disappear if THREE.ArrowHelper implements the
 83    * raycast method, for more information see the [online documentation]
 84    * {@link https://threejs.org/docs/#api/helpers/ArrowHelper}.
 85    *
 86    */
 87   EmperorArrowHelper.prototype.raycast = function(raycaster, intersects) {
 88     // Two considerations:
 89     // * Don't raycast the label since that one is self-explanatory
 90     // * Don't raycast to the line as it adds a lot of noise to the raycaster.
 91     //   If raycasting is enabled for lines, this will result in incorrect
 92     //   intersects showing as the closest to the ray i.e. wrong labels.
 93     this.cone.raycast(raycaster, intersects);
 94   };
 95 
 96   /**
 97    *
 98    * Set the arrow's color
 99    *
100    * @param {THREE.Color} color The color to set for the line, cone and label.
101    *
102    */
103   EmperorArrowHelper.prototype.setColor = function(color) {
104     THREE.ArrowHelper.prototype.setColor.call(this, color);
105     this.label.material.color.set(color);
106   };
107 
108   /**
109    *
110    * Change the vector where the arrow points to
111    *
112    * @param {THREE.Vector3} target The vector where the arrow will point to.
113    * Note, the label will also change position.
114    *
115    */
116   EmperorArrowHelper.prototype.setPointsTo = function(target) {
117     var length;
118 
119     // calculate the length before normalizing to a unit vector
120     target = target.sub(ZERO);
121     length = ZERO.distanceTo(target);
122     target.normalize();
123 
124     this.setDirection(target.sub(ZERO));
125     this.setLength(length);
126 
127     this.label.position.copy(this.cone.position);
128   };
129 
130   /**
131    * Dispose of underlying objects
132    */
133   EmperorArrowHelper.prototype.dispose = function() {
134     // dispose each object according to THREE's guide
135     this.label.material.map.dispose();
136     this.label.material.dispose();
137     this.label.geometry.dispose();
138 
139     this.cone.material.dispose();
140     this.cone.geometry.dispose();
141 
142     this.line.material.dispose();
143     this.line.geometry.dispose();
144 
145     this.remove(this.label);
146     this.remove(this.cone);
147     this.remove(this.line);
148 
149     this.label = null;
150     this.cone = null;
151     this.line = null;
152   };
153 
154   /**
155    *
156    * Create a generic THREE.Line object
157    *
158    * @param {float[]} start The x, y and z coordinates of one of the ends
159    * of the line.
160    * @param {float[]} end The x, y and z coordinates of one of the ends
161    * of the line.
162    * @param {integer} color Hexadecimal base that specifies the color of the
163    * line.
164    * @param {float} width The width of the line being drawn.
165    * @param {boolean} transparent Whether the line will be transparent or not.
166    *
167    * @return {THREE.Line}
168    * @function makeLine
169    */
170   function makeLine(start, end, color, width, transparent) {
171     // based on the example described in:
172     // https://github.com/mrdoob/three.js/wiki/Drawing-lines
173     var material, geometry, line;
174 
175     // make the material transparent and with full opacity
176     material = new THREE.LineBasicMaterial({color: color, linewidth: width});
177     material.matrixAutoUpdate = true;
178     material.transparent = transparent;
179     material.opacity = 1.0;
180 
181     // add the two vertices to the geometry
182     geometry = new THREE.Geometry();
183     geometry.vertices.push(new THREE.Vector3(start[0], start[1], start[2]));
184     geometry.vertices.push(new THREE.Vector3(end[0], end[1], end[2]));
185 
186     // the line will contain the two vertices and the described material
187     line = new THREE.Line(geometry, material);
188 
189     return line;
190   }
191 
192   /**
193    *
194    * @class EmperorLineSegments
195    *
196    * Subclass of THREE.LineSegments to make vertex modifications easier.
197    *
198    * @return {EmperorLineSegments}
199    * @extends THREE.LineSegments
200    */
201   function EmperorLineSegments(geometry, material) {
202     THREE.LineSegments.call(this, geometry, material);
203 
204     return this;
205   }
206   EmperorLineSegments.prototype = Object.create(THREE.LineSegments.prototype);
207   EmperorLineSegments.prototype.constructor = THREE.LineSegments;
208 
209   /**
210    *
211    * Set the start and end points for a line in the collection.
212    *
213    * @param {Integer} i The index of the line;
214    * @param {Float[]} start An array of the starting point of the line ([x, y,
215    * z]).
216    * @param {Float[]} start An array of the ending point of the line ([x, y,
217    * z]).
218    */
219   EmperorLineSegments.prototype.setLineAtIndex = function(i, start, end) {
220     var vertices = this.geometry.attributes.position.array;
221 
222     vertices[(i * 6)] = start[0];
223     vertices[(i * 6) + 1] = start[1];
224     vertices[(i * 6) + 2] = start[2];
225     vertices[(i * 6) + 3] = end[0];
226     vertices[(i * 6) + 4] = end[1];
227     vertices[(i * 6) + 5] = end[2];
228   };
229 
230   /**
231    *
232    * Create a collection of disconnected lines.
233    *
234    * This function is specially useful when creating a lot of lines as it uses
235    * a BufferGeometry for improved performance.
236    *
237    * @param {Array[]} vertices List of vertices used to create the lines. Each
238    * line is connected on as (vertices[i], vertices[i+1),
239    * (vertices[i+2], vertices[i+3]), etc.
240    * @param {integer} color Hexadecimal base that specifies the color of the
241    * line.
242    *
243    * @return {EmperorLineSegments}
244    * @function makeLineCollection
245    *
246    */
247   function makeLineCollection(vertices, color) {
248     // based on https://jsfiddle.net/wilt/bd8trrLx/
249     var material = new THREE.LineBasicMaterial({
250       color: color || 0xff0000
251     });
252 
253     var positions = new Float32Array(vertices.length * 3);
254 
255     for (var i = 0; i < vertices.length; i++) {
256 
257       positions[i * 3] = vertices[i][0];
258       positions[i * 3 + 1] = vertices[i][1];
259       positions[i * 3 + 2] = vertices[i][2];
260 
261     }
262 
263     var indices = _.range(vertices.length);
264     var geometry = new THREE.BufferGeometry();
265     geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
266     geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(indices), 1));
267 
268     return new EmperorLineSegments(geometry, material);
269   }
270 
271   /**
272    *
273    * Create a generic Arrow object (composite of a cone and line)
274    *
275    * @param {float[]} from The x, y and z coordinates where the arrow
276    * originates from.
277    * @param {float[]} to The x, y and z coordinates where the arrow points to.
278    * @param {integer} color Hexadecimal base that specifies the color of the
279    * line.
280    * @param {String} name The text to be used in the label, and the name of
281    * the line and cone (used for raycasting).
282    *
283    * @return {THREE.Object3D}
284    * @function makeArrow
285    */
286   function makeArrow(from, to, color, name) {
287     var target, origin, direction, length, arrow;
288 
289     target = new THREE.Vector3(to[0], to[1], to[2]);
290     origin = new THREE.Vector3(from[0], from[1], from[2]);
291 
292     length = origin.distanceTo(target);
293 
294     // https://stackoverflow.com/a/20558498/379593
295     direction = target.sub(origin);
296 
297     direction.normalize();
298 
299     // don't set the head size or width, defaults are good enough
300     arrow = new EmperorArrowHelper(direction, origin, length, color,
301                                    undefined, undefined, name);
302 
303     return arrow;
304   }
305 
306   /**
307    * Returns a new trajectory line dynamic mesh
308    */
309   function drawTrajectoryLineDynamic(trajectory, currentFrame, color, radius) {
310     // based on the example described in:
311     // https://github.com/mrdoob/three.js/wiki/Drawing-lines
312     var material, points = [], lineGeometry, limit = 0, path;
313 
314     _trajectory = trajectory.representativeInterpolatedCoordinatesAtIndex(
315                                                                   currentFrame);
316     if (_trajectory === null || _trajectory.length == 0)
317       return null;
318 
319     material = new THREE.MeshPhongMaterial({
320         color: color,
321         transparent: false});
322 
323     for (var index = 0; index < _trajectory.length; index++) {
324       points.push(new THREE.Vector3(_trajectory[index].x,
325                   _trajectory[index].y, _trajectory[index].z));
326     }
327 
328     path = new THREE.EmperorTrajectory(points);
329 
330     // the line will contain the two vertices and the described material
331     // we increase the number of points to have a smoother transition on
332     // edges i. e. where the trajectory changes the direction it is going
333     lineGeometry = new THREE.TubeGeometry(path,
334                                     (points.length - 1) * NUM_TUBE_SEGMENTS,
335                                     radius,
336                                     NUM_TUBE_CROSS_SECTION_POINTS,
337                                     false);
338 
339     return new THREE.Mesh(lineGeometry, material);
340   }
341 
342   /**
343    * Disposes a trajectory line dynamic mesh
344    */
345   function disposeTrajectoryLineDynamic(mesh) {
346     mesh.geometry.dispose();
347     mesh.material.dispose();
348   }
349 
350   /**
351    * Returns a new trajectory line static mesh
352    */
353   function drawTrajectoryLineStatic(trajectory, color, radius) {
354     var _trajectory = trajectory.coordinates;
355 
356     var material = new THREE.MeshPhongMaterial({
357       color: color,
358       transparent: false}
359     );
360 
361     var allPoints = [];
362     for (var index = 0; index < _trajectory.length; index++) {
363       allPoints.push(new THREE.Vector3(_trajectory[index].x,
364                   _trajectory[index].y, _trajectory[index].z));
365     }
366 
367     var path = new THREE.EmperorTrajectory(allPoints);
368 
369     //Tubes are straight segments, but adding vertices along them might change
370     //lighting effects under certain models and lighting conditions.
371     var tubeBufferGeom = new THREE.TubeBufferGeometry(
372                                 path,
373                                 (allPoints.length - 1) * NUM_TUBE_SEGMENTS,
374                                 radius,
375                                 NUM_TUBE_CROSS_SECTION_POINTS,
376                                 false);
377 
378     return new THREE.Mesh(tubeBufferGeom, material);
379   }
380 
381   /**
382    * Disposes a trajectory line static mesh
383    */
384   function disposeTrajectoryLineStatic(mesh) {
385     mesh.geometry.dispose();
386     mesh.material.dispose();
387   }
388 
389   function updateStaticTrajectoryDrawRange(trajectory, currentFrame, threeMesh)
390   {
391     //Reverse engineering the number of points in a THREE tube is not fun, and
392     //may be implementation/version dependent.
393     //Number of points drawn per tube segment =
394     //  2 (triangles) * 3 (points per triangle) * NUM_TUBE_CROSS_SECTION_POINTS
395     //Number of tube segments per pair of consecutive points =
396     //  NUM_TUBE_SEGMENTS
397 
398     var multiplier = 2 * 3 * NUM_TUBE_CROSS_SECTION_POINTS * NUM_TUBE_SEGMENTS;
399     if (currentFrame < trajectory._intervalValues.length)
400     {
401       var intervalValue = trajectory._intervalValues[currentFrame];
402       threeMesh.geometry.setDrawRange(0, intervalValue * multiplier);
403     }
404     else
405     {
406       threeMesh.geometry.setDrawRange(0,
407                             (trajectory.coordinates.length - 1) * multiplier);
408     }
409   }
410 
411 
412   /**
413    *
414    * Create a THREE object that displays 2D text, this implementation is based
415    * on the answer found
416    * [here]{@link http://stackoverflow.com/a/14106703/379593}
417    *
418    * The text is returned scaled to its size in pixels, hence you'll need to
419    * scale it down depending on the scene's dimensions.
420    *
421    * Warning: The text sizes vary slightly depending on the browser and OS you
422    * use. This is specially important for testing.
423    *
424    * @param {float[]} position The x, y, and z location of the label.
425    * @param {string} text The text to be shown on screen.
426    * @param {integer|string} Color Hexadecimal base that represents the color
427    * of the text.
428    *
429    * @return {THREE.Sprite} Object with the text displaying in it.
430    * @function makeLabel
431    **/
432   function makeLabel(position, text, color) {
433     // the font size determines the resolution relative to the sprite object
434     var fontSize = 32, canvas, context, measure;
435 
436     canvas = document.createElement('canvas');
437     context = canvas.getContext('2d');
438 
439     // set the font size so we can measure the width
440     context.font = fontSize + 'px Arial';
441     measure = context.measureText(text);
442 
443     // make the dimensions a power of 2 (for use in THREE.js)
444     canvas.width = THREE.Math.ceilPowerOfTwo(measure.width);
445     canvas.height = THREE.Math.ceilPowerOfTwo(fontSize);
446 
447     // after changing the canvas' size we need to reset the font attributes
448     context.textAlign = 'center';
449     context.textBaseline = 'middle';
450     context.font = fontSize + 'px Arial';
451     if (_.isNumber(color)) {
452       context.fillStyle = '#' + color.toString(16);
453     }
454     else {
455       context.fillStyle = color;
456     }
457     context.fillText(text, canvas.width / 2, canvas.height / 2);
458 
459     var amap = new THREE.Texture(canvas);
460     amap.needsUpdate = true;
461 
462     var mat = new THREE.SpriteMaterial({
463         map: amap,
464         transparent: true,
465         color: color
466     });
467 
468     var sp = new THREE.Sprite(mat);
469     sp.position.set(position[0], position[1], position[2]);
470     sp.scale.set(canvas.width, canvas.height, 1);
471 
472     // add an extra attribute so we can render this properly when we use
473     // SVGRenderer
474     sp.text = text;
475 
476     return sp;
477   }
478 
479   /**
480    *
481    * Format an SVG string with labels and colors.
482    *
483    * @param {string[]} labels The names for the label.
484    * @param {integer[]} colors The colors for each label.
485    *
486    * @return {string} SVG string with the labels and colors values formated as
487    * a legend.
488    * @function formatSVGLegend
489    */
490   function formatSVGLegend(labels, colors) {
491     var labels_svg = '', pos_y = 1, increment = 40, max_len = 0, rect_width,
492     font_size = 12;
493 
494     for (var i = 0; i < labels.length; i++) {
495       // add the rectangle with the corresponding color
496       labels_svg += '<rect height="27" width="27" y="' + pos_y +
497         '" x="5" style="stroke-width:1;stroke:rgb(0,0,0)" fill="' +
498         colors[i] + '"/>';
499 
500       // add the name of the category
501       labels_svg += '<text xml:space="preserve" y="' + (pos_y + 20) +
502         '" x="40" font-size="' + font_size +
503         '" stroke-width="0" stroke="#000000" fill="#000000">' + labels[i] +
504         '</text>';
505 
506       pos_y += increment;
507     }
508 
509     // get the name with the maximum number of characters and get the length
510     max_len = _.max(labels, function(a) {return a.length}).length;
511 
512       // duplicate the size of the rectangle to make sure it fits the labels
513       rect_width = font_size * max_len * 2;
514 
515     labels_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' +
516       rect_width + '" height="' + (pos_y - 10) + '"><g>' + labels_svg +
517       '</g></svg>';
518 
519     return labels_svg;
520   }
521 
522   return {'formatSVGLegend': formatSVGLegend, 'makeLine': makeLine,
523           'makeLabel': makeLabel, 'makeArrow': makeArrow,
524           'drawTrajectoryLineStatic': drawTrajectoryLineStatic,
525           'disposeTrajectoryLineStatic': disposeTrajectoryLineStatic,
526           'drawTrajectoryLineDynamic': drawTrajectoryLineDynamic,
527           'disposeTrajectoryLineDynamic': disposeTrajectoryLineDynamic,
528           'updateStaticTrajectoryDrawRange': updateStaticTrajectoryDrawRange,
529           'makeLineCollection': makeLineCollection};
530 });
531