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 π 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