      1 /*
      2     Copyright 2008-2023
      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 /**
     30  * @fileoverview Simple prefix parser for measurements and expressions of measurements.
     31  * An expression is given as
     32  * <ul>
     33  * <li> array starting with an operator as first element, followed
     34  * by one or more operands,
     35  * <li> number.
     36  * </ul>
     37  * <p>
     38  * Possible operands are:
     39  * <ul>
     40  * <li> '+', '-', '*', '/'
     41  * </ul>
     42  *
     43  * @example
     44  *
     45  */
     46 import JXG from "../jxg";
     47 import Type from "../utils/type";
     48 import Mat from "../math/math";
     49 import Const from "../base/constants";
     50
     51 /**
     52  * Prefix expression parser, i.e. a poor man's parser.
     53  * This is a simple prefix parser for measurements and expressions of measurements,
     54  * see {@link Measurement}.
     55  * An expression is given as
     56  * <ul>
     57  * <li> array starting with an operator as first element, followed
     58  * by one or more operands,
     59  * <li> number.
     60  * </ul>
     61  * <p>
     62  * Possible operators are:
     63  * <ul>
     64  * <li> '+', '-', '*', '/': binary operators
     65  * <li> 'Area', 'Radius', 'Value', 'V', 'L': arbitrary methods of JSXGraph elements, supplied as strings.
     66  * <li> 'exec': call a function
     67  * </ul>
     68  * <p>
     69  * Possible operands are:
     70  * <ul>
     71  * <li> numbers
     72  * <li> strings
     73  * <li> JSXGraph elements in case the operator is a method. Example: ['Area', circle] calls
     74  * the method circle.Area().
     75  * <li> prefix expressions (for binary operators)
     76  * <li> 'exec': call functions. Example: ['exec', 'sin', ['V', slider]] computes 'Math.sin(slider.Value())'.
     77  * As functions only functions in Math or JXG.Math are allowed.
     78  * </ul>
     79  * @namespace
     80  *
     81  * @example
     82  *   ['+', 100, 200]
     83  * @example
     84  * var p1 = board.create('point', [1, 1]);
     85  * var p2 = board.create('point', [1, 3]);
     86  * var seg = board.create('segment', [[-2,-3], [-2, 3]]);
     87  *
     88  * // Valid prefix expression: ['L', seg]
     89  *
     90  * @example
     91  * var p1 = board.create('point', [1, 1]);
     92  * var p2 = board.create('point', [1, 3]);
     93  * var seg = board.create('segment', [[-2,-3], [-2, 3]]);
     94  * var ci = board.create('circle', [p1, 7]);
     95  *
     96  * // Valid prefix expression:  ['+', ['Radius', ci], ['L', seg]]
     97  *
     98  * @example
     99  * var ang = board.create('angle', [[4, 0], [0, 0], [2, 2]]);
    100  * // Valid prefix expression:  ['V', ang, 'degrees']);
    101  */
    102 JXG.PrefixParser = {
    103     /**
    104      * Parse a prefix expression and apply an action.
    105      * @param {array|number} term Expression
    106      * @param {String} action Determines what to do. So far, the only
    107      * action available is 'execute', which evaluates the expression.
    108      * @returns {Number} What ever the action does.
    109      */
    110     parse: function (term, action) {
    111         var method, i, le, res, fun, v;
    112
    113         if (Type.isNumber(term) || Type.isString(term)) {
    114             return term;
    115         }
    116         if (!Type.isArray(term) || term.length < 2) {
    117             throw new Error('prefixParser.parse: term is not an array, number or string');
    118         }
    119
    120         method = term[0];
    121         le = term.length;
    122
    123         if (action === 'execute') {
    124             if (Type.isInArray(['+', '-', '*', '/'], method)) {
    125
    126                 res = this.parse(term[1], action);
    127                 for (i = 2; i < le; i++) {
    128                     v = this.parse(term[i], action);
    129                     switch (method) {
    130                         case '+':
    131                             res += v;
    132                             break;
    133                         case '-':
    134                             res -= v;
    135                             break;
    136                         case '*':
    137                             res *= v;
    138                             break;
    139                         case '/':
    140                             res /= v;
    141                             break;
    142                         default:
    143                     }
    144                 }
    145             } else if (method === 'exec') {
    146                 fun = term[1];
    147                 v = [];
    148                 for (i = 2; i < le; i++) {
    149                     v.push(this.parse(term[i], action));
    150                 }
    151                 if (Type.exists(Math[fun])) {
    152                     res = Math[fun].apply(this, v);
    153                 } else if (Type.exists(Mat[fun])) {
    154                     res = Mat[fun].apply(this, v);
    155                 } else {
    156                     throw new Error("PrefixParser.parse: " + fun + " is not allowed");
    157                 }
    158             } else {
    159                 // Allow shortcut 'V' for 'Value'
    160                 fun = term[0];
    161                 if (fun === 'V') {
    162                     fun = 'Value';
    163                 }
    164
    165                 if (!Type.exists(term[1][fun])) {
    166                     throw new Error("PrefixParser.parse: " + fun + " is not a method of " + term[1]);
    167                 }
    168                 v = [];
    169                 for (i = 2; i < le; i++) {
    170                     v.push(this.parse(term[i], action));
    171                 }
    172                 res = term[1][fun].apply(term[1], v);
    173             }
    174         }
    175
    176         return res;
    177     },
    178
    179     /**
    180      * Determine the dimension of the resulting value, i.e. ['L', obj] as well as
    181      * ['+', ['L', obj1], ['L', obj2]] have dimension 1.
    182      * <p>
    183      * ['+', ['Area', obj1], ['L', obj2]] will retrun NaN, because the two
    184      * operands have conflicting dimensions.
    185      * <p>
    186      * If an element is a measurement element, then it's dimension can be set as attribute.
    187      * This overrules the computed dimension.
    188      *
    189      * @param {Array|Number} term Prefix expression
    190      * @returns Number
    191      */
    192     dimension: function (term) {
    193         var method, i, le, res, fun, d, v, unit;
    194
    195         if (Type.isNumber(term)) {
    196             return 0;
    197         }
    198         if (!Type.isArray(term) || term.length < 2) {
    199             throw new Error('PrefixParser.dimension: term is not an array');
    200         }
    201
    202         method = term[0];
    203         le = term.length;
    204
    205         if (Type.isInArray(['+', '-', '*', '/'], method)) {
    206
    207             res = this.dimension(term[1]);
    208             for (i = 2; i < le; i++) {
    209                 v = this.dimension(term[i]);
    210                 switch (method) {
    211                     case '+':
    212                         if (v !== res) {
    213                             res = NaN;
    214                         }
    215                         break;
    216                     case '-':
    217                         if (v !== res) {
    218                             res = NaN;
    219                         }
    220                         break;
    221                     case '*':
    222                         res += v;
    223                         break;
    224                     case '/':
    225                         res -= v;
    226                         break;
    227                     default:
    228                 }
    229             }
    230
    231         } else if (method === 'exec') {
    232             if (term[2].type === Type.OBJECT_TYPE_MEASUREMENT) {
    233                 res = term[2].Dimension();
    234                 // If attribute "dim" is set, this overrules anything else.
    235                 if (Type.exists(term[2].visProp.dim)) {
    236                     d = Type.evaluate(term[2].visProp.dim);
    237                     if (d !== null) {
    238                         res = d;
    239                     }
    240                 }
    241             } else {
    242                 res = 0;
    243             }
    244         } else {
    245             // Allow shortcut 'V' for 'Value'
    246             fun = term[0];
    247
    248             switch (fun) {
    249                 case 'L':
    250                 case 'Length':
    251                 case 'Perimeter':
    252                 case 'Radius':
    253                 case 'R':
    254                     res = 1;
    255                     break;
    256                 case 'Area':
    257                 case 'A':
    258                     res = 2;
    259                     break;
    260                 default: // 'V', 'Value'
    261                     if (term[1].type === Type.OBJECT_TYPE_MEASUREMENT) {
    262                         res = term[1].Dimension();
    263                         // If attribute "dim" is set, this overrules anything else.
    264                         if (Type.exists(term[1].visProp.dim)) {
    265                             d = Type.evaluate(term[1].visProp.dim);
    266                             if (d !== null) {
    267                                 res = d;
    268                             }
    269                         }
    270                     } else {
    271                         res = 0;
    272
    273                         if (fun === 'Value' || fun === 'V') {
    274                             // The Value method of sector, angle and arc does not have the same dimension
    275                             // for all units.
    276                             if ([Const.OBJECT_TYPE_ARC, Const.OBJECT_TYPE_SECTOR, Const.OBJECT_TYPE_ANGLE].indexOf(term[1].type) >= 0) {
    277                                 unit = '';
    278                                 if (term.length === 3 && Type.isString(term[2])) {
    279                                     unit = term[2].toLowerCase();
    280                                 }
    281                                 if (unit === '') {
    282                                     // Default values:
    283                                     if (term[1].type === Const.OBJECT_TYPE_ANGLE) {
    284                                         // Default for angle.Value() is radians, i.e. dim 0
    285                                         res = 0;
    286                                     } else {
    287                                         // Default for sector|arc.Value() is length, i.e. dim 1
    288                                         res = 1;
    289                                     }
    290                                 } else if (unit.indexOf('len') === 0) {
    291                                     // Length has dim 1
    292                                     res = 1;
    293                                 } else {
    294                                     // Angles in various units has dimension 0
    295                                     res = 0;
    296                                 }
    297                             }
    298                         }
    299                     }
    300             }
    301         }
    302
    303         return res;
    304     },
    305
    306     /**
    307      * Convert a prefix expression into a new prefix expression in which
    308      * JSXGraph elements have been replaced by their ids.
    309      *
    310      * @param {Array|Number} term
    311      * @returns {Array|Number}
    312      */
    313     toPrefix: function (term) {
    314         var method, i, le, res;
    315
    316         if (Type.isNumber(term)) {
    317             return term;
    318         }
    319         if (!Type.isArray(term) || term.length < 2) {
    320             throw new Error('PrefixParser.toPrefix: term is not an array');
    321         }
    322
    323         method = term[0];
    324         le = term.length;
    325         res = [method];
    326
    327         for (i = 1; i < le; i++) {
    328             if (Type.isInArray(['+', '-', '*', '/'], method)) {
    329                 res.push(this.toPrefix(term[i]));
    330             } else {
    331                 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) {
    332                     res = term[i].toPrefix();
    333                 } else if (method === 'exec') {
    334                     if (i === 1) {
    335                         res.push(term[i]);
    336                     } else {
    337                         res.push(this.toPrefix(term[i]));
    338                     }
    339                 } else {
    340                     res = [method, term[i].id];
    341                 }
    342             }
    343         }
    344
    345         return res;
    346     },
    347
    348     /**
    349      * Determine parent elements of a prefix expression.
    350      * @param {Array|Number} term prefix expression
    351      * @returns Array
    352      * @private
    353      */
    354     getParents: function (term) {
    355         var method, i, le, res;
    356
    357         if (Type.isNumber(term)) {
    358             return [];
    359         }
    360         if (!Type.isArray(term) || term.length < 2) {
    361             throw new Error('PrefixParser.getParents: term is not an array');
    362         }
    363
    364         method = term[0];
    365         le = term.length;
    366         res = [];
    367
    368         for (i = 1; i < le; i++) {
    369             if (Type.isInArray(['+', '-', '*', '/'], method)) {
    370                 res = res.concat(this.getParents(term[i]));
    371             } else {
    372                 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) {
    373                     res = res.concat(term[i].getParents());
    374                 } else if (method === 'exec') {
    375                     if (i > 1) {
    376                         res = res.concat(this.getParents(term[i]));
    377                     }
    378                 } else {
    379                     res.push(term[i]);
    380                 }
    381             }
    382         }
    383
    384         return res;
    385     }
    386 };
    387
    388 export default JXG.PrefixParser;