1 /*
  2     Copyright 2008-2026
  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 'Slope':
257                 case 'Angle':
258                     res = 0;
259                     break;
260                 case 'L':
261                 case 'Length':
262                 case 'Perimeter':
263                 case 'Diameter':
264                 case 'Radius':
265                 case 'R':
266                 case 'DeltaX':
267                 case 'DeltaY':
268                     res = 1;
269                     break;
270                 case 'Area':
271                 case 'A':
272                     res = 2;
273                     break;
274                 default: // 'V', 'Value'
275                     if (term[1].type === Type.OBJECT_TYPE_MEASUREMENT) {
276                         res = term[1].Dimension();
277                         // If attribute "dim" is set, this overrules anything else.
278                         if (Type.exists(term[1].visProp.dim)) {
279                             d = term[1].evalVisProp('dim');
280                             if (d !== null) {
281                                 res = d;
282                             }
283                         }
284                     } else {
285                         res = 0;
286 
287                         if (fun === 'Value' || fun === 'V') {
288                             // The Value method of sector, angle and arc does not have the same dimension
289                             // for all units.
290                             if ([Const.OBJECT_TYPE_ARC, Const.OBJECT_TYPE_SECTOR, Const.OBJECT_TYPE_ANGLE].indexOf(term[1].type) >= 0) {
291                                 unit = '';
292                                 if (term.length === 3 && Type.isString(term[2])) {
293                                     unit = term[2].toLowerCase();
294                                 }
295                                 if (unit === '') {
296                                     // Default values:
297                                     if (term[1].type === Const.OBJECT_TYPE_ANGLE) {
298                                         // Default for angle.Value() is radians, i.e. dim 0
299                                         res = 0;
300                                     } else {
301                                         // Default for sector|arc.Value() is length, i.e. dim 1
302                                         res = 1;
303                                     }
304                                 } else if (unit.indexOf('len') === 0) {
305                                     // Length has dim 1
306                                     res = 1;
307                                 } else {
308                                     // Angles in various units has dimension 0
309                                     res = 0;
310                                 }
311                             }
312                         }
313                     }
314             }
315         }
316 
317         return res;
318     },
319 
320     /**
321      * Convert a prefix expression into a new prefix expression in which
322      * JSXGraph elements have been replaced by their ids.
323      *
324      * @param {Array|Number} term
325      * @returns {Array|Number}
326      */
327     toPrefix: function (term) {
328         var method, i, le, res;
329 
330         if (Type.isNumber(term)) {
331             return term;
332         }
333         if (!Type.isArray(term) || term.length < 2) {
334             throw new Error('PrefixParser.toPrefix: term is not an array');
335         }
336 
337         method = term[0];
338         le = term.length;
339         res = [method];
340 
341         for (i = 1; i < le; i++) {
342             if (Type.isInArray(['+', '-', '*', '/'], method)) {
343                 res.push(this.toPrefix(term[i]));
344             } else {
345                 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) {
346                     res = term[i].toPrefix();
347                 } else if (method === 'exec') {
348                     if (i === 1) {
349                         res.push(term[i]);
350                     } else {
351                         res.push(this.toPrefix(term[i]));
352                     }
353                 } else {
354                     res = [method, term[i].id];
355                 }
356             }
357         }
358 
359         return res;
360     },
361 
362     /**
363      * Determine parent elements of a prefix expression.
364      * @param {Array|Number} term prefix expression
365      * @returns Array
366      * @private
367      */
368     getParents: function (term) {
369         var method, i, le, res;
370 
371         if (Type.isNumber(term)) {
372             return [];
373         }
374         if (!Type.isArray(term) || term.length < 2) {
375             throw new Error('PrefixParser.getParents: term is not an array');
376         }
377 
378         method = term[0];
379         le = term.length;
380         res = [];
381 
382         for (i = 1; i < le; i++) {
383             if (Type.isInArray(['+', '-', '*', '/'], method)) {
384                 Type.concat(res, this.getParents(term[i]));
385             } else {
386                 if (method === 'V' && term[i].type === Type.OBJECT_TYPE_MEASUREMENT) {
387                     Type.concat(res, term[i].getParents());
388                 } else if (method === 'exec') {
389                     if (i > 1) {
390                         Type.concat(res, this.getParents(term[i]));
391                     }
392                 } else {
393                     res.push(term[i]);
394                 }
395             }
396         }
397 
398         return res;
399     }
400 };
401 
402 export default JXG.PrefixParser;