1 /*
  2     Copyright 2008-2026
  3         Matthias Ehmann,
  4         Carsten Miller,
  5         Alfred Wassermann
  6 
  7     This file is part of JSXGraph.
  8 
  9     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 10 
 11     You can redistribute it and/or modify it under the terms of the
 12 
 13       * GNU Lesser General Public License as published by
 14         the Free Software Foundation, either version 3 of the License, or
 15         (at your option) any later version
 16       OR
 17       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 18 
 19     JSXGraph is distributed in the hope that it will be useful,
 20     but WITHOUT ANY WARRANTY; without even the implied warranty of
 21     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 22     GNU Lesser General Public License for more details.
 23 
 24     You should have received a copy of the GNU Lesser General Public License and
 25     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 26     and <https://opensource.org/licenses/MIT/>.
 27  */
 28 
 29 /*global JXG: true, define: true*/
 30 /*jslint nomen: true, plusplus: true*/
 31 
 32 /**
 33  * @fileoverview Implementation of smart labels..
 34  */
 35 
 36 import JXG from "../jxg.js";
 37 import Const from "../base/constants.js";
 38 import Type from "../utils/type.js";
 39 
 40 /**
 41  * @class Customized text elements for displaying measurements of JSXGraph elements,
 42  * Examples are length of a
 43  * segment, perimeter or area of a circle or polygon (including polygonal chain),
 44  * slope of a line, value of an angle, and coordinates of a point.
 45  * <p>
 46  * If additionally a text, or a function is supplied and the content is not the empty string,
 47  * that text is displayed instead of the measurement.
 48  * <p>
 49  * Smartlabels use custom made CSS layouts defined in jsxgraph.css. Therefore, the inclusion of the file jsxgraph.css is mandatory or
 50  * the CSS classes have to be replaced by other classes.
 51  * <p>
 52  * The default attributes for smartlabels are defined for each type of measured element in the following sub-objects.
 53  * This is a deviation from the usual JSXGraph attribute usage.
 54  * <ul>
 55  *  <li> <tt>JXG.Options.smartlabelangle</tt> for smartlabels of angle objects
 56  *  <li> <tt>JXG.Options.smartlabelcircle</tt> for smartlabels of circle objects
 57  *  <li> <tt>JXG.Options.smartlabelline</tt> for smartlabels of line objects
 58  *  <li> <tt>JXG.Options.smartlabelpoint</tt> for smartlabels of point objects.
 59  *  <li> <tt>JXG.Options.smartlabelpolygon</tt> for smartlabels of polygon objects.
 60  * </ul>
 61  *
 62  *
 63  * @pseudo
 64  * @name Smartlabel
 65  * @augments JXG.Text
 66  * @constructor
 67  * @type JXG.Text
 68  * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown.
 69  * @param {JXG.GeometryElement} Parent parent object: point, line, circle, polygon, angle.
 70  * @param {String|Function} Txt Optional text. In case, this content is not the empty string,
 71  *  the measurement is overwritten by this text.
 72  *
 73  * @example
 74  * var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false});
 75  * board.create('smartlabel', [p1], {digits: 1, baseUnit: 'm', dir: 'col', useMathJax: false});
 76  *
 77  * </pre><div id="JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f" class="jxgbox" style="width: 300px; height: 300px;"></div>
 78  * <script type="text/javascript">
 79  *     (function() {
 80  *         var board = JXG.JSXGraph.initBoard('JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f',
 81  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
 82  *     var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false});
 83  *     board.create('smartlabel', [p1], {digits: 1, baseUnit: 'cm', dir: 'col', useMathJax: false});
 84  *
 85  *     })();
 86  *
 87  * </script><pre>
 88  *
 89  * @example
 90  * var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}});
 91  * board.create('smartlabel', [s1], {baseUnit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false});
 92  * board.create('smartlabel', [s1], {baseUnit: 'm',  measure: 'slope', prefix: 'Δ = ', useMathJax: false});
 93  *
 94  *
 95  * </pre><div id="JXGfb4423dc-ee3a-4122-a186-82123019a835" class="jxgbox" style="width: 300px; height: 300px;"></div>
 96  * <script type="text/javascript">
 97  *     (function() {
 98  *         var board = JXG.JSXGraph.initBoard('JXGfb4423dc-ee3a-4122-a186-82123019a835',
 99  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
100  *     var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}});
101  *     board.create('smartlabel', [s1], {baseUnit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false});
102  *     board.create('smartlabel', [s1], {baseUnit: 'm',  measure: 'slope', prefix: 'Δ = ', useMathJax: false});
103  *
104  *
105  *     })();
106  *
107  * </script><pre>
108  *
109  * @example
110  * var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}});
111  * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false});
112  * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false});
113  * board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false});
114  *
115  *
116  * </pre><div id="JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93" class="jxgbox" style="width: 300px; height: 300px;"></div>
117  * <script type="text/javascript">
118  *     (function() {
119  *         var board = JXG.JSXGraph.initBoard('JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93',
120  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
121  *     var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}});
122  *     board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false});
123  *     board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false});
124  *     board.create('smartlabel', [c1], {baseUnit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false});
125  *
126  *
127  *     })();
128  *
129  * </script><pre>
130  *
131  * @example
132  * var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {});
133  * board.create('smartlabel', [p2], {
134  *     baseUnit: 'm',
135  *     measure: 'area',
136  *     prefix: 'A = ',
137  *     cssClass: 'smart-label-pure smart-label-polygon',
138  *     highlightCssClass: 'smart-label-pure smart-label-polygon',
139  *     useMathJax: false
140  * });
141  * board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], {
142  *     measure: 'perimeter',
143  *     cssClass: 'smart-label-outline smart-label-polygon',
144  *     highlightCssClass: 'smart-label-outline smart-label-polygon',
145  *     useMathJax: false
146  * });
147  *
148  * </pre><div id="JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8" class="jxgbox" style="width: 300px; height: 300px;"></div>
149  * <script type="text/javascript">
150  *     (function() {
151  *         var board = JXG.JSXGraph.initBoard('JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8',
152  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
153  *     var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {});
154  *     board.create('smartlabel', [p2], {
155  *         baseUnit: 'm',
156  *         measure: 'area',
157  *         prefix: 'A = ',
158  *         cssClass: 'smart-label-pure smart-label-polygon',
159  *         highlightCssClass: 'smart-label-pure smart-label-polygon',
160  *         useMathJax: false
161  *     });
162  *     board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], {
163  *         measure: 'perimeter',
164  *         cssClass: 'smart-label-outline smart-label-polygon',
165  *         highlightCssClass: 'smart-label-outline smart-label-polygon',
166  *         useMathJax: false
167  *     });
168  *
169  *     })();
170  *
171  * </script><pre>
172  *
173  * @example
174  * var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false});
175  * var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', baseUnit: '°', useMathJax: false});
176  *
177  * </pre><div id="JXG48d6d1ae-e04a-45f4-a743-273976712c0b" class="jxgbox" style="width: 300px; height: 300px;"></div>
178  * <script type="text/javascript">
179  *     (function() {
180  *         var board = JXG.JSXGraph.initBoard('JXG48d6d1ae-e04a-45f4-a743-273976712c0b',
181  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
182  *     var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false});
183  *     var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', baseUnit: '°', useMathJax: false});
184  *
185  *     })();
186  *
187  * </script><pre>
188  *
189  */
190 JXG.createSmartLabel = function (board, parents, attributes) {
191     var el, attr,
192         p, user_supplied_text;
193 
194     if (parents.length === 0 || (
195         [Const.OBJECT_CLASS_POINT, Const.OBJECT_CLASS_LINE, Const.OBJECT_CLASS_CIRCLE].indexOf(parents[0].elementClass) < 0 &&
196         [Const.OBJECT_TYPE_POLYGON, Const.OBJECT_TYPE_ANGLE].indexOf(parents[0].type) < 0
197     )) {
198         throw new Error(
199             "JSXGraph: Can't create smartlabel with parent types " +
200             "'" + typeof parents[0] + "', " +
201             "'" + typeof parents[1] + "'."
202         );
203     }
204 
205     p = parents[0];
206     user_supplied_text = parents[1] || '';
207 
208     attr = Type.copyAttributes(attributes, board.options, 'smartlabel');
209 
210     if (p.elementClass === Const.OBJECT_CLASS_POINT) {
211         attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelpoint'));
212 
213     } else if (p.elementClass === Const.OBJECT_CLASS_LINE) {
214         attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelline'));
215         /**
216          * @class
217          * @ignore
218          */
219         attr.rotate = function (self) {
220             var orientation = self.evalVisProp('orientation'),
221                 add;
222             switch (orientation) {
223                 case 'none':
224                     return 0;
225                 case 'orthogonal':
226                     add = 270;
227                     break;
228                 case 'orthogonal-inverted':
229                     add = 90;
230                     break;
231                 case 'parallel-inverted':
232                 case 'inverted':
233                     add = 0;
234                     break;
235                 default:
236                     add = 360;
237             }
238             return (Math.atan(p.getSlope()) * 180 / Math.PI + add) % 360;
239         };
240         /**
241          * @class
242          * @ignore
243          */
244         attr.visible = function (self) {
245             var orientation = self.evalVisProp('orientation'),
246                 thres = self.evalVisProp('visibleThreshold'),
247                 sizeLabel = self.getSize(),
248                 sizeParent,
249                 c1, c2,
250                 dx, dy;
251 
252             c1 = p.point1.coords.scrCoords;
253             c2 = p.point2.coords.scrCoords;
254             dx = c2[1] - c1[1];
255             dy = c2[2] - c1[2];
256             sizeParent = Math.floor(Math.sqrt(dx * dx + dy * dy));
257 
258             switch (orientation) {
259                 case 'parallel':
260                 case 'parallel-inverted':
261                 case 'inverted':
262                     return sizeLabel[0] < sizeParent * thres;
263 
264                 case 'orthogonal':
265                 case 'orthogonal-inverted':
266                     return sizeLabel[1] < sizeParent * thres;
267 
268                 case 'none':
269                 default:
270                     return p.L() >= 1.5;
271             }
272         };
273 
274     } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) {
275         attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelcircle'));
276         /**
277          * @class
278          * @ignore
279          */
280         attr.visible = function (self) {
281             var sizeLabel = self.getSize(),
282                 thres = self.evalVisProp('visibleThreshold'),
283                 sizeParent,
284                 c1, c2,
285                 dx, dy;
286 
287             c1 = p.center.coords.scrCoords;
288             if (p.point2) {
289                 c2 = p.point2.coords.scrCoords;
290             } else {
291                 c2 = new JXG.Coords(JXG.COORDS_BY_USER, [p.center.coords.usrCoords[0], p.center.coords.usrCoords[1] + p.Radius()], p.board).scrCoords;
292             }
293             dx = c2[1] - c1[1];
294             dy = c2[2] - c1[2];
295             sizeParent = Math.floor(Math.sqrt(dx * dx + dy * dy)) * 2;
296 
297             return sizeLabel[0] < sizeParent * thres;
298         };
299 
300     } else if (p.type === Const.OBJECT_TYPE_POLYGON) {
301         attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelpolygon'));
302     } else if (p.type === Const.OBJECT_TYPE_ANGLE) {
303         attr = Type.merge(attr, Type.copyAttributes(attributes, board.options, 'smartlabelangle'));
304         /**
305          * @class
306          * @ignore
307          */
308         attr.rotate = function () {
309             var c1 = p.center.coords.usrCoords,
310                 c2 = p.getLabelAnchor().usrCoords,
311                 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360;
312             return (v > 90 && v < 270) ? v + 180 : v;
313         };
314         /**
315          * @class
316          * @ignore
317          */
318         attr.anchorX = function () {
319             var c1 = p.center.coords.usrCoords,
320                 c2 = p.getLabelAnchor().usrCoords,
321                 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360;
322             return (v > 90 && v < 270) ? 'right' : 'left';
323         };
324     }
325 
326     if (p.elementClass === Const.OBJECT_CLASS_POINT) {
327         el = board.create('text', [
328             function () { return p.X(); },
329             function () { return p.Y(); },
330             ''
331         ], attr);
332 
333     } else if (p.elementClass === Const.OBJECT_CLASS_LINE) {
334 
335         if (attr.measure === 'length') {
336             el = board.create('text', [
337                 function () { return (p.point1.X() + p.point2.X()) * 0.5; },
338                 function () { return (p.point1.Y() + p.point2.Y()) * 0.5; },
339                 ''
340             ], attr);
341 
342         } else if (attr.measure === 'slope') {
343             el = board.create('text', [
344                 function () { return (p.point1.X() * 0.25 + p.point2.X() * 0.75); },
345                 function () { return (p.point1.Y() * 0.25 + p.point2.Y() * 0.75); },
346                 ''
347             ], attr);
348         }
349 
350     } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) {
351         if (attr.measure === 'radius') {
352             el = board.create('text', [
353                 function () { return p.center.X() + p.Radius() * 0.5; },
354                 function () { return p.center.Y(); },
355                 ''
356             ], attr);
357 
358         } else if (attr.measure === 'area') {
359             el = board.create('text', [
360                 function () { return p.center.X(); },
361                 function () { return p.center.Y() + p.Radius() * 0.5; },
362                 ''
363             ], attr);
364 
365         } else if (attr.measure === 'circumference' || attr.measure === 'perimeter') {
366             el = board.create('text', [
367                 function () { return p.getLabelAnchor(); },
368                 ''
369             ], attr);
370 
371         }
372     } else if (p.type === Const.OBJECT_TYPE_POLYGON) {
373         if (attr.measure === 'area') {
374             el = board.create('text', [
375                 function () { return p.getTextAnchor(); },
376                 ''
377             ], attr);
378 
379         } else if (attr.measure === 'perimeter') {
380             el = board.create('text', [
381                 function () {
382                     var last = p.borders.length - 1;
383                     if (last >= 0) {
384                         return [
385                             (p.borders[last].point1.X() + p.borders[last].point2.X()) * 0.5,
386                             (p.borders[last].point1.Y() + p.borders[last].point2.Y()) * 0.5
387                         ];
388                     } else {
389                         return p.getTextAnchor();
390                     }
391                 },
392                 ''
393             ], attr);
394         }
395 
396     } else if (p.type === Const.OBJECT_TYPE_ANGLE) {
397         el = board.create('text', [
398             function () {
399                 return p.getLabelAnchor();
400             },
401             ''
402         ], attr);
403     }
404 
405     if (!Type.exists(el)) {
406         return null;
407     }
408 
409     el.elType = 'smartlabel';
410 
411     el.parentObject = p;
412 
413     el.Value = function () {
414         var mType = this.evalVisProp('measure');
415 
416         switch (mType) {
417             case 'length':
418                 return p.L();
419 
420             case 'slope':
421                 return p.Slope();
422 
423             case 'area':
424                 return p.Area();
425 
426             case 'radius':
427                 return p.Radius();
428 
429             case 'perimeter':
430             case 'circumference':
431                 return p.Perimeter();
432 
433             case 'rad':
434                 return p.Value();
435 
436             case 'deg':
437                 return p.Value() * 180 / Math.PI;
438 
439             case 'coords':
440                 return [p.X(), p.Y()];
441 
442             default:
443                 return 0.0;
444         }
445     };
446 
447     el.Dimension = function () {
448         var mType = this.evalVisProp('measure');
449 
450         switch (mType) {
451             case 'area':
452                 return 2;
453 
454             case 'length':
455             case 'radius':
456             case 'perimeter':
457             case 'circumference':
458                 return 1;
459 
460             case 'slope':
461                 return 0;
462 
463             case 'rad':
464             case 'deg':
465                 // Angles in various units has dimension 0
466                 return 0;
467 
468             case 'coords':
469                 return 1;
470 
471             default:
472                 return 0;
473         }
474     };
475 
476     el.Unit = function (dimension) {
477         var unit = '',
478             units = el.evalVisProp('units'),
479             dim = dimension,
480             dims = {}, i;
481 
482         if (!Type.exists(dim)) {
483             dim = el.Dimension();
484         }
485 
486         if (Type.isArray(dimension)) {
487             for (i = 0; i < dimension.length; i++) {
488                 dims['dim' + dimension[i]] = el.Unit(dimension[i]);
489             }
490             return dims;
491         }
492 
493         if (Type.isObject(units) && Type.exists(units[dim]) && units[dim] !== false) {
494             unit = el.eval(units[dim]);
495         } else if (Type.isObject(units) && Type.exists(units['dim' + dim]) && units['dim' + dim] !== false) {
496             // In some cases, object keys must not be numbers. This allows key 'dim1' instead of '1'.
497             unit = el.eval(units['dim' + dim]);
498         } else {
499             unit = el.evalVisProp('baseUnit');
500 
501             if (unit === '' && el.evalVisProp('unit') !== '') {
502                 // Backwards compatibility
503                 unit = el.evalVisProp('unit');
504             }
505 
506             if (dim === 0) {
507                 unit = '';
508             } else if (dim > 1 && unit !== '') {
509                 unit = unit + '^{' + dim + '}';
510             }
511         }
512 
513         return unit;
514     };
515 
516     el.setText(function () {
517         var digits, val, u,
518             txt = Type.evaluate(user_supplied_text),
519             str = '',
520             pre = '',
521             suf = '',
522             dir,
523             mj,
524             i;
525 
526         if (txt !== '') {
527             return txt;
528         }
529 
530         val = el.Value();
531         digits = el.evalVisProp('digits');
532         u = el.Unit();
533         pre = '';
534         suf = '';
535         dir = el.evalVisProp('dir');
536         mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex');
537 
538         if (el.evalVisProp('showPrefix')) {
539             pre = el.evalVisProp('prefix');
540         }
541         if (el.evalVisProp('showSuffix')) {
542             suf = el.evalVisProp('suffix');
543         }
544 
545         if (el.useLocale()) {
546             if (Type.isArray(val)) {
547                 for (i = 0; i < val.length; i++) {
548                     val[i] = el.formatNumberLocale(val[i], digits);
549                 }
550             } else {
551                 val = el.formatNumberLocale(val, digits);
552             }
553         } else {
554             if (Type.isArray(val)) {
555                 for (i = 0; i < val.length; i++) {
556                     val[i] = Type.toFixed(val[i], digits);
557                 }
558             } else {
559                 val = Type.toFixed(val, digits);
560             }
561         }
562 
563         if (Type.isFunction(el.visProp.formatvalue)) {
564             val = el.visProp.formatvalue(el, val);
565         }
566 
567         if (Type.isArray(val)) {
568             if (dir === 'row') {
569                 str = [];
570                 if (mj) {
571                     str.push('\\(', pre);
572                     for (i = 0; i < val.length; i++) {
573                         str.push(val[i], '\\,', u);
574                         if (i < val.length - 1) {
575                             str.push(' / ');
576                         }
577                     }
578                     str.push(suf, '\\)');
579                 } else {
580                     str.push(pre);
581                     for (i = 0; i < val.length; i++) {
582                         str.push(val[i], ' ', u);
583                         if (i < val.length - 1) {
584                             str.push(' / ');
585                         }
586                     }
587                     str.push(suf);
588                 }
589                 str = str.join('');
590             } else if (dir.indexOf('col') === 0) { // Starts with 'col'
591                 str = [];
592                 if (mj) {
593                     str.push('\\(', pre, '\\left(\\array{');
594                     for (i = 0; i < val.length; i++) {
595                         str.push(val[i], '\\,', u);
596                         if (i < val.length - 1) {
597                             str.push('\\\\ ');
598                         }
599                     }
600                     str.push('}\\right)', suf, '\\)');
601 
602                 } else {
603                     str.push(pre);
604                     for (i = 0; i < val.length; i++) {
605                         str.push(val[i], ' ', u);
606                         if (i < val.length - 1) {
607                             str.push('<br />');
608                         }
609                     }
610                     str.push(suf);
611                 }
612                 str = str.join('');
613             }
614         } else {
615             if (mj) {
616                 str = ['\\(', pre, val, '\\,', u, suf, '\\)'].join('');
617             } else {
618                 str = [pre, val, u, suf].join('');
619             }
620         }
621 
622         return str;
623     });
624 
625     p.addChild(el);
626     el.setParents([p]);
627 
628     el.methodMap = Type.deepCopy(el.methodMap, {
629         Value: "Value",
630         V: "Value",
631         Dimension: "Dimension",
632         Unit: "Unit",
633         parent: "parentObject",
634         parentObject: "parentObject"
635     });
636 
637     return el;
638 };
639 
640 JXG.registerElement("smartlabel", JXG.createSmartLabel);
641