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;