1 /* 2 Copyright 2008-2025 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 * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode. 452 */ 453 if (ev_d === "html" || this.board.renderer.type === "vml") { 454 if (Type.exists(node.offsetWidth)) { 455 that = this; 456 window.setTimeout(function () { 457 that.size = [node.offsetWidth, node.offsetHeight]; 458 that.needsUpdate = true; 459 that.updateRenderer(); 460 }, 0); 461 // In case, there is non-zero padding or borders 462 // the following approach does not longer work. 463 // s = [node.offsetWidth, node.offsetHeight]; 464 // if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight 465 // that = this; 466 // window.setTimeout(function () { 467 // that.size = [node.offsetWidth, node.offsetHeight]; 468 // that.needsUpdate = true; 469 // that.updateRenderer(); 470 // }, 0); 471 // } else { 472 // this.size = s; 473 // } 474 } else { 475 this.size = this.crudeSizeEstimate(); 476 } 477 } else if (ev_d === "internal") { 478 if (this.board.renderer.type === "svg") { 479 that = this; 480 window.setTimeout(function () { 481 try { 482 tmp = node.getBBox(); 483 that.size = [tmp.width, tmp.height]; 484 that.needsUpdate = true; 485 that.updateRenderer(); 486 } catch (e) {} 487 }, 0); 488 } else if (this.board.renderer.type === "canvas") { 489 this.size = this.crudeSizeEstimate(); 490 } 491 } 492 493 return this; 494 }, 495 496 /** 497 * A very crude estimation of the dimensions of the textbox in case nothing else is available. 498 * @returns {Array} 499 */ 500 crudeSizeEstimate: function () { 501 var ev_fs = parseFloat(this.evalVisProp('fontsize')); 502 return [ev_fs * this.plaintext.length * 0.45, ev_fs * 0.9]; 503 }, 504 505 /** 506 * Decode unicode entities into characters. 507 * @param {String} string 508 * @returns {String} 509 */ 510 utf8_decode: function (string) { 511 return string.replace(/(\w+);/g, function (m, p1) { 512 return String.fromCharCode(parseInt(p1, 16)); 513 }); 514 }, 515 516 /** 517 * Replace _{} by <sub> 518 * @param {String} te String containing _{}. 519 * @returns {String} Given string with _{} replaced by <sub>. 520 */ 521 replaceSub: function (te) { 522 if (!te.indexOf) { 523 return te; 524 } 525 526 var j, 527 i = te.indexOf("_{"); 528 529 // The regexp in here are not used for filtering but to provide some kind of sugar for label creation, 530 // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway. 531 /*jslint regexp: true*/ 532 while (i >= 0) { 533 te = te.slice(0, i) + te.slice(i).replace(/_\{/, "<sub>"); 534 j = te.indexOf("}", i + 4); 535 if (j >= 0) { 536 te = te.slice(0, j) + te.slice(j).replace(/\}/, "</sub>"); 537 } 538 i = te.indexOf("_{"); 539 } 540 541 i = te.indexOf("_"); 542 while (i >= 0) { 543 te = te.slice(0, i) + te.slice(i).replace(/_(.?)/, "<sub>$1</sub>"); 544 i = te.indexOf("_"); 545 } 546 547 return te; 548 }, 549 550 /** 551 * Replace ^{} by <sup> 552 * @param {String} te String containing ^{}. 553 * @returns {String} Given string with ^{} replaced by <sup>. 554 */ 555 replaceSup: function (te) { 556 if (!te.indexOf) { 557 return te; 558 } 559 560 var j, 561 i = te.indexOf("^{"); 562 563 // The regexp in here are not used for filtering but to provide some kind of sugar for label creation, 564 // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway. 565 /*jslint regexp: true*/ 566 while (i >= 0) { 567 te = te.slice(0, i) + te.slice(i).replace(/\^\{/, "<sup>"); 568 j = te.indexOf("}", i + 4); 569 if (j >= 0) { 570 te = te.slice(0, j) + te.slice(j).replace(/\}/, "</sup>"); 571 } 572 i = te.indexOf("^{"); 573 } 574 575 i = te.indexOf("^"); 576 while (i >= 0) { 577 te = te.slice(0, i) + te.slice(i).replace(/\^(.?)/, "<sup>$1</sup>"); 578 i = te.indexOf("^"); 579 } 580 581 return te; 582 }, 583 584 /** 585 * Return the width of the text element. 586 * @returns {Array} [width, height] in pixel 587 */ 588 getSize: function () { 589 return this.size; 590 }, 591 592 /** 593 * Move the text to new coordinates. 594 * @param {number} x 595 * @param {number} y 596 * @returns {object} reference to the text object. 597 */ 598 setCoords: function (x, y) { 599 var coordsAnchor, dx, dy; 600 if (Type.isArray(x) && x.length > 1) { 601 y = x[1]; 602 x = x[0]; 603 } 604 605 if (this.evalVisProp('islabel') && Type.exists(this.element)) { 606 coordsAnchor = this.element.getLabelAnchor(); 607 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX; 608 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY; 609 610 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]); 611 } else { 612 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]); 613 } 614 615 // this should be a local update, otherwise there might be problems 616 // with the tick update routine resulting in orphaned tick labels 617 this.fullUpdate(); 618 619 return this; 620 }, 621 622 /** 623 * Evaluates the text. 624 * Then, the update function of the renderer 625 * is called. 626 */ 627 update: function (fromParent) { 628 if (!this.needsUpdate) { 629 return this; 630 } 631 632 this.updateCoords(fromParent); 633 this.updateText(); 634 635 if (this.evalVisProp('display') === "internal") { 636 if (Type.isString(this.plaintext)) { 637 this.plaintext = this.utf8_decode(this.plaintext); 638 } 639 } 640 641 this.checkForSizeUpdate(); 642 if (this.needsSizeUpdate) { 643 this.updateSize(); 644 } 645 646 return this; 647 }, 648 649 /** 650 * Used to save updateSize() calls. 651 * Called in JXG.Text.update 652 * That means this.update() has been called. 653 * More tests are in JXG.Renderer.updateTextStyle. The latter tests 654 * are one update off. But this should pose not too many problems, since 655 * it affects fontSize and cssClass changes. 656 * 657 * @private 658 */ 659 checkForSizeUpdate: function () { 660 if (this.board.infobox && this.id === this.board.infobox.id) { 661 this.needsSizeUpdate = false; 662 } else { 663 // For some magic reason it is more efficient on the iPad to 664 // call updateSize() for EVERY text element EVERY time. 665 this.needsSizeUpdate = this.plaintextOld !== this.plaintext; 666 667 if (this.needsSizeUpdate) { 668 this.plaintextOld = this.plaintext; 669 } 670 } 671 }, 672 673 /** 674 * The update function of the renderer 675 * is called. 676 * @private 677 */ 678 updateRenderer: function () { 679 if ( 680 //this.board.updateQuality === this.board.BOARD_QUALITY_HIGH && 681 this.evalVisProp('autoposition') 682 ) { 683 this.setAutoPosition().updateConstraint(); 684 } 685 return this.updateRendererGeneric("updateText"); 686 }, 687 688 /** 689 * Converts shortened math syntax into correct syntax: 3x instead of 3*x or 690 * (a+b)(3+1) instead of (a+b)*(3+1). 691 * 692 * @private 693 * @param{String} expr Math term 694 * @returns {string} expanded String 695 */ 696 expandShortMath: function (expr) { 697 var re = /([)0-9.])\s*([(a-zA-Z_])/g; 698 return expr.replace(re, "$1*$2"); 699 }, 700 701 /** 702 * Converts the GEONExT syntax of the <value> terms into JavaScript. 703 * Also, all Objects whose name appears in the term are searched and 704 * the text is added as child to these objects. 705 * This method is called if the attribute parse==true is set. 706 * 707 * Obsolete, replaced by JXG.Text.valueTagToJessieCode 708 * 709 * @param{String} contentStr String to be parsed 710 * @param{Boolean} [expand] Optional flag if shortened math syntax is allowed (e.g. 3x instead of 3*x). 711 * @param{Boolean} [avoidGeonext2JS] Optional flag if geonext2JS should be called. For backwards compatibility 712 * this has to be set explicitly to true. 713 * @param{Boolean} [outputTeX] Optional flag which has to be true if the resulting term will be sent to MathJax or KaTeX. 714 * If true, "_" and "^" are NOT replaced by HTML tags sub and sup. Default: false, i.e. the replacement is done. 715 * This flag allows the combination of <value> tag containing calculations with TeX output. 716 * 717 * @deprecated 718 * @private 719 * @see JXG.GeonextParser#geonext2JS 720 * @see JXG.Text#valueTagToJessieCode 721 * 722 */ 723 generateTerm: function (contentStr, expand, avoidGeonext2JS) { 724 var res, 725 term, 726 i, 727 j, 728 plaintext = '""'; 729 730 // Revert possible jc replacement 731 contentStr = contentStr || ""; 732 contentStr = contentStr.replace(/\r/g, ""); 733 contentStr = contentStr.replace(/\n/g, ""); 734 contentStr = contentStr.replace(/"/g, "'"); 735 contentStr = contentStr.replace(/'/g, "\\'"); 736 737 // Old GEONExT syntax, not (yet) supported as TeX output. 738 // Otherwise, the else clause should be used. 739 // That means, i.e. the <arc> tag and <sqrt> tag are not 740 // converted into TeX syntax. 741 contentStr = contentStr.replace(/&arc;/g, "∠"); 742 contentStr = contentStr.replace(/<arc\s*\/>/g, "∠"); 743 contentStr = contentStr.replace(/<arc\s*\/>/g, "∠"); 744 contentStr = contentStr.replace(/<sqrt\s*\/>/g, "√"); 745 746 contentStr = contentStr.replace(/<value>/g, "<value>"); 747 contentStr = contentStr.replace(/<\/value>/g, "</value>"); 748 749 // Convert GEONExT syntax into JavaScript syntax 750 i = contentStr.indexOf("<value>"); 751 j = contentStr.indexOf("</value>"); 752 if (i >= 0) { 753 while (i >= 0) { 754 plaintext += 755 ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"'; 756 // plaintext += ' + "' + this.replaceSub(contentStr.slice(0, i)) + '"'; 757 758 term = contentStr.slice(i + 7, j); 759 term = term.replace(/\s+/g, ""); // Remove all whitespace 760 if (expand === true) { 761 term = this.expandShortMath(term); 762 } 763 if (avoidGeonext2JS) { 764 res = term; 765 } else { 766 res = GeonextParser.geonext2JS(term, this.board); 767 } 768 res = res.replace(/\\"/g, "'"); 769 res = res.replace(/\\'/g, "'"); 770 771 // GEONExT-Hack: apply rounding once only. 772 if (res.indexOf("toFixed") < 0) { 773 // output of a value tag 774 if ( 775 Type.isNumber( 776 Type.bind(this.board.jc.snippet(res, true, '', false), this)() 777 ) 778 ) { 779 // may also be a string 780 plaintext += '+(' + res + ').toFixed(' + this.evalVisProp('digits') + ')'; 781 } else { 782 plaintext += '+(' + res + ')'; 783 } 784 } else { 785 plaintext += '+(' + res + ')'; 786 } 787 788 contentStr = contentStr.slice(j + 8); 789 i = contentStr.indexOf("<value>"); 790 j = contentStr.indexOf("</value>"); 791 } 792 } 793 794 plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"'; 795 plaintext = this.convertGeonextAndSketchometry2CSS(plaintext); 796 797 // This should replace e.g. π by π 798 plaintext = plaintext.replace(/&/g, "&"); 799 plaintext = plaintext.replace(/"/g, "'"); 800 801 return plaintext; 802 }, 803 804 /** 805 * Replace value-tags in string by JessieCode functions. 806 * @param {String} contentStr 807 * @returns String 808 * @private 809 * @example 810 * "The x-coordinate of A is <value>X(A)</value>" 811 * 812 */ 813 valueTagToJessieCode: function (contentStr) { 814 var res, term, 815 i, j, 816 expandShortMath = true, 817 textComps = [], 818 tick = '"'; 819 820 contentStr = contentStr || ""; 821 contentStr = contentStr.replace(/\r/g, ""); 822 contentStr = contentStr.replace(/\n/g, ""); 823 824 contentStr = contentStr.replace(/<value>/g, "<value>"); 825 contentStr = contentStr.replace(/<\/value>/g, "</value>"); 826 827 // Convert content of value tag (GEONExT/JessieCode) syntax into JavaScript syntax 828 i = contentStr.indexOf("<value>"); 829 j = contentStr.indexOf("</value>"); 830 if (i >= 0) { 831 while (i >= 0) { 832 // Add string fragment before <value> tag 833 textComps.push(tick + this.escapeTicks(contentStr.slice(0, i)) + tick); 834 835 term = contentStr.slice(i + 7, j); 836 term = term.replace(/\s+/g, ""); // Remove all whitespace 837 if (expandShortMath === true) { 838 term = this.expandShortMath(term); 839 } 840 res = term; 841 res = res.replace(/\\"/g, "'").replace(/\\'/g, "'"); 842 843 // // Hack: apply rounding once only. 844 // if (res.indexOf("toFixed") < 0) { 845 // // Output of a value tag 846 // // Run the JessieCode parser 847 // if ( 848 // Type.isNumber( 849 // Type.bind(this.board.jc.snippet(res, true, "", false), this)() 850 // ) 851 // ) { 852 // // Output is number 853 // // textComps.push( 854 // // '(' + res + ').toFixed(' + this.evalVisProp('digits') + ')' 855 // // ); 856 // textComps.push('(' + res + ')'); 857 // } else { 858 // // Output is a string 859 // textComps.push("(" + res + ")"); 860 // } 861 // } else { 862 textComps.push("(" + res + ")"); 863 // } 864 contentStr = contentStr.slice(j + 8); 865 i = contentStr.indexOf("<value>"); 866 j = contentStr.indexOf("</value>"); 867 } 868 } 869 // Add trailing string fragment 870 textComps.push(tick + this.escapeTicks(contentStr) + tick); 871 872 // return textComps.join(" + ").replace(/&/g, "&"); 873 for (i = 0; i < textComps.length; i++) { 874 textComps[i] = textComps[i].replace(/&/g, "&"); 875 } 876 return textComps; 877 }, 878 879 /** 880 * Simple math rendering using HTML / CSS only. In case of array, 881 * handle each entry separately and return array with the 882 * rendering strings. 883 * 884 * @param {String|Array} s 885 * @returns {String|Array} 886 * @see JXG.Text#convertGeonextAndSketchometry2CSS 887 * @private 888 * @see JXG.Text#replaceSub 889 * @see JXG.Text#replaceSup 890 * @see JXG.Text#convertGeonextAndSketchometry2CSS 891 */ 892 poorMansTeX: function (s) { 893 var i, a; 894 if (Type.isArray(s)) { 895 a = []; 896 for (i = 0; i < s.length; i++) { 897 a.push(this.poorMansTeX(s[i])); 898 } 899 return a; 900 } 901 902 s = s 903 .replace(/<arc\s*\/*>/g, "∠") 904 .replace(/<arc\s*\/*>/g, "∠") 905 .replace(/<sqrt\s*\/*>/g, "√") 906 .replace(/<sqrt\s*\/*>/g, "√"); 907 return this.convertGeonextAndSketchometry2CSS(this.replaceSub(this.replaceSup(s)), true); 908 }, 909 910 /** 911 * Replace ticks by URI escape sequences 912 * 913 * @param {String} s 914 * @returns String 915 * @private 916 * 917 */ 918 escapeTicks: function (s) { 919 return s.replace(/"/g, "%22").replace(/'/g, "%27"); 920 }, 921 922 /** 923 * Replace escape sequences for ticks by ticks 924 * 925 * @param {String} s 926 * @returns String 927 * @private 928 */ 929 unescapeTicks: function (s) { 930 return s.replace(/%22/g, '"').replace(/%27/g, "'"); 931 }, 932 933 /** 934 * Converts the GEONExT tags <overline> and <arrow> to 935 * HTML span tags with proper CSS formatting. 936 * @private 937 * @see JXG.Text.poorMansTeX 938 * @see JXG.Text._setText 939 */ 940 convertGeonext2CSS: function (s) { 941 if (Type.isString(s)) { 942 s = s.replace( 943 /(<|<)overline(>|>)/g, 944 "<span style=text-decoration:overline;>" 945 ); 946 s = s.replace(/(<|<)\/overline(>|>)/g, "</span>"); 947 s = s.replace( 948 /(<|<)arrow(>|>)/g, 949 "<span style=text-decoration:overline;>" 950 ); 951 s = s.replace(/(<|<)\/arrow(>|>)/g, "</span>"); 952 } 953 954 return s; 955 }, 956 957 /** 958 * Converts the sketchometry tag <sketchofont> to 959 * HTML span tags with proper CSS formatting. 960 * 961 * @param {String|Function|Number} s Text 962 * @param {Boolean} escape Flag if ticks should be escaped. Escaping is necessary 963 * if s is a text. It has to be avoided if s is a function returning text. 964 * @private 965 * @see JXG.Text._setText 966 * @see JXG.Text.convertGeonextAndSketchometry2CSS 967 * 968 */ 969 convertSketchometry2CSS: function (s, escape) { 970 var t1 = "<span class=\"sketcho sketcho-inherit sketcho-", 971 t2 = "\"></span>"; 972 973 if (Type.isString(s)) { 974 if (escape) { 975 t1 = this.escapeTicks(t1); 976 t2 = this.escapeTicks(t2); 977 } 978 s = s.replace(/(<|<)sketchofont(>|>)/g, t1); 979 s = s.replace(/(<|<)\/sketchofont(>|>)/g, t2); 980 } 981 982 return s; 983 }, 984 985 /** 986 * Alias for convertGeonext2CSS and convertSketchometry2CSS 987 * 988 * @param {String|Function|Number} s Text 989 * @param {Boolean} escape Flag if ticks should be escaped 990 * @private 991 * @see JXG.Text.convertGeonext2CSS 992 * @see JXG.Text.convertSketchometry2CSS 993 */ 994 convertGeonextAndSketchometry2CSS: function (s, escape) { 995 s = this.convertGeonext2CSS(s); 996 s = this.convertSketchometry2CSS(s, escape); 997 return s; 998 }, 999 1000 /** 1001 * Finds dependencies in a given term and notifies the parents by adding the 1002 * dependent object to the found objects child elements. 1003 * @param {String} content String containing dependencies for the given object. 1004 * @private 1005 */ 1006 notifyParents: function (content) { 1007 var search, 1008 res = null; 1009 1010 // revert possible jc replacement 1011 content = content.replace(/<value>/g, "<value>"); 1012 content = content.replace(/<\/value>/g, "</value>"); 1013 1014 do { 1015 search = /<value>([\w\s*/^\-+()[\],<>=!]+)<\/value>/; 1016 res = search.exec(content); 1017 1018 if (res !== null) { 1019 GeonextParser.findDependencies(this, res[1], this.board); 1020 content = content.slice(res.index); 1021 content = content.replace(search, ""); 1022 } 1023 } while (res !== null); 1024 1025 return this; 1026 }, 1027 1028 // documented in element.js 1029 getParents: function () { 1030 var p; 1031 if (this.relativeCoords !== undefined) { 1032 // Texts with anchor elements, excluding labels 1033 p = [ 1034 this.relativeCoords.usrCoords[1], 1035 this.relativeCoords.usrCoords[2], 1036 this.orgText 1037 ]; 1038 } else { 1039 // Other texts 1040 p = [this.Z(), this.X(), this.Y(), this.orgText]; 1041 } 1042 1043 if (this.parents.length !== 0) { 1044 p = this.parents; 1045 } 1046 1047 return p; 1048 }, 1049 1050 /** 1051 * Returns the bounding box of the text element in user coordinates as an 1052 * array of length 4: [upper left x, upper left y, lower right x, lower right y]. 1053 * The method assumes that the lower left corner is at position [el.X(), el.Y()] 1054 * of the text element el, i.e. the attributes anchorX, anchorY are ignored. 1055 * 1056 * <p> 1057 * <strong>Attention:</strong> for labels, [0, 0, 0, 0] is returned. 1058 * 1059 * @returns Array 1060 */ 1061 bounds: function () { 1062 var c = this.coords.usrCoords; 1063 1064 if ( 1065 this.evalVisProp('islabel') || 1066 this.board.unitY === 0 || 1067 this.board.unitX === 0 1068 ) { 1069 return [0, 0, 0, 0]; 1070 } 1071 return [ 1072 c[1], 1073 c[2] + this.size[1] / this.board.unitY, 1074 c[1] + this.size[0] / this.board.unitX, 1075 c[2] 1076 ]; 1077 }, 1078 1079 /** 1080 * Returns the value of the attribute "anchorX". If this equals "auto", 1081 * returns "left", "middle", or "right", depending on the 1082 * value of the attribute "position". 1083 * @returns String 1084 */ 1085 getAnchorX: function () { 1086 var a = this.evalVisProp('anchorx'); 1087 if (a === "auto") { 1088 switch (this.visProp.position) { 1089 case "top": 1090 case "bot": 1091 return "middle"; 1092 case "rt": 1093 case "lrt": 1094 case "urt": 1095 return "left"; 1096 case "lft": 1097 case "llft": 1098 case "ulft": 1099 default: 1100 return "right"; 1101 } 1102 } 1103 return a; 1104 }, 1105 1106 /** 1107 * Returns the value of the attribute "anchorY". If this equals "auto", 1108 * returns "bottom", "middle", or "top", depending on the 1109 * value of the attribute "position". 1110 * @returns String 1111 */ 1112 getAnchorY: function () { 1113 var a = this.evalVisProp('anchory'); 1114 if (a === "auto") { 1115 switch (this.visProp.position) { 1116 case "top": 1117 case "ulft": 1118 case "urt": 1119 return "bottom"; 1120 case "bot": 1121 case "lrt": 1122 case "llft": 1123 return "top"; 1124 case "rt": 1125 case "lft": 1126 default: 1127 return "middle"; 1128 } 1129 } 1130 return a; 1131 }, 1132 1133 /** 1134 * Computes the number of overlaps of a box of w pixels width, h pixels height 1135 * and center (x, y) 1136 * 1137 * An overlap occurs when either: 1138 * <ol> 1139 * <li> For labels/points: Their bounding boxes intersect 1140 * <li> For other objects: The object contains the center point of the box 1141 * </ol> 1142 * 1143 * @private 1144 * @param {Number} x x-coordinate of the center (screen coordinates) 1145 * @param {Number} y y-coordinate of the center (screen coordinates) 1146 * @param {Number} w width of the box in pixel 1147 * @param {Number} h width of the box in pixel 1148 * @param {Array} [whiteList] array of ids which should be ignored 1149 * @return {Number} Number of overlapping elements 1150 */ 1151 getNumberOfConflicts: function(x, y, w, h, whiteList) { 1152 whiteList = whiteList || []; 1153 var count = 0, 1154 i, obj, 1155 coords, 1156 saveHasInnerPoints, 1157 savePointPrecision = this.board.options.precision.hasPoint, 1158 objCenterX, objCenterY, 1159 objWidth, objHeight; 1160 1161 // set a new precision for hasPoint 1162 // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5; 1163 this.board.options.precision.hasPoint = (w + h) * 0.3; 1164 1165 // loop over all objects 1166 for (i = 0; i < this.board.objectsList.length; i++) { 1167 obj = this.board.objectsList[i]; 1168 1169 //Skip the object if it is not meant to influence label position 1170 if ( 1171 obj.visPropCalc.visible && 1172 obj !== this && 1173 whiteList.indexOf(obj.id) === -1 && 1174 obj.evalVisProp("ignoreforlabelautoposition") !== true 1175 ) { 1176 // Save hasinnerpoints and temporarily disable to handle polygon areas 1177 saveHasInnerPoints = obj.visProp.hasinnerpoints; 1178 obj.visProp.hasinnerpoints = false; 1179 1180 // If is label or point use other conflict detection 1181 if ( 1182 obj.visProp.islabel || 1183 obj.elementClass === Const.OBJECT_CLASS_POINT 1184 ) { 1185 // get coords and size of the object 1186 coords = obj.coords.scrCoords; 1187 objCenterX = coords[1]; 1188 objCenterY = coords[2]; 1189 objWidth = obj.size[0]; 1190 objHeight = obj.size[1]; 1191 1192 // move coords to the center of the label 1193 if (obj.visProp.islabel) { 1194 // Vertical adjustment 1195 if (obj.visProp.anchory === 'top') { 1196 objCenterY = objCenterY + objHeight / 2; 1197 } else { 1198 objCenterY = objCenterY - objHeight / 2; 1199 } 1200 1201 // Horizontal adjustment 1202 if (obj.visProp.anchorx === 'left') { 1203 objCenterX = objCenterX + objWidth / 2; 1204 } else { 1205 objCenterX = objCenterX - objWidth / 2; 1206 } 1207 } else { 1208 // Points are treated dimensionless 1209 objWidth = 0; 1210 objHeight = 0; 1211 } 1212 1213 // Check for overlap 1214 if ( 1215 Math.abs(objCenterX - x) < (w + objWidth) / 2 && 1216 Math.abs(objCenterY - y) < (h + objHeight) / 2 1217 ) { 1218 count++; 1219 } 1220 1221 //if not label or point check conflict with hasPoint 1222 } else if (obj.hasPoint(x, y)) { 1223 count++; 1224 } 1225 1226 // Restore original hasinnerpoints 1227 obj.visProp.hasinnerpoints = saveHasInnerPoints; 1228 } 1229 } 1230 1231 // Restore original precision 1232 this.board.options.precision.hasPoint = savePointPrecision; 1233 1234 return count; 1235 }, 1236 /** 1237 * Calculates the score of a label position with a given radius and angle. The score is calculated by the following rules: 1238 * <ul> 1239 * <li> the maximum score is 0 1240 * <li> if the label is outside of the bounding box, the score is reduced by 1 1241 * <li> for each conflict, the score is reduced by 1 1242 * <li> the score is reduced by the displacement (angle difference between old and new position) of the label 1243 * <li> the score is reduced by the angle between the original label position and the new label position 1244 * </ul> 1245 * 1246 * @param {number} radius radius in pixels 1247 * @param {number} angle angle in radians 1248 * @returns {number} Position score, higher values indicate better positions 1249 */ 1250 calculateScore: function(radius, angle) { 1251 var x, y, co, si, angleCurrentOffset, angleDifference, 1252 score = 0, 1253 cornerPoint = [0,0], 1254 w = this.getSize()[0], 1255 h = this.getSize()[1], 1256 anchorCoords, 1257 currentOffset = this.evalVisProp("offset"), 1258 boundingBox = this.board.getBoundingBox(); 1259 1260 if (this.evalVisProp('islabel') && Type.exists(this.element)) { 1261 anchorCoords = this.element.getLabelAnchor().scrCoords; 1262 } else { 1263 return 0; 1264 } 1265 co = Math.cos(angle); 1266 si = Math.sin(angle); 1267 1268 // calculate new position with srccoords, radius and angle 1269 x = anchorCoords[1] + radius * co; 1270 y = anchorCoords[2] - radius * si; 1271 1272 // if the label was placed on the left side of the element, the anchorx is set to "right" 1273 if (co < 0) { 1274 cornerPoint[0] = x - w; 1275 x -= w / 2; 1276 } else { 1277 cornerPoint[0] = x + w; 1278 x += w / 2; 1279 } 1280 1281 // If the label was placed on the bottom side of the element, so the anchory is set to "top" 1282 if (si < 0) { 1283 cornerPoint[1] = y + h; 1284 y += h / 2; 1285 } else { 1286 cornerPoint[1] = y - h; 1287 y -= h / 2; 1288 } 1289 1290 // If label was not in bounding box, score is reduced by 1 1291 if( 1292 cornerPoint[0] < 0 || 1293 cornerPoint[0] > (boundingBox[2] - boundingBox[0]) * this.board.unitX || 1294 cornerPoint[1] < 0 || 1295 cornerPoint[1] > (boundingBox[1] - boundingBox[3]) * this.board.unitY 1296 ) { 1297 score -= 1; 1298 } 1299 1300 // Per conflict, score is reduced by 1 1301 score -= this.getNumberOfConflicts(x, y, w, h, Type.evaluate(this.visProp.autopositionwhitelist)); 1302 1303 // Calculate displacement, minimum score is 0 if radius is minRadius, maximum score is < 1 when radius is maxRadius 1304 score -= radius / this.evalVisProp("autopositionmindistance") / 10 - 0.1; 1305 1306 // Calculate angle between current offset and new offset 1307 angleCurrentOffset = Math.atan2(currentOffset[1], currentOffset[0]); 1308 1309 // If angle is negative, add 2*PI to get positive angle 1310 if (angleCurrentOffset < 0) { 1311 angleCurrentOffset += 2 * Math.PI; 1312 } 1313 1314 // Calculate displacement by angle between original label position and new label position, 1315 // use cos to check if angle is on the right side. 1316 // 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 1317 if (co > 0 && Math.cos(angleCurrentOffset) > 0 && Math.abs(angle - angleCurrentOffset) > Math.PI) { 1318 angleDifference = Math.abs(angle - angleCurrentOffset - 2 * Math.PI); 1319 } else { 1320 angleDifference = Math.abs(angle - angleCurrentOffset); 1321 } 1322 1323 // Minimum score is 0 if angle difference is 0, maximum score is pi / 10 1324 score -= angleDifference / 10; 1325 1326 return score; 1327 }, 1328 1329 /** 1330 * Automatically positions the label by finding the optimal position. 1331 * Aims to minimize conflicts while maintaining readability. 1332 * <p> 1333 * The method tests 60 different angles (0 to 2Ï€) at 3 different distances (radii). 1334 * It evaluates each position using calculateScore(radius, angle) and chooses the position with the highest score. 1335 * Then the label's anchor points and offset are adjusted accordingly. 1336 * 1337 * @returns {JXG.Text} Reference to the text object. 1338 */ 1339 setAutoPosition: function() { 1340 var radius, angle, radiusStep, 1341 i, 1342 bestScore = -Infinity, bestRadius, bestAngle, 1343 minRadius = this.evalVisProp("autopositionmindistance"), 1344 maxRadius = this.evalVisProp("autopositionmaxdistance"), 1345 score, 1346 co, si, 1347 currentOffset = this.evalVisProp("offset"), 1348 currentRadius, 1349 currentAngle, 1350 numAngles = 60, 1351 numRadius = 4; 1352 1353 if ( 1354 this === this.board.infobox || 1355 !this.element || 1356 !this.visPropCalc.visible || 1357 !this.evalVisProp('islabel') 1358 ) { 1359 return this; 1360 } 1361 1362 // Calculate current position 1363 currentRadius = Math.sqrt(currentOffset[0] * currentOffset[0] + currentOffset[1] * currentOffset[1]); 1364 currentAngle = Math.atan2(currentOffset[1], currentOffset[0]); 1365 1366 if (this.calculateScore(currentRadius, currentAngle) === 0) { 1367 return this; 1368 } 1369 1370 // Initialize search at min radius 1371 radius = minRadius; 1372 // Calculate step size 1373 radiusStep = (maxRadius - minRadius) / (numRadius - 1); 1374 1375 // Test the different radii 1376 while (maxRadius - radius > -0.01) { 1377 1378 // Radius gets bigger so just check if its smaller than maxnumber of angles. 1379 for (i = 0; i < numAngles; i++) { 1380 1381 // calculate angle 1382 angle = i / numAngles * 2 * Math.PI; 1383 1384 // calculate score 1385 score = this.calculateScore(radius, angle); 1386 1387 // if score is better than bestScore, set bestAngle, bestRadius and bestScore 1388 if (score > bestScore) { 1389 bestAngle = angle; 1390 bestRadius = radius; 1391 bestScore = score; 1392 } 1393 1394 // if bestScore is 0, break, because it can't get better 1395 if (bestScore === 0) { 1396 radius = maxRadius; 1397 break; 1398 } 1399 } 1400 1401 radius += radiusStep; 1402 } 1403 1404 co = Math.cos(bestAngle); 1405 si = Math.sin(bestAngle); 1406 1407 // If label is on the left side of the element, the anchorx is set to "right" 1408 if (co < 0) { 1409 this.visProp.anchorx = "right"; 1410 } else { 1411 this.visProp.anchorx = "left"; 1412 } 1413 1414 // If label is on the bottom side of the element, so the anchory is set to "top" 1415 if (si < 0) { 1416 this.visProp.anchory = "top"; 1417 } else { 1418 this.visProp.anchory = "bottom"; 1419 } 1420 1421 // Set offset 1422 this.visProp.offset = [bestRadius * co, bestRadius * si]; 1423 1424 return this; 1425 } 1426 1427 // /** 1428 // * Computes the number of overlaps of a box of w pixels width, h pixels height 1429 // * and center (x, y) 1430 // * 1431 // * @private 1432 // * @param {Number} x x-coordinate of the center (screen coordinates) 1433 // * @param {Number} y y-coordinate of the center (screen coordinates) 1434 // * @param {Number} w width of the box in pixel 1435 // * @param {Number} h width of the box in pixel 1436 // * @param {Array} [whiteList] array of ids which should be ignored 1437 // * @return {Number} Number of overlapping elements 1438 // */ 1439 // getNumberOfConflicts: function (x, y, w, h, whiteList) { 1440 // whiteList = whiteList || []; 1441 // var count = 0, 1442 // i, obj, le, 1443 // savePointPrecision, 1444 // saveHasInnerPoints; 1445 1446 // // Set the precision of hasPoint to half the max if label isn't too long 1447 // savePointPrecision = this.board.options.precision.hasPoint; 1448 // // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5; 1449 // this.board.options.precision.hasPoint = (w + h) * 0.25; 1450 // // TODO: 1451 // // Make it compatible with the objects' visProp.precision attribute 1452 // for (i = 0, le = this.board.objectsList.length; i < le; i++) { 1453 // obj = this.board.objectsList[i]; 1454 // saveHasInnerPoints = obj.visProp.hasinnerpoints; 1455 // obj.visProp.hasinnerpoints = false; 1456 // if ( 1457 // obj.visPropCalc.visible && 1458 // obj.elType !== "axis" && 1459 // obj.elType !== "ticks" && 1460 // obj !== this.board.infobox && 1461 // obj !== this && 1462 // obj.hasPoint(x, y) && 1463 // whiteList.indexOf(obj.id) === -1 1464 // ) { 1465 // count++; 1466 // } 1467 // obj.visProp.hasinnerpoints = saveHasInnerPoints; 1468 // } 1469 // this.board.options.precision.hasPoint = savePointPrecision; 1470 1471 // return count; 1472 // }, 1473 1474 // /** 1475 // * Sets the offset of a label element to the position with the least number 1476 // * of overlaps with other elements, while retaining the distance to its 1477 // * anchor element. Twelve different angles are possible. 1478 // * 1479 // * @returns {JXG.Text} Reference to the text object. 1480 // */ 1481 // setAutoPosition: function () { 1482 // var x, y, cx, cy, 1483 // anchorCoords, 1484 // // anchorX, anchorY, 1485 // w = this.size[0], 1486 // h = this.size[1], 1487 // start_angle, angle, 1488 // optimum = { 1489 // conflicts: Infinity, 1490 // angle: 0, 1491 // r: 0 1492 // }, 1493 // max_r, delta_r, 1494 // conflicts, offset, r, 1495 // num_positions = 12, 1496 // step = (2 * Math.PI) / num_positions, 1497 // j, dx, dy, co, si; 1498 1499 // if ( 1500 // this === this.board.infobox || 1501 // !this.visPropCalc.visible || 1502 // !this.evalVisProp('islabel') || 1503 // !this.element 1504 // ) { 1505 // return this; 1506 // } 1507 1508 // // anchorX = this.evalVisProp('anchorx'); 1509 // // anchorY = this.evalVisProp('anchory'); 1510 // offset = this.evalVisProp('offset'); 1511 // anchorCoords = this.element.getLabelAnchor(); 1512 // cx = anchorCoords.scrCoords[1]; 1513 // cy = anchorCoords.scrCoords[2]; 1514 1515 // // Set dx, dy as the relative position of the center of the label 1516 // // to its anchor element ignoring anchorx and anchory. 1517 // dx = offset[0]; 1518 // dy = offset[1]; 1519 1520 // conflicts = this.getNumberOfConflicts(cx + dx, cy - dy, w, h, this.evalVisProp('autopositionwhitelist')); 1521 // if (conflicts === 0) { 1522 // return this; 1523 // } 1524 // // console.log(this.id, conflicts, w, h); 1525 // // r = Geometry.distance([0, 0], offset, 2); 1526 1527 // r = this.evalVisProp('autopositionmindistance'); 1528 // max_r = this.evalVisProp('autopositionmaxdistance'); 1529 // delta_r = 0.2 * r; 1530 1531 // start_angle = Math.atan2(dy, dx); 1532 1533 // optimum.conflicts = conflicts; 1534 // optimum.angle = start_angle; 1535 // optimum.r = r; 1536 1537 // while (optimum.conflicts > 0 && r <= max_r) { 1538 // for ( 1539 // j = 1, angle = start_angle + step; 1540 // j < num_positions && optimum.conflicts > 0; 1541 // j++ 1542 // ) { 1543 // co = Math.cos(angle); 1544 // si = Math.sin(angle); 1545 1546 // x = cx + r * co; 1547 // y = cy - r * si; 1548 1549 // conflicts = this.getNumberOfConflicts(x, y, w, h, this.evalVisProp('autopositionwhitelist')); 1550 // if (conflicts < optimum.conflicts) { 1551 // optimum.conflicts = conflicts; 1552 // optimum.angle = angle; 1553 // optimum.r = r; 1554 // } 1555 // if (optimum.conflicts === 0) { 1556 // break; 1557 // } 1558 // angle += step; 1559 // } 1560 // r += delta_r; 1561 // } 1562 // // console.log(this.id, "after", optimum) 1563 // r = optimum.r; 1564 // co = Math.cos(optimum.angle); 1565 // si = Math.sin(optimum.angle); 1566 // this.visProp.offset = [r * co, r * si]; 1567 1568 // if (co < -0.2) { 1569 // this.visProp.anchorx = "right"; 1570 // } else if (co > 0.2) { 1571 // this.visProp.anchorx = "left"; 1572 // } else { 1573 // this.visProp.anchorx = "middle"; 1574 // } 1575 1576 // return this; 1577 // } 1578 } 1579 ); 1580 1581 /** 1582 * @class Constructs a text element. 1583 * 1584 * 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 1585 * given in {@link Text#anchor}. 1586 * <p> 1587 * HTML, MathJaX, KaTeX and GEONExT syntax can be handled. 1588 * <p> 1589 * There are two ways to display texts: 1590 * <ul> 1591 * <li> using the text element of the renderer (canvas or svg). In most cases this is the suitable approach if speed matters. 1592 * However, advanced rendering like MathJax, KaTeX or HTML/CSS are not possible. 1593 * <li> using HTML <div>. This is the most flexible approach. The drawback is that HTML can only be display "above" the geometry elements. 1594 * If HTML should be displayed in an inbetween layer, conder to use an element of type {@link ForeignObject} (available in svg renderer, only). 1595 * </ul> 1596 * @pseudo 1597 * @name Text 1598 * @augments JXG.Text 1599 * @constructor 1600 * @type JXG.Text 1601 * 1602 * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements. 1603 * <p> 1604 * Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T 1605 * constraint, or a function which takes no parameter and returns a number. Every parent element beside the last determines one coordinate. 1606 * If a coordinate is 1607 * 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 1608 * 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 1609 * 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 1610 * parent elements are given they will be interpreted as homogeneous coordinates. 1611 * <p> 1612 * The text to display may be given as string or as function returning a string. 1613 * 1614 * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' an HTML division tag is created to display 1615 * 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 1616 * applied. 1617 * <p> 1618 * In case of 'internal', an SVG text element is used to display the text. 1619 * @see JXG.Text 1620 * @example 1621 * // Create a fixed text at position [0,1]. 1622 * var t1 = board.create('text',[0,1,"Hello World"]); 1623 * </pre><div class="jxgbox" id="JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div> 1624 * <script type="text/javascript"> 1625 * var t1_board = JXG.JSXGraph.initBoard('JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1626 * var t1 = t1_board.create('text',[0,1,"Hello World"]); 1627 * </script><pre> 1628 * @example 1629 * // Create a variable text at a variable position. 1630 * var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1631 * var graph = board.create('text', 1632 * [function(x){ return s.Value();}, 1, 1633 * function(){return "The value of s is"+JXG.toFixed(s.Value(), 2);} 1634 * ] 1635 * ); 1636 * </pre><div class="jxgbox" id="JXG5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div> 1637 * <script type="text/javascript"> 1638 * var t2_board = JXG.JSXGraph.initBoard('JXG5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false}); 1639 * var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]); 1640 * var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+JXG.toFixed(s.Value(), 2);}]); 1641 * </script><pre> 1642 * @example 1643 * // Create a text bound to the point A 1644 * var p = board.create('point',[0, 1]), 1645 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1646 * 1647 * </pre><div class="jxgbox" id="JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723" style="width: 300px; height: 300px;"></div> 1648 * <script type="text/javascript"> 1649 * (function() { 1650 * var board = JXG.JSXGraph.initBoard('JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723', 1651 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1652 * var p = board.create('point',[0, 1]), 1653 * t = board.create('text',[0, -1,"Hello World"], {anchor: p}); 1654 * 1655 * })(); 1656 * 1657 * </script><pre> 1658 * 1659 */ 1660 JXG.createText = function (board, parents, attributes) { 1661 var t, 1662 attr = Type.copyAttributes(attributes, board.options, "text"), 1663 coords = parents.slice(0, -1), 1664 content = parents[parents.length - 1]; 1665 1666 // Backwards compatibility 1667 attr.anchor = attr.parent || attr.anchor; 1668 t = CoordsElement.create(JXG.Text, board, coords, attr, content); 1669 1670 if (!t) { 1671 throw new Error( 1672 "JSXGraph: Can't create text with parent types '" + 1673 typeof parents[0] + 1674 "' and '" + 1675 typeof parents[1] + 1676 "'." + 1677 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]" 1678 ); 1679 } 1680 1681 if (attr.rotate !== 0) { 1682 // This is the default value, i.e. no rotation 1683 t.addRotation(attr.rotate); 1684 } 1685 1686 return t; 1687 }; 1688 1689 JXG.registerElement("text", JXG.createText); 1690 1691 /** 1692 * @class Labels are text objects tied to other elements like points, lines and curves. 1693 * Labels are handled internally by JSXGraph, only. There is NO constructor "board.create('label', ...)". 1694 * 1695 * @description 1696 * Labels for points are positioned with the attributes {@link Text#anchorX}, {@link Text#anchorX} and {@link Label#offset}. 1697 * <p> 1698 * Labels for lines, segments, curves and circles can be controlled additionally by the attributes {@link Label#position} and 1699 * {@link Label#distance}, i.e. for a segment [A, B] one could use the follwoing attributes: 1700 * <ul> 1701 * <li> "position": determines, where in the direction of the segment from A to B the label is placed 1702 * <li> "distance": determines the (orthogonal) distance of the label from the line segment. It is a factor which is multiplied by the font-size. 1703 * <li> "offset: [h, v]": a final correction in pixel (horizontally: h, vertically: v) 1704 * <li> "anchorX" ('left', 'middle', 'right') and "anchorY" ('bottom', 'middle', 'top'): determines which part of the 1705 * label string is the anchor position that is positioned to the coordinates determined by "position", "distance" and "offset". 1706 * </ul> 1707 * 1708 * @pseudo 1709 * @name Label 1710 * @augments JXG.Text 1711 * @constructor 1712 * @type JXG.Text 1713 */ 1714 // See element.js#createLabel 1715 1716 /** 1717 * [[x,y], [w px, h px], [range] 1718 */ 1719 JXG.createHTMLSlider = function (board, parents, attributes) { 1720 var t, 1721 par, 1722 attr = Type.copyAttributes(attributes, board.options, "htmlslider"); 1723 1724 if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) { 1725 throw new Error( 1726 "JSXGraph: Can't create htmlslider with parent types '" + 1727 typeof parents[0] + 1728 "' and '" + 1729 typeof parents[1] + 1730 "'." + 1731 "\nPossible parents are: [[x,y], [min, start, max]]" 1732 ); 1733 } 1734 1735 // Backwards compatibility 1736 attr.anchor = attr.parent || attr.anchor; 1737 attr.fixed = attr.fixed || true; 1738 1739 par = [ 1740 parents[0][0], 1741 parents[0][1], 1742 '<form style="display:inline">' + 1743 '<input type="range" /><span></span><input type="text" />' + 1744 "</form>" 1745 ]; 1746 1747 t = JXG.createText(board, par, attr); 1748 t.type = Type.OBJECT_TYPE_HTMLSLIDER; 1749 1750 t.rendNodeForm = t.rendNode.childNodes[0]; 1751 1752 t.rendNodeRange = t.rendNodeForm.childNodes[0]; 1753 t.rendNodeRange.min = parents[1][0]; 1754 t.rendNodeRange.max = parents[1][2]; 1755 t.rendNodeRange.step = attr.step; 1756 t.rendNodeRange.value = parents[1][1]; 1757 1758 t.rendNodeLabel = t.rendNodeForm.childNodes[1]; 1759 t.rendNodeLabel.id = t.rendNode.id + "_label"; 1760 1761 if (attr.withlabel) { 1762 t.rendNodeLabel.innerHTML = t.name + "="; 1763 } 1764 1765 t.rendNodeOut = t.rendNodeForm.childNodes[2]; 1766 t.rendNodeOut.value = parents[1][1]; 1767 1768 try { 1769 t.rendNodeForm.id = t.rendNode.id + "_form"; 1770 t.rendNodeRange.id = t.rendNode.id + "_range"; 1771 t.rendNodeOut.id = t.rendNode.id + "_out"; 1772 } catch (e) { 1773 JXG.debug(e); 1774 } 1775 1776 t.rendNodeRange.style.width = attr.widthrange + "px"; 1777 t.rendNodeRange.style.verticalAlign = "middle"; 1778 t.rendNodeOut.style.width = attr.widthout + "px"; 1779 1780 t._val = parents[1][1]; 1781 1782 if (JXG.supportsVML()) { 1783 /* 1784 * OnChange event is used for IE browsers 1785 * The range element is supported since IE10 1786 */ 1787 Env.addEvent(t.rendNodeForm, "change", priv.HTMLSliderInputEventHandler, t); 1788 } else { 1789 /* 1790 * OnInput event is used for non-IE browsers 1791 */ 1792 Env.addEvent(t.rendNodeForm, "input", priv.HTMLSliderInputEventHandler, t); 1793 } 1794 1795 t.Value = function () { 1796 return this._val; 1797 }; 1798 1799 return t; 1800 }; 1801 1802 JXG.registerElement("htmlslider", JXG.createHTMLSlider); 1803 1804 export default JXG.Text; 1805 // export default { 1806 // Text: JXG.Text, 1807 // createText: JXG.createText, 1808 // createHTMLSlider: JXG.createHTMLSlider 1809 // }; 1810