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 = content;
 46     this.plaintext = '';
 47 
 48     this.isDraggable = false;
 49 
 50     if ((this.element = JXG.getRef(this.board, attributes.anchor))) {
 51         var anchor;
 52         if (this.visProp.islabel) {
 53             //anchor = this.element.getLabelAnchor();
 54             this.relativeCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [parseFloat(coords[0]), parseFloat(coords[1])], this.board);
 55         } else {
 56             //anchor = this.element.getTextAnchor();
 57             this.relativeCoords = new JXG.Coords(JXG.COORDS_BY_USER, [parseFloat(coords[0]), parseFloat(coords[1])], this.board);
 58         }
 59         this.element.addChild(this);
 60 
 61         this.coords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [0,0], this.board);
 62             //[parseFloat(this.visProp.offsets[0]) + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1],
 63             // parseFloat(this.visProp.offsets[1]) + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]], this.board);
 64         this.isDraggable = true;
 65     } else {
 66         if (JXG.isNumber(coords[0]) && JXG.isNumber(coords[1])) {
 67             this.isDraggable = true;
 68         }
 69         this.X = JXG.createFunction(coords[0], this.board, null, true);
 70         this.Y = JXG.createFunction(coords[1], this.board, null, true);
 71 
 72         this.coords = new JXG.Coords(JXG.COORDS_BY_USER, [this.X(),this.Y()], this.board);
 73         var fs = 'this.coords.setCoordinates(JXG.COORDS_BY_USER,[this.X(),this.Y()]);';
 74         this.updateCoords = new Function('',fs);
 75     }
 76 
 77     if (typeof this.content === 'function') {
 78         this.updateText = function() { this.plaintext = this.content(); };
 79     } else {
 80         if (JXG.isNumber(this.content)) {
 81             this.content = (this.content).toFixed(this.visProp.digits);
 82         } else {
 83             if (this.visProp.useasciimathml) {
 84                 this.content = "'`" + this.content + "`'";              // Convert via ASCIIMathML
 85             } else {
 86                 this.content = this.generateTerm(this.content);   // Converts GEONExT syntax into JavaScript string
 87             }
 88         }
 89         this.updateText = new Function('this.plaintext = ' + this.content + ';');
 90     }
 91 
 92     this.updateText();                    // First evaluation of the content.
 93     
 94     this.id = this.board.setId(this, 'T');
 95     this.board.renderer.drawText(this);
 96 
 97     if(!this.visProp.visible) {
 98         this.board.renderer.hide(this);
 99     }
