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, contentStr, element, coords, id, name, digits, isLabel, display, layer) {
 41     this.constructor();
 42 
 43     this.type = JXG.OBJECT_TYPE_TEXT;
 44     this.elementClass = JXG.OBJECT_CLASS_OTHER;                
 45 
 46     this.init(board, id, name);
 47     this.contentStr = contentStr;
 48     this.plaintextStr = '';
 49 
 50     /**
 51      * Set the display layer.
 52      */
 53     if (layer == null) layer = board.options.layer['text'];
 54     this.layer = layer;
 55 
 56     /**
 57      * There is choice between 'html' and 'internal'
 58      * 'internal' is the text element of SVG and the textpath element 
 59      * of VML.
 60      */
 61     this.display = display || 'html'; 
 62     
 63     if((typeof isLabel != 'undefined') && (isLabel != null)) {
 64         this.isLabel = isLabel;
 65     }
 66     else {
 67         this.isLabel = false;
 68     }
 69     
 70     /**
 71      * The text color of the given text.
 72      * @type {string}
 73      * @name JXG.Text#strokeColor
 74      */
 75     this.visProp['strokeColor'] = this.board.options.text.strokeColor;
 76     /**
 77      * The text opacity of the given text.
 78      * @type {string}
 79      * @name JXG.Text#strokeOpacity
 80      */
 81      /**
 82      * The font size of the given text.
 83      * @type {string}
 84      * @name JXG.Text#fontSize
 85      * @default {@link JXG.Options.fontSize}
 86      */
 87     //this.visProp['fontSize'] = this.board.options.text.fontSize;
 88 
 89     this.visProp['visible'] = true;
 90     //this.show = true; // noch noetig? BV
 91 
 92     if (digits!=null) {
 93         this.digits = digits;
 94     } else {
 95         this.digits = 2;
 96     }
 97 
 98     /**
 99      * Coordinates of the text.
100      * @ private
101      * @type JXG.Coords
102      */
103     if ((this.element = this.board.objects[element])){
104         var anchor;
105         if(!this.isLabel) {
106             anchor = this.element.getTextAnchor();
107         }
108         else {
109             anchor = this.element.getLabelAnchor();
110         }      
111         this.element.addChild(this);
112         this.relativeCoords = new JXG.Coords(JXG.COORDS_BY_SCREEN, [parseFloat(coords[0]),parseFloat(coords[1])],this.board);     
113         this.coords = new JXG.Coords(JXG.COORDS_BY_SCREEN, 
114             [this.relativeCoords.scrCoords[1]+anchor.scrCoords[1],
115              this.relativeCoords.scrCoords[2]+anchor.scrCoords[2]], this.board);
116     } else {
117         this.X = JXG.createFunction(coords[0],this.board,'');
118         this.Y = JXG.createFunction(coords[1],this.board,'');
119         this.coords = new JXG.Coords(JXG.COORDS_BY_USER, [this.X(),this.Y()], this.board);
120         var fs = 'this.coords.setCoordinates(JXG.COORDS_BY_USER,[this.X(),this.Y()]);';
121         this.updateCoords = new Function('',fs);
122     }
123 
124     if (typeof this.contentStr=='function') {
125         this.updateText = function() { this.plaintextStr = this.contentStr(); };
126     } else {
127         var plaintext;
128         if (typeof this.contentStr=='number') {
129             plaintext = (this.contentStr).toFixed(this.digits);  
130         } else {
131             if (this.board.options.text.useASCIIMathML) {
132                 plaintext = "'`"+this.contentStr+"`'";              // Convert via ASCIIMathML
133             } else {
134                 plaintext = this.generateTerm(this.contentStr);   // Converts GEONExT syntax into JavaScript string
135             }
136         }
137         this.updateText = new Function('this.plaintextStr = ' + plaintext + ';');
138     }
139     this.updateText();                    // First evaluation of the string.
140                                           // Needed for display='internal' and Canvas 
141     if(!this.isLabel) {
142         this.id = this.board.setId(this, 'T');
143         this.board.renderer.drawText(this);
144         if(!this.visProp['visible']) {
145             this.board.renderer.hide(this);
146         }
147     }
148     if (typeof this.contentStr=='string') {
149         this.notifyParents(this.contentStr);
150     }
151     this.size = [1.0,1.0];
152 };
153 JXG.Text.prototype = new JXG.GeometryElement();
154 
155 /**
156  * @private
157  * Empty function (for the moment). It is needed for highlighting
158  * @param {int} x
159  * @param {int} y Find closest point on the text to (xy)
160  * @return Always returns false
161  */
162 JXG.Text.prototype.hasPoint = function (x,y) {
163     return false;
164 };
165 
166 /**
167  * Overwrite the text.
168  * @param {string,function} str
169  * @return {object} reference to the text object.
170  */
171 JXG.Text.prototype.setText = function(text) {
172     var plaintext;
173     if (JXG.isNumber(text)) {
174         plaintext = (text).toFixed(this.digits);  
175         this.updateText = new Function('this.plaintextStr = ' + plaintext + ';');
176     } else if (JXG.isFunction(text)) {
177         this.updateText = function() { this.plaintextStr = text(); };
178     } else {
179         plaintext = this.generateTerm(text);   // Converts GEONExT syntax into JavaScript string
180         this.updateText = new Function('this.plaintextStr = ' + plaintext + ';');
181     }
182     this.updateText();
183     this.updateSize();
184     return this;
185 };
186 
187 /**
188  * Recompute the width and the height of the text box.
189  * Update array this.size with pixel values.
190  * The result may differ from browser to browser
191  * by some pixels.
192  * In IE and canvas we use a very crude estimation of the dimensions of
193  * the textbox. 
194  * In JSXGraph this.size is necessary for applying rotations in IE and
195  * for aligning text.
196  * @return {}
197  */
198 JXG.Text.prototype.updateSize = function () {
199     // Here comes a very crude estimation of the dimensions of
200     // the textbox. It is only necessary for the IE.
201     if (this.display=='html' && this.board.renderer.type!='vml') {
202         this.size = [this.rendNode.offsetWidth, this.rendNode.offsetHeight];
203     } else if (this.display=='internal' && this.board.renderer.type=='svg') {
204         this.size = [this.rendNode.getBBox().width, this.rendNode.getBBox().height];
205     } else if (this.board.renderer.type=='vml' || (this.display=='internal' && this.board.renderer.type=='canvas')) { 
206         this.size = [parseFloat(this.visProp['fontSize'])*this.plaintextStr.length*0.45,parseFloat(this.visProp['fontSize'])*0.9]        
207     }
208 };    
209 
210 /**
211  * Return the width of the text element.
212  * @return {array} [width, height] in pixel
213  */
214 JXG.Text.prototype.getSize = function () {
215     return this.size;
216 };    
217 
218 /**
219  * Set the text to new, fixed coordinates.
220  * @param {number} x
221  * @param {number} y
222  * @return {object} reference to the text object.
223  */
224 JXG.Text.prototype.setCoords = function (x,y) {
225     this.X = function() { return x; };
226     this.Y = function() { return y; };
227     this.coords = new JXG.Coords(JXG.COORDS_BY_USER, [x,y], this.board);
228     return this;
229 };
230 
231 /**
232  * Evaluates the text.
233  * Then, the update function of the renderer
234  * is called. 
235  */
236 JXG.Text.prototype.update = function () {
237     var anchor, plainOld;
238     if (this.needsUpdate && !this.frozen) {
239         if (this.relativeCoords){
240             anchor;
241             if(!this.isLabel) {
242                 anchor = this.element.getTextAnchor();
243             }
244             else {
245                 anchor = this.element.getLabelAnchor();
246             }
247             this.coords.setCoordinates(JXG.COORDS_BY_SCREEN, 
248                 [this.relativeCoords.scrCoords[1]+anchor.scrCoords[1],
249                  this.relativeCoords.scrCoords[2]+anchor.scrCoords[2]]);
250         } else {
251             this.updateCoords();
252         }
253     }   
254     if (this.needsUpdate) {
255         this.updateText();
256         this.updateSize();
257         this.updateTransform();
258     }
259     return this;
260 };
261 
262 /**
263  * The update function of the renderer
264  * is called. 
265  * @private
266  */
267 JXG.Text.prototype.updateRenderer = function () {
268     if (this.needsUpdate) {
269         this.board.renderer.updateText(this);
270         this.needsUpdate = false;
271     }
272     return this;
273 };
274 
275 JXG.Text.prototype.updateTransform = function () {
276     if (this.transformations.length==0) {
277         return;
278     }
279     for (var i=0;i<this.transformations.length;i++) {
280         this.transformations[i].update();
281     }
282 };
283 
284 /**
285  * Converts the GEONExT syntax of the <value> terms into JavaScript.
286  * Also, all Objects whose name appears in the term are searched and
287  * the text is added as child to these objects.
288  * @private
289  * @see Algebra
290  * @see #geonext2JS.
291  */
292 JXG.Text.prototype.generateTerm = function (contentStr) {
293     var res = null;
294     var elements = this.board.elementsByName;
295     var plaintext = '""';
296     contentStr = contentStr.replace(/\r/g,''); 
297     contentStr = contentStr.replace(/\n/g,''); 
298     contentStr = contentStr.replace(/\"/g,'\\"'); 
299     contentStr = contentStr.replace(/\'/g,"\\'"); 
300     contentStr = contentStr.replace(/&arc;/g,'∠'); 
301     contentStr = contentStr.replace(/<arc\s*\/>/g,'∠'); 
302     contentStr = contentStr.replace(/<sqrt\s*\/>/g,'√'); 
303 
304     // Convert GEONExT syntax into  JavaScript syntax
305     var i;
306     //var i = contentStr.indexOf('<mp>');
307     //contentStr = contentStr.slice(i+4);
308     //i = contentStr.indexOf('</mp>');
309     //contentStr = contentStr.slice(0,i);
310 
311     i = contentStr.indexOf('<value>');
312     var j = contentStr.indexOf('</value>');
313     if (i>=0) {
314         while (i>=0) {
315             plaintext += ' + "'+ JXG.GeonextParser.replaceSub(JXG.GeonextParser.replaceSup(contentStr.slice(0,i))) + '"';
316             var term = contentStr.slice(i+7,j);
317             var res = JXG.GeonextParser.geonext2JS(term, this.board); 
318             res = res.replace(/\\"/g,'"');
319             res = res.replace(/\\'/g,"'");
320             if (res.indexOf('toFixed')<0) {  // GEONExT-Hack: apply rounding once only.  
321                 plaintext += '+('+ res + ').toFixed('+(this.digits)+')';
322             } else {
323                 plaintext += '+('+ res + ')';
324             }
325             contentStr = contentStr.slice(j+8);
326             i = contentStr.indexOf('<value>');
327             j = contentStr.indexOf('</value>');
328         }
329     } //else {
330     plaintext += ' + "' + JXG.GeonextParser.replaceSub(JXG.GeonextParser.replaceSup(contentStr)) + '"';
331     //}
332     plaintext = plaintext.replace(/<overline>/g,'<span style=text-decoration:overline>');
333     plaintext = plaintext.replace(/<\/overline>/g,'</span>');
334     plaintext = plaintext.replace(/<arrow>/g,'<span style=text-decoration:overline>');
335     plaintext = plaintext.replace(/<\/arrow>/g,'</span>');
336 /*    i = plaintext.indexOf('<name>');
337     j = plaintext.indexOf('</name>');
338     while (i>=0) {
339         var head = plaintext.slice(0,i+6);
340         var mid = plaintext.slice(i+6,j);
341         var tail = plaintext.slice(j);
342         mid = JXG.GeonextParser.replaceSub(JXG.GeonextParser.replaceSup(mid));
343         plaintext = head + mid + tail;
344         i = plaintext.indexOf('<name>',i+7);
345         j = plaintext.indexOf('</name>',i+7);
346     }
347 */
348     plaintext = plaintext.replace(/&/g,'&'); // This should replace &pi; by π
349     return plaintext;
350 };
351 
352 /**
353  * Finds dependencies in a given term and notifies the parents by adding the
354  * dependent object to the found objects child elements.
355  * @param {String} term String containing dependencies for the given object.
356  * @private
357  */
358 JXG.Text.prototype.notifyParents = function (contentStr) {
359     var res = null;
360     var elements = this.board.elementsByName;
361 
362     do {
363         var search = /<value>([\w\s\*\/\^\-\+\(\)\[\],<>=!]+)<\/value>/;
364         res = search.exec(contentStr);
365         if (res!=null) {
366             JXG.GeonextParser.findDependencies(this,res[1],this.board);
367             contentStr = contentStr.substr(res.index);
368             contentStr = contentStr.replace(search,'');
369         }
370     } while (res!=null);
371     return this;
372 };
373 
374 /**
375  * @class This element is used to provide a constructor for text, which is just a wrapper for element {@link Text}. 
376  * @pseudo
377  * @description
378  * @name Text
379  * @augments JXG.GeometryElement
380  * @constructor
381  * @type JXG.Text
382  *
383  * @param {number,function_number,function_String,function} x,y,str Parent elements for text elements.
384  *                     <p>
385  *                     x and y are the coordinates of the lower left corner of the text box. The position of the text is fixed, 
386  *                     x and y are numbers. The position is variable if x or y are functions.
387  *                     <p>
388  *                     The text to display may be given as string or as function returning a string.
389  *
390  * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display
391  * 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.
392  * @see JXG.Text
393  * @example
394  * // Create a fixed text at position [0,1].
395  *   var t1 = board.create('text',[0,1,"Hello World"]); 
396  * </pre><div id="896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
397  * <script type="text/javascript">
398  *   var t1_board = JXG.JSXGraph.initBoard('896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
399  *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
400  * </script><pre>
401  * @example
402  * // Create a variable text at a variable position.
403  *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
404  *   var graph = board.create('text', 
405  *                        [function(x){ return s.Value();}, 1,
406  *                         function(){return "The value of s is"+s.Value().toFixed(2);}
407  *                        ]
408  *                     );
409  * </pre><div id="5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
410  * <script type="text/javascript">
411  *   var t2_board = JXG.JSXGraph.initBoard('5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
412  *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
413  *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+s.Value().toFixed(2);}]);
414  * </script><pre>
415  */
416 JXG.createText = function(board, parentArr, atts) {
417     atts = JXG.checkAttributes(atts,{layer:null,display:board.options.text.defaultDisplay,parent:null});  // 'html' or 'internal'
418     if(atts['parent'] != null) { atts['parent'] = atts['parent'].id;}
419     return new JXG.Text(board, parentArr[parentArr.length-1], atts['parent'], parentArr, atts['id'], atts['name'], atts['digits'], false, atts['display'],atts['layer']);
420 };
421 
422 JXG.JSXGraph.registerElement('text', JXG.createText);
423