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