      1 /*
      2     Copyright 2008-2024
      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 = Type.evaluate(li.label.visProp.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 Measurement element. Under the hood this is a text element which has a method Value. The text to be displayed
    198  * is the result of the evaluation of a prefix expression, see {@link JXG.PrefixParser}.
    199  * <p>
    200  * The purpose of this element is to display values of measurements of geometric objects, like the radius of a circle,
    201  * as well as expressions consisting of measurements.
    202  *
    203  * @pseudo
    204  * @name Measurement
    205  * @augments Text
    206  * @constructor
    207  * @type JXG.Text
    208  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    209  * @param {Point|Array_Point|Array_Array} x,y,expression
    210  * Here, expression is a prefix expression, see {@link JXG.PrefixParser}.
    211  * @example
    212  * var p1 = board.create('point', [1, 1]);
    213  * var p2 = board.create('point', [1, 3]);
    214  * var ci1 = board.create('circle', [p1, p2]);
    215  *
    216  * var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
    217  *     visible: true,
    218  *     prefix: 'area: ',
    219  *     baseUnit: 'cm'
    220  * });
    221  *
    222  * var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
    223  *     prefix: 'radius: ',
    224  *     baseUnit: 'cm'
    225  * });
    226  *
    227  * </pre><div id="JXG6359237a-79bc-4689-92fc-38d3ebeb769d" class="jxgbox" style="width: 300px; height: 300px;"></div>
    228  * <script type="text/javascript">
    229  *     (function() {
    230  *         var board = JXG.JSXGraph.initBoard('JXG6359237a-79bc-4689-92fc-38d3ebeb769d',
    231  *             {boundingbox: [-5, 5, 5, -5], axis: true, showcopyright: false, shownavigation: false});
    232  *     var p1 = board.create('point', [1, 1]);
    233  *     var p2 = board.create('point', [1, 3]);
    234  *     var ci1 = board.create('circle', [p1, p2]);
    235  *
    236  *     var m1 = board.create('measurement', [1, -2, ['Area', ci1]], {
    237  *         visible: true,
    238  *         prefix: 'area: ',
    239  *         baseUnit: 'cm'
    240  *     });
    241  *
    242  *     var m2 = board.create('measurement', [1, -4, ['Radius', ci1]], {
    243  *         prefix: 'radius: ',
    244  *         baseUnit: 'cm'
    245  *     });
    246  *
    247  *     })();
    248  *
    249  * </script><pre>
    250  *
    251  * @example
    252  * var p1 = board.create('point', [1, 1]);
    253  * var p2 = board.create('point', [1, 3]);
    254  * var ci1 = board.create('circle', [p1, p2]);
    255  * var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
    256  * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
    257  *
    258  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
    259  *     prefix: 'm1: ',
    260  *     baseUnit: 'cm'
    261  * });
    262  *
    263  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
    264  *     prefix: 'm2: ',
    265  *     baseUnit: 'cm'
    266  * });
    267  *
    268  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
    269  *     prefix: 'm3: ',
    270  *     baseUnit: 'cm',
    271  *     dim: 1
    272  * });
    273  *
    274  * var m4 = board.create('measurement', [2, -6,
    275  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
    276  *     ], {
    277  *     prefix: 'm4: ',
    278  *     baseUnit: 'cm'
    279  * });
    280  *
    281  * </pre><div id="JXG49903663-6450-401e-b0d9-f025a6677d4a" class="jxgbox" style="width: 300px; height: 300px;"></div>
    282  * <script type="text/javascript">
    283  *     (function() {
    284  *         var board = JXG.JSXGraph.initBoard('JXG49903663-6450-401e-b0d9-f025a6677d4a',
    285  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
    286  *     var p1 = board.create('point', [1, 1]);
    287  *     var p2 = board.create('point', [1, 3]);
    288  *     var ci1 = board.create('circle', [p1, p2]);
    289  *     var seg = board.create('segment', [[-2,-3], [-2, 3]], { firstArrow: true, lastArrow: true});
    290  *     var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], {name:'a'});
    291  *
    292  * var m1 = board.create('measurement', [-6, -2, ['Radius', ci1]], {
    293  *     prefix: 'm1: ',
    294  *     baseUnit: 'cm'
    295  * });
    296  *
    297  * var m2 = board.create('measurement', [-6, -4, ['L', seg]], {
    298  *     prefix: 'm2: ',
    299  *     baseUnit: 'cm'
    300  * });
    301  *
    302  * var m3 = board.create('measurement', [-6, -6, ['V', sli]], {
    303  *     prefix: 'm3: ',
    304  *     baseUnit: 'cm',
    305  *     dim: 1
    306  * });
    307  *
    308  * var m4 = board.create('measurement', [2, -6,
    309  *         ['+', ['V', m1], ['V', m2], ['V', m3]]
    310  *     ], {
    311  *     prefix: 'm4: ',
    312  *     baseUnit: 'cm'
    313  * });
    314  *
    315  *     })();
    316  *
    317  * </script><pre>
    318  *
    319  */
    320 JXG.createMeasurement = function (board, parents, attributes) {
    321     var el, attr,
    322         x, y, term,
    323         i;
    324
    325     attr = Type.copyAttributes(attributes, board.options, "measurement");
    326
    327     x = parents[0];
    328     y = parents[1];
    329     term = parents[2];
    330
    331     el = board.create("text", [x, y, ''], attr);
    332     el.type = Type.OBJECT_TYPE_MEASUREMENT;
    333     el.elType = 'measurement';
    334
    335     el.Value = function () {
    336         return Prefix.parse(term, 'execute');
    337     };
    338
    339     el.Dimension = function () {
    340         var d = Type.evaluate(el.visProp.dim);
    341
    342         if (d !== null) {
    343             return d;
    344         }
    345         return Prefix.dimension(term);
    346     };
    347
    348     el.Unit = function () {
    349         var unit = '',
    350             units = Type.evaluate(el.visProp.units),
    351             dim = el.Dimension();
    352
    353         if (Type.isObject(units) && Type.exists(units[dim]) && units[dim] !== false) {
    354             unit = Type.evaluate(units[dim]);
    355         } else if (Type.isObject(units) && Type.exists(units['dim' + dim]) && units['dim' + dim] !== false) {
    356             // In some cases, object keys must not be numbers. This allows key 'dim1' instead of '1'.
    357             unit = Type.evaluate(units['dim' + dim]);
    358         } else {
    359             unit = Type.evaluate(el.visProp.baseunit);
    360
    361             if (dim === 0) {
    362                 unit = '';
    363             } else if (dim > 1 && unit !== '') {
    364                 unit = unit + '^{' + dim + '}';
    365             }
    366         }
    367
    368         return unit;
    369     };
    370
    371     el.getTerm = function () {
    372         return term;
    373     };
    374
    375     el.getMethod = function () {
    376         var method = term[0];
    377         if (method === "V") {
    378             method = "Value";
    379         }
    380         return method;
    381     };
    382
    383     el.toPrefix = function () {
    384         return Prefix.toPrefix(term);
    385     };
    386
    387     el.getParents = function () {
    388         return Prefix.getParents(term);
    389     };
    390     el.addParents(el.getParents());
    391     for (i = 0; i < el.parents.length; i++) {
    392         board.select(el.parents[i]).addChild(el);
    393     }
    394
    395     /**
    396      * @class
    397      * @ignore
    398      */
    399     el.setText(function () {
    400         var prefix = '',
    401             suffix = '',
    402             dim = el.Dimension(),
    403             digits = Type.evaluate(el.visProp.digits),
    404             unit = el.Unit(),
    405             val = el.Value(),
    406             i;
    407
    408         if (Type.evaluate(el.visProp.showprefix)) {
    409             prefix = el.visProp.formatprefix.apply(el, [Type.evaluate(el.visProp.prefix)]);
    410         }
    411         if (Type.evaluate(el.visProp.showsuffix)) {
    412             suffix = el.visProp.formatsuffix.apply(el, [Type.evaluate(el.visProp.suffix)]);
    413         }
    414
    415         if (Type.isNumber(val)) {
    416             if (digits === 'none') {
    417                 // do nothing
    418             } else if (digits === 'auto') {
    419                 if (el.useLocale()) {
    420                     val = el.formatNumberLocale(val);
    421                 } else {
    422                     val = Type.autoDigits(val);
    423                 }
    424             } else {
    425                 if (el.useLocale()) {
    426                     val = el.formatNumberLocale(val, digits);
    427                 } else {
    428                     val = Type.toFixed(val, digits);
    429                 }
    430             }
    431         } else if (Type.isArray(val)) {
    432             for (i = 0; i < val.length; i++) {
    433                 if (!Type.isNumber(val[i])) {
    434                     continue;
    435                 }
    436                 if (digits === 'none') {
    437                     // do nothing
    438                 } else if (digits === 'auto') {
    439                     if (el.useLocale()) {
    440                         val[i] = el.formatNumberLocale(val[i]);
    441                     } else {
    442                         val[i] = Type.autoDigits(val[i]);
    443                     }
    444                 } else {
    445                     if (el.useLocale()) {
    446                         val[i] = el.formatNumberLocale(val[i], digits);
    447                     } else {
    448                         val[i] = Type.toFixed(val[i], digits);
    449                     }
    450                 }
    451             }
    452         }
    453
    454         if (dim === 'coords' && Type.isArray(val)) {
    455             if (val.length === 2) {
    456                 val.unshift(undefined);
    457             }
    458             val = el.visProp.formatcoords.apply(el, [val[1], val[2], val[0]]);
    459         }
    460
    461         if (dim === 'direction' && Type.isArray(val)) {
    462             val = el.visProp.formatdirection.apply(el, [val[0], val[1]]);
    463         }
    464
    465         if (Type.isString(dim)) {
    466             return prefix + val + suffix;
    467         }
    468
    469         if (isNaN(dim)) {
    470             return prefix + 'NaN' + suffix;
    471         }
    472
    473         return prefix + val + unit + suffix;
    474     });
    475
    476     el.methodMap = Type.deepCopy(el.methodMap, {
    477         Value: "Value",
    478         Dimension: "Dimension",
    479         Unit: "Unit",
    480         getTerm: "getTerm",
    481         Term: "getTerm",
    482         getMethod: "getMethod",
    483         Method: "getMethod",
    484         getParents: "getParents",
    485         Parents: "getParents"
    486     });
    487
    488     return el;
    489 };
    490
    491 JXG.registerElement("measurement", JXG.createMeasurement);
    492