1 /* 2 Copyright 2008-2026 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Andreas Walter, 8 Alfred Wassermann, 9 Peter Wilfahrt 10 11 This file is part of JSXGraph. 12 13 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 14 15 You can redistribute it and/or modify it under the terms of the 16 17 * GNU Lesser General Public License as published by 18 the Free Software Foundation, either version 3 of the License, or 19 (at your option) any later version 20 OR 21 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 22 23 JSXGraph is distributed in the hope that it will be useful, 24 but WITHOUT ANY WARRANTY; without even the implied warranty of 25 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 GNU Lesser General Public License for more details. 27 28 You should have received a copy of the GNU Lesser General Public License and 29 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 30 and <https://opensource.org/licenses/MIT/>. 31 */ 32 33 /*global JXG: true, define: true, html_sanitize: true*/ 34 /*jslint nomen: true, plusplus: true*/ 35 36 /** 37 * @fileoverview type.js contains several functions to help deal with javascript's weak types. 38 * This file mainly consists of detector functions which verify if a variable is or is not of 39 * a specific type and converter functions that convert variables to another type or normalize 40 * the type of a variable. 41 */ 42 43 import JXG from "../jxg.js"; 44 import Const from "../base/constants.js"; 45 import Mat from "../math/math.js"; 46 47 JXG.extend( 48 JXG, 49 /** @lends JXG */ { 50 /** 51 * Checks if the given object is an JSXGraph board. 52 * @param {Object} v 53 * @returns {Boolean} 54 */ 55 isBoard: function (v) { 56 return v !== null && 57 typeof v === "object" && 58 this.isNumber(v.BOARD_MODE_NONE) && 59 this.isObject(v.objects) && 60 this.isObject(v.jc) && 61 this.isFunction(v.update) && 62 !!v.containerObj && 63 this.isString(v.id); 64 }, 65 66 /** 67 * Checks if the given string is an id within the given board. 68 * @param {JXG.Board} board 69 * @param {String} s 70 * @returns {Boolean} 71 */ 72 isId: function (board, s) { 73 return typeof s === "string" && !!board.objects[s]; 74 }, 75 76 /** 77 * Checks if the given string is a name within the given board. 78 * @param {JXG.Board} board 79 * @param {String} s 80 * @returns {Boolean} 81 */ 82 isName: function (board, s) { 83 return typeof s === "string" && !!board.elementsByName[s]; 84 }, 85 86 /** 87 * Checks if the given string is a group id within the given board. 88 * @param {JXG.Board} board 89 * @param {String} s 90 * @returns {Boolean} 91 */ 92 isGroup: function (board, s) { 93 return typeof s === "string" && !!board.groups[s]; 94 }, 95 96 /** 97 * Checks if the value of a given variable is of type string. 98 * @param v A variable of any type. 99 * @returns {Boolean} True, if v is of type string. 100 */ 101 isString: function (v) { 102 return typeof v === 'string'; 103 }, 104 105 /** 106 * Checks if the value of a given variable is of type number. 107 * @param v A variable of any type. 108 * @param {Boolean} [acceptStringNumber=false] If set to true, the function returns true for e.g. v='3.1415'. 109 * @param {Boolean} [acceptNaN=true] If set to false, the function returns false for v=NaN. 110 * @returns {Boolean} True, if v is of type number. 111 */ 112 isNumber: function (v, acceptStringNumber, acceptNaN) { 113 var result = ( 114 typeof v === 'number' || Object.prototype.toString.call(v) === '[Object Number]' 115 ); 116 acceptStringNumber = acceptStringNumber || false; 117 acceptNaN = acceptNaN === undefined ? true : acceptNaN; 118 119 if (acceptStringNumber) { 120 result = result || ('' + parseFloat(v)) === v; 121 } 122 if (!acceptNaN) { 123 result = result && !isNaN(v); 124 } 125 return result; 126 }, 127 128 /** 129 * Checks if a given variable references a function. 130 * @param v A variable of any type. 131 * @returns {Boolean} True, if v is a function. 132 */ 133 isFunction: function (v) { 134 return typeof v === 'function'; 135 }, 136 137 /** 138 * Checks if a given variable references an array. 139 * @param v A variable of any type. 140 * @returns {Boolean} True, if v is of type array. 141 */ 142 isArray: function (v) { 143 var r; 144 145 // use the ES5 isArray() method and if that doesn't exist use a fallback. 146 if (Array.isArray) { 147 r = Array.isArray(v); 148 } else { 149 r = 150 v !== null && 151 typeof v === "object" && 152 typeof v.splice === "function" && 153 typeof v.join === 'function'; 154 } 155 156 return r; 157 }, 158 159 /** 160 * Tests if the input variable is an Object 161 * @param v 162 */ 163 isObject: function (v) { 164 return typeof v === "object" && !this.isArray(v); 165 }, 166 167 /** 168 * Tests if the input variable is a DOM Document or DocumentFragment node 169 * @param v A variable of any type 170 */ 171 isDocumentOrFragment: function (v) { 172 return this.isObject(v) && ( 173 v.nodeType === 9 || // Node.DOCUMENT_NODE 174 v.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE 175 ); 176 }, 177 178 /** 179 * Checks if a given variable is a reference of a JSXGraph Point element. 180 * @param v A variable of any type. 181 * @returns {Boolean} True, if v is of type JXG.Point. 182 */ 183 isPoint: function (v) { 184 if (v !== null && typeof v === "object" && this.exists(v.elementClass)) { 185 return v.elementClass === Const.OBJECT_CLASS_POINT; 186 } 187 188 return false; 189 }, 190 191 /** 192 * Checks if a given variable is a reference of a JSXGraph Point3D element. 193 * @param v A variable of any type. 194 * @returns {Boolean} True, if v is of type JXG.Point3D. 195 */ 196 isPoint3D: function (v) { 197 if (v !== null && typeof v === "object" && this.exists(v.type)) { 198 return v.type === Const.OBJECT_TYPE_POINT3D; 199 } 200 201 return false; 202 }, 203 204 /** 205 * Checks if a given variable is a reference of a JSXGraph Point element or an array of length at least two or 206 * a function returning an array of length two or three. 207 * @param {JXG.Board} board 208 * @param v A variable of any type. 209 * @returns {Boolean} True, if v is of type JXG.Point. 210 */ 211 isPointType: function (board, v) { 212 var val, p; 213 214 if (this.isArray(v)) { 215 return true; 216 } 217 if (this.isFunction(v)) { 218 val = v(); 219 if (this.isArray(val) && val.length > 1) { 220 return true; 221 } 222 } 223 p = board.select(v); 224 return this.isPoint(p); 225 }, 226 227 /** 228 * Checks if a given variable is a reference of a JSXGraph Point3D element or an array of length three 229 * or a function returning an array of length three. 230 * @param {JXG.Board} board 231 * @param v A variable of any type. 232 * @returns {Boolean} True, if v is of type JXG.Point3D or an array of length at least 3, or a function returning 233 * such an array. 234 */ 235 isPointType3D: function (board, v) { 236 var val, p; 237 238 if (this.isArray(v) && v.length >= 3) { 239 return true; 240 } 241 if (this.isFunction(v)) { 242 val = v(); 243 if (this.isArray(val) && val.length >= 3) { 244 return true; 245 } 246 } 247 p = board.select(v); 248 return this.isPoint3D(p); 249 }, 250 251 /** 252 * Checks if a given variable is a reference of a JSXGraph transformation element or an array 253 * of JSXGraph transformation elements. 254 * @param v A variable of any type. 255 * @returns {Boolean} True, if v is of type JXG.Transformation. 256 */ 257 isTransformationOrArray: function (v) { 258 if (v !== null) { 259 if (this.isArray(v) && v.length > 0) { 260 return this.isTransformationOrArray(v[0]); 261 } 262 if (typeof v === 'object') { 263 return v.type === Const.OBJECT_TYPE_TRANSFORMATION; 264 } 265 } 266 return false; 267 }, 268 269 /** 270 * Checks if v is an empty object or empty. 271 * @param v {Object|Array} 272 * @returns {boolean} True, if v is an empty object or array. 273 */ 274 isEmpty: function (v) { 275 return Object.keys(v).length === 0; 276 }, 277 278 /** 279 * Checks if a given variable is neither undefined nor null. You should not use this together with global 280 * variables! 281 * @param v A variable of any type. 282 * @param {Boolean} [checkEmptyString=false] If set to true, it is also checked whether v is not equal to ''. 283 * @returns {Boolean} True, if v is neither undefined nor null. 284 */ 285 exists: function (v, checkEmptyString) { 286 /* eslint-disable eqeqeq */ 287 var result = !(v == undefined || v === null); 288 /* eslint-enable eqeqeq */ 289 checkEmptyString = checkEmptyString || false; 290 291 if (checkEmptyString) { 292 return result && v !== ""; 293 } 294 return result; 295 }, 296 // exists: (function (undef) { 297 // return function (v, checkEmptyString) { 298 // var result = !(v === undef || v === null); 299 300 // checkEmptyString = checkEmptyString || false; 301 302 // if (checkEmptyString) { 303 // return result && v !== ''; 304 // } 305 // return result; 306 // }; 307 // }()), 308 309 /** 310 * Handle default parameters. 311 * @param v Given value 312 * @param d Default value 313 * @returns <tt>d</tt>, if <tt>v</tt> is undefined or null. 314 */ 315 def: function (v, d) { 316 if (this.exists(v)) { 317 return v; 318 } 319 320 return d; 321 }, 322 323 /** 324 * Converts a string containing either <strong>true</strong> or <strong>false</strong> into a boolean value. 325 * @param {String} s String containing either <strong>true</strong> or <strong>false</strong>. 326 * @returns {Boolean} String typed boolean value converted to boolean. 327 */ 328 str2Bool: function (s) { 329 if (!this.exists(s)) { 330 return true; 331 } 332 333 if (typeof s === 'boolean') { 334 return s; 335 } 336 337 if (this.isString(s)) { 338 return s.toLowerCase() === 'true'; 339 } 340 341 return false; 342 }, 343 344 /** 345 * Converts a given CSS style string into a JavaScript object. Uses JSON.parse. 346 * Has problems with CSS expressions containing blanks, like 347 * `background: #aaaaaa url("../jsxgraph/img/favicon.png")`. 348 * 349 * @param {String} cssString String containing CSS styles. 350 * @returns {Object} Object containing CSS styles. 351 * @see JXG#css2js 352 * @deprecated 353 */ 354 cssParse: function (cssString) { 355 var str = cssString; 356 if (!this.isString(str)) return {}; 357 358 str = str.replace(/\s*;\s*$/g, ''); 359 str = str.replace(/\s*;\s*/g, '","'); 360 str = str.replace(/\s*:\s*/g, '":"'); 361 str = str.trim(); 362 str = '{"' + str + '"}'; 363 364 return JSON.parse(str); 365 }, 366 367 /** 368 * Converts string containing CSS properties into 369 * array with key-value pair objects. 370 * 371 * @example 372 * "color:blue; background-color:yellow" is converted to 373 * [{'color': 'blue'}, {'backgroundColor': 'yellow'}] 374 * 375 * @param {String} cssString String containing CSS properties 376 * @return {Array} Array of CSS key-value pairs 377 */ 378 css2js: function (cssString) { 379 var pairs = [], 380 i, len, 381 key, val, 382 s, 383 list = JXG.trim(cssString).replace(/;$/, "").split(";"); 384 385 len = list.length; 386 for (i = 0; i < len; ++i) { 387 if (JXG.trim(list[i]) !== "") { 388 s = list[i].split(":"); 389 key = JXG.trim( 390 // CSS syntax to camel case: font-family -> fontFamily 391 s[0].replace(/-([a-z])/gi, function (match, char) { return char.toUpperCase(); }) 392 ); 393 val = JXG.trim(s[1]); 394 pairs.push({ key: key, val: val }); 395 } 396 } 397 return pairs; 398 }, 399 400 /** 401 * Converts a given object into a CSS style string. 402 * @param {Object} styles Object containing CSS styles. 403 * @returns {String} String containing CSS styles. 404 */ 405 cssStringify: function (styles) { 406 var str = '', 407 attr, val; 408 if (!this.isObject(styles)) return ''; 409 410 for (attr in styles) { 411 if (!styles.hasOwnProperty(attr)) continue; 412 val = styles[attr]; 413 if (!this.isString(val) && !this.isNumber(val)) continue; 414 415 str += attr + ':' + val + '; '; 416 } 417 str = str.trim(); 418 419 return str; 420 }, 421 422 /** 423 * Convert a String, a number or a function into a function. This method is used in Transformation.js 424 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 425 * by a JessieCode string, thus it must be a valid reference only in case one of the param 426 * values is of type string. 427 * @param {Array} param An array containing strings, numbers, or functions. 428 * @param {Number} n Length of <tt>param</tt>. 429 * @returns {Function} A function taking one parameter k which specifies the index of the param element 430 * to evaluate. 431 */ 432 createEvalFunction: function (board, param, n) { 433 var f = [], func, i, e, 434 deps = {}; 435 436 for (i = 0; i < n; i++) { 437 f[i] = this.createFunction(param[i], board); 438 for (e in f[i].deps) { 439 deps[e] = f[i].deps; 440 } 441 } 442 443 func = function (k) { 444 return f[k](); 445 }; 446 func.deps = deps; 447 448 return func; 449 }, 450 451 /** 452 * Convert a String, number or function into a function. 453 * @param {String|Number|Function} term A variable of type string, function or number. 454 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 455 * by a JessieCode/GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 456 * values is of type string. 457 * @param {String} variableName Only required if function is supplied as JessieCode string or evalGeonext is set to true. 458 * Describes the variable name of the variable in a JessieCode/GEONE<sub>X</sub>T string given as term. 459 * @param {Boolean} [evalGeonext=false] Obsolete and ignored! Set this true 460 * if term should be treated as a GEONE<sub>X</sub>T string. 461 * @returns {Function} A function evaluating the value given by term or null if term is not of type string, 462 * function or number. 463 */ 464 createFunction: function (term, board, variableName, evalGeonext) { 465 var f = null; 466 467 // if ((!this.exists(evalGeonext) || evalGeonext) && this.isString(term)) { 468 if (this.isString(term)) { 469 // Convert GEONExT syntax into JavaScript syntax 470 //newTerm = JXG.GeonextParser.geonext2JS(term, board); 471 //return new Function(variableName,'return ' + newTerm + ';'); 472 //term = JXG.GeonextParser.replaceNameById(term, board); 473 //term = JXG.GeonextParser.geonext2JS(term, board); 474 475 f = board.jc.snippet(term, true, variableName, false); 476 } else if (this.isFunction(term)) { 477 f = term; 478 f.deps = (this.isObject(term.deps)) ? term.deps : {}; 479 } else if (this.isNumber(term) || this.isArray(term)) { 480 /** @ignore */ 481 f = function () { return term; }; 482 f.deps = {}; 483 // } else if (this.isString(term)) { 484 // // In case of string function like fontsize 485 // /** @ignore */ 486 // f = function () { return term; }; 487 // f.deps = {}; 488 } 489 490 if (f !== null) { 491 f.origin = term; 492 f.variable = variableName; 493 } 494 495 return f; 496 }, 497 498 /** 499 * Test if the parents array contains existing points. If instead parents contains coordinate arrays or 500 * function returning coordinate arrays 501 * free points with these coordinates are created. 502 * 503 * @param {JXG.Board} board Board object 504 * @param {Array} parents Array containing parent elements for a new object. This array may contain 505 * <ul> 506 * <li> {@link JXG.Point} objects 507 * <li> {@link JXG.GeometryElement#name} of {@link JXG.Point} objects 508 * <li> {@link JXG.GeometryElement#id} of {@link JXG.Point} objects 509 * <li> Coordinates of points given as array of numbers of length two or three, e.g. [2, 3]. 510 * <li> Coordinates of points given as array of functions of length two or three. Each function returns one coordinate, e.g. 511 * [function(){ return 2; }, function(){ return 3; }] 512 * <li> Function returning coordinates, e.g. function() { return [2, 3]; } 513 * </ul> 514 * In the last three cases a new point will be created. 515 * @param {String} attrClass Main attribute class of newly created points, see {@link JXG#copyAttributes} 516 * @param {Array} attrArray List of subtype attributes for the newly created points. The list of subtypes is mapped to the list of new points. 517 * @returns {Array} List of newly created {@link JXG.Point} elements or false if not all returned elements are points. 518 */ 519 providePoints: function (board, parents, attributes, attrClass, attrArray) { 520 var i, 521 j, 522 len, 523 lenAttr = 0, 524 points = [], 525 attr, 526 val; 527 528 if (!this.isArray(parents)) { 529 parents = [parents]; 530 } 531 len = parents.length; 532 if (this.exists(attrArray)) { 533 lenAttr = attrArray.length; 534 } 535 if (lenAttr === 0) { 536 attr = this.copyAttributes(attributes, board.options, attrClass); 537 } 538 539 for (i = 0; i < len; ++i) { 540 if (lenAttr > 0) { 541 j = Math.min(i, lenAttr - 1); 542 attr = this.copyAttributes( 543 attributes, 544 board.options, 545 attrClass, 546 attrArray[j].toLowerCase() 547 ); 548 } 549 if (this.isArray(parents[i]) && parents[i].length > 1) { 550 points.push(board.create("point", parents[i], attr)); 551 points[points.length - 1]._is_new = true; 552 } else if (this.isFunction(parents[i])) { 553 val = parents[i](); 554 if (this.isArray(val) && val.length > 1) { 555 points.push(board.create("point", [parents[i]], attr)); 556 points[points.length - 1]._is_new = true; 557 } 558 } else { 559 points.push(board.select(parents[i])); 560 } 561 562 if (!this.isPoint(points[i])) { 563 return false; 564 } 565 } 566 567 return points; 568 }, 569 570 /** 571 * Test if the parents array contains existing points. If instead parents contains coordinate arrays or 572 * function returning coordinate arrays 573 * free points with these coordinates are created. 574 * 575 * @param {JXG.View3D} view View3D object 576 * @param {Array} parents Array containing parent elements for a new object. This array may contain 577 * <ul> 578 * <li> {@link JXG.Point3D} objects 579 * <li> {@link JXG.GeometryElement#name} of {@link JXG.Point3D} objects 580 * <li> {@link JXG.GeometryElement#id} of {@link JXG.Point3D} objects 581 * <li> Coordinates of 3D points given as array of numbers of length three, e.g. [2, 3, 1]. 582 * <li> Coordinates of 3D points given as array of functions of length three. Each function returns one coordinate, e.g. 583 * [function(){ return 2; }, function(){ return 3; }, function(){ return 1; }] 584 * <li> Function returning coordinates, e.g. function() { return [2, 3, 1]; } 585 * </ul> 586 * In the last three cases a new 3D point will be created. 587 * @param {String} attrClass Main attribute class of newly created 3D points, see {@link JXG#copyAttributes} 588 * @param {Array} attrArray List of subtype attributes for the newly created 3D points. The list of subtypes is mapped to the list of new 3D points. 589 * @returns {Array} List of newly created {@link JXG.Point3D} elements or false if not all returned elements are 3D points. 590 */ 591 providePoints3D: function (view, parents, attributes, attrClass, attrArray) { 592 var i, 593 j, 594 len, 595 lenAttr = 0, 596 points = [], 597 attr, 598 val; 599 600 if (!this.isArray(parents)) { 601 parents = [parents]; 602 } 603 len = parents.length; 604 if (this.exists(attrArray)) { 605 lenAttr = attrArray.length; 606 } 607 if (lenAttr === 0) { 608 attr = this.copyAttributes(attributes, view.board.options, attrClass); 609 } 610 611 for (i = 0; i < len; ++i) { 612 if (lenAttr > 0) { 613 j = Math.min(i, lenAttr - 1); 614 attr = this.copyAttributes( 615 attributes, 616 view.board.options, 617 attrClass, 618 attrArray[j] 619 ); 620 } 621 622 if (this.isArray(parents[i]) && parents[i].length > 0 && parents[i].every((x)=>this.isArray(x) && this.isNumber(x[0]))) { 623 // Testing for array-of-arrays-of-numbers, like [[1,2,3],[2,3,4]] 624 for (j = 0; j < parents[i].length; j++) { 625 points.push(view.create("point3d", parents[i][j], attr));; 626 points[points.length - 1]._is_new = true; 627 } 628 } else if (this.isArray(parents[i]) && parents[i].every((x)=> this.isNumber(x) || this.isFunction(x))) { 629 // Single array [1,2,3] 630 points.push(view.create("point3d", parents[i], attr)); 631 points[points.length - 1]._is_new = true; 632 633 } else if (this.isPoint3D(parents[i])) { 634 points.push(parents[i]); 635 } else if (this.isFunction(parents[i])) { 636 val = parents[i](); 637 if (this.isArray(val) && val.length > 1) { 638 points.push(view.create("point3d", [parents[i]], attr)); 639 points[points.length - 1]._is_new = true; 640 } 641 } else { 642 points.push(view.select(parents[i])); 643 } 644 645 if (!this.isPoint3D(points[i])) { 646 return false; 647 } 648 } 649 650 return points; 651 }, 652 653 /** 654 * Generates a function which calls the function fn in the scope of owner. 655 * @param {Function} fn Function to call. 656 * @param {Object} owner Scope in which fn is executed. 657 * @returns {Function} A function with the same signature as fn. 658 */ 659 bind: function (fn, owner) { 660 return function () { 661 return fn.apply(owner, arguments); 662 }; 663 }, 664 665 /** 666 * If <tt>val</tt> is a function, it will be evaluated without giving any parameters, else the input value 667 * is just returned. 668 * @param val Could be anything. Preferably a number or a function. If it is an array, evaluate() recurses 669 * into the elements. 670 * @returns If <tt>val</tt> is a function, it is evaluated and the result is returned. Otherwise <tt>val</tt> is returned. 671 */ 672 evaluate: function (val) { 673 var i, le, arr; 674 675 if (this.isFunction(val)) { 676 return val(); 677 } 678 if (this.isArray(val)) { 679 le = val.length; 680 arr = []; 681 for (i = 0; i < le; i++) { 682 arr.push(this.evaluate(val[i])); 683 } 684 return arr; 685 } 686 687 return val; 688 }, 689 690 /** 691 * Search an array for a given value. 692 * @param {Array} array 693 * @param value 694 * @param {String} [sub] Use this property if the elements of the array are objects. 695 * @returns {Number} The index of the first appearance of the given value, or 696 * <tt>-1</tt> if the value was not found. 697 */ 698 indexOf: function (array, value, sub) { 699 var i, 700 s = this.exists(sub); 701 702 if (Array.indexOf && !s) { 703 return array.indexOf(value); 704 } 705 706 for (i = 0; i < array.length; i++) { 707 if ((s && array[i][sub] === value) || (!s && array[i] === value)) { 708 return i; 709 } 710 } 711 712 return -1; 713 }, 714 715 /** 716 * Eliminates duplicate entries in an array consisting of numbers and strings. 717 * @param {Array} a An array of numbers and/or strings. 718 * @returns {Array} The array with duplicate entries eliminated. 719 */ 720 eliminateDuplicates: function (a) { 721 var i, 722 len = a.length, 723 result = [], 724 obj = {}; 725 726 for (i = 0; i < len; i++) { 727 obj[a[i]] = 0; 728 } 729 730 for (i in obj) { 731 if (obj.hasOwnProperty(i)) { 732 result.push(i); 733 } 734 } 735 736 return result; 737 }, 738 739 /** 740 * Swaps to array elements. 741 * @param {Array} arr 742 * @param {Number} i 743 * @param {Number} j 744 * @returns {Array} Reference to the given array. 745 */ 746 swap: function (arr, i, j) { 747 var tmp; 748 749 tmp = arr[i]; 750 arr[i] = arr[j]; 751 arr[j] = tmp; 752 753 return arr; 754 }, 755 756 /** 757 * Generates a copy of an array and removes the duplicate entries. 758 * The original array will be altered. 759 * @param {Array} arr 760 * @returns {Array} 761 * 762 * @see JXG.toUniqueArrayFloat 763 */ 764 uniqueArray: function (arr) { 765 var i, 766 j, 767 isArray, 768 ret = []; 769 770 if (arr.length === 0) { 771 return []; 772 } 773 774 for (i = 0; i < arr.length; i++) { 775 isArray = this.isArray(arr[i]); 776 777 if (!this.exists(arr[i])) { 778 arr[i] = ""; 779 continue; 780 } 781 for (j = i + 1; j < arr.length; j++) { 782 if (isArray && JXG.cmpArrays(arr[i], arr[j])) { 783 arr[i] = []; 784 } else if (!isArray && arr[i] === arr[j]) { 785 arr[i] = ""; 786 } 787 } 788 } 789 790 j = 0; 791 792 for (i = 0; i < arr.length; i++) { 793 isArray = this.isArray(arr[i]); 794 795 if (!isArray && arr[i] !== "") { 796 ret[j] = arr[i]; 797 j++; 798 } else if (isArray && arr[i].length !== 0) { 799 ret[j] = arr[i].slice(0); 800 j++; 801 } 802 } 803 804 arr = ret; 805 return ret; 806 }, 807 808 /** 809 * Generates a sorted copy of an array containing numbers and removes the duplicate entries up to a supplied precision eps. 810 * An array element arr[i] will be removed if abs(arr[i] - arr[i-1]) is less than eps. 811 * 812 * The original array will stay unaltered. 813 * @param {Array} arr 814 * @returns {Array} 815 * 816 * @param {Array} arr Array of numbers 817 * @param {Number} eps Precision 818 * @returns {Array} 819 * 820 * @example 821 * var arr = [2.3, 4, Math.PI, 2.300001, Math.PI+0.000000001]; 822 * console.log(JXG.toUniqueArrayFloat(arr, 0.00001)); 823 * // Output: Array(3) [ 2.3, 3.141592653589793, 4 ] 824 * 825 * @see JXG.uniqueArray 826 */ 827 toUniqueArrayFloat: function (arr, eps) { 828 var a, 829 i, le; 830 831 // if (false && Type.exists(arr.toSorted)) { 832 // a = arr.toSorted(function(a, b) { return a - b; }); 833 // } else { 834 // } 835 // Backwards compatibility to avoid toSorted 836 a = arr.slice(); 837 a.sort(function (a, b) { return a - b; }); 838 le = a.length; 839 for (i = le - 1; i > 0; i--) { 840 if (Math.abs(a[i] - a[i - 1]) < eps) { 841 a.splice(i, 1); 842 } 843 } 844 return a; 845 }, 846 847 /** 848 * Checks if an array contains an element equal to <tt>val</tt> but does not check the type! 849 * @param {Array} arr 850 * @param val 851 * @returns {Boolean} 852 */ 853 isInArray: function (arr, val) { 854 return JXG.indexOf(arr, val) > -1; 855 }, 856 857 /** 858 * Converts an array of {@link JXG.Coords} objects into a coordinate matrix. 859 * @param {Array} coords 860 * @param {Boolean} split 861 * @returns {Array} 862 */ 863 coordsArrayToMatrix: function (coords, split) { 864 var i, 865 x = [], 866 m = []; 867 868 for (i = 0; i < coords.length; i++) { 869 if (split) { 870 x.push(coords[i].usrCoords[1]); 871 m.push(coords[i].usrCoords[2]); 872 } else { 873 m.push([coords[i].usrCoords[1], coords[i].usrCoords[2]]); 874 } 875 } 876 877 if (split) { 878 m = [x, m]; 879 } 880 881 return m; 882 }, 883 884 /** 885 * Compare two arrays. 886 * @param {Array} a1 887 * @param {Array} a2 888 * @returns {Boolean} <tt>true</tt>, if the arrays coefficients are of same type and value. 889 */ 890 cmpArrays: function (a1, a2) { 891 var i; 892 893 // trivial cases 894 if (a1 === a2) { 895 return true; 896 } 897 898 if (a1.length !== a2.length) { 899 return false; 900 } 901 902 for (i = 0; i < a1.length; i++) { 903 if (this.isArray(a1[i]) && this.isArray(a2[i])) { 904 if (!this.cmpArrays(a1[i], a2[i])) { 905 return false; 906 } 907 } else if (a1[i] !== a2[i]) { 908 return false; 909 } 910 } 911 912 return true; 913 }, 914 915 /** 916 * Removes an element from the given array 917 * @param {Array} ar 918 * @param el 919 * @returns {Array} 920 */ 921 removeElementFromArray: function (ar, el) { 922 var i; 923 924 for (i = 0; i < ar.length; i++) { 925 if (ar[i] === el) { 926 ar.splice(i, 1); 927 return ar; 928 } 929 } 930 931 return ar; 932 }, 933 934 /** 935 * Truncate a number <tt>n</tt> after <tt>p</tt> decimals. 936 * @param {Number} n 937 * @param {Number} p 938 * @returns {Number} 939 */ 940 trunc: function (n, p) { 941 p = JXG.def(p, 0); 942 943 return this.toFixed(n, p); 944 }, 945 946 /** 947 * Decimal adjustment of a number. 948 * From https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Math/round 949 * 950 * @param {String} type The type of adjustment. 951 * @param {Number} value The number. 952 * @param {Number} exp The exponent (the 10 logarithm of the adjustment base). 953 * @returns {Number} The adjusted value. 954 * 955 * @private 956 */ 957 _decimalAdjust: function (type, value, exp) { 958 // If the exp is undefined or zero... 959 if (exp === undefined || +exp === 0) { 960 return Math[type](value); 961 } 962 963 value = +value; 964 exp = +exp; 965 // If the value is not a number or the exp is not an integer... 966 if (isNaN(value) || !(typeof exp === "number" && exp % 1 === 0)) { 967 return NaN; 968 } 969 970 // Shift 971 value = value.toString().split('e'); 972 value = Math[type](+(value[0] + "e" + (value[1] ? +value[1] - exp : -exp))); 973 974 // Shift back 975 value = value.toString().split('e'); 976 return +(value[0] + "e" + (value[1] ? +value[1] + exp : exp)); 977 }, 978 979 /** 980 * Round a number to given number of decimal digits. 981 * 982 * Example: JXG._toFixed(3.14159, -2) gives 3.14 983 * @param {Number} value Number to be rounded 984 * @param {Number} exp Number of decimal digits given as negative exponent 985 * @return {Number} Rounded number. 986 * 987 * @private 988 */ 989 _round10: function (value, exp) { 990 return this._decimalAdjust("round", value, exp); 991 }, 992 993 /** 994 * "Floor" a number to given number of decimal digits. 995 * 996 * Example: JXG._toFixed(3.14159, -2) gives 3.14 997 * @param {Number} value Number to be floored 998 * @param {Number} exp Number of decimal digits given as negative exponent 999 * @return {Number} "Floored" number. 1000 * 1001 * @private 1002 */ 1003 _floor10: function (value, exp) { 1004 return this._decimalAdjust("floor", value, exp); 1005 }, 1006 1007 /** 1008 * "Ceil" a number to given number of decimal digits. 1009 * 1010 * Example: JXG._toFixed(3.14159, -2) gives 3.15 1011 * @param {Number} value Number to be ceiled 1012 * @param {Number} exp Number of decimal digits given as negative exponent 1013 * @return {Number} "Ceiled" number. 1014 * 1015 * @private 1016 */ 1017 _ceil10: function (value, exp) { 1018 return this._decimalAdjust("ceil", value, exp); 1019 }, 1020 1021 /** 1022 * Replacement of the default toFixed() method. 1023 * It does a correct rounding (independent of the browser) and 1024 * returns "0.00" for toFixed(-0.000001, 2) instead of "-0.00" which 1025 * is returned by JavaScript's toFixed() 1026 * 1027 * @memberOf JXG 1028 * @param {Number} num Number tp be rounded 1029 * @param {Number} digits Decimal digits 1030 * @return {String} Rounded number is returned as string 1031 */ 1032 toFixed: function (num, digits) { 1033 return this._round10(num, -digits).toFixed(digits); 1034 }, 1035 1036 /** 1037 * Truncate a number <tt>val</tt> automatically. 1038 * @memberOf JXG 1039 * @param val 1040 * @returns {Number} 1041 */ 1042 autoDigits: function (val) { 1043 var x = Math.abs(val), 1044 str; 1045 1046 if (x >= 0.1) { 1047 str = this.toFixed(val, 2); 1048 } else if (x >= 0.01) { 1049 str = this.toFixed(val, 4); 1050 } else if (x >= 0.0001) { 1051 str = this.toFixed(val, 6); 1052 } else { 1053 str = val; 1054 } 1055 return str; 1056 }, 1057 1058 /** 1059 * Convert value v. If v has the form 1060 * <ul> 1061 * <li> 'x%': return floating point number x * percentOfWhat * 0.01 1062 * <li> 'xfr': return floating point number x * percentOfWhat 1063 * <li> 'xpx': return x * convertPx or convertPx(x) or x 1064 * <li> x or 'x': return floating point number x 1065 * </ul> 1066 * @param {String|Number} v 1067 * @param {Number} percentOfWhat 1068 * @param {Function|Number|*} convertPx 1069 * @returns {String|Number} 1070 */ 1071 parseNumber: function(v, percentOfWhat, convertPx) { 1072 var str; 1073 1074 if (this.isString(v) && v.indexOf('%') > -1) { 1075 str = v.replace(/\s+%\s+/, ''); 1076 return parseFloat(str) * percentOfWhat * 0.01; 1077 } 1078 if (this.isString(v) && v.indexOf('fr') > -1) { 1079 str = v.replace(/\s+fr\s+/, ''); 1080 return parseFloat(str) * percentOfWhat; 1081 } 1082 if (this.isString(v) && v.indexOf('px') > -1) { 1083 str = v.replace(/\s+px\s+/, ''); 1084 str = parseFloat(str); 1085 if(this.isFunction(convertPx)) { 1086 return convertPx(str); 1087 } else if(this.isNumber(convertPx)) { 1088 return str * convertPx; 1089 } else { 1090 return str; 1091 } 1092 } 1093 // Number or String containing no unit 1094 return parseFloat(v); 1095 }, 1096 1097 /** 1098 * Parse a string for label positioning of the form 'left pos' or 'pos right' 1099 * and return e.g. 1100 * <tt>{ side: 'left', pos: 'pos' }</tt>. 1101 * @param {String} str 1102 * @returns {Obj} <tt>{ side, pos }</tt> 1103 */ 1104 parsePosition: function(str) { 1105 var a, i, 1106 side = '', 1107 pos = ''; 1108 1109 str = str.trim(); 1110 if (str !== '') { 1111 a = str.split(/[ ,]+/); 1112 for (i = 0; i < a.length; i++) { 1113 if (a[i] === 'left' || a[i] === 'right') { 1114 side = a[i]; 1115 } else { 1116 pos = a[i]; 1117 } 1118 } 1119 } 1120 1121 return { 1122 side: side, 1123 pos: pos 1124 }; 1125 }, 1126 1127 /** 1128 * Extracts the keys of a given object. 1129 * @param object The object the keys are to be extracted 1130 * @param onlyOwn If true, hasOwnProperty() is used to verify that only keys are collected 1131 * the object owns itself and not some other object in the prototype chain. 1132 * @returns {Array} All keys of the given object. 1133 */ 1134 keys: function (object, onlyOwn) { 1135 var keys = [], 1136 property; 1137 1138 // the caller decides if we use hasOwnProperty 1139 /*jslint forin:true*/ 1140 for (property in object) { 1141 if (onlyOwn) { 1142 if (object.hasOwnProperty(property)) { 1143 keys.push(property); 1144 } 1145 } else { 1146 keys.push(property); 1147 } 1148 } 1149 /*jslint forin:false*/ 1150 1151 return keys; 1152 }, 1153 1154 /** 1155 * This outputs an object with a base class reference to the given object. This is useful if 1156 * you need a copy of an e.g. attributes object and want to overwrite some of the attributes 1157 * without changing the original object. 1158 * @param {Object} obj Object to be embedded. 1159 * @returns {Object} An object with a base class reference to <tt>obj</tt>. 1160 */ 1161 clone: function (obj) { 1162 var cObj = {}; 1163 1164 cObj.prototype = obj; 1165 1166 return cObj; 1167 }, 1168 1169 /** 1170 * Embeds an existing object into another one just like {@link #clone} and copies the contents of the second object 1171 * to the new one. Warning: The copied properties of obj2 are just flat copies. 1172 * @param {Object} obj Object to be copied. 1173 * @param {Object} obj2 Object with data that is to be copied to the new one as well. 1174 * @returns {Object} Copy of given object including some new/overwritten data from obj2. 1175 */ 1176 cloneAndCopy: function (obj, obj2) { 1177 var r, 1178 cObj = function () { 1179 return undefined; 1180 }; 1181 1182 cObj.prototype = obj; 1183 1184 // no hasOwnProperty on purpose 1185 /*jslint forin:true*/ 1186 /*jshint forin:true*/ 1187 1188 for (r in obj2) { 1189 cObj[r] = obj2[r]; 1190 } 1191 1192 /*jslint forin:false*/ 1193 /*jshint forin:false*/ 1194 1195 return cObj; 1196 }, 1197 1198 /** 1199 * Recursively merges obj2 into obj1 in-place. Contrary to {@link JXG#deepCopy} this won't create a new object 1200 * but instead will overwrite obj1. 1201 * <p> 1202 * In contrast to method JXG.mergeAttr, merge recurses into any kind of object, e.g. DOM object and JSXGraph objects. 1203 * So, please be careful. 1204 * @param {Object} obj1 1205 * @param {Object} obj2 1206 * @returns {Object} 1207 * @see JXG.mergeAttr 1208 * 1209 * @example 1210 * JXG.Options = JXG.merge(JXG.Options, { 1211 * board: { 1212 * showNavigation: false, 1213 * showInfobox: true 1214 * }, 1215 * point: { 1216 * face: 'o', 1217 * size: 4, 1218 * fillColor: '#eeeeee', 1219 * highlightFillColor: '#eeeeee', 1220 * strokeColor: 'white', 1221 * highlightStrokeColor: 'white', 1222 * showInfobox: 'inherit' 1223 * } 1224 * }); 1225 * 1226 * </pre><div id="JXGc5bf0f2a-bd5a-4612-97c2-09f17b1bbc6b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1227 * <script type="text/javascript"> 1228 * (function() { 1229 * var board = JXG.JSXGraph.initBoard('JXGc5bf0f2a-bd5a-4612-97c2-09f17b1bbc6b', 1230 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 1231 * JXG.Options = JXG.merge(JXG.Options, { 1232 * board: { 1233 * showNavigation: false, 1234 * showInfobox: true 1235 * }, 1236 * point: { 1237 * face: 'o', 1238 * size: 4, 1239 * fillColor: '#eeeeee', 1240 * highlightFillColor: '#eeeeee', 1241 * strokeColor: 'white', 1242 * highlightStrokeColor: 'white', 1243 * showInfobox: 'inherit' 1244 * } 1245 * }); 1246 * 1247 * 1248 * })(); 1249 * 1250 * </script><pre> 1251 */ 1252 merge: function (obj1, obj2) { 1253 var i, j, o, oo; 1254 1255 for (i in obj2) { 1256 if (obj2.hasOwnProperty(i)) { 1257 o = obj2[i]; 1258 if (this.isArray(o)) { 1259 if (!obj1[i]) { 1260 obj1[i] = []; 1261 } 1262 1263 for (j = 0; j < o.length; j++) { 1264 oo = obj2[i][j]; 1265 if (typeof obj2[i][j] === 'object') { 1266 obj1[i][j] = this.merge(obj1[i][j], oo); 1267 } else { 1268 obj1[i][j] = obj2[i][j]; 1269 } 1270 } 1271 } else if (typeof o === 'object') { 1272 if (!obj1[i]) { 1273 obj1[i] = {}; 1274 } 1275 1276 obj1[i] = this.merge(obj1[i], o); 1277 } else { 1278 if (typeof obj1 === 'boolean') { 1279 // This is necessary in the following scenario: 1280 // lastArrow == false 1281 // and call of 1282 // setAttribute({lastArrow: {type: 7}}) 1283 obj1 = {}; 1284 } 1285 obj1[i] = o; 1286 } 1287 } 1288 } 1289 1290 return obj1; 1291 }, 1292 1293 /** 1294 * Creates a deep copy of an existing object, i.e. arrays or sub-objects are copied component resp. 1295 * element-wise instead of just copying the reference. If a second object is supplied, the two objects 1296 * are merged into one object. The properties of the second object have priority. 1297 * @param {Object} obj This object will be copied. 1298 * @param {Object} obj2 This object will merged into the newly created object 1299 * @param {Boolean} [toLower=false] If true the keys are convert to lower case. This is needed for visProp, see JXG#copyAttributes 1300 * @returns {Object} copy of obj or merge of obj and obj2. 1301 */ 1302 deepCopy: function (obj, obj2, toLower) { 1303 var c, i, prop, i2; 1304 1305 toLower = toLower || false; 1306 if (typeof obj !== 'object' || obj === null) { 1307 return obj; 1308 } 1309 1310 // Missing hasOwnProperty is on purpose in this function 1311 if (this.isArray(obj)) { 1312 c = []; 1313 for (i = 0; i < obj.length; i++) { 1314 prop = obj[i]; 1315 // Attention: typeof null === 'object' 1316 if (prop !== null && typeof prop === 'object') { 1317 // We certainly do not want to recurse into a JSXGraph object. 1318 // This would for sure result in an infinite recursion. 1319 // As alternative we copy the id of the object. 1320 if (this.exists(prop.board)) { 1321 c[i] = prop.id; 1322 } else { 1323 c[i] = this.deepCopy(prop, {}, toLower); 1324 } 1325 } else { 1326 c[i] = prop; 1327 } 1328 } 1329 } else { 1330 c = {}; 1331 for (i in obj) { 1332 if (obj.hasOwnProperty(i)) { 1333 i2 = toLower ? i.toLowerCase() : i; 1334 prop = obj[i]; 1335 if (prop !== null && typeof prop === 'object') { 1336 if (this.exists(prop.board)) { 1337 c[i2] = prop.id; 1338 } else { 1339 c[i2] = this.deepCopy(prop, {}, toLower); 1340 } 1341 } else { 1342 c[i2] = prop; 1343 } 1344 } 1345 } 1346 1347 for (i in obj2) { 1348 if (obj2.hasOwnProperty(i)) { 1349 i2 = toLower ? i.toLowerCase() : i; 1350 1351 prop = obj2[i]; 1352 if (prop !== null && typeof prop === 'object') { 1353 if (this.isArray(prop) || !this.exists(c[i2])) { 1354 c[i2] = this.deepCopy(prop, {}, toLower); 1355 } else { 1356 c[i2] = this.deepCopy(c[i2], prop, toLower); 1357 } 1358 } else { 1359 c[i2] = prop; 1360 } 1361 } 1362 } 1363 } 1364 1365 return c; 1366 }, 1367 1368 /** 1369 * In-place (deep) merging of attributes. Allows attributes like `{shadow: {enabled: true...}}` 1370 * <p> 1371 * In contrast to method JXG.merge, mergeAttr does not recurse into DOM objects and JSXGraph objects. Instead 1372 * handles (pointers) to these objects are used. 1373 * 1374 * @param {Object} attr Object with attributes - usually containing default options - that will be changed in-place. 1375 * @param {Object} special Special option values which overwrite (recursively) the default options 1376 * @param {Boolean} [toLower=true] If true the keys are converted to lower case. 1377 * @param {Boolean} [ignoreUndefinedSpecials=false] If true the values in special that are undefined are not used. 1378 * 1379 * @see JXG.merge 1380 * 1381 */ 1382 mergeAttr: function (attr, special, toLower, ignoreUndefinedSpecials) { 1383 var e, e2, o; 1384 1385 toLower = toLower || true; 1386 ignoreUndefinedSpecials = ignoreUndefinedSpecials || false; 1387 1388 for (e in special) { 1389 if (special.hasOwnProperty(e)) { 1390 e2 = (toLower) ? e.toLowerCase(): e; 1391 // Key already exists, but not in lower case 1392 if (e2 !== e && attr.hasOwnProperty(e)) { 1393 if (attr.hasOwnProperty(e2)) { 1394 // Lower case key already exists - this should not happen 1395 // We have to unify the two key-value pairs 1396 // It is not clear which has precedence. 1397 this.mergeAttr(attr[e2], attr[e], toLower); 1398 } else { 1399 attr[e2] = attr[e]; 1400 } 1401 delete attr[e]; 1402 } 1403 1404 o = special[e]; 1405 if (this.isObject(o) && o !== null && 1406 // Do not recurse into a document object or a JSXGraph object 1407 !this.isDocumentOrFragment(o) && !this.exists(o.board) && 1408 // Do not recurse if a string is provided as "new String(...)" 1409 typeof o.valueOf() !== 'string') { 1410 if (attr[e2] === undefined || attr[e2] === null || !this.isObject(attr[e2])) { 1411 // The last test handles the case: 1412 // attr.draft = false; 1413 // special.draft = { strokewidth: 4} 1414 attr[e2] = {}; 1415 } 1416 this.mergeAttr(attr[e2], o, toLower); 1417 } else if(!ignoreUndefinedSpecials || this.exists(o)) { 1418 // Flat copy 1419 // This is also used in the cases 1420 // attr.shadow = { enabled: true ...} 1421 // special.shadow = false; 1422 // and 1423 // special.anchor is a JSXGraph element 1424 attr[e2] = o; 1425 } 1426 } 1427 } 1428 }, 1429 1430 /** 1431 * Convert an object to a new object containing only 1432 * lower case properties. 1433 * 1434 * @param {Object} obj 1435 * @returns Object 1436 * @example 1437 * var attr = JXG.keysToLowerCase({radiusPoint: {visible: false}}); 1438 * 1439 * // return {radiuspoint: {visible: false}} 1440 */ 1441 keysToLowerCase: function (obj) { 1442 var key, val, 1443 keys = Object.keys(obj), 1444 n = keys.length, 1445 newObj = {}; 1446 1447 if (typeof obj !== 'object') { 1448 return obj; 1449 } 1450 1451 while (n--) { 1452 key = keys[n]; 1453 if (obj.hasOwnProperty(key)) { 1454 // We recurse into an object only if it is 1455 // neither a DOM node nor an JSXGraph object 1456 val = obj[key]; 1457 if (typeof val === 'object' && val !== null && 1458 !this.isArray(val) && 1459 !this.exists(val.nodeType) && 1460 !this.exists(val.board)) { 1461 newObj[key.toLowerCase()] = this.keysToLowerCase(val); 1462 } else { 1463 newObj[key.toLowerCase()] = val; 1464 } 1465 } 1466 } 1467 return newObj; 1468 }, 1469 1470 /** 1471 * Generates an attributes object that is filled with default values from the Options object 1472 * and overwritten by the user specified attributes. 1473 * @param {Object} attributes user specified attributes 1474 * @param {Object} options defaults options 1475 * @param {String} s variable number of strings, e.g. 'slider', subtype 'point1'. Must be provided in lower case! 1476 * @returns {Object} The resulting attributes object 1477 */ 1478 copyAttributes: function (attributes, options, s) { 1479 var a, arg, i, len, o, isAvail, 1480 primitives = { 1481 circle: 1, 1482 curve: 1, 1483 foreignobject: 1, 1484 image: 1, 1485 line: 1, 1486 point: 1, 1487 polygon: 1, 1488 text: 1, 1489 ticks: 1, 1490 integral: 1 1491 }; 1492 1493 len = arguments.length; 1494 // Old code: if (len < 3 || primitives[s]) { 1495 // If len > 3, the element is certainly not a primitive object, 1496 // e.g. copyAttributes(attributes, JXG.Options, 'line', 'point1'). 1497 // That is, a later create('point', ...) will be the primitive call. 1498 // This will not yet cover all cases of inheritance. 1499 if (len < 3 || (len === 3 && primitives[s])) { 1500 // Default options from Options.elements 1501 a = JXG.deepCopy(options.elements, null, true); 1502 } else { 1503 a = {}; 1504 } 1505 1506 // Only the layer of the main element is set. 1507 if (len < 4 && this.exists(s) && this.exists(options.layer[s])) { 1508 a.layer = options.layer[s]; 1509 } 1510 1511 // Default options from the specific element like 'line' in 1512 // copyAttribute(attributes, board.options, 'line') 1513 // but also like in 1514 // Type.copyAttributes(attributes, board.options, 'view3d', 'az', 'slider'); 1515 o = options; 1516 isAvail = true; 1517 for (i = 2; i < len; i++) { 1518 arg = arguments[i]; 1519 if (this.exists(o[arg])) { 1520 o = o[arg]; 1521 } else { 1522 isAvail = false; 1523 break; 1524 } 1525 } 1526 if (isAvail) { 1527 a = JXG.deepCopy(a, o, true); 1528 } 1529 1530 // Merge the specific options given in the parameter 'attributes' 1531 // into the default options. 1532 // Additionally, we step into a sub-element of attribute like line.point1 - 1533 // in case it is supplied as in 1534 // copyAttribute(attributes, board.options, 'line', 'point1') 1535 // In this case we would merge attributes.point1 into the global line.point1 attributes. 1536 o = (typeof attributes === 'object') ? this.keysToLowerCase(attributes) : {}; 1537 isAvail = true; 1538 for (i = 3; i < len; i++) { 1539 arg = arguments[i].toLowerCase(); 1540 if (this.exists(o[arg])) { 1541 o = o[arg]; 1542 } else { 1543 isAvail = false; 1544 break; 1545 } 1546 } 1547 if (isAvail) { 1548 this.mergeAttr(a, o, true); 1549 } 1550 1551 if (arguments[2] === 'board') { 1552 // For board attributes we are done now. 1553 return a; 1554 } 1555 1556 // Special treatment of labels 1557 o = options; 1558 isAvail = true; 1559 for (i = 2; i < len; i++) { 1560 arg = arguments[i]; 1561 if (this.exists(o[arg])) { 1562 o = o[arg]; 1563 } else { 1564 isAvail = false; 1565 break; 1566 } 1567 } 1568 if (isAvail && this.exists(o.label)) { 1569 a.label = JXG.deepCopy(o.label, a.label, true); 1570 } 1571 a.label = JXG.deepCopy(options.label, a.label, true); 1572 1573 return a; 1574 }, 1575 1576 /** 1577 * Copy all prototype methods from object "superObject" to object 1578 * "subObject". The constructor of superObject will be available 1579 * in subObject as subObject.constructor[constructorName]. 1580 * @param {Object} subObject A JavaScript object which receives new methods. 1581 * @param {Object} superObject A JavaScript object which lends its prototype methods to subObject 1582 * @param {String} constructorName Under this name the constructor of superObj will be available 1583 * in subObject. 1584 * @private 1585 */ 1586 copyPrototypeMethods: function (subObject, superObject, constructorName) { 1587 var key; 1588 1589 subObject.prototype[constructorName] = superObject.prototype.constructor; 1590 for (key in superObject.prototype) { 1591 if (superObject.prototype.hasOwnProperty(key)) { 1592 if (key === 'methodMap') { 1593 JXG.copyMethodMap(subObject, superObject.prototype.methodMap); 1594 } else { 1595 subObject.prototype[key] = superObject.prototype[key]; 1596 } 1597 } 1598 } 1599 }, 1600 1601 /** 1602 * Create a copy of methodMap in "objectClass.prototype" and optional extend it. 1603 * If objectClass.prototype.methodMap does not exist, it will be initialized. 1604 * 1605 * The methodMap determines which methods can be called from within JessieCode and under which name it 1606 * can be used. The map is saved in an object, the name of a property is the name of the method used in JessieCode, 1607 * the value of a property is the name of the method in JavaScript. 1608 * 1609 * @param {Object} objectClass 1610 * @param {Object} [extension] 1611 * @private 1612 */ 1613 copyMethodMap: function (objectClass, extension) { 1614 extension = extension || {}; 1615 1616 objectClass.prototype.methodMap = objectClass.prototype.methodMap || {}; 1617 objectClass.prototype.methodMap = this.deepCopy(objectClass.prototype.methodMap, extension); 1618 }, 1619 1620 /** 1621 * Copy methodMap of "object.prototype" to objects instance and optional extend it. 1622 * If extension is of type String and extensionValue is defined, a key-value-pair is added. 1623 * 1624 * The methodMap determines which methods can be called from within JessieCode and under which name it 1625 * can be used. The map is saved in an object, the name of a property is the name of the method used in JessieCode, 1626 * the value of a property is the name of the method in JavaScript. 1627 * 1628 * @param {Object} object 1629 * @param {Object|String} [extension] 1630 * @param {String} [extensionValue] 1631 * @private 1632 */ 1633 extendInstanceMethodMap: function (object, extension, extensionValue) { 1634 extension = extension || {}; 1635 1636 // Create own copy only if instance still uses prototype version 1637 if (!object.hasOwnProperty("methodMap")) { 1638 object.methodMap = Object.assign({}, object.methodMap); 1639 } 1640 1641 if (this.isObject(extension)) { 1642 object.methodMap = this.deepCopy(object.methodMap, extension); 1643 } else if (this.isString(extension) && this.exists(extensionValue)) { 1644 object.methodMap[extension] = extensionValue; 1645 } 1646 }, 1647 1648 /** 1649 * Create a stripped down version of a JSXGraph element for cloning to the background. 1650 * Used in {JXG.GeometryElement#cloneToBackground} for creating traces. 1651 * 1652 * @param {JXG.GeometryElement} el Element to be cloned 1653 * @returns Object Cloned element 1654 * @private 1655 */ 1656 getCloneObject: function(el) { 1657 var obj, key, 1658 copy = {}; 1659 1660 copy.id = el.id + "T" + el.numTraces; 1661 el.numTraces += 1; 1662 1663 copy.coords = el.coords; 1664 obj = this.deepCopy(el.visProp, el.visProp.traceattributes, true); 1665 copy.visProp = {}; 1666 for (key in obj) { 1667 if (obj.hasOwnProperty(key)) { 1668 if ( 1669 key.indexOf('aria') !== 0 && 1670 key.indexOf('highlight') !== 0 && 1671 key.indexOf('attractor') !== 0 && 1672 key !== 'label' && 1673 key !== 'needsregularupdate' && 1674 key !== 'infoboxdigits' 1675 ) { 1676 copy.visProp[key] = el.eval(obj[key]); 1677 } 1678 } 1679 } 1680 copy.evalVisProp = function(val) { 1681 return copy.visProp[val]; 1682 }; 1683 copy.eval = function(val) { 1684 return val; 1685 }; 1686 1687 copy.visProp.layer = el.board.options.layer.trace; 1688 copy.visProp.tabindex = null; 1689 copy.visProp.highlight = false; 1690 copy.board = el.board; 1691 copy.elementClass = el.elementClass; 1692 1693 this.clearVisPropOld(copy); 1694 copy.visPropCalc = { 1695 visible: el.evalVisProp('visible') 1696 }; 1697 1698 return copy; 1699 }, 1700 1701 /** 1702 * Converts a JavaScript object into a JSON string. 1703 * @param {Object} obj A JavaScript object, functions will be ignored. 1704 * @param {Boolean} [noquote=false] No quotes around the name of a property. 1705 * @returns {String} The given object stored in a JSON string. 1706 * @deprecated 1707 */ 1708 toJSON: function (obj, noquote) { 1709 var list, prop, i, s, val; 1710 1711 noquote = JXG.def(noquote, false); 1712 1713 // check for native JSON support: 1714 if (JSON !== undefined && JSON.stringify && !noquote) { 1715 try { 1716 s = JSON.stringify(obj); 1717 return s; 1718 } catch (e) { 1719 // if something goes wrong, e.g. if obj contains functions we won't return 1720 // and use our own implementation as a fallback 1721 } 1722 } 1723 1724 switch (typeof obj) { 1725 case "object": 1726 if (obj) { 1727 list = []; 1728 1729 if (this.isArray(obj)) { 1730 for (i = 0; i < obj.length; i++) { 1731 list.push(JXG.toJSON(obj[i], noquote)); 1732 } 1733 1734 return "[" + list.join(",") + "]"; 1735 } 1736 1737 for (prop in obj) { 1738 if (obj.hasOwnProperty(prop)) { 1739 try { 1740 val = JXG.toJSON(obj[prop], noquote); 1741 } catch (e2) { 1742 val = ""; 1743 } 1744 1745 if (noquote) { 1746 list.push(prop + ":" + val); 1747 } else { 1748 list.push('"' + prop + '":' + val); 1749 } 1750 } 1751 } 1752 1753 return "{" + list.join(",") + "} "; 1754 } 1755 return 'null'; 1756 case "string": 1757 return "'" + obj.replace(/(["'])/g, "\\$1") + "'"; 1758 case "number": 1759 case "boolean": 1760 return obj.toString(); 1761 } 1762 1763 return '0'; 1764 }, 1765 1766 /** 1767 * Resets visPropOld. 1768 * @param {JXG.GeometryElement} el 1769 * @returns {GeometryElement} 1770 */ 1771 clearVisPropOld: function (el) { 1772 el.visPropOld = { 1773 cssclass: "", 1774 cssdefaultstyle: "", 1775 cssstyle: "", 1776 fillcolor: "", 1777 fillopacity: "", 1778 firstarrow: false, 1779 fontsize: -1, 1780 lastarrow: false, 1781 left: -100000, 1782 linecap: "", 1783 shadow: false, 1784 strokecolor: "", 1785 strokeopacity: "", 1786 strokewidth: "", 1787 tabindex: -100000, 1788 transitionduration: 0, 1789 top: -100000, 1790 visible: null 1791 }; 1792 1793 return el; 1794 }, 1795 1796 /** 1797 * Checks if an object contains a key, whose value equals to val. 1798 * @param {Object} obj 1799 * @param val 1800 * @returns {Boolean} 1801 */ 1802 isInObject: function (obj, val) { 1803 var el; 1804 1805 for (el in obj) { 1806 if (obj.hasOwnProperty(el)) { 1807 if (obj[el] === val) { 1808 return true; 1809 } 1810 } 1811 } 1812 1813 return false; 1814 }, 1815 1816 /** 1817 * Replaces all occurences of & by &, > by >, and < by <. 1818 * @param {String} str 1819 * @returns {String} 1820 */ 1821 escapeHTML: function (str) { 1822 return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 1823 }, 1824 1825 /** 1826 * Eliminates all substrings enclosed by < and > and replaces all occurences of 1827 * & by &, > by >, and < by <. 1828 * @param {String} str 1829 * @returns {String} 1830 */ 1831 unescapeHTML: function (str) { 1832 // This regex is NOT insecure. We are replacing everything found with '' 1833 /*jslint regexp:true*/ 1834 return str 1835 .replace(/<\/?[^>]+>/gi, "") 1836 .replace(/&/g, "&") 1837 .replace(/</g, "<") 1838 .replace(/>/g, ">"); 1839 }, 1840 1841 /** 1842 * Makes a string lower case except for the first character which will be upper case. 1843 * @param {String} str Arbitrary string 1844 * @returns {String} The capitalized string. 1845 */ 1846 capitalize: function (str) { 1847 return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); 1848 }, 1849 1850 /** 1851 * Make numbers given as strings nicer by removing all unnecessary leading and trailing zeroes. 1852 * @param {String} str 1853 * @returns {String} 1854 */ 1855 trimNumber: function (str) { 1856 str = str.replace(/^0+/, ""); 1857 str = str.replace(/0+$/, ""); 1858 1859 if (str[str.length - 1] === "." || str[str.length - 1] === ",") { 1860 str = str.slice(0, -1); 1861 } 1862 1863 if (str[0] === "." || str[0] === ",") { 1864 str = "0" + str; 1865 } 1866 1867 return str; 1868 }, 1869 1870 /** 1871 * Filter an array of elements. 1872 * @param {Array} list 1873 * @param {Object|function} filter 1874 * @returns {Array} 1875 */ 1876 filterElements: function (list, filter) { 1877 var i, 1878 f, 1879 item, 1880 flower, 1881 value, 1882 visPropValue, 1883 pass, 1884 l = list.length, 1885 result = []; 1886 1887 if (this.exists(filter) && typeof filter !== "function" && typeof filter !== 'object') { 1888 return result; 1889 } 1890 1891 for (i = 0; i < l; i++) { 1892 pass = true; 1893 item = list[i]; 1894 1895 if (typeof filter === 'object') { 1896 for (f in filter) { 1897 if (filter.hasOwnProperty(f)) { 1898 flower = f.toLowerCase(); 1899 1900 if (typeof item[f] === 'function') { 1901 value = item[f](); 1902 } else { 1903 value = item[f]; 1904 } 1905 1906 if (item.visProp && typeof item.visProp[flower] === 'function') { 1907 visPropValue = item.visProp[flower](); 1908 } else { 1909 visPropValue = item.visProp && item.visProp[flower]; 1910 } 1911 1912 if (typeof filter[f] === 'function') { 1913 pass = filter[f](value) || filter[f](visPropValue); 1914 } else { 1915 pass = value === filter[f] || visPropValue === filter[f]; 1916 } 1917 1918 if (!pass) { 1919 break; 1920 } 1921 } 1922 } 1923 } else if (typeof filter === 'function') { 1924 pass = filter(item); 1925 } 1926 1927 if (pass) { 1928 result.push(item); 1929 } 1930 } 1931 1932 return result; 1933 }, 1934 1935 /** 1936 * Remove all leading and trailing whitespaces from a given string. 1937 * @param {String} str 1938 * @returns {String} 1939 */ 1940 trim: function (str) { 1941 // str = str.replace(/^\s+/, ''); 1942 // str = str.replace(/\s+$/, ''); 1943 // 1944 // return str; 1945 return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ""); 1946 }, 1947 1948 /** 1949 * Convert a floating point number to a string integer + fraction. 1950 * Returns either a string of the form '3 1/3' (in case of useTeX=false) 1951 * or '3 \\frac{1}{3}' (in case of useTeX=true). 1952 * 1953 * @param {Number} x 1954 * @param {Boolean} [useTeX=false] 1955 * @param {Number} [order=0.001] 1956 * @returns {String} 1957 * @see JXG.Math.decToFraction 1958 */ 1959 toFraction: function (x, useTeX, order) { 1960 var arr = Mat.decToFraction(x, order), 1961 str = ''; 1962 1963 if (arr[1] === 0 && arr[2] === 0) { 1964 // 0 1965 str += '0'; 1966 } else { 1967 // Sign 1968 if (arr[0] < 0) { 1969 str += '-'; 1970 } 1971 if (arr[2] === 0) { 1972 // Integer 1973 str += arr[1]; 1974 } else if (!(arr[2] === 1 && arr[3] === 1)) { 1975 // Proper fraction 1976 if (arr[1] !== 0) { 1977 // Absolute value larger than 1 1978 str += arr[1] + ' '; 1979 } 1980 // Add fractional part 1981 if (useTeX === true) { 1982 str += '\\frac{' + arr[2] + '}{' + arr[3] + '}'; 1983 } else { 1984 str += arr[2] + '/' + arr[3]; 1985 } 1986 } 1987 } 1988 return str; 1989 }, 1990 1991 /** 1992 * Concat array src to array dest. 1993 * Uses push instead of JavaScript concat, which is much 1994 * faster. 1995 * The array dest is changed in place. 1996 * <p><b>Attention:</b> if "dest" is an anonymous array, the correct result is returned from the function. 1997 * 1998 * @param {Array} dest 1999 * @param {Array} src 2000 * @returns Array 2001 */ 2002 concat: function(dest, src) { 2003 var i, 2004 le = src.length; 2005 for (i = 0; i < le; i++) { 2006 dest.push(src[i]); 2007 } 2008 return dest; 2009 }, 2010 2011 /** 2012 * Convert HTML tags to entities or use html_sanitize if the google caja html sanitizer is available. 2013 * @param {String} str 2014 * @param {Boolean} caja 2015 * @returns {String} Sanitized string 2016 */ 2017 sanitizeHTML: function (str, caja) { 2018 if (typeof html_sanitize === "function" && caja) { 2019 return html_sanitize( 2020 str, 2021 function () { 2022 return undefined; 2023 }, 2024 function (id) { 2025 return id; 2026 } 2027 ); 2028 } 2029 2030 if (str && typeof str === 'string') { 2031 str = str.replace(/</g, "<").replace(/>/g, ">"); 2032 } 2033 2034 return str; 2035 }, 2036 2037 /** 2038 * If <tt>s</tt> is a slider, it returns the sliders value, otherwise it just returns the given value. 2039 * @param {*} s 2040 * @returns {*} s.Value() if s is an element of type slider, s otherwise 2041 */ 2042 evalSlider: function (s) { 2043 if (s && s.type === Const.OBJECT_TYPE_GLIDER && typeof s.Value === 'function') { 2044 return s.Value(); 2045 } 2046 2047 return s; 2048 }, 2049 2050 /** 2051 * Convert a string containing a MAXIMA /STACK expression into a JSXGraph / JessieCode string 2052 * or an array of JSXGraph / JessieCode strings. 2053 * <p> 2054 * This function is meanwhile superseded by stack_jxg.stack2jsxgraph. 2055 * 2056 * @deprecated 2057 * 2058 * @example 2059 * console.log( JXG.stack2jsxgraph("%e**x") ); 2060 * // Output: 2061 * // "EULER**x" 2062 * 2063 * @example 2064 * console.log( JXG.stack2jsxgraph("[%pi*(x**2 - 1), %phi*(x - 1), %gamma*(x+1)]") ); 2065 * // Output: 2066 * // [ "PI*(x**2 - 1)", "1.618033988749895*(x - 1)", "0.5772156649015329*(x+1)" ] 2067 * 2068 * @param {String} str 2069 * @returns String 2070 */ 2071 stack2jsxgraph: function(str) { 2072 var t; 2073 2074 t = str. 2075 replace(/%pi/g, 'PI'). 2076 replace(/%e/g, 'EULER'). 2077 replace(/%phi/g, '1.618033988749895'). 2078 replace(/%gamma/g, '0.5772156649015329'). 2079 trim(); 2080 2081 // String containing array -> array containing strings 2082 if (t[0] === '[' && t[t.length - 1] === ']') { 2083 t = t.slice(1, -1).split(/\s*,\s*/); 2084 } 2085 2086 return t; 2087 } 2088 } 2089 ); 2090 2091 export default JXG; 2092