      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