1 /* 2 Copyright 2008-2026 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 dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /* 33 Some functionalities in this file were developed as part of a software project 34 with students. We would like to thank all contributors for their help: 35 36 Winter semester 2024/2025: 37 Philipp Ditz, 38 Florian Hein, 39 Pirmin Hinderling, 40 Tim Sauer 41 */ 42 43 /*global JXG: true, define: true, window: true*/ 44 /*jslint nomen: true, plusplus: true*/ 45 46 /** 47 * @fileoverview In this file the Text element is defined. 48 */ 49 50 import JXG from "../jxg.js"; 51 import Const from "./constants.js"; 52 import GeometryElement from "./element.js"; 53 import GeonextParser from "../parser/geonext.js"; 54 import Env from "../utils/env.js"; 55 import Type from "../utils/type.js"; 56 import Mat from "../math/math.js"; 57 import CoordsElement from "./coordselement.js"; 58 59 var priv = { 60 /** 61 * @class 62 * @ignore 63 */ 64 HTMLSliderInputEventHandler: function () { 65 this._val = parseFloat(this.rendNodeRange.value); 66 this.rendNodeOut.value = this.rendNodeRange.value; 67 this.board.update(); 68 } 69 }; 70 71 /** 72 * Construct and handle texts. 73 * 74 * The coordinates can be relative to the coordinates of an element 75 * given in {@link JXG.Options#text.anchor}. 76 * 77 * MathJax, HTML and GEONExT syntax can be handled. 78 * @class Creates a new text object. Do not use this constructor to create a text. Use {@link JXG.Board#create} with 79 * type {@link Text} instead. 80 * @augments JXG.GeometryElement 81 * @augments JXG.CoordsElement 82 * @param {string|JXG.Board} board The board the new text is drawn on. 83 * @param {Array} coordinates An array with the user coordinates of the text. 84 * @param {Object} attributes An object containing visual properties and optional a name and a id. 85 * @param {string|function} content A string or a function returning a string. 86 * 87 */ 88 JXG.Text = function (board, coords, attributes, content) { 89 var tmp; 90 91 this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_TEXT); 92 93 this.element = this.board.select(attributes.anchor); 94 this.coordsConstructor(coords, this.evalVisProp('islabel')); 95 96 this.content = ""; 97 this.plaintext = ""; 98 this.plaintextOld = null; 99 this.orgText = ""; 100 101 this.needsSizeUpdate = false; 102 // Only used by infobox anymore 103 this.hiddenByParent = false; 104 105 /** 106 * Width and height of the text element in pixel. 107 * 108 * @private 109 * @type Array 110 */ 111 this.size = [1.0, 1.0]; 112 this.id = this.board.setId(this, 'T'); 113 114 this.board.renderer.drawText(this); 115 this.board.finalizeAdding(this); 116 117 // Set text before drawing 118 // this._createFctUpdateText(content); 119 // this.updateText(); 120 121 // Set attribute visible to true. This is necessary to 122 // create all sub-elements for button, input and checkbox 123 tmp = this.visProp.visible; 124 this.visProp.visible = true; 125 this.setText(content); 126 // Restore the correct attribute visible. 127 this.visProp.visible = tmp; 128 129 if (Type.isString(this.content)) { 130 this.notifyParents(this.content); 131 } 132 this.elType = 'text'; 133 134 this.methodMap = Type.deepCopy(this.methodMap, { 135 setText: "setTextJessieCode", 136 // free: 'free', 137 move: "setCoords", 138 Size: "getSize", 139 setAutoPosition: "setAutoPosition" 140 }); 141 }; 142 143 JXG.Text.prototype = new GeometryElement(); 144 Type.copyPrototypeMethods(JXG.Text, CoordsElement, 'coordsConstructor'); 145 146 JXG.extend( 147 JXG.Text.prototype, 148 /** @lends JXG.Text.prototype */ { 149 /** 150 * @private 151 * @param {Number} x 152 * @param {Number} y 153 * @returns {Boolean} 154 */ 155 // Test if the screen coordinates (x,y) are in a small stripe 156 // at the left side or at the right side of the text. 157 // Sensitivity is set in this.board.options.precision.hasPoint. 158 // If dragarea is set to 'all' (default), tests if the screen 159 // coordinates (x,y) are in within the text boundary. 160 hasPoint: function (x, y) { 161 var lft, rt, top, bot, ax, ay, type, r; 162 163 if (Type.isObject(this.evalVisProp('precision'))) { 164 type = this.board._inputDevice; 165 r = this.evalVisProp('precision.' + type); 166 } else { 167 // 'inherit' 168 r = this.board.options.precision.hasPoint; 169 } 170 if (this.transformations.length > 0) { 171 //Transform the mouse/touch coordinates 172 // back to the original position of the text. 173 lft = Mat.matVecMult( 174 Mat.inverse(this.board.renderer.joinTransforms(this, this.transformations)), 175 [1, x, y] 176 ); 177 x = lft[1]; 178 y = lft[2]; 179 } 180 181 ax = this.getAnchorX(); 182 if (ax === 'right') { 183 lft = this.coords.scrCoords[1] - this.size[0]; 184 } else if (ax === 'middle') { 185 lft = this.coords.scrCoords[1] - 0.5 * this.size[0]; 186 } else { 187 lft = this.coords.scrCoords[1]; 188 } 189 rt = lft + this.size[0]; 190 191 ay = this.getAnchorY(); 192 if (ay === 'top') { 193 bot = this.coords.scrCoords[2] + this.size[1]; 194 } else if (ay === 'middle') { 195 bot = this.coords.scrCoords[2] + 0.5 * this.size[1]; 196 } else { 197 bot = this.coords.scrCoords[2]; 198 } 199 top = bot - this.size[1]; 200 201 if (this.evalVisProp('dragarea') === 'all') { 202 return x >= lft - r && x < rt + r && y >= top - r && y <= bot + r; 203 } 204 // e.g. 'small' 205 return ( 206 y >= top - r && 207 y <= bot + r && 208 ((x >= lft - r && x <= lft + 2 * r) || (x >= rt - 2 * r && x <= rt + r)) 209 ); 210 }, 211 212 /** 213 * This sets the updateText function of this element depending on the type of text content passed. 214 * Used by {@link JXG.Text#_setText}. 215 * @param {String|Function|Number} text 216 * @private 217 * @see JXG.Text#_setText 218 */ 219 _createFctUpdateText: function (text) { 220 var updateText, e, digits, 221 resolvedText, 222 i, that, 223 ev_p = this.evalVisProp('parse'), 224 ev_um = this.evalVisProp('usemathjax'), 225 ev_uk = this.evalVisProp('usekatex'), 226 convertJessieCode = false; 227 228 this.orgText = text; 229 230 if (Type.isFunction(text)) { 231 /** 232 * Dynamically created function to update the content 233 * of a text. Can not be overwritten. 234 * <p> 235 * <value> tags will not be evaluated if text is provided by a function 236 * <p> 237 * Sets the property <tt>plaintext</tt> of the text element. 238 * 239 * @private 240 */ 241 this.updateText = function () { 242 resolvedText = text().toString(); // Evaluate function 243 if (ev_p && !ev_um && !ev_uk) { 244 this.plaintext = this.replaceSub( 245 this.replaceSup( 246 this.convertGeonextAndSketchometry2CSS(resolvedText, false) 247 ) 248 ); 249 } else { 250 this.plaintext = resolvedText; 251 } 252 }; 253 } else { 254 if (Type.isNumber(text) && this.evalVisProp('formatnumber')) { 255 if (this.evalVisProp('tofraction')) { 256 if (ev_um) { 257 this.content = '\\(' + Type.toFraction(text, true) + '\\)'; 258 } else { 259 this.content = Type.toFraction(text, ev_uk); 260 } 261 } else { 262 digits = this.evalVisProp('digits'); 263 if (this.useLocale()) { 264 this.content = this.formatNumberLocale(text, digits); 265 } else { 266 this.content = Type.toFixed(text, digits); 267 } 268 } 269 } else if (Type.isString(text) && ev_p) { 270 if (this.evalVisProp('useasciimathml')) { 271 // ASCIIMathML 272 // value-tags are not supported 273 this.content = "'`" + text + "`'"; 274 } else if (ev_um || ev_uk) { 275 // MathJax or KaTeX 276 // Replace value-tags by functions 277 // sketchofont is ignored 278 this.content = this.valueTagToJessieCode(text); 279 if (!Type.isArray(this.content)) { 280 // For some reason we don't have to mask backslashes in an array of strings 281 // anymore. 282 // 283 // for (i = 0; i < this.content.length; i++) { 284 // this.content[i] = this.content[i].replace(/\\/g, "\\\\"); // Replace single backslash by double 285 // } 286 // } else { 287 this.content = this.content.replace(/\\/g, "\\\\"); // Replace single backslash by double 288 } 289 } else { 290 // No TeX involved. 291 // Converts GEONExT syntax into JavaScript string 292 // Short math is allowed 293 // Replace value-tags by functions 294 // Avoid geonext2JS calls 295 this.content = this.poorMansTeX(this.valueTagToJessieCode(text)); 296 } 297 convertJessieCode = true; 298 } else { 299 this.content = text; 300 } 301 302 // Generate function which returns the text to be displayed 303 if (convertJessieCode) { 304 // Convert JessieCode to JS function 305 if (Type.isArray(this.content)) { 306 // This is the case if the text contained value-tags. 307 // These value-tags consist of JessieCode snippets 308 // which are now replaced by JavaScript functions 309 that = this; 310 for (i = 0; i < this.content.length; i++) { 311 if (this.content[i][0] !== '"') { 312 this.content[i] = this.board.jc.snippet(this.content[i], true, "", false); 313 for (e in this.content[i].deps) { 314 this.addParents(this.content[i].deps[e]); 315 this.content[i].deps[e].addChild(this); 316 } 317 } 318 } 319 320 updateText = function() { 321 var i, t, 322 digits = that.evalVisProp('digits'), 323 txt = ''; 324 325 for (i = 0; i < that.content.length; i++) { 326 if (Type.isFunction(that.content[i])) { 327 t = that.content[i](); 328 if (that.useLocale()) { 329 t = that.formatNumberLocale(t, digits); 330 } else { 331 t = Type.toFixed(t, digits); 332 } 333 } else { 334 t = that.content[i]; 335 // Instead of 't.at(t.length - 1)' also 't.(-1)' should work. 336 // However in Moodle 4.2 't.(-1)' returns an empty string. 337 // In plain HTML pages it works. 338 if (t[0] === '"' && t[t.length - 1] === '"') { 339 t = t.slice(1, -1); 340 } 341 } 342 343 txt += t; 344 } 345 return txt; 346 }; 347 } else { 348 updateText = this.board.jc.snippet(this.content, true, "", false); 349 for (e in updateText.deps) { 350 this.addParents(updateText.deps[e]); 351 updateText.deps[e].addChild(this); 352 } 353 } 354 355 // Ticks have been escaped in valueTagToJessieCode 356 this.updateText = function () { 357 this.plaintext = this.unescapeTicks(updateText()); 358 }; 359 } else { 360 this.updateText = function () { 361 this.plaintext = this.content; // text; 362 }; 363 } 364 } 365 }, 366 367 /** 368 * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because 369 * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode. 370 * @param {String|Function|Number} text 371 * @returns {JXG.Text} 372 * @private 373 */ 374 _setText: function (text) { 375 this._createFctUpdateText(text); 376 377 // First evaluation of the string. 378 // We need this for display='internal' and Canvas 379 this.updateText(); 380 this.fullUpdate(); 381 382 // We do not call updateSize for the infobox to speed up rendering 383 if (!this.board.infobox || this.id !== this.board.infobox.id) { 384 this.updateSize(); // updateSize() is called at least once. 385 } 386 387 // This may slow down canvas renderer 388 // if (this.board.renderer.type === 'canvas') { 389 // this.board.fullUpdate(); 390 // } 391 392 return this; 393 }, 394 395 /** 396 * Defines new content but converts < and > to HTML entities before updating the DOM. 397 * @param {String|function} text 398 */ 399 setTextJessieCode: function (text) { 400 var s; 401 402 this.visProp.castext = text; 403 if (Type.isFunction(text)) { 404 s = function () { 405 return Type.sanitizeHTML(text()); 406 }; 407 } else { 408 if (Type.isNumber(text)) { 409 s = text; 410 } else { 411 s = Type.sanitizeHTML(text); 412 } 413 } 414 415 return this._setText(s); 416 }, 417 418 /** 419 * Defines new content. 420 * @param {String|function} text 421 * @returns {JXG.Text} Reference to the text object. 422 */ 423 setText: function (text) { 424 return this._setText(text); 425 }, 426 427 /** 428 * Recompute the width and the height of the text box. 429 * Updates the array {@link JXG.Text#size} with pixel values. 430 * The result may differ from browser to browser 431 * by some pixels. 432 * In canvas an old IEs we use a very crude estimation of the dimensions of 433 * the textbox. 434 * JSXGraph needs {@link JXG.Text#size} for applying rotations in IE and 435 * for aligning text. 436 * 437 * @return {this} [description] 438 */ 439 updateSize: function () { 440 var tmp, 441 that, 442 node, 443 ev_d = this.evalVisProp('display'); 444 445 if (!Env.isBrowser || this.board.renderer.type === 'no') { 446 return this; 447 } 448 node = this.rendNode; 449 450 451 /** 452 * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode. 453 */ 454 if (ev_d === "html" || this.board.renderer.type === 'vml') { 455 if (Type.exists(node.offsetWidth)) { 456 that = this; 457 window.setTimeout(function () { 458 that.size = [node.offsetWidth, node.offsetHeight]; 459 460 // This would be the way to determine the height of a MathJax formula 461 // rendered with SVG (i.e. using tex-svg-nofont). 462 // This is needed if the text element's anchorY === 'middle' 463 // and the text is included in a board.renderer.dumpToDataURI() call 464 // with MathJax formulas. 465 // if (Type.exists(node.firstChild) && node.firstChild.nodeName === 'MJX-CONTAINER' && 466 // Type.exists(node.firstChild.firstChild && node.firstChild.firstChild.nodeName === 'SVG') 467 // ) { 468 // // console.log(that.visProp.fontsize * 0.5) 469 // // that.size[1] += 2 * that.visProp.fontsize; 470 // that.size = [node.firstChild.firstChild.scrollWidth, node.firstChild.firstChild.scrollHeight]; 471 // } 472 473 that.needsUpdate = true; 474 that.updateRenderer(); 475 }, 0); 476 // In case, there is non-zero padding or borders 477 // the following approach does not longer work. 478 // s = [node.offsetWidth, node.offsetHeight]; 479 // if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight 480 // that = this; 481 // window.setTimeout(function () { 482 // that.size = [node.offsetWidth, node.offsetHeight]; 483 // that.needsUpdate = true; 484 // that.updateRenderer(); 485 // }, 0); 486 // } else { 487 // this.size = s; 488 // } 489 } else { 490 this.size = this.crudeSizeEstimate(); 491 } 492 } else if (ev_d === 'internal') { 493 if (this.board.renderer.type === 'svg') { 494 that = this; 495 window.setTimeout(function () { 496 try { 497 tmp = node.getBBox(); 498 that.size = [tmp.width, tmp.height]; 499 that.needsUpdate = true; 500 that.updateRenderer(); 501 } catch (e) {} 502 }, 0); 503 } else if (this.board.renderer.type === 'canvas') { 504 this.size = this.crudeSizeEstimate(); 505 } 506 } 507 508 return this; 509 }, 510 511 /** 512 * A very crude estimation of the dimensions of the textbox in case nothing else is available. 513 * @returns {Array} 514 */ 515 crudeSizeEstimate: function () { 516 var ev_fs = parseFloat(this.evalVisProp('fontsize')); 517 return [ev_fs * this.plaintext.length * 0.45, ev_fs * 0.9]; 518 }, 519 520 /** 521 * Decode unicode entities into characters. 522 * @param {String} string 523 * @returns {String} 524 */ 525 utf8_decode: function (string) { 526 return string.replace(/(\w+);/g, function (m, p1) { 527 return String.fromCharCode(parseInt(p1, 16)); 528 }); 529 }, 530 531 /** 532 * Replace _{} by <sub> 533 * @param {String} te String containing _{}. 534 * @returns {String} Given string with _{} replaced by <sub>. 535 */ 536 replaceSub: function (te) { 537 if (!te.indexOf) { 538 return te; 539 } 540 541 var j, 542 i = te.indexOf("_{"); 543 544 // The regexp in here are not used for filtering but to provide some kind of sugar for label creation, 545 // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway. 546 /*jslint regexp: true*/ 547 while (i >= 0) { 548 te = te.slice(0, i) + te.slice(i).replace(/_\{/, "<sub>"); 549 j = te.indexOf("}", i + 4); 550 if (j >= 0) { 551 te = te.slice(0, j) + te.slice(j).replace(/\}/, "</sub>"); 552 } 553 i = te.indexOf("_{"); 554 } 555 556 i = te.indexOf("_"); 557 while (i >= 0) { 558 te = te.slice(0, i) + te.slice(i).replace(/_(.?)/, "<sub>$1</sub>"); 559 i = te.indexOf("_"); 560 } 561 562 return te; 563 }, 564 565 /** 566 * Replace ^{} by <sup> 567 * @param {String} te String containing ^{}. 568 * @returns {String} Given string with ^{} replaced by <sup>. 569 */ 570 replaceSup: function (te) { 571 if (!te.indexOf) { 572 return te; 573 } 574 575 var j, 576 i = te.indexOf("^{"); 577 578 // The regexp in here are not used for filtering but to provide some kind of sugar for label creation, 579 // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway. 580 /*jslint regexp: true*/ 581 while (i >= 0) { 582 te = te.slice(0, i) + te.slice(i).replace(/\^\{/, "<sup>"); 583 j = te.indexOf("}", i + 4); 584 if (j >= 0) { 585 te = te.slice(0, j) + te.slice(j).replace(/\}/, "</sup>"); 586 } 587 i = te.indexOf("^{"); 588 } 589 590 i = te.indexOf("^"); 591 while (i >= 0) { 592 te = te.slice(0, i) + te.slice(i).replace(/\^(.?)/, "<sup>$1</sup>"); 593 i = te.indexOf("^"); 594 } 595 596 return te; 597 }, 598 599 /** 600 * Return the width of the text element. 601 * @returns {Array} [width, height] in pixel 602 */ 603 getSize: function () { 604 return this.size; 605 }, 606 607 /** 608 * Move the text to new coordinates. 609 * @param {number} x 610 * @param {number} y 611 * @returns {object} reference to the text object. 612 */ 613 setCoords: function (x, y) { 614 var coordsAnchor, dx, dy; 615 if (Type.isArray(x) && x.length > 1) { 616 y = x[1]; 617 x = x[0]; 618 } 619 620 if (this.evalVisProp('islabel') && Type.exists(this.element)) { 621 coordsAnchor = this.element.getLabelAnchor(); 622 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX; 623 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY; 624 625 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]); 626 } else { 627 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]); 628 } 629 630 // this should be a local update, otherwise there might be problems 631 // with the tick update routine resulting in orphaned tick labels 632 this.fullUpdate(); 633 634 return this; 635 }, 636 637 /** 638 * Evaluates the text. 639 * Then, the update function of the renderer 640 * is called. 641 */ 642 update: function (fromParent) { 643 if (!this.needsUpdate) { 644 return this; 645 } 646 647 this.updateCoords(fromParent); 648 this.updateText(); 649 650 if (this.evalVisProp('display') === 'internal') { 651 if (Type.isString(this.plaintext)) { 652 this.plaintext = this.utf8_decode(this.plaintext); 653 } 654 } 655 656 this.checkForSizeUpdate(); 657 if (this.needsSizeUpdate) { 658 this.updateSize(); 659 } 660 661 return this; 662 }, 663 664 /** 665 * Used to save updateSize() calls. 666 * Called in JXG.Text.update 667 * That means this.update() has been called. 668 * More tests are in JXG.Renderer.updateTextStyle. The latter tests 669 * are one update off. But this should pose not too many problems, since 670 * it affects fontSize and cssClass changes. 671 * 672 * @private 673 */ 674 checkForSizeUpdate: function () { 675 if (this.board.infobox && this.id === this.board.infobox.id) { 676 this.needsSizeUpdate = false; 677 } else { 678 // For some magic reason it is more efficient on the iPad to 679 // call updateSize() for EVERY text element EVERY time. 680 this.needsSizeUpdate = this.plaintextOld !== this.plaintext; 681 682 if (this.needsSizeUpdate) { 683 this.plaintextOld = this.plaintext; 684 } 685 } 686 }, 687 688 /** 689 * The update function of the renderer 690 * is called. 691 * @private 692 */ 693 updateRenderer: function () { 694 if ( 695 //this.board.updateQuality === this.board.BOARD_QUALITY_HIGH && 696 this.evalVisProp('autoposition') 697 ) { 698 this.setAutoPosition().updateConstraint(); 699 } 700 return this.updateRendererGeneric('updateText'); 701 }, 702 703 /** 704 * Converts shortened math syntax into correct syntax: 3x instead of 3*x or 705 * (a+b)(3+1) instead of (a+b)*(3+1). 706 * 707 * @private 708 * @param{String} expr Math term 709 * @returns {string} expanded String 710 */ 711 expandShortMath: function (expr) { 712 var re = /([)0-9.])\s*([(a-zA-Z_])/g; 713 return expr.replace(re, "$1*$2"); 714 }, 715 716 /** 717 * Converts the GEONExT syntax of the <value> terms into JavaScript. 718 * Also, all Objects whose name appears in the term are searched and 719 * the text is added as child to these objects. 720 * This method is called if the attribute parse==true is set. 721 * 722 * Obsolete, replaced by JXG.Text.valueTagToJessieCode 723 * 724 * @param{String} contentStr String to be parsed 725 * @param{Boolean} [expand] Optional flag if shortened math syntax is allowed (e.g. 3x instead of 3*x). 726 * @param{Boolean} [avoidGeonext2JS] Optional flag if geonext2JS should be called. For backwards compatibility 727 * this has to be set explicitly to true. 728 * @param{Boolean} [outputTeX] Optional flag which has to be true if the resulting term will be sent to MathJax or KaTeX. 729 * If true, "_" and "^" are NOT replaced by HTML tags sub and sup. Default: false, i.e. the replacement is done. 730 * This flag allows the combination of <value> tag containing calculations with TeX output. 731 * 732 * @deprecated 733 * @private 734 * @see JXG.GeonextParser#geonext2JS 735 * @see JXG.Text#valueTagToJessieCode 736 * 737 */ 738 generateTerm: function (contentStr, expand, avoidGeonext2JS) { 739 var res, 740 term, 741 i, 742 j, 743 plaintext = '""'; 744 745 // Revert possible jc replacement 746 contentStr = contentStr || ""; 747 contentStr = contentStr.replace(/\r/g, ""); 748 contentStr = contentStr.replace(/\n/g, ""); 749 contentStr = contentStr.replace(/"/g, "'"); 750 contentStr = contentStr.replace(/'/g, "\\'"); 751 752 // Old GEONExT syntax, not (yet) supported as TeX output. 753 // Otherwise, the else clause should be used. 754 // That means, i.e. the <arc> tag and <sqrt> tag are not 755 // converted into TeX syntax. 756 contentStr = contentStr.replace(/&arc;/g, "∠"); 757 contentStr = contentStr.replace(/<arc\s*\/>/g, "∠"); 758 contentStr = contentStr.replace(/<arc\s*\/>/g, "∠"); 759 contentStr = contentStr.replace(/<sqrt\s*\/>/g, "√"); 760 761 contentStr = contentStr.replace(/<value>/g, "<value>"); 762 contentStr = contentStr.replace(/<\/value>/g, "</value>"); 763 764 // Convert GEONExT syntax into JavaScript syntax 765 i = contentStr.indexOf("<value>"); 766 j = contentStr.indexOf("</value>"); 767 if (i >= 0) { 768 while (i >= 0) { 769 plaintext += 770 ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"'; 771 // plaintext += ' + "' + this.replaceSub(contentStr.slice(0, i)) + '"'; 772 773 term = contentStr.slice(i + 7, j); 774 term = term.replace(/\s+/g, ""); // Remove all whitespace 775 if (expand === true) { 776 term = this.expandShortMath(term); 777 } 778 if (avoidGeonext2JS) { 779 res = term; 780 } else { 781 res = GeonextParser.geonext2JS(term, this.board); 782 } 783 res = res.replace(/\\"/g, "'"); 784 res = res.replace(/\\'/g, "'"); 785 786 // GEONExT-Hack: apply rounding once only. 787 if (res.indexOf('toFixed') < 0) { 788 // output of a value tag 789 if ( 790 Type.isNumber( 791 Type.bind(this.board.jc.snippet(res, true, '', false), this)() 792 ) 793 ) { 794 // may also be a string 795 plaintext += '+(' + res + ').toFixed(' + this.evalVisProp('digits') + ')'; 796 } else { 797 plaintext += '+(' + res + ')'; 798 } 799 } else { 800 plaintext += '+(' + res + ')'; 801 } 802 803 contentStr = contentStr.slice(j + 8); 804 i = contentStr.indexOf("<value>"); 805 j = contentStr.indexOf("</value>"); 806 } 807 } 808 809 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"'; 810 plaintext = this.convertGeonextAndSketchometry2CSS(plaintext); 811 812 // This should replace e.g. π by π 813 plaintext = plaintext.replace(/&/g, "&"); 814 plaintext = plaintext.replace(/"/g, "'"); 815 816 return plaintext; 817 }, 818 819 /** 820 * Replace value-tags in string by JessieCode functions. 821 * @param {String} contentStr 822 * @returns String 823 * @private 824 * @example 825 * "The x-coordinate of A is <value>X(A)</value>" 826 * 827 */ 828 valueTagToJessieCode: function (contentStr) { 829 var res, term, 830 i, j, 831 expandShortMath = true, 832 textComps = [], 833 tick = '"'; 834 835 contentStr = contentStr || ""; 836 contentStr = contentStr.replace(/\r/g, ""); 837 contentStr = contentStr.replace(/\n/g, ""); 838 839 contentStr = contentStr.replace(/<value>/g, "<value>"); 840 contentStr = contentStr.replace(/<\/value>/g, "</value>"); 841 842 // Convert content of value tag (GEONExT/JessieCode) syntax into JavaScript syntax 843 i = contentStr.indexOf("<value>"); 844 j = contentStr.indexOf("</value>"); 845 if (i >= 0) { 846 while (i >= 0) { 847 // Add string fragment before <value> tag 848 textComps.push(tick + this.escapeTicks(contentStr.slice(0, i)) + tick); 849 850 term = contentStr.slice(i + 7, j); 851 term = term.replace(/\s+/g, ""); // Remove all whitespace 852 if (expandShortMath === true) { 853 term = this.expandShortMath(term); 854 } 855 res = term; 856 res = res.replace(/\\"/g, "'").replace(/\\'/g, "'"); 857 858 // // Hack: apply rounding once only. 859 // if (res.indexOf('toFixed') < 0) { 860 // // Output of a value tag 861 // // Run the JessieCode parser 862 // if ( 863 // Type.isNumber( 864 // Type.bind(this.board.jc.snippet(res, true, "", false), this)() 865 // ) 866 // ) { 867 // // Output is number 868 // // textComps.push( 869 // // '(' + res + ').toFixed(' + this.evalVisProp('digits') + ')' 870 // // ); 871 // textComps.push('(' + res + ')'); 872 // } else { 873 // // Output is a string 874 // textComps.push("(" + res + ")"); 875 // } 876 // } else { 877 textComps.push("(" + res + ")"); 878 // } 879 contentStr = contentStr.slice(j + 8); 880 i = contentStr.indexOf("<value>"); 881 j = contentStr.indexOf("</value>"); 882 } 883 } 884 // Add trailing string fragment 885 textComps.push(tick + this.escapeTicks(contentStr) + tick); 886 887 // return textComps.join(" + ").replace(/&/g, "&"); 888 for (i = 0; i < textComps.length; i++) { 889 textComps[i] = textComps[i].replace(/&/g, "&"); 890 } 891 return textComps; 892 }, 893 894 /** 895 * Simple math rendering using HTML / CSS only. In case of array, 896 * handle each entry separately and return array with the 897 * rendering strings. 898 * 899 * @param {String|Array} s 900 * @returns {String|Array} 901 * @see JXG.Text#convertGeonextAndSketchometry2CSS 902 * @private 903 * @see JXG.Text#replaceSub 904 * @see JXG.Text#replaceSup 905 * @see JXG.Text#convertGeonextAndSketchometry2CSS 906 */ 907 poorMansTeX: function (s) { 908 var i, a; 909 if (Type.isArray(s)) { 910 a = []; 911 for (i = 0; i < s.length; i++) { 912 a.push(this.poorMansTeX(s[i])); 913 } 914 return a; 915 } 916 917 s = s 918 .replace(/<arc\s*\/*>/g, "∠") 919 .replace(/<arc\s*\/*>/g, "∠") 920 .replace(/<sqrt\s*\/*>/g, "√") 921 .replace(/<sqrt\s*\/*>/g, "√"); 922 return this.convertGeonextAndSketchometry2CSS(this.replaceSub(this.replaceSup(s)), true); 923 }, 924 925 /** 926 * Replace ticks by URI escape sequences 927 * 928 * @param {String} s 929 * @returns String 930 * @private 931 * 932 */ 933 escapeTicks: function (s) { 934 return s.replace(/"/g, "%22").replace(/'/g, "%27"); 935 }, 936 937 /** 938 * Replace escape sequences for ticks by ticks 939 * 940 * @param {String} s 941 * @returns String 942 * @private 943 */ 944 unescapeTicks: function (s) { 945 return s.replace(/%22/g, '"').replace(/%27/g, "'"); 946 }, 947 948 /** 949 * Converts the GEONExT tags <overline> and <arrow> to 950 * HTML span tags with proper CSS formatting. 951 * @private 952 * @see JXG.Text.poorMansTeX 953 * @see JXG.Text._setText 954 */ 955 convertGeonext2CSS: function (s) { 956 if (Type.isString(s)) { 957 s = s.replace( 958 /(<|<)overline(>|>)/g, 959 "<span style=text-decoration:overline;>" 960 ); 961 s = s.replace(/(<|<)\/overline(>|>)/g, "</span>"); 962 s = s.replace( 963 /(<|<)arrow(>|>)/g, 964 "<span style=text-decoration:overline;>" 965 ); 966 s = s.replace(/(<|<)\/arrow(>|>)/g, "</span>"); 967 } 968 969 return s; 970 }, 971 972 /** 973 * Converts the sketchometry tag <sketchofont> to 974 * HTML span tags with proper CSS formatting. 975 * 976 * @param {String|Function|Number} s Text 977 * @param {Boolean} escape Flag if ticks should be escaped. Escaping is necessary 978 * if s is a text. It has to be avoided if s is a function returning text. 979 * @private 980 * @see JXG.Text._setText 981 * @see JXG.Text.convertGeonextAndSketchometry2CSS 982 * 983 */ 984 convertSketchometry2CSS: function (s, escape) { 985 var t1 = "<span class=\"sketcho sketcho-inherit sketcho-", 986 t2 = "\"></span>"; 987 988 if (Type.isString(s)) { 989 if (escape) { 990 t1 = this.escapeTicks(t1); 991 t2 = this.escapeTicks(t2); 992 } 993 s = s.replace(/(<|<)sketchofont(>|>)/g, t1); 994 s = s.replace(/(<|<)\/sketchofont(>|>)/g, t2); 995 } 996 997 return s; 998 }, 999 1000 /** 1001 * Alias for convertGeonext2CSS and convertSketchometry2CSS 1002 * 1003 * @param {String|Function|Number} s Text 1004 * @param {Boolean} escape Flag if ticks should be escaped 1005 * @private 1006 * @see JXG.Text.convertGeonext2CSS 1007 * @see JXG.Text.convertSketchometry2CSS 1008 */ 1009 convertGeonextAndSketchometry2CSS: function (s, escape) { 1010 s = this.convertGeonext2CSS(s); 1011 s = this.convertSketchometry2CSS(s, escape); 1012 return s; 1013 }, 1014 1015 /** 1016 * Finds dependencies in a given term and notifies the parents by adding the 1017 * dependent object to the found objects child elements. 1018 * @param {String} content String containing dependencies for the given object. 1019 * @private 1020 */ 1021 notifyParents: function (content) { 1022 var search, 1023 res = null; 1024 1025 // revert possible jc replacement 1026 content = content.replace(/<value>/g, "<value>"); 1027 content = content.replace(/<\/value>/g, "</value>"); 1028 1029 do { 1030 search = /<value>([\w\s*/^\-+()[\],<>=!]+)<\/value>/; 1031 res = search.exec(content); 1032 1033 if (res !== null) { 1034 GeonextParser.findDependencies(this, res[1], this.board); 1035 content = content.slice(res.index); 1036 content = content.replace(search, ""); 1037 } 1038 } while (res !== null); 1039 1040 return this; 1041 }, 1042 1043 // documented in element.js 1044 getParents: function () { 1045 var p; 1046 if (this.relativeCoords !== undefined) { 1047 // Texts with anchor elements, excluding labels 1048 p = [ 1049 this.relativeCoords.usrCoords[1], 1050 this.relativeCoords.usrCoords[2], 1051 this.orgText 1052 ]; 1053 } else { 1054 // Other texts 1055 p = [this.Z(), this.X(), this.Y(), this.orgText]; 1056 } 1057 1058 if (this.parents.length !== 0) { 1059 p = this.parents; 1060 } 1061 1062 return p; 1063 }, 1064 1065 /** 1066 * Returns the bounding box of the text element in user coordinates as an 1067 * array of length 4: [upper left x, upper left y, lower right x, lower right y]. 1068 * The method assumes that the lower left corner is at position [el.X(), el.Y()] 1069 * of the text element el, i.e. the attributes anchorX, anchorY are ignored. 1070 * 1071 * <p> 1072 * <strong>Attention:</strong> for labels, [0, 0, 0, 0] is returned. 1073 * 1074 * @returns Array 1075 */ 1076 bounds: function () { 1077 var c = this.coords.usrCoords; 1078 1079 if ( 1080 this.evalVisProp('islabel') || 1081 this.board.unitY === 0 || 1082 this.board.unitX === 0 1083 ) { 1084 return [0, 0, 0, 0]; 1085 } 1086 return [ 1087 c[1], 1088 c[2] + this.size[1] / this.board.unitY, 1089 c[1] + this.size[0] / this.board.unitX, 1090 c[2] 1091 ]; 1092 }, 1093 1094 /** 1095 * Returns the value of the attribute "anchorX". If this equals "auto", 1096 * returns "left", "middle", or "right", depending on the 1097 * value of the attribute "position". 1098 * @returns String 1099 */ 1100 getAnchorX: function () { 1101 var a = this.evalVisProp('anchorx'); 1102 if (a === 'auto') { 1103 switch (this.visProp.position) { 1104 case "top": 1105 case "bot": 1106 return 'middle'; 1107 case "rt": 1108 case "lrt": 1109 case "urt": 1110 return 'left'; 1111 case "lft": 1112 case "llft": 1113 case "ulft": 1114 default: 1115 return 'right'; 1116 } 1117 } 1118 return a; 1119 }, 1120 1121 /** 1122 * Returns the value of the attribute "anchorY". If this equals "auto", 1123 * returns "bottom", "middle", or "top", depending on the 1124 * value of the attribute "position". 1125 * @returns String 1126 */ 1127 getAnchorY: function () { 1128 var a = this.evalVisProp('anchory'); 1129 if (a === 'auto') { 1130 switch (this.visProp.position) { 1131 case "top": 1132 case "ulft": 1133 case "urt": 1134 return 'bottom'; 1135 case "bot": 1136 case "lrt": 1137 case "llft": 1138 return 'top'; 1139 case "rt": 1140 case "lft": 1141 default: 1142 return 'middle'; 1143 } 1144 } 1145 return a; 1146 }, 1147 1148 /** 1149 * Computes the number of overlaps of a box of w pixels width, h pixels height 1150 * and center (x, y) 1151 * 1152 * An overlap occurs when either: 1153 * <ol> 1154 * <li> For labels/points: Their bounding boxes intersect 1155 * <li> For other objects: The object contains the center point of the box 1156 * </ol> 1157 * 1158 * @private 1159 * @param {Number} x x-coordinate of the center (screen coordinates) 1160 * @param {Number} y y-coordinate of the center (screen coordinates) 1161 * @param {Number} w width of the box in pixel 1162 * @param {Number} h width of the box in pixel 1163 * @param {Array} [whiteList] array of ids which should be ignored 1164 * @return {Number} Number of overlapping elements 1165 */ 1166 getNumberOfConflicts: function(x, y, w, h, whiteList) { 1167 whiteList = whiteList || []; 1168 var count = 0, 1169 i, obj, 1170 coords, 1171 saveHasInnerPoints, 1172 savePointPrecision = this.board.options.precision.hasPoint, 1173 objCenterX, objCenterY, 1174 objWidth, objHeight; 1175 1176 // set a new precision for hasPoint 1177 // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5; 1178 this.board.options.precision.hasPoint = (w + h) * 0.3; 1179 1180 // loop over all objects 1181 for (i = 0; i < this.board.objectsList.length; i++) { 1182 obj = this.board.objectsList[i]; 1183 1184 //Skip the object if it is not meant to influence label position 1185 if ( 1186 obj.visPropCalc.visible && 1187 obj !== this && 1188 whiteList.indexOf(obj.id) === -1 && 1189 obj.evalVisProp('ignoreforlabelautoposition') !== true 1190 ) { 1191 // Save hasinnerpoints and temporarily disable to handle polygon areas 1192 saveHasInnerPoints = obj.visProp.hasinnerpoints; 1193 obj.visProp.hasinnerpoints = false; 1194 1195 // If is label or point use other conflict detection 1196 if ( 1197 obj.visProp.islabel || 1198 obj.elementClass === Const.OBJECT_CLASS_POINT 1199 ) { 1200 // get coords and size of the object 1201 coords = obj.coords.scrCoords; 1202 objCenterX = coords[1]; 1203 objCenterY = coords[2]; 1204 objWidth = obj.size[0]; 1205 objHeight = obj.size[1]; 1206 1207 // move coords to the center of the label 1208 if (obj.visProp.islabel) { 1209 // Vertical adjustment 1210 if (obj.visProp.anchory === 'top') { 1211 objCenterY = objCenterY + objHeight / 2; 1212 } else { 1213 objCenterY = objCenterY - objHeight / 2; 1214 } 1215 1216 // Horizontal adjustment 1217 if (obj.visProp.anchorx === 'left') { 1218 objCenterX = objCenterX + objWidth / 2; 1219 } else { 1220 objCenterX = objCenterX - objWidth / 2; 1221 } 1222 } else { 1223 // Points are treated dimensionless 1224 objWidth = 0; 1225 objHeight = 0; 1226 } 1227 1228 // Check for overlap 1229 if ( 1230 Math.abs(objCenterX - x) < (w + objWidth) / 2 && 1231 Math.abs(objCenterY - y) < (h + objHeight) / 2 1232 ) { 1233 count++; 1234 } 1235 1236 //if not label or point check conflict with hasPoint 1237 } else if (obj.hasPoint(x, y)) { 1238 count++; 1239 } 1240 1241 // Restore original hasinnerpoints 1242 obj.visProp.hasinnerpoints = saveHasInnerPoints; 1243 } 1244 } 1245 1246 // Restore original precision 1247 this.board.options.precision.hasPoint = savePointPrecision; 1248 1249 return count; 1250 }, 1251 /** 1252 * Calculates the score of a label position with a given radius and angle. The score is calculated by the following rules: 1253 * <ul> 1254 * <li> the maximum score is 0 1255 * <li> if the label is outside of the bounding box, the score is reduced by 1 1256 * <li> for each conflict, the score is reduced by 1 1257 * <li> the score is reduced by the displacement (angle difference between old and new position) of the label 1258 * <li> the score is reduced by the angle between the original label position and the new label position 1259 * </ul> 1260 * 1261 * @param {number} radius radius in pixels 1262 * @param {number} angle angle in radians 1263 * @returns {number} Position score, higher values indicate better positions 1264 */ 1265 calculateScore: function(radius, angle) { 1266 var x, y, co, si, angleCurrentOffset, angleDifference, 1267 score = 0, 1268 cornerPoint = [0,0], 1269 w = this.getSize()[0], 1270 h = this.getSize()[1], 1271 anchorCoords, 1272 currentOffset = this.evalVisProp('offset'), 1273 boundingBox = this.board.getBoundingBox(); 1274 1275 if (this.evalVisProp('islabel') && Type.exists(this.element)) { 1276 anchorCoords = this.element.getLabelAnchor().scrCoords; 1277 } else { 1278 return 0; 1279 } 1280 co = Math.cos(angle); 1281 si = Math.sin(angle); 1282 1283 // calculate new position with srccoords, radius and angle 1284 x = anchorCoords[1] + radius * co; 1285 y = anchorCoords[2] - radius * si; 1286 1287 // if the label was placed on the left side of the element, the anchorx is set to "right" 1288 if (co < 0) { 1289 cornerPoint[0] = x - w; 1290 x -= w / 2; 1291 } else { 1292 cornerPoint[0] = x + w; 1293 x += w / 2; 1294 } 1295 1296 // If the label was placed on the bottom side of the element, so the anchory is set to "top" 1297 if (si < 0) { 1298 cornerPoint[1] = y + h; 1299 y += h / 2; 1300 } else { 1301 cornerPoint[1] = y - h; 1302 y -= h / 2; 1303 } 1304 1305 // If label was not in bounding box, score is reduced by 1 1306 if( 1307 cornerPoint[0] < 0 || 1308 cornerPoint[0] > (boundingBox[2] - boundingBox[0]) * this.board.unitX || 1309 cornerPoint[1] < 0 || 1310 cornerPoint[1] > (boundingBox[1] - boundingBox[3]) * this.board.unitY 1311 ) { 1312 score -= 1; 1313 } 1314 1315 // Per conflict, score is reduced by 1 1316 score -= this.getNumberOfConflicts(x, y, w, h, Type.evaluate(this.visProp.autopositionwhitelist)); 1317 1318 // Calculate displacement, minimum score is 0 if radius is minRadius, maximum score is < 1 when radius is maxRadius 1319 score -= radius / this.evalVisProp('autopositionmindistance') / 10 - 0.1; 1320 1321 // Calculate angle between current offset and new offset 1322 angleCurrentOffset = Math.atan2(currentOffset[1], currentOffset[0]); 1323 1324 // If angle is negative, add 2*PI to get positive angle 1325 if (angleCurrentOffset < 0) { 1326 angleCurrentOffset += 2 * Math.PI; 1327 } 1328 1329 // Calculate displacement by angle between original label position and new label position, 1330 // use cos to check if angle is on the right side. 1331 // If both angles are on the right side and more than 180° apart, add 2*PI. e.g. 0.1 and 6.1 are near each other 1332 if (co > 0 && Math.cos(angleCurrentOffset) > 0 && Math.abs(angle - angleCurrentOffset) > Math.PI) { 1333 angleDifference = Math.abs(angle - angleCurrentOffset - 2 * Math.PI); 1334 } else { 1335 angleDifference = Math.abs(angle - angleCurrentOffset); 1336 } 1337 1338 // Minimum score is 0 if angle difference is 0, maximum score is pi / 10 1339 score -= angleDifference / 10; 1340 1341 return score; 1342 }, 1343 1344 /** 1345 * Automatically positions the label by finding the optimal position. 1346 * Aims to minimize conflicts while maintaining readability. 1347 * <p> 1348 * The method tests 60 different angles (0 to 2Ï€) at 3 different distances (radii). 1349 * It evaluates each position using calculateScore(radius, angle) and chooses the position with the highest score. 1350 * Then the label's anchor points and offset are adjusted accordingly. 1351 * 1352 * @returns {JXG.Text} Reference to the text object. 1353 */ 1354 setAutoPosition: function() { 1355 var radius, angle, radiusStep, 1356 i, 1357 bestScore = -Infinity, bestRadius, bestAngle, 1358 minRadius = this.evalVisProp('autopositionmindistance'), 1359 maxRadius = this.evalVisProp('autopositionmaxdistance'), 1360 score, 1361 co, si, 1362 currentOffset = this.evalVisProp('offset'), 1363 currentRadius, 1364 currentAngle, 1365 numAngles = 60, 1366 numRadius = 4; 1367 1368 if ( 1369 this === this.board.infobox || 1370 !this.element || 1371 !this.visPropCalc.visible || 1372 !this.evalVisProp('islabel') 1373 ) { 1374 return this; 1375 } 1376 1377 // Calculate current position 1378 currentRadius = Math.sqrt(currentOffset[0] * currentOffset[0] + currentOffset[1] * currentOffset[1]); 1379 currentAngle = Math.atan2(currentOffset[1], currentOffset[0]); 1380 1381 if (this.calculateScore(currentRadius, currentAngle) === 0) { 1382 return this; 1383 } 1384 1385 // Initialize search at min radius 1386 radius = minRadius; 1387 // Calculate step size 1388 radiusStep = (maxRadius - minRadius) / (numRadius - 1); 1389 1390 // Test the different radii 1391 while (maxRadius - radius > -0.01) { 1392 1393 // Radius gets bigger so just check if its smaller than maxnumber of angles. 1394 for (i = 0; i < numAngles; i++) { 1395 1396 // calculate angle 1397 angle = i / numAngles * 2 * Math.PI; 1398 1399 // calculate score 1400 score = this.calculateScore(radius, angle); 1401 1402 // if score is better than bestScore, set bestAngle, bestRadius and bestScore 1403 if (score > bestScore) { 1404 bestAngle = angle; 1405 bestRadius = radius; 1406 bestScore = score; 1407 } 1408 1409 // if bestScore is 0, break, because it can't get better 1410 if (bestScore === 0) { 1411 radius = maxRadius; 1412 break; 1413 } 1414 } 1415 1416 radius += radiusStep; 1417 } 1418 1419 co = Math.cos(bestAngle); 1420 si = Math.sin(bestAngle); 1421 1422 // If label is on the left side of the element, the anchorx is set to "right" 1423 if (co < 0) { 1424 this.visProp.anchorx = 'right'; 1425 } else { 1426 this.visProp.anchorx = 'left'; 1427 } 1428 1429 // If label is on the bottom side of the element, so the anchory is set to "top" 1430 if (si < 0) { 1431 this.visProp.anchory = 'top'; 1432 } else { 1433 this.visProp.anchory = 'bottom'; 1434 } 1435 1436 // Set offset 1437 this.visProp.offset = [bestRadius * co, bestRadius * si]; 1438 1439 return this; 1440 } 1441 1442 // /** 1443 // * Computes the number of overlaps of a box of w pixels width, h pixels height 1444 // * and center (x, y) 1445 // * 1446 // * @private 1447 // * @param {Number} x x-coordinate of the center (screen coordinates) 1448 // * @param {Number} y y-coordinate of the center (screen coordinates) 1449 // * @param {Number} w width of the box in pixel 1450 // * @param {Number} h width of the box in pixel 1451 // * @param {Array} [whiteList] array of ids which should be ignored 1452 // * @return {Number} Number of overlapping elements 1453 // */ 1454 // getNumberOfConflicts: function (x, y, w, h, whiteList) { 1455 // whiteList = whiteList || []; 1456 // var count = 0, 1457 // i, obj, le, 1458 // savePointPrecision, 1459 // saveHasInnerPoints; 1460 1461 // // Set the precision of hasPoint to half the max if label isn't too long 1462 // savePointPrecision = this.board.options.precision.hasPoint; 1463 // // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5; 1464 // this.board.options.precision.hasPoint = (w + h) * 0.25; 1465 // // TODO: 1466 // // Make it compatible with the objects' visProp.precision attribute 1467 // for (i = 0, le = this.board.objectsList.length; i < le; i++) { 1468 // obj = this.board.objectsList[i]; 1469 // saveHasInnerPoints = obj.visProp.hasinnerpoints; 1470 // obj.visProp.hasinnerpoints = false; 1471 // if ( 1472 // obj.visPropCalc.visible && 1473 // obj.elType !== "axis" && 1474 // obj.elType !== "ticks" && 1475 // obj !== this.board.infobox && 1476 // obj !== this && 1477 // obj.hasPoint(x, y) && 1478 // whiteList.indexOf(obj.id) === -1 1479 // ) { 1480 // count++; 1481 // } 1482 // obj.visProp.hasinnerpoints = saveHasInnerPoints; 1483 // } 1484 // this.board.options.precision.hasPoint = savePointPrecision; 1485 1486 // return count; 1487 // }, 1488 1489 // /** 1490 // * Sets the offset of a label element to the position with the least number 1491 // * of overlaps with other elements, while retaining the distance to its 1492 // * anchor element. Twelve different angles are possible. 1493 // * 1494 // * @returns {JXG.Text} Reference to the text object. 1495 // */ 1496 // setAutoPosition: function () { 1497 // var x, y, cx, cy, 1498 // anchorCoords, 1499 // // anchorX, anchorY, 1500 // w = this.size[0], 1501 // h = this.size[1], 1502 // start_angle, angle, 1503 // optimum = { 1504 // conflicts: Infinity, 1505 // angle: 0, 1506 // r: 0 1507 // }, 1508 // max_r, delta_r, 1509 // conflicts, offset, r, 1510 // num_positions = 12, 1511 // step = (2 * Math.PI) / num_positions, 1512 // j, dx, dy, co, si; 1513 1514 // if ( 1515 // this === this.board.infobox || 1516 // !this.visPropCalc.visible || 1517 // !this.evalVisProp('islabel') || 1518 // !this.element 1519 // ) { 1520 // return this; 1521 // } 1522 1523 // // anchorX = this.evalVisProp('anchorx'); 1524 // // anchorY = this.evalVisProp('anchory'); 1525 // offset = this.evalVisProp('offset'); 1526 // anchorCoords = this.element.getLabelAnchor(); 1527 // cx = anchorCoords.scrCoords[1]; 1528 // cy = anchorCoords.scrCoords[2]; 1529 1530 // // Set dx, dy as the relative position of the center of the label 1531 // // to its anchor element ignoring anchorx and anchory. 1532 // dx = offset[0]; 1533 // dy = offset[1]; 1534 1535 // conflicts = this.getNumberOfConflicts(cx + dx, cy - dy, w, h, this.evalVisProp('autopositionwhitelist')); 1536 // if (conflicts === 0) { 1537 // return this; 1538 // } 1539 // // console.log(this.id, conflicts, w, h); 1540 // // r = Geometry.distance([0, 0], offset, 2); 1541 1542 // r = this.evalVisProp('autopositionmindistance'); 1543 // max_r = this.evalVisProp('autopositionmaxdistance'); 1544 // delta_r = 0.2 * r; 1545 1546 // start_angle = Math.atan2(dy, dx); 1547 1548 // optimum.conflicts = conflicts; 1549 // optimum.angle = start_angle; 1550 // optimum.r = r; 1551 1552 // while (optimum.conflicts > 0 && r <= max_r) { 1553 // for ( 1554 // j = 1, angle = start_angle + step; 1555 // j < num_positions && optimum.conflicts > 0; 1556 // j++ 1557 // ) { 1558 // co = Math.cos(angle); 1559 // si = Math.sin(angle); 1560 1561 // x = cx + r * co; 1562 // y = cy - r * si; 1563 1564 // conflicts = this.getNumberOfConflicts(x, y, w, h, this.evalVisProp('autopositionwhitelist')); 1565 // if (conflicts < optimum.conflicts) { 1566 // optimum.conflicts = conflicts; 1567 // optimum.angle = angle; 1568 // optimum.r = r; 1569 // } 1570 // if (optimum.conflicts === 0) { 1571 // break; 1572 // } 1573 // angle += step; 1574 // } 1575 // r += delta_r; 1576 // } 1577 // // console.log(this.id, "after", optimum) 1578 // r = optimum.r; 1579 // co = Math.cos(optimum.angle); 1580 // si = Math.sin(optimum.angle); 1581 // this.visProp.offset = [r * co, r * si]; 1582 1583 // if (co < -0.2) { 1584 // this.visProp.anchorx = 'right' 1585 // } else if (co > 0.2) { 1586 // this.visProp.anchorx = 'left' 1587 // } else { 1588 // this.visProp.anchorx = 'middle' 1589 // } 1590 1591 // return this; 1592 // } 1593 } 1594 ); 1595 1596 /** 1597 * @class Constructs a text element. 1598 * 1599 * The coordinates can either be absolute (i.e. respective to the coordinate system of the board) or be relative to the coordinates of an element 1600 * given in {@link Text#anchor}. 1601 * <p> 1602 * HTML, MathJaX, KaTeX and GEONExT syntax can be handled. 1603 * <p> 1604 * There are two ways to display texts: 1605 * <ul> 1606 * <li> using the text element of the renderer (canvas or svg). In most cases this is the suitable approach if speed matters. 1607 * However, advanced rendering like MathJax, KaTeX or HTML/CSS are not possible. 1608 * <li> using HTML <div>. This is the most flexible approach. The drawback is that HTML can only be display "above" the geometry elements. 1609 * If HTML should be displayed in an inbetween layer, conder to use an element of type {@link ForeignObject} (available in svg renderer, only). 1610 * </ul> 1611 * @pseudo 1612 * @name Text 1613 * @augments JXG.Text 1614 * @constructor 1615 * @type JXG.Text 1616 * 1617 * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements. 1618 * <p> 1619 * Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T 1620 * constraint, or a function which takes no parameter and returns a number. Every parent element beside the last determines one coordinate. 1621 * If a coordinate is 1622 * given by a number, the number determines the initial position of a free text. If given by a string or a function that coordinate will be constrained 1623 * that means the user won't be able to change the texts's position directly by mouse because it will be calculated automatically depending on the string 1624 * or the function's return value. If two parent elements are given the coordinates will be interpreted as 2D affine Euclidean coordinates, if three such 1625 * parent elements are given they will be interpreted as homogeneous coordinates. 1626 * <p> 1627 * The text to display may be given as string or as function returning a string. 1628 * 1629 * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' an HTML division tag is created to display 1630 * the text. In this case it is also possible to use MathJax, KaTeX, or ASCIIMathML. If neither of these is used, basic Math rendering is 1631 * applied. 1632 * <p> 1633 * In case of 'internal', an SVG text element is used to display the text. 1634 * @see JXG.Text 1635 * @example 1636 * // Create a fixed text at position [0,1]. 1637 * var t1 = board.create('text',[0,1,"Hello World"]); 1638 * </pre><div class="jxgbox" id="JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div> 1639 * <script type="text/javascript"> 1640 * var t1_board = JXG.JSXGraph.initBoard('JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1641 * var t1 = t1_board.create('text',[0,1,"Hello World"]); 1642 * </script><pre> 1643 * @example 1644 * // Create a variable text at a variable position. 1645 * var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1646 * var graph = board.create('text', 1647 * [function(x){ return s.Value();}, 1, 1648 * function(){return "The value of s is"+JXG.toFixed(s.Value(), 2);} 1649 * ] 1650 * ); 1651 * </pre><div class="jxgbox" id="JXG5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div> 1652 * <script type="text/javascript"> 1653 * var t2_board = JXG.JSXGraph.initBoard('JXG5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1654 * var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1655 * var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+JXG.toFixed(s.Value(), 2);}]); 1656 * </script><pre> 1657 * @example 1658 * // Create a text bound to the point A 1659 * var p = board.create('point',[0, 1]), 1660 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1661 * 1662 * </pre><div class="jxgbox" id="JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723" style="width: 300px; height: 300px;"></div> 1663 * <script type="text/javascript"> 1664 * (function() { 1665 * var board = JXG.JSXGraph.initBoard('JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723', 1666 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1667 * var p = board.create('point',[0, 1]), 1668 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1669 * 1670 * })(); 1671 * 1672 * </script><pre> 1673 * 1674 */ 1675 JXG.createText = function (board, parents, attributes) { 1676 var t, 1677 attr = Type.copyAttributes(attributes, board.options, 'text'), 1678 coords = parents.slice(0, -1), 1679 content = parents[parents.length - 1]; 1680 1681 // Backwards compatibility 1682 attr.anchor = attr.parent || attr.anchor; 1683 t = CoordsElement.create(JXG.Text, board, coords, attr, content); 1684 1685 if (!t) { 1686 throw new Error( 1687 "JSXGraph: Can't create text with parent types '" + 1688 typeof parents[0] + 1689 "' and '" + 1690 typeof parents[1] + 1691 "'." + 1692 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]" 1693 ); 1694 } 1695 1696 if (attr.rotate !== 0) { 1697 // This is the default value, i.e. no rotation 1698 t.addRotation(attr.rotate); 1699 } 1700 1701 return t; 1702 }; 1703 1704 JXG.registerElement("text", JXG.createText); 1705 1706 /** 1707 * @class Labels are text objects tied to other elements like points, lines and curves. 1708 * Labels are handled internally by JSXGraph, only. There is NO constructor "board.create('label', ...)". 1709 * 1710 * @description 1711 * Labels for points are positioned with the attributes {@link Text#anchorX}, {@link Text#anchorX} and {@link Label#offset}. 1712 * <p> 1713 * Labels for lines, segments, curves and circles can be controlled additionally by the attributes {@link Label#position} and 1714 * {@link Label#distance}, i.e. for a segment [A, B] one could use the follwoing attributes: 1715 * <ul> 1716 * <li> "position": determines, where in the direction of the segment from A to B the label is placed 1717 * <li> "distance": determines the (orthogonal) distance of the label from the line segment. It is a factor which is multiplied by the font-size. 1718 * <li> "offset: [h, v]": a final correction in pixel (horizontally: h, vertically: v) 1719 * <li> "anchorX" ('left', 'middle', 'right') and "anchorY" ('bottom', 'middle', 'top'): determines which part of the 1720 * label string is the anchor position that is positioned to the coordinates determined by "position", "distance" and "offset". 1721 * </ul> 1722 * 1723 * @pseudo 1724 * @name Label 1725 * @augments JXG.Text 1726 * @constructor 1727 * @type JXG.Text 1728 */ 1729 // See element.js#createLabel 1730 1731 /** 1732 * [[x,y], [w px, h px], [range] 1733 */ 1734 JXG.createHTMLSlider = function (board, parents, attributes) { 1735 var t, 1736 par, 1737 attr = Type.copyAttributes(attributes, board.options, 'htmlslider'); 1738 1739 if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) { 1740 throw new Error( 1741 "JSXGraph: Can't create htmlslider with parent types '" + 1742 typeof parents[0] + 1743 "' and '" + 1744 typeof parents[1] + 1745 "'." + 1746 "\nPossible parents are: [[x,y], [min, start, max]]" 1747 ); 1748 } 1749 1750 // Backwards compatibility 1751 attr.anchor = attr.parent || attr.anchor; 1752 attr.fixed = attr.fixed || true; 1753 1754 par = [ 1755 parents[0][0], 1756 parents[0][1], 1757 '<form style="display:inline">' + 1758 '<input type="range" /><span></span><input type="text" />' + 1759 "</form>" 1760 ]; 1761 1762 t = JXG.createText(board, par, attr); 1763 t.type = Type.OBJECT_TYPE_HTMLSLIDER; 1764 1765 t.rendNodeForm = t.rendNode.childNodes[0]; 1766 1767 t.rendNodeRange = t.rendNodeForm.childNodes[0]; 1768 t.rendNodeRange.min = parents[1][0]; 1769 t.rendNodeRange.max = parents[1][2]; 1770 t.rendNodeRange.step = attr.step; 1771 t.rendNodeRange.value = parents[1][1]; 1772 1773 t.rendNodeLabel = t.rendNodeForm.childNodes[1]; 1774 t.rendNodeLabel.id = t.rendNode.id + "_label"; 1775 1776 if (attr.withlabel) { 1777 t.rendNodeLabel.innerText = t.name + "="; 1778 } 1779 1780 t.rendNodeOut = t.rendNodeForm.childNodes[2]; 1781 t.rendNodeOut.value = parents[1][1]; 1782 1783 try { 1784 t.rendNodeForm.id = t.rendNode.id + "_form"; 1785 t.rendNodeRange.id = t.rendNode.id + "_range"; 1786 t.rendNodeOut.id = t.rendNode.id + "_out"; 1787 } catch (e) { 1788 JXG.debug(e); 1789 } 1790 1791 t.rendNodeRange.style.width = attr.widthrange + 'px'; 1792 t.rendNodeRange.style.verticalAlign = 'middle'; 1793 t.rendNodeOut.style.width = attr.widthout + 'px'; 1794 1795 t._val = parents[1][1]; 1796 1797 if (JXG.supportsVML()) { 1798 /* 1799 * OnChange event is used for IE browsers 1800 * The range element is supported since IE10 1801 */ 1802 Env.addEvent(t.rendNodeForm, "change", priv.HTMLSliderInputEventHandler, t); 1803 } else { 1804 /* 1805 * OnInput event is used for non-IE browsers 1806 */ 1807 Env.addEvent(t.rendNodeForm, "input", priv.HTMLSliderInputEventHandler, t); 1808 } 1809 1810 t.Value = function () { 1811 return this._val; 1812 }; 1813 1814 return t; 1815 }; 1816 1817 JXG.registerElement("htmlslider", JXG.createHTMLSlider); 1818 1819 export default JXG.Text; 1820 // export default { 1821 // Text: JXG.Text, 1822 // createText: JXG.createText, 1823 // createHTMLSlider: JXG.createHTMLSlider 1824 // }; 1825