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, unit: '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, unit: '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], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false});
 92  * board.create('smartlabel', [s1], {unit: '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], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false});
102  *     board.create('smartlabel', [s1], {unit: '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], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false});
112  * board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false});
113  * board.create('smartlabel', [c1], {unit: '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], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false});
123  *     board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false});
124  *     board.create('smartlabel', [c1], {unit: '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  *     unit: '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  *         unit: '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 + '=', unit: '°', 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 + '=', unit: '°', 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         getTextFun, txt_fun;
194 
195     if (parents.length === 0 || (
196         [Const.OBJECT_CLASS_POINT, Const.OBJECT_CLASS_LINE,Const.OBJECT_CLASS_CIRCLE].indexOf(parents[0].elementClass) < 0 &&
197         [Const.OBJECT_TYPE_POLYGON, Const.OBJECT_TYPE_ANGLE].indexOf(parents[0].type) < 0
198         )
199     ) {
200         throw new Error(
201             "JSXGraph: Can't create smartlabel with parent types " +
202                 "'" + typeof parents[0] + "', " +
203                 "'" + typeof parents[1] + "'."
204         );
205     }
206 
207     p = parents[0];
208     user_supplied_text = parents[1] || '';
209 
210     if (p.elementClass === Const.OBJECT_CLASS_POINT) {
211         attr = Type.copyAttributes(attributes, board.options, 'smartlabelpoint');
212 
213     } else if (p.elementClass === Const.OBJECT_CLASS_LINE) {
214         attr = Type.copyAttributes(attributes, board.options, 'smartlabelline');
215         /**
216          * @class
217          * @ignore
218          */
219         attr.rotate = function () {
220             return (Math.atan(p.getSlope()) * 180 / Math.PI + 360) % 360;
221         };
222         /**
223          * @class
224          * @ignore
225          */
226         attr.visible = function () { return (p.L() < 1.5) ? false : true; };
227 
228     } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) {
229         attr = Type.copyAttributes(attributes, board.options, 'smartlabelcircle');
230         /**
231          * @class
232          * @ignore
233          */
234         attr.visible = function () { return (p.Radius() < 1.5) ? false : true; };
235 
236     } else if (p.type === Const.OBJECT_TYPE_POLYGON) {
237         attr = Type.copyAttributes(attributes, board.options, 'smartlabelpolygon');
238     } else if (p.type === Const.OBJECT_TYPE_ANGLE) {
239         attr = Type.copyAttributes(attributes, board.options, 'smartlabelangle');
240         /**
241          * @class
242          * @ignore
243          */
244         attr.rotate = function () {
245             var c1 = p.center.coords.usrCoords,
246                 c2 = p.getLabelAnchor().usrCoords,
247                 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360;
248             return (v > 90 && v < 270) ? v + 180 : v;
249         };
250         /**
251          * @class
252          * @ignore
253          */
254         attr.anchorX = function () {
255             var c1 = p.center.coords.usrCoords,
256                 c2 = p.getLabelAnchor().usrCoords,
257                 v = (Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI + 360) % 360;
258             return (v > 90 && v < 270) ? 'right' : 'left';
259         };
260     }
261 
262     getTextFun = function (el, p, elType, mType) {
263         var measure;
264         switch (mType) {
265             case 'length':
266                 /**
267                  * @ignore
268                  */
269                 measure = function () { return p.L(); };
270                 break;
271             case 'slope':
272                 /**
273                  * @ignore
274                  */
275                 measure = function () { return p.Slope(); };
276                 break;
277             case 'area':
278                 /**
279                  * @ignore
280                  */
281                 measure = function () { return p.Area(); };
282                 break;
283             case 'radius':
284                 /**
285                  * @ignore
286                  */
287                 measure = function () { return p.Radius(); };
288                 break;
289             case 'perimeter':
290                 /**
291                  * @ignore
292                  */
293                 measure = function () { return p.Perimeter(); };
294                 break;
295             case 'rad':
296                 /**
297                  * @ignore
298                  */
299                 measure = function () { return p.Value(); };
300                 break;
301             case 'deg':
302                 /**
303                  * @ignore
304                  */
305                 measure = function () { return p.Value() * 180 / Math.PI; };
306                 break;
307             default:
308                 /**
309                  * @ignore
310                  */
311                 measure = function () { return 0.0; };
312         }
313 
314         return function () {
315             var str = '',
316                 val,
317                 txt = Type.evaluate(user_supplied_text),
318                 digits = el.evalVisProp('digits'),
319                 u = el.evalVisProp('unit'),
320                 pre = el.evalVisProp('prefix'),
321                 suf = el.evalVisProp('suffix'),
322                 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex');
323 
324             if (txt === '') {
325                 if (el.useLocale()) {
326                     val = el.formatNumberLocale(measure(), digits);
327                 } else {
328                     val = Type.toFixed(measure(), digits);
329                 }
330                 if (mj) {
331                     str = ['\\(', pre, val, '\\,', u, suf, '\\)'].join('');
332                 } else {
333                     str = [pre, val, u, suf].join('');
334                 }
335             } else {
336                 str = txt;
337             }
338             return str;
339         };
340     };
341 
342     if (p.elementClass === Const.OBJECT_CLASS_POINT) {
343         el = board.create('text', [
344             function () { return p.X(); },
345             function () { return p.Y(); },
346             ''
347         ], attr);
348 
349         txt_fun = function () {
350             var str = '',
351                 txt = Type.evaluate(user_supplied_text),
352                 digits = el.evalVisProp('digits'),
353                 u = el.evalVisProp('unit'),
354                 pre = el.evalVisProp('prefix'),
355                 suf = el.evalVisProp('suffix'),
356                 dir = el.evalVisProp('dir'),
357                 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex'),
358                 x, y;
359 
360             if (el.useLocale()) {
361                 x = el.formatNumberLocale(p.X(), digits);
362                 y = el.formatNumberLocale(p.Y(), digits);
363             } else {
364                 x = Type.toFixed(p.X(), digits);
365                 y = Type.toFixed(p.Y(), digits);
366             }
367 
368             if (txt === '') {
369                 if (dir === 'row') {
370                     if (mj) {
371                         str = ['\\(', pre, x, '\\,', u, ' / ', y, '\\,', u, suf, '\\)'].join('');
372                     } else {
373                         str = [pre, x, ' ', u, ' / ', y, ' ', u, suf].join('');
374                     }
375                 } else if (dir.indexOf('col') === 0) { // Starts with 'col'
376                     if (mj) {
377                         str = ['\\(', pre, '\\left(\\array{', x, '\\,', u, '\\\\ ', y, '\\,', u, '}\\right)', suf, '\\)'].join('');
378                     } else {
379                         str = [pre, x, ' ', u, '<br />', y, ' ', u, suf].join('');
380                     }
381                 }
382             } else {
383                 str = txt;
384             }
385             return str;
386         };
387 
388     } else if (p.elementClass === Const.OBJECT_CLASS_LINE) {
389 
390         if (attr.measure === 'length') {
391             el = board.create('text', [
392                 function () { return (p.point1.X() + p.point2.X()) * 0.5; },
393                 function () { return (p.point1.Y() + p.point2.Y()) * 0.5; },
394                 ''
395             ], attr);
396             txt_fun = getTextFun(el, p, 'line', 'length');
397 
398         } else if (attr.measure === 'slope') {
399             el = board.create('text', [
400                 function () { return (p.point1.X() * 0.25 + p.point2.X() * 0.75); },
401                 function () { return (p.point1.Y() * 0.25 + p.point2.Y() * 0.75); },
402                 ''
403             ], attr);
404             txt_fun = getTextFun(el, p, 'line', 'slope');
405         }
406 
407     } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) {
408         if (attr.measure === 'radius') {
409             el = board.create('text', [
410                 function () { return p.center.X() + p.Radius() * 0.5; },
411                 function () { return p.center.Y(); },
412                 ''
413             ], attr);
414             txt_fun = getTextFun(el, p, 'circle', 'radius');
415 
416         } else if (attr.measure === 'area') {
417             el = board.create('text', [
418                 function () { return p.center.X(); },
419                 function () { return p.center.Y() + p.Radius() * 0.5; },
420                 ''
421             ], attr);
422             txt_fun = getTextFun(el, p, 'circle', 'area');
423 
424         } else if (attr.measure === 'circumference' || attr.measure === 'perimeter') {
425             el = board.create('text', [
426                 function () { return p.getLabelAnchor(); },
427                 ''
428             ], attr);
429             txt_fun = getTextFun(el, p, 'circle', 'perimeter');
430 
431         }
432     } else if (p.type === Const.OBJECT_TYPE_POLYGON) {
433         if (attr.measure === 'area') {
434             el = board.create('text', [
435                 function () { return p.getTextAnchor(); },
436                 ''
437             ], attr);
438             txt_fun = getTextFun(el, p, 'polygon', 'area');
439 
440         } else if (attr.measure === 'perimeter') {
441             el = board.create('text', [
442                 function () {
443                     var last = p.borders.length - 1;
444                     if (last >= 0) {
445                         return [
446                             (p.borders[last].point1.X() + p.borders[last].point2.X()) * 0.5,
447                             (p.borders[last].point1.Y() + p.borders[last].point2.Y()) * 0.5
448                         ];
449                     } else {
450                         return p.getTextAnchor();
451                     }
452                 },
453                 ''
454             ], attr);
455             txt_fun = getTextFun(el, p, 'polygon', 'perimeter');
456         }
457 
458     } else if (p.type === Const.OBJECT_TYPE_ANGLE) {
459         el = board.create('text', [
460             function () {
461                 return p.getLabelAnchor();
462             },
463             ''
464         ], attr);
465         txt_fun = getTextFun(el, p, 'angle', attr.measure);
466     }
467 
468     if (Type.exists(el)) {
469         el.setText(txt_fun);
470         p.addChild(el);
471         el.setParents([p]);
472     }
473 
474     return el;
475 };
476 
477 JXG.registerElement("smartlabel", JXG.createSmartLabel);
478