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;