1 /*
  2     Copyright 2008-2024
  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 Smart label. These are customized text elements for displaying measurements of JSXGraph elements, like length of a
 42  * segment, perimeter or area of a circle or polygon (including polygonal chain), slope of a line, value of an angle, and coordinates of a point.
 43  * <p>
 44  * If additionally a text, or a function is supplied and the content is not the empty string,
 45  * that text is displayed instead of the measurement.
 46  * <p>
 47  * Smartlabels use custom made CSS layouts defined in jsxgraph.css. Therefore, the inclusion of the file jsxgraph.css is mandatory or
 48  * the CSS classes have to be replaced by other classes.
 49  * <p>
 50  * The default attributes for smartlabels are defined for each type of measured element in the following sub-objects.
 51  * This is a deviation from the usual JSXGraph attribute usage.
 52  * <ul>
 53  *  <li> <tt>JXG.Options.smartlabelangle</tt> for smartlabels of angle objects
 54  *  <li> <tt>JXG.Options.smartlabelcircle</tt> for smartlabels of circle objects
 55  *  <li> <tt>JXG.Options.smartlabelline</tt> for smartlabels of line objects
 56  *  <li> <tt>JXG.Options.smartlabelpoint</tt> for smartlabels of point objects.
 57  *  <li> <tt>JXG.Options.smartlabelpolygon</tt> for smartlabels of polygon objects.
 58  * </ul>
 59  *
 60  *
 61  * @pseudo
 62  * @name Smartlabel
 63  * @augments JXG.Text
 64  * @constructor
 65  * @type JXG.Text
 66  * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown.
 67  * @param {JXG.GeometryElement} Parent parent object: point, line, circle, polygon, angle.
 68  * @param {String|Function} Txt Optional text. In case, this content is not the empty string,
 69  *  the measurement is overwritten by this text.
 70  *
 71  * @example
 72  * var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false});
 73  * board.create('smartlabel', [p1], {digits: 1, unit: 'm', dir: 'col', useMathJax: false});
 74  *
 75  * </pre><div id="JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f" class="jxgbox" style="width: 300px; height: 300px;"></div>
 76  * <script type="text/javascript">
 77  *     (function() {
 78  *         var board = JXG.JSXGraph.initBoard('JXG30cd1f9e-7e78-48f3-91a2-9abd466a754f',
 79  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
 80  *     var p1 = board.create('point', [3, 4], {showInfobox: false, withLabel: false});
 81  *     board.create('smartlabel', [p1], {digits: 1, unit: 'cm', dir: 'col', useMathJax: false});
 82  *
 83  *     })();
 84  *
 85  * </script><pre>
 86  *
 87  * @example
 88  * var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}});
 89  * board.create('smartlabel', [s1], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false});
 90  * board.create('smartlabel', [s1], {unit: 'm',  measure: 'slope', prefix: 'Δ = ', useMathJax: false});
 91  *
 92  *
 93  * </pre><div id="JXGfb4423dc-ee3a-4122-a186-82123019a835" class="jxgbox" style="width: 300px; height: 300px;"></div>
 94  * <script type="text/javascript">
 95  *     (function() {
 96  *         var board = JXG.JSXGraph.initBoard('JXGfb4423dc-ee3a-4122-a186-82123019a835',
 97  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
 98  *     var s1 = board.create('line', [[-7, 2], [6, -6]], {point1: {visible:true}, point2: {visible:true}});
 99  *     board.create('smartlabel', [s1], {unit: 'm', measure: 'length', prefix: 'L = ', useMathJax: false});
100  *     board.create('smartlabel', [s1], {unit: 'm',  measure: 'slope', prefix: 'Δ = ', useMathJax: false});
101  *
102  *
103  *     })();
104  *
105  * </script><pre>
106  *
107  * @example
108  * var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}});
109  * board.create('smartlabel', [c1], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false});
110  * board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false});
111  * board.create('smartlabel', [c1], {unit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false});
112  *
113  *
114  * </pre><div id="JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93" class="jxgbox" style="width: 300px; height: 300px;"></div>
115  * <script type="text/javascript">
116  *     (function() {
117  *         var board = JXG.JSXGraph.initBoard('JXG763c4700-8273-4eb7-9ed9-1dc6c2c52e93',
118  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
119  *     var c1 = board.create('circle', [[0, 1], [4, 1]], {point2: {visible: true}});
120  *     board.create('smartlabel', [c1], {unit: 'm', measure: 'perimeter', prefix: 'U = ', useMathJax: false});
121  *     board.create('smartlabel', [c1], {unit: 'm', measure: 'area', prefix: 'A = ', useMathJax: false});
122  *     board.create('smartlabel', [c1], {unit: 'm', measure: 'radius', prefix: 'R = ', useMathJax: false});
123  *
124  *
125  *     })();
126  *
127  * </script><pre>
128  *
129  * @example
130  * var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {});
131  * board.create('smartlabel', [p2], {
132  *     unit: 'm',
133  *     measure: 'area',
134  *     prefix: 'A = ',
135  *     cssClass: 'smart-label-pure smart-label-polygon',
136  *     highlightCssClass: 'smart-label-pure smart-label-polygon',
137  *     useMathJax: false
138  * });
139  * board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], {
140  *     measure: 'perimeter',
141  *     cssClass: 'smart-label-outline smart-label-polygon',
142  *     highlightCssClass: 'smart-label-outline smart-label-polygon',
143  *     useMathJax: false
144  * });
145  *
146  * </pre><div id="JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8" class="jxgbox" style="width: 300px; height: 300px;"></div>
147  * <script type="text/javascript">
148  *     (function() {
149  *         var board = JXG.JSXGraph.initBoard('JXG376425ac-b4e5-41f2-979c-6ff32a01e9c8',
150  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
151  *     var p2 = board.create('polygon', [[-6, -5], [7, -7], [-4, 3]], {});
152  *     board.create('smartlabel', [p2], {
153  *         unit: 'm',
154  *         measure: 'area',
155  *         prefix: 'A = ',
156  *         cssClass: 'smart-label-pure smart-label-polygon',
157  *         highlightCssClass: 'smart-label-pure smart-label-polygon',
158  *         useMathJax: false
159  *     });
160  *     board.create('smartlabel', [p2, () => 'X: ' + p2.vertices[0].X().toFixed(1)], {
161  *         measure: 'perimeter',
162  *         cssClass: 'smart-label-outline smart-label-polygon',
163  *         highlightCssClass: 'smart-label-outline smart-label-polygon',
164  *         useMathJax: false
165  *     });
166  *
167  *     })();
168  *
169  * </script><pre>
170  *
171  * @example
172  * var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false});
173  * var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', unit: '°', useMathJax: false});
174  *
175  * </pre><div id="JXG48d6d1ae-e04a-45f4-a743-273976712c0b" class="jxgbox" style="width: 300px; height: 300px;"></div>
176  * <script type="text/javascript">
177  *     (function() {
178  *         var board = JXG.JSXGraph.initBoard('JXG48d6d1ae-e04a-45f4-a743-273976712c0b',
179  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
180  *     var a1 = board.create('angle', [[1, -1], [1, 2], [1, 5]], {name: 'β', withLabel: false});
181  *     var sma = board.create('smartlabel', [a1], {digits: 1, prefix: a1.name + '=', unit: '°', useMathJax: false});
182  *
183  *     })();
184  *
185  * </script><pre>
186  *
187  */
188 JXG.createSmartLabel = function (board, parents, attributes) {
189     var el, attr,
190         p, user_supplied_text,
191         getTextFun, txt_fun;
192 
193     if (parents.length === 0 || (
194         [Const.OBJECT_CLASS_POINT, Const.OBJECT_CLASS_LINE,Const.OBJECT_CLASS_CIRCLE].indexOf(parents[0].elementClass) < 0 &&
195         [Const.OBJECT_TYPE_POLYGON, Const.OBJECT_TYPE_ANGLE].indexOf(parents[0].type) < 0
196         )
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     if (p.elementClass === Const.OBJECT_CLASS_POINT) {
209         attr = Type.copyAttributes(attributes, board.options, 'smartlabelpoint');
210 
211     } else if (p.elementClass === Const.OBJECT_CLASS_LINE) {
212         attr = Type.copyAttributes(attributes, board.options, 'smartlabelline');
213         /**
214          * @class
215          * @ignore
216          */
217         attr.rotate = function () { return Math.atan(p.getSlope()) * 180 / Math.PI; };
218         /**
219          * @class
220          * @ignore
221          */
222         attr.visible = function () { return (p.L() < 1.5) ? false : true; };
223 
224     } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) {
225         attr = Type.copyAttributes(attributes, board.options, 'smartlabelcircle');
226         /**
227          * @class
228          * @ignore
229          */
230         attr.visible = function () { return (p.Radius() < 1.5) ? false : true; };
231 
232     } else if (p.type === Const.OBJECT_TYPE_POLYGON) {
233         attr = Type.copyAttributes(attributes, board.options, 'smartlabelpolygon');
234     } else if (p.type === Const.OBJECT_TYPE_ANGLE) {
235         attr = Type.copyAttributes(attributes, board.options, 'smartlabelangle');
236         /**
237          * @class
238          * @ignore
239          */
240         attr.rotate = function () {
241             var c1 = p.center.coords.usrCoords,
242                 c2 = p.getLabelAnchor().usrCoords,
243                 v = Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI;
244             return (v > 90 && v < 270) ? v + 180 : v;
245         };
246         /**
247          * @class
248          * @ignore
249          */
250         attr.anchorX = function () {
251             var c1 = p.center.coords.usrCoords,
252                 c2 = p.getLabelAnchor().usrCoords,
253                 v = Math.atan2(c2[2] - c1[2], c2[1] - c1[1]) * 180 / Math.PI;
254             return (v > 90 && v < 270) ? 'right' : 'left';
255         };
256     }
257 
258     getTextFun = function (el, p, elType, mType) {
259         var measure;
260         switch (mType) {
261             case 'length':
262                 /**
263                  * @ignore
264                  */
265                 measure = function () { return p.L(); };
266                 break;
267             case 'slope':
268                 /**
269                  * @ignore
270                  */
271                 measure = function () { return p.Slope(); };
272                 break;
273             case 'area':
274                 /**
275                  * @ignore
276                  */
277                 measure = function () { return p.Area(); };
278                 break;
279             case 'radius':
280                 /**
281                  * @ignore
282                  */
283                 measure = function () { return p.Radius(); };
284                 break;
285             case 'perimeter':
286                 /**
287                  * @ignore
288                  */
289                 measure = function () { return p.Perimeter(); };
290                 break;
291             case 'rad':
292                 /**
293                  * @ignore
294                  */
295                 measure = function () { return p.Value(); };
296                 break;
297             case 'deg':
298                 /**
299                  * @ignore
300                  */
301                 measure = function () { return p.Value() * 180 / Math.PI; };
302                 break;
303             default:
304                 /**
305                  * @ignore
306                  */
307                 measure = function () { return 0.0; };
308         }
309 
310         return function () {
311             var str = '',
312                 val,
313                 txt = Type.evaluate(user_supplied_text),
314                 digits = el.evalVisProp('digits'),
315                 u = el.evalVisProp('unit'),
316                 pre = el.evalVisProp('prefix'),
317                 suf = el.evalVisProp('suffix'),
318                 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex');
319 
320             if (txt === '') {
321                 if (el.useLocale()) {
322                     val = el.formatNumberLocale(measure(), digits);
323                 } else {
324                     val = Type.toFixed(measure(), digits);
325                 }
326                 if (mj) {
327                     str = ['\\(', pre, val, '\\,', u, suf, '\\)'].join('');
328                 } else {
329                     str = [pre, val, u, suf].join('');
330                 }
331             } else {
332                 str = txt;
333             }
334             return str;
335         };
336     };
337 
338     if (p.elementClass === Const.OBJECT_CLASS_POINT) {
339         el = board.create('text', [
340             function () { return p.X(); },
341             function () { return p.Y(); },
342             ''
343         ], attr);
344 
345         txt_fun = function () {
346             var str = '',
347                 txt = Type.evaluate(user_supplied_text),
348                 digits = el.evalVisProp('digits'),
349                 u = el.evalVisProp('unit'),
350                 pre = el.evalVisProp('prefix'),
351                 suf = el.evalVisProp('suffix'),
352                 dir = el.evalVisProp('dir'),
353                 mj = el.evalVisProp('usemathjax') || el.evalVisProp('usekatex'),
354                 x, y;
355 
356             if (el.useLocale()) {
357                 x = el.formatNumberLocale(p.X(), digits);
358                 y = el.formatNumberLocale(p.Y(), digits);
359             } else {
360                 x = Type.toFixed(p.X(), digits);
361                 y = Type.toFixed(p.Y(), digits);
362             }
363 
364             if (txt === '') {
365                 if (dir === 'row') {
366                     if (mj) {
367                         str = ['\\(', pre, x, '\\,', u, ' / ', y, '\\,', u, suf, '\\)'].join('');
368                     } else {
369                         str = [pre, x, ' ', u, ' / ', y, ' ', u, suf].join('');
370                     }
371                 } else if (dir.indexOf('col') === 0) { // Starts with 'col'
372                     if (mj) {
373                         str = ['\\(', pre, '\\left(\\array{', x, '\\,', u, '\\\\ ', y, '\\,', u, '}\\right)', suf, '\\)'].join('');
374                     } else {
375                         str = [pre, x, ' ', u, '<br/>', y, ' ', u, suf].join('');
376                     }
377                 }
378             } else {
379                 str = txt;
380             }
381             return str;
382         };
383 
384     } else if (p.elementClass === Const.OBJECT_CLASS_LINE) {
385 
386         if (attr.measure === 'length') {
387             el = board.create('text', [
388                 function () { return (p.point1.X() + p.point2.X()) * 0.5; },
389                 function () { return (p.point1.Y() + p.point2.Y()) * 0.5; },
390                 ''
391             ], attr);
392             txt_fun = getTextFun(el, p, 'line', 'length');
393 
394         } else if (attr.measure === 'slope') {
395             el = board.create('text', [
396                 function () { return (p.point1.X() * 0.25 + p.point2.X() * 0.75); },
397                 function () { return (p.point1.Y() * 0.25 + p.point2.Y() * 0.75); },
398                 ''
399             ], attr);
400             txt_fun = getTextFun(el, p, 'line', 'slope');
401         }
402 
403     } else if (p.elementClass === Const.OBJECT_CLASS_CIRCLE) {
404         if (attr.measure === 'radius') {
405             el = board.create('text', [
406                 function () { return p.center.X() + p.Radius() * 0.5; },
407                 function () { return p.center.Y(); },
408                 ''
409             ], attr);
410             txt_fun = getTextFun(el, p, 'circle', 'radius');
411 
412         } else if (attr.measure === 'area') {
413             el = board.create('text', [
414                 function () { return p.center.X(); },
415                 function () { return p.center.Y() + p.Radius() * 0.5; },
416                 ''
417             ], attr);
418             txt_fun = getTextFun(el, p, 'circle', 'area');
419 
420         } else if (attr.measure === 'circumference' || attr.measure === 'perimeter') {
421             el = board.create('text', [
422                 function () { return p.getLabelAnchor(); },
423                 ''
424             ], attr);
425             txt_fun = getTextFun(el, p, 'circle', 'perimeter');
426 
427         }
428     } else if (p.type === Const.OBJECT_TYPE_POLYGON) {
429         if (attr.measure === 'area') {
430             el = board.create('text', [
431                 function () { return p.getTextAnchor(); },
432                 ''
433             ], attr);
434             txt_fun = getTextFun(el, p, 'polygon', 'area');
435 
436         } else if (attr.measure === 'perimeter') {
437             el = board.create('text', [
438                 function () {
439                     var last = p.borders.length - 1;
440                     if (last >= 0) {
441                         return [
442                             (p.borders[last].point1.X() + p.borders[last].point2.X()) * 0.5,
443                             (p.borders[last].point1.Y() + p.borders[last].point2.Y()) * 0.5
444                         ];
445                     } else {
446                         return p.getTextAnchor();
447                     }
448                 },
449                 ''
450             ], attr);
451             txt_fun = getTextFun(el, p, 'polygon', 'perimeter');
452         }
453 
454     } else if (p.type === Const.OBJECT_TYPE_ANGLE) {
455         el = board.create('text', [
456             function () {
457                 return p.getLabelAnchor();
458             },
459             ''
460         ], attr);
461         txt_fun = getTextFun(el, p, 'angle', attr.measure);
462     }
463 
464     if (Type.exists(el)) {
465         el.setText(txt_fun);
466         p.addChild(el);
467         el.setParents([p]);
468     }
469 
470     return el;
471 };
472 
473 JXG.registerElement("smartlabel", JXG.createSmartLabel);
474