1 /*
  2     Copyright 2008-2011
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software: you can redistribute it and/or modify
 13     it under the terms of the GNU Lesser General Public License as published by
 14     the Free Software Foundation, either version 3 of the License, or
 15     (at your option) any later version.
 16 
 17     JSXGraph is distributed in the hope that it will be useful,
 18     but WITHOUT ANY WARRANTY; without even the implied warranty of
 19     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 20     GNU Lesser General Public License for more details.
 21 
 22     You should have received a copy of the GNU Lesser General Public License
 23     along with JSXGraph.  If not, see <http://www.gnu.org/licenses/>.
 24 */
 25 
 26 /**
 27  * @fileoverview In this file the Text element is defined.
 28  */
 29 
 30 
 31 /**
 32  * Construct and handle texts.
 33  * @class Text: On creation the GEONExT syntax
 34  * of <value>-terms 
 35  * are converted into JavaScript syntax.
 36  * The coordinates can be relative to the coordinates of an element "element".
 37  * @constructor
 38  * @return A new geometry element Text
 39  */
 40 JXG.Text = function (board, content, coords, attributes) {
 41     this.constructor(board, attributes, JXG.OBJECT_TYPE_TEXT, JXG.OBJECT_CLASS_OTHER);
 42 
 43     var i;
 44 
 45     this.content = '';
 46     this.plaintext = '';
 47 
 48     this.isDraggable = false;
 49     this.needsSizeUpdate = false;
 50 
 51     if ((this.element = JXG.getRef(this.board, attributes.anchor))) {
 52         var anchor;
 53         if (this.visProp.islabel) {
 54             this.relativeCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [parseFloat(coords[0]), parseFloat(coords[1])], this.board);
 55         } else {
 56             this.relativeCoords = new JXG.Coords(JXG.COORDS_BY_USER, [parseFloat(coords[0]), parseFloat(coords[1])], this.board);
 57         }
 58         this.element.addChild(this);
 59         
 60         this.X = function () {
 61             var sx, coords, anchor;
 62 
 63             if (this.visProp.islabel) {
 64                 sx =  parseFloat(this.visProp.offset[0]);
 65                 anchor = this.element.getLabelAnchor();
 66                 coords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1], 0], this.board);
 67                 
 68                 return coords.usrCoords[1];
 69             } else {
 70                 anchor = this.element.getTextAnchor();
 71                 
 72                 return this.relativeCoords.usrCoords[1] + anchor.usrCoords[1];
 73             }
 74         };
 75         
 76         this.Y = function () {
 77             var sy, coords, anchor;
 78             
 79             if (this.visProp.islabel) {
 80                 sy = -parseFloat(this.visProp.offset[1]);
 81                 anchor = this.element.getLabelAnchor();
 82                 coords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [0, sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], this.board);
 83 
 84                 return coords.usrCoords[2];
 85             } else {
 86                 anchor = this.element.getTextAnchor();
 87                 
 88                 return this.relativeCoords.usrCoords[2] + anchor.usrCoords[2];
 89             }
 90         };
 91 
 92         this.coords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [0,0], this.board);
 93         this.isDraggable = true;
 94     } else {
 95         if (JXG.isNumber(coords[0]) && JXG.isNumber(coords[1])) {
 96             this.isDraggable = true;
 97         }
 98         this.X = JXG.createFunction(coords[0], this.board, null, true);
 99         this.Y = JXG.createFunction(coords[1], this.board, null, true);
100 
101         this.coords = new JXG.Coords(JXG.COORDS_BY_USER, [this.X(),this.Y()], this.board);
102     }
103     this.Z = JXG.createFunction(1.0, this.board, '');
104 
105     this.size = [1.0, 1.0];
106 
107     this.id = this.board.setId(this, 'T');
108     this.board.renderer.drawText(this);
109 
110     this.setText(content);
111     this.updateSize();
112 
113     if(!this.visProp.visible) {
114         this.board.renderer.hide(this);
115     }
116     
117     if (typeof this.content === 'string') {
118         this.notifyParents(this.content);
119     }
120 
121     this.elType = 'text';
122 
123     this.methodMap = JXG.deepCopy(this.methodMap, {
124         setText: 'setTextJessieCode',
125         free: 'free',
126         move: 'setCoords'
127     });
128 
129     return this;
130 };
131 JXG.Text.prototype = new JXG.GeometryElement();
132 
133 JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ {
134     /**
135      * @private
136      * Test if the the screen coordinates (x,y) are in a small stripe 
137      * at the left side or at the right side of the text.
138      * Sensitivity is set in this.board.options.precision.hasPoint.
139      * @param {Number} x
140      * @param {Number} y 
141      * @return {Boolean} 
142      */
143     hasPoint: function (x, y) {
144         var lft, rt, top, bot,
145             r = this.board.options.precision.hasPoint;
146             
147         if (this.visProp.anchorx === 'right') {
148             lft = this.coords.scrCoords[1] - this.size[0];
149         } else if (this.visProp.anchorx === 'middle') {
150             lft = this.coords.scrCoords[1] - 0.5*this.size[0];
151         } else { 
152             lft = this.coords.scrCoords[1];
153         }
154         rt = lft + this.size[0];
155 
156         if (this.visProp.anchory === 'top') {
157             bot = this.coords.scrCoords[2] + this.size[1];
158         } else if (this.visProp.anchorx === 'middle') {
159             bot = this.coords.scrCoords[2] + 0.5 * this.size[1];
160         } else { 
161             bot = this.coords.scrCoords[2];
162         }
163         top = bot - this.size[1];
164         
165         return (y >= top-r && y <= bot + r)
166                 && ((x >= lft - r  && x <= lft + 2*r) 
167                     ||
168                     (x >= rt - 2*r && x <= rt + r)
169                    );
170     },
171 
172     /**
173      * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because
174      * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode.
175      * @param text
176      * @return {JXG.Text}
177      * @private
178      */
179     _setText: function (text) {
180         this.needsSizeUpdate = false;
181         
182         if (typeof text === 'function') {
183             this.updateText = function() { this.plaintext = text(); };
184             this.needsSizeUpdate = true;
185         } else {
186             if (JXG.isNumber(text)) {
187                 this.content = (text).toFixed(this.visProp.digits);
188             } else {
189                 if (this.visProp.useasciimathml) {
190                     this.content = "'`" + text + "`'";              // Convert via ASCIIMathML
191                     this.needsSizeUpdate = true;
192                 } else {
193                     this.content = this.generateTerm(text);   // Converts GEONExT syntax into JavaScript string
194                 }
195             }
196             this.updateText = new Function('this.plaintext = ' + this.content + '; ');
197         }
198 
199         this.updateText();                    // First evaluation of the string.
200                                               // Needed for display='internal' and Canvas
201         this.prepareUpdate().update().updateRenderer();
202 
203         return this;
204     },
205 
206     /**
207      * Defines new content but converts < and > to HTML entities before updating the DOM.
208      * @param {String|function} text
209      */
210     setTextJessieCode: function (text) {
211         var s;
212 
213         this.visProp.castext = text;
214 
215         if (typeof text === 'function') {
216             s = function () {
217                 return text().replace(/</g, '<').replace(/>/g, '>');
218             };
219         } else {
220             if (JXG.isNumber(text)) {
221                 s = text;
222             } else {
223                 s = text.replace(/</g, '<').replace(/>/g, '>');
224             }
225         }
226         
227         return this._setText(s);
228     },
229 
230     /**
231      * Defines new content.
232      * @param {String|function} text
233      * @return {JXG.Text} Reference to the text object.
234      */
235     setText: function(text) {
236         this._setText(text);
237     },
238     
239     /**
240      * Recompute the width and the height of the text box.
241      * Update array this.size with pixel values.
242      * The result may differ from browser to browser
243      * by some pixels.
244      * In IE and canvas we use a very crude estimation of the dimensions of
245      * the textbox. 
246      * In JSXGraph this.size is necessary for applying rotations in IE and
247      * for aligning text.
248      */
249     updateSize: function () {
250         var tmp;
251 
252         if (typeof document === 'undefined') {
253             return this;
254         }
255 
256         if (this.visProp.display=='html' && this.board.renderer.type !== 'vml' && this.board.renderer.type !== 'no') {
257             this.size = [this.rendNode.offsetWidth, this.rendNode.offsetHeight];
258         } else if (this.visProp.display === 'internal' && this.board.renderer.type === 'svg') {
259             try {
260                 tmp = this.rendNode.getBBox();          // getBBox broken in FF 13? No.
261                 this.size = [tmp.width, tmp.height];
262             } catch (e) {
263             }
264         } else if (this.board.renderer.type === 'vml' || (this.visProp.display === 'internal' && this.board.renderer.type === 'canvas')) {
265             // Here comes a very crude estimation of the dimensions of the textbox. 
266             this.size = [parseFloat(this.visProp.fontsize)*this.plaintext.length*0.45, parseFloat(this.visProp.fontsize)*0.9]
267         }
268         return this;
269     },
270 
271     /**
272      * Return the width of the text element.
273      * @return {Array} [width, height] in pixel
274      */
275     getSize: function () {
276         return this.size;
277     },
278 
279     /**
280      * Move the text to new coordinates.
281      * @param {number} x
282      * @param {number} y
283      * @return {object} reference to the text object.
284      */
285     setCoords: function (x, y) {
286         if (JXG.isArray(x) && x.length > 1) {
287             y = x[1];
288             x = x[0];
289         }
290 
291         this.X = function() { return x; };
292         this.Y = function() { return y; };
293         this.coords.setCoordinates(JXG.COORDS_BY_USER, [x, y]);
294 
295         // this should be a local update, otherwise there might be problems
296         // with the tick update routine resulting in orphaned tick labels
297         this.prepareUpdate().update().updateRenderer();
298 
299         return this;
300     },
301 
302     free: function () {
303         this.X = JXG.createFunction(this.X(), this.board, '');
304         this.Y = JXG.createFunction(this.Y(), this.board, '');
305 
306         this.isDraggable = true;
307     },
308 
309     /**
310      * Evaluates the text.
311      * Then, the update function of the renderer
312      * is called. 
313      */
314     update: function () {
315         var anchor, sx, sy;
316 
317         if (this.needsUpdate) {
318             this.updateCoords();
319             this.updateText();
320             if (this.needsSizeUpdate) {
321                 this.updateSize();
322             }
323             this.updateTransform();
324         }
325         return this;
326     },
327 
328     /**
329      * Updates the coordinates of the text element.
330      */
331     updateCoords: function () {
332         this.coords.setCoordinates(JXG.COORDS_BY_USER, [this.X(), this.Y()]);
333     },
334 
335     /**
336      * The update function of the renderert
337      * is called. 
338      * @private
339      */
340     updateRenderer: function () {
341         if (this.needsUpdate) {
342             this.board.renderer.updateText(this);
343             //this.updateSize();
344             this.needsUpdate = false;
345         }
346         return this;
347     },
348 
349     updateTransform: function () {
350         var i;
351 
352         if (this.transformations.length==0) {
353             return;
354         }
355 
356         for (i = 0; i < this.transformations.length; i++) {
357             this.transformations[i].update();
358         }
359 
360         return this;
361     },
362 
363     /**
364      * Converts the GEONExT syntax of the <value> terms into JavaScript.
365      * Also, all Objects whose name appears in the term are searched and
366      * the text is added as child to these objects.
367      * @private
368      * @see Algebra
369      * @see #geonext2JS.
370      */
371     generateTerm: function (contentStr) {
372         var res,
373             plaintext = '""',
374             term;
375 
376         contentStr = contentStr || '';
377         contentStr = contentStr.replace(/\r/g,''); 
378         contentStr = contentStr.replace(/\n/g,''); 
379         contentStr = contentStr.replace(/\"/g,'\\"'); 
380         contentStr = contentStr.replace(/\'/g,"\\'"); 
381         contentStr = contentStr.replace(/&arc;/g,'∠'); 
382         contentStr = contentStr.replace(/<arc\s*\/>/g,'∠'); 
383         contentStr = contentStr.replace(/<sqrt\s*\/>/g,'√'); 
384 
385         // Convert GEONExT syntax into  JavaScript syntax
386         var i;
387 
388         i = contentStr.indexOf('<value>');
389         var j = contentStr.indexOf('</value>');
390         if (i>=0) {
391             this.needsSizeUpdate = true;
392             while (i>=0) {
393                 plaintext += ' + "'+ JXG.GeonextParser.replaceSub(JXG.GeonextParser.replaceSup(contentStr.slice(0,i))) + '"';
394                 term = contentStr.slice(i+7,j);
395                 res = JXG.GeonextParser.geonext2JS(term, this.board);
396                 res = res.replace(/\\"/g,'"');
397                 res = res.replace(/\\'/g,"'");
398 
399                 if (res.indexOf('toFixed')<0) {  // GEONExT-Hack: apply rounding once only.  
400                     if (JXG.isNumber( (JXG.bind(new Function('return '+res+';'), this))() )) {          // output of a value tag
401                                                                                         // may also be a string
402                         plaintext += '+('+ res + ').toFixed('+(this.visProp.digits)+')';
403                     } else {
404                         plaintext += '+('+ res + ')';   
405                     }
406                 } else {
407                     plaintext += '+('+ res + ')';
408                 }
409                 contentStr = contentStr.slice(j+8);
410                 i = contentStr.indexOf('<value>');
411                 j = contentStr.indexOf('</value>');
412             }
413         } //else {
414         plaintext += ' + "' + JXG.GeonextParser.replaceSub(JXG.GeonextParser.replaceSup(contentStr)) + '"';
415         //}
416         plaintext = plaintext.replace(/<overline>/g,'<span style=text-decoration:overline>');
417         plaintext = plaintext.replace(/<\/overline>/g,'</span>');
418         plaintext = plaintext.replace(/<arrow>/g,'<span style=text-decoration:overline>');
419         plaintext = plaintext.replace(/<\/arrow>/g,'</span>');
420 
421         plaintext = plaintext.replace(/&/g,'&'); // This should replace &pi; by π
422         return plaintext;
423     },
424 
425     /**
426      * Finds dependencies in a given term and notifies the parents by adding the
427      * dependent object to the found objects child elements.
428      * @param {String} content String containing dependencies for the given object.
429      * @private
430      */
431     notifyParents: function (content) {
432         var res = null;
433 
434         do {
435             var search = /<value>([\w\s\*\/\^\-\+\(\)\[\],<>=!]+)<\/value>/;
436             res = search.exec(content);
437             if (res!=null) {
438                 JXG.GeonextParser.findDependencies(this,res[1],this.board);
439                 content = content.substr(res.index);
440                 content = content.replace(search,'');
441             }
442         } while (res!=null);
443         return this;
444     },
445 
446     bounds: function () {
447         var c = this.coords.usrCoords;
448 
449         return this.visProp.islabel ? [0, 0, 0, 0] : [c[1], c[2]+this.size[1], c[1]+this.size[0], c[2]];
450     },
451     
452     /**
453      * Sets x and y coordinate of the text.
454      * @param {number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}.
455      * @param {Array} coords coordinates in screen/user units
456      * @param {Array} oldcoords previous coordinates in screen/user units
457      */
458     setPositionDirectly: function (method, coords, oldcoords) {
459         var c = new JXG.Coords(method, coords, this.board),
460             oldc = new JXG.Coords(method, oldcoords, this.board),
461             dc,  v;
462             
463         if (this.relativeCoords) {
464             if (this.visProp.islabel) {
465                 dc = JXG.Math.Statistics.subtract(c.scrCoords, oldc.scrCoords);        
466                 this.relativeCoords.scrCoords[1] += dc[1];
467                 this.relativeCoords.scrCoords[2] += dc[2];
468             } else {
469                 dc = JXG.Math.Statistics.subtract(c.usrCoords, oldc.usrCoords);        
470                 this.relativeCoords.usrCoords[1] += dc[1];
471                 this.relativeCoords.usrCoords[2] += dc[2];
472             }
473         } else {
474             dc = JXG.Math.Statistics.subtract(c.usrCoords, oldc.usrCoords);    
475             v = [this.Z(), this.X(), this.Y()];            
476             this.X = JXG.createFunction(v[1]+dc[1], this.board, '');
477             this.Y = JXG.createFunction(v[2]+dc[2], this.board, '');
478         }
479 
480         return this;
481     }
482     
483 });
484 
485 /**
486  * @class This element is used to provide a constructor for text, which is just a wrapper for element {@link Text}. 
487  * @pseudo
488  * @description
489  * @name Text
490  * @augments JXG.GeometryElement
491  * @constructor
492  * @type JXG.Text
493  *
494  * @param {number,function_number,function_String,function} x,y,str Parent elements for text elements.
495  *                     <p>
496  *                     x and y are the coordinates of the lower left corner of the text box. The position of the text is fixed, 
497  *                     x and y are numbers. The position is variable if x or y are functions.
498  *                     <p>
499  *                     The text to display may be given as string or as function returning a string.
500  *
501  * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display
502  * the text. In this case it is also possible to use ASCIIMathML. Incase of 'internal', a SVG or VML text element is used to display the text.
503  * @see JXG.Text
504  * @example
505  * // Create a fixed text at position [0,1].
506  *   var t1 = board.create('text',[0,1,"Hello World"]); 
507  * </pre><div id="896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
508  * <script type="text/javascript">
509  *   var t1_board = JXG.JSXGraph.initBoard('896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
510  *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
511  * </script><pre>
512  * @example
513  * // Create a variable text at a variable position.
514  *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
515  *   var graph = board.create('text', 
516  *                        [function(x){ return s.Value();}, 1,
517  *                         function(){return "The value of s is"+s.Value().toFixed(2);}
518  *                        ]
519  *                     );
520  * </pre><div id="5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
521  * <script type="text/javascript">
522  *   var t2_board = JXG.JSXGraph.initBoard('5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
523  *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
524  *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+s.Value().toFixed(2);}]);
525  * </script><pre>
526  */
527 JXG.createText = function(board, parents, attributes) {
528     var t, 
529         attr = JXG.copyAttributes(attributes, board.options, 'text');
530 
531     // downwards compatibility
532     attr.anchor = attr.parent || attr.anchor;
533 
534     t = new JXG.Text(board, parents[parents.length-1], parents, attr);
535 
536     if (typeof parents[parents.length-1] !== 'function') {
537         t.parents = parents;
538     }
539 
540     if (JXG.evaluate(attr.rotate) != 0 && attr.display=='internal') {
541         t.addRotation(JXG.evaluate(attr.rotate));
542     }
543 
544     return t;
545 };
546 
547 JXG.JSXGraph.registerElement('text', JXG.createText);
548