      1 /*
      2     Copyright 2008-2022
      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 <http://www.gnu.org/licenses/>
     29     and <http://opensource.org/licenses/MIT/>.
     30  */
     31
     32 /*global JXG: true, define: true, window: true*/
     33 /*jslint nomen: true, plusplus: true*/
     34
     35 /* depends:
     36  jxg
     37  base/constants
     38  base/coords
     39  base/element
     40  parser/geonext
     41  math/statistics
     42  utils/env
     43  utils/type
     44  */
     45
     46 /**
     47  * @fileoverview In this file the Text element is defined.
     48  */
     49
     50 define([
     51     'jxg', 'base/constants', 'base/element', 'parser/geonext',
     52     'utils/env', 'utils/type', 'math/math', 'base/coordselement'
     53 ], function (JXG, Const, GeometryElement, GeonextParser, Env, Type, Mat, CoordsElement) {
     54
     55     "use strict";
     56
     57     var priv = {
     58         HTMLSliderInputEventHandler: function () {
     59             this._val = parseFloat(this.rendNodeRange.value);
     60             this.rendNodeOut.value = this.rendNodeRange.value;
     61             this.board.update();
     62         }
     63     };
     64
     65     /**
     66      * Construct and handle texts.
     67      *
     68      * The coordinates can be relative to the coordinates of an element
     69      * given in {@link JXG.Options#text.anchor}.
     70      *
     71      * MathJax, HTML and GEONExT syntax can be handled.
     72      * @class Creates a new text object. Do not use this constructor to create a text. Use {@link JXG.Board#create} with
     73      * type {@link Text} instead.
     74      * @augments JXG.GeometryElement
     75      * @augments JXG.CoordsElement
     76      * @param {string|JXG.Board} board The board the new text is drawn on.
     77      * @param {Array} coordinates An array with the user coordinates of the text.
     78      * @param {Object} attributes An object containing visual properties and optional a name and a id.
     79      * @param {string|function} content A string or a function returning a string.
     80      *
     81      */
     82     JXG.Text = function (board, coords, attributes, content) {
     83         this.constructor(board, attributes, Const.OBJECT_TYPE_TEXT, Const.OBJECT_CLASS_TEXT);
     84
     85         this.element = this.board.select(attributes.anchor);
     86         this.coordsConstructor(coords, Type.evaluate(this.visProp.islabel));
     87
     88         this.content = '';
     89         this.plaintext = '';
     90         this.plaintextOld = null;
     91         this.orgText = '';
     92
     93         this.needsSizeUpdate = false;
     94         // Only used by infobox anymore
     95         this.hiddenByParent = false;
     96
     97         /**
     98          * Width and height of the the text element in pixel.
     99          *
    100          * @private
    101          * @type Array
    102          */
    103         this.size = [1.0, 1.0];
    104         this.id = this.board.setId(this, 'T');
    105
    106         this.board.renderer.drawText(this);
    107         this.board.finalizeAdding(this);
    108
    109         // Set text before drawing
    110         // this._createFctUpdateText(content);
    111         // this.updateText();
    112
    113         this.setText(content);
    114
    115         if (Type.isString(this.content)) {
    116             this.notifyParents(this.content);
    117         }
    118         this.elType = 'text';
    119
    120         this.methodMap = Type.deepCopy(this.methodMap, {
    121             setText: 'setTextJessieCode',
    122             // free: 'free',
    123             move: 'setCoords'
    124         });
    125     };
    126
    127     JXG.Text.prototype = new GeometryElement();
    128     Type.copyPrototypeMethods(JXG.Text, CoordsElement, 'coordsConstructor');
    129
    130     JXG.extend(JXG.Text.prototype, /** @lends JXG.Text.prototype */ {
    131         /**
    132          * @private
    133          * Test if the the screen coordinates (x,y) are in a small stripe
    134          * at the left side or at the right side of the text.
    135          * Sensitivity is set in this.board.options.precision.hasPoint.
    136          * If dragarea is set to 'all' (default), tests if the the screen
    137          * coordinates (x,y) are in within the text boundary.
    138          * @param {Number} x
    139          * @param {Number} y
    140          * @returns {Boolean}
    141          */
    142         hasPoint: function (x, y) {
    143             var lft, rt, top, bot, ax, ay, type, r;
    144
    145             if (Type.isObject(Type.evaluate(this.visProp.precision))) {
    146                 type = this.board._inputDevice;
    147                 r = Type.evaluate(this.visProp.precision[type]);
    148             } else {
    149                 // 'inherit'
    150                 r = this.board.options.precision.hasPoint;
    151             }
    152             if (this.transformations.length > 0) {
    153                 //Transform the mouse/touch coordinates
    154                 // back to the original position of the text.
    155                 lft = Mat.matVecMult(Mat.inverse(this.board.renderer.joinTransforms(this, this.transformations)), [1, x, y]);
    156                 x = lft[1];
    157                 y = lft[2];
    158             }
    159
    160             ax = this.getAnchorX();
    161             if (ax === 'right') {
    162                 lft = this.coords.scrCoords[1] - this.size[0];
    163             } else if (ax === 'middle') {
    164                 lft = this.coords.scrCoords[1] - 0.5 * this.size[0];
    165             } else {
    166                 lft = this.coords.scrCoords[1];
    167             }
    168             rt = lft + this.size[0];
    169
    170             ay = this.getAnchorY();
    171             if (ay === 'top') {
    172                 bot = this.coords.scrCoords[2] + this.size[1];
    173             } else if (ay === 'middle') {
    174                 bot = this.coords.scrCoords[2] + 0.5 * this.size[1];
    175             } else {
    176                 bot = this.coords.scrCoords[2];
    177             }
    178             top = bot - this.size[1];
    179
    180             if (Type.evaluate(this.visProp.dragarea) === 'all') {
    181                 return x >= lft - r && x < rt + r && y >= top - r && y <= bot + r;
    182             }
    183             // e.g. 'small'
    184             return (y >= top - r && y <= bot + r) &&
    185                 ((x >= lft - r && x <= lft + 2 * r) ||
    186                     (x >= rt - 2 * r && x <= rt + r));
    187         },
    188
    189         /**
    190          * This sets the updateText function of this element depending on the type of text content passed.
    191          * Used by {@link JXG.Text#_setText} and {@link JXG.Text} constructor.
    192          * @param {String|Function|Number} text
    193          * @private
    194          */
    195         _createFctUpdateText: function (text) {
    196             var updateText, resolvedText,
    197                 ev_p = Type.evaluate(this.visProp.parse),
    198                 ev_um = Type.evaluate(this.visProp.usemathjax),
    199                 ev_uk = Type.evaluate(this.visProp.usekatex);
    200
    201             this.orgText = text;
    202             if (Type.isFunction(text)) {
    203                 this.updateText = function () {
    204                     resolvedText = text().toString();
    205                     if (ev_p && !ev_um && !ev_uk) {
    206                         this.plaintext = this.replaceSub(this.replaceSup(this.convertGeonextAndSketchometry2CSS(resolvedText)));
    207                     } else {
    208                         this.plaintext = resolvedText;
    209                     }
    210                 };
    211             } else if (Type.isString(text) && !ev_p) {   // Do not parse
    212                 this.updateText = function () {
    213                     this.plaintext = text;
    214                 };
    215             } else {                                     // Parse
    216                 if (Type.isNumber(text)) {
    217                     this.content = Type.toFixed(text, Type.evaluate(this.visProp.digits));
    218                 } else {
    219                     if (Type.evaluate(this.visProp.useasciimathml)) {
    220                         // Convert via ASCIIMathML
    221                         this.content = "'`" + text + "`'";
    222                     } else if (ev_um || ev_uk) {
    223                         this.content = "'" + text + "'";
    224                     } else {
    225                         // Converts GEONExT syntax into JavaScript string
    226                         // Short math is allowed
    227                         // Avoid geonext2JS calls
    228                         this.content = this.generateTerm(text, true, true);
    229                     }
    230                 }
    231                 updateText = this.board.jc.snippet(this.content, true, '', false);
    232                 this.updateText = function () {
    233                     this.plaintext = updateText();
    234                 };
    235             }
    236         },
    237
    238         /**
    239          * Defines new content. This is used by {@link JXG.Text#setTextJessieCode} and {@link JXG.Text#setText}. This is required because
    240          * JessieCode needs to filter all Texts inserted into the DOM and thus has to replace setText by setTextJessieCode.
    241          * @param {String|Function|Number} text
    242          * @returns {JXG.Text}
    243          * @private
    244          */
    245         _setText: function (text) {
    246             this._createFctUpdateText(text);
    247
    248             // First evaluation of the string.
    249             // We need this for display='internal' and Canvas
    250             this.updateText();
    251             this.fullUpdate();
    252
    253             // We do not call updateSize for the infobox to speed up rendering
    254             if (!this.board.infobox || this.id !== this.board.infobox.id) {
    255                 this.updateSize();    // updateSize() is called at least once.
    256             }
    257
    258             // This may slow down canvas renderer
    259             // if (this.board.renderer.type === 'canvas') {
    260             //     this.board.fullUpdate();
    261             // }
    262
    263             return this;
    264         },
    265
    266         /**
    267          * Defines new content but converts < and > to HTML entities before updating the DOM.
    268          * @param {String|function} text
    269          */
    270         setTextJessieCode: function (text) {
    271             var s;
    272
    273             this.visProp.castext = text;
    274             if (Type.isFunction(text)) {
    275                 s = function () {
    276                     return Type.sanitizeHTML(text());
    277                 };
    278             } else {
    279                 if (Type.isNumber(text)) {
    280                     s = text;
    281                 } else {
    282                     s = Type.sanitizeHTML(text);
    283                 }
    284             }
    285
    286             return this._setText(s);
    287         },
    288
    289         /**
    290          * Defines new content.
    291          * @param {String|function} text
    292          * @returns {JXG.Text} Reference to the text object.
    293          */
    294         setText: function (text) {
    295             return this._setText(text);
    296         },
    297
    298         /**
    299          * Recompute the width and the height of the text box.
    300          * Updates the array {@link JXG.Text#size} with pixel values.
    301          * The result may differ from browser to browser
    302          * by some pixels.
    303          * In canvas an old IEs we use a very crude estimation of the dimensions of
    304          * the textbox.
    305          * JSXGraph needs {@link JXG.Text#size} for applying rotations in IE and
    306          * for aligning text.
    307          *
    308          * @return {[type]} [description]
    309          */
    310         updateSize: function () {
    311             var tmp, that, node,
    312                 ev_d = Type.evaluate(this.visProp.display);
    313
    314             if (!Env.isBrowser || this.board.renderer.type === 'no') {
    315                 return this;
    316             }
    317             node = this.rendNode;
    318
    319             /**
    320              * offsetWidth and offsetHeight seem to be supported for internal vml elements by IE10+ in IE8 mode.
    321              */
    322             if (ev_d === 'html' || this.board.renderer.type === 'vml') {
    323                 if (Type.exists(node.offsetWidth)) {
    324                     that = this;
    325                     window.setTimeout(function () {
    326                         that.size = [node.offsetWidth, node.offsetHeight];
    327                         that.needsUpdate = true;
    328                         that.updateRenderer();
    329                     }, 0);
    330                     // In case, there is non-zero padding or borders
    331                     // the following approach does not longer work.
    332                     // s = [node.offsetWidth, node.offsetHeight];
    333                     // if (s[0] === 0 && s[1] === 0) { // Some browsers need some time to set offsetWidth and offsetHeight
    334                     //     that = this;
    335                     //     window.setTimeout(function () {
    336                     //         that.size = [node.offsetWidth, node.offsetHeight];
    337                     //         that.needsUpdate = true;
    338                     //         that.updateRenderer();
    339                     //     }, 0);
    340                     // } else {
    341                     //     this.size = s;
    342                     // }
    343                 } else {
    344                     this.size = this.crudeSizeEstimate();
    345                 }
    346             } else if (ev_d === 'internal') {
    347                 if (this.board.renderer.type === 'svg') {
    348                     that = this;
    349                     window.setTimeout(function () {
    350                         try {
    351                             tmp = node.getBBox();
    352                             that.size = [tmp.width, tmp.height];
    353                             that.needsUpdate = true;
    354                             that.updateRenderer();
    355                         } catch (e) {
    356                         }
    357                     }, 0);
    358                 } else if (this.board.renderer.type === 'canvas') {
    359                     this.size = this.crudeSizeEstimate();
    360                 }
    361             }
    362
    363             return this;
    364         },
    365
    366         /**
    367          * A very crude estimation of the dimensions of the textbox in case nothing else is available.
    368          * @returns {Array}
    369          */
    370         crudeSizeEstimate: function () {
    371             var ev_fs = parseFloat(Type.evaluate(this.visProp.fontsize));
    372             return [ev_fs * this.plaintext.length * 0.45, ev_fs * 0.9];
    373         },
    374
    375         /**
    376          * Decode unicode entities into characters.
    377          * @param {String} string
    378          * @returns {String}
    379          */
    380         utf8_decode: function (string) {
    381             return string.replace(/&#x(\w+);/g, function (m, p1) {
    382                 return String.fromCharCode(parseInt(p1, 16));
    383             });
    384         },
    385
    386         /**
    387          * Replace _{} by <sub>
    388          * @param {String} te String containing _{}.
    389          * @returns {String} Given string with _{} replaced by <sub>.
    390          */
    391         replaceSub: function (te) {
    392             if (!te.indexOf) {
    393                 return te;
    394             }
    395
    396             var j,
    397                 i = te.indexOf('_{');
    398
    399             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
    400             // i.e. replacing _{...} with <sub>...</sub>. What is passed would get out anyway.
    401             /*jslint regexp: true*/
    402
    403             while (i >= 0) {
    404                 te = te.substr(0, i) + te.substr(i).replace(/_\{/, '<sub>');
    405                 j = te.substr(i).indexOf('}');
    406                 if (j >= 0) {
    407                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sub>');
    408                 }
    409                 i = te.indexOf('_{');
    410             }
    411
    412             i = te.indexOf('_');
    413             while (i >= 0) {
    414                 te = te.substr(0, i) + te.substr(i).replace(/_(.?)/, '<sub>$1</sub>');
    415                 i = te.indexOf('_');
    416             }
    417
    418             return te;
    419         },
    420
    421         /**
    422          * Replace ^{} by <sup>
    423          * @param {String} te String containing ^{}.
    424          * @returns {String} Given string with ^{} replaced by <sup>.
    425          */
    426         replaceSup: function (te) {
    427             if (!te.indexOf) {
    428                 return te;
    429             }
    430
    431             var j,
    432                 i = te.indexOf('^{');
    433
    434             // the regexp in here are not used for filtering but to provide some kind of sugar for label creation,
    435             // i.e. replacing ^{...} with <sup>...</sup>. What is passed would get out anyway.
    436             /*jslint regexp: true*/
    437
    438             while (i >= 0) {
    439                 te = te.substr(0, i) + te.substr(i).replace(/\^\{/, '<sup>');
    440                 j = te.substr(i).indexOf('}');
    441                 if (j >= 0) {
    442                     te = te.substr(0, j) + te.substr(j).replace(/\}/, '</sup>');
    443                 }
    444                 i = te.indexOf('^{');
    445             }
    446
    447             i = te.indexOf('^');
    448             while (i >= 0) {
    449                 te = te.substr(0, i) + te.substr(i).replace(/\^(.?)/, '<sup>$1</sup>');
    450                 i = te.indexOf('^');
    451             }
    452
    453             return te;
    454         },
    455
    456         /**
    457          * Return the width of the text element.
    458          * @returns {Array} [width, height] in pixel
    459          */
    460         getSize: function () {
    461             return this.size;
    462         },
    463
    464         /**
    465          * Move the text to new coordinates.
    466          * @param {number} x
    467          * @param {number} y
    468          * @returns {object} reference to the text object.
    469          */
    470         setCoords: function (x, y) {
    471             var coordsAnchor, dx, dy;
    472             if (Type.isArray(x) && x.length > 1) {
    473                 y = x[1];
    474                 x = x[0];
    475             }
    476
    477             if (Type.evaluate(this.visProp.islabel) && Type.exists(this.element)) {
    478                 coordsAnchor = this.element.getLabelAnchor();
    479                 dx = (x - coordsAnchor.usrCoords[1]) * this.board.unitX;
    480                 dy = -(y - coordsAnchor.usrCoords[2]) * this.board.unitY;
    481
    482                 this.relativeCoords.setCoordinates(Const.COORDS_BY_SCREEN, [dx, dy]);
    483             } else {
    484                 /*
    485                 this.X = function () {
    486                     return x;
    487                 };
    488
    489                 this.Y = function () {
    490                     return y;
    491                 };
    492                 */
    493                 this.coords.setCoordinates(Const.COORDS_BY_USER, [x, y]);
    494             }
    495
    496             // this should be a local update, otherwise there might be problems
    497             // with the tick update routine resulting in orphaned tick labels
    498             this.fullUpdate();
    499
    500             return this;
    501         },
    502
    503         /**
    504          * Evaluates the text.
    505          * Then, the update function of the renderer
    506          * is called.
    507          */
    508         update: function (fromParent) {
    509             if (!this.needsUpdate) {
    510                 return this;
    511             }
    512
    513             this.updateCoords(fromParent);
    514             this.updateText();
    515
    516             if (Type.evaluate(this.visProp.display) === 'internal') {
    517                 if (Type.isString(this.plaintext)) {
    518                     this.plaintext = this.utf8_decode(this.plaintext);
    519                 }
    520             }
    521
    522             this.checkForSizeUpdate();
    523             if (this.needsSizeUpdate) {
    524                 this.updateSize();
    525             }
    526
    527             return this;
    528         },
    529
    530         /**
    531          * Used to save updateSize() calls.
    532          * Called in JXG.Text.update
    533          * That means this.update() has been called.
    534          * More tests are in JXG.Renderer.updateTextStyle. The latter tests
    535          * are one update off. But this should pose not too many problems, since
    536          * it affects fontSize and cssClass changes.
    537          *
    538          * @private
    539          */
    540         checkForSizeUpdate: function () {
    541             if (this.board.infobox && this.id === this.board.infobox.id) {
    542                 this.needsSizeUpdate = false;
    543             } else {
    544                 // For some magic reason it is more efficient on the iPad to
    545                 // call updateSize() for EVERY text element EVERY time.
    546                 this.needsSizeUpdate = (this.plaintextOld !== this.plaintext);
    547
    548                 if (this.needsSizeUpdate) {
    549                     this.plaintextOld = this.plaintext;
    550                 }
    551             }
    552
    553         },
    554
    555         /**
    556          * The update function of the renderert
    557          * is called.
    558          * @private
    559          */
    560         updateRenderer: function () {
    561             if (//this.board.updateQuality === this.board.BOARD_QUALITY_HIGH &&
    562                 Type.evaluate(this.visProp.autoposition)) {
    563
    564                 this.setAutoPosition()
    565                     .updateConstraint();
    566             }
    567             return this.updateRendererGeneric('updateText');
    568         },
    569
    570         /**
    571          * Converts shortened math syntax into correct syntax:  3x instead of 3*x or
    572          * (a+b)(3+1) instead of (a+b)*(3+1).
    573          *
    574          * @private
    575          * @param{String} expr Math term
    576          * @returns {string} expanded String
    577          */
    578         expandShortMath: function (expr) {
    579             var re = /([)0-9.])\s*([(a-zA-Z_])/g;
    580             return expr.replace(re, '$1*$2');
    581         },
    582
    583         /**
    584          * Converts the GEONExT syntax of the <value> terms into JavaScript.
    585          * Also, all Objects whose name appears in the term are searched and
    586          * the text is added as child to these objects.
    587          *
    588          * @param{String} contentStr String to be parsed
    589          * @param{Boolean} [expand] Optional flag if shortened math syntax is allowed (e.g. 3x instead of 3*x).
    590          * @param{Boolean} [avoidGeonext2JS] Optional flag if geonext2JS should be called. For backwards compatibility
    591          * this has to be set explicitely to true.
    592          * @private
    593          * @see JXG.GeonextParser.geonext2JS
    594          */
    595         generateTerm: function (contentStr, expand, avoidGeonext2JS) {
    596             var res, term, i, j,
    597                 plaintext = '""';
    598
    599             // revert possible jc replacement
    600             contentStr = contentStr || '';
    601             contentStr = contentStr.replace(/\r/g, '');
    602             contentStr = contentStr.replace(/\n/g, '');
    603             contentStr = contentStr.replace(/"/g, '\'');
    604             contentStr = contentStr.replace(/'/g, "\\'");
    605
    606             contentStr = contentStr.replace(/&arc;/g, '∠');
    607             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
    608             contentStr = contentStr.replace(/<arc\s*\/>/g, '∠');
    609             contentStr = contentStr.replace(/<sqrt\s*\/>/g, '√');
    610
    611             contentStr = contentStr.replace(/<value>/g, '<value>');
    612             contentStr = contentStr.replace(/<\/value>/g, '</value>');
    613
    614             // Convert GEONExT syntax into  JavaScript syntax
    615             i = contentStr.indexOf('<value>');
    616             j = contentStr.indexOf('</value>');
    617             if (i >= 0) {
    618                 while (i >= 0) {
    619                     plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr.slice(0, i))) + '"';
    620                     term = contentStr.slice(i + 7, j);
    621                     term = term.replace(/\s+/g, ''); // Remove all whitespace
    622                     if (expand === true) {
    623                         term = this.expandShortMath(term);
    624                     }
    625                     if (avoidGeonext2JS) {
    626                         res = term;
    627                     } else {
    628                         res = GeonextParser.geonext2JS(term, this.board);
    629                     }
    630                     res = res.replace(/\\"/g, "'");
    631                     res = res.replace(/\\'/g, "'");
    632
    633                     // GEONExT-Hack: apply rounding once only.
    634                     if (res.indexOf('toFixed') < 0) {
    635                         // output of a value tag
    636                         if (Type.isNumber((Type.bind(this.board.jc.snippet(res, true, '', false), this))())) {
    637                             // may also be a string
    638                             plaintext += '+(' + res + ').toFixed(' + (Type.evaluate(this.visProp.digits)) + ')';
    639                         } else {
    640                             plaintext += '+(' + res + ')';
    641                         }
    642                     } else {
    643                         plaintext += '+(' + res + ')';
    644                     }
    645
    646                     contentStr = contentStr.slice(j + 8);
    647                     i = contentStr.indexOf('<value>');
    648                     j = contentStr.indexOf('</value>');
    649                 }
    650             }
    651
    652             plaintext += ' + "' + this.replaceSub(this.replaceSup(contentStr)) + '"';
    653             plaintext = this.convertGeonextAndSketchometry2CSS(plaintext);
    654
    655             // This should replace &pi; by π
    656             plaintext = plaintext.replace(/&/g, '&');
    657             plaintext = plaintext.replace(/"/g, "'");
    658
    659             return plaintext;
    660         },
    661
    662         /**
    663          * Converts the GEONExT tags <overline> and <arrow> to
    664          * HTML span tags with proper CSS formatting.
    665          * @private
    666          * @see JXG.Text.generateTerm
    667          * @see JXG.Text._setText
    668          */
    669         convertGeonext2CSS: function (s) {
    670             if (Type.isString(s)) {
    671                 s = s.replace(
    672                     /(<|<)overline(>|>)/g,
    673                     '<span style=text-decoration:overline;>'
    674                 );
    675                 s = s.replace(
    676                     /(<|<)\/overline(>|>)/g,
    677                     '</span>'
    678                 );
    679                 s = s.replace(
    680                     /(<|<)arrow(>|>)/g,
    681                     '<span style=text-decoration:overline;>'
    682                 );
    683                 s = s.replace(
    684                     /(<|<)\/arrow(>|>)/g,
    685                     '</span>'
    686                 );
    687             }
    688
    689             return s;
    690         },
    691
    692         /**
    693          * Converts the sketchometry tag <sketchofont> to
    694          * HTML span tags with proper CSS formatting.
    695          * @private
    696          * @see JXG.Text.generateTerm
    697          * @see JXG.Text._setText
    698          */
    699         convertSketchometry2CSS: function (s) {
    700             if (Type.isString(s)) {
    701                 s = s.replace(
    702                     /(<|<)sketchofont(>|>)/g,
    703                     '<span style=font-family:sketchometry-light;font-weight:500;>'
    704                 );
    705                 s = s.replace(
    706                     /(<|<)\/sketchofont(>|>)/g,
    707                     '</span>'
    708                 );
    709                 s = s.replace(
    710                     /(<|<)sketchometry-light(>|>)/g,
    711                     '<span style=font-family:sketchometry-light;font-weight:500;>'
    712                 );
    713                 s = s.replace(
    714                     /(<|<)\/sketchometry-light(>|>)/g,
    715                     '</span>'
    716                 );
    717             }
    718
    719             return s;
    720         },
    721
    722         /**
    723          * Alias for convertGeonext2CSS and convertSketchometry2CSS
    724          * @private
    725          * @see JXG.Text.convertGeonext2CSS
    726          * @see JXG.Text.convertSketchometry2CSS
    727          */
    728         convertGeonextAndSketchometry2CSS: function (s){
    729             s = this.convertGeonext2CSS(s);
    730             s = this.convertSketchometry2CSS(s);
    731             return s;
    732         },
    733
    734         /**
    735          * Finds dependencies in a given term and notifies the parents by adding the
    736          * dependent object to the found objects child elements.
    737          * @param {String} content String containing dependencies for the given object.
    738          * @private
    739          */
    740         notifyParents: function (content) {
    741             var search,
    742                 res = null;
    743
    744             // revert possible jc replacement
    745             content = content.replace(/<value>/g, '<value>');
    746             content = content.replace(/<\/value>/g, '</value>');
    747
    748             do {
    749                 search = /<value>([\w\s*/^\-+()[\],<>=!]+)<\/value>/;
    750                 res = search.exec(content);
    751
    752                 if (res !== null) {
    753                     GeonextParser.findDependencies(this, res[1], this.board);
    754                     content = content.substr(res.index);
    755                     content = content.replace(search, '');
    756                 }
    757             } while (res !== null);
    758
    759             return this;
    760         },
    761
    762         // documented in element.js
    763         getParents: function () {
    764             var p;
    765             if (this.relativeCoords !== undefined) { // Texts with anchor elements, excluding labels
    766                 p = [this.relativeCoords.usrCoords[1], this.relativeCoords.usrCoords[2], this.orgText];
    767             } else {                                 // Other texts
    768                 p = [this.Z(), this.X(), this.Y(), this.orgText];
    769             }
    770
    771             if (this.parents.length !== 0) {
    772                 p = this.parents;
    773             }
    774
    775             return p;
    776         },
    777
    778         bounds: function () {
    779             var c = this.coords.usrCoords;
    780
    781             if (Type.evaluate(this.visProp.islabel) || this.board.unitY === 0 || this.board.unitX === 0) {
    782                 return [0, 0, 0, 0];
    783             }
    784             return [c[1], c[2] + this.size[1] / this.board.unitY, c[1] + this.size[0] / this.board.unitX, c[2]];
    785         },
    786
    787         getAnchorX: function () {
    788             var a = Type.evaluate(this.visProp.anchorx);
    789             if (a === 'auto') {
    790                 switch (this.visProp.position) {
    791                     case 'top':
    792                     case 'bot':
    793                         return 'middle';
    794                     case 'rt':
    795                     case 'lrt':
    796                     case 'urt':
    797                         return 'left';
    798                     case 'lft':
    799                     case 'llft':
    800                     case 'ulft':
    801                     default:
    802                         return 'right';
    803                 }
    804             }
    805             return a;
    806         },
    807
    808         getAnchorY: function () {
    809             var a = Type.evaluate(this.visProp.anchory);
    810             if (a === 'auto') {
    811                 switch (this.visProp.position) {
    812                     case 'top':
    813                     case 'ulft':
    814                     case 'urt':
    815                         return 'bottom';
    816                     case 'bot':
    817                     case 'lrt':
    818                     case 'llft':
    819                         return 'top';
    820                     case 'rt':
    821                     case 'lft':
    822                     default:
    823                         return 'middle';
    824                 }
    825             }
    826             return a;
    827         },
    828
    829         /**
    830          * Computes the number of overlaps of a box of w pixels width, h pixels height
    831          * and center (x, y)
    832          *
    833          * @private
    834          * @param  {Number} x x-coordinate of the center (screen coordinates)
    835          * @param  {Number} y y-coordinate of the center (screen coordinates)
    836          * @param  {Number} w width of the box in pixel
    837          * @param  {Number} h width of the box in pixel
    838          * @return {Number}   Number of overlapping elements
    839          */
    840         getNumberofConflicts: function (x, y, w, h) {
    841             var count = 0,
    842                 i, obj, le,
    843                 savePointPrecision;
    844
    845             // Set the precision of hasPoint to half the max if label isn't too long
    846             savePointPrecision = this.board.options.precision.hasPoint;
    847             // this.board.options.precision.hasPoint = Math.max(w, h) * 0.5;
    848             this.board.options.precision.hasPoint = (w + h) * 0.25;
    849             // TODO:
    850             // Make it compatible with the objects' visProp.precision attribute
    851             for (i = 0, le = this.board.objectsList.length; i < le; i++) {
    852                 obj = this.board.objectsList[i];
    853                 if (obj.visPropCalc.visible &&
    854                     obj.elType !== 'axis' &&
    855                     obj.elType !== 'ticks' &&
    856                     obj !== this.board.infobox &&
    857                     obj !== this &&
    858                     obj.hasPoint(x, y)) {
    859
    860                     count++;
    861                 }
    862             }
    863             this.board.options.precision.hasPoint = savePointPrecision;
    864
    865             return count;
    866         },
    867
    868         /**
    869          * Sets the offset of a label element to the position with the least number
    870          * of overlaps with other elements, while retaining the distance to its
    871          * anchor element. Twelve different angles are possible.
    872          *
    873          * @returns {JXG.Text} Reference to the text object.
    874          */
    875         setAutoPosition: function () {
    876             var x, y, cx, cy,
    877                 anchorCoords,
    878                 // anchorX, anchorY,
    879                 w = this.size[0],
    880                 h = this.size[1],
    881                 start_angle, angle,
    882                 optimum = {
    883                     conflicts: Infinity,
    884                     angle: 0,
    885                     r: 0
    886                 },
    887                 max_r, delta_r,
    888                 conflicts, offset, r,
    889                 num_positions = 12,
    890                 step = 2 * Math.PI / num_positions,
    891                 j, dx, dy, co, si;
    892
    893             if (this === this.board.infobox ||
    894                 !this.visPropCalc.visible ||
    895                 !Type.evaluate(this.visProp.islabel) ||
    896                 !this.element) {
    897                 return this;
    898             }
    899
    900             // anchorX = Type.evaluate(this.visProp.anchorx);
    901             // anchorY = Type.evaluate(this.visProp.anchory);
    902             offset = Type.evaluate(this.visProp.offset);
    903             anchorCoords = this.element.getLabelAnchor();
    904             cx = anchorCoords.scrCoords[1];
    905             cy = anchorCoords.scrCoords[2];
    906
    907             // Set dx, dy as the relative position of the center of the label
    908             // to its anchor element ignoring anchorx and anchory.
    909             dx = offset[0];
    910             dy = offset[1];
    911
    912             conflicts = this.getNumberofConflicts(cx + dx, cy - dy, w, h);
    913             if (conflicts === 0) {
    914                 return this;
    915             }
    916             // console.log(this.id, conflicts, w, h);
    917             // r = Geometry.distance([0, 0], offset, 2);
    918
    919             r = 12;
    920             max_r = 28;
    921             delta_r = 0.2 * r;
    922
    923             start_angle = Math.atan2(dy, dx);
    924
    925             optimum.conflicts = conflicts;
    926             optimum.angle = start_angle;
    927             optimum.r = r;
    928
    929             while (optimum.conflicts > 0 && r < max_r) {
    930                 for (j = 1, angle = start_angle + step; j < num_positions && optimum.conflicts > 0; j++) {
    931                     co = Math.cos(angle);
    932                     si = Math.sin(angle);
    933
    934                     x = cx + r * co;
    935                     y = cy - r * si;
    936
    937                     conflicts = this.getNumberofConflicts(x, y, w, h);
    938                     if (conflicts < optimum.conflicts) {
    939                         optimum.conflicts = conflicts;
    940                         optimum.angle = angle;
    941                         optimum.r = r;
    942                     }
    943                     if (optimum.conflicts === 0) {
    944                         break;
    945                     }
    946                     angle += step;
    947                 }
    948                 r += delta_r;
    949             }
    950             // console.log(this.id, "after", optimum)
    951             r = optimum.r;
    952             co = Math.cos(optimum.angle);
    953             si = Math.sin(optimum.angle);
    954             this.visProp.offset = [r * co, r * si];
    955
    956             if (co < -0.2) {
    957                 this.visProp.anchorx = 'right';
    958             } else if (co > 0.2) {
    959                 this.visProp.anchorx = 'left';
    960             } else {
    961                 this.visProp.anchorx = 'middle';
    962             }
    963
    964             return this;
    965         }
    966     });
    967
    968     /**
    969      * @class Construct and handle texts.
    970      *
    971      * The coordinates can be relative to the coordinates of an element
    972      * given in {@link JXG.Options#text.anchor}.
    973      *
    974      * MathJaX, HTML and GEONExT syntax can be handled.
    975      * @pseudo
    976      * @description
    977      * @name Text
    978      * @augments JXG.Text
    979      * @constructor
    980      * @type JXG.Text
    981      *
    982      * @param {number,function_number,function_number,function_String,function} z_,x,y,str Parent elements for text elements.
    983      *                     <p>
    984      *   Parent elements can be two or three elements of type number, a string containing a GEONE<sub>x</sub>T
    985      *   constraint, or a function which takes no parameter and returns a number. Every parent element determines one coordinate. If a coordinate is
    986      *   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
    987      *   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
    988      *   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
    989      *   parent elements are given they will be interpreted as homogeneous coordinates.
    990      *                     <p>
    991      *                     The text to display may be given as string or as function returning a string.
    992      *
    993      * There is the attribute 'display' which takes the values 'html' or 'internal'. In case of 'html' a HTML division tag is created to display
    994      * the text. In this case it is also possible to use ASCIIMathML. Incase of 'internal', a SVG or VML text element is used to display the text.
    995      * @see JXG.Text
    996      * @example
    997      * // Create a fixed text at position [0,1].
    998      *   var t1 = board.create('text',[0,1,"Hello World"]);
    999      * </pre><div class="jxgbox" id="JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7" style="width: 300px; height: 300px;"></div>
    1000      * <script type="text/javascript">
    1001      *   var t1_board = JXG.JSXGraph.initBoard('JXG896013aa-f24e-4e83-ad50-7bc7df23f6b7', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
    1002      *   var t1 = t1_board.create('text',[0,1,"Hello World"]);
    1003      * </script><pre>
    1004      * @example
    1005      * // Create a variable text at a variable position.
    1006      *   var s = board.create('slider',[[0,4],[3,4],[-2,0,2]]);
    1007      *   var graph = board.create('text',
    1008      *                        [function(x){ return s.Value();}, 1,
    1009      *                         function(){return "The value of s is"+JXG.toFixed(s.Value(), 2);}
    1010      *                        ]
    1011      *                     );
    1012      * </pre><div class="jxgbox" id="JXG5441da79-a48d-48e8-9e53-75594c384a1c" style="width: 300px; height: 300px;"></div>
    1013      * <script type="text/javascript">
    1014      *   var t2_board = JXG.JSXGraph.initBoard('JXG5441da79-a48d-48e8-9e53-75594c384a1c', {boundingbox: [-3, 6, 5, -3], axis: true, showcopyright: false, shownavigation: false});
    1015      *   var s = t2_board.create('slider',[[0,4],[3,4],[-2,0,2]]);
    1016      *   var t2 = t2_board.create('text',[function(x){ return s.Value();}, 1, function(){return "The value of s is "+JXG.toFixed(s.Value(), 2);}]);
    1017      * </script><pre>
    1018      * @example
    1019      * // Create a text bound to the point A
    1020      * var p = board.create('point',[0, 1]),
    1021      *     t = board.create('text',[0, -1,"Hello World"], {anchor: p});
    1022      *
    1023      * </pre><div class="jxgbox" id="JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723" style="width: 300px; height: 300px;"></div>
    1024      * <script type="text/javascript">
    1025      *     (function() {
    1026      *         var board = JXG.JSXGraph.initBoard('JXGff5a64b2-2b9a-11e5-8dd9-901b0e1b8723',
    1027      *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    1028      *     var p = board.create('point',[0, 1]),
    1029      *         t = board.create('text',[0, -1,"Hello World"], {anchor: p});
    1030      *
    1031      *     })();
    1032      *
    1033      * </script><pre>
    1034      *
    1035      */
    1036     JXG.createText = function (board, parents, attributes) {
    1037         var t,
    1038             attr = Type.copyAttributes(attributes, board.options, 'text'),
    1039             coords = parents.slice(0, -1),
    1040             content = parents[parents.length - 1];
    1041
    1042         // downwards compatibility
    1043         attr.anchor = attr.parent || attr.anchor;
    1044         t = CoordsElement.create(JXG.Text, board, coords, attr, content);
    1045
    1046         if (!t) {
    1047             throw new Error("JSXGraph: Can't create text with parent types '" +
    1048                 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." +
    1049                 "\nPossible parent types: [x,y], [z,x,y], [element,transformation]");
    1050         }
    1051
    1052         if (attr.rotate !== 0 && attr.display === 'internal') { // This is the default value, i.e. no rotation
    1053             t.addRotation(attr.rotate);
    1054         }
    1055
    1056         return t;
    1057     };
    1058
    1059     JXG.registerElement('text', JXG.createText);
    1060
    1061     /**
    1062      * @class Labels are text objects tied to other elements like points, lines and curves.
    1063      * Labels are handled internally by JSXGraph, only. There is NO constructor "board.create('label', ...)".
    1064      *
    1065      * @pseudo
    1066      * @description
    1067      * @name Label
    1068      * @augments JXG.Text
    1069      * @constructor
    1070      * @type JXG.Text
    1071      */
    1072     //  See element.js#createLabel
    1073
    1074     /**
    1075      * [[x,y], [w px, h px], [range]
    1076      */
    1077     JXG.createHTMLSlider = function (board, parents, attributes) {
    1078         var t, par,
    1079             attr = Type.copyAttributes(attributes, board.options, 'htmlslider');
    1080
    1081         if (parents.length !== 2 || parents[0].length !== 2 || parents[1].length !== 3) {
    1082             throw new Error("JSXGraph: Can't create htmlslider with parent types '" +
    1083                 (typeof parents[0]) + "' and '" + (typeof parents[1]) + "'." +
    1084                 "\nPossible parents are: [[x,y], [min, start, max]]");
    1085         }
    1086
    1087         // backwards compatibility
    1088         attr.anchor = attr.parent || attr.anchor;
    1089         attr.fixed = attr.fixed || true;
    1090
    1091         par = [parents[0][0], parents[0][1],
    1092             '<form style="display:inline">' +
    1093             '<input type="range" /><span></span><input type="text" />' +
    1094             '</form>'];
    1095
    1096         t = JXG.createText(board, par, attr);
    1097         t.type = Type.OBJECT_TYPE_HTMLSLIDER;
    1098
    1099         t.rendNodeForm = t.rendNode.childNodes[0];
    1100
    1101         t.rendNodeRange = t.rendNodeForm.childNodes[0];
    1102         t.rendNodeRange.min = parents[1][0];
    1103         t.rendNodeRange.max = parents[1][2];
    1104         t.rendNodeRange.step = attr.step;
    1105         t.rendNodeRange.value = parents[1][1];
    1106
    1107         t.rendNodeLabel = t.rendNodeForm.childNodes[1];
    1108         t.rendNodeLabel.id = t.rendNode.id + '_label';
    1109
    1110         if (attr.withlabel) {
    1111             t.rendNodeLabel.innerHTML = t.name + '=';
    1112         }
    1113
    1114         t.rendNodeOut = t.rendNodeForm.childNodes[2];
    1115         t.rendNodeOut.value = parents[1][1];
    1116
    1117         try {
    1118             t.rendNodeForm.id = t.rendNode.id + '_form';
    1119             t.rendNodeRange.id = t.rendNode.id + '_range';
    1120             t.rendNodeOut.id = t.rendNode.id + '_out';
    1121         } catch (e) {
    1122             JXG.debug(e);
    1123         }
    1124
    1125         t.rendNodeRange.style.width = attr.widthrange + 'px';
    1126         t.rendNodeRange.style.verticalAlign = 'middle';
    1127         t.rendNodeOut.style.width = attr.widthout + 'px';
    1128
    1129         t._val = parents[1][1];
    1130
    1131         if (JXG.supportsVML()) {
    1132             /*
    1133             * OnChange event is used for IE browsers
    1134             * The range element is supported since IE10
    1135             */
    1136             Env.addEvent(t.rendNodeForm, 'change', priv.HTMLSliderInputEventHandler, t);
    1137         } else {
    1138             /*
    1139             * OnInput event is used for non-IE browsers
    1140             */
    1141             Env.addEvent(t.rendNodeForm, 'input', priv.HTMLSliderInputEventHandler, t);
    1142         }
    1143
    1144         t.Value = function () {
    1145             return this._val;
    1146         };
    1147
    1148         return t;
    1149     };
    1150
    1151     JXG.registerElement('htmlslider', JXG.createHTMLSlider);
    1152
    1153     return {
    1154         Text: JXG.Text,
    1155         createText: JXG.createText,
    1156         createHTMLSlider: JXG.createHTMLSlider
    1157     };
    1158 });
    1159