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 = term[2].evalVisProp('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 = term[1].evalVisProp('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;