      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 = Type.evaluate(el.visProp.digits),
    315                 u = Type.evaluate(el.visProp.unit),
    316                 pre = Type.evaluate(el.visProp.prefix),
    317                 suf = Type.evaluate(el.visProp.suffix),
    318                 mj = Type.evaluate(el.visProp.usemathjax) || Type.evaluate(el.visProp.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 = Type.evaluate(el.visProp.digits),
    349                 u = Type.evaluate(el.visProp.unit),
    350                 pre = Type.evaluate(el.visProp.prefix),
    351                 suf = Type.evaluate(el.visProp.suffix),
    352                 dir = Type.evaluate(el.visProp.dir),
    353                 mj = Type.evaluate(el.visProp.usemathjax) || Type.evaluate(el.visProp.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