      1 /*
      2     Copyright 2008-2025
      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 <https://www.gnu.org/licenses/>
     29     and <https://opensource.org/licenses/MIT/>.
     30  */
     31
     32 /*global JXG: true, define: true*/
     33 /*jslint nomen: true, plusplus: true*/
     34
     35 /**
     36  * @fileoverview Geometry objects for measurements are defined in this file. This file stores all
     37  * style and functional properties that are required to use a tape measure on
     38  * a board.
     39  */
     40
     41 import JXG from "../jxg.js";
     42 import Type from "../utils/type.js";
     43 import GeometryElement from "../base/element.js";
     44 import Prefix from "../parser/prefix.js";
     45
     46 /**
     47  * @class A tape measure can be used to measure distances between points.
     48  * <p>
     49  * The two defining points of the tape measure (which is a segment) do not inherit by default the attribute "visible" from
     50  * the segment. Otherwise the tape meassure would be inaccessible if the two points coincide and the segment is hidden.
     51  *
     52  * @pseudo
     53  * @name Tapemeasure
     54  * @augments Segment
     55  * @constructor
     56  * @type JXG.Segment
     57  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
     58  * @param {Array_Array} start,end, The two arrays give the initial position where the tape measure
     59  * is drawn on the board.
     60  * @example
     61  * // Create a tape measure
     62  * var p1 = board.create('point', [0,0]);
     63  * var p2 = board.create('point', [1,1]);
     64  * var p3 = board.create('point', [3,1]);
     65  * var tape = board.create('tapemeasure', [[1, 2], [4, 2]], {name:'dist'});
     66  * </pre><div class="jxgbox" id="JXG6d9a2cda-22fe-4cd1-9d94-34283b1bdc01" style="width: 200px; height: 200px;"></div>
     67  * <script type="text/javascript">
     68  *   (function () {
     69  *     var board = JXG.JSXGraph.initBoard('JXG6d9a2cda-22fe-4cd1-9d94-34283b1bdc01', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false});
     70  *     var p1 = board.create('point', [0,0]);
     71  *     var p2 = board.create('point', [1,1]);
     72  *     var p3 = board.create('point', [3,1]);
     73  *     var tape = board.create('tapemeasure', [[1, 2], [4, 2]], {name:'dist'} );
     74  *   })();
     75  * </script><pre>
     76  */
     77 JXG.createTapemeasure = function (board, parents, attributes) {
     78     var pos0, pos1, attr, withTicks, withText, digits, li, p1, p2, n, ti;
     79
     80     pos0 = parents[0];
     81     pos1 = parents[1];
     82
     83     // start point
     84     attr = Type.copyAttributes(attributes, board.options, "tapemeasure", "point1");
     85     p1 = board.create("point", pos0, attr);
     86
     87     // end point
     88     attr = Type.copyAttributes(attributes, board.options, "tapemeasure", "point2");
     89     p2 = board.create("point", pos1, attr);
     90
     91     p1.setAttribute({ignoredSnapToPoints: [p2.id]});
     92     p2.setAttribute({ignoredSnapToPoints: [p1.id]});
     93
     94     // tape measure line
     95     attr = Type.copyAttributes(attributes, board.options, "tapemeasure");
     96     withTicks = attr.withticks;
     97     withText = attr.withlabel;
     98     digits = attr.digits;
     99
    100     if (digits === 2 && attr.precision !== 2) {
    101         // Backward compatibility
    102         digits = attr.precision;
    103     }
    104
    105     // Below, we will replace the label by the measurement function.
    106     if (withText) {
    107         attr.withlabel = true;
    108     }
    109     li = board.create("segment", [p1, p2], attr);
    110     // p1, p2 are already added to li.inherits
    111
    112     if (withText) {
    113         if (attributes.name && attributes.name !== "") {
    114             n = attributes.name + " = ";
    115         } else {
    116             n = "";
    117         }
    118         li.label.setText(function () {
    119             var digits = li.label.evalVisProp('digits');
    120
    121             if (li.label.useLocale()) {
    122                 return n + li.label.formatNumberLocale(p1.Dist(p2), digits);
    123             }
    124             return n + Type.toFixed(p1.Dist(p2), digits);
    125         });
    126     }
    127
    128     if (withTicks) {
    129         attr = Type.copyAttributes(attributes, board.options, "tapemeasure", "ticks");
    130         //ticks  = 2;
    131         ti = board.create("ticks", [li, 0.1], attr);
    132         li.inherits.push(ti);
    133     }
    134
    135     // override the segments's remove method to ensure the removal of all elements
    136     /** @ignore */
    137     li.remove = function () {
    138         if (withTicks) {
    139             li.removeTicks(ti);
    140         }
    141
    142         board.removeObject(p2);
    143         board.removeObject(p1);
    144
    145         GeometryElement.prototype.remove.call(this);
    146     };
    147
    148     /**
    149      * Returns the length of the tape measure.
    150      * @name Value
    151      * @memberOf Tapemeasure.prototype
    152      * @function
    153      * @returns {Number} length of tape measure.
    154      */
    155     li.Value = function () {
    156         return p1.Dist(p2);
    157     };
    158
    159     p1.dump = false;
    160     p2.dump = false;
    161
    162     li.elType = "tapemeasure";
    163     li.getParents = function () {
    164         return [
    165             [p1.X(), p1.Y()],
    166             [p2.X(), p2.Y()]
    167         ];
    168     };
    169
    170     li.subs = {
    171         point1: p1,
    172         point2: p2
    173     };
    174
    175     if (withTicks) {
    176         ti.dump = false;
    177     }
    178
    179     li.methodMap = JXG.deepCopy(li.methodMap, {
    180         Value: "Value"
    181     });
    182
    183     li.prepareUpdate().update();
    184     if (!board.isSuspendedUpdate) {
    185         li.updateVisibility().updateRenderer();
    186         // The point updates are necessary in case of snapToGrid==true
    187         li.point1.updateVisibility().updateRenderer();
    188         li.point2.updateVisibility().updateRenderer();
    189     }
    190
    191     return li;
    192 };
    193
    194 JXG.registerElement("tapemeasure", JXG.createTapemeasure);
    195
    196 /**
    197  * @class Display measurements of geometric elements and the arithmetic operations of measurements.
    198  * Under the hood this is a text element which has a method Value. The text to be displayed
    199  * is the result of the evaluation of a prefix expression, see {@link JXG.PrefixParser}.
    200  * <p>
    201  * The purpose of this element is to display values of measurements of geometric objects, like the radius of a circle,
    202  * as well as expressions consisting of measurements.
    203  *
    204  * @pseudo
    205  * @name Measurement
    206  * @augments Text
    207  * @constructor
    208  * @type JXG.Text
    209  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    210  * @param {Point|Array_Point|Array_Array} x,y,expression
    211  * Here, expression is a prefix expression, see {@link JXG.PrefixParser}.
    212  * @example
    213  * var p1 = board.create('point', [1, 1]);
    214  * var p2 = board.create('point', [1, 3]);
    215  * var ci1 = board.create('circle', [p1, p2]);
    216  *
    217  * var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
    218  *     visible: true,
    219  *     prefix: 'area: ',
    220  *     baseUnit: 'cm'
    221  * });
    222  *
    223  * var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
    224  *     prefix: 'radius: ',
    225  *     baseUnit: 'cm'
    226  * });
    227  *
    228  * </pre><div id="JXG6359237a-79bc-4689-92fc-38d3ebeb769d" class="jxgbox" style="width: 300px; height: 300px;"></div>
    229  * <script type="text/javascript">
    230  *     (function() {
    231  *         var board = JXG.JSXGraph.initBoard('JXG6359237a-79bc-4689-92fc-38d3ebeb769d',
    232  *             {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright: false, shownavigation: false});
    233  *     var p1 = board.create('point', [1, 1]);
    234  *     var p2 = board.create('point', [1, 3]);
    235  *     var ci1 = board.create('circle', [p1, p2]);
    236  *
    237  *     var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
    238  *         visible: true,
    239  *         prefix: 'area: ',
    240  *         baseUnit: 'cm'
    241  *     });
    242  *
    243  *     var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
    244  *         prefix: 'radius: ',
    245  *         baseUnit: 'cm'
    246  *     });
    247  *
    248  *     })();
    249  *
    250  * </script><pre>
    251  *
    252  * @example
    253  * var p1 = board.create('point', [1, 1]);
    254  * var p2 = board.create('point', [1, 3]);
    255  * var ci1 = board.create('circle', [p1, p2]);
    256  * var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
    257  * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
    258  *
    259  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
    260  *     prefix: 'm1: ',
    261  *     baseUnit: 'cm'
    262  * });
    263  *
    264  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
    265  *     prefix: 'm2: ',
    266  *     baseUnit: 'cm'
    267  * });
    268  *
    269  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
    270  *     prefix: 'm3: ',
    271  *     baseUnit: 'cm',
    272  *     dim: 1
    273  * });
    274  *
    275  * var m4 = board.create('measurement', [2, -6,
    276  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
    277  *     ], {
    278  *     prefix: 'm4: ',
    279  *     baseUnit: 'cm'
    280  * });
    281  *
    282  * </pre><div id="JXG49903663-6450-401e-b0d9-f025a6677d4a" class="jxgbox" style="width: 300px; height: 300px;"></div>
    283  * <script type="text/javascript">
    284  *     (function() {
    285  *         var board = JXG.JSXGraph.initBoard('JXG49903663-6450-401e-b0d9-f025a6677d4a',
    286  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    287  *     var p1 = board.create('point', [1, 1]);
    288  *     var p2 = board.create('point', [1, 3]);
    289  *     var ci1 = board.create('circle', [p1, p2]);
    290  *     var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
    291  *     var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
    292  *
    293  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
    294  *     prefix: 'm1: ',
    295  *     baseUnit: 'cm'
    296  * });
    297  *
    298  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
    299  *     prefix: 'm2: ',
    300  *     baseUnit: 'cm'
    301  * });
    302  *
    303  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
    304  *     prefix: 'm3: ',
    305  *     baseUnit: 'cm',
    306  *     dim: 1
    307  * });
    308  *
    309  * var m4 = board.create('measurement', [2, -6,
    310  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
    311  *     ], {
    312  *     prefix: 'm4: ',
    313  *     baseUnit: 'cm'
    314  * });
    315  *
    316  *     })();
    317  *
    318  * </script><pre>
    319  *
    320  */
    321 JXG.createMeasurement = function (board, parents, attributes) {
    322     var el, attr,
    323         x, y, term,
    324         i;
    325
    326     attr = Type.copyAttributes(attributes, board.options, "measurement");
    327
    328     x = parents[0];
    329     y = parents[1];
    330     term = parents[2];
    331
    332     el = board.create("text", [x, y, ''], attr);
    333     el.type = Type.OBJECT_TYPE_MEASUREMENT;
    334     el.elType = 'measurement';
    335
    336     el.Value = function () {
    337         return Prefix.parse(term, 'execute');
    338     };
    339
    340     el.Dimension = function () {
    341         var d = el.evalVisProp('dim');
    342
    343         if (d !== null) {
    344             return d;
    345         }
    346         return Prefix.dimension(term);
    347     };
    348
    349     el.Unit = function (dimension) {
    350         var unit = '',
    351             units = el.evalVisProp('units'),
    352             dim = dimension,
    353             dims = {}, i;
    354
    355         if (!Type.exists(dim)) {
    356             dim = el.Dimension();
    357         }
    358
    359         if (Type.isArray(dimension)) {
    360             for (i = 0; i < dimension.length; i++) {
    361                 dims['dim' + dimension[i]] = el.Unit(dimension[i]);
    362             }
    363             return dims;
    364         }
    365
    366         if (Type.isObject(units) && Type.exists(units[dim]) && units[dim] !== false) {
    367             unit = el.eval(units[dim]);
    368         } else if (Type.isObject(units) && Type.exists(units['dim' + dim]) && units['dim' + dim] !== false) {
    369             // In some cases, object keys must not be numbers. This allows key 'dim1' instead of '1'.
    370             unit = el.eval(units['dim' + dim]);
    371         } else {
    372             unit = el.evalVisProp('baseunit');
    373
    374             if (dim === 0) {
    375                 unit = '';
    376             } else if (dim > 1 && unit !== '') {
    377                 unit = unit + '^{' + dim + '}';
    378             }
    379         }
    380
    381         return unit;
    382     };
    383
    384     el.getTerm = function () {
    385         return term;
    386     };
    387
    388     el.getMethod = function () {
    389         var method = term[0];
    390         if (method === "V") {
    391             method = "Value";
    392         }
    393         return method;
    394     };
    395
    396     el.toPrefix = function () {
    397         return Prefix.toPrefix(term);
    398     };
    399
    400     el.getParents = function () {
    401         return Prefix.getParents(term);
    402     };
    403     el.addParents(el.getParents());
    404     for (i = 0; i < el.parents.length; i++) {
    405         board.select(el.parents[i]).addChild(el);
    406     }
    407
    408     /**
    409      * @class
    410      * @ignore
    411      */
    412     el.setText(function () {
    413         var prefix = '',
    414             suffix = '',
    415             dim = el.Dimension(),
    416             digits = el.evalVisProp('digits'),
    417             unit = el.Unit(),
    418             val = el.Value(),
    419             i;
    420
    421         if (el.evalVisProp('showprefix')) {
    422             prefix = el.evalVisProp('prefix');
    423         }
    424         if (el.evalVisProp('showsuffix')) {
    425             suffix = el.evalVisProp('suffix');
    426         }
    427
    428         if (Type.isNumber(val)) {
    429             if (digits === 'none') {
    430                 // do nothing
    431             } else if (digits === 'auto') {
    432                 if (el.useLocale()) {
    433                     val = el.formatNumberLocale(val);
    434                 } else {
    435                     val = Type.autoDigits(val);
    436                 }
    437             } else {
    438                 if (el.useLocale()) {
    439                     val = el.formatNumberLocale(val, digits);
    440                 } else {
    441                     val = Type.toFixed(val, digits);
    442                 }
    443             }
    444         } else if (Type.isArray(val)) {
    445             for (i = 0; i < val.length; i++) {
    446                 if (!Type.isNumber(val[i])) {
    447                     continue;
    448                 }
    449                 if (digits === 'none') {
    450                     // do nothing
    451                 } else if (digits === 'auto') {
    452                     if (el.useLocale()) {
    453                         val[i] = el.formatNumberLocale(val[i]);
    454                     } else {
    455                         val[i] = Type.autoDigits(val[i]);
    456                     }
    457                 } else {
    458                     if (el.useLocale()) {
    459                         val[i] = el.formatNumberLocale(val[i], digits);
    460                     } else {
    461                         val[i] = Type.toFixed(val[i], digits);
    462                     }
    463                 }
    464             }
    465         }
    466
    467         if (dim === 'coords' && Type.isArray(val)) {
    468             if (val.length === 2) {
    469                 val.unshift(undefined);
    470             }
    471             val = el.visProp.formatcoords(el, val[1], val[2], val[0]);
    472         }
    473
    474         if (dim === 'direction' && Type.isArray(val)) {
    475             val = el.visProp.formatdirection(el, val[0], val[1]);
    476         }
    477
    478         if (Type.isString(dim)) {
    479             return prefix + val + suffix;
    480         }
    481
    482         if (isNaN(dim)) {
    483             return prefix + 'NaN' + suffix;
    484         }
    485
    486         return prefix + val + unit + suffix;
    487     });
    488
    489     el.methodMap = Type.deepCopy(el.methodMap, {
    490         Value: "Value",
    491         Dimension: "Dimension",
    492         Unit: "Unit",
    493         getTerm: "getTerm",
    494         Term: "getTerm",
    495         getMethod: "getMethod",
    496         Method: "getMethod",
    497         getParents: "getParents",
    498         Parents: "getParents"
    499     });
    500
    501     return el;
    502 };
    503
    504 JXG.registerElement("measurement", JXG.createMeasurement);
    505