      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 /**
     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.js";
     47 import Type from "../utils/type.js";
     48 import Mat from "../math/math.js";
     49 import Const from "../base/constants.js";
     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                 fun = term[0];
    160
    161                 // Allow shortcut 'V' for 'Value'
    162                 if (fun === 'V') {
    163                     fun = 'Value';
    164                 }
    165
    166                 // get coords always with z
    167                 // (its visibility is controlled by the attribute function formatCoords)
    168                 if (fun === 'Coords') {
    169                     term[2] = 'true';
    170                 }
    171
    172                 if (!Type.exists(term[1][fun])) {
    173                     throw new Error("PrefixParser.parse: " + fun + " is not a method of " + term[1]);
    174                 }
    175                 v = [];
    176                 for (i = 2; i < le; i++) {
    177                     v.push(this.parse(term[i], action));
    178                 }
    179                 res = term[1][fun].apply(term[1], v);
    180             }
    181         }
    182
    183         return res;
    184     },
    185
    186     /**
    187      * Determine the dimension of the resulting value, i.e. ['L', obj] as well as
    188      * ['+', ['L', obj1], ['L', obj2]] have dimension 1.
    189      * <p>
    190      * ['+', ['Area', obj1], ['L', obj2]] will retrun NaN, because the two
    191      * operands have conflicting dimensions.
    192      * <p>
    193      * If an element is a measurement element, then it's dimension can be set as attribute.
    194      * This overrules the computed dimension.
    195      *
    196      * @param {Array|Number} term Prefix expression
    197      * @returns Number
    198      */
    199     dimension: function (term) {
    200         var method, i, le, res, fun, d, v, unit;
    201
    202         if (Type.isNumber(term)) {
    203             return 0;
    204         }
    205         if (!Type.isArray(term) || term.length < 2) {
    206             throw new Error('PrefixParser.dimension: term is not an array');
    207         }
    208
    209         method = term[0];
    210         le = term.length;
    211
    212         if (Type.isInArray(['+', '-', '*', '/'], method)) {
    213
    214             res = this.dimension(term[1]);
    215             for (i = 2; i < le; i++) {
    216                 v = this.dimension(term[i]);
    217                 switch (method) {
    218                     case '+':
    219                         if (v !== res) {
    220                             res = NaN;
    221                         }
    222                         break;
    223                     case '-':
    224                         if (v !== res) {
    225                             res = NaN;
    226                         }
    227                         break;
    228                     case '*':
    229                         res += v;
    230                         break;
    231                     case '/':
    232                         res -= v;
    233                         break;
    234                     default:
    235                 }
    236             }
    237
    238         } else if (method === 'exec') {
    239             if (term[2].type === Type.OBJECT_TYPE_MEASUREMENT) {
    240                 res = term[2].Dimension();
    241                 // If attribute "dim" is set, this overrules anything else.
    242                 if (Type.exists(term[2].visProp.dim)) {
    243                     d = Type.evaluate(term[2].visProp.dim);
    244                     if (d !== null) {
    245                         res = d;
    246                     }
    247                 }
    248             } else {
    249                 res = 0;
    250             }
    251         } else {
    252             // Allow shortcut 'V' for 'Value'
    253             fun = term[0];
    254
    255             switch (fun) {
    256                 case 'L':
    257                 case 'Length':
    258                 case 'Perimeter':
    259                 case 'Radius':
    260                 case 'R':
    261                     res = 1;
    262                     break;
    263                 case 'Area':
    264                 case 'A':
    265                     res = 2;
    266                     break;
    267                 default: // 'V', 'Value'
    268                     if (term[1].type === Type.OBJECT_TYPE_MEASUREMENT) {
    269                         res = term[1].Dimension();
    270                         // If attribute "dim" is set, this overrules anything else.
    271                         if (Type.exists(term[1].visProp.dim)) {
    272                             d = Type.evaluate(term[1].visProp.dim);
    273                             if (d !== null) {
    274                                 res = d;
    275                             }
    276                         }
    277                     } else {
    278                         res = 0;
    279
    280                         if (fun === 'Value' || fun === 'V') {
    281                             // The Value method of sector, angle and arc does not have the same dimension
    282                             // for all units.
    283                             if ([Const.OBJECT_TYPE_ARC, Const.OBJECT_TYPE_SECTOR, Const.OBJECT_TYPE_ANGLE].indexOf(term[1].type) >= 0) {
    284                                 unit = '';
    285                                 if (term.length === 3 && Type.isString(term[2])) {
    286                                     unit = term[2].toLowerCase();
    287                                 }
    288                                 if (unit === '') {
    289                                     // Default values:
    290                                     if (term[1].type === Const.OBJECT_TYPE_ANGLE) {
    291                                         // Default for angle.Value() is radians, i.e. dim 0
    292                                         res = 0;
    293                                     } else {
    294                                         // Default for sector|arc.Value() is length, i.e. dim 1
    295                                         res = 1;
    296                                     }
    297                                 } else if (unit.indexOf('len') === 0) {
    298                                     // Length has dim 1
    299                                     res = 1;
    300                                 } else {
    301                                     // Angles in various units has dimension 0
    302                                     res = 0;
    303                                 }
    304                             }
    305                         }
    306                     }
    307             }
    308         }
    309
    310         return res;
    311     },
    312
    313     /**
    314      * Convert a prefix expression into a new prefix expression in which
    315      * JSXGraph elements have been replaced by their ids.
    316      *
    317      * @param {Array|Number} term
    318      * @returns {Array|Number}
    319      */
    320     toPrefix: function (term) {
    321         var method, i, le, res;
    322
    323         if (Type.isNumber(term)) {
    324             return term;
    325         }
    326         if (!Type.isArray(term) || term.length < 2) {
    327             throw new Error('PrefixParser.toPrefix: term is not an array');
    328         }
    329
    330         method = term[0];
    331         le = term.length;
    332         res = [method];
    333
    334         for (i = 1; i < le; i++) {
    335             if (Type.isInArray(['+', '-', '*', '/'], method)) {
    336                 res.push(this.toPrefix(term[i]));
    337             } else {
    338                 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) {
    339                     res = term[i].toPrefix();
    340                 } else if (method === 'exec') {
    341                     if (i === 1) {
    342                         res.push(term[i]);
    343                     } else {
    344                         res.push(this.toPrefix(term[i]));
    345                     }
    346                 } else {
    347                     res = [method, term[i].id];
    348                 }
    349             }
    350         }
    351
    352         return res;
    353     },
    354
    355     /**
    356      * Determine parent elements of a prefix expression.
    357      * @param {Array|Number} term prefix expression
    358      * @returns Array
    359      * @private
    360      */
    361     getParents: function (term) {
    362         var method, i, le, res;
    363
    364         if (Type.isNumber(term)) {
    365             return [];
    366         }
    367         if (!Type.isArray(term) || term.length < 2) {
    368             throw new Error('PrefixParser.getParents: term is not an array');
    369         }
    370
    371         method = term[0];
    372         le = term.length;
    373         res = [];
    374
    375         for (i = 1; i < le; i++) {
    376             if (Type.isInArray(['+', '-', '*', '/'], method)) {
    377                 Type.concat(res, this.getParents(term[i]));
    378             } else {
    379                 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) {
    380                     Type.concat(res, term[i].getParents());
    381                 } else if (method === 'exec') {
    382                     if (i > 1) {
    383                         Type.concat(res, this.getParents(term[i]));
    384                     }
    385                 } else {
    386                     res.push(term[i]);
    387                 }
    388             }
    389         }
    390
    391         return res;
    392     }
    393 };
    394
    395 export default JXG.PrefixParser;