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