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(/&#x(\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. &pi; 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