100     
101     if (typeof this.content === 'string') {
102         this.notifyParents(this.content);
103     }
104     this.size = [1.0, 1.0];
105 
106     this.elType = 'text';
107 
108     this.methodMap = JXG.deepCopy(this.methodMap, {
109         setText: 'setTextJessieCode',
110         free: 'free',
111         move: 'setCoords'
112     });
113 
114     return this;
115 };
116 JXG.Text.prototype = new JXG.GeometryElement();
117 
118 JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ {
119     /**
120      * @private
121      * Test if the distance between th screen coordinates (x,y) 
122      * and the lower left corner of the text is smaller than
123      * this.board.options.precision.hasPoint in maximum norm.
124      * @param {Number} x
125      * @param {Number} y 
126      * @return {Boolean} 
127      */
128     hasPoint: function (x, y) {
129         var dx = x-this.coords.scrCoords[1],
130             dy = this.coords.scrCoords[2]-y,
131             r = this.board.options.precision.hasPoint;
132 
133         return dx >= -r && dx <= 2 * r && dy >= -r && dy <= 2 * r;
134     },
135 
136     /**
137      * Defines new content but converts < and > to HTML entities before updating the DOM.
138      * @param {String|function} text
139      */
140     setTextJessieCode: function (text) {
141         var s;
142 
143         this.visProp.castext = text;
144 
145         if (typeof text === 'function') {
146             s = function () {
147                 return text().replace(/</g, '<').replace(/>/g, '>');
148             };
149         } else {
150             if (JXG.isNumber(text)) {
151                 s = text;
152             } else {
153                 s = text.replace(/</g, '<').replace(/>/g, '>');
154             }
155         }
156         
157         return this.setText(s);
158     },
159 
160     /**
161      * Defines new content.
162      * @param {String|function} text
163      * @return {JXG.Text} Reference to the text object.
164      */
165     setText: function(text) {
166         if (typeof text === 'function') {
167             this.updateText = function() { this.plaintext = text(); };
168         } else {
169             if (JXG.isNumber(text)) {
170                 this.content = (text).toFixed(this.visProp.digits);
171             } else {
172                 if (this.visProp.useasciimathml) {
173                     this.content = "'`" + text + "`'";              // Convert via ASCIIMathML
174                 } else {
175                     this.content = this.generateTerm(text);   // Converts GEONExT syntax into JavaScript string
176                 }
177             }
178             this.updateText = new Function('this.plaintext = ' + this.content + ';');
179         }
180 
181         this.updateText();                    // First evaluation of the string.
182                                               // Needed for display='internal' and Canvas
183         this.updateSize();
184         this.needsUpdate = true;
185         this.update();
186         this.updateRenderer();
187 
188         return this;
189     },
190 
191     /**
192      * Recompute the width and the height of the text box.
193      * Update array this.size with pixel values.
194      * The result may differ from browser to browser
195      * by some pixels.
196      * In IE and canvas we use a very crude estimation of the dimensions of
197      * the textbox. 
198      * In JSXGraph this.size is necessary for applying rotations in IE and
199      * for aligning text.
200      */
201     updateSize: function () {
202         // Here comes a very crude estimation of the dimensions of
203         // the textbox. It is only necessary for the IE.
204         if (this.visProp.display=='html' && this.board.renderer.type!='vml') {
205             this.size = [this.rendNode.offsetWidth, this.rendNode.offsetHeight];
206         } else if (this.visProp.display=='internal' && this.board.renderer.type=='svg') {
207             this.size = [this.rendNode.getBBox().width, this.rendNode.getBBox().height];
208         } else if (this.board.renderer.type=='vml' || (this.visProp.display=='internal' && this.board.renderer.type=='canvas')) { 
209             this.size = [parseFloat(this.visProp.fontsize)*this.plaintext.length*0.45, parseFloat(this.visProp.fontsize)*0.9]
210         }
211         return this;
212     },
213 
214     /**
215      * Return the width of the text element.
216      * @return {Array} [width, height] in pixel
217      */
218     getSize: function () {
219         return this.size;
220     },
221 
222     /**
223      * Move the text to new coordinates.
224      * @param {number} x
225      * @param {number} y
226      * @return {object} reference to the text object.
227      */
228     setCoords: function (x,y) {
229         if (JXG.isArray(x) && x.length > 1) {
230             y = x[1];
231             x = x[0];
232         }
233 
234         this.X = function() { return x; };
235         this.Y = function() { return y; };
236         this.coords = new JXG.Coords(JXG.COORDS_BY_USER, [x, y], this.board);
237 
238         this.board.update();
239 
240         return this;
241     },
242 
243     free: function () {
244         this.X = JXG.createFunction(this.X(), this.board, '');
245         this.Y = JXG.createFunction(this.Y(), this.board, '');
246 
247         this.isDraggable = true;
248     },
249 
250     /**
251      * Evaluates the text.
252      * Then, the update function of the renderer
253      * is called. 
254      */
255     update: function () {
256         var anchor, sx, sy;
257 
258         if (this.needsUpdate) {
259             if (this.relativeCoords) {
260                 if (this.visProp.islabel) {
261                     sx =  parseFloat(this.visProp.offsets[0]);
262                     sy = -parseFloat(this.visProp.offsets[1]);
263                     anchor = this.element.getLabelAnchor();
264                     this.coords.setCoordinates(JXG.COORDS_BY_SCREEN,
265                         [sx + this.relativeCoords.scrCoords[1] + anchor.scrCoords[1],
266                          sy + this.relativeCoords.scrCoords[2] + anchor.scrCoords[2]]);
267                 } else {
268                     anchor = this.element.getTextAnchor();
269                     this.coords.setCoordinates(JXG.COORDS_BY_USER,
270                         [this.relativeCoords.usrCoords[1] + anchor.usrCoords[1],
271                          this.relativeCoords.usrCoords[2] + anchor.usrCoords[2]]);
272                 }
273             } else {
274                 this.updateCoords();
275             }
276             this.updateText();
277             //this.updateSize();
278             this.updateTransform();
279         }
280         return this;
281     },
282 
283     /**
284      * The update function of the renderert
285      * is called. 
286      * @private
287      */
288     updateRenderer: function () {
289         if (this.needsUpdate) {
290             this.board.renderer.updateText(this);
291             this.needsUpdate = false;
292         }
293         return this;
294     },
295 
296     updateTransform: function () {
297         if (this.transformations.length==0) {
298             return;
299         }
300 
301         for (var i=0;i<this.transformations.length;i++) {
302             this.transformations[i].update();
303         }
304 
305 	    return this;
306     },
307 
308     /**
309      * Converts the GEONExT syntax of the <value> terms into JavaScript.
310      * Also, all Objects whose name appears in the term are searched and
311      * the text is added as child to these objects.
312      * @private
313      * @see Algebra
314      * @see #geonext2JS.
315      */
316     generateTerm: function (contentStr) {
317         var res,
318             plaintext = '""',
319             term;
320 
321         contentStr = contentStr || '';
322         contentStr = contentStr.replace(/\r/g,''); 
323         contentStr = contentStr.replace(/\n/g,''); 
324         contentStr = contentStr.replace(/\"/g,'\\"'); 
325         contentStr = contentStr.replace(/\'/g,"\\'"); 
326         contentStr = contentStr.replace(/&arc;/g,'∠'); 
327         contentStr = contentStr.replace(/<arc\s*\/>/g,'∠'); 
328         contentStr = contentStr.replace(/<sqrt\s*\/>/g,'√'); 
329 
330         // Convert GEONExT syntax into  JavaScript syntax
331         var i;
332 
333         i = contentStr.indexOf('<value>');
334         var j = contentStr.indexOf('</value>');
335         if (i>=0) {
336             while (i>=0) {
337                 plaintext += ' + "'+ JXG.GeonextParser.replaceSub(JXG.GeonextParser.replaceSup(contentStr.slice(0,i))) + '"';
338                 term = contentStr.slice(i+7,j);
339                 res = JXG.GeonextParser.geonext2JS(term, this.board);
340                 res = res.replace(/\\"/g,'"');
341                 res = res.replace(/\\'/g,"'");
342 
343                 if (res.indexOf('toFixed')<0) {  // GEONExT-Hack: apply rounding once only.  
344                     if (JXG.isNumber( (JXG.bind(new Function('return '+res+';'), this))() )) {          // output of a value tag
345                                                                                         // may also be a string
346                         plaintext += '+('+ res + ').toFixed('+(this.visProp.digits)+')';
347                     } else {
348                         plaintext += '+('+ res + ')';   
349                     }
350                 } else {
351                     plaintext += '+('+ res + ')';
352                 }
353                 contentStr = contentStr.slice(j+8);
354                 i = contentStr.indexOf('<value>');
355                 j = contentStr.indexOf('</value>');
356             }
357         } //else {
358         plaintext += ' + "' + JXG.GeonextParser.replaceSub(JXG.GeonextParser.replaceSup(contentStr)) + '"';
359         //}
360         plaintext = plaintext.replace(/<overline>/g,'<span style=text-decoration:overline>');
361         plaintext = plaintext.replace(/<\/overline>/g,'</span>');
362         plaintext = plaintext.replace(/<arrow>/g,'<span style=text-decoration:overline>');
363         plaintext = plaintext.replace(/<\/arrow>/g,'</span>');
364 
365         plaintext = plaintext.replace(/&/g,'&'); // This should replace &pi; by π
366         return plaintext;
367     },
368 
369     /**
370      * Finds dependencies in a given term and notifies the parents by adding the
371      * dependent object to the found objects child elements.
372      * @param {String} content String containing dependencies for the given object.
373      * @private
374      */
375     notifyParents: function (content) {
376         var res = null;
377 
378         do {
379             var search = /<value>([\w\s\*\/\^\-\+\(\)\[\],<>=!]+)<\/value>/;
380             res = search.exec(content);
381             if (res!=null) {
382                 JXG.GeonextParser.findDependencies(this,res[1],this.board);
383                 content = content.substr(res.index);
384                 content = content.replace(search,'');
385             }
386         } while (res!=null);
387         return this;
388     },
389 
390     bounds: function () {
391         var c = this.coords.usrCoords;
392 
393         return this.visProp.islabel ? [0, 0, 0, 0] : [c[1], c[2]+this.size[1], c[1]+this.size[0], c[2]];
394     },
395     
396     /**
397      * Sets x and y coordinate of the text.
398      * @param {number} method The type of coordinates used here. Possible values are {@link JXG.COORDS_BY_USER} and {@link JXG.COORDS_BY_SCREEN}.
399      * @param {number} x x coordinate in screen/user units
400      * @param {number} y y coordinate in screen/user units
401      * @param {number} oldx previous x coordinate in screen/user units
402      * @param {number} oldy previous y coordinate in screen/user units
403      */
404     setPositionDirectly: function (method, x, y, oldx, oldy) {
405         var i, 
406             dx, dy,
407             newCoords, oldCoords;
408             
409         if (this.relativeCoords) {
410             if (this.visProp.islabel) {
411                 if (method == JXG.COORDS_BY_USER) {
412                     oldCoords = new JXG.Coords(JXG.COORDS_BY_USER, [oldx,oldy], this.board);
413                     newCoords = new JXG.Coords(JXG.COORDS_BY_USER, [x,y], this.board);
414                     dx = newCoords.scrCoords[1]-oldCoords.scrCoords[1];
415                     dy = newCoords.scrCoords[2]-oldCoords.scrCoords[2];
416                 } else {
417                     dx = x - oldx;
418                     dy = y - oldy;
419                 }
420                 this.relativeCoords.scrCoords[1] += dx;
421                 this.relativeCoords.scrCoords[2] += dy;
422             } else {
423                 if (method == JXG.COORDS_BY_SCREEN) {
424                     oldCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [oldx,oldy], this.board);
425                     newCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [x,y], this.board);
426                     dx = newCoords.usrCoords[1]-oldCoords.usrCoords[1];
427                     dy = newCoords.usrCoords[2]-oldCoords.usrCoords[2];
428                 } else {
429                     dx = x - oldx;
430                     dy = y - oldy;
431                 }
432                 this.relativeCoords.usrCoords[1] += dx;
433                 this.relativeCoords.usrCoords[2] += dy;
434             }
435         } else {
436             if (method == JXG.COORDS_BY_SCREEN) {
437                 newCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [x,y], this.board);
438                 x = newCoords.usrCoords[1];
439                 y = newCoords.usrCoords[2];
440             }
441             this.X = JXG.createFunction(x,this.board,'');
442             this.Y = JXG.createFunction(y,this.board,'');
443         }
444 
445         /*
446          // Update the initial coordinates. This is needed for free points
447         // that have a transformation bound to it.
448             for (i=this.transformations.length-1;i>=0;i--) {
449                 if (method == JXG.COORDS_BY_SCREEN) {
450                     newCoords = (new JXG.Coords(method, [x, y], this.board)).usrCoords;                
451                 } else {
452                     newCoords = [1,x,y];
453                 }
454                 this.initialCoords = new JXG.Coords(JXG.COORDS_BY_USER, 
455                         JXG.Math.matVecMult(JXG.Math.inverse(this.transformations[i].matrix), newCoords), 
456                         this.board);      
457             }
458         */
459         return this;
460     }
461     
462 });
463 
464 /**
465  * @class This element is used to provide a constructor for text, which is just a wrapper for element {@link Text}. 
466  * @pseudo
467  * @description
468  * @name Text
469  * @augments JXG.GeometryElement
470  * @constructor
471  * @type JXG.Text
472  *
473  * @param {number,function_number,function_String,function} x,y,str Parent elements for text elements.
474  *                     <p>
475  *                     x and y are the coordinates of the lower left corner of the text box. The position of the text is fixed, 
476  *                     x and y are numbers. The position is variable if x or y are functions.
477  *                     <p>
478  *                     The text to display may be given as string or as function returning a string.
479  *
480  * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display
481  * 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.
482  * @see JXG.Text
483  * @example
484  * // Create a fixed text at position [0,1].
485  *   var t1 = board.create('text',[0,1,"Hello World"]); 
486  * </pre><div id="896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
487  * <script type="text/javascript">
488  *   var t1_board = JXG.JSXGraph.initBoard('896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
489  *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
490  * </script><pre>
491  * @example
492  * // Create a variable text at a variable position.
493  *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
494  *   var graph = board.create('text', 
495  *                        [function(x){ return s.Value();}, 1,
496  *                         function(){return "The value of s is"+s.Value().toFixed(2);}
497  *                        ]
498  *                     );
499  * </pre><div id="5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
500  * <script type="text/javascript">
501  *   var t2_board = JXG.JSXGraph.initBoard('5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
502  *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
503  *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+s.Value().toFixed(2);}]);
504  * </script><pre>
505  */
506 JXG.createText = function(board, parents, attributes) {
507     var attr, t;
508 
509     attr = JXG.copyAttributes(attributes, board.options, 'text');
510 
511     // downwards compatibility
512     attr.anchor = attr.parent || attr.anchor;
513 
514     t = new JXG.Text(board, parents[parents.length-1], parents, attr);
515 
516     if (typeof parents[parents.length-1] !== 'function') {
517         t.parents = parents;
518     }
519 
520     return t;
521 };
522 
523 JXG.JSXGraph.registerElement('text', JXG.createText);
524