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