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;