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