1 /* 2 Copyright 2008-2021 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 <http://www.gnu.org/licenses/> 30 and <http://opensource.org/licenses/MIT/>. 31 */ 32 33 /*global JXG: true, define: true, html_sanitize: true*/ 34 /*jslint nomen: true, plusplus: true*/ 35 36 /* depends: 37 jxg 38 base/constants 39 */ 40 41 /** 42 * @fileoverview type.js contains several functions to help deal with javascript's weak types. 43 * This file mainly consists of detector functions which verify if a variable is or is not of 44 * a specific type and converter functions that convert variables to another type or normalize 45 * the type of a variable. 46 */ 47 48 define([ 49 'jxg', 'base/constants' 50 ], function (JXG, Const) { 51 52 'use strict'; 53 54 JXG.extend(JXG, /** @lends JXG */ { 55 /** 56 * Checks if the given string is an id within the given board. 57 * @param {JXG.Board} board 58 * @param {String} s 59 * @returns {Boolean} 60 */ 61 isId: function (board, s) { 62 return (typeof s === 'string') && !!board.objects[s]; 63 }, 64 65 /** 66 * Checks if the given string is a name within the given board. 67 * @param {JXG.Board} board 68 * @param {String} s 69 * @returns {Boolean} 70 */ 71 isName: function (board, s) { 72 return typeof s === 'string' && !!board.elementsByName[s]; 73 }, 74 75 /** 76 * Checks if the given string is a group id within the given board. 77 * @param {JXG.Board} board 78 * @param {String} s 79 * @returns {Boolean} 80 */ 81 isGroup: function (board, s) { 82 return typeof s === 'string' && !!board.groups[s]; 83 }, 84 85 /** 86 * Checks if the value of a given variable is of type string. 87 * @param v A variable of any type. 88 * @returns {Boolean} True, if v is of type string. 89 */ 90 isString: function (v) { 91 return typeof v === 'string'; 92 }, 93 94 /** 95 * Checks if the value of a given variable is of type number. 96 * @param v A variable of any type. 97 * @returns {Boolean} True, if v is of type number. 98 */ 99 isNumber: function (v) { 100 return typeof v === 'number' || Object.prototype.toString.call(v) === '[Object Number]'; 101 }, 102 103 /** 104 * Checks if a given variable references a function. 105 * @param v A variable of any type. 106 * @returns {Boolean} True, if v is a function. 107 */ 108 isFunction: function (v) { 109 return typeof v === 'function'; 110 }, 111 112 /** 113 * Checks if a given variable references an array. 114 * @param v A variable of any type. 115 * @returns {Boolean} True, if v is of type array. 116 */ 117 isArray: function (v) { 118 var r; 119 120 // use the ES5 isArray() method and if that doesn't exist use a fallback. 121 if (Array.isArray) { 122 r = Array.isArray(v); 123 } else { 124 r = (v !== null && typeof v === 'object' && typeof v.splice === 'function' && typeof v.join === 'function'); 125 } 126 127 return r; 128 }, 129 130 /** 131 * Tests if the input variable is an Object 132 * @param v 133 */ 134 isObject: function (v) { 135 return typeof v === 'object' && !this.isArray(v); 136 }, 137 138 /** 139 * Checks if a given variable is a reference of a JSXGraph Point element. 140 * @param v A variable of any type. 141 * @returns {Boolean} True, if v is of type JXG.Point. 142 */ 143 isPoint: function (v) { 144 if (v !== null && typeof v === 'object') { 145 return (v.elementClass === Const.OBJECT_CLASS_POINT); 146 } 147 148 return false; 149 }, 150 151 /** 152 * Checks if a given variable is a reference of a JSXGraph Point element or an array of length at least two or 153 * a function returning an array of length two or three. 154 * @param {JXG.Board} board 155 * @param v A variable of any type. 156 * @returns {Boolean} True, if v is of type JXG.Point. 157 */ 158 isPointType: function (board, v) { 159 var val, p; 160 161 if (this.isArray(v)) { 162 return true; 163 } 164 if (this.isFunction(v)) { 165 val = v(); 166 if (this.isArray(val) && val.length > 1) { 167 return true; 168 } 169 } 170 p = board.select(v); 171 return this.isPoint(p); 172 }, 173 174 /** 175 * Checks if a given variable is a reference of a JSXGraph transformation element or an array 176 * of JSXGraph transformation elements. 177 * @param v A variable of any type. 178 * @returns {Boolean} True, if v is of type JXG.Transformation. 179 */ 180 isTransformationOrArray: function (v) { 181 if (v !== null) { 182 if (this.isArray(v) && v.length > 0) { 183 return this.isTransformationOrArray(v[0]); 184 } 185 if (typeof v === 'object') { 186 return (v.type === Const.OBJECT_TYPE_TRANSFORMATION); 187 } 188 } 189 return false; 190 }, 191 192 /** 193 * Checks if a given variable is neither undefined nor null. You should not use this together with global 194 * variables! 195 * @param v A variable of any type. 196 * @param {Boolean} [checkEmptyString=false] If set to true, it is also checked whether v is not equal to ''. 197 * @returns {Boolean} True, if v is neither undefined nor null. 198 */ 199 exists: function (v, checkEmptyString) { 200 var result = !(v == undefined || v === null); 201 checkEmptyString = checkEmptyString || false; 202 203 if (checkEmptyString) { 204 return result && v !== ''; 205 } 206 return result; 207 }, 208 // exists: (function (undef) { 209 // return function (v, checkEmptyString) { 210 // var result = !(v === undef || v === null); 211 212 // checkEmptyString = checkEmptyString || false; 213 214 // if (checkEmptyString) { 215 // return result && v !== ''; 216 // } 217 // return result; 218 // }; 219 // }()), 220 221 /** 222 * Checks if v is an empty object or empty. 223 * @param v {Object|Array} 224 * @returns {boolean} True, if v is an empty object or array. 225 */ 226 isEmpty: function(v) { 227 return Object.keys(v).length === 0; 228 }, 229 230 /** 231 * Handle default parameters. 232 * @param v Given value 233 * @param d Default value 234 * @returns <tt>d</tt>, if <tt>v</tt> is undefined or null. 235 */ 236 def: function (v, d) { 237 if (this.exists(v)) { 238 return v; 239 } 240 241 return d; 242 }, 243 244 /** 245 * Converts a string containing either <strong>true</strong> or <strong>false</strong> into a boolean value. 246 * @param {String} s String containing either <strong>true</strong> or <strong>false</strong>. 247 * @returns {Boolean} String typed boolean value converted to boolean. 248 */ 249 str2Bool: function (s) { 250 if (!this.exists(s)) { 251 return true; 252 } 253 254 if (typeof s === 'boolean') { 255 return s; 256 } 257 258 if (this.isString(s)) { 259 return (s.toLowerCase() === 'true'); 260 } 261 262 return false; 263 }, 264 265 /** 266 * Convert a String, a number or a function into a function. This method is used in Transformation.js 267 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 268 * by a GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 269 * values is of type string. 270 * @param {Array} param An array containing strings, numbers, or functions. 271 * @param {Number} n Length of <tt>param</tt>. 272 * @returns {Function} A function taking one parameter k which specifies the index of the param element 273 * to evaluate. 274 */ 275 createEvalFunction: function (board, param, n) { 276 var f = [], i; 277 278 for (i = 0; i < n; i++) { 279 f[i] = JXG.createFunction(param[i], board, '', true); 280 } 281 282 return function (k) { 283 return f[k](); 284 }; 285 }, 286 287 /** 288 * Convert a String, number or function into a function. 289 * @param {String|Number|Function} term A variable of type string, function or number. 290 * @param {JXG.Board} board Reference to a JSXGraph board. It is required to resolve dependencies given 291 * by a GEONE<sub>X</sub>T string, thus it must be a valid reference only in case one of the param 292 * values is of type string. 293 * @param {String} variableName Only required if evalGeonext is set to true. Describes the variable name 294 * of the variable in a GEONE<sub>X</sub>T string given as term. 295 * @param {Boolean} [evalGeonext=true] Set this true, if term should be treated as a GEONE<sub>X</sub>T string. 296 * @returns {Function} A function evaluation the value given by term or null if term is not of type string, 297 * function or number. 298 */ 299 createFunction: function (term, board, variableName, evalGeonext) { 300 var f = null; 301 302 if ((!this.exists(evalGeonext) || evalGeonext) && this.isString(term)) { 303 // Convert GEONExT syntax into JavaScript syntax 304 //newTerm = JXG.GeonextParser.geonext2JS(term, board); 305 //return new Function(variableName,'return ' + newTerm + ';'); 306 307 //term = JXG.GeonextParser.replaceNameById(term, board); 308 //term = JXG.GeonextParser.geonext2JS(term, board); 309 f = board.jc.snippet(term, true, variableName, true); 310 } else if (this.isFunction(term)) { 311 f = term; 312 } else if (this.isNumber(term)) { 313 /** @ignore */ 314 f = function () { 315 return term; 316 }; 317 } else if (this.isString(term)) { 318 // In case of string function like fontsize 319 /** @ignore */ 320 f = function () { 321 return term; 322 }; 323 } 324 325 if (f !== null) { 326 f.origin = term; 327 } 328 329 return f; 330 }, 331 332 /** 333 * Test if the parents array contains existing points. If instead parents contains coordinate arrays or 334 * function returning coordinate arrays 335 * free points with these coordinates are created. 336 * 337 * @param {JXG.Board} board Board object 338 * @param {Array} parents Array containing parent elements for a new object. This array may contain 339 * <ul> 340 * <li> {@link JXG.Point} objects 341 * <li> {@link JXG.GeometryElement#name} of {@link JXG.Point} objects 342 * <li> {@link JXG.GeometryElement#id} of {@link JXG.Point} objects 343 * <li> Coordinates of points given as array of numbers of length two or three, e.g. [2, 3]. 344 * <li> Coordinates of points given as array of functions of length two or three. Each function returns one coordinate, e.g. 345 * [function(){ return 2; }, function(){ return 3; }] 346 * <li> Function returning coordinates, e.g. function() { return [2, 3]; } 347 * </ul> 348 * In the last three cases a new point will be created. 349 * @param {String} attrClass Main attribute class of newly created points, see {@link JXG#copyAttributes} 350 * @param {Array} attrArray List of subtype attributes for the newly created points. The list of subtypes is mapped to the list of new points. 351 * @returns {Array} List of newly created {@link JXG.Point} elements or false if not all returned elements are points. 352 */ 353 providePoints: function (board, parents, attributes, attrClass, attrArray) { 354 var i, j, 355 len, 356 lenAttr = 0, 357 points = [], attr, val; 358 359 if (!this.isArray(parents)) { 360 parents = [parents]; 361 } 362 len = parents.length; 363 if (this.exists(attrArray)) { 364 lenAttr = attrArray.length; 365 } 366 if (lenAttr === 0) { 367 attr = this.copyAttributes(attributes, board.options, attrClass); 368 } 369 370 for (i = 0; i < len; ++i) { 371 if (lenAttr > 0) { 372 j = Math.min(i, lenAttr - 1); 373 attr = this.copyAttributes(attributes, board.options, attrClass, attrArray[j]); 374 } 375 if (this.isArray(parents[i]) && parents[i].length > 1) { 376 points.push(board.create('point', parents[i], attr)); 377 points[points.length - 1]._is_new = true; 378 } else if (this.isFunction(parents[i])) { 379 val = parents[i](); 380 if (this.isArray(val) && (val.length > 1)) { 381 points.push(board.create('point', [parents[i]], attr)); 382 points[points.length - 1]._is_new = true; 383 } 384 } else { 385 points.push(board.select(parents[i])); 386 } 387 388 if (!this.isPoint(points[i])) { 389 return false; 390 } 391 } 392 393 return points; 394 }, 395 396 /** 397 * Generates a function which calls the function fn in the scope of owner. 398 * @param {Function} fn Function to call. 399 * @param {Object} owner Scope in which fn is executed. 400 * @returns {Function} A function with the same signature as fn. 401 */ 402 bind: function (fn, owner) { 403 return function () { 404 return fn.apply(owner, arguments); 405 }; 406 }, 407 408 /** 409 * If <tt>val</tt> is a function, it will be evaluated without giving any parameters, else the input value 410 * is just returned. 411 * @param val Could be anything. Preferably a number or a function. 412 * @returns If <tt>val</tt> is a function, it is evaluated and the result is returned. Otherwise <tt>val</tt> is returned. 413 */ 414 evaluate: function (val) { 415 if (this.isFunction(val)) { 416 return val(); 417 } 418 419 return val; 420 }, 421 422 /** 423 * Search an array for a given value. 424 * @param {Array} array 425 * @param value 426 * @param {String} [sub] Use this property if the elements of the array are objects. 427 * @returns {Number} The index of the first appearance of the given value, or 428 * <tt>-1</tt> if the value was not found. 429 */ 430 indexOf: function (array, value, sub) { 431 var i, s = this.exists(sub); 432 433 if (Array.indexOf && !s) { 434 return array.indexOf(value); 435 } 436 437 for (i = 0; i < array.length; i++) { 438 if ((s && array[i][sub] === value) || (!s && array[i] === value)) { 439 return i; 440 } 441 } 442 443 return -1; 444 }, 445 446 /** 447 * Eliminates duplicate entries in an array consisting of numbers and strings. 448 * @param {Array} a An array of numbers and/or strings. 449 * @returns {Array} The array with duplicate entries eliminated. 450 */ 451 eliminateDuplicates: function (a) { 452 var i, 453 len = a.length, 454 result = [], 455 obj = {}; 456 457 for (i = 0; i < len; i++) { 458 obj[a[i]] = 0; 459 } 460 461 for (i in obj) { 462 if (obj.hasOwnProperty(i)) { 463 result.push(i); 464 } 465 } 466 467 return result; 468 }, 469 470 /** 471 * Swaps to array elements. 472 * @param {Array} arr 473 * @param {Number} i 474 * @param {Number} j 475 * @returns {Array} Reference to the given array. 476 */ 477 swap: function (arr, i, j) { 478 var tmp; 479 480 tmp = arr[i]; 481 arr[i] = arr[j]; 482 arr[j] = tmp; 483 484 return arr; 485 }, 486 487 /** 488 * Generates a copy of an array and removes the duplicate entries. The original 489 * Array will be altered. 490 * @param {Array} arr 491 * @returns {Array} 492 */ 493 uniqueArray: function (arr) { 494 var i, j, isArray, ret = []; 495 496 if (arr.length === 0) { 497 return []; 498 } 499 500 for (i = 0; i < arr.length; i++) { 501 isArray = this.isArray(arr[i]); 502 503 if (!this.exists(arr[i])) { 504 arr[i] = ''; 505 continue; 506 } 507 for (j = i + 1; j < arr.length; j++) { 508 if (isArray && JXG.cmpArrays(arr[i], arr[j])) { 509 arr[i] = []; 510 } else if (!isArray && arr[i] === arr[j]) { 511 arr[i] = ''; 512 } 513 } 514 } 515 516 j = 0; 517 518 for (i = 0; i < arr.length; i++) { 519 isArray = this.isArray(arr[i]); 520 521 if (!isArray && arr[i] !== '') { 522 ret[j] = arr[i]; 523 j++; 524 } else if (isArray && arr[i].length !== 0) { 525 ret[j] = (arr[i].slice(0)); 526 j++; 527 } 528 } 529 530 arr = ret; 531 return ret; 532 }, 533 534 /** 535 * Checks if an array contains an element equal to <tt>val</tt> but does not check the type! 536 * @param {Array} arr 537 * @param val 538 * @returns {Boolean} 539 */ 540 isInArray: function (arr, val) { 541 return JXG.indexOf(arr, val) > -1; 542 }, 543 544 /** 545 * Converts an array of {@link JXG.Coords} objects into a coordinate matrix. 546 * @param {Array} coords 547 * @param {Boolean} split 548 * @returns {Array} 549 */ 550 coordsArrayToMatrix: function (coords, split) { 551 var i, 552 x = [], 553 m = []; 554 555 for (i = 0; i < coords.length; i++) { 556 if (split) { 557 x.push(coords[i].usrCoords[1]); 558 m.push(coords[i].usrCoords[2]); 559 } else { 560 m.push([coords[i].usrCoords[1], coords[i].usrCoords[2]]); 561 } 562 } 563 564 if (split) { 565 m = [x, m]; 566 } 567 568 return m; 569 }, 570 571 /** 572 * Compare two arrays. 573 * @param {Array} a1 574 * @param {Array} a2 575 * @returns {Boolean} <tt>true</tt>, if the arrays coefficients are of same type and value. 576 */ 577 cmpArrays: function (a1, a2) { 578 var i; 579 580 // trivial cases 581 if (a1 === a2) { 582 return true; 583 } 584 585 if (a1.length !== a2.length) { 586 return false; 587 } 588 589 for (i = 0; i < a1.length; i++) { 590 if (this.isArray(a1[i]) && this.isArray(a2[i])) { 591 if (!this.cmpArrays(a1[i], a2[i])) { 592 return false; 593 } 594 } else if (a1[i] !== a2[i]) { 595 return false; 596 } 597 } 598 599 return true; 600 }, 601 602 /** 603 * Removes an element from the given array 604 * @param {Array} ar 605 * @param el 606 * @returns {Array} 607 */ 608 removeElementFromArray: function (ar, el) { 609 var i; 610 611 for (i = 0; i < ar.length; i++) { 612 if (ar[i] === el) { 613 ar.splice(i, 1); 614 return ar; 615 } 616 } 617 618 return ar; 619 }, 620 621 /** 622 * Truncate a number <tt>n</tt> after <tt>p</tt> decimals. 623 * @param {Number} n 624 * @param {Number} p 625 * @returns {Number} 626 */ 627 trunc: function (n, p) { 628 p = JXG.def(p, 0); 629 630 return this.toFixed(n, p); 631 }, 632 633 /** 634 * Decimal adjustment of a number. 635 * From https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Math/round 636 * 637 * @param {String} type The type of adjustment. 638 * @param {Number} value The number. 639 * @param {Number} exp The exponent (the 10 logarithm of the adjustment base). 640 * @returns {Number} The adjusted value. 641 * 642 * @private 643 */ 644 _decimalAdjust: function (type, value, exp) { 645 // If the exp is undefined or zero... 646 if (exp === undefined || +exp === 0) { 647 return Math[type](value); 648 } 649 650 value = +value; 651 exp = +exp; 652 // If the value is not a number or the exp is not an integer... 653 if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { 654 return NaN; 655 } 656 657 // Shift 658 value = value.toString().split('e'); 659 value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); 660 661 // Shift back 662 value = value.toString().split('e'); 663 return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); 664 }, 665 666 /** 667 * Round a number to given number of decimal digits. 668 * 669 * Example: JXG._toFixed(3.14159, -2) gives 3.14 670 * @param {Number} value Number to be rounded 671 * @param {Number} exp Number of decimal digits given as negative exponent 672 * @return {Number} Rounded number. 673 * 674 * @private 675 */ 676 _round10: function (value, exp) { 677 return this._decimalAdjust('round', value, exp); 678 }, 679 680 /** 681 * "Floor" a number to given number of decimal digits. 682 * 683 * Example: JXG._toFixed(3.14159, -2) gives 3.14 684 * @param {Number} value Number to be floored 685 * @param {Number} exp Number of decimal digits given as negative exponent 686 * @return {Number} "Floored" number. 687 * 688 * @private 689 */ 690 _floor10: function (value, exp) { 691 return this._decimalAdjust('floor', value, exp); 692 }, 693 694 /** 695 * "Ceil" a number to given number of decimal digits. 696 * 697 * Example: JXG._toFixed(3.14159, -2) gives 3.15 698 * @param {Number} value Number to be ceiled 699 * @param {Number} exp Number of decimal digits given as negative exponent 700 * @return {Number} "Ceiled" number. 701 * 702 * @private 703 */ 704 _ceil10: function (value, exp) { 705 return this._decimalAdjust('ceil', value, exp); 706 }, 707 708 /** 709 * Replacement of the default toFixed() method. 710 * It does a correct rounding (independent of the browser) and 711 * returns "0.00" for toFixed(-0.000001, 2) instead of "-0.00" which 712 * is returned by JavaScript's toFixed() 713 * 714 * @memberOf JXG 715 * @param {Number} num Number tp be rounded 716 * @param {Number} precision Decimal digits 717 * @return {String} Rounded number is returned as string 718 */ 719 toFixed: function (num, precision) { 720 return this._round10(num, -precision).toFixed(precision); 721 }, 722 723 /** 724 * Truncate a number <tt>val</tt> automatically. 725 * @memberOf JXG 726 * @param val 727 * @returns {Number} 728 */ 729 autoDigits: function (val) { 730 var x = Math.abs(val), 731 str; 732 733 if (x > 0.1) { 734 str = this.toFixed(val, 2); 735 } else if (x >= 0.01) { 736 str = this.toFixed(val, 4); 737 } else if (x >= 0.0001) { 738 str = this.toFixed(val, 6); 739 } else { 740 str = val; 741 } 742 return str; 743 }, 744 745 /** 746 * Extracts the keys of a given object. 747 * @param object The object the keys are to be extracted 748 * @param onlyOwn If true, hasOwnProperty() is used to verify that only keys are collected 749 * the object owns itself and not some other object in the prototype chain. 750 * @returns {Array} All keys of the given object. 751 */ 752 keys: function (object, onlyOwn) { 753 var keys = [], property; 754 755 // the caller decides if we use hasOwnProperty 756 /*jslint forin:true*/ 757 for (property in object) { 758 if (onlyOwn) { 759 if (object.hasOwnProperty(property)) { 760 keys.push(property); 761 } 762 } else { 763 keys.push(property); 764 } 765 } 766 /*jslint forin:false*/ 767 768 return keys; 769 }, 770 771 /** 772 * This outputs an object with a base class reference to the given object. This is useful if 773 * you need a copy of an e.g. attributes object and want to overwrite some of the attributes 774 * without changing the original object. 775 * @param {Object} obj Object to be embedded. 776 * @returns {Object} An object with a base class reference to <tt>obj</tt>. 777 */ 778 clone: function (obj) { 779 var cObj = {}; 780 781 cObj.prototype = obj; 782 783 return cObj; 784 }, 785 786 /** 787 * Embeds an existing object into another one just like {@link #clone} and copies the contents of the second object 788 * to the new one. Warning: The copied properties of obj2 are just flat copies. 789 * @param {Object} obj Object to be copied. 790 * @param {Object} obj2 Object with data that is to be copied to the new one as well. 791 * @returns {Object} Copy of given object including some new/overwritten data from obj2. 792 */ 793 cloneAndCopy: function (obj, obj2) { 794 var r, 795 cObj = function () { return undefined; }; 796 797 cObj.prototype = obj; 798 799 // no hasOwnProperty on purpose 800 /*jslint forin:true*/ 801 /*jshint forin:true*/ 802 803 for (r in obj2) { 804 cObj[r] = obj2[r]; 805 } 806 807 /*jslint forin:false*/ 808 /*jshint forin:false*/ 809 810 return cObj; 811 }, 812 813 /** 814 * Recursively merges obj2 into obj1. Contrary to {@link JXG#deepCopy} this won't create a new object 815 * but instead will overwrite obj1. 816 * @param {Object} obj1 817 * @param {Object} obj2 818 * @returns {Object} 819 */ 820 merge: function (obj1, obj2) { 821 var i, j; 822 823 for (i in obj2) { 824 if (obj2.hasOwnProperty(i)) { 825 if (this.isArray(obj2[i])) { 826 if (!obj1[i]) { 827 obj1[i] = []; 828 } 829 830 for (j = 0; j < obj2[i].length; j++) { 831 if (typeof obj2[i][j] === 'object') { 832 obj1[i][j] = this.merge(obj1[i][j], obj2[i][j]); 833 } else { 834 obj1[i][j] = obj2[i][j]; 835 } 836 } 837 } else if (typeof obj2[i] === 'object') { 838 if (!obj1[i]) { 839 obj1[i] = {}; 840 } 841 842 obj1[i] = this.merge(obj1[i], obj2[i]); 843 } else { 844 obj1[i] = obj2[i]; 845 } 846 } 847 } 848 849 return obj1; 850 }, 851 852 /** 853 * Creates a deep copy of an existing object, i.e. arrays or sub-objects are copied component resp. 854 * element-wise instead of just copying the reference. If a second object is supplied, the two objects 855 * are merged into one object. The properties of the second object have priority. 856 * @param {Object} obj This object will be copied. 857 * @param {Object} obj2 This object will merged into the newly created object 858 * @param {Boolean} [toLower=false] If true the keys are convert to lower case. This is needed for visProp, see JXG#copyAttributes 859 * @returns {Object} copy of obj or merge of obj and obj2. 860 */ 861 deepCopy: function (obj, obj2, toLower) { 862 var c, i, prop, i2; 863 864 toLower = toLower || false; 865 866 if (typeof obj !== 'object' || obj === null) { 867 return obj; 868 } 869 870 // missing hasOwnProperty is on purpose in this function 871 if (this.isArray(obj)) { 872 c = []; 873 for (i = 0; i < obj.length; i++) { 874 prop = obj[i]; 875 if (typeof prop === 'object') { 876 // We certainly do not want to recurse into a JSXGraph object. 877 // This would for sure result in an infinite recursion. 878 // As alternative we copy the id of the object. 879 if (this.exists(prop.board)) { 880 c[i] = prop.id; 881 } else { 882 c[i] = this.deepCopy(prop); 883 } 884 } else { 885 c[i] = prop; 886 } 887 } 888 } else { 889 c = {}; 890 for (i in obj) { 891 if (obj.hasOwnProperty(i)) { 892 i2 = toLower ? i.toLowerCase() : i; 893 prop = obj[i]; 894 if (prop !== null && typeof prop === 'object') { 895 if (this.exists(prop.board)) { 896 c[i2] = prop.id; 897 } else { 898 c[i2] = this.deepCopy(prop); 899 } 900 } else { 901 c[i2] = prop; 902 } 903 } 904 } 905 906 for (i in obj2) { 907 if (obj2.hasOwnProperty(i)) { 908 i2 = toLower ? i.toLowerCase() : i; 909 910 prop = obj2[i]; 911 if (typeof prop === 'object') { 912 if (this.isArray(prop) || !this.exists(c[i2])) { 913 c[i2] = this.deepCopy(prop); 914 } else { 915 c[i2] = this.deepCopy(c[i2], prop, toLower); 916 } 917 } else { 918 c[i2] = prop; 919 } 920 } 921 } 922 } 923 924 return c; 925 }, 926 927 /** 928 * Generates an attributes object that is filled with default values from the Options object 929 * and overwritten by the user specified attributes. 930 * @param {Object} attributes user specified attributes 931 * @param {Object} options defaults options 932 * @param {String} s variable number of strings, e.g. 'slider', subtype 'point1'. 933 * @returns {Object} The resulting attributes object 934 */ 935 copyAttributes: function (attributes, options, s) { 936 var a, i, len, o, isAvail, 937 primitives = { 938 'circle': 1, 939 'curve': 1, 940 'image': 1, 941 'line': 1, 942 'point': 1, 943 'polygon': 1, 944 'text': 1, 945 'ticks': 1, 946 'integral': 1 947 }; 948 949 len = arguments.length; 950 if (len < 3 || primitives[s]) { 951 // default options from Options.elements 952 a = JXG.deepCopy(options.elements, null, true); 953 } else { 954 a = {}; 955 } 956 957 // Only the layer of the main element is set. 958 if (len < 4 && this.exists(s) && this.exists(options.layer[s])) { 959 a.layer = options.layer[s]; 960 } 961 962 // default options from specific elements 963 o = options; 964 isAvail = true; 965 for (i = 2; i < len; i++) { 966 if (this.exists(o[arguments[i]])) { 967 o = o[arguments[i]]; 968 } else { 969 isAvail = false; 970 break; 971 } 972 } 973 if (isAvail) { 974 a = JXG.deepCopy(a, o, true); 975 } 976 977 // options from attributes 978 o = attributes; 979 isAvail = true; 980 for (i = 3; i < len; i++) { 981 if (this.exists(o[arguments[i]])) { 982 o = o[arguments[i]]; 983 } else { 984 isAvail = false; 985 break; 986 } 987 } 988 if (isAvail) { 989 this.extend(a, o, null, true); 990 } 991 992 if (arguments[2] === 'board') { 993 // For board attributes we are done now. 994 return a; 995 } 996 997 // Special treatment of labels 998 o = options; 999 isAvail = true; 1000 for (i = 2; i < len; i++) { 1001 if (this.exists(o[arguments[i]])) { 1002 o = o[arguments[i]]; 1003 } else { 1004 isAvail = false; 1005 break; 1006 } 1007 } 1008 if (isAvail && this.exists(o.label)) { 1009 a.label = JXG.deepCopy(o.label, a.label); 1010 } 1011 a.label = JXG.deepCopy(options.label, a.label); 1012 1013 return a; 1014 }, 1015 1016 /** 1017 * Copy all prototype methods from object "superObject" to object 1018 * "subObject". The constructor of superObject will be available 1019 * in subObject as subObject.constructor[constructorName]. 1020 * @param {Object} subObj A JavaScript object which receives new methods. 1021 * @param {Object} superObj A JavaScript object which lends its prototype methods to subObject 1022 * @returns {String} constructorName Under this name the constructor of superObj will be available 1023 * in subObject. 1024 * @private 1025 */ 1026 copyPrototypeMethods: function (subObject, superObject, constructorName) { 1027 var key; 1028 1029 subObject.prototype[constructorName] = superObject.prototype.constructor; 1030 for (key in superObject.prototype) { 1031 if (superObject.prototype.hasOwnProperty(key)) { 1032 subObject.prototype[key] = superObject.prototype[key]; 1033 } 1034 } 1035 }, 1036 1037 /** 1038 * Converts a JavaScript object into a JSON string. 1039 * @param {Object} obj A JavaScript object, functions will be ignored. 1040 * @param {Boolean} [noquote=false] No quotes around the name of a property. 1041 * @returns {String} The given object stored in a JSON string. 1042 */ 1043 toJSON: function (obj, noquote) { 1044 var list, prop, i, s, val; 1045 1046 noquote = JXG.def(noquote, false); 1047 1048 // check for native JSON support: 1049 if (typeof JSON && JSON.stringify && !noquote) { 1050 try { 1051 s = JSON.stringify(obj); 1052 return s; 1053 } catch (e) { 1054 // if something goes wrong, e.g. if obj contains functions we won't return 1055 // and use our own implementation as a fallback 1056 } 1057 } 1058 1059 switch (typeof obj) { 1060 case 'object': 1061 if (obj) { 1062 list = []; 1063 1064 if (this.isArray(obj)) { 1065 for (i = 0; i < obj.length; i++) { 1066 list.push(JXG.toJSON(obj[i], noquote)); 1067 } 1068 1069 return '[' + list.join(',') + ']'; 1070 } 1071 1072 for (prop in obj) { 1073 if (obj.hasOwnProperty(prop)) { 1074 try { 1075 val = JXG.toJSON(obj[prop], noquote); 1076 } catch (e2) { 1077 val = ''; 1078 } 1079 1080 if (noquote) { 1081 list.push(prop + ':' + val); 1082 } else { 1083 list.push('"' + prop + '":' + val); 1084 } 1085 } 1086 } 1087 1088 return '{' + list.join(',') + '} '; 1089 } 1090 return 'null'; 1091 case 'string': 1092 return '\'' + obj.replace(/(["'])/g, '\\$1') + '\''; 1093 case 'number': 1094 case 'boolean': 1095 return obj.toString(); 1096 } 1097 1098 return '0'; 1099 }, 1100 1101 /** 1102 * Resets visPropOld. 1103 * @param {JXG.GeometryElement} el 1104 * @returns {GeometryElement} 1105 */ 1106 clearVisPropOld: function (el) { 1107 el.visPropOld = { 1108 cssclass: '', 1109 cssdefaultstyle: '', 1110 cssstyle: '', 1111 fillcolor: '', 1112 fillopacity: '', 1113 firstarrow: false, 1114 fontsize: -1, 1115 lastarrow: false, 1116 left: -100000, 1117 linecap: '', 1118 shadow: false, 1119 strokecolor: '', 1120 strokeopacity: '', 1121 strokewidth: '', 1122 tabindex: -100000, 1123 transitionduration: 0, 1124 top: -100000, 1125 visible: null 1126 }; 1127 1128 return el; 1129 }, 1130 1131 /** 1132 * Checks if an object contains a key, whose value equals to val. 1133 * @param {Object} obj 1134 * @param val 1135 * @returns {Boolean} 1136 */ 1137 isInObject: function (obj, val) { 1138 var el; 1139 1140 for (el in obj) { 1141 if (obj.hasOwnProperty(el)) { 1142 if (obj[el] === val) { 1143 return true; 1144 } 1145 } 1146 } 1147 1148 return false; 1149 }, 1150 1151 /** 1152 * Replaces all occurences of & by &, > by >, and < by <. 1153 * @param {String} str 1154 * @returns {String} 1155 */ 1156 escapeHTML: function (str) { 1157 return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 1158 }, 1159 1160 /** 1161 * Eliminates all substrings enclosed by < and > and replaces all occurences of 1162 * & by &, > by >, and < by <. 1163 * @param {String} str 1164 * @returns {String} 1165 */ 1166 unescapeHTML: function (str) { 1167 // This regex is NOT insecure. We are replacing everything found with '' 1168 /*jslint regexp:true*/ 1169 return str.replace(/<\/?[^>]+>/gi, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 1170 }, 1171 1172 /** 1173 * Makes a string lower case except for the first character which will be upper case. 1174 * @param {String} str Arbitrary string 1175 * @returns {String} The capitalized string. 1176 */ 1177 capitalize: function (str) { 1178 return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase(); 1179 }, 1180 1181 /** 1182 * Make numbers given as strings nicer by removing all unnecessary leading and trailing zeroes. 1183 * @param {String} str 1184 * @returns {String} 1185 */ 1186 trimNumber: function (str) { 1187 str = str.replace(/^0+/, ''); 1188 str = str.replace(/0+$/, ''); 1189 1190 if (str[str.length - 1] === '.' || str[str.length - 1] === ',') { 1191 str = str.slice(0, -1); 1192 } 1193 1194 if (str[0] === '.' || str[0] === ',') { 1195 str = '0' + str; 1196 } 1197 1198 return str; 1199 }, 1200 1201 /** 1202 * Filter an array of elements. 1203 * @param {Array} list 1204 * @param {Object|function} filter 1205 * @returns {Array} 1206 */ 1207 filterElements: function (list, filter) { 1208 var i, f, item, flower, value, visPropValue, pass, 1209 l = list.length, 1210 result = []; 1211 1212 if (typeof filter !== 'function' && typeof filter !== 'object') { 1213 return result; 1214 } 1215 1216 for (i = 0; i < l; i++) { 1217 pass = true; 1218 item = list[i]; 1219 1220 if (typeof filter === 'object') { 1221 for (f in filter) { 1222 if (filter.hasOwnProperty(f)) { 1223 flower = f.toLowerCase(); 1224 1225 if (typeof item[f] === 'function') { 1226 value = item[f](); 1227 } else { 1228 value = item[f]; 1229 } 1230 1231 if (item.visProp && typeof item.visProp[flower] === 'function') { 1232 visPropValue = item.visProp[flower](); 1233 } else { 1234 visPropValue = item.visProp && item.visProp[flower]; 1235 } 1236 1237 if (typeof filter[f] === 'function') { 1238 pass = filter[f](value) || filter[f](visPropValue); 1239 } else { 1240 pass = (value === filter[f] || visPropValue === filter[f]); 1241 } 1242 1243 if (!pass) { 1244 break; 1245 } 1246 } 1247 } 1248 } else if (typeof filter === 'function') { 1249 pass = filter(item); 1250 } 1251 1252 if (pass) { 1253 result.push(item); 1254 } 1255 } 1256 1257 return result; 1258 }, 1259 1260 /** 1261 * Remove all leading and trailing whitespaces from a given string. 1262 * @param {String} str 1263 * @returns {String} 1264 */ 1265 trim: function (str) { 1266 // str = str.replace(/^\s+/, ''); 1267 // str = str.replace(/\s+$/, ''); 1268 // 1269 // return str; 1270 return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); 1271 }, 1272 1273 /** 1275 * @param {String} str 1276 * @param {Boolean} caja 1277 * @returns {String} Sanitized string 1278 */ 1279 sanitizeHTML: function (str, caja) { 1280 if (typeof html_sanitize === 'function' && caja) { 1281 return html_sanitize(str, function () { return undefined; }, function (id) { return id; }); 1282 } 1283 1284 if (str) { 1285 str = str.replace(/</g, '<').replace(/>/g, '>'); 1286 } 1287 1288 return str; 1289 }, 1290 1291 /** 1292 * If <tt>s</tt> is a slider, it returns the sliders value, otherwise it just returns the given value. 1293 * @param {*} s 1294 * @returns {*} s.Value() if s is an element of type slider, s otherwise 1295 */ 1296 evalSlider: function (s) { 1297 if (s && s.type === Const.OBJECT_TYPE_GLIDER && typeof s.Value === 'function') { 1298 return s.Value(); 1299 } 1300 1301 return s; 1302 } 1303 }); 1304 1305 return JXG; 1306 }); 1